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

     1  package signature
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sync"
     7  
     8  	"github.com/onflow/crypto"
     9  
    10  	"github.com/onflow/flow-go/consensus/hotstuff"
    11  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    12  	"github.com/onflow/flow-go/model/flow"
    13  	"github.com/onflow/flow-go/module/signature"
    14  )
    15  
    16  // signerInfo holds information about a signer, its weight and index
    17  type signerInfo struct {
    18  	weight uint64
    19  	index  int
    20  }
    21  
    22  // WeightedSignatureAggregator implements consensus/hotstuff.WeightedSignatureAggregator.
    23  // It is a wrapper around module/signature.SignatureAggregatorSameMessage, which implements a
    24  // mapping from node IDs (as used by HotStuff) to index-based addressing of authorized
    25  // signers (as used by SignatureAggregatorSameMessage).
    26  //
    27  // Similarly to module/signature.SignatureAggregatorSameMessage, this module assumes proofs of possession (PoP)
    28  // of all identity public keys are valid.
    29  type WeightedSignatureAggregator struct {
    30  	aggregator  *signature.SignatureAggregatorSameMessage // low level crypto BLS aggregator, agnostic of weights and flow IDs
    31  	ids         flow.IdentityList                         // all possible ids (only gets updated by constructor)
    32  	idToInfo    map[flow.Identifier]signerInfo            // auxiliary map to lookup signer weight and index by ID (only gets updated by constructor)
    33  	totalWeight uint64                                    // weight collected (gets updated)
    34  	lock        sync.RWMutex                              // lock for atomic updates to totalWeight and collectedIDs
    35  
    36  	// collectedIDs tracks the Identities of all nodes whose signatures have been collected so far.
    37  	// The reason for tracking the duplicate signers at this module level is that having no duplicates
    38  	// is a Hotstuff constraint, rather than a cryptographic aggregation constraint. We are planning to
    39  	// extend the cryptographic primitives to support multiplicity higher than 1 in the future.
    40  	// Therefore, we already add the logic for identifying duplicates here.
    41  	collectedIDs map[flow.Identifier]struct{} // map of collected IDs (gets updated)
    42  }
    43  
    44  var _ hotstuff.WeightedSignatureAggregator = (*WeightedSignatureAggregator)(nil)
    45  
    46  // NewWeightedSignatureAggregator returns a weighted aggregator initialized with a list of flow
    47  // identities, their respective public keys, a message and a domain separation tag. The identities
    48  // represent the list of all possible signers.
    49  // This aggregator is only safe if PoPs of all identity keys are valid. This constructor does not
    50  // verify the PoPs but assumes they have been validated outside this module.
    51  // The constructor errors if:
    52  // - the list of identities is empty
    53  // - if the length of keys does not match the length of identities
    54  // - if one of the keys is not a valid public key.
    55  //
    56  // A weighted aggregator is used for one aggregation only. A new instance should be used for each
    57  // signature aggregation task in the protocol.
    58  func NewWeightedSignatureAggregator(
    59  	ids flow.IdentityList, // list of all authorized signers
    60  	pks []crypto.PublicKey, // list of corresponding public keys used for signature verifications
    61  	message []byte, // message to get an aggregated signature for
    62  	dsTag string, // domain separation tag used by the signature
    63  ) (*WeightedSignatureAggregator, error) {
    64  	if len(ids) != len(pks) {
    65  		return nil, fmt.Errorf("keys length %d and identities length %d do not match", len(pks), len(ids))
    66  	}
    67  
    68  	// build the internal map for a faster look-up
    69  	idToInfo := make(map[flow.Identifier]signerInfo)
    70  	for i, id := range ids {
    71  		idToInfo[id.NodeID] = signerInfo{
    72  			weight: id.InitialWeight,
    73  			index:  i,
    74  		}
    75  	}
    76  
    77  	// instantiate low-level crypto aggregator, which works based on signer indices instead of nodeIDs
    78  	agg, err := signature.NewSignatureAggregatorSameMessage(message, dsTag, pks)
    79  	if err != nil {
    80  		return nil, fmt.Errorf("instantiating index-based signature aggregator failed: %w", err)
    81  	}
    82  
    83  	return &WeightedSignatureAggregator{
    84  		aggregator:   agg,
    85  		ids:          ids,
    86  		idToInfo:     idToInfo,
    87  		collectedIDs: make(map[flow.Identifier]struct{}),
    88  	}, nil
    89  }
    90  
    91  // Verify verifies the signature under the stored public keys and message.
    92  // Expected errors during normal operations:
    93  //   - model.InvalidSignerError if signerID is invalid (not a consensus participant)
    94  //   - model.ErrInvalidSignature if signerID is valid but signature is cryptographically invalid
    95  //
    96  // The function is thread-safe.
    97  func (w *WeightedSignatureAggregator) Verify(signerID flow.Identifier, sig crypto.Signature) error {
    98  	info, ok := w.idToInfo[signerID]
    99  	if !ok {
   100  		return model.NewInvalidSignerErrorf("%v is not an authorized signer", signerID)
   101  	}
   102  
   103  	ok, err := w.aggregator.Verify(info.index, sig) // no error expected during normal operation
   104  	if err != nil {
   105  		return fmt.Errorf("couldn't verify signature from %s: %w", signerID, err)
   106  	}
   107  	if !ok {
   108  		return fmt.Errorf("invalid signature from %s: %w", signerID, model.ErrInvalidSignature)
   109  	}
   110  	return nil
   111  }
   112  
   113  // TrustedAdd adds a signature to the internal set of signatures and adds the signer's
   114  // weight to the total collected weight, iff the signature is _not_ a duplicate.
   115  //
   116  // The total weight of all collected signatures (excluding duplicates) is returned regardless
   117  // of any returned error.
   118  // The function errors with:
   119  //   - model.InvalidSignerError if signerID is invalid (not a consensus participant)
   120  //   - model.DuplicatedSignerError if the signer has been already added
   121  //
   122  // The function is thread-safe.
   123  func (w *WeightedSignatureAggregator) TrustedAdd(signerID flow.Identifier, sig crypto.Signature) (uint64, error) {
   124  	info, found := w.idToInfo[signerID]
   125  	if !found {
   126  		return w.TotalWeight(), model.NewInvalidSignerErrorf("%v is not an authorized signer", signerID)
   127  	}
   128  
   129  	// atomically update the signatures pool and the total weight
   130  	w.lock.Lock()
   131  	defer w.lock.Unlock()
   132  
   133  	// check for repeated occurrence of signerID (in anticipation of aggregator supporting multiplicities larger than 1 in the future)
   134  	if _, duplicate := w.collectedIDs[signerID]; duplicate {
   135  		return w.totalWeight, model.NewDuplicatedSignerErrorf("signature from %v was already added", signerID)
   136  	}
   137  
   138  	err := w.aggregator.TrustedAdd(info.index, sig)
   139  	if err != nil {
   140  		// During normal operations, signature.InvalidSignerIdxError or signature.DuplicatedSignerIdxError should never occur.
   141  		return w.totalWeight, fmt.Errorf("unexpected exception while trusted add of signature from %v: %w", signerID, err)
   142  	}
   143  	w.totalWeight += info.weight
   144  	w.collectedIDs[signerID] = struct{}{}
   145  
   146  	return w.totalWeight, nil
   147  }
   148  
   149  // TotalWeight returns the total weight presented by the collected signatures.
   150  // The function is thread-safe
   151  func (w *WeightedSignatureAggregator) TotalWeight() uint64 {
   152  	w.lock.RLock()
   153  	defer w.lock.RUnlock()
   154  	return w.totalWeight
   155  }
   156  
   157  // Aggregate aggregates the signatures and returns the aggregated signature.
   158  // The function performs a final verification and errors if the aggregated signature is invalid. This is
   159  // required for the function safety since `TrustedAdd` allows adding invalid signatures.
   160  // The function errors with:
   161  //   - model.InsufficientSignaturesError if no signatures have been added yet
   162  //   - model.InvalidSignatureIncludedError if:
   163  //   - some signature(s), included via TrustedAdd, fail to deserialize (regardless of the aggregated public key)
   164  //     -- or all signatures deserialize correctly but some signature(s), included via TrustedAdd, are
   165  //     invalid (while aggregated public key is valid)
   166  //     -- model.InvalidAggregatedKeyError if all signatures deserialize correctly but the signer's
   167  //     staking public keys sum up to an invalid key (BLS identity public key).
   168  //     Any aggregated signature would fail the cryptographic verification under the identity public
   169  //     key and therefore such signature is considered invalid. Such scenario can only happen if
   170  //     staking public keys of signers were forged to add up to the identity public key.
   171  //     Under the assumption that all staking key PoPs are valid, this error case can only
   172  //     happen if all signers are malicious and colluding. If there is at least one honest signer,
   173  //     there is a negligible probability that the aggregated key is identity.
   174  //
   175  // The function is thread-safe.
   176  func (w *WeightedSignatureAggregator) Aggregate() (flow.IdentifierList, []byte, error) {
   177  	w.lock.Lock()
   178  	defer w.lock.Unlock()
   179  
   180  	// Aggregate includes the safety check of the aggregated signature
   181  	indices, aggSignature, err := w.aggregator.Aggregate()
   182  	if err != nil {
   183  		if signature.IsInsufficientSignaturesError(err) {
   184  			return nil, nil, model.NewInsufficientSignaturesError(err)
   185  		}
   186  		if errors.Is(err, signature.ErrIdentityPublicKey) {
   187  			return nil, nil, model.NewInvalidAggregatedKeyError(err)
   188  		}
   189  		if signature.IsInvalidSignatureIncludedError(err) {
   190  			return nil, nil, model.NewInvalidSignatureIncludedError(err)
   191  		}
   192  		return nil, nil, fmt.Errorf("unexpected error during signature aggregation: %w", err)
   193  	}
   194  	signerIDs := make([]flow.Identifier, 0, len(indices))
   195  	for _, index := range indices {
   196  		signerIDs = append(signerIDs, w.ids[index].NodeID)
   197  	}
   198  
   199  	return signerIDs, aggSignature, nil
   200  }