Skip to main content

dpp/block/epoch/
mod.rs

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