1use std::collections::HashMap;
6
7use blst::min_sig::{AggregatePublicKey, AggregateSignature, Signature};
8use iota_types::{
9 Bls12381PublicKey, Bls12381Signature, CheckpointSummary, ValidatorAggregatedSignature,
10 ValidatorCommittee, ValidatorSignature,
11};
12use signature::{Error as SignatureError, Verifier};
13
14use crate::bls12381::{Bls12381VerifyingKey, BlstError};
15
16#[derive(Debug)]
17struct ExtendedValidatorCommittee {
18 committee: ValidatorCommittee,
19 verifying_keys: Vec<Bls12381VerifyingKey>,
20 public_key_to_index: HashMap<Bls12381PublicKey, usize>,
21 total_weight: u64,
22 quorum_threshold: u64,
23}
24
25struct MemberInfo<'a> {
26 verifying_key: &'a Bls12381VerifyingKey,
27 weight: u64,
28 index: usize,
29}
30
31impl ExtendedValidatorCommittee {
32 fn new(committee: ValidatorCommittee) -> Result<Self, SignatureError> {
33 let mut public_key_to_index = HashMap::new();
34 let mut verifying_keys = Vec::new();
35
36 let mut total_weight = 0;
37 for (idx, member) in committee.members.iter().enumerate() {
38 assert_eq!(idx, verifying_keys.len());
39 verifying_keys.push(Bls12381VerifyingKey::new(&member.public_key)?);
40 public_key_to_index.insert(member.public_key, idx);
41 total_weight += member.stake;
42 }
43
44 let quorum_threshold = ((total_weight - 1) / 3) * 2 + 1;
45
46 Ok(Self {
47 committee,
48 verifying_keys,
49 public_key_to_index,
50 total_weight,
51 quorum_threshold,
52 })
53 }
54
55 fn committee(&self) -> &ValidatorCommittee {
56 &self.committee
57 }
58
59 #[allow(unused)]
60 fn total_weight(&self) -> u64 {
61 self.total_weight
62 }
63
64 #[allow(unused)]
65 fn quorum_threshold(&self) -> u64 {
66 self.quorum_threshold
67 }
68
69 fn verifying_key(
70 &self,
71 public_key: &Bls12381PublicKey,
72 ) -> Result<&Bls12381VerifyingKey, SignatureError> {
73 self.public_key_to_index
74 .get(public_key)
75 .and_then(|idx| self.verifying_keys.get(*idx))
76 .ok_or_else(|| {
77 SignatureError::from_source(format!(
78 "signature from public_key {public_key} does not belong to this committee",
79 ))
80 })
81 }
82
83 fn member(&self, public_key: &Bls12381PublicKey) -> Result<MemberInfo<'_>, SignatureError> {
84 self.public_key_to_index
85 .get(public_key)
86 .ok_or_else(|| {
87 SignatureError::from_source(format!(
88 "signature from public_key {public_key} does not belong to this committee",
89 ))
90 })
91 .and_then(|idx| self.member_by_idx(*idx))
92 }
93
94 fn member_by_idx(&self, idx: usize) -> Result<MemberInfo<'_>, SignatureError> {
95 let verifying_key = self.verifying_keys.get(idx).ok_or_else(|| {
96 SignatureError::from_source(format!(
97 "index {idx} out of bounds; committee has {} members",
98 self.committee().members.len(),
99 ))
100 })?;
101 let weight = self
102 .committee()
103 .members
104 .get(idx)
105 .ok_or_else(|| {
106 SignatureError::from_source(format!(
107 "index {idx} out of bounds; committee has {} members",
108 self.committee().members.len(),
109 ))
110 })?
111 .stake;
112
113 Ok(MemberInfo {
114 verifying_key,
115 weight,
116 index: idx,
117 })
118 }
119}
120
121#[derive(Debug)]
122pub struct ValidatorCommitteeSignatureVerifier {
123 committee: ExtendedValidatorCommittee,
124}
125
126impl ValidatorCommitteeSignatureVerifier {
127 pub fn new(committee: ValidatorCommittee) -> Result<Self, SignatureError> {
128 ExtendedValidatorCommittee::new(committee).map(|committee| Self { committee })
129 }
130
131 pub fn committee(&self) -> &ValidatorCommittee {
132 self.committee.committee()
133 }
134
135 pub fn verify_checkpoint_summary(
136 &self,
137 summary: &CheckpointSummary,
138 signature: &ValidatorAggregatedSignature,
139 ) -> Result<(), SignatureError> {
140 let message = summary.signing_message();
141 self.verify(&message, signature)
142 }
143}
144
145impl Verifier<ValidatorSignature> for ValidatorCommitteeSignatureVerifier {
146 fn verify(&self, message: &[u8], signature: &ValidatorSignature) -> Result<(), SignatureError> {
147 if signature.epoch != self.committee().epoch {
148 return Err(SignatureError::from_source(format!(
149 "signature epoch {} does not match committee epoch {}",
150 signature.epoch,
151 self.committee().epoch
152 )));
153 }
154
155 let verifying_key = self.committee.verifying_key(&signature.public_key)?;
156 verifying_key.verify(message, &signature.signature)
157 }
158}
159
160impl Verifier<ValidatorAggregatedSignature> for ValidatorCommitteeSignatureVerifier {
161 fn verify(
162 &self,
163 message: &[u8],
164 signature: &ValidatorAggregatedSignature,
165 ) -> Result<(), SignatureError> {
166 if signature.epoch != self.committee().epoch {
167 return Err(SignatureError::from_source(format!(
168 "signature epoch {} does not match committee epoch {}",
169 signature.epoch,
170 self.committee().epoch
171 )));
172 }
173
174 let mut signed_weight = 0;
175 let mut bitmap = signature.bitmap.iter();
176
177 let mut aggregated_public_key = {
178 let idx = bitmap.next().ok_or_else(|| {
179 SignatureError::from_source("signature bitmap must have at least one entry")
180 })?;
181
182 let member = self.committee.member_by_idx(idx as usize)?;
183
184 signed_weight += member.weight;
185 AggregatePublicKey::from_public_key(&member.verifying_key.0)
186 };
187
188 for idx in bitmap {
189 let member = self.committee.member_by_idx(idx as usize)?;
190
191 signed_weight += member.weight;
192 aggregated_public_key
193 .add_public_key(&member.verifying_key.0, false) .map_err(BlstError)
195 .map_err(SignatureError::from_source)?;
196 }
197
198 Bls12381VerifyingKey(aggregated_public_key.to_public_key())
199 .verify(message, &signature.signature)?;
200
201 if signed_weight >= self.committee.quorum_threshold {
202 Ok(())
203 } else {
204 Err(SignatureError::from_source(format!(
205 "insufficient signing weight {}; quorum threshold is {}",
206 signed_weight, self.committee.quorum_threshold,
207 )))
208 }
209 }
210}
211
212#[derive(Debug)]
213pub struct ValidatorCommitteeSignatureAggregator {
214 verifier: ValidatorCommitteeSignatureVerifier,
215 signatures: std::collections::BTreeMap<usize, ValidatorSignature>,
216 signed_weight: u64,
217 message: Vec<u8>,
218}
219
220impl ValidatorCommitteeSignatureAggregator {
221 pub fn new_checkpoint_summary(
222 committee: ValidatorCommittee,
223 summary: &CheckpointSummary,
224 ) -> Result<Self, SignatureError> {
225 let verifier = ValidatorCommitteeSignatureVerifier::new(committee)?;
226 let message = summary.signing_message();
227
228 Ok(Self {
229 verifier,
230 signatures: Default::default(),
231 signed_weight: 0,
232 message,
233 })
234 }
235
236 pub fn committee(&self) -> &ValidatorCommittee {
237 self.verifier.committee()
238 }
239
240 pub fn add_signature(&mut self, signature: ValidatorSignature) -> Result<(), SignatureError> {
241 use std::collections::btree_map::Entry;
242
243 if signature.epoch != self.verifier.committee().epoch {
244 return Err(SignatureError::from_source(format!(
245 "signature epoch {} does not match committee epoch {}",
246 signature.epoch,
247 self.committee().epoch
248 )));
249 }
250
251 let member = self.verifier.committee.member(&signature.public_key)?;
252
253 member
254 .verifying_key
255 .verify(&self.message, &signature.signature)?;
256
257 match self.signatures.entry(member.index) {
258 Entry::Vacant(v) => {
259 v.insert(signature);
260 }
261 Entry::Occupied(_) => {
262 return Err(SignatureError::from_source(
263 "duplicate signature from same committee member",
264 ));
265 }
266 }
267
268 self.signed_weight += member.weight;
269
270 Ok(())
271 }
272
273 pub fn finish(&self) -> Result<ValidatorAggregatedSignature, SignatureError> {
274 if self.signed_weight < self.verifier.committee.quorum_threshold {
275 return Err(SignatureError::from_source(format!(
276 "signature weight of {} is insufficient to reach quorum threshold of {}",
277 self.signed_weight, self.verifier.committee.quorum_threshold
278 )));
279 }
280
281 let mut iter = self.signatures.iter();
282 let (member_idx, signature) = iter.next().ok_or_else(|| {
283 SignatureError::from_source("signature map must have at least one entry")
284 })?;
285
286 let mut bitmap = roaring::RoaringBitmap::new();
287 bitmap.insert(*member_idx as u32);
288 let agg_sig = AggregateSignature::from_signature(
289 &Signature::from_bytes(signature.signature.inner())
290 .expect("signature was already verified"),
291 );
292
293 let (agg_sig, bitmap) = iter.fold(
294 (agg_sig, bitmap),
295 |(mut agg_sig, mut bitmap), (member_idx, signature)| {
296 bitmap.insert(*member_idx as u32);
297 agg_sig
298 .add_signature(
299 &Signature::from_bytes(signature.signature.inner())
300 .expect("signature was already verified"),
301 false,
302 )
303 .expect("signature was already verified");
304 (agg_sig, bitmap)
305 },
306 );
307
308 let aggregated_signature = ValidatorAggregatedSignature {
309 epoch: self.verifier.committee().epoch,
310 signature: Bls12381Signature::new(agg_sig.to_signature().to_bytes()),
311 bitmap,
312 };
313
314 self.verifier.verify(&self.message, &aggregated_signature)?;
316
317 Ok(aggregated_signature)
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use iota_types::ValidatorCommitteeMember;
324 use test_strategy::proptest;
325
326 use super::*;
327 use crate::bls12381::Bls12381PrivateKey;
328
329 #[proptest]
330 fn basic_aggregation(private_keys: [Bls12381PrivateKey; 4], summary: CheckpointSummary) {
331 let committee = ValidatorCommittee {
332 epoch: summary.epoch,
333 members: private_keys
334 .iter()
335 .map(|key| ValidatorCommitteeMember {
336 public_key: key.public_key(),
337 stake: 1,
338 })
339 .collect(),
340 };
341
342 let mut aggregator =
343 ValidatorCommitteeSignatureAggregator::new_checkpoint_summary(committee, &summary)
344 .unwrap();
345
346 aggregator.finish().unwrap_err();
348
349 aggregator
350 .add_signature(private_keys[0].sign_checkpoint_summary(&summary))
351 .unwrap();
352
353 aggregator
355 .add_signature(private_keys[0].sign_checkpoint_summary(&summary))
356 .unwrap_err();
357
358 aggregator.finish().unwrap_err();
360
361 aggregator
362 .add_signature(private_keys[1].sign_checkpoint_summary(&summary))
363 .unwrap();
364 aggregator
365 .add_signature(private_keys[2].sign_checkpoint_summary(&summary))
366 .unwrap();
367
368 let signature = aggregator.finish().unwrap();
370 aggregator
371 .verifier
372 .verify_checkpoint_summary(&summary, &signature)
373 .unwrap();
374
375 aggregator
377 .add_signature(private_keys[3].sign_checkpoint_summary(&summary))
378 .unwrap();
379 let signature = aggregator.finish().unwrap();
380 aggregator
381 .verifier
382 .verify_checkpoint_summary(&summary, &signature)
383 .unwrap();
384 }
385}