From ec259f806c898f882ff5350ffb28ffe5f8b28db9 Mon Sep 17 00:00:00 2001 From: Peter Zafonte Date: Thu, 7 May 2026 16:23:28 -0400 Subject: [PATCH] feat(core): expose Block::check_context_free Wraps btck_block_check from bitcoin/bitcoin#33908 to validate a block's structure without chainstate or block index access. Proof-of-work and merkle-root checks are optional via the BLOCK_CHECK_BASE / _POW / _MERKLE / _ALL flags, exposed through a block_check_flags submodule mirroring verify_flags. Adds AsPtr for ChainParams and re-exports the owned BlockValidationState so callers can construct one. Test mirrors upstream's btck_check_block_context_free case-by-case. --- CHANGELOG.md | 1 + src/core/block.rs | 229 ++++++++++++++++++++++++++++++++++++++++--- src/core/mod.rs | 10 +- src/ffi/constants.rs | 13 ++- src/lib.rs | 13 ++- src/state/context.rs | 6 ++ tests/test.rs | 2 +- 7 files changed, 252 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fad0ca1c..d49669ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Added `Block::check` to perform context-free validation of a block (size, weight, coinbase, transactions, sigops), with optional proof-of-work and merkle-root checks toggled via the `BLOCK_CHECK_BASE` / `_POW` / `_MERKLE` / `_ALL` flags. Returns a `BlockCheckResult` enum carrying the validation state on failure. - Added `BlockTreeEntry::ancestor` to look up an ancestor block at a given height. Returns `None` if the height is out of range. This operation is O(log N). - Added `Transaction::locktime()` to retrieve a transaction's `nLockTime` value as a `u32`. - Added `TxIn::sequence()` to retrieve an input's `nSequence` value as a `u32`. diff --git a/src/core/block.rs b/src/core/block.rs index 13f8829d..0b64cf44 100644 --- a/src/core/block.rs +++ b/src/core/block.rs @@ -128,21 +128,21 @@ use std::{ }; use libbitcoinkernel_sys::{ - btck_Block, btck_BlockHash, btck_BlockHeader, btck_BlockSpentOutputs, btck_Coin, - btck_TransactionSpentOutputs, btck_block_copy, btck_block_count_transactions, - btck_block_create, btck_block_destroy, btck_block_get_hash, btck_block_get_header, - btck_block_get_transaction_at, btck_block_hash_copy, btck_block_hash_create, - btck_block_hash_destroy, btck_block_hash_equals, btck_block_hash_to_bytes, - btck_block_header_copy, btck_block_header_create, btck_block_header_destroy, - btck_block_header_get_bits, btck_block_header_get_hash, btck_block_header_get_nonce, - btck_block_header_get_prev_hash, btck_block_header_get_timestamp, + btck_Block, btck_BlockCheckFlags, btck_BlockHash, btck_BlockHeader, btck_BlockSpentOutputs, + btck_BlockValidationState, btck_Coin, btck_TransactionSpentOutputs, btck_block_check, + btck_block_copy, btck_block_count_transactions, btck_block_create, btck_block_destroy, + btck_block_get_hash, btck_block_get_header, btck_block_get_transaction_at, + btck_block_hash_copy, btck_block_hash_create, btck_block_hash_destroy, btck_block_hash_equals, + btck_block_hash_to_bytes, btck_block_header_copy, btck_block_header_create, + btck_block_header_destroy, btck_block_header_get_bits, btck_block_header_get_hash, + btck_block_header_get_nonce, btck_block_header_get_prev_hash, btck_block_header_get_timestamp, btck_block_header_get_version, btck_block_header_to_bytes, btck_block_spent_outputs_copy, btck_block_spent_outputs_count, btck_block_spent_outputs_destroy, btck_block_spent_outputs_get_transaction_spent_outputs_at, btck_block_to_bytes, - btck_coin_confirmation_height, btck_coin_copy, btck_coin_destroy, btck_coin_get_output, - btck_coin_is_coinbase, btck_transaction_spent_outputs_copy, - btck_transaction_spent_outputs_count, btck_transaction_spent_outputs_destroy, - btck_transaction_spent_outputs_get_coin_at, + btck_chain_parameters_get_consensus_params, btck_coin_confirmation_height, btck_coin_copy, + btck_coin_destroy, btck_coin_get_output, btck_coin_is_coinbase, + btck_transaction_spent_outputs_copy, btck_transaction_spent_outputs_count, + btck_transaction_spent_outputs_destroy, btck_transaction_spent_outputs_get_coin_at, }; use crate::{ @@ -150,10 +150,37 @@ use crate::{ ffi::{ c_helpers::present, sealed::{AsPtr, FromMutPtr, FromPtr}, + BTCK_BLOCK_CHECK_FLAGS_ALL, BTCK_BLOCK_CHECK_FLAGS_BASE, BTCK_BLOCK_CHECK_FLAGS_MERKLE, + BTCK_BLOCK_CHECK_FLAGS_POW, }, + notifications::types::BlockValidationState, + state::context::ChainParams, KernelError, }; +/// Run only base context-free block checks (no PoW, no Merkle root). +pub const BLOCK_CHECK_BASE: btck_BlockCheckFlags = BTCK_BLOCK_CHECK_FLAGS_BASE; + +/// Enable Proof-of-Work verification via the block header. +pub const BLOCK_CHECK_POW: btck_BlockCheckFlags = BTCK_BLOCK_CHECK_FLAGS_POW; + +/// Enable Merkle-root verification (and mutation detection). +pub const BLOCK_CHECK_MERKLE: btck_BlockCheckFlags = BTCK_BLOCK_CHECK_FLAGS_MERKLE; + +/// Enable all available context-free block checks (PoW + Merkle root). +pub const BLOCK_CHECK_ALL: btck_BlockCheckFlags = BTCK_BLOCK_CHECK_FLAGS_ALL; + +/// Outcome of [`Block::check`]. +/// +/// On failure, the [`BlockValidationState`] carries details that can be +/// inspected via [`BlockValidationStateExt`](crate::notifications::BlockValidationStateExt). +pub enum BlockCheckResult { + /// The block passed the requested context-free checks. + Valid, + /// The block failed; the state holds the validation details. + Invalid(BlockValidationState), +} + use super::transaction::{TransactionRef, TxOutRef}; /// Common operations for block hashes, implemented by both owned and borrowed types. @@ -942,6 +969,61 @@ impl Block { pub fn transactions(&self) -> BlockTransactionIter<'_> { BlockTransactionIter::new(self) } + + /// Performs context-free validation checks on this block. + /// + /// Runs base structural checks (size, weight, coinbase, transactions, + /// sigops) without requiring chainstate or block index access. + /// Proof-of-work and merkle-root checks are optional and toggled via `flags`. + /// + /// # Arguments + /// * `chain_params` - Chain parameters providing consensus rules + /// * `flags` - Bitmask of [`BLOCK_CHECK_BASE`], [`BLOCK_CHECK_POW`], + /// [`BLOCK_CHECK_MERKLE`], or [`BLOCK_CHECK_ALL`] + /// + /// # Returns + /// [`BlockCheckResult::Valid`] on success, otherwise + /// [`BlockCheckResult::Invalid`] carrying the validation state. + /// + /// # Examples + /// ```no_run + /// # use bitcoinkernel::{ + /// # prelude::*, Block, BlockCheckResult, ChainParams, ChainType, BLOCK_CHECK_ALL, + /// # }; + /// # fn example() -> Result<(), bitcoinkernel::KernelError> { + /// # let block_data = vec![0u8; 100]; // placeholder + /// # let block = Block::new(&block_data)?; + /// let chain_params = ChainParams::new(ChainType::Mainnet); + /// + /// match block.check(&chain_params, BLOCK_CHECK_ALL) { + /// BlockCheckResult::Valid => println!("ok"), + /// BlockCheckResult::Invalid(state) => println!("failed: {:?}", state.result()), + /// } + /// # Ok(()) + /// # } + /// ``` + pub fn check( + &self, + chain_params: &ChainParams, + flags: btck_BlockCheckFlags, + ) -> BlockCheckResult { + let state = BlockValidationState::new(); + let consensus_params = + unsafe { btck_chain_parameters_get_consensus_params(chain_params.as_ptr()) }; + let result = unsafe { + btck_block_check( + self.inner, + consensus_params, + flags, + state.as_ptr() as *mut btck_BlockValidationState, + ) + }; + if c_helpers::verification_passed(result) { + BlockCheckResult::Valid + } else { + BlockCheckResult::Invalid(state) + } + } } impl AsPtr for Block { @@ -2288,4 +2370,127 @@ mod tests { "00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048" ); } + + const MAINNET_BLOCK_1_HEX: &str = "010000006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c\ + 68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1cdb606e857233e0e61bc6649ff\ + ff001d01e362990101000000010000000000000000000000000000000000000000000000000000000000000000\ + ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae\ + 1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c8\ + 58eeac00000000"; + + #[test] + fn check_valid_block_passes_base_and_all() { + use crate::prelude::*; + use crate::ChainType; + + let raw_block = hex::decode(MAINNET_BLOCK_1_HEX).unwrap(); + let chain_params = ChainParams::new(ChainType::Mainnet); + let block = Block::new(&raw_block).unwrap(); + + assert!(matches!( + block.check(&chain_params, BLOCK_CHECK_BASE), + BlockCheckResult::Valid + )); + assert!(matches!( + block.check(&chain_params, BLOCK_CHECK_ALL), + BlockCheckResult::Valid + )); + } + + #[test] + fn check_mutated_merkle_root() { + use crate::prelude::*; + use crate::{BlockValidationResult, ChainType, ValidationMode}; + + const MERKLE_ROOT_OFFSET: usize = 4 // version + + 32; // prev_hash + + let mut raw_block = hex::decode(MAINNET_BLOCK_1_HEX).unwrap(); + raw_block[MERKLE_ROOT_OFFSET] ^= 0x01; + let chain_params = ChainParams::new(ChainType::Mainnet); + let block = Block::new(&raw_block).unwrap(); + + match block.check(&chain_params, BLOCK_CHECK_MERKLE) { + BlockCheckResult::Invalid(state) => { + assert_eq!(state.mode(), ValidationMode::Invalid); + assert_eq!(state.result(), BlockValidationResult::Mutated); + } + _ => assert!(false), + } + + // Mutating the merkle root also alters the block hash, so the + // combined ALL check fails on the PoW path before the merkle + // path runs. + match block.check(&chain_params, BLOCK_CHECK_ALL) { + BlockCheckResult::Invalid(state) => { + assert_eq!(state.mode(), ValidationMode::Invalid); + assert_eq!(state.result(), BlockValidationResult::InvalidHeader); + } + _ => assert!(false), + } + + assert!(matches!( + block.check(&chain_params, BLOCK_CHECK_BASE), + BlockCheckResult::Valid + )); + } + + #[test] + fn check_invalid_pow() { + use crate::prelude::*; + use crate::{BlockValidationResult, ChainType, ValidationMode}; + + const NBITS_OFFSET: usize = 4 // version + + 32 // prev_hash + + 32 // merkle_root + + 4; // timestamp + + let mut raw_block = hex::decode(MAINNET_BLOCK_1_HEX).unwrap(); + raw_block[NBITS_OFFSET + 3] = 0x1c; + let chain_params = ChainParams::new(ChainType::Mainnet); + let block = Block::new(&raw_block).unwrap(); + + match block.check(&chain_params, BLOCK_CHECK_POW) { + BlockCheckResult::Invalid(state) => { + assert_eq!(state.mode(), ValidationMode::Invalid); + assert_eq!(state.result(), BlockValidationResult::InvalidHeader); + } + _ => assert!(false), + } + + assert!(matches!( + block.check(&chain_params, BLOCK_CHECK_MERKLE), + BlockCheckResult::Valid + )); + } + + #[test] + fn check_tampered_coinbase() { + use crate::prelude::*; + use crate::{BlockValidationResult, ChainType, ValidationMode}; + + const COINBASE_PREVOUT_N_OFFSET: usize = 4 // version + + 32 // prev_hash + + 32 // merkle_root + + 4 // timestamp + + 4 // bits + + 4 // nonce + + 1 // tx count varint + + 4 // tx version + + 1 // vin count + + 32; // prevout hash + + let mut raw_block = hex::decode(MAINNET_BLOCK_1_HEX).unwrap(); + raw_block[COINBASE_PREVOUT_N_OFFSET] = 0x00; + let chain_params = ChainParams::new(ChainType::Mainnet); + let block = Block::new(&raw_block).unwrap(); + + match block.check(&chain_params, BLOCK_CHECK_BASE) { + BlockCheckResult::Invalid(state) => { + assert_eq!(state.mode(), ValidationMode::Invalid); + assert_eq!(state.result(), BlockValidationResult::Consensus); + } + _ => assert!(false), + } + } } diff --git a/src/core/mod.rs b/src/core/mod.rs index 1ab2f07b..127c49bc 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -5,8 +5,8 @@ pub mod transaction; pub mod verify; pub use block::{ - Block, BlockHash, BlockHeader, BlockSpentOutputs, BlockSpentOutputsRef, Coin, CoinRef, - TransactionSpentOutputs, TransactionSpentOutputsRef, + Block, BlockCheckResult, BlockHash, BlockHeader, BlockSpentOutputs, BlockSpentOutputsRef, Coin, + CoinRef, TransactionSpentOutputs, TransactionSpentOutputsRef, }; pub use block_tree_entry::BlockTreeEntry; pub use script::{ScriptPubkey, ScriptPubkeyRef}; @@ -29,3 +29,9 @@ pub mod verify_flags { VERIFY_DERSIG, VERIFY_NONE, VERIFY_NULLDUMMY, VERIFY_P2SH, VERIFY_TAPROOT, VERIFY_WITNESS, }; } + +pub mod block_check_flags { + pub use super::block::{ + BLOCK_CHECK_ALL, BLOCK_CHECK_BASE, BLOCK_CHECK_MERKLE, BLOCK_CHECK_POW, + }; +} diff --git a/src/ffi/constants.rs b/src/ffi/constants.rs index 74e2e5b4..bec2e98b 100644 --- a/src/ffi/constants.rs +++ b/src/ffi/constants.rs @@ -1,7 +1,7 @@ use crate::{ - btck_BlockValidationResult, btck_ChainType, btck_LogCategory, btck_LogLevel, - btck_ScriptVerificationFlags, btck_ScriptVerifyStatus, btck_SynchronizationState, - btck_ValidationMode, btck_Warning, + btck_BlockCheckFlags, btck_BlockValidationResult, btck_ChainType, btck_LogCategory, + btck_LogLevel, btck_ScriptVerificationFlags, btck_ScriptVerifyStatus, + btck_SynchronizationState, btck_ValidationMode, btck_Warning, }; // Synchronization States @@ -72,6 +72,13 @@ pub const BTCK_SCRIPT_VERIFICATION_FLAGS_ALL: btck_ScriptVerificationFlags = | BTCK_SCRIPT_VERIFICATION_FLAGS_WITNESS | BTCK_SCRIPT_VERIFICATION_FLAGS_TAPROOT; +// Block Check Flags +pub const BTCK_BLOCK_CHECK_FLAGS_BASE: btck_BlockCheckFlags = 0; +pub const BTCK_BLOCK_CHECK_FLAGS_POW: btck_BlockCheckFlags = 1 << 0; +pub const BTCK_BLOCK_CHECK_FLAGS_MERKLE: btck_BlockCheckFlags = 1 << 1; +pub const BTCK_BLOCK_CHECK_FLAGS_ALL: btck_BlockCheckFlags = + BTCK_BLOCK_CHECK_FLAGS_POW | BTCK_BLOCK_CHECK_FLAGS_MERKLE; + // Chain types pub const BTCK_CHAIN_TYPE_MAINNET: btck_ChainType = 0; pub const BTCK_CHAIN_TYPE_TESTNET: btck_ChainType = 1; diff --git a/src/lib.rs b/src/lib.rs index 935aaaa7..49f5ee38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -297,10 +297,11 @@ impl std::error::Error for KernelError { } pub use crate::core::{ - verify, Block, BlockHash, BlockHeader, BlockSpentOutputs, BlockSpentOutputsRef, BlockTreeEntry, - Coin, CoinRef, PrecomputedTransactionData, ScriptPubkey, ScriptPubkeyRef, ScriptVerifyError, - Transaction, TransactionRef, TransactionSpentOutputs, TransactionSpentOutputsRef, TxIn, - TxInRef, TxOut, TxOutPoint, TxOutPointRef, TxOutRef, Txid, TxidRef, + verify, Block, BlockCheckResult, BlockHash, BlockHeader, BlockSpentOutputs, + BlockSpentOutputsRef, BlockTreeEntry, Coin, CoinRef, PrecomputedTransactionData, ScriptPubkey, + ScriptPubkeyRef, ScriptVerifyError, Transaction, TransactionRef, TransactionSpentOutputs, + TransactionSpentOutputsRef, TxIn, TxInRef, TxOut, TxOutPoint, TxOutPointRef, TxOutRef, Txid, + TxidRef, }; pub use crate::log::{disable_logging, Log, LogCategory, LogLevel, Logger}; @@ -317,6 +318,10 @@ pub use crate::state::{ ContextBuilder, ProcessBlockHeaderResult, ProcessBlockResult, }; +pub use crate::core::block_check_flags::{ + BLOCK_CHECK_ALL, BLOCK_CHECK_BASE, BLOCK_CHECK_MERKLE, BLOCK_CHECK_POW, +}; + pub use crate::core::verify_flags::{ VERIFY_ALL, VERIFY_ALL_PRE_TAPROOT, VERIFY_CHECKLOCKTIMEVERIFY, VERIFY_CHECKSEQUENCEVERIFY, VERIFY_DERSIG, VERIFY_NONE, VERIFY_NULLDUMMY, VERIFY_P2SH, VERIFY_TAPROOT, VERIFY_WITNESS, diff --git a/src/state/context.rs b/src/state/context.rs index ea2062fe..ab87080b 100644 --- a/src/state/context.rs +++ b/src/state/context.rs @@ -143,6 +143,12 @@ impl Drop for ChainParams { } } +impl AsPtr for ChainParams { + fn as_ptr(&self) -> *const btck_ChainParameters { + self.inner as *const _ + } +} + /// The main context for the Bitcoin Kernel library. /// /// The [`Context`] manages the global state of the Bitcoin Kernel library diff --git a/tests/test.rs b/tests/test.rs index 364e41be..e692609e 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -7,7 +7,7 @@ mod tests { use bitcoinkernel::state::chainstate::ProcessBlockHeaderResult; use bitcoinkernel::{ prelude::*, verify, Block, BlockHash, BlockHeader, BlockSpentOutputs, BlockTreeEntry, - BlockValidationStateRef, ChainParams, ChainType, ChainstateManager, + BlockValidationResult, BlockValidationStateRef, ChainParams, ChainType, ChainstateManager, ChainstateManagerBuilder, Coin, Context, ContextBuilder, KernelError, Log, Logger, PrecomputedTransactionData, ScriptPubkey, ScriptVerifyError, Transaction, TransactionSpentOutputs, TxIn, TxOut, ValidationMode, VERIFY_ALL, VERIFY_ALL_PRE_TAPROOT,