sidestr-core 0.4.1

sidestr user-activated sidechains beside a Bitcoin-family parent: the chain document, the parents table, signed blocks generic over the header family (BIP-325 challenge, no subsidy), the peg-in claim and peg-out burn rules, Knots' unified sighash, the block file and an in-memory validating chain. A port of siding by Melvin Carvalho.
Documentation
//! The parents a chain can sit beside (SPEC 3.2): a short alias per chain,
//! the long kernel id it resolves to, and the header family the chain inherits.
//!
//! A port of `siding/lib/parents.mjs`. Old spellings (the kernel's long ids
//! such as `btc:testnet4-blake2b`) stay accepted, so no running chain's
//! document changes. `mainnet` picks the key and address encodings a node
//! expects (WIF `0x80` vs `0xef`).
//!
//! The alias resolves to the block that fixes which chain is meant: the
//! genesis, and for a fork, the first block on the fork's side, since a fork
//! shares its origin's genesis. A validator that does not know an alias
//! refuses the chain by name.
//!
//! ```
//! use sidestr_core::parents::{resolve_parent, parent_alias, Family};
//!
//! let p = resolve_parent("btc:testnet4").unwrap();
//! assert_eq!(p.alias, "tbtc4");
//! assert_eq!(p.family, Family::Stock);
//! assert_eq!(parent_alias("doge"), None);
//! assert!(resolve_parent("ltc").is_err()); // reserved until a validator carries it
//! ```

use crate::error::{Error, Result};

/// The header family a chain inherits from its parent (SPEC 3): nothing in
/// the document names a header format or a hash; the parent decides both.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Family {
    /// The stock 80-byte Bitcoin header, hashed with SHA256d.
    Stock,
    /// Knots' 164-byte v2 header with BLAKE2b proof of work (`sidestr-header`).
    Blake2b,
}

/// The proof-of-work hash the parent uses; informational for the reserved
/// parents, load-bearing for the two families this table carries.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Pow {
    /// Double SHA-256 (Bitcoin).
    Sha256d,
    /// BLAKE2b over the v2 header (Knots).
    Blake2b,
    /// scrypt (Litecoin; reserved).
    Scrypt,
    /// verthash (Vertcoin; reserved).
    Verthash,
}

/// The block that fixes a fork's identity: the first block on the fork's side.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Fork {
    /// Height of the fork block.
    pub height: u32,
    /// Display-order hash of the fork block.
    pub hash: &'static str,
}

/// One row of the SPEC 3.2 table.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Parent {
    /// The short alias new documents use.
    pub alias: &'static str,
    /// The kernel's long network id, accepted as an old spelling; `None` when reserved.
    pub network: Option<&'static str>,
    /// Human label.
    pub label: &'static str,
    /// Header family the chain inherits.
    pub family: Family,
    /// The parent's proof-of-work hash.
    pub pow: Pow,
    /// Whether keys and addresses use mainnet encodings.
    pub mainnet: bool,
    /// The parent's genesis hash, display order; `None` when reserved.
    pub genesis: Option<&'static str>,
    /// The fork block, for a fork of another chain in the table.
    pub fork: Option<Fork>,
    /// Reserved: in the table so the name is taken, refused until a validator carries its rules.
    pub reserved: bool,
}

/// Bitcoin's coinbase maturity: a mined coin is spendable at 100
/// confirmations. Every parent but `txbt4` asks this, and so does every
/// sidestr chain for its own coinbases ([`crate::rules::Params`]).
pub const COINBASE_MATURITY: u32 = 100;

/// The coinbase maturity a `txbt4` (BLAKE2b testnet4) node asks since Knots
/// 29.4.2: 6,705 confirmations (bitcoin-blake/reef `2bd3cb8`,
/// `lib/wallet.mjs COINBASE_MATURITY`).
pub const TXBT4_COINBASE_MATURITY: u32 = 6_705;

