Skip to main content

hyli_model/node/
consensus.rs

1use std::fmt::Display;
2
3use borsh::{BorshDeserialize, BorshSerialize};
4use serde::{Deserialize, Serialize};
5use sha3::{Digest, Sha3_256};
6use utils::TimestampMs;
7use utoipa::ToSchema;
8
9use crate::{staking::*, *};
10
11pub type Slot = u64;
12pub type View = u64;
13
14#[derive(Clone, Serialize, Deserialize, ToSchema, Debug)]
15pub struct ConsensusInfo {
16    pub slot: Slot,
17    pub view: View,
18    pub round_leader: ValidatorPublicKey,
19    pub last_timestamp: TimestampMs,
20    pub validators: Vec<ValidatorPublicKey>,
21}
22
23// -----------------------------
24// --- Consensus data model ----
25// -----------------------------
26
27#[derive(
28    Debug,
29    Serialize,
30    Deserialize,
31    Clone,
32    BorshSerialize,
33    BorshDeserialize,
34    PartialEq,
35    Eq,
36    Hash,
37    Ord,
38    PartialOrd,
39)]
40pub struct ValidatorCandidacy {
41    // No need to store a specific pubkeey as it will be part of the signature of this message.
42    pub peer_address: String,
43}
44
45impl Display for SignedByValidator<ValidatorCandidacy> {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        write!(f, "Pubkey: {}", self.signature.validator)
48    }
49}
50
51#[derive(
52    Debug,
53    Clone,
54    Default,
55    Serialize,
56    Deserialize,
57    BorshSerialize,
58    BorshDeserialize,
59    PartialEq,
60    Eq,
61    Ord,
62    PartialOrd,
63)]
64pub struct ConsensusProposal {
65    pub slot: Slot,
66    pub parent_hash: ConsensusProposalHash,
67    pub cut: Cut,
68    pub staking_actions: Vec<ConsensusStakingAction>,
69    pub timestamp: TimestampMs,
70}
71
72/// This is the hash of the proposal, signed by validators
73/// Any consensus-critical data should be hashed here.
74impl Hashed<ConsensusProposalHash> for ConsensusProposal {
75    fn hashed(&self) -> ConsensusProposalHash {
76        let mut hasher = Sha3_256::new();
77        hasher.update(self.slot.to_le_bytes());
78        self.cut.iter().for_each(|(lane_id, hash, _, _)| {
79            lane_id.update_hasher(&mut hasher);
80            hasher.update(&hash.0);
81        });
82        self.staking_actions.iter().for_each(|val| match val {
83            ConsensusStakingAction::Bond { candidate } => {
84                hasher.update(&candidate.signature.validator.0)
85            }
86            ConsensusStakingAction::PayFeesForDaDi {
87                lane_id,
88                cumul_size,
89            } => {
90                lane_id.update_hasher(&mut hasher);
91                hasher.update(cumul_size.0.to_le_bytes())
92            }
93        });
94        hasher.update(self.timestamp.0.to_le_bytes());
95        hasher.update(&self.parent_hash.0);
96        ConsensusProposalHash(hasher.finalize().to_vec())
97    }
98}
99
100impl std::hash::Hash for ConsensusProposal {
101    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
102        Hashed::<ConsensusProposalHash>::hashed(self).hash(state);
103    }
104}
105
106impl Display for ConsensusProposal {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        write!(
109            f,
110            "Hash: {}, Parent Hash: {}, Slot: {}, Cut: {}, staking_actions: {:?}",
111            self.hashed(),
112            self.parent_hash,
113            self.slot,
114            CutDisplay(&self.cut),
115            self.staking_actions,
116        )
117    }
118}
119
120impl Display for ConsensusProposalHash {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        write!(f, "{}", hex::encode(&self.0))
123    }
124}
125
126/// Represents the operations that can be performed by the consensus
127#[derive(
128    BorshSerialize,
129    BorshDeserialize,
130    Debug,
131    Clone,
132    Deserialize,
133    Serialize,
134    PartialEq,
135    Eq,
136    Ord,
137    PartialOrd,
138)]
139pub enum ConsensusStakingAction {
140    /// Bonding a new validator candidate
141    Bond {
142        // Boxed to reduce size of the enum
143        // cf https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant
144        candidate: Box<SignedByValidator<ValidatorCandidacy>>,
145    },
146
147    /// DaDi = Data Dissemination
148    PayFeesForDaDi {
149        lane_id: LaneId,
150        cumul_size: LaneBytesSize,
151    },
152}
153
154impl From<SignedByValidator<ValidatorCandidacy>> for ConsensusStakingAction {
155    fn from(val: SignedByValidator<ValidatorCandidacy>) -> Self {
156        ConsensusStakingAction::Bond {
157            candidate: Box::new(val),
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164
165    #[test]
166    fn test_consensus_proposal_hash() {
167        use super::*;
168        let proposal = ConsensusProposal {
169            slot: 1,
170            cut: Cut::default(),
171            staking_actions: vec![],
172            timestamp: TimestampMs(1),
173            parent_hash: b"".into(),
174        };
175        let hash = proposal.hashed();
176        assert_eq!(hash.0.len(), 32);
177    }
178    #[test]
179    fn test_consensus_proposal_hash_all_items() {
180        use super::*;
181        let mut a = ConsensusProposal {
182            slot: 1,
183            cut: vec![(
184                LaneId::new(ValidatorPublicKey(vec![1])),
185                b"propA".into(),
186                LaneBytesSize(1),
187                AggregateSignature::default(),
188            )],
189            staking_actions: vec![SignedByValidator::<ValidatorCandidacy> {
190                msg: ValidatorCandidacy {
191                    peer_address: "peer".to_string(),
192                },
193                signature: ValidatorSignature {
194                    signature: Signature(vec![1, 2, 3]),
195                    validator: ValidatorPublicKey(vec![1, 2, 3]),
196                },
197            }
198            .into()],
199            timestamp: TimestampMs(1),
200            parent_hash: b"parent".into(),
201        };
202        let mut b = ConsensusProposal {
203            slot: 1,
204            cut: vec![(
205                LaneId::new(ValidatorPublicKey(vec![1])),
206                b"propA".into(),
207                LaneBytesSize(1),
208                AggregateSignature {
209                    signature: Signature(vec![1, 2, 3]),
210                    validators: vec![ValidatorPublicKey(vec![1, 2, 3])],
211                },
212            )],
213            staking_actions: vec![SignedByValidator::<ValidatorCandidacy> {
214                msg: ValidatorCandidacy {
215                    peer_address: "different".to_string(),
216                },
217                signature: ValidatorSignature {
218                    signature: Signature(vec![3, 4, 5]),
219                    validator: ValidatorPublicKey(vec![3, 4, 5]),
220                },
221            }
222            .into()],
223            timestamp: TimestampMs(1),
224            parent_hash: b"parent".into(),
225        };
226        assert_ne!(a.hashed(), b.hashed());
227        if let ConsensusStakingAction::Bond { candidate: a } =
228            b.staking_actions.first_mut().unwrap()
229        {
230            a.msg.peer_address = "peer".to_string()
231        }
232        assert_ne!(a.hashed(), b.hashed());
233        if let ConsensusStakingAction::Bond { candidate: a } =
234            b.staking_actions.first_mut().unwrap()
235        {
236            a.signature = ValidatorSignature {
237                signature: Signature(vec![1, 2, 3]),
238                validator: ValidatorPublicKey(vec![1, 2, 3]),
239            };
240        }
241        assert_eq!(a.hashed(), b.hashed());
242        a.timestamp = TimestampMs(2);
243        assert_ne!(a.hashed(), b.hashed());
244        b.timestamp = TimestampMs(2);
245        assert_eq!(a.hashed(), b.hashed());
246
247        a.slot = 2;
248        assert_ne!(a.hashed(), b.hashed());
249        b.slot = 2;
250        assert_eq!(a.hashed(), b.hashed());
251
252        a.parent_hash = b"different".into();
253        assert_ne!(a.hashed(), b.hashed());
254        b.parent_hash = b"different".into();
255        assert_eq!(a.hashed(), b.hashed());
256    }
257}