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