1use std::collections::{BTreeMap, HashMap, HashSet};
34
35use bitcoin::hashes::Hash;
36use bitcoin::{BlockHash, OutPoint, Script, Target, Transaction, TxOut, Txid};
37
38use crate::block::{
39 block_weight, merkle_root_of_txs, verify_block_signature, witness_root_of_txs, HeaderFamily,
40 SidestrBlock,
41};
42use crate::marker::{looks_like_pegout, parse_claims, parse_pegout, Burn};
43use crate::sighash::verify_taproot_key_path;
44
45#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct Params {
50 pub max_block_weight: u64,
52 pub max_money: u64,
54 pub max_block_sigops_cost: u64,
56 pub coinbase_maturity: u32,
58 pub max_future_block_time: u32,
60 pub bip34_height: u32,
62 pub segwit_height: u32,
64 pub bip65_height: u32,
66 pub bip66_height: u32,
68}
69
70impl Default for Params {
71 fn default() -> Self {
72 Self {
73 max_block_weight: 4_000_000,
74 max_money: 2_100_000_000_000_000,
75 max_block_sigops_cost: 80_000,
76 coinbase_maturity: 100,
77 max_future_block_time: 7_200,
78 bip34_height: 1,
79 segwit_height: 0,
80 bip65_height: 1,
81 bip66_height: 1,
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct RuleResult {
89 pub rule: String,
91 pub ok: Option<bool>,
93}
94
95impl RuleResult {
96 pub fn new(rule: &str, ok: Option<bool>) -> Self {
98 Self {
99 rule: rule.to_string(),
100 ok,
101 }
102 }
103}
104
105#[derive(Debug, Clone, Default, PartialEq, Eq)]
107pub struct Verdict {
108 pub results: Vec<RuleResult>,
110}
111
112impl Verdict {
113 fn push(&mut self, rule: &str, ok: Option<bool>) {
114 self.results.push(RuleResult::new(rule, ok));
115 }
116 pub fn ok(&self) -> bool {
118 self.results.iter().all(|r| r.ok != Some(false))
119 }
120 pub fn failed(&self) -> Vec<String> {
122 self.results
123 .iter()
124 .filter(|r| r.ok == Some(false))
125 .map(|r| r.rule.clone())
126 .collect()
127 }
128 pub fn extend(&mut self, other: Verdict) {
130 self.results.extend(other.results);
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct Coin {
137 pub output: TxOut,
139 pub height: u32,
141 pub coinbase: bool,
143}
144
145pub type Utxo = HashMap<OutPoint, Coin>;
147
148#[derive(Debug, Clone, Default, PartialEq, Eq)]
154pub struct Records {
155 pub claims: HashMap<(String, u32), u32>,
157 pub pegouts: BTreeMap<(String, u32), Burn>,
159}
160
161impl Records {
162 pub fn claimed(&self, txid: &str, vout: u32) -> bool {
164 self.claims.contains_key(&(txid.to_string(), vout))
165 }
166 pub fn pegouts(&self) -> Vec<Burn> {
168 let mut v: Vec<Burn> = self.pegouts.values().cloned().collect();
169 v.sort_by_key(|b| b.height);
170 v
171 }
172}
173
174#[derive(Debug, Clone)]
176pub struct Overlay<'a> {
177 pub challenge: &'a Script,
179 pub pegout_min: u64,
181 pub genesis_subsidy: u64,
185}
186
187#[derive(Debug, Clone)]
191pub struct HeaderContext<'a, F: HeaderFamily> {
192 pub height: u32,
194 pub prev: Option<&'a F::Header>,
196 pub mtp_window: &'a [F::Header],
198 pub now: Option<u32>,
200}
201
202pub fn median_time_past<F: HeaderFamily>(family: &F, window: &[F::Header]) -> u32 {
204 let start = window.len().saturating_sub(11);
205 let mut times: Vec<u32> = window[start..].iter().map(|h| family.time(h)).collect();
206 times.sort_unstable();
207 times[times.len() >> 1]
208}
209
210pub fn validate_header<F: HeaderFamily>(
213 family: &F,
214 params: &Params,
215 header: &F::Header,
216 ctx: &HeaderContext<F>,
217) -> Verdict {
218 let mut v = Verdict::default();
219 v.push(
220 "btc:rule-header-prev-link",
221 ctx.prev
222 .map(|p| family.prev(header) == family.block_hash(p)),
223 );
224 v.push(
225 "btc:rule-header-pow",
226 Some(Target::from_compact(family.bits(header)).is_met_by(family.block_hash(header))),
227 );
228 v.push(
230 "btc:rule-header-difficulty",
231 ctx.prev.map(|p| family.bits(header) == family.bits(p)),
232 );
233 let need = 11.min(ctx.height as usize);
234 v.push(
235 "btc:rule-header-mtp",
236 (ctx.mtp_window.len() >= need && !ctx.mtp_window.is_empty())
237 .then(|| family.time(header) > median_time_past(family, ctx.mtp_window)),
238 );
239 v.push(
240 "btc:rule-header-time-future",
241 ctx.now.map(|now| {
242 u64::from(family.time(header))
243 <= u64::from(now) + u64::from(params.max_future_block_time)
244 }),
245 );
246 let min_version = if ctx.height >= params.bip65_height {
247 4
248 } else if ctx.height >= params.bip66_height {
249 3
250 } else if ctx.height >= params.bip34_height {
251 2
252 } else {
253 1
254 };
255 v.push(
258 "btc:rule-header-version",
259 Some(family.version_number(header) >= min_version),
260 );
261 v.push("btc:rule-header-timewarp", None); v.results.extend(family.header_rules(header, ctx.height));
263 v
264}
265
266fn is_coinbase(tx: &Transaction) -> bool {
269 tx.input.len() == 1 && tx.input[0].previous_output == OutPoint::null()
270}
271
272fn sum_out(tx: &Transaction) -> Option<u64> {
273 tx.output
274 .iter()
275 .try_fold(0u64, |s, o| s.checked_add(o.value.to_sat()))
276}
277
278pub fn validate_transaction(params: &Params, tx: &Transaction, coinbase: bool) -> Verdict {
281 let mut v = Verdict::default();
282 v.push("btc:rule-tx-inputs-nonempty", Some(!tx.input.is_empty()));
283 v.push("btc:rule-tx-outputs-nonempty", Some(!tx.output.is_empty()));
284 v.push(
285 "btc:rule-tx-size",
286 Some(tx.weight().to_wu() <= params.max_block_weight),
287 );
288 v.push(
289 "btc:rule-tx-output-values",
290 Some(
291 tx.output
292 .iter()
293 .all(|o| o.value.to_sat() <= params.max_money)
294 && sum_out(tx).is_some_and(|s| s <= params.max_money),
295 ),
296 );
297 v.push(
298 "btc:rule-tx-inputs-unique",
299 Some(
300 tx.input
301 .iter()
302 .map(|i| i.previous_output)
303 .collect::<HashSet<_>>()
304 .len()
305 == tx.input.len(),
306 ),
307 );
308 v.push(
309 "btc:rule-tx-prevouts",
310 Some(if coinbase {
311 is_coinbase(tx)
312 } else {
313 tx.input
314 .iter()
315 .all(|i| i.previous_output.txid != Txid::all_zeros())
316 }),
317 );
318 v.push(
319 "btc:rule-tx-coinbase-script",
320 coinbase.then(|| {
321 tx.input
322 .first()
323 .is_some_and(|i| (2..=100).contains(&i.script_sig.len()))
324 }),
325 );
326 v
327}
328
329fn legacy_sigops(txdata: &[Transaction]) -> u64 {
334 txdata.iter().fold(0u64, |n, tx| {
335 let ins = tx.input.iter().fold(0u64, |n, i| {
336 n.saturating_add(i.script_sig.count_sigops_legacy() as u64)
337 });
338 let outs = tx.output.iter().fold(0u64, |n, o| {
339 n.saturating_add(o.script_pubkey.count_sigops_legacy() as u64)
340 });
341 n.saturating_add(ins).saturating_add(outs)
342 })
343}
344
345pub fn validate_block_structure<F: HeaderFamily>(
348 family: &F,
349 params: &Params,
350 overlay: &Overlay,
351 block: &F::Block,
352) -> Verdict {
353 let txdata = block.txdata();
354 let mut v = Verdict::default();
355 let txids: Vec<Txid> = txdata.iter().map(Transaction::compute_txid).collect();
356 v.push(
357 "btc:rule-block-coinbase-first",
358 Some(txdata.first().is_some_and(is_coinbase)),
359 );
360 v.push(
361 "btc:rule-block-coinbase-single",
362 Some(txdata.iter().skip(1).all(|tx| !is_coinbase(tx))),
363 );
364 v.push(
365 "btc:rule-block-merkle-root",
366 Some(
367 !txdata.is_empty() && merkle_root_of_txs(txdata) == family.merkle_root(block.header()),
368 ),
369 );
370 v.push(
371 "btc:rule-block-tx-duplicates",
372 Some(txids.iter().collect::<HashSet<_>>().len() == txids.len()),
373 );
374 v.push(
375 "btc:rule-block-sigops",
376 Some(legacy_sigops(txdata).saturating_mul(4) <= params.max_block_sigops_cost),
377 );
378 v.push(
379 "btc:rule-block-weight",
380 Some(block_weight(family, block) <= params.max_block_weight),
381 );
382 v.push(
383 "btc:rule-block-transactions",
384 Some(
385 txdata
386 .iter()
387 .enumerate()
388 .all(|(i, tx)| validate_transaction(params, tx, i == 0).ok()),
389 ),
390 );
391 v.push(
392 "sidestr:rule-block-signature",
393 Some(verify_block_signature(family, block, overlay.challenge)),
394 );
395 v.results
396 .extend(family.block_rules(block.header(), txdata.len()));
397 v
398}
399
400#[derive(Debug, Clone, Default)]
406pub struct Spending {
407 pub fees: u64,
409 pub missing: Vec<OutPoint>,
411 pub deficits: Vec<Txid>,
413 pub premature: Vec<OutPoint>,
415 pub seqlock_violations: Vec<OutPoint>,
417 pub seqlock_unknown: usize,
419 pub resolved: Vec<(usize, usize, TxOut)>,
421}
422
423pub fn resolve_spending(
426 params: &Params,
427 txdata: &[Transaction],
428 utxo: &Utxo,
429 height: u32,
430) -> Spending {
431 const SEQ_DISABLE: u32 = 0x8000_0000;
432 const SEQ_TYPE: u32 = 0x0040_0000;
433 const SEQ_MASK: u32 = 0x0000_ffff;
434 let mut s = Spending::default();
435 let mut spent_here: HashSet<OutPoint> = HashSet::new();
436 let mut created_here: HashMap<OutPoint, Coin> = HashMap::new();
437 for (ti, tx) in txdata.iter().enumerate() {
438 let txid = tx.compute_txid();
439 if ti > 0 {
440 let mut in_sum = 0u64;
441 let mut values_ok = true;
442 for (ii, inp) in tx.input.iter().enumerate() {
443 let key = inp.previous_output;
444 if spent_here.contains(&key) {
445 s.missing.push(key);
446 values_ok = false;
447 } else {
448 let coin = created_here.get(&key).or_else(|| utxo.get(&key));
449 let mut coin_height = None;
450 match coin {
451 Some(c) => {
452 in_sum = in_sum.saturating_add(c.output.value.to_sat());
453 s.resolved.push((ti, ii, c.output.clone()));
454 coin_height = Some(c.height);
455 if c.coinbase
456 && height.saturating_sub(c.height) < params.coinbase_maturity
457 {
458 s.premature.push(key);
459 }
460 }
461 None => {
462 s.missing.push(key);
463 values_ok = false;
464 }
465 }
466 let seq = inp.sequence.0;
467 if tx.version.0 >= 2 && seq & SEQ_DISABLE == 0 {
468 let value = seq & SEQ_MASK;
469 if value > 0 {
470 if seq & SEQ_TYPE != 0 {
471 s.seqlock_unknown += 1;
472 } else if let Some(ch) = coin_height {
473 if u64::from(height) < u64::from(ch) + u64::from(value) {
474 s.seqlock_violations.push(key);
475 }
476 } else {
477 s.seqlock_unknown += 1;
478 }
479 }
480 }
481 }
482 spent_here.insert(key);
483 }
484 if values_ok {
485 match sum_out(tx) {
486 Some(out_sum) if in_sum >= out_sum => {
487 s.fees = s.fees.saturating_add(in_sum - out_sum)
488 }
489 _ => s.deficits.push(txid),
490 }
491 }
492 }
493 for (vout, o) in tx.output.iter().enumerate() {
494 if !o.script_pubkey.is_op_return() {
495 created_here.insert(
496 OutPoint {
497 txid,
498 vout: vout as u32,
499 },
500 Coin {
501 output: o.clone(),
502 height,
503 coinbase: ti == 0,
504 },
505 );
506 }
507 }
508 }
509 s
510}
511
512#[derive(Debug)]
514pub struct BlockContext<'a, F: HeaderFamily> {
515 pub block: &'a F::Block,
517 pub height: u32,
519 pub spending: &'a Spending,
521 pub mtp: Option<u32>,
523 pub records: &'a Records,
525}
526
527pub trait BlockRule<F: HeaderFamily>: core::fmt::Debug {
531 fn id(&self) -> &str;
533 fn check(&self, ctx: &BlockContext<F>) -> Option<bool>;
535}
536
537fn bip34_height_lenient(coinbase: &Transaction) -> Option<u32> {
543 let script = coinbase.input.first()?.script_sig.as_bytes();
544 let op = *script.first()?;
545 if (0x51..=0x60).contains(&op) {
546 return Some(u32::from(op - 0x50));
547 }
548 let len = usize::from(op);
549 if !(1..=5).contains(&len) || script.len() < 1 + len {
550 return None;
551 }
552 let n = (1..=len)
553 .rev()
554 .fold(0u64, |n, i| n * 256 + u64::from(script[i]));
555 u32::try_from(n).ok()
556}
557
558fn witness_commitment_in(coinbase: &Transaction) -> Option<[u8; 32]> {
561 coinbase
562 .output
563 .iter()
564 .rev()
565 .find(|o| {
566 o.script_pubkey.len() >= 38
567 && o.script_pubkey
568 .as_bytes()
569 .starts_with(&[0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed])
570 })
571 .and_then(|o| o.script_pubkey.as_bytes()[6..38].try_into().ok())
572}
573
574fn witness_commitment_hash(txdata: &[Transaction]) -> [u8; 32] {
578 let root = witness_root_of_txs(txdata);
579 let mut cat = [0u8; 64];
580 cat[..32].copy_from_slice(&root);
581 if let Some(reserved) = txdata[0]
582 .input
583 .first()
584 .and_then(|i| i.witness.iter().next())
585 {
586 let n = reserved.len().min(32);
587 cat[32..32 + n].copy_from_slice(&reserved[..n]);
588 }
589 bitcoin::hashes::sha256d::Hash::hash(&cat).to_byte_array()
590}
591
592#[derive(Debug)]
594pub struct Candidate<'a, F: HeaderFamily> {
595 pub block: &'a F::Block,
597 pub height: u32,
599 pub utxo: &'a Utxo,
601 pub mtp: Option<u32>,
603 pub records: &'a Records,
605 pub extra: &'a [Box<dyn BlockRule<F>>],
607}
608
609pub fn validate_block_context<F: HeaderFamily>(
613 family: &F,
614 params: &Params,
615 overlay: &Overlay,
616 c: &Candidate<F>,
617) -> (Verdict, Spending, Records) {
618 let Candidate {
619 block,
620 height,
621 utxo,
622 mtp,
623 records,
624 extra,
625 } = *c;
626 let txdata = block.txdata();
627 let spending = resolve_spending(params, txdata, utxo, height);
628 let mut v = Verdict::default();
629 let cb = &txdata[0];
630 let mut next = Records::default();
631
632 v.push(
633 "btc:rule-blockctx-bip34-height",
634 (height >= params.bip34_height).then(|| bip34_height_lenient(cb) == Some(height)),
635 );
636 let mut unknown = false;
638 let mut final_ok = true;
639 for tx in txdata {
640 let lt = tx.lock_time.to_consensus_u32();
641 if lt == 0 || tx.input.iter().all(|i| i.sequence.0 == 0xffff_ffff) {
642 continue;
643 }
644 if lt < 500_000_000 {
645 if lt >= height {
646 final_ok = false;
647 }
648 } else if let Some(m) = mtp {
649 if lt >= m {
650 final_ok = false;
651 }
652 } else {
653 unknown = true;
654 }
655 }
656 v.push(
657 "btc:rule-blockctx-finality",
658 if !final_ok {
659 Some(false)
660 } else if unknown {
661 None
662 } else {
663 Some(true)
664 },
665 );
666 v.push(
667 "btc:rule-blockctx-sequence-locks",
668 if !spending.seqlock_violations.is_empty() {
669 Some(false)
670 } else if spending.seqlock_unknown > 0 {
671 None
672 } else {
673 Some(true)
674 },
675 );
676 v.push(
677 "btc:rule-blockctx-inputs-available",
678 Some(spending.missing.is_empty()),
679 );
680 v.push(
681 "btc:rule-blockctx-coinbase-maturity",
682 Some(spending.premature.is_empty()),
683 );
684 v.push("btc:rule-blockctx-fees", Some(spending.deficits.is_empty()));
685 let (claims, claim_errors) = parse_claims(cb);
689 let paid = claims
690 .iter()
691 .try_fold(0u64, |s, c| s.checked_add(c.payout.value));
692 let subsidy = if height == 0 {
693 overlay.genesis_subsidy
694 } else {
695 0
696 };
697 v.push(
698 "btc:rule-blockctx-coinbase-amount",
699 Some(
700 claim_errors.is_empty()
701 && paid.is_some_and(|paid| {
702 sum_out(cb).is_some_and(|s| {
703 s <= subsidy.saturating_add(spending.fees).saturating_add(paid)
704 })
705 }),
706 ),
707 );
708 let has_witness = txdata
709 .iter()
710 .any(|tx| tx.input.iter().any(|i| !i.witness.is_empty()));
711 v.push(
712 "btc:rule-blockctx-witness-commitment",
713 (height >= params.segwit_height && has_witness)
714 .then(|| witness_commitment_in(cb) == Some(witness_commitment_hash(txdata))),
715 );
716 let sighash = family.sighash_rules(height);
720 let mut scripts_ok = true;
721 let mut by_tx: BTreeMap<usize, BTreeMap<usize, TxOut>> = BTreeMap::new();
722 for (ti, ii, prevout) in &spending.resolved {
723 by_tx.entry(*ti).or_default().insert(*ii, prevout.clone());
724 }
725 for (ti, resolved) in &by_tx {
726 let tx = &txdata[*ti];
727 if resolved.len() != tx.input.len() {
728 scripts_ok = false;
729 continue;
730 }
731 let prevouts: Vec<TxOut> = (0..tx.input.len()).map(|i| resolved[&i].clone()).collect();
732 for ii in 0..tx.input.len() {
733 if verify_taproot_key_path(tx, ii, &prevouts, sighash).is_err() {
734 scripts_ok = false;
735 }
736 }
737 }
738 v.push("btc:rule-blockctx-scripts", Some(scripts_ok));
739
740 let pegouts_ok = (|| {
743 if cb
744 .output
745 .iter()
746 .any(|o| parse_pegout(&o.script_pubkey).is_some())
747 {
748 return false;
749 }
750 for tx in txdata.iter().skip(1) {
751 let txid = tx.compute_txid().to_string();
752 for (vout, o) in tx.output.iter().enumerate() {
753 if !looks_like_pegout(o) {
756 continue;
757 }
758 let Some(script) = parse_pegout(&o.script_pubkey) else {
759 return false;
760 };
761 if o.value.to_sat() < overlay.pegout_min {
762 return false;
763 }
764 let key = (txid.clone(), vout as u32);
765 if records
766 .pegouts
767 .get(&key)
768 .is_some_and(|b| b.height != height)
769 {
770 return false;
771 }
772 next.pegouts.insert(
773 key,
774 Burn {
775 txid: txid.clone(),
776 vout: vout as u32,
777 script,
778 value: o.value.to_sat(),
779 height,
780 },
781 );
782 }
783 }
784 true
785 })();
786 v.push("sidestr:rule-pegouts", Some(pegouts_ok));
787 let claims_ok = (|| {
790 if !claim_errors.is_empty() {
791 return false;
792 }
793 let mut in_block = HashSet::new();
794 for c in &claims {
795 let op = (c.txid.clone(), c.vout);
796 if !in_block.insert(op.clone()) {
797 return false;
798 }
799 if records.claims.get(&op).is_some_and(|&at| at != height) {
800 return false;
801 }
802 }
803 for op in in_block {
804 next.claims.insert(op, height);
805 }
806 true
807 })();
808 v.push("sidestr:rule-claims", Some(claims_ok));
809 let ctx = BlockContext {
810 block,
811 height,
812 spending: &spending,
813 mtp,
814 records,
815 };
816 for rule in extra {
817 let ok = rule.check(&ctx);
818 v.push(rule.id(), ok);
819 }
820 (v, spending, next)
821}
822
823pub fn apply_block(utxo: &mut Utxo, txdata: &[Transaction], height: u32) -> (usize, usize) {
827 let mut created = 0;
828 let mut spent = 0;
829 for (i, tx) in txdata.iter().enumerate() {
830 if i > 0 {
831 for inp in &tx.input {
832 if utxo.remove(&inp.previous_output).is_some() {
833 spent += 1;
834 }
835 }
836 }
837 let txid = tx.compute_txid();
838 for (vout, o) in tx.output.iter().enumerate() {
839 if !o.script_pubkey.is_op_return() {
840 utxo.insert(
841 OutPoint {
842 txid,
843 vout: vout as u32,
844 },
845 Coin {
846 output: o.clone(),
847 height,
848 coinbase: i == 0,
849 },
850 );
851 created += 1;
852 }
853 }
854 }
855 (created, spent)
856}
857
858pub fn genesis_hash<F: HeaderFamily>(family: &F, block: &F::Block) -> BlockHash {
864 family.block_hash(block.header())
865}