use std::collections::{BTreeMap, HashMap, HashSet};
use bitcoin::hashes::Hash;
use bitcoin::{BlockHash, OutPoint, Script, Target, Transaction, TxOut, Txid};
use crate::block::{
block_weight, merkle_root_of_txs, verify_block_signature, witness_root_of_txs, HeaderFamily,
SidestrBlock,
};
use crate::marker::{looks_like_pegout, parse_claims, parse_pegout, Burn};
use crate::sighash::verify_taproot_key_path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Params {
pub max_block_weight: u64,
pub max_money: u64,
pub max_block_sigops_cost: u64,
pub coinbase_maturity: u32,
pub max_future_block_time: u32,
pub bip34_height: u32,
pub segwit_height: u32,
pub bip65_height: u32,
pub bip66_height: u32,
}
impl Default for Params {
fn default() -> Self {
Self {
max_block_weight: 4_000_000,
max_money: 2_100_000_000_000_000,
max_block_sigops_cost: 80_000,
coinbase_maturity: 100,
max_future_block_time: 7_200,
bip34_height: 1,
segwit_height: 0,
bip65_height: 1,
bip66_height: 1,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuleResult {
pub rule: String,
pub ok: Option<bool>,
}
impl RuleResult {
pub fn new(rule: &str, ok: Option<bool>) -> Self {
Self {
rule: rule.to_string(),
ok,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Verdict {
pub results: Vec<RuleResult>,
}
impl Verdict {
fn push(&mut self, rule: &str, ok: Option<bool>) {
self.results.push(RuleResult::new(rule, ok));
}
pub fn ok(&self) -> bool {
self.results.iter().all(|r| r.ok != Some(false))
}
pub fn failed(&self) -> Vec<String> {
self.results
.iter()
.filter(|r| r.ok == Some(false))
.map(|r| r.rule.clone())
.collect()
}
pub fn extend(&mut self, other: Verdict) {
self.results.extend(other.results);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Coin {
pub output: TxOut,
pub height: u32,
pub coinbase: bool,
}
pub type Utxo = HashMap<OutPoint, Coin>;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Records {
pub claims: HashMap<(String, u32), u32>,
pub pegouts: BTreeMap<(String, u32), Burn>,
}
impl Records {
pub fn claimed(&self, txid: &str, vout: u32) -> bool {
self.claims.contains_key(&(txid.to_string(), vout))
}
pub fn pegouts(&self) -> Vec<Burn> {
let mut v: Vec<Burn> = self.pegouts.values().cloned().collect();
v.sort_by_key(|b| b.height);
v
}
}
#[derive(Debug, Clone)]
pub struct Overlay<'a> {
pub challenge: &'a Script,
pub pegout_min: u64,
pub genesis_subsidy: u64,
}
#[derive(Debug, Clone)]
pub struct HeaderContext<'a, F: HeaderFamily> {
pub height: u32,
pub prev: Option<&'a F::Header>,
pub mtp_window: &'a [F::Header],
pub now: Option<u32>,
}
pub fn median_time_past<F: HeaderFamily>(family: &F, window: &[F::Header]) -> u32 {
let start = window.len().saturating_sub(11);
let mut times: Vec<u32> = window[start..].iter().map(|h| family.time(h)).collect();
times.sort_unstable();
times[times.len() >> 1]
}
pub fn validate_header<F: HeaderFamily>(
family: &F,
params: &Params,
header: &F::Header,
ctx: &HeaderContext<F>,
) -> Verdict {
let mut v = Verdict::default();
v.push(
"btc:rule-header-prev-link",
ctx.prev
.map(|p| family.prev(header) == family.block_hash(p)),
);
v.push(
"btc:rule-header-pow",
Some(Target::from_compact(family.bits(header)).is_met_by(family.block_hash(header))),
);
v.push(
"btc:rule-header-difficulty",
ctx.prev.map(|p| family.bits(header) == family.bits(p)),
);
let need = 11.min(ctx.height as usize);
v.push(
"btc:rule-header-mtp",
(ctx.mtp_window.len() >= need && !ctx.mtp_window.is_empty())
.then(|| family.time(header) > median_time_past(family, ctx.mtp_window)),
);
v.push(
"btc:rule-header-time-future",
ctx.now.map(|now| {
u64::from(family.time(header))
<= u64::from(now) + u64::from(params.max_future_block_time)
}),
);
let min_version = if ctx.height >= params.bip65_height {
4
} else if ctx.height >= params.bip66_height {
3
} else if ctx.height >= params.bip34_height {
2
} else {
1
};
v.push(
"btc:rule-header-version",
Some(family.version_number(header) >= min_version),
);
v.push("btc:rule-header-timewarp", None); v.results.extend(family.header_rules(header, ctx.height));
v
}
fn is_coinbase(tx: &Transaction) -> bool {
tx.input.len() == 1 && tx.input[0].previous_output == OutPoint::null()
}
fn sum_out(tx: &Transaction) -> Option<u64> {
tx.output
.iter()
.try_fold(0u64, |s, o| s.checked_add(o.value.to_sat()))
}
pub fn validate_transaction(params: &Params, tx: &Transaction, coinbase: bool) -> Verdict {
let mut v = Verdict::default();
v.push("btc:rule-tx-inputs-nonempty", Some(!tx.input.is_empty()));
v.push("btc:rule-tx-outputs-nonempty", Some(!tx.output.is_empty()));
v.push(
"btc:rule-tx-size",
Some(tx.weight().to_wu() <= params.max_block_weight),
);
v.push(
"btc:rule-tx-output-values",
Some(
tx.output
.iter()
.all(|o| o.value.to_sat() <= params.max_money)
&& sum_out(tx).is_some_and(|s| s <= params.max_money),
),
);
v.push(
"btc:rule-tx-inputs-unique",
Some(
tx.input
.iter()
.map(|i| i.previous_output)
.collect::<HashSet<_>>()
.len()
== tx.input.len(),
),
);
v.push(
"btc:rule-tx-prevouts",
Some(if coinbase {
is_coinbase(tx)
} else {
tx.input
.iter()
.all(|i| i.previous_output.txid != Txid::all_zeros())
}),
);
v.push(
"btc:rule-tx-coinbase-script",
coinbase.then(|| {
tx.input
.first()
.is_some_and(|i| (2..=100).contains(&i.script_sig.len()))
}),
);
v
}
fn legacy_sigops(txdata: &[Transaction]) -> u64 {
txdata.iter().fold(0u64, |n, tx| {
let ins = tx.input.iter().fold(0u64, |n, i| {
n.saturating_add(i.script_sig.count_sigops_legacy() as u64)
});
let outs = tx.output.iter().fold(0u64, |n, o| {
n.saturating_add(o.script_pubkey.count_sigops_legacy() as u64)
});
n.saturating_add(ins).saturating_add(outs)
})
}
pub fn validate_block_structure<F: HeaderFamily>(
family: &F,
params: &Params,
overlay: &Overlay,
block: &F::Block,
) -> Verdict {
let txdata = block.txdata();
let mut v = Verdict::default();
let txids: Vec<Txid> = txdata.iter().map(Transaction::compute_txid).collect();
v.push(
"btc:rule-block-coinbase-first",
Some(txdata.first().is_some_and(is_coinbase)),
);
v.push(
"btc:rule-block-coinbase-single",
Some(txdata.iter().skip(1).all(|tx| !is_coinbase(tx))),
);
v.push(
"btc:rule-block-merkle-root",
Some(
!txdata.is_empty() && merkle_root_of_txs(txdata) == family.merkle_root(block.header()),
),
);
v.push(
"btc:rule-block-tx-duplicates",
Some(txids.iter().collect::<HashSet<_>>().len() == txids.len()),
);
v.push(
"btc:rule-block-sigops",
Some(legacy_sigops(txdata).saturating_mul(4) <= params.max_block_sigops_cost),
);
v.push(
"btc:rule-block-weight",
Some(block_weight(family, block) <= params.max_block_weight),
);
v.push(
"btc:rule-block-transactions",
Some(
txdata
.iter()
.enumerate()
.all(|(i, tx)| validate_transaction(params, tx, i == 0).ok()),
),
);
v.push(
"sidestr:rule-block-signature",
Some(verify_block_signature(family, block, overlay.challenge)),
);
v.results
.extend(family.block_rules(block.header(), txdata.len()));
v
}
#[derive(Debug, Clone, Default)]
pub struct Spending {
pub fees: u64,
pub missing: Vec<OutPoint>,
pub deficits: Vec<Txid>,
pub premature: Vec<OutPoint>,
pub seqlock_violations: Vec<OutPoint>,
pub seqlock_unknown: usize,
pub resolved: Vec<(usize, usize, TxOut)>,
}
pub fn resolve_spending(
params: &Params,
txdata: &[Transaction],
utxo: &Utxo,
height: u32,
) -> Spending {
const SEQ_DISABLE: u32 = 0x8000_0000;
const SEQ_TYPE: u32 = 0x0040_0000;
const SEQ_MASK: u32 = 0x0000_ffff;
let mut s = Spending::default();
let mut spent_here: HashSet<OutPoint> = HashSet::new();
let mut created_here: HashMap<OutPoint, Coin> = HashMap::new();
for (ti, tx) in txdata.iter().enumerate() {
let txid = tx.compute_txid();
if ti > 0 {
let mut in_sum = 0u64;
let mut values_ok = true;
for (ii, inp) in tx.input.iter().enumerate() {
let key = inp.previous_output;
if spent_here.contains(&key) {
s.missing.push(key);
values_ok = false;
} else {
let coin = created_here.get(&key).or_else(|| utxo.get(&key));
let mut coin_height = None;
match coin {
Some(c) => {
in_sum = in_sum.saturating_add(c.output.value.to_sat());
s.resolved.push((ti, ii, c.output.clone()));
coin_height = Some(c.height);
if c.coinbase
&& height.saturating_sub(c.height) < params.coinbase_maturity
{
s.premature.push(key);
}
}
None => {
s.missing.push(key);
values_ok = false;
}
}
let seq = inp.sequence.0;
if tx.version.0 >= 2 && seq & SEQ_DISABLE == 0 {
let value = seq & SEQ_MASK;
if value > 0 {
if seq & SEQ_TYPE != 0 {
s.seqlock_unknown += 1;
} else if let Some(ch) = coin_height {
if u64::from(height) < u64::from(ch) + u64::from(value) {
s.seqlock_violations.push(key);
}
} else {
s.seqlock_unknown += 1;
}
}
}
}
spent_here.insert(key);
}
if values_ok {
match sum_out(tx) {
Some(out_sum) if in_sum >= out_sum => {
s.fees = s.fees.saturating_add(in_sum - out_sum)
}
_ => s.deficits.push(txid),
}
}
}
for (vout, o) in tx.output.iter().enumerate() {
if !o.script_pubkey.is_op_return() {
created_here.insert(
OutPoint {
txid,
vout: vout as u32,
},
Coin {
output: o.clone(),
height,
coinbase: ti == 0,
},
);
}
}
}
s
}
#[derive(Debug)]
pub struct BlockContext<'a, F: HeaderFamily> {
pub block: &'a F::Block,
pub height: u32,
pub spending: &'a Spending,
pub mtp: Option<u32>,
pub records: &'a Records,
}
pub trait BlockRule<F: HeaderFamily>: core::fmt::Debug {
fn id(&self) -> &str;
fn check(&self, ctx: &BlockContext<F>) -> Option<bool>;
}
fn bip34_height_lenient(coinbase: &Transaction) -> Option<u32> {
let script = coinbase.input.first()?.script_sig.as_bytes();
let op = *script.first()?;
if (0x51..=0x60).contains(&op) {
return Some(u32::from(op - 0x50));
}
let len = usize::from(op);
if !(1..=5).contains(&len) || script.len() < 1 + len {
return None;
}
let n = (1..=len)
.rev()
.fold(0u64, |n, i| n * 256 + u64::from(script[i]));
u32::try_from(n).ok()
}
fn witness_commitment_in(coinbase: &Transaction) -> Option<[u8; 32]> {
coinbase
.output
.iter()
.rev()
.find(|o| {
o.script_pubkey.len() >= 38
&& o.script_pubkey
.as_bytes()
.starts_with(&[0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed])
})
.and_then(|o| o.script_pubkey.as_bytes()[6..38].try_into().ok())
}
fn witness_commitment_hash(txdata: &[Transaction]) -> [u8; 32] {
let root = witness_root_of_txs(txdata);
let mut cat = [0u8; 64];
cat[..32].copy_from_slice(&root);
if let Some(reserved) = txdata[0]
.input
.first()
.and_then(|i| i.witness.iter().next())
{
let n = reserved.len().min(32);
cat[32..32 + n].copy_from_slice(&reserved[..n]);
}
bitcoin::hashes::sha256d::Hash::hash(&cat).to_byte_array()
}
#[derive(Debug)]
pub struct Candidate<'a, F: HeaderFamily> {
pub block: &'a F::Block,
pub height: u32,
pub utxo: &'a Utxo,
pub mtp: Option<u32>,
pub records: &'a Records,
pub extra: &'a [Box<dyn BlockRule<F>>],
}
pub fn validate_block_context<F: HeaderFamily>(
family: &F,
params: &Params,
overlay: &Overlay,
c: &Candidate<F>,
) -> (Verdict, Spending, Records) {
let Candidate {
block,
height,
utxo,
mtp,
records,
extra,
} = *c;
let txdata = block.txdata();
let spending = resolve_spending(params, txdata, utxo, height);
let mut v = Verdict::default();
let cb = &txdata[0];
let mut next = Records::default();
v.push(
"btc:rule-blockctx-bip34-height",
(height >= params.bip34_height).then(|| bip34_height_lenient(cb) == Some(height)),
);
let mut unknown = false;
let mut final_ok = true;
for tx in txdata {
let lt = tx.lock_time.to_consensus_u32();
if lt == 0 || tx.input.iter().all(|i| i.sequence.0 == 0xffff_ffff) {
continue;
}
if lt < 500_000_000 {
if lt >= height {
final_ok = false;
}
} else if let Some(m) = mtp {
if lt >= m {
final_ok = false;
}
} else {
unknown = true;
}
}
v.push(
"btc:rule-blockctx-finality",
if !final_ok {
Some(false)
} else if unknown {
None
} else {
Some(true)
},
);
v.push(
"btc:rule-blockctx-sequence-locks",
if !spending.seqlock_violations.is_empty() {
Some(false)
} else if spending.seqlock_unknown > 0 {
None
} else {
Some(true)
},
);
v.push(
"btc:rule-blockctx-inputs-available",
Some(spending.missing.is_empty()),
);
v.push(
"btc:rule-blockctx-coinbase-maturity",
Some(spending.premature.is_empty()),
);
v.push("btc:rule-blockctx-fees", Some(spending.deficits.is_empty()));
let (claims, claim_errors) = parse_claims(cb);
let paid = claims
.iter()
.try_fold(0u64, |s, c| s.checked_add(c.payout.value));
let subsidy = if height == 0 {
overlay.genesis_subsidy
} else {
0
};
v.push(
"btc:rule-blockctx-coinbase-amount",
Some(
claim_errors.is_empty()
&& paid.is_some_and(|paid| {
sum_out(cb).is_some_and(|s| {
s <= subsidy.saturating_add(spending.fees).saturating_add(paid)
})
}),
),
);
let has_witness = txdata
.iter()
.any(|tx| tx.input.iter().any(|i| !i.witness.is_empty()));
v.push(
"btc:rule-blockctx-witness-commitment",
(height >= params.segwit_height && has_witness)
.then(|| witness_commitment_in(cb) == Some(witness_commitment_hash(txdata))),
);
let sighash = family.sighash_rules(height);
let mut scripts_ok = true;
let mut by_tx: BTreeMap<usize, BTreeMap<usize, TxOut>> = BTreeMap::new();
for (ti, ii, prevout) in &spending.resolved {
by_tx.entry(*ti).or_default().insert(*ii, prevout.clone());
}
for (ti, resolved) in &by_tx {
let tx = &txdata[*ti];
if resolved.len() != tx.input.len() {
scripts_ok = false;
continue;
}
let prevouts: Vec<TxOut> = (0..tx.input.len()).map(|i| resolved[&i].clone()).collect();
for ii in 0..tx.input.len() {
if verify_taproot_key_path(tx, ii, &prevouts, sighash).is_err() {
scripts_ok = false;
}
}
}
v.push("btc:rule-blockctx-scripts", Some(scripts_ok));
let pegouts_ok = (|| {
if cb
.output
.iter()
.any(|o| parse_pegout(&o.script_pubkey).is_some())
{
return false;
}
for tx in txdata.iter().skip(1) {
let txid = tx.compute_txid().to_string();
for (vout, o) in tx.output.iter().enumerate() {
if !looks_like_pegout(o) {
continue;
}
let Some(script) = parse_pegout(&o.script_pubkey) else {
return false;
};
if o.value.to_sat() < overlay.pegout_min {
return false;
}
let key = (txid.clone(), vout as u32);
if records
.pegouts
.get(&key)
.is_some_and(|b| b.height != height)
{
return false;
}
next.pegouts.insert(
key,
Burn {
txid: txid.clone(),
vout: vout as u32,
script,
value: o.value.to_sat(),
height,
},
);
}
}
true
})();
v.push("sidestr:rule-pegouts", Some(pegouts_ok));
let claims_ok = (|| {
if !claim_errors.is_empty() {
return false;
}
let mut in_block = HashSet::new();
for c in &claims {
let op = (c.txid.clone(), c.vout);
if !in_block.insert(op.clone()) {
return false;
}
if records.claims.get(&op).is_some_and(|&at| at != height) {
return false;
}
}
for op in in_block {
next.claims.insert(op, height);
}
true
})();
v.push("sidestr:rule-claims", Some(claims_ok));
let ctx = BlockContext {
block,
height,
spending: &spending,
mtp,
records,
};
for rule in extra {
let ok = rule.check(&ctx);
v.push(rule.id(), ok);
}
(v, spending, next)
}
pub fn apply_block(utxo: &mut Utxo, txdata: &[Transaction], height: u32) -> (usize, usize) {
let mut created = 0;
let mut spent = 0;
for (i, tx) in txdata.iter().enumerate() {
if i > 0 {
for inp in &tx.input {
if utxo.remove(&inp.previous_output).is_some() {
spent += 1;
}
}
}
let txid = tx.compute_txid();
for (vout, o) in tx.output.iter().enumerate() {
if !o.script_pubkey.is_op_return() {
utxo.insert(
OutPoint {
txid,
vout: vout as u32,
},
Coin {
output: o.clone(),
height,
coinbase: i == 0,
},
);
created += 1;
}
}
}
(created, spent)
}
pub fn genesis_hash<F: HeaderFamily>(family: &F, block: &F::Block) -> BlockHash {
family.block_hash(block.header())
}