1mod header_verify;
30mod script_verify;
31mod tx_verify;
32
33use crate::chain_params::ChainParams;
34use bitcoin::block::Bip34Error;
35use bitcoin::blockdata::block::Header as BitcoinHeader;
36use bitcoin::blockdata::constants::{COINBASE_MATURITY, MAX_BLOCK_SIGOPS_COST};
37use bitcoin::blockdata::weight::WITNESS_SCALE_FACTOR;
38use bitcoin::consensus::Encodable;
39use bitcoin::{
40 Amount, Block as BitcoinBlock, BlockHash, OutPoint, ScriptBuf, TxMerkleNode, TxOut, Txid,
41 VarInt, Weight,
42};
43pub use header_verify::{Error as HeaderError, HeaderVerifier};
44use sc_client_api::{AuxStore, Backend, StorageProvider};
45use sp_blockchain::HeaderBackend;
46use sp_runtime::traits::Block as BlockT;
47use std::collections::{HashMap, HashSet};
48use std::marker::PhantomData;
49use std::sync::Arc;
50use subcoin_primitives::consensus::{TxError, check_transaction_sanity};
51use subcoin_primitives::runtime::{Coin, bitcoin_block_subsidy};
52use subcoin_primitives::{CoinStorageKey, MAX_BLOCK_WEIGHT};
53use tx_verify::{get_legacy_sig_op_count, is_final_tx};
54
55#[derive(Copy, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
57#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
58pub enum ScriptEngine {
59 #[default]
61 Core,
62 None,
64 Subcoin,
67}
68
69#[derive(Copy, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
71#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
72pub enum BlockVerification {
73 None,
75 #[default]
77 Full,
78 HeaderOnly,
80}
81
82#[derive(Debug)]
84pub struct TransactionContext {
85 pub block_number: u32,
87 pub tx_index: usize,
89 pub txid: Txid,
91}
92
93impl std::fmt::Display for TransactionContext {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 write!(
96 f,
97 "Tx #{}:{}: {}",
98 self.block_number, self.tx_index, self.txid
99 )
100 }
101}
102
103#[derive(Debug, thiserror::Error)]
105pub enum Error {
106 #[error("Invalid merkle root in block#{0}")]
108 BadMerkleRoot(BlockHash),
109 #[error("Block#{0} has an empty transaction list")]
111 EmptyTransactionList(BlockHash),
112 #[error("Block#{0} exceeds maximum allowed size")]
114 BlockTooLarge(BlockHash),
115 #[error("First transaction in block#{0} is not a coinbase")]
117 FirstTransactionIsNotCoinbase(BlockHash),
118 #[error("Block#{0} contains multiple coinbase transactions")]
120 MultipleCoinbase(BlockHash),
121 #[error("Block#{block_hash} has incorrect coinbase height, (expected: {expected}, got: {got})")]
122 BadCoinbaseBlockHeight {
123 block_hash: BlockHash,
124 got: u32,
125 expected: u32,
126 },
127 #[error("Premature spend of coinbase transaction in block#{0}")]
129 PrematureSpendOfCoinbase(BlockHash),
130
131 #[error("Block#{block_hash} contains duplicate transaction at index {index}")]
133 DuplicateTransaction { block_hash: BlockHash, index: usize },
134 #[error("Transaction in block#{0} is not finalized")]
136 TransactionNotFinal(BlockHash),
137 #[error(
139 "Transaction in block #{block_number},{block_hash} exceeds signature operation limit (max: {MAX_BLOCK_SIGOPS_COST})"
140 )]
141 TooManySigOps {
142 block_hash: BlockHash,
143 block_number: u32,
144 },
145 #[error("Invalid witness commitment in block#{0}")]
147 BadWitnessCommitment(BlockHash),
148
149 #[error(
151 "Missing UTXO in state for transaction ({context}) in block {block_hash}. Missing UTXO: {missing_utxo:?}"
152 )]
153 MissingUtxoInState {
154 block_hash: BlockHash,
155 context: TransactionContext,
157 missing_utxo: OutPoint,
159 },
160 #[error("UTXO already spent in current block (#{block_number},{block_hash}:{txid}: {utxo:?})")]
162 AlreadySpentInCurrentBlock {
163 block_hash: BlockHash,
164 block_number: u32,
165 txid: Txid,
166 utxo: OutPoint,
167 },
168 #[error(
170 "Block#{block_hash} has an invalid transaction: total input {value_in} < total output {value_out}"
171 )]
172 InsufficientFunds {
173 block_hash: BlockHash,
174 value_in: u64,
175 value_out: u64,
176 },
177 #[error("Block#{0} reward exceeds allowed amount (subsidy + fees)")]
179 InvalidBlockReward(BlockHash),
180 #[error(
182 "Script verification failure in block#{block_hash}. Context: {context}, input_index: {input_index}: {error:?}"
183 )]
184 InvalidScript {
185 block_hash: BlockHash,
186 context: TransactionContext,
187 input_index: usize,
188 error: Box<subcoin_script::Error>,
189 },
190 #[error("BIP34 error in block#{0}: {1:?}")]
191 Bip34(BlockHash, Bip34Error),
192 #[error("Bitcoin codec error: {0:?}")]
193 BitcoinCodec(bitcoin::io::Error),
194 #[error(transparent)]
195 BitcoinConsensus(#[from] bitcoinconsensus::Error),
196 #[error(transparent)]
198 Client(#[from] sp_blockchain::Error),
199 #[error(transparent)]
200 Transaction(#[from] TxError),
201 #[error(transparent)]
203 Header(#[from] HeaderError),
204}
205
206#[derive(Clone)]
208pub struct BlockVerifier<Block, Client, BE> {
209 client: Arc<Client>,
210 chain_params: ChainParams,
211 header_verifier: HeaderVerifier<Block, Client>,
212 block_verification: BlockVerification,
213 coin_storage_key: Arc<dyn CoinStorageKey>,
214 script_engine: ScriptEngine,
215 _phantom: PhantomData<(Block, BE)>,
216}
217
218impl<Block, Client, BE> BlockVerifier<Block, Client, BE> {
219 pub fn new(
221 client: Arc<Client>,
222 network: bitcoin::Network,
223 block_verification: BlockVerification,
224 coin_storage_key: Arc<dyn CoinStorageKey>,
225 script_engine: ScriptEngine,
226 ) -> Self {
227 let chain_params = ChainParams::new(network);
228 let header_verifier = HeaderVerifier::new(client.clone(), chain_params.clone());
229 Self {
230 client,
231 chain_params,
232 header_verifier,
233 block_verification,
234 coin_storage_key,
235 script_engine,
236 _phantom: Default::default(),
237 }
238 }
239}
240
241impl<Block, Client, BE> BlockVerifier<Block, Client, BE>
242where
243 Block: BlockT,
244 BE: Backend<Block>,
245 Client: HeaderBackend<Block> + StorageProvider<Block, BE> + AuxStore,
246{
247 pub fn verify_block(&self, block_number: u32, block: &BitcoinBlock) -> Result<(), Error> {
252 let txids = self.check_block_sanity(block_number, block)?;
253
254 self.contextual_check_block(block_number, block.block_hash(), block, txids)
255 }
256
257 fn contextual_check_block(
258 &self,
259 block_number: u32,
260 block_hash: BlockHash,
261 block: &BitcoinBlock,
262 txids: HashMap<usize, Txid>,
263 ) -> Result<(), Error> {
264 match self.block_verification {
265 BlockVerification::Full => {
266 let lock_time_cutoff = self.header_verifier.verify(&block.header)?;
267
268 if block_number >= self.chain_params.segwit_height
269 && !block.check_witness_commitment()
270 {
271 return Err(Error::BadWitnessCommitment(block_hash));
272 }
273
274 if block.weight() > MAX_BLOCK_WEIGHT {
276 return Err(Error::BlockTooLarge(block_hash));
277 }
278
279 self.verify_transactions(block_number, block, txids, lock_time_cutoff)?;
280 }
281 BlockVerification::HeaderOnly => {
282 self.header_verifier.verify(&block.header)?;
283 }
284 BlockVerification::None => {}
285 }
286
287 Ok(())
288 }
289
290 fn check_block_sanity(
301 &self,
302 block_number: u32,
303 block: &BitcoinBlock,
304 ) -> Result<HashMap<usize, Txid>, Error> {
305 let block_hash = block.block_hash();
306
307 if block.txdata.is_empty() {
308 return Err(Error::EmptyTransactionList(block_hash));
309 }
310
311 if Weight::from_wu((block.txdata.len() * WITNESS_SCALE_FACTOR) as u64) > MAX_BLOCK_WEIGHT
313 || Weight::from_wu((block_base_size(block) * WITNESS_SCALE_FACTOR) as u64)
314 > MAX_BLOCK_WEIGHT
315 {
316 return Err(Error::BlockTooLarge(block_hash));
317 }
318
319 if !block.txdata[0].is_coinbase() {
320 return Err(Error::FirstTransactionIsNotCoinbase(block_hash));
321 }
322
323 let tx_count = block.txdata.len();
325
326 let mut seen_transactions = HashSet::with_capacity(tx_count);
327 let mut txids = HashMap::with_capacity(tx_count);
328
329 let mut sig_ops = 0;
330
331 for (index, tx) in block.txdata.iter().enumerate() {
332 if index > 0 && tx.is_coinbase() {
333 return Err(Error::MultipleCoinbase(block_hash));
334 }
335
336 let txid = tx.compute_txid();
337 if !seen_transactions.insert(txid) {
338 return Err(Error::DuplicateTransaction { block_hash, index });
340 }
341
342 check_transaction_sanity(tx)?;
343
344 sig_ops += get_legacy_sig_op_count(tx);
345
346 txids.insert(index, txid);
347 }
348
349 if sig_ops * WITNESS_SCALE_FACTOR > MAX_BLOCK_SIGOPS_COST as usize {
350 return Err(Error::TooManySigOps {
351 block_hash,
352 block_number,
353 });
354 }
355
356 let hashes = block
358 .txdata
359 .iter()
360 .enumerate()
361 .filter_map(|(index, _obj)| txids.get(&index).map(|txid| txid.to_raw_hash()));
362
363 let maybe_merkle_root: Option<TxMerkleNode> =
364 bitcoin::merkle_tree::calculate_root(hashes).map(|h| h.into());
365
366 if !maybe_merkle_root
367 .map(|merkle_root| block.header.merkle_root == merkle_root)
368 .unwrap_or(false)
369 {
370 return Err(Error::BadMerkleRoot(block_hash));
371 }
372
373 Ok(txids)
374 }
375
376 fn verify_transactions(
377 &self,
378 block_number: u32,
379 block: &BitcoinBlock,
380 txids: HashMap<usize, Txid>,
381 lock_time_cutoff: u32,
382 ) -> Result<(), Error> {
383 let parent_number = block_number - 1;
384 let parent_hash =
385 self.client
386 .hash(parent_number.into())?
387 .ok_or(sp_blockchain::Error::Backend(format!(
388 "Parent block #{parent_number} not found"
389 )))?;
390
391 let get_txid = |tx_index: usize| {
392 txids
393 .get(&tx_index)
394 .copied()
395 .expect("Txid must exist as initialized in `check_block_sanity()`; qed")
396 };
397
398 let tx_context = |tx_index| TransactionContext {
399 block_number,
400 tx_index,
401 txid: get_txid(tx_index),
402 };
403
404 let flags = script_verify::get_block_script_flags(
405 block_number,
406 block.block_hash(),
407 &self.chain_params,
408 );
409
410 let block_hash = block.block_hash();
411
412 let mut block_fee = 0;
413 let mut spent_coins_in_block = HashSet::new();
414
415 let mut tx_buffer = if matches!(self.script_engine, ScriptEngine::Core) {
417 Vec::<u8>::with_capacity(4096)
418 } else {
419 Vec::new()
420 };
421
422 for (tx_index, tx) in block.txdata.iter().enumerate() {
425 if tx_index == 0 {
426 if block_number >= self.chain_params.params.bip34_height {
428 let block_height_in_coinbase = block
429 .bip34_block_height()
430 .map_err(|err| Error::Bip34(block_hash, err))?
431 as u32;
432 if block_height_in_coinbase != block_number {
433 return Err(Error::BadCoinbaseBlockHeight {
434 block_hash,
435 expected: block_number,
436 got: block_height_in_coinbase,
437 });
438 }
439 }
440
441 continue;
442 }
443
444 if !is_final_tx(tx, block_number, lock_time_cutoff) {
445 return Err(Error::TransactionNotFinal(block_hash));
446 }
447
448 tx_buffer.clear();
449 tx.consensus_encode(&mut tx_buffer)
450 .map_err(Error::BitcoinCodec)?;
451
452 let spending_transaction = tx_buffer.as_slice();
453
454 let access_coin = |out_point: OutPoint| -> Result<(TxOut, bool, u32), Error> {
455 let maybe_coin = match self.find_utxo_in_state(parent_hash, out_point) {
456 Some(coin) => {
457 let Coin {
458 is_coinbase,
459 amount,
460 height,
461 script_pubkey,
462 } = coin;
463
464 let txout = TxOut {
465 value: Amount::from_sat(amount),
466 script_pubkey: ScriptBuf::from_bytes(script_pubkey),
467 };
468
469 Some((txout, is_coinbase, height))
470 }
471 None => find_utxo_in_current_block(block, out_point, tx_index, get_txid)
472 .map(|(txout, is_coinbase)| (txout, is_coinbase, block_number)),
473 };
474
475 maybe_coin.ok_or_else(|| Error::MissingUtxoInState {
476 block_hash,
477 context: tx_context(tx_index),
478 missing_utxo: out_point,
479 })
480 };
481
482 let mut value_in = 0;
484 let mut sig_ops_cost = 0;
485
486 for (input_index, input) in tx.input.iter().enumerate() {
488 let coin = input.previous_output;
489
490 if spent_coins_in_block.contains(&coin) {
491 return Err(Error::AlreadySpentInCurrentBlock {
492 block_hash,
493 block_number,
494 txid: get_txid(tx_index),
495 utxo: coin,
496 });
497 }
498
499 let (spent_output, is_coinbase, coin_height) = access_coin(coin)?;
501
502 if is_coinbase && block_number - coin_height < COINBASE_MATURITY {
504 return Err(Error::PrematureSpendOfCoinbase(block_hash));
505 }
506
507 match self.script_engine {
516 ScriptEngine::Core => {
517 script_verify::verify_input_script(
518 &spent_output,
519 spending_transaction,
520 input_index,
521 flags,
522 )?;
523 }
524 ScriptEngine::None => {
525 }
527 ScriptEngine::Subcoin => {
528 let mut checker = subcoin_script::TransactionSignatureChecker::new(
529 tx,
530 input_index,
531 spent_output.value.to_sat(),
532 );
533
534 let verify_flags =
535 subcoin_script::VerifyFlags::from_bits(flags).expect("Invalid flags");
536
537 let script_result = subcoin_script::verify_script(
538 &input.script_sig,
539 &spent_output.script_pubkey,
540 &input.witness,
541 &verify_flags,
542 &mut checker,
543 );
544
545 if let Err(script_err) = script_result {
546 let context = tx_context(tx_index);
547 return Err(Error::InvalidScript {
548 block_hash,
549 context,
550 input_index,
551 error: Box::new(script_err),
552 });
553 }
554 }
555 }
556
557 spent_coins_in_block.insert(coin);
558 value_in += spent_output.value.to_sat();
559 }
560
561 sig_ops_cost += tx.total_sigop_cost(|out_point: &OutPoint| {
566 access_coin(*out_point).map(|(txout, _, _)| txout).ok()
567 });
568
569 if sig_ops_cost > MAX_BLOCK_SIGOPS_COST as usize {
570 return Err(Error::TooManySigOps {
571 block_hash,
572 block_number,
573 });
574 }
575
576 let value_out = tx
577 .output
578 .iter()
579 .map(|output| output.value.to_sat())
580 .sum::<u64>();
581
582 let tx_fee = value_in
585 .checked_sub(value_out)
586 .ok_or(Error::InsufficientFunds {
587 block_hash,
588 value_in,
589 value_out,
590 })?;
591
592 block_fee += tx_fee;
593 }
594
595 let coinbase_value = block.txdata[0]
596 .output
597 .iter()
598 .map(|output| output.value.to_sat())
599 .sum::<u64>();
600
601 let subsidy = bitcoin_block_subsidy(block_number);
602
603 if coinbase_value > block_fee + subsidy {
605 return Err(Error::InvalidBlockReward(block_hash));
606 }
607
608 Ok(())
609 }
610
611 fn find_utxo_in_state(&self, block_hash: Block::Hash, out_point: OutPoint) -> Option<Coin> {
613 use codec::Decode;
614
615 let OutPoint { txid, vout } = out_point;
621 let storage_key = self.coin_storage_key.storage_key(txid, vout);
622
623 let maybe_storage_data = self
624 .client
625 .storage(block_hash, &sc_client_api::StorageKey(storage_key))
626 .ok()
627 .flatten();
628
629 maybe_storage_data.and_then(|data| Coin::decode(&mut data.0.as_slice()).ok())
630 }
631}
632
633fn find_utxo_in_current_block(
635 block: &BitcoinBlock,
636 out_point: OutPoint,
637 tx_index: usize,
638 get_txid: impl Fn(usize) -> Txid,
639) -> Option<(TxOut, bool)> {
640 let OutPoint { txid, vout } = out_point;
641 block
642 .txdata
643 .iter()
644 .take(tx_index)
645 .enumerate()
646 .find_map(|(index, tx)| (get_txid(index) == txid).then_some((tx, index == 0)))
647 .and_then(|(tx, is_coinbase)| {
648 tx.output
649 .get(vout as usize)
650 .cloned()
651 .map(|txout| (txout, is_coinbase))
652 })
653}
654
655fn block_base_size(block: &BitcoinBlock) -> usize {
661 let mut size = BitcoinHeader::SIZE;
662
663 size += VarInt::from(block.txdata.len()).size();
664 size += block.txdata.iter().map(|tx| tx.base_size()).sum::<usize>();
665
666 size
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672 use bitcoin::consensus::encode::deserialize_hex;
673
674 #[test]
675 fn test_find_utxo_in_current_block() {
676 let test_block = std::env::current_dir()
677 .unwrap()
678 .parent()
679 .unwrap()
680 .parent()
681 .unwrap()
682 .join("test_data")
683 .join("btc_mainnet_385044.data");
684 let raw_block = std::fs::read_to_string(test_block).unwrap();
685 let block = deserialize_hex::<BitcoinBlock>(raw_block.trim()).unwrap();
686
687 let txids = block
688 .txdata
689 .iter()
690 .enumerate()
691 .map(|(index, tx)| (index, tx.compute_txid()))
692 .collect::<HashMap<_, _>>();
693
694 let out_point = OutPoint {
696 txid: "2b102a19161e5c93f71e16f9e8c9b2438f362c51ecc8f2a62e3c31d7615dd17d"
697 .parse()
698 .unwrap(),
699 vout: 1,
700 };
701
702 assert_eq!(
705 find_utxo_in_current_block(&block, out_point, 36, |index| txids
706 .get(&index)
707 .copied()
708 .unwrap())
709 .map(|(txout, is_coinbase)| (txout.value.to_sat(), is_coinbase))
710 .unwrap(),
711 (295600000, false)
712 );
713 }
714}