Skip to main content

sidestr_core/
mirror.rs

1//! A mirror's block file read from memory: what a client holds after one
2//! `GET <mirror>/blocks.dat` (SPEC 11), replayed into a validated
3//! [`StateOf`] without a file system. The framing is the block file's,
4//! `[u32le height][u32le size][block bytes]` repeated
5//! (`bitcoin-blake/blaketestnode` `lib/blockfile.mjs`); `blockfile` (feature
6//! `std`) writes and reads the same records on disk.
7//!
8//! Nothing here trusts the mirror. The genesis is judged against the chain
9//! document and held to its `genesisHash`, and every later block passes the
10//! same rules as [`StateOf::apply`], so a mirror can serve a short chain or
11//! none at all, but never a block the signer did not seal. A client that
12//! wants to know how far behind the mirror is compares the replayed tip with
13//! the signer's own kind-33333 announcement.
14//!
15//! ```
16//! use bitcoin::secp256k1::SecretKey;
17//! use sidestr_core::block::{challenge_for, pubkey_of, SidestrBlock};
18//! use sidestr_core::document::ChainDocument;
19//! use sidestr_core::mirror::{encode_record, records};
20//! use sidestr_core::state::{NextBlock, State};
21//!
22//! let key = SecretKey::from_slice(&[7u8; 32]).unwrap();
23//! let json = format!(r#"{{"id":"sidestr:example","name":"example","parent":"tbtc4","challenge":"{}",
24//!   "powLimit":"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","addressPrefix":"ex",
25//!   "genesisTime":1790000000,"signer":"{}","pegs":[]}}"#, challenge_for(&pubkey_of(&key)).to_hex_string(), pubkey_of(&key));
26//! let doc = ChainDocument::from_json(&json).unwrap();
27//!
28//! // a producer makes three blocks and a mirror serves them as a block file
29//! let genesis = State::genesis_block_for(&doc, &key).unwrap();
30//! let mut dat = encode_record(0, &genesis.encode());
31//! let mut chain = State::from_genesis(doc.clone(), &genesis, None).unwrap();
32//! for i in 1..=3 {
33//!     let (_, block) = chain.produce(&key, &NextBlock { time: 1790000000 + i, claims: vec![] }, None).unwrap();
34//!     dat.extend(encode_record(i, &block.encode()));
35//! }
36//! assert_eq!(records(&dat).unwrap().len(), 4);
37//!
38//! // a client replays the bytes against the document it already trusts
39//! let replayed = State::replay(doc, &dat, None).unwrap();
40//! assert_eq!(replayed.tip(), chain.tip());
41//! ```
42
43use crate::block::{HeaderFamily, SidestrBlock};
44use crate::document::ChainDocument;
45use crate::error::{Error, Result};
46use crate::state::StateOf;
47
48/// The per-record prefix: height and size, both `u32le`.
49pub const RECORD_HEADER: usize = 8;
50
51/// One record of a block file: its height and the block bytes.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct Record<'a> {
54    /// The height the record claims. [`StateOf::replay`] holds it to the
55    /// height the chain reaches by applying the block.
56    pub height: u32,
57    /// The block's consensus bytes.
58    pub bytes: &'a [u8],
59}
60
61/// Split a block file into its records. A trailing record that is cut short
62/// (a mirror caught mid-write, a truncated download) is
63/// [`Error::BlockFile`], named with its offset; a client retries rather than
64/// replaying a chain it half holds.
65pub 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
93/// One record's bytes, `[u32le height][u32le size][block]`: what
94/// `blockfile::append_block` writes, for a test or a tool that assembles a
95/// block file in memory.
96pub 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    /// Replay a mirror's block file held in memory into a validated state.
106    /// The first record must be height 0 and is judged as the genesis
107    /// ([`StateOf::from_genesis`], held to the document's `genesisHash`);
108    /// every later record must be the next height and passes
109    /// [`StateOf::apply`]. `now` is the clock for the future-time rule
110    /// (`None` skips it). An empty file is [`Error::BlockFile`]: a chain
111    /// always has its genesis.
112    pub fn replay(doc: ChainDocument, dat: &[u8], now: Option<u32>) -> Result<Self> {
113        Self::replay_with(doc, dat, now, |_, _, _| {})
114    }
115
116    /// [`StateOf::replay`], calling `on_block(before, height, block)` for
117    /// each block ahead of applying it: `before` is the state at the previous
118    /// height (`None` for the genesis). A wallet uses it to read its own
119    /// history, since which coins a block spends is known only before the
120    /// block is applied. A block the rules refuse stops the replay with the
121    /// error; `on_block` has then seen it, so a caller keeps nothing from a
122    /// call that returned an error.
123    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    /// A producer's chain of `n` blocks after the genesis, as a mirror serves it.
171    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        // block 1 as the signer sealed it, then its time moved by a second:
247        // the seal no longer commits to the header the mirror serves
248        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}