Skip to main content

dpp/block/epoch/
mod.rs

1use crate::{InvalidVectorSizeError, ProtocolError};
2use bincode::{BorrowDecode, Decode, Encode};
3use serde::{Deserialize, Serialize};
4
5/// Epoch key offset
6pub const EPOCH_KEY_OFFSET: u16 = 256;
7
8/// The Highest allowed Epoch
9pub const MAX_EPOCH: u16 = u16::MAX - EPOCH_KEY_OFFSET;
10
11/// Epoch index type
12pub type EpochIndex = u16;
13
14pub const EPOCH_0: Epoch = Epoch {
15    index: 0,
16    key: [1, 0],
17};
18
19// We make this immutable because it should never be changed or updated
20// @immutable
21/// Epoch struct
22#[derive(Serialize, Clone, Eq, PartialEq, Copy, Debug)]
23#[serde(rename_all = "camelCase")]
24pub struct Epoch {
25    /// Epoch index
26    pub index: EpochIndex,
27
28    /// Key
29    #[serde(skip)]
30    pub key: [u8; 2],
31}
32
33impl Default for Epoch {
34    fn default() -> Self {
35        Self::new(0).unwrap()
36    }
37}
38
39impl Epoch {
40    /// Create new epoch
41    pub fn new(index: EpochIndex) -> Result<Self, ProtocolError> {
42        let index_with_offset = index
43            .checked_add(EPOCH_KEY_OFFSET)
44            .ok_or(ProtocolError::Overflow("stored epoch index too high"))?;
45        Ok(Self {
46            index,
47            key: index_with_offset.to_be_bytes(),
48        })
49    }
50}
51
52impl TryFrom<EpochIndex> for Epoch {
53    type Error = ProtocolError;
54
55    fn try_from(value: EpochIndex) -> Result<Self, Self::Error> {
56        Self::new(value)
57    }
58}
59
60impl TryFrom<&Vec<u8>> for Epoch {
61    type Error = ProtocolError;
62
63    fn try_from(value: &Vec<u8>) -> Result<Self, Self::Error> {
64        let key = value.clone().try_into().map_err(|_| {
65            ProtocolError::InvalidVectorSizeError(InvalidVectorSizeError::new(2, value.len()))
66        })?;
67        let index_with_offset = u16::from_be_bytes(key);
68        let index = index_with_offset
69            .checked_sub(EPOCH_KEY_OFFSET)
70            .ok_or(ProtocolError::Overflow("value too low, must have offset"))?;
71        Ok(Epoch { index, key })
72    }
73}
74
75impl Encode for Epoch {
76    fn encode<E: bincode::enc::Encoder>(
77        &self,
78        encoder: &mut E,
79    ) -> Result<(), bincode::error::EncodeError> {
80        self.index.encode(encoder)
81    }
82}
83
84// Manual Deserialize (Serialize stays derived with `serde(skip)` on `key`):
85// the `key` field is derived from `index`, so deserialization must recompute
86// it via `Epoch::new` to preserve the invariant rather than trusting wire
87// input. Not a wire-shape customization — the shape matches the derive.
88impl<'de> Deserialize<'de> for Epoch {
89    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
90    where
91        D: serde::Deserializer<'de>,
92    {
93        #[derive(Deserialize)]
94        struct EpochData {
95            index: EpochIndex,
96        }
97
98        let data = EpochData::deserialize(deserializer)?;
99        Epoch::new(data.index).map_err(serde::de::Error::custom)
100    }
101}
102
103impl<C> Decode<C> for Epoch {
104    fn decode<D: bincode::de::Decoder<Context = C>>(
105        decoder: &mut D,
106    ) -> Result<Self, bincode::error::DecodeError> {
107        let index = EpochIndex::decode(decoder)?;
108        Epoch::new(index).map_err(|e| bincode::error::DecodeError::OtherString(e.to_string()))
109    }
110}
111
112impl<'de, C> BorrowDecode<'de, C> for Epoch {
113    fn borrow_decode<D: bincode::de::BorrowDecoder<'de, Context = C>>(
114        decoder: &mut D,
115    ) -> Result<Self, bincode::error::DecodeError> {
116        let index = EpochIndex::borrow_decode(decoder)?;
117        Epoch::new(index).map_err(|e| bincode::error::DecodeError::OtherString(e.to_string()))
118    }
119}
120
121// --- canonical conversion trait impls (unification pass 1) ---
122#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
123impl crate::serialization::JsonConvertible for Epoch {}
124
125#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
126impl crate::serialization::ValueConvertible for Epoch {}
127
128#[cfg(all(
129    test,
130    feature = "json-conversion",
131    feature = "value-conversion",
132    feature = "serde-conversion"
133))]
134mod json_convertible_tests_epoch {
135    use super::*;
136    use platform_value::platform_value;
137    use serde_json::json;
138
139    fn fixture() -> Epoch {
140        Epoch::new(7).expect("epoch")
141    }
142
143    #[test]
144    fn json_round_trip_with_full_wire_shape() {
145        use crate::serialization::JsonConvertible;
146        let original = fixture();
147        let json = original.to_json().expect("to_json");
148        // `key` is `#[serde(skip)]` and reconstructed from `index` on deserialize.
149        // Only `index` appears on the wire. JSON erases the u16 distinction —
150        // the value-path assertion below uses `7u16` to lock in the typed variant.
151        assert_eq!(json, json!({"index": 7}));
152        let recovered = Epoch::from_json(json).expect("from_json");
153        assert_eq!(original, recovered);
154    }
155
156    #[test]
157    fn value_round_trip_with_full_wire_shape() {
158        use crate::serialization::ValueConvertible;
159        let original = fixture();
160        let value = original.to_object().expect("to_object");
161        // `index` is `EpochIndex` (u16) → `Value::U16`.
162        assert_eq!(value, platform_value!({"index": 7u16}));
163        let recovered = Epoch::from_object(value).expect("from_object");
164        assert_eq!(original, recovered);
165    }
166}