Remove mempool transactions which commit to an unmineable branch ID

This commit is contained in:
Jack Grigg 2018-02-09 23:16:55 +00:00
parent 233c9eb635
commit 34a64fe0a2
No known key found for this signature in database
GPG Key ID: 665DBCD284F7DAFF
10 changed files with 233 additions and 8 deletions

View File

@ -29,6 +29,7 @@ testScripts=(
'mempool_spendcoinbase.py'
'mempool_reorg.py'
'mempool_tx_input_limit.py'
'mempool_nu_activation.py'
'httpbasics.py'
'zapwallettxes.py'
'proxy_test.py'

View File

@ -0,0 +1,119 @@
#!/usr/bin/env python2
# Copyright (c) 2018 The Zcash developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, initialize_chain_clean, \
start_node, connect_nodes, wait_and_assert_operationid_status
from decimal import Decimal
# Test mempool behaviour around network upgrade activation
class MempoolUpgradeActivationTest(BitcoinTestFramework):
alert_filename = None # Set by setup_network
def setup_network(self):
args = ["-checkmempool", "-debug=mempool", "-blockmaxsize=4000", "-nuparams=5ba81b19:200"]
self.nodes = []
self.nodes.append(start_node(0, self.options.tmpdir, args))
self.nodes.append(start_node(1, self.options.tmpdir, args))
connect_nodes(self.nodes[1], 0)
self.is_network_split = False
self.sync_all
def setup_chain(self):
print "Initializing test directory "+self.options.tmpdir
initialize_chain_clean(self.options.tmpdir, 2)
def run_test(self):
self.nodes[1].generate(100)
self.sync_all()
# Mine 97 blocks. After this, nodes[1] blocks
# 1 to 97 are spend-able.
self.nodes[0].generate(97)
self.sync_all()
# Shield some ZEC
node1_taddr = self.nodes[1].getnewaddress()
node0_zaddr = self.nodes[0].z_getnewaddress()
recipients = [{'address': node0_zaddr, 'amount': Decimal('10')}]
myopid = self.nodes[1].z_sendmany(node1_taddr, recipients, 1, Decimal('0'))
print wait_and_assert_operationid_status(self.nodes[1], myopid)
self.sync_all()
# Mine block 198. After this, the mempool expects
# block 199, which is the last Sprout block.
self.nodes[0].generate(1)
self.sync_all()
# Mempool should be empty.
assert_equal(set(self.nodes[0].getrawmempool()), set())
# Check node 0 shielded balance
assert_equal(self.nodes[0].z_getbalance(node0_zaddr), Decimal('10'))
# Fill the mempool with twice as many transactions as can fit into blocks
node0_taddr = self.nodes[0].getnewaddress()
sprout_txids = []
while self.nodes[1].getmempoolinfo()['bytes'] < 2 * 4000:
sprout_txids.append(self.nodes[1].sendtoaddress(node0_taddr, Decimal('0.001')))
self.sync_all()
# Spends should be in the mempool
sprout_mempool = set(self.nodes[0].getrawmempool())
assert_equal(sprout_mempool, set(sprout_txids))
# Mine block 199. After this, the mempool expects
# block 200, which is the first Overwinter block.
self.nodes[0].generate(1)
self.sync_all()
# mempool should be empty.
assert_equal(set(self.nodes[0].getrawmempool()), set())
# Block 199 should contain a subset of the original mempool
# (with all other transactions having been dropped)
block_txids = self.nodes[0].getblock(self.nodes[0].getbestblockhash())['tx']
assert(len(block_txids) < len(sprout_txids))
for txid in block_txids[1:]: # Exclude coinbase
assert(txid in sprout_txids)
# Create some transparent Overwinter transactions
overwinter_txids = [self.nodes[1].sendtoaddress(node0_taddr, Decimal('0.001')) for i in range(10)]
self.sync_all()
# Create a shielded Overwinter transaction
recipients = [{'address': node0_taddr, 'amount': Decimal('10')}]
myopid = self.nodes[0].z_sendmany(node0_zaddr, recipients, 1, Decimal('0'))
shielded = wait_and_assert_operationid_status(self.nodes[0], myopid)
assert(shielded != None)
overwinter_txids.append(shielded)
self.sync_all()
# Spends should be in the mempool
assert_equal(set(self.nodes[0].getrawmempool()), set(overwinter_txids))
# Node 0 note should be unspendable
assert_equal(self.nodes[0].z_getbalance(node0_zaddr), Decimal('0'))
# Invalidate block 199.
self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash())
# BUG: Ideally, the mempool should now only contain the transactions
# that were in block 199, the Overwinter transactions having been dropped.
# However, because chainActive is not updated until after the transactions
# in the disconnected block have been re-added to the mempool, the height
# seen by AcceptToMemoryPool is one greater than it should be. This causes
# the block 199 transactions to be validated against the Overwinter rules,
# and rejected because they (obviously) fail.
#assert_equal(set(self.nodes[0].getrawmempool()), set(block_txids[1:]))
assert_equal(set(self.nodes[0].getrawmempool()), set())
# Node 0 note should be spendable again
assert_equal(self.nodes[0].z_getbalance(node0_zaddr), Decimal('10'))
if __name__ == '__main__':
MempoolUpgradeActivationTest().main()

View File

@ -26,6 +26,9 @@ struct NUInfo {
extern const struct NUInfo NetworkUpgradeInfo[];
// Consensus branch id to identify pre-overwinter (Sprout) consensus rules.
static const uint32_t SPROUT_BRANCH_ID = NetworkUpgradeInfo[Consensus::BASE_SPROUT].nBranchId;
/**
* Checks the state of a given network upgrade based on block height.
* Caller must check that the height is >= 0 (and handle unknown heights).

View File

@ -1,6 +1,7 @@
#include <gtest/gtest.h>
#include <gtest/gtest-spi.h>
#include "consensus/upgrades.h"
#include "consensus/validation.h"
#include "core_io.h"
#include "main.h"
@ -83,7 +84,7 @@ TEST(Mempool, PriorityStatsDoNotCrash) {
unsigned int nHeight = 92045;
double dPriority = view.GetPriority(tx, nHeight);
CTxMemPoolEntry entry(tx, nFees, nTime, dPriority, nHeight, true, false);
CTxMemPoolEntry entry(tx, nFees, nTime, dPriority, nHeight, true, false, SPROUT_BRANCH_ID);
// Check it does not crash (ie. the death test fails)
EXPECT_NONFATAL_FAILURE(EXPECT_DEATH(testPool.addUnchecked(tx.GetHash(), entry), ""), "");

View File

@ -1199,7 +1199,12 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
}
}
CTxMemPoolEntry entry(tx, nFees, GetTime(), dPriority, chainActive.Height(), mempool.HasNoInputsOf(tx), fSpendsCoinbase);
// Grab the branch ID we expect this transaction to commit to. We don't
// yet know if it does, but if the entry gets added to the mempool, then
// it has passed ContextualCheckInputs and therefore this is correct.
auto consensusBranchId = CurrentEpochBranchId(chainActive.Height() + 1, Params().GetConsensus());
CTxMemPoolEntry entry(tx, nFees, GetTime(), dPriority, chainActive.Height(), mempool.HasNoInputsOf(tx), fSpendsCoinbase, consensusBranchId);
unsigned int nSize = entry.GetTxSize();
// Accept a tx if it contains joinsplits and has at least the default fee specified by z_sendmany.
@ -2430,7 +2435,10 @@ void static UpdateTip(CBlockIndex *pindexNew) {
}
}
/** Disconnect chainActive's tip. You probably want to call mempool.removeForReorg after this, with cs_main held. */
/**
* Disconnect chainActive's tip. You probably want to call mempool.removeForReorg and
* mempool.removeWithoutBranchId after this, with cs_main held.
*/
bool static DisconnectTip(CValidationState &state, bool fBare = false) {
CBlockIndex *pindexDelete = chainActive.Tip();
assert(pindexDelete);
@ -2493,6 +2501,7 @@ static int64_t nTimePostConnect = 0;
/**
* Connect a new block to chainActive. pblock is either NULL or a pointer to a CBlock
* corresponding to pindexNew, to bypass loading it again from disk.
* You probably want to call mempool.removeWithoutBranchId after this, with cs_main held.
*/
bool static ConnectTip(CValidationState &state, CBlockIndex *pindexNew, CBlock *pblock) {
assert(pindexNew->pprev == chainActive.Tip());
@ -2691,6 +2700,8 @@ static bool ActivateBestChainStep(CValidationState &state, CBlockIndex *pindexMo
if (fBlocksDisconnected) {
mempool.removeForReorg(pcoinsTip, chainActive.Tip()->nHeight + 1, STANDARD_LOCKTIME_VERIFY_FLAGS);
}
mempool.removeWithoutBranchId(
CurrentEpochBranchId(chainActive.Tip()->nHeight + 1, Params().GetConsensus()));
mempool.check(pcoinsTip);
// Callbacks/notifications for a new best chain.
@ -2778,6 +2789,8 @@ bool InvalidateBlock(CValidationState& state, CBlockIndex *pindex) {
// unconditionally valid already, so force disconnect away from it.
if (!DisconnectTip(state)) {
mempool.removeForReorg(pcoinsTip, chainActive.Tip()->nHeight + 1, STANDARD_LOCKTIME_VERIFY_FLAGS);
mempool.removeWithoutBranchId(
CurrentEpochBranchId(chainActive.Tip()->nHeight + 1, Params().GetConsensus()));
return false;
}
}
@ -2794,6 +2807,8 @@ bool InvalidateBlock(CValidationState& state, CBlockIndex *pindex) {
InvalidChainFound(pindex);
mempool.removeForReorg(pcoinsTip, chainActive.Tip()->nHeight + 1, STANDARD_LOCKTIME_VERIFY_FLAGS);
mempool.removeWithoutBranchId(
CurrentEpochBranchId(chainActive.Tip()->nHeight + 1, Params().GetConsensus()));
return true;
}

View File

@ -2,6 +2,7 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "consensus/upgrades.h"
#include "main.h"
#include "txmempool.h"
#include "util.h"
@ -156,4 +157,59 @@ BOOST_AUTO_TEST_CASE(MempoolIndexingTest)
BOOST_CHECK(it == pool.mapTx.get<1>().end());
}
BOOST_AUTO_TEST_CASE(RemoveWithoutBranchId) {
CTxMemPool pool(CFeeRate(0));
TestMemPoolEntryHelper entry;
entry.nFee = 10000LL;
entry.hadNoDependencies = true;
// Add some Sprout transactions
for (auto i = 1; i < 11; i++) {
CMutableTransaction tx = CMutableTransaction();
tx.vout.resize(1);
tx.vout[0].scriptPubKey = CScript() << OP_11 << OP_EQUAL;
tx.vout[0].nValue = i * COIN;
pool.addUnchecked(tx.GetHash(), entry.BranchId(NetworkUpgradeInfo[Consensus::BASE_SPROUT].nBranchId).FromTx(tx));
}
BOOST_CHECK_EQUAL(pool.size(), 10);
// Check the pool only contains Sprout transactions
for (CTxMemPool::indexed_transaction_set::const_iterator it = pool.mapTx.begin(); it != pool.mapTx.end(); it++) {
BOOST_CHECK_EQUAL(it->GetValidatedBranchId(), NetworkUpgradeInfo[Consensus::BASE_SPROUT].nBranchId);
}
// Add some dummy transactions
for (auto i = 1; i < 11; i++) {
CMutableTransaction tx = CMutableTransaction();
tx.vout.resize(1);
tx.vout[0].scriptPubKey = CScript() << OP_11 << OP_EQUAL;
tx.vout[0].nValue = i * COIN + 100;
pool.addUnchecked(tx.GetHash(), entry.BranchId(NetworkUpgradeInfo[Consensus::UPGRADE_TESTDUMMY].nBranchId).FromTx(tx));
}
BOOST_CHECK_EQUAL(pool.size(), 20);
// Add some Overwinter transactions
for (auto i = 1; i < 11; i++) {
CMutableTransaction tx = CMutableTransaction();
tx.vout.resize(1);
tx.vout[0].scriptPubKey = CScript() << OP_11 << OP_EQUAL;
tx.vout[0].nValue = i * COIN + 200;
pool.addUnchecked(tx.GetHash(), entry.BranchId(NetworkUpgradeInfo[Consensus::UPGRADE_OVERWINTER].nBranchId).FromTx(tx));
}
BOOST_CHECK_EQUAL(pool.size(), 30);
// Remove transactions that are not for Overwinter
pool.removeWithoutBranchId(NetworkUpgradeInfo[Consensus::UPGRADE_OVERWINTER].nBranchId);
BOOST_CHECK_EQUAL(pool.size(), 10);
// Check the pool only contains Overwinter transactions
for (CTxMemPool::indexed_transaction_set::const_iterator it = pool.mapTx.begin(); it != pool.mapTx.end(); it++) {
BOOST_CHECK_EQUAL(it->GetValidatedBranchId(), NetworkUpgradeInfo[Consensus::UPGRADE_OVERWINTER].nBranchId);
}
// Roll back to Sprout
pool.removeWithoutBranchId(NetworkUpgradeInfo[Consensus::BASE_SPROUT].nBranchId);
BOOST_CHECK_EQUAL(pool.size(), 0);
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -106,7 +106,8 @@ TestingSetup::~TestingSetup()
CTxMemPoolEntry TestMemPoolEntryHelper::FromTx(CMutableTransaction &tx, CTxMemPool *pool) {
return CTxMemPoolEntry(tx, nFee, nTime, dPriority, nHeight,
pool ? pool->HasNoInputsOf(tx) : hadNoDependencies, spendsCoinbase);
pool ? pool->HasNoInputsOf(tx) : hadNoDependencies,
spendsCoinbase, nBranchId);
}
void Shutdown(void* parg)

View File

@ -1,6 +1,7 @@
#ifndef BITCOIN_TEST_TEST_BITCOIN_H
#define BITCOIN_TEST_TEST_BITCOIN_H
#include "consensus/upgrades.h"
#include "pubkey.h"
#include "txdb.h"
@ -48,10 +49,12 @@ struct TestMemPoolEntryHelper
unsigned int nHeight;
bool hadNoDependencies;
bool spendsCoinbase;
uint32_t nBranchId;
TestMemPoolEntryHelper() :
nFee(0), nTime(0), dPriority(0.0), nHeight(1),
hadNoDependencies(false), spendsCoinbase(false) { }
hadNoDependencies(false), spendsCoinbase(false),
nBranchId(SPROUT_BRANCH_ID) { }
CTxMemPoolEntry FromTx(CMutableTransaction &tx, CTxMemPool *pool = NULL);
@ -62,5 +65,6 @@ struct TestMemPoolEntryHelper
TestMemPoolEntryHelper &Height(unsigned int _height) { nHeight = _height; return *this; }
TestMemPoolEntryHelper &HadNoDependencies(bool _hnd) { hadNoDependencies = _hnd; return *this; }
TestMemPoolEntryHelper &SpendsCoinbase(bool _flag) { spendsCoinbase = _flag; return *this; }
TestMemPoolEntryHelper &BranchId(uint32_t _branchId) { nBranchId = _branchId; return *this; }
};
#endif

View File

@ -28,10 +28,10 @@ CTxMemPoolEntry::CTxMemPoolEntry():
CTxMemPoolEntry::CTxMemPoolEntry(const CTransaction& _tx, const CAmount& _nFee,
int64_t _nTime, double _dPriority,
unsigned int _nHeight, bool poolHasNoInputsOf,
bool _spendsCoinbase):
bool _spendsCoinbase, uint32_t _nBranchId):
tx(_tx), nFee(_nFee), nTime(_nTime), dPriority(_dPriority), nHeight(_nHeight),
hadNoDependencies(poolHasNoInputsOf),
spendsCoinbase(_spendsCoinbase)
spendsCoinbase(_spendsCoinbase), nBranchId(_nBranchId)
{
nTxSize = ::GetSerializeSize(tx, SER_NETWORK, PROTOCOL_VERSION);
nModSize = tx.CalculateModifiedSize(nTxSize);
@ -283,6 +283,28 @@ void CTxMemPool::removeForBlock(const std::vector<CTransaction>& vtx, unsigned i
minerPolicyEstimator->processBlock(nBlockHeight, entries, fCurrentEstimate);
}
/**
* Called whenever the tip changes. Removes transactions which don't commit to
* the given branch ID from the mempool.
*/
void CTxMemPool::removeWithoutBranchId(uint32_t nMemPoolBranchId)
{
LOCK(cs);
std::list<CTransaction> transactionsToRemove;
for (indexed_transaction_set::const_iterator it = mapTx.begin(); it != mapTx.end(); it++) {
const CTransaction& tx = it->GetTx();
if (it->GetValidatedBranchId() != nMemPoolBranchId) {
transactionsToRemove.push_back(tx);
}
}
for (const CTransaction& tx : transactionsToRemove) {
std::list<CTransaction> removed;
remove(tx, removed, true);
}
}
void CTxMemPool::clear()
{
LOCK(cs);

View File

@ -51,11 +51,12 @@ private:
unsigned int nHeight; //! Chain height when entering the mempool
bool hadNoDependencies; //! Not dependent on any other txs when it entered the mempool
bool spendsCoinbase; //! keep track of transactions that spend a coinbase
uint32_t nBranchId; //! Branch ID this transaction is known to commit to, cached for efficiency
public:
CTxMemPoolEntry(const CTransaction& _tx, const CAmount& _nFee,
int64_t _nTime, double _dPriority, unsigned int _nHeight,
bool poolHasNoInputsOf, bool spendsCoinbase);
bool poolHasNoInputsOf, bool spendsCoinbase, uint32_t nBranchId);
CTxMemPoolEntry();
CTxMemPoolEntry(const CTxMemPoolEntry& other);
@ -70,6 +71,7 @@ public:
size_t DynamicMemoryUsage() const { return nUsageSize; }
bool GetSpendsCoinbase() const { return spendsCoinbase; }
uint32_t GetValidatedBranchId() const { return nBranchId; }
};
// extracts a TxMemPoolEntry's transaction hash
@ -168,6 +170,7 @@ public:
void removeConflicts(const CTransaction &tx, std::list<CTransaction>& removed);
void removeForBlock(const std::vector<CTransaction>& vtx, unsigned int nBlockHeight,
std::list<CTransaction>& conflicts, bool fCurrentEstimate = true);
void removeWithoutBranchId(uint32_t nMemPoolBranchId);
void clear();
void queryHashes(std::vector<uint256>& vtxid);
void pruneSpent(const uint256& hash, CCoins &coins);