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 }