github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/timeoutcollector/aggregation.go (about)

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