wallet: Implement `z_getaddressforaccount`

Closes zcash/zcash#5180.
This commit is contained in:
Jack Grigg 2022-01-13 22:43:21 +00:00
parent fe384eeb1f
commit 8617622e0d
6 changed files with 191 additions and 21 deletions

View File

@ -3,9 +3,11 @@
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://www.opensource.org/licenses/mit-license.php .
from test_framework.authproxy import JSONRPCException
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_raises_message,
start_nodes,
)
@ -26,6 +28,31 @@ class WalletAccountsTest(BitcoinTestFramework):
account1 = self.nodes[0].z_getnewaccount()
assert_equal(account1['account'], 1)
# Generate the first address for account 0.
addr0 = self.nodes[0].z_getaddressforaccount(0)
assert_equal(addr0['account'], 0)
assert_equal(set(addr0['pools']), set(['transparent', 'sapling']))
ua0 = addr0['unifiedaddress']
# We pick mnemonic phrases to ensure that we can always generate the default
# address in account 0; this is however not necessarily at diversifier index 0.
# We should be able to generate it directly and get the exact same data.
j = addr0['diversifier_index']
assert_equal(self.nodes[0].z_getaddressforaccount(0, j), addr0)
if j > 0:
# We should get an error if we generate the address at diversifier index 0.
assert_raises_message(
JSONRPCException,
'no address at diversifier index 0',
self.nodes[0].z_getaddressforaccount, 0, 0)
# The first address for account 1 is different to account 0.
addr1 = self.nodes[0].z_getaddressforaccount(1)
assert_equal(addr1['account'], 1)
assert_equal(set(addr1['pools']), set(['transparent', 'sapling']))
ua1 = addr1['unifiedaddress']
assert(ua0 != ua1)
if __name__ == '__main__':
WalletAccountsTest().main()

View File

@ -595,4 +595,30 @@ BOOST_AUTO_TEST_CASE(test_ParseArbitraryInt)
BOOST_CHECK_EQUAL((*v)[21], 0x10);
}
BOOST_AUTO_TEST_CASE(test_ArbitraryIntStr)
{
BOOST_CHECK_EQUAL(ArbitraryIntStr({}), "0");
BOOST_CHECK_EQUAL(ArbitraryIntStr({0}), "0");
BOOST_CHECK_EQUAL(ArbitraryIntStr({0, 0}), "0");
BOOST_CHECK_EQUAL(ArbitraryIntStr({0, 0, 0}), "0");
BOOST_CHECK_EQUAL(ArbitraryIntStr({0, 0, 0, 0}), "0");
BOOST_CHECK_EQUAL(ArbitraryIntStr({1}), "1");
BOOST_CHECK_EQUAL(ArbitraryIntStr({2}), "2");
BOOST_CHECK_EQUAL(ArbitraryIntStr({10}), "10");
BOOST_CHECK_EQUAL(ArbitraryIntStr({100}), "100");
BOOST_CHECK_EQUAL(ArbitraryIntStr({0xff}), "255");
BOOST_CHECK_EQUAL(ArbitraryIntStr({0x00, 0x01}), "256");
BOOST_CHECK_EQUAL(ArbitraryIntStr({0xff, 0x01}), "511");
BOOST_CHECK_EQUAL(ArbitraryIntStr({0x00, 0x02}), "512");
BOOST_CHECK_EQUAL(
ArbitraryIntStr({0xff, 0xff, 0xff, 0xff}),
"4294967295");
BOOST_CHECK_EQUAL(
ArbitraryIntStr({0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}),
"309485009821345068724781055");
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -64,6 +64,13 @@ std::string base_blob<BITS>::ToString() const
return (GetHex());
}
// Explicit instantiations for base_blob<88>
template base_blob<88>::base_blob(const std::vector<unsigned char>&);
template std::string base_blob<88>::GetHex() const;
template std::string base_blob<88>::ToString() const;
template void base_blob<88>::SetHex(const char*);
template void base_blob<88>::SetHex(const std::string&);
// Explicit instantiations for base_blob<160>
template base_blob<160>::base_blob(const std::vector<unsigned char>&);
template std::string base_blob<160>::GetHex() const;

View File

@ -528,3 +528,38 @@ std::optional<std::vector<uint8_t>> ParseArbitraryInt(const std::string& num_str
}
return result;
}
std::string ArbitraryIntStr(std::vector<uint8_t> bytes)
{
// Only serialize up to the most significant non-zero byte.
size_t end = bytes.size();
for (; end > 0 && bytes[end - 1] == 0; --end) {}
std::string result;
while (end > 0) {
// "Divide" bytes by 10.
uint16_t rem = 0;
for (int i = end - 1; i >= 0; --i) {
uint16_t tmp = rem * 256 + bytes[i];
rem = tmp % 10;
auto b = tmp / 10;
assert(b < 256);
bytes[i] = b;
}
// Write out the remainder as the next lowest digit.
result = tfm::format("%d%s", rem, result);
// If we've moved all the bits out of the MSB, drop it.
if (bytes[end - 1] == 0) {
end--;
}
}
// Handle the all-zero bytes case.
if (result.empty()) {
return "0";
} else {
return result;
}
}

View File

@ -168,5 +168,9 @@ bool ConvertBits(const O& outfn, I it, I end) {
}
std::optional<std::vector<uint8_t>> ParseArbitraryInt(const std::string& s);
/**
* Serializes the given little-endian byte iterator to a decimal string.
*/
std::string ArbitraryIntStr(std::vector<uint8_t> i);
#endif // BITCOIN_UTILSTRENCODINGS_H

View File

@ -21,12 +21,14 @@
#include "util.h"
#include "util/match.h"
#include "utilmoneystr.h"
#include "utilstrencodings.h"
#include "wallet.h"
#include "walletdb.h"
#include "primitives/transaction.h"
#include "zcbenchmarks.h"
#include "script/interpreter.h"
#include "zcash/Address.hpp"
#include "zcash/address/zip32.h"
#include "utiltime.h"
#include "asyncrpcoperation.h"
@ -3085,13 +3087,18 @@ UniValue z_getaddressforaccount(const UniValue& params, bool fHelp)
if (!fExperimentalOrchardWallet) {
throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Error: the Orchard wallet experimental extensions are disabled.");
}
int64_t account = params[0].get_int64();
if (account < 0 || account >= ZCASH_LEGACY_ACCOUNT) {
LOCK(pwalletMain->cs_wallet);
int64_t accountInt = params[0].get_int64();
if (accountInt < 0 || accountInt >= ZCASH_LEGACY_ACCOUNT) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid account number, must be 0 <= account <= (2^31)-2.");
}
// TODO: Check that the account is known to the wallet.
std::vector<uint8_t> parsed_diversifier_index;
libzcash::AccountId account = accountInt;
std::optional<libzcash::diversifier_index_t> j = std::nullopt;
if (params.size() >= 2) {
// TODO: Handle '*'
if (params[1].getType() != UniValue::VNUM) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid diversifier index, must be an unsigned integer.");
}
@ -3099,36 +3106,100 @@ UniValue z_getaddressforaccount(const UniValue& params, bool fHelp)
if (!parsed_diversifier_index_opt.has_value()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "diversifier index must be a decimal integer.");
}
parsed_diversifier_index = parsed_diversifier_index_opt.value();
auto parsed_diversifier_index = parsed_diversifier_index_opt.value();
if (parsed_diversifier_index.size() > ZC_DIVERSIFIER_SIZE) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "diversifier index is too large.");
}
} else {
// TODO get next unused diversifier index from wallet
// Extend the byte array to the correct length for diversifier_index_t.
parsed_diversifier_index.resize(ZC_DIVERSIFIER_SIZE);
j = libzcash::diversifier_index_t(parsed_diversifier_index);
}
// TODO:
// diversifier_t diversifier{};
// std::copy(parsed_diversifier_index.begin(), parsed_diversifier_index.end(), diversifier.begin());
UniValue pools(UniValue::VARR);
std::set<libzcash::ReceiverType> receivers;
if (params.size() >= 3) {
pools = params[2].get_array();
const auto& pools = params[2].get_array();
for (unsigned int i = 0; i < pools.size(); i++) {
const std::string& p = pools[i].get_str();
if (!(p == "transparent" || p == "sapling" || p == "orchard")) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "pool arguments must be \"transparent\", \"sapling\", or \"orchard\"");
if (p == "transparent") {
receivers.insert(ReceiverType::P2PKH);
} else if (p == "sapling") {
receivers.insert(ReceiverType::Sapling);
} else {
throw JSONRPCError(RPC_INVALID_PARAMETER, "pool arguments must be \"transparent\", or \"sapling\"");
}
}
} else {
// default is all
pools.push_back("transparent");
pools.push_back("sapling");
pools.push_back("orchard");
// Default is the best and second-best shielded pools, and the transparent pool.
receivers = {ReceiverType::P2PKH, ReceiverType::Sapling};
}
EnsureWalletIsUnlocked();
// Generate the first UA for this account, using the best and next-best shielded pools
// and the transparent pool.
auto res = pwalletMain->GenerateUnifiedAddress(account, receivers, j);
UniValue result(UniValue::VOBJ);
result.pushKV("account", account);
result.pushKV("diversifier_index", params[1].write());
result.pushKV("account", (uint64_t)account);
std::visit(match {
[&](std::pair<libzcash::UnifiedAddress, libzcash::diversifier_index_t> addr) {
result.pushKV("unifiedaddress", KeyIO(Params()).EncodePaymentAddress(addr.first));
UniValue j;
j.setNumStr(ArbitraryIntStr(std::vector(addr.second.begin(), addr.second.end())));
result.pushKV("diversifier_index", j);
},
[&](AddressGenerationError err) {
std::string strErr;
switch (err) {
case AddressGenerationError::NoSuchAccount:
strErr = tfm::format("Error: account %d has not been generated by z_getnewaccount.", account);
break;
case AddressGenerationError::ExistingAddressMismatch:
strErr = tfm::format(
"Error: address at diversifier index %s was already generated with different receiver types.",
params[1].getValStr());
break;
case AddressGenerationError::NoAddressForDiversifier:
strErr = tfm::format(
"Error: no address at diversifier index %s.",
ArbitraryIntStr(std::vector(j.value().begin(), j.value().end())));
break;
case AddressGenerationError::InvalidTransparentChildIndex:
strErr = tfm::format(
"Error: diversifier index %s cannot generate an address with a transparent receiver.",
ArbitraryIntStr(std::vector(j.value().begin(), j.value().end())));
break;
default:
// By construction, we will not see these errors here:
// - InvalidReceiverTypes
// - WalletEncrypted
//
// If we see these, the user either has generated many addresses, or
// was very unlucky with their mnemonic phrase generation:
// - DiversifierSpaceExhausted
strErr = tfm::format("Error: ran out of diversifier indices. Generate a new account with z_getnewaccount");
}
throw JSONRPCError(RPC_WALLET_ERROR, strErr);
},
}, res);
UniValue pools(UniValue::VARR);
for (const auto& receiver : receivers) {
switch (receiver) {
case ReceiverType::P2PKH:
pools.push_back("transparent");
break;
case ReceiverType::Sapling:
pools.push_back("sapling");
break;
default:
// Unreachable
assert(false);
}
}
result.pushKV("pools", pools);
result.pushKV("unifiedaddress", "TODO");
return result;
}