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 }