Skip to main content

amaru_kernel/cardano/
point.rs

1// Copyright 2025 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::{self, Debug, Display};
16
17use crate::{Hash, HeaderHash, ORIGIN_HASH, Slot, cbor, size::HEADER};
18
19#[derive(Default, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
20pub enum Point {
21    #[default]
22    Origin,
23    Specific(Slot, HeaderHash),
24}
25
26impl Point {
27    pub fn slot_or_default(&self) -> Slot {
28        match self {
29            Point::Origin => Slot::from(0),
30            Point::Specific(slot, _) => *slot,
31        }
32    }
33
34    pub fn hash(&self) -> HeaderHash {
35        match self {
36            // By convention, the hash of `Genesis` is all 0s.
37            Point::Origin => ORIGIN_HASH,
38            Point::Specific(_, header_hash) => *header_hash,
39        }
40    }
41}
42
43impl Debug for Point {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Point::Origin => write!(f, "Origin"),
47            Point::Specific(slot, _hash) => write!(f, "Specific({slot}, {})", self.hash()),
48        }
49    }
50}
51
52impl Display for Point {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        write!(f, "{}.{}", self.slot_or_default(), self.hash())
55    }
56}
57
58impl From<&Point> for HeaderHash {
59    fn from(point: &Point) -> Self {
60        point.hash()
61    }
62}
63
64/// Utility function to parse a point from a string.
65///
66/// Expects the input to be of the form `<point>.<hash>`, where `<point>` is a number and `<hash>`
67/// is a hex-encoded 32 bytes hash.
68/// The first argument is the string to parse, the `bail` function is user to
69/// produce the error type `E` in case of failure to parse.
70impl TryFrom<&str> for Point {
71    type Error = String;
72
73    fn try_from(raw_str: &str) -> Result<Self, Self::Error> {
74        let mut split = raw_str.split('.');
75
76        let slot = split
77            .next()
78            .ok_or("missing slot number before '.'")
79            .and_then(|s| s.parse::<u64>().map_err(|_| "failed to parse point's slot as a non-negative integer"))?;
80
81        let block_header_hash = split
82            .next()
83            .ok_or("missing block header hash after '.'".to_string())
84            .and_then(|s| s.parse::<HeaderHash>().map_err(|e| format!("failed to parse block header hash: {}", e)))?;
85
86        Ok(Point::Specific(Slot::from(slot), block_header_hash))
87    }
88}
89
90impl cbor::encode::Encode<()> for Point {
91    fn encode<W: cbor::encode::Write>(
92        &self,
93        e: &mut cbor::encode::Encoder<W>,
94        _ctx: &mut (),
95    ) -> Result<(), cbor::encode::Error<W::Error>> {
96        match self {
97            Point::Origin => e.array(0)?,
98            Point::Specific(slot, hash) => e.array(2)?.encode(slot)?.encode(hash)?,
99        };
100
101        Ok(())
102    }
103}
104
105impl<'b> cbor::decode::Decode<'b, ()> for Point {
106    fn decode(d: &mut cbor::decode::Decoder<'b>, _ctx: &mut ()) -> Result<Self, cbor::decode::Error> {
107        let size = d.array()?;
108
109        match size {
110            Some(0) => Ok(Point::Origin),
111            Some(2) => {
112                let slot = d.decode()?;
113                let hash = d.bytes()?;
114                if hash.len() != HEADER {
115                    return Err(cbor::decode::Error::message("header hash must be 32 bytes"));
116                }
117                Ok(Point::Specific(slot, Hash::from(hash)))
118            }
119            _ => Err(cbor::decode::Error::message("can't decode Point from array of size")),
120        }
121    }
122}
123
124#[cfg(any(test, feature = "test-utils"))]
125pub use tests::*;
126
127#[cfg(any(test, feature = "test-utils"))]
128mod tests {
129    use proptest::prelude::*;
130
131    use crate::{Point, Slot, any_header_hash, prop_cbor_roundtrip};
132
133    prop_cbor_roundtrip!(Point, any_point());
134
135    prop_compose! {
136        fn any_slot()(n in 0u64..=1000) -> Slot {
137            Slot::from(n)
138        }
139    }
140
141    prop_compose! {
142        pub fn any_specific_point()(slot in any_slot(), header_hash in any_header_hash()) -> Point {
143            Point::Specific(slot, header_hash)
144        }
145    }
146
147    pub fn any_point() -> impl Strategy<Value = Point> {
148        prop_oneof![
149            1 => Just(Point::Origin),
150            3 => any_specific_point(),
151        ]
152    }
153
154    #[cfg(test)]
155    mod internal {
156        use test_case::test_case;
157
158        use super::*;
159        use crate::Hash;
160
161        #[test_case(Point::Origin => "Origin")]
162        #[test_case(
163            Point::Specific(
164                Slot::from(42),
165                Hash::new([
166                  254, 252, 156,   3, 124,  63, 156, 139,
167                   79, 183, 138, 155,  15,  19, 123,  94,
168                  208, 128,  60,  61,  70, 189,  45,  14,
169                   64, 197, 159, 169,  12, 160,   2, 193
170                ])
171            ) => "Specific(42, fefc9c037c3f9c8b4fb78a9b0f137b5ed0803c3d46bd2d0e40c59fa90ca002c1)";
172            "specific"
173        )]
174        fn better_debug_point(point: Point) -> String {
175            format!("{point:?}")
176        }
177
178        #[test_case(
179            Point::Origin => "0.0000000000000000000000000000000000000000000000000000000000000000";
180           "origin"
181        )]
182        #[test_case(
183            Point::Specific(
184                Slot::from(42),
185                Hash::new([
186                  254, 252, 156,   3, 124,  63, 156, 139,
187                   79, 183, 138, 155,  15,  19, 123,  94,
188                  208, 128,  60,  61,  70, 189,  45,  14,
189                   64, 197, 159, 169,  12, 160,   2, 193
190                ])
191            ) => "42.fefc9c037c3f9c8b4fb78a9b0f137b5ed0803c3d46bd2d0e40c59fa90ca002c1";
192            "specific"
193        )]
194        fn better_display_point(point: Point) -> String {
195            format!("{point}")
196        }
197
198        #[test]
199        fn test_parse_point() {
200            let error = Point::try_from("42.0123456789abcdef").unwrap_err();
201            assert_eq!(error, "failed to parse block header hash: Invalid string length");
202        }
203
204        #[test]
205        fn test_parse_real_point() {
206            let point =
207                Point::try_from("70070379.d6fe6439aed8bddc10eec22c1575bf0648e4a76125387d9e985e9a3f8342870d").unwrap();
208            match point {
209                Point::Specific(slot, _hash) => {
210                    assert_eq!(70070379, slot.as_u64());
211                }
212                _ => panic!("expected a specific point"),
213            }
214        }
215    }
216}