bindex 0.1.0

Bitcoin indexing library in Rust
Documentation
use std::ops::ControlFlow;

use bitcoin::hashes::Hash;
use bitcoin_slices::{bsl, Parse, Visit};

use crate::index::{BlockBytes, Error, HashPrefixRow, IndexedBlock, SpentBytes, TxNum};

bitcoin::hashes::hash_newtype! {
    /// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#script-hashes
    #[hash_newtype(backward)]
    pub struct ScriptHash(bitcoin::hashes::sha256::Hash);
}

impl ScriptHash {
    pub fn new(script: &bitcoin::Script) -> Self {
        Self::hash(script.as_bytes())
    }
}

struct IndexVisitor<'a> {
    rows: &'a mut Vec<HashPrefixRow>,
    txnum: TxNum,
}

impl<'a> IndexVisitor<'a> {
    fn new(txnum: TxNum, rows: &'a mut Vec<HashPrefixRow>) -> Self {
        Self { txnum, rows }
    }

    fn add(&mut self, script: &bitcoin::Script) {
        if script.is_op_return() {
            // skip indexing unspendable outputs
            return;
        }
        let prefix = ScriptHash::new(script).into();
        self.rows.push(HashPrefixRow::new(prefix, self.txnum));
    }

    fn finish_tx(&mut self) {
        self.txnum.increment_by(1)
    }
}

impl bitcoin_slices::Visitor for IndexVisitor<'_> {
    fn visit_tx_out(&mut self, _vout: usize, tx_out: &bsl::TxOut) -> ControlFlow<()> {
        self.add(bitcoin::Script::from_bytes(tx_out.script_pubkey()));
        ControlFlow::Continue(())
    }

    fn visit_transaction(&mut self, _tx: &bsl::Transaction) -> ControlFlow<()> {
        // Updated after all txouts are scanned
        self.finish_tx();
        ControlFlow::Continue(())
    }
}

struct Spent;

impl AsRef<[u8]> for Spent {
    fn as_ref(&self) -> &[u8] {
        &[]
    }
}

fn visit_spent<'a>(
    slice: &'a [u8],
    visit: &mut IndexVisitor,
) -> bitcoin_slices::SResult<'a, Spent> {
    let mut consumed = 0;
    let txs_count = bsl::scan_len(slice, &mut consumed)?;

    for _ in 0..txs_count {
        let outputs_count = bsl::scan_len(&slice[consumed..], &mut consumed)?;

        for _ in 0..outputs_count {
            let tx_out = bsl::TxOut::parse(&slice[consumed..])?;
            consumed += tx_out.consumed();
            let script_pubkey = tx_out.parsed().script_pubkey();
            visit.add(bitcoin::Script::from_bytes(script_pubkey));
        }
        visit.finish_tx();
    }

    Ok(bitcoin_slices::ParseResult::new(&slice[consumed..], Spent))
}

fn add_block_rows(
    block: &BlockBytes,
    txnum: TxNum,
    rows: &mut Vec<HashPrefixRow>,
) -> Result<TxNum, Error> {
    let mut visitor = IndexVisitor::new(txnum, rows);
    let res = bsl::Block::visit(&block.0, &mut visitor).map_err(Error::Parse)?;
    if !res.remaining().is_empty() {
        return Err(Error::Leftover(res.remaining().len()));
    }
    Ok(visitor.txnum)
}

fn add_spent_rows(
    spent: &SpentBytes,
    txnum: TxNum,
    rows: &mut Vec<HashPrefixRow>,
) -> Result<TxNum, Error> {
    let mut visitor = IndexVisitor::new(txnum, rows);
    let res = visit_spent(&spent.0, &mut visitor).map_err(Error::Parse)?;
    if !res.remaining().is_empty() {
        return Err(Error::Leftover(res.remaining().len()));
    }
    Ok(visitor.txnum)
}

pub fn index(
    block: &BlockBytes,
    spent: &SpentBytes,
    txnum: TxNum,
) -> Result<IndexedBlock<HashPrefixRow>, Error> {
    let mut result = IndexedBlock::new(txnum);
    let txnum1 = add_block_rows(block, txnum, &mut result.rows)?;
    let txnum2 = add_spent_rows(spent, txnum, &mut result.rows)?;
    assert_eq!(txnum1, txnum2);
    result.next_txnum = txnum1;
    Ok(result)
}

#[cfg(test)]
mod tests {
    use bitcoin::consensus::{deserialize, encode::Decodable};
    use hex_lit::hex;

    use super::*;
    use crate::index::{Batch, Prefix, TxNumRange};

