Skip to main content

amaru_kernel/cardano/
era_name.rs

1// Copyright 2026 PRAGMA
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{fmt, str::FromStr};
16
17use crate::cbor;
18
19#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, Hash)]
20#[repr(u8)]
21pub enum EraName {
22    Byron = 1,
23    Shelley = 2,
24    Allegra = 3,
25    Mary = 4,
26    Alonzo = 5,
27    Babbage = 6,
28    Conway = 7,
29    Dijkstra = 8,
30}
31
32pub const ERA_NAMES: [EraName; 8] = [
33    EraName::Byron,
34    EraName::Shelley,
35    EraName::Allegra,
36    EraName::Mary,
37    EraName::Alonzo,
38    EraName::Babbage,
39    EraName::Conway,
40    EraName::Dijkstra,
41];
42
43const ERA_STRINGS: [&str; 8] = ["Byron", "Shelley", "Allegra", "Mary", "Alonzo", "Babbage", "Conway", "Dijkstra"];
44
45impl EraName {
46    pub const fn is_tagged_on_network(self) -> bool {
47        !matches!(self, EraName::Byron)
48    }
49
50    pub const fn as_str(self) -> &'static str {
51        ERA_STRINGS[self as usize - 1]
52    }
53
54    pub const fn header_variant(self) -> u8 {
55        (self as u8) - 1
56    }
57
58    pub const fn from_header_variant(variant: u8) -> Result<EraName, EraNameError> {
59        // see https://github.com/IntersectMBO/ouroboros-consensus/blob/bf6ade5fea033dc5d38a02a22d2370fcbc18a3fe/ouroboros-consensus-cardano/cddl/base.cddl
60        // in conjunction with
61        // https://github.com/IntersectMBO/ouroboros-consensus/blob/bf6ade5fea033dc5d38a02a22d2370fcbc18a3fe/ouroboros-consensus-cardano/cddl/node-to-node/chainsync/header.cddl
62        match variant {
63            0 => Ok(EraName::Byron),
64            1 => Ok(EraName::Shelley),
65            2 => Ok(EraName::Allegra),
66            3 => Ok(EraName::Mary),
67            4 => Ok(EraName::Alonzo),
68            5 => Ok(EraName::Babbage),
69            6 => Ok(EraName::Conway),
70            7 => Ok(EraName::Dijkstra),
71            _ => Err(EraNameError::InvalidEraTag(variant)),
72        }
73    }
74}
75
76impl fmt::Display for EraName {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        write!(f, "{:?}", self)
79    }
80}
81
82impl FromStr for EraName {
83    type Err = EraNameError;
84
85    fn from_str(s: &str) -> Result<Self, Self::Err> {
86        let (_, era_name) = ERA_STRINGS
87            .into_iter()
88            .zip(ERA_NAMES)
89            .find(|(era_str, _)| era_str == &s)
90            .ok_or(EraNameError::InvalidEraName(s.to_string()))?;
91        Ok(era_name)
92    }
93}
94
95impl From<EraName> for u8 {
96    fn from(value: EraName) -> Self {
97        value as u8
98    }
99}
100
101#[derive(Debug, thiserror::Error, PartialEq, Eq)]
102pub enum EraNameError {
103    #[error("Invalid era name: {0}")]
104    InvalidEraName(String),
105    #[error("Invalid era tag: {0}")]
106    InvalidEraTag(u8),
107}
108
109impl TryFrom<u8> for EraName {
110    type Error = EraNameError;
111
112    fn try_from(value: u8) -> Result<Self, Self::Error> {
113        let ix = value as usize;
114        if ix == 0 || ix > ERA_NAMES.len() {
115            return Err(EraNameError::InvalidEraTag(value));
116        }
117        Ok(ERA_NAMES[ix - 1])
118    }
119}
120
121impl<'de> serde::Deserialize<'de> for EraName {
122    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
123    where
124        D: serde::Deserializer<'de>,
125    {
126        if deserializer.is_human_readable() {
127            let s = String::deserialize(deserializer)?;
128            Self::from_str(&s).map_err(serde::de::Error::custom)
129        } else {
130            let value = u8::deserialize(deserializer)?;
131            Self::try_from(value).map_err(serde::de::Error::custom)
132        }
133    }
134}
135
136impl serde::Serialize for EraName {
137    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
138    where
139        S: serde::Serializer,
140    {
141        if serializer.is_human_readable() {
142            serializer.serialize_str(self.as_str())
143        } else {
144            serializer.serialize_u8(*self as u8)
145        }
146    }
147}
148
149impl<C> cbor::Encode<C> for EraName {
150    fn encode<W: cbor::encode::Write>(
151        &self,
152        e: &mut cbor::Encoder<W>,
153        _ctx: &mut C,
154    ) -> Result<(), cbor::encode::Error<W::Error>> {
155        e.u8(*self as u8)?;
156        Ok(())
157    }
158}
159
160impl<'b, C> cbor::Decode<'b, C> for EraName {
161    fn decode(d: &mut cbor::Decoder<'b>, _ctx: &mut C) -> Result<Self, cbor::decode::Error> {
162        let value = d.u8()?;
163        EraName::try_from(value).map_err(cbor::decode::Error::message)
164    }
165}
166
167#[cfg(any(test, feature = "test-utils"))]
168pub fn any_era_name() -> impl proptest::prelude::Strategy<Value = EraName> {
169    proptest::sample::select(&ERA_NAMES)
170}
171
172#[cfg(test)]
173mod tests {
174    use std::str::FromStr;
175
176    use proptest::prelude::*;
177
178    use super::*;
179    use crate::to_cbor;
180
181    proptest! {
182        #[test]
183        fn era_name_string_roundtrip(era_name in any_era_name()) {
184            let string = format!("{}", era_name);
185            assert_eq!(EraName::from_str(&string), Ok(era_name));
186        }
187
188        #[test]
189        fn era_name_debug_roundtrip(era_name in any_era_name()) {
190            let string = format!("{:?}", era_name);
191            assert_eq!(EraName::from_str(&string), Ok(era_name));
192        }
193
194        #[test]
195        fn era_name_encode_decode_roundtrip(era_name in any_era_name()) {
196            let buffer = cbor::to_vec(era_name).unwrap();
197            let decoded: EraName = cbor::decode(&buffer).unwrap();
198            assert_eq!(era_name, decoded);
199        }
200
201        #[test]
202        fn era_name_serde_string_roundtrip(era_name in any_era_name()) {
203            let string = serde_json::to_string(&era_name).unwrap();
204            let decoded: EraName = serde_json::from_str(&string).unwrap();
205            assert_eq!(era_name, decoded);
206        }
207
208        #[test]
209        fn era_name_serde_binary_roundtrip(era_name in any_era_name()) {
210            let bytes = cbor4ii::serde::to_vec(Vec::new(), &era_name).unwrap();
211            let decoded: EraName = cbor4ii::serde::from_slice(&bytes).unwrap();
212            assert_eq!(era_name, decoded);
213        }
214    }
215
216    #[test]
217    fn cbor_encoding() {
218        let cbor = to_cbor(&ERA_NAMES);
219        assert_eq!(cbor, &[0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
220    }
221
222    #[test]
223    fn serde_string_encoding() {
224        let string = serde_json::to_string(&ERA_NAMES).unwrap();
225        assert_eq!(string, r#"["Byron","Shelley","Allegra","Mary","Alonzo","Babbage","Conway","Dijkstra"]"#);
226    }
227
228    #[test]
229    fn serde_binary_encoding() {
230        let bytes = cbor4ii::serde::to_vec(Vec::new(), &ERA_NAMES).unwrap();
231        assert_eq!(bytes, &[0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
232    }
233}