diff --git a/beacon-chain/core/blocks/block_operations.go b/beacon-chain/core/blocks/block_operations.go index 17c46b45733a..276fc3dbc084 100644 --- a/beacon-chain/core/blocks/block_operations.go +++ b/beacon-chain/core/blocks/block_operations.go @@ -203,16 +203,16 @@ func ProcessBlockHeader( } // Verify proposer signature. - if err := VerifyBlockHeaderSignature(beaconState, block); err != nil { + if err := VerifyBlockSignature(beaconState, block); err != nil { return nil, err } return beaconState, nil } -// VerifyBlockHeaderSignature verifies the proposer signature of a beacon block. -func VerifyBlockHeaderSignature(beaconState *stateTrie.BeaconState, block *ethpb.SignedBeaconBlock) error { - proposer, err := beaconState.ValidatorAtIndexReadOnly(block.Block.ProposerIndex) +// VerifyBlockSignature verifies the proposer signature of a beacon block. +func VerifyBlockSignature(beaconState *stateTrie.BeaconState, block *ethpb.SignedBeaconBlock) error { + proposer, err := beaconState.ValidatorAtIndex(block.Block.ProposerIndex) if err != nil { return err } @@ -222,7 +222,7 @@ func VerifyBlockHeaderSignature(beaconState *stateTrie.BeaconState, block *ethpb if err != nil { return err } - proposerPubKey := proposer.PublicKey() + proposerPubKey := proposer.PublicKey return helpers.VerifyBlockSigningRoot(block.Block, proposerPubKey[:], block.Signature, domain) } diff --git a/beacon-chain/core/helpers/signing_root.go b/beacon-chain/core/helpers/signing_root.go index 643de5171db8..b151af734110 100644 --- a/beacon-chain/core/helpers/signing_root.go +++ b/beacon-chain/core/helpers/signing_root.go @@ -98,6 +98,28 @@ func VerifyBlockSigningRoot(blk *ethpb.BeaconBlock, pub []byte, signature []byte return nil } +// VerifyBlockHeaderSigningRoot verifies the signing root of a block header given it's public key, signature and domain. +func VerifyBlockHeaderSigningRoot(blkHdr *ethpb.BeaconBlockHeader, pub []byte, signature []byte, domain []byte) error { + publicKey, err := bls.PublicKeyFromBytes(pub) + if err != nil { + return errors.Wrap(err, "could not convert bytes to public key") + } + sig, err := bls.SignatureFromBytes(signature) + if err != nil { + return errors.Wrap(err, "could not convert bytes to signature") + } + root, err := signingRoot(func() ([32]byte, error) { + return stateutil.BlockHeaderRoot(blkHdr) + }, domain) + if err != nil { + return errors.Wrap(err, "could not compute signing root") + } + if !sig.Verify(root[:], publicKey) { + return ErrSigFailedToVerify + } + return nil +} + // ComputeDomain returns the domain version for BLS private key to sign and verify with a zeroed 4-byte // array as the fork version. // diff --git a/beacon-chain/sync/validate_beacon_blocks.go b/beacon-chain/sync/validate_beacon_blocks.go index 7e35f338fbef..df8094f1eb3d 100644 --- a/beacon-chain/sync/validate_beacon_blocks.go +++ b/beacon-chain/sync/validate_beacon_blocks.go @@ -111,7 +111,7 @@ func (r *Service) validateBeaconBlockPubSub(ctx context.Context, pid peer.ID, ms return false } - if err := blocks.VerifyBlockHeaderSignature(parentState, blk); err != nil { + if err := blocks.VerifyBlockSignature(parentState, blk); err != nil { log.WithError(err).WithField("blockSlot", blk.Block.Slot).Warn("Could not verify block signature") return false } diff --git a/slasher/beaconclient/chain_data.go b/slasher/beaconclient/chain_data.go index 3f7c595b08fc..805df94f6740 100644 --- a/slasher/beaconclient/chain_data.go +++ b/slasher/beaconclient/chain_data.go @@ -27,21 +27,25 @@ func (bs *Service) ChainHead( return res, nil } -// GenesisValidatorsRoot requests the beacon chain genesis validators -// root via gRPC. +// GenesisValidatorsRoot requests or fetch from memory the beacon chain genesis +// validators root via gRPC. func (bs *Service) GenesisValidatorsRoot( ctx context.Context, ) ([]byte, error) { ctx, span := trace.StartSpan(ctx, "beaconclient.GenesisValidatorsRoot") defer span.End() - res, err := bs.nodeClient.GetGenesis(ctx, &ptypes.Empty{}) - if err != nil { - return nil, errors.Wrap(err, "Could not retrieve genesis data") - } - if res == nil { - return nil, errors.Wrap(err, " genesis data") + + if bs.genesisValidatorRoot == nil { + res, err := bs.nodeClient.GetGenesis(ctx, &ptypes.Empty{}) + if err != nil { + return nil, errors.Wrap(err, "could not retrieve genesis data") + } + if res == nil { + return nil, errors.Wrap(err, "nil genesis data") + } + bs.genesisValidatorRoot = res.GenesisValidatorsRoot } - return res.GenesisValidatorsRoot, nil + return bs.genesisValidatorRoot, nil } // Poll the beacon node every syncStatusPollingInterval until the node diff --git a/slasher/beaconclient/chain_data_test.go b/slasher/beaconclient/chain_data_test.go index 766b3b07c140..d10b53d3df26 100644 --- a/slasher/beaconclient/chain_data_test.go +++ b/slasher/beaconclient/chain_data_test.go @@ -56,6 +56,14 @@ func TestService_GenesisValidatorsRoot(t *testing.T) { if !bytes.Equal(res, wanted.GenesisValidatorsRoot) { t.Errorf("Wanted %#x, received %#x", wanted.GenesisValidatorsRoot, res) } + // test next fetch uses memory and not the rpc call. + res, err = bs.GenesisValidatorsRoot(context.Background()) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(res, wanted.GenesisValidatorsRoot) { + t.Errorf("Wanted %#x, received %#x", wanted.GenesisValidatorsRoot, res) + } } func TestService_QuerySyncStatus(t *testing.T) { diff --git a/slasher/beaconclient/service.go b/slasher/beaconclient/service.go index 5b7cb576fa7f..bfa20930bf7b 100644 --- a/slasher/beaconclient/service.go +++ b/slasher/beaconclient/service.go @@ -58,6 +58,7 @@ type Service struct { receivedAttestationsBuffer chan *ethpb.IndexedAttestation collectedAttestationsBuffer chan []*ethpb.IndexedAttestation publicKeyCache *cache.PublicKeyCache + genesisValidatorRoot []byte } // Config options for the beaconclient service. diff --git a/slasher/detection/proposals/detector.go b/slasher/detection/proposals/detector.go index 9fa058ca6dfb..7bc4cd57b0c7 100644 --- a/slasher/detection/proposals/detector.go +++ b/slasher/detection/proposals/detector.go @@ -46,5 +46,8 @@ func (dd *ProposeDetector) DetectDoublePropose( } return ps, nil } + if err := dd.slasherDB.SaveBlockHeader(ctx, incomingBlk); err != nil { + return nil, err + } return nil, nil } diff --git a/slasher/rpc/BUILD.bazel b/slasher/rpc/BUILD.bazel index df86d24549cc..b8f328bbbf76 100644 --- a/slasher/rpc/BUILD.bazel +++ b/slasher/rpc/BUILD.bazel @@ -70,7 +70,9 @@ go_test( embed = [":go_default_library"], deps = [ "//beacon-chain/core/helpers:go_default_library", + "//beacon-chain/state/stateutil:go_default_library", "//shared/bls:go_default_library", + "//shared/bytesutil:go_default_library", "//shared/mock:go_default_library", "//shared/p2putils:go_default_library", "//shared/params:go_default_library", diff --git a/slasher/rpc/server.go b/slasher/rpc/server.go index b14ab32a4cbf..a5dde6f3c44c 100644 --- a/slasher/rpc/server.go +++ b/slasher/rpc/server.go @@ -34,6 +34,23 @@ type Server struct { func (ss *Server) IsSlashableAttestation(ctx context.Context, req *ethpb.IndexedAttestation) (*slashpb.AttesterSlashingResponse, error) { ctx, span := trace.StartSpan(ctx, "detection.IsSlashableAttestation") defer span.End() + + if req == nil { + return nil, status.Error(codes.InvalidArgument, "nil request provided") + } + if req.Data == nil { + return nil, status.Error(codes.InvalidArgument, "nil request data provided") + } + if req.Data.Target == nil { + return nil, status.Error(codes.InvalidArgument, "nil request data target provided") + } + if req.Data.Source == nil { + return nil, status.Error(codes.InvalidArgument, "nil request data source provided") + } + if req.Signature == nil { + return nil, status.Error(codes.InvalidArgument, "nil signature provided") + } + err := attestationutil.IsValidAttestationIndices(ctx, req) if err != nil { return nil, err @@ -91,5 +108,47 @@ func (ss *Server) IsSlashableAttestation(ctx context.Context, req *ethpb.Indexed // IsSlashableBlock returns an proposer slashing if the block submitted // is a double proposal. func (ss *Server) IsSlashableBlock(ctx context.Context, req *ethpb.SignedBeaconBlockHeader) (*slashpb.ProposerSlashingResponse, error) { - return nil, errors.New("unimplemented") + ctx, span := trace.StartSpan(ctx, "detection.IsSlashableBlock") + defer span.End() + + if req == nil { + return nil, status.Error(codes.InvalidArgument, "nil request provided") + } + if req.Header == nil { + return nil, status.Error(codes.InvalidArgument, "nil header provided") + + } + if req.Signature == nil { + return nil, status.Error(codes.InvalidArgument, "nil signature provided") + } + gvr, err := ss.beaconClient.GenesisValidatorsRoot(ctx) + if err != nil { + return nil, err + } + blockEpoch := helpers.SlotToEpoch(req.Header.Slot) + fork, err := p2putils.Fork(blockEpoch) + if err != nil { + return nil, err + } + domain, err := helpers.Domain(fork, blockEpoch, params.BeaconConfig().DomainBeaconProposer, gvr) + if err != nil { + return nil, err + } + pkMap, err := ss.beaconClient.FindOrGetPublicKeys(ctx, []uint64{req.Header.ProposerIndex}) + if err := helpers.VerifyBlockHeaderSigningRoot( + req.Header, pkMap[req.Header.ProposerIndex], req.Signature, domain); err != nil { + return nil, err + } + slashing, err := ss.detector.DetectDoubleProposals(ctx, req) + if err != nil { + return nil, status.Errorf(codes.Internal, "Could not detect proposer slashing for block: %v: %v", req, err) + } + psr := &slashpb.ProposerSlashingResponse{} + if slashing != nil { + psr = &slashpb.ProposerSlashingResponse{ + ProposerSlashing: []*ethpb.ProposerSlashing{slashing}, + } + } + return psr, nil + } diff --git a/slasher/rpc/server_test.go b/slasher/rpc/server_test.go index 4e9e7d393ea3..1232058cca60 100644 --- a/slasher/rpc/server_test.go +++ b/slasher/rpc/server_test.go @@ -7,7 +7,9 @@ import ( "github.com/golang/mock/gomock" ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" + "github.com/prysmaticlabs/prysm/beacon-chain/state/stateutil" "github.com/prysmaticlabs/prysm/shared/bls" + "github.com/prysmaticlabs/prysm/shared/bytesutil" "github.com/prysmaticlabs/prysm/shared/mock" "github.com/prysmaticlabs/prysm/shared/p2putils" "github.com/prysmaticlabs/prysm/shared/params" @@ -17,7 +19,7 @@ import ( "github.com/prysmaticlabs/prysm/slasher/detection" ) -func Test_DetectionFlow(t *testing.T) { +func TestServer_IsSlashableAttestation(t *testing.T) { db := testDB.SetupSlasherDB(t, false) ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -94,7 +96,7 @@ func Test_DetectionFlow(t *testing.T) { incomingAtt.Signature = marshalledSig - domain, err = helpers.Domain(fork, incomingAtt.Data.Target.Epoch, params.BeaconConfig().DomainBeaconAttester, wantedGenesis.GenesisValidatorsRoot) + domain, err = helpers.Domain(fork, savedAttestation.Data.Target.Epoch, params.BeaconConfig().DomainBeaconAttester, wantedGenesis.GenesisValidatorsRoot) if err != nil { t.Fatal(err) } @@ -127,7 +129,6 @@ func Test_DetectionFlow(t *testing.T) { gomock.Any(), gomock.Any(), ).Return(wantedValidators2, nil) - nClient.EXPECT().GetGenesis(gomock.Any(), gomock.Any()).Return(wantedGenesis, nil) slashing, err := server.IsSlashableAttestation(ctx, incomingAtt) if err != nil { t.Fatalf("got error while trying to detect slashing: %v", err) @@ -136,3 +137,105 @@ func Test_DetectionFlow(t *testing.T) { t.Fatalf("only one slashing should have been found. got: %v", len(slashing.AttesterSlashing)) } } + +func TestServer_IsSlashableBlock(t *testing.T) { + db := testDB.SetupSlasherDB(t, false) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + bClient := mock.NewMockBeaconChainClient(ctrl) + nClient := mock.NewMockNodeClient(ctrl) + ctx := context.Background() + + _, keys, err := testutil.DeterministicDepositsAndKeys(4) + if err != nil { + t.Fatal(err) + } + wantedValidators := ðpb.Validators{ + ValidatorList: []*ethpb.Validators_ValidatorContainer{ + { + Index: 1, Validator: ðpb.Validator{PublicKey: keys[1].PublicKey().Marshal()}, + }, + }, + } + bClient.EXPECT().ListValidators( + gomock.Any(), + gomock.Any(), + ).Return(wantedValidators, nil) + + wantedGenesis := ðpb.Genesis{ + GenesisValidatorsRoot: []byte("I am genesis"), + } + nClient.EXPECT().GetGenesis(gomock.Any(), gomock.Any()).Return(wantedGenesis, nil) + savedBlock := ðpb.SignedBeaconBlockHeader{ + Header: ðpb.BeaconBlockHeader{ + Slot: 1, + ProposerIndex: 1, + BodyRoot: bytesutil.PadTo([]byte("body root"), 32), + }, + } + incomingBlock := ðpb.SignedBeaconBlockHeader{ + Header: ðpb.BeaconBlockHeader{ + Slot: 1, + ProposerIndex: 1, + BodyRoot: bytesutil.PadTo([]byte("body root2"), 32), + }, + } + cfg := &detection.Config{ + SlasherDB: db, + } + incomingBlockEpoch := helpers.SlotToEpoch(incomingBlock.Header.Slot) + fork, err := p2putils.Fork(incomingBlockEpoch) + if err != nil { + t.Fatal(err) + } + domain, err := helpers.Domain(fork, incomingBlockEpoch, params.BeaconConfig().DomainBeaconProposer, wantedGenesis.GenesisValidatorsRoot) + if err != nil { + t.Fatal(err) + } + bhr, err := stateutil.BlockHeaderRoot(incomingBlock.Header) + if err != nil { + t.Error(err) + } + root, err := helpers.ComputeSigningRoot(bhr, domain) + if err != nil { + t.Error(err) + } + blockSig := keys[incomingBlock.Header.ProposerIndex].Sign(root[:]) + marshalledSig := blockSig.Marshal() + incomingBlock.Signature = marshalledSig + + savedBlockEpoch := helpers.SlotToEpoch(savedBlock.Header.Slot) + domain, err = helpers.Domain(fork, savedBlockEpoch, params.BeaconConfig().DomainBeaconProposer, wantedGenesis.GenesisValidatorsRoot) + if err != nil { + t.Fatal(err) + } + bhr, err = stateutil.BlockHeaderRoot(savedBlock.Header) + if err != nil { + t.Error(err) + } + root, err = helpers.ComputeSigningRoot(bhr, domain) + if err != nil { + t.Error(err) + } + blockSig = keys[savedBlock.Header.ProposerIndex].Sign(root[:]) + marshalledSig = blockSig.Marshal() + savedBlock.Signature = marshalledSig + bcCfg := &beaconclient.Config{BeaconClient: bClient, NodeClient: nClient, SlasherDB: db} + bs, err := beaconclient.NewBeaconClientService(ctx, bcCfg) + ds := detection.NewDetectionService(ctx, cfg) + server := Server{ctx: ctx, detector: ds, slasherDB: db, beaconClient: bs} + slashings, err := server.IsSlashableBlock(ctx, savedBlock) + if err != nil { + t.Fatalf("got error while trying to detect slashing: %v", err) + } + if len(slashings.ProposerSlashing) != 0 { + t.Fatalf("Found slashings while no slashing should have been found on first block: %v slashing found: %v", savedBlock, slashings) + } + slashing, err := server.IsSlashableBlock(ctx, incomingBlock) + if err != nil { + t.Fatalf("got error while trying to detect slashing: %v", err) + } + if len(slashing.ProposerSlashing) != 1 { + t.Fatalf("only one slashing should have been found. got: %v", len(slashing.ProposerSlashing)) + } +}