update listaddresses RPC for UAs, Orchard

Co-authored-by: Kris Nuttycombe <kris@nutty.land>
This commit is contained in:
Larry Ruane 2022-03-21 14:35:19 -06:00 committed by Jack Grigg
parent 612f3e7e81
commit e575e0f217
2 changed files with 262 additions and 60 deletions

View File

@ -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()

View File

@ -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);