1use kcode_k1_transaction_store::TxId;
2
3pub const SUBSYSTEM_BYTES: usize = 20;
4pub const PUBLIC_KEY_BYTES: usize = 32;
5pub const SIGNATURE_BYTES: usize = 64;
6pub const MIN_TRANSACTION_BYTES: usize = 136;
7pub const GENESIS_PARENT: TxId = TxId::from_bytes([0xff; 12]);
8pub const REGISTER_AT_TIP: TxId = TxId::from_bytes([
9 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
10]);
11
12#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub struct SubsystemId {
14 bytes: [u8; SUBSYSTEM_BYTES],
15}
16
17impl SubsystemId {
18 pub fn from_bytes(bytes: [u8; SUBSYSTEM_BYTES]) -> Result<Self, String> {
19 let logical_len = bytes
20 .iter()
21 .position(|byte| *byte == 0)
22 .unwrap_or(SUBSYSTEM_BYTES);
23 if logical_len == 0 {
24 return Err("subsystem ID must not be empty".to_owned());
25 }
26 if bytes[logical_len..].iter().any(|byte| *byte != 0) {
27 return Err("subsystem ID has nonzero bytes after padding".to_owned());
28 }
29 std::str::from_utf8(&bytes[..logical_len]).map_err(|error| error.to_string())?;
30 Ok(Self { bytes })
31 }
32
33 #[allow(clippy::should_implement_trait)]
34 pub fn from_str(value: &str) -> Result<Self, String> {
35 if value.is_empty() {
36 return Err("subsystem ID must not be empty".to_owned());
37 }
38 if value.len() > SUBSYSTEM_BYTES {
39 return Err("subsystem ID must contain at most 20 bytes".to_owned());
40 }
41 if value.as_bytes().contains(&0) {
42 return Err("subsystem ID must not contain NUL".to_owned());
43 }
44 let mut bytes = [0; SUBSYSTEM_BYTES];
45 bytes[..value.len()].copy_from_slice(value.as_bytes());
46 Self::from_bytes(bytes)
47 }
48
49 pub const fn as_bytes(&self) -> &[u8; SUBSYSTEM_BYTES] {
50 &self.bytes
51 }
52
53 pub fn as_str(&self) -> &str {
54 let logical_len = self
55 .bytes
56 .iter()
57 .position(|byte| *byte == 0)
58 .unwrap_or(SUBSYSTEM_BYTES);
59 std::str::from_utf8(&self.bytes[..logical_len])
60 .expect("SubsystemId always contains canonical UTF-8")
61 }
62}
63
64pub struct Transaction<'a> {
65 bytes: &'a [u8],
66 subsystem: SubsystemId,
67}
68
69impl<'a> Transaction<'a> {
70 pub fn parse(bytes: &'a [u8]) -> Result<Self, String> {
71 if bytes.len() < MIN_TRANSACTION_BYTES {
72 return Err(format!(
73 "transaction must contain at least {MIN_TRANSACTION_BYTES} bytes"
74 ));
75 }
76
77 let subsystem_bytes = bytes[52..72]
78 .try_into()
79 .map_err(|_| "invalid subsystem ID".to_owned())?;
80 let subsystem = SubsystemId::from_bytes(subsystem_bytes)?;
81 Ok(Self { bytes, subsystem })
82 }
83
84 pub fn parent(&self) -> TxId {
85 TxId::from_bytes(self.bytes[..12].try_into().expect("fixed parent range"))
86 }
87
88 pub fn timestamp(&self) -> u64 {
89 u64::from_le_bytes(
90 self.bytes[12..20]
91 .try_into()
92 .expect("fixed timestamp range"),
93 )
94 }
95
96 pub fn creator(&self) -> &[u8; PUBLIC_KEY_BYTES] {
97 self.bytes[20..52].try_into().expect("fixed creator range")
98 }
99
100 pub fn subsystem(&self) -> SubsystemId {
101 self.subsystem
102 }
103
104 pub fn payload(&self) -> &'a [u8] {
105 &self.bytes[72..self.bytes.len() - SIGNATURE_BYTES]
106 }
107
108 pub fn signature(&self) -> &'a [u8; SIGNATURE_BYTES] {
109 self.bytes[self.bytes.len() - SIGNATURE_BYTES..]
110 .try_into()
111 .expect("fixed signature range")
112 }
113
114 pub fn signing_bytes(&self) -> &'a [u8] {
115 &self.bytes[..self.bytes.len() - SIGNATURE_BYTES]
116 }
117}
118
119pub fn build_signed_transaction<F>(
120 parent: TxId,
121 timestamp: u64,
122 creator: [u8; PUBLIC_KEY_BYTES],
123 subsystem: SubsystemId,
124 payload: &[u8],
125 signer: F,
126) -> Result<Vec<u8>, String>
127where
128 F: FnOnce(&[u8]) -> Result<[u8; SIGNATURE_BYTES], String>,
129{
130 let final_len = MIN_TRANSACTION_BYTES
131 .checked_add(payload.len())
132 .ok_or_else(|| "transaction length exceeds usize".to_owned())?;
133 let mut bytes = Vec::with_capacity(final_len);
134
135 bytes.extend_from_slice(parent.as_bytes());
136 bytes.extend_from_slice(×tamp.to_le_bytes());
137 bytes.extend_from_slice(&creator);
138 bytes.extend_from_slice(subsystem.as_bytes());
139 bytes.extend_from_slice(payload);
140
141 let signature = signer(&bytes)?;
142 bytes.extend_from_slice(&signature);
143
144 debug_assert_eq!(bytes.len(), final_len);
145 debug_assert_eq!(bytes.capacity(), final_len);
146 Ok(bytes)
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use std::cell::Cell;
153
154 fn transaction(payload: &[u8], subsystem: [u8; SUBSYSTEM_BYTES]) -> Vec<u8> {
155 let mut bytes = Vec::new();
156 bytes.extend_from_slice(&[1; 12]);
157 bytes.extend_from_slice(&0x0102_0304_0506_0708_u64.to_le_bytes());
158 bytes.extend_from_slice(&[2; PUBLIC_KEY_BYTES]);
159 bytes.extend_from_slice(&subsystem);
160 bytes.extend_from_slice(payload);
161 bytes.extend_from_slice(&[3; SIGNATURE_BYTES]);
162 bytes
163 }
164
165 #[test]
166 fn parses_exact_fields_and_empty_payload() {
167 let subsystem = *b"12345678901234567890";
168 let bytes = transaction(&[], subsystem);
169 let parsed = Transaction::parse(&bytes).unwrap();
170
171 assert_eq!(parsed.parent().into_bytes(), [1; 12]);
172 assert_eq!(parsed.timestamp(), 0x0102_0304_0506_0708);
173 assert_eq!(parsed.creator(), &[2; PUBLIC_KEY_BYTES]);
174 assert_eq!(parsed.subsystem().as_bytes(), &subsystem);
175 assert_eq!(parsed.subsystem().as_str(), "12345678901234567890");
176 assert_eq!(parsed.payload(), &[]);
177 assert_eq!(parsed.signature(), &[3; SIGNATURE_BYTES]);
178 assert_eq!(parsed.signing_bytes(), &bytes[..72]);
179 assert_eq!(bytes.len(), MIN_TRANSACTION_BYTES);
180 }
181
182 #[test]
183 fn subsystem_logical_lengths_and_padding() {
184 for value in ["a", "1234567890123456789", "12345678901234567890"] {
185 let subsystem = SubsystemId::from_str(value).unwrap();
186 assert_eq!(subsystem.as_str(), value);
187 assert_eq!(&subsystem.as_bytes()[..value.len()], value.as_bytes());
188 assert!(
189 subsystem.as_bytes()[value.len()..]
190 .iter()
191 .all(|byte| *byte == 0)
192 );
193 assert_eq!(
194 SubsystemId::from_bytes(*subsystem.as_bytes()).unwrap(),
195 subsystem
196 );
197 }
198 }
199
200 #[test]
201 fn subsystem_multibyte_boundaries() {
202 let nineteen = "éééééééééa";
203 let twenty = "éééééééééé";
204 assert_eq!(nineteen.len(), 19);
205 assert_eq!(twenty.len(), 20);
206 assert_eq!(SubsystemId::from_str(nineteen).unwrap().as_str(), nineteen);
207 assert_eq!(SubsystemId::from_str(twenty).unwrap().as_str(), twenty);
208 assert!(SubsystemId::from_str("ééééééééééa").is_err());
209 }
210
211 #[test]
212 fn rejects_noncanonical_subsystem_ids() {
213 assert!(SubsystemId::from_str("").is_err());
214 assert!(SubsystemId::from_str("123456789012345678901").is_err());
215 assert!(SubsystemId::from_str("a\0b").is_err());
216 assert!(SubsystemId::from_bytes([0; SUBSYSTEM_BYTES]).is_err());
217
218 let mut embedded = [0; SUBSYSTEM_BYTES];
219 embedded[..3].copy_from_slice(b"a\0b");
220 assert!(SubsystemId::from_bytes(embedded).is_err());
221
222 let mut invalid_utf8 = [0; SUBSYSTEM_BYTES];
223 invalid_utf8[0] = 0xff;
224 assert!(SubsystemId::from_bytes(invalid_utf8).is_err());
225 }
226
227 #[test]
228 fn builds_and_parses_short_subsystem_transaction() {
229 let parent = TxId::from_bytes([9; 12]);
230 let timestamp = 0x0102_0304_0506_0708;
231 let creator = [7; PUBLIC_KEY_BYTES];
232 let subsystem = SubsystemId::from_str("k1-groups-subsystem").unwrap();
233 let payload = b"payload";
234 let calls = Cell::new(0);
235
236 let bytes =
237 build_signed_transaction(parent, timestamp, creator, subsystem, payload, |prefix| {
238 calls.set(calls.get() + 1);
239 assert_eq!(&prefix[52..72], subsystem.as_bytes());
240 Ok([5; SIGNATURE_BYTES])
241 })
242 .unwrap();
243
244 assert_eq!(calls.get(), 1);
245 let parsed = Transaction::parse(&bytes).unwrap();
246 assert_eq!(parsed.parent(), parent);
247 assert_eq!(parsed.timestamp(), timestamp);
248 assert_eq!(parsed.creator(), &creator);
249 assert_eq!(parsed.subsystem(), subsystem);
250 assert_eq!(parsed.subsystem().as_str(), "k1-groups-subsystem");
251 assert_eq!(parsed.payload(), payload);
252 assert_eq!(parsed.signature(), &[5; SIGNATURE_BYTES]);
253 }
254
255 #[test]
256 fn existing_twenty_byte_wire_bytes_are_unchanged() {
257 let subsystem = SubsystemId::from_str("abcdefghijklmnopqrst").unwrap();
258 assert_eq!(subsystem.as_bytes(), b"abcdefghijklmnopqrst");
259 let bytes = build_signed_transaction(
260 GENESIS_PARENT,
261 0,
262 [0; PUBLIC_KEY_BYTES],
263 subsystem,
264 &[],
265 |_| Ok([0; SIGNATURE_BYTES]),
266 )
267 .unwrap();
268 assert_eq!(&bytes[52..72], b"abcdefghijklmnopqrst");
269 assert_eq!(bytes.len(), MIN_TRANSACTION_BYTES);
270 }
271
272 #[test]
273 fn preserves_payload_signature_and_signing_prefix() {
274 let bytes = transaction(b"payload", *b"abcdefghijklmnopqrst");
275 let parsed = Transaction::parse(&bytes).unwrap();
276
277 assert_eq!(parsed.payload(), b"payload");
278 assert_eq!(parsed.signature(), &[3; SIGNATURE_BYTES]);
279 assert_eq!(
280 parsed.signing_bytes(),
281 &bytes[..bytes.len() - SIGNATURE_BYTES]
282 );
283 }
284
285 #[test]
286 fn builds_empty_payload_transaction() {
287 let subsystem = SubsystemId::from_bytes(*b"12345678901234567890").unwrap();
288 let bytes = build_signed_transaction(
289 GENESIS_PARENT,
290 0,
291 [0; PUBLIC_KEY_BYTES],
292 subsystem,
293 &[],
294 |_| Ok([0; SIGNATURE_BYTES]),
295 )
296 .unwrap();
297
298 assert_eq!(bytes.len(), MIN_TRANSACTION_BYTES);
299 assert!(Transaction::parse(&bytes).unwrap().payload().is_empty());
300 }
301
302 #[test]
303 fn propagates_signer_error_unchanged() {
304 let subsystem = SubsystemId::from_str("short").unwrap();
305 let calls = Cell::new(0);
306
307 let result = build_signed_transaction(
308 GENESIS_PARENT,
309 0,
310 [0; PUBLIC_KEY_BYTES],
311 subsystem,
312 b"payload",
313 |_| {
314 calls.set(calls.get() + 1);
315 Err("signing failed exactly".to_owned())
316 },
317 );
318
319 assert_eq!(calls.get(), 1);
320 assert_eq!(result.unwrap_err(), "signing failed exactly");
321 }
322
323 #[test]
324 fn rejects_short_or_noncanonical_transactions() {
325 assert!(Transaction::parse(&[0; MIN_TRANSACTION_BYTES - 1]).is_err());
326 assert!(Transaction::parse(&transaction(&[], [0xff; SUBSYSTEM_BYTES])).is_err());
327
328 let mut embedded = [0; SUBSYSTEM_BYTES];
329 embedded[..3].copy_from_slice(b"a\0b");
330 assert!(Transaction::parse(&transaction(&[], embedded)).is_err());
331 }
332
333 #[test]
334 fn exposes_registration_sentinels() {
335 assert_eq!(GENESIS_PARENT.into_bytes(), [0xff; 12]);
336 assert_eq!(
337 REGISTER_AT_TIP.into_bytes(),
338 [
339 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe
340 ]
341 );
342 assert_ne!(REGISTER_AT_TIP, GENESIS_PARENT);
343 }
344}