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