bitcoin/src/test/validation_block_tests.cpp
Wladimir J. van der Laan 312d27b11c
Merge #17477: Remove the mempool's NotifyEntryAdded and NotifyEntryRemoved signals
e57980b473 [mempool] Remove NotifyEntryAdded and NotifyEntryRemoved callbacks (John Newbery)
2dd561f361 [validation] Remove pool member from ConnectTrace (John Newbery)
969b65f3f5 [validation] Remove NotifyEntryRemoved callback from ConnectTrace (John Newbery)
5613f9842b [validation] Remove conflictedTxs from PerBlockConnectTrace (John Newbery)
cdb893443c [validation interface] Remove vtxConflicted from BlockConnected (John Newbery)
1168394d75 [wallet] Notify conflicted transactions in TransactionRemovedFromMempool (John Newbery)

Pull request description:

  These boost signals were added in #9371, before we had a `TransactionRemovedFromMempool` method in the validation interface. The `NotifyEntryAdded` callback was used by validation to build a vector of conflicted transactions when connecting a block, which the wallet was notified of in the `BlockConnected` CValidationInterface callback.

  Now that we have a `TransactionRemovedFromMempool` callback, we can fire that signal directly from the mempool for conflicted transactions.

  Note that #9371 was implemented to ensure `-walletnotify` events were fired for these conflicted transaction. We inadvertently stopped sending these notifications in #16624 (Sep 2019 commit 7e89994). We should probably fix that, but in a different PR.

ACKs for top commit:
  jonatack:
    Re-ACK e57980b
  ryanofsky:
    Code review ACK e57980b473, no code changes since previous review, but helpful new code comments have been added and the PR description is now more clear about where the old code came from

Tree-SHA512: 3bdbaf1ef2731e788462d4756e69c42a1efdcf168691ce1bbfdaa4b7b55ac3c5b1fd4ab7b90bcdec653703600501b4224d252cfc086aef28f9ce0da3b0563a69
2020-03-19 17:26:51 +01:00

343 lines
13 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};
namespace validation_block_tests {
struct MinerTestingSetup : public RegTestingSetup {
std::shared_ptr<CBlock> Block(const uint256& prev_hash);
std::shared_ptr<const CBlock> GoodBlock(const uint256& prev_hash);
std::shared_ptr<const CBlock> BadBlock(const uint256& prev_hash);
std::shared_ptr<CBlock> FinalizeBlock(std::shared_ptr<CBlock> pblock);
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);
};
} // namespace validation_block_tests
BOOST_FIXTURE_TEST_SUITE(validation_block_tests, MinerTestingSetup)
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) 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> MinerTestingSetup::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(*m_node.mempool, 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> MinerTestingSetup::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> MinerTestingSetup::GoodBlock(const uint256& prev_hash)
{
return FinalizeBlock(Block(prev_hash));
}
// construct an invalid block (but with a valid header)
std::shared_ptr<const CBlock> MinerTestingSetup::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 MinerTestingSetup::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) {
UninterruptibleSleep(std::chrono::milliseconds{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()