Add RPC test for wallet_listunspent changes

This commit is contained in:
Kris Nuttycombe 2022-09-21 20:10:28 -06:00 committed by Greg Pfeil
parent b745992489
commit 2c487c9430
7 changed files with 170 additions and 53 deletions

View File

@ -62,6 +62,7 @@ BASE_SCRIPTS= [
'wallet_overwintertx.py',
'wallet_persistence.py',
'wallet_listnotes.py',
'wallet_listunspent.py',
# vv Tests less than 60s vv
'orchard_reorg.py',
'fundrawtransaction.py',

View File

@ -56,17 +56,18 @@ class BitcoinTestFramework(object):
# Connect the nodes as a "chain". This allows us
# to split the network between nodes 1 and 2 to get
# two halves that can work on competing chains.
connect_nodes_bi(self.nodes, 0, 1)
# If we joined network halves, connect the nodes from the joint
# on outward. This ensures that chains are properly reorganised.
if not split:
connect_nodes_bi(self.nodes, 1, 2)
sync_blocks(self.nodes[1:3])
if do_mempool_sync:
sync_mempools(self.nodes[1:3])
if len(self.nodes) >= 4:
connect_nodes_bi(self.nodes, 2, 3)
if not split:
connect_nodes_bi(self.nodes, 1, 2)
sync_blocks(self.nodes[1:3])
if do_mempool_sync:
sync_mempools(self.nodes[1:3])
connect_nodes_bi(self.nodes, 0, 1)
connect_nodes_bi(self.nodes, 2, 3)
self.is_network_split = split
self.sync_all(do_mempool_sync)

View File

@ -307,15 +307,15 @@ def initialize_chain(test_dir, num_nodes, cachedir, cache_behavior='current'):
wait_bitcoinds()
for i in range(MAX_NODES):
# record the system time at which the cache was regenerated
with open(log_filename(cachedir, i, 'cache_config.json'), "w", encoding="utf8") as cache_conf_file:
with open(node_file(cachedir, i, 'cache_config.json'), "w", encoding="utf8") as cache_conf_file:
cache_config = { "cache_time": time.time() }
cache_conf_json = json.dumps(cache_config, indent=4)
cache_conf_file.write(cache_conf_json)
os.remove(log_filename(cachedir, i, "debug.log"))
os.remove(log_filename(cachedir, i, "db.log"))
os.remove(log_filename(cachedir, i, "peers.dat"))
os.remove(log_filename(cachedir, i, "fee_estimates.dat"))
os.remove(node_file(cachedir, i, "debug.log"))
os.remove(node_file(cachedir, i, "db.log"))
os.remove(node_file(cachedir, i, "peers.dat"))
os.remove(node_file(cachedir, i, "fee_estimates.dat"))
def init_from_cache():
for i in range(num_nodes):
@ -351,7 +351,7 @@ def initialize_chain(test_dir, num_nodes, cachedir, cache_behavior='current'):
for i in range(MAX_NODES):
node_path = os.path.join(cachedir, 'node'+str(i))
if os.path.isdir(node_path):
if not os.path.isfile(log_filename(cachedir, i, 'cache_config.json')):
if not os.path.isfile(node_file(cachedir, i, 'cache_config.json')):
return True
else:
return True
@ -461,8 +461,8 @@ def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, binary=None):
raise
return rpcs
def log_filename(dirname, n_node, logname):
return os.path.join(dirname, "node"+str(n_node), "regtest", logname)
def node_file(dirname, n_node, filename):
return os.path.join(dirname, "node"+str(n_node), "regtest", filename)
def check_node(i):
bitcoind_processes[i].poll()

View File

