1mod block;
2mod index_panorama;
3mod panorama;
4mod params;
5mod tallies;
6mod unit;
7
8#[cfg(test)]
9pub(crate) mod tests;
10
11pub(crate) use params::Params;
12use quanta::Clock;
13use serde::{Deserialize, Serialize};
14
15pub(crate) use index_panorama::{IndexObservation, IndexPanorama};
16pub use panorama::{Observation, Panorama};
17pub(super) use unit::Unit;
18
19use std::{
20 borrow::Borrow,
21 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
22 iter,
23};
24
25use datasize::DataSize;
26use itertools::Itertools;
27use thiserror::Error;
28use tracing::{error, info, trace, warn};
29
30use casper_types::{TimeDiff, Timestamp};
31
32use crate::{
33 components::consensus::{
34 highway_core::{
35 endorsement::{Endorsement, SignedEndorsement},
36 evidence::Evidence,
37 highway::{Endorsements, HashedWireUnit, SignedWireUnit, WireUnit},
38 ENABLE_ENDORSEMENTS,
39 },
40 traits::Context,
41 utils::{ValidatorIndex, ValidatorMap, Weight},
42 LeaderSequence,
43 },
44 utils::ds,
45};
46use block::Block;
47use tallies::Tallies;
48
49pub(super) const TODO_ENDORSEMENT_EVIDENCE_DISABLED: bool = true;
54
55const PING_TIMEOUT: u64 = 3;
58
59#[derive(Debug, Error, Eq, PartialEq, Clone)]
60pub(crate) enum UnitError {
61 #[error("The unit is a ballot but doesn't cite any block.")]
62 MissingBlock,
63 #[error("The panorama's length {} doesn't match the number of validators.", _0)]
64 PanoramaLength(usize),
65 #[error("The unit accuses its own creator as faulty.")]
66 FaultyCreator,
67 #[error("The panorama has a unit from {:?} in the slot for {:?}.", _0, _1)]
68 PanoramaIndex(ValidatorIndex, ValidatorIndex),
69 #[error("The panorama is missing units indirectly cited via {:?}.", _0)]
70 InconsistentPanorama(ValidatorIndex),
71 #[error("The unit contains the wrong sequence number.")]
72 SequenceNumber,
73 #[error("The unit's timestamp is older than a justification's.")]
74 Timestamps,
75 #[error("The creator is not a validator.")]
76 Creator,
77 #[error("The unit was created for a wrong instance ID.")]
78 InstanceId,
79 #[error("The signature is invalid.")]
80 Signature,
81 #[error("The round length has somehow changed within a round.")]
82 RoundLengthChangedWithinRound,
83 #[error("The round length is greater than the maximum allowed by the chainspec.")]
84 RoundLengthGreaterThanMaximum,
85 #[error("This would be the third unit in that round. Only two are allowed.")]
86 ThreeUnitsInRound,
87 #[error(
88 "A block must be the leader's ({:?}) first unit, at the beginning of the round.",
89 _0
90 )]
91 NonLeaderBlock(ValidatorIndex),
92 #[error("The unit is a block, but its parent is already a terminal block.")]
93 ValueAfterTerminalBlock,
94 #[error("The unit's creator is banned.")]
95 Banned,
96 #[error("The unit's endorsed units were not a superset of its justifications.")]
97 EndorsementsNotMonotonic,
98 #[error("The LNC rule was violated. Vote cited ({:?}) naively.", _0)]
99 LncNaiveCitation(ValidatorIndex),
100 #[error(
101 "Wire unit endorses hash but does not see it. Hash: {:?}; Wire unit: {:?}",
102 hash,
103 wire_unit
104 )]
105 EndorsedButUnseen { hash: String, wire_unit: String },
106}
107
108#[derive(Clone, DataSize, Debug, Deserialize, Eq, PartialEq, Hash, Serialize)]
114pub enum Fault<C>
115where
116 C: Context,
117{
118 Banned,
121 Direct(Evidence<C>),
123 Indirect,
125}
126
127impl<C: Context> Fault<C> {
128 pub fn evidence(&self) -> Option<&Evidence<C>> {
130 match self {
131 Fault::Banned | Fault::Indirect => None,
132 Fault::Direct(ev) => Some(ev),
133 }
134 }
135}
136
137#[derive(Debug, Clone, DataSize, Serialize, Deserialize)]
143pub struct State<C>
144where
145 C: Context,
146{
147 params: Params,
149 weights: ValidatorMap<Weight>,
151 leader_sequence: LeaderSequence,
153 #[data_size(with = ds::hashmap_sample)]
157 units: HashMap<C::Hash, Unit<C>>,
158 #[data_size(with = ds::hashmap_sample)]
161 blocks: HashMap<C::Hash, Block<C>>,
162 faults: HashMap<ValidatorIndex, Fault<C>>,
166 panorama: Panorama<C>,
169 #[data_size(with = ds::hashmap_sample)]
172 endorsements: HashMap<C::Hash, ValidatorMap<Option<C::Signature>>>,
173 #[data_size(with = ds::hashmap_sample)]
177 incomplete_endorsements: HashMap<C::Hash, BTreeMap<ValidatorIndex, C::Signature>>,
178 pings: ValidatorMap<Timestamp>,
180 #[data_size(skip)] #[serde(skip, default)]
183 clock: Clock,
185}
186
187impl<C: Context> State<C> {
188 pub(crate) fn new<I, IB, IB2>(
189 weights: I,
190 params: Params,
191 banned: IB,
192 cannot_propose: IB2,
193 ) -> State<C>
194 where
195 I: IntoIterator,
196 I::Item: Borrow<Weight>,
197 IB: IntoIterator<Item = ValidatorIndex>,
198 IB2: IntoIterator<Item = ValidatorIndex>,
199 {
200 let weights = ValidatorMap::from(weights.into_iter().map(|w| *w.borrow()).collect_vec());
201 assert!(
202 !weights.is_empty(),
203 "cannot initialize Highway with no validators"
204 );
205 let mut panorama = Panorama::new(weights.len());
206 let faults: HashMap<_, _> = banned.into_iter().map(|idx| (idx, Fault::Banned)).collect();
207 for idx in faults.keys() {
208 assert!(
209 idx.0 < weights.len() as u32,
210 "invalid banned validator index"
211 );
212 panorama[*idx] = Observation::Faulty;
213 }
214 let mut can_propose: ValidatorMap<bool> = weights.iter().map(|_| true).collect();
215 for idx in cannot_propose {
216 assert!(
217 idx.0 < weights.len() as u32,
218 "invalid validator index for exclusion from leader sequence"
219 );
220 can_propose[idx] = false;
221 }
222 let leader_sequence = LeaderSequence::new(params.seed(), &weights, can_propose);
223 let pings = iter::repeat(params.start_timestamp())
224 .take(weights.len())
225 .collect();
226 State {
227 params,
228 weights,
229 leader_sequence,
230 units: HashMap::new(),
231 blocks: HashMap::new(),
232 faults,
233 panorama,
234 endorsements: HashMap::new(),
235 incomplete_endorsements: HashMap::new(),
236 pings,
237 clock: Clock::new(),
238 }
239 }
240
241 pub fn params(&self) -> &Params {
243 &self.params
244 }
245
246 pub fn validator_count(&self) -> usize {
248 self.weights.len()
249 }
250
251 pub fn weight(&self, idx: ValidatorIndex) -> Weight {
253 self.weights[idx]
254 }
255
256 pub fn weights(&self) -> &ValidatorMap<Weight> {
258 &self.weights
259 }
260
261 pub fn faulty_weight_in(&self, panorama: &Panorama<C>) -> Weight {
263 panorama
264 .iter()
265 .zip(&self.weights)
266 .filter(|(obs, _)| **obs == Observation::Faulty)
267 .map(|(_, w)| *w)
268 .sum()
269 }
270
271 pub fn faulty_weight(&self) -> Weight {
273 self.faulty_weight_in(self.panorama())
274 }
275
276 pub fn total_weight(&self) -> Weight {
278 self.leader_sequence.total_weight()
279 }
280
281 pub fn maybe_evidence(&self, idx: ValidatorIndex) -> Option<&Evidence<C>> {
283 self.maybe_fault(idx).and_then(Fault::evidence)
284 }
285
286 pub fn maybe_endorsements(&self, unit: &C::Hash) -> Option<Endorsements<C>> {
288 self.endorsements.get(unit).map(|signatures| Endorsements {
289 unit: *unit,
290 endorsers: signatures.iter_some().map(|(i, sig)| (i, *sig)).collect(),
291 })
292 }
293
294 pub fn has_evidence(&self, idx: ValidatorIndex) -> bool {
296 self.maybe_evidence(idx).is_some()
297 }
298
299 pub fn has_all_endorsements<I: IntoIterator<Item = ValidatorIndex>>(
301 &self,
302 unit: &C::Hash,
303 v_ids: I,
304 ) -> bool {
305 if self.endorsements.contains_key(unit) {
306 true } else if let Some(sigs) = self.incomplete_endorsements.get(unit) {
308 v_ids.into_iter().all(|v_id| sigs.contains_key(&v_id))
309 } else {
310 v_ids.into_iter().next().is_none()
311 }
312 }
313
314 pub fn is_endorsed(&self, hash: &C::Hash) -> bool {
317 self.endorsements.contains_key(hash)
318 }
319
320 pub fn needs_endorsements(&self, unit: &SignedWireUnit<C>) -> Option<C::Hash> {
322 unit.wire_unit()
323 .endorsed
324 .iter()
325 .find(|hash| !self.endorsements.contains_key(hash))
326 .cloned()
327 }
328
329 pub fn last_seen(&self, idx: ValidatorIndex) -> Timestamp {
332 self.pings[idx]
333 }
334
335 pub fn mark_faulty(&mut self, idx: ValidatorIndex) {
337 self.panorama[idx] = Observation::Faulty;
338 self.faults.entry(idx).or_insert(Fault::Indirect);
339 }
340
341 pub fn maybe_fault(&self, idx: ValidatorIndex) -> Option<&Fault<C>> {
343 self.faults.get(&idx)
344 }
345
346 pub fn is_faulty(&self, idx: ValidatorIndex) -> bool {
348 self.faults.contains_key(&idx)
349 }
350
351 pub fn faulty_validators(&self) -> impl Iterator<Item = ValidatorIndex> + '_ {
353 self.faults.keys().cloned()
354 }
355
356 pub fn iter_correct_hashes(&self) -> impl Iterator<Item = &C::Hash> {
358 self.panorama.iter_correct_hashes()
359 }
360
361 pub fn maybe_unit(&self, hash: &C::Hash) -> Option<&Unit<C>> {
363 self.units.get(hash)
364 }
365
366 pub fn has_unit(&self, hash: &C::Hash) -> bool {
368 self.units.contains_key(hash)
369 }
370
371 pub fn unit(&self, hash: &C::Hash) -> &Unit<C> {
373 self.maybe_unit(hash).expect("unit hash must exist")
374 }
375
376 pub fn maybe_block(&self, hash: &C::Hash) -> Option<&Block<C>> {
378 self.blocks.get(hash)
379 }
380
381 pub fn block(&self, hash: &C::Hash) -> &Block<C> {
383 self.maybe_block(hash).expect("block hash must exist")
384 }
385
386 pub fn panorama(&self) -> &Panorama<C> {
388 &self.panorama
389 }
390
391 pub fn leader(&self, timestamp: Timestamp) -> ValidatorIndex {
399 self.leader_sequence.leader(timestamp.millis())
400 }
401
402 pub(crate) fn add_valid_unit(&mut self, swunit: SignedWireUnit<C>) {
406 let wunit = swunit.wire_unit();
407 let hash = swunit.hash();
408 if self.has_unit(&hash) {
409 warn!(%hash, "called add_valid_unit twice");
410 return;
411 }
412 let instance_id = wunit.instance_id;
413 let fork_choice = self.fork_choice(&wunit.panorama).cloned();
414 let (unit, maybe_value) = Unit::new(swunit, fork_choice.as_ref(), self);
415 if let Some(value) = maybe_value {
416 let block = Block::new(fork_choice, value, self);
417 self.blocks.insert(hash, block);
418 }
419 self.add_ping(unit.creator, unit.timestamp);
420 self.units.insert(hash, unit);
421
422 let unit = self.unit(&hash);
424 let creator = unit.creator;
425 let new_obs = match (&self.panorama[creator], &unit.panorama[creator]) {
426 (Observation::Faulty, _) => Observation::Faulty,
427 (obs0, obs1) if obs0 == obs1 => Observation::Correct(hash),
428 (Observation::None, _) => panic!("missing creator's previous unit"),
429 (Observation::Correct(hash0), _) => {
430 let prev0 = self.find_in_swimlane(hash0, unit.seq_number).unwrap();
436 let wunit0 = self.wire_unit(prev0, instance_id).unwrap();
437 let wunit1 = self.wire_unit(&hash, instance_id).unwrap();
438 self.add_evidence(Evidence::Equivocation(wunit0, wunit1));
439 Observation::Faulty
440 }
441 };
442 self.panorama[creator] = new_obs;
443 }
444
445 pub(crate) fn add_evidence(&mut self, evidence: Evidence<C>) -> bool {
450 if TODO_ENDORSEMENT_EVIDENCE_DISABLED && matches!(evidence, Evidence::Endorsements { .. }) {
451 return false;
452 }
453 let idx = evidence.perpetrator();
454 match self.faults.get(&idx) {
455 Some(&Fault::Banned | &Fault::Direct(_)) => return false,
456 None | Some(&Fault::Indirect) => (),
457 }
458 trace!(?evidence, "marking validator #{} as faulty", idx.0);
460 self.faults.insert(idx, Fault::Direct(evidence));
461 self.panorama[idx] = Observation::Faulty;
462 true
463 }
464
465 pub(crate) fn add_endorsements(&mut self, endorsements: Endorsements<C>) {
472 let uhash = *endorsements.unit();
473 if self.endorsements.contains_key(&uhash) {
474 return; }
476 info!("Received endorsements of {:?}", uhash);
477 self.incomplete_endorsements
478 .entry(uhash)
479 .or_default()
480 .extend(endorsements.endorsers);
481 let endorsed: Weight = self.incomplete_endorsements[&uhash]
482 .keys()
483 .map(|vidx| self.weight(*vidx))
484 .sum();
485 #[allow(clippy::arithmetic_side_effects)] let threshold = self.total_weight() / 2;
489 if endorsed > threshold {
490 info!(%uhash, "Unit endorsed by at least 1/2 of validators.");
491 let mut fully_endorsed = self.incomplete_endorsements.remove(&uhash).unwrap();
493 let endorsed_map = self
494 .weights()
495 .keys()
496 .map(|vidx| fully_endorsed.remove(&vidx))
497 .collect();
498 self.endorsements.insert(uhash, endorsed_map);
499 }
500 }
501
502 pub fn has_endorsement(&self, uhash: &C::Hash, vidx: ValidatorIndex) -> bool {
504 self.endorsements
505 .get(uhash)
506 .map(|vmap| vmap[vidx].is_some())
507 .unwrap_or(false)
508 || self
509 .incomplete_endorsements
510 .get(uhash)
511 .map(|ends| ends.contains_key(&vidx))
512 .unwrap_or(false)
513 }
514
515 pub(crate) fn add_ping(&mut self, creator: ValidatorIndex, timestamp: Timestamp) {
517 self.pings[creator] = self.pings[creator].max(timestamp);
518 }
519
520 pub fn has_ping(&self, creator: ValidatorIndex, timestamp: Timestamp) -> bool {
522 self.pings
523 .get(creator)
524 .is_some_and(|ping_time| *ping_time >= timestamp)
525 }
526
527 pub(crate) fn is_online(&self, vidx: ValidatorIndex, now: Timestamp) -> bool {
530 self.pings.has(vidx)
531 && self.pings[vidx]
532 .saturating_add(self.params.max_round_length().saturating_mul(PING_TIMEOUT))
533 >= now
534 }
535
536 pub fn find_conflicting_endorsements(
542 &self,
543 endorsements: &Endorsements<C>,
544 instance_id: &C::InstanceId,
545 ) -> Vec<Evidence<C>> {
546 if TODO_ENDORSEMENT_EVIDENCE_DISABLED {
547 return Vec::new();
548 }
549 let uhash = endorsements.unit();
550 let unit = self.unit(uhash);
551 if !self.has_evidence(unit.creator) {
552 return vec![]; }
554
555 let is_new_endorsement = |&&(vidx, _): &&(ValidatorIndex, _)| {
558 if self.has_evidence(vidx) {
559 false
560 } else if let Some(known_endorsements) = self.endorsements.get(uhash) {
561 known_endorsements[vidx].is_none()
562 } else if let Some(known_endorsements) = self.incomplete_endorsements.get(uhash) {
563 !known_endorsements.contains_key(&vidx)
564 } else {
565 true
566 }
567 };
568 let new_endorsements = endorsements.endorsers.iter().filter(is_new_endorsement);
569
570 let conflicting_endorsements = new_endorsements.filter_map(|&(vidx, ref sig)| {
573 let known_endorsements = self.endorsements.iter();
575 let known_incomplete_endorsements = self.incomplete_endorsements.iter();
576 let known_vidx_endorsements = known_endorsements
577 .filter_map(|(uhash2, sigs2)| sigs2[vidx].as_ref().map(|sig2| (uhash2, sig2)));
578 let known_vidx_incomplete_endorsements = known_incomplete_endorsements
579 .filter_map(|(uhash2, sigs2)| sigs2.get(&vidx).map(|sig2| (uhash2, sig2)));
580 known_vidx_endorsements
583 .chain(known_vidx_incomplete_endorsements)
584 .find(|(uhash2, _)| {
585 let unit2 = self.unit(uhash2);
586 let ee_limit = self.params().endorsement_evidence_limit();
587 self.unit(uhash2).creator == unit.creator
588 && !self.is_compatible(uhash, uhash2)
589 && unit.seq_number.saturating_add(ee_limit) >= unit2.seq_number
590 && unit2.seq_number.saturating_add(ee_limit) >= unit.seq_number
591 })
592 .map(|(uhash2, sig2)| {
593 if unit.seq_number <= self.unit(uhash2).seq_number {
594 (vidx, uhash, sig, uhash2, sig2)
595 } else {
596 (vidx, uhash2, sig2, uhash, sig)
597 }
598 })
599 });
600
601 conflicting_endorsements
604 .map(|(vidx, uhash1, sig1, uhash2, sig2)| {
605 let unit1 = self.unit(uhash1);
606 let swimlane2 = self
607 .swimlane(uhash2)
608 .skip(1)
609 .take_while(|(_, pred2)| pred2.seq_number >= unit1.seq_number)
610 .map(|(pred2_hash, _)| self.wire_unit(pred2_hash, *instance_id).unwrap())
611 .collect();
612 Evidence::Endorsements {
613 endorsement1: SignedEndorsement::new(Endorsement::new(*uhash1, vidx), *sig1),
614 unit1: self.wire_unit(uhash1, *instance_id).unwrap(),
615 endorsement2: SignedEndorsement::new(Endorsement::new(*uhash2, vidx), *sig2),
616 unit2: self.wire_unit(uhash2, *instance_id).unwrap(),
617 swimlane2,
618 }
619 })
620 .collect()
621 }
622
623 pub fn wire_unit(
625 &self,
626 hash: &C::Hash,
627 instance_id: C::InstanceId,
628 ) -> Option<SignedWireUnit<C>> {
629 let unit = self.maybe_unit(hash)?.clone();
630 let maybe_block = self.maybe_block(hash);
631 let value = maybe_block.map(|block| block.value.clone());
632 let endorsed = unit.claims_endorsed().cloned().collect();
633 #[allow(clippy::arithmetic_side_effects)] let round_exp =
635 (unit.round_len() / self.params().min_round_length()).trailing_zeros() as u8;
636 let wunit = WireUnit {
637 panorama: unit.panorama.clone(),
638 creator: unit.creator,
639 instance_id,
640 value,
641 seq_number: unit.seq_number,
642 timestamp: unit.timestamp,
643 round_exp,
644 endorsed,
645 };
646 Some(SignedWireUnit {
647 hashed_wire_unit: HashedWireUnit::new_with_hash(wunit, *hash),
648 signature: unit.signature,
649 })
650 }
651
652 pub fn fork_choice<'a>(&'a self, pan: &Panorama<C>) -> Option<&'a C::Hash> {
659 let start = self.clock.start();
660 let to_entry = |(obs, w): (&Observation<C>, &Weight)| {
662 let bhash = &self.unit(obs.correct()?).block;
663 Some((self.block(bhash).height, bhash, *w))
664 };
665 let mut tallies: Tallies<C> = pan.iter().zip(&self.weights).filter_map(to_entry).collect();
666 loop {
667 let (height, bhash) = tallies.find_decided(self)?;
669 tallies = tallies.filter_descendants(height, bhash, self);
671 if tallies.is_empty() {
673 let end = self.clock.end();
674 let delta = self.clock.delta(start, end).as_nanos();
675 trace!(%delta,"Time taken for fork-choice to run");
676 return Some(bhash);
677 }
678 }
679 }
680
681 pub fn find_ancestor_proposal<'a>(
685 &'a self,
686 hash: &'a C::Hash,
687 height: u64,
688 ) -> Option<&'a C::Hash> {
689 let block = self.block(hash);
690 if block.height < height {
691 return None;
692 }
693 if block.height == height {
694 return Some(hash);
695 }
696 #[allow(clippy::arithmetic_side_effects)] let diff = block.height - height;
698 let max_i = log2(diff) as usize;
700 #[allow(clippy::arithmetic_side_effects)]
702 let i = max_i.min(block.skip_idx.len() - 1);
703 self.find_ancestor_proposal(&block.skip_idx[i], height)
704 }
705
706 pub(crate) fn pre_validate_unit(&self, swunit: &SignedWireUnit<C>) -> Result<(), UnitError> {
709 let wunit = swunit.wire_unit();
710 let creator = wunit.creator;
711 if creator.0 as usize >= self.validator_count() {
712 error!("Nonexistent validator should be rejected in Highway::pre_validate_unit.");
713 return Err(UnitError::Creator); }
715 if Some(&Fault::Banned) == self.faults.get(&creator) {
716 return Err(UnitError::Banned);
717 }
718 let rl_millis = self.params.min_round_length().millis();
719 #[allow(clippy::arithmetic_side_effects)] if wunit.round_exp as u32 > rl_millis.leading_zeros()
721 || rl_millis << wunit.round_exp > self.params.max_round_length().millis()
722 {
723 return Err(UnitError::RoundLengthGreaterThanMaximum);
724 }
725 if wunit.value.is_none() && !wunit.panorama.has_correct() {
726 return Err(UnitError::MissingBlock);
727 }
728 if wunit.panorama.len() != self.validator_count() {
729 return Err(UnitError::PanoramaLength(wunit.panorama.len()));
730 }
731 if wunit.panorama[creator].is_faulty() {
732 return Err(UnitError::FaultyCreator);
733 }
734 Ok(())
735 }
736
737 pub(crate) fn validate_unit(&self, swunit: &SignedWireUnit<C>) -> Result<(), UnitError> {
740 let wunit = swunit.wire_unit();
741 let creator = wunit.creator;
742 let panorama = &wunit.panorama;
743 let timestamp = wunit.timestamp;
744 panorama.validate(self)?;
745 if panorama.iter_correct(self).any(|v| v.timestamp > timestamp)
746 || wunit.timestamp < self.params.start_timestamp()
747 {
748 return Err(UnitError::Timestamps);
749 }
750 if wunit.seq_number != panorama.next_seq_num(self, creator) {
751 return Err(UnitError::SequenceNumber);
752 }
753 #[allow(clippy::arithmetic_side_effects)] let round_len =
755 TimeDiff::from_millis(self.params.min_round_length().millis() << wunit.round_exp);
756 let r_id = round_id(timestamp, round_len);
757 let maybe_prev_unit = wunit.previous().map(|vh| self.unit(vh));
758 if let Some(prev_unit) = maybe_prev_unit {
759 if prev_unit.round_len() != round_len {
760 let max_rl = prev_unit.round_len().max(round_len);
763 #[allow(clippy::arithmetic_side_effects)] if prev_unit.timestamp.millis() / max_rl.millis()
765 == timestamp.millis() / max_rl.millis()
766 {
767 return Err(UnitError::RoundLengthChangedWithinRound);
768 }
769 }
770 if let Some(prev2_unit) = prev_unit.previous().map(|h2| self.unit(h2)) {
772 if prev2_unit.round_id() == r_id {
773 return Err(UnitError::ThreeUnitsInRound);
774 }
775 }
776 }
777 if ENABLE_ENDORSEMENTS {
778 let endorsements_in_panorama = panorama
780 .iter_correct_hashes()
781 .flat_map(|hash| self.unit(hash).claims_endorsed())
782 .collect::<HashSet<_>>();
783 if endorsements_in_panorama
784 .iter()
785 .any(|&e| !wunit.endorsed.iter().any(|h| h == e))
786 {
787 return Err(UnitError::EndorsementsNotMonotonic);
788 }
789 for hash in &wunit.endorsed {
790 if !wunit.panorama.sees(self, hash) {
791 return Err(UnitError::EndorsedButUnseen {
792 hash: format!("{:?}", hash),
793 wire_unit: format!("{:?}", wunit),
794 });
795 }
796 }
797 }
798 if wunit.value.is_some() {
799 if maybe_prev_unit.is_some_and(|pv| pv.round_id() == r_id)
802 || timestamp != r_id
803 || self.leader(r_id) != creator
804 {
805 return Err(UnitError::NonLeaderBlock(self.leader(r_id)));
806 }
807 let is_terminal = |hash: &C::Hash| self.is_terminal_block(hash);
809 if self.fork_choice(panorama).is_some_and(is_terminal) {
810 return Err(UnitError::ValueAfterTerminalBlock);
811 }
812 }
813 match self.validate_lnc(creator, panorama, &wunit.endorsed) {
814 None => Ok(()),
815 Some(vidx) => Err(UnitError::LncNaiveCitation(vidx)),
816 }
817 }
818
819 pub(crate) fn is_terminal_block(&self, bhash: &C::Hash) -> bool {
821 self.blocks.get(bhash).is_some_and(|block| {
822 block.height.saturating_add(1) >= self.params.end_height()
823 && self.unit(bhash).timestamp >= self.params.end_timestamp()
824 })
825 }
826
827 pub(super) fn is_correct_proposal(&self, unit: &Unit<C>) -> bool {
829 !self.is_faulty(unit.creator)
830 && self.leader(unit.timestamp) == unit.creator
831 && unit.timestamp == round_id(unit.timestamp, unit.round_len)
832 }
833
834 pub fn find_in_swimlane<'a>(
837 &'a self,
838 hash: &'a C::Hash,
839 seq_number: u64,
840 ) -> Option<&'a C::Hash> {
841 let unit = self.unit(hash);
842 match unit.seq_number.checked_sub(seq_number) {
843 None => None, Some(0) => Some(hash), Some(diff) => {
846 let max_i = log2(diff) as usize; #[allow(clippy::arithmetic_side_effects)]
851 let i = max_i.min(unit.skip_idx.len() - 1);
852 self.find_in_swimlane(&unit.skip_idx[i], seq_number)
853 }
854 }
855 }
856
857 pub fn swimlane<'a>(
860 &'a self,
861 uhash: &'a C::Hash,
862 ) -> impl Iterator<Item = (&'a C::Hash, &'a Unit<C>)> {
863 let mut next = Some(uhash);
864 iter::from_fn(move || {
865 let current = next?;
866 let unit = self.unit(current);
867 next = unit.previous();
868 Some((current, unit))
869 })
870 }
871
872 pub fn ancestor_hashes<'a>(&'a self, bhash: &'a C::Hash) -> impl Iterator<Item = &'a C::Hash> {
875 let mut next = self.block(bhash).parent();
876 iter::from_fn(move || {
877 let current = next?;
878 next = self.block(current).parent();
879 Some(current)
880 })
881 }
882
883 #[cfg(test)]
885 pub(crate) fn unit_count(&self) -> usize {
886 self.units.len()
887 }
888
889 pub fn seen_endorsed(&self, pan: &Panorama<C>) -> BTreeSet<C::Hash> {
891 if !ENABLE_ENDORSEMENTS {
892 return Default::default();
893 };
894 let mut result: BTreeSet<C::Hash> = pan
896 .iter_correct_hashes()
897 .flat_map(|hash| self.unit(hash).endorsed.iter().cloned())
898 .collect();
899 for hash in self.endorsements.keys() {
902 if !result.contains(hash) && pan.sees(self, hash) {
903 result.insert(*hash);
904 }
905 }
906 result
907 }
908
909 pub(crate) fn retain_evidence_only(&mut self) {
911 self.units.clear();
912 self.blocks.clear();
913 for obs in self.panorama.iter_mut() {
914 if obs.is_correct() {
915 *obs = Observation::None;
916 }
917 }
918 self.endorsements.clear();
919 self.incomplete_endorsements.clear();
920 }
921
922 fn validate_lnc(
927 &self,
928 creator: ValidatorIndex,
929 panorama: &Panorama<C>,
930 endorsed: &BTreeSet<C::Hash>,
931 ) -> Option<ValidatorIndex> {
932 if !ENABLE_ENDORSEMENTS {
933 return None;
934 }
935 let violates_lnc =
936 |eq_idx: &ValidatorIndex| !self.satisfies_lnc_for(creator, panorama, endorsed, *eq_idx);
937 panorama.iter_faulty().find(violates_lnc)
938 }
939
940 fn satisfies_lnc_for(
943 &self,
944 creator: ValidatorIndex,
945 panorama: &Panorama<C>,
946 endorsed: &BTreeSet<C::Hash>,
947 eq_idx: ValidatorIndex,
948 ) -> bool {
949 let mut maybe_naive_fork = None;
956 {
957 let seen_by_endorsed = |hash| endorsed.iter().any(|e_hash| self.sees(e_hash, hash));
959
960 let mut to_visit: Vec<_> = panorama.iter_correct_hashes().collect();
962 let mut added_to_to_visit: HashSet<_> = to_visit.iter().cloned().collect();
964 while let Some(hash) = to_visit.pop() {
965 if seen_by_endorsed(hash) {
966 continue; }
968 let unit = self.unit(hash);
969 match &unit.panorama[eq_idx] {
970 Observation::Correct(eq_hash) => {
971 if !seen_by_endorsed(eq_hash) {
974 match maybe_naive_fork {
976 None => maybe_naive_fork = Some(eq_hash),
978 Some(other_hash) => {
979 if self.sees_correct(eq_hash, other_hash) {
982 maybe_naive_fork = Some(eq_hash);
983 } else if !self.sees_correct(other_hash, eq_hash) {
984 return false; }
986 }
987 }
988 }
989 }
990 Observation::None => (),
992 Observation::Faulty => to_visit.extend(
995 unit.panorama
996 .iter_correct_hashes()
997 .filter(|hash| added_to_to_visit.insert(hash)),
998 ),
999 }
1000 }
1001 }
1002 let naive_fork = match maybe_naive_fork {
1003 None => return true, Some(naive_fork) => naive_fork,
1005 };
1006
1007 let mut maybe_pred_hash = panorama[creator].correct();
1011 while let Some(pred_hash) = maybe_pred_hash {
1012 let pred_unit = self.unit(pred_hash);
1013 let seen_by_endorsed = |hash| {
1015 pred_unit
1016 .endorsed
1017 .iter()
1018 .any(|e_hash| self.sees(e_hash, hash))
1019 };
1020 let mut to_visit = vec![pred_hash];
1022 let mut added_to_to_visit: HashSet<_> = to_visit.iter().cloned().collect();
1024 while let Some(hash) = to_visit.pop() {
1025 if seen_by_endorsed(hash) {
1026 continue; }
1028 let unit = self.unit(hash);
1029 match &unit.panorama[eq_idx] {
1030 Observation::Correct(eq_hash) => {
1031 if !seen_by_endorsed(eq_hash) && !self.is_compatible(eq_hash, naive_fork) {
1032 return false;
1033 }
1034 }
1035 Observation::None => (),
1037 Observation::Faulty => to_visit.extend(
1040 unit.panorama
1041 .iter_correct_hashes()
1042 .filter(|hash| added_to_to_visit.insert(hash)),
1043 ),
1044 }
1045 }
1046 if !pred_unit.panorama[eq_idx].is_faulty() {
1047 return true;
1050 }
1051 maybe_pred_hash = pred_unit.previous();
1052 }
1053 true }
1055
1056 pub fn sees_correct(&self, hash0: &C::Hash, hash1: &C::Hash) -> bool {
1059 hash0 == hash1 || self.unit(hash0).panorama.sees_correct(self, hash1)
1060 }
1061
1062 pub fn sees(&self, hash0: &C::Hash, hash1: &C::Hash) -> bool {
1064 hash0 == hash1 || self.unit(hash0).panorama.sees(self, hash1)
1065 }
1066
1067 fn is_compatible(&self, hash0: &C::Hash, hash1: &C::Hash) -> bool {
1069 hash0 == hash1
1070 || self.unit(hash0).panorama.sees(self, hash1)
1071 || self.unit(hash1).panorama.sees(self, hash0)
1072 }
1073
1074 pub fn confirmation_panorama(&self, creator: ValidatorIndex, uhash: &C::Hash) -> Panorama<C> {
1076 self.valid_panorama(creator, self.inclusive_panorama(uhash))
1077 }
1078
1079 pub fn valid_panorama(&self, creator: ValidatorIndex, mut pan: Panorama<C>) -> Panorama<C> {
1084 let maybe_prev_uhash = self.panorama()[creator].correct();
1086 if let Some(prev_uhash) = maybe_prev_uhash {
1087 if pan[creator].correct() != Some(prev_uhash) {
1088 pan = pan.merge(self, &self.inclusive_panorama(prev_uhash));
1089 }
1090 }
1091 let endorsed = self.seen_endorsed(&pan);
1092 if self.validate_lnc(creator, &pan, &endorsed).is_none() {
1093 return pan;
1094 }
1095 pan = maybe_prev_uhash.map_or_else(
1099 || Panorama::new(self.validator_count()),
1100 |prev_uhash| self.inclusive_panorama(prev_uhash),
1101 );
1102 for faulty_v in self.faulty_validators() {
1103 pan[faulty_v] = Observation::Faulty;
1104 }
1105 for endorsed_hash in &endorsed {
1106 if !pan.sees_correct(self, endorsed_hash) {
1107 pan = pan.merge(self, &self.inclusive_panorama(endorsed_hash));
1108 }
1109 }
1110 pan
1111 }
1112
1113 pub fn inclusive_panorama(&self, uhash: &C::Hash) -> Panorama<C> {
1115 let unit = self.unit(uhash);
1116 let mut pan = unit.panorama.clone();
1117 pan[unit.creator] = Observation::Correct(*uhash);
1118 pan
1119 }
1120}
1121
1122pub(crate) fn round_id(timestamp: Timestamp, round_len: TimeDiff) -> Timestamp {
1128 if round_len.millis() == 0 {
1129 error!("called round_id with round_len 0.");
1130 return timestamp;
1131 }
1132 #[allow(clippy::arithmetic_side_effects)] Timestamp::from((timestamp.millis() / round_len.millis()) * round_len.millis())
1134}
1135
1136fn log2(x: u64) -> u32 {
1139 x.saturating_add(1)
1142 .checked_next_power_of_two()
1143 .unwrap_or(0)
1144 .trailing_zeros()
1145 .saturating_sub(1)
1146}