aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/compat.h2
-rw-r--r--src/miner.cpp2
-rw-r--r--src/miner.h23
-rw-r--r--src/test/mempool_tests.cpp46
-rw-r--r--src/txmempool.cpp4
-rw-r--r--src/txmempool.h74
-rw-r--r--test/functional/README.md16
-rwxr-xr-xtest/functional/bumpfee.py2
-rwxr-xr-xtest/functional/create_cache.py1
-rwxr-xr-xtest/functional/multiwallet.py48
-rwxr-xr-xtest/functional/segwit.py56
-rw-r--r--test/functional/test_framework/blocktools.py64
-rwxr-xr-xtest/functional/test_framework/test_framework.py7
-rwxr-xr-xtest/functional/test_framework/test_node.py68
-rwxr-xr-xtest/functional/test_runner.py24
15 files changed, 273 insertions, 164 deletions
diff --git a/src/compat.h b/src/compat.h
index 65e9683e2f..aae84b1181 100644
--- a/src/compat.h
+++ b/src/compat.h
@@ -33,7 +33,7 @@
#include <ws2tcpip.h>
#include <stdint.h>
#else
-#include <sys/fcntl.h>
+#include <fcntl.h>
#include <sys/mman.h>
#include <sys/select.h>
#include <sys/socket.h>
diff --git a/src/miner.cpp b/src/miner.cpp
index 4e63ab4df0..dda52790c6 100644
--- a/src/miner.cpp
+++ b/src/miner.cpp
@@ -352,7 +352,7 @@ void BlockAssembler::addPackageTxs(int &nPackagesSelected, int &nDescendantsUpda
// Try to compare the mapTx entry to the mapModifiedTx entry
iter = mempool.mapTx.project<0>(mi);
if (modit != mapModifiedTx.get<ancestor_score>().end() &&
- CompareModifiedEntry()(*modit, CTxMemPoolModifiedEntry(iter))) {
+ CompareTxMemPoolEntryByAncestorFee()(*modit, CTxMemPoolModifiedEntry(iter))) {
// The best entry in mapModifiedTx has higher score
// than the one from mapTx.
// Switch which transaction (package) to consider
diff --git a/src/miner.h b/src/miner.h
index 698b4a4788..9c086332d4 100644
--- a/src/miner.h
+++ b/src/miner.h
@@ -41,6 +41,12 @@ struct CTxMemPoolModifiedEntry {
nSigOpCostWithAncestors = entry->GetSigOpCostWithAncestors();
}
+ int64_t GetModifiedFee() const { return iter->GetModifiedFee(); }
+ uint64_t GetSizeWithAncestors() const { return nSizeWithAncestors; }
+ CAmount GetModFeesWithAncestors() const { return nModFeesWithAncestors; }
+ size_t GetTxSize() const { return iter->GetTxSize(); }
+ const CTransaction& GetTx() const { return iter->GetTx(); }
+
CTxMemPool::txiter iter;
uint64_t nSizeWithAncestors;
CAmount nModFeesWithAncestors;
@@ -67,21 +73,6 @@ struct modifiedentry_iter {
}
};
-// This matches the calculation in CompareTxMemPoolEntryByAncestorFee,
-// except operating on CTxMemPoolModifiedEntry.
-// TODO: refactor to avoid duplication of this logic.
-struct CompareModifiedEntry {
- bool operator()(const CTxMemPoolModifiedEntry &a, const CTxMemPoolModifiedEntry &b) const
- {
- double f1 = (double)a.nModFeesWithAncestors * b.nSizeWithAncestors;
- double f2 = (double)b.nModFeesWithAncestors * a.nSizeWithAncestors;
- if (f1 == f2) {
- return CTxMemPool::CompareIteratorByHash()(a.iter, b.iter);
- }
- return f1 > f2;
- }
-};
-
// A comparator that sorts transactions based on number of ancestors.
// This is sufficient to sort an ancestor package in an order that is valid
// to appear in a block.
@@ -106,7 +97,7 @@ typedef boost::multi_index_container<
// Reuse same tag from CTxMemPool's similar index
boost::multi_index::tag<ancestor_score>,
boost::multi_index::identity<CTxMemPoolModifiedEntry>,
- CompareModifiedEntry
+ CompareTxMemPoolEntryByAncestorFee
>
>
> indexed_modified_transaction_set;
diff --git a/src/test/mempool_tests.cpp b/src/test/mempool_tests.cpp
index f56f341498..1766c6a093 100644
--- a/src/test/mempool_tests.cpp
+++ b/src/test/mempool_tests.cpp
@@ -287,35 +287,6 @@ BOOST_AUTO_TEST_CASE(MempoolIndexingTest)
pool.removeRecursive(pool.mapTx.find(tx9.GetHash())->GetTx());
pool.removeRecursive(pool.mapTx.find(tx8.GetHash())->GetTx());
- /* Now check the sort on the mining score index.
- * Final order should be:
- *
- * tx7 (2M)
- * tx2 (20k)
- * tx4 (15000)
- * tx1/tx5 (10000)
- * tx3/6 (0)
- * (Ties resolved by hash)
- */
- sortedOrder.clear();
- sortedOrder.push_back(tx7.GetHash().ToString());
- sortedOrder.push_back(tx2.GetHash().ToString());
- sortedOrder.push_back(tx4.GetHash().ToString());
- if (tx1.GetHash() < tx5.GetHash()) {
- sortedOrder.push_back(tx5.GetHash().ToString());
- sortedOrder.push_back(tx1.GetHash().ToString());
- } else {
- sortedOrder.push_back(tx1.GetHash().ToString());
- sortedOrder.push_back(tx5.GetHash().ToString());
- }
- if (tx3.GetHash() < tx6.GetHash()) {
- sortedOrder.push_back(tx6.GetHash().ToString());
- sortedOrder.push_back(tx3.GetHash().ToString());
- } else {
- sortedOrder.push_back(tx3.GetHash().ToString());
- sortedOrder.push_back(tx6.GetHash().ToString());
- }
- CheckSort<mining_score>(pool, sortedOrder);
}
BOOST_AUTO_TEST_CASE(MempoolAncestorIndexingTest)
@@ -427,6 +398,23 @@ BOOST_AUTO_TEST_CASE(MempoolAncestorIndexingTest)
sortedOrder.erase(sortedOrder.end()-2);
sortedOrder.insert(sortedOrder.begin(), tx7.GetHash().ToString());
CheckSort<ancestor_score>(pool, sortedOrder);
+
+ // High-fee parent, low-fee child
+ // tx7 -> tx8
+ CMutableTransaction tx8 = CMutableTransaction();
+ tx8.vin.resize(1);
+ tx8.vin[0].prevout = COutPoint(tx7.GetHash(), 0);
+ tx8.vin[0].scriptSig = CScript() << OP_11;
+ tx8.vout.resize(1);
+ tx8.vout[0].scriptPubKey = CScript() << OP_11 << OP_EQUAL;
+ tx8.vout[0].nValue = 10*COIN;
+
+ // Check that we sort by min(feerate, ancestor_feerate):
+ // set the fee so that the ancestor feerate is above tx1/5,
+ // but the transaction's own feerate is lower
+ pool.addUnchecked(tx8.GetHash(), entry.Fee(5000LL).FromTx(tx8));
+ sortedOrder.insert(sortedOrder.end()-1, tx8.GetHash().ToString());
+ CheckSort<ancestor_score>(pool, sortedOrder);
}
diff --git a/src/txmempool.cpp b/src/txmempool.cpp
index ffb024aef9..d1edde284f 100644
--- a/src/txmempool.cpp
+++ b/src/txmempool.cpp
@@ -906,8 +906,8 @@ bool CCoinsViewMemPool::GetCoin(const COutPoint &outpoint, Coin &coin) const {
size_t CTxMemPool::DynamicMemoryUsage() const {
LOCK(cs);
- // Estimate the overhead of mapTx to be 15 pointers + an allocation, as no exact formula for boost::multi_index_contained is implemented.
- return memusage::MallocUsage(sizeof(CTxMemPoolEntry) + 15 * sizeof(void*)) * mapTx.size() + memusage::DynamicUsage(mapNextTx) + memusage::DynamicUsage(mapDeltas) + memusage::DynamicUsage(mapLinks) + memusage::DynamicUsage(vTxHashes) + cachedInnerUsage;
+ // Estimate the overhead of mapTx to be 12 pointers + an allocation, as no exact formula for boost::multi_index_contained is implemented.
+ return memusage::MallocUsage(sizeof(CTxMemPoolEntry) + 12 * sizeof(void*)) * mapTx.size() + memusage::DynamicUsage(mapNextTx) + memusage::DynamicUsage(mapDeltas) + memusage::DynamicUsage(mapLinks) + memusage::DynamicUsage(vTxHashes) + cachedInnerUsage;
}
void CTxMemPool::RemoveStaged(setEntries &stage, bool updateDescendants, MemPoolRemovalReason reason) {
diff --git a/src/txmempool.h b/src/txmempool.h
index 512e70f8fa..d6f8e7094b 100644
--- a/src/txmempool.h
+++ b/src/txmempool.h
@@ -206,18 +206,14 @@ class CompareTxMemPoolEntryByDescendantScore
public:
bool operator()(const CTxMemPoolEntry& a, const CTxMemPoolEntry& b) const
{
- bool fUseADescendants = UseDescendantScore(a);
- bool fUseBDescendants = UseDescendantScore(b);
+ double a_mod_fee, a_size, b_mod_fee, b_size;
- double aModFee = fUseADescendants ? a.GetModFeesWithDescendants() : a.GetModifiedFee();
- double aSize = fUseADescendants ? a.GetSizeWithDescendants() : a.GetTxSize();
-
- double bModFee = fUseBDescendants ? b.GetModFeesWithDescendants() : b.GetModifiedFee();
- double bSize = fUseBDescendants ? b.GetSizeWithDescendants() : b.GetTxSize();
+ GetModFeeAndSize(a, a_mod_fee, a_size);
+ GetModFeeAndSize(b, b_mod_fee, b_size);
// Avoid division by rewriting (a/b > c/d) as (a*d > c*b).
- double f1 = aModFee * bSize;
- double f2 = aSize * bModFee;
+ double f1 = a_mod_fee * b_size;
+ double f2 = a_size * b_mod_fee;
if (f1 == f2) {
return a.GetTime() >= b.GetTime();
@@ -225,12 +221,21 @@ public:
return f1 < f2;
}
- // Calculate which score to use for an entry (avoiding division).
- bool UseDescendantScore(const CTxMemPoolEntry &a) const
+ // Return the fee/size we're using for sorting this entry.
+ void GetModFeeAndSize(const CTxMemPoolEntry &a, double &mod_fee, double &size) const
{
+ // Compare feerate with descendants to feerate of the transaction, and
+ // return the fee/size for the max.
double f1 = (double)a.GetModifiedFee() * a.GetSizeWithDescendants();
double f2 = (double)a.GetModFeesWithDescendants() * a.GetTxSize();
- return f2 > f1;
+
+ if (f2 > f1) {
+ mod_fee = a.GetModFeesWithDescendants();
+ size = a.GetSizeWithDescendants();
+ } else {
+ mod_fee = a.GetModifiedFee();
+ size = a.GetTxSize();
+ }
}
};
@@ -261,33 +266,53 @@ public:
}
};
+/** \class CompareTxMemPoolEntryByAncestorScore
+ *
+ * Sort an entry by min(score/size of entry's tx, score/size with all ancestors).
+ */
class CompareTxMemPoolEntryByAncestorFee
{
public:
- bool operator()(const CTxMemPoolEntry& a, const CTxMemPoolEntry& b) const
+ template<typename T>
+ bool operator()(const T& a, const T& b) const
{
- double aFees = a.GetModFeesWithAncestors();
- double aSize = a.GetSizeWithAncestors();
+ double a_mod_fee, a_size, b_mod_fee, b_size;
- double bFees = b.GetModFeesWithAncestors();
- double bSize = b.GetSizeWithAncestors();
+ GetModFeeAndSize(a, a_mod_fee, a_size);
+ GetModFeeAndSize(b, b_mod_fee, b_size);
// Avoid division by rewriting (a/b > c/d) as (a*d > c*b).
- double f1 = aFees * bSize;
- double f2 = aSize * bFees;
+ double f1 = a_mod_fee * b_size;
+ double f2 = a_size * b_mod_fee;
if (f1 == f2) {
return a.GetTx().GetHash() < b.GetTx().GetHash();
}
-
return f1 > f2;
}
+
+ // Return the fee/size we're using for sorting this entry.
+ template <typename T>
+ void GetModFeeAndSize(const T &a, double &mod_fee, double &size) const
+ {
+ // Compare feerate with ancestors to feerate of the transaction, and
+ // return the fee/size for the min.
+ double f1 = (double)a.GetModifiedFee() * a.GetSizeWithAncestors();
+ double f2 = (double)a.GetModFeesWithAncestors() * a.GetTxSize();
+
+ if (f1 > f2) {
+ mod_fee = a.GetModFeesWithAncestors();
+ size = a.GetSizeWithAncestors();
+ } else {
+ mod_fee = a.GetModifiedFee();
+ size = a.GetTxSize();
+ }
+ }
};
// Multi_index tag names
struct descendant_score {};
struct entry_time {};
-struct mining_score {};
struct ancestor_score {};
class CBlockPolicyEstimator;
@@ -356,7 +381,6 @@ public:
* - transaction hash
* - feerate [we use max(feerate of tx, feerate of tx with all descendants)]
* - time in mempool
- * - mining score (feerate modified by any fee deltas from PrioritiseTransaction)
*
* Note: the term "descendant" refers to in-mempool transactions that depend on
* this one, while "ancestor" refers to in-mempool transactions that a given
@@ -446,12 +470,6 @@ public:
boost::multi_index::identity<CTxMemPoolEntry>,
CompareTxMemPoolEntryByEntryTime
>,
- // sorted by score (for mining prioritization)
- boost::multi_index::ordered_unique<
- boost::multi_index::tag<mining_score>,
- boost::multi_index::identity<CTxMemPoolEntry>,
- CompareTxMemPoolEntryByScore
- >,
// sorted by fee rate with ancestors
boost::multi_index::ordered_non_unique<
boost::multi_index::tag<ancestor_score>,
diff --git a/test/functional/README.md b/test/functional/README.md
index 4d52751b05..d6ce490ab3 100644
--- a/test/functional/README.md
+++ b/test/functional/README.md
@@ -27,6 +27,20 @@ don't have test cases for.
`set_test_params()`, `add_options()` and `setup_xxxx()` methods at the top of
the subclass, then locally-defined helper methods, then the `run_test()` method.
+#### Naming guidelines
+
+- Name the test `<area>_test.py`, where area can be one of the following:
+ - `feature` for tests for full features that aren't wallet/mining/mempool, eg `feature_rbf.py`
+ - `interface` for tests for other interfaces (REST, ZMQ, etc), eg `interface_rest.py`
+ - `mempool` for tests for mempool behaviour, eg `mempool_reorg.py`
+ - `mining` for tests for mining features, eg `mining_prioritisetransaction.py`
+ - `p2p` for tests that explicitly test the p2p interface, eg `p2p_disconnect_ban.py`
+ - `rpc` for tests for individual RPC methods or features, eg `rpc_listtransactions.py`
+ - `wallet` for tests for wallet features, eg `wallet_keypool.py`
+- use an underscore to separate words
+ - exception: for tests for specific RPCs or command line options which don't include underscores, name the test after the exact RPC or argument name, eg `rpc_decodescript.py`, not `rpc_decode_script.py`
+- Don't use the redundant word `test` in the name, eg `interface_zmq.py`, not `interface_zmq_test.py`
+
#### General test-writing advice
- Set `self.num_nodes` to the minimum number of nodes necessary for the test.
@@ -73,7 +87,7 @@ start the networking thread. (Continue with the test logic in your existing
thread.)
- Can be used to write tests where specific P2P protocol behavior is tested.
-Examples tests are `p2p-accept-block.py`, `p2p-compactblocks.py`.
+Examples tests are `p2p-acceptblock.py`, `p2p-compactblocks.py`.
#### Comptool
diff --git a/test/functional/bumpfee.py b/test/functional/bumpfee.py
index 5cbd9f5cf7..1e5620736b 100755
--- a/test/functional/bumpfee.py
+++ b/test/functional/bumpfee.py
@@ -14,7 +14,7 @@ added in the future, they should try to follow the same convention and not
make assumptions about execution order.
"""
-from segwit import send_to_witness
+from test_framework.blocktools import send_to_witness
from test_framework.test_framework import BitcoinTestFramework
from test_framework import blocktools
from test_framework.mininode import CTransaction
diff --git a/test/functional/create_cache.py b/test/functional/create_cache.py
index 4c79814a26..9665c50a92 100755
--- a/test/functional/create_cache.py
+++ b/test/functional/create_cache.py
@@ -16,6 +16,7 @@ class CreateCache(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 0
+ self.supports_cli = True
def setup_network(self):
pass
diff --git a/test/functional/multiwallet.py b/test/functional/multiwallet.py
index d0c40e5446..12d9e9f48d 100755
--- a/test/functional/multiwallet.py
+++ b/test/functional/multiwallet.py
@@ -17,9 +17,16 @@ class MultiWalletTest(BitcoinTestFramework):
self.setup_clean_chain = True
self.num_nodes = 1
self.extra_args = [['-wallet=w1', '-wallet=w2', '-wallet=w3', '-wallet=w']]
+ self.supports_cli = True
def run_test(self):
- assert_equal(set(self.nodes[0].listwallets()), {"w1", "w2", "w3", "w"})
+ node = self.nodes[0]
+
+ data_dir = lambda *p: os.path.join(node.datadir, 'regtest', *p)
+ wallet_dir = lambda *p: data_dir('wallets', *p)
+ wallet = lambda name: node.get_wallet_rpc(name)
+
+ assert_equal(set(node.listwallets()), {"w1", "w2", "w3", "w"})
self.stop_node(0)
@@ -27,39 +34,38 @@ class MultiWalletTest(BitcoinTestFramework):
self.assert_start_raises_init_error(0, ['-wallet=w1', '-wallet=w1'], 'Error loading wallet w1. Duplicate -wallet filename specified.')
# should not initialize if wallet file is a directory
- wallet_dir = os.path.join(self.options.tmpdir, 'node0', 'regtest', 'wallets')
- os.mkdir(os.path.join(wallet_dir, 'w11'))
+ os.mkdir(wallet_dir('w11'))
self.assert_start_raises_init_error(0, ['-wallet=w11'], 'Error loading wallet w11. -wallet filename must be a regular file.')
# should not initialize if one wallet is a copy of another
- shutil.copyfile(os.path.join(wallet_dir, 'w2'), os.path.join(wallet_dir, 'w22'))
+ shutil.copyfile(wallet_dir('w2'), wallet_dir('w22'))
self.assert_start_raises_init_error(0, ['-wallet=w2', '-wallet=w22'], 'duplicates fileid')
# should not initialize if wallet file is a symlink
- os.symlink(os.path.join(wallet_dir, 'w1'), os.path.join(wallet_dir, 'w12'))
+ os.symlink(wallet_dir('w1'), wallet_dir('w12'))
self.assert_start_raises_init_error(0, ['-wallet=w12'], 'Error loading wallet w12. -wallet filename must be a regular file.')
# should not initialize if the specified walletdir does not exist
self.assert_start_raises_init_error(0, ['-walletdir=bad'], 'Error: Specified -walletdir "bad" does not exist')
# should not initialize if the specified walletdir is not a directory
- not_a_dir = os.path.join(wallet_dir, 'notadir')
+ not_a_dir = wallet_dir('notadir')
open(not_a_dir, 'a').close()
- self.assert_start_raises_init_error(0, ['-walletdir='+not_a_dir], 'Error: Specified -walletdir "' + not_a_dir + '" is not a directory')
+ self.assert_start_raises_init_error(0, ['-walletdir=' + not_a_dir], 'Error: Specified -walletdir "' + not_a_dir + '" is not a directory')
# if wallets/ doesn't exist, datadir should be the default wallet dir
- wallet_dir2 = os.path.join(self.options.tmpdir, 'node0', 'regtest', 'walletdir')
- os.rename(wallet_dir, wallet_dir2)
+ wallet_dir2 = data_dir('walletdir')
+ os.rename(wallet_dir(), wallet_dir2)
self.start_node(0, ['-wallet=w4', '-wallet=w5'])
- assert_equal(set(self.nodes[0].listwallets()), {"w4", "w5"})
- w5 = self.nodes[0].get_wallet_rpc("w5")
+ assert_equal(set(node.listwallets()), {"w4", "w5"})
+ w5 = wallet("w5")
w5.generate(1)
self.stop_node(0)
# now if wallets/ exists again, but the rootdir is specified as the walletdir, w4 and w5 should still be loaded
- os.rename(wallet_dir2, wallet_dir)
- self.start_node(0, ['-wallet=w4', '-wallet=w5', '-walletdir=' + os.path.join(self.options.tmpdir, 'node0', 'regtest')])
- assert_equal(set(self.nodes[0].listwallets()), {"w4", "w5"})
- w5 = self.nodes[0].get_wallet_rpc("w5")
+ os.rename(wallet_dir2, wallet_dir())
+ self.start_node(0, ['-wallet=w4', '-wallet=w5', '-walletdir=' + data_dir()])
+ assert_equal(set(node.listwallets()), {"w4", "w5"})
+ w5 = wallet("w5")
w5_info = w5.getwalletinfo()
assert_equal(w5_info['immature_balance'], 50)
@@ -67,11 +73,11 @@ class MultiWalletTest(BitcoinTestFramework):
self.start_node(0, self.extra_args[0])
- w1 = self.nodes[0].get_wallet_rpc("w1")
- w2 = self.nodes[0].get_wallet_rpc("w2")
- w3 = self.nodes[0].get_wallet_rpc("w3")
- w4 = self.nodes[0].get_wallet_rpc("w")
- wallet_bad = self.nodes[0].get_wallet_rpc("bad")
+ w1 = wallet("w1")
+ w2 = wallet("w2")
+ w3 = wallet("w3")
+ w4 = wallet("w")
+ wallet_bad = wallet("bad")
w1.generate(1)
@@ -79,7 +85,7 @@ class MultiWalletTest(BitcoinTestFramework):
assert_raises_rpc_error(-18, "Requested wallet does not exist or is not loaded", wallet_bad.getwalletinfo)
# accessing wallet RPC without using wallet endpoint fails
- assert_raises_rpc_error(-19, "Wallet file not specified", self.nodes[0].getwalletinfo)
+ assert_raises_rpc_error(-19, "Wallet file not specified", node.getwalletinfo)
# check w1 wallet balance
w1_info = w1.getwalletinfo()
diff --git a/test/functional/segwit.py b/test/functional/segwit.py
index 5a4a7468e5..7d5c760ad9 100755
--- a/test/functional/segwit.py
+++ b/test/functional/segwit.py
@@ -4,10 +4,18 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test the SegWit changeover logic."""
+from test_framework.address import (
+ key_to_p2sh_p2wpkh,
+ key_to_p2wpkh,
+ program_to_witness,
+ script_to_p2sh_p2wsh,
+ script_to_p2wsh,
+)
+from test_framework.blocktools import witness_script, send_to_witness
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import *
from test_framework.mininode import sha256, CTransaction, CTxIn, COutPoint, CTxOut, COIN, ToHex, FromHex
-from test_framework.address import script_to_p2sh, key_to_p2pkh, key_to_p2sh_p2wpkh, key_to_p2wpkh, script_to_p2sh_p2wsh, script_to_p2wsh, program_to_witness
+from test_framework.address import script_to_p2sh, key_to_p2pkh
from test_framework.script import CScript, OP_HASH160, OP_CHECKSIG, OP_0, hash160, OP_EQUAL, OP_DUP, OP_EQUALVERIFY, OP_1, OP_2, OP_CHECKMULTISIG, OP_TRUE
from io import BytesIO
@@ -16,52 +24,6 @@ NODE_2 = 2
WIT_V0 = 0
WIT_V1 = 1
-# Create a scriptPubKey corresponding to either a P2WPKH output for the
-# given pubkey, or a P2WSH output of a 1-of-1 multisig for the given
-# pubkey. Returns the hex encoding of the scriptPubKey.
-def witness_script(use_p2wsh, pubkey):
- if (use_p2wsh == False):
- # P2WPKH instead
- pubkeyhash = hash160(hex_str_to_bytes(pubkey))
- pkscript = CScript([OP_0, pubkeyhash])
- else:
- # 1-of-1 multisig
- witness_program = CScript([OP_1, hex_str_to_bytes(pubkey), OP_1, OP_CHECKMULTISIG])
- scripthash = sha256(witness_program)
- pkscript = CScript([OP_0, scripthash])
- return bytes_to_hex_str(pkscript)
-
-# Return a transaction (in hex) that spends the given utxo to a segwit output,
-# optionally wrapping the segwit output using P2SH.
-def create_witness_tx(node, use_p2wsh, utxo, pubkey, encode_p2sh, amount):
- if use_p2wsh:
- program = CScript([OP_1, hex_str_to_bytes(pubkey), OP_1, OP_CHECKMULTISIG])
- addr = script_to_p2sh_p2wsh(program) if encode_p2sh else script_to_p2wsh(program)
- else:
- addr = key_to_p2sh_p2wpkh(pubkey) if encode_p2sh else key_to_p2wpkh(pubkey)
- if not encode_p2sh:
- assert_equal(node.validateaddress(addr)['scriptPubKey'], witness_script(use_p2wsh, pubkey))
- return node.createrawtransaction([utxo], {addr: amount})
-
-# Create a transaction spending a given utxo to a segwit output corresponding
-# to the given pubkey: use_p2wsh determines whether to use P2WPKH or P2WSH;
-# encode_p2sh determines whether to wrap in P2SH.
-# sign=True will have the given node sign the transaction.
-# insert_redeem_script will be added to the scriptSig, if given.
-def send_to_witness(use_p2wsh, node, utxo, pubkey, encode_p2sh, amount, sign=True, insert_redeem_script=""):
- tx_to_witness = create_witness_tx(node, use_p2wsh, utxo, pubkey, encode_p2sh, amount)
- if (sign):
- signed = node.signrawtransaction(tx_to_witness)
- assert("errors" not in signed or len(["errors"]) == 0)
- return node.sendrawtransaction(signed["hex"])
- else:
- if (insert_redeem_script):
- tx = FromHex(CTransaction(), tx_to_witness)
- tx.vin[0].scriptSig += CScript([hex_str_to_bytes(insert_redeem_script)])
- tx_to_witness = ToHex(tx)
-
- return node.sendrawtransaction(tx_to_witness)
-
def getutxo(txid):
utxo = {}
utxo["vout"] = 0
diff --git a/test/functional/test_framework/blocktools.py b/test/functional/test_framework/blocktools.py
index 93af0037e9..642ef98a27 100644
--- a/test/functional/test_framework/blocktools.py
+++ b/test/functional/test_framework/blocktools.py
@@ -4,8 +4,24 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Utilities for manipulating blocks and transactions."""
+from .address import (
+ key_to_p2sh_p2wpkh,
+ key_to_p2wpkh,
+ script_to_p2sh_p2wsh,
+ script_to_p2wsh,
+)
from .mininode import *
-from .script import CScript, OP_TRUE, OP_CHECKSIG, OP_RETURN
+from .script import (
+ CScript,
+ OP_0,
+ OP_1,
+ OP_CHECKMULTISIG,
+ OP_CHECKSIG,
+ OP_RETURN,
+ OP_TRUE,
+ hash160,
+)
+from .util import assert_equal
# Create a block (with regtest difficulty)
def create_block(hashprev, coinbase, nTime=None):
@@ -108,3 +124,49 @@ def get_legacy_sigopcount_tx(tx, fAccurate=True):
# scriptSig might be of type bytes, so convert to CScript for the moment
count += CScript(j.scriptSig).GetSigOpCount(fAccurate)
return count
+
+# Create a scriptPubKey corresponding to either a P2WPKH output for the
+# given pubkey, or a P2WSH output of a 1-of-1 multisig for the given
+# pubkey. Returns the hex encoding of the scriptPubKey.
+def witness_script(use_p2wsh, pubkey):
+ if (use_p2wsh == False):
+ # P2WPKH instead
+ pubkeyhash = hash160(hex_str_to_bytes(pubkey))
+ pkscript = CScript([OP_0, pubkeyhash])
+ else:
+ # 1-of-1 multisig
+ witness_program = CScript([OP_1, hex_str_to_bytes(pubkey), OP_1, OP_CHECKMULTISIG])
+ scripthash = sha256(witness_program)
+ pkscript = CScript([OP_0, scripthash])
+ return bytes_to_hex_str(pkscript)
+
+# Return a transaction (in hex) that spends the given utxo to a segwit output,
+# optionally wrapping the segwit output using P2SH.
+def create_witness_tx(node, use_p2wsh, utxo, pubkey, encode_p2sh, amount):
+ if use_p2wsh:
+ program = CScript([OP_1, hex_str_to_bytes(pubkey), OP_1, OP_CHECKMULTISIG])
+ addr = script_to_p2sh_p2wsh(program) if encode_p2sh else script_to_p2wsh(program)
+ else:
+ addr = key_to_p2sh_p2wpkh(pubkey) if encode_p2sh else key_to_p2wpkh(pubkey)
+ if not encode_p2sh:
+ assert_equal(node.validateaddress(addr)['scriptPubKey'], witness_script(use_p2wsh, pubkey))
+ return node.createrawtransaction([utxo], {addr: amount})
+
+# Create a transaction spending a given utxo to a segwit output corresponding
+# to the given pubkey: use_p2wsh determines whether to use P2WPKH or P2WSH;
+# encode_p2sh determines whether to wrap in P2SH.
+# sign=True will have the given node sign the transaction.
+# insert_redeem_script will be added to the scriptSig, if given.
+def send_to_witness(use_p2wsh, node, utxo, pubkey, encode_p2sh, amount, sign=True, insert_redeem_script=""):
+ tx_to_witness = create_witness_tx(node, use_p2wsh, utxo, pubkey, encode_p2sh, amount)
+ if (sign):
+ signed = node.signrawtransaction(tx_to_witness)
+ assert("errors" not in signed or len(["errors"]) == 0)
+ return node.sendrawtransaction(signed["hex"])
+ else:
+ if (insert_redeem_script):
+ tx = FromHex(CTransaction(), tx_to_witness)
+ tx.vin[0].scriptSig += CScript([hex_str_to_bytes(insert_redeem_script)])
+ tx_to_witness = ToHex(tx)
+
+ return node.sendrawtransaction(tx_to_witness)
diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py
index e42f3e60c2..5d4f8e6720 100755
--- a/test/functional/test_framework/test_framework.py
+++ b/test/functional/test_framework/test_framework.py
@@ -62,6 +62,7 @@ class BitcoinTestFramework():
self.setup_clean_chain = False
self.nodes = []
self.mocktime = 0
+ self.supports_cli = False
self.set_test_params()
assert hasattr(self, "num_nodes"), "Test must set self.num_nodes in set_test_params()"
@@ -91,6 +92,8 @@ class BitcoinTestFramework():
help="Location of the test framework config file")
parser.add_option("--pdbonfailure", dest="pdbonfailure", default=False, action="store_true",
help="Attach a python debugger if test fails")
+ parser.add_option("--usecli", dest="usecli", default=False, action="store_true",
+ help="use bitcoin-cli instead of RPC for all commands")
self.add_options(parser)
(self.options, self.args) = parser.parse_args()
@@ -113,6 +116,8 @@ class BitcoinTestFramework():
success = TestStatus.FAILED
try:
+ if self.options.usecli and not self.supports_cli:
+ raise SkipTest("--usecli specified but test does not support using CLI")
self.setup_chain()
self.setup_network()
self.run_test()
@@ -213,7 +218,7 @@ class BitcoinTestFramework():
assert_equal(len(extra_args), num_nodes)
assert_equal(len(binary), num_nodes)
for i in range(num_nodes):
- self.nodes.append(TestNode(i, self.options.tmpdir, extra_args[i], rpchost, timewait=timewait, binary=binary[i], stderr=None, mocktime=self.mocktime, coverage_dir=self.options.coveragedir))
+ self.nodes.append(TestNode(i, self.options.tmpdir, extra_args[i], rpchost, timewait=timewait, binary=binary[i], stderr=None, mocktime=self.mocktime, coverage_dir=self.options.coveragedir, use_cli=self.options.usecli))
def start_node(self, i, extra_args=None, stderr=None):
"""Start a bitcoind"""
diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py
index a9248c764e..589a8f3969 100755
--- a/test/functional/test_framework/test_node.py
+++ b/test/functional/test_framework/test_node.py
@@ -10,6 +10,7 @@ import http.client
import json
import logging
import os
+import re
import subprocess
import time
@@ -22,6 +23,9 @@ from .util import (
p2p_port,
)
+# For Python 3.4 compatibility
+JSONDecodeError = getattr(json, "JSONDecodeError", ValueError)
+
BITCOIND_PROC_WAIT_TIMEOUT = 60
class TestNode():
@@ -38,7 +42,7 @@ class TestNode():
To make things easier for the test writer, any unrecognised messages will
be dispatched to the RPC connection."""
- def __init__(self, i, dirname, extra_args, rpchost, timewait, binary, stderr, mocktime, coverage_dir):
+ def __init__(self, i, dirname, extra_args, rpchost, timewait, binary, stderr, mocktime, coverage_dir, use_cli=False):
self.index = i
self.datadir = os.path.join(dirname, "node" + str(i))
self.rpchost = rpchost
@@ -58,6 +62,7 @@ class TestNode():
self.args = [self.binary, "-datadir=" + self.datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-logtimemicros", "-debug", "-debugexclude=libevent", "-debugexclude=leveldb", "-mocktime=" + str(mocktime), "-uacomment=testnode%d" % i]
self.cli = TestNodeCLI(os.getenv("BITCOINCLI", "bitcoin-cli"), self.datadir)
+ self.use_cli = use_cli
self.running = False
self.process = None
@@ -69,9 +74,12 @@ class TestNode():
self.p2ps = []
def __getattr__(self, name):
- """Dispatches any unrecognised messages to the RPC connection."""
- assert self.rpc_connected and self.rpc is not None, "Error: no RPC connection"
- return getattr(self.rpc, name)
+ """Dispatches any unrecognised messages to the RPC connection or a CLI instance."""
+ if self.use_cli:
+ return getattr(self.cli, name)
+ else:
+ assert self.rpc_connected and self.rpc is not None, "Error: no RPC connection"
+ return getattr(self.rpc, name)
def start(self, extra_args=None, stderr=None):
"""Start the node."""
@@ -110,10 +118,13 @@ class TestNode():
raise AssertionError("Unable to connect to bitcoind")
def get_wallet_rpc(self, wallet_name):
- assert self.rpc_connected
- assert self.rpc
- wallet_path = "wallet/%s" % wallet_name
- return self.rpc / wallet_path
+ if self.use_cli:
+ return self.cli("-rpcwallet={}".format(wallet_name))
+ else:
+ assert self.rpc_connected
+ assert self.rpc
+ wallet_path = "wallet/%s" % wallet_name
+ return self.rpc / wallet_path
def stop_node(self):
"""Stop the node."""
@@ -187,6 +198,16 @@ class TestNode():
p.peer_disconnect()
del self.p2ps[:]
+class TestNodeCLIAttr:
+ def __init__(self, cli, command):
+ self.cli = cli
+ self.command = command
+
+ def __call__(self, *args, **kwargs):
+ return self.cli.send_cli(self.command, *args, **kwargs)
+
+ def get_request(self, *args, **kwargs):
+ return lambda: self(*args, **kwargs)
class TestNodeCLI():
"""Interface to bitcoin-cli for an individual node"""
@@ -196,17 +217,26 @@ class TestNodeCLI():
self.binary = binary
self.datadir = datadir
self.input = None
+ self.log = logging.getLogger('TestFramework.bitcoincli')
def __call__(self, *args, input=None):
# TestNodeCLI is callable with bitcoin-cli command-line args
- self.args = [str(arg) for arg in args]
- self.input = input
- return self
+ cli = TestNodeCLI(self.binary, self.datadir)
+ cli.args = [str(arg) for arg in args]
+ cli.input = input
+ return cli
def __getattr__(self, command):
- def dispatcher(*args, **kwargs):
- return self.send_cli(command, *args, **kwargs)
- return dispatcher
+ return TestNodeCLIAttr(self, command)
+
+ def batch(self, requests):
+ results = []
+ for request in requests:
+ try:
+ results.append(dict(result=request()))
+ except JSONRPCException as e:
+ results.append(dict(error=e))
+ return results
def send_cli(self, command, *args, **kwargs):
"""Run bitcoin-cli command. Deserializes returned string as python object."""
@@ -218,10 +248,18 @@ class TestNodeCLI():
if named_args:
p_args += ["-named"]
p_args += [command] + pos_args + named_args
+ self.log.debug("Running bitcoin-cli command: %s" % command)
process = subprocess.Popen(p_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
cli_stdout, cli_stderr = process.communicate(input=self.input)
returncode = process.poll()
if returncode:
+ match = re.match(r'error code: ([-0-9]+)\nerror message:\n(.*)', cli_stderr)
+ if match:
+ code, message = match.groups()
+ raise JSONRPCException(dict(code=int(code), message=message))
# Ignore cli_stdout, raise with cli_stderr
raise subprocess.CalledProcessError(returncode, self.binary, output=cli_stderr)
- return json.loads(cli_stdout, parse_float=decimal.Decimal)
+ try:
+ return json.loads(cli_stdout, parse_float=decimal.Decimal)
+ except JSONDecodeError:
+ return cli_stdout.rstrip("\n")
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index 2d5ea84814..72ad300e7e 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -94,6 +94,7 @@ BASE_SCRIPTS= [
'mempool_reorg.py',
'mempool_persist.py',
'multiwallet.py',
+ 'multiwallet.py --usecli',
'httpbasics.py',
'multi_rpc.py',
'proxy_test.py',
@@ -271,6 +272,7 @@ def main():
sys.exit(0)
check_script_list(config["environment"]["SRCDIR"])
+ check_script_prefixes()
if not args.keepcache:
shutil.rmtree("%s/test/cache" % config["environment"]["BUILDDIR"], ignore_errors=True)
@@ -469,6 +471,28 @@ class TestResult():
return self.status != "Failed"
+def check_script_prefixes():
+ """Check that no more than `EXPECTED_VIOLATION_COUNT` of the
+ test scripts don't start with one of the allowed name prefixes."""
+ EXPECTED_VIOLATION_COUNT = 77
+
+ # LEEWAY is provided as a transition measure, so that pull-requests
+ # that introduce new tests that don't conform with the naming
+ # convention don't immediately cause the tests to fail.
+ LEEWAY = 10
+
+ good_prefixes_re = re.compile("(example|feature|interface|mempool|mining|p2p|rpc|wallet)_")
+ bad_script_names = [script for script in ALL_SCRIPTS if good_prefixes_re.match(script) is None]
+
+ if len(bad_script_names) < EXPECTED_VIOLATION_COUNT:
+ print("{}HURRAY!{} Number of functional tests violating naming convention reduced!".format(BOLD[1], BOLD[0]))
+ print("Consider reducing EXPECTED_VIOLATION_COUNT from %d to %d" % (EXPECTED_VIOLATION_COUNT, len(bad_script_names)))
+ elif len(bad_script_names) > EXPECTED_VIOLATION_COUNT:
+ print("INFO: %d tests not meeting naming conventions (expected %d):" % (len(bad_script_names), EXPECTED_VIOLATION_COUNT))
+ print(" %s" % ("\n ".join(sorted(bad_script_names))))
+ assert len(bad_script_names) <= EXPECTED_VIOLATION_COUNT + LEEWAY, "Too many tests not following naming convention! (%d found, expected: <= %d)" % (len(bad_script_names), EXPECTED_VIOLATION_COUNT)
+
+
def check_script_list(src_dir):
"""Check scripts directory.