Add Orchard & unified address support to z_viewtransaction.

Fixes #5186
This commit is contained in:
Steven Smith 2022-03-16 23:53:46 -07:00 committed by Kris Nuttycombe
parent 315392ce3b
commit 445f4de0f4
7 changed files with 192 additions and 35 deletions

View File

@ -12,8 +12,10 @@ from test_framework.util import (
assert_raises_message, assert_raises_message,
connect_nodes_bi, connect_nodes_bi,
get_coinbase_address, get_coinbase_address,
nuparams,
DEFAULT_FEE, DEFAULT_FEE,
DEFAULT_FEE_ZATS DEFAULT_FEE_ZATS,
NU5_BRANCH_ID,
) )
from test_framework.util import wait_and_assert_operationid_status, start_nodes from test_framework.util import wait_and_assert_operationid_status, start_nodes
from decimal import Decimal from decimal import Decimal
@ -33,7 +35,12 @@ class ListReceivedTest (BitcoinTestFramework):
def setup_network(self): def setup_network(self):
self.nodes = start_nodes( self.nodes = start_nodes(
self.num_nodes, self.options.tmpdir, self.num_nodes, self.options.tmpdir,
extra_args=[['-experimentalfeatures', '-orchardwallet']] * self.num_nodes) extra_args=[[
'-experimentalfeatures',
'-orchardwallet',
nuparams(NU5_BRANCH_ID, 225),
]] * self.num_nodes
)
connect_nodes_bi(self.nodes, 0, 1) connect_nodes_bi(self.nodes, 0, 1)
connect_nodes_bi(self.nodes, 1, 2) connect_nodes_bi(self.nodes, 1, 2)
connect_nodes_bi(self.nodes, 0, 2) connect_nodes_bi(self.nodes, 0, 2)
@ -422,7 +429,7 @@ class ListReceivedTest (BitcoinTestFramework):
assert_equal(r[0]['blockheight'], height+5) assert_equal(r[0]['blockheight'], height+5)
assert_equal(r[0]['blockindex'], 1) assert_equal(r[0]['blockindex'], 1)
assert 'blocktime' in r[0] assert 'blocktime' in r[0]
assert_equal(r[1]['pool'], 'transparent') assert_equal(r[1]['pool'], 'transparent')
assert_equal(r[1]['txid'], txid_taddr) assert_equal(r[1]['txid'], txid_taddr)
assert_equal(r[1]['amount'], Decimal('0.2')) assert_equal(r[1]['amount'], Decimal('0.2'))
@ -435,9 +442,101 @@ class ListReceivedTest (BitcoinTestFramework):
assert_equal(r[1]['blockindex'], -1) # not yet mined assert_equal(r[1]['blockindex'], -1) # not yet mined
assert 'blocktime' in r[1] assert 'blocktime' in r[1]
def test_received_orchard(self, height):
self.generate_and_sync(height+1)
taddr = self.nodes[1].getnewaddress()
acct1 = self.nodes[1].z_getnewaccount()['account']
acct2 = self.nodes[1].z_getnewaccount()['account']
addrResO = self.nodes[1].z_getaddressforaccount(acct1, ['orchard'])
assert_equal(addrResO['pools'], ['orchard'])
uao = addrResO['unifiedaddress']
addrResSO = self.nodes[1].z_getaddressforaccount(acct2, ['sapling', 'orchard'])
assert_equal(addrResSO['pools'], ['sapling', 'orchard'])
uaso = addrResSO['unifiedaddress']
self.nodes[0].sendtoaddress(taddr, 4.0)
self.generate_and_sync(height+2)
opid = self.nodes[1].z_sendmany(taddr, [
{'address': uao, 'amount': 1, 'memo': my_memo},
{'address': uaso, 'amount': 2},
])
txid0 = wait_and_assert_operationid_status(self.nodes[1], opid)
self.sync_all()
# Decrypted transaction details should be correct, even though
# the transaction is still just in the mempool
pt = self.nodes[1].z_viewtransaction(txid0)
assert_equal(pt['txid'], txid0)
assert_equal(len(pt['spends']), 0)
assert_equal(len(pt['outputs']), 2)
# Outputs are not returned in a defined order but the amounts are deterministic
outputs = sorted(pt['outputs'], key=lambda x: x['valueZat'])
assert_equal(outputs[0]['type'], 'orchard')
assert_equal(outputs[0]['address'], uao)
assert_equal(outputs[0]['value'], Decimal('1'))
assert_equal(outputs[0]['valueZat'], 100000000)
assert_equal(outputs[0]['output'], 0)
assert_equal(outputs[0]['outgoing'], False)
assert_equal(outputs[0]['memo'], my_memo)
assert_equal(outputs[0]['memoStr'], my_memo_str)
assert_equal(outputs[1]['type'], 'orchard')
assert_equal(outputs[1]['address'], uaso)
assert_equal(outputs[1]['value'], Decimal('2'))
assert_equal(outputs[1]['valueZat'], 200000000)
assert_equal(outputs[1]['output'], 1)
assert_equal(outputs[1]['outgoing'], False)
assert_equal(outputs[1]['memo'], no_memo)
assert 'memoStr' not in outputs[1]
self.generate_and_sync(height+3)
opid = self.nodes[1].z_sendmany(uao, [
{'address': uaso, 'amount': Decimal('0.4')}
])
txid1 = wait_and_assert_operationid_status(self.nodes[1], opid)
self.sync_all()
pt = self.nodes[1].z_viewtransaction(txid1)
assert_equal(pt['txid'], txid1)
assert_equal(len(pt['spends']), 1) # one spend we can see
assert_equal(len(pt['outputs']), 2) # one output + one change output we can see
spends = pt['spends']
assert_equal(spends[0]['type'], 'orchard')
assert_equal(spends[0]['spend'], 0)
assert_equal(spends[0]['txidPrev'], txid0)
assert_equal(spends[0]['outputPrev'], 0)
assert_equal(spends[0]['address'], uao)
assert_equal(spends[0]['value'], Decimal('1.0'))
assert_equal(spends[0]['valueZat'], 100000000)
outputs = sorted(pt['outputs'], key=lambda x: x['valueZat'])
assert_equal(outputs[0]['type'], 'orchard')
assert_equal(outputs[0]['address'], uaso)
assert_equal(outputs[0]['value'], Decimal('0.4'))
assert_equal(outputs[0]['valueZat'], 40000000)
assert_equal(outputs[0]['outgoing'], False)
assert_equal(outputs[0]['memo'], no_memo)
assert_equal(outputs[1]['type'], 'orchard')
# TODO: scrub change addresses
#assert_equal(outputs[1]['address'], '<change>')
assert_equal(outputs[1]['value'], Decimal('0.59999'))
assert_equal(outputs[1]['valueZat'], 59999000)
assert_equal(outputs[1]['outgoing'], False)
assert_equal(outputs[1]['memo'], no_memo)
def run_test(self): def run_test(self):
self.test_received_sprout(200) self.test_received_sprout(200)
self.test_received_sapling(214) self.test_received_sapling(214)
self.test_received_orchard(230)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -81,6 +81,11 @@ std::string SaplingOutPoint::ToString() const
return strprintf("SaplingOutPoint(%s, %u)", hash.ToString().substr(0, 10), n); return strprintf("SaplingOutPoint(%s, %u)", hash.ToString().substr(0, 10), n);
} }
std::string OrchardOutPoint::ToString() const
{
return strprintf("OrchardOutPoint(%s, %u)", hash.ToString().substr(0, 10), n);
}
CTxIn::CTxIn(COutPoint prevoutIn, CScript scriptSigIn, uint32_t nSequenceIn) CTxIn::CTxIn(COutPoint prevoutIn, CScript scriptSigIn, uint32_t nSequenceIn)
{ {
prevout = prevoutIn; prevout = prevoutIn;

View File

@ -107,8 +107,8 @@ typedef void (*push_spend_action_idx_callback_t)(void* rec, uint32_t actionIdx);
* the provided `callbackReceiver` referent using the `push_cb` callback. Note that * the provided `callbackReceiver` referent using the `push_cb` callback. Note that
* this callback can perform transformations on the provided RawOrchardActionIVK in this * this callback can perform transformations on the provided RawOrchardActionIVK in this
* process. For each action spending one of the wallet's notes, this method will pass * process. For each action spending one of the wallet's notes, this method will pass
* a `uint32_t` action index corresponding to that action to the `callbackReceiver` referent; * a `uint32_t` action index corresponding to that action to the `callbackReceiver` referent;
* using the specified callback; usually, this will push the value into a result vector owned * using the specified callback; usually, this will push the value into a result vector owned
* by the caller. * by the caller.
* *
* The provided bundle must be a component of the transaction from which `txid` was * The provided bundle must be a component of the transaction from which `txid` was
@ -272,6 +272,7 @@ struct RawOrchardActionSpend {
uint32_t spendActionIdx; uint32_t spendActionIdx;
unsigned char outpointTxId[32]; unsigned char outpointTxId[32];
uint32_t outpointActionIdx; uint32_t outpointActionIdx;
OrchardRawAddressPtr* receivedAt;
CAmount noteValue; CAmount noteValue;
}; };

View File

@ -999,6 +999,7 @@ pub struct FFIActionSpend {
spend_action_idx: u32, spend_action_idx: u32,
outpoint_txid: [u8; 32], outpoint_txid: [u8; 32],
outpoint_action_idx: u32, outpoint_action_idx: u32,
received_at: *mut Address,
value: i64, value: i64,
} }
@ -1070,6 +1071,7 @@ pub extern "C" fn orchard_wallet_get_txdata(
spend_action_idx: idx as u32, spend_action_idx: idx as u32,
outpoint_txid: *outpoint.txid.as_ref(), outpoint_txid: *outpoint.txid.as_ref(),
outpoint_action_idx: outpoint.action_idx as u32, outpoint_action_idx: outpoint.action_idx as u32,
received_at: Box::into_raw(Box::new(dnote.note.recipient())),
value: dnote.note.value().inner() as i64, value: dnote.note.value().inner() as i64,
}) })
}) { }) {

View File

@ -113,14 +113,20 @@ public:
class OrchardActionSpend { class OrchardActionSpend {
private: private:
OrchardOutPoint outPoint; OrchardOutPoint outPoint;
libzcash::OrchardRawAddress receivedAt;
CAmount noteValue; CAmount noteValue;
public: public:
OrchardActionSpend(OrchardOutPoint outPoint, CAmount noteValue): outPoint(outPoint), noteValue(noteValue) { } OrchardActionSpend(OrchardOutPoint outPoint, libzcash::OrchardRawAddress receivedAt, CAmount noteValue):
outPoint(outPoint), receivedAt(receivedAt), noteValue(noteValue) { }
OrchardOutPoint GetOutPoint() const { OrchardOutPoint GetOutPoint() const {
return outPoint; return outPoint;
} }
const libzcash::OrchardRawAddress& GetReceivedAt() const {
return receivedAt;
}
CAmount GetNoteValue() const { CAmount GetNoteValue() const {
return noteValue; return noteValue;
} }
@ -137,7 +143,7 @@ public:
libzcash::OrchardRawAddress recipient, CAmount noteValue, std::array<unsigned char, 512> memo, bool isOutgoing): libzcash::OrchardRawAddress recipient, CAmount noteValue, std::array<unsigned char, 512> memo, bool isOutgoing):
recipient(recipient), noteValue(noteValue), memo(memo), isOutgoing(isOutgoing) { } recipient(recipient), noteValue(noteValue), memo(memo), isOutgoing(isOutgoing) { }
const libzcash::OrchardRawAddress& GetRecipient() { const libzcash::OrchardRawAddress& GetRecipient() const {
return recipient; return recipient;
} }
@ -428,6 +434,7 @@ public:
std::move(std::begin(rawSpend.outpointTxId), std::end(rawSpend.outpointTxId), txid.begin()); std::move(std::begin(rawSpend.outpointTxId), std::end(rawSpend.outpointTxId), txid.begin());
auto spend = OrchardActionSpend( auto spend = OrchardActionSpend(
OrchardOutPoint(txid, rawSpend.outpointActionIdx), OrchardOutPoint(txid, rawSpend.outpointActionIdx),
libzcash::OrchardRawAddress(rawSpend.receivedAt),
rawSpend.noteValue); rawSpend.noteValue);
reinterpret_cast<OrchardActions*>(receiver)->AddSpend(rawSpend.spendActionIdx, spend); reinterpret_cast<OrchardActions*>(receiver)->AddSpend(rawSpend.spendActionIdx, spend);
} }