impl Parent {
    /// How deep a coin mined on this **parent** must be before a payment
    /// spending it leaves: [`TXBT4_COINBASE_MATURITY`] beside `txbt4`,
    /// [`COINBASE_MATURITY`] elsewhere.
    ///
    /// Why `txbt4` differs (Reef `2bd3cb8`): by consensus a reward mined at
    /// 151,406 or later needs 6,705 confirmations in blocks 151,550 to
    /// 158,110 (100 before and after, and for older rewards), but the mempool
    /// of every node upgraded to Knots 29.4.2 asks 6,705 of **every** reward,
    /// whatever its height, so a payment spending a younger one is refused
    /// (`bad-txns-premature-spend-of-coinbase`) by the node that relays it
    /// and never leaves. A wallet counts what lets a payment leave. Reef
    /// speaks only of testnet4; `xbt` keeps Bitcoin's 100 until a Knots
    /// release says otherwise for mainnet.
    ///
    /// This is a fact about parent coins, for whatever reports or selects
    /// them. It is not the sidechain's own maturity, which stays 100
    /// ([`crate::rules::Params::coinbase_maturity`]).
    ///
    /// ```
    /// use sidestr_core::parents::resolve_parent;
    /// assert_eq!(resolve_parent("txbt4").unwrap().coinbase_maturity(), 6_705);
    /// assert_eq!(resolve_parent("tbtc4").unwrap().coinbase_maturity(), 100);
    /// ```
    pub const fn coinbase_maturity(&self) -> u32 {
        if matches!(self.family, Family::Blake2b) && !self.mainnet {
            TXBT4_COINBASE_MATURITY
        } else {
            COINBASE_MATURITY
        }
    }

    /// Whether a coin on this parent, mined (or confirmed) at `coin_height`,
    /// is spendable with the parent's tip at `tip`: not a coinbase, or at
    /// least [`Self::coinbase_maturity`] confirmations deep, counting its own
    /// block (`tip − coin_height + 1`; Reef `isMature`).
    ///
    /// ```
    /// use sidestr_core::parents::resolve_parent;
    /// let txbt4 = resolve_parent("txbt4").unwrap();
    /// // the reward of block 152,079: refused at 152,201 (123 deep), spendable at 158,783
    /// assert!(!txbt4.is_mature(152_079, true, 152_201));
    /// assert!(txbt4.is_mature(152_079, true, 158_783));
    /// assert!(txbt4.is_mature(152_079, false, 152_079));
    /// ```
    pub const fn is_mature(&self, coin_height: u32, coinbase: bool, tip: u32) -> bool {
        !coinbase || (tip >= coin_height && tip - coin_height + 1 >= self.coinbase_maturity())
    }
}

/// The table itself (SPEC 3.2), in the order the spec lists it.
pub const PARENTS: [Parent; 6] = [
    Parent {
        alias: "btc",
        network: Some("btc:mainnet"),
        label: "Bitcoin mainnet",
        family: Family::Stock,
        pow: Pow::Sha256d,
        mainnet: true,
        genesis: Some("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"),
        fork: None,
        reserved: false,
    },
    Parent {
        alias: "tbtc4",
        network: Some("btc:testnet4"),
        label: "Bitcoin testnet4",
        family: Family::Stock,
        pow: Pow::Sha256d,
        mainnet: false,
        genesis: Some("00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043"),
        fork: None,
        reserved: false,
    },
    Parent {
        alias: "xbt",
        network: Some("btc:mainnet-blake2b"),
        label: "BLAKE2b mainnet (Knots)",
        family: Family::Blake2b,
        pow: Pow::Blake2b,
        mainnet: true,
        genesis: Some("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"),
        fork: Some(Fork {
            height: 961_640,
            hash: "0000000000000050c1e5f69672f459293be14f46e5a494e7a8c8541396f18eeb",
        }),
        reserved: false,
    },
    Parent {
        alias: "txbt4",
        network: Some("btc:testnet4-blake2b"),
        label: "BLAKE2b testnet4 (Knots)",
        family: Family::Blake2b,
        pow: Pow::Blake2b,
        mainnet: false,
        genesis: Some("00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043"),
        fork: Some(Fork {
            height: 150_308,
            hash: "000000000000b9d1b7e1bb0e77215ee92c6ef7ec8f4473e23908380649e779b6",
        }),
        reserved: false,
    },
    Parent {
        alias: "ltc",
        network: None,
        label: "Litecoin mainnet",
        family: Family::Stock,
        pow: Pow::Scrypt,
        mainnet: true,
        genesis: None,
        fork: None,
        reserved: true,
    },
    Parent {
        alias: "vtc",
        network: None,
        label: "Vertcoin mainnet",
        family: Family::Stock,
        pow: Pow::Verthash,
        mainnet: true,
        genesis: None,
        fork: None,
        reserved: true,
    },
];

