#![allow(missing_docs)]
use crate::types::{CrossingType, UnitId};
use serde::{Deserialize, Serialize};
mod signature_bytes {
use serde::{self, Deserialize, Deserializer, Serializer};
pub fn serialize<S>(bytes: &[u8; 64], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_bytes(bytes)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 64], D::Error>
where
D: Deserializer<'de>,
{
let v: Vec<u8> = Vec::deserialize(deserializer)?;
v.try_into().map_err(|v: Vec<u8>| {
serde::de::Error::custom(format!("expected 64 bytes for signature, got {}", v.len()))
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CrossingRecord {
pub source: UnitId,
pub destination: UnitId,
pub cycle_index: u64,
pub sequence_number: u8,
pub crossing_type: CrossingType,
pub timestamp_ns: u64,
pub prev_hash: [u8; 32],
pub chain_hash: [u8; 32],
#[serde(with = "signature_bytes")]
pub signature: [u8; 64],
}
impl CrossingRecord {
pub fn compute_chain_hash(
source: UnitId,
destination: UnitId,
cycle_index: u64,
sequence_number: u8,
crossing_type: CrossingType,
timestamp_ns: u64,
prev_hash: &[u8; 32],
) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(&[source as u8]);
hasher.update(&[destination as u8]);
hasher.update(&cycle_index.to_le_bytes());
hasher.update(&[sequence_number]);
hasher.update(&[crossing_type as u8]);
hasher.update(×tamp_ns.to_le_bytes());
hasher.update(prev_hash);
*hasher.finalize().as_bytes()
}
pub fn verify_chain_hash(&self) -> bool {
let expected = Self::compute_chain_hash(
self.source,
self.destination,
self.cycle_index,
self.sequence_number,
self.crossing_type,
self.timestamp_ns,
&self.prev_hash,
);
self.chain_hash == expected
}
pub fn signable_bytes(&self) -> &[u8; 32] {
&self.chain_hash
}
}
#[derive(Debug, Serialize)]
pub struct CrossingRecordSigningView<'a> {
pub source: UnitId,
pub destination: UnitId,
pub cycle_index: u64,
pub sequence_number: u8,
pub crossing_type: CrossingType,
pub timestamp_ns: u64,
pub prev_hash: &'a [u8; 32],
pub chain_hash: &'a [u8; 32],
}
impl<'a> From<&'a CrossingRecord> for CrossingRecordSigningView<'a> {
fn from(record: &'a CrossingRecord) -> Self {
Self {
source: record.source,
destination: record.destination,
cycle_index: record.cycle_index,
sequence_number: record.sequence_number,
crossing_type: record.crossing_type,
timestamp_ns: record.timestamp_ns,
prev_hash: &record.prev_hash,
chain_hash: &record.chain_hash,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_record(seq: u8, prev: [u8; 32]) -> CrossingRecord {
let chain_hash = CrossingRecord::compute_chain_hash(
UnitId::FU,
UnitId::MU,
1,
seq,
CrossingType::Vertical,
1_000_000_000,
&prev,
);
CrossingRecord {
source: UnitId::FU,
destination: UnitId::MU,
cycle_index: 1,
sequence_number: seq,
crossing_type: CrossingType::Vertical,
timestamp_ns: 1_000_000_000,
prev_hash: prev,
chain_hash,
signature: [0u8; 64], }
}
#[test]
fn chain_hash_is_deterministic() {
let r1 = make_record(1, [0u8; 32]);
let r2 = make_record(1, [0u8; 32]);
assert_eq!(r1.chain_hash, r2.chain_hash);
}
#[test]
fn chain_hash_changes_with_sequence() {
let r1 = make_record(1, [0u8; 32]);
let r2 = make_record(2, [0u8; 32]);
assert_ne!(r1.chain_hash, r2.chain_hash);
}
#[test]
fn chain_hash_changes_with_prev_hash() {
let r1 = make_record(1, [0u8; 32]);
let r2 = make_record(1, [1u8; 32]);
assert_ne!(r1.chain_hash, r2.chain_hash);
}
#[test]
fn chain_hash_verification_passes_for_correct_record() {
let r = make_record(1, [0u8; 32]);
assert!(r.verify_chain_hash());
}
#[test]
fn chain_hash_verification_fails_on_tamper() {
let mut r = make_record(1, [0u8; 32]);
r.timestamp_ns = 999; assert!(!r.verify_chain_hash());
}
#[test]
fn crossing_records_chain_correctly() {
let r1 = make_record(1, [0u8; 32]);
let r2 = make_record(2, r1.chain_hash);
let r3 = make_record(3, r2.chain_hash);
assert!(r1.verify_chain_hash());
assert!(r2.verify_chain_hash());
assert!(r3.verify_chain_hash());
assert_eq!(r2.prev_hash, r1.chain_hash);
assert_eq!(r3.prev_hash, r2.chain_hash);
}
#[test]
fn source_and_destination_affect_hash() {
let h1 = CrossingRecord::compute_chain_hash(
UnitId::FU,
UnitId::MU,
1,
1,
CrossingType::Vertical,
1_000,
&[0u8; 32],
);
let h2 = CrossingRecord::compute_chain_hash(
UnitId::MU,
UnitId::CU,
1,
1,
CrossingType::Vertical,
1_000,
&[0u8; 32],
);
assert_ne!(h1, h2);
}
#[test]
fn crossing_type_affects_hash() {
let h1 = CrossingRecord::compute_chain_hash(
UnitId::FU,
UnitId::MU,
1,
1,
CrossingType::Vertical,
1_000,
&[0u8; 32],
);
let h2 = CrossingRecord::compute_chain_hash(
UnitId::FU,
UnitId::MU,
1,
1,
CrossingType::Horizontal,
1_000,
&[0u8; 32],
);
assert_ne!(
h1, h2,
"Vertical and horizontal crossings must produce different hashes"
);
}
#[test]
fn signing_view_canonical_cbor_byte_equal_for_two_clones()
-> Result<(), Box<dyn std::error::Error>> {
let r1 = make_record(3, [7u8; 32]);
let r2 = r1.clone();
let v1 = CrossingRecordSigningView::from(&r1);
let v2 = CrossingRecordSigningView::from(&r2);
let mut buf1 = Vec::new();
ciborium::ser::into_writer(&v1, &mut buf1)?;
let mut buf2 = Vec::new();
ciborium::ser::into_writer(&v2, &mut buf2)?;
assert_eq!(buf1, buf2, "canonical-CBOR encoding must be deterministic");
assert!(!buf1.is_empty(), "encoding must not be empty");
Ok(())
}
#[test]
fn signing_view_encoding_changes_with_each_bound_field()
-> Result<(), Box<dyn std::error::Error>> {
let base = make_record(1, [0u8; 32]);
let mut buf_base = Vec::new();
ciborium::ser::into_writer(&CrossingRecordSigningView::from(&base), &mut buf_base)?;
let mut mut_seq = base.clone();
mut_seq.sequence_number = 7;
let mut buf_seq = Vec::new();
ciborium::ser::into_writer(&CrossingRecordSigningView::from(&mut_seq), &mut buf_seq)?;
assert_ne!(buf_base, buf_seq, "sequence_number must affect view encoding");
let mut mut_ts = base.clone();
mut_ts.timestamp_ns = base.timestamp_ns + 1;
let mut buf_ts = Vec::new();
ciborium::ser::into_writer(&CrossingRecordSigningView::from(&mut_ts), &mut buf_ts)?;
assert_ne!(buf_base, buf_ts, "timestamp_ns must affect view encoding");
let mut mut_prev = base.clone();
mut_prev.prev_hash = [9u8; 32];
let mut buf_prev = Vec::new();
ciborium::ser::into_writer(&CrossingRecordSigningView::from(&mut_prev), &mut buf_prev)?;
assert_ne!(buf_base, buf_prev, "prev_hash must affect view encoding");
Ok(())
}
#[test]
fn signing_view_encoding_invariant_under_signature_mutation()
-> Result<(), Box<dyn std::error::Error>> {
let r1 = make_record(1, [0u8; 32]);
let mut r2 = r1.clone();
r2.signature = [0xFFu8; 64];
let mut buf1 = Vec::new();
ciborium::ser::into_writer(&CrossingRecordSigningView::from(&r1), &mut buf1)?;
let mut buf2 = Vec::new();
ciborium::ser::into_writer(&CrossingRecordSigningView::from(&r2), &mut buf2)?;
assert_eq!(
buf1, buf2,
"view encoding must be invariant under signature mutation \
(the field excluded from the view)"
);
Ok(())
}
}