View File

@ -59,6 +59,7 @@ using namespace libzcash;
const std::string ADDR_TYPE_SPROUT = "sprout"; const std::string ADDR_TYPE_SPROUT = "sprout";
const std::string ADDR_TYPE_SAPLING = "sapling"; const std::string ADDR_TYPE_SAPLING = "sapling";
const std::string ADDR_TYPE_ORCHARD = "orchard";
extern UniValue TxJoinSplitToJSON(const CTransaction& tx); extern UniValue TxJoinSplitToJSON(const CTransaction& tx);
@ -4073,15 +4074,15 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet); LOCK2(cs_main, pwalletMain->cs_wallet);
uint256 hash; uint256 txid;
hash.SetHex(params[0].get_str()); txid.SetHex(params[0].get_str());
UniValue entry(UniValue::VOBJ); UniValue entry(UniValue::VOBJ);
if (!pwalletMain->mapWallet.count(hash)) if (!pwalletMain->mapWallet.count(txid))
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id"); throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id");
const CWalletTx& wtx = pwalletMain->mapWallet[hash]; const CWalletTx& wtx = pwalletMain->mapWallet[txid];
entry.pushKV("txid", hash.GetHex()); entry.pushKV("txid", txid.GetHex());
UniValue spends(UniValue::VARR); UniValue spends(UniValue::VARR);
UniValue outputs(UniValue::VARR); UniValue outputs(UniValue::VARR);
@ -4220,16 +4221,11 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp)
assert(pwalletMain->GetSaplingFullViewingKey(wtxPrev.mapSaplingNoteData.at(op).ivk, extfvk)); assert(pwalletMain->GetSaplingFullViewingKey(wtxPrev.mapSaplingNoteData.at(op).ivk, extfvk));
ovks.insert(extfvk.fvk.ovk); ovks.insert(extfvk.fvk.ovk);
// If the note belongs to a Sapling address that is part of an account in the // Show the address that was cached at transaction construction as the
// wallet, show the corresponding Unified Address. // recipient.
std::string address = keyIO.EncodePaymentAddress([&]() { std::string address = keyIO.EncodePaymentAddress(
auto ua = pwalletMain->FindUnifiedAddressByReceiver(pa); pwalletMain->GetPaymentAddressForRecipient(txid, pa)
if (ua.has_value()) { );
return libzcash::PaymentAddress{ua.value()};
} else {
return libzcash::PaymentAddress{pa};
}
}());
UniValue entry(UniValue::VOBJ); UniValue entry(UniValue::VOBJ);
entry.pushKV("type", ADDR_TYPE_SAPLING); entry.pushKV("type", ADDR_TYPE_SAPLING);
@ -4244,7 +4240,7 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp)
// Sapling outputs // Sapling outputs
for (uint32_t i = 0; i < wtx.vShieldedOutput.size(); ++i) { for (uint32_t i = 0; i < wtx.vShieldedOutput.size(); ++i) {
auto op = SaplingOutPoint(hash, i); auto op = SaplingOutPoint(txid, i);
SaplingNotePlaintext notePt; SaplingNotePlaintext notePt;
SaplingPaymentAddress pa; SaplingPaymentAddress pa;
@ -4274,16 +4270,11 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp)
} }
auto memo = notePt.memo(); auto memo = notePt.memo();
// If the note belongs to a Sapling address that is part of an account in the // Show the address that was cached at transaction construction as the
// wallet, show the corresponding Unified Address. // recipient.
std::string address = keyIO.EncodePaymentAddress([&]() { std::string address = keyIO.EncodePaymentAddress(
auto ua = pwalletMain->FindUnifiedAddressByReceiver(pa); pwalletMain->GetPaymentAddressForRecipient(txid, pa)
if (ua.has_value()) { );
return libzcash::PaymentAddress{ua.value()};
} else {
return libzcash::PaymentAddress{pa};
}
}());
UniValue entry(UniValue::VOBJ); UniValue entry(UniValue::VOBJ);
entry.pushKV("type", ADDR_TYPE_SAPLING); entry.pushKV("type", ADDR_TYPE_SAPLING);
@ -4295,7 +4286,57 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp)
addMemo(entry, memo); addMemo(entry, memo);
outputs.push_back(entry); outputs.push_back(entry);
} }
// TODO unified addresses, orchard, see #5186
std::vector<uint256> ovksVector(ovks.begin(), ovks.end());
OrchardActions orchardActions = pwalletMain->GetOrchardWallet().GetTxActions(wtx, ovksVector);
// Orchard spends
for (auto & pair : orchardActions.GetSpends()) {
// TODO test with Orchard spends after rebased on feature/wallet-orchard
auto actionIdx = pair.first;
OrchardActionSpend orchardActionSpend = pair.second;
auto outpoint = orchardActionSpend.GetOutPoint();
auto receivedAt = orchardActionSpend.GetReceivedAt();
auto noteValue = orchardActionSpend.GetNoteValue();
// if mapWallet doesn't contain the spent outpoint's txid,
// the wallet is corrupt, so using `.at` here is fine.
UniValue entry(UniValue::VOBJ);
entry.pushKV("type", ADDR_TYPE_ORCHARD);
entry.pushKV("spend", (int) actionIdx);
entry.pushKV("txidPrev", outpoint.hash.GetHex());
entry.pushKV("outputPrev", (int) outpoint.n);
auto ua = pwalletMain->FindUnifiedAddressByReceiver(receivedAt);
assert(ua.has_value());
std::string addrStr = keyIO.EncodePaymentAddress(ua.value());
entry.pushKV("address", addrStr);
entry.pushKV("value", ValueFromAmount(noteValue));
entry.pushKV("valueZat", noteValue);
spends.push_back(entry);
}
// Orchard outputs
for (const auto& [actionIdx, orchardActionOutput] : orchardActions.GetOutputs()) {
auto noteValue = orchardActionOutput.GetNoteValue();
auto recipient = orchardActionOutput.GetRecipient();
auto memo = orchardActionOutput.GetMemo();
// Show the address that was cached at transaction construction as the
// recipient.
std::string address = keyIO.EncodePaymentAddress(
pwalletMain->GetPaymentAddressForRecipient(txid, recipient)
);
UniValue entry(UniValue::VOBJ);
entry.pushKV("type", ADDR_TYPE_ORCHARD);
entry.pushKV("output", (int) actionIdx);
entry.pushKV("outgoing", orchardActionOutput.IsOutgoing());
entry.pushKV("address", address);
entry.pushKV("value", ValueFromAmount(noteValue));
entry.pushKV("valueZat", noteValue);
addMemo(entry, memo);
outputs.push_back(entry);
}
entry.pushKV("spends", spends); entry.pushKV("spends", spends);
entry.pushKV("outputs", outputs); entry.pushKV("outputs", outputs);
@ -4625,7 +4666,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
} }
if (!recipientAddrs.insert(addr.value()).second) { if (!recipientAddrs.insert(addr.value()).second) {
throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, duplicated recipient from address: ") + addrStr); throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, duplicated recipient address: ") + addrStr);
} }
UniValue memoValue = find_value(o, "memo"); UniValue memoValue = find_value(o, "memo");

View File

@ -1190,6 +1190,8 @@ public:
pwalletdbEncryption = NULL; pwalletdbEncryption = NULL;
} }
OrchardWallet& GetOrchardWallet() { return orchardWallet; }
void SetNull(const CChainParams& params) void SetNull(const CChainParams& params)
{ {
nWalletVersion = FEATURE_BASE; nWalletVersion = FEATURE_BASE;