github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/verification/combined_signer_v3_test.go (about) 1 package verification 2 3 import ( 4 "testing" 5 6 "github.com/stretchr/testify/assert" 7 "github.com/stretchr/testify/mock" 8 "github.com/stretchr/testify/require" 9 10 "github.com/onflow/crypto" 11 12 "github.com/onflow/flow-go/consensus/hotstuff" 13 "github.com/onflow/flow-go/consensus/hotstuff/mocks" 14 "github.com/onflow/flow-go/consensus/hotstuff/model" 15 "github.com/onflow/flow-go/consensus/hotstuff/signature" 16 "github.com/onflow/flow-go/model/encoding" 17 "github.com/onflow/flow-go/model/flow" 18 "github.com/onflow/flow-go/module" 19 "github.com/onflow/flow-go/module/local" 20 modulemock "github.com/onflow/flow-go/module/mock" 21 msig "github.com/onflow/flow-go/module/signature" 22 "github.com/onflow/flow-go/state/protocol" 23 "github.com/onflow/flow-go/utils/unittest" 24 ) 25 26 // Test that when beacon key is available for a view, a signed block can pass the validation 27 // the sig is a random beacon sig. 28 func TestCombinedSignWithBeaconKeyV3(t *testing.T) { 29 // prepare data 30 beaconKey := unittest.RandomBeaconPriv() 31 pk := beaconKey.PublicKey() 32 view := uint64(20) 33 34 fblock := unittest.BlockFixture() 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 := NewCombinedSignerV3(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 := NewCombinedVerifierV3(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 71 beaconSig, err := beaconKey.Sign(msg, msig.NewBLSHasher(msig.RandomBeaconTag)) 72 require.NoError(t, err) 73 74 expectedSig := msig.EncodeSingleSig(encoding.SigTypeRandomBeacon, beaconSig) 75 require.Equal(t, expectedSig, proposal.SigData) 76 77 // Vote from a node that is _not_ part of the Random Beacon committee should be rejected. 78 // Specifically, we expect that the verifier recognizes the `protocol.IdentityNotFoundError` 79 // as a sign of an invalid vote and wraps it into a `model.InvalidSignerError`. 80 *dkg = mocks.DKG{} // overwrite DKG mock with a new one 81 dkg.On("KeyShare", signerID).Return(nil, protocol.IdentityNotFoundError{NodeID: signerID}) 82 err = verifier.VerifyVote(nodeID, vote.SigData, proposal.Block.View, proposal.Block.BlockID) 83 require.True(t, model.IsInvalidSignerError(err)) 84 } 85 86 // Test that when beacon key is not available for a view, a signed block can pass the validation 87 // the sig is a staking sig 88 func TestCombinedSignWithNoBeaconKeyV3(t *testing.T) { 89 // prepare data 90 beaconKey := unittest.RandomBeaconPriv() 91 pk := beaconKey.PublicKey() 92 view := uint64(20) 93 94 fblock := unittest.BlockFixture() 95 fblock.Header.View = view 96 block := model.BlockFromFlow(fblock.Header) 97 signerID := fblock.Header.ProposerID 98 99 beaconKeyStore := modulemock.NewRandomBeaconKeyStore(t) 100 beaconKeyStore.On("ByView", view).Return(nil, module.ErrNoBeaconKeyForEpoch) 101 102 stakingPriv := unittest.StakingPrivKeyFixture() 103 nodeID := &unittest.IdentityFixture().IdentitySkeleton 104 nodeID.NodeID = signerID 105 nodeID.StakingPubKey = stakingPriv.PublicKey() 106 107 me, err := local.New(*nodeID, stakingPriv) 108 require.NoError(t, err) 109 signer := NewCombinedSignerV3(me, beaconKeyStore) 110 111 dkg := &mocks.DKG{} 112 dkg.On("KeyShare", signerID).Return(pk, nil) 113 114 committee := &mocks.DynamicCommittee{} 115 // even if the node failed DKG, and has no random beacon private key, 116 // but other nodes, who completed and succeeded DKG, have a public key 117 // for this failed node, which can be used to verify signature from 118 // this failed node. 119 committee.On("DKG", mock.Anything).Return(dkg, nil) 120 121 packer := signature.NewConsensusSigDataPacker(committee) 122 verifier := NewCombinedVerifierV3(committee, packer) 123 124 proposal, err := signer.CreateProposal(block) 125 require.NoError(t, err) 126 127 vote := proposal.ProposerVote() 128 err = verifier.VerifyVote(nodeID, vote.SigData, proposal.Block.View, proposal.Block.BlockID) 129 require.NoError(t, err) 130 131 // check that a created proposal's signature is a combined staking sig and random beacon sig 132 msg := MakeVoteMessage(block.View, block.BlockID) 133 stakingSig, err := stakingPriv.Sign(msg, msig.NewBLSHasher(msig.ConsensusVoteTag)) 134 require.NoError(t, err) 135 136 expectedSig := msig.EncodeSingleSig(encoding.SigTypeStaking, stakingSig) 137 138 // check the signature only has staking sig 139 require.Equal(t, expectedSig, proposal.SigData) 140 } 141 142 // Test_VerifyQC checks that a QC where either signer list is empty is rejected as invalid 143 func Test_VerifyQCV3(t *testing.T) { 144 header := unittest.BlockHeaderFixture() 145 block := model.BlockFromFlow(header) 146 msg := MakeVoteMessage(block.View, block.BlockID) 147 148 // generate some BLS key as a stub of the random beacon group key and use it to generate a reconstructed beacon sig 149 privGroupKey, beaconSig := generateSignature(t, msg, msig.RandomBeaconTag) 150 dkg := &mocks.DKG{} 151 dkg.On("GroupKey").Return(privGroupKey.PublicKey(), nil) 152 dkg.On("Size").Return(uint(20)) 153 committee := &mocks.DynamicCommittee{} 154 committee.On("DKG", mock.Anything).Return(dkg, nil) 155 156 // generate 17 BLS keys as stubs for staking keys and use them to generate an aggregated staking sig 157 privStakingKeys, aggStakingSig := generateAggregatedSignature(t, 17, msg, msig.ConsensusVoteTag) 158 // generate 11 BLS keys as stubs for individual random beacon key shares and use them to generate an aggregated rand beacon sig 159 privRbKeyShares, aggRbSig := generateAggregatedSignature(t, 11, msg, msig.RandomBeaconTag) 160 161 stakingSigners := generateIdentitiesForPrivateKeys(t, privStakingKeys) 162 rbSigners := generateIdentitiesForPrivateKeys(t, privRbKeyShares) 163 registerPublicRbKeys(t, dkg, rbSigners.NodeIDs(), privRbKeyShares) 164 allSigners := append(append(flow.IdentityList{}, stakingSigners...), rbSigners...).ToSkeleton() 165 166 packedSigData := unittest.RandomBytes(1021) 167 unpackedSigData := hotstuff.BlockSignatureData{ 168 StakingSigners: stakingSigners.NodeIDs(), 169 AggregatedStakingSig: aggStakingSig, 170 RandomBeaconSigners: rbSigners.NodeIDs(), 171 AggregatedRandomBeaconSig: aggRbSig, 172 ReconstructedRandomBeaconSig: beaconSig, 173 } 174 175 // first, we check that our testing setup works for a correct QC 176 t.Run("valid QC", func(t *testing.T) { 177 packer := &mocks.Packer{} 178 packer.On("Unpack", mock.Anything, packedSigData).Return(&unpackedSigData, nil) 179 180 verifier := NewCombinedVerifierV3(committee, packer) 181 err := verifier.VerifyQC(allSigners, packedSigData, block.View, block.BlockID) 182 require.NoError(t, err) 183 }) 184 185 // Here, we test correct verification of a QC, where all replicas signed with their 186 // random beacon keys. This is optimal happy path. 187 // * empty list of staking signers 188 // * _no_ aggregated staking sig in QC 189 // The Verifier should accept such QC 190 t.Run("all replicas signed with random beacon keys", func(t *testing.T) { 191 sd := unpackedSigData // copy correct QC 192 sd.StakingSigners = []flow.Identifier{} 193 sd.AggregatedStakingSig = []byte{} 194 195 packer := &mocks.Packer{} 196 packer.On("Unpack", mock.Anything, packedSigData).Return(&sd, nil) 197 verifier := NewCombinedVerifierV3(committee, packer) 198 err := verifier.VerifyQC(allSigners, packedSigData, block.View, block.BlockID) 199 require.NoError(t, err) 200 }) 201 202 // Modify the correct QC: 203 // * empty list of staking signers 204 // * but an aggregated staking sig is given 205 // The Verifier should recognize this as an invalid QC. 206 t.Run("empty staking signers but aggregated staking sig in QC", func(t *testing.T) { 207 sd := unpackedSigData // copy correct QC 208 sd.StakingSigners = []flow.Identifier{} 209 210 packer := &mocks.Packer{} 211 packer.On("Unpack", mock.Anything, packedSigData).Return(&sd, nil) 212 verifier := NewCombinedVerifierV3(committee, packer) 213 err := verifier.VerifyQC(allSigners, packedSigData, block.View, block.BlockID) 214 require.True(t, model.IsInvalidFormatError(err)) 215 }) 216 217 // Modify the correct QC: empty list of random beacon signers. 218 // The Verifier should recognize this as an invalid QC 219 t.Run("empty random beacon signers", func(t *testing.T) { 220 sd := unpackedSigData // copy correct QC 221 sd.RandomBeaconSigners = []flow.Identifier{} 222 223 packer := &mocks.Packer{} 224 packer.On("Unpack", mock.Anything, packedSigData).Return(&sd, nil) 225 verifier := NewCombinedVerifierV3(committee, packer) 226 err := verifier.VerifyQC(allSigners, packedSigData, block.View, block.BlockID) 227 require.True(t, model.IsInvalidFormatError(err)) 228 }) 229 230 // Modify the correct QC: too few random beacon signers. 231 // The Verifier should recognize this as an invalid QC 232 t.Run("too few random beacon signers", func(t *testing.T) { 233 // In total, we have 20 DKG participants, i.e. we require at least 10 random 234 // beacon sig shares. But we only supply 5 aggregated key shares. 235 sd := unpackedSigData // copy correct QC 236 sd.RandomBeaconSigners = rbSigners[:5].NodeIDs() 237 sd.AggregatedRandomBeaconSig = aggregatedSignature(t, privRbKeyShares[:5], msg, msig.RandomBeaconTag) 238 239 packer := &mocks.Packer{} 240 packer.On("Unpack", mock.Anything, packedSigData).Return(&sd, nil) 241 verifier := NewCombinedVerifierV3(committee, packer) 242 err := verifier.VerifyQC(allSigners, packedSigData, block.View, block.BlockID) 243 require.True(t, model.IsInvalidFormatError(err)) 244 }) 245 246 } 247 248 // Test_VerifyQC_EmptySignersV3 checks that Verifier returns an `model.InsufficientSignaturesError` 249 // if `signers` input is empty or nil. This check should happen _before_ the Verifier calls into 250 // any sub-components, because some (e.g. `crypto.AggregateBLSPublicKeys`) don't provide sufficient 251 // sentinel errors to distinguish between internal problems and external byzantine inputs. 252 func Test_VerifyQC_EmptySignersV3(t *testing.T) { 253 committee := &mocks.DynamicCommittee{} 254 dkg := &mocks.DKG{} 255 pk := &modulemock.PublicKey{} 256 pk.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil) 257 dkg.On("GroupKey").Return(pk) 258 committee.On("DKG", mock.Anything).Return(dkg, nil) 259 packer := signature.NewConsensusSigDataPacker(committee) 260 verifier := NewCombinedVerifier(committee, packer) 261 262 header := unittest.BlockHeaderFixture() 263 block := model.BlockFromFlow(header) 264 // sigData with empty signers 265 emptySignersInput := model.SignatureData{ 266 SigType: []byte{}, 267 AggregatedStakingSig: unittest.SignatureFixture(), 268 AggregatedRandomBeaconSig: unittest.SignatureFixture(), 269 ReconstructedRandomBeaconSig: unittest.SignatureFixture(), 270 } 271 encoder := new(model.SigDataPacker) 272 sigData, err := encoder.Encode(&emptySignersInput) 273 require.NoError(t, err) 274 275 err = verifier.VerifyQC(flow.IdentitySkeletonList{}, sigData, block.View, block.BlockID) 276 require.True(t, model.IsInsufficientSignaturesError(err)) 277 278 err = verifier.VerifyQC(nil, sigData, block.View, block.BlockID) 279 require.True(t, model.IsInsufficientSignaturesError(err)) 280 } 281 282 // TestCombinedSign_BeaconKeyStore_ViewForUnknownEpochv3 tests that if the beacon 283 // key store reports the view of the entity to sign has no known epoch, an 284 // exception should be raised. 285 func TestCombinedSign_BeaconKeyStore_ViewForUnknownEpochv3(t *testing.T) { 286 beaconKeyStore := modulemock.NewRandomBeaconKeyStore(t) 287 beaconKeyStore.On("ByView", mock.Anything).Return(nil, model.ErrViewForUnknownEpoch) 288 289 stakingPriv := unittest.StakingPrivKeyFixture() 290 nodeID := unittest.IdentityFixture() 291 nodeID.StakingPubKey = stakingPriv.PublicKey() 292 293 me, err := local.New(nodeID.IdentitySkeleton, stakingPriv) 294 require.NoError(t, err) 295 signer := NewCombinedSigner(me, beaconKeyStore) 296 297 fblock := unittest.BlockHeaderFixture() 298 block := model.BlockFromFlow(fblock) 299 300 vote, err := signer.CreateVote(block) 301 require.Error(t, err) 302 assert.Nil(t, vote) 303 } 304 305 func generateIdentitiesForPrivateKeys(t *testing.T, pivKeys []crypto.PrivateKey) flow.IdentityList { 306 ids := make([]*flow.Identity, 0, len(pivKeys)) 307 for _, k := range pivKeys { 308 id := unittest.IdentityFixture( 309 unittest.WithRole(flow.RoleConsensus), 310 unittest.WithStakingPubKey(k.PublicKey()), 311 ) 312 ids = append(ids, id) 313 } 314 return ids 315 } 316 317 func registerPublicRbKeys(t *testing.T, dkg *mocks.DKG, signerIDs []flow.Identifier, pivKeys []crypto.PrivateKey) { 318 assert.Equal(t, len(signerIDs), len(pivKeys), "one signer ID per key expected") 319 for k, id := range signerIDs { 320 dkg.On("KeyShare", id).Return(pivKeys[k].PublicKey(), nil) 321 } 322 } 323 324 // generateAggregatedSignature generates `n` private BLS keys, signs `msg` which each key, 325 // and aggregates the resulting sigs. Returns private keys and aggregated sig. 326 func generateAggregatedSignature(t *testing.T, n int, msg []byte, tag string) ([]crypto.PrivateKey, crypto.Signature) { 327 sigs := make([]crypto.Signature, 0, n) 328 privs := make([]crypto.PrivateKey, 0, n) 329 for ; n > 0; n-- { 330 priv, sig := generateSignature(t, msg, tag) 331 sigs = append(sigs, sig) 332 privs = append(privs, priv) 333 } 334 agg, err := crypto.AggregateBLSSignatures(sigs) 335 require.NoError(t, err) 336 return privs, agg 337 } 338 339 // generateSignature creates a single private BLS 12-381 key, signs the provided `message` with 340 // using domain separation `tag` and return the private key and signature. 341 func generateSignature(t *testing.T, message []byte, tag string) (crypto.PrivateKey, crypto.Signature) { 342 priv := unittest.PrivateKeyFixture(crypto.BLSBLS12381, crypto.KeyGenSeedMinLen) 343 sig, err := priv.Sign(message, msig.NewBLSHasher(tag)) 344 require.NoError(t, err) 345 return priv, sig 346 } 347 348 func aggregatedSignature(t *testing.T, pivKeys []crypto.PrivateKey, message []byte, tag string) crypto.Signature { 349 hasher := msig.NewBLSHasher(tag) 350 sigs := make([]crypto.Signature, 0, len(pivKeys)) 351 for _, k := range pivKeys { 352 sig, err := k.Sign(message, hasher) 353 require.NoError(t, err) 354 sigs = append(sigs, sig) 355 } 356 agg, err := crypto.AggregateBLSSignatures(sigs) 357 require.NoError(t, err) 358 return agg 359 }