Add correct selection of change addresses to z_sendmany

This also alters `TransactionBuilder::SendChangeTo` to take a
`libzcash::ChangeAddress` value and thus avoids the invalid
t-addr case of CTxDestination.
This commit is contained in:
Kris Nuttycombe 2022-01-19 17:00:13 -07:00
parent 992a47103d
commit c21ffff790
21 changed files with 320 additions and 217 deletions

19
Cargo.lock generated
View File

@ -472,7 +472,7 @@ checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]]
name = "equihash"
version = "0.1.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=7801dddf35ca247345cf4f5c5e48791297cad531#7801dddf35ca247345cf4f5c5e48791297cad531"
source = "git+https://github.com/zcash/librustzcash.git?rev=a01290869a4f0c4e05bdcca550795ea0e76e8550#a01290869a4f0c4e05bdcca550795ea0e76e8550"
dependencies = [
"blake2b_simd 1.0.0",
"byteorder",
@ -481,7 +481,7 @@ dependencies = [
[[package]]
name = "f4jumble"
version = "0.0.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=7801dddf35ca247345cf4f5c5e48791297cad531#7801dddf35ca247345cf4f5c5e48791297cad531"
source = "git+https://github.com/zcash/librustzcash.git?rev=a01290869a4f0c4e05bdcca550795ea0e76e8550#a01290869a4f0c4e05bdcca550795ea0e76e8550"
dependencies = [
"blake2b_simd 1.0.0",
]
@ -1841,10 +1841,9 @@ dependencies = [
[[package]]
name = "zcash_address"
version = "0.0.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=7801dddf35ca247345cf4f5c5e48791297cad531#7801dddf35ca247345cf4f5c5e48791297cad531"
source = "git+https://github.com/zcash/librustzcash.git?rev=a01290869a4f0c4e05bdcca550795ea0e76e8550#a01290869a4f0c4e05bdcca550795ea0e76e8550"
dependencies = [
"bech32",
"blake2b_simd 1.0.0",
"bs58",
"f4jumble",
"zcash_encoding",
@ -1853,7 +1852,7 @@ dependencies = [
[[package]]
name = "zcash_encoding"
version = "0.0.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=7801dddf35ca247345cf4f5c5e48791297cad531#7801dddf35ca247345cf4f5c5e48791297cad531"
source = "git+https://github.com/zcash/librustzcash.git?rev=a01290869a4f0c4e05bdcca550795ea0e76e8550#a01290869a4f0c4e05bdcca550795ea0e76e8550"
dependencies = [
"byteorder",
"nonempty",
@ -1862,7 +1861,7 @@ dependencies = [
[[package]]
name = "zcash_history"
version = "0.2.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=7801dddf35ca247345cf4f5c5e48791297cad531#7801dddf35ca247345cf4f5c5e48791297cad531"
source = "git+https://github.com/zcash/librustzcash.git?rev=a01290869a4f0c4e05bdcca550795ea0e76e8550#a01290869a4f0c4e05bdcca550795ea0e76e8550"
dependencies = [
"bigint",
"blake2b_simd 1.0.0",
@ -1872,7 +1871,7 @@ dependencies = [
[[package]]
name = "zcash_note_encryption"
version = "0.1.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=7801dddf35ca247345cf4f5c5e48791297cad531#7801dddf35ca247345cf4f5c5e48791297cad531"
source = "git+https://github.com/zcash/librustzcash.git?rev=a01290869a4f0c4e05bdcca550795ea0e76e8550#a01290869a4f0c4e05bdcca550795ea0e76e8550"
dependencies = [
"chacha20",
"chacha20poly1305",
@ -1883,7 +1882,7 @@ dependencies = [
[[package]]
name = "zcash_primitives"
version = "0.5.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=7801dddf35ca247345cf4f5c5e48791297cad531#7801dddf35ca247345cf4f5c5e48791297cad531"
source = "git+https://github.com/zcash/librustzcash.git?rev=a01290869a4f0c4e05bdcca550795ea0e76e8550#a01290869a4f0c4e05bdcca550795ea0e76e8550"
dependencies = [
"aes",
"bip0039",
@ -1901,11 +1900,9 @@ dependencies = [
"incrementalmerkletree",
"jubjub",
"lazy_static",
"log",
"memuse",
"nonempty",
"orchard",
"pasta_curves",
"rand",
"rand_core 0.6.3",
"sha2",
@ -1917,7 +1914,7 @@ dependencies = [
[[package]]
name = "zcash_proofs"
version = "0.5.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=7801dddf35ca247345cf4f5c5e48791297cad531#7801dddf35ca247345cf4f5c5e48791297cad531"
source = "git+https://github.com/zcash/librustzcash.git?rev=a01290869a4f0c4e05bdcca550795ea0e76e8550#a01290869a4f0c4e05bdcca550795ea0e76e8550"
dependencies = [
"bellman",
"blake2b_simd 1.0.0",

View File

@ -71,8 +71,8 @@ panic = 'abort'
codegen-units = 1
[patch.crates-io]
zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "7801dddf35ca247345cf4f5c5e48791297cad531" }
zcash_history = { git = "https://github.com/zcash/librustzcash.git", rev = "7801dddf35ca247345cf4f5c5e48791297cad531" }
zcash_note_encryption = { git = "https://github.com/zcash/librustzcash.git", rev = "7801dddf35ca247345cf4f5c5e48791297cad531" }
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "7801dddf35ca247345cf4f5c5e48791297cad531" }
zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "7801dddf35ca247345cf4f5c5e48791297cad531" }
zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "a01290869a4f0c4e05bdcca550795ea0e76e8550" }
zcash_history = { git = "https://github.com/zcash/librustzcash.git", rev = "a01290869a4f0c4e05bdcca550795ea0e76e8550" }
zcash_note_encryption = { git = "https://github.com/zcash/librustzcash.git", rev = "a01290869a4f0c4e05bdcca550795ea0e76e8550" }
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "a01290869a4f0c4e05bdcca550795ea0e76e8550" }
zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "a01290869a4f0c4e05bdcca550795ea0e76e8550" }

View File

@ -268,17 +268,6 @@ TEST(TransactionBuilder, RejectsInvalidTransparentOutput)
ASSERT_THROW(builder.AddTransparentOutput(taddr, 50), UniValue);
}
TEST(TransactionBuilder, RejectsInvalidTransparentChangeAddress)
{
SelectParams(CBaseChainParams::REGTEST);
const Consensus::Params& consensusParams = Params().GetConsensus();
// Default CTxDestination type is an invalid address
CTxDestination taddr;
auto builder = TransactionBuilder(consensusParams, 1);
ASSERT_THROW(builder.SendChangeTo(taddr), UniValue);
}
TEST(TransactionBuilder, FailsWithNegativeChange)
{
auto consensusParams = RegtestActivateSapling();
@ -344,7 +333,6 @@ TEST(TransactionBuilder, ChangeOutput)
CKey tsk = AddTestCKeyToKeyStore(keystore);
auto tkeyid = tsk.GetPubKey().GetID();
auto scriptPubKey = GetScriptForDestination(tkeyid);
CTxDestination taddr = tkeyid;
// No change address and no Sapling spends
{
@ -387,7 +375,7 @@ TEST(TransactionBuilder, ChangeOutput)
{
auto builder = TransactionBuilder(consensusParams, 1, &keystore);
builder.AddTransparentInput(COutPoint(), scriptPubKey, 25000);
builder.SendChangeTo(taddr);
builder.SendChangeTo(tkeyid, {});
auto tx = builder.Build().GetTxOrThrow();
EXPECT_EQ(tx.vin.size(), 1);

View File

@ -306,6 +306,17 @@ extern "C" {
unsigned char *xfvk_i
);
/**
* Derive the Sapling FVK for an internal BIP44 chain from the
* corresponding external chain FVK.
*/
void librustzcash_zip32_sapling_derive_internal_fvk(
const unsigned char *fvk,
const unsigned char *dk,
unsigned char *fvk_ret,
unsigned char *dk_ret
);
/**
* Derive a PaymentAddress from a (SaplingFullViewingKey, DiversifierKey)
* pair. Returns 'false' if no valid address can be derived for the

View File

@ -55,7 +55,7 @@ use zcash_primitives::{
},
sapling::{merkle_hash, spend_sig},
transaction::components::Amount,
zip32::{self, sapling_address, sapling_find_address},
zip32::{self, sapling_address, sapling_derive_internal_fvk, sapling_find_address},
};
use zcash_proofs::{
circuit::sapling::TREE_DEPTH as SAPLING_TREE_DEPTH,
@ -1081,6 +1081,25 @@ pub extern "C" fn librustzcash_zip32_xfvk_derive(
true
}
/// Derive the Sapling internal
#[no_mangle]
pub extern "C" fn librustzcash_zip32_sapling_derive_internal_fvk(
fvk: *const [c_uchar; 96],
dk: *const [c_uchar; 32],
fvk_ret: *mut [c_uchar; 96],
dk_ret: *mut [c_uchar; 32],
) {
let fvk = FullViewingKey::read(&unsafe { *fvk }[..]).expect("valid Sapling FullViewingKey");
let dk = zip32::DiversifierKey(unsafe { *dk });
let (fvk_internal, dk_internal) = sapling_derive_internal_fvk(&fvk, &dk);
let fvk_ret = unsafe { &mut *fvk_ret };
let dk_ret = unsafe { &mut *dk_ret };
fvk_ret.copy_from_slice(&fvk_internal.to_bytes());
dk_ret.copy_from_slice(&dk_internal.0);
}
/// Derive a PaymentAddress from an ExtendedFullViewingKey.
#[no_mangle]
pub extern "C" fn librustzcash_zip32_sapling_address(

View File

@ -280,29 +280,31 @@ void TransactionBuilder::SetFee(CAmount fee)
this->fee = fee;
}
void TransactionBuilder::SendChangeTo(libzcash::SaplingPaymentAddress changeAddr, uint256 ovk)
{
saplingChangeAddr = std::make_pair(ovk, changeAddr);
sproutChangeAddr = std::nullopt;
// TODO: remove support for transparent change?
void TransactionBuilder::SendChangeTo(
const libzcash::RecipientAddress& changeAddr,
const uint256& ovk) {
tChangeAddr = std::nullopt;
}
void TransactionBuilder::SendChangeTo(libzcash::SproutPaymentAddress changeAddr)
{
sproutChangeAddr = changeAddr;
saplingChangeAddr = std::nullopt;
tChangeAddr = std::nullopt;
}
void TransactionBuilder::SendChangeTo(CTxDestination& changeAddr)
{
if (!IsValidDestination(changeAddr)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid change address, not a valid taddr.");
}
tChangeAddr = changeAddr;
saplingChangeAddr = std::nullopt;
sproutChangeAddr = std::nullopt;
std::visit(match {
[&](const CKeyID& keyId) {
tChangeAddr = keyId;
},
[&](const CScriptID& scriptId) {
tChangeAddr = scriptId;
},
[&](const libzcash::SaplingPaymentAddress& changeDest) {
saplingChangeAddr = std::make_pair(ovk, changeDest);
}
}, changeAddr);
}
void TransactionBuilder::SendChangeToSprout(const libzcash::SproutPaymentAddress& zaddr) {
tChangeAddr = std::nullopt;
saplingChangeAddr = std::nullopt;
sproutChangeAddr = zaddr;
}
TransactionBuilderResult TransactionBuilder::Build()

View File

@ -13,6 +13,7 @@
#include "script/script.h"
#include "script/standard.h"
#include "uint256.h"
#include "util/match.h"
#include "zcash/Address.hpp"
#include "zcash/IncrementalMerkleTree.hpp"
#include "zcash/JoinSplit.hpp"
@ -170,11 +171,8 @@ public:
void AddTransparentOutput(const CTxDestination& to, CAmount value);
void SendChangeTo(libzcash::SaplingPaymentAddress changeAddr, uint256 ovk);
void SendChangeTo(libzcash::SproutPaymentAddress);
void SendChangeTo(CTxDestination& changeAddr);
void SendChangeTo(const libzcash::RecipientAddress& changeAddr, const uint256& ovk);
void SendChangeToSprout(const libzcash::SproutPaymentAddress& changeAddr);
TransactionBuilderResult Build();

View File

@ -6,6 +6,8 @@
extern UniValue signrawtransaction(const UniValue& params, bool fHelp);
// TODO: instead of passing a TestMode flag, tests should override `CommitTransaction`
// on the wallet.
UniValue SendTransaction(CTransaction& tx, std::optional<std::reference_wrapper<CReserveKey>> reservekey, bool testmode) {
UniValue o(UniValue::VOBJ);
// Send the transaction

View File

@ -60,31 +60,6 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
assert(!recipients_.empty());
assert(ztxoSelector.RequireSpendingKeys());
std::visit(match {
[&](const AccountZTXOPattern& acct) {
isfromtaddr_ =
acct.GetReceiverTypes().empty() ||
acct.GetReceiverTypes().count(ReceiverType::P2PKH) > 0 ||
acct.GetReceiverTypes().count(ReceiverType::P2SH) > 0;
},
[&](const CKeyID& keyId) {
isfromtaddr_ = true;
},
[&](const CScriptID& scriptId) {
isfromtaddr_ = true;
},
[&](const libzcash::SproutPaymentAddress& addr) {
isfromsprout_ = true;
},
[&](const libzcash::SaplingPaymentAddress& addr) {
isfromsapling_ = true;
}
}, ztxoSelector.GetPattern());
if ((isfromsprout_ || isfromsapling_) && minDepth == 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Minconf cannot be zero when sending from a shielded address");
}
// calculate the target totals
for (const SendManyRecipient& recipient : recipients_) {
std::visit(match {
@ -98,7 +73,7 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
},
[&](const libzcash::SaplingPaymentAddress& addr) {
txOutputAmounts_.z_outputs_total += recipient.amount;
if (isfromsprout_ && !allowRevealedAmounts_) {
if (ztxoSelector_.SelectsSprout() && !allowRevealedAmounts_) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Sending between shielded pools is not enabled by default because it will "
@ -192,10 +167,6 @@ void AsyncRPCOperation_sendmany::main() {
//
// At least 4. and 5. differ from the Rust transaction builder.
uint256 AsyncRPCOperation_sendmany::main_impl() {
// TODO UA: this check will become meaningless.
bool isfromzaddr_ = isfromsprout_ || isfromsapling_;
assert(isfromtaddr_ != isfromzaddr_);
CAmount sendAmount = txOutputAmounts_.z_outputs_total + txOutputAmounts_.t_outputs_total;
CAmount targetAmount = sendAmount + fee_;
@ -245,14 +216,6 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
spendable.LogInputs(getId());
// At least one of z_sprout_inputs_ and z_sapling_inputs_ must be empty by design
//
// TODO: This restriction is true by construction as we have no mechanism
// for filtering for notes that will select both Sprout and Sapling notes
// simultaneously, but even if we did it would likely be safe to remove
// this limitation.
assert(spendable.sproutNoteEntries.empty() || spendable.saplingNoteEntries.empty());
CAmount t_inputs_total{0};
CAmount z_inputs_total{0};
for (const auto& t : spendable.utxos) {
@ -265,25 +228,16 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
z_inputs_total += t.note.value();
}
// TODO UA: these restrictions should be removed.
assert(!isfromtaddr_ || z_inputs_total == 0);
assert(!isfromzaddr_ || t_inputs_total == 0);
if (isfromtaddr_ && (t_inputs_total < targetAmount)) {
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS,
strprintf("Insufficient transparent funds, have %s, need %s",
FormatMoney(t_inputs_total), FormatMoney(targetAmount)));
}
if (isfromzaddr_ && (z_inputs_total < targetAmount)) {
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS,
strprintf("Insufficient shielded funds, have %s, need %s",
FormatMoney(z_inputs_total), FormatMoney(targetAmount)));
if (z_inputs_total > 0 && mindepth_ == 0) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Minconf cannot be zero when sending from a shielded address");
}
// When spending transparent coinbase outputs, all inputs must be fully
// consumed, and they may only be sent to shielded recipients.
if (spendable.HasTransparentCoinbase()) {
if (t_inputs_total != targetAmount) {
if (t_inputs_total + z_inputs_total != targetAmount) {
throw JSONRPCError(
RPC_WALLET_ERROR,
strprintf(
@ -299,66 +253,55 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
}
}
if (isfromtaddr_) {
LogPrint("zrpc", "%s: spending %s to send %s with fee %s\n",
getId(), FormatMoney(targetAmount), FormatMoney(sendAmount), FormatMoney(fee_));
} else {
LogPrint("zrpcunsafe", "%s: spending %s to send %s with fee %s\n",
getId(), FormatMoney(targetAmount), FormatMoney(sendAmount), FormatMoney(fee_));
}
LogPrint("zrpcunsafe", "%s: spending %s to send %s with fee %s\n",
getId(), FormatMoney(targetAmount), FormatMoney(sendAmount), FormatMoney(fee_));
LogPrint("zrpc", "%s: transparent input: %s (to choose from)\n", getId(), FormatMoney(t_inputs_total));
LogPrint("zrpcunsafe", "%s: private input: %s (to choose from)\n", getId(), FormatMoney(z_inputs_total));
LogPrint("zrpc", "%s: transparent output: %s\n", getId(), FormatMoney(txOutputAmounts_.t_outputs_total));
LogPrint("zrpcunsafe", "%s: private output: %s\n", getId(), FormatMoney(txOutputAmounts_.z_outputs_total));
LogPrint("zrpc", "%s: fee: %s\n", getId(), FormatMoney(fee_));
CReserveKey keyChange(pwalletMain);
uint256 ovk;
auto getDefaultOVK = [&]() {
HDSeed seed = pwalletMain->GetHDSeedForRPC();
return ovkForShieldingFromTaddr(seed);
};
auto setTransparentChangeRecipient = [&]() {
LOCK2(cs_main, pwalletMain->cs_wallet);
EnsureWalletIsUnlocked();
CPubKey vchPubKey;
bool ret = keyChange.GetReservedKey(vchPubKey);
if (!ret) {
// should never fail, as we just unlocked
throw JSONRPCError(
RPC_WALLET_KEYPOOL_RAN_OUT,
"Could not generate a taddr to use as a change address");
}
CTxDestination changeAddr = vchPubKey.GetID();
builder_.SendChangeTo(changeAddr);
};
// FIXME: use the appropriate shielded pool change address for the
// source unified address account (or the legacy account), and the
// associated OVK
auto ovks = this->SelectOVKs();
auto selectorAccountId = pwalletMain->FindAccountForSelector(ztxoSelector_);
std::visit(match {
[&](const CKeyID& keyId) {
auto accountId = selectorAccountId.value_or(ZCASH_LEGACY_ACCOUNT);
builder_.SendChangeTo(
pwalletMain->GenerateChangeAddressForAccount(accountId, { libzcash::ChangeType::Sapling }).value(),
ovks.first);
},
[&](const CScriptID& scriptId) {
auto accountId = selectorAccountId.value_or(ZCASH_LEGACY_ACCOUNT);
builder_.SendChangeTo(
pwalletMain->GenerateChangeAddressForAccount(accountId, { libzcash::ChangeType::Sapling }).value(),
ovks.first);
},
[&](const libzcash::SproutPaymentAddress& addr) {
ovk = getDefaultOVK();
builder_.SendChangeTo(addr);
// for Sprout, we return change to the originating address.
builder_.SendChangeToSprout(addr);
},
[&](const libzcash::SaplingPaymentAddress& addr) {
libzcash::SaplingExtendedSpendingKey saplingKey;
assert(pwalletMain->GetSaplingExtendedSpendingKey(addr, saplingKey));
ovk = saplingKey.expsk.full_viewing_key().ovk;
builder_.SendChangeTo(addr, ovk);
// for Sapling, if using a legacy address, return change to the
// originating address; otherwise return it to the Sapling internal
// address corresponding to the UFVK.
if (selectorAccountId.has_value()) {
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
selectorAccountId.value(), { libzcash::ChangeType::Sapling }).value();
builder_.SendChangeTo(changeAddr, ovks.first);
} else {
builder_.SendChangeTo(addr, ovks.first);
}
},
[&](const auto& other) {
ovk = getDefaultOVK();
setTransparentChangeRecipient();
[&](const AccountZTXOPattern& acct) {
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
selectorAccountId.value(), { libzcash::ChangeType::Sapling });
assert(changeAddr.has_value());
builder_.SendChangeTo(changeAddr.value(), ovks.first);
}
}, ztxoSelector_.GetPattern());
// Track the total of notes that we've added to the builder
// Track the total of notes that we've added to the builder. This
// shouldn't strictly be necessary, given `spendable.LimitToAmount`
CAmount sum = 0;
// Create Sapling outpoints
@ -410,7 +353,7 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
auto value = r.amount;
auto memo = get_memo_from_hex_string(r.memo.has_value() ? r.memo.value() : "");
builder_.AddSaplingOutput(ovk, addr, value, memo);
builder_.AddSaplingOutput(ovks.second, addr, value, memo);
}
}, r.address);
}
@ -466,12 +409,20 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
auto buildResult = builder_.Build();
auto tx = buildResult.GetTxOrThrow();
UniValue sendResult = SendTransaction(tx, keyChange, testmode);
UniValue sendResult = SendTransaction(tx, std::nullopt, testmode);
set_result(sendResult);
return tx.GetHash();
}
std::pair<uint256, uint256> AsyncRPCOperation_sendmany::SelectOVKs() const {
//TODO
uint256 internalOVK;
uint256 externalOVK;
return std::make_pair(internalOVK, externalOVK);
}
/**
* Compute a dust threshold based upon a standard p2pkh txout.
*/
@ -523,4 +474,3 @@ UniValue AsyncRPCOperation_sendmany::getStatus() const {
obj.pushKV("params", contextinfo_ );
return obj;
}

View File

@ -75,13 +75,19 @@ private:
CAmount fee_;
UniValue contextinfo_; // optional data to include in return value from getStatus()
bool isfromtaddr_{false};
bool isfromsprout_{false};
bool isfromsapling_{false};
bool allowRevealedAmounts_{false};
uint32_t transparentRecipients_{0};
TxOutputAmounts txOutputAmounts_;
/**
* Compute the internal and external OVKs to use in transaction construction, given
* the payment source and the set of types that correspond to outputs selected for
* being spent in the transaction.
*/
std::pair<uint256, uint256> SelectOVKs() const;
static CAmount DefaultDustThreshold();
static std::array<unsigned char, ZC_MEMO_SIZE> get_memo_from_hex_string(std::string s);
@ -89,7 +95,6 @@ private:
uint256 main_impl();
};
// To test private methods, a friend class can act as a proxy
class TEST_FRIEND_AsyncRPCOperation_sendmany {
public:

View File

@ -258,6 +258,7 @@ bool ShieldToAddress::operator()(const libzcash::SaplingPaymentAddress &zaddr) c
// generate a common one from the HD seed. This ensures the data is
// recoverable, while keeping it logically separate from the ZIP 32
// Sapling key hierarchy, which the user might not be using.
// FIXME: update to use the ZIP-316 OVK
HDSeed seed = pwalletMain->GetHDSeedForRPC();
uint256 ovk = ovkForShieldingFromTaddr(seed);

View File

@ -2199,7 +2199,7 @@ TEST(WalletTests, GenerateUnifiedAddress) {
ua->first.GetSaplingReceiver(),
ufvk.GetSaplingKey().value().Address(ua->second));
auto u4r = wallet.GetUnifiedForReceiver(ua->first.GetSaplingReceiver().value());
auto u4r = wallet.FindUnifiedAddressByReceiver(ua->first.GetSaplingReceiver().value());
EXPECT_EQ(u4r, ua->first);
// Explicitly trigger the invalid transparent child index failure

View File

@ -4044,7 +4044,7 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp)
// If the note belongs to a Sapling address that is part of an account in the
// wallet, show the corresponding Unified Address.
std::string address;
const auto ua = pwalletMain->GetUnifiedForReceiver(pa);
const auto ua = pwalletMain->FindUnifiedAddressByReceiver(pa);
if (ua.has_value()) {
address = keyIO.EncodePaymentAddress(ua.value());
} else {
@ -4097,7 +4097,7 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp)
// If the note belongs to a Sapling address that is part of an account in the
// wallet, show the corresponding Unified Address.
std::string address;
const auto ua = pwalletMain->GetUnifiedForReceiver(pa);
const auto ua = pwalletMain->FindUnifiedAddressByReceiver(pa);
if (ua.has_value()) {
address = keyIO.EncodePaymentAddress(ua.value());
} else {

View File

@ -536,7 +536,7 @@ bool CWallet::AddUnifiedFullViewingKey(const libzcash::UnifiedFullViewingKey &uf
return CWalletDB(strWalletFile).WriteUnifiedFullViewingKey(ufvk);
}
std::optional<ZcashdUnifiedFullViewingKey> CWallet::GetUnifiedFullViewingKeyByAccount(libzcash::AccountId accountId) {
std::optional<ZcashdUnifiedFullViewingKey> CWallet::GetUnifiedFullViewingKeyByAccount(libzcash::AccountId accountId) const {
if (!mnemonicHDChain.has_value()) {
throw std::runtime_error(
"CWallet::GetUnifiedFullViewingKeyByAccount(): Wallet is missing mnemonic seed metadata.");
@ -1546,6 +1546,22 @@ bool CWallet::SelectorMatchesAddress(
}, selector.GetPattern());
}
std::optional<RecipientAddress> CWallet::GenerateChangeAddressForAccount(
libzcash::AccountId accountId,
std::set<libzcash::ChangeType> changeOptions) {
auto ufvk = this->GetUnifiedFullViewingKeyByAccount(accountId);
if (ufvk.has_value()) {
for (libzcash::ChangeType t : changeOptions) {
if (t == libzcash::ChangeType::Transparent && accountId == ZCASH_LEGACY_ACCOUNT) {
return GenerateNewKey().GetID();
} else {
return ufvk.value().GetChangeAddress(changeOptions);
}
}
}
return std::nullopt;
}
SpendableInputs CWallet::FindSpendableInputs(
ZTXOSelector selector,
bool allowTransparentCoinbase,
@ -5980,8 +5996,12 @@ std::optional<libzcash::AccountId> CWallet::GetUnifiedAccountId(const libzcash::
}
}
std::optional<UnifiedAddress> CWallet::GetUnifiedForReceiver(const Receiver& receiver) const {
return std::visit(LookupUnifiedAddress(*this), receiver);
std::optional<ZcashdUnifiedFullViewingKey> CWallet::FindUFVKByReceiver(const libzcash::Receiver& receiver) const {
return std::visit(UFVKForReceiver(*this), receiver);
}
std::optional<UnifiedAddress> CWallet::FindUnifiedAddressByReceiver(const Receiver& receiver) const {
return std::visit(UnifiedAddressForReceiver(*this), receiver);
}
//
@ -6213,7 +6233,6 @@ KeyAddResult AddViewingKeyToWallet::operator()(const libzcash::SaplingExtendedFu
return KeyNotAdded;
}
}
KeyAddResult AddViewingKeyToWallet::operator()(const libzcash::UnifiedFullViewingKey& no) const {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unified full viewing key import is not yet supported.");
}
@ -6271,14 +6290,53 @@ KeyAddResult AddSpendingKeyToWallet::operator()(const libzcash::SaplingExtendedS
}
}
std::optional<libzcash::UnifiedAddress> LookupUnifiedAddress::operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const {
// UFVKForReceiver :: (CWallet&, Receiver) -> std::optional<ZcashdUnifiedFullViewingKey>
std::optional<libzcash::ZcashdUnifiedFullViewingKey> UFVKForReceiver::operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const {
auto ufvkPair = wallet.GetUFVKMetadataForReceiver(saplingAddr);
if (ufvkPair.has_value()) {
auto ufvkid = ufvkPair.value().first;
auto ufvk = wallet.GetUnifiedFullViewingKey(ufvkid);
if (!(ufvk.has_value() && ufvk.value().GetSaplingKey().has_value())) {
throw std::runtime_error("CWallet::LookupUnifiedAddress(): UFVK has no Sapling key part.");
}
// If we have UFVK metadata, `GetUnifiedFullViewingKey` should always
// return a non-nullopt value, and since we obtained that metadata by
// lookup from as Sapling address, it should have a Sapling key component.
assert(ufvk.has_value() && ufvk.value().GetSaplingKey().has_value());
return ufvk.value();
} else {
return std::nullopt;
}
}
std::optional<libzcash::ZcashdUnifiedFullViewingKey> UFVKForReceiver::operator()(const CScriptID& scriptId) const {
// We do not currently generate unified addresses containing P2SH components,
// so there's nothing to look up here.
return std::nullopt;
}
std::optional<libzcash::ZcashdUnifiedFullViewingKey> UFVKForReceiver::operator()(const CKeyID& keyId) const {
auto ufvkPair = wallet.GetUFVKMetadataForReceiver(keyId);
if (ufvkPair.has_value()) {
auto ufvkid = ufvkPair.value().first;
// transparent address UFVK metadata is always accompanied by the child
// index at which the address was produced
assert(ufvkPair.value().second.has_value());
auto ufvk = wallet.GetUnifiedFullViewingKey(ufvkid);
assert(ufvk.has_value() && ufvk.value().GetTransparentKey().has_value());
return ufvk.value();
} else {
return std::nullopt;
}
}
std::optional<libzcash::ZcashdUnifiedFullViewingKey> UFVKForReceiver::operator()(const libzcash::UnknownReceiver& receiver) const {
return std::nullopt;
}
// UnifiedAddressForReceiver :: (CWallet&, Receiver) -> std::optional<UnifiedAddress>
std::optional<libzcash::UnifiedAddress> UnifiedAddressForReceiver::operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const {
auto ufvkPair = wallet.GetUFVKMetadataForReceiver(saplingAddr);
if (ufvkPair.has_value()) {
auto ufvkid = ufvkPair.value().first;
auto ufvk = wallet.GetUnifiedFullViewingKey(ufvkid);
assert(ufvk.has_value() && ufvk.value().GetSaplingKey().has_value());
diversifier_index_t j;
// If the wallet is missing metadata at this UFVK id, it is probably
@ -6300,10 +6358,12 @@ std::optional<libzcash::UnifiedAddress> LookupUnifiedAddress::operator()(const l
return std::nullopt;
}
}
std::optional<libzcash::UnifiedAddress> LookupUnifiedAddress::operator()(const CScriptID& scriptId) const {
std::optional<libzcash::UnifiedAddress> UnifiedAddressForReceiver::operator()(const CScriptID& scriptId) const {
// We do not currently generate unified addresses containing P2SH components,
// so there's nothing to look up here.
return std::nullopt;
}
std::optional<libzcash::UnifiedAddress> LookupUnifiedAddress::operator()(const CKeyID& keyId) const {
std::optional<libzcash::UnifiedAddress> UnifiedAddressForReceiver::operator()(const CKeyID& keyId) const {
auto ufvkPair = wallet.GetUFVKMetadataForReceiver(keyId);
if (ufvkPair.has_value()) {
auto ufvkid = ufvkPair.value().first;
@ -6313,7 +6373,7 @@ std::optional<libzcash::UnifiedAddress> LookupUnifiedAddress::operator()(const C
diversifier_index_t j = ufvkPair.value().second.value();
auto ufvk = wallet.GetUnifiedFullViewingKey(ufvkid);
if (!(ufvk.has_value() && ufvk.value().GetTransparentKey().has_value())) {
throw std::runtime_error("CWallet::LookupUnifiedAddress(): UFVK has no P2PKH key part.");
throw std::runtime_error("CWallet::UnifiedAddressForReceiver(): UFVK has no P2PKH key part.");
}
// If the wallet is missing metadata at this UFVK id, it is probably
@ -6333,7 +6393,7 @@ std::optional<libzcash::UnifiedAddress> LookupUnifiedAddress::operator()(const C
return std::nullopt;
}
}
std::optional<libzcash::UnifiedAddress> LookupUnifiedAddress::operator()(const libzcash::UnknownReceiver& receiver) const {
std::optional<libzcash::UnifiedAddress> UnifiedAddressForReceiver::operator()(const libzcash::UnknownReceiver& receiver) const {
return std::nullopt;
}

View File

@ -1256,6 +1256,10 @@ public:
*/
std::optional<libzcash::AccountId> FindAccountForSelector(const ZTXOSelector& paymentSource) const;
std::optional<libzcash::RecipientAddress> GenerateChangeAddressForAccount(
libzcash::AccountId accountId,
std::set<libzcash::ChangeType> changeOptions);
SpendableInputs FindSpendableInputs(
ZTXOSelector paymentSource,
bool allowTransparentCoinbase,
@ -1413,7 +1417,7 @@ public:
//! Retrieves the UFVK derived from the wallet's mnemonic seed for the specified account.
std::optional<libzcash::ZcashdUnifiedFullViewingKey>
GetUnifiedFullViewingKeyByAccount(libzcash::AccountId account);
GetUnifiedFullViewingKeyByAccount(libzcash::AccountId account) const;
//! Generate a new unified address for the specified account, diversifier, and
//! set of receiver types.
@ -1433,7 +1437,9 @@ public:
std::optional<libzcash::UFVKId> FindUnifiedFullViewingKey(const libzcash::UnifiedAddress& addr) const;
std::optional<libzcash::AccountId> GetUnifiedAccountId(const libzcash::UFVKId& ufvkId) const;
std::optional<libzcash::UnifiedAddress> GetUnifiedForReceiver(const libzcash::Receiver& receiver) const;
std::optional<libzcash::ZcashdUnifiedFullViewingKey> FindUFVKByReceiver(const libzcash::Receiver& receiver) const;
std::optional<libzcash::UnifiedAddress> FindUnifiedAddressByReceiver(const libzcash::Receiver& receiver) const;
/**
* Increment the next transaction order id
@ -1857,12 +1863,25 @@ public:
KeyAddResult operator()(const libzcash::SaplingExtendedSpendingKey &sk) const;
};
class LookupUnifiedAddress {
class UFVKForReceiver {
private:
const CWallet& wallet;
public:
LookupUnifiedAddress(const CWallet& wallet): wallet(wallet) {}
UFVKForReceiver(const CWallet& wallet): wallet(wallet) {}
std::optional<libzcash::ZcashdUnifiedFullViewingKey> operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const;
std::optional<libzcash::ZcashdUnifiedFullViewingKey> operator()(const CScriptID& scriptId) const;
std::optional<libzcash::ZcashdUnifiedFullViewingKey> operator()(const CKeyID& keyId) const;
std::optional<libzcash::ZcashdUnifiedFullViewingKey> operator()(const libzcash::UnknownReceiver& receiver) const;
};
class UnifiedAddressForReceiver {
private:
const CWallet& wallet;
public:
UnifiedAddressForReceiver(const CWallet& wallet): wallet(wallet) {}
std::optional<libzcash::UnifiedAddress> operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const;
std::optional<libzcash::UnifiedAddress> operator()(const CScriptID& scriptId) const;

View File

@ -23,39 +23,6 @@ const unsigned char ZCASH_UFVK_ID_PERSONAL[BLAKE2bPersonalBytes] =
namespace libzcash {
class UnknownReceiver {
public:
uint32_t typecode;
std::vector<uint8_t> data;
UnknownReceiver(uint32_t typecode, std::vector<uint8_t> data) :
typecode(typecode), data(data) {}
friend inline bool operator==(const UnknownReceiver& a, const UnknownReceiver& b) {
return a.typecode == b.typecode && a.data == b.data;
}
friend inline bool operator<(const UnknownReceiver& a, const UnknownReceiver& b) {
// We don't know for certain the preference order of unknown receivers, but it is
// _likely_ that the higher typecode has higher preference. The exact sort order
// doesn't really matter, as unknown receivers have lower preference than known
// receivers.
return (a.typecode > b.typecode ||
(a.typecode == b.typecode && a.data < b.data));
}
};
/**
* Receivers that can appear in a Unified Address.
*
* These types are given in order of preference (as defined in ZIP 316), so that sorting
* variants by `operator<` is equivalent to sorting by preference.
*/
typedef std::variant<
SaplingPaymentAddress,
CScriptID,
CKeyID,
UnknownReceiver> Receiver;
bool HasKnownReceiverType(const Receiver& receiver);
struct ReceiverIterator {
@ -90,11 +57,6 @@ private:
size_t cur;
};
/** A recipient address to which a unified address can be resolved */
typedef std::variant<
CKeyID,
CScriptID,
libzcash::SaplingPaymentAddress> RecipientAddress;
class UnifiedAddress {
std::vector<Receiver> receivers;

View File

@ -17,6 +17,7 @@ const size_t SerializedSaplingPaymentAddressSize = 43;
const size_t SerializedSaplingFullViewingKeySize = 96;
const size_t SerializedSaplingExpandedSpendingKeySize = 96;
const size_t SerializedSaplingSpendingKeySize = 32;
const size_t SerializedSaplingDiversifierKeySize = 32;
typedef std::array<unsigned char, ZC_DIVERSIFIER_SIZE> diversifier_t;
@ -133,7 +134,7 @@ public:
SaplingExpandedSpendingKey expanded_spending_key() const;
SaplingFullViewingKey full_viewing_key() const;
// Can derive Sapling addr from default diversifier
// Can derive Sapling addr from default diversifier
SaplingPaymentAddress default_address() const;
};

View File

@ -134,3 +134,7 @@ std::optional<std::pair<UnifiedAddress, diversifier_index_t>> ZcashdUnifiedFullV
const diversifier_index_t& j) const {
return FindAddress(j, {ReceiverType::P2PKH, ReceiverType::Sapling});
}
RecipientAddress ZcashdUnifiedFullViewingKey::GetChangeAddress(const std::set<ChangeType>& changeOptions) const {
throw std::runtime_error("TODO");
}

View File

@ -7,6 +7,7 @@
#include "bip44.h"
#include "key_constants.h"
#include "script/script.h"
#include "zip32.h"
namespace libzcash {
@ -18,6 +19,21 @@ enum class ReceiverType: uint32_t {
//Orchard = 0x03
};
/** A recipient address to which a unified address can be resolved */
typedef std::variant<
CKeyID,
CScriptID,
libzcash::SaplingPaymentAddress> RecipientAddress;
/**
* An enumeration of the types of change that a transaction may
* produce.
*/
enum class ChangeType {
Sapling,
Transparent,
};
/**
* Test whether the specified list of receiver types contains a
* shielded receiver type
@ -32,7 +48,41 @@ bool HasTransparent(const std::set<ReceiverType>& receiverTypes);
class ZcashdUnifiedSpendingKey;
class UnknownReceiver {
public:
uint32_t typecode;
std::vector<uint8_t> data;
UnknownReceiver(uint32_t typecode, std::vector<uint8_t> data) :
typecode(typecode), data(data) {}
friend inline bool operator==(const UnknownReceiver& a, const UnknownReceiver& b) {
return a.typecode == b.typecode && a.data == b.data;
}
friend inline bool operator<(const UnknownReceiver& a, const UnknownReceiver& b) {
// We don't know for certain the preference order of unknown receivers, but it is
// _likely_ that the higher typecode has higher preference. The exact sort order
// doesn't really matter, as unknown receivers have lower preference than known
// receivers.
return (a.typecode > b.typecode ||
(a.typecode == b.typecode && a.data < b.data));
}
};
/**
* Receivers that can appear in a Unified Address.
*
* These types are given in order of preference (as defined in ZIP 316), so that sorting
* variants by `operator<` is equivalent to sorting by preference.
*/
typedef std::variant<
SaplingPaymentAddress,
CScriptID,
CKeyID,
UnknownReceiver> Receiver;
// prototypes for the classes handling ZIP-316 encoding (in Address.hpp)
// TODO: ZIP-316 encoding should probably be moved here
class UnifiedAddress;
class UnifiedFullViewingKey;
@ -116,6 +166,14 @@ public:
*/
std::optional<std::pair<UnifiedAddress, diversifier_index_t>> FindAddress(const diversifier_index_t& j) const;
/**
* Return the change address for this UFVK, given the provided
* set of receiver types for pools involved in this transaction.
* If the provided set is empty, return the change address
* corresponding to the most-preferred pool.
*/
RecipientAddress GetChangeAddress(const std::set<ChangeType>& changeOptions) const;
friend bool operator==(const ZcashdUnifiedFullViewingKey& a, const ZcashdUnifiedFullViewingKey& b)
{
return a.transparentKey == b.transparentKey && a.saplingKey == b.saplingKey;

View File

@ -126,6 +126,25 @@ libzcash::SaplingPaymentAddress SaplingDiversifiableFullViewingKey::DefaultAddre
}
}
libzcash::SaplingPaymentAddress SaplingDiversifiableFullViewingKey::GetChangeAddress() const {
CDataStream ss_fvk(SER_NETWORK, PROTOCOL_VERSION);
ss_fvk << fvk;
CSerializeData fvk_bytes(ss_fvk.begin(), ss_fvk.end());
SaplingDiversifiableFullViewingKey internalDFVK;
CSerializeData fvk_bytes_ret(libzcash::SerializedSaplingFullViewingKeySize);
librustzcash_zip32_sapling_derive_internal_fvk(
reinterpret_cast<unsigned char*>(fvk_bytes.data()),
dk.begin(),
reinterpret_cast<unsigned char*>(fvk_bytes_ret.data()),
internalDFVK.dk.begin());
CDataStream ss_fvk_ret(fvk_bytes_ret, SER_NETWORK, PROTOCOL_VERSION);
ss_fvk_ret >> internalDFVK.fvk;
return internalDFVK.DefaultAddress();
}
SaplingExtendedSpendingKey SaplingExtendedSpendingKey::Master(const HDSeed& seed)
{
auto rawSeed = seed.RawSeed();
@ -208,6 +227,10 @@ SaplingExtendedFullViewingKey SaplingExtendedSpendingKey::ToXFVK() const
return ret;
}
SaplingExtendedSpendingKey SaplingExtendedSpendingKey::DeriveInternalKey() const {
throw std::runtime_error("Not yet implemented");
}
HDKeyPath Zip32AccountKeyPath(
uint32_t bip44CoinType,
libzcash::AccountId accountId,

View File

@ -155,6 +155,8 @@ public:
libzcash::SaplingPaymentAddress DefaultAddress() const;
libzcash::SaplingPaymentAddress GetChangeAddress() const;
ADD_SERIALIZE_METHODS;
template <typename Stream, typename Operation>
@ -237,11 +239,12 @@ struct SaplingExtendedSpendingKey {
static std::pair<SaplingExtendedSpendingKey, HDKeyPath> ForAccount(const HDSeed& seed, uint32_t bip44CoinType, libzcash::AccountId accountId);
static std::pair<SaplingExtendedSpendingKey, HDKeyPath> Legacy(const HDSeed& seed, uint32_t bip44CoinType, uint32_t addressIndex);
SaplingExtendedSpendingKey Derive(uint32_t i) const;
SaplingExtendedFullViewingKey ToXFVK() const;
SaplingExtendedSpendingKey DeriveInternalKey() const;
friend bool operator==(const SaplingExtendedSpendingKey& a, const SaplingExtendedSpendingKey& b)
{
return a.depth == b.depth &&