use crate::{
decode::{self, DecodeError},
observation::Chain,
validation::{BlockValidationError, validate_block_integrity},
};
use bitcoin::{
Block, BlockHash, Transaction, WitnessMerkleNode,
block::Header,
hashes::{Hash, sha256d},
};
use serde::{Deserialize, Serialize};
const MAX_TRANSACTIONS: u32 = 1_000_000;
const MAX_PATH_LEVELS: usize = 20;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MerklePath {
pub index: u32,
pub tx_count: u32,
pub siblings: Vec<[u8; 32]>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransactionProof {
pub transaction: Vec<u8>,
pub txid_path: MerklePath,
pub coinbase: Vec<u8>,
pub coinbase_path: MerklePath,
pub witness_path: MerklePath,
pub coinbase_witness_path: MerklePath,
}
pub struct BlockProofBuilder {
txid_levels: Vec<Vec<[u8; 32]>>,
witness_levels: Vec<Vec<[u8; 32]>>,
coinbase_targets: Vec<bool>,
}
#[derive(Clone, Debug)]
pub struct IncludedTransaction {
transaction: Transaction,
raw: Vec<u8>,
block_hash: BlockHash,
position: u32,
}
impl IncludedTransaction {
pub fn transaction(&self) -> &Transaction {
&self.transaction
}
pub fn raw(&self) -> &[u8] {
&self.raw
}
pub fn block_hash(&self) -> BlockHash {
self.block_hash
}
pub fn position(&self) -> u32 {
self.position
}
}
#[derive(Debug)]
pub enum InclusionError {
Decode(DecodeError),
Integrity(BlockValidationError),
LimitExceeded,
InvalidPath,
InconsistentPaths,
InvalidPadding,
MerkleRootMismatch,
InvalidCoinbase,
CoinbaseTarget,
InvalidReservedValue,
MissingWitnessCommitment,
WitnessRootMismatch,
WitnessCommitmentMismatch,
}
impl std::fmt::Display for InclusionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Decode(cause) => write!(f, "decode inclusion transaction: {cause}"),
Self::Integrity(cause) => write!(f, "proof source block integrity: {cause}"),
Self::LimitExceeded => f.write_str("inclusion proof exceeds resource limits"),
Self::InvalidPath => f.write_str("invalid Merkle path index, count or length"),
Self::InconsistentPaths => {
f.write_str("inconsistent inclusion proof positions or counts")
}
Self::InvalidPadding => f.write_str("odd Merkle padding must duplicate the last hash"),
Self::MerkleRootMismatch => f.write_str("inclusion transaction Merkle root mismatch"),
Self::InvalidCoinbase => f.write_str("inclusion proof requires a real coinbase"),
Self::CoinbaseTarget => {
f.write_str("inclusion target must be a noncoinbase transaction")
}
Self::InvalidReservedValue => {
f.write_str("coinbase witness must contain one 32-byte reserved value")
}
Self::MissingWitnessCommitment => f.write_str("coinbase witness commitment absent"),
Self::WitnessRootMismatch => {
f.write_str("transaction and zero coinbase witness roots differ")
}
Self::WitnessCommitmentMismatch => f.write_str("inclusion witness commitment mismatch"),
}
}
}
impl std::error::Error for InclusionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Decode(cause) => Some(cause),
Self::Integrity(cause) => Some(cause),
_ => None,
}
}
}
impl MerklePath {
fn check(&self) -> Result<(), InclusionError> {
if self.tx_count > MAX_TRANSACTIONS || self.siblings.len() > MAX_PATH_LEVELS {
return Err(InclusionError::LimitExceeded);
}
if self.tx_count == 0 || self.index >= self.tx_count {
return Err(InclusionError::InvalidPath);
}
let mut width = self.tx_count;
let mut levels = 0;
while width > 1 {
width = width.div_ceil(2);
levels += 1;
}
if self.siblings.len() != levels {
return Err(InclusionError::InvalidPath);
}
Ok(())
}
fn root(&self, mut hash: [u8; 32]) -> Result<[u8; 32], InclusionError> {
self.check()?;
let mut index = self.index;
let mut width = self.tx_count;
for sibling in &self.siblings {
if index % 2 == 0 {
if index + 1 == width && *sibling != hash {
return Err(InclusionError::InvalidPadding);
}
hash = parent(hash, *sibling);
} else {
hash = parent(*sibling, hash);
}
index /= 2;
width = width.div_ceil(2);
}
Ok(hash)
}
}
impl TransactionProof {
pub fn verify(
&self,
chain: Chain,
expected: &Header,
) -> Result<IncludedTransaction, InclusionError> {
let paths = [
&self.txid_path,
&self.coinbase_path,
&self.witness_path,
&self.coinbase_witness_path,
];
for path in paths {
path.check()?;
}
if paths
.iter()
.any(|path| path.tx_count != self.txid_path.tx_count)
|| self.txid_path.index != self.witness_path.index
|| self.coinbase_path.index != 0
|| self.coinbase_witness_path.index != 0
{
return Err(InclusionError::InconsistentPaths);
}
if self.txid_path.index == 0 {
return Err(InclusionError::CoinbaseTarget);
}
let transaction =
decode::transaction(&self.transaction, chain).map_err(InclusionError::Decode)?;
let coinbase =
decode::transaction(&self.coinbase, chain).map_err(InclusionError::Decode)?;
if transaction.is_coinbase() {
return Err(InclusionError::CoinbaseTarget);
}
if !coinbase.is_coinbase() {
return Err(InclusionError::InvalidCoinbase);
}
let expected_root = expected.merkle_root.to_byte_array();
if self
.txid_path
.root(transaction.compute_txid().to_byte_array())?
!= expected_root
|| self
.coinbase_path
.root(coinbase.compute_txid().to_byte_array())?
!= expected_root
{
return Err(InclusionError::MerkleRootMismatch);
}
let mut witness = coinbase.input[0].witness.iter();
let reserved = witness.next().ok_or(InclusionError::InvalidReservedValue)?;
if reserved.len() != 32 || witness.next().is_some() {
return Err(InclusionError::InvalidReservedValue);
}
const MAGIC: [u8; 6] = [0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed];
let commitment = coinbase
.output
.iter()
.rev()
.find_map(|output| {
let script = output.script_pubkey.as_bytes();
(script.len() >= 38 && script[..6] == MAGIC).then(|| &script[6..38])
})
.ok_or(InclusionError::MissingWitnessCommitment)?;
let witness_root = self
.witness_path
.root(transaction.compute_wtxid().to_byte_array())?;
if self.coinbase_witness_path.root([0; 32])? != witness_root {
return Err(InclusionError::WitnessRootMismatch);
}
let computed = Block::compute_witness_commitment(
&WitnessMerkleNode::from_byte_array(witness_root),
reserved,
);
if computed.as_byte_array().as_slice() != commitment {
return Err(InclusionError::WitnessCommitmentMismatch);
}
Ok(IncludedTransaction {
transaction,
raw: self.transaction.clone(),
block_hash: expected.block_hash(),
position: self.txid_path.index,
})
}
pub fn from_block(
block: &Block,
position: u32,
raw_transaction: Vec<u8>,
raw_coinbase: Vec<u8>,
) -> Result<Self, InclusionError> {
BlockProofBuilder::new(block)?.proof(position, raw_transaction, raw_coinbase)
}
}
impl BlockProofBuilder {
pub fn new(block: &Block) -> Result<Self, InclusionError> {
if block.txdata.len() > MAX_TRANSACTIONS as usize {
return Err(InclusionError::LimitExceeded);
}
validate_block_integrity(block, block.block_hash()).map_err(InclusionError::Integrity)?;
let txids = block
.txdata
.iter()
.map(|tx| tx.compute_txid().to_byte_array())
.collect::<Vec<_>>();
let witnesses = block
.txdata
.iter()
.enumerate()
.map(|(index, tx)| {
if index == 0 {
[0; 32]
} else {
tx.compute_wtxid().to_byte_array()
}
})
.collect::<Vec<_>>();
Ok(Self {
txid_levels: build_levels(txids),
witness_levels: build_levels(witnesses),
coinbase_targets: block.txdata.iter().map(Transaction::is_coinbase).collect(),
})
}
pub fn proof(
&self,
position: u32,
raw_transaction: Vec<u8>,
raw_coinbase: Vec<u8>,
) -> Result<TransactionProof, InclusionError> {
if position == 0 {
return Err(InclusionError::CoinbaseTarget);
}
let is_coinbase = self
.coinbase_targets
.get(position as usize)
.ok_or(InclusionError::InvalidPath)?;
if *is_coinbase {
return Err(InclusionError::CoinbaseTarget);
}
Ok(TransactionProof {
transaction: raw_transaction,
txid_path: self.path(&self.txid_levels, position),
coinbase: raw_coinbase,
coinbase_path: self.path(&self.txid_levels, 0),
witness_path: self.path(&self.witness_levels, position),
coinbase_witness_path: self.path(&self.witness_levels, 0),
})
}
fn path(&self, levels: &[Vec<[u8; 32]>], position: u32) -> MerklePath {
let mut index = position as usize;
let siblings = levels[..levels.len() - 1]
.iter()
.map(|level| {
let sibling = level.get(index ^ 1).copied().unwrap_or(level[index]);
index /= 2;
sibling
})
.collect();
MerklePath {
index: position,
tx_count: self.coinbase_targets.len() as u32,
siblings,
}
}
}
fn parent(left: [u8; 32], right: [u8; 32]) -> [u8; 32] {
let mut bytes = [0; 64];
bytes[..32].copy_from_slice(&left);
bytes[32..].copy_from_slice(&right);
sha256d::Hash::hash(&bytes).to_byte_array()
}
fn build_levels(hashes: Vec<[u8; 32]>) -> Vec<Vec<[u8; 32]>> {
let mut levels = vec![hashes];
while levels.last().unwrap().len() > 1 {
let next = levels
.last()
.unwrap()
.chunks(2)
.map(|pair| parent(pair[0], *pair.get(1).unwrap_or(&pair[0])))
.collect();
levels.push(next);
}
levels
}