    // Block 100000
    const BLOCK_HEX: &str = "0100000050120119172a610421a6c3011dd330d9df07b63616c2cc1f1cd00200000000006657a9252aacd5c0b2940996ecff952228c3067cc38d4885efb5a4ac4247e9f337221b4d4c86041b0f2b57100401000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08044c86041b020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79a073dc4444acb83c4ec7a0e2f99dd7457516c5817242da796924ca4e99947d087fedf9ce467cb9f7c6287078f801df276fdf84ac000000000100000001032e38e9c0a84c6046d687d10556dcacc41d275ec55fc00779ac88fdf357a187000000008c493046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57da93b58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1ebcc6028d9cdb1c3af7748014104f46db5e9d61a9dc27b8d64ad23e7383a4e6ca164593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f25f87c161bc6f8a630121df2b3d3ffffffff0200e32321000000001976a914c398efa9c392ba6013c5e04ee729755ef7f58b3288ac000fe208010000001976a914948c765a6914d43f2a7ac177da2c2f6b52de3d7c88ac000000000100000001c33ebff2a709f13d9f9a7569ab16a32786af7d7e2de09265e41c61d078294ecf010000008a4730440220032d30df5ee6f57fa46cddb5eb8d0d9fe8de6b342d27942ae90a3231e0ba333e02203deee8060fdc70230a7f5b4ad7d7bc3e628cbe219a886b84269eaeb81e26b4fe014104ae31c31bf91278d99b8377a35bbce5b27d9fff15456839e919453fc7b3f721f0ba403ff96c9deeb680e5fd341c0fc3a7b90da4631ee39560639db462e9cb850fffffffff0240420f00000000001976a914b0dcbf97eabf4404e31d952477ce822dadbe7e1088acc060d211000000001976a9146b1281eec25ab4e1e0793ff4e08ab1abb3409cd988ac0000000001000000010b6072b386d4a773235237f64c1126ac3b240c84b917a3909ba1c43ded5f51f4000000008c493046022100bb1ad26df930a51cce110cf44f7a48c3c561fd977500b1ae5d6b6fd13d0b3f4a022100c5b42951acedff14abba2736fd574bdb465f3e6f8da12e2c5303954aca7f78f3014104a7135bfe824c97ecc01ec7d7e336185c81e2aa2c41ab175407c09484ce9694b44953fcb751206564a9c24dd094d42fdbfdd5aad3e063ce6af4cfaaea4ea14fbbffffffff0140420f00000000001976a91439aa3d569e06a1d7926dc4be1193c99bf2eb9ee088ac00000000";
    const SPENT_HEX: &str = "04000100f2052a010000001976a91471d7dd96d9edda09180fe9d57a477b5acc9cad1188ac0100a3e111000000001976a91435fbee6a3bf8d99f17724ec54787567393a8a6b188ac0140420f00000000001976a914c4eb47ecfdcf609a1848ee79acc2fa49d3caad7088ac";

    #[test]
    fn test_index_block() -> Result<(), Error> {
        let block_bytes = BlockBytes(hex!(BLOCK_HEX).to_vec());
        let spent_bytes = SpentBytes(hex!(SPENT_HEX).to_vec());
        let txnum = TxNum(10);

        let mut block_rows = vec![];
        assert_eq!(
            add_block_rows(&block_bytes, txnum, &mut block_rows)?,
            TxNum(14)
        );

        assert_eq!(
            block_rows,
            vec![
                HashPrefixRow::new(Prefix(hex!("e2151d493a1f9999")), TxNum(10)),
                HashPrefixRow::new(Prefix(hex!("050b00fb9d5f7a63")), TxNum(11)),
                HashPrefixRow::new(Prefix(hex!("b5a1091a739a6aba")), TxNum(11)),
                HashPrefixRow::new(Prefix(hex!("03b0bfb44fd9d852")), TxNum(12)),
                HashPrefixRow::new(Prefix(hex!("0faa9934b57389f2")), TxNum(12)),
                HashPrefixRow::new(Prefix(hex!("4a569bc2092bcaf9")), TxNum(13))
            ]
        );

        let mut spent_rows = vec![];
        assert_eq!(
            add_spent_rows(&spent_bytes, txnum, &mut spent_rows)?,
            TxNum(14)
        );

        assert_eq!(
            spent_rows,
            vec![
                HashPrefixRow::new(Prefix(hex!("4d5bea28470692cd")), TxNum(11)),
                HashPrefixRow::new(Prefix(hex!("e9b09b065b5f43c2")), TxNum(12)),
                HashPrefixRow::new(Prefix(hex!("2e7cdb30882b427d")), TxNum(13)),
            ]
        );

        // Verify spent outputs indexing
        let mut test_spent_rows = vec![];
        assert_eq!(
            decode_spent(&spent_bytes.0, txnum, &mut test_spent_rows)?,
            TxNum(14)
        );
        assert_eq!(test_spent_rows, spent_rows);

        // Verify public interface
        let block: bitcoin::Block = deserialize(&block_bytes.0).unwrap();
        let batch = Batch::build(
            TxNumRange::new(txnum, TxNum(14)),
            block.block_hash(),
            &block_bytes,
            &spent_bytes,
        )?;

        assert_eq!(batch.header.next_txnum(), TxNum(14));
        assert_eq!(batch.header.hash(), block.block_hash());
        assert_eq!(batch.scripthash_rows, [block_rows, spent_rows].concat());

        Ok(())
    }

    fn decode_spent(
        buf: &[u8],
        txnum: TxNum,
        rows: &mut Vec<HashPrefixRow>,
    ) -> Result<TxNum, Error> {
        let mut visitor = IndexVisitor::new(txnum, rows);
        let mut r = bitcoin::io::Cursor::new(buf);
        let txs_count = bitcoin::VarInt::consensus_decode_from_finite_reader(&mut r)?.0;
        for _ in 0..txs_count {
            let outputs_count = bitcoin::VarInt::consensus_decode_from_finite_reader(&mut r)?.0;
            for _ in 0..outputs_count {
                let output = bitcoin::TxOut::consensus_decode_from_finite_reader(&mut r)?;
                visitor.add(&output.script_pubkey);
            }
            visitor.finish_tx();
        }
        let pos: usize = r.position().try_into().unwrap();
        if pos == buf.len() {
            Ok(visitor.txnum)
        } else {
            Err(Error::Leftover(buf.len() - pos))
        }
    }
}