/// Alias or long id → the alias; `None` when neither (`siding/lib/parents.mjs parentAlias`).
pub fn parent_alias(id: &str) -> Option<&'static str> {
    PARENTS
        .iter()
        .find(|p| p.alias == id || p.network == Some(id))
        .map(|p| p.alias)
}

/// Alias or long id → the table row; an error for a reserved or unknown parent
/// (`siding/lib/parents.mjs resolveParent`).
pub fn resolve_parent(id: &str) -> Result<&'static Parent> {
    let alias = parent_alias(id).ok_or_else(|| Error::UnknownParent(id.to_string()))?;
    let p = PARENTS
        .iter()
        .find(|p| p.alias == alias)
        .expect("alias came from the table");
    if p.reserved {
        return Err(Error::ReservedParent {
            alias: p.alias,
            label: p.label,
        });
    }
    Ok(p)
}

/// Whether the parent hands down the BLAKE2b header family.
pub fn is_blake2b(id: &str) -> Result<bool> {
    Ok(resolve_parent(id)?.family == Family::Blake2b)
}

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

    // siding/test/parents-test.mjs
    #[test]
    fn table() {
        assert_eq!(
            resolve_parent("txbt4").unwrap().network,
            Some("btc:testnet4-blake2b")
        );
        assert_eq!(
            resolve_parent("btc:testnet4-blake2b").unwrap().alias,
            "txbt4"
        );
        assert_eq!(
            resolve_parent("xbt").unwrap().network,
            Some("btc:mainnet-blake2b")
        );
        assert_eq!(resolve_parent("btc:mainnet-blake2b").unwrap().alias, "xbt");
        let by = |a: &str| PARENTS.iter().find(|p| p.alias == a).unwrap();
        assert_eq!(by("xbt").genesis, by("btc").genesis);
        assert_eq!(by("txbt4").genesis, by("tbtc4").genesis);
        assert_eq!(by("txbt4").fork.unwrap().height, 150_308);
        assert_eq!(by("xbt").fork.unwrap().height, 961_640);
        assert!(is_blake2b("txbt4").unwrap() && is_blake2b("xbt").unwrap());
        assert!(!is_blake2b("btc").unwrap() && !is_blake2b("tbtc4").unwrap());
        assert_eq!(parent_alias("xbt4"), None);
        assert!(matches!(
            resolve_parent("ltc"),
            Err(Error::ReservedParent { alias: "ltc", .. })
        ));
        let e = resolve_parent("doge").unwrap_err().to_string();
        assert!(e.contains("unknown parent \"doge\"") && e.contains("txbt4"));
        assert!(resolve_parent("xbt").unwrap().mainnet && resolve_parent("btc").unwrap().mainnet);
        assert!(
            !resolve_parent("txbt4").unwrap().mainnet
                && !resolve_parent("btc:testnet4-blake2b").unwrap().mainnet
        );
        for name in ["__proto__", "constructor", "toString", ""] {
            assert_eq!(parent_alias(name), None);
        }
    }

    // bitcoin-blake/reef test/wallet-test.mjs at 2bd3cb8, counted against the parent's fact
    #[test]
    fn parent_coinbase_maturity() {
        let txbt4 = resolve_parent("txbt4").unwrap();
        assert_eq!(txbt4.coinbase_maturity(), 6_705);
        // mature at exactly 6,705 confirmations, not 6,704
        assert!(txbt4.is_mature(100, true, 6_804));
        assert!(!txbt4.is_mature(100, true, 6_803));
        // the reward of block 152,079 is not spendable at 152,201 and is at 158,783
        assert!(!txbt4.is_mature(152_079, true, 152_201));
        assert!(txbt4.is_mature(152_079, true, 158_783));
        assert!(!txbt4.is_mature(152_079, true, 158_782));
        // a coin that is not a reward is spendable when confirmed; a tip below the coin is not
        assert!(txbt4.is_mature(152_079, false, 152_079));
        assert!(!txbt4.is_mature(152_079, true, 152_000));
        for alias in ["btc", "tbtc4", "xbt"] {
            let p = resolve_parent(alias).unwrap();
            assert_eq!(p.coinbase_maturity(), 100, "{alias}");
            assert!(p.is_mature(100, true, 199) && !p.is_mature(100, true, 198));
        }
        // the sidechain's own rule is untouched
        assert_eq!(crate::rules::Params::default().coinbase_maturity, 100);
    }
}