@ -0,0 +1,120 @@
#!/usr/bin/env python3
# Copyright (c) 2016-2022 The Zcash developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://www.opensource.org/licenses/mit-license.php .
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
get_coinbase_address,
nuparams,
start_nodes,
wait_and_assert_operationid_status,
NU5_BRANCH_ID
)
from decimal import Decimal
class WalletListUnspent(BitcoinTestFramework):
def setup_nodes(self):
return start_nodes(4, self.options.tmpdir, [[
nuparams(NU5_BRANCH_ID, 201),
]] * 4)
def run_test(self):
assert_equal(self.nodes[0].getbalance(), 250)
assert_equal(self.nodes[1].getbalance(), 250)
# Activate NU5
self.nodes[1].generate(1) # height 201
self.sync_all()
assert_equal(self.nodes[0].getbalance(), 260) # additional 10 ZEC matured
# Shield some coinbase funds so that they become spendable
n1acct = self.nodes[1].z_getnewaccount()['account']
n1uaddr = self.nodes[1].z_getaddressforaccount(n1acct)['address']
opid = self.nodes[0].z_sendmany(
get_coinbase_address(self.nodes[0]),
[{'address': n1uaddr, 'amount': 10}],
1, 0, 'AllowRevealedSenders')
wait_and_assert_operationid_status(self.nodes[0], opid)
self.sync_all()
self.nodes[1].generate(2)
self.sync_all() # height 203
assert_equal(self.nodes[0].getbalance(), 270) # 260 - 10 (spent) + 20 (matured)
assert_equal(
self.nodes[1].z_getbalanceforaccount(n1acct, 1)['pools']['orchard']['valueZat'],
Decimal('1000000000'))
# Get a bare legacy transparent address for node 0
n0addr = self.nodes[0].getnewaddress()
# Send funds to the node 0 address so we have transparent funds to spend.
opid = self.nodes[1].z_sendmany(
n1uaddr,
[{'address': n0addr, 'amount': 10}],
1, 0, 'AllowRevealedRecipients')
wait_and_assert_operationid_status(self.nodes[1], opid)
self.sync_all()
self.nodes[1].generate(2)
self.sync_all() # height 205
assert_equal(self.nodes[0].getbalance(), 300) # 270 + 20 (matured) + 10 (received)
print("----------------------------------------------------------------")
unspent_205 = self.nodes[0].listunspent(0, 999999, [])
for item in sorted(unspent_205, key=lambda item: item['confirmations']):
print(str(item))
# We will then perform several spends, and then check the list of
# unspent notes as of various heights.
opid = self.nodes[0].z_sendmany(
'ANY_TADDR',
[{'address': n1uaddr, 'amount': 2}],
1, 0, 'AllowRevealedSenders')
wait_and_assert_operationid_status(self.nodes[0], opid)
self.nodes[0].generate(2)
self.sync_all() # height 207
assert_equal(self.nodes[0].getbalance(), 318) # 300 + 20 (matured) - 2 (sent)
opid = self.nodes[0].z_sendmany(
'ANY_TADDR',
[{'address': n1uaddr, 'amount': 3}],
1, 0, 'AllowRevealedSenders')
wait_and_assert_operationid_status(self.nodes[0], opid)
self.nodes[0].generate(2)
self.sync_all() # height 209
assert_equal(self.nodes[0].getbalance(), 335) # 318 + 20 (matured) - 3 (sent)
opid = self.nodes[0].z_sendmany(
'ANY_TADDR',
[{'address': n1uaddr, 'amount': 5}],
1, 0, 'AllowRevealedSenders')
wait_and_assert_operationid_status(self.nodes[0], opid)
self.nodes[0].generate(2)
self.sync_all() # height 211
assert_equal(self.nodes[0].getbalance(), 350) # 325 + 20 (matured) - 5 (sent)
def unspent_total(unspent):
total = 0
for item in unspent:
total += item['amount']
return total
unspent_205 = self.nodes[0].listunspent(0, 999999, [], 205)
print("----------------------------------------------------------------")
for item in sorted(unspent_205, key=lambda item: item['confirmations']):
print(str(item))
assert_equal(unspent_total(self.nodes[0].listunspent(0, 999999, [], 205)), 300)
assert_equal(unspent_total(self.nodes[0].listunspent(0, 999999, [], 207)), 318)
assert_equal(unspent_total(self.nodes[0].listunspent(0, 999999, [], 209)), 335)
assert_equal(unspent_total(self.nodes[0].listunspent(0, 999999, [], 211)), 350)
if __name__ == '__main__':
WalletListUnspent().main()

View File

@ -2396,7 +2396,7 @@ UniValue listunspent(const UniValue& params, bool fHelp)
if (fHelp || params.size() > 4)
throw runtime_error(
"listunspent ( minconf maxconf [\"address\",...] unspent_as_of)\n"
"listunspent ( minconf maxconf [\"address\",...] as_of_height)\n"
"\nReturns array of unspent transparent transaction outputs with between minconf and\n"
"maxconf (inclusive) confirmations. Use `z_listunspent` instead to see information\n"
"related to unspent shielded notes. Results may be optionally filtered to only include\n"
@ -2462,16 +2462,9 @@ UniValue listunspent(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet);
std::optional<int> unspentAsOfDepth;
std::optional<int> asOfHeight;
if (params.size() > 3) {
auto asOfHeight = params[3].get_int();
// it's fine to specify a height that's greater than the current height
if (chainActive.Height() > asOfHeight) {
unspentAsOfDepth = chainActive.Height() - asOfHeight;
if (unspentAsOfDepth.value() > nMinDepth) {
nMinDepth = unspentAsOfDepth.value();
}
}
asOfHeight = params[3].get_int();
}
UniValue results(UniValue::VARR);
@ -2485,7 +2478,7 @@ UniValue listunspent(const UniValue& params, bool fHelp)
false, // fOnlySpendable
nMinDepth,
destinations.empty() ? nullptr : &destinations,
unspentAsOfDepth);
asOfHeight);
for (const COutput& out : vecOutputs) {
if (out.nDepth < nMinDepth || out.nDepth > nMaxDepth)
continue;

View File

@ -2413,7 +2413,7 @@ SpendableInputs CWallet::FindSpendableInputs(
* Outpoint is spent if any non-conflicted transaction
* spends it:
*/
bool CWallet::IsSpent(const uint256& hash, unsigned int n, std::optional<int> unspentAsOfDepth) const
bool CWallet::IsSpent(const uint256& hash, unsigned int n, std::optional<int> asOfDepth) const
{
const COutPoint outpoint(hash, n);
pair<TxSpends::const_iterator, TxSpends::const_iterator> range;
@ -2423,8 +2423,11 @@ bool CWallet::IsSpent(const uint256& hash, unsigned int n, std::optional<int> un
{
const uint256& wtxid = it->second;
std::map<uint256, CWalletTx>::const_iterator mit = mapWallet.find(wtxid);
if (mit != mapWallet.end() && mit->second.GetDepthInMainChain() >= unspentAsOfDepth.value_or(0))
return true; // Spent
if (mit != mapWallet.end()) {
auto confirmations = mit->second.GetDepthInMainChain();
if (confirmations >= asOfDepth.value_or(0))
return true;
}
}
return false;
}
@ -5247,7 +5250,7 @@ void CWallet::AvailableCoins(vector<COutput>& vCoins,
bool fOnlySpendable,
int nMinDepth,
std::set<CTxDestination>* onlyFilterByDests,
std::optional<int> unspentAsOfDepth) const
const std::optional<int>& asOfHeight) const
{
assert(nMinDepth >= 0);
AssertLockHeld(cs_main);
@ -5256,30 +5259,26 @@ void CWallet::AvailableCoins(vector<COutput>& vCoins,
vCoins.clear();
{
for (map<uint256, CWalletTx>::const_iterator it = mapWallet.begin(); it != mapWallet.end(); ++it)
{
const uint256& wtxid = it->first;
const CWalletTx* pcoin = &(*it).second;
if (!CheckFinalTx(*pcoin))
for (const auto& [wtxid, pcoin] : mapWallet) {
if (!CheckFinalTx(pcoin))
continue;
if (fOnlyConfirmed && !pcoin->IsTrusted())
if (fOnlyConfirmed && !pcoin.IsTrusted())
continue;
if (pcoin->IsCoinBase() && !fIncludeCoinBase)
if (pcoin.IsCoinBase() && !fIncludeCoinBase)
continue;
bool isCoinbase = pcoin->IsCoinBase();
if (isCoinbase && pcoin->GetBlocksToMaturity() > 0)
bool isCoinbase = pcoin.IsCoinBase();
if (isCoinbase && pcoin.GetBlocksToMaturity(asOfHeight) > 0)
continue;
int nDepth = pcoin->GetDepthInMainChain();
int nDepth = pcoin.GetDepthInMainChain();
if (nDepth < nMinDepth)
continue;
for (unsigned int i = 0; i < pcoin->vout.size(); i++) {
const auto& output = pcoin->vout[i];
for (unsigned int i = 0; i < pcoin.vout.size(); i++) {
const auto& output = pcoin.vout[i];
isminetype mine = IsMine(output);
bool isSpendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) ||
@ -5296,10 +5295,10 @@ void CWallet::AvailableCoins(vector<COutput>& vCoins,
}
}
if (!(IsSpent(wtxid, i, unspentAsOfDepth)) && mine != ISMINE_NO &&
!IsLockedCoin((*it).first, i) && (pcoin->vout[i].nValue > 0 || fIncludeZeroValue) &&
(!coinControl || !coinControl->HasSelected() || coinControl->fAllowOtherInputs || coinControl->IsSelected((*it).first, i)))
vCoins.push_back(COutput(pcoin, i, nDepth, isSpendable, isCoinbase));
if (!(IsSpent(wtxid, i, asOfHeight)) && mine != ISMINE_NO &&
!IsLockedCoin(wtxid, i) && (pcoin.vout[i].nValue > 0 || fIncludeZeroValue) &&
(!coinControl || !coinControl->HasSelected() || coinControl->fAllowOtherInputs || coinControl->IsSelected(wtxid, i)))
vCoins.push_back(COutput(&pcoin, i, nDepth, isSpendable, isCoinbase));
}
}
}
@ -6985,7 +6984,7 @@ void CMerkleTx::SetMerkleBranch(const CBlock& block)
}
}
int CMerkleTx::GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet) const
int CMerkleTx::GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet, const std::optional<int>& asOfHeight) const
{
if (hashBlock.IsNull() || nIndex == -1)
return 0;
@ -6996,8 +6995,11 @@ int CMerkleTx::GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet) const
if (mi == mapBlockIndex.end())
return 0;
CBlockIndex* pindex = (*mi).second;
if (!pindex || !chainActive.Contains(pindex))
if (!pindex ||
!chainActive.Contains(pindex) ||
(asOfHeight.has_value() && pindex->nHeight > asOfHeight.value())) {
return 0;
}
pindexRet = pindex;
return chainActive.Height() - pindex->nHeight + 1;
@ -7013,7 +7015,7 @@ int CMerkleTx::GetDepthInMainChain(const CBlockIndex* &pindexRet) const
return nResult;
}
int CMerkleTx::GetBlocksToMaturity() const
int CMerkleTx::GetBlocksToMaturity(const std::optional<int>& asOfHeight) const
{
if (!IsCoinBase())
return 0;

View File

@ -399,7 +399,7 @@ struct SaplingNoteEntry
class CMerkleTx : public CTransaction
{
private:
int GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet) const;
int GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet, const std::optional<int>& asOfHeight = std::nullopt) const;
public:
uint256 hashBlock;
@ -444,7 +444,7 @@ public:
int GetDepthInMainChain(const CBlockIndex* &pindexRet) const;
int GetDepthInMainChain() const { const CBlockIndex *pindexRet; return GetDepthInMainChain(pindexRet); }
bool IsInMainChain() const { const CBlockIndex *pindexRet; return GetDepthInMainChainINTERNAL(pindexRet) > 0; }
int GetBlocksToMaturity() const;
int GetBlocksToMaturity(const std::optional<int>& asOfHeight = std::nullopt) const;
/** Pass this transaction to the mempool. Fails if absolute fee exceeds maxTxFee. */
bool AcceptToMemoryPool(CValidationState& state, bool fLimitFree=true, bool fRejectAbsurdFee=true);
};
@ -1437,7 +1437,7 @@ public:
bool fOnlySpendable=false,
int nMinDepth = 0,
std::set<CTxDestination>* onlyFilterByDests = nullptr,
std::optional<int> unspentAsOfDepth = std::nullopt) const;
const std::optional<int>& unspentAsOfDepth = std::nullopt) const;
/**
* Shuffle and select coins until nTargetValue is reached while avoiding