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        std::str::from_utf8(&bytes).map_err(|error| error.to_string())?;
20        Ok(Self { bytes })
21    }
22
23    #[allow(clippy::should_implement_trait)]
24    pub fn from_str(value: &str) -> Result<Self, String> {
25        let bytes: [u8; SUBSYSTEM_BYTES] = value
26            .as_bytes()
27            .try_into()
28            .map_err(|_| "subsystem ID must contain exactly 20 bytes".to_owned())?;
29        Self::from_bytes(bytes)
30    }
31
32    pub const fn as_bytes(&self) -> &[u8; SUBSYSTEM_BYTES] {
33        &self.bytes
34    }
35
36    pub fn as_str(&self) -> &str {
37        std::str::from_utf8(&self.bytes).expect("SubsystemId always contains valid UTF-8")
38    }
39}
40
41pub struct Transaction<'a> {
42    bytes: &'a [u8],
43    subsystem: SubsystemId,
44}
45
46impl<'a> Transaction<'a> {
47    pub fn parse(bytes: &'a [u8]) -> Result<Self, String> {
48        if bytes.len() < MIN_TRANSACTION_BYTES {
49            return Err(format!(
50                "transaction must contain at least {MIN_TRANSACTION_BYTES} bytes"
51            ));
52        }
53
54        let subsystem_bytes = bytes[52..72]
55            .try_into()
56            .map_err(|_| "invalid subsystem ID".to_owned())?;
57        let subsystem = SubsystemId::from_bytes(subsystem_bytes)?;
58        Ok(Self { bytes, subsystem })
59    }
60
61    pub fn parent(&self) -> TxId {
62        TxId::from_bytes(self.bytes[..12].try_into().expect("fixed parent range"))
63    }
64
65    pub fn timestamp(&self) -> u64 {
66        u64::from_le_bytes(
67            self.bytes[12..20]
68                .try_into()
69                .expect("fixed timestamp range"),
70        )
71    }
72
73    pub fn creator(&self) -> &[u8; PUBLIC_KEY_BYTES] {
74        self.bytes[20..52].try_into().expect("fixed creator range")
75    }
76
77    pub fn subsystem(&self) -> SubsystemId {
78        self.subsystem
79    }
80
81    pub fn payload(&self) -> &'a [u8] {
82        &self.bytes[72..self.bytes.len() - SIGNATURE_BYTES]
83    }
84
85    pub fn signature(&self) -> &'a [u8; SIGNATURE_BYTES] {
86        self.bytes[self.bytes.len() - SIGNATURE_BYTES..]
87            .try_into()
88            .expect("fixed signature range")
89    }
90
91    pub fn signing_bytes(&self) -> &'a [u8] {
92        &self.bytes[..self.bytes.len() - SIGNATURE_BYTES]
93    }
94}
95
96pub fn build_signed_transaction<F>(
97    parent: TxId,
98    timestamp: u64,
99    creator: [u8; PUBLIC_KEY_BYTES],
100    subsystem: SubsystemId,
101    payload: &[u8],
102    signer: F,
103) -> Result<Vec<u8>, String>
104where
105    F: FnOnce(&[u8]) -> Result<[u8; SIGNATURE_BYTES], String>,
106{
107    let final_len = MIN_TRANSACTION_BYTES
108        .checked_add(payload.len())
109        .ok_or_else(|| "transaction length exceeds usize".to_owned())?;
110    let mut bytes = Vec::with_capacity(final_len);
111
112    bytes.extend_from_slice(parent.as_bytes());
113    bytes.extend_from_slice(&timestamp.to_le_bytes());
114    bytes.extend_from_slice(&creator);
115    bytes.extend_from_slice(subsystem.as_bytes());
116    bytes.extend_from_slice(payload);
117
118    let signature = signer(&bytes)?;
119    bytes.extend_from_slice(&signature);
120
121    debug_assert_eq!(bytes.len(), final_len);
122    debug_assert_eq!(bytes.capacity(), final_len);
123    Ok(bytes)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use std::cell::Cell;
130
131    fn transaction(payload: &[u8], subsystem: [u8; SUBSYSTEM_BYTES]) -> Vec<u8> {
132        let mut bytes = Vec::new();
133        bytes.extend_from_slice(&[1; 12]);
134        bytes.extend_from_slice(&0x0102_0304_0506_0708_u64.to_le_bytes());
135        bytes.extend_from_slice(&[2; PUBLIC_KEY_BYTES]);
136        bytes.extend_from_slice(&subsystem);
137        bytes.extend_from_slice(payload);
138        bytes.extend_from_slice(&[3; SIGNATURE_BYTES]);
139        bytes
140    }
141
142    #[test]
143    fn parses_exact_fields_and_empty_payload() {
144        let subsystem = *b"12345678901234567890";
145        let bytes = transaction(&[], subsystem);
146        let parsed = Transaction::parse(&bytes).unwrap();
147
148        assert_eq!(parsed.parent().into_bytes(), [1; 12]);
149        assert_eq!(parsed.timestamp(), 0x0102_0304_0506_0708);
150        assert_eq!(parsed.creator(), &[2; PUBLIC_KEY_BYTES]);
151        assert_eq!(parsed.subsystem().as_bytes(), &subsystem);
152        assert_eq!(parsed.payload(), &[]);
153        assert_eq!(parsed.signature(), &[3; SIGNATURE_BYTES]);
154        assert_eq!(parsed.signing_bytes(), &bytes[..72]);
155        assert_eq!(bytes.len(), MIN_TRANSACTION_BYTES);
156    }
157
158    #[test]
159    fn preserves_payload_signature_and_signing_prefix() {
160        let bytes = transaction(b"payload", *b"abcdefghijklmnopqrst");
161        let parsed = Transaction::parse(&bytes).unwrap();
162
163        assert_eq!(parsed.payload(), b"payload");
164        assert_eq!(parsed.signature(), &[3; SIGNATURE_BYTES]);
165        assert_eq!(
166            parsed.signing_bytes(),
167            &bytes[..bytes.len() - SIGNATURE_BYTES]
168        );
169    }
170
171    #[test]
172    fn builds_exact_signed_transaction_and_calls_signer_once() {
173        let parent = TxId::from_bytes([9; 12]);
174        let timestamp = 0x0102_0304_0506_0708;
175        let creator = [7; PUBLIC_KEY_BYTES];
176        let subsystem = SubsystemId::from_bytes(*b"abcdefghijklmnopqrst").unwrap();
177        let payload = b"payload";
178        let calls = Cell::new(0);
179
180        let bytes =
181            build_signed_transaction(parent, timestamp, creator, subsystem, payload, |prefix| {
182                calls.set(calls.get() + 1);
183
184                let mut expected = Vec::new();
185                expected.extend_from_slice(parent.as_bytes());
186                expected.extend_from_slice(&timestamp.to_le_bytes());
187                expected.extend_from_slice(&creator);
188                expected.extend_from_slice(subsystem.as_bytes());
189                expected.extend_from_slice(payload);
190                assert_eq!(prefix, expected);
191
192                Ok([5; SIGNATURE_BYTES])
193            })
194            .unwrap();
195
196        assert_eq!(calls.get(), 1);
197        assert_eq!(bytes.len(), MIN_TRANSACTION_BYTES + payload.len());
198        assert_eq!(bytes.capacity(), bytes.len());
199        assert_eq!(
200            &bytes[bytes.len() - SIGNATURE_BYTES..],
201            &[5; SIGNATURE_BYTES]
202        );
203
204        let parsed = Transaction::parse(&bytes).unwrap();
205        assert_eq!(parsed.parent(), parent);
206        assert_eq!(parsed.timestamp(), timestamp);
207        assert_eq!(parsed.creator(), &creator);
208        assert_eq!(parsed.subsystem(), subsystem);
209        assert_eq!(parsed.payload(), payload);
210        assert_eq!(parsed.signature(), &[5; SIGNATURE_BYTES]);
211    }
212
213    #[test]
214    fn builds_empty_payload_transaction() {
215        let subsystem = SubsystemId::from_bytes(*b"12345678901234567890").unwrap();
216        let bytes = build_signed_transaction(
217            GENESIS_PARENT,
218            0,
219            [0; PUBLIC_KEY_BYTES],
220            subsystem,
221            &[],
222            |_| Ok([0; SIGNATURE_BYTES]),
223        )
224        .unwrap();
225
226        assert_eq!(bytes.len(), MIN_TRANSACTION_BYTES);
227        assert!(Transaction::parse(&bytes).unwrap().payload().is_empty());
228    }
229
230    #[test]
231    fn propagates_signer_error_unchanged() {
232        let subsystem = SubsystemId::from_bytes(*b"12345678901234567890").unwrap();
233        let calls = Cell::new(0);
234
235        let result = build_signed_transaction(
236            GENESIS_PARENT,
237            0,
238            [0; PUBLIC_KEY_BYTES],
239            subsystem,
240            b"payload",
241            |_| {
242                calls.set(calls.get() + 1);
243                Err("signing failed exactly".to_owned())
244            },
245        );
246
247        assert_eq!(calls.get(), 1);
248        assert_eq!(result.unwrap_err(), "signing failed exactly");
249    }
250
251    #[test]
252    fn rejects_short_transactions() {
253        assert!(Transaction::parse(&[0; MIN_TRANSACTION_BYTES - 1]).is_err());
254    }
255
256    #[test]
257    fn accepts_exact_multibyte_utf8_subsystem() {
258        let value = "éééééééééé";
259        let subsystem = SubsystemId::from_str(value).unwrap();
260
261        assert_eq!(subsystem.as_str(), value);
262        assert_eq!(subsystem.as_bytes().len(), SUBSYSTEM_BYTES);
263    }
264
265    #[test]
266    fn rejects_wrong_subsystem_byte_length() {
267        assert!(SubsystemId::from_str("short").is_err());
268        assert!(SubsystemId::from_str("123456789012345678901").is_err());
269    }
270
271    #[test]
272    fn rejects_invalid_subsystem_utf8() {
273        assert!(SubsystemId::from_bytes([0xff; SUBSYSTEM_BYTES]).is_err());
274    }
275
276    #[test]
277    fn rejects_transaction_with_invalid_subsystem_utf8() {
278        let bytes = transaction(&[], [0xff; SUBSYSTEM_BYTES]);
279
280        assert!(Transaction::parse(&bytes).is_err());
281    }
282
283    #[test]
284    fn exposes_registration_sentinels() {
285        assert_eq!(GENESIS_PARENT.into_bytes(), [0xff; 12]);
286        assert_eq!(
287            REGISTER_AT_TIP.into_bytes(),
288            [
289                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe
290            ]
291        );
292        assert_ne!(REGISTER_AT_TIP, GENESIS_PARENT);
293    }
294}