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 }