update listaddresses RPC for UAs, Orchard
Co-authored-by: Kris Nuttycombe <kris@nutty.land>
This commit is contained in:
parent
612f3e7e81
commit
e575e0f217
|
@ -4,53 +4,103 @@
|
|||
# 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
|
||||
from test_framework.util import assert_equal, start_nodes, connect_nodes_bi, NU5_BRANCH_ID
|
||||
from test_framework.mininode import nuparams
|
||||
|
||||
# Test wallet address behaviour across network upgrades
|
||||
class WalletAddressesTest(BitcoinTestFramework):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# need 2 nodes to import addresses
|
||||
self.num_nodes = 2
|
||||
self.setup_clean_chain = True
|
||||
|
||||
def run_test(self):
|
||||
def addr_checks(default_type):
|
||||
# Check default type, as well as explicit types
|
||||
types_and_addresses = [
|
||||
(default_type, self.nodes[0].z_getnewaddress()),
|
||||
('sprout', self.nodes[0].z_getnewaddress('sprout')),
|
||||
('sapling', self.nodes[0].z_getnewaddress('sapling')),
|
||||
]
|
||||
|
||||
all_addresses = self.nodes[0].z_listaddresses()
|
||||
|
||||
for addr_type, addr in types_and_addresses:
|
||||
res = self.nodes[0].z_validateaddress(addr)
|
||||
assert(res['isvalid'])
|
||||
assert(res['ismine'])
|
||||
assert_equal(res['type'], addr_type)
|
||||
assert(addr in all_addresses)
|
||||
|
||||
listed_addresses = self.nodes[0].listaddresses()
|
||||
legacy_random_src = next(src for src in listed_addresses if src['source'] == 'legacy_random')
|
||||
legacy_hdseed_src = next(src for src in listed_addresses if src['source'] == 'legacy_hdseed')
|
||||
for addr_type, addr in types_and_addresses:
|
||||
if addr_type == 'sprout':
|
||||
assert(addr in legacy_random_src['sprout']['addresses'])
|
||||
if addr_type == 'sapling':
|
||||
assert(addr in [x for obj in legacy_hdseed_src['sapling'] for x in obj['addresses']])
|
||||
|
||||
# Sanity-check the test harness
|
||||
assert_equal(self.nodes[0].getblockcount(), 200)
|
||||
|
||||
# Current height = 200 -> Sapling
|
||||
# Default address type is Sapling
|
||||
print("Testing height 200 (Sapling)")
|
||||
addr_checks('sapling')
|
||||
|
||||
self.nodes[0].generate(1)
|
||||
def setup_network(self):
|
||||
self.nodes = start_nodes(
|
||||
self.num_nodes, self.options.tmpdir,
|
||||
extra_args=[['-experimentalfeatures', '-orchardwallet', nuparams(NU5_BRANCH_ID, 2),]] * self.num_nodes)
|
||||
connect_nodes_bi(self.nodes, 0, 1)
|
||||
self.is_network_split = False
|
||||
self.sync_all()
|
||||
|
||||
# Current height = 201 -> Sapling
|
||||
# Default address type is Sapling
|
||||
print("Testing height 201 (Sapling)")
|
||||
addr_checks('sapling')
|
||||
def run_test(self):
|
||||
print("Testing height 1 (Sapling)")
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
assert_equal(self.nodes[0].getblockcount(), 1)
|
||||
listed_addresses = self.nodes[0].listaddresses()
|
||||
# There should be a single address from the coinbase
|
||||
assert len(listed_addresses) > 0
|
||||
assert_equal(listed_addresses[0]['source'], 'legacy_random')
|
||||
assert 'transparent' in listed_addresses[0]
|
||||
|
||||
taddr_import = self.nodes[1].getnewaddress()
|
||||
self.nodes[0].importaddress(taddr_import)
|
||||
listed_addresses = self.nodes[0].listaddresses()
|
||||
imported_watchonly_src = next(src for src in listed_addresses if src['source'] == 'imported_watchonly')
|
||||
assert_equal(imported_watchonly_src['transparent']['addresses'][0], taddr_import)
|
||||
|
||||
account = self.nodes[0].z_getnewaccount()['account']
|
||||
types_and_addresses = [
|
||||
('sprout', self.nodes[0].z_getnewaddress('sprout')),
|
||||
('sapling', self.nodes[0].z_getnewaddress('sapling')),
|
||||
('unified', self.nodes[0].z_getaddressforaccount(account)['unifiedaddress']),
|
||||
]
|
||||
|
||||
for addr_type, addr in types_and_addresses:
|
||||
res = self.nodes[0].z_validateaddress(addr)
|
||||
assert res['isvalid']
|
||||
# assert res['ismine'] # this isn't present for unified addresses
|
||||
assert_equal(res['type'], addr_type)
|
||||
|
||||
listed_addresses = self.nodes[0].listaddresses()
|
||||
legacy_random_src = next(src for src in listed_addresses if src['source'] == 'legacy_random')
|
||||
legacy_hdseed_src = next(src for src in listed_addresses if src['source'] == 'legacy_hdseed')
|
||||
mnemonic_seed_src = next(src for src in listed_addresses if src['source'] == 'mnemonic_seed')
|
||||
for addr_type, addr in types_and_addresses:
|
||||
if addr_type == 'sprout':
|
||||
assert addr in legacy_random_src['sprout']['addresses']
|
||||
if addr_type == 'sapling':
|
||||
assert addr in [x for obj in legacy_hdseed_src['sapling'] for x in obj['addresses']]
|
||||
assert_equal(legacy_hdseed_src['sapling'][0]['zip32KeyPath'], "m/32'/1'/2147483647'/0'")
|
||||
if addr_type == 'unified':
|
||||
unified_obj = mnemonic_seed_src['unified']
|
||||
assert_equal(unified_obj[0]['account'], 0)
|
||||
assert_equal(unified_obj[0]['addresses'][0]['address'], addr)
|
||||
assert 'diversifier_index' in unified_obj[0]['addresses'][0]
|
||||
assert_equal(unified_obj[0]['addresses'][0]['receiver_types'], ['p2pkh', 'sapling', 'orchard'])
|
||||
|
||||
print("Testing height 2 (NU5)")
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
assert_equal(self.nodes[0].getblockcount(), 2)
|
||||
# Sprout address generation is no longer allowed
|
||||
types_and_addresses = [
|
||||
('sapling', self.nodes[0].z_getnewaddress('sapling')),
|
||||
('unified', self.nodes[0].z_getaddressforaccount(account)['unifiedaddress']),
|
||||
]
|
||||
|
||||
for addr_type, addr in types_and_addresses:
|
||||
res = self.nodes[0].z_validateaddress(addr)
|
||||
assert res['isvalid']
|
||||
# assert res['ismine'] # this isn't present for unified addresses
|
||||
assert_equal(res['type'], addr_type)
|
||||
|
||||
listed_addresses = self.nodes[0].listaddresses()
|
||||
legacy_random_src = next(src for src in listed_addresses if src['source'] == 'legacy_random')
|
||||
legacy_hdseed_src = next(src for src in listed_addresses if src['source'] == 'legacy_hdseed')
|
||||
for addr_type, addr in types_and_addresses:
|
||||
if addr_type == 'sapling':
|
||||
assert addr in [x for obj in legacy_hdseed_src['sapling'] for x in obj['addresses']]
|
||||
|
||||
print("Generate mature coinbase, spend to create and detect change")
|
||||
self.nodes[0].generate(100)
|
||||
self.sync_all()
|
||||
self.nodes[0].sendmany('', {taddr_import: 1})
|
||||
listed_addresses = self.nodes[0].listaddresses()
|
||||
legacy_random_src = next(src for src in listed_addresses if src['source'] == 'legacy_random')
|
||||
assert len(legacy_random_src['transparent']['changeAddresses']) > 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
WalletAddressesTest().main()
|
||||
|
|
|
@ -346,7 +346,7 @@ UniValue listaddresses(const UniValue& params, bool fHelp)
|
|||
"\nResult:\n"
|
||||
"[\n"
|
||||
" {\n"
|
||||
" \"source\": \"imported|imported_watchonly|keypool|legacy_seed|mnemonic_seed\"\n"
|
||||
" \"source\": \"imported|imported_watchonly|legacy_random|legacy_seed|mnemonic_seed\"\n"
|
||||
" \"transparent\": {\n"
|
||||
" \"addresses\": [\"t14oHp2v54vfmdgQ3v3SNuQga8JKHTNi2a1\", ...],\n"
|
||||
" \"changeAddresses\": [\"t14oHp2v54vfmdgQ3v3SNuQga8JKHTNi2a1\", ...]\n"
|
||||
|
@ -358,16 +358,35 @@ UniValue listaddresses(const UniValue& params, bool fHelp)
|
|||
" {\n"
|
||||
" \"zip32KeyPath\": \"m/32'/133'/0'\", -- optional field, not present for imported/watchonly sources,\n"
|
||||
" \"addresses\": [\n"
|
||||
" \"ztbx5DLDxa5ZLFTchHhoPNkKs57QzSyib6UqXpEdy76T1aUdFxJt1w9318Z8DJ73XzbnWHKEZP9Yjg712N5kMmP4QzS9iC9\",\n"
|
||||
" \"zs1z7rejlpsa98s2rrrfkwmaxu53e4ue0ulcrw0h4x5g8jl04tak0d3mm47vdtahatqrlkngh9slya\",\n"
|
||||
" ...\n"
|
||||
" ]\n"
|
||||
" },\n"
|
||||
" ...\n"
|
||||
" ]\n"
|
||||
" ],\n"
|
||||
" \"unified\": [ -- each element in this list represents a set of diversified Unified Addresses derived from a single UFVK.\n"
|
||||
" {\n"
|
||||
" \"account\": 0,\n"
|
||||
" \"seedfp\": \"hexstring\",\n"
|
||||
" \"addresses\": [\n"
|
||||
" {\n"
|
||||
" \"diversifier_index\": 0,\n"
|
||||
" \"receiver_types\": [\n"
|
||||
" \"sapling\",\n"
|
||||
" ...\n"
|
||||
" ],\n"
|
||||
" \"address\": \"...\"\n"
|
||||
" },\n"
|
||||
" ...\n"
|
||||
" ]\n"
|
||||
" },\n"
|
||||
" ...\n"
|
||||
" ],\n"
|
||||
" ...\n"
|
||||
" },\n"
|
||||
" ...\n"
|
||||
"]\n"
|
||||
"In the case that a source does not have addresses for a pool, the key\n"
|
||||
"]"
|
||||
"\nIn the case that a source does not have addresses for a pool, the key\n"
|
||||
"associated with that pool will be absent.\n"
|
||||
"\nExamples:\n"
|
||||
+ HelpExampleCli("listaddresses", "")
|
||||
|
@ -387,7 +406,29 @@ UniValue listaddresses(const UniValue& params, bool fHelp)
|
|||
// Get the CTxDestination values for all the entries in the transparent address book.
|
||||
// This will include any address that has been generated by this wallet.
|
||||
for (const std::pair<CTxDestination, CAddressBookData>& item : pwalletMain->mapAddressBook) {
|
||||
t_generated_dests.insert(item.first);
|
||||
std::optional<PaymentAddressSource> source;
|
||||
std::visit(match {
|
||||
[&](const CKeyID& addr) {
|
||||
source = GetSourceForPaymentAddress(pwalletMain)(addr);
|
||||
},
|
||||
[&](const CScriptID& addr) {
|
||||
source = GetSourceForPaymentAddress(pwalletMain)(addr);
|
||||
},
|
||||
[&](const CNoDestination& addr) {}
|
||||
}, item.first);
|
||||
if (source.has_value()) {
|
||||
switch (source.value()) {
|
||||
case PaymentAddressSource::Random:
|
||||
t_generated_dests.insert(item.first);
|
||||
break;
|
||||
case PaymentAddressSource::ImportedWatchOnly:
|
||||
t_watchonly_dests.insert(item.first);
|
||||
break;
|
||||
default:
|
||||
// Not going to be in the address book.
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we have every address that holds a balance. While this is likely to be redundant
|
||||
|
@ -395,17 +436,25 @@ UniValue listaddresses(const UniValue& params, bool fHelp)
|
|||
// there is not a guarantee that an externally generated address (such as one associated with
|
||||
// a future unified incoming viewing key) will have been added to the address book.
|
||||
for (const std::pair<CTxDestination, CAmount>& item : pwalletMain->GetAddressBalances()) {
|
||||
auto script = GetScriptForDestination(item.first);
|
||||
if (pwalletMain->HaveWatchOnly(script)) {
|
||||
t_watchonly_dests.insert(item.first);
|
||||
} else if (t_generated_dests.count(item.first) == 0) {
|
||||
// assume that if we didn't add the address to the addrbook
|
||||
// that it's a change address. Ideally we'd have a better way
|
||||
// of checking this by exploring the transaction graph;
|
||||
t_change_dests.insert(item.first);
|
||||
} else {
|
||||
// already accounted for in the address book
|
||||
}
|
||||
if (t_generated_dests.count(item.first) == 0 &&
|
||||
t_watchonly_dests.count(item.first) == 0) {
|
||||
std::optional<PaymentAddressSource> source;
|
||||
std::visit(match {
|
||||
[&](const CKeyID& addr) {
|
||||
source = GetSourceForPaymentAddress(pwalletMain)(addr);
|
||||
},
|
||||
[&](const CScriptID& addr) {
|
||||
source = GetSourceForPaymentAddress(pwalletMain)(addr);
|
||||
},
|
||||
[&](const CNoDestination& addr) {}
|
||||
}, item.first);
|
||||
if (source.has_value() && source.value() == PaymentAddressSource::Random) {
|
||||
// assume that if we didn't add the address to the addrbook
|
||||
// that it's a change address. Ideally we'd have a better way
|
||||
// of checking this by exploring the transaction graph;
|
||||
t_change_dests.insert(item.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// sprout addresses
|
||||
|
@ -470,8 +519,10 @@ UniValue listaddresses(const UniValue& params, bool fHelp)
|
|||
}
|
||||
}
|
||||
|
||||
// inner function that groups Sapling addresses by IVK for use in all sources
|
||||
// that can contain Sapling addresses
|
||||
// Inner function that groups Sapling addresses by IVK for use in all sources
|
||||
// that can contain Sapling addresses. Sapling components of unified addresses,
|
||||
// i.e. those that are associated with account IDs that are not the legacy account,
|
||||
// will not be included in the entry.
|
||||
auto add_sapling = [&](
|
||||
const std::set<SaplingPaymentAddress>& addrs,
|
||||
const PaymentAddressSource source,
|
||||
|
@ -484,10 +535,14 @@ UniValue listaddresses(const UniValue& params, bool fHelp)
|
|||
if (GetSourceForPaymentAddress(pwalletMain)(addr) == source) {
|
||||
SaplingIncomingViewingKey ivkRet;
|
||||
if (pwalletMain->GetSaplingIncomingViewingKey(addr, ivkRet)) {
|
||||
// Do not include any address that is associated with a unified key.
|
||||
auto ua = pwalletMain->FindUnifiedAddressByReceiver(addr);
|
||||
if (!ua.has_value()) {
|
||||
ivkAddrs[ivkRet].push_back(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
UniValue ivk_groups(UniValue::VARR);
|
||||
|
@ -598,6 +653,103 @@ UniValue listaddresses(const UniValue& params, bool fHelp)
|
|||
entry.pushKV("source", "legacy_hdseed");
|
||||
|
||||
bool hasData = add_sapling(saplingAddresses, PaymentAddressSource::LegacyHDSeed, entry);
|
||||
if (hasData) {
|
||||
ret.push_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// mnemonic seed source
|
||||
{
|
||||
UniValue entry(UniValue::VOBJ);
|
||||
entry.pushKV("source", "mnemonic_seed");
|
||||
|
||||
bool hasData = false;
|
||||
// transparent
|
||||
// For each address in pwalletMain->mapKeyMetadata, include any address
|
||||
// for which its HD keypath metadata indicates it is associated with the legacy
|
||||
// account ID. All such addresses are derived from the mnemonic seed.
|
||||
UniValue mnemonic_taddrs(UniValue::VARR);
|
||||
for (const auto& [keyId, keyMeta] : pwalletMain->mapKeyMetadata) {
|
||||
auto account = libzcash::ParseHDKeypathAccount(44, Params().BIP44CoinType(), keyMeta.hdKeypath);
|
||||
if (account.has_value() && account.value() == ZCASH_LEGACY_ACCOUNT) {
|
||||
mnemonic_taddrs.push_back(keyIO.EncodeDestination(keyId));
|
||||
hasData = true;
|
||||
}
|
||||
}
|
||||
if (hasData) {
|
||||
// This extra "level" of JSON allows attributes to be added in
|
||||
// the future in a backward-compatible way.
|
||||
UniValue mnemonic_transparent(UniValue::VOBJ);
|
||||
mnemonic_transparent.pushKV("addresses", mnemonic_taddrs);
|
||||
entry.pushKV("transparent", mnemonic_transparent);
|
||||
}
|
||||
|
||||
// sapling
|
||||
hasData |= add_sapling(saplingAddresses, PaymentAddressSource::MnemonicHDSeed, entry);
|
||||
|
||||
// unified
|
||||
// here, we want to use the information in mapUfvkAddressMetadata to report all the unified addresses
|
||||
UniValue unified_groups(UniValue::VARR);
|
||||
auto hdChain = pwalletMain->GetMnemonicHDChain();
|
||||
for (const auto& [ufvkid, addrmeta] : pwalletMain->mapUfvkAddressMetadata) {
|
||||
auto account = pwalletMain->GetUnifiedAccountId(ufvkid);
|
||||
if (account.has_value() && hdChain.has_value()) {
|
||||
// double-check that the ufvkid we get from address metadata is actually
|
||||
// associated with the mnemonic HD chain
|
||||
auto ufvkCheck = pwalletMain->mapUnifiedAccountKeys.find(
|
||||
std::make_pair(hdChain.value().GetSeedFingerprint(), account.value())
|
||||
);
|
||||
if (ufvkCheck != pwalletMain->mapUnifiedAccountKeys.end() && ufvkCheck->second == ufvkid) {
|
||||
UniValue unified_group(UniValue::VOBJ);
|
||||
unified_group.pushKV("account", uint64_t(account.value()));
|
||||
unified_group.pushKV("seedfp", hdChain.value().GetSeedFingerprint().GetHex());
|
||||
|
||||
UniValue unified_addrs(UniValue::VARR);
|
||||
auto ufvk = pwalletMain->GetUnifiedFullViewingKey(ufvkid).value();
|
||||
for (const auto& [j, receiverTypes] : addrmeta.GetKnownReceiverSetsByDiversifierIndex()) {
|
||||
// We know we can use std::get here safely because we previously
|
||||
// generated a valid address for this diversifier & set of
|
||||
// receiver types.
|
||||
UniValue addrEntry(UniValue::VOBJ);
|
||||
auto addr = std::get<std::pair<libzcash::UnifiedAddress, diversifier_index_t>>(
|
||||
ufvk.Address(j, receiverTypes)
|
||||
);
|
||||
UniValue receiverTypesEntry(UniValue::VARR);
|
||||
for (auto t : receiverTypes) {
|
||||
switch(t) {
|
||||
case ReceiverType::P2PKH:
|
||||
receiverTypesEntry.push_back("p2pkh");
|
||||
break;
|
||||
case ReceiverType::P2SH:
|
||||
receiverTypesEntry.push_back("p2sh");
|
||||
break;
|
||||
case ReceiverType::Sapling:
|
||||
receiverTypesEntry.push_back("sapling");
|
||||
break;
|
||||
case ReceiverType::Orchard:
|
||||
receiverTypesEntry.push_back("orchard");
|
||||
break;
|
||||
}
|
||||
}
|
||||
{
|
||||
UniValue jVal;
|
||||
jVal.setNumStr(ArbitraryIntStr(std::vector(j.begin(), j.end())));
|
||||
addrEntry.pushKV("diversifier_index", jVal);
|
||||
}
|
||||
addrEntry.pushKV("receiver_types", receiverTypesEntry);
|
||||
addrEntry.pushKV("address", keyIO.EncodePaymentAddress(addr.first));
|
||||
unified_addrs.push_back(addrEntry);
|
||||
}
|
||||
unified_group.pushKV("addresses", unified_addrs);
|
||||
unified_groups.push_back(unified_group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!unified_groups.empty()) {
|
||||
entry.pushKV("unified", unified_groups);
|
||||
hasData = true;
|
||||
}
|
||||
|
||||
if (hasData) {
|
||||
ret.push_back(entry);
|
||||
|
|
Loading…
Reference in New Issue