aboutsummaryrefslogtreecommitdiff
path: root/src/test/validation_block_tests.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/test/validation_block_tests.cpp')
-rw-r--r--src/test/validation_block_tests.cpp152
1 files changed, 149 insertions, 3 deletions
diff --git a/src/test/validation_block_tests.cpp b/src/test/validation_block_tests.cpp
index 6a9813442b..b3368d44b6 100644
--- a/src/test/validation_block_tests.cpp
+++ b/src/test/validation_block_tests.cpp
@@ -10,6 +10,7 @@
#include <miner.h>
#include <pow.h>
#include <random.h>
+#include <script/standard.h>
#include <test/setup_common.h>
#include <util/time.h>
#include <validation.h>
@@ -21,6 +22,8 @@ struct RegtestingSetup : public TestingSetup {
RegtestingSetup() : TestingSetup(CBaseChainParams::REGTEST) {}
};
+static const std::vector<unsigned char> V_OP_TRUE{OP_TRUE};
+
BOOST_FIXTURE_TEST_SUITE(validation_block_tests, RegtestingSetup)
struct TestSubscriber : public CValidationInterface {
@@ -62,8 +65,21 @@ std::shared_ptr<CBlock> Block(const uint256& prev_hash)
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(1);
+ 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));
@@ -72,6 +88,9 @@ std::shared_ptr<CBlock> Block(const uint256& prev_hash)
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())) {
@@ -82,13 +101,13 @@ std::shared_ptr<CBlock> FinalizeBlock(std::shared_ptr<CBlock> pblock)
}
// construct a valid block
-const std::shared_ptr<const CBlock> GoodBlock(const uint256& prev_hash)
+std::shared_ptr<const CBlock> GoodBlock(const uint256& prev_hash)
{
return FinalizeBlock(Block(prev_hash));
}
// construct an invalid block (but with a valid header)
-const std::shared_ptr<const CBlock> BadBlock(const uint256& prev_hash)
+std::shared_ptr<const CBlock> BadBlock(const uint256& prev_hash)
{
auto pblock = Block(prev_hash);
@@ -188,4 +207,131 @@ BOOST_AUTO_TEST_CASE(processnewblock_signals_ordering)
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);
+ CValidationState state;
+ std::list<CTransactionRef> plTxnReplaced;
+ for (const auto& tx : txs) {
+ BOOST_REQUIRE(AcceptToMemoryPool(
+ ::mempool,
+ state,
+ tx,
+ /* pfMissingInputs */ &ignored,
+ &plTxnReplaced,
+ /* bypass_limits */ false,
+ /* nAbsurdFee */ 0));
+ }
+ }
+
+ // Check that all txs are in the pool
+ {
+ LOCK(::mempool.cs);
+ BOOST_CHECK_EQUAL(::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(::mempool.cs);
+ if (::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(::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()