1use crate::block::{HeaderFamily, SidestrBlock};
44use crate::document::ChainDocument;
45use crate::error::{Error, Result};
46use crate::state::StateOf;
47
48pub const RECORD_HEADER: usize = 8;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct Record<'a> {
54 pub height: u32,
57 pub bytes: &'a [u8],
59}
60
61pub fn records(dat: &[u8]) -> Result<Vec<Record<'_>>> {
66 let mut out = Vec::new();
67 let mut at = 0usize;
68 while at < dat.len() {
69 let head = dat.get(at..at + RECORD_HEADER).ok_or_else(|| {
70 Error::BlockFile(format!(
71 "a record at byte {at} is cut short: {} bytes left, the prefix is 8",
72 dat.len() - at
73 ))
74 })?;
75 let height = u32::from_le_bytes([head[0], head[1], head[2], head[3]]);
76 let size = u32::from_le_bytes([head[4], head[5], head[6], head[7]]) as usize;
77 let start = at + RECORD_HEADER;
78 let end = start.checked_add(size).filter(|e| *e <= dat.len()).ok_or_else(|| {
79 Error::BlockFile(format!(
80 "the record for height {height} at byte {at} runs past the file: size {size}, {} bytes left",
81 dat.len() - start
82 ))
83 })?;
84 out.push(Record {
85 height,
86 bytes: &dat[start..end],
87 });
88 at = end;
89 }
90 Ok(out)
91}
92
93pub fn encode_record(height: u32, block: &[u8]) -> Vec<u8> {
97 let mut out = Vec::with_capacity(RECORD_HEADER + block.len());
98 out.extend_from_slice(&height.to_le_bytes());
99 out.extend_from_slice(&(block.len() as u32).to_le_bytes());
100 out.extend_from_slice(block);
101 out
102}
103
104impl<F: HeaderFamily> StateOf<F> {
105 pub fn replay(doc: ChainDocument, dat: &[u8], now: Option<u32>) -> Result<Self> {
113 Self::replay_with(doc, dat, now, |_, _, _| {})
114 }
115
116 pub fn replay_with(
124 doc: ChainDocument,
125 dat: &[u8],
126 now: Option<u32>,
127 mut on_block: impl FnMut(Option<&Self>, u32, &F::Block),
128 ) -> Result<Self> {
129 let recs = records(dat)?;
130 let (first, rest) = recs
131 .split_first()
132 .ok_or_else(|| Error::BlockFile("the block file is empty: no genesis".into()))?;
133 if first.height != 0 {
134 return Err(Error::BlockFile(format!(
135 "the block file starts at height {}, not the genesis",
136 first.height
137 )));
138 }
139 let genesis = F::Block::decode(first.bytes)?;
140 on_block(None, 0, &genesis);
141 let mut state = Self::from_genesis(doc, &genesis, None)?;
142 for rec in rest {
143 let block = F::Block::decode(rec.bytes)?;
144 on_block(Some(&state), rec.height, &block);
145 state.apply(rec.height, &block, None, now)?;
146 }
147 Ok(state)
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::block::{challenge_for, pubkey_of};
155 use crate::state::{NextBlock, State};
156 use bitcoin::secp256k1::SecretKey;
157
158 fn doc_and_key() -> (ChainDocument, SecretKey) {
159 let key = SecretKey::from_slice(&[7u8; 32]).unwrap();
160 let json = format!(
161 r#"{{"id":"sidestr:example","name":"example","parent":"tbtc4","challenge":"{}",
162 "powLimit":"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","addressPrefix":"ex",
163 "genesisTime":1790000000,"signer":"{}","pegs":[]}}"#,
164 challenge_for(&pubkey_of(&key)).to_hex_string(),
165 pubkey_of(&key)
166 );
167 (ChainDocument::from_json(&json).unwrap(), key)
168 }
169
170 fn mirror_of(n: u32) -> (ChainDocument, Vec<u8>, State) {
172 let (doc, key) = doc_and_key();
173 let genesis = State::genesis_block_for(&doc, &key).unwrap();
174 let mut dat = encode_record(0, &genesis.encode());
175 let mut chain = State::from_genesis(doc.clone(), &genesis, None).unwrap();
176 for i in 1..=n {
177 let (_, block) = chain
178 .produce(
179 &key,
180 &NextBlock {
181 time: 1_790_000_000 + i,
182 claims: vec![],
183 },
184 None,
185 )
186 .unwrap();
187 dat.extend(encode_record(i, &block.encode()));
188 }
189 (doc, dat, chain)
190 }
191
192 #[test]
193 fn replays_to_the_producers_tip() {
194 let (doc, dat, chain) = mirror_of(5);
195 let replayed = State::replay(doc, &dat, None).unwrap();
196 assert_eq!(replayed.height(), 5);
197 assert_eq!(replayed.tip(), chain.tip());
198 }
199
200 #[test]
201 fn the_callback_sees_every_block_with_the_state_before_it() {
202 let (doc, dat, _) = mirror_of(3);
203 let mut seen = Vec::new();
204 State::replay_with(doc, &dat, None, |before, h, _| {
205 seen.push((h, before.map(|s| s.height())));
206 })
207 .unwrap();
208 assert_eq!(
209 seen,
210 vec![(0, None), (1, Some(0)), (2, Some(1)), (3, Some(2))]
211 );
212 }
213
214 #[test]
215 fn a_truncated_tail_is_refused_not_half_replayed() {
216 let (doc, dat, _) = mirror_of(2);
217 let cut = &dat[..dat.len() - 3];
218 assert!(matches!(
219 State::replay(doc, cut, None),
220 Err(Error::BlockFile(_))
221 ));
222 }
223
224 #[test]
225 fn a_file_that_does_not_start_at_the_genesis_is_refused() {
226 let (doc, dat, _) = mirror_of(2);
227 let first = records(&dat).unwrap()[0].bytes.len() + RECORD_HEADER;
228 assert!(matches!(
229 State::replay(doc, &dat[first..], None),
230 Err(Error::BlockFile(_))
231 ));
232 }
233
234 #[test]
235 fn a_skipped_height_is_refused() {
236 let (doc, dat, _) = mirror_of(3);
237 let recs = records(&dat).unwrap();
238 let mut gap = encode_record(0, recs[0].bytes);
239 gap.extend(encode_record(2, recs[2].bytes));
240 assert!(State::replay(doc, &gap, None).is_err());
241 }
242
243 #[test]
244 fn a_block_the_signer_did_not_seal_is_refused() {
245 let (doc, dat, _) = mirror_of(1);
246 let recs = records(&dat).unwrap();
249 let mut forged =
250 <crate::block::Stock as HeaderFamily>::Block::decode(recs[1].bytes).unwrap();
251 forged.header.time += 1;
252 let mut dat = encode_record(0, recs[0].bytes);
253 dat.extend(encode_record(1, &forged.encode()));
254 assert!(State::replay(doc, &dat, None).is_err());
255 }
256
257 #[test]
258 fn an_empty_file_has_no_genesis() {
259 let (doc, _) = doc_and_key();
260 assert!(matches!(
261 State::replay(doc, &[], None),
262 Err(Error::BlockFile(_))
263 ));
264 }
265}