Skip to content

Latest commit

 

History

History
133 lines (109 loc) · 7.2 KB

transactions-from-blocks.adoc

File metadata and controls

133 lines (109 loc) · 7.2 KB

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.

Abbreviated block transaction validation
flowchart LR
    process_block["ProcessBlock()"]
    process_new_block["ProcessNewBlock()"]
    check_block_header["CheckBlockHeader()"]
    block_merkle["BlockMerkleRoot()"]
    check_transaction["CheckTransaction()"]
    subgraph sub_check_block ["CheckBlock()"]
        direction TB
        check_block_header --> block_merkle
        block_merkle --> check_transaction
    end

    accept_block_header["AcceptBlockHeader()"]
    check_block_index["CheckBlockIndex()"]
    check_block["CheckBlock()"]
    contextual_check_block["ContextualCheckBlock()"]
    save_block_disk["SaveBlockToDisk()"]
    recv_block_tx["ReceivedBlockTransactions()"]
    subgraph sub_accept_block["AcceptBlock()"]
        direction TB
        accept_block_header --> check_block_index
        check_block_index --> check_block
        check_block --> contextual_check_block
        contextual_check_block --> save_block_disk
        save_block_disk --> recv_block_tx
    end

    activate_best_chain_step["ActivateBestChainStep()"]
    connect_tip["ConnectTip()"]
    connect_block["ConnectBlock()"]

    subgraph activate_chain["ActivateBestChain()"]
        direction TB
        activate_best_chain_step --> connect_tip
        connect_tip --> connect_block
    end

    process_block --> process_new_block
    process_new_block --> sub_check_block
    sub_check_block --> sub_accept_block
    sub_accept_block --> activate_chain

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.

Tip
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.

Note

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():

validation.cpp#ChainstateManager::ProcessNewBlock()
    // 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
    // https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2019-February/016697.html.  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:

validation.cpp#CChainState::AcceptBlock()
    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;
            m_blockman.m_dirty_blockindex.insert(pindex);
        }
        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.