use crate::document::ChainDocument;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Family {
Stock,
Blake2b,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Pow {
Sha256d,
Blake2b,
Scrypt,
Verthash,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Fork {
pub height: u32,
pub hash: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Parent {
pub alias: &'static str,
pub network: Option<&'static str>,
pub label: &'static str,
pub family: Family,
pub pow: Pow,
pub mainnet: bool,
pub genesis: Option<&'static str>,
pub fork: Option<Fork>,
pub reserved: bool,
}
pub const COINBASE_MATURITY: u32 = 100;
pub const TXBT4_COINBASE_MATURITY: u32 = 6_705;
impl Parent {
pub const fn coinbase_maturity(&self) -> u32 {
if matches!(self.family, Family::Blake2b) && !self.mainnet {
TXBT4_COINBASE_MATURITY
} else {
COINBASE_MATURITY
}
}
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())
}
}
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,
},
];
pub fn parent_alias(id: &str) -> Option<&'static str> {
PARENTS
.iter()
.find(|p| p.alias == id || p.network == Some(id))
.map(|p| p.alias)
}
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)
}
pub fn is_blake2b(id: &str) -> Result<bool> {
Ok(resolve_parent(id)?.family == Family::Blake2b)
}
pub const MAX_NESTING: u32 = 16;
pub fn is_chain_hash_parent(id: &str) -> bool {
id.len() == 64 && id.bytes().all(|b| b.is_ascii_hexdigit())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NestedParent {
pub hash: String,
pub alias: String,
pub path: Vec<String>,
pub root: &'static Parent,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParentRef {
Table(&'static Parent),
Chain(NestedParent),
}
impl ParentRef {
pub fn root(&self) -> &'static Parent {
match self {
ParentRef::Table(p) => p,
ParentRef::Chain(n) => n.root,
}
}
pub fn family(&self) -> Family {
self.root().family
}
pub fn pow(&self) -> Pow {
self.root().pow
}
pub fn mainnet(&self) -> bool {
self.root().mainnet
}
pub fn depth(&self) -> u32 {
match self {
ParentRef::Table(_) => 0,
ParentRef::Chain(n) => n.path.len() as u32,
}
}
pub fn chain_hash(&self) -> Option<&str> {
match self {
ParentRef::Table(_) => None,
ParentRef::Chain(n) => Some(&n.hash),
}
}
pub fn coinbase_maturity(&self) -> u32 {
match self {
ParentRef::Table(p) => p.coinbase_maturity(),
ParentRef::Chain(_) => COINBASE_MATURITY,
}
}
}
pub fn resolve_parent_with<F>(id: &str, mut lookup: F) -> Result<ParentRef>
where
F: FnMut(&str) -> Result<ChainDocument>,
{
if !is_chain_hash_parent(id) {
return resolve_parent(id).map(ParentRef::Table);
}
let hash = id.to_ascii_lowercase();
let mut path: Vec<String> = Vec::new();
let mut alias = None;
let mut next = hash.clone();
while (path.len() as u32) < MAX_NESTING {
if path.contains(&next) {
return Err(Error::Document(format!(
"parent chain {next} names itself among its own parents"
)));
}
let doc = lookup(&next)?;
alias.get_or_insert_with(|| doc.id.clone());
path.push(next);
if !is_chain_hash_parent(&doc.parent) {
let root = resolve_parent(&doc.parent)?;
return Ok(ParentRef::Chain(NestedParent {
hash,
alias: alias.expect("set on the first document"),
path,
root,
}));
}
next = doc.parent.to_ascii_lowercase();
}
Err(Error::Document(format!(
"parent chain {hash} is nested more than {MAX_NESTING} chains deep"
)))
}
#[cfg(test)]
mod tests {
use super::*;
#[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);
}
}
fn doc_beside(parent: &str, name: &str) -> ChainDocument {
let d = ChainDocument::from_json(include_str!("../fixtures/trial/chain.json")).unwrap();
ChainDocument {
id: format!("sidestr:{name}"),
name: name.into(),
parent: parent.into(),
..d
}
}
#[test]
fn a_chain_hash_resolves_through_the_parent_chain_s_document() {
let (a, b) = ("aa".repeat(32), "BB".repeat(32));
let docs = |h: &str| -> Result<ChainDocument> {
match h {
x if x == "aa".repeat(32) => Ok(doc_beside(&"BB".repeat(32), "a")),
x if x == "bb".repeat(32) => Ok(doc_beside("btc:testnet4-blake2b", "b")),
other => Err(Error::Document(format!("no chain event {other}"))),
}
};
let p = resolve_parent_with(&a, docs).unwrap();
assert_eq!(p.family(), Family::Blake2b);
assert_eq!(p.pow(), Pow::Blake2b);
assert!(!p.mainnet());
assert_eq!(p.root().alias, "txbt4");
assert_eq!(p.depth(), 2);
assert_eq!(p.chain_hash(), Some(a.as_str()));
assert_eq!(p.coinbase_maturity(), COINBASE_MATURITY);
let ParentRef::Chain(n) = &p else { panic!() };
assert_eq!(n.alias, "sidestr:a");
assert_eq!(n.path, vec![a.clone(), b.to_ascii_lowercase()]);
assert_eq!(resolve_parent_with(&a.to_uppercase(), docs).unwrap(), p);
let stock = resolve_parent_with(&a, |_| Ok(doc_beside("tbtc4", "s"))).unwrap();
assert_eq!(stock.family(), Family::Stock);
assert_eq!(stock.depth(), 1);
let e = resolve_parent_with(&"cc".repeat(32), docs).unwrap_err();
assert!(e.to_string().contains("no chain event"), "{e}");
}
#[test]
fn a_table_parent_never_asks_the_lookup_and_answers_as_resolve_parent() {
for id in ["btc", "tbtc4", "xbt", "txbt4", "btc:testnet4-blake2b"] {
let p = resolve_parent_with(id, |_| panic!("no lookup for {id}")).unwrap();
assert_eq!(p, ParentRef::Table(resolve_parent(id).unwrap()));
assert_eq!(p.depth(), 0);
assert_eq!(p.chain_hash(), None);
assert_eq!(
p.coinbase_maturity(),
resolve_parent(id).unwrap().coinbase_maturity()
);
}
for id in ["ltc", "doge", "", &"ab".repeat(31)] {
assert_eq!(
resolve_parent_with(id, |_| panic!("no lookup"))
.unwrap_err()
.to_string(),
resolve_parent(id).unwrap_err().to_string()
);
}
}
#[test]
fn nesting_refuses_a_cycle_a_bad_root_and_unbounded_depth() {
let (a, b) = ("aa".repeat(32), "bb".repeat(32));
let cyc = resolve_parent_with(&a, |h| Ok(doc_beside(if h == a { &b } else { &a }, "c")))
.unwrap_err();
assert!(cyc.to_string().contains("names itself"), "{cyc}");
let reserved = resolve_parent_with(&a, |_| Ok(doc_beside("ltc", "l"))).unwrap_err();
assert!(matches!(
reserved,
Error::ReservedParent { alias: "ltc", .. }
));
let unknown = resolve_parent_with(&a, |_| Ok(doc_beside("doge", "d"))).unwrap_err();
assert!(matches!(unknown, Error::UnknownParent(_)));
let mut n = 0u32;
let deep = resolve_parent_with(&a, |_| {
n += 1;
Ok(doc_beside(&format!("{n:064x}"), "deep"))
})
.unwrap_err();
assert!(deep.to_string().contains("more than 16"), "{deep}");
assert_eq!(n, MAX_NESTING);
let mut m = 0u32;
let edge = resolve_parent_with(&a, |_| {
m += 1;
Ok(if m == MAX_NESTING {
doc_beside("tbtc4", "edge")
} else {
doc_beside(&format!("{m:064x}"), "edge")
})
})
.unwrap();
assert_eq!(edge.depth(), MAX_NESTING);
}
#[test]
fn parent_coinbase_maturity() {
let txbt4 = resolve_parent("txbt4").unwrap();
assert_eq!(txbt4.coinbase_maturity(), 6_705);
assert!(txbt4.is_mature(100, true, 6_804));
assert!(!txbt4.is_mature(100, true, 6_803));
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));
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));
}
assert_eq!(crate::rules::Params::default().coinbase_maturity, 100);
}
}