Transactions from blocks

Transactions learned about from blocks:

  • Might not be present in our mempool

  • Are not being considered for entry into our mempool and therefore do not have to pass policy checks

  • Are only subject to consensus checks

This means that we can validate these transactions based only on our copy of the UTXO set and the data contained within the block itself. We call ProcessBlock() when processing new blocks received from the P2P network (in net_processing.cpp) from net message types: NetMsgType::CMPCTBLOCK, NetMsgType::BLOCKTXN and NetMsgType::BLOCK.

block tx validation
Figure 1. Abbreviated block transaction validation

The general flow of ProcessBlock() is that will call CheckBlock(), AcceptBlock() and then ActivateBestChain(). A block which has passed successfully through CheckBlock() and AcceptBlock() has not passed full consensus validation.

CheckBlock() does some cheap, context-independent structural validity checks, along with (re-)checking the proof of work in the header, however these checks just determine that the block is "valid-enough" to proceed to AcceptBlock().

Once the checks have been completed, the block.fChecked value is set to true. This will enable any subsequent calls to this function with this block to be skipped.

AcceptBlock() is used to persist the block to disk so that we can (validate it and) add it to our chain immediately, use it later, or discard it later. AcceptBlock() makes a second call to CheckBlock() but because block.fChecked was set to true on the first pass this second check will be skipped.

AcceptBlock() contains an inner call to CheckBlock() because it can also be called directly by CChainState::LoadExternalBlockFile() where CheckBlock() will not have been previously called.

It also now runs some contextual checks such as checking the block time, transaction lock times (transaction are "finalized") and witness commitments are either non-existent or valid (link). After this the block will be serialized to disk.

At this stage we might still be writing blocks to disk that will fail full consensus checks. However, if they have reached here they have passed proof of work and structural checks, so consensus failures may be due to us missing intermediate blocks, or that there are competing chain tips. In these cases this block may still be useful to us in the future.

Once the block has been written to disk by AcceptBlock() full validation of the block and its transactions begins via CChainState::ActivateBestChain() and its inner call to ActivateBestChainStep().

As part of ProcessBlock() we end up calling CheckBlock() twice: once on the inner ProcessNewBlock() and, if this first is successful, once again inside of AcceptBlock(). We find the following code comment inside ProcessBlock():

    // Skipping AcceptBlock() for CheckBlock() failures means that we will never mark a block as invalid if
    // CheckBlock() fails.  This is protective against consensus failure if there are any unknown forms of block
    // malleability that cause CheckBlock() to fail; see e.g. CVE-2012-2459 and
    //  Because CheckBlock() is
    // not very expensive, the anti-DoS benefits of caching failure (of a definitely-invalid block) are not substantial.
    bool ret = CheckBlock(*block, state, chainparams.GetConsensus());
    if (ret) {
        // Store to disk
        ret = ActiveChainstate().AcceptBlock(block, state, &pindex, force_processing, nullptr, new_block);

The threat vector being addressed is that a malicious node could create a block (with malleated merkle tree interior) but still have it compute the same merkle root. This would lead to nodes marking this block as invalid as expected. However, a valid un-malleated block with the same merkle root, which we might receive later from an honest peer, would be rejected by our node because we cache "bad" blocks using the m_blockman.m_dirty_blockindex set:

    if (!CheckBlock(block, state, m_params.GetConsensus()) ||
        !ContextualCheckBlock(block, state, m_params.GetConsensus(), pindex->pprev)) {
        if (state.IsInvalid() && state.GetResult() != BlockValidationResult::BLOCK_MUTATED) {
            pindex->nStatus |= BLOCK_FAILED_VALID;
        return error("%s: %s", __func__, state.ToString());

The rationale for caching bad blocks is so that we don’t expend resources re-validating and propagating them, opening ourselves and the wider network up to a DoS vector, where an attacker can flood nodes with invalid blocks and hope they expend resources gossiping and re-validating them.

Therefore we call CheckBlock() first, and only try AcceptBlock() if this passes.

Note here how the developers have had to balance consideration for sensitive validation code, staying in consensus with the rest of the network and avoiding potential P2P DoS attacks. This type of thinking is common across the codebase.