Skip to main content

urma_chain/
validation.rs

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}