github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/verification/combined_signer_v2_test.go (about) 1 package verification 2 3 import ( 4 "testing" 5 6 "github.com/onflow/crypto" 7 "github.com/stretchr/testify/assert" 8 "github.com/stretchr/testify/mock" 9 "github.com/stretchr/testify/require" 10 11 "github.com/onflow/flow-go/consensus/hotstuff/mocks" 12 "github.com/onflow/flow-go/consensus/hotstuff/model" 13 "github.com/onflow/flow-go/consensus/hotstuff/signature" 14 "github.com/onflow/flow-go/model/flow" 15 "github.com/onflow/flow-go/module" 16 "github.com/onflow/flow-go/module/local" 17 modulemock "github.com/onflow/flow-go/module/mock" 18 msig "github.com/onflow/flow-go/module/signature" 19 "github.com/onflow/flow-go/state/protocol" 20 "github.com/onflow/flow-go/utils/unittest" 21 ) 22 23 // Test that when beacon key is available for a view, a signed block can pass the validation 24 // the sig include both staking sig and random beacon sig. 25 func TestCombinedSignWithBeaconKey(t *testing.T) { 26 identities := unittest.IdentityListFixture(4, unittest.WithRole(flow.RoleConsensus)) 27 28 // prepare data 29 beaconKey := unittest.RandomBeaconPriv() 30 pk := beaconKey.PublicKey() 31 view := uint64(20) 32 33 fblock := unittest.BlockFixture() 34 fblock.Header.ProposerID = identities[0].NodeID 35 fblock.Header.View = view 36 block := model.BlockFromFlow(fblock.Header) 37 signerID := fblock.Header.ProposerID 38 39 beaconKeyStore := modulemock.NewRandomBeaconKeyStore(t) 40 beaconKeyStore.On("ByView", view).Return(beaconKey, nil) 41 42 stakingPriv := unittest.StakingPrivKeyFixture() 43 nodeID := &unittest.IdentityFixture().IdentitySkeleton 44 nodeID.NodeID = signerID 45 nodeID.StakingPubKey = stakingPriv.PublicKey() 46 47 me, err := local.New(*nodeID, stakingPriv) 48 require.NoError(t, err) 49 signer := NewCombinedSigner(me, beaconKeyStore) 50 51 dkg := &mocks.DKG{} 52 dkg.On("KeyShare", signerID).Return(pk, nil) 53 54 committee := &mocks.DynamicCommittee{} 55 committee.On("DKG", mock.Anything).Return(dkg, nil) 56 57 packer := signature.NewConsensusSigDataPacker(committee) 58 verifier := NewCombinedVerifier(committee, packer) 59 60 // check that a created proposal can be verified by a verifier 61 proposal, err := signer.CreateProposal(block) 62 require.NoError(t, err) 63 64 vote := proposal.ProposerVote() 65 err = verifier.VerifyVote(nodeID, vote.SigData, proposal.Block.View, proposal.Block.BlockID) 66 require.NoError(t, err) 67 68 // check that a created proposal's signature is a combined staking sig and random beacon sig 69 msg := MakeVoteMessage(block.View, block.BlockID) 70 stakingSig, err := stakingPriv.Sign(msg, msig.NewBLSHasher(msig.ConsensusVoteTag)) 71 require.NoError(t, err) 72 73 beaconSig, err := beaconKey.Sign(msg, msig.NewBLSHasher(msig.RandomBeaconTag)) 74 require.NoError(t, err) 75 76 expectedSig := msig.EncodeDoubleSig(stakingSig, beaconSig) 77 require.Equal(t, expectedSig, proposal.SigData) 78 79 // vote should be valid 80 vote, err = signer.CreateVote(block) 81 require.NoError(t, err) 82 83 err = verifier.VerifyVote(nodeID, vote.SigData, proposal.Block.View, proposal.Block.BlockID) 84 require.NoError(t, err) 85 86 // vote on different block should be invalid 87 blockWrongID := *block 88 blockWrongID.BlockID[0]++ 89 err = verifier.VerifyVote(nodeID, vote.SigData, blockWrongID.View, blockWrongID.BlockID) 90 require.ErrorIs(t, err, model.ErrInvalidSignature) 91 92 // vote with a wrong view should be invalid 93 blockWrongView := *block 94 blockWrongView.View++ 95 err = verifier.VerifyVote(nodeID, vote.SigData, blockWrongID.View, blockWrongID.BlockID) 96 require.ErrorIs(t, err, model.ErrInvalidSignature) 97 98 // vote by different signer should be invalid 99 wrongVoter := &identities[1].IdentitySkeleton 100 wrongVoter.StakingPubKey = unittest.StakingPrivKeyFixture().PublicKey() 101 err = verifier.VerifyVote(wrongVoter, vote.SigData, block.View, block.BlockID) 102 require.ErrorIs(t, err, model.ErrInvalidSignature) 103 104 // vote with changed signature should be invalid 105 brokenSig := append([]byte{}, vote.SigData...) // copy 106 brokenSig[4]++ 107 err = verifier.VerifyVote(nodeID, brokenSig, block.View, block.BlockID) 108 require.ErrorIs(t, err, model.ErrInvalidSignature) 109 110 // Vote from a node that is _not_ part of the Random Beacon committee should be rejected. 111 // Specifically, we expect that the verifier recognizes the `protocol.IdentityNotFoundError` 112 // as a sign of an invalid vote and wraps it into a `model.InvalidSignerError`. 113 *dkg = mocks.DKG{} // overwrite DKG mock with a new one 114 dkg.On("KeyShare", signerID).Return(nil, protocol.IdentityNotFoundError{NodeID: signerID}) 115 err = verifier.VerifyVote(nodeID, vote.SigData, proposal.Block.View, proposal.Block.BlockID) 116 require.True(t, model.IsInvalidSignerError(err)) 117 } 118 119 // Test that when beacon key is not available for a view, a signed block can pass the validation 120 // the sig only include staking sig 121 func TestCombinedSignWithNoBeaconKey(t *testing.T) { 122 // prepare data 123 beaconKey := unittest.RandomBeaconPriv() 124 pk := beaconKey.PublicKey() 125 view := uint64(20) 126 127 fblock := unittest.BlockFixture() 128 fblock.Header.View = view 129 block := model.BlockFromFlow(fblock.Header) 130 signerID := fblock.Header.ProposerID 131 132 beaconKeyStore := modulemock.NewRandomBeaconKeyStore(t) 133 beaconKeyStore.On("ByView", view).Return(nil, module.ErrNoBeaconKeyForEpoch) 134 135 stakingPriv := unittest.StakingPrivKeyFixture() 136 nodeID := &unittest.IdentityFixture().IdentitySkeleton 137 nodeID.NodeID = signerID 138 nodeID.StakingPubKey = stakingPriv.PublicKey() 139 140 me, err := local.New(*nodeID, stakingPriv) 141 require.NoError(t, err) 142 signer := NewCombinedSigner(me, beaconKeyStore) 143 144 dkg := &mocks.DKG{} 145 dkg.On("KeyShare", signerID).Return(pk, nil) 146 147 committee := &mocks.DynamicCommittee{} 148 // even if the node failed DKG, and has no random beacon private key, 149 // but other nodes, who completed and succeeded DKG, have a public key 150 // for this failed node, which can be used to verify signature from 151 // this failed node. 152 committee.On("DKG", mock.Anything).Return(dkg, nil) 153 154 packer := signature.NewConsensusSigDataPacker(committee) 155 verifier := NewCombinedVerifier(committee, packer) 156 157 proposal, err := signer.CreateProposal(block) 158 require.NoError(t, err) 159 160 vote := proposal.ProposerVote() 161 err = verifier.VerifyVote(nodeID, vote.SigData, proposal.Block.View, proposal.Block.BlockID) 162 require.NoError(t, err) 163 164 // As the proposer does not have a Random Beacon Key, it should sign solely with its staking key. 165 // In this case, the SigData should be identical to the staking sig. 166 expectedStakingSig, err := stakingPriv.Sign( 167 MakeVoteMessage(block.View, block.BlockID), 168 msig.NewBLSHasher(msig.ConsensusVoteTag), 169 ) 170 require.NoError(t, err) 171 require.Equal(t, expectedStakingSig, crypto.Signature(proposal.SigData)) 172 } 173 174 // Test_VerifyQC_EmptySigners checks that Verifier returns an `model.InsufficientSignaturesError` 175 // if `signers` input is empty or nil. This check should happen _before_ the Verifier calls into 176 // any sub-components, because some (e.g. `crypto.AggregateBLSPublicKeys`) don't provide sufficient 177 // sentinel errors to distinguish between internal problems and external byzantine inputs. 178 func Test_VerifyQC_EmptySigners(t *testing.T) { 179 committee := &mocks.DynamicCommittee{} 180 dkg := &mocks.DKG{} 181 pk := &modulemock.PublicKey{} 182 pk.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil) 183 dkg.On("GroupKey").Return(pk) 184 committee.On("DKG", mock.Anything).Return(dkg, nil) 185 186 packer := signature.NewConsensusSigDataPacker(committee) 187 verifier := NewCombinedVerifier(committee, packer) 188 189 header := unittest.BlockHeaderFixture() 190 block := model.BlockFromFlow(header) 191 192 // sigData with empty signers 193 emptySignersInput := model.SignatureData{ 194 SigType: []byte{}, 195 AggregatedStakingSig: unittest.SignatureFixture(), 196 AggregatedRandomBeaconSig: unittest.SignatureFixture(), 197 ReconstructedRandomBeaconSig: unittest.SignatureFixture(), 198 } 199 encoder := new(model.SigDataPacker) 200 sigData, err := encoder.Encode(&emptySignersInput) 201 require.NoError(t, err) 202 203 err = verifier.VerifyQC(flow.IdentitySkeletonList{}, sigData, block.View, block.BlockID) 204 require.True(t, model.IsInsufficientSignaturesError(err)) 205 206 err = verifier.VerifyQC(nil, sigData, block.View, block.BlockID) 207 require.True(t, model.IsInsufficientSignaturesError(err)) 208 } 209 210 // TestCombinedSign_BeaconKeyStore_ViewForUnknownEpoch tests that if the beacon 211 // key store reports the view of the entity to sign has no known epoch, an 212 // exception should be raised. 213 func TestCombinedSign_BeaconKeyStore_ViewForUnknownEpoch(t *testing.T) { 214 beaconKeyStore := modulemock.NewRandomBeaconKeyStore(t) 215 beaconKeyStore.On("ByView", mock.Anything).Return(nil, model.ErrViewForUnknownEpoch) 216 217 stakingPriv := unittest.StakingPrivKeyFixture() 218 nodeID := unittest.IdentityFixture() 219 nodeID.StakingPubKey = stakingPriv.PublicKey() 220 221 me, err := local.New(nodeID.IdentitySkeleton, stakingPriv) 222 require.NoError(t, err) 223 signer := NewCombinedSigner(me, beaconKeyStore) 224 225 fblock := unittest.BlockHeaderFixture() 226 block := model.BlockFromFlow(fblock) 227 228 vote, err := signer.CreateVote(block) 229 require.Error(t, err) 230 assert.Nil(t, vote) 231 }