github.com/onflow/flow-go@v0.33.17/consensus/hotstuff/timeoutcollector/aggregation.go (about)

     1  package timeoutcollector
     2  
     3  import (
     4  	"fmt"
     5  	"sync"
     6  
     7  	"github.com/onflow/flow-go/consensus/hotstuff"
     8  	"github.com/onflow/flow-go/consensus/hotstuff/model"
     9  	"github.com/onflow/flow-go/consensus/hotstuff/verification"
    10  	"github.com/onflow/flow-go/crypto"
    11  	"github.com/onflow/flow-go/crypto/hash"
    12  	"github.com/onflow/flow-go/model/flow"
    13  	msig "github.com/onflow/flow-go/module/signature"
    14  )
    15  
    16  // signerInfo holds information about a signer, its public key and weight
    17  type signerInfo struct {
    18  	pk     crypto.PublicKey
    19  	weight uint64
    20  }
    21  
    22  // sigInfo holds signature and high QC view submitted by some signer
    23  type sigInfo struct {
    24  	sig          crypto.Signature
    25  	newestQCView uint64
    26  }
    27  
    28  // TimeoutSignatureAggregator implements consensus/hotstuff.TimeoutSignatureAggregator.
    29  // It performs timeout specific BLS aggregation over multiple distinct messages.
    30  // We perform timeout signature aggregation for some concrete view, utilizing the protocol specification
    31  // that timeouts sign the message: hash(view, newestQCView), where newestQCView can have different values
    32  // for different replicas.
    33  // View and the identities of all authorized replicas are
    34  // specified when the TimeoutSignatureAggregator is instantiated.
    35  // Each signer is allowed to sign at most once.
    36  // Aggregation uses BLS scheme. Mitigation against rogue attacks is done using Proof Of Possession (PoP).
    37  // Implementation is only safe under the assumption that all proofs of possession (PoP) of the public keys
    38  // are valid. This module does not perform the PoPs validity checks, it assumes verification was done
    39  // outside the module.
    40  // Implementation is thread-safe.
    41  type TimeoutSignatureAggregator struct {
    42  	lock          sync.RWMutex
    43  	hasher        hash.Hasher
    44  	idToInfo      map[flow.Identifier]signerInfo // auxiliary map to lookup signer weight and public key (only gets updated by constructor)
    45  	idToSignature map[flow.Identifier]sigInfo    // signatures indexed by the signer ID
    46  	totalWeight   uint64                         // total accumulated weight
    47  	view          uint64                         // view for which we are aggregating signatures
    48  }
    49  
    50  var _ hotstuff.TimeoutSignatureAggregator = (*TimeoutSignatureAggregator)(nil)
    51  
    52  // NewTimeoutSignatureAggregator returns a multi message signature aggregator initialized with a predefined view
    53  // for which we aggregate signatures, list of flow identities,
    54  // their respective public keys and a domain separation tag. The identities
    55  // represent the list of all authorized signers.
    56  // The constructor does not verify PoPs of input public keys, it assumes verification was done outside
    57  // this module.
    58  // The constructor errors if:
    59  // - the list of identities is empty
    60  // - if one of the keys is not a valid public key.
    61  //
    62  // A multi message sig aggregator is used for aggregating timeouts for a single view only. A new instance should be used for each
    63  // signature aggregation task in the protocol.
    64  func NewTimeoutSignatureAggregator(
    65  	view uint64, // view for which we are aggregating signatures
    66  	ids flow.IdentityList, // list of all authorized signers
    67  	dsTag string, // domain separation tag used by the signature
    68  ) (*TimeoutSignatureAggregator, error) {
    69  	if len(ids) == 0 {
    70  		return nil, fmt.Errorf("number of participants must be larger than 0, got %d", len(ids))
    71  	}
    72  	// sanity check for BLS keys
    73  	for i, identity := range ids {
    74  		if identity.StakingPubKey.Algorithm() != crypto.BLSBLS12381 {
    75  			return nil, fmt.Errorf("key at index %d is not a BLS key", i)
    76  		}
    77  	}
    78  
    79  	// build the internal map for a faster look-up
    80  	idToInfo := make(map[flow.Identifier]signerInfo)
    81  	for _, id := range ids {
    82  		idToInfo[id.NodeID] = signerInfo{
    83  			pk:     id.StakingPubKey,
    84  			weight: id.Weight,
    85  		}
    86  	}
    87  
    88  	return &TimeoutSignatureAggregator{
    89  		hasher:        msig.NewBLSHasher(dsTag), // concurrency safe
    90  		idToInfo:      idToInfo,
    91  		idToSignature: make(map[flow.Identifier]sigInfo),
    92  		view:          view,
    93  	}, nil
    94  }
    95  
    96  // VerifyAndAdd verifies the signature under the stored public keys and adds signature with corresponding
    97  // newest QC view to the internal set. Internal set and collected weight is modified iff the signer ID is not a duplicate and signature _is_ valid.
    98  // The total weight of all collected signatures (excluding duplicates) is returned regardless
    99  // of any returned error.
   100  // Expected errors during normal operations:
   101  //   - model.InvalidSignerError if signerID is invalid (not a consensus participant)
   102  //   - model.DuplicatedSignerError if the signer has been already added
   103  //   - model.ErrInvalidSignature if signerID is valid but signature is cryptographically invalid
   104  //
   105  // The function is thread-safe.
   106  func (a *TimeoutSignatureAggregator) VerifyAndAdd(signerID flow.Identifier, sig crypto.Signature, newestQCView uint64) (totalWeight uint64, exception error) {
   107  	info, ok := a.idToInfo[signerID]
   108  	if !ok {
   109  		return a.TotalWeight(), model.NewInvalidSignerErrorf("%v is not an authorized signer", signerID)
   110  	}
   111  
   112  	// to avoid expensive signature verification we will proceed with double lock style check
   113  	if a.hasSignature(signerID) {
   114  		return a.TotalWeight(), model.NewDuplicatedSignerErrorf("signature from %v was already added", signerID)
   115  	}
   116  
   117  	msg := verification.MakeTimeoutMessage(a.view, newestQCView)
   118  	valid, err := info.pk.Verify(sig, msg, a.hasher)
   119  	if err != nil {
   120  		return a.TotalWeight(), fmt.Errorf("couldn't verify signature from %s: %w", signerID, err)
   121  	}
   122  	if !valid {
   123  		return a.TotalWeight(), fmt.Errorf("invalid signature from %s: %w", signerID, model.ErrInvalidSignature)
   124  	}
   125  
   126  	a.lock.Lock()
   127  	defer a.lock.Unlock()
   128  
   129  	if _, duplicate := a.idToSignature[signerID]; duplicate {
   130  		return a.totalWeight, model.NewDuplicatedSignerErrorf("signature from %v was already added", signerID)
   131  	}
   132  
   133  	a.idToSignature[signerID] = sigInfo{
   134  		sig:          sig,
   135  		newestQCView: newestQCView,
   136  	}
   137  	a.totalWeight += info.weight
   138  
   139  	return a.totalWeight, nil
   140  }
   141  
   142  func (a *TimeoutSignatureAggregator) hasSignature(singerID flow.Identifier) bool {
   143  	a.lock.RLock()
   144  	defer a.lock.RUnlock()
   145  	_, found := a.idToSignature[singerID]
   146  	return found
   147  }
   148  
   149  // TotalWeight returns the total weight presented by the collected signatures.
   150  // The function is thread-safe
   151  func (a *TimeoutSignatureAggregator) TotalWeight() uint64 {
   152  	a.lock.RLock()
   153  	defer a.lock.RUnlock()
   154  	return a.totalWeight
   155  }
   156  
   157  // View returns view for which aggregation happens
   158  // The function is thread-safe
   159  func (a *TimeoutSignatureAggregator) View() uint64 {
   160  	return a.view
   161  }
   162  
   163  // Aggregate aggregates the signatures and returns the aggregated signature.
   164  // The resulting aggregated signature is guaranteed to be valid, as all individual
   165  // signatures are pre-validated before their addition.
   166  // Expected errors during normal operations:
   167  //   - model.InsufficientSignaturesError if no signatures have been added yet
   168  //
   169  // This function is thread-safe
   170  func (a *TimeoutSignatureAggregator) Aggregate() ([]hotstuff.TimeoutSignerInfo, crypto.Signature, error) {
   171  	a.lock.RLock()
   172  	defer a.lock.RUnlock()
   173  
   174  	sharesNum := len(a.idToSignature)
   175  	signatures := make([]crypto.Signature, 0, sharesNum)
   176  	signersData := make([]hotstuff.TimeoutSignerInfo, 0, sharesNum)
   177  	for id, info := range a.idToSignature {
   178  		signatures = append(signatures, info.sig)
   179  		signersData = append(signersData, hotstuff.TimeoutSignerInfo{
   180  			NewestQCView: info.newestQCView,
   181  			Signer:       id,
   182  		})
   183  	}
   184  
   185  	aggSignature, err := crypto.AggregateBLSSignatures(signatures)
   186  	if err != nil {
   187  		// `AggregateBLSSignatures` returns two possible errors:
   188  		//  - crypto.BLSAggregateEmptyListError if `signatures` slice is empty, i.e no signatures have been added yet:
   189  		//    respond with model.InsufficientSignaturesError
   190  		//  - crypto.invalidSignatureError if some signature(s) could not be decoded, which should be impossible since
   191  		//    we check all signatures before adding them (there is no `TrustedAdd` method in this module)
   192  		if crypto.IsBLSAggregateEmptyListError(err) {
   193  			return nil, nil, model.NewInsufficientSignaturesErrorf("cannot aggregate an empty list of signatures: %w", err)
   194  		}
   195  		// any other error here is a symptom of an internal bug
   196  		return nil, nil, fmt.Errorf("unexpected internal error during BLS signature aggregation: %w", err)
   197  	}
   198  
   199  	// TODO-1: add logic to check if only one `NewestQCView` is used. In that case
   200  	// check the aggregated signature is not identity (that's enough to ensure
   201  	// aggregated key is not identity, given all signatures are individually valid)
   202  	// This is not implemented for now because `VerifyTC` does not error for an identity public key
   203  	// (that's because the crypto layer currently does not return false when verifying signatures using `VerifyBLSSignatureManyMessages`
   204  	//  and encountering identity public keys)
   205  	//
   206  	// TODO-2: check if the logic should be extended to look at the partial aggregated signatures of all
   207  	// signatures against the same message.
   208  	return signersData, aggSignature, nil
   209  }