Auto merge of #4146 - str4d:z_viewtransaction, r=str4d

z_viewtransaction

This RPC method returns all decryptable information for any transaction in the wallet.

Several values are conditionally included in the output for convenience:
- `recovered`: True if an output is not for a Sapling address in the wallet.
- `memoStr`: The text form of an output's memo, if it is valid UTF-8.
- Values are provided both in decimal currency units, and integer zatoshis.
This commit is contained in:
Homu 2020-02-19 20:52:47 +00:00
commit 27c04c8f9c
7 changed files with 471 additions and 5 deletions

View File

@ -120,6 +120,10 @@ Files: depends/sources/qpid-proton-*.tar.gz
Copyright: 2012-2017 The Apache Software Foundation
License: Apache-Qpid-Proton-with-BSD-Subcomponents
Files: depends/sources/utfcpp-*.tar.gz
Copyright: 2006 Nemanja Trifunovic
License: Boost-Software-License-1.0
Files: src/secp256k1/build-aux/m4/ax_jni_include_dir.m4
Copyright: 2008 Don Anderson <dda@sleepycat.com>
License: GNU-All-permissive-License

View File

@ -67,7 +67,7 @@ rust_crates := \
crate_winapi_x86_64_pc_windows_gnu
rust_packages := rust $(rust_crates) librustzcash
proton_packages := proton
zcash_packages := libsodium
zcash_packages := libsodium utfcpp
packages := boost openssl libevent zeromq $(zcash_packages) googletest
native_packages := native_ccache

View File

@ -0,0 +1,10 @@
package=utfcpp
$(package)_version=3.1
$(package)_download_path=https://github.com/nemtrif/$(package)/archive/
$(package)_file_name=$(package)-$($(package)_version).tar.gz
$(package)_download_file=v$($(package)_version).tar.gz
$(package)_sha256_hash=ab531c3fd5d275150430bfaca01d7d15e017a188183be932322f2f651506b096
define $(package)_stage_cmds
cp -a ./source $($(package)_staging_dir)$(host_prefix)/include
endef

View File

@ -8,7 +8,8 @@ from test_framework.util import assert_equal, assert_true, assert_false
from test_framework.util import wait_and_assert_operationid_status
from decimal import Decimal
my_memo = 'c0ffee' # stay awake
my_memo_str = 'c0ffee' # stay awake
my_memo = '633066666565'
my_memo = my_memo + '0'*(1024-len(my_memo))
no_memo = 'f6' + ('0'*1022) # see section 5.5 of the protocol spec
@ -28,15 +29,73 @@ class ListReceivedTest (BitcoinTestFramework):
self.generate_and_sync(height+1)
taddr = self.nodes[1].getnewaddress()
zaddr1 = self.nodes[1].z_getnewaddress(release)
zaddrExt = self.nodes[3].z_getnewaddress(release)
self.nodes[0].sendtoaddress(taddr, 2.0)
self.nodes[0].sendtoaddress(taddr, 4.0)
self.generate_and_sync(height+2)
# Send 1 ZEC to zaddr1
opid = self.nodes[1].z_sendmany(taddr,
[{'address': zaddr1, 'amount': 1, 'memo': my_memo}])
opid = self.nodes[1].z_sendmany(taddr, [
{'address': zaddr1, 'amount': 1, 'memo': my_memo},
{'address': zaddrExt, 'amount': 2},
])
txid = wait_and_assert_operationid_status(self.nodes[1], opid)
self.sync_all()
# Decrypted transaction details should be correct
pt = self.nodes[1].z_viewtransaction(txid)
assert_equal(pt['txid'], txid)
assert_equal(len(pt['spends']), 0)
assert_equal(len(pt['outputs']), 1 if release == 'sprout' else 2)
# Output orders can be randomized, so we check the output
# positions and contents separately
outputs = []
assert_equal(pt['outputs'][0]['type'], release)
if release == 'sprout':
assert_equal(pt['outputs'][0]['js'], 0)
jsOutputPrev = pt['outputs'][0]['jsOutput']
elif pt['outputs'][0]['address'] == zaddr1:
assert_equal(pt['outputs'][0]['outgoing'], False)
assert_equal(pt['outputs'][0]['memoStr'], my_memo_str)
else:
assert_equal(pt['outputs'][0]['outgoing'], True)
outputs.append({
'address': pt['outputs'][0]['address'],
'value': pt['outputs'][0]['value'],
'valueZat': pt['outputs'][0]['valueZat'],
'memo': pt['outputs'][0]['memo'],
})
if release != 'sprout':
assert_equal(pt['outputs'][1]['type'], release)
if pt['outputs'][1]['address'] == zaddr1:
assert_equal(pt['outputs'][1]['outgoing'], False)
assert_equal(pt['outputs'][1]['memoStr'], my_memo_str)
else:
assert_equal(pt['outputs'][1]['outgoing'], True)
outputs.append({
'address': pt['outputs'][1]['address'],
'value': pt['outputs'][1]['value'],
'valueZat': pt['outputs'][1]['valueZat'],
'memo': pt['outputs'][1]['memo'],
})
assert({
'address': zaddr1,
'value': Decimal('1'),
'valueZat': 100000000,
'memo': my_memo,
} in outputs)
if release != 'sprout':
assert({
'address': zaddrExt,
'value': Decimal('2'),
'valueZat': 200000000,
'memo': no_memo,
} in outputs)
r = self.nodes[1].z_listreceivedbyaddress(zaddr1)
assert_equal(0, len(r), "Should have received no confirmed note")
@ -55,6 +114,7 @@ class ListReceivedTest (BitcoinTestFramework):
assert_equal(r, self.nodes[1].z_listreceivedbyaddress(zaddr1))
# Generate some change by sending part of zaddr1 to zaddr2
txidPrev = txid
zaddr2 = self.nodes[1].z_getnewaddress(release)
opid = self.nodes[1].z_sendmany(zaddr1,
[{'address': zaddr2, 'amount': 0.6}])
@ -62,6 +122,65 @@ class ListReceivedTest (BitcoinTestFramework):
self.sync_all()
self.generate_and_sync(height+4)
# Decrypted transaction details should be correct
pt = self.nodes[1].z_viewtransaction(txid)
assert_equal(pt['txid'], txid)
assert_equal(len(pt['spends']), 1)
assert_equal(len(pt['outputs']), 2)
assert_equal(pt['spends'][0]['type'], release)
assert_equal(pt['spends'][0]['txidPrev'], txidPrev)
if release == 'sprout':
assert_equal(pt['spends'][0]['js'], 0)
# jsSpend is randomised during transaction creation
assert_equal(pt['spends'][0]['jsPrev'], 0)
assert_equal(pt['spends'][0]['jsOutputPrev'], jsOutputPrev)
else:
assert_equal(pt['spends'][0]['spend'], 0)
assert_equal(pt['spends'][0]['outputPrev'], 0)
assert_equal(pt['spends'][0]['address'], zaddr1)
assert_equal(pt['spends'][0]['value'], Decimal('1.0'))
assert_equal(pt['spends'][0]['valueZat'], 100000000)
# Output orders can be randomized, so we check the output
# positions and contents separately
outputs = []
assert_equal(pt['outputs'][0]['type'], release)
if release == 'sapling':
assert_equal(pt['outputs'][0]['output'], 0)
assert_equal(pt['outputs'][0]['outgoing'], False)
outputs.append({
'address': pt['outputs'][0]['address'],
'value': pt['outputs'][0]['value'],
'valueZat': pt['outputs'][0]['valueZat'],
'memo': pt['outputs'][0]['memo'],
})
assert_equal(pt['outputs'][1]['type'], release)
if release == 'sapling':
assert_equal(pt['outputs'][1]['output'], 1)
assert_equal(pt['outputs'][1]['outgoing'], False)
outputs.append({
'address': pt['outputs'][1]['address'],
'value': pt['outputs'][1]['value'],
'valueZat': pt['outputs'][1]['valueZat'],
'memo': pt['outputs'][1]['memo'],
})
assert({
'address': zaddr2,
'value': Decimal('0.6'),
'valueZat': 60000000,
'memo': no_memo,
} in outputs)
assert({
'address': zaddr1,
'value': Decimal('0.3999'),
'valueZat': 39990000,
'memo': no_memo,
} in outputs)
# zaddr1 should have a note with change
r = self.nodes[1].z_listreceivedbyaddress(zaddr1, 0)
r = sorted(r, key = lambda received: received['amount'])

View File

@ -36,6 +36,7 @@
#include <stdint.h>
#include <boost/assign/list_of.hpp>
#include <utf8.h>
#include <univalue.h>
@ -3524,6 +3525,226 @@ UniValue z_gettotalbalance(const UniValue& params, bool fHelp)
return result;
}
UniValue z_viewtransaction(const UniValue& params, bool fHelp)
{
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() != 1)
throw runtime_error(
"z_viewtransaction \"txid\"\n"
"\nGet detailed shielded information about in-wallet transaction <txid>\n"
"\nArguments:\n"
"1. \"txid\" (string, required) The transaction id\n"
"\nResult:\n"
"{\n"
" \"txid\" : \"transactionid\", (string) The transaction id\n"
" \"spends\" : [\n"
" {\n"
" \"type\" : \"sprout|sapling\", (string) The type of address\n"
" \"js\" : n, (numeric, sprout) the index of the JSDescription within vJoinSplit\n"
" \"jsSpend\" : n, (numeric, sprout) the index of the spend within the JSDescription\n"
" \"spend\" : n, (numeric, sapling) the index of the spend within vShieldedSpend\n"
" \"txidPrev\" : \"transactionid\", (string) The id for the transaction this note was created in\n"
" \"jsPrev\" : n, (numeric, sprout) the index of the JSDescription within vJoinSplit\n"
" \"jsOutputPrev\" : n, (numeric, sprout) the index of the output within the JSDescription\n"
" \"outputPrev\" : n, (numeric, sapling) the index of the output within the vShieldedOutput\n"
" \"address\" : \"zcashaddress\", (string) The Zcash address involved in the transaction\n"
" \"value\" : x.xxx (numeric) The amount in " + CURRENCY_UNIT + "\n"
" \"valueZat\" : xxxx (numeric) The amount in zatoshis\n"
" }\n"
" ,...\n"
" ],\n"
" \"outputs\" : [\n"
" {\n"
" \"type\" : \"sprout|sapling\", (string) The type of address\n"
" \"js\" : n, (numeric, sprout) the index of the JSDescription within vJoinSplit\n"
" \"jsOutput\" : n, (numeric, sprout) the index of the output within the JSDescription\n"
" \"output\" : n, (numeric, sapling) the index of the output within the vShieldedOutput\n"
" \"address\" : \"zcashaddress\", (string) The Zcash address involved in the transaction\n"
" \"outgoing\" : true|false (boolean, sapling) True if the output is not for an address in the wallet\n"
" \"value\" : x.xxx (numeric) The amount in " + CURRENCY_UNIT + "\n"
" \"valueZat\" : xxxx (numeric) The amount in zatoshis\n"
" \"memo\" : \"hexmemo\", (string) Hexademical string representation of the memo field\n"
" \"memoStr\" : \"memo\", (string) Only returned if memo contains valid UTF-8 text.\n"
" }\n"
" ,...\n"
" ],\n"
"}\n"
"\nExamples:\n"
+ HelpExampleCli("z_viewtransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"")
+ HelpExampleRpc("z_viewtransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"")
);
LOCK2(cs_main, pwalletMain->cs_wallet);
uint256 hash;
hash.SetHex(params[0].get_str());
UniValue entry(UniValue::VOBJ);
if (!pwalletMain->mapWallet.count(hash))
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id");
const CWalletTx& wtx = pwalletMain->mapWallet[hash];
entry.push_back(Pair("txid", hash.GetHex()));
UniValue spends(UniValue::VARR);
UniValue outputs(UniValue::VARR);
auto addMemo = [](UniValue &entry, std::array<unsigned char, ZC_MEMO_SIZE> &memo) {
entry.push_back(Pair("memo", HexStr(memo)));
// If the leading byte is 0xF4 or lower, the memo field should be interpreted as a
// UTF-8-encoded text string.
if (memo[0] <= 0xf4) {
// Trim off trailing zeroes
auto end = std::find_if(
memo.rbegin(),
memo.rend(),
[](unsigned char v) { return v != 0; });
std::string memoStr(memo.begin(), end.base());
if (utf8::is_valid(memoStr)) {
entry.push_back(Pair("memoStr", memoStr));
}
}
};
// Sprout spends
for (size_t i = 0; i < wtx.vJoinSplit.size(); ++i) {
for (size_t j = 0; j < wtx.vJoinSplit[i].nullifiers.size(); ++j) {
auto nullifier = wtx.vJoinSplit[i].nullifiers[j];
// Fetch the note that is being spent, if ours
auto res = pwalletMain->mapSproutNullifiersToNotes.find(nullifier);
if (res == pwalletMain->mapSproutNullifiersToNotes.end()) {
continue;
}
auto jsop = res->second;
auto wtxPrev = pwalletMain->mapWallet.at(jsop.hash);
auto decrypted = wtxPrev.DecryptSproutNote(jsop);
auto notePt = decrypted.first;
auto pa = decrypted.second;
UniValue entry(UniValue::VOBJ);
entry.push_back(Pair("type", ADDR_TYPE_SPROUT));
entry.push_back(Pair("js", (int)i));
entry.push_back(Pair("jsSpend", (int)j));
entry.push_back(Pair("txidPrev", jsop.hash.GetHex()));
entry.push_back(Pair("jsPrev", (int)jsop.js));
entry.push_back(Pair("jsOutputPrev", (int)jsop.n));
entry.push_back(Pair("address", EncodePaymentAddress(pa)));
entry.push_back(Pair("value", ValueFromAmount(notePt.value())));
entry.push_back(Pair("valueZat", notePt.value()));
spends.push_back(entry);
}
}
// Sprout outputs
for (auto & pair : wtx.mapSproutNoteData) {
JSOutPoint jsop = pair.first;
auto decrypted = wtx.DecryptSproutNote(jsop);
auto notePt = decrypted.first;
auto pa = decrypted.second;
auto memo = notePt.memo();
UniValue entry(UniValue::VOBJ);
entry.push_back(Pair("type", ADDR_TYPE_SPROUT));
entry.push_back(Pair("js", (int)jsop.js));
entry.push_back(Pair("jsOutput", (int)jsop.n));
entry.push_back(Pair("address", EncodePaymentAddress(pa)));
entry.push_back(Pair("value", ValueFromAmount(notePt.value())));
entry.push_back(Pair("valueZat", notePt.value()));
addMemo(entry, memo);
outputs.push_back(entry);
}
// Collect OutgoingViewingKeys for recovering output information
std::set<uint256> ovks;
{
// Generate the common ovk for recovering t->z outputs.
HDSeed seed = pwalletMain->GetHDSeedForRPC();
ovks.insert(ovkForShieldingFromTaddr(seed));
}
// Sapling spends
for (size_t i = 0; i < wtx.vShieldedSpend.size(); ++i) {
auto spend = wtx.vShieldedSpend[i];
// Fetch the note that is being spent
auto res = pwalletMain->mapSaplingNullifiersToNotes.find(spend.nullifier);
if (res == pwalletMain->mapSaplingNullifiersToNotes.end()) {
continue;
}
auto op = res->second;
auto wtxPrev = pwalletMain->mapWallet.at(op.hash);
auto decrypted = wtxPrev.DecryptSaplingNote(op).get();
auto notePt = decrypted.first;
auto pa = decrypted.second;
// Store the OutgoingViewingKey for recovering outputs
libzcash::SaplingFullViewingKey fvk;
assert(pwalletMain->GetSaplingFullViewingKey(wtxPrev.mapSaplingNoteData.at(op).ivk, fvk));
ovks.insert(fvk.ovk);
UniValue entry(UniValue::VOBJ);
entry.push_back(Pair("type", ADDR_TYPE_SAPLING));
entry.push_back(Pair("spend", (int)i));
entry.push_back(Pair("txidPrev", op.hash.GetHex()));
entry.push_back(Pair("outputPrev", (int)op.n));
entry.push_back(Pair("address", EncodePaymentAddress(pa)));
entry.push_back(Pair("value", ValueFromAmount(notePt.value())));
entry.push_back(Pair("valueZat", notePt.value()));
spends.push_back(entry);
}
// Sapling outputs
for (uint32_t i = 0; i < wtx.vShieldedOutput.size(); ++i) {
auto op = SaplingOutPoint(hash, i);
SaplingNotePlaintext notePt;
SaplingPaymentAddress pa;
bool isOutgoing;
auto decrypted = wtx.DecryptSaplingNote(op);
if (decrypted) {
notePt = decrypted->first;
pa = decrypted->second;
isOutgoing = false;
} else {
// Try recovering the output
auto recovered = wtx.RecoverSaplingNote(op, ovks);
if (recovered) {
notePt = recovered->first;
pa = recovered->second;
isOutgoing = true;
} else {
// Unreadable
continue;
}
}
auto memo = notePt.memo();
UniValue entry(UniValue::VOBJ);
entry.push_back(Pair("type", ADDR_TYPE_SAPLING));
entry.push_back(Pair("output", (int)op.n));
entry.push_back(Pair("outgoing", isOutgoing));
entry.push_back(Pair("address", EncodePaymentAddress(pa)));
entry.push_back(Pair("value", ValueFromAmount(notePt.value())));
entry.push_back(Pair("valueZat", notePt.value()));
addMemo(entry, memo);
outputs.push_back(entry);
}
entry.push_back(Pair("spends", spends));
entry.push_back(Pair("outputs", outputs));
return entry;
}
UniValue z_getoperationresult(const UniValue& params, bool fHelp)
{
if (!EnsureWalletIsAvailable(fHelp))
@ -4809,6 +5030,7 @@ static const CRPCCommand commands[] =
{ "wallet", "z_importviewingkey", &z_importviewingkey, true },
{ "wallet", "z_exportwallet", &z_exportwallet, true },
{ "wallet", "z_importwallet", &z_importwallet, true },
{ "wallet", "z_viewtransaction", &z_viewtransaction, false },
// TODO: rearrange into another category
{ "disclosure", "z_getpaymentdisclosure", &z_getpaymentdisclosure, true },
{ "disclosure", "z_validatepaymentdisclosure", &z_validatepaymentdisclosure, true }

View File

@ -2244,6 +2244,107 @@ void CWalletTx::SetSaplingNoteData(mapSaplingNoteData_t &noteData)
}
}
std::pair<SproutNotePlaintext, SproutPaymentAddress> CWalletTx::DecryptSproutNote(
JSOutPoint jsop) const
{
LOCK(pwallet->cs_wallet);
auto nd = this->mapSproutNoteData.at(jsop);
SproutPaymentAddress pa = nd.address;
// Get cached decryptor
ZCNoteDecryption decryptor;
if (!pwallet->GetNoteDecryptor(pa, decryptor)) {
// Note decryptors are created when the wallet is loaded, so it should always exist
throw std::runtime_error(strprintf(
"Could not find note decryptor for payment address %s",
EncodePaymentAddress(pa)));
}
auto hSig = this->vJoinSplit[jsop.js].h_sig(*pzcashParams, this->joinSplitPubKey);
try {
SproutNotePlaintext plaintext = SproutNotePlaintext::decrypt(
decryptor,
this->vJoinSplit[jsop.js].ciphertexts[jsop.n],
this->vJoinSplit[jsop.js].ephemeralKey,
hSig,
(unsigned char) jsop.n);
return std::make_pair(plaintext, pa);
} catch (const note_decryption_failed &err) {
// Couldn't decrypt with this spending key
throw std::runtime_error(strprintf(
"Could not decrypt note for payment address %s",
EncodePaymentAddress(pa)));
} catch (const std::exception &exc) {
// Unexpected failure
throw std::runtime_error(strprintf(
"Error while decrypting note for payment address %s: %s",
EncodePaymentAddress(pa), exc.what()));
}
}
boost::optional<std::pair<
SaplingNotePlaintext,
SaplingPaymentAddress>> CWalletTx::DecryptSaplingNote(SaplingOutPoint op) const
{
// Check whether we can decrypt this SaplingOutPoint
if (this->mapSaplingNoteData.count(op) == 0) {
return boost::none;
}
auto output = this->vShieldedOutput[op.n];
auto nd = this->mapSaplingNoteData.at(op);
auto maybe_pt = SaplingNotePlaintext::decrypt(
output.encCiphertext,
nd.ivk,
output.ephemeralKey,
output.cm);
assert(static_cast<bool>(maybe_pt));
auto notePt = maybe_pt.get();
auto maybe_pa = nd.ivk.address(notePt.d);
assert(static_cast<bool>(maybe_pa));
auto pa = maybe_pa.get();
return std::make_pair(notePt, pa);
}
boost::optional<std::pair<
SaplingNotePlaintext,
SaplingPaymentAddress>> CWalletTx::RecoverSaplingNote(
SaplingOutPoint op, std::set<uint256>& ovks) const
{
auto output = this->vShieldedOutput[op.n];
for (auto ovk : ovks) {
auto outPt = SaplingOutgoingPlaintext::decrypt(
output.outCiphertext,
ovk,
output.cv,
output.cm,
output.ephemeralKey);
if (!outPt) {
continue;
}
auto maybe_pt = SaplingNotePlaintext::decrypt(
output.encCiphertext,
output.ephemeralKey,
outPt->esk,
outPt->pk_d,
output.cm);
assert(static_cast<bool>(maybe_pt));
auto notePt = maybe_pt.get();
return std::make_pair(notePt, SaplingPaymentAddress(notePt.d, outPt->pk_d));
}
// Couldn't recover with any of the provided OutgoingViewingKeys
return boost::none;
}
int64_t CWalletTx::GetTxTime() const
{
int64_t n = nTimeSmart;

View File

@ -563,6 +563,16 @@ public:
void SetSproutNoteData(mapSproutNoteData_t &noteData);
void SetSaplingNoteData(mapSaplingNoteData_t &noteData);
std::pair<libzcash::SproutNotePlaintext, libzcash::SproutPaymentAddress> DecryptSproutNote(
JSOutPoint jsop) const;
boost::optional<std::pair<
libzcash::SaplingNotePlaintext,
libzcash::SaplingPaymentAddress>> DecryptSaplingNote(SaplingOutPoint op) const;
boost::optional<std::pair<
libzcash::SaplingNotePlaintext,
libzcash::SaplingPaymentAddress>> RecoverSaplingNote(
SaplingOutPoint op, std::set<uint256>& ovks) const;
//! filter decides which addresses will count towards the debit
CAmount GetDebit(const isminefilter& filter) const;
CAmount GetCredit(const isminefilter& filter) const;