diff --git a/beacon-chain/core/blocks/block_operations.go b/beacon-chain/core/blocks/block_operations.go index bbc5e64b67..b259c78093 100644 --- a/beacon-chain/core/blocks/block_operations.go +++ b/beacon-chain/core/blocks/block_operations.go @@ -342,8 +342,7 @@ func ProcessProposerSlashings( if int(slashing.ProposerIndex) >= len(beaconState.Validators) { return nil, fmt.Errorf("invalid proposer index given in slashing %d", slashing.ProposerIndex) } - proposer := beaconState.Validators[slashing.ProposerIndex] - if err = verifyProposerSlashing(beaconState, proposer, slashing); err != nil { + if err = VerifyProposerSlashing(beaconState, slashing); err != nil { return nil, errors.Wrapf(err, "could not verify proposer slashing %d", idx) } beaconState, err = v.SlashValidator( @@ -356,13 +355,15 @@ func ProcessProposerSlashings( return beaconState, nil } -func verifyProposerSlashing( +// VerifyProposerSlashing verifies that the data provided fro slashing is valid. +func VerifyProposerSlashing( beaconState *pb.BeaconState, - proposer *ethpb.Validator, slashing *ethpb.ProposerSlashing, ) error { headerEpoch1 := helpers.SlotToEpoch(slashing.Header_1.Slot) headerEpoch2 := helpers.SlotToEpoch(slashing.Header_2.Slot) + proposer := beaconState.Validators[slashing.ProposerIndex] + if headerEpoch1 != headerEpoch2 { return fmt.Errorf("mismatched header epochs, received %d == %d", headerEpoch1, headerEpoch2) } diff --git a/beacon-chain/sync/BUILD.bazel b/beacon-chain/sync/BUILD.bazel index a08200b657..fba270a767 100644 --- a/beacon-chain/sync/BUILD.bazel +++ b/beacon-chain/sync/BUILD.bazel @@ -15,6 +15,7 @@ go_library( "subscriber.go", "subscriber_handlers.go", "validate_attester_slashing.go", + "validate_proposer_slashing.go", "validate_voluntary_exit.go", ], importpath = "github.com/prysmaticlabs/prysm/beacon-chain/sync", @@ -51,6 +52,7 @@ go_test( "rpc_test.go", "subscriber_test.go", "validate_attetser_slashing_test.go", + "validate_proposer_slashing_test.go", "validate_voluntary_exit_test.go", ], embed = [":go_default_library"], diff --git a/beacon-chain/sync/subscriber.go b/beacon-chain/sync/subscriber.go index ce83adee2c..63ae13df7b 100644 --- a/beacon-chain/sync/subscriber.go +++ b/beacon-chain/sync/subscriber.go @@ -53,8 +53,8 @@ func (r *RegularSync) registerSubscribers() { ) r.subscribe( "/eth2/proposer_slashing", - noopValidator, - notImplementedSubHandler, // TODO(3147): Implement. + r.validateProposerSlashing, + r.proposerSlashingSubscriber, ) r.subscribe( "/eth2/attester_slashing", diff --git a/beacon-chain/sync/subscriber_handlers.go b/beacon-chain/sync/subscriber_handlers.go index 15171cd220..7755b7877a 100644 --- a/beacon-chain/sync/subscriber_handlers.go +++ b/beacon-chain/sync/subscriber_handlers.go @@ -14,3 +14,8 @@ func (s *RegularSync) attesterSlashingSubscriber(ctx context.Context, msg proto. // TODO(#3259): Requires handlers in operations service to be implemented. return nil } + +func (s *RegularSync) proposerSlashingSubscriber(ctx context.Context, msg proto.Message) error { + // TODO(#3259): Requires handlers in operations service to be implemented. + return nil +} diff --git a/beacon-chain/sync/validate_proposer_slashing.go b/beacon-chain/sync/validate_proposer_slashing.go new file mode 100644 index 0000000000..095d053a79 --- /dev/null +++ b/beacon-chain/sync/validate_proposer_slashing.go @@ -0,0 +1,62 @@ +package sync + +import ( + "context" + + "github.com/gogo/protobuf/proto" + "github.com/karlseguin/ccache" + "github.com/prysmaticlabs/prysm/beacon-chain/core/blocks" + "github.com/prysmaticlabs/prysm/beacon-chain/p2p" + ethpb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1" + "github.com/prysmaticlabs/prysm/shared/hashutil" +) + +// seenProposerSlashings represents a cache of all the seen slashings +var seenProposerSlashings = ccache.New(ccache.Configure()) + +func propSlashingCacheKey(slashing *ethpb.ProposerSlashing) (string, error) { + hash, err := hashutil.HashProto(slashing) + if err != nil { + return "", err + } + return string(hash[:]), nil +} + +// Clients who receive a proposer slashing on this topic MUST validate the conditions within VerifyProposerSlashing before +// forwarding it across the network. +func (r *RegularSync) validateProposerSlashing(ctx context.Context, msg proto.Message, p p2p.Broadcaster) bool { + slashing, ok := msg.(*ethpb.ProposerSlashing) + if !ok { + return false + } + cacheKey, err := propSlashingCacheKey(slashing) + if err != nil { + log.WithError(err).Warn("could not hash proposer slashing") + return false + } + + invalidKey := invalid + cacheKey + if seenProposerSlashings.Get(invalidKey) != nil { + return false + } + if seenProposerSlashings.Get(cacheKey) != nil { + return false + } + state, err := r.db.HeadState(ctx) + if err != nil { + log.WithError(err).Error("Failed to get head state") + return false + } + + if err := blocks.VerifyProposerSlashing(state, slashing); err != nil { + log.WithError(err).Warn("Received invalid proposer slashing") + seenProposerSlashings.Set(invalidKey, true /*value*/, oneYear /*TTL*/) + return false + } + seenProposerSlashings.Set(cacheKey, true /*value*/, oneYear /*TTL*/) + + if err := p.Broadcast(ctx, slashing); err != nil { + log.WithError(err).Error("Failed to propagate proposer slashing") + } + return true +} diff --git a/beacon-chain/sync/validate_proposer_slashing_test.go b/beacon-chain/sync/validate_proposer_slashing_test.go new file mode 100644 index 0000000000..f7acb5a2d5 --- /dev/null +++ b/beacon-chain/sync/validate_proposer_slashing_test.go @@ -0,0 +1,135 @@ +package sync + +import ( + "context" + "crypto/rand" + "testing" + + "github.com/prysmaticlabs/go-ssz" + "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" + "github.com/prysmaticlabs/prysm/beacon-chain/db" + dbtest "github.com/prysmaticlabs/prysm/beacon-chain/db/testing" + p2ptest "github.com/prysmaticlabs/prysm/beacon-chain/p2p/testing" + pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1" + ethpb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1" + "github.com/prysmaticlabs/prysm/shared/bls" + "github.com/prysmaticlabs/prysm/shared/bytesutil" + "github.com/prysmaticlabs/prysm/shared/params" +) + +func setupValidProposerSlashing(t *testing.T, db db.Database) *ethpb.ProposerSlashing { + ctx := context.Background() + validators := make([]*ethpb.Validator, 100) + for i := 0; i < len(validators); i++ { + validators[i] = ðpb.Validator{ + EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance, + Slashed: false, + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + WithdrawableEpoch: params.BeaconConfig().FarFutureEpoch, + ActivationEpoch: 0, + } + } + validatorBalances := make([]uint64, len(validators)) + for i := 0; i < len(validatorBalances); i++ { + validatorBalances[i] = params.BeaconConfig().MaxEffectiveBalance + } + + currentSlot := uint64(0) + beaconState := &pb.BeaconState{ + Validators: validators, + Slot: currentSlot, + Balances: validatorBalances, + Fork: &pb.Fork{ + CurrentVersion: params.BeaconConfig().GenesisForkVersion, + PreviousVersion: params.BeaconConfig().GenesisForkVersion, + Epoch: 0, + }, + Slashings: make([]uint64, params.BeaconConfig().EpochsPerSlashingsVector), + RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), + ActiveIndexRoots: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), + } + + domain := helpers.Domain( + beaconState, + helpers.CurrentEpoch(beaconState), + params.BeaconConfig().DomainBeaconProposer, + ) + privKey, err := bls.RandKey(rand.Reader) + if err != nil { + t.Errorf("Could not generate random private key: %v", err) + } + + header1 := ðpb.BeaconBlockHeader{ + Slot: 0, + StateRoot: []byte("A"), + } + signingRoot, err := ssz.SigningRoot(header1) + if err != nil { + t.Errorf("Could not get signing root of beacon block header: %v", err) + } + header1.Signature = privKey.Sign(signingRoot[:], domain).Marshal()[:] + + header2 := ðpb.BeaconBlockHeader{ + Slot: 0, + StateRoot: []byte("B"), + } + signingRoot, err = ssz.SigningRoot(header2) + if err != nil { + t.Errorf("Could not get signing root of beacon block header: %v", err) + } + header2.Signature = privKey.Sign(signingRoot[:], domain).Marshal()[:] + + slashing := ðpb.ProposerSlashing{ + ProposerIndex: 1, + Header_1: header1, + Header_2: header2, + } + + beaconState.Validators[1].PublicKey = privKey.PublicKey().Marshal()[:] + + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + t.Fatal(err) + } + headBlockRoot := bytesutil.ToBytes32(b) + if err := db.SaveState(ctx, beaconState, headBlockRoot); err != nil { + t.Fatal(err) + } + if err := db.SaveHeadBlockRoot(ctx, headBlockRoot); err != nil { + t.Fatal(err) + } + return slashing +} + +func TestValidateProposerSlashing_ValidSlashing(t *testing.T) { + db := dbtest.SetupDB(t) + defer dbtest.TeardownDB(t, db) + p2p := p2ptest.NewTestP2P(t) + ctx := context.Background() + + slashing := setupValidProposerSlashing(t, db) + + r := &RegularSync{ + p2p: p2p, + db: db, + } + + if !r.validateProposerSlashing(ctx, slashing, p2p) { + t.Error("Failed validation") + } + + if !p2p.BroadcastCalled { + t.Error("Broadcast was not called") + } + + // A second message with the same information should not be valid for processing or + // propagation. + p2p.BroadcastCalled = false + if r.validateProposerSlashing(ctx, slashing, p2p) { + t.Error("Passed validation when should have failed") + } + + if p2p.BroadcastCalled { + t.Error("broadcast was called when it should not have been called") + } +}