cuprate_types/hard_fork.rs
1//! The [`HardFork`] type.
2use std::time::Duration;
3
4use strum::{
5 AsRefStr, Display, EnumCount, EnumIs, EnumString, FromRepr, IntoStaticStr, VariantArray,
6};
7
8use monero_serai::block::BlockHeader;
9
10#[cfg(feature = "epee")]
11use cuprate_epee_encoding::{
12 error,
13 macros::bytes::{Buf, BufMut},
14 EpeeValue, Marker,
15};
16
17/// Target block time for hf 1.
18///
19/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/difficulty.html#target-seconds>
20const BLOCK_TIME_V1: Duration = Duration::from_secs(60);
21/// Target block time from v2.
22///
23/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/difficulty.html#target-seconds>
24const BLOCK_TIME_V2: Duration = Duration::from_secs(120);
25
26/// An error working with a [`HardFork`].
27#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)]
28pub enum HardForkError {
29 /// The raw-HF value is not a valid [`HardFork`].
30 #[error("The hard-fork is unknown")]
31 HardForkUnknown,
32 /// The [`HardFork`] version is incorrect.
33 #[error("The block is on an incorrect hard-fork")]
34 VersionIncorrect,
35 /// The block's [`HardFork`] vote was below the current [`HardFork`].
36 #[error("The block's vote is for a previous hard-fork")]
37 VoteTooLow,
38}
39
40/// An identifier for every hard-fork Monero has had.
41#[derive(
42 Default,
43 Debug,
44 PartialEq,
45 Eq,
46 PartialOrd,
47 Ord,
48 Copy,
49 Clone,
50 Hash,
51 EnumCount,
52 Display,
53 AsRefStr,
54 EnumIs,
55 EnumString,
56 FromRepr,
57 IntoStaticStr,
58 VariantArray,
59)]
60#[cfg_attr(any(feature = "proptest"), derive(proptest_derive::Arbitrary))]
61#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
62#[cfg_attr(feature = "serde", serde(try_from = "u8", into = "u8"))]
63#[repr(u8)]
64pub enum HardFork {
65 #[default]
66 V1 = 1,
67 V2,
68 V3,
69 V4,
70 V5,
71 V6,
72 V7,
73 V8,
74 V9,
75 V10,
76 V11,
77 V12,
78 V13,
79 V14,
80 V15,
81 V16,
82}
83
84impl HardFork {
85 /// The latest [`HardFork`].
86 ///
87 /// ```rust
88 /// # use cuprate_types::HardFork;
89 /// assert_eq!(HardFork::LATEST, HardFork::V16);
90 /// ```
91 pub const LATEST: Self = Self::VARIANTS[Self::COUNT - 1];
92
93 /// Returns the hard-fork for a blocks [`BlockHeader::hardfork_version`] field.
94 ///
95 /// ref: <https://monero-book.cuprate.org/consensus_rules/hardforks.html#blocks-version-and-vote>
96 ///
97 /// # Errors
98 /// Will return [`Err`] if the version is not a valid [`HardFork`].
99 ///
100 /// ```rust
101 /// # use cuprate_types::{HardFork, HardForkError};
102 /// # use strum::VariantArray;
103 /// assert_eq!(HardFork::from_version(0), Err(HardForkError::HardForkUnknown));
104 /// assert_eq!(HardFork::from_version(17), Err(HardForkError::HardForkUnknown));
105 ///
106 /// for (version, hf) in HardFork::VARIANTS.iter().enumerate() {
107 /// // +1 because enumerate starts at 0, hf starts at 1.
108 /// assert_eq!(*hf, HardFork::from_version(version as u8 + 1).unwrap());
109 /// }
110 /// ```
111 #[inline]
112 pub const fn from_version(version: u8) -> Result<Self, HardForkError> {
113 match Self::from_repr(version) {
114 Some(this) => Ok(this),
115 None => Err(HardForkError::HardForkUnknown),
116 }
117 }
118
119 /// Returns the hard-fork for a blocks [`BlockHeader::hardfork_signal`] (vote) field.
120 ///
121 /// <https://monero-book.cuprate.org/consensus_rules/hardforks.html#blocks-version-and-vote>
122 ///
123 /// ```rust
124 /// # use cuprate_types::{HardFork, HardForkError};
125 /// # use strum::VariantArray;
126 /// // 0 is interpreted as 1.
127 /// assert_eq!(HardFork::from_vote(0), HardFork::V1);
128 /// // Unknown defaults to `LATEST`.
129 /// assert_eq!(HardFork::from_vote(17), HardFork::V16);
130 ///
131 /// for (vote, hf) in HardFork::VARIANTS.iter().enumerate() {
132 /// // +1 because enumerate starts at 0, hf starts at 1.
133 /// assert_eq!(*hf, HardFork::from_vote(vote as u8 + 1));
134 /// }
135 /// ```
136 #[inline]
137 pub fn from_vote(vote: u8) -> Self {
138 if vote == 0 {
139 // A vote of 0 is interpreted as 1 as that's what Monero used to default to.
140 Self::V1
141 } else {
142 // This must default to the latest hard-fork!
143 Self::from_version(vote).unwrap_or(Self::LATEST)
144 }
145 }
146
147 /// Returns the [`HardFork`] version and vote from this block header.
148 ///
149 /// # Errors
150 /// Will return [`Err`] if the [`BlockHeader::hardfork_version`] is not a valid [`HardFork`].
151 #[inline]
152 pub fn from_block_header(header: &BlockHeader) -> Result<(Self, Self), HardForkError> {
153 Ok((
154 Self::from_version(header.hardfork_version)?,
155 Self::from_vote(header.hardfork_signal),
156 ))
157 }
158
159 /// Returns the raw hard-fork value, as it would appear in [`BlockHeader::hardfork_version`].
160 ///
161 /// ```rust
162 /// # use cuprate_types::{HardFork, HardForkError};
163 /// # use strum::VariantArray;
164 /// for (i, hf) in HardFork::VARIANTS.iter().enumerate() {
165 /// // +1 because enumerate starts at 0, hf starts at 1.
166 /// assert_eq!(hf.as_u8(), i as u8 + 1);
167 /// }
168 /// ```
169 pub const fn as_u8(self) -> u8 {
170 self as u8
171 }
172
173 /// Returns the next hard-fork.
174 pub fn next_fork(self) -> Option<Self> {
175 Self::from_version(self as u8 + 1).ok()
176 }
177
178 /// Returns the target block time for this hardfork.
179 ///
180 /// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/difficulty.html#target-seconds>
181 pub const fn block_time(self) -> Duration {
182 match self {
183 Self::V1 => BLOCK_TIME_V1,
184 _ => BLOCK_TIME_V2,
185 }
186 }
187
188 /// Returns `true` if `self` is [`Self::LATEST`].
189 ///
190 /// ```rust
191 /// # use cuprate_types::HardFork;
192 /// # use strum::VariantArray;
193 ///
194 /// for hf in HardFork::VARIANTS.iter() {
195 /// if *hf == HardFork::LATEST {
196 /// assert!(hf.is_latest());
197 /// } else {
198 /// assert!(!hf.is_latest());
199 /// }
200 /// }
201 /// ```
202 pub const fn is_latest(self) -> bool {
203 matches!(self, Self::LATEST)
204 }
205}
206
207impl TryFrom<u8> for HardFork {
208 type Error = HardForkError;
209 fn try_from(version: u8) -> Result<Self, Self::Error> {
210 Self::from_version(version)
211 }
212}
213
214impl From<HardFork> for u8 {
215 fn from(value: HardFork) -> Self {
216 value.as_u8()
217 }
218}
219
220#[cfg(feature = "epee")]
221impl EpeeValue for HardFork {
222 const MARKER: Marker = u8::MARKER;
223
224 fn read<B: Buf>(r: &mut B, marker: &Marker) -> error::Result<Self> {
225 let u = u8::read(r, marker)?;
226 Self::from_repr(u).ok_or(error::Error::Format("unknown hardfork"))
227 }
228
229 fn write<B: BufMut>(self, w: &mut B) -> error::Result<()> {
230 let u = self.as_u8();
231 u8::write(u, w)?;
232 Ok(())
233 }
234}