1use bitcoin::{Block, BlockHash, Network, hashes::Hash};
2
3#[derive(Debug)]
4pub enum BlockValidationError {
5 HashMismatch,
6 MerkleRootMismatch,
7 WitnessCommitmentMismatch,
8 ProofOfWork(bitcoin::block::ValidationError),
9 UnsupportedNetwork,
10 TargetLimit,
11}
12
13impl std::fmt::Display for BlockValidationError {
14 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15 match self {
16 Self::HashMismatch => f.write_str("block hash mismatch"),
17 Self::MerkleRootMismatch => f.write_str("transaction Merkle root mismatch"),
18 Self::WitnessCommitmentMismatch => f.write_str("witness commitment mismatch"),
19 Self::ProofOfWork(cause) => write!(f, "block proof of work: {cause}"),
20 Self::UnsupportedNetwork => f.write_str("only regtest and testnet4 are supported"),
21 Self::TargetLimit => f.write_str("block target exceeds network proof-of-work limit"),
22 }
23 }
24}
25
26impl std::error::Error for BlockValidationError {
27 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
28 match self {
29 Self::ProofOfWork(cause) => Some(cause),
30 _ => None,
31 }
32 }
33}
34
35pub fn validate_block_integrity(
36 block: &Block,
37 expected_hash: BlockHash,
38) -> Result<(), BlockValidationError> {
39 if block.block_hash() != expected_hash {
40 return Err(BlockValidationError::HashMismatch);
41 }
42 if !block.check_merkle_root() {
43 return Err(BlockValidationError::MerkleRootMismatch);
44 }
45 if !check_witness_commitment(block) {
46 return Err(BlockValidationError::WitnessCommitmentMismatch);
47 }
48 Ok(())
49}
50
51fn check_witness_commitment(block: &Block) -> bool {
52 let Some(coinbase) = block.txdata.first() else {
53 return false;
54 };
55 let commitment = coinbase.output.iter().rfind(|output| {
56 let script = output.script_pubkey.as_bytes();
57 script.len() >= 38 && script.starts_with(&[0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed])
58 });
59 match commitment {
60 Some(output) => {
61 if !coinbase.is_coinbase() {
62 return false;
63 }
64 let witness = &coinbase.input[0].witness;
65 if witness.len() != 1 {
66 return false;
67 }
68 let Some(reserved) = witness.iter().next() else {
69 return false;
70 };
71 if reserved.len() != 32 {
72 return false;
73 }
74 let Some(root) = block.witness_root() else {
75 return false;
76 };
77 let expected = Block::compute_witness_commitment(&root, reserved);
78 output.script_pubkey.as_bytes()[6..38] == expected.as_byte_array()[..]
79 }
80 None => block
81 .txdata
82 .iter()
83 .all(|tx| tx.input.iter().all(|input| input.witness.is_empty())),
84 }
85}
86
87pub fn validate_block(block: &Block, expected_hash: BlockHash) -> Result<(), BlockValidationError> {
88 validate_block_integrity(block, expected_hash)?;
89 block
90 .header
91 .validate_pow(block.header.target())
92 .map_err(BlockValidationError::ProofOfWork)?;
93 Ok(())
94}
95
96pub fn validate_block_for_network(
97 block: &Block,
98 expected_hash: BlockHash,
99 network: Network,
100) -> Result<(), BlockValidationError> {
101 if !matches!(network, Network::Regtest | Network::Testnet4) {
102 return Err(BlockValidationError::UnsupportedNetwork);
103 }
104 if block.header.target() > network.params().max_attainable_target {
105 return Err(BlockValidationError::TargetLimit);
106 }
107 validate_block(block, expected_hash)
108}