urma-chain 0.3.0

URMA blockchain observations, transaction decoding and proof-of-work validation
Documentation
use bitcoin::{Block, BlockHash, Network, hashes::Hash};

#[derive(Debug)]
pub enum BlockValidationError {
    HashMismatch,
    MerkleRootMismatch,
    WitnessCommitmentMismatch,
    ProofOfWork(bitcoin::block::ValidationError),
    UnsupportedNetwork,
    TargetLimit,
}

impl std::fmt::Display for BlockValidationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::HashMismatch => f.write_str("block hash mismatch"),
            Self::MerkleRootMismatch => f.write_str("transaction Merkle root mismatch"),
            Self::WitnessCommitmentMismatch => f.write_str("witness commitment mismatch"),
            Self::ProofOfWork(cause) => write!(f, "block proof of work: {cause}"),
            Self::UnsupportedNetwork => f.write_str("only regtest and testnet4 are supported"),
            Self::TargetLimit => f.write_str("block target exceeds network proof-of-work limit"),
        }
    }
}

impl std::error::Error for BlockValidationError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::ProofOfWork(cause) => Some(cause),
            _ => None,
        }
    }
}

pub fn validate_block_integrity(
    block: &Block,
    expected_hash: BlockHash,
) -> Result<(), BlockValidationError> {
    if block.block_hash() != expected_hash {
        return Err(BlockValidationError::HashMismatch);
    }
    if !block.check_merkle_root() {
        return Err(BlockValidationError::MerkleRootMismatch);
    }
    if !check_witness_commitment(block) {
        return Err(BlockValidationError::WitnessCommitmentMismatch);
    }
    Ok(())
}

fn check_witness_commitment(block: &Block) -> bool {
    let Some(coinbase) = block.txdata.first() else {
        return false;
    };
    let commitment = coinbase.output.iter().rfind(|output| {
        let script = output.script_pubkey.as_bytes();
        script.len() >= 38 && script.starts_with(&[0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed])
    });
    match commitment {
        Some(output) => {
            if !coinbase.is_coinbase() {
                return false;
            }
            let witness = &coinbase.input[0].witness;
            if witness.len() != 1 {
                return false;
            }
            let Some(reserved) = witness.iter().next() else {
                return false;
            };
            if reserved.len() != 32 {
                return false;
            }
            let Some(root) = block.witness_root() else {
                return false;
            };
            let expected = Block::compute_witness_commitment(&root, reserved);
            output.script_pubkey.as_bytes()[6..38] == expected.as_byte_array()[..]
        }
        None => block
            .txdata
            .iter()
            .all(|tx| tx.input.iter().all(|input| input.witness.is_empty())),
    }
}

pub fn validate_block(block: &Block, expected_hash: BlockHash) -> Result<(), BlockValidationError> {
    validate_block_integrity(block, expected_hash)?;
    block
        .header
        .validate_pow(block.header.target())
        .map_err(BlockValidationError::ProofOfWork)?;
    Ok(())
}

pub fn validate_block_for_network(
    block: &Block,
    expected_hash: BlockHash,
    network: Network,
) -> Result<(), BlockValidationError> {
    if !matches!(network, Network::Regtest | Network::Testnet4) {
        return Err(BlockValidationError::UnsupportedNetwork);
    }
    if block.header.target() > network.params().max_attainable_target {
        return Err(BlockValidationError::TargetLimit);
    }
    validate_block(block, expected_hash)
}