1use sha2::{Digest, Sha256};
10
11pub const GENESIS_HEAD: [u8; 32] = [0u8; 32];
13
14#[must_use]
16pub fn chain_hash(prev: &[u8; 32], seq: u64, value: &[u8]) -> [u8; 32] {
17 let mut h = Sha256::new();
18 h.update(prev);
19 h.update(seq.to_be_bytes());
20 h.update(value);
21 h.finalize().into()
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct ChainLink {
27 pub seq: u64,
29 pub prev_head: [u8; 32],
31}
32
33#[derive(Debug, Clone, krabka_macros::FieldDefaults)]
35pub struct ChainState {
36 next_seq: u64,
37 #[default(GENESIS_HEAD)]
38 head: [u8; 32],
39}
40
41impl ChainState {
42 #[must_use]
43 pub fn new() -> Self { Self::default() }
44
45 #[must_use]
47 pub fn resume(next_seq: u64, head: [u8; 32]) -> Self { Self { next_seq, head } }
48
49 #[tracing::instrument(level = "debug", skip_all, fields(seq = self.next_seq, bytes = value.len()))]
55 pub fn extend(&mut self, value: &[u8]) -> ChainLink {
56 let seq = self.next_seq;
57 let prev = self.head;
58 self.head = chain_hash(&prev, seq, value);
59 self.next_seq += 1;
60 ChainLink {
61 seq,
62 prev_head: prev,
63 }
64 }
65
66 #[must_use]
67 pub fn head(&self) -> [u8; 32] { self.head }
68
69 #[must_use]
70 pub fn next_seq(&self) -> u64 { self.next_seq }
71}
72
73#[must_use]
75pub fn to_hex(bytes: &[u8]) -> String { hex::encode(bytes) }
76
77#[must_use]
79pub fn from_hex32(s: &str) -> Option<[u8; 32]> {
80 let mut out = [0u8; 32];
81 hex::decode_to_slice(s, &mut out).ok()?;
82 Some(out)
83}
84
85#[cfg(test)]
86mod tests {
87 use {super::*, assert2::check};
88
89 #[test]
90 fn genesis_is_zero() {
91 check!(GENESIS_HEAD == [0u8; 32]);
92 }
93
94 #[test]
95 fn chain_hash_is_deterministic_and_order_sensitive() {
96 let a = chain_hash(&GENESIS_HEAD, 0, b"alpha");
97 let b = chain_hash(&GENESIS_HEAD, 0, b"alpha");
98 check!(a == b);
99 check!(chain_hash(&GENESIS_HEAD, 1, b"alpha") != a);
101 check!(chain_hash(&GENESIS_HEAD, 0, b"beta") != a);
103 check!(chain_hash(&a, 0, b"alpha") != a);
105 }
106
107 #[test]
108 fn chain_state_advances_and_reports_prev() {
109 let mut c = ChainState::new();
110 check!(c.next_seq() == 0);
111 let link0 = c.extend(b"r0");
112 check!(
113 link0
114 == ChainLink {
115 seq: 0,
116 prev_head: GENESIS_HEAD
117 }
118 );
119 check!((c.next_seq(), c.head()) == (1, chain_hash(&GENESIS_HEAD, 0, b"r0")));
120
121 let link1 = c.extend(b"r1");
122 check!(
123 link1
124 == ChainLink {
125 seq: 1,
126 prev_head: chain_hash(&GENESIS_HEAD, 0, b"r0"),
127 }
128 );
129 check!(c.head() == chain_hash(&link1.prev_head, 1, b"r1"));
130 }
131
132 #[test]
133 fn resume_sets_next_seq_and_head_and_continues() {
134 let head = chain_hash(&GENESIS_HEAD, 4, b"r4");
135 let mut c = ChainState::resume(5, head);
136 check!((c.next_seq(), c.head()) == (5, head));
137 let link = c.extend(b"r5");
138 check!(
139 link == ChainLink {
140 seq: 5,
141 prev_head: head
142 }
143 );
144 check!(c.head() == chain_hash(&head, 5, b"r5"));
145 }
146
147 #[yare::parameterized(
148 round_trip = {
149 to_hex(&chain_hash(&GENESIS_HEAD, 7, b"x")),
150 Some(chain_hash(&GENESIS_HEAD, 7, b"x")),
151 },
152 non_hex = { "zz".to_owned(), None },
153 odd_length = { "abc".to_owned(), None },
154 uppercase = {
155 to_hex(&chain_hash(&GENESIS_HEAD, 7, b"x")).to_uppercase(),
156 Some(chain_hash(&GENESIS_HEAD, 7, b"x")),
157 },
158 )]
159 fn hex_round_trips(input: String, expected: Option<[u8; 32]>) {
160 check!(from_hex32(&input) == expected);
161 }
162}