use kcode_k1_transaction_store::TxId;
pub const SUBSYSTEM_BYTES: usize = 20;
pub const PUBLIC_KEY_BYTES: usize = 32;
pub const SIGNATURE_BYTES: usize = 64;
pub const MIN_TRANSACTION_BYTES: usize = 136;
pub const GENESIS_PARENT: TxId = TxId::from_bytes([0xff; 12]);
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct SubsystemId {
bytes: [u8; SUBSYSTEM_BYTES],
}
impl SubsystemId {
pub fn from_bytes(bytes: [u8; SUBSYSTEM_BYTES]) -> Result<Self, String> {
std::str::from_utf8(&bytes).map_err(|error| error.to_string())?;
Ok(Self { bytes })
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(value: &str) -> Result<Self, String> {
let bytes: [u8; SUBSYSTEM_BYTES] = value
.as_bytes()
.try_into()
.map_err(|_| "subsystem ID must contain exactly 20 bytes".to_owned())?;
Self::from_bytes(bytes)
}
pub const fn as_bytes(&self) -> &[u8; SUBSYSTEM_BYTES] {
&self.bytes
}
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.bytes).expect("SubsystemId always contains valid UTF-8")
}
}
pub struct Transaction<'a> {
bytes: &'a [u8],
subsystem: SubsystemId,
}
impl<'a> Transaction<'a> {
pub fn parse(bytes: &'a [u8]) -> Result<Self, String> {
if bytes.len() < MIN_TRANSACTION_BYTES {
return Err(format!(
"transaction must contain at least {MIN_TRANSACTION_BYTES} bytes"
));
}
let subsystem_bytes = bytes[52..72]
.try_into()
.map_err(|_| "invalid subsystem ID".to_owned())?;
let subsystem = SubsystemId::from_bytes(subsystem_bytes)?;
Ok(Self { bytes, subsystem })
}
pub fn parent(&self) -> TxId {
TxId::from_bytes(self.bytes[..12].try_into().expect("fixed parent range"))
}
pub fn timestamp(&self) -> u64 {
u64::from_le_bytes(
self.bytes[12..20]
.try_into()
.expect("fixed timestamp range"),
)
}
pub fn creator(&self) -> &[u8; PUBLIC_KEY_BYTES] {
self.bytes[20..52].try_into().expect("fixed creator range")
}
pub fn subsystem(&self) -> SubsystemId {
self.subsystem
}
pub fn payload(&self) -> &'a [u8] {
&self.bytes[72..self.bytes.len() - SIGNATURE_BYTES]
}
pub fn signature(&self) -> &'a [u8; SIGNATURE_BYTES] {
self.bytes[self.bytes.len() - SIGNATURE_BYTES..]
.try_into()
.expect("fixed signature range")
}
pub fn signing_bytes(&self) -> &'a [u8] {
&self.bytes[..self.bytes.len() - SIGNATURE_BYTES]
}
}
pub fn build_signed_transaction<F>(
parent: TxId,
timestamp: u64,
creator: [u8; PUBLIC_KEY_BYTES],
subsystem: SubsystemId,
payload: &[u8],
signer: F,
) -> Result<Vec<u8>, String>
where
F: FnOnce(&[u8]) -> Result<[u8; SIGNATURE_BYTES], String>,
{
let final_len = MIN_TRANSACTION_BYTES
.checked_add(payload.len())
.ok_or_else(|| "transaction length exceeds usize".to_owned())?;
let mut bytes = Vec::with_capacity(final_len);
bytes.extend_from_slice(parent.as_bytes());
bytes.extend_from_slice(×tamp.to_le_bytes());
bytes.extend_from_slice(&creator);
bytes.extend_from_slice(subsystem.as_bytes());
bytes.extend_from_slice(payload);
let signature = signer(&bytes)?;
bytes.extend_from_slice(&signature);
debug_assert_eq!(bytes.len(), final_len);
debug_assert_eq!(bytes.capacity(), final_len);
Ok(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
fn transaction(payload: &[u8], subsystem: [u8; SUBSYSTEM_BYTES]) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&[1; 12]);
bytes.extend_from_slice(&0x0102_0304_0506_0708_u64.to_le_bytes());
bytes.extend_from_slice(&[2; PUBLIC_KEY_BYTES]);
bytes.extend_from_slice(&subsystem);
bytes.extend_from_slice(payload);
bytes.extend_from_slice(&[3; SIGNATURE_BYTES]);
bytes
}
#[test]
fn parses_exact_fields_and_empty_payload() {
let subsystem = *b"12345678901234567890";
let bytes = transaction(&[], subsystem);
let parsed = Transaction::parse(&bytes).unwrap();
assert_eq!(parsed.parent().into_bytes(), [1; 12]);
assert_eq!(parsed.timestamp(), 0x0102_0304_0506_0708);
assert_eq!(parsed.creator(), &[2; PUBLIC_KEY_BYTES]);
assert_eq!(parsed.subsystem().as_bytes(), &subsystem);
assert_eq!(parsed.payload(), &[]);
assert_eq!(parsed.signature(), &[3; SIGNATURE_BYTES]);
assert_eq!(parsed.signing_bytes(), &bytes[..72]);
assert_eq!(bytes.len(), MIN_TRANSACTION_BYTES);
}
#[test]
fn preserves_payload_signature_and_signing_prefix() {
let bytes = transaction(b"payload", *b"abcdefghijklmnopqrst");
let parsed = Transaction::parse(&bytes).unwrap();
assert_eq!(parsed.payload(), b"payload");
assert_eq!(parsed.signature(), &[3; SIGNATURE_BYTES]);
assert_eq!(
parsed.signing_bytes(),
&bytes[..bytes.len() - SIGNATURE_BYTES]
);
}
#[test]
fn builds_exact_signed_transaction_and_calls_signer_once() {
let parent = TxId::from_bytes([9; 12]);
let timestamp = 0x0102_0304_0506_0708;
let creator = [7; PUBLIC_KEY_BYTES];
let subsystem = SubsystemId::from_bytes(*b"abcdefghijklmnopqrst").unwrap();
let payload = b"payload";
let calls = Cell::new(0);
let bytes =
build_signed_transaction(parent, timestamp, creator, subsystem, payload, |prefix| {
calls.set(calls.get() + 1);
let mut expected = Vec::new();
expected.extend_from_slice(parent.as_bytes());
expected.extend_from_slice(×tamp.to_le_bytes());
expected.extend_from_slice(&creator);
expected.extend_from_slice(subsystem.as_bytes());
expected.extend_from_slice(payload);
assert_eq!(prefix, expected);
Ok([5; SIGNATURE_BYTES])
})
.unwrap();
assert_eq!(calls.get(), 1);
assert_eq!(bytes.len(), MIN_TRANSACTION_BYTES + payload.len());
assert_eq!(bytes.capacity(), bytes.len());
assert_eq!(
&bytes[bytes.len() - SIGNATURE_BYTES..],
&[5; SIGNATURE_BYTES]
);
let parsed = Transaction::parse(&bytes).unwrap();
assert_eq!(parsed.parent(), parent);
assert_eq!(parsed.timestamp(), timestamp);
assert_eq!(parsed.creator(), &creator);
assert_eq!(parsed.subsystem(), subsystem);
assert_eq!(parsed.payload(), payload);
assert_eq!(parsed.signature(), &[5; SIGNATURE_BYTES]);
}
#[test]
fn builds_empty_payload_transaction() {
let subsystem = SubsystemId::from_bytes(*b"12345678901234567890").unwrap();
let bytes = build_signed_transaction(
GENESIS_PARENT,
0,
[0; PUBLIC_KEY_BYTES],
subsystem,
&[],
|_| Ok([0; SIGNATURE_BYTES]),
)
.unwrap();
assert_eq!(bytes.len(), MIN_TRANSACTION_BYTES);
assert!(Transaction::parse(&bytes).unwrap().payload().is_empty());
}
#[test]
fn propagates_signer_error_unchanged() {
let subsystem = SubsystemId::from_bytes(*b"12345678901234567890").unwrap();
let calls = Cell::new(0);
let result = build_signed_transaction(
GENESIS_PARENT,
0,
[0; PUBLIC_KEY_BYTES],
subsystem,
b"payload",
|_| {
calls.set(calls.get() + 1);
Err("signing failed exactly".to_owned())
},
);
assert_eq!(calls.get(), 1);
assert_eq!(result.unwrap_err(), "signing failed exactly");
}
#[test]
fn rejects_short_transactions() {
assert!(Transaction::parse(&[0; MIN_TRANSACTION_BYTES - 1]).is_err());
}
#[test]
fn accepts_exact_multibyte_utf8_subsystem() {
let value = "éééééééééé";
let subsystem = SubsystemId::from_str(value).unwrap();
assert_eq!(subsystem.as_str(), value);
assert_eq!(subsystem.as_bytes().len(), SUBSYSTEM_BYTES);
}
#[test]
fn rejects_wrong_subsystem_byte_length() {
assert!(SubsystemId::from_str("short").is_err());
assert!(SubsystemId::from_str("123456789012345678901").is_err());
}
#[test]
fn rejects_invalid_subsystem_utf8() {
assert!(SubsystemId::from_bytes([0xff; SUBSYSTEM_BYTES]).is_err());
}
#[test]
fn rejects_transaction_with_invalid_subsystem_utf8() {
let bytes = transaction(&[], [0xff; SUBSYSTEM_BYTES]);
assert!(Transaction::parse(&bytes).is_err());
}
#[test]
fn exposes_genesis_sentinel() {
assert_eq!(GENESIS_PARENT.into_bytes(), [0xff; 12]);
}
}