txoo 0.10.0

A Bitcoin transaction-output oracle
Documentation
//! An oracle for the Bitcoin network that produces attestations to the chain tip and to the outputs spent in each block.

#![forbid(unsafe_code)]
#![warn(rustdoc::broken_intra_doc_links)]
#![warn(missing_docs)]
#![cfg_attr(all(not(feature = "std"), not(test)), no_std)]

#[cfg(not(any(feature = "std", feature = "no-std")))]
compile_error!("at least one of the `std` or `no-std` features must be enabled");

use core::str::FromStr;
use bitcoin::io;

extern crate alloc;
extern crate core;

pub use bitcoin;

use alloc::vec::Vec;
use bitcoin::consensus::{Decodable, Encodable};
use bitcoin::hashes::hex::FromHex;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::{Hash, HashEngine};
use bitcoin::secp256k1::constants::SCHNORR_SIGNATURE_SIZE;
use bitcoin::secp256k1::schnorr::Signature;
use bitcoin::secp256k1::{All, Message, PublicKey, Secp256k1};
use bitcoin::{secp256k1, BlockHash, Network};
use bitcoin::blockdata::block::Header as BlockHeader;
// Fun Fact, this change is usless because in bitcoin .32 the
// FilterHeader return under the bitcoin root crate again. Meh!
use bitcoin::hash_types::FilterHeader;
#[cfg(feature = "use-serde")]
use serde::{Deserialize, Serialize};

/// A GCS filter for spent outpoints in a block
pub mod filter;
/// Oracle proof that outpoints were spent or unspent in a block
pub mod proof;
/// Attestation source
#[cfg(all(feature = "source", feature = "std"))]
pub mod source;
/// SPV proof that transactions are included in a block
pub mod spv;
/// Utilities
pub mod util;

/// An attestation by the oracle to a certain chain tip and spend filter at a certain time
#[derive(Clone, Debug)]
#[cfg_attr(feature = "use-serde", derive(Serialize, Deserialize))]
pub struct Attestation {
    /// The block hash of the chain tip
    pub block_hash: BlockHash,
    /// The block height of the chain tip
    pub block_height: u32,
    /// The filter header of the chain tip (hash of the previous header and the GCS filter)
    pub filter_header: FilterHeader,
    /// The time the attestation was created, as seconds since the unix epoch
    pub time: u64,
}

impl Decodable for Attestation {
    fn consensus_decode<D: io::Read + ?Sized>(
        d: &mut D,
    ) -> Result<Self, bitcoin::consensus::encode::Error> {
        let block_hash = Decodable::consensus_decode(d)?;
        let block_height = Decodable::consensus_decode(d)?;
        let filter_header = Decodable::consensus_decode(d)?;
        let time = Decodable::consensus_decode(d)?;
        Ok(Attestation {
            block_hash,
            block_height,
            filter_header,
            time,
        })
    }
}

impl Encodable for Attestation {
    fn consensus_encode<S: io::Write + ?Sized>(&self, s: &mut S) -> Result<usize, io::Error> {
        let mut len = 0;
        len += self.block_hash.consensus_encode(s)?;
        len += self.block_height.consensus_encode(s)?;
        len += self.filter_header.consensus_encode(s)?;
        len += self.time.consensus_encode(s)?;
        Ok(len)
    }
}

impl Attestation {
    /// Signature hash for the attestation
    pub fn hash(&self) -> Sha256Hash {
        let mut engine = Sha256Hash::engine();
        engine.input(&self.block_hash[..]);
        engine.input(&self.block_height.to_le_bytes());
        engine.input(&self.filter_header[..]);
        engine.input(&self.time.to_le_bytes());
        Sha256Hash::from_engine(engine)
    }
}

/// A signed attestation
#[derive(Clone)]
#[cfg_attr(feature = "use-serde", derive(Serialize, Deserialize))]
pub struct SignedAttestation {
    /// The attestation
    pub attestation: Attestation,
    /// The schnorr signature over the attestation, serialized as TBD
    pub signature: Signature,
}

impl Encodable for SignedAttestation {
    fn consensus_encode<S: io::Write + ?Sized>(&self, s: &mut S) -> Result<usize, io::Error> {
        let mut len = 0;
        len += self.attestation.consensus_encode(s)?;
        s.write_all(&self.signature[..])?;
        len += SCHNORR_SIGNATURE_SIZE;
        Ok(len)
    }
}

impl Decodable for SignedAttestation {
    fn consensus_decode<D: io::Read + ?Sized>(
        d: &mut D,
    ) -> Result<Self, bitcoin::consensus::encode::Error> {
        let attestation = Decodable::consensus_decode(d)?;
        let mut signature = [0u8; SCHNORR_SIGNATURE_SIZE];
        d.read_exact(&mut signature)?;
        let signature = Signature::from_slice(&signature).expect("signature is valid");
        Ok(SignedAttestation {
            attestation,
            signature,
        })
    }
}

impl SignedAttestation {
    /// Verify the attestation signature
    pub fn verify(&self, pubkey: &PublicKey, secp: &Secp256k1<All>) -> bool {
        let xpubkey = secp256k1::XOnlyPublicKey::from(pubkey.clone());
        let message = Message::from_digest(self.attestation.hash().to_byte_array());
        secp.verify_schnorr(&self.signature, &message, &xpubkey)
            .is_ok()
    }
}

/// Basic information about an oracle
#[derive(Clone, Debug)]
#[cfg_attr(feature = "use-serde", derive(Serialize, Deserialize))]
pub struct OracleSetup {
    /// The network
    pub network: Network,
    /// The first block the oracle produced attestations for.
    /// Note that the `prev_filter_header` is defined as all zeros for this block.
    /// This parameter cannot be changed without invalidating all the oracle's attestations.
    pub start_block: u32,
    /// The oracle public key
    pub public_key: PublicKey,
}

/// Mainnet checkpoints.
/// (height, block_hash, filter_header, block_header)
/// Note that we start a fresh txood install from the *first* checkpoint, not the last one.
pub const CHECKPOINTS_BITCOIN: &[(u32, &str, &str, &str)] = &[
    (
        717500,
        "0000000000000000000226bd443784c02a0ad04be78d5966ecccc727129454c8",
        "5494f5e59cdb4816d69f9777d936a7dab5826e1beb8b1e1c3f88cb54b6ff49e4",
        "006000200080f7688389afc62f5e659a5cc96f955e3766a883520100000000000000000077b008af850244de0974ed664f91788dff044815d59f40c8d1b885df9de0ce888c67d761ab980b173be525b5",
    ),
    (
        770000,
        "00000000000000000004ea65f5ffe55bfc0adbc001d3a8e154cc9f19da959ba8",
        "b9bfb0048201dc42c7a35c479570925959782be3736a1bc32d072e84e9bf16e4",
        "00004020574e3006158b4eb0bbb58d9705026baa85f36229d5d6050000000000000000007bbc932b46eb980ad91425923b8548b2c698f70d17b84ed99d7e91899d7ea64b91a3b26390f5071788d1480e",
    ),
    (
        812000,
        "00000000000000000000ebd093127365a54c5c9332362757426c8b9fac719e40",
        "bb46d92c9b542c2e353072a49dc5d0f96cc49dfe18ff32b5389de1ebaab9c7a3",
        "000020205491c41bff43317d8cc7e5969c820f5673934294e35f00000000000000000000b1c05152830de98123de79c4aac7301620f7e622d2c4c67236bb48ce85403d8bab4729650fe90417758d6ddc",
    ),
    (
        849700,
        "000000000000000000018dd05c43695521b8adbb61e686644933fb4d08bf2ed6",
        "b8eef088c0bb4684f6d12747d7816aee24017d20b67ae5c663dd45b64811c87f",
        "00a0632521b2a87f158aef31a1b7f1336fa61c70473c90c2557801000000000000000000af6dc886c386d8bf479e1ff1013eeae5ab19cd595e276ffe284fceda9358b8eb64847d66255d0317c8ee3111",
    )
];

/// Testnet checkpoints: (height, block_hash, filter_header, block_header)
///
/// Note that `txood --use-checkpoint` starts from the *first* checkpoint, not the last one, so that attestations
/// are generated for old clients.
pub const CHECKPOINTS_TESTNET: &[(u32, &str, &str, &str)] = &[
    (
        2425000,
        "00000000898adbf7816acbb9bc46f58260c7e58a9a0018dd22a4bb20dae2b12f",
        "0223b6153cc868f8a3bfa7aa17e8db28ee58300a1a7e584905bf834dec1348ca",
        "0000002075212b88091673403f6d7efb9d43ec5004be49ad59a1b3ccf4e60000000000001f0989025687d3ce94fabd6837cc2da9c94122561be7813ea13968079813c6888d351664ffff001df6dc1b1f",
    ),
    (
        2469000,
        "00000000000000a359f92219e8e3634ae3d0fd48f1978a4c58f8eb8af358f069",
        "b95e23ecf2e90f58c430070452100818e3b2a923e7d010db14bcb1ddd192896b",
        "000000205da9faffdc6ee5ad3a47579e3048770dcf3600558bd33acead0000000000000031d6ec4fc91ad4498f6dbf0406a7fd2fd4aca17d7d40ecdc2a5bcf32c42b6d3b78ecc2647bdd001a49755bd5",
    ),
    (
        2542800,
        "00000000000037f642b41b676f2fbeb26758cb935e4748de64972b08c19ac265",
        "16547889b0e0116531f807af394f7d4f230eb3789b25f8298772ff14a3f74359",
        "0000e02022abb275f6de8b255f070ac6ffcb7c9e81a28b28605cf7861b1a000000000000dd7810a2fb2de9cf67747d16b44e5945e55deb04fffe7a234897138937c4705cc3bc7a65ffff001d65b009f3",
    ),
    (
        2577600,
        "000000000000001077f67343afc9a58e5f8faeca3bf0c2b7497fe8c9c9c540a7",
        "0cbc28a0b1ab22ea1e2728521e042ae2582ec3da7e706f60fd52317f44b34724",
        "0000a020468ec3433406edd4e501bd3d9d548cd41b0cde1d2438c5c91c0000000000000011cad3107167cadb1c512182b953cd5a9c64202c152fe9d370589b42d994b85a1b4cc665b575211984b5c8e4",
    ),
    (
        2862000,
        "00000000000003ab245d9f16d6add79ace94f2538e36d25705819fcb4b481e0a",
        "1e1cbedecdba81ebaf4c2577e76304e0344471cbdddf1a5ea282ae375a08339c",
        "0000a020917a8253a0c5c8b82c7825ce2650c94bfeb0267ea1193ba8591a39ef0000000099003d46370e4f1efe93b18a9e365cb5fe24e191b2f8220b7cd2bfa8bdaadffb39a27d66fcff031a04107bd9",
    ),
];

/// Decode a checkpoint tuple
pub fn decode_checkpoint(
    checkpoint: (u32, &str, &str, &str),
) -> (u32, BlockHash, FilterHeader, BlockHeader) {
    let (height, block_hash_hex, filter_header_hex, block_header_hex) = checkpoint;
    let block_hash = BlockHash::from_str(block_hash_hex).unwrap();
    let filter_header = FilterHeader::from_str(filter_header_hex).unwrap();
    let block_header_bytes = Vec::from_hex(block_header_hex).unwrap();
    let block_header = BlockHeader::consensus_decode(&mut block_header_bytes.as_slice()).unwrap();
    assert_eq!(block_header.block_hash(), block_hash);
    (height, block_hash, filter_header, block_header)
}

/// Get the latest checkpoint for the given network
pub fn get_latest_checkpoint(
    network: Network,
) -> Option<(u32, BlockHash, FilterHeader, BlockHeader)> {
    let checkpoints = match network {
        Network::Bitcoin => CHECKPOINTS_BITCOIN,
        Network::Testnet => CHECKPOINTS_TESTNET,
        _ => return None,
    };
    Some(decode_checkpoint(checkpoints[checkpoints.len() - 1]))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn checkpoints_valid_test() {
        // we depend on the assert inside decode_checkpoint
        for checkpoint in CHECKPOINTS_BITCOIN {
            decode_checkpoint(*checkpoint);
        }

        for checkpoint in CHECKPOINTS_TESTNET {
            decode_checkpoint(*checkpoint);
        }
    }
}