Skip to main content

hns_primitives/
lib.rs

1#![doc = "Strongly typed, allocation-free Handshake protocol values."]
2
3use core::{cmp::Ordering, fmt};
4
5use thiserror::Error;
6
7macro_rules! semantic_bytes {
8    ($name:ident, $size:expr) => {
9        #[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
10        pub struct $name([u8; $size]);
11
12        impl Default for $name {
13            fn default() -> Self {
14                Self([0; $size])
15            }
16        }
17
18        impl $name {
19            pub const LENGTH: usize = $size;
20
21            pub const fn new(bytes: [u8; $size]) -> Self {
22                Self(bytes)
23            }
24
25            pub const fn into_bytes(self) -> [u8; $size] {
26                self.0
27            }
28
29            pub const fn as_bytes(&self) -> &[u8; $size] {
30                &self.0
31            }
32
33            pub fn from_hex(value: &str) -> Result<Self, HexValueError> {
34                let mut bytes = [0_u8; $size];
35                hex::decode_to_slice(value, &mut bytes).map_err(|_| HexValueError::InvalidHex {
36                    expected_bytes: $size,
37                })?;
38                Ok(Self(bytes))
39            }
40        }
41
42        impl fmt::Debug for $name {
43            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44                write!(formatter, "{}({})", stringify!($name), hex::encode(self.0))
45            }
46        }
47
48        impl fmt::Display for $name {
49            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50                formatter.write_str(&hex::encode(self.0))
51            }
52        }
53
54        impl From<[u8; $size]> for $name {
55            fn from(bytes: [u8; $size]) -> Self {
56                Self(bytes)
57            }
58        }
59    };
60}
61
62macro_rules! semantic_integer {
63    ($name:ident, $inner:ty) => {
64        #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
65        pub struct $name($inner);
66
67        impl $name {
68            pub const fn new(value: $inner) -> Self {
69                Self(value)
70            }
71
72            pub const fn get(self) -> $inner {
73                self.0
74            }
75        }
76
77        impl From<$inner> for $name {
78            fn from(value: $inner) -> Self {
79                Self(value)
80            }
81        }
82    };
83}
84
85#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
86pub enum HexValueError {
87    #[error("invalid hexadecimal value; expected exactly {expected_bytes} bytes")]
88    InvalidHex { expected_bytes: usize },
89}
90
91semantic_bytes!(BlockHash, 32);
92semantic_bytes!(TransactionHash, 32);
93semantic_bytes!(NameHash, 32);
94semantic_bytes!(TreeRoot, 32);
95semantic_bytes!(MerkleRoot, 32);
96semantic_bytes!(WitnessRoot, 32);
97semantic_bytes!(ReservedRoot, 32);
98semantic_bytes!(PowMask, 32);
99semantic_bytes!(ShareHash, 32);
100semantic_bytes!(PowHash, 32);
101semantic_bytes!(ScriptHash, 32);
102semantic_bytes!(OfferId, 32);
103semantic_bytes!(PeerIdentity, 33);
104semantic_bytes!(RegistryFingerprint, 32);
105
106/// Canonical Handshake transaction output reference.
107///
108/// The all-zero transaction hash paired with `u32::MAX` is HSD's null
109/// outpoint. Name-state ownership and transaction inputs intentionally share
110/// this type so an authenticated owner cannot be detached from its output
111/// index by an adapter-specific representation.
112#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
113pub struct Outpoint {
114    /// Internal little-endian transaction hash bytes used by Handshake wire
115    /// encodings and HSD's name tree.
116    pub transaction_hash: TransactionHash,
117    /// Zero-based output index, or `u32::MAX` only for the null sentinel.
118    pub index: u32,
119}
120
121impl Outpoint {
122    pub const NULL: Self = Self {
123        transaction_hash: TransactionHash::new([0; 32]),
124        index: u32::MAX,
125    };
126
127    /// Whether this is HSD's exact null-outpoint sentinel.
128    pub fn is_null(self) -> bool {
129        self.index == u32::MAX && self.transaction_hash.into_bytes() == [0; 32]
130    }
131
132    /// Encode the fixed-width transaction-input representation.
133    ///
134    /// NameState values use the same hash but compact-size encode the index;
135    /// that distinct encoding is owned by `hns-covenants`.
136    pub fn encode(self) -> [u8; 36] {
137        let mut encoded = [0_u8; 36];
138        encoded[..32].copy_from_slice(self.transaction_hash.as_bytes());
139        encoded[32..].copy_from_slice(&self.index.to_le_bytes());
140        encoded
141    }
142}
143
144impl Default for Outpoint {
145    fn default() -> Self {
146        Self::NULL
147    }
148}
149
150semantic_integer!(Height, u32);
151semantic_integer!(BlockTime, u64);
152semantic_integer!(Dollarydoos, u64);
153semantic_integer!(CompactTarget, u32);
154semantic_integer!(RequestId, u64);
155semantic_integer!(EventSequence, u64);
156semantic_integer!(PolicyGeneration, u64);
157
158impl RequestId {
159    pub const fn is_valid(self) -> bool {
160        self.0 != 0
161    }
162}
163
164#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
165pub enum ArithmeticError {
166    #[error("numeric overflow")]
167    Overflow,
168    #[error("numeric underflow")]
169    Underflow,
170}
171
172impl Dollarydoos {
173    pub fn checked_add(self, other: Self) -> Result<Self, ArithmeticError> {
174        self.0
175            .checked_add(other.0)
176            .map(Self)
177            .ok_or(ArithmeticError::Overflow)
178    }
179
180    pub fn checked_sub(self, other: Self) -> Result<Self, ArithmeticError> {
181        self.0
182            .checked_sub(other.0)
183            .map(Self)
184            .ok_or(ArithmeticError::Underflow)
185    }
186}
187
188/// Unsigned 256-bit chainwork in little-endian 64-bit limbs.
189#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
190pub struct Chainwork([u64; 4]);
191
192impl Chainwork {
193    pub const ZERO: Self = Self([0; 4]);
194
195    pub const fn from_limbs_le(limbs: [u64; 4]) -> Self {
196        Self(limbs)
197    }
198
199    pub const fn limbs_le(self) -> [u64; 4] {
200        self.0
201    }
202
203    pub fn from_be_bytes(bytes: [u8; 32]) -> Self {
204        let mut limbs = [0_u64; 4];
205        for (index, limb) in limbs.iter_mut().rev().enumerate() {
206            let start = index * 8;
207            *limb = u64::from_be_bytes(
208                bytes[start..start + 8]
209                    .try_into()
210                    .expect("eight-byte chunk"),
211            );
212        }
213        Self(limbs)
214    }
215
216    pub fn to_be_bytes(self) -> [u8; 32] {
217        let mut bytes = [0_u8; 32];
218        for (index, limb) in self.0.iter().rev().enumerate() {
219            let start = index * 8;
220            bytes[start..start + 8].copy_from_slice(&limb.to_be_bytes());
221        }
222        bytes
223    }
224
225    pub fn checked_add(self, other: Self) -> Result<Self, ArithmeticError> {
226        let mut output = [0_u64; 4];
227        let mut carry = false;
228        for (index, output_limb) in output.iter_mut().enumerate() {
229            let (sum, first_carry) = self.0[index].overflowing_add(other.0[index]);
230            let (sum, second_carry) = sum.overflowing_add(u64::from(carry));
231            *output_limb = sum;
232            carry = first_carry || second_carry;
233        }
234        if carry {
235            Err(ArithmeticError::Overflow)
236        } else {
237            Ok(Self(output))
238        }
239    }
240
241    pub fn checked_sub(self, other: Self) -> Result<Self, ArithmeticError> {
242        if self < other {
243            return Err(ArithmeticError::Underflow);
244        }
245        let mut output = [0_u64; 4];
246        let mut borrow = false;
247        for (index, output_limb) in output.iter_mut().enumerate() {
248            let (difference, first_borrow) = self.0[index].overflowing_sub(other.0[index]);
249            let (difference, second_borrow) = difference.overflowing_sub(u64::from(borrow));
250            *output_limb = difference;
251            borrow = first_borrow || second_borrow;
252        }
253        debug_assert!(!borrow);
254        Ok(Self(output))
255    }
256
257    pub fn checked_mul_u64(self, multiplier: u64) -> Result<Self, ArithmeticError> {
258        let mut output = [0_u64; 4];
259        let mut carry = 0_u128;
260        for (index, output_limb) in output.iter_mut().enumerate() {
261            let product = u128::from(self.0[index]) * u128::from(multiplier) + carry;
262            *output_limb = product as u64;
263            carry = product >> 64;
264        }
265        if carry == 0 {
266            Ok(Self(output))
267        } else {
268            Err(ArithmeticError::Overflow)
269        }
270    }
271
272    pub fn checked_div_u64(self, divisor: u64) -> Result<Self, ArithmeticError> {
273        if divisor == 0 {
274            return Err(ArithmeticError::Underflow);
275        }
276        let mut output = [0_u64; 4];
277        let mut remainder = 0_u128;
278        for index in (0..4).rev() {
279            let dividend = (remainder << 64) | u128::from(self.0[index]);
280            output[index] = (dividend / u128::from(divisor)) as u64;
281            remainder = dividend % u128::from(divisor);
282        }
283        Ok(Self(output))
284    }
285}
286
287impl Ord for Chainwork {
288    fn cmp(&self, other: &Self) -> Ordering {
289        for index in (0..4).rev() {
290            match self.0[index].cmp(&other.0[index]) {
291                Ordering::Equal => {}
292                ordering => return ordering,
293            }
294        }
295        Ordering::Equal
296    }
297}
298
299impl PartialOrd for Chainwork {
300    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
301        Some(self.cmp(other))
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn semantic_hashes_do_not_interchange() {
311        let block = BlockHash::new([7; 32]);
312        let transaction = TransactionHash::new([7; 32]);
313        assert_eq!(block.as_bytes(), transaction.as_bytes());
314        assert_eq!(block.to_string().len(), 64);
315    }
316
317    #[test]
318    fn outpoint_null_and_fixed_transaction_encoding_are_exact() {
319        assert_eq!(Outpoint::default(), Outpoint::NULL);
320        assert!(Outpoint::NULL.is_null());
321        let outpoint = Outpoint {
322            transaction_hash: TransactionHash::new([0x42; 32]),
323            index: 0x1020_3040,
324        };
325        assert!(!outpoint.is_null());
326        assert_eq!(&outpoint.encode()[..32], &[0x42; 32]);
327        assert_eq!(&outpoint.encode()[32..], &[0x40, 0x30, 0x20, 0x10]);
328    }
329
330    #[test]
331    fn chainwork_addition_carries_and_detects_overflow() {
332        let left = Chainwork::from_limbs_le([u64::MAX, 1, 0, 0]);
333        let right = Chainwork::from_limbs_le([1, 2, 0, 0]);
334        assert_eq!(
335            left.checked_add(right).expect("fits").limbs_le(),
336            [0, 4, 0, 0]
337        );
338        assert_eq!(
339            Chainwork::from_limbs_le([u64::MAX; 4]).checked_add(right),
340            Err(ArithmeticError::Overflow)
341        );
342    }
343
344    #[test]
345    fn chainwork_numeric_order_and_checked_operations_are_256_bit() {
346        let lower = Chainwork::from_limbs_le([u64::MAX, 0, 0, 0]);
347        let higher = Chainwork::from_limbs_le([0, 1, 0, 0]);
348        assert!(higher > lower);
349        assert_eq!(
350            higher.checked_sub(lower).expect("fits").limbs_le(),
351            [1, 0, 0, 0]
352        );
353        assert_eq!(
354            Chainwork::from_limbs_le([u64::MAX, 0, 0, 0])
355                .checked_mul_u64(2)
356                .expect("fits")
357                .limbs_le(),
358            [u64::MAX - 1, 1, 0, 0]
359        );
360        assert_eq!(
361            Chainwork::from_limbs_le([0, 1, 0, 0])
362                .checked_div_u64(2)
363                .expect("fits")
364                .limbs_le(),
365            [1_u64 << 63, 0, 0, 0]
366        );
367        let bytes = [0x5a; 32];
368        assert_eq!(Chainwork::from_be_bytes(bytes).to_be_bytes(), bytes);
369    }
370
371    #[test]
372    fn amounts_never_use_floating_point() {
373        assert_eq!(
374            Dollarydoos::new(2)
375                .checked_add(Dollarydoos::new(3))
376                .expect("fits")
377                .get(),
378            5
379        );
380        assert_eq!(
381            Dollarydoos::new(0).checked_sub(Dollarydoos::new(1)),
382            Err(ArithmeticError::Underflow)
383        );
384    }
385}