Motivation
Recent work on exposing the Bitcoin kernel C header API, the usage of API, has increased demand for certain changes to block validation, e,g an ongoing effort to modularise block validation #32317. However, validation, especially block, is very critical and changes to it demand the utmost scrutiny and a high bar. Which is most of the time slow and careful code review and testing.
As noticeable during review of #32317, a subtle issue (https://github.com/bitcoin/bitcoin/pull/32317#discussion_r2819423976) went unnoticed for some time, which suggests that the current test coverage around block validation paths is not sufficiently comprehensive.
We have a comprehensive block validation functional test but that is not enough because
Block validation today is exercised through multiple entry points, such as TestBlockValidity, ProcessNewBlock, ConnectTip, VerifyDB, so having a structured test suite that targets malformed blocks across those validation paths/simulated ones, and testing invariants to ensure subtle regressions do not slip through review is important.
This PR aims to improve confidence in validation changes by introducing a structured set of unit tests that target specific consensus failure modes, alongside a fuzz target that aims stress-tests the validation pipeline and enforces key invariants. The goal is not to fully de-risk validation changes, but to significantly improve the likelihood of catching subtle bugs early in CI.
Changes
-
This pr introduces block validation unit tests that are split into header, stateless, contextual, and stateful validation cases.
-
The header tests exercise failures related purely to block headers by constructing otherwise valid blocks and mutating header fields, ensuring that header-level checks fail with the expected validation results and reject reasons.
-
The stateless tests include malformed block structures such as missing or multiple coinbase transactions, invalid merkle roots, and duplicate transactions. They also cover transaction-level structural issues such as empty inputs or outputs, invalid output values including negative or overflowing amounts, and duplicate inputs within a transaction. Each test constructs a minimally valid block and then applies targeted mutations to trigger specific consensus failures, verifying both the validation result and the exact reject reason string.
-
The contextual tests cover rules that depend on chain context. These include enforcing correct coinbase height encoding, detecting coinbase overpayment, preventing premature spending of coinbase outputs, and checking transaction finality conditions. These tests rely on the active chain state and ensure that contextual validation correctly rejects blocks that violate consensus rules tied to block height or historical state.
-
The stateful tests construct blocks that spend from UTXO sets and then introduce failures such as missing or already spent inputs, input value overflows, and cases where total inputs are less than total outputs. Script validation failures are also covered by creating real spends that fail script execution under consensus rules. These tests verify that stateful validation correctly enforces value conservation and script correctness.
-
To support these tests, reusable helpers are introduced in test/util/block_validity.h. These helpers centralize the invocation of TestBlockValidity and standardize assertions on validation results, reject reasons, and debug messages. They also provide utilities for constructing spendable UTXOs, which simplifies the creation of stateful test scenarios and improves readability and consistency across tests. The aim is to also add other block validation paths in this util that can be easily invoked by this mutated blocks.
In addition to the unit tests, this PR adds a new fuzz target named validation_block_validity. The fuzzer begins from either a minimally constructed valid block or a block produced by BlockAssembler and then applies a series of mutations. These mutations affect header fields such as version, time, difficulty bits, nonce, and previous block hash, as well as transaction-level structure by duplicating, removing, reordering, or inserting malformed transactions. Contextual mutations include modifying the coinbase transaction in ways that violate consensus rules and appending valid spends of matured coinbase outputs. The merkle root is either recomputed or replaced with random data.
After mutation, the fuzzer invokes TestBlockValidity with randomized flags controlling proof-of-work and merkle root checks. It then asserts two critical invariants: that the chainstate height remains unchanged, ensuring that validation is side-effect free, and that exactly one of IsValid, IsInvalid, or IsError is set on the returned validation state. This provides a smoke test for the block validity and could uncover edge cases not explicitly covered by unit tests.
Open questions
There is some overlap between these tests and existing validation tests. Is it better to update existing tests to use the helpers introduced here, or to keep this suite separate for SOC. Additionally, while the current tests cover a substantial portion of consensus rules and some soft fork logic, they are not exhaustive. What is considered sufficient?.