1use crate::{
2 BlockNumber, RoundNumber, Validator, ValidatorSet, AggregateSignature, SignatureScheme, Signer,
3 Blockchain,
4};
5
6#[derive(Debug)]
12#[cfg_attr(feature = "alloc", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
13pub struct Commit<A: AggregateSignature> {
14 pub(crate) block_number: BlockNumber,
16
17 pub(crate) round_number: RoundNumber,
24
25 pub(crate) aggregate_signature: A,
27}
28
29pub type CommitFor<B> =
31 Commit<<<B as Blockchain>::SignatureScheme as SignatureScheme>::AggregateSignature>;
32
33impl<A: AggregateSignature> Clone for Commit<A> {
34 fn clone(&self) -> Self {
35 Self {
36 block_number: self.block_number,
37 round_number: self.round_number,
38 aggregate_signature: self.aggregate_signature.clone(),
39 }
40 }
41}
42
43#[must_use]
48pub(crate) fn validators_satisfy_threshold<V: Validator>(
49 validators: impl IntoIterator<Item = V>,
50 validator_set: &(impl ?Sized + ValidatorSet<Validator = V>),
51) -> bool {
52 validators
54 .into_iter()
55 .try_fold(0u16, |accum, validator| {
56 validator_set.weight(&validator).and_then(|weight| accum.checked_add(u16::from(weight)))
57 })
58 .is_some_and(|sum| sum >= validator_set.threshold())
59}
60
61#[doc(hidden)]
62pub(crate) enum CommitSegment<'genesis, 'block_hash> {
63 Dst([u8; 1]),
64 Genesis(&'genesis [u8]),
65 U64([u8; 8]),
66 Block(&'block_hash [u8]),
67}
68impl AsRef<[u8]> for CommitSegment<'_, '_> {
69 fn as_ref(&self) -> &[u8] {
70 match self {
71 Self::Dst(dst) => dst.as_slice(),
72 Self::Genesis(genesis) => genesis,
73 Self::U64(number) => number.as_slice(),
74 Self::Block(block_hash) => block_hash,
75 }
76 }
77}
78
79impl<A: AggregateSignature> Commit<A> {
80 #[must_use]
82 pub fn block_number(&self) -> BlockNumber {
83 self.block_number
84 }
85
86 #[must_use]
87 pub(crate) fn signature_message<'genesis, 'block_hash>(
88 genesis: &'genesis [u8],
89 block_number: BlockNumber,
90 round_number: RoundNumber,
91 block_hash: &'block_hash [u8],
92 ) -> <[CommitSegment<'genesis, 'block_hash>; 6] as IntoIterator>::IntoIter {
93 [
94 CommitSegment::Dst([0]),
95 CommitSegment::Dst([u8::try_from(genesis.as_ref().len()).unwrap()]),
100 CommitSegment::Genesis(genesis),
101 CommitSegment::U64(u64::from(block_number.0).to_le_bytes()),
102 CommitSegment::U64(u64::from(round_number.0).to_le_bytes()),
103 CommitSegment::Block(block_hash),
108 ]
109 .into_iter()
110 }
111
112 #[must_use]
113 pub(crate) async fn sign<S: ?Sized + SignatureScheme<AggregateSignature = A>>(
114 signer: &(impl ?Sized + Signer<Signature = <S as SignatureScheme>::Signature>),
115 genesis: &[u8],
116 block_number: BlockNumber,
117 round_number: RoundNumber,
118 block_hash: &[u8],
119 ) -> <S as SignatureScheme>::Signature {
120 signer.sign(Self::signature_message(genesis, block_number, round_number, block_hash)).await
121 }
122
123 #[must_use]
124 pub(crate) fn verify_precommit<S: ?Sized + SignatureScheme<AggregateSignature = A>>(
125 signature_scheme: &S,
126 validator: &S::Validator,
127 genesis: &[u8],
128 block_number: BlockNumber,
129 round_number: RoundNumber,
130 block_hash: &[u8],
131 signature: &S::Signature,
132 ) -> bool {
133 signature_scheme.verify(
134 validator,
135 Self::signature_message(genesis, block_number, round_number, block_hash),
136 signature,
137 )
138 }
139
140 #[must_use]
142 pub fn verify<S: ?Sized + SignatureScheme<AggregateSignature = A>>(
143 &self,
144 validator_set: &(impl ?Sized + ValidatorSet<Validator = S::Validator>),
145 signature_scheme: &S,
146 genesis: impl AsRef<[u8]>,
147 block_hash: impl AsRef<[u8]>,
148 ) -> bool {
149 let Ok(validators) = signature_scheme.verify_aggregate(
151 Self::signature_message(
152 genesis.as_ref(),
153 self.block_number,
154 self.round_number,
155 block_hash.as_ref(),
156 ),
157 &self.aggregate_signature,
158 ) else {
159 return false;
160 };
161
162 validators_satisfy_threshold(validators, validator_set)
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 struct RandomCommit {
172 genesis_len: u8,
173 #[expect(clippy::as_conversions)]
174 genesis: [u8; u8::MAX as usize],
175
176 block_number: BlockNumber,
177 round_number: RoundNumber,
178
179 block_hash_len: u16,
180 #[expect(clippy::as_conversions)]
181 block_hash: [u8; u16::MAX as usize],
182 }
183
184 impl RandomCommit {
185 fn new() -> Self {
186 use core::num::NonZero;
187 use rand_core::{TryRngCore as _, OsRng};
188
189 #[expect(clippy::as_conversions, clippy::cast_possible_truncation)]
190 let genesis_len = OsRng.try_next_u64().unwrap() as u8;
191 #[expect(clippy::as_conversions)]
192 let mut genesis = [0xff; u8::MAX as usize];
193 OsRng.try_fill_bytes(&mut genesis[.. usize::from(genesis_len)]).unwrap();
194
195 let block_number =
196 BlockNumber(NonZero::new(OsRng.try_next_u64().unwrap().saturating_add(1)).unwrap());
197 let round_number =
198 RoundNumber(NonZero::new(OsRng.try_next_u64().unwrap().saturating_add(1)).unwrap());
199
200 #[expect(clippy::as_conversions, clippy::cast_possible_truncation)]
201 let block_hash_len = OsRng.try_next_u64().unwrap() as u16;
202 #[expect(clippy::as_conversions, clippy::large_stack_arrays)]
203 let mut block_hash = [0xff; u16::MAX as usize];
204 OsRng.try_fill_bytes(&mut block_hash[.. usize::from(block_hash_len)]).unwrap();
205
206 Self { genesis_len, genesis, block_number, round_number, block_hash_len, block_hash }
207 }
208
209 fn genesis(&self) -> &[u8] {
210 &self.genesis[.. usize::from(self.genesis_len)]
211 }
212
213 fn block_hash(&self) -> &[u8] {
214 &self.block_hash[.. usize::from(self.block_hash_len)]
215 }
216 }
217
218 #[cfg(feature = "alloc")]
219 #[test]
220 fn signature_message() {
221 for _ in 0 .. 128 {
222 let commit = RandomCommit::new();
223
224 let expected = [
225 [0].as_slice(),
226 &[commit.genesis_len],
227 commit.genesis(),
228 &u64::from(commit.block_number).to_le_bytes(),
229 &u64::from(commit.round_number).to_le_bytes(),
230 commit.block_hash(),
231 ]
232 .concat();
233
234 let mut concatenated = alloc::vec![];
235 for chunk in Commit::<
236 <crate::TestSignatureScheme as SignatureScheme>::AggregateSignature
237 >::signature_message(
238 commit.genesis(),
239 commit.block_number,
240 commit.round_number,
241 commit.block_hash(),
242 ) {
243 concatenated.extend(chunk.as_ref());
244 }
245
246 assert_eq!(expected, concatenated);
247 }
248 }
249
250 #[cfg(feature = "alloc")]
251 #[test]
252 fn sign_and_verify_precommit() {
253 use core::{
254 pin::pin,
255 task::{Poll, Waker, Context},
256 future::Future as _,
257 };
258
259 use crate::TestSignatureScheme;
260
261 let mut context = Context::from_waker(Waker::noop());
262 let signature_scheme = TestSignatureScheme::new();
263
264 for i in 0 .. u8::MAX {
265 let signer = signature_scheme.signer(i);
266 let commit = RandomCommit::new();
267 let Poll::Ready(mut signature) = pin!(Commit::<
268 <TestSignatureScheme as SignatureScheme>::AggregateSignature,
269 >::sign::<TestSignatureScheme>(
270 &signer,
271 commit.genesis(),
272 commit.block_number,
273 commit.round_number,
274 commit.block_hash(),
275 ))
276 .poll(&mut context) else {
277 panic!("`TestSignatureScheme::sign` returned `Poll::Pending`")
278 };
279 assert!(
280 Commit::<<TestSignatureScheme as SignatureScheme>::AggregateSignature>::verify_precommit(
281 &signature_scheme,
282 &i,
283 commit.genesis(),
284 commit.block_number,
285 commit.round_number,
286 commit.block_hash(),
287 &signature
288 )
289 );
290 signature[0] ^= 1;
291 assert!(
292 !Commit::<<TestSignatureScheme as SignatureScheme>::AggregateSignature>::verify_precommit(
293 &signature_scheme,
294 &i,
295 commit.genesis(),
296 commit.block_number,
297 commit.round_number,
298 commit.block_hash(),
299 &signature
300 )
301 );
302 }
303 }
304
305 #[test]
306 fn verify_commit() {
307 use core::{
308 num::NonZero,
309 pin::pin,
310 task::{Poll, Waker, Context},
311 future::Future as _,
312 };
313 use alloc::{vec::Vec, vec, collections::BTreeMap};
314
315 use crate::TestSignatureScheme;
316
317 let signature_scheme = TestSignatureScheme::new();
318 let commit = RandomCommit::new();
319
320 let signature = |validator| {
321 let mut context = Context::from_waker(Waker::noop());
322 let signer = signature_scheme.signer(validator);
323 let Poll::Ready(signature) = pin!(Commit::<
324 <TestSignatureScheme as SignatureScheme>::AggregateSignature,
325 >::sign::<TestSignatureScheme>(
326 &signer,
327 commit.genesis(),
328 commit.block_number,
329 commit.round_number,
330 commit.block_hash(),
331 ))
332 .poll(&mut context) else {
333 panic!("`TestSignatureScheme::sign` returned `Poll::Pending`")
334 };
335 signature
336 };
337 let signatures = [signature(0), signature(2), signature(3)];
338
339 let aggregate_signature = signature_scheme.aggregate(
340 Commit::<<TestSignatureScheme as SignatureScheme>::AggregateSignature>::signature_message(
341 commit.genesis(),
342 commit.block_number,
343 commit.round_number,
344 commit.block_hash(),
345 ),
346 [(&0, &signatures[0])],
347 );
348
349 let actual_commit = Commit {
350 block_number: commit.block_number,
351 round_number: commit.round_number,
352 aggregate_signature,
353 };
354 assert_eq!(actual_commit.block_number(), commit.block_number);
355
356 let verify = |valid, actual_commit: &Commit<_>, weights: Vec<(_, u16)>| {
357 assert_eq!(
358 actual_commit.verify(
359 &weights
360 .into_iter()
361 .map(|(validator, weight)| (validator, NonZero::new(weight).unwrap()))
362 .collect::<BTreeMap<_, _>>(),
363 &signature_scheme,
364 commit.genesis(),
365 commit.block_hash()
366 ),
367 valid
368 );
369 };
370 verify(true, &actual_commit, vec![(0, 1)]);
372 verify(false, &actual_commit, vec![(0, 1), (1, 1)]);
374 verify(false, &actual_commit, vec![(0, 2), (1, 1)]);
376 verify(true, &actual_commit, vec![(0, 3), (1, 1)]);
377
378 {
380 let mut actual_commit = actual_commit.clone();
381 *actual_commit.aggregate_signature.last_mut().unwrap() ^= 1;
382 verify(false, &actual_commit, vec![(0, 1)]);
383 }
384
385 {
387 let aggregate_signature = signature_scheme.aggregate(
388 Commit::<<TestSignatureScheme as SignatureScheme>::AggregateSignature>::signature_message(
389 commit.genesis(),
390 commit.block_number,
391 commit.round_number,
392 commit.block_hash(),
393 ),
394 [0, 2, 3].iter().zip(signatures.iter()),
395 );
396 let actual_commit = Commit {
397 block_number: commit.block_number,
398 round_number: commit.round_number,
399 aggregate_signature,
400 };
401 assert_eq!(actual_commit.block_number(), commit.block_number);
402 verify(true, &actual_commit, vec![(0, 1), (1, 1), (2, 1), (3, 1)]);
403 }
404 }
405}