iota_sdk_crypto/
validator.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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) // Keys are already verified
194                .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        // Double check that the aggregated sig still verifies
315        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        // Aggregating with no sigs fails
347        aggregator.finish().unwrap_err();
348
349        aggregator
350            .add_signature(private_keys[0].sign_checkpoint_summary(&summary))
351            .unwrap();
352
353        // Aggregating with a sig from the same committee member more than once fails
354        aggregator
355            .add_signature(private_keys[0].sign_checkpoint_summary(&summary))
356            .unwrap_err();
357
358        // Aggregating with insufficient weight fails
359        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        // Aggregating with sufficient weight succeeds and verifies
369        let signature = aggregator.finish().unwrap();
370        aggregator
371            .verifier
372            .verify_checkpoint_summary(&summary, &signature)
373            .unwrap();
374
375        // We can add the last sig and still be successful
376        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}