github.com/onflow/flow-go@v0.33.17/consensus/hotstuff/timeoutcollector/aggregation_test.go (about) 1 package timeoutcollector 2 3 import ( 4 "math/rand" 5 "sync" 6 "testing" 7 8 "github.com/stretchr/testify/require" 9 10 "github.com/onflow/flow-go/crypto" 11 "github.com/onflow/flow-go/crypto/hash" 12 13 "github.com/onflow/flow-go/consensus/hotstuff" 14 "github.com/onflow/flow-go/consensus/hotstuff/model" 15 "github.com/onflow/flow-go/consensus/hotstuff/verification" 16 "github.com/onflow/flow-go/model/flow" 17 msig "github.com/onflow/flow-go/module/signature" 18 "github.com/onflow/flow-go/utils/unittest" 19 ) 20 21 // createAggregationData is a helper which creates fixture data for testing 22 func createAggregationData(t *testing.T, signersNumber int) ( 23 *TimeoutSignatureAggregator, 24 flow.IdentityList, 25 []crypto.PublicKey, 26 []crypto.Signature, 27 []hotstuff.TimeoutSignerInfo, 28 [][]byte, 29 []hash.Hasher) { 30 31 // create message and tag 32 tag := "random_tag" 33 hasher := msig.NewBLSHasher(tag) 34 sigs := make([]crypto.Signature, 0, signersNumber) 35 signersInfo := make([]hotstuff.TimeoutSignerInfo, 0, signersNumber) 36 msgs := make([][]byte, 0, signersNumber) 37 hashers := make([]hash.Hasher, 0, signersNumber) 38 39 // create keys, identities and signatures 40 ids := make([]*flow.Identity, 0, signersNumber) 41 pks := make([]crypto.PublicKey, 0, signersNumber) 42 view := 10 + uint64(rand.Uint32()) 43 for i := 0; i < signersNumber; i++ { 44 sk := unittest.PrivateKeyFixture(crypto.BLSBLS12381, crypto.KeyGenSeedMinLen) 45 identity := unittest.IdentityFixture(unittest.WithStakingPubKey(sk.PublicKey())) 46 // id 47 ids = append(ids, identity) 48 // keys 49 newestQCView := uint64(rand.Intn(int(view))) 50 msg := verification.MakeTimeoutMessage(view, newestQCView) 51 // signatures 52 sig, err := sk.Sign(msg, hasher) 53 require.NoError(t, err) 54 sigs = append(sigs, sig) 55 56 pks = append(pks, identity.StakingPubKey) 57 signersInfo = append(signersInfo, hotstuff.TimeoutSignerInfo{ 58 NewestQCView: newestQCView, 59 Signer: identity.NodeID, 60 }) 61 hashers = append(hashers, hasher) 62 msgs = append(msgs, msg) 63 } 64 aggregator, err := NewTimeoutSignatureAggregator(view, ids, tag) 65 require.NoError(t, err) 66 return aggregator, ids, pks, sigs, signersInfo, msgs, hashers 67 } 68 69 // TestNewTimeoutSignatureAggregator tests different happy and unhappy path scenarios when constructing 70 // multi message signature aggregator. 71 func TestNewTimeoutSignatureAggregator(t *testing.T) { 72 tag := "random_tag" 73 74 sk := unittest.PrivateKeyFixture(crypto.ECDSAP256, crypto.KeyGenSeedMinLen) 75 signer := unittest.IdentityFixture(unittest.WithStakingPubKey(sk.PublicKey())) 76 // wrong key type 77 _, err := NewTimeoutSignatureAggregator(0, flow.IdentityList{signer}, tag) 78 require.Error(t, err) 79 // empty signers 80 _, err = NewTimeoutSignatureAggregator(0, flow.IdentityList{}, tag) 81 require.Error(t, err) 82 } 83 84 // TestTimeoutSignatureAggregator_HappyPath tests happy path when aggregating signatures 85 // Tests verification, adding and aggregation. Test is performed in concurrent environment 86 func TestTimeoutSignatureAggregator_HappyPath(t *testing.T) { 87 signersNum := 20 88 aggregator, ids, pks, sigs, signersData, msgs, hashers := createAggregationData(t, signersNum) 89 90 // only add a subset of the signatures 91 subSet := signersNum / 2 92 expectedWeight := uint64(0) 93 var wg sync.WaitGroup 94 for i, sig := range sigs[subSet:] { 95 wg.Add(1) 96 // test thread safety 97 go func(i int, sig crypto.Signature) { 98 defer wg.Done() 99 index := i + subSet 100 // test VerifyAndAdd 101 _, err := aggregator.VerifyAndAdd(ids[index].NodeID, sig, signersData[index].NewestQCView) 102 // ignore weight as comparing against expected weight is not thread safe 103 require.NoError(t, err) 104 }(i, sig) 105 expectedWeight += ids[i+subSet].Weight 106 } 107 108 wg.Wait() 109 actualSignersInfo, aggSig, err := aggregator.Aggregate() 110 require.NoError(t, err) 111 require.ElementsMatch(t, signersData[subSet:], actualSignersInfo) 112 113 ok, err := crypto.VerifyBLSSignatureManyMessages(pks[subSet:], aggSig, msgs[subSet:], hashers[subSet:]) 114 require.NoError(t, err) 115 require.True(t, ok) 116 117 // add remaining signatures in one thread in order to test the returned weight 118 for i, sig := range sigs[:subSet] { 119 weight, err := aggregator.VerifyAndAdd(ids[i].NodeID, sig, signersData[i].NewestQCView) 120 require.NoError(t, err) 121 expectedWeight += ids[i].Weight 122 require.Equal(t, expectedWeight, weight) 123 // test TotalWeight 124 require.Equal(t, expectedWeight, aggregator.TotalWeight()) 125 } 126 actualSignersInfo, aggSig, err = aggregator.Aggregate() 127 require.NoError(t, err) 128 require.ElementsMatch(t, signersData, actualSignersInfo) 129 130 ok, err = crypto.VerifyBLSSignatureManyMessages(pks, aggSig, msgs, hashers) 131 require.NoError(t, err) 132 require.True(t, ok) 133 } 134 135 // TestTimeoutSignatureAggregator_VerifyAndAdd tests behavior of VerifyAndAdd under invalid input data. 136 func TestTimeoutSignatureAggregator_VerifyAndAdd(t *testing.T) { 137 signersNum := 20 138 139 // Unhappy paths 140 t.Run("invalid signer ID", func(t *testing.T) { 141 aggregator, _, _, sigs, signersInfo, _, _ := createAggregationData(t, signersNum) 142 // generate an ID that is not in the node ID list 143 invalidId := unittest.IdentifierFixture() 144 145 weight, err := aggregator.VerifyAndAdd(invalidId, sigs[0], signersInfo[0].NewestQCView) 146 require.Equal(t, uint64(0), weight) 147 require.Equal(t, uint64(0), aggregator.TotalWeight()) 148 require.True(t, model.IsInvalidSignerError(err)) 149 }) 150 151 t.Run("duplicate signature", func(t *testing.T) { 152 aggregator, ids, _, sigs, signersInfo, _, _ := createAggregationData(t, signersNum) 153 expectedWeight := uint64(0) 154 // add signatures 155 for i, sig := range sigs { 156 weight, err := aggregator.VerifyAndAdd(ids[i].NodeID, sig, signersInfo[i].NewestQCView) 157 expectedWeight += ids[i].Weight 158 require.Equal(t, expectedWeight, weight) 159 require.NoError(t, err) 160 } 161 // add same duplicates and test thread safety 162 var wg sync.WaitGroup 163 for i, sig := range sigs { 164 wg.Add(1) 165 // test thread safety 166 go func(i int, sig crypto.Signature) { 167 defer wg.Done() 168 weight, err := aggregator.VerifyAndAdd(ids[i].NodeID, sigs[i], signersInfo[i].NewestQCView) // same signature for same index 169 // weight should not change 170 require.Equal(t, expectedWeight, weight) 171 require.True(t, model.IsDuplicatedSignerError(err)) 172 weight, err = aggregator.VerifyAndAdd(ids[i].NodeID, sigs[(i+1)%signersNum], signersInfo[(i+1)%signersNum].NewestQCView) // different signature for same index 173 // weight should not change 174 require.Equal(t, expectedWeight, weight) 175 require.True(t, model.IsDuplicatedSignerError(err)) 176 weight, err = aggregator.VerifyAndAdd(ids[(i+1)%signersNum].NodeID, sigs[(i+1)%signersNum], signersInfo[(i+1)%signersNum].NewestQCView) // different signature for same index 177 // weight should not change 178 require.Equal(t, expectedWeight, weight) 179 require.True(t, model.IsDuplicatedSignerError(err)) 180 }(i, sig) 181 } 182 wg.Wait() 183 }) 184 } 185 186 // TestTimeoutSignatureAggregator_Aggregate tests that Aggregate performs internal checks and 187 // doesn't produce aggregated signature even when feed with invalid signatures. 188 func TestTimeoutSignatureAggregator_Aggregate(t *testing.T) { 189 signersNum := 20 190 191 t.Run("invalid signature", func(t *testing.T) { 192 var err error 193 aggregator, ids, pks, sigs, signersInfo, msgs, hashers := createAggregationData(t, signersNum) 194 // replace sig with random one 195 sk := unittest.PrivateKeyFixture(crypto.BLSBLS12381, crypto.KeyGenSeedMinLen) 196 sigs[0], err = sk.Sign([]byte("dummy"), hashers[0]) 197 require.NoError(t, err) 198 199 // test VerifyAndAdd 200 _, err = aggregator.VerifyAndAdd(ids[0].NodeID, sigs[0], signersInfo[0].NewestQCView) 201 require.ErrorIs(t, err, model.ErrInvalidSignature) 202 203 // add signatures for aggregation including corrupt sigs[0] 204 expectedWeight := uint64(0) 205 for i, sig := range sigs { 206 weight, err := aggregator.VerifyAndAdd(ids[i].NodeID, sig, signersInfo[i].NewestQCView) 207 if err == nil { 208 expectedWeight += ids[i].Weight 209 } 210 require.Equal(t, expectedWeight, weight) 211 } 212 signers, aggSig, err := aggregator.Aggregate() 213 require.NoError(t, err) 214 // we should have signers for all signatures except first one since it's invalid 215 require.Equal(t, len(signers), len(ids)-1) 216 217 ok, err := crypto.VerifyBLSSignatureManyMessages(pks[1:], aggSig, msgs[1:], hashers[1:]) 218 require.NoError(t, err) 219 require.True(t, ok) 220 }) 221 222 t.Run("aggregating empty set of signatures", func(t *testing.T) { 223 aggregator, _, _, _, _, _, _ := createAggregationData(t, signersNum) 224 225 // no signatures were added => aggregate should error with 226 signersData, aggSig, err := aggregator.Aggregate() 227 require.True(t, model.IsInsufficientSignaturesError(err)) 228 require.Nil(t, signersData) 229 require.Nil(t, aggSig) 230 231 // Also, _after_ attempting to add a signature from unknown `signerID`: 232 // calling `Aggregate()` should error with `model.InsufficientSignaturesError`, 233 // as still zero signatures are stored. 234 _, err = aggregator.VerifyAndAdd(unittest.IdentifierFixture(), unittest.SignatureFixture(), 0) 235 require.True(t, model.IsInvalidSignerError(err)) 236 237 signersData, aggSig, err = aggregator.Aggregate() 238 require.True(t, model.IsInsufficientSignaturesError(err)) 239 require.Nil(t, signersData) 240 require.Nil(t, aggSig) 241 }) 242 }