anvil/eth/backend/tempo_consensus/
certificate.rs1use alloy_primitives::{B256, Bytes};
18use commonware_codec::{
19 FixedSize, Write,
20 varint::{MAX_U64_VARINT_SIZE, UInt},
21};
22use commonware_cryptography::{
23 bls12381::{
24 dkg::feldman_desmedt::Output,
25 primitives::{
26 group::{G1, Share},
27 ops::threshold,
28 variant::MinSig,
29 },
30 },
31 ed25519::PublicKey,
32};
33use commonware_parallel::Sequential;
34
35const MAX_CERTIFICATE_BYTES: usize = 3 * MAX_U64_VARINT_SIZE + B256::len_bytes() + 2 * G1::SIZE;
37
38pub(super) fn encode(
40 epoch: u64,
41 view: u64,
42 parent_view: u64,
43 digest: B256,
44 output: &Output<MinSig, PublicKey>,
45 share: &Share,
46) -> Bytes {
47 let mut certificate = Vec::with_capacity(MAX_CERTIFICATE_BYTES);
48 UInt(epoch).write(&mut certificate);
49 UInt(view).write(&mut certificate);
50 let seed = threshold::sign_message::<MinSig>(share, b"TEMPO_SEED", &certificate);
51
52 UInt(parent_view).write(&mut certificate);
53 certificate.extend_from_slice(digest.as_slice());
54 let vote = threshold::sign_message::<MinSig>(share, b"TEMPO_FINALIZE", &certificate);
55 let (vote, seed) = threshold::recover_pair(output.public(), [&vote], [&seed], &Sequential)
56 .expect("the local committee has exactly one share and a threshold of one");
57 vote.write(&mut certificate);
58 seed.write(&mut certificate);
59 certificate.into()
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use commonware_codec::DecodeExt;
66 use commonware_cryptography::{
67 Signer,
68 bls12381::{
69 dkg::feldman_desmedt,
70 primitives::{ops, sharing::Mode},
71 },
72 ed25519::PrivateKey,
73 };
74 use commonware_utils::{N3f1, TryFromIterator, ordered::Set};
75 use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng};
76
77 #[test]
78 fn finalization_encodes_and_signs_variable_length_proposals() {
79 let (output, share) = committee(7);
80 let cases = [
82 ((7, 9, 8), 0x42, &[7, 9, 8][..], 2),
83 ((127, 128, 127), 0, &[0x7f, 0x80, 0x01, 0x7f][..], 3),
84 ((128, 129, 128), 0, &[0x80, 0x01, 0x81, 0x01, 0x80, 0x01][..], 4),
85 ((16_383, 16_384, 16_383), 0, &[0xff, 0x7f, 0x80, 0x80, 0x01, 0xff, 0x7f][..], 5),
86 (
87 (16_384, 16_385, 16_384),
88 0,
89 &[0x80, 0x80, 0x01, 0x81, 0x80, 0x01, 0x80, 0x80, 0x01][..],
90 6,
91 ),
92 ];
93 for ((epoch, view, parent_view), digest_byte, prefix, round_len) in cases {
94 let certificate =
95 encode(epoch, view, parent_view, B256::repeat_byte(digest_byte), &output, &share);
96 let proposal_len = prefix.len() + 32;
97 assert_eq!(&certificate[..prefix.len()], prefix);
98 assert_eq!(&certificate[prefix.len()..proposal_len], &[digest_byte; 32]);
99 assert_eq!(certificate.len(), proposal_len + 96);
100
101 let vote = G1::decode(&certificate[proposal_len..proposal_len + 48]).unwrap();
102 let seed = G1::decode(&certificate[proposal_len + 48..]).unwrap();
103 let public = output.public().public();
104 assert!(
105 ops::verify_message::<MinSig>(
106 public,
107 b"TEMPO_FINALIZE",
108 &certificate[..proposal_len],
109 &vote,
110 )
111 .is_ok()
112 );
113 assert!(
114 ops::verify_message::<MinSig>(public, b"TEMPO_SEED", &prefix[..round_len], &seed)
115 .is_ok()
116 );
117 }
118 }
119
120 #[test]
121 fn finalization_seed_depends_only_on_round() {
122 let (output, share) = committee(7);
123 let first = encode(7, 9, 8, B256::repeat_byte(0x42), &output, &share);
124 let second = encode(7, 9, 6, B256::repeat_byte(0x24), &output, &share);
125 assert_ne!(&first[35..83], &second[35..83]);
126 assert_eq!(&first[83..], &second[83..]);
127 assert_eq!(first, encode(7, 9, 8, B256::repeat_byte(0x42), &output, &share));
128 }
129
130 fn committee(seed: u8) -> (Output<MinSig, PublicKey>, Share) {
131 let proposer = PrivateKey::decode(&[seed; 32][..]).unwrap().public_key();
132 let (output, shares) = feldman_desmedt::deal::<MinSig, _, N3f1>(
133 ChaCha20Rng::from_seed([seed; 32]),
134 Mode::NonZeroCounter,
135 Set::try_from_iter([proposer]).unwrap(),
136 )
137 .unwrap();
138 (output, shares.into_iter().next().unwrap().1)
139 }
140}