diff --git a/Cargo.lock b/Cargo.lock index b1af35628f..39f9b66d99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -827,6 +827,7 @@ checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" dependencies = [ "clap_builder", "clap_derive 4.2.0", + "once_cell", ] [[package]] @@ -2961,6 +2962,19 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" +[[package]] +name = "highway-rewards-analysis" +version = "0.1.0" +dependencies = [ + "bincode", + "casper-hashing", + "casper-node", + "casper-types", + "clap 4.2.7", + "flate2", + "serde", +] + [[package]] name = "hmac" version = "0.12.1" diff --git a/Cargo.toml b/Cargo.toml index 3c4773e543..95eaa0d63d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "types", "utils/global-state-update-gen", "utils/validation", + "utils/highway-rewards-analysis", ] default-members = [ @@ -25,6 +26,7 @@ default-members = [ "types", "utils/global-state-update-gen", "utils/validation", + "utils/highway-rewards-analysis", ] exclude = ["utils/nctl/remotes/casper-client-rs"] diff --git a/node/src/components.rs b/node/src/components.rs index 17c0fbf08a..af24afea45 100644 --- a/node/src/components.rs +++ b/node/src/components.rs @@ -47,7 +47,7 @@ pub(crate) mod block_accumulator; pub(crate) mod block_synchronizer; pub(crate) mod block_validator; -pub(crate) mod consensus; +pub mod consensus; pub mod contract_runtime; pub(crate) mod deploy_acceptor; pub(crate) mod deploy_buffer; diff --git a/node/src/components/consensus.rs b/node/src/components/consensus.rs index 220d640b9e..d9aecaa31f 100644 --- a/node/src/components/consensus.rs +++ b/node/src/components/consensus.rs @@ -7,15 +7,15 @@ mod config; mod consensus_protocol; mod era_supervisor; #[macro_use] -mod highway_core; +pub mod highway_core; pub(crate) mod error; mod leader_sequence; mod metrics; -mod protocols; +pub mod protocols; #[cfg(test)] pub(crate) mod tests; mod traits; -pub(crate) mod utils; +pub mod utils; mod validator_change; use std::{ @@ -56,7 +56,7 @@ use crate::{ use protocols::{highway::HighwayProtocol, zug::Zug}; use traits::Context; -pub(crate) use cl_context::ClContext; +pub use cl_context::ClContext; pub(crate) use config::{ChainspecConsensusExt, Config}; pub(crate) use consensus_protocol::{BlockContext, EraReport, ProposedBlock}; pub(crate) use era_supervisor::{debug::EraDump, EraSupervisor, SerializedMessage}; @@ -123,6 +123,7 @@ pub struct TimerId(pub u8); #[derive(DataSize, Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct ActionId(pub u8); +/// Payload for a block to be proposed. #[derive(DataSize, Debug, From)] pub struct NewBlockPayload { pub(crate) era_id: EraId, @@ -130,6 +131,7 @@ pub struct NewBlockPayload { pub(crate) block_context: BlockContext, } +/// The result of validation of a ProposedBlock. #[derive(DataSize, Debug, From)] pub struct ResolveValidity { era_id: EraId, diff --git a/node/src/components/consensus/cl_context.rs b/node/src/components/consensus/cl_context.rs index 7d8225010f..251f1022dd 100644 --- a/node/src/components/consensus/cl_context.rs +++ b/node/src/components/consensus/cl_context.rs @@ -56,7 +56,7 @@ impl ConsensusValueT for Arc { /// The collection of types used for cryptography, IDs and blocks in the Casper node. #[derive(Clone, DataSize, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] -pub(crate) struct ClContext; +pub struct ClContext; impl Context for ClContext { type ConsensusValue = Arc; diff --git a/node/src/components/consensus/highway_core.rs b/node/src/components/consensus/highway_core.rs index 311f91b10a..c0cd2524e7 100644 --- a/node/src/components/consensus/highway_core.rs +++ b/node/src/components/consensus/highway_core.rs @@ -33,8 +33,8 @@ mod test_macros; pub(crate) mod active_validator; -pub(crate) mod finality_detector; -pub(crate) mod highway; +pub mod finality_detector; +pub mod highway; pub(crate) mod state; pub(super) mod synchronizer; @@ -43,7 +43,7 @@ mod evidence; #[cfg(test)] pub(crate) mod highway_testing; -pub(crate) use state::State; +pub use state::{Observation, Panorama, State}; // Enables the endorsement mechanism. const ENABLE_ENDORSEMENTS: bool = false; diff --git a/node/src/components/consensus/highway_core/endorsement.rs b/node/src/components/consensus/highway_core/endorsement.rs index c20bcaadf9..99be3dbcee 100644 --- a/node/src/components/consensus/highway_core/endorsement.rs +++ b/node/src/components/consensus/highway_core/endorsement.rs @@ -24,7 +24,7 @@ pub(crate) enum EndorsementError { serialize = "C::Hash: Serialize", deserialize = "C::Hash: Deserialize<'de>", ))] -pub(crate) struct Endorsement +pub struct Endorsement where C: Context, { @@ -42,7 +42,8 @@ impl Endorsement { } } - pub(crate) fn hash(&self) -> C::Hash { + /// Returns the hash of the endorsement. + pub fn hash(&self) -> C::Hash { ::hash( &bincode::serialize(&(self.unit, self.creator)).expect("serialize endorsement"), ) @@ -74,7 +75,7 @@ mod specimen_support { serialize = "C::Signature: Serialize", deserialize = "C::Signature: Deserialize<'de>", ))] -pub(crate) struct SignedEndorsement +pub struct SignedEndorsement where C: Context, { @@ -92,19 +93,23 @@ impl SignedEndorsement { } } - pub(crate) fn unit(&self) -> &C::Hash { + /// Returns the unit being endorsed. + pub fn unit(&self) -> &C::Hash { &self.endorsement.unit } - pub(crate) fn validator_idx(&self) -> ValidatorIndex { + /// Returns the creator of the endorsement. + pub fn validator_idx(&self) -> ValidatorIndex { self.endorsement.creator } - pub(crate) fn signature(&self) -> &C::Signature { + /// Returns the signature of the endorsement. + pub fn signature(&self) -> &C::Signature { &self.signature } - pub(crate) fn hash(&self) -> C::Hash { + /// Returns the hash of the endorsement. + pub fn hash(&self) -> C::Hash { self.endorsement.hash() } } diff --git a/node/src/components/consensus/highway_core/evidence.rs b/node/src/components/consensus/highway_core/evidence.rs index 5667edde00..85fe61bdf2 100644 --- a/node/src/components/consensus/highway_core/evidence.rs +++ b/node/src/components/consensus/highway_core/evidence.rs @@ -11,7 +11,7 @@ use crate::components::consensus::{ /// An error due to invalid evidence. #[derive(Debug, Error, Eq, PartialEq)] -pub(crate) enum EvidenceError { +pub enum EvidenceError { #[error("The sequence numbers in the equivocating units are different.")] EquivocationDifferentSeqNumbers, #[error("The creators in the equivocating units are different.")] @@ -57,7 +57,7 @@ pub mod relaxed { deserialize = "C::Hash: Deserialize<'de>", ))] #[strum_discriminants(derive(strum::EnumIter))] - pub(crate) enum Evidence + pub enum Evidence where C: Context, { @@ -80,11 +80,11 @@ pub mod relaxed { }, } } -pub(crate) use relaxed::{Evidence, EvidenceDiscriminants}; +pub use relaxed::{Evidence, EvidenceDiscriminants}; impl Evidence { /// Returns the ID of the faulty validator. - pub(crate) fn perpetrator(&self) -> ValidatorIndex { + pub fn perpetrator(&self) -> ValidatorIndex { match self { Evidence::Equivocation(unit1, _) => unit1.wire_unit().creator, Evidence::Endorsements { endorsement1, .. } => endorsement1.validator_idx(), @@ -96,7 +96,7 @@ impl Evidence { /// /// - For an equivocation, it checks whether the creators, sequence numbers and instance IDs of /// the two units are the same. - pub(crate) fn validate( + pub fn validate( &self, validators: &Validators, instance_id: &C::InstanceId, diff --git a/node/src/components/consensus/highway_core/finality_detector.rs b/node/src/components/consensus/highway_core/finality_detector.rs index 6e72b63472..32e4563a29 100644 --- a/node/src/components/consensus/highway_core/finality_detector.rs +++ b/node/src/components/consensus/highway_core/finality_detector.rs @@ -1,3 +1,5 @@ +//! Functions for detecting finality of proposed blocks and calculating rewards. + mod horizon; mod rewards; @@ -18,6 +20,10 @@ use crate::components::consensus::{ utils::{ValidatorIndex, Weight}, }; use horizon::Horizon; +pub use rewards::{ + assigned_weight_and_latest_unit, compute_rewards, compute_rewards_for, find_max_quora, + round_participation, RoundParticipation, +}; /// An error returned if the configured fault tolerance has been exceeded. #[derive(Debug)] @@ -179,7 +185,7 @@ impl FinalityDetector { let state = highway.state(); // Compute the rewards, and replace each validator index with the validator ID. - let rewards = rewards::compute_rewards(state, bhash); + let rewards = compute_rewards(state, bhash); let rewards_iter = rewards.enumerate(); let rewards = rewards_iter.map(|(vidx, r)| (to_id(vidx), *r)).collect(); diff --git a/node/src/components/consensus/highway_core/finality_detector/rewards.rs b/node/src/components/consensus/highway_core/finality_detector/rewards.rs index 4f2528b2fd..c8939866fe 100644 --- a/node/src/components/consensus/highway_core/finality_detector/rewards.rs +++ b/node/src/components/consensus/highway_core/finality_detector/rewards.rs @@ -12,7 +12,7 @@ use crate::components::consensus::{ /// Returns the map of rewards to be paid out when the block `bhash` gets finalized. /// /// This is the sum of all rewards for finalization of ancestors of `bhash`, as seen from `bhash`. -pub(crate) fn compute_rewards(state: &State, bhash: &C::Hash) -> ValidatorMap { +pub fn compute_rewards(state: &State, bhash: &C::Hash) -> ValidatorMap { // The unit that introduced the payout block. let payout_unit = state.unit(bhash); // The panorama of the payout block: Rewards must only use this panorama, since it defines @@ -38,17 +38,15 @@ pub(crate) fn compute_rewards(state: &State, bhash: &C::Hash) -> rewards } -/// Returns the rewards for finalizing the block with hash `proposal_h`. -fn compute_rewards_for( - state: &State, - panorama: &Panorama, - proposal_h: &C::Hash, -) -> ValidatorMap { - let proposal_unit = state.unit(proposal_h); - let r_id = proposal_unit.round_id(); - - // Only consider messages in round `r_id` for the summit. To compute the assigned weight, we - // also include validators who didn't send a message in that round, but were supposed to. +/// Finds the total assigned weight of a particular round and the latest unit of each validator in +/// that round. +pub fn assigned_weight_and_latest_unit<'a, 'b: 'a, 'c: 'a, C: Context>( + state: &'b State, + panorama: &'c Panorama, + r_id: Timestamp, +) -> (Weight, ValidatorMap>) { + // To compute the assigned weight, we also include validators who didn't send a message in that + // round, but were supposed to. let mut assigned_weight = Weight(0); let mut latest = ValidatorMap::from(vec![None; panorama.len()]); for (idx, obs) in panorama.enumerate() { @@ -59,13 +57,17 @@ fn compute_rewards_for( } assigned_weight += state.weight(idx); } + (assigned_weight, latest) +} - if assigned_weight.is_zero() { - return ValidatorMap::from(vec![0; latest.len()]); - } - +/// Finds the maximum quora of level-1 summits in which different validators participated. +pub fn find_max_quora( + state: &State, + proposal_h: &C::Hash, + latest: &ValidatorMap>, +) -> ValidatorMap { // Find all level-1 summits. For each validator, store the highest quorum it is a part of. - let horizon = Horizon::level0(proposal_h, state, &latest); + let horizon = Horizon::level0(proposal_h, state, latest); let (mut committee, _) = horizon.prune_committee(Weight(1), latest.keys_some().collect()); let mut max_quorum = ValidatorMap::from(vec![Weight(0); latest.len()]); while let Some(quorum) = horizon.committee_quorum(&committee) { @@ -77,6 +79,26 @@ fn compute_rewards_for( max_quorum[vidx] = quorum; } } + max_quorum +} + +/// Returns the rewards for finalizing the block with hash `proposal_h`. +pub fn compute_rewards_for( + state: &State, + panorama: &Panorama, + proposal_h: &C::Hash, +) -> ValidatorMap { + let proposal_unit = state.unit(proposal_h); + let r_id = proposal_unit.round_id(); + + // Only consider messages in round `r_id` for the summit. + let (assigned_weight, latest) = assigned_weight_and_latest_unit(state, panorama, r_id); + + if assigned_weight.is_zero() { + return ValidatorMap::from(vec![0; latest.len()]); + } + + let max_quorum = find_max_quora(state, proposal_h, &latest); let faulty_w: Weight = panorama.iter_faulty().map(|vidx| state.weight(vidx)).sum(); @@ -113,7 +135,7 @@ fn compute_rewards_for( /// Information about how a validator participated in a particular round. #[derive(Debug, Eq, PartialEq)] -enum RoundParticipation<'a, C: Context> { +pub enum RoundParticipation<'a, C: Context> { /// The validator was not assigned: The round ID was not the beginning of one of their rounds. Unassigned, /// The validator was assigned but did not create any messages in that round. @@ -123,7 +145,7 @@ enum RoundParticipation<'a, C: Context> { } /// Returns information about the participation of a validator with `obs` in round `r_id`. -fn round_participation<'a, C: Context>( +pub fn round_participation<'a, C: Context>( state: &'a State, obs: &'a Observation, r_id: Timestamp, diff --git a/node/src/components/consensus/highway_core/highway.rs b/node/src/components/consensus/highway_core/highway.rs index 75f77397c8..682485463d 100644 --- a/node/src/components/consensus/highway_core/highway.rs +++ b/node/src/components/consensus/highway_core/highway.rs @@ -1,7 +1,9 @@ +//! The implementation of the Highway consensus protocol. + mod vertex; pub(crate) use crate::components::consensus::highway_core::state::Params; -pub(crate) use vertex::{ +pub use vertex::{ Dependency, Endorsements, HashedWireUnit, Ping, SignedWireUnit, Vertex, WireUnit, }; diff --git a/node/src/components/consensus/highway_core/highway/vertex.rs b/node/src/components/consensus/highway_core/highway/vertex.rs index c8f38611fd..1be85d8636 100644 --- a/node/src/components/consensus/highway_core/highway/vertex.rs +++ b/node/src/components/consensus/highway_core/highway/vertex.rs @@ -50,13 +50,17 @@ mod relaxed { deserialize = "C::Hash: Deserialize<'de>", ))] #[strum_discriminants(derive(strum::EnumIter))] - pub(crate) enum Dependency + pub enum Dependency where C: Context, { + /// The hash of a unit. Unit(C::Hash), + /// The index of the validator against which evidence is needed. Evidence(ValidatorIndex), + /// The hash of the unit to be endorsed. Endorsement(C::Hash), + /// The ping by a particular validator for a particular timestamp. Ping(ValidatorIndex, Timestamp), } @@ -71,21 +75,25 @@ mod relaxed { deserialize = "C::Hash: Deserialize<'de>", ))] #[strum_discriminants(derive(strum::EnumIter))] - pub(crate) enum Vertex + pub enum Vertex where C: Context, { + /// A signed unit of the consensus DAG. Unit(SignedWireUnit), + /// Evidence of a validator's transgression. Evidence(Evidence), + /// Endorsements for a unit. Endorsements(Endorsements), + /// A ping conveying the activity of its creator. Ping(Ping), } } -pub(crate) use relaxed::{Dependency, DependencyDiscriminants, Vertex, VertexDiscriminants}; +pub use relaxed::{Dependency, DependencyDiscriminants, Vertex, VertexDiscriminants}; impl Dependency { /// Returns whether this identifies a unit, as opposed to other types of vertices. - pub(crate) fn is_unit(&self) -> bool { + pub fn is_unit(&self) -> bool { matches!(self, Dependency::Unit(_)) } } @@ -97,7 +105,7 @@ impl Vertex { /// `C::ConsensusValue` is a transaction, it should be validated first (correct signature, /// structure, gas limit, etc.). If it is a hash of a transaction, the transaction should be /// obtained _and_ validated. Only after that, the vertex can be considered valid. - pub(crate) fn value(&self) -> Option<&C::ConsensusValue> { + pub fn value(&self) -> Option<&C::ConsensusValue> { match self { Vertex::Unit(swunit) => swunit.wire_unit().value.as_ref(), Vertex::Evidence(_) | Vertex::Endorsements(_) | Vertex::Ping(_) => None, @@ -105,7 +113,7 @@ impl Vertex { } /// Returns the unit hash of this vertex (if it is a unit). - pub(crate) fn unit_hash(&self) -> Option { + pub fn unit_hash(&self) -> Option { match self { Vertex::Unit(swunit) => Some(swunit.hash()), Vertex::Evidence(_) | Vertex::Endorsements(_) | Vertex::Ping(_) => None, @@ -113,7 +121,7 @@ impl Vertex { } /// Returns the seq number of this vertex (if it is a unit). - pub(crate) fn unit_seq_number(&self) -> Option { + pub fn unit_seq_number(&self) -> Option { match self { Vertex::Unit(swunit) => Some(swunit.wire_unit().seq_number), _ => None, @@ -121,12 +129,12 @@ impl Vertex { } /// Returns whether this is evidence, as opposed to other types of vertices. - pub(crate) fn is_evidence(&self) -> bool { + pub fn is_evidence(&self) -> bool { matches!(self, Vertex::Evidence(_)) } /// Returns a `Timestamp` provided the vertex is a `Vertex::Unit` or `Vertex::Ping`. - pub(crate) fn timestamp(&self) -> Option { + pub fn timestamp(&self) -> Option { match self { Vertex::Unit(signed_wire_unit) => Some(signed_wire_unit.wire_unit().timestamp), Vertex::Ping(ping) => Some(ping.timestamp()), @@ -134,7 +142,8 @@ impl Vertex { } } - pub(crate) fn creator(&self) -> Option { + /// Returns the creator of this vertex, if one is defined. + pub fn creator(&self) -> Option { match self { Vertex::Unit(signed_wire_unit) => Some(signed_wire_unit.wire_unit().creator), Vertex::Ping(ping) => Some(ping.creator), @@ -142,7 +151,8 @@ impl Vertex { } } - pub(crate) fn id(&self) -> Dependency { + /// Returns the ID of this vertex. + pub fn id(&self) -> Dependency { match self { Vertex::Unit(signed_wire_unit) => Dependency::Unit(signed_wire_unit.hash()), Vertex::Evidence(evidence) => Dependency::Evidence(evidence.perpetrator()), @@ -152,7 +162,7 @@ impl Vertex { } /// Returns a reference to the unit, or `None` if this is not a unit. - pub(crate) fn unit(&self) -> Option<&SignedWireUnit> { + pub fn unit(&self) -> Option<&SignedWireUnit> { match self { Vertex::Unit(signed_wire_unit) => Some(signed_wire_unit), _ => None, @@ -160,7 +170,7 @@ impl Vertex { } /// Returns true whether unit is a proposal. - pub(crate) fn is_proposal(&self) -> bool { + pub fn is_proposal(&self) -> bool { self.value().is_some() } } @@ -293,12 +303,13 @@ mod specimen_support { } } +/// A `WireUnit` together with its hash and a cryptographic signature by its creator. #[derive(DataSize, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] #[serde(bound( serialize = "C::Hash: Serialize", deserialize = "C::Hash: Deserialize<'de>", ))] -pub(crate) struct SignedWireUnit +pub struct SignedWireUnit where C: Context, { @@ -318,17 +329,20 @@ impl SignedWireUnit { } } - pub(crate) fn wire_unit(&self) -> &WireUnit { + /// Returns the inner `WireUnit`. + pub fn wire_unit(&self) -> &WireUnit { self.hashed_wire_unit.wire_unit() } - pub(crate) fn hash(&self) -> C::Hash { + /// Returns this unit's hash. + pub fn hash(&self) -> C::Hash { self.hashed_wire_unit.hash() } } +/// A `WireUnit` together with its hash. #[derive(Clone, DataSize, Debug, Eq, PartialEq, Hash)] -pub(crate) struct HashedWireUnit +pub struct HashedWireUnit where C: Context, { @@ -346,15 +360,18 @@ where Self::new_with_hash(wire_unit, hash) } - pub(crate) fn into_inner(self) -> WireUnit { + /// Returns the inner `WireUnit`. + pub fn into_inner(self) -> WireUnit { self.wire_unit } - pub(crate) fn wire_unit(&self) -> &WireUnit { + /// Returns a reference to the inner `WireUnit`. + pub fn wire_unit(&self) -> &WireUnit { &self.wire_unit } - pub(crate) fn hash(&self) -> C::Hash { + /// Returns this unit's hash. + pub fn hash(&self) -> C::Hash { self.hash } @@ -383,18 +400,26 @@ impl<'de, C: Context> Deserialize<'de> for HashedWireUnit { serialize = "C::Hash: Serialize", deserialize = "C::Hash: Deserialize<'de>", ))] -pub(crate) struct WireUnit +pub struct WireUnit where C: Context, { - pub(crate) panorama: Panorama, - pub(crate) creator: ValidatorIndex, - pub(crate) instance_id: C::InstanceId, - pub(crate) value: Option, - pub(crate) seq_number: u64, - pub(crate) timestamp: Timestamp, - pub(crate) round_exp: u8, - pub(crate) endorsed: BTreeSet, + /// The panorama of cited units. + pub panorama: Panorama, + /// The index of the creator of this unit. + pub creator: ValidatorIndex, + /// The consensus instance ID for which this unit was created. + pub instance_id: C::InstanceId, + /// The consensus value included in the unit, if any. + pub value: Option, + /// The sequence number of this unit in the creator's swimlane. + pub seq_number: u64, + /// Timestamp of when the unit was created. + pub timestamp: Timestamp, + /// The current round exponent of the unit's creator. + pub round_exp: u8, + /// The units this unit endorses. + pub endorsed: BTreeSet, } impl Debug for WireUnit { @@ -427,7 +452,7 @@ impl WireUnit { } /// Returns the creator's previous unit. - pub(crate) fn previous(&self) -> Option<&C::Hash> { + pub fn previous(&self) -> Option<&C::Hash> { self.panorama[self.creator].correct() } @@ -438,17 +463,20 @@ impl WireUnit { } } +/// A set of endorsements for a unit. #[derive(Clone, DataSize, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] #[serde(bound( serialize = "C::Hash: Serialize", deserialize = "C::Hash: Deserialize<'de>", ))] -pub(crate) struct Endorsements +pub struct Endorsements where C: Context, { - pub(crate) unit: C::Hash, - pub(crate) endorsers: Vec<(ValidatorIndex, C::Signature)>, + /// The endorsed unit. + pub unit: C::Hash, + /// The endorsements for the unit. + pub endorsers: Vec<(ValidatorIndex, C::Signature)>, } impl Endorsements { @@ -479,7 +507,7 @@ impl From> for Endorsements { serialize = "C::Hash: Serialize", deserialize = "C::Hash: Deserialize<'de>", ))] -pub(crate) struct Ping +pub struct Ping where C: Context, { @@ -507,12 +535,12 @@ impl Ping { } /// The creator who signals that it is online. - pub(crate) fn creator(&self) -> ValidatorIndex { + pub fn creator(&self) -> ValidatorIndex { self.creator } /// The timestamp when the ping was created. - pub(crate) fn timestamp(&self) -> Timestamp { + pub fn timestamp(&self) -> Timestamp { self.timestamp } diff --git a/node/src/components/consensus/highway_core/state.rs b/node/src/components/consensus/highway_core/state.rs index 7a54833bc0..a250140209 100644 --- a/node/src/components/consensus/highway_core/state.rs +++ b/node/src/components/consensus/highway_core/state.rs @@ -13,7 +13,7 @@ use quanta::Clock; use serde::{Deserialize, Serialize}; pub(crate) use index_panorama::{IndexObservation, IndexPanorama}; -pub(crate) use panorama::{Observation, Panorama}; +pub use panorama::{Observation, Panorama}; pub(super) use unit::Unit; use std::{ @@ -111,7 +111,7 @@ pub(crate) enum UnitError { /// be replaced with `Direct` evidence, which has the same effect but doesn't rely on information /// from other consensus protocol instances. #[derive(Clone, DataSize, Debug, Deserialize, Eq, PartialEq, Hash, Serialize)] -pub(crate) enum Fault +pub enum Fault where C: Context, { @@ -125,7 +125,8 @@ where } impl Fault { - pub(crate) fn evidence(&self) -> Option<&Evidence> { + /// Returns the evidence included in this `Fault`. + pub fn evidence(&self) -> Option<&Evidence> { match self { Fault::Banned | Fault::Indirect => None, Fault::Direct(ev) => Some(ev), @@ -138,8 +139,8 @@ impl Fault { /// Both observers and active validators must instantiate this, pass in all incoming vertices from /// peers, and use a [FinalityDetector](../finality_detector/struct.FinalityDetector.html) to /// determine the outcome of the consensus process. -#[derive(Debug, Clone, DataSize, Serialize)] -pub(crate) struct State +#[derive(Debug, Clone, DataSize, Serialize, Deserialize)] +pub struct State where C: Context, { @@ -198,7 +199,7 @@ impl State { { let weights = ValidatorMap::from(weights.into_iter().map(|w| *w.borrow()).collect_vec()); assert!( - weights.len() > 0, + !weights.is_empty(), "cannot initialize Highway with no validators" ); let mut panorama = Panorama::new(weights.len()); @@ -238,27 +239,27 @@ impl State { } /// Returns the fixed parameters. - pub(crate) fn params(&self) -> &Params { + pub fn params(&self) -> &Params { &self.params } /// Returns the number of validators. - pub(crate) fn validator_count(&self) -> usize { + pub fn validator_count(&self) -> usize { self.weights.len() } /// Returns the `idx`th validator's voting weight. - pub(crate) fn weight(&self, idx: ValidatorIndex) -> Weight { + pub fn weight(&self, idx: ValidatorIndex) -> Weight { self.weights[idx] } /// Returns the map of validator weights. - pub(crate) fn weights(&self) -> &ValidatorMap { + pub fn weights(&self) -> &ValidatorMap { &self.weights } /// Returns the total weight of all validators marked faulty in this panorama. - pub(crate) fn faulty_weight_in(&self, panorama: &Panorama) -> Weight { + pub fn faulty_weight_in(&self, panorama: &Panorama) -> Weight { panorama .iter() .zip(&self.weights) @@ -268,22 +269,22 @@ impl State { } /// Returns the total weight of all known-faulty validators. - pub(crate) fn faulty_weight(&self) -> Weight { + pub fn faulty_weight(&self) -> Weight { self.faulty_weight_in(self.panorama()) } /// Returns the sum of all validators' voting weights. - pub(crate) fn total_weight(&self) -> Weight { + pub fn total_weight(&self) -> Weight { self.leader_sequence.total_weight() } /// Returns evidence against validator nr. `idx`, if present. - pub(crate) fn maybe_evidence(&self, idx: ValidatorIndex) -> Option<&Evidence> { + pub fn maybe_evidence(&self, idx: ValidatorIndex) -> Option<&Evidence> { self.maybe_fault(idx).and_then(Fault::evidence) } /// Returns endorsements for `unit`, if any. - pub(crate) fn maybe_endorsements(&self, unit: &C::Hash) -> Option> { + pub fn maybe_endorsements(&self, unit: &C::Hash) -> Option> { self.endorsements.get(unit).map(|signatures| Endorsements { unit: *unit, endorsers: signatures.iter_some().map(|(i, sig)| (i, *sig)).collect(), @@ -291,12 +292,12 @@ impl State { } /// Returns whether evidence against validator nr. `idx` is known. - pub(crate) fn has_evidence(&self, idx: ValidatorIndex) -> bool { + pub fn has_evidence(&self, idx: ValidatorIndex) -> bool { self.maybe_evidence(idx).is_some() } /// Returns whether we have all endorsements for `unit`. - pub(crate) fn has_all_endorsements>( + pub fn has_all_endorsements>( &self, unit: &C::Hash, v_ids: I, @@ -312,12 +313,12 @@ impl State { /// Returns whether we have seen enough endorsements for the unit. /// Unit is endorsed when it has endorsements from more than 50% of the validators (by weight). - pub(crate) fn is_endorsed(&self, hash: &C::Hash) -> bool { + pub fn is_endorsed(&self, hash: &C::Hash) -> bool { self.endorsements.contains_key(hash) } /// Returns hash of unit that needs to be endorsed. - pub(crate) fn needs_endorsements(&self, unit: &SignedWireUnit) -> Option { + pub fn needs_endorsements(&self, unit: &SignedWireUnit) -> Option { unit.wire_unit() .endorsed .iter() @@ -327,63 +328,63 @@ impl State { /// Returns the timestamp of the last ping or unit received from the validator, or the start /// timestamp if we haven't received anything yet. - pub(crate) fn last_seen(&self, idx: ValidatorIndex) -> Timestamp { + pub fn last_seen(&self, idx: ValidatorIndex) -> Timestamp { self.pings[idx] } /// Marks the given validator as faulty, unless it is already banned or we have direct evidence. - pub(crate) fn mark_faulty(&mut self, idx: ValidatorIndex) { + pub fn mark_faulty(&mut self, idx: ValidatorIndex) { self.panorama[idx] = Observation::Faulty; self.faults.entry(idx).or_insert(Fault::Indirect); } /// Returns the fault type of validator nr. `idx`, if it is known to be faulty. - pub(crate) fn maybe_fault(&self, idx: ValidatorIndex) -> Option<&Fault> { + pub fn maybe_fault(&self, idx: ValidatorIndex) -> Option<&Fault> { self.faults.get(&idx) } /// Returns whether validator nr. `idx` is known to be faulty. - pub(crate) fn is_faulty(&self, idx: ValidatorIndex) -> bool { + pub fn is_faulty(&self, idx: ValidatorIndex) -> bool { self.faults.contains_key(&idx) } /// Returns an iterator over all faulty validators. - pub(crate) fn faulty_validators(&self) -> impl Iterator + '_ { + pub fn faulty_validators(&self) -> impl Iterator + '_ { self.faults.keys().cloned() } /// Returns an iterator over latest unit hashes from honest validators. - pub(crate) fn iter_correct_hashes(&self) -> impl Iterator { + pub fn iter_correct_hashes(&self) -> impl Iterator { self.panorama.iter_correct_hashes() } /// Returns the unit with the given hash, if present. - pub(crate) fn maybe_unit(&self, hash: &C::Hash) -> Option<&Unit> { + pub fn maybe_unit(&self, hash: &C::Hash) -> Option<&Unit> { self.units.get(hash) } /// Returns whether the unit with the given hash is known. - pub(crate) fn has_unit(&self, hash: &C::Hash) -> bool { + pub fn has_unit(&self, hash: &C::Hash) -> bool { self.units.contains_key(hash) } /// Returns the unit with the given hash. Panics if not found. - pub(crate) fn unit(&self, hash: &C::Hash) -> &Unit { + pub fn unit(&self, hash: &C::Hash) -> &Unit { self.maybe_unit(hash).expect("unit hash must exist") } /// Returns the block contained in the unit with the given hash, if present. - pub(crate) fn maybe_block(&self, hash: &C::Hash) -> Option<&Block> { + pub fn maybe_block(&self, hash: &C::Hash) -> Option<&Block> { self.blocks.get(hash) } /// Returns the block contained in the unit with the given hash. Panics if not found. - pub(crate) fn block(&self, hash: &C::Hash) -> &Block { + pub fn block(&self, hash: &C::Hash) -> &Block { self.maybe_block(hash).expect("block hash must exist") } /// Returns the complete protocol state's latest panorama. - pub(crate) fn panorama(&self) -> &Panorama { + pub fn panorama(&self) -> &Panorama { &self.panorama } @@ -394,7 +395,7 @@ impl State { /// validators excluded. This ensures that once the validator set has been decided, correct /// validators' slots never get reassigned to someone else, even if after the fact someone is /// excluded as a leader. - pub(crate) fn leader(&self, timestamp: Timestamp) -> ValidatorIndex { + pub fn leader(&self, timestamp: Timestamp) -> ValidatorIndex { self.leader_sequence.leader(timestamp.millis()) } @@ -497,7 +498,7 @@ impl State { } /// Returns whether this state already includes an endorsement of `uhash` by `vidx`. - pub(crate) fn has_endorsement(&self, uhash: &C::Hash, vidx: ValidatorIndex) -> bool { + pub fn has_endorsement(&self, uhash: &C::Hash, vidx: ValidatorIndex) -> bool { self.endorsements .get(uhash) .map(|vmap| vmap[vidx].is_some()) @@ -515,7 +516,7 @@ impl State { } /// Returns `true` if the latest timestamp we have is older than the given timestamp. - pub(crate) fn has_ping(&self, creator: ValidatorIndex, timestamp: Timestamp) -> bool { + pub fn has_ping(&self, creator: ValidatorIndex, timestamp: Timestamp) -> bool { self.pings .get(creator) .map_or(false, |ping_time| *ping_time >= timestamp) @@ -533,7 +534,7 @@ impl State { /// /// Endorsements must be validated before calling this: The endorsers must exist, the /// signatures must be valid and the endorsed unit must be present in `self.units`. - pub(crate) fn find_conflicting_endorsements( + pub fn find_conflicting_endorsements( &self, endorsements: &Endorsements, instance_id: &C::InstanceId, @@ -616,7 +617,7 @@ impl State { } /// Returns the `SignedWireUnit` with the given hash, if it is present in the state. - pub(crate) fn wire_unit( + pub fn wire_unit( &self, hash: &C::Hash, instance_id: C::InstanceId, @@ -649,7 +650,7 @@ impl State { /// all of its ancestors. At each level the block with the highest score is selected from the /// children of the previously selected block (or from all blocks at height 0), until a block /// is reached that has no children with any votes. - pub(crate) fn fork_choice<'a>(&'a self, pan: &Panorama) -> Option<&'a C::Hash> { + pub fn fork_choice<'a>(&'a self, pan: &Panorama) -> Option<&'a C::Hash> { let start = self.clock.start(); // Collect all correct votes in a `Tallies` map, sorted by height. let to_entry = |(obs, w): (&Observation, &Weight)| { @@ -674,8 +675,8 @@ impl State { /// Returns the ancestor of the block with the given `hash`, on the specified `height`, or /// `None` if the block's height is lower than that. - /// NOTE: Panics if used on non-proposal hashes. For those use [`find_ancestor_unit`]. - pub(crate) fn find_ancestor_proposal<'a>( + /// NOTE: Panics if used on non-proposal hashes. + pub fn find_ancestor_proposal<'a>( &'a self, hash: &'a C::Hash, height: u64, @@ -827,7 +828,7 @@ impl State { /// Returns the hash of the message with the given sequence number from the creator of `hash`, /// or `None` if the sequence number is higher than that of the unit with `hash`. - pub(crate) fn find_in_swimlane<'a>( + pub fn find_in_swimlane<'a>( &'a self, hash: &'a C::Hash, seq_number: u64, @@ -850,7 +851,7 @@ impl State { /// Returns an iterator over units (with hashes) by the same creator, in reverse chronological /// order, starting with the specified unit. Panics if no unit with `uhash` exists. - pub(crate) fn swimlane<'a>( + pub fn swimlane<'a>( &'a self, uhash: &'a C::Hash, ) -> impl Iterator)> { @@ -865,10 +866,7 @@ impl State { /// Returns an iterator over all hashes of ancestors of the block `bhash`, excluding `bhash` /// itself. Panics if `bhash` is not the hash of a known block. - pub(crate) fn ancestor_hashes<'a>( - &'a self, - bhash: &'a C::Hash, - ) -> impl Iterator { + pub fn ancestor_hashes<'a>(&'a self, bhash: &'a C::Hash) -> impl Iterator { let mut next = self.block(bhash).parent(); iter::from_fn(move || { let current = next?; @@ -884,7 +882,7 @@ impl State { } /// Returns the set of units (by hash) that are endorsed and seen from the panorama. - pub(crate) fn seen_endorsed(&self, pan: &Panorama) -> BTreeSet { + pub fn seen_endorsed(&self, pan: &Panorama) -> BTreeSet { if !ENABLE_ENDORSEMENTS { return Default::default(); }; @@ -1052,12 +1050,12 @@ impl State { /// Returns whether the unit with `hash0` sees the one with `hash1` (i.e. `hash0 ≥ hash1`), /// and sees `hash1`'s creator as correct. - pub(crate) fn sees_correct(&self, hash0: &C::Hash, hash1: &C::Hash) -> bool { + pub fn sees_correct(&self, hash0: &C::Hash, hash1: &C::Hash) -> bool { hash0 == hash1 || self.unit(hash0).panorama.sees_correct(self, hash1) } /// Returns whether the unit with `hash0` sees the one with `hash1` (i.e. `hash0 ≥ hash1`). - pub(crate) fn sees(&self, hash0: &C::Hash, hash1: &C::Hash) -> bool { + pub fn sees(&self, hash0: &C::Hash, hash1: &C::Hash) -> bool { hash0 == hash1 || self.unit(hash0).panorama.sees(self, hash1) } @@ -1069,11 +1067,7 @@ impl State { } /// Returns the panorama of the confirmation for the leader unit `uhash`. - pub(crate) fn confirmation_panorama( - &self, - creator: ValidatorIndex, - uhash: &C::Hash, - ) -> Panorama { + pub fn confirmation_panorama(&self, creator: ValidatorIndex, uhash: &C::Hash) -> Panorama { self.valid_panorama(creator, self.inclusive_panorama(uhash)) } @@ -1081,11 +1075,7 @@ impl State { /// to the given one. It is only modified if necessary for validity: /// * Cite `creator`'s previous unit, i.e. don't equivocate. /// * Satisfy the LNC, i.e. don't add new naively cited forks. - pub(crate) fn valid_panorama( - &self, - creator: ValidatorIndex, - mut pan: Panorama, - ) -> Panorama { + pub fn valid_panorama(&self, creator: ValidatorIndex, mut pan: Panorama) -> Panorama { // Make sure the panorama sees the creator's own previous unit. let maybe_prev_uhash = self.panorama()[creator].correct(); if let Some(prev_uhash) = maybe_prev_uhash { @@ -1116,7 +1106,7 @@ impl State { } /// Returns panorama of a unit where latest entry of the creator is that unit's hash. - pub(crate) fn inclusive_panorama(&self, uhash: &C::Hash) -> Panorama { + pub fn inclusive_panorama(&self, uhash: &C::Hash) -> Panorama { let unit = self.unit(uhash); let mut pan = unit.panorama.clone(); pan[unit.creator] = Observation::Correct(*uhash); diff --git a/node/src/components/consensus/highway_core/state/block.rs b/node/src/components/consensus/highway_core/state/block.rs index 67e9d06736..8e58fedd4f 100644 --- a/node/src/components/consensus/highway_core/state/block.rs +++ b/node/src/components/consensus/highway_core/state/block.rs @@ -1,24 +1,24 @@ use datasize::DataSize; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use super::State; use crate::components::consensus::traits::Context; /// A block: Chains of blocks are the consensus values in the CBC Casper sense. -#[derive(Clone, DataSize, Debug, Eq, PartialEq, Serialize)] -pub(crate) struct Block +#[derive(Clone, DataSize, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct Block where C: Context, { /// The total number of ancestors, i.e. the height in the blockchain. - pub(crate) height: u64, + pub height: u64, /// The payload, e.g. a list of transactions. - pub(crate) value: C::ConsensusValue, + pub value: C::ConsensusValue, /// A skip list index of the block's ancestors. /// /// For every `p = 1 << i` that divides `height`, this contains an `i`-th entry pointing to the /// ancestor with `height - p`. - pub(crate) skip_idx: Vec, + pub skip_idx: Vec, } impl Block { @@ -47,7 +47,7 @@ impl Block { } /// Returns the block's parent, or `None` if it has height 0. - pub(crate) fn parent(&self) -> Option<&C::Hash> { + pub fn parent(&self) -> Option<&C::Hash> { self.skip_idx.first() } diff --git a/node/src/components/consensus/highway_core/state/panorama.rs b/node/src/components/consensus/highway_core/state/panorama.rs index b1dd31b37c..9d4e7a0277 100644 --- a/node/src/components/consensus/highway_core/state/panorama.rs +++ b/node/src/components/consensus/highway_core/state/panorama.rs @@ -31,7 +31,7 @@ mod relaxed { deserialize = "C::Hash: Deserialize<'de>", ))] #[strum_discriminants(derive(strum::EnumIter))] - pub(crate) enum Observation + pub enum Observation where C: Context, { @@ -43,7 +43,7 @@ mod relaxed { Faulty, } } -pub(crate) use relaxed::{Observation, ObservationDiscriminants}; +pub use relaxed::{Observation, ObservationDiscriminants}; impl Debug for Observation where @@ -60,28 +60,31 @@ where impl Observation { /// Returns the unit hash, if this is a correct observation. - pub(crate) fn correct(&self) -> Option<&C::Hash> { + pub fn correct(&self) -> Option<&C::Hash> { match self { Self::None | Self::Faulty => None, Self::Correct(hash) => Some(hash), } } - pub(crate) fn is_correct(&self) -> bool { + /// Returns `true` if this `Observation` is an `Observation::Correct`. + pub fn is_correct(&self) -> bool { match self { Self::None | Self::Faulty => false, Self::Correct(_) => true, } } - pub(crate) fn is_faulty(&self) -> bool { + /// Returns `true` if this `Observation` is an `Observation::Faulty`. + pub fn is_faulty(&self) -> bool { match self { Self::Faulty => true, Self::None | Self::Correct(_) => false, } } - pub(crate) fn is_none(&self) -> bool { + /// Returns `true` if this `Observation` is an `Observation::None`. + pub fn is_none(&self) -> bool { match self { Self::None => true, Self::Faulty | Self::Correct(_) => false, @@ -110,7 +113,7 @@ impl Observation { } /// The observed behavior of all validators at some point in time. -pub(crate) type Panorama = ValidatorMap>; +pub type Panorama = ValidatorMap>; impl Panorama { /// Creates a new, empty panorama. @@ -119,33 +122,30 @@ impl Panorama { } /// Returns `true` if there is at least one correct observation. - pub(crate) fn has_correct(&self) -> bool { + pub fn has_correct(&self) -> bool { self.iter().any(Observation::is_correct) } /// Returns an iterator over all honest validators' latest units. - pub(crate) fn iter_correct<'a>( - &'a self, - state: &'a State, - ) -> impl Iterator> { + pub fn iter_correct<'a>(&'a self, state: &'a State) -> impl Iterator> { let to_unit = move |vh: &C::Hash| state.unit(vh); self.iter_correct_hashes().map(to_unit) } /// Returns an iterator over all honest validators' latest units' hashes. - pub(crate) fn iter_correct_hashes(&self) -> impl Iterator { + pub fn iter_correct_hashes(&self) -> impl Iterator { self.iter().filter_map(Observation::correct) } /// Returns an iterator over all faulty validators' indices. - pub(crate) fn iter_faulty(&self) -> impl Iterator + '_ { + pub fn iter_faulty(&self) -> impl Iterator + '_ { self.enumerate() .filter(|(_, obs)| obs.is_faulty()) .map(|(i, _)| i) } /// Returns an iterator over all faulty validators' indices. - pub(crate) fn iter_none(&self) -> impl Iterator + '_ { + pub fn iter_none(&self) -> impl Iterator + '_ { self.enumerate() .filter(|(_, obs)| obs.is_none()) .map(|(i, _)| i) @@ -160,7 +160,7 @@ impl Panorama { } /// Returns `true` if `self` sees the creator of `hash` as correct, and sees that unit. - pub(crate) fn sees_correct(&self, state: &State, hash: &C::Hash) -> bool { + pub fn sees_correct(&self, state: &State, hash: &C::Hash) -> bool { let unit = state.unit(hash); let can_see = |latest_hash: &C::Hash| { Some(hash) == state.find_in_swimlane(latest_hash, unit.seq_number) @@ -171,7 +171,7 @@ impl Panorama { } /// Returns `true` if `self` sees the unit with the specified `hash`. - pub(crate) fn sees(&self, state: &State, hash_to_be_found: &C::Hash) -> bool { + pub fn sees(&self, state: &State, hash_to_be_found: &C::Hash) -> bool { let unit_to_be_found = state.unit(hash_to_be_found); let mut visited = HashSet::new(); let mut to_visit: Vec<_> = self.iter_correct_hashes().collect(); @@ -208,7 +208,7 @@ impl Panorama { /// Returns the panorama seeing all units seen by `self` with a timestamp no later than /// `timestamp`. Accusations are preserved regardless of the evidence's timestamp. - pub(crate) fn cutoff(&self, state: &State, timestamp: Timestamp) -> Panorama { + pub fn cutoff(&self, state: &State, timestamp: Timestamp) -> Panorama { let obs_cutoff = |obs: &Observation| match obs { Observation::Correct(vhash) => state .swimlane(vhash) @@ -228,7 +228,7 @@ impl Panorama { /// Returns whether `self` can possibly come later in time than `other`, i.e. it can see /// every honest message and every fault seen by `other`. - pub(super) fn geq(&self, state: &State, other: &Panorama) -> bool { + pub fn geq(&self, state: &State, other: &Panorama) -> bool { let mut pairs_iter = self.iter().zip(other); pairs_iter.all(|(obs_self, obs_other)| obs_self.geq(state, obs_other)) } diff --git a/node/src/components/consensus/highway_core/state/params.rs b/node/src/components/consensus/highway_core/state/params.rs index 8a222c4195..a37beb1e51 100644 --- a/node/src/components/consensus/highway_core/state/params.rs +++ b/node/src/components/consensus/highway_core/state/params.rs @@ -1,11 +1,11 @@ use datasize::DataSize; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use super::{TimeDiff, Timestamp}; /// Protocol parameters for Highway. -#[derive(Debug, DataSize, Clone, Serialize)] -pub(crate) struct Params { +#[derive(Debug, DataSize, Clone, Serialize, Deserialize)] +pub struct Params { seed: u64, block_reward: u64, reduced_block_reward: u64, @@ -68,55 +68,55 @@ impl Params { } /// Returns the random seed. - pub(crate) fn seed(&self) -> u64 { + pub fn seed(&self) -> u64 { self.seed } /// Returns the total reward for a finalized block. - pub(crate) fn block_reward(&self) -> u64 { + pub fn block_reward(&self) -> u64 { self.block_reward } /// Returns the reduced block reward that is paid out even if the heaviest summit does not /// exceed half the total weight. This is at most `block_reward`. - pub(crate) fn reduced_block_reward(&self) -> u64 { + pub fn reduced_block_reward(&self) -> u64 { self.reduced_block_reward } /// Returns the minimum round length. This is always greater than 0. - pub(crate) fn min_round_length(&self) -> TimeDiff { + pub fn min_round_length(&self) -> TimeDiff { self.min_round_len } /// Returns the maximum round length. - pub(crate) fn max_round_length(&self) -> TimeDiff { + pub fn max_round_length(&self) -> TimeDiff { self.max_round_len } /// Returns the initial round length. - pub(crate) fn init_round_len(&self) -> TimeDiff { + pub fn init_round_len(&self) -> TimeDiff { self.init_round_len } /// Returns the minimum height of the last block. - pub(crate) fn end_height(&self) -> u64 { + pub fn end_height(&self) -> u64 { self.end_height } /// Returns the start timestamp of the era. - pub(crate) fn start_timestamp(&self) -> Timestamp { + pub fn start_timestamp(&self) -> Timestamp { self.start_timestamp } /// Returns the minimum timestamp of the last block. - pub(crate) fn end_timestamp(&self) -> Timestamp { + pub fn end_timestamp(&self) -> Timestamp { self.end_timestamp } /// Returns the maximum number of additional units included in evidence for conflicting /// endorsements. If you endorse two conflicting forks at sequence numbers that differ by more /// than this, you get away with it and are not marked faulty. - pub(crate) fn endorsement_evidence_limit(&self) -> u64 { + pub fn endorsement_evidence_limit(&self) -> u64 { self.endorsement_evidence_limit } } diff --git a/node/src/components/consensus/highway_core/state/unit.rs b/node/src/components/consensus/highway_core/state/unit.rs index 2dd0e05bd4..775c5cc4c4 100644 --- a/node/src/components/consensus/highway_core/state/unit.rs +++ b/node/src/components/consensus/highway_core/state/unit.rs @@ -1,7 +1,7 @@ use std::collections::BTreeSet; use datasize::DataSize; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use casper_types::{TimeDiff, Timestamp}; @@ -17,39 +17,39 @@ use crate::components::consensus::{ /// A unit sent to or received from the network. /// /// This is only instantiated when it gets added to a `State`, and only once it has been validated. -#[derive(Clone, DataSize, Debug, Eq, PartialEq, Serialize)] -pub(crate) struct Unit +#[derive(Clone, DataSize, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct Unit where C: Context, { /// The list of latest units and faults observed by the creator of this message. /// The panorama must be valid, and this unit's creator must not be marked as faulty. - pub(crate) panorama: Panorama, + pub panorama: Panorama, /// The number of earlier messages by the same creator. /// This must be `0` if the creator's entry in the panorama is `None`. Otherwise it must be /// the previous unit's sequence number plus one. - pub(crate) seq_number: u64, + pub seq_number: u64, /// The validator who created and sent this unit. - pub(crate) creator: ValidatorIndex, + pub creator: ValidatorIndex, /// The block this unit votes for. Either it or its parent must be the fork choice. - pub(crate) block: C::Hash, + pub block: C::Hash, /// A skip list index of the creator's swimlane, i.e. the previous unit by the same creator. /// /// For every `p = 1 << i` that divides `seq_number`, this contains an `i`-th entry pointing to /// the older unit with `seq_number - p`. - pub(crate) skip_idx: Vec, + pub skip_idx: Vec, /// This unit's timestamp, in milliseconds since the epoch. This must not be earlier than the /// timestamp of any unit cited in the panorama. - pub(crate) timestamp: Timestamp, + pub timestamp: Timestamp, /// Original signature of the `SignedWireUnit`. - pub(crate) signature: C::Signature, + pub signature: C::Signature, /// The length of the current round, that this message belongs to. /// /// All cited units by `creator` in the same round must have the same round length. - pub(crate) round_len: TimeDiff, + pub round_len: TimeDiff, /// Units that this one claims are endorsed. /// All of these must be cited (directly or indirectly) by the panorama. - pub(crate) endorsed: BTreeSet, + pub endorsed: BTreeSet, } impl Unit { @@ -101,17 +101,17 @@ impl Unit { } /// Returns the creator's previous unit. - pub(crate) fn previous(&self) -> Option<&C::Hash> { + pub fn previous(&self) -> Option<&C::Hash> { self.skip_idx.first() } /// Returns the time at which the round containing this unit began. - pub(crate) fn round_id(&self) -> Timestamp { + pub fn round_id(&self) -> Timestamp { state::round_id(self.timestamp, self.round_len) } /// Returns the length of the round containing this unit. - pub(crate) fn round_len(&self) -> TimeDiff { + pub fn round_len(&self) -> TimeDiff { self.round_len } @@ -120,7 +120,7 @@ impl Unit { /// /// NOTE: Returns `false` if `vidx` is faulty or hasn't produced any units according to the /// creator of `vhash`. - pub(crate) fn new_hash_obs(&self, state: &State, vidx: ValidatorIndex) -> bool { + pub fn new_hash_obs(&self, state: &State, vidx: ValidatorIndex) -> bool { let latest_obs = self.panorama[vidx].correct(); let penultimate_obs = self .previous() @@ -132,7 +132,7 @@ impl Unit { } /// Returns an iterator over units this one claims are endorsed. - pub(crate) fn claims_endorsed(&self) -> impl Iterator { + pub fn claims_endorsed(&self) -> impl Iterator { self.endorsed.iter() } } diff --git a/node/src/components/consensus/leader_sequence.rs b/node/src/components/consensus/leader_sequence.rs index 1b6f29df6b..c45b2bffb2 100644 --- a/node/src/components/consensus/leader_sequence.rs +++ b/node/src/components/consensus/leader_sequence.rs @@ -1,13 +1,13 @@ use datasize::DataSize; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use tracing::error; use crate::components::consensus::utils::{ValidatorIndex, ValidatorMap, Weight}; /// A pseudorandom sequence of validator indices, distributed by weight. -#[derive(Debug, Clone, DataSize, Serialize)] +#[derive(Debug, Clone, DataSize, Serialize, Deserialize)] pub(crate) struct LeaderSequence { /// Cumulative validator weights: Entry `i` contains the sum of the weights of validators `0` /// through `i`. diff --git a/node/src/components/consensus/protocols.rs b/node/src/components/consensus/protocols.rs index 1f47740d17..dfbf9a6498 100644 --- a/node/src/components/consensus/protocols.rs +++ b/node/src/components/consensus/protocols.rs @@ -1,3 +1,5 @@ -pub(crate) mod common; +//! Implementations of consensus protocols. + +pub mod common; pub(crate) mod highway; pub(crate) mod zug; diff --git a/node/src/components/consensus/protocols/common.rs b/node/src/components/consensus/protocols/common.rs index 4c8e597151..95f7f13e97 100644 --- a/node/src/components/consensus/protocols/common.rs +++ b/node/src/components/consensus/protocols/common.rs @@ -1,3 +1,5 @@ +//! Utilities common to different consensus algorithms. + use itertools::Itertools; use num_rational::Ratio; use std::collections::{BTreeMap, HashSet}; @@ -12,7 +14,7 @@ use casper_types::U512; /// Computes the validator set given the stakes and the faulty and inactive /// reports from the previous eras. -pub(crate) fn validators( +pub fn validators( faulty: &HashSet, inactive: &HashSet, validator_stakes: BTreeMap, diff --git a/node/src/components/consensus/utils.rs b/node/src/components/consensus/utils.rs index a202e259dc..1e71d7833f 100644 --- a/node/src/components/consensus/utils.rs +++ b/node/src/components/consensus/utils.rs @@ -1,5 +1,7 @@ +//! Various utilities relevant to consensus. + mod validators; mod weight; -pub(crate) use validators::{Validator, ValidatorIndex, ValidatorMap, Validators}; -pub(crate) use weight::Weight; +pub use validators::{Validator, ValidatorIndex, ValidatorMap, Validators}; +pub use weight::Weight; diff --git a/node/src/components/consensus/utils/validators.rs b/node/src/components/consensus/utils/validators.rs index 20debb0bdd..50b4175fcf 100644 --- a/node/src/components/consensus/utils/validators.rs +++ b/node/src/components/consensus/utils/validators.rs @@ -20,7 +20,7 @@ use crate::utils::ds; #[derive( Copy, Clone, DataSize, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize, )] -pub(crate) struct ValidatorIndex(pub(crate) u32); +pub struct ValidatorIndex(pub u32); impl From for ValidatorIndex { fn from(idx: u32) -> Self { @@ -30,7 +30,7 @@ impl From for ValidatorIndex { /// Information about a validator: their ID and weight. #[derive(Clone, DataSize, Debug, Eq, PartialEq)] -pub(crate) struct Validator { +pub struct Validator { weight: Weight, id: VID, banned: bool, @@ -49,18 +49,20 @@ impl> From<(VID, W)> for Validator { } impl Validator { - pub(crate) fn id(&self) -> &VID { + /// Returns the validator's ID. + pub fn id(&self) -> &VID { &self.id } - pub(crate) fn weight(&self) -> Weight { + /// Returns the validator's weight. + pub fn weight(&self) -> Weight { self.weight } } /// The validator IDs and weight map. #[derive(Debug, DataSize, Clone)] -pub(crate) struct Validators +pub struct Validators where VID: Eq + Hash, { @@ -70,39 +72,47 @@ where } impl Validators { - pub(crate) fn total_weight(&self) -> Weight { + /// Returns the total weight of the set of validators. + pub fn total_weight(&self) -> Weight { self.total_weight } /// Returns the weight of the validator with the given index. /// /// *Panics* if the validator index does not exist. - pub(crate) fn weight(&self, idx: ValidatorIndex) -> Weight { + pub fn weight(&self, idx: ValidatorIndex) -> Weight { self.validators[idx.0 as usize].weight } + /// Returns `true` if the map is empty. + pub fn is_empty(&self) -> bool { + self.validators.is_empty() + } + /// Returns the number of validators. - pub(crate) fn len(&self) -> usize { + pub fn len(&self) -> usize { self.validators.len() } - pub(crate) fn get_index(&self, id: &VID) -> Option { + /// Gets the index of a validator with the given ID. Returns `None` if no such validator is in + /// the set. + pub fn get_index(&self, id: &VID) -> Option { self.index_by_id.get(id).cloned() } /// Returns validator ID by index, or `None` if it doesn't exist. - pub(crate) fn id(&self, idx: ValidatorIndex) -> Option<&VID> { + pub fn id(&self, idx: ValidatorIndex) -> Option<&VID> { self.validators.get(idx.0 as usize).map(Validator::id) } /// Returns an iterator over all validators, sorted by ID. - pub(crate) fn iter(&self) -> impl Iterator> { + pub fn iter(&self) -> impl Iterator> { self.validators.iter() } /// Marks the validator with that ID as banned, if it exists, and excludes it from the leader /// sequence. - pub(crate) fn ban(&mut self, vid: &VID) { + pub fn ban(&mut self, vid: &VID) { if let Some(idx) = self.get_index(vid) { self.validators[idx.0 as usize].banned = true; self.validators[idx.0 as usize].can_propose = false; @@ -110,14 +120,14 @@ impl Validators { } /// Marks the validator as excluded from the leader sequence. - pub(crate) fn set_cannot_propose(&mut self, vid: &VID) { + pub fn set_cannot_propose(&mut self, vid: &VID) { if let Some(idx) = self.get_index(vid) { self.validators[idx.0 as usize].can_propose = false; } } /// Returns an iterator of all indices of banned validators. - pub(crate) fn iter_banned_idx(&self) -> impl Iterator + '_ { + pub fn iter_banned_idx(&self) -> impl Iterator + '_ { self.iter() .enumerate() .filter(|(_, v)| v.banned) @@ -125,14 +135,15 @@ impl Validators { } /// Returns an iterator of all indices of validators that are not allowed to propose values. - pub(crate) fn iter_cannot_propose_idx(&self) -> impl Iterator + '_ { + pub fn iter_cannot_propose_idx(&self) -> impl Iterator + '_ { self.iter() .enumerate() .filter(|(_, v)| !v.can_propose) .map(|(idx, _)| ValidatorIndex::from(idx as u32)) } - pub(crate) fn enumerate_ids<'a>(&'a self) -> impl Iterator { + /// Returns an iterator of pairs (validator index, validator ID). + pub fn enumerate_ids<'a>(&'a self) -> impl Iterator { let to_idx = |(idx, v): (usize, &'a Validator)| (ValidatorIndex::from(idx as u32), v.id()); self.iter().enumerate().map(to_idx) @@ -191,8 +202,9 @@ impl fmt::Display for Validators { } } +/// A map from the set of validators to some values. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, AsRef, From, Hash)] -pub(crate) struct ValidatorMap(Vec); +pub struct ValidatorMap(Vec); impl fmt::Display for ValidatorMap> where @@ -227,39 +239,44 @@ where impl ValidatorMap { /// Returns the value for the given validator, or `None` if the index is out of range. - pub(crate) fn get(&self, idx: ValidatorIndex) -> Option<&T> { + pub fn get(&self, idx: ValidatorIndex) -> Option<&T> { self.0.get(idx.0 as usize) } /// Returns the number of values. This must equal the number of validators. - pub(crate) fn len(&self) -> usize { + pub fn len(&self) -> usize { self.0.len() } + /// Returns `true` if this ValidatorMap is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + /// Returns an iterator over all values. - pub(crate) fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.0.iter() } /// Returns an iterator over mutable references to all values. - pub(crate) fn iter_mut(&mut self) -> impl Iterator { + pub fn iter_mut(&mut self) -> impl Iterator { self.0.iter_mut() } /// Returns an iterator over all values, by validator index. - pub(crate) fn enumerate(&self) -> impl Iterator { + pub fn enumerate(&self) -> impl Iterator { self.iter() .enumerate() .map(|(idx, value)| (ValidatorIndex(idx as u32), value)) } /// Returns `true` if `self` has an entry for validator number `idx`. - pub(crate) fn has(&self, idx: ValidatorIndex) -> bool { + pub fn has(&self, idx: ValidatorIndex) -> bool { self.0.len() > idx.0 as usize } /// Returns an iterator over all validator indices. - pub(crate) fn keys(&self) -> impl Iterator { + pub fn keys(&self) -> impl Iterator { (0..self.len()).map(|idx| ValidatorIndex(idx as u32)) } @@ -339,12 +356,12 @@ impl> Add> for ValidatorMa impl ValidatorMap> { /// Returns the keys of all validators whose value is `Some`. - pub(crate) fn keys_some(&self) -> impl Iterator + '_ { + pub fn keys_some(&self) -> impl Iterator + '_ { self.iter_some().map(|(vidx, _)| vidx) } /// Returns an iterator over all values that are present, together with their index. - pub(crate) fn iter_some(&self) -> impl Iterator + '_ { + pub fn iter_some(&self) -> impl Iterator + '_ { self.enumerate() .filter_map(|(vidx, opt)| opt.as_ref().map(|val| (vidx, val))) } diff --git a/node/src/components/consensus/utils/weight.rs b/node/src/components/consensus/utils/weight.rs index eb938d9816..6850c9941f 100644 --- a/node/src/components/consensus/utils/weight.rs +++ b/node/src/components/consensus/utils/weight.rs @@ -5,7 +5,7 @@ use std::{ use datasize::DataSize; use derive_more::{Add, AddAssign, From, Sub, SubAssign, Sum}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; /// A vote weight. #[derive( @@ -20,13 +20,14 @@ use serde::Serialize; Ord, Add, Serialize, + Deserialize, Sub, AddAssign, SubAssign, Sum, From, )] -pub(crate) struct Weight(pub(crate) u64); +pub struct Weight(pub u64); impl Weight { /// Checked addition. Returns `None` if overflow occurred. diff --git a/node/src/lib.rs b/node/src/lib.rs index d7938250d1..b4a5e7b21b 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -64,7 +64,7 @@ pub(crate) use components::{ upgrade_watcher::Config as UpgradeWatcherConfig, }; pub use components::{ - contract_runtime, + consensus, contract_runtime, rpc_server::rpcs, storage::{self, Config as StorageConfig}, }; diff --git a/node/src/types/block.rs b/node/src/types/block.rs index c570fe36e0..0c89f354f3 100644 --- a/node/src/types/block.rs +++ b/node/src/types/block.rs @@ -226,7 +226,7 @@ impl From for Error { #[derive( Clone, DataSize, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize, Default, )] -pub(crate) struct BlockPayload { +pub struct BlockPayload { deploys: Vec, transfers: Vec, accusations: Vec, diff --git a/utils/highway-rewards-analysis/Cargo.toml b/utils/highway-rewards-analysis/Cargo.toml new file mode 100644 index 0000000000..91d252b13a --- /dev/null +++ b/utils/highway-rewards-analysis/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "highway-rewards-analysis" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bincode = "1" +clap = { version = "4", features = ["derive"] } +casper-hashing = { path = "../../hashing" } +casper-node = { path = "../../node" } +casper-types = { path = "../../types" } +flate2 = "1" +serde = "1" diff --git a/utils/highway-rewards-analysis/README.md b/utils/highway-rewards-analysis/README.md new file mode 100644 index 0000000000..2f95e074df --- /dev/null +++ b/utils/highway-rewards-analysis/README.md @@ -0,0 +1,11 @@ +# Highway State Analyzer + +This tool analyzes a Highway protocol state dump and prints some information that may be helpful in explaining decreased reward amounts. + +Usage: `highway-rewards-analysis [-v] FILE` + +`FILE` should contain a Highway protocol state dump in the Bincode format, either plain or gzip-compressed. The `-v` flag causes the tool to print some additional information (see below). + +The tool prints out 10 nodes that missed the most rounds in the era contained in the dump, as well as 10 nodes with the lowest average maximum level-1 summit quorum. The reward for a given block for a node is proportional to the maximum quorum of a level-1 summit containing that node in the round in which it was proposed - such quora for all the rounds in the era are what is averaged in the latter statistic. + +If the `-v` flag is set, in addition to printing the 10 nodes with the most rounds missed, the tool also prints which rounds were missed by those nodes. diff --git a/utils/highway-rewards-analysis/src/main.rs b/utils/highway-rewards-analysis/src/main.rs new file mode 100644 index 0000000000..541c422d2e --- /dev/null +++ b/utils/highway-rewards-analysis/src/main.rs @@ -0,0 +1,201 @@ +use std::{ + collections::{BTreeMap, HashSet}, + fs::File, + io::Read, +}; + +use clap::Parser; +use flate2::read::GzDecoder; +use serde::{Deserialize, Serialize}; + +use casper_node::consensus::{ + highway_core::{ + finality_detector::{ + assigned_weight_and_latest_unit, find_max_quora, round_participation, + RoundParticipation, + }, + State, + }, + protocols::common::validators, + utils::Validators, + ClContext, +}; +use casper_types::{EraId, PublicKey, Timestamp, U512}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + filename: String, + #[arg(short, long)] + verbose: bool, +} + +/// Debug dump of era used for serialization. +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct EraDump { + /// The era that is being dumped. + pub id: EraId, + + /// The scheduled starting time of this era. + pub start_time: Timestamp, + /// The height of this era's first block. + pub start_height: u64, + + // omitted: pending blocks + /// Validators that have been faulty in any of the recent BONDED_ERAS switch blocks. This + /// includes `new_faulty`. + pub faulty: HashSet, + /// Validators that are excluded from proposing new blocks. + pub cannot_propose: HashSet, + /// Accusations collected in this era so far. + pub accusations: HashSet, + /// The validator weights. + pub validators: BTreeMap, + + /// The state of the highway instance associated with the era. + pub highway_state: State, +} + +fn main() { + let args = Args::parse(); + + let mut data = vec![]; + let mut file = File::open(&args.filename).unwrap(); + + if args.filename.ends_with(".gz") { + let mut gz = GzDecoder::new(file); + gz.read_to_end(&mut data).unwrap(); + } else { + file.read_to_end(&mut data).unwrap(); + } + + let dump: EraDump = bincode::deserialize(&data).unwrap(); + + let validators = + validators::(&dump.faulty, &dump.cannot_propose, dump.validators.clone()); + + print_faults(&validators, &dump.highway_state); + + print_skipped_rounds(&validators, &dump, args.verbose); + + print_lowest_quorum_participation(&validators, &dump); +} + +fn print_faults(validators: &Validators, state: &State) { + if state.faulty_validators().count() == 0 { + return; + } + + println!("Faults:"); + for vid in state.faulty_validators() { + let fault = state.maybe_fault(vid).unwrap(); + println!("{}: {:?}", validators.id(vid).unwrap(), fault); + } + println!(); +} + +const TOP_TO_PRINT: usize = 10; + +fn round_num(dump: &EraDump, round_id: Timestamp) -> u64 { + let min_round_length = dump.highway_state.params().min_round_length(); + (round_id.millis() - dump.start_time.millis()) / min_round_length.millis() +} + +fn print_skipped_rounds(validators: &Validators, dump: &EraDump, verbose: bool) { + let state = &dump.highway_state; + let highest_block = state.fork_choice(state.panorama()).unwrap(); + let all_blocks = std::iter::once(highest_block).chain(state.ancestor_hashes(highest_block)); + let mut skipped_rounds = vec![vec![]; validators.len()]; + + for block_hash in all_blocks { + let block_unit = state.unit(block_hash); + let round_id = block_unit.round_id(); + for i in 0..validators.len() as u32 { + let observation = state.panorama().get(i.into()).unwrap(); + let round_participation = round_participation(state, observation, round_id); + if matches!(round_participation, RoundParticipation::No) { + skipped_rounds[i as usize].push(round_id); + } + } + } + + for rounds in skipped_rounds.iter_mut() { + rounds.sort(); + } + + let mut num_skipped_rounds: Vec<_> = skipped_rounds + .iter() + .enumerate() + .map(|(i, skipped)| (i as u32, skipped.len())) + .collect(); + num_skipped_rounds.sort_by_key(|(_, len)| *len); + + println!("{} validators who skipped the most rounds:", TOP_TO_PRINT); + for (vid, len) in num_skipped_rounds.iter().rev().take(TOP_TO_PRINT) { + println!( + "{}: skipped {} rounds", + validators.id((*vid).into()).unwrap(), + len + ); + } + + if verbose { + println!(); + for (vid, _) in num_skipped_rounds.iter().rev().take(TOP_TO_PRINT) { + let skipped_rounds: Vec<_> = skipped_rounds[*vid as usize] + .iter() + .map(|rid| format!("{}", round_num(dump, *rid))) + .collect(); + println!( + "{} skipped rounds: {}", + validators.id((*vid).into()).unwrap(), + skipped_rounds.join(", ") + ); + } + } + + println!(); +} + +fn print_lowest_quorum_participation(validators: &Validators, dump: &EraDump) { + let state = &dump.highway_state; + let highest_block = state.fork_choice(state.panorama()).unwrap(); + let mut quora_sum = vec![0.0; validators.len()]; + let mut num_rounds = 0; + + let hb_unit = state.unit(highest_block); + for bhash in state.ancestor_hashes(highest_block) { + let proposal_unit = state.unit(bhash); + let r_id = proposal_unit.round_id(); + + let (assigned_weight, latest) = + assigned_weight_and_latest_unit(state, &hb_unit.panorama, r_id); + + let max_quora = find_max_quora(state, bhash, &latest); + let max_quora: Vec = max_quora + .into_iter() + .map(|quorum_w| quorum_w.0 as f32 / assigned_weight.0 as f32 * 100.0) + .collect(); + + for (q_sum, max_q) in quora_sum.iter_mut().zip(&max_quora) { + *q_sum += max_q; + } + num_rounds += 1; + } + + let mut quora_avg: Vec<_> = quora_sum + .into_iter() + .enumerate() + .map(|(vid, q_sum)| (vid as u32, q_sum / num_rounds as f32)) + .collect(); + quora_avg.sort_by(|(_, q_avg1), (_, q_avg2)| q_avg1.partial_cmp(q_avg2).unwrap()); + + println!("{} validators with lowest average max quora:", TOP_TO_PRINT); + for (vid, q_avg) in quora_avg.iter().take(TOP_TO_PRINT) { + println!( + "{}: average max quorum {:3.1}%", + validators.id((*vid).into()).unwrap(), + q_avg + ); + } +}