Merge pull request #6243 from sellout/extract-memo

Factor out memo parsing from asyncrpcoperation_sendmany
This commit is contained in:
Kris Nuttycombe 2022-12-08 13:07:33 -07:00 committed by GitHub
commit 26df6834ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 223 additions and 126 deletions

View File

@ -323,12 +323,14 @@ BITCOIN_CORE_H = \
wallet/asyncrpcoperation_shieldcoinbase.h \
wallet/crypter.h \
wallet/db.h \
wallet/memo.h \
wallet/orchard.h \
wallet/paymentdisclosure.h \
wallet/paymentdisclosuredb.h \
wallet/rpcwallet.h \
wallet/wallet.h \
wallet/walletdb.h \
wallet/wallet_tx_builder.h \
warnings.h \
zmq/zmqabstractnotifier.h \
zmq/zmqconfig.h\

View File

@ -254,8 +254,8 @@ TransactionBuilder::TransactionBuilder(
const Consensus::Params& consensusParams,
int nHeight,
std::optional<uint256> orchardAnchor,
CKeyStore* keystore,
CCoinsViewCache* coinsView,
const CKeyStore* keystore,
const CCoinsViewCache* coinsView,
CCriticalSection* cs_coinsView) :
consensusParams(consensusParams),
nHeight(nHeight),

View File

@ -290,8 +290,8 @@ public:
const Consensus::Params& consensusParams,
int nHeight,
std::optional<uint256> orchardAnchor,
CKeyStore* keyStore = nullptr,
CCoinsViewCache* coinsView = nullptr,
const CKeyStore* keyStore = nullptr,
const CCoinsViewCache* coinsView = nullptr,
CCriticalSection* cs_coinsView = nullptr);
// TransactionBuilder should never be copied

View File

@ -30,6 +30,7 @@
#include "zcash/IncrementalMerkleTree.hpp"
#include "miner.h"
#include "wallet/paymentdisclosuredb.h"
#include "wallet/wallet_tx_builder.h"
#include <array>
#include <iostream>
@ -46,7 +47,7 @@ using namespace libzcash;
AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
TransactionBuilder builder,
ZTXOSelector ztxoSelector,
std::vector<SendManyRecipient> recipients,
std::vector<ResolvedPayment> recipients,
int minDepth,
unsigned int anchorDepth,
TransactionStrategy strategy,
@ -64,7 +65,7 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
sendFromAccount_ = pwalletMain->FindAccountForSelector(ztxoSelector_).value_or(ZCASH_LEGACY_ACCOUNT);
// Determine the target totals and recipient pools
for (const SendManyRecipient& recipient : recipients_) {
for (const ResolvedPayment& recipient : recipients_) {
std::visit(match {
[&](const CKeyID& addr) {
transparentRecipients_ += 1;
@ -559,15 +560,14 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
builder_.AddTransparentOutput(scriptId, r.amount);
},
[&](const libzcash::SaplingPaymentAddress& addr) {
auto value = r.amount;
auto memo = get_memo_from_hex_string(r.memo.value_or(""));
builder_.AddSaplingOutput(ovks.second, addr, value, memo);
builder_.AddSaplingOutput(
ovks.second, addr, r.amount,
r.memo.has_value() ? r.memo.value().ToBytes() : Memo::NoMemo().ToBytes());
},
[&](const libzcash::OrchardRawAddress& addr) {
auto value = r.amount;
auto memo = r.memo.has_value() ? std::optional(get_memo_from_hex_string(r.memo.value())) : std::nullopt;
builder_.AddOrchardOutput(ovks.second, addr, value, memo);
builder_.AddOrchardOutput(
ovks.second, addr, r.amount,
r.memo.has_value() ? std::optional(r.memo.value().ToBytes()) : std::nullopt);
}
}, r.address);
}
@ -770,32 +770,6 @@ CAmount AsyncRPCOperation_sendmany::DefaultDustThreshold() {
return txout.GetDustThreshold(minRelayTxFee);
}
std::array<unsigned char, ZC_MEMO_SIZE> AsyncRPCOperation_sendmany::get_memo_from_hex_string(std::string s) {
// initialize to default memo (no_memo), see section 5.5 of the protocol spec
std::array<unsigned char, ZC_MEMO_SIZE> memo = {{0xF6}};
std::vector<unsigned char> rawMemo = ParseHex(s.c_str());
// If ParseHex comes across a non-hex char, it will stop but still return results so far.
size_t slen = s.length();
if (slen % 2 !=0 || (slen>0 && rawMemo.size()!=slen/2)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Memo must be in hexadecimal format");
}
if (rawMemo.size() > ZC_MEMO_SIZE) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf("Memo size of %d bytes is too big, maximum allowed is %d bytes", rawMemo.size(), ZC_MEMO_SIZE));
}
// copy vector into boost array
int lenMemo = rawMemo.size();
for (int i = 0; i < ZC_MEMO_SIZE && i < lenMemo; i++) {
memo[i] = rawMemo[i];
}
return memo;
}
/**
* Override getStatus() to append the operation's input parameters to the default status object.
*/

View File

@ -13,6 +13,7 @@
#include "zcash/Address.hpp"
#include "wallet.h"
#include "wallet/paymentdisclosure.h"
#include "wallet/wallet_tx_builder.h"
#include <array>
#include <optional>
@ -25,15 +26,6 @@
using namespace libzcash;
class SendManyRecipient : public RecipientMapping {
public:
CAmount amount;
std::optional<std::string> memo;
SendManyRecipient(std::optional<libzcash::UnifiedAddress> ua_, libzcash::RecipientAddress address_, CAmount amount_, std::optional<std::string> memo_) :
RecipientMapping(ua_, address_), amount(amount_), memo(memo_) {}
};
class TxOutputAmounts {
public:
CAmount t_outputs_total{0};
@ -46,7 +38,7 @@ public:
AsyncRPCOperation_sendmany(
TransactionBuilder builder,
ZTXOSelector ztxoSelector,
std::vector<SendManyRecipient> recipients,
std::vector<ResolvedPayment> recipients,
int minDepth,
unsigned int anchorDepth,
TransactionStrategy strategy,
@ -71,7 +63,7 @@ private:
TransactionBuilder builder_;
ZTXOSelector ztxoSelector_;
std::vector<SendManyRecipient> recipients_;
std::vector<ResolvedPayment> recipients_;
int mindepth_{1};
unsigned int anchordepth_{nAnchorConfirmations};
CAmount fee_;
@ -93,8 +85,6 @@ private:
static CAmount DefaultDustThreshold();
static std::array<unsigned char, ZC_MEMO_SIZE> get_memo_from_hex_string(std::string s);
uint256 main_impl();
};
@ -105,10 +95,6 @@ public:
TEST_FRIEND_AsyncRPCOperation_sendmany(std::shared_ptr<AsyncRPCOperation_sendmany> ptr) : delegate(ptr) {}
std::array<unsigned char, ZC_MEMO_SIZE> get_memo_from_hex_string(std::string s) {
return delegate->get_memo_from_hex_string(s);
}
uint256 main_impl() {
return delegate->main_impl();
}

View File

@ -11,6 +11,7 @@
#include "wallet/asyncrpcoperation_mergetoaddress.h"
#include "wallet/asyncrpcoperation_shieldcoinbase.h"
#include "wallet/asyncrpcoperation_sendmany.h"
#include "wallet/memo.h"
#include "zcash/JoinSplit.hpp"
#include <librustzcash.h>
@ -287,7 +288,7 @@ TEST(WalletRPCTests, RPCZsendmanyTaddrToSapling)
mtx = CreateNewContextualCMutableTransaction(consensusParams, nextBlockHeight, false);
auto selector = pwalletMain->ZTXOSelectorForAddress(taddr, true, false).value();
std::vector<SendManyRecipient> recipients = { SendManyRecipient(std::nullopt, pa, 1*COIN, "ABCD") };
std::vector<ResolvedPayment> recipients = { ResolvedPayment(std::nullopt, pa, 1*COIN, Memo::FromHexOrThrow("ABCD")) };
TransactionStrategy strategy(PrivacyPolicy::AllowRevealedSenders);
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 0, 0, strategy));
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);

87
src/wallet/memo.h Normal file
View File

@ -0,0 +1,87 @@
// Copyright (c) 2022 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
#ifndef ZCASH_WALLET_MEMO_H
#define ZCASH_WALLET_MEMO_H
#include "util/strencodings.h"
#include "zcash/Zcash.h"
#include "tinyformat.h"
enum class MemoError {
HexDecodeError,
MemoTooLong
};
typedef std::array<unsigned char, ZC_MEMO_SIZE> MemoBytes;
class Memo {
private:
MemoBytes value;
public:
// initialize to default memo (no_memo), see section 5.5 of the protocol spec
Memo(): value({{0xF6}}) { }
Memo(MemoBytes value): value(value) {}
static Memo NoMemo() {
return Memo();
}
static std::variant<MemoError, Memo> FromHex(const std::string& memoHex) {
Memo result;
std::vector<unsigned char> rawMemo = ParseHex(memoHex.c_str());
// If ParseHex comes across a non-hex char, it will stop but still return results so far.
size_t slen = memoHex.length();
if (slen != rawMemo.size() * 2) {
return MemoError::HexDecodeError;
}
if (rawMemo.size() > ZC_MEMO_SIZE) {
return MemoError::MemoTooLong;
}
auto rest = std::copy(rawMemo.begin(), rawMemo.end(), result.value.begin());
std::fill(rest, result.value.end(), 0);
return result;
}
static Memo FromHexOrThrow(const std::string& memoHex) {
return std::visit(match {
[&](Memo memo) {
return memo;
},
[&](MemoError err) {
switch (err) {
case MemoError::HexDecodeError:
throw std::runtime_error(
"Invalid parameter, expected memo data in hexadecimal format.");
case MemoError::MemoTooLong:
throw std::runtime_error(strprintf(
"Invalid parameter, memo is longer than the maximum allowed %d characters.",
ZC_MEMO_SIZE));
default:
assert(false);
}
// unreachable, but the compiler can't tell
return Memo::NoMemo();
}
}, Memo::FromHex(memoHex));
}
// This copies, because if it returns a reference to the underlying value,
// using an idiom like `Memo::FromHexOrThrow("abcd").ToBytes()` is unsafe
// and can result in pointing to memory that has been deallocated.
MemoBytes ToBytes() const {
return value;
}
std::string ToHex() const {
return HexStr(value);
}
};
#endif

View File

@ -41,6 +41,7 @@
#include "wallet/asyncrpcoperation_saplingmigration.h"
#include "wallet/asyncrpcoperation_sendmany.h"
#include "wallet/asyncrpcoperation_shieldcoinbase.h"
#include "wallet/wallet_tx_builder.h"
#include <stdint.h>
@ -4988,7 +4989,7 @@ UniValue z_getoperationstatus_IMPL(const UniValue& params, bool fRemoveFinishedO
size_t EstimateTxSize(
const ZTXOSelector& ztxoSelector,
const std::vector<SendManyRecipient>& recipients,
const std::vector<ResolvedPayment>& recipients,
int nextBlockHeight) {
CMutableTransaction mtx;
mtx.fOverwintered = true;
@ -5002,7 +5003,7 @@ size_t EstimateTxSize(
size_t txsize = 0;
size_t taddrRecipientCount = 0;
size_t orchardRecipientCount = 0;
for (const SendManyRecipient& recipient : recipients) {
for (const ResolvedPayment& recipient : recipients) {
std::visit(match {
[&](const CKeyID&) {
taddrRecipientCount += 1;
@ -5207,7 +5208,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
}
std::set<RecipientAddress> recipientAddrs;
std::vector<SendManyRecipient> recipients;
std::vector<ResolvedPayment> recipients;
CAmount nTotalOut = 0;
size_t nOrchardOutputs = 0;
for (const UniValue& o : outputs.getValues()) {
@ -5249,18 +5250,34 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
}
UniValue memoValue = find_value(o, "memo");
std::optional<std::string> memo;
std::optional<Memo> memo;
if (!memoValue.isNull()) {
memo = memoValue.get_str();
auto memoHex = memoValue.get_str();
if (!std::visit(libzcash::IsShieldedRecipient(), addr.value())) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, memos cannot be sent to transparent addresses.");
} else if (!IsHex(memo.value())) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected memo data in hexadecimal format.");
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Invalid parameter, memos cannot be sent to transparent addresses.");
}
if (memo.value().length() > ZC_MEMO_SIZE*2) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, size of memo is larger than maximum allowed %d", ZC_MEMO_SIZE ));
}
std::visit(match {
[&](MemoError err) {
switch (err) {
case MemoError::HexDecodeError:
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Invalid parameter, expected memo data in hexadecimal format.");
case MemoError::MemoTooLong:
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf("Invalid parameter, size of memo is larger than maximum allowed %d", ZC_MEMO_SIZE ));
default:
assert(false);
}
},
[&](Memo result) {
memo = result;
}
}, Memo::FromHex(memoHex));
}
UniValue av = find_value(o, "amount");
@ -5292,7 +5309,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
}
}
recipients.push_back(SendManyRecipient(ua, addr.value(), nAmount, memo));
recipients.push_back(ResolvedPayment(ua, addr.value(), nAmount, memo));
nTotalOut += nAmount;
}
if (recipients.empty()) {

View File

@ -20,6 +20,7 @@
#include "wallet/asyncrpcoperation_mergetoaddress.h"
#include "wallet/asyncrpcoperation_sendmany.h"
#include "wallet/asyncrpcoperation_shieldcoinbase.h"
#include "wallet/memo.h"
#include "init.h"
#include "util/test.h"
@ -1237,7 +1238,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
{
auto selector = pwalletMain->ZTXOSelectorForAddress(taddr1, true, false).value();
TransactionBuilder builder(consensusParams, nHeight + 1, std::nullopt, pwalletMain);
std::vector<SendManyRecipient> recipients = { SendManyRecipient(std::nullopt, zaddr1, 100*COIN, "DEADBEEF") };
std::vector<ResolvedPayment> recipients = { ResolvedPayment(std::nullopt, zaddr1, 100*COIN, Memo::FromHexOrThrow("DEADBEEF")) };
TransactionStrategy strategy;
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1, 1, strategy));
operation->main();
@ -1250,7 +1251,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
{
auto selector = pwalletMain->ZTXOSelectorForAddress(zaddr1, true, false).value();
TransactionBuilder builder(consensusParams, nHeight + 1, std::nullopt, pwalletMain);
std::vector<SendManyRecipient> recipients = { SendManyRecipient(std::nullopt, taddr1, 100*COIN, "DEADBEEF") };
std::vector<ResolvedPayment> recipients = { ResolvedPayment(std::nullopt, taddr1, 100*COIN, Memo::FromHexOrThrow("DEADBEEF")) };
TransactionStrategy strategy(PrivacyPolicy::AllowRevealedRecipients);
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1, 1, strategy));
operation->main();
@ -1258,59 +1259,36 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
std::string msg = operation->getErrorMessage();
BOOST_CHECK(msg.find("Insufficient funds: have 0.00") != string::npos);
}
}
// get_memo_from_hex_string())
{
auto selector = pwalletMain->ZTXOSelectorForAddress(zaddr1, true, false).value();
TransactionBuilder builder(consensusParams, nHeight + 1, std::nullopt, pwalletMain);
std::vector<SendManyRecipient> recipients = { SendManyRecipient(std::nullopt, zaddr1, 100*COIN, "DEADBEEF") };
TransactionStrategy strategy;
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1, 1, strategy));
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);
TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr);
std::string memo = "DEADBEEF";
std::array<unsigned char, ZC_MEMO_SIZE> array = proxy.get_memo_from_hex_string(memo);
BOOST_CHECK_EQUAL(array[0], 0xDE);
BOOST_CHECK_EQUAL(array[1], 0xAD);
BOOST_CHECK_EQUAL(array[2], 0xBE);
BOOST_CHECK_EQUAL(array[3], 0xEF);
for (int i=4; i<ZC_MEMO_SIZE; i++) {
BOOST_CHECK_EQUAL(array[i], 0x00); // zero padding
}
// memo is longer than allowed
std::vector<char> v (2 * (ZC_MEMO_SIZE+1));
std::fill(v.begin(),v.end(), 'A');
std::string bigmemo(v.begin(), v.end());
try {
proxy.get_memo_from_hex_string(bigmemo);
} catch (const UniValue& objError) {
BOOST_CHECK( find_error(objError, "too big"));
}
// invalid hexadecimal string
std::fill(v.begin(),v.end(), '@'); // not a hex character
std::string badmemo(v.begin(), v.end());
try {
proxy.get_memo_from_hex_string(badmemo);
} catch (const UniValue& objError) {
BOOST_CHECK( find_error(objError, "hexadecimal format"));
}
// odd length hexadecimal string
std::fill(v.begin(),v.end(), 'A');
v.resize(v.size() - 1);
assert(v.size() %2 == 1); // odd length
std::string oddmemo(v.begin(), v.end());
try {
proxy.get_memo_from_hex_string(oddmemo);
} catch (const UniValue& objError) {
BOOST_CHECK( find_error(objError, "hexadecimal format"));
}
BOOST_AUTO_TEST_CASE(memo_hex_parsing) {
std::string memo = "DEADBEEF";
MemoBytes memoBytes = Memo::FromHexOrThrow(memo).ToBytes();
BOOST_CHECK_EQUAL(memoBytes[0], 0xDE);
BOOST_CHECK_EQUAL(memoBytes[1], 0xAD);
BOOST_CHECK_EQUAL(memoBytes[2], 0xBE);
BOOST_CHECK_EQUAL(memoBytes[3], 0xEF);
for (int i=4; i<ZC_MEMO_SIZE; i++) {
BOOST_CHECK_EQUAL(memoBytes[i], 0x00); // zero padding
}
// memo is longer than allowed
std::vector<char> v (2 * (ZC_MEMO_SIZE+1));
std::fill(v.begin(),v.end(), 'A');
std::string bigmemo(v.begin(), v.end());
BOOST_CHECK(std::get<MemoError>(Memo::FromHex(bigmemo)) == MemoError::MemoTooLong);
// invalid hexadecimal string
std::fill(v.begin(),v.end(), '@'); // not a hex character
std::string badmemo(v.begin(), v.end());
BOOST_CHECK(std::get<MemoError>(Memo::FromHex(badmemo)) == MemoError::HexDecodeError);
// odd length hexadecimal string
std::fill(v.begin(),v.end(), 'A');
v.resize(v.size() - 1);
assert(v.size() %2 == 1); // odd length
std::string oddmemo(v.begin(), v.end());
BOOST_CHECK(std::get<MemoError>(Memo::FromHex(oddmemo)) == MemoError::HexDecodeError);
}
/*

View File

@ -7851,7 +7851,7 @@ bool ZTXOSelector::SelectsOrchard() const {
bool SpendableInputs::LimitToAmount(
const CAmount amountRequired,
const CAmount dustThreshold,
std::set<OutputPool> recipientPools)
const std::set<OutputPool>& recipientPools)
{
assert(amountRequired >= 0 && dustThreshold > 0);
// Calling this method twice is a programming error.

View File

@ -906,24 +906,48 @@ public:
* This method must only be called once.
*/
bool LimitToAmount(
CAmount amount,
CAmount dustThreshold,
std::set<libzcash::OutputPool> recipientPools);
const CAmount amount,
const CAmount dustThreshold,
const std::set<libzcash::OutputPool>& recipientPools);
/**
* Compute the total ZEC amount of spendable inputs.
*/
CAmount Total() const {
CAmount result = 0;
result += GetTransparentBalance();
result += GetSproutBalance();
result += GetSaplingBalance();
result += GetOrchardBalance();
return result;
}
CAmount GetTransparentBalance() const {
CAmount result = 0;
for (const auto& t : utxos) {
result += t.Value();
}
return result;
}
CAmount GetSproutBalance() const {
CAmount result = 0;
for (const auto& t : sproutNoteEntries) {
result += t.note.value();
}
return result;
}
CAmount GetSaplingBalance() const {
CAmount result = 0;
for (const auto& t : saplingNoteEntries) {
result += t.note.value();
}
return result;
}
CAmount GetOrchardBalance() const {
CAmount result = 0;
for (const auto& t : orchardNoteMetadata) {
result += t.GetNoteValue();
}

View File

@ -0,0 +1,28 @@
// Copyright (c) 2022 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
#ifndef ZCASH_WALLET_WALLET_TX_BUILDER_H
#define ZCASH_WALLET_WALLET_TX_BUILDER_H
#include "wallet/memo.h"
using namespace libzcash;
/**
* A payment that has been resolved to send to a specific
* recipient address in a single pool.
*/
class ResolvedPayment : public RecipientMapping {
public:
CAmount amount;
std::optional<Memo> memo;
ResolvedPayment(
std::optional<libzcash::UnifiedAddress> ua,
libzcash::RecipientAddress address,
CAmount amount,
std::optional<Memo> memo) :
RecipientMapping(ua, address), amount(amount), memo(memo) {}
};
#endif