diff --git a/beacon-chain/core/blocks/withdrawals.go b/beacon-chain/core/blocks/withdrawals.go index 4d5370eded69..1836fc00ab4c 100644 --- a/beacon-chain/core/blocks/withdrawals.go +++ b/beacon-chain/core/blocks/withdrawals.go @@ -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) { diff --git a/beacon-chain/core/blocks/withdrawals_test.go b/beacon-chain/core/blocks/withdrawals_test.go index 5686b8209789..a964cf08a81e 100644 --- a/beacon-chain/core/blocks/withdrawals_test.go +++ b/beacon-chain/core/blocks/withdrawals_test.go @@ -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 := ðpb.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(ðpb.BeaconState{ + Validators: registry, + Fork: ðpb.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 := ðpb.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() diff --git a/beacon-chain/node/BUILD.bazel b/beacon-chain/node/BUILD.bazel index 27feb2318c25..21384c629978 100644 --- a/beacon-chain/node/BUILD.bazel +++ b/beacon-chain/node/BUILD.bazel @@ -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", diff --git a/beacon-chain/node/node.go b/beacon-chain/node/node.go index a0c66b51ec59..f698bea2b1ac 100644 --- a/beacon-chain/node/node.go +++ b/beacon-chain/node/node.go @@ -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), diff --git a/beacon-chain/operations/blstoexec/pool.go b/beacon-chain/operations/blstoexec/pool.go index 1f6720b6274c..2d82742da20b 100644 --- a/beacon-chain/operations/blstoexec/pool.go +++ b/beacon-chain/operations/blstoexec/pool.go @@ -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 +} diff --git a/beacon-chain/operations/blstoexec/pool_test.go b/beacon-chain/operations/blstoexec/pool_test.go index c6494c797494..f3c2989fd15d 100644 --- a/beacon-chain/operations/blstoexec/pool_test.go +++ b/beacon-chain/operations/blstoexec/pool_test.go @@ -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 := ð.SignedBLSToExecutionChange{ + Message: ð.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 := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(0), + }} + pool.InsertBLSToExecChange(change) + change = ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(10), + }} + pool.InsertBLSToExecChange(change) + change = ð.SignedBLSToExecutionChange{ + Message: ð.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 := ð.SignedBLSToExecutionChange{ + Message: ð.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 := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(0), + }} + pool.InsertBLSToExecChange(firstChange) + secondChange := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(10), + }} + pool.InsertBLSToExecChange(secondChange) + thirdChange := ð.SignedBLSToExecutionChange{ + Message: ð.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)) + }) +} diff --git a/beacon-chain/p2p/gossip_scoring_params.go b/beacon-chain/p2p/gossip_scoring_params.go index 5497fb1c18fc..83a78a6f60ad 100644 --- a/beacon-chain/p2p/gossip_scoring_params.go +++ b/beacon-chain/p2p/gossip_scoring_params.go @@ -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 } diff --git a/beacon-chain/p2p/gossip_topic_mappings.go b/beacon-chain/p2p/gossip_topic_mappings.go index 511f8662d9ef..113f0207b698 100644 --- a/beacon-chain/p2p/gossip_topic_mappings.go +++ b/beacon-chain/p2p/gossip_topic_mappings.go @@ -20,6 +20,7 @@ var gossipTopicMappings = map[string]proto.Message{ AggregateAndProofSubnetTopicFormat: ðpb.SignedAggregateAttestationAndProof{}, SyncContributionAndProofSubnetTopicFormat: ðpb.SignedContributionAndProof{}, SyncCommitteeSubnetTopicFormat: ðpb.SyncCommitteeMessage{}, + BlsToExecutionChangeSubnetTopicFormat: ðpb.SignedBLSToExecutionChange{}, } // GossipTopicMappings is a function to return the assigned data type diff --git a/beacon-chain/p2p/topics.go b/beacon-chain/p2p/topics.go index 1412603cf4b5..ee9af95f96e8 100644 --- a/beacon-chain/p2p/topics.go +++ b/beacon-chain/p2p/topics.go @@ -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 ) diff --git a/beacon-chain/sync/BUILD.bazel b/beacon-chain/sync/BUILD.bazel index aa97b11ec413..37aadd3d03e1 100644 --- a/beacon-chain/sync/BUILD.bazel +++ b/beacon-chain/sync/BUILD.bazel @@ -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", diff --git a/beacon-chain/sync/fork_watcher.go b/beacon-chain/sync/fork_watcher.go index b3b05dc738ec..067807ec731e 100644 --- a/beacon-chain/sync/fork_watcher.go +++ b/beacon-chain/sync/fork_watcher.go @@ -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 diff --git a/beacon-chain/sync/options.go b/beacon-chain/sync/options.go index 62ce046e83b0..4a4c088fa6cc 100644 --- a/beacon-chain/sync/options.go +++ b/beacon-chain/sync/options.go @@ -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 diff --git a/beacon-chain/sync/service.go b/beacon-chain/sync/service.go index 727e94623030..a63541287d33 100644 --- a/beacon-chain/sync/service.go +++ b/beacon-chain/sync/service.go @@ -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 diff --git a/beacon-chain/sync/subscriber.go b/beacon-chain/sync/subscriber.go index 04da9cac9259..3e69291b23f2 100644 --- a/beacon-chain/sync/subscriber.go +++ b/beacon-chain/sync/subscriber.go @@ -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. diff --git a/beacon-chain/sync/subscriber_bls_to_execution_change.go b/beacon-chain/sync/subscriber_bls_to_execution_change.go new file mode 100644 index 000000000000..eb732ab64a1a --- /dev/null +++ b/beacon-chain/sync/subscriber_bls_to_execution_change.go @@ -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", ðpb.SignedBLSToExecutionChange{}, msg) + } + s.cfg.blsToExecPool.InsertBLSToExecChange(blsMsg) + return nil +} diff --git a/beacon-chain/sync/validate_bls_to_execution_change.go b/beacon-chain/sync/validate_bls_to_execution_change.go new file mode 100644 index 000000000000..fba805906ba3 --- /dev/null +++ b/beacon-chain/sync/validate_bls_to_execution_change.go @@ -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 +} diff --git a/beacon-chain/sync/validate_bls_to_execution_change_test.go b/beacon-chain/sync/validate_bls_to_execution_change_test.go new file mode 100644 index 000000000000..7a743fc5e6e7 --- /dev/null +++ b/beacon-chain/sync/validate_bls_to_execution_change_test.go @@ -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: ðpb.SignedBLSToExecutionChange{ + Message: ðpb.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: ðpb.SignedBLSToExecutionChange{ + Message: ðpb.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(ðpb.SignedBLSToExecutionChange{ + Message: ðpb.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: ðpb.SignedBLSToExecutionChange{ + Message: ðpb.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: ðpb.SignedBLSToExecutionChange{ + Message: ðpb.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: ðpb.SignedBLSToExecutionChange{ + Message: ðpb.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: ðpb.SignedBLSToExecutionChange{ + Message: ðpb.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: ðpb.SignedBLSToExecutionChange{ + Message: ðpb.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: ðpb.SignedBLSToExecutionChange{ + Message: ðpb.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) + } + }) + } +}