Add payload attestation helper functions (#14258)

* Add `IndexedPayloadAttestation` container

* Add `GetPayloadAttestingIndices` and its unit test

* Add `GetIndexedPayloadAttestation` and its unit test

* Add `is_valid_indexed_payload_attestation` and its unit test

* Create a smaller set of validators for faster unit test

* Pass context to `GetPayloadTimelinessCommittee`

* Iterate `ValidatorsReadOnly` instead of copying all validators
This commit is contained in:
JihoonSong
2024-08-01 00:58:01 +09:00
committed by Potuz
parent 88f217f562
commit bce9d7cbfa
5 changed files with 445 additions and 0 deletions

View File

@@ -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",

View File

@@ -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
}

View File

@@ -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] = &ethpb.Validator{
PublicKey: pubkey,
WithdrawalCredentials: make([]byte, 32),
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
}
}
// Create a beacon state.
state, err := state_native.InitializeFromProtoEpbs(&ethpb.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 := &eth.PayloadAttestation{
AggregationBits: aggregationBits,
Data: &eth.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] = &ethpb.Validator{
PublicKey: publicKey,
WithdrawalCredentials: make([]byte, 32),
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
}
}
// Create a beacon state.
state, err := state_native.InitializeFromProtoEpbs(&ethpb.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 := &eth.PayloadAttestation{
AggregationBits: aggregationBits,
Data: &eth.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] = &ethpb.Validator{
PublicKey: secretKeys[i].PublicKey().Marshal(),
WithdrawalCredentials: make([]byte, 32),
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
}
}
// Create a beacon state.
state, err := state_native.InitializeFromProtoEpbs(&ethpb.BeaconStateEPBS{
Validators: validators,
Fork: &ethpb.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: &eth.PayloadAttestationData{
BeaconBlockRoot: make([]byte, fieldparams.RootLength),
},
Signature: make([]byte, fieldparams.BLSSignatureLength),
},
},
{
attestation: &epbs.IndexedPayloadAttestation{
AttestingIndices: []primitives.ValidatorIndex{13, 19},
Data: &eth.PayloadAttestationData{
BeaconBlockRoot: make([]byte, fieldparams.RootLength),
},
Signature: make([]byte, fieldparams.BLSSignatureLength),
},
},
{
attestation: &epbs.IndexedPayloadAttestation{
AttestingIndices: []primitives.ValidatorIndex{123, 234, 345},
Data: &eth.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: &eth.PayloadAttestationData{
BeaconBlockRoot: make([]byte, fieldparams.RootLength),
},
Signature: make([]byte, fieldparams.BLSSignatureLength),
},
},
{
attestation: &epbs.IndexedPayloadAttestation{
AttestingIndices: []primitives.ValidatorIndex{5},
Data: &eth.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)
}
}

View File

@@ -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",
],
)

View File

@@ -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
}