Skip to main content

sidestr_core/
chain.rs

1//! A sidestr chain on disk (feature `std`): the [`StateOf`] replayed from the
2//! block file in a directory, created with the genesis when absent, and
3//! every accepted block written down (`siding/lib/chain.mjs open`, `addBlock`,
4//! `produce`). The clock is the system's.
5//!
6//! [`Chain`] is the stock instantiation; a BLAKE2b chain is
7//! `ChainOf<Blake2bV2>` with the family from `sidestr-header`, and replays a
8//! mirror's `blocks.dat` the same way.
9//!
10//! ```no_run
11//! use sidestr_core::{chain::Chain, document::ChainDocument, block::key_from_hex};
12//!
13//! let doc = ChainDocument::from_json(&std::fs::read_to_string("chain.json").unwrap()).unwrap();
14//! // a validator: replays what is on disk, refuses a genesis that is not the document's
15//! let chain = Chain::open(doc.clone(), "state", None).unwrap();
16//! println!("{} at {} with {} coins", chain.state().genesis_hash(), chain.state().height(), chain.state().utxo().len());
17//! // a producer: the key is a file, never an argument
18//! let key = key_from_hex(&std::fs::read_to_string("signer.key").unwrap()).unwrap();
19//! let mut chain = Chain::open(doc, "state", Some(&key)).unwrap();
20//! let added = chain.produce(&key, vec![]).unwrap();
21//! println!("block {} {}", added.height, added.hash);
22//! ```
23
24use std::path::{Path, PathBuf};
25use std::time::{SystemTime, UNIX_EPOCH};
26
27use bitcoin::consensus::encode::deserialize;
28use bitcoin::secp256k1::SecretKey;
29use bitcoin::BlockHash;
30
31use crate::block::{HeaderFamily, SidestrBlock, Stock};
32use crate::blockfile::{append_block, read_block, read_index, write_index, Index};
33use crate::document::ChainDocument;
34use crate::error::{Error, Result};
35use crate::state::{Applied, ClaimRequest, NextBlock, StateOf, Submitted};
36
37/// The chain with its block file, for header family `F`.
38#[derive(Debug)]
39pub struct ChainOf<F: HeaderFamily> {
40    state: StateOf<F>,
41    dir: PathBuf,
42    index: Index,
43}
44
45/// The chain on disk beside a stock parent: [`ChainOf`] over [`Stock`].
46pub type Chain = ChainOf<Stock>;
47
48/// Seconds since the epoch, as a header time.
49pub fn now() -> u32 {
50    SystemTime::now()
51        .duration_since(UNIX_EPOCH)
52        .map(|d| d.as_secs() as u32)
53        .unwrap_or(0)
54}
55
56impl<F: HeaderFamily> ChainOf<F> {
57    /// Replay `dir/blocks.dat`, creating it with the genesis when absent —
58    /// which needs the signer's key. `open()` refuses a block file whose
59    /// block 0 does not hash to the document's `genesisHash`, and any later
60    /// block the rules refuse.
61    pub fn open(
62        doc: ChainDocument,
63        dir: impl AsRef<Path>,
64        key: Option<&SecretKey>,
65    ) -> Result<Self> {
66        doc.validate()?;
67        let federated = doc.signers.is_some();
68        Self::open_with(doc, dir, |doc| {
69            let key = key.ok_or_else(|| {
70                Error::Chain("no chain on disk and no key to make the genesis".into())
71            })?;
72            if federated {
73                return Err(Error::Federation(
74                    "a federated chain's genesis needs k signatures: open_sealed(doc, dir, seal)"
75                        .into(),
76                ));
77            }
78            StateOf::<F>::genesis_block_for(doc, key)
79        })
80    }
81
82    /// [`ChainOf::open`] for a federated chain (`siding/lib/chain.mjs open(null,
83    /// { seal })`): when no chain is on disk, `seal` receives the unsigned
84    /// genesis ([`StateOf::build_genesis_for`]) and returns it sealed by `k`
85    /// signatures — [`crate::federation::seal_federated`] with the partials
86    /// the signers made. With a chain on disk, `seal` is not called.
87    pub fn open_sealed(
88        doc: ChainDocument,
89        dir: impl AsRef<Path>,
90        seal: impl FnOnce(&F::Block) -> Result<F::Block>,
91    ) -> Result<Self> {
92        doc.validate()?;
93        Self::open_with(doc, dir, |doc| seal(&StateOf::<F>::build_genesis_for(doc)?))
94    }
95
96    fn open_with(
97        doc: ChainDocument,
98        dir: impl AsRef<Path>,
99        genesis: impl FnOnce(&ChainDocument) -> Result<F::Block>,
100    ) -> Result<Self> {
101        StateOf::<F>::family_of(&doc)?;
102        let dir = dir.as_ref().to_path_buf();
103        std::fs::create_dir_all(&dir)?;
104        let dat = dir.join("blocks.dat");
105        let idx = dir.join("blocks.json");
106        let (state, index) = match read_index(&idx)? {
107            None => {
108                let genesis = genesis(&doc)?;
109                let state = StateOf::<F>::from_genesis(doc, &genesis, None)?;
110                let mut index = Index::new(&state.document().id);
111                append_block(
112                    &dat,
113                    &mut index,
114                    0,
115                    &state.genesis_hash().to_string(),
116                    &genesis.encode(),
117                )?;
118                write_index(&idx, &index)?;
119                (state, index)
120            }
121            Some(index) => {
122                let first = index
123                    .blocks
124                    .first()
125                    .ok_or_else(|| Error::Chain("the block file has no genesis".into()))?;
126                if first.height != 0 {
127                    return Err(Error::Chain(format!(
128                        "the block file starts at {}, not the genesis",
129                        first.height
130                    )));
131                }
132                let genesis = F::Block::decode(&read_block(&dat, first)?)?;
133                let mut state = StateOf::<F>::from_genesis(
134                    doc,
135                    &genesis,
136                    Some(
137                        first
138                            .hash
139                            .parse()
140                            .map_err(|_| Error::Encoding("bad hash in the index".into()))?,
141                    ),
142                )?;
143                for e in &index.blocks[1..] {
144                    let expect: BlockHash = e
145                        .hash
146                        .parse()
147                        .map_err(|_| Error::Encoding("bad hash in the index".into()))?;
148                    let block = F::Block::decode(&read_block(&dat, e)?)?;
149                    state.apply(e.height, &block, Some(expect), Some(now()))?;
150                }
151                (state, index)
152            }
153        };
154        Ok(Self { state, dir, index })
155    }
156
157    /// The chain in memory.
158    pub fn state(&self) -> &StateOf<F> {
159        &self.state
160    }
161    /// The block file's index.
162    pub fn index(&self) -> &Index {
163        &self.index
164    }
165    /// `dir/blocks.dat`.
166    pub fn dat_path(&self) -> PathBuf {
167        self.dir.join("blocks.dat")
168    }
169    /// `dir/blocks.json`.
170    pub fn index_path(&self) -> PathBuf {
171        self.dir.join("blocks.json")
172    }
173
174    fn write(&mut self, height: u32, hash: BlockHash, bytes: &[u8]) -> Result<()> {
175        append_block(
176            self.dat_path(),
177            &mut self.index,
178            height,
179            &hash.to_string(),
180            bytes,
181        )?;
182        write_index(self.index_path(), &self.index)
183    }
184
185    /// Accept a block from elsewhere (a mirror): validated, applied, written.
186    pub fn add_block(&mut self, bytes: &[u8], expect: Option<BlockHash>) -> Result<Applied> {
187        let r = self.state.add_block_bytes(bytes, expect, Some(now()))?;
188        self.write(r.height, r.hash, bytes)?;
189        Ok(r)
190    }
191
192    /// A transaction for the mempool (SPEC 11), consensus bytes.
193    pub fn submit(&mut self, tx_bytes: &[u8]) -> Result<Submitted> {
194        let tx = deserialize(tx_bytes).map_err(|e| Error::Encoding(e.to_string()))?;
195        self.state.submit(tx)
196    }
197
198    /// One signer: build the next block from the mempool and these claims,
199    /// sign, apply, write.
200    pub fn produce(&mut self, key: &SecretKey, claims: Vec<ClaimRequest>) -> Result<Applied> {
201        let t = now();
202        let (r, block) = self
203            .state
204            .produce(key, &NextBlock { time: t, claims }, Some(t))?;
205        self.write(r.height, r.hash, &block.encode())?;
206        Ok(r)
207    }
208}