zksync_consensus_roles/validator/messages/v2/
replica_timeout.rs1use std::collections::{BTreeMap, HashMap};
2
3use anyhow::Context as _;
4use zksync_protobuf::{read_optional, read_required, ProtoFmt};
5
6use super::{
7 BlockHeader, CommitQC, CommitQCVerifyError, ReplicaCommit, ReplicaCommitVerifyError, Signers,
8 View,
9};
10use crate::{
11 proto::validator as proto,
12 validator::{self, EpochNumber, GenesisHash, Signed},
13};
14
15#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
18pub struct ReplicaTimeout {
19 pub view: View,
21 pub high_vote: Option<ReplicaCommit>,
23 pub high_qc: Option<CommitQC>,
25}
26
27impl ReplicaTimeout {
28 pub fn verify(
30 &self,
31 genesis_hash: GenesisHash,
32 epoch: EpochNumber,
33 validators_schedule: &validator::Schedule,
34 ) -> Result<(), ReplicaTimeoutVerifyError> {
35 self.view
36 .verify(genesis_hash, epoch)
37 .map_err(ReplicaTimeoutVerifyError::BadView)?;
38
39 if let Some(v) = &self.high_vote {
40 v.verify(genesis_hash, epoch)
41 .map_err(ReplicaTimeoutVerifyError::InvalidHighVote)?;
42 }
43
44 if let Some(qc) = &self.high_qc {
45 qc.verify(genesis_hash, epoch, validators_schedule)
46 .map_err(ReplicaTimeoutVerifyError::InvalidHighQC)?;
47 }
48
49 Ok(())
50 }
51}
52
53impl ProtoFmt for ReplicaTimeout {
54 type Proto = proto::ReplicaTimeoutV2;
55
56 fn read(r: &Self::Proto) -> anyhow::Result<Self> {
57 Ok(Self {
58 view: read_required(&r.view).context("view")?,
59 high_vote: read_optional(&r.high_vote).context("high_vote")?,
60 high_qc: read_optional(&r.high_qc).context("high_qc")?,
61 })
62 }
63
64 fn build(&self) -> Self::Proto {
65 Self::Proto {
66 view: Some(self.view.build()),
67 high_vote: self.high_vote.as_ref().map(ProtoFmt::build),
68 high_qc: self.high_qc.as_ref().map(ProtoFmt::build),
69 }
70 }
71}
72
73#[derive(thiserror::Error, Debug)]
75pub enum ReplicaTimeoutVerifyError {
76 #[error("view: {0:#}")]
78 BadView(anyhow::Error),
79 #[error("invalid high_vote: {0:#}")]
81 InvalidHighVote(ReplicaCommitVerifyError),
82 #[error("invalid high_qc: {0:#}")]
84 InvalidHighQC(CommitQCVerifyError),
85}
86
87#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct TimeoutQC {
105 pub view: View,
107 pub map: BTreeMap<ReplicaTimeout, Signers>,
109 pub signature: validator::AggregateSignature,
111}
112
113impl TimeoutQC {
114 pub fn new(view: View) -> Self {
116 Self {
117 view,
118 map: BTreeMap::new(),
119 signature: validator::AggregateSignature::default(),
120 }
121 }
122
123 pub fn high_vote(&self, validators_schedule: &validator::Schedule) -> Option<BlockHeader> {
128 let mut count: HashMap<_, u64> = HashMap::new();
129 for (msg, signers) in &self.map {
130 if let Some(v) = &msg.high_vote {
131 *count.entry(v.proposal).or_default() += signers.weight(validators_schedule);
132 }
133 }
134
135 let min = validators_schedule.subquorum_threshold();
136 let mut high_votes: Vec<_> = count.into_iter().filter(|x| x.1 >= min).collect();
137
138 if high_votes.len() == 1 {
139 high_votes.pop().map(|x| x.0)
140 } else {
141 None
142 }
143 }
144
145 pub fn high_qc(&self) -> Option<&CommitQC> {
147 self.map
148 .keys()
149 .filter_map(|m| m.high_qc.as_ref())
150 .max_by_key(|qc| qc.view().number)
151 }
152
153 pub fn add(
155 &mut self,
156 msg: &Signed<ReplicaTimeout>,
157 genesis_hash: GenesisHash,
158 epoch: EpochNumber,
159 validators_schedule: &validator::Schedule,
160 ) -> Result<(), TimeoutQCAddError> {
161 let Some(i) = validators_schedule.index(&msg.key) else {
163 return Err(TimeoutQCAddError::SignerNotInCommittee {
164 signer: Box::new(msg.key.clone()),
165 });
166 };
167
168 if self.map.values().any(|s| s.0[i]) {
170 return Err(TimeoutQCAddError::DuplicateSigner {
171 signer: Box::new(msg.key.clone()),
172 });
173 };
174
175 msg.verify().map_err(TimeoutQCAddError::BadSignature)?;
177
178 if msg.msg.view != self.view {
180 return Err(TimeoutQCAddError::InconsistentViews);
181 };
182
183 msg.msg
185 .verify(genesis_hash, epoch, validators_schedule)
186 .map_err(TimeoutQCAddError::InvalidMessage)?;
187
188 let e = self
190 .map
191 .entry(msg.msg.clone())
192 .or_insert_with(|| Signers::new(validators_schedule.len()));
193 e.0.set(i, true);
194 self.signature.add(&msg.sig);
195
196 Ok(())
197 }
198
199 pub fn verify(
201 &self,
202 genesis_hash: GenesisHash,
203 epoch: EpochNumber,
204 validators_schedule: &validator::Schedule,
205 ) -> Result<(), TimeoutQCVerifyError> {
206 self.view
207 .verify(genesis_hash, epoch)
208 .map_err(TimeoutQCVerifyError::BadView)?;
209
210 let mut sum = Signers::new(validators_schedule.len());
211
212 for (i, (msg, signers)) in self.map.iter().enumerate() {
214 if msg.view != self.view {
215 return Err(TimeoutQCVerifyError::InconsistentView(i));
216 }
217 if signers.len() != sum.len() {
218 return Err(TimeoutQCVerifyError::WrongSignersLength(i));
219 }
220 if signers.is_empty() {
221 return Err(TimeoutQCVerifyError::NoSignersAssigned(i));
222 }
223 if !(&sum & signers).is_empty() {
224 return Err(TimeoutQCVerifyError::OverlappingSignatureSet(i));
225 }
226 msg.verify(genesis_hash, epoch, validators_schedule)
227 .map_err(|err| TimeoutQCVerifyError::InvalidMessage(i, err))?;
228
229 sum |= signers;
230 }
231
232 let weight = sum.weight(validators_schedule);
234 let threshold = validators_schedule.quorum_threshold();
235 if weight < threshold {
236 return Err(TimeoutQCVerifyError::NotEnoughWeight {
237 got: weight,
238 want: threshold,
239 });
240 }
241
242 let messages_and_keys = self.map.clone().into_iter().flat_map(|(msg, signers)| {
244 validators_schedule
245 .keys()
246 .enumerate()
247 .filter(|(i, _)| signers.0[*i])
248 .map(|(_, pk)| (msg.clone(), pk))
249 .collect::<Vec<_>>()
250 });
251
252 self.signature
254 .verify_messages(messages_and_keys)
255 .map_err(TimeoutQCVerifyError::BadSignature)
256 }
257
258 pub fn weight(&self, validators_schedule: &validator::Schedule) -> u64 {
260 self.map
261 .values()
262 .map(|signers| signers.weight(validators_schedule))
263 .sum()
264 }
265}
266
267impl Ord for TimeoutQC {
268 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
269 self.view.number.cmp(&other.view.number)
270 }
271}
272
273impl PartialOrd for TimeoutQC {
274 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
275 Some(self.cmp(other))
276 }
277}
278
279impl ProtoFmt for TimeoutQC {
280 type Proto = proto::TimeoutQcv2;
281
282 fn read(r: &Self::Proto) -> anyhow::Result<Self> {
283 let mut map = BTreeMap::new();
284
285 for (msg, signers) in r.msgs.iter().zip(r.signers.iter()) {
286 map.insert(
287 ReplicaTimeout::read(msg).context("msg")?,
288 Signers::read(signers).context("signers")?,
289 );
290 }
291
292 Ok(Self {
293 view: read_required(&r.view).context("view")?,
294 map,
295 signature: read_required(&r.sig).context("sig")?,
296 })
297 }
298
299 fn build(&self) -> Self::Proto {
300 let (msgs, signers) = self
301 .map
302 .iter()
303 .map(|(msg, signers)| (msg.build(), signers.build()))
304 .unzip();
305
306 Self::Proto {
307 view: Some(self.view.build()),
308 msgs,
309 signers,
310 sig: Some(self.signature.build()),
311 }
312 }
313}
314
315#[derive(thiserror::Error, Debug)]
317pub enum TimeoutQCAddError {
318 #[error("Signer not in committee: {signer:?}")]
320 SignerNotInCommittee {
321 signer: Box<validator::PublicKey>,
323 },
324 #[error("Message from the same signer already in QC: {signer:?}")]
326 DuplicateSigner {
327 signer: Box<validator::PublicKey>,
329 },
330 #[error("Bad signature: {0:#}")]
332 BadSignature(#[source] anyhow::Error),
333 #[error("Trying to add a message from a different view")]
335 InconsistentViews,
336 #[error("Invalid message: {0:#}")]
338 InvalidMessage(ReplicaTimeoutVerifyError),
339}
340
341#[derive(thiserror::Error, Debug)]
343pub enum TimeoutQCVerifyError {
344 #[error("Bad view: {0:#}")]
346 BadView(anyhow::Error),
347 #[error("Message with inconsistent view: number [{0}]")]
349 InconsistentView(usize),
350 #[error("Invalid message: number [{0}], {1:#}")]
352 InvalidMessage(usize, ReplicaTimeoutVerifyError),
353 #[error("Message with wrong signers length: number [{0}]")]
355 WrongSignersLength(usize),
356 #[error("Message with no signers assigned: number [{0}]")]
358 NoSignersAssigned(usize),
359 #[error("Message with overlapping signature set: number [{0}]")]
361 OverlappingSignatureSet(usize),
362 #[error("Signers have not reached threshold weight: got {got}, want {want}")]
364 NotEnoughWeight {
365 got: u64,
367 want: u64,
369 },
370 #[error("Bad signature: {0:#}")]
372 BadSignature(#[source] anyhow::Error),
373}