Skip to main content

kcode_k1_transaction_id/
lib.rs

1use sha2::{Digest, Sha256};
2use std::fmt::{Debug, Display, Formatter};
3use std::str::FromStr;
4
5pub const TX_ID_BYTES: usize = 12;
6const TX_ID_TEXT_BYTES: usize = TX_ID_BYTES * 2;
7
8#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub struct TxId([u8; TX_ID_BYTES]);
10
11impl TxId {
12    pub const fn from_bytes(bytes: [u8; TX_ID_BYTES]) -> Self {
13        Self(bytes)
14    }
15
16    pub const fn into_bytes(self) -> [u8; TX_ID_BYTES] {
17        self.0
18    }
19
20    pub const fn as_bytes(&self) -> &[u8; TX_ID_BYTES] {
21        &self.0
22    }
23
24    pub fn for_transaction(transaction: &[u8]) -> Self {
25        let digest = Sha256::digest(transaction);
26        let mut bytes = [0; TX_ID_BYTES];
27        bytes.copy_from_slice(&digest[..TX_ID_BYTES]);
28        Self(bytes)
29    }
30
31    pub fn verify(&self, transaction: &[u8]) -> bool {
32        *self == Self::for_transaction(transaction)
33    }
34}
35
36impl Display for TxId {
37    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
38        for byte in self.0 {
39            write!(formatter, "{byte:02x}")?;
40        }
41        Ok(())
42    }
43}
44
45impl Debug for TxId {
46    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
47        write!(formatter, "TxId({self})")
48    }
49}
50
51impl FromStr for TxId {
52    type Err = ParseTxIdError;
53
54    fn from_str(source: &str) -> Result<Self, Self::Err> {
55        let source = source.as_bytes();
56        if source.len() != TX_ID_TEXT_BYTES {
57            return Err(ParseTxIdError);
58        }
59
60        let mut bytes = [0; TX_ID_BYTES];
61        for (index, pair) in source.chunks_exact(2).enumerate() {
62            bytes[index] = decode_hex(pair[0])? << 4 | decode_hex(pair[1])?;
63        }
64        Ok(Self(bytes))
65    }
66}
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub struct ParseTxIdError;
70
71impl Display for ParseTxIdError {
72    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
73        formatter.write_str("invalid transaction ID")
74    }
75}
76
77impl std::error::Error for ParseTxIdError {}
78
79pub struct TxIdHasher(Sha256);
80
81impl TxIdHasher {
82    pub fn new() -> Self {
83        Self(Sha256::new())
84    }
85
86    pub fn update(&mut self, bytes: &[u8]) {
87        self.0.update(bytes);
88    }
89
90    pub fn finish(self) -> TxId {
91        let digest = self.0.finalize();
92        let mut bytes = [0; TX_ID_BYTES];
93        bytes.copy_from_slice(&digest[..TX_ID_BYTES]);
94        TxId::from_bytes(bytes)
95    }
96}
97
98impl Default for TxIdHasher {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104fn decode_hex(byte: u8) -> Result<u8, ParseTxIdError> {
105    match byte {
106        b'0'..=b'9' => Ok(byte - b'0'),
107        b'a'..=b'f' => Ok(byte - b'a' + 10),
108        _ => Err(ParseTxIdError),
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use std::hint::black_box;
116    use std::time::{Duration, Instant};
117
118    const PERFORMANCE_LIMIT: Duration = Duration::from_secs(30);
119
120    #[test]
121    fn derives_known_transaction_ids() {
122        assert_eq!(
123            TxId::for_transaction(b"").to_string(),
124            "e3b0c44298fc1c149afbf4c8"
125        );
126        assert_eq!(
127            TxId::for_transaction(b"abc").to_string(),
128            "ba7816bf8f01cfea414140de"
129        );
130    }
131
132    #[test]
133    fn round_trips_bytes_and_text() {
134        let bytes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 254, 255];
135        let id = TxId::from_bytes(bytes);
136        assert_eq!(id.as_bytes(), &bytes);
137        assert_eq!(id.into_bytes(), bytes);
138        assert_eq!(id.to_string(), "00010203040506070809feff");
139        assert_eq!(id.to_string().parse::<TxId>(), Ok(id));
140        assert_eq!(format!("{id:?}"), "TxId(00010203040506070809feff)");
141    }
142
143    #[test]
144    fn rejects_noncanonical_text() {
145        for source in [
146            "",
147            "00010203040506070809fef",
148            "00010203040506070809feff0",
149            "00010203040506070809FEFF",
150            "00010203040506070809fegf",
151            "é0010203040506070809feff",
152        ] {
153            assert_eq!(source.parse::<TxId>(), Err(ParseTxIdError));
154        }
155    }
156
157    #[test]
158    fn streaming_matches_one_shot_for_every_partition() {
159        let transaction: Vec<u8> = (0..4099).map(|index| (index % 251) as u8).collect();
160        let expected = TxId::for_transaction(&transaction);
161
162        for width in 1..=257 {
163            let mut hasher = TxIdHasher::default();
164            hasher.update(&[]);
165            for chunk in transaction.chunks(width) {
166                hasher.update(chunk);
167            }
168            assert_eq!(hasher.finish(), expected);
169        }
170
171        assert!(expected.verify(&transaction));
172        assert!(!expected.verify(b"different"));
173        assert_eq!(TxIdHasher::new().finish(), TxId::for_transaction(b""));
174    }
175
176    #[test]
177    fn exposes_value_traits() {
178        fn require_traits<T: Copy + Debug + Eq + std::hash::Hash + Ord + Send + Sync>() {}
179        require_traits::<TxId>();
180
181        let low = TxId::from_bytes([0; TX_ID_BYTES]);
182        let high = TxId::from_bytes([255; TX_ID_BYTES]);
183        let mut map = std::collections::HashMap::new();
184        map.insert(low, high);
185        assert_eq!(map[&low], high);
186        assert!(low < high);
187    }
188
189    #[test]
190    fn one_shot_hashing_load_stays_within_contract() {
191        let transaction = vec![37; 64 * 1024 * 1024];
192        let started = Instant::now();
193        black_box(TxId::for_transaction(black_box(&transaction)));
194        assert!(started.elapsed() <= PERFORMANCE_LIMIT);
195    }
196
197    #[test]
198    fn incremental_hashing_load_stays_within_contract() {
199        let transaction = vec![73; 64 * 1024 * 1024];
200        let started = Instant::now();
201        let mut hasher = TxIdHasher::new();
202        for chunk in transaction.chunks(64) {
203            hasher.update(black_box(chunk));
204        }
205        black_box(hasher.finish());
206        assert!(started.elapsed() <= PERFORMANCE_LIMIT);
207    }
208
209    #[test]
210    fn value_operations_load_stays_within_contract() {
211        let started = Instant::now();
212        for index in 0..100_000_u32 {
213            let transaction = [index as u8; 32];
214            let derived = TxId::for_transaction(black_box(&transaction));
215            let copied = TxId::from_bytes(derived.into_bytes());
216            black_box(copied.as_bytes());
217            assert!(copied.verify(black_box(&transaction)));
218        }
219        assert!(started.elapsed() <= PERFORMANCE_LIMIT);
220    }
221
222    #[test]
223    fn text_operations_load_stays_within_contract() {
224        let id = TxId::from_bytes([171; TX_ID_BYTES]);
225        let started = Instant::now();
226        for _ in 0..100_000 {
227            let text = black_box(id.to_string());
228            let parsed = text.parse::<TxId>().expect("canonical text must parse");
229            black_box(format!("{parsed:?}"));
230        }
231        assert!(started.elapsed() <= PERFORMANCE_LIMIT);
232    }
233}