diff --git a/src/chainparams.cpp b/src/chainparams.cpp index c31665b595a5..55e20adcdef7 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -241,6 +241,9 @@ class CMainParams : public CChainParams vFixedSeeds = std::vector(pnSeed6_main, pnSeed6_main + ARRAYLEN(pnSeed6_main)); + // Reject non-standard transactions by default + fRequireStandard = true; + // Sapling bech32HRPs[SAPLING_PAYMENT_ADDRESS] = "ps"; bech32HRPs[SAPLING_FULL_VIEWING_KEY] = "pviews"; @@ -364,6 +367,8 @@ class CTestNetParams : public CChainParams vFixedSeeds = std::vector(pnSeed6_test, pnSeed6_test + ARRAYLEN(pnSeed6_test)); + fRequireStandard = false; + // Sapling bech32HRPs[SAPLING_PAYMENT_ADDRESS] = "ptestsapling"; bech32HRPs[SAPLING_FULL_VIEWING_KEY] = "pviewtestsapling"; @@ -486,6 +491,9 @@ class CRegTestParams : public CChainParams // Testnet pivx BIP44 coin type is '1' (All coin's testnet default) base58Prefixes[EXT_COIN_TYPE] = {0x80, 0x00, 0x00, 0x01}; + // Reject non-standard transactions by default + fRequireStandard = true; + // Sapling bech32HRPs[SAPLING_PAYMENT_ADDRESS] = "ptestsapling"; bech32HRPs[SAPLING_FULL_VIEWING_KEY] = "pviewtestsapling"; diff --git a/src/chainparams.h b/src/chainparams.h index 5b08d6c30d46..88c052f2edd9 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -67,7 +67,10 @@ class CChainParams int GetDefaultPort() const { return nDefaultPort; } const CBlock& GenesisBlock() const { return genesis; } - + /** Policy: Filter transactions that do not match well-defined patterns */ + bool RequireStandard() const { return fRequireStandard; } + /** If this chain is exclusively used for testing */ + bool IsTestChain() const { return IsTestnet() || IsRegTestNet(); } /** Make miner wait to have peers to avoid wasting work */ bool MiningRequiresPeers() const { return !IsRegTestNet(); } /** Headers first syncing is disabled */ @@ -99,6 +102,7 @@ class CChainParams std::vector base58Prefixes[MAX_BASE58_TYPES]; std::string bech32HRPs[MAX_BECH32_TYPES]; std::vector vFixedSeeds; + bool fRequireStandard; }; /** diff --git a/src/init.cpp b/src/init.cpp index 1301b25ea140..a5bdf7b672b6 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -558,6 +558,12 @@ std::string HelpMessage(HelpMessageMode mode) strUsage += HelpMessageOpt("-budgetvotemode=", _("Change automatic finalized budget voting behavior. mode=auto: Vote for only exact finalized budget match to my generated budget. (string, default: auto)")); strUsage += HelpMessageGroup(_("Node relay options:")); + if (showDebug) { + strUsage += HelpMessageOpt("-acceptnonstdtxn", + strprintf("Relay and mine \"non-standard\" transactions (%sdefault: %u)", + "testnet/regtest only; ", + !CreateChainParams(CBaseChainParams::TESTNET)->RequireStandard())); + } strUsage += HelpMessageOpt("-datacarrier", strprintf(_("Relay and mine data carrier transactions (default: %u)"), DEFAULT_ACCEPT_DATACARRIER)); strUsage += HelpMessageOpt("-datacarriersize", strprintf(_("Maximum size of data in data carrier transactions we relay and mine (default: %u)"), MAX_OP_RETURN_RELAY)); if (showDebug) { @@ -1135,6 +1141,11 @@ bool AppInitParameterInteraction() return UIError(AmountErrMsg("minrelaytxfee", gArgs.GetArg("-minrelaytxfee", ""))); } + const CChainParams& chainparams = Params(); + fRequireStandard = !gArgs.GetBoolArg("-acceptnonstdtxn", !chainparams.RequireStandard()); + if (!chainparams.IsTestChain() && !fRequireStandard) + return UIError(strprintf("acceptnonstdtxn is not currently supported for %s chain", chainparams.NetworkIDString())); + #ifdef ENABLE_WALLET strWalletFile = gArgs.GetArg("-wallet", DEFAULT_WALLET_DAT); if (!CWallet::ParameterInteraction()) diff --git a/src/validation.cpp b/src/validation.cpp index 19c3285bbb57..a27f6862f389 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -98,6 +98,7 @@ int nScriptCheckThreads = 0; std::atomic fImporting{false}; std::atomic fReindex{false}; bool fTxIndex = true; +bool fRequireStandard = true; bool fCheckBlockIndex = false; bool fVerifyingBlocks = false; size_t nCoinCacheUsage = 5000 * 300; @@ -440,7 +441,7 @@ bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState &state, const C // Rather not work on nonstandard transactions std::string reason; - if (!IsStandardTx(_tx, nextBlockHeight, reason)) + if (fRequireStandard && !IsStandardTx(_tx, nextBlockHeight, reason)) return state.DoS(0, false, REJECT_NONSTANDARD, reason); // is it already in the memory pool? const uint256& hash = tx.GetHash(); @@ -518,7 +519,7 @@ bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState &state, const C view.SetBackend(dummy); // Check for non-standard pay-to-script-hash in inputs - if (!Params().IsRegTestNet() && !AreInputsStandard(tx, view)) + if (fRequireStandard && !AreInputsStandard(tx, view)) return state.Invalid(false, REJECT_NONSTANDARD, "bad-txns-nonstandard-inputs"); // Check that the transaction doesn't have an excessive number of @@ -2123,7 +2124,7 @@ bool static ConnectTip(CValidationState& state, CBlockIndex* pindexNew, const st if (!rv) { if (state.IsInvalid()) InvalidBlockFound(pindexNew, state); - return error("ConnectTip() : ConnectBlock %s failed", pindexNew->GetBlockHash().ToString()); + return error("%s: ConnectBlock %s failed, %s", __func__, pindexNew->GetBlockHash().ToString(), FormatStateMessage(state)); } nTime3 = GetTimeMicros(); nTimeConnectTotal += nTime3 - nTime2; diff --git a/src/validation.h b/src/validation.h index cf31cefd805d..d9fcf0a17d7f 100644 --- a/src/validation.h +++ b/src/validation.h @@ -142,6 +142,7 @@ extern std::atomic fImporting; extern std::atomic fReindex; extern int nScriptCheckThreads; extern bool fTxIndex; +extern bool fRequireStandard; extern bool fCheckBlockIndex; extern size_t nCoinCacheUsage; extern CFeeRate minRelayTxFee; diff --git a/test/functional/p2p_invalid_block.py b/test/functional/p2p_invalid_block.py index 0fa2368fd28f..29a31dfe54f7 100755 --- a/test/functional/p2p_invalid_block.py +++ b/test/functional/p2p_invalid_block.py @@ -1,86 +1,72 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2016 The Bitcoin Core developers +# Copyright (c) 2015-2017 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test node responses to invalid blocks. -from test_framework.test_framework import ComparisonTestFramework -from test_framework.util import * -from test_framework.comptool import TestManager, TestInstance, RejectResult -from test_framework.blocktools import * -import copy -import time - - -''' In this test we connect to one node over p2p, and test block requests: 1) Valid blocks should be requested and become chain tip. 2) Invalid block with duplicated transaction should be re-requested. 3) Invalid block with bad coinbase value should be rejected and not re-requested. -''' +""" +import copy -# Use the ComparisonTestFramework with 1 node: only use --testbinary. -class InvalidBlockRequestTest(ComparisonTestFramework): +from test_framework.blocktools import create_block, create_coinbase, create_transaction +from test_framework.messages import COIN +from test_framework.mininode import network_thread_start, P2PDataStore +from test_framework.test_framework import PivxTestFramework +from test_framework.util import assert_equal - ''' - Can either run this test as 1 node with expected answers, or two and compare them. - Change the "outcome" variable from each TestInstance object to only do the comparison. - ''' - def __init__(self): - super().__init__() +class InvalidBlockRequestTest(PivxTestFramework): + def set_test_params(self): self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [["-whitelist=127.0.0.1"]] def run_test(self): - test = TestManager(self, self.options.tmpdir) - test.add_all_connections(self.nodes) - self.tip = None - self.block_time = None - NetworkThread().start() # Start up network handling in another thread - test.run() - - def get_tests(self): - if self.tip is None: - self.tip = int("0x" + self.nodes[0].getbestblockhash(), 0) - self.block_time = int(time.time())+1 - - ''' - Create a new block with an anyone-can-spend coinbase - ''' + # Add p2p connection to node0 + node = self.nodes[0] # convenience reference to the node + node.add_p2p_connection(P2PDataStore()) + network_thread_start() + node.p2p.wait_for_verack() + + best_block = node.getblock(node.getbestblockhash()) + tip = int(node.getbestblockhash(), 16) + height = best_block["height"] + 1 + block_time = best_block["time"] + 1 + + self.log.info("Create a new block with an anyone-can-spend coinbase") + height = 1 - block = create_block(self.tip, create_coinbase(height), self.block_time) - self.block_time += 1 + block = create_block(tip, create_coinbase(height), block_time) block.solve() # Save the coinbase for later - self.block1 = block - self.tip = block.sha256 - height += 1 - yield TestInstance([[block, True]]) - - ''' - Now we need that block to mature so we can spend the coinbase. - ''' - test = TestInstance(sync_every_block=False) - for i in range(100): - block = create_block(self.tip, create_coinbase(height), self.block_time) - block.solve() - self.tip = block.sha256 - self.block_time += 1 - test.blocks_and_transactions.append([block, True]) - height += 1 - yield test - - ''' - Now we use merkle-root malleability to generate an invalid block with - same blockheader. - Manufacture a block with 3 transactions (coinbase, spend of prior - coinbase, spend of that spend). Duplicate the 3rd transaction to - leave merkle root and blockheader unchanged but invalidate the block. - ''' - block2 = create_block(self.tip, create_coinbase(height), self.block_time) - self.block_time += 1 + block1 = block + tip = block.sha256 + node.p2p.send_blocks_and_test([block1], node, success=True) + + self.log.info("Mature the block.") + node.generate(100) + + best_block = node.getblock(node.getbestblockhash()) + tip = int(node.getbestblockhash(), 16) + height = best_block["height"] + 1 + block_time = best_block["time"] + 1 + + # Use merkle-root malleability to generate an invalid block with + # same blockheader (CVE-2012-2459). + # Manufacture a block with 3 transactions (coinbase, spend of prior + # coinbase, spend of that spend). Duplicate the 3rd transaction to + # leave merkle root and blockheader unchanged but invalidate the block. + # For more information on merkle-root malleability see src/consensus/merkle.cpp. + self.log.info("Test merkle root malleability.") + + block2 = create_block(tip, create_coinbase(height), block_time) + block_time += 1 # b'0x51' is OP_TRUE - tx1 = create_transaction(self.block1.vtx[0], 0, b'\x51', 50 * COIN) + tx1 = create_transaction(block1.vtx[0], 0, b'\x51', 50 * COIN) tx2 = create_transaction(tx1, 0, b'\x51', 50 * COIN) block2.vtx.extend([tx1, tx2]) @@ -94,26 +80,70 @@ def get_tests(self): block2.vtx.append(tx2) assert_equal(block2.hashMerkleRoot, block2.calc_merkle_root()) assert_equal(orig_hash, block2.rehash()) - assert(block2_orig.vtx != block2.vtx) + assert block2_orig.vtx != block2.vtx - self.tip = block2.sha256 - yield TestInstance([[block2, RejectResult(16, b'bad-txns-duplicate')], [block2_orig, True]]) - height += 1 + node.p2p.send_blocks_and_test([block2], node, success=False, reject_reason='bad-txns-duplicate') + + # Check transactions for duplicate inputs (CVE-2018-17144) + self.log.info("Test duplicate input block.") + + block2_dup = copy.deepcopy(block2_orig) + block2_dup.vtx[2].vin.append(block2_dup.vtx[2].vin[0]) + block2_dup.vtx[2].rehash() + block2_dup.hashMerkleRoot = block2_dup.calc_merkle_root() + block2_dup.rehash() + block2_dup.solve() + node.p2p.send_blocks_and_test([block2_dup], node, success=False, reject_reason='bad-txns-inputs-duplicate') - ''' - Make sure that a totally screwed up block is not valid. - ''' - block3 = create_block(self.tip, create_coinbase(height), self.block_time) - self.block_time += 1 - block3.vtx[0].vout[0].nValue = 100 * COIN # Too high! - block3.vtx[0].sha256=None + self.log.info("Test very broken block.") + + block3 = create_block(tip, create_coinbase(height), block_time) + block_time += 1 + block3.vtx[0].vout[0].nValue = 251 * COIN # Too high! + block3.vtx[0].sha256 = None block3.vtx[0].calc_sha256() block3.hashMerkleRoot = block3.calc_merkle_root() block3.rehash() block3.solve() - yield TestInstance([[block3, RejectResult(16, b'bad-cb-amount')]]) + node.p2p.send_blocks_and_test([block3], node, success=False, reject_reason='bad-cb-amount') + + + # Complete testing of CVE-2012-2459 by sending the original block. + # It should be accepted even though it has the same hash as the mutated one. + self.log.info("Test accepting original block after rejecting its mutated version.") + node.p2p.send_blocks_and_test([block2_orig], node, success=True, timeout=5) + + # Update tip info + height += 1 + block_time += 1 + tip = int(block2_orig.hash, 16) + + # Complete testing of CVE-2018-17144, by checking for the inflation bug. + # Create a block that spends the output of a tx in a previous block. + block4 = create_block(tip, create_coinbase(height), block_time) + tx3 = create_transaction(tx2, 0, b'\x51', 50 * COIN) + + # Duplicates input + tx3.vin.append(tx3.vin[0]) + tx3.rehash() + block4.vtx.append(tx3) + block4.hashMerkleRoot = block4.calc_merkle_root() + block4.rehash() + block4.solve() + self.log.info("Test inflation by duplicating input") + node.p2p.send_blocks_and_test([block4], node, success=False, reject_reason='bad-txns-inputs-duplicate') + + self.log.info("Test output value > input value out of range") + # Can be removed when 'feature_block.py' is added to the suite. + tx4 = create_transaction(tx2, 0, b'\x51', 260 * COIN) + block4 = create_block(tip, create_coinbase(height), block_time) + block4.vtx.extend([tx4]) + block4.hashMerkleRoot = block4.calc_merkle_root() + block4.rehash() + block4.solve() + node.p2p.send_blocks_and_test([block4], node, success=False, reject_reason='bad-txns-in-belowout') if __name__ == '__main__': InvalidBlockRequestTest().main() diff --git a/test/functional/p2p_invalid_tx.py b/test/functional/p2p_invalid_tx.py index acf2de033473..3fcdc9473ffd 100755 --- a/test/functional/p2p_invalid_tx.py +++ b/test/functional/p2p_invalid_tx.py @@ -1,74 +1,144 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2016 The Bitcoin Core developers +# Copyright (c) 2015-2017 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying -# file COPYING or http://www.opensource.org/licenses/mit-license.php. +# file COPYING or https://www.opensource.org/licenses/mit-license.php. +"""Test node responses to invalid transactions. -from test_framework.test_framework import ComparisonTestFramework -from test_framework.comptool import TestManager, TestInstance, RejectResult -from test_framework.blocktools import * -import time +In this test we connect to one node over p2p, and test tx requests.""" +from test_framework.blocktools import create_block, create_coinbase, create_transaction +from test_framework.messages import ( + COIN, + COutPoint, + CTransaction, + CTxIn, + CTxOut, +) +from test_framework.mininode import network_thread_start, P2PDataStore, network_thread_join +from test_framework.test_framework import PivxTestFramework +from test_framework.util import ( + assert_equal, + wait_until, +) -''' -In this test we connect to one node over p2p, and test tx requests. -''' +class InvalidTxRequestTest(PivxTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [["-acceptnonstdtxn=1"]] -# Use the ComparisonTestFramework with 1 node: only use --testbinary. -class InvalidTxRequestTest(ComparisonTestFramework): + def bootstrap_p2p(self, *, num_connections=1): + """Add a P2P connection to the node. - ''' - Can either run this test as 1 node with expected answers, or two and compare them. - Change the "outcome" variable from each TestInstance object to only do the comparison. - ''' - def __init__(self): - super().__init__() - self.num_nodes = 1 + Helper to connect and wait for version handshake.""" + for _ in range(num_connections): + self.nodes[0].add_p2p_connection(P2PDataStore()) + network_thread_start() + self.nodes[0].p2p.wait_for_verack() + + def reconnect_p2p(self, **kwargs): + """Tear down and bootstrap the P2P connection to the node. + + The node gets disconnected several times in this test. This helper + method reconnects the p2p and restarts the network thread.""" + self.nodes[0].disconnect_p2ps() + network_thread_join() + self.bootstrap_p2p(**kwargs) def run_test(self): - test = TestManager(self, self.options.tmpdir) - test.add_all_connections(self.nodes) - self.tip = None - self.block_time = None - NetworkThread().start() # Start up network handling in another thread - test.run() - - def get_tests(self): - if self.tip is None: - self.tip = int("0x" + self.nodes[0].getbestblockhash(), 0) - self.block_time = int(time.time())+1 - - ''' - Create a new block with an anyone-can-spend coinbase - ''' + node = self.nodes[0] # convenience reference to the node + + self.bootstrap_p2p() # Add one p2p connection to the node + + best_block = self.nodes[0].getbestblockhash() + tip = int(best_block, 16) + best_block_time = self.nodes[0].getblock(best_block)['time'] + block_time = best_block_time + 1 + + self.log.info("Create a new block with an anyone-can-spend coinbase.") height = 1 - block = create_block(self.tip, create_coinbase(height), self.block_time) - self.block_time += 1 + block = create_block(tip, create_coinbase(height), block_time) block.solve() # Save the coinbase for later - self.block1 = block - self.tip = block.sha256 - height += 1 - yield TestInstance([[block, True]]) - - ''' - Now we need that block to mature so we can spend the coinbase. - ''' - test = TestInstance(sync_every_block=False) - for i in range(100): - block = create_block(self.tip, create_coinbase(height), self.block_time) - block.solve() - self.tip = block.sha256 - self.block_time += 1 - test.blocks_and_transactions.append([block, True]) - height += 1 - yield test + block1 = block + tip = block.sha256 + node.p2p.send_blocks_and_test([block], node, success=True) + + self.log.info("Mature the block.") + self.nodes[0].generate(100) # b'\x64' is OP_NOTIF # Transaction will be rejected with code 16 (REJECT_INVALID) - tx1 = create_transaction(self.block1.vtx[0], 0, b'\x64', 50 * COIN - 12000) - yield TestInstance([[tx1, RejectResult(16, b'mandatory-script-verify-flag-failed')]]) + # and we get disconnected immediately + self.log.info('Test a transaction that is rejected') + tx1 = create_transaction(block1.vtx[0], 0, b'\x64', 50 * COIN - 12000) + node.p2p.send_txs_and_test([tx1], node, success=False, expect_disconnect=True) + + # Make two p2p connections to provide the node with orphans + # * p2ps[0] will send valid orphan txs (one with low fee) + # * p2ps[1] will send an invalid orphan tx (and is later disconnected for that) + self.reconnect_p2p(num_connections=2) + + self.log.info('Test orphan transaction handling ... ') + # Create a root transaction that we withhold until all dependend transactions + # are sent out and in the orphan cache + tx_withhold = CTransaction() + tx_withhold.vin.append(CTxIn(outpoint=COutPoint(block1.vtx[0].sha256, 0))) + tx_withhold.vout.append(CTxOut(nValue=50 * COIN - 12000, scriptPubKey=b'\x51')) + tx_withhold.calc_sha256() + + # Our first orphan tx with some outputs to create further orphan txs + tx_orphan_1 = CTransaction() + tx_orphan_1.vin.append(CTxIn(outpoint=COutPoint(tx_withhold.sha256, 0))) + tx_orphan_1.vout = [CTxOut(nValue=10 * COIN, scriptPubKey=b'\x51')] * 3 + tx_orphan_1.calc_sha256() + + # A valid transaction with low fee + tx_orphan_2_no_fee = CTransaction() + tx_orphan_2_no_fee.vin.append(CTxIn(outpoint=COutPoint(tx_orphan_1.sha256, 0))) + tx_orphan_2_no_fee.vout.append(CTxOut(nValue=10 * COIN, scriptPubKey=b'\x51')) + + # A valid transaction with sufficient fee + tx_orphan_2_valid = CTransaction() + tx_orphan_2_valid.vin.append(CTxIn(outpoint=COutPoint(tx_orphan_1.sha256, 1))) + tx_orphan_2_valid.vout.append(CTxOut(nValue=10 * COIN - 12000, scriptPubKey=b'\x51')) + tx_orphan_2_valid.calc_sha256() + + # An invalid transaction with negative fee + tx_orphan_2_invalid = CTransaction() + tx_orphan_2_invalid.vin.append(CTxIn(outpoint=COutPoint(tx_orphan_1.sha256, 2))) + tx_orphan_2_invalid.vout.append(CTxOut(nValue=11 * COIN, scriptPubKey=b'\x51')) + + self.log.info('Send the orphans ... ') + # Send valid orphan txs from p2ps[0] + node.p2p.send_txs_and_test([tx_orphan_1, tx_orphan_2_no_fee, tx_orphan_2_valid], node, success=False) + # Send invalid tx from p2ps[1] + node.p2ps[1].send_txs_and_test([tx_orphan_2_invalid], node, success=False) + + assert_equal(0, node.getmempoolinfo()['size']) # Mempool should be empty + assert_equal(2, len(node.getpeerinfo())) # p2ps[1] is still connected + + self.log.info('Send the withhold tx ... ') + node.p2p.send_txs_and_test([tx_withhold], node, success=True) + + # Transactions that should end up in the mempool + expected_mempool = { + t.hash + for t in [ + tx_withhold, # The transaction that is the root for all orphans + tx_orphan_1, # The orphan transaction that splits the coins + tx_orphan_2_valid, # The valid transaction (with sufficient fee) + ] + } + # Transactions that do not end up in the mempool + # tx_orphan_no_fee, because it has too low fee (p2ps[0] is not disconnected for relaying that tx) + # tx_orphan_invalid, because it has negative fee (p2ps[1] is disconnected for relaying that tx) + + # future to do: p2ps[1] is still connected because we aren't banning the peer, we are just removing + # 'tx_orphan_2_invalid' transaction from the orphans pool. + #wait_until(lambda: 1 == len(node.getpeerinfo()), timeout=12) # p2ps[1] is no longer connected + assert_equal(expected_mempool, set(node.getrawmempool())) - # TODO: test further transactions... if __name__ == '__main__': InvalidTxRequestTest().main() diff --git a/test/functional/test_framework/blocktools.py b/test/functional/test_framework/blocktools.py index 43465e03699e..61980e8c015f 100644 --- a/test/functional/test_framework/blocktools.py +++ b/test/functional/test_framework/blocktools.py @@ -5,7 +5,7 @@ """Utilities for manipulating blocks and transactions.""" from test_framework.mininode import * -from test_framework.script import CScript, OP_TRUE, OP_CHECKSIG +from test_framework.script import CScript, CScriptNum, CScriptOp, OP_TRUE, OP_CHECKSIG, OP_1 # Create a block (with regtest difficulty) @@ -42,19 +42,23 @@ def serialize_script_num(value): r[-1] |= 0x80 return r -def cbase_scriptsig(height): - return ser_string(serialize_script_num(height)) - def cbase_value(height): #return ((50 * COIN) >> int(height/150)) return (250 * COIN) +def script_BIP34_coinbase_height(height): + if height <= 16: + res = CScriptOp.encode_op_n(height) + # Append dummy to increase scriptSig size above 2 (see bad-cb-length consensus rule) + return CScript([res, OP_1]) + return CScript([CScriptNum(height)]) + # Create a coinbase transaction, assuming no miner fees. # If pubkey is passed in, the coinbase output will be a P2PK output; # otherwise an anyone-can-spend output. def create_coinbase(height, pubkey = None): coinbase = CTransaction() - coinbase.vin = [CTxIn(NullOutPoint, cbase_scriptsig(height), 0xffffffff)] + coinbase.vin = [CTxIn(NullOutPoint, script_BIP34_coinbase_height(height), 0xffffffff)] coinbaseoutput = CTxOut() coinbaseoutput.nValue = cbase_value(height) if (pubkey != None): @@ -100,7 +104,7 @@ def get_legacy_sigopcount_tx(tx, fAccurate=True): ### PIVX specific blocktools ### def create_coinbase_pos(height): coinbase = CTransaction() - coinbase.vin = [CTxIn(NullOutPoint, cbase_scriptsig(height), 0xffffffff)] + coinbase.vin = [CTxIn(NullOutPoint, script_BIP34_coinbase_height(height), 0xffffffff)] coinbase.vout = [CTxOut(0, b"")] coinbase.calc_sha256() return coinbase diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index 08d4c3c0173e..0796d09344f8 100644 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -41,6 +41,10 @@ # NODE_GETUTXO = (1 << 1) NODE_BLOOM = (1 << 2) +MSG_TX = 1 +MSG_BLOCK = 2 +MSG_TYPE_MASK = 0xffffffff >> 2 + # Serialization/deserialization tools def sha256(s): return hashlib.new('sha256', s).digest() @@ -209,7 +213,6 @@ def __repr__(self): return "CAddress(nServices=%i ip=%s port=%i)" % (self.nServices, self.ip, self.port) - class CInv(): typemap = { 0: "MSG_ERROR", diff --git a/test/functional/test_framework/mininode.py b/test/functional/test_framework/mininode.py index 6bb1566a369f..0659811aba72 100755 --- a/test/functional/test_framework/mininode.py +++ b/test/functional/test_framework/mininode.py @@ -10,7 +10,9 @@ found in the mini-node branch of http://github.com/jgarzik/pynode. P2PConnection: A low-level connection object to a node's P2P interface -P2PInterface: A high-level interface object for communicating to a node over P2P""" +P2PInterface: A high-level interface object for communicating to a node over P2P +P2PDataStore: A p2p interface class that keeps a store of transactions and blocks + and can respond correctly to getdata and getheaders messages""" import asyncore from collections import defaultdict from io import BytesIO @@ -87,7 +89,7 @@ def peer_connect(self, dstaddr, dstport, net="regtest"): self.network = net self.disconnect = False - logger.info('Connecting to PIVX Node: %s:%d' % (self.dstaddr, self.dstport)) + logger.debug('Connecting to PIVX Node: %s:%d' % (self.dstaddr, self.dstport)) try: self.connect((dstaddr, dstport)) @@ -358,10 +360,22 @@ def wait_for_block(self, blockhash, timeout=60): wait_until(test_function, timeout=timeout, lock=mininode_lock) def wait_for_getdata(self, timeout=60): + """Waits for a getdata message. + + Receiving any getdata message will satisfy the predicate. the last_message["getdata"] + value must be explicitly cleared before calling this method, or this will return + immediately with success. TODO: change this method to take a hash value and only + return true if the correct block/tx has been requested.""" test_function = lambda: self.last_message.get("getdata") wait_until(test_function, timeout=timeout, lock=mininode_lock) def wait_for_getheaders(self, timeout=60): + """Waits for a getheaders message. + + Receiving any getheaders message will satisfy the predicate. the last_message["getheaders"] + value must be explicitly cleared before calling this method, or this will return + immediately with success. TODO: change this method to take a hash value and only + return true if the correct block header has been requested.""" test_function = lambda: self.last_message.get("getheaders") wait_until(test_function, timeout=timeout, lock=mininode_lock) @@ -442,3 +456,89 @@ def network_thread_join(timeout=10): for thread in network_threads: thread.join(timeout) assert not thread.is_alive() + +class P2PDataStore(P2PInterface): + """A P2P data store class. + + Keeps a block and transaction store and responds correctly to getdata and getheaders requests.""" + + def __init__(self): + super().__init__() + # store of blocks. key is block hash, value is a CBlock object + self.block_store = {} + self.last_block_hash = '' + # store of txs. key is txid, value is a CTransaction object + self.tx_store = {} + self.getdata_requests = [] + + def on_getdata(self, message): + """Check for the tx/block in our stores and if found, reply with an inv message.""" + for inv in message.inv: + self.getdata_requests.append(inv.hash) + if (inv.type & MSG_TYPE_MASK) == MSG_TX and inv.hash in self.tx_store.keys(): + self.send_message(msg_tx(self.tx_store[inv.hash])) + elif (inv.type & MSG_TYPE_MASK) == MSG_BLOCK and inv.hash in self.block_store.keys(): + self.send_message(msg_block(self.block_store[inv.hash])) + else: + logger.debug('getdata message type {} received.'.format(hex(inv.type))) + + def send_blocks_and_test(self, blocks, node, success=True, reject_reason=None, expect_disconnect=False, timeout=60): + """Send blocks to test node and test whether the tip advances. + + - add all blocks to our block_store + - send the blocks + - if success is True: assert that the node's tip advances to the most recent block + - if success is False: assert that the node's tip doesn't advance + - if reject_reason is set: assert that the correct reject message is logged""" + + with mininode_lock: + for block in blocks: + self.block_store[block.sha256] = block + self.last_block_hash = block.sha256 + + reject_reason = [reject_reason] if reject_reason else [] + with node.assert_debug_log(reject_reason): + self.send_message(msg_block(blocks[-1])) + + if expect_disconnect: + self.wait_for_disconnect() + else: + self.sync_with_ping() + + if success: + wait_until(lambda: node.getbestblockhash() == blocks[-1].hash, timeout=timeout) + else: + assert node.getbestblockhash() != blocks[-1].hash + + def send_txs_and_test(self, txs, node, *, success=True, expect_disconnect=False, reject_reason=None): + """Send txs to test node and test whether they're accepted to the mempool. + + - add all txs to our tx_store + - send tx messages for all txs + - if success is True/False: assert that the txs are/are not accepted to the mempool + - if expect_disconnect is True: Skip the sync with ping + - if reject_reason is set: assert that the correct reject message is logged.""" + + with mininode_lock: + for tx in txs: + self.tx_store[tx.sha256] = tx + + reject_reason = [reject_reason] if reject_reason else [] + with node.assert_debug_log(expected_msgs=reject_reason): + for tx in txs: + self.send_message(msg_tx(tx)) + + if expect_disconnect: + self.wait_for_disconnect() + else: + self.sync_with_ping() + + raw_mempool = node.getrawmempool() + if success: + # Check that all txs are now in the mempool + for tx in txs: + assert tx.hash in raw_mempool, "{} not found in mempool".format(tx.hash) + else: + # Check that none of the txs are now in the mempool + for tx in txs: + assert tx.hash not in raw_mempool, "{} tx found in mempool".format(tx.hash) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 64bb0c1b5d3f..f1805f2f100c 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -68,6 +68,7 @@ 'rpc_fundrawtransaction.py', # ~ 227 sec 'mining_pos_coldStaking.py', # ~ 220 sec 'wallet_import_rescan.py', # ~ 204 sec + 'p2p_invalid_block.py', # ~ 213 sec 'feature_logging.py', # ~ 195 sec 'wallet_abandonconflict.py', # ~ 188 sec 'feature_blockindexstats.py', # ~ 167 sec @@ -88,6 +89,7 @@ 'feature_reindex.py', # ~ 110 sec 'interface_http.py', # ~ 105 sec 'feature_blockhashcache.py', # ~ 100 sec + 'p2p_invalid_tx.py', # ~ 98 sec 'wallet_listtransactions.py', # ~ 97 sec 'wallet_listreceivedby.py', # ~ 94 sec 'mining_pos_fakestake.py', # ~ 94 sec @@ -124,8 +126,6 @@ # 'rpc_getchaintips.py', # 'rpc_users.py', # 'mining_prioritisetransaction.py', - # 'p2p_invalid_block.py', - # 'p2p_invalid_tx.py', # 'mining_basic.py', # 'wallet_bumpfee.py', # 'wallet_listsinceblock.py',