1use 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 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
64impl 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}