Add Gossip Handler For BLS To Execution Changes (#11690)

This commit is contained in:
Nishant Das 2022-11-27 03:07:05 +08:00 committed by GitHub
parent f9e0d4b13a
commit a23a5052bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 690 additions and 31 deletions

View File

@ -67,12 +67,29 @@ func ProcessBLSToExecutionChanges(
// + address_change.to_execution_address
// )
func processBLSToExecutionChange(st state.BeaconState, signed *ethpb.SignedBLSToExecutionChange) (state.BeaconState, error) {
// Checks that the message passes the validation conditions.
val, err := ValidateBLSToExecutionChange(st, signed)
if err != nil {
return nil, err
}
message := signed.Message
newCredentials := make([]byte, executionToBLSPadding)
newCredentials[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte
val.WithdrawalCredentials = append(newCredentials, message.ToExecutionAddress...)
err = st.UpdateValidatorAtIndex(message.ValidatorIndex, val)
return st, err
}
// ValidateBLSToExecutionChange validates the execution change message against the state and returns the
// validator referenced by the message.
func ValidateBLSToExecutionChange(st state.ReadOnlyBeaconState, signed *ethpb.SignedBLSToExecutionChange) (*ethpb.Validator, error) {
if signed == nil {
return st, errNilSignedWithdrawalMessage
return nil, errNilSignedWithdrawalMessage
}
message := signed.Message
if message == nil {
return st, errNilWithdrawalMessage
return nil, errNilWithdrawalMessage
}
val, err := st.ValidatorAtIndex(message.ValidatorIndex)
@ -91,12 +108,7 @@ func processBLSToExecutionChange(st state.BeaconState, signed *ethpb.SignedBLSTo
if !bytes.Equal(digest[1:], cred[1:]) {
return nil, errInvalidWithdrawalCredentials
}
newCredentials := make([]byte, executionToBLSPadding)
newCredentials[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte
val.WithdrawalCredentials = append(newCredentials, message.ToExecutionAddress...)
err = st.UpdateValidatorAtIndex(message.ValidatorIndex, val)
return st, err
return val, nil
}
func ProcessWithdrawals(st state.BeaconState, withdrawals []*enginev1.Withdrawal) (state.BeaconState, error) {

View File

@ -70,6 +70,47 @@ func TestProcessBLSToExecutionChange(t *testing.T) {
require.DeepEqual(t, message.ToExecutionAddress, val.WithdrawalCredentials[12:])
})
t.Run("happy case only validation", func(t *testing.T) {
priv, err := bls.RandKey()
require.NoError(t, err)
pubkey := priv.PublicKey().Marshal()
message := &ethpb.BLSToExecutionChange{
ToExecutionAddress: []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13},
ValidatorIndex: 0,
FromBlsPubkey: pubkey,
}
hashFn := ssz.NewHasherFunc(hash.CustomSHA256Hasher())
digest := hashFn.Hash(pubkey)
digest[0] = params.BeaconConfig().BLSWithdrawalPrefixByte
registry := []*ethpb.Validator{
{
WithdrawalCredentials: digest[:],
},
}
st, err := state_native.InitializeFromProtoPhase0(&ethpb.BeaconState{
Validators: registry,
Fork: &ethpb.Fork{
CurrentVersion: params.BeaconConfig().GenesisForkVersion,
PreviousVersion: params.BeaconConfig().GenesisForkVersion,
},
Slot: params.BeaconConfig().SlotsPerEpoch * 5,
})
require.NoError(t, err)
signature, err := signing.ComputeDomainAndSign(st, time.CurrentEpoch(st), message, params.BeaconConfig().DomainBLSToExecutionChange, priv)
require.NoError(t, err)
signed := &ethpb.SignedBLSToExecutionChange{
Message: message,
Signature: signature,
}
val, err := blocks.ValidateBLSToExecutionChange(st, signed)
require.NoError(t, err)
require.DeepEqual(t, digest[:], val.WithdrawalCredentials)
})
t.Run("non-existent validator", func(t *testing.T) {
priv, err := bls.RandKey()

View File

@ -32,6 +32,7 @@ go_library(
"//beacon-chain/monitor:go_default_library",
"//beacon-chain/node/registration:go_default_library",
"//beacon-chain/operations/attestations:go_default_library",
"//beacon-chain/operations/blstoexec:go_default_library",
"//beacon-chain/operations/slashings:go_default_library",
"//beacon-chain/operations/synccommittee:go_default_library",
"//beacon-chain/operations/voluntaryexits:go_default_library",

View File

@ -34,6 +34,7 @@ import (
"github.com/prysmaticlabs/prysm/v3/beacon-chain/monitor"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/node/registration"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/attestations"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/blstoexec"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/slashings"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/synccommittee"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/voluntaryexits"
@ -94,6 +95,7 @@ type BeaconNode struct {
exitPool voluntaryexits.PoolManager
slashingsPool slashings.PoolManager
syncCommitteePool synccommittee.Pool
blsToExecPool blstoexec.PoolManager
depositCache *depositcache.DepositCache
proposerIdsCache *cache.ProposerPayloadIDsCache
stateFeed *event.Feed
@ -171,6 +173,7 @@ func New(cliCtx *cli.Context, opts ...Option) (*BeaconNode, error) {
exitPool: voluntaryexits.NewPool(),
slashingsPool: slashings.NewPool(),
syncCommitteePool: synccommittee.NewPool(),
blsToExecPool: blstoexec.NewPool(),
slasherBlockHeadersFeed: new(event.Feed),
slasherAttestationsFeed: new(event.Feed),
serviceFlagOpts: &serviceFlagOpts{},
@ -674,6 +677,7 @@ func (b *BeaconNode) registerSyncService() error {
regularsync.WithExitPool(b.exitPool),
regularsync.WithSlashingPool(b.slashingsPool),
regularsync.WithSyncCommsPool(b.syncCommitteePool),
regularsync.WithBlsToExecPool(b.blsToExecPool),
regularsync.WithStateGen(b.stateGen),
regularsync.WithSlasherAttestationsFeed(b.slasherAttestationsFeed),
regularsync.WithSlasherBlockHeadersFeed(b.slasherBlockHeadersFeed),

View File

@ -17,6 +17,7 @@ type PoolManager interface {
BLSToExecChangesForInclusion() ([]*ethpb.SignedBLSToExecutionChange, error)
InsertBLSToExecChange(change *ethpb.SignedBLSToExecutionChange)
MarkIncluded(change *ethpb.SignedBLSToExecutionChange) error
ValidatorExists(idx types.ValidatorIndex) bool
}
// Pool is a concrete implementation of PoolManager.
@ -107,3 +108,14 @@ func (p *Pool) MarkIncluded(change *ethpb.SignedBLSToExecutionChange) error {
p.pending.Remove(node)
return nil
}
// ValidatorExists checks if the bls to execution change object exists
// for that particular validator.
func (p *Pool) ValidatorExists(idx types.ValidatorIndex) bool {
p.lock.RLock()
defer p.lock.RUnlock()
node := p.m[idx]
return node != nil
}

View File

@ -256,3 +256,76 @@ func TestMarkIncluded(t *testing.T) {
assert.NotNil(t, pool.m[1])
})
}
func TestValidatorExists(t *testing.T) {
t.Run("no validators in pool", func(t *testing.T) {
pool := NewPool()
assert.Equal(t, false, pool.ValidatorExists(0))
})
t.Run("validator added to pool", func(t *testing.T) {
pool := NewPool()
change := &eth.SignedBLSToExecutionChange{
Message: &eth.BLSToExecutionChange{
ValidatorIndex: types.ValidatorIndex(0),
}}
pool.InsertBLSToExecChange(change)
assert.Equal(t, true, pool.ValidatorExists(0))
})
t.Run("multiple validators added to pool", func(t *testing.T) {
pool := NewPool()
change := &eth.SignedBLSToExecutionChange{
Message: &eth.BLSToExecutionChange{
ValidatorIndex: types.ValidatorIndex(0),
}}
pool.InsertBLSToExecChange(change)
change = &eth.SignedBLSToExecutionChange{
Message: &eth.BLSToExecutionChange{
ValidatorIndex: types.ValidatorIndex(10),
}}
pool.InsertBLSToExecChange(change)
change = &eth.SignedBLSToExecutionChange{
Message: &eth.BLSToExecutionChange{
ValidatorIndex: types.ValidatorIndex(30),
}}
pool.InsertBLSToExecChange(change)
assert.Equal(t, true, pool.ValidatorExists(0))
assert.Equal(t, true, pool.ValidatorExists(10))
assert.Equal(t, true, pool.ValidatorExists(30))
})
t.Run("validator added and then removed", func(t *testing.T) {
pool := NewPool()
change := &eth.SignedBLSToExecutionChange{
Message: &eth.BLSToExecutionChange{
ValidatorIndex: types.ValidatorIndex(0),
}}
pool.InsertBLSToExecChange(change)
require.NoError(t, pool.MarkIncluded(change))
assert.Equal(t, false, pool.ValidatorExists(0))
})
t.Run("multiple validators added to pool and removed", func(t *testing.T) {
pool := NewPool()
firstChange := &eth.SignedBLSToExecutionChange{
Message: &eth.BLSToExecutionChange{
ValidatorIndex: types.ValidatorIndex(0),
}}
pool.InsertBLSToExecChange(firstChange)
secondChange := &eth.SignedBLSToExecutionChange{
Message: &eth.BLSToExecutionChange{
ValidatorIndex: types.ValidatorIndex(10),
}}
pool.InsertBLSToExecChange(secondChange)
thirdChange := &eth.SignedBLSToExecutionChange{
Message: &eth.BLSToExecutionChange{
ValidatorIndex: types.ValidatorIndex(30),
}}
pool.InsertBLSToExecChange(thirdChange)
assert.NoError(t, pool.MarkIncluded(firstChange))
assert.NoError(t, pool.MarkIncluded(thirdChange))
assert.Equal(t, false, pool.ValidatorExists(0))
assert.Equal(t, true, pool.ValidatorExists(10))
assert.Equal(t, false, pool.ValidatorExists(30))
})
}

View File

@ -41,6 +41,9 @@ const (
// voluntaryExitWeight specifies the scoring weight that we apply to
// our voluntary exit topic.
voluntaryExitWeight = 0.05
// blsToExecutionChangeWeight specifies the scoring weight that we apply to
// our bls to execution topic.
blsToExecutionChangeWeight = 0.05
// maxInMeshScore describes the max score a peer can attain from being in the mesh.
maxInMeshScore = 10
@ -116,6 +119,8 @@ func (s *Service) topicScoreParams(topic string) (*pubsub.TopicScoreParams, erro
return defaultProposerSlashingTopicParams(), nil
case strings.Contains(topic, GossipAttesterSlashingMessage):
return defaultAttesterSlashingTopicParams(), nil
case strings.Contains(topic, GossipBlsToExecutionChangeMessage):
return defaultBlsToExecutionChangeTopicParams(), nil
default:
return nil, errors.Errorf("unrecognized topic provided for parameter registration: %s", topic)
}
@ -473,6 +478,28 @@ func defaultVoluntaryExitTopicParams() *pubsub.TopicScoreParams {
}
}
func defaultBlsToExecutionChangeTopicParams() *pubsub.TopicScoreParams {
return &pubsub.TopicScoreParams{
TopicWeight: blsToExecutionChangeWeight,
TimeInMeshWeight: maxInMeshScore / inMeshCap(),
TimeInMeshQuantum: inMeshTime(),
TimeInMeshCap: inMeshCap(),
FirstMessageDeliveriesWeight: 2,
FirstMessageDeliveriesDecay: scoreDecay(oneHundredEpochs),
FirstMessageDeliveriesCap: 5,
MeshMessageDeliveriesWeight: 0,
MeshMessageDeliveriesDecay: 0,
MeshMessageDeliveriesCap: 0,
MeshMessageDeliveriesThreshold: 0,
MeshMessageDeliveriesWindow: 0,
MeshMessageDeliveriesActivation: 0,
MeshFailurePenaltyWeight: 0,
MeshFailurePenaltyDecay: 0,
InvalidMessageDeliveriesWeight: -2000,
InvalidMessageDeliveriesDecay: scoreDecay(invalidDecayPeriod),
}
}
func oneSlotDuration() time.Duration {
return time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Second
}
@ -531,7 +558,7 @@ func scoreByWeight(weight, threshold float64) float64 {
func maxScore() float64 {
totalWeight := beaconBlockWeight + aggregateWeight + syncContributionWeight +
attestationTotalWeight + syncCommitteesTotalWeight + attesterSlashingWeight +
proposerSlashingWeight + voluntaryExitWeight
proposerSlashingWeight + voluntaryExitWeight + blsToExecutionChangeWeight
return (maxInMeshScore + maxFirstDeliveryScore) * totalWeight
}

View File

@ -20,6 +20,7 @@ var gossipTopicMappings = map[string]proto.Message{
AggregateAndProofSubnetTopicFormat: &ethpb.SignedAggregateAttestationAndProof{},
SyncContributionAndProofSubnetTopicFormat: &ethpb.SignedContributionAndProof{},
SyncCommitteeSubnetTopicFormat: &ethpb.SyncCommitteeMessage{},
BlsToExecutionChangeSubnetTopicFormat: &ethpb.SignedBLSToExecutionChange{},
}
// GossipTopicMappings is a function to return the assigned data type

View File

@ -26,6 +26,8 @@ const (
GossipAggregateAndProofMessage = "beacon_aggregate_and_proof"
// GossipContributionAndProofMessage is the name for the sync contribution and proof message type.
GossipContributionAndProofMessage = "sync_committee_contribution_and_proof"
// GossipBlsToExecutionChangeMessage is the name for the bls to execution change message type.
GossipBlsToExecutionChangeMessage = "bls_to_execution_change"
// Topic Formats
//
@ -45,4 +47,6 @@ const (
AggregateAndProofSubnetTopicFormat = GossipProtocolAndDigest + GossipAggregateAndProofMessage
// SyncContributionAndProofSubnetTopicFormat is the topic format for the sync aggregate and proof subnet.
SyncContributionAndProofSubnetTopicFormat = GossipProtocolAndDigest + GossipContributionAndProofMessage
// BlsToExecutionChangeSubnetTopicFormat is the topic format for the bls to execution change subnet.
BlsToExecutionChangeSubnetTopicFormat = GossipProtocolAndDigest + GossipBlsToExecutionChangeMessage
)

View File

@ -31,6 +31,7 @@ go_library(
"subscriber_beacon_aggregate_proof.go",
"subscriber_beacon_attestation.go",
"subscriber_beacon_blocks.go",
"subscriber_bls_to_execution_change.go",
"subscriber_handlers.go",
"subscriber_sync_committee_message.go",
"subscriber_sync_contribution_proof.go",
@ -40,6 +41,7 @@ go_library(
"validate_attester_slashing.go",
"validate_beacon_attestation.go",
"validate_beacon_blocks.go",
"validate_bls_to_execution_change.go",
"validate_proposer_slashing.go",
"validate_sync_committee_message.go",
"validate_sync_contribution_proof.go",
@ -71,6 +73,7 @@ go_library(
"//beacon-chain/db/filters:go_default_library",
"//beacon-chain/execution:go_default_library",
"//beacon-chain/operations/attestations:go_default_library",
"//beacon-chain/operations/blstoexec:go_default_library",
"//beacon-chain/operations/slashings:go_default_library",
"//beacon-chain/operations/synccommittee:go_default_library",
"//beacon-chain/operations/voluntaryexits:go_default_library",
@ -158,6 +161,7 @@ go_test(
"validate_attester_slashing_test.go",
"validate_beacon_attestation_test.go",
"validate_beacon_blocks_test.go",
"validate_bls_to_execution_change_test.go",
"validate_proposer_slashing_test.go",
"validate_sync_committee_message_test.go",
"validate_sync_contribution_proof_test.go",
@ -185,6 +189,7 @@ go_test(
"//beacon-chain/execution/testing:go_default_library",
"//beacon-chain/forkchoice/doubly-linked-tree:go_default_library",
"//beacon-chain/operations/attestations:go_default_library",
"//beacon-chain/operations/blstoexec:go_default_library",
"//beacon-chain/operations/slashings:go_default_library",
"//beacon-chain/p2p:go_default_library",
"//beacon-chain/p2p/encoder:go_default_library",

View File

@ -49,26 +49,16 @@ func (s *Service) registerForUpcomingFork(currEpoch types.Epoch) error {
// will subscribe the new topics in advance.
if isNextForkEpoch {
nextEpoch := currEpoch + 1
switch nextEpoch {
case params.BeaconConfig().AltairForkEpoch:
digest, err := forks.ForkDigestFromEpoch(nextEpoch, genRoot[:])
if err != nil {
return errors.Wrap(err, "Could not retrieve fork digest")
}
if s.subHandler.digestExists(digest) {
return nil
}
s.registerSubscribers(nextEpoch, digest)
digest, err := forks.ForkDigestFromEpoch(nextEpoch, genRoot[:])
if err != nil {
return errors.Wrap(err, "could not retrieve fork digest")
}
if s.subHandler.digestExists(digest) {
return nil
}
s.registerSubscribers(nextEpoch, digest)
if nextEpoch == params.BeaconConfig().AltairForkEpoch {
s.registerRPCHandlersAltair()
case params.BeaconConfig().BellatrixForkEpoch:
digest, err := forks.ForkDigestFromEpoch(nextEpoch, genRoot[:])
if err != nil {
return errors.Wrap(err, "could not retrieve fork digest")
}
if s.subHandler.digestExists(digest) {
return nil
}
s.registerSubscribers(nextEpoch, digest)
}
}
return nil
@ -109,9 +99,7 @@ func (s *Service) deregisterFromPastFork(currEpoch types.Epoch) error {
if err != nil {
return errors.Wrap(err, "failed to determine previous epoch fork data")
}
switch prevFork.Epoch {
case params.BeaconConfig().GenesisEpoch:
if prevFork.Epoch == params.BeaconConfig().GenesisEpoch {
s.unregisterPhase0Handlers()
}
// Run through all our current active topics and see

View File

@ -8,6 +8,7 @@ import (
"github.com/prysmaticlabs/prysm/v3/beacon-chain/db"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/execution"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/attestations"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/blstoexec"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/slashings"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/synccommittee"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/voluntaryexits"
@ -66,6 +67,13 @@ func WithSyncCommsPool(syncCommsPool synccommittee.Pool) Option {
}
}
func WithBlsToExecPool(blsToExecPool blstoexec.PoolManager) Option {
return func(s *Service) error {
s.cfg.blsToExecPool = blsToExecPool
return nil
}
}
func WithChainService(chain blockchainService) Option {
return func(s *Service) error {
s.cfg.chain = chain

View File

@ -26,6 +26,7 @@ import (
"github.com/prysmaticlabs/prysm/v3/beacon-chain/db"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/execution"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/attestations"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/blstoexec"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/slashings"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/synccommittee"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/voluntaryexits"
@ -75,6 +76,7 @@ type config struct {
exitPool voluntaryexits.PoolManager
slashingPool slashings.PoolManager
syncCommsPool synccommittee.Pool
blsToExecPool blstoexec.PoolManager
chain blockchainService
initialSync Checker
stateNotifier statefeed.Notifier

View File

@ -120,6 +120,16 @@ func (s *Service) registerSubscribers(epoch types.Epoch, digest [4]byte) {
)
}
}
// New Gossip Topic in Capella
if epoch >= params.BeaconConfig().CapellaForkEpoch {
s.subscribe(
p2p.BlsToExecutionChangeSubnetTopicFormat,
s.validateBlsToExecutionChange,
s.blsToExecutionChangeSubscriber,
digest,
)
}
}
// subscribe to a given topic with a given validator and subscription handler.

View File

@ -0,0 +1,18 @@
package sync
import (
"context"
"github.com/pkg/errors"
ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1"
"google.golang.org/protobuf/proto"
)
func (s *Service) blsToExecutionChangeSubscriber(ctx context.Context, msg proto.Message) error {
blsMsg, ok := msg.(*ethpb.SignedBLSToExecutionChange)
if !ok {
return errors.Errorf("incorrect type of message received, wanted %T but got %T", &ethpb.SignedBLSToExecutionChange{}, msg)
}
s.cfg.blsToExecPool.InsertBLSToExecChange(blsMsg)
return nil
}

View File

@ -0,0 +1,64 @@
package sync
import (
"context"
pubsub "github.com/libp2p/go-libp2p-pubsub"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/core/blocks"
"github.com/prysmaticlabs/prysm/v3/monitoring/tracing"
ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1"
"go.opencensus.io/trace"
)
func (s *Service) validateBlsToExecutionChange(ctx context.Context, pid peer.ID, msg *pubsub.Message) (pubsub.ValidationResult, error) {
// Validation runs on publish (not just subscriptions), so we should approve any message from
// ourselves.
if pid == s.cfg.p2p.PeerID() {
return pubsub.ValidationAccept, nil
}
// The head state will be too far away to validate any execution change.
if s.cfg.initialSync.Syncing() {
return pubsub.ValidationIgnore, nil
}
ctx, span := trace.StartSpan(ctx, "sync.validateBlsToExecutionChange")
defer span.End()
m, err := s.decodePubsubMessage(msg)
if err != nil {
tracing.AnnotateError(span, err)
return pubsub.ValidationReject, err
}
blsChange, ok := m.(*ethpb.SignedBLSToExecutionChange)
if !ok {
return pubsub.ValidationReject, errWrongMessage
}
// Check that the validator hasn't submitted a previous execution change.
if s.cfg.blsToExecPool.ValidatorExists(blsChange.Message.ValidatorIndex) {
return pubsub.ValidationIgnore, nil
}
st, err := s.cfg.chain.HeadState(ctx)
if err != nil {
return pubsub.ValidationIgnore, err
}
// Validate that the execution change object is valid.
_, err = blocks.ValidateBLSToExecutionChange(st, blsChange)
if err != nil {
return pubsub.ValidationReject, err
}
// Validate the signature of the message using our batch gossip verifier.
sigBatch, err := blocks.BLSChangesSignatureBatch(ctx, st, []*ethpb.SignedBLSToExecutionChange{blsChange})
if err != nil {
return pubsub.ValidationReject, err
}
res, err := s.validateWithBatchVerifier(ctx, "bls to execution change", sigBatch)
if res != pubsub.ValidationAccept {
return res, err
}
msg.ValidatorData = blsChange // Used in downstream subscriber
return pubsub.ValidationAccept, nil
}

View File

@ -0,0 +1,389 @@
package sync
import (
"context"
"fmt"
"testing"
"time"
"github.com/golang/snappy"
pubsub "github.com/libp2p/go-libp2p-pubsub"
pubsubpb "github.com/libp2p/go-libp2p-pubsub/pb"
"github.com/libp2p/go-libp2p/core/peer"
mockChain "github.com/prysmaticlabs/prysm/v3/beacon-chain/blockchain/testing"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/core/signing"
testingdb "github.com/prysmaticlabs/prysm/v3/beacon-chain/db/testing"
doublylinkedtree "github.com/prysmaticlabs/prysm/v3/beacon-chain/forkchoice/doubly-linked-tree"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/blstoexec"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/p2p"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/p2p/encoder"
mockp2p "github.com/prysmaticlabs/prysm/v3/beacon-chain/p2p/testing"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/state/stategen"
mockSync "github.com/prysmaticlabs/prysm/v3/beacon-chain/sync/initial-sync/testing"
"github.com/prysmaticlabs/prysm/v3/config/params"
ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v3/testing/assert"
"github.com/prysmaticlabs/prysm/v3/testing/util"
"github.com/prysmaticlabs/prysm/v3/time/slots"
)
func TestService_ValidateBlsToExecutionChange(t *testing.T) {
beaconDB := testingdb.SetupDB(t)
defaultTopic := p2p.BlsToExecutionChangeSubnetTopicFormat
fakeDigest := []byte{0xAB, 0x00, 0xCC, 0x9E}
wantedExecAddress := []byte{0xd8, 0xdA, 0x6B, 0xF2, 0x69, 0x64, 0xaF, 0x9D, 0x7e, 0xEd, 0x9e, 0x03, 0xE5, 0x34, 0x15, 0xD3, 0x7a, 0xA9, 0x60, 0x45}
defaultTopic = defaultTopic + "/" + encoder.ProtocolSuffixSSZSnappy
chainService := &mockChain.ChainService{
Genesis: time.Now(),
ValidatorsRoot: [32]byte{'A'},
}
emptySig := [96]byte{}
type args struct {
ctx context.Context
pid peer.ID
msg *ethpb.SignedBLSToExecutionChange
topic string
}
tests := []struct {
name string
svc *Service
setupSvc func(s *Service, msg *ethpb.SignedBLSToExecutionChange, topic string) (*Service, string)
args args
want pubsub.ValidationResult
}{
{
name: "Is syncing",
svc: NewService(context.Background(),
WithP2P(mockp2p.NewTestP2P(t)),
WithInitialSync(&mockSync.Sync{IsSyncing: true}),
WithChainService(chainService),
WithStateNotifier(chainService.StateNotifier()),
WithOperationNotifier(chainService.OperationNotifier()),
),
setupSvc: func(s *Service, msg *ethpb.SignedBLSToExecutionChange, topic string) (*Service, string) {
s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
s.cfg.beaconDB = beaconDB
s.initCaches()
return s, topic
},
args: args{
ctx: context.Background(),
pid: "random",
topic: "junk",
msg: &ethpb.SignedBLSToExecutionChange{
Message: &ethpb.BLSToExecutionChange{
ValidatorIndex: 0,
FromBlsPubkey: make([]byte, 48),
ToExecutionAddress: make([]byte, 20),
},
Signature: emptySig[:],
}},
want: pubsub.ValidationIgnore,
},
{
name: "Bad Topic",
svc: NewService(context.Background(),
WithP2P(mockp2p.NewTestP2P(t)),
WithInitialSync(&mockSync.Sync{IsSyncing: false}),
WithChainService(chainService),
WithStateNotifier(chainService.StateNotifier()),
WithOperationNotifier(chainService.OperationNotifier()),
),
setupSvc: func(s *Service, msg *ethpb.SignedBLSToExecutionChange, topic string) (*Service, string) {
s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
s.cfg.beaconDB = beaconDB
s.initCaches()
return s, topic
},
args: args{
ctx: context.Background(),
pid: "random",
topic: "junk",
msg: &ethpb.SignedBLSToExecutionChange{
Message: &ethpb.BLSToExecutionChange{
ValidatorIndex: 0,
FromBlsPubkey: make([]byte, 48),
ToExecutionAddress: make([]byte, 20),
},
Signature: emptySig[:],
}},
want: pubsub.ValidationReject,
},
{
name: "Already Seen Message",
svc: NewService(context.Background(),
WithP2P(mockp2p.NewTestP2P(t)),
WithInitialSync(&mockSync.Sync{IsSyncing: false}),
WithChainService(chainService),
WithStateNotifier(chainService.StateNotifier()),
WithOperationNotifier(chainService.OperationNotifier()),
WithBlsToExecPool(blstoexec.NewPool()),
),
setupSvc: func(s *Service, msg *ethpb.SignedBLSToExecutionChange, topic string) (*Service, string) {
s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
s.cfg.beaconDB = beaconDB
s.initCaches()
s.cfg.blsToExecPool.InsertBLSToExecChange(&ethpb.SignedBLSToExecutionChange{
Message: &ethpb.BLSToExecutionChange{
ValidatorIndex: 10,
FromBlsPubkey: make([]byte, 48),
ToExecutionAddress: make([]byte, 20),
},
Signature: emptySig[:],
})
return s, topic
},
args: args{
ctx: context.Background(),
pid: "random",
topic: fmt.Sprintf(defaultTopic, fakeDigest),
msg: &ethpb.SignedBLSToExecutionChange{
Message: &ethpb.BLSToExecutionChange{
ValidatorIndex: 10,
FromBlsPubkey: make([]byte, 48),
ToExecutionAddress: make([]byte, 20),
},
Signature: emptySig[:],
}},
want: pubsub.ValidationIgnore,
},
{
name: "Non-existent Validator Index",
svc: NewService(context.Background(),
WithP2P(mockp2p.NewTestP2P(t)),
WithInitialSync(&mockSync.Sync{IsSyncing: false}),
WithChainService(chainService),
WithStateNotifier(chainService.StateNotifier()),
WithOperationNotifier(chainService.OperationNotifier()),
WithBlsToExecPool(blstoexec.NewPool()),
),
setupSvc: func(s *Service, msg *ethpb.SignedBLSToExecutionChange, topic string) (*Service, string) {
s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
s.cfg.beaconDB = beaconDB
s.initCaches()
st, _ := util.DeterministicGenesisStateCapella(t, 128)
s.cfg.chain = &mockChain.ChainService{
ValidatorsRoot: [32]byte{'A'},
Genesis: time.Now().Add(-time.Second * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Duration(10)),
State: st,
}
msg.Message.ValidatorIndex = 130
return s, topic
},
args: args{
ctx: context.Background(),
pid: "random",
topic: fmt.Sprintf(defaultTopic, fakeDigest),
msg: &ethpb.SignedBLSToExecutionChange{
Message: &ethpb.BLSToExecutionChange{
ValidatorIndex: 0,
FromBlsPubkey: make([]byte, 48),
ToExecutionAddress: make([]byte, 20),
},
Signature: emptySig[:],
}},
want: pubsub.ValidationReject,
},
{
name: "Invalid Withdrawal Pubkey",
svc: NewService(context.Background(),
WithP2P(mockp2p.NewTestP2P(t)),
WithInitialSync(&mockSync.Sync{IsSyncing: false}),
WithChainService(chainService),
WithStateNotifier(chainService.StateNotifier()),
WithOperationNotifier(chainService.OperationNotifier()),
WithBlsToExecPool(blstoexec.NewPool()),
),
setupSvc: func(s *Service, msg *ethpb.SignedBLSToExecutionChange, topic string) (*Service, string) {
s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
s.cfg.beaconDB = beaconDB
s.initCaches()
st, keys := util.DeterministicGenesisStateCapella(t, 128)
s.cfg.chain = &mockChain.ChainService{
ValidatorsRoot: [32]byte{'A'},
Genesis: time.Now().Add(-time.Second * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Duration(10)),
State: st,
}
msg.Message.ValidatorIndex = 50
// Provide invalid withdrawal key for validator
msg.Message.FromBlsPubkey = keys[0].PublicKey().Marshal()
msg.Message.ToExecutionAddress = wantedExecAddress
return s, topic
},
args: args{
ctx: context.Background(),
pid: "random",
topic: fmt.Sprintf(defaultTopic, fakeDigest),
msg: &ethpb.SignedBLSToExecutionChange{
Message: &ethpb.BLSToExecutionChange{
ValidatorIndex: 0,
FromBlsPubkey: make([]byte, 48),
ToExecutionAddress: make([]byte, 20),
},
Signature: emptySig[:],
}},
want: pubsub.ValidationReject,
},
{
name: "Invalid Credentials in State",
svc: NewService(context.Background(),
WithP2P(mockp2p.NewTestP2P(t)),
WithInitialSync(&mockSync.Sync{IsSyncing: false}),
WithChainService(chainService),
WithStateNotifier(chainService.StateNotifier()),
WithOperationNotifier(chainService.OperationNotifier()),
WithBlsToExecPool(blstoexec.NewPool()),
),
setupSvc: func(s *Service, msg *ethpb.SignedBLSToExecutionChange, topic string) (*Service, string) {
s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
s.cfg.beaconDB = beaconDB
s.initCaches()
st, keys := util.DeterministicGenesisStateCapella(t, 128)
assert.NoError(t, st.ApplyToEveryValidator(func(idx int, val *ethpb.Validator) (bool, *ethpb.Validator, error) {
newCreds := make([]byte, 32)
newCreds[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte
copy(newCreds[12:], wantedExecAddress)
val.WithdrawalCredentials = newCreds
return true, val, nil
}))
s.cfg.chain = &mockChain.ChainService{
ValidatorsRoot: [32]byte{'A'},
Genesis: time.Now().Add(-time.Second * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Duration(10)),
State: st,
}
msg.Message.ValidatorIndex = 50
// Provide Correct withdrawal pubkey
msg.Message.FromBlsPubkey = keys[51].PublicKey().Marshal()
msg.Message.ToExecutionAddress = wantedExecAddress
return s, topic
},
args: args{
ctx: context.Background(),
pid: "random",
topic: fmt.Sprintf(defaultTopic, fakeDigest),
msg: &ethpb.SignedBLSToExecutionChange{
Message: &ethpb.BLSToExecutionChange{
ValidatorIndex: 0,
FromBlsPubkey: make([]byte, 48),
ToExecutionAddress: make([]byte, 20),
},
Signature: emptySig[:],
}},
want: pubsub.ValidationReject,
},
{
name: "Invalid Execution Change Signature",
svc: NewService(context.Background(),
WithP2P(mockp2p.NewTestP2P(t)),
WithInitialSync(&mockSync.Sync{IsSyncing: false}),
WithChainService(chainService),
WithStateNotifier(chainService.StateNotifier()),
WithOperationNotifier(chainService.OperationNotifier()),
WithBlsToExecPool(blstoexec.NewPool()),
),
setupSvc: func(s *Service, msg *ethpb.SignedBLSToExecutionChange, topic string) (*Service, string) {
s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
s.cfg.beaconDB = beaconDB
s.initCaches()
st, keys := util.DeterministicGenesisStateCapella(t, 128)
s.cfg.chain = &mockChain.ChainService{
ValidatorsRoot: [32]byte{'A'},
Genesis: time.Now().Add(-time.Second * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Duration(10)),
State: st,
}
msg.Message.ValidatorIndex = 50
// Provide invalid withdrawal key for validator
msg.Message.FromBlsPubkey = keys[51].PublicKey().Marshal()
msg.Message.ToExecutionAddress = wantedExecAddress
badSig := make([]byte, 96)
copy(badSig, []byte{'j', 'u', 'n', 'k'})
msg.Signature = badSig
return s, topic
},
args: args{
ctx: context.Background(),
pid: "random",
topic: fmt.Sprintf(defaultTopic, fakeDigest),
msg: &ethpb.SignedBLSToExecutionChange{
Message: &ethpb.BLSToExecutionChange{
ValidatorIndex: 0,
FromBlsPubkey: make([]byte, 48),
ToExecutionAddress: make([]byte, 20),
},
Signature: emptySig[:],
}},
want: pubsub.ValidationReject,
},
{
name: "Valid Execution Change Message",
svc: NewService(context.Background(),
WithP2P(mockp2p.NewTestP2P(t)),
WithInitialSync(&mockSync.Sync{IsSyncing: false}),
WithChainService(chainService),
WithStateNotifier(chainService.StateNotifier()),
WithOperationNotifier(chainService.OperationNotifier()),
WithBlsToExecPool(blstoexec.NewPool()),
),
setupSvc: func(s *Service, msg *ethpb.SignedBLSToExecutionChange, topic string) (*Service, string) {
s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
s.cfg.beaconDB = beaconDB
s.initCaches()
st, keys := util.DeterministicGenesisStateCapella(t, 128)
s.cfg.chain = &mockChain.ChainService{
ValidatorsRoot: [32]byte{'A'},
Genesis: time.Now().Add(-time.Second * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Duration(10)),
State: st,
}
msg.Message.ValidatorIndex = 50
// Provide invalid withdrawal key for validator
msg.Message.FromBlsPubkey = keys[51].PublicKey().Marshal()
msg.Message.ToExecutionAddress = wantedExecAddress
epoch := slots.ToEpoch(st.Slot())
domain, err := signing.Domain(st.Fork(), epoch, params.BeaconConfig().DomainBLSToExecutionChange, st.GenesisValidatorsRoot())
assert.NoError(t, err)
htr, err := signing.SigningData(msg.Message.HashTreeRoot, domain)
assert.NoError(t, err)
msg.Signature = keys[51].Sign(htr[:]).Marshal()
return s, topic
},
args: args{
ctx: context.Background(),
pid: "random",
topic: fmt.Sprintf(defaultTopic, fakeDigest),
msg: &ethpb.SignedBLSToExecutionChange{
Message: &ethpb.BLSToExecutionChange{
ValidatorIndex: 0,
FromBlsPubkey: make([]byte, 48),
ToExecutionAddress: make([]byte, 20),
},
Signature: emptySig[:],
}},
want: pubsub.ValidationAccept,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.svc, tt.args.topic = tt.setupSvc(tt.svc, tt.args.msg, tt.args.topic)
marshalledObj, err := tt.args.msg.MarshalSSZ()
assert.NoError(t, err)
marshalledObj = snappy.Encode(nil, marshalledObj)
msg := &pubsub.Message{
Message: &pubsubpb.Message{
Data: marshalledObj,
Topic: &tt.args.topic,
},
ReceivedFrom: "",
ValidatorData: nil,
}
if got, err := tt.svc.validateBlsToExecutionChange(tt.args.ctx, tt.args.pid, msg); got != tt.want {
_ = err
t.Errorf("validateBlsToExecutionChange() = %v, want %v", got, tt.want)
}
})
}
}