Skip to main content

kcode_k1_transaction/
lib.rs

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(&timestamp.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}