kcode_k1_transaction/
lib.rs1use 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]);
8
9#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub struct SubsystemId {
11 bytes: [u8; SUBSYSTEM_BYTES],
12}
13
14impl SubsystemId {
15 pub fn from_bytes(bytes: [u8; SUBSYSTEM_BYTES]) -> Result<Self, String> {
16 std::str::from_utf8(&bytes).map_err(|error| error.to_string())?;
17 Ok(Self { bytes })
18 }
19
20 #[allow(clippy::should_implement_trait)]
21 pub fn from_str(value: &str) -> Result<Self, String> {
22 let bytes: [u8; SUBSYSTEM_BYTES] = value
23 .as_bytes()
24 .try_into()
25 .map_err(|_| "subsystem ID must contain exactly 20 bytes".to_string())?;
26 Self::from_bytes(bytes)
27 }
28
29 pub const fn as_bytes(&self) -> &[u8; SUBSYSTEM_BYTES] {
30 &self.bytes
31 }
32
33 pub fn as_str(&self) -> &str {
34 std::str::from_utf8(&self.bytes).expect("SubsystemId always contains valid UTF-8")
35 }
36}
37
38pub struct Transaction<'a> {
39 bytes: &'a [u8],
40 subsystem: SubsystemId,
41}
42
43impl<'a> Transaction<'a> {
44 pub fn parse(bytes: &'a [u8]) -> Result<Self, String> {
45 if bytes.len() < MIN_TRANSACTION_BYTES {
46 return Err(format!(
47 "transaction must contain at least {MIN_TRANSACTION_BYTES} bytes"
48 ));
49 }
50 let subsystem_bytes = bytes[52..72]
51 .try_into()
52 .map_err(|_| "invalid subsystem ID".to_string())?;
53 let subsystem = SubsystemId::from_bytes(subsystem_bytes)?;
54 Ok(Self { bytes, subsystem })
55 }
56
57 pub fn parent(&self) -> TxId {
58 TxId::from_bytes(self.bytes[..12].try_into().expect("fixed parent range"))
59 }
60
61 pub fn timestamp(&self) -> u64 {
62 u64::from_le_bytes(
63 self.bytes[12..20]
64 .try_into()
65 .expect("fixed timestamp range"),
66 )
67 }
68
69 pub fn creator(&self) -> &[u8; PUBLIC_KEY_BYTES] {
70 self.bytes[20..52].try_into().expect("fixed creator range")
71 }
72
73 pub fn subsystem(&self) -> SubsystemId {
74 self.subsystem
75 }
76
77 pub fn payload(&self) -> &'a [u8] {
78 &self.bytes[72..self.bytes.len() - SIGNATURE_BYTES]
79 }
80
81 pub fn signature(&self) -> &'a [u8; SIGNATURE_BYTES] {
82 self.bytes[self.bytes.len() - SIGNATURE_BYTES..]
83 .try_into()
84 .expect("fixed signature range")
85 }
86
87 pub fn signing_bytes(&self) -> &'a [u8] {
88 &self.bytes[..self.bytes.len() - SIGNATURE_BYTES]
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 fn transaction(payload: &[u8], subsystem: [u8; SUBSYSTEM_BYTES]) -> Vec<u8> {
97 let mut bytes = Vec::new();
98 bytes.extend_from_slice(&[1; 12]);
99 bytes.extend_from_slice(&0x0102_0304_0506_0708_u64.to_le_bytes());
100 bytes.extend_from_slice(&[2; PUBLIC_KEY_BYTES]);
101 bytes.extend_from_slice(&subsystem);
102 bytes.extend_from_slice(payload);
103 bytes.extend_from_slice(&[3; SIGNATURE_BYTES]);
104 bytes
105 }
106
107 #[test]
108 fn parses_exact_fields_and_empty_payload() {
109 let subsystem = *b"12345678901234567890";
110 let bytes = transaction(&[], subsystem);
111 let parsed = Transaction::parse(&bytes).unwrap();
112
113 assert_eq!(parsed.parent().into_bytes(), [1; 12]);
114 assert_eq!(parsed.timestamp(), 0x0102_0304_0506_0708);
115 assert_eq!(parsed.creator(), &[2; PUBLIC_KEY_BYTES]);
116 assert_eq!(parsed.subsystem().as_bytes(), &subsystem);
117 assert_eq!(parsed.payload(), &[]);
118 assert_eq!(parsed.signature(), &[3; SIGNATURE_BYTES]);
119 assert_eq!(parsed.signing_bytes(), &bytes[..72]);
120 assert_eq!(bytes.len(), MIN_TRANSACTION_BYTES);
121 }
122
123 #[test]
124 fn preserves_payload_signature_and_signing_prefix() {
125 let bytes = transaction(b"payload", *b"abcdefghijklmnopqrst");
126 let parsed = Transaction::parse(&bytes).unwrap();
127
128 assert_eq!(parsed.payload(), b"payload");
129 assert_eq!(parsed.signature(), &[3; SIGNATURE_BYTES]);
130 assert_eq!(
131 parsed.signing_bytes(),
132 &bytes[..bytes.len() - SIGNATURE_BYTES]
133 );
134 }
135
136 #[test]
137 fn rejects_short_transactions() {
138 assert!(Transaction::parse(&[0; MIN_TRANSACTION_BYTES - 1]).is_err());
139 }
140
141 #[test]
142 fn accepts_exact_multibyte_utf8_subsystem() {
143 let value = "éééééééééé";
144 let subsystem = SubsystemId::from_str(value).unwrap();
145
146 assert_eq!(subsystem.as_str(), value);
147 assert_eq!(subsystem.as_bytes().len(), SUBSYSTEM_BYTES);
148 }
149
150 #[test]
151 fn rejects_wrong_subsystem_byte_length() {
152 assert!(SubsystemId::from_str("short").is_err());
153 assert!(SubsystemId::from_str("123456789012345678901").is_err());
154 }
155
156 #[test]
157 fn rejects_invalid_subsystem_utf8() {
158 assert!(SubsystemId::from_bytes([0xff; SUBSYSTEM_BYTES]).is_err());
159 }
160
161 #[test]
162 fn rejects_transaction_with_invalid_subsystem_utf8() {
163 let bytes = transaction(&[], [0xff; SUBSYSTEM_BYTES]);
164
165 assert!(Transaction::parse(&bytes).is_err());
166 }
167
168 #[test]
169 fn exposes_genesis_sentinel() {
170 assert_eq!(GENESIS_PARENT.into_bytes(), [0xff; 12]);
171 }
172}