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}