mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-05-04 17:00:52 +02:00
fa538813b1c382cf135cbf2a0cc3fa01f36964d8 scripted-diff: Replace ::mempool with m_node.mempool in tests (MarcoFalke) 8888ad02e204b0fa7a2ea2cfed2fc3f298cf1623 test: Replace recursive lock with locking annotations (MarcoFalke) fac07f2038a3ccd5edadc6e6122c02fa30e697bd node: Add reference to mempool in NodeContext (MarcoFalke) Pull request description: This is the first step toward making the mempool a global that is not initialized before main. #### Motivation Currently the mempool is a global that is initialized before the `main` function. This is confusing and easy to get wrong. E.g. the mempool constructor queries state that has not been initialized, like randomness (fixed), or command line arguments (not an issue last time I checked). Also without having the chainstate (chain tip) initialized first, it doesn't make conceptually sense to have a mempool, since the mempool builds txs on top of the utxo set (chain tip). Finally, in the future someone might want to run a consensus-only full node (`-nowallet -noblockfilter -no... -nomempool` command line options) that only verifies blocks and updates the utxo set. This is conceptually the same change that has already been done for the connection manager `CConnman`. ACKs for top commit: jnewbery: utACK fa538813b1c382cf135cbf2a0cc3fa01f36964d8 ariard: Tested ACK fa53881. Tree-SHA512: 2c446a8a51476354aad7126c2b833500d36b24490caa94f847b2bdc622054de0dae28980f23e3d91b1b492dc32931656d98dbd019af9e4e58f2f8c5375aac694
334 lines
12 KiB
C++
334 lines
12 KiB
C++
// Copyright (c) 2018-2019 The Bitcoin Core developers
|
|
// Distributed under the MIT software license, see the accompanying
|
|
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
|
|
#include <boost/test/unit_test.hpp>
|
|
|
|
#include <chainparams.h>
|
|
#include <consensus/merkle.h>
|
|
#include <consensus/validation.h>
|
|
#include <miner.h>
|
|
#include <pow.h>
|
|
#include <random.h>
|
|
#include <script/standard.h>
|
|
#include <test/util/setup_common.h>
|
|
#include <util/time.h>
|
|
#include <validation.h>
|
|
#include <validationinterface.h>
|
|
|
|
#include <thread>
|
|
|
|
static const std::vector<unsigned char> V_OP_TRUE{OP_TRUE};
|
|
|
|
BOOST_FIXTURE_TEST_SUITE(validation_block_tests, RegTestingSetup)
|
|
|
|
struct TestSubscriber : public CValidationInterface {
|
|
uint256 m_expected_tip;
|
|
|
|
explicit TestSubscriber(uint256 tip) : m_expected_tip(tip) {}
|
|
|
|
void UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork, bool fInitialDownload) override
|
|
{
|
|
BOOST_CHECK_EQUAL(m_expected_tip, pindexNew->GetBlockHash());
|
|
}
|
|
|
|
void BlockConnected(const std::shared_ptr<const CBlock>& block, const CBlockIndex* pindex, const std::vector<CTransactionRef>& txnConflicted) override
|
|
{
|
|
BOOST_CHECK_EQUAL(m_expected_tip, block->hashPrevBlock);
|
|
BOOST_CHECK_EQUAL(m_expected_tip, pindex->pprev->GetBlockHash());
|
|
|
|
m_expected_tip = block->GetHash();
|
|
}
|
|
|
|
void BlockDisconnected(const std::shared_ptr<const CBlock>& block, const CBlockIndex* pindex) override
|
|
{
|
|
BOOST_CHECK_EQUAL(m_expected_tip, block->GetHash());
|
|
BOOST_CHECK_EQUAL(m_expected_tip, pindex->GetBlockHash());
|
|
|
|
m_expected_tip = block->hashPrevBlock;
|
|
}
|
|
};
|
|
|
|
std::shared_ptr<CBlock> Block(const uint256& prev_hash)
|
|
{
|
|
static int i = 0;
|
|
static uint64_t time = Params().GenesisBlock().nTime;
|
|
|
|
CScript pubKey;
|
|
pubKey << i++ << OP_TRUE;
|
|
|
|
auto ptemplate = BlockAssembler(Params()).CreateNewBlock(pubKey);
|
|
auto pblock = std::make_shared<CBlock>(ptemplate->block);
|
|
pblock->hashPrevBlock = prev_hash;
|
|
pblock->nTime = ++time;
|
|
|
|
pubKey.clear();
|
|
{
|
|
WitnessV0ScriptHash witness_program;
|
|
CSHA256().Write(&V_OP_TRUE[0], V_OP_TRUE.size()).Finalize(witness_program.begin());
|
|
pubKey << OP_0 << ToByteVector(witness_program);
|
|
}
|
|
|
|
// Make the coinbase transaction with two outputs:
|
|
// One zero-value one that has a unique pubkey to make sure that blocks at the same height can have a different hash
|
|
// Another one that has the coinbase reward in a P2WSH with OP_TRUE as witness program to make it easy to spend
|
|
CMutableTransaction txCoinbase(*pblock->vtx[0]);
|
|
txCoinbase.vout.resize(2);
|
|
txCoinbase.vout[1].scriptPubKey = pubKey;
|
|
txCoinbase.vout[1].nValue = txCoinbase.vout[0].nValue;
|
|
txCoinbase.vout[0].nValue = 0;
|
|
txCoinbase.vin[0].scriptWitness.SetNull();
|
|
pblock->vtx[0] = MakeTransactionRef(std::move(txCoinbase));
|
|
|
|
return pblock;
|
|
}
|
|
|
|
std::shared_ptr<CBlock> FinalizeBlock(std::shared_ptr<CBlock> pblock)
|
|
{
|
|
LOCK(cs_main); // For LookupBlockIndex
|
|
GenerateCoinbaseCommitment(*pblock, LookupBlockIndex(pblock->hashPrevBlock), Params().GetConsensus());
|
|
|
|
pblock->hashMerkleRoot = BlockMerkleRoot(*pblock);
|
|
|
|
while (!CheckProofOfWork(pblock->GetHash(), pblock->nBits, Params().GetConsensus())) {
|
|
++(pblock->nNonce);
|
|
}
|
|
|
|
return pblock;
|
|
}
|
|
|
|
// construct a valid block
|
|
std::shared_ptr<const CBlock> GoodBlock(const uint256& prev_hash)
|
|
{
|
|
return FinalizeBlock(Block(prev_hash));
|
|
}
|
|
|
|
// construct an invalid block (but with a valid header)
|
|
std::shared_ptr<const CBlock> BadBlock(const uint256& prev_hash)
|
|
{
|
|
auto pblock = Block(prev_hash);
|
|
|
|
CMutableTransaction coinbase_spend;
|
|
coinbase_spend.vin.push_back(CTxIn(COutPoint(pblock->vtx[0]->GetHash(), 0), CScript(), 0));
|
|
coinbase_spend.vout.push_back(pblock->vtx[0]->vout[0]);
|
|
|
|
CTransactionRef tx = MakeTransactionRef(coinbase_spend);
|
|
pblock->vtx.push_back(tx);
|
|
|
|
auto ret = FinalizeBlock(pblock);
|
|
return ret;
|
|
}
|
|
|
|
void BuildChain(const uint256& root, int height, const unsigned int invalid_rate, const unsigned int branch_rate, const unsigned int max_size, std::vector<std::shared_ptr<const CBlock>>& blocks)
|
|
{
|
|
if (height <= 0 || blocks.size() >= max_size) return;
|
|
|
|
bool gen_invalid = InsecureRandRange(100) < invalid_rate;
|
|
bool gen_fork = InsecureRandRange(100) < branch_rate;
|
|
|
|
const std::shared_ptr<const CBlock> pblock = gen_invalid ? BadBlock(root) : GoodBlock(root);
|
|
blocks.push_back(pblock);
|
|
if (!gen_invalid) {
|
|
BuildChain(pblock->GetHash(), height - 1, invalid_rate, branch_rate, max_size, blocks);
|
|
}
|
|
|
|
if (gen_fork) {
|
|
blocks.push_back(GoodBlock(root));
|
|
BuildChain(blocks.back()->GetHash(), height - 1, invalid_rate, branch_rate, max_size, blocks);
|
|
}
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(processnewblock_signals_ordering)
|
|
{
|
|
// build a large-ish chain that's likely to have some forks
|
|
std::vector<std::shared_ptr<const CBlock>> blocks;
|
|
while (blocks.size() < 50) {
|
|
blocks.clear();
|
|
BuildChain(Params().GenesisBlock().GetHash(), 100, 15, 10, 500, blocks);
|
|
}
|
|
|
|
bool ignored;
|
|
BlockValidationState state;
|
|
std::vector<CBlockHeader> headers;
|
|
std::transform(blocks.begin(), blocks.end(), std::back_inserter(headers), [](std::shared_ptr<const CBlock> b) { return b->GetBlockHeader(); });
|
|
|
|
// Process all the headers so we understand the toplogy of the chain
|
|
BOOST_CHECK(ProcessNewBlockHeaders(headers, state, Params()));
|
|
|
|
// Connect the genesis block and drain any outstanding events
|
|
BOOST_CHECK(ProcessNewBlock(Params(), std::make_shared<CBlock>(Params().GenesisBlock()), true, &ignored));
|
|
SyncWithValidationInterfaceQueue();
|
|
|
|
// subscribe to events (this subscriber will validate event ordering)
|
|
const CBlockIndex* initial_tip = nullptr;
|
|
{
|
|
LOCK(cs_main);
|
|
initial_tip = ::ChainActive().Tip();
|
|
}
|
|
TestSubscriber sub(initial_tip->GetBlockHash());
|
|
RegisterValidationInterface(&sub);
|
|
|
|
// create a bunch of threads that repeatedly process a block generated above at random
|
|
// this will create parallelism and randomness inside validation - the ValidationInterface
|
|
// will subscribe to events generated during block validation and assert on ordering invariance
|
|
std::vector<std::thread> threads;
|
|
for (int i = 0; i < 10; i++) {
|
|
threads.emplace_back([&blocks]() {
|
|
bool ignored;
|
|
FastRandomContext insecure;
|
|
for (int i = 0; i < 1000; i++) {
|
|
auto block = blocks[insecure.randrange(blocks.size() - 1)];
|
|
ProcessNewBlock(Params(), block, true, &ignored);
|
|
}
|
|
|
|
// to make sure that eventually we process the full chain - do it here
|
|
for (auto block : blocks) {
|
|
if (block->vtx.size() == 1) {
|
|
bool processed = ProcessNewBlock(Params(), block, true, &ignored);
|
|
assert(processed);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& t : threads) {
|
|
t.join();
|
|
}
|
|
while (GetMainSignals().CallbacksPending() > 0) {
|
|
MilliSleep(100);
|
|
}
|
|
|
|
UnregisterValidationInterface(&sub);
|
|
|
|
LOCK(cs_main);
|
|
BOOST_CHECK_EQUAL(sub.m_expected_tip, ::ChainActive().Tip()->GetBlockHash());
|
|
}
|
|
|
|
/**
|
|
* Test that mempool updates happen atomically with reorgs.
|
|
*
|
|
* This prevents RPC clients, among others, from retrieving immediately-out-of-date mempool data
|
|
* during large reorgs.
|
|
*
|
|
* The test verifies this by creating a chain of `num_txs` blocks, matures their coinbases, and then
|
|
* submits txns spending from their coinbase to the mempool. A fork chain is then processed,
|
|
* invalidating the txns and evicting them from the mempool.
|
|
*
|
|
* We verify that the mempool updates atomically by polling it continuously
|
|
* from another thread during the reorg and checking that its size only changes
|
|
* once. The size changing exactly once indicates that the polling thread's
|
|
* view of the mempool is either consistent with the chain state before reorg,
|
|
* or consistent with the chain state after the reorg, and not just consistent
|
|
* with some intermediate state during the reorg.
|
|
*/
|
|
BOOST_AUTO_TEST_CASE(mempool_locks_reorg)
|
|
{
|
|
bool ignored;
|
|
auto ProcessBlock = [&ignored](std::shared_ptr<const CBlock> block) -> bool {
|
|
return ProcessNewBlock(Params(), block, /* fForceProcessing */ true, /* fNewBlock */ &ignored);
|
|
};
|
|
|
|
// Process all mined blocks
|
|
BOOST_REQUIRE(ProcessBlock(std::make_shared<CBlock>(Params().GenesisBlock())));
|
|
auto last_mined = GoodBlock(Params().GenesisBlock().GetHash());
|
|
BOOST_REQUIRE(ProcessBlock(last_mined));
|
|
|
|
// Run the test multiple times
|
|
for (int test_runs = 3; test_runs > 0; --test_runs) {
|
|
BOOST_CHECK_EQUAL(last_mined->GetHash(), ::ChainActive().Tip()->GetBlockHash());
|
|
|
|
// Later on split from here
|
|
const uint256 split_hash{last_mined->hashPrevBlock};
|
|
|
|
// Create a bunch of transactions to spend the miner rewards of the
|
|
// most recent blocks
|
|
std::vector<CTransactionRef> txs;
|
|
for (int num_txs = 22; num_txs > 0; --num_txs) {
|
|
CMutableTransaction mtx;
|
|
mtx.vin.push_back(CTxIn{COutPoint{last_mined->vtx[0]->GetHash(), 1}, CScript{}});
|
|
mtx.vin[0].scriptWitness.stack.push_back(V_OP_TRUE);
|
|
mtx.vout.push_back(last_mined->vtx[0]->vout[1]);
|
|
mtx.vout[0].nValue -= 1000;
|
|
txs.push_back(MakeTransactionRef(mtx));
|
|
|
|
last_mined = GoodBlock(last_mined->GetHash());
|
|
BOOST_REQUIRE(ProcessBlock(last_mined));
|
|
}
|
|
|
|
// Mature the inputs of the txs
|
|
for (int j = COINBASE_MATURITY; j > 0; --j) {
|
|
last_mined = GoodBlock(last_mined->GetHash());
|
|
BOOST_REQUIRE(ProcessBlock(last_mined));
|
|
}
|
|
|
|
// Mine a reorg (and hold it back) before adding the txs to the mempool
|
|
const uint256 tip_init{last_mined->GetHash()};
|
|
|
|
std::vector<std::shared_ptr<const CBlock>> reorg;
|
|
last_mined = GoodBlock(split_hash);
|
|
reorg.push_back(last_mined);
|
|
for (size_t j = COINBASE_MATURITY + txs.size() + 1; j > 0; --j) {
|
|
last_mined = GoodBlock(last_mined->GetHash());
|
|
reorg.push_back(last_mined);
|
|
}
|
|
|
|
// Add the txs to the tx pool
|
|
{
|
|
LOCK(cs_main);
|
|
TxValidationState state;
|
|
std::list<CTransactionRef> plTxnReplaced;
|
|
for (const auto& tx : txs) {
|
|
BOOST_REQUIRE(AcceptToMemoryPool(
|
|
*m_node.mempool,
|
|
state,
|
|
tx,
|
|
&plTxnReplaced,
|
|
/* bypass_limits */ false,
|
|
/* nAbsurdFee */ 0));
|
|
}
|
|
}
|
|
|
|
// Check that all txs are in the pool
|
|
{
|
|
LOCK(m_node.mempool->cs);
|
|
BOOST_CHECK_EQUAL(m_node.mempool->mapTx.size(), txs.size());
|
|
}
|
|
|
|
// Run a thread that simulates an RPC caller that is polling while
|
|
// validation is doing a reorg
|
|
std::thread rpc_thread{[&]() {
|
|
// This thread is checking that the mempool either contains all of
|
|
// the transactions invalidated by the reorg, or none of them, and
|
|
// not some intermediate amount.
|
|
while (true) {
|
|
LOCK(m_node.mempool->cs);
|
|
if (m_node.mempool->mapTx.size() == 0) {
|
|
// We are done with the reorg
|
|
break;
|
|
}
|
|
// Internally, we might be in the middle of the reorg, but
|
|
// externally the reorg to the most-proof-of-work chain should
|
|
// be atomic. So the caller assumes that the returned mempool
|
|
// is consistent. That is, it has all txs that were there
|
|
// before the reorg.
|
|
assert(m_node.mempool->mapTx.size() == txs.size());
|
|
continue;
|
|
}
|
|
LOCK(cs_main);
|
|
// We are done with the reorg, so the tip must have changed
|
|
assert(tip_init != ::ChainActive().Tip()->GetBlockHash());
|
|
}};
|
|
|
|
// Submit the reorg in this thread to invalidate and remove the txs from the tx pool
|
|
for (const auto& b : reorg) {
|
|
ProcessBlock(b);
|
|
}
|
|
// Check that the reorg was eventually successful
|
|
BOOST_CHECK_EQUAL(last_mined->GetHash(), ::ChainActive().Tip()->GetBlockHash());
|
|
|
|
// We can join the other thread, which returns when the reorg was successful
|
|
rpc_thread.join();
|
|
}
|
|
}
|
|
BOOST_AUTO_TEST_SUITE_END()
|