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  }