sidestr-core 0.1.0

sidestr user-activated sidechains beside a Bitcoin-family parent: the chain document, the parents table, signed stock-header blocks (BIP-325 challenge, no subsidy), the peg-in claim and peg-out burn rules, the block file and an in-memory validating chain. A port of siding by Melvin Carvalho.
Documentation
//! A sidestr chain on disk (feature `std`): the [`State`] replayed from the
//! block file in a directory, created with the genesis when absent, and
//! every accepted block written down (`siding/lib/chain.mjs open`, `addBlock`,
//! `produce`). The clock is the system's.
//!
//! ```no_run
//! use sidestr_core::{chain::Chain, document::ChainDocument, block::key_from_hex};
//!
//! let doc = ChainDocument::from_json(&std::fs::read_to_string("chain.json").unwrap()).unwrap();
//! // a validator: replays what is on disk, refuses a genesis that is not the document's
//! let chain = Chain::open(doc.clone(), "state", None).unwrap();
//! println!("{} at {} with {} coins", chain.state().genesis_hash(), chain.state().height(), chain.state().utxo().len());
//! // a producer: the key is a file, never an argument
//! let key = key_from_hex(&std::fs::read_to_string("signer.key").unwrap()).unwrap();
//! let mut chain = Chain::open(doc, "state", Some(&key)).unwrap();
//! let added = chain.produce(&key, vec![]).unwrap();
//! println!("block {} {}", added.height, added.hash);
//! ```

use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

use bitcoin::consensus::encode::{deserialize, serialize};
use bitcoin::secp256k1::SecretKey;
use bitcoin::{Block, BlockHash};

use crate::blockfile::{append_block, read_block, read_index, write_index, Index};
use crate::document::ChainDocument;
use crate::error::{Error, Result};
use crate::state::{Applied, ClaimRequest, NextBlock, State, Submitted};

/// The chain with its block file.
#[derive(Debug)]
pub struct Chain {
    state: State,
    dir: PathBuf,
    index: Index,
}

/// Seconds since the epoch, as a header time.
pub fn now() -> u32 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs() as u32)
        .unwrap_or(0)
}

impl Chain {
    /// Replay `dir/blocks.dat`, creating it with the genesis when absent —
    /// which needs the signer's key. `open()` refuses a block file whose
    /// block 0 does not hash to the document's `genesisHash`, and any later
    /// block the rules refuse.
    pub fn open(
        doc: ChainDocument,
        dir: impl AsRef<Path>,
        key: Option<&SecretKey>,
    ) -> Result<Self> {
        let dir = dir.as_ref().to_path_buf();
        std::fs::create_dir_all(&dir)?;
        let dat = dir.join("blocks.dat");
        let idx = dir.join("blocks.json");
        let (state, index) = match read_index(&idx)? {
            None => {
                let key = key.ok_or_else(|| {
                    Error::Chain("no chain on disk and no key to make the genesis".into())
                })?;
                let genesis = State::genesis_block_for(&doc, key)?;
                let state = State::from_genesis(doc, &genesis, None)?;
                let mut index = Index::new(&state.document().id);
                append_block(
                    &dat,
                    &mut index,
                    0,
                    &state.genesis_hash().to_string(),
                    &serialize(&genesis),
                )?;
                write_index(&idx, &index)?;
                (state, index)
            }
            Some(index) => {
                let first = index
                    .blocks
                    .first()
                    .ok_or_else(|| Error::Chain("the block file has no genesis".into()))?;
                if first.height != 0 {
                    return Err(Error::Chain(format!(
                        "the block file starts at {}, not the genesis",
                        first.height
                    )));
                }
                let genesis: Block = deserialize(&read_block(&dat, first)?)
                    .map_err(|e| Error::Encoding(e.to_string()))?;
                let mut state = State::from_genesis(
                    doc,
                    &genesis,
                    Some(
                        first
                            .hash
                            .parse()
                            .map_err(|_| Error::Encoding("bad hash in the index".into()))?,
                    ),
                )?;
                for e in &index.blocks[1..] {
                    let expect: BlockHash = e
                        .hash
                        .parse()
                        .map_err(|_| Error::Encoding("bad hash in the index".into()))?;
                    let block: Block = deserialize(&read_block(&dat, e)?)
                        .map_err(|err| Error::Encoding(err.to_string()))?;
                    state.apply(e.height, &block, Some(expect), Some(now()))?;
                }
                (state, index)
            }
        };
        Ok(Self { state, dir, index })
    }

    /// The chain in memory.
    pub fn state(&self) -> &State {
        &self.state
    }
    /// The block file's index.
    pub fn index(&self) -> &Index {
        &self.index
    }
    /// `dir/blocks.dat`.
    pub fn dat_path(&self) -> PathBuf {
        self.dir.join("blocks.dat")
    }
    /// `dir/blocks.json`.
    pub fn index_path(&self) -> PathBuf {
        self.dir.join("blocks.json")
    }

    fn write(&mut self, height: u32, hash: BlockHash, bytes: &[u8]) -> Result<()> {
        append_block(
            self.dat_path(),
            &mut self.index,
            height,
            &hash.to_string(),
            bytes,
        )?;
        write_index(self.index_path(), &self.index)
    }

    /// Accept a block from elsewhere (a mirror): validated, applied, written.
    pub fn add_block(&mut self, bytes: &[u8], expect: Option<BlockHash>) -> Result<Applied> {
        let r = self.state.add_block_bytes(bytes, expect, Some(now()))?;
        self.write(r.height, r.hash, bytes)?;
        Ok(r)
    }

    /// A transaction for the mempool (SPEC 11), consensus bytes.
    pub fn submit(&mut self, tx_bytes: &[u8]) -> Result<Submitted> {
        let tx = deserialize(tx_bytes).map_err(|e| Error::Encoding(e.to_string()))?;
        self.state.submit(tx)
    }

    /// One signer: build the next block from the mempool and these claims,
    /// sign, apply, write.
    pub fn produce(&mut self, key: &SecretKey, claims: Vec<ClaimRequest>) -> Result<Applied> {
        let t = now();
        let (r, block) = self
            .state
            .produce(key, &NextBlock { time: t, claims }, Some(t))?;
        self.write(r.height, r.hash, &serialize(&block))?;
        Ok(r)
    }
}