kcode_k1_transaction_id/
lib.rs1use 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}