diff --git a/.cargo/config.offline b/.cargo/config.offline index e386661d5..28c93ea74 100644 --- a/.cargo/config.offline +++ b/.cargo/config.offline @@ -11,7 +11,7 @@ replace-with = "vendored-sources" [source."https://github.com/zcash/orchard.git"] git = "https://github.com/zcash/orchard.git" -rev = "a5f701f3186fb619b40f2dee9939e64e4c9eb7cc" +rev = "f4587f790d7317df85a9ee77ce693a06ed6d8d02" replace-with = "vendored-sources" [source."https://github.com/zcash/incrementalmerkletree.git"] diff --git a/Cargo.lock b/Cargo.lock index 7f2a74e3b..2b20b6cae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1242,7 +1242,7 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "orchard" version = "0.1.0-beta.1" -source = "git+https://github.com/zcash/orchard.git?rev=a5f701f3186fb619b40f2dee9939e64e4c9eb7cc#a5f701f3186fb619b40f2dee9939e64e4c9eb7cc" +source = "git+https://github.com/zcash/orchard.git?rev=f4587f790d7317df85a9ee77ce693a06ed6d8d02#f4587f790d7317df85a9ee77ce693a06ed6d8d02" dependencies = [ "aes", "arrayvec 0.7.2", diff --git a/Cargo.toml b/Cargo.toml index 19ab20586..9227b9a9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,7 +88,7 @@ codegen-units = 1 [patch.crates-io] hdwallet = { git = "https://github.com/nuttycom/hdwallet", rev = "576683b9f2865f1118c309017ff36e01f84420c9" } incrementalmerkletree = { git = "https://github.com/zcash/incrementalmerkletree.git", rev = "62c33e4480a71170b02b9eb7d4b0160194f414ee" } -orchard = { git = "https://github.com/zcash/orchard.git", rev = "a5f701f3186fb619b40f2dee9939e64e4c9eb7cc" } +orchard = { git = "https://github.com/zcash/orchard.git", rev = "f4587f790d7317df85a9ee77ce693a06ed6d8d02" } zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289" } zcash_encoding = { git = "https://github.com/zcash/librustzcash.git", rev = "67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289" } zcash_history = { git = "https://github.com/zcash/librustzcash.git", rev = "67cb63a5a6b1f2c4cf5a061dbe80e9a71477e289" } diff --git a/qa/rpc-tests/wallet_accounts.py b/qa/rpc-tests/wallet_accounts.py index 92ff5a8b8..1f9024468 100755 --- a/qa/rpc-tests/wallet_accounts.py +++ b/qa/rpc-tests/wallet_accounts.py @@ -173,11 +173,10 @@ class WalletAccountsTest(BitcoinTestFramework): txid = wait_and_assert_operationid_status(self.nodes[0], opid) # The wallet should detect the new note as belonging to the UA. - # TODO: Uncomment once z_viewtransaction shows Orchard details (#5186). - #tx_details = self.nodes[0].z_viewtransaction(txid) - #assert_equal(len(tx_details['outputs']), 1) - #assert_equal(tx_details['outputs'][0]['type'], 'orchard') - #assert_equal(tx_details['outputs'][0]['address'], ua0) + tx_details = self.nodes[0].z_viewtransaction(txid) + assert_equal(len(tx_details['outputs']), 1) + assert_equal(tx_details['outputs'][0]['type'], 'orchard') + assert_equal(tx_details['outputs'][0]['address'], ua0) # The new balance should not be visible with the default minconf, but should be # visible with minconf=0. diff --git a/qa/rpc-tests/wallet_listreceived.py b/qa/rpc-tests/wallet_listreceived.py index 67d083cce..48cc29724 100755 --- a/qa/rpc-tests/wallet_listreceived.py +++ b/qa/rpc-tests/wallet_listreceived.py @@ -12,8 +12,10 @@ from test_framework.util import ( assert_raises_message, connect_nodes_bi, get_coinbase_address, + nuparams, DEFAULT_FEE, - DEFAULT_FEE_ZATS + DEFAULT_FEE_ZATS, + NU5_BRANCH_ID, ) from test_framework.util import wait_and_assert_operationid_status, start_nodes from decimal import Decimal @@ -33,7 +35,12 @@ class ListReceivedTest (BitcoinTestFramework): def setup_network(self): self.nodes = start_nodes( 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, 1, 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]['blockindex'], 1) assert 'blocktime' in r[0] - + assert_equal(r[1]['pool'], 'transparent') assert_equal(r[1]['txid'], txid_taddr) assert_equal(r[1]['amount'], Decimal('0.2')) @@ -435,9 +442,117 @@ class ListReceivedTest (BitcoinTestFramework): assert_equal(r[1]['blockindex'], -1) # not yet mined 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) + + acct_node0 = self.nodes[0].z_getnewaccount()['account'] + ua_node0 = self.nodes[0].z_getaddressforaccount(acct_node0, ['sapling', 'orchard'])['unifiedaddress'] + + 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]['action'], 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]['action'], 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.3')}, + {'address': ua_node0, 'amount': Decimal('0.2')} + ]) + 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']), 3) # one output + one change output we can see + + spends = pt['spends'] + assert_equal(spends[0]['type'], 'orchard') + assert_equal(spends[0]['action'], 0) + assert_equal(spends[0]['txidPrev'], txid0) + assert_equal(spends[0]['actionPrev'], 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'], ua_node0) + assert_equal(outputs[0]['value'], Decimal('0.2')) + assert_equal(outputs[0]['valueZat'], 20000000) + assert_equal(outputs[0]['outgoing'], True) + assert_equal(outputs[0]['walletInternal'], False) + assert_equal(outputs[0]['memo'], no_memo) + + assert_equal(outputs[1]['type'], 'orchard') + assert_equal(outputs[1]['address'], uaso) + assert_equal(outputs[1]['value'], Decimal('0.3')) + assert_equal(outputs[1]['valueZat'], 30000000) + assert_equal(outputs[1]['outgoing'], False) + assert_equal(outputs[1]['walletInternal'], False) + assert_equal(outputs[1]['memo'], no_memo) + + # Verify that we observe the change output + print("###", uao, "###") + assert_equal(outputs[2]['type'], 'orchard') + assert_equal(outputs[2]['value'], Decimal('0.49999')) + assert_equal(outputs[2]['valueZat'], 49999000) + assert_equal(outputs[2]['outgoing'], False) + assert_equal(outputs[2]['walletInternal'], True) + assert_equal(outputs[2]['memo'], no_memo) + # The change address should have been erased + assert_true('address' not in outputs[2]) + def run_test(self): self.test_received_sprout(200) self.test_received_sapling(214) + self.test_received_orchard(230) if __name__ == '__main__': diff --git a/src/gtest/test_keys.cpp b/src/gtest/test_keys.cpp index 6d68cdb57..6c247ef7f 100644 --- a/src/gtest/test_keys.cpp +++ b/src/gtest/test_keys.cpp @@ -69,40 +69,8 @@ TEST(Keys, EncodeAndDecodeSapling) #define MAKE_STRING(x) std::string((x), (x) + sizeof(x)) namespace libzcash { - class ReceiverToString { - public: - ReceiverToString() {} - - std::string operator()(const OrchardRawAddress &zaddr) const { - CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); - ss << zaddr; - return tfm::format("Orchard(%s)", HexStr(ss.begin(), ss.end())); - } - - std::string operator()(const SaplingPaymentAddress &zaddr) const { - CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); - ss << zaddr; - return tfm::format("Sapling(%s)", HexStr(ss.begin(), ss.end())); - } - - std::string operator()(const CScriptID &p2sh) const { - return tfm::format("P2SH(%s)", p2sh.GetHex()); - } - - std::string operator()(const CKeyID &p2pkh) const { - return tfm::format("P2PKH(%s)", p2pkh.GetHex()); - } - - std::string operator()(const UnknownReceiver &unknown) const { - return tfm::format( - "Unknown(%x, %s)", - unknown.typecode, - HexStr(unknown.data.begin(), unknown.data.end())); - } - }; - void PrintTo(const Receiver& receiver, std::ostream* os) { - *os << std::visit(ReceiverToString(), receiver); + *os << DebugPrintReceiver(receiver); } void PrintTo(const UnifiedAddress& ua, std::ostream* os) { *os << "UnifiedAddress(" << testing::PrintToString(ua.GetReceiversAsParsed()) << ")"; diff --git a/src/gtest/test_keystore.cpp b/src/gtest/test_keystore.cpp index 5e9863cc1..6cdc8023d 100644 --- a/src/gtest/test_keystore.cpp +++ b/src/gtest/test_keystore.cpp @@ -564,8 +564,8 @@ TEST(KeystoreTests, StoreAndRetrieveUFVK) { // we trial-decrypt diversifiers (which also means we learn the index). auto ufvkmetaUnadded = keyStore.GetUFVKMetadataForReceiver(saplingReceiver); EXPECT_TRUE(ufvkmetaUnadded.has_value()); - EXPECT_EQ(ufvkmetaUnadded.value().first, ufvkid); - EXPECT_EQ(ufvkmetaUnadded.value().second.value(), addrPair.second); + EXPECT_EQ(ufvkmetaUnadded.value().GetUFVKId(), ufvkid); + EXPECT_EQ(ufvkmetaUnadded.value().GetDiversifierIndex().value(), addrPair.second); // Adding the Sapling addr -> ivk map entry causes us to find the same UFVK, // but as we're no longer trial-decrypting we don't learn the index. @@ -574,8 +574,8 @@ TEST(KeystoreTests, StoreAndRetrieveUFVK) { auto ufvkmeta = keyStore.GetUFVKMetadataForReceiver(saplingReceiver); EXPECT_TRUE(ufvkmeta.has_value()); - EXPECT_EQ(ufvkmeta.value().first, ufvkid); - EXPECT_FALSE(ufvkmeta.value().second.has_value()); + EXPECT_EQ(ufvkmeta.value().GetUFVKId(), ufvkid); + EXPECT_FALSE(ufvkmeta.value().GetDiversifierIndex().has_value()); } TEST(KeystoreTests, StoreAndRetrieveUFVKByOrchard) { @@ -603,8 +603,8 @@ TEST(KeystoreTests, StoreAndRetrieveUFVKByOrchard) { // we trial-decrypt diversifiers (which also means we learn the index). auto ufvkmetaUnadded = keyStore.GetUFVKMetadataForReceiver(orchardReceiver); EXPECT_TRUE(ufvkmetaUnadded.has_value()); - EXPECT_EQ(ufvkmetaUnadded.value().first, ufvkid); - EXPECT_EQ(ufvkmetaUnadded.value().second.value(), addrPair.second); + EXPECT_EQ(ufvkmetaUnadded.value().GetUFVKId(), ufvkid); + EXPECT_EQ(ufvkmetaUnadded.value().GetDiversifierIndex().value(), addrPair.second); } TEST(KeystoreTests, AddTransparentReceiverForUnifiedAddress) { @@ -627,7 +627,7 @@ TEST(KeystoreTests, AddTransparentReceiverForUnifiedAddress) { ufvkmeta = keyStore.GetUFVKMetadataForReceiver(addrPair.first.GetP2PKHReceiver().value()); EXPECT_TRUE(ufvkmeta.has_value()); - EXPECT_EQ(ufvkmeta.value().first, ufvkid); + EXPECT_EQ(ufvkmeta.value().GetUFVKId(), ufvkid); } diff --git a/src/keystore.cpp b/src/keystore.cpp index 1af6a3218..30a8ede83 100644 --- a/src/keystore.cpp +++ b/src/keystore.cpp @@ -383,13 +383,13 @@ std::optional CBasicKeyStore::GetUnifiedF } } -std::optional>> +std::optional CBasicKeyStore::GetUFVKMetadataForReceiver(const libzcash::Receiver& receiver) const { return std::visit(FindUFVKId(*this), receiver); } -std::optional>> +std::optional CBasicKeyStore::GetUFVKMetadataForAddress(const libzcash::UnifiedAddress& addr) const { std::optional ufvkId; @@ -398,33 +398,36 @@ CBasicKeyStore::GetUFVKMetadataForAddress(const libzcash::UnifiedAddress& addr) for (const auto& receiver : addr) { // skip unknown receivers if (libzcash::HasKnownReceiverType(receiver)) { - auto tmp = GetUFVKMetadataForReceiver(receiver); - if (ufvkId.has_value() && tmp.has_value()) { + auto rmeta = GetUFVKMetadataForReceiver(receiver); + // We should never generate unified addresses with internal receivers + assert(!(rmeta.has_value() && rmeta.value().IsInternalAddress())); + + if (ufvkId.has_value() && rmeta.has_value()) { // If the unified address contains receivers that are associated with // different UFVKs, we cannot return a singular value. - if (tmp.value().first != ufvkId.value()) { + if (rmeta.value().GetUFVKId() != ufvkId.value()) { return std::nullopt; } - if (tmp.value().second.has_value()) { + if (rmeta.value().GetDiversifierIndex().has_value()) { if (j.has_value()) { - if (tmp.value().second.value() != j.value()) { + if (rmeta.value().GetDiversifierIndex().value() != j.value()) { jConflict = true; j = std::nullopt; } } else if (!jConflict) { - j = tmp.value().second.value(); + j = rmeta.value().GetDiversifierIndex().value(); } } - } else if (tmp.has_value()) { - ufvkId = tmp.value().first; - j = tmp.value().second; + } else if (rmeta.has_value()) { + ufvkId = rmeta.value().GetUFVKId(); + j = rmeta.value().GetDiversifierIndex(); } } } if (ufvkId.has_value()) { - return std::make_pair(ufvkId.value(), j); + return AddressUFVKMetadata(ufvkId.value(), j, false); } else { return std::nullopt; } @@ -465,22 +468,27 @@ std::optional CBasicKeyStore::GetUFVKIdForViewingKey(const lib return result; } -std::optional>> -FindUFVKId::operator()(const libzcash::OrchardRawAddress& orchardAddr) const { +// +// FindUFVKId :: (KeyStore, Receiver) -> std::optional +// + +std::optional FindUFVKId::operator()(const libzcash::OrchardRawAddress& orchardAddr) const { for (const auto& [k, v] : keystore.mapUnifiedFullViewingKeys) { auto fvk = v.GetOrchardKey(); if (fvk.has_value()) { auto d_idx = fvk.value().ToIncomingViewingKey().DecryptDiversifier(orchardAddr); if (d_idx.has_value()) { - return std::make_pair(k, d_idx); + return AddressUFVKMetadata(k, d_idx, false); + } + auto internal_d_idx = fvk.value().ToInternalIncomingViewingKey().DecryptDiversifier(orchardAddr); + if (internal_d_idx.has_value()) { + return AddressUFVKMetadata(k, internal_d_idx, true); } } } return std::nullopt; } - -std::optional>> -FindUFVKId::operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const { +std::optional FindUFVKId::operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const { const auto saplingIvk = keystore.mapSaplingIncomingViewingKeys.find(saplingAddr); if (saplingIvk != keystore.mapSaplingIncomingViewingKeys.end()) { // We have either generated this as a receiver via `z_getaddressforaccount` or a @@ -488,7 +496,7 @@ FindUFVKId::operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const // this via trial-decryption of a note. const auto ufvkId = keystore.mapSaplingKeyUnified.find(saplingIvk->second); if (ufvkId != keystore.mapSaplingKeyUnified.end()) { - return std::make_pair(ufvkId->second, std::nullopt); + return AddressUFVKMetadata(ufvkId->second, std::nullopt, false); } else { // If we have the addr -> ivk map entry but not the ivk -> UFVK map entry, // then this is definitely a legacy Sapling address. @@ -506,7 +514,13 @@ FindUFVKId::operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const auto d_idx = dfvk.value().DecryptDiversifier(saplingAddr.d); auto derived_addr = dfvk.value().Address(d_idx); if (derived_addr.has_value() && derived_addr.value() == saplingAddr) { - return std::make_pair(k, d_idx); + return AddressUFVKMetadata(k, d_idx, false); + } + + auto internal_d_idx = dfvk.value().DecryptInternalDiversifier(saplingAddr.d); + auto derived_internal_addr = dfvk.value().InternalAddress(internal_d_idx); + if (derived_internal_addr.has_value() && derived_internal_addr.value() == saplingAddr) { + return AddressUFVKMetadata(k, internal_d_idx, true); } } } @@ -514,25 +528,26 @@ FindUFVKId::operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const // We definitely don't know of any UFVK linked to this Sapling address. return std::nullopt; } -std::optional>> -FindUFVKId::operator()(const CScriptID& scriptId) const { +std::optional FindUFVKId::operator()(const CScriptID& scriptId) const { const auto metadata = keystore.mapP2SHUnified.find(scriptId); if (metadata != keystore.mapP2SHUnified.end()) { - return metadata->second; + // At present we never generate transparent internal addresses, so this + // must be an external address + return AddressUFVKMetadata(metadata->second.first, metadata->second.second, false); } else { return std::nullopt; } } -std::optional>> -FindUFVKId::operator()(const CKeyID& keyId) const { +std::optional FindUFVKId::operator()(const CKeyID& keyId) const { const auto metadata = keystore.mapP2PKHUnified.find(keyId); if (metadata != keystore.mapP2PKHUnified.end()) { - return metadata->second; + // At present we never generate transparent internal addresses, so this + // must be an external address + return AddressUFVKMetadata(metadata->second.first, metadata->second.second, false); } else { return std::nullopt; } } -std::optional>> -FindUFVKId::operator()(const libzcash::UnknownReceiver& receiver) const { +std::optional FindUFVKId::operator()(const libzcash::UnknownReceiver& receiver) const { return std::nullopt; } diff --git a/src/keystore.h b/src/keystore.h index 10a421cc8..34184299e 100644 --- a/src/keystore.h +++ b/src/keystore.h @@ -18,6 +18,20 @@ #include +class AddressUFVKMetadata { +private: + libzcash::UFVKId ufvkId; + std::optional j; + bool internalAddress; +public: + AddressUFVKMetadata(libzcash::UFVKId ufvkId, std::optional j, bool internalAddress) + : ufvkId(ufvkId), j(j), internalAddress(internalAddress) {} + + libzcash::UFVKId GetUFVKId() const { return ufvkId; } + std::optional GetDiversifierIndex() const { return j; } + bool IsInternalAddress() const { return internalAddress; } +}; + /** A virtual base class for key stores */ class CKeyStore { @@ -127,8 +141,7 @@ public: virtual std::optional GetUnifiedFullViewingKey( const libzcash::UFVKId& keyId) const = 0; - virtual std::optional>> - GetUFVKMetadataForReceiver( + virtual std::optional GetUFVKMetadataForReceiver( const libzcash::Receiver& receiver) const = 0; /** @@ -136,8 +149,7 @@ public: * UFVK, return that key's metadata. If all the receivers correspond to * the same diversifier index, that diversifier index is also returned. */ - virtual std::optional>> - GetUFVKMetadataForAddress( + virtual std::optional GetUFVKMetadataForAddress( const libzcash::UnifiedAddress& addr) const = 0; virtual std::optional GetUFVKIdForViewingKey( @@ -379,29 +391,27 @@ public: virtual std::optional GetUnifiedFullViewingKey( const libzcash::UFVKId& keyId) const; - virtual std::optional>> - GetUFVKMetadataForReceiver( + virtual std::optional GetUFVKMetadataForReceiver( const libzcash::Receiver& receiver) const; std::optional GetUFVKForReceiver( const libzcash::Receiver& receiver) const { auto ufvkMeta = GetUFVKMetadataForReceiver(receiver); if (ufvkMeta.has_value()) { - return GetUnifiedFullViewingKey(ufvkMeta.value().first); + return GetUnifiedFullViewingKey(ufvkMeta.value().GetUFVKId()); } else { return std::nullopt; } } - virtual std::optional>> - GetUFVKMetadataForAddress( + virtual std::optional GetUFVKMetadataForAddress( const libzcash::UnifiedAddress& addr) const; std::optional GetUFVKForAddress( const libzcash::UnifiedAddress& addr) const { auto ufvkMeta = GetUFVKMetadataForAddress(addr); if (ufvkMeta.has_value()) { - return GetUnifiedFullViewingKey(ufvkMeta.value().first); + return GetUnifiedFullViewingKey(ufvkMeta.value().GetUFVKId()); } else { return std::nullopt; } @@ -425,16 +435,11 @@ private: public: FindUFVKId(const CBasicKeyStore& keystore): keystore(keystore) {} - std::optional>> - operator()(const libzcash::OrchardRawAddress& orchardAddr) const; - std::optional>> - operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const; - std::optional>> - operator()(const CScriptID& scriptId) const; - std::optional>> - operator()(const CKeyID& keyId) const; - std::optional>> - operator()(const libzcash::UnknownReceiver& receiver) const; + std::optional operator()(const libzcash::OrchardRawAddress& orchardAddr) const; + std::optional operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const; + std::optional operator()(const CScriptID& scriptId) const; + std::optional operator()(const CKeyID& keyId) const; + std::optional operator()(const libzcash::UnknownReceiver& receiver) const; }; #endif // BITCOIN_KEYSTORE_H diff --git a/src/primitives/transaction.cpp b/src/primitives/transaction.cpp index 2263ab91b..b8f3358c0 100644 --- a/src/primitives/transaction.cpp +++ b/src/primitives/transaction.cpp @@ -81,6 +81,11 @@ std::string SaplingOutPoint::ToString() const 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) { prevout = prevoutIn; diff --git a/src/rust/include/rust/orchard/wallet.h b/src/rust/include/rust/orchard/wallet.h index 2e3a344f7..41c4398aa 100644 --- a/src/rust/include/rust/orchard/wallet.h +++ b/src/rust/include/rust/orchard/wallet.h @@ -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 * 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 - * 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 + * 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 * by the caller. * * The provided bundle must be a component of the transaction from which `txid` was @@ -263,6 +263,59 @@ void orchard_wallet_get_filtered_notes( push_note_callback_t push_cb ); +/** + * A C struct used to transfer Orchard action spend information across the FFI boundary. + * This must have the same in-memory representation as the `FFIActionSpend` type in + * orchard_ffi/wallet.rs. + */ +struct RawOrchardActionSpend { + uint32_t spendActionIdx; + unsigned char outpointTxId[32]; + uint32_t outpointActionIdx; + OrchardRawAddressPtr* receivedAt; + CAmount noteValue; +}; + +/** + * A C struct used to transfer Orchard action output information across the FFI boundary. + * This must have the same in-memory representation as the `FFIActionOutput` type in + * orchard_ffi/wallet.rs. + */ +struct RawOrchardActionOutput { + uint32_t outputActionIdx; + OrchardRawAddressPtr* addr; + CAmount noteValue; + unsigned char memo[512]; + bool isOutgoing; +}; + +typedef void (*push_spend_t)(void* callbackReceiver, const RawOrchardActionSpend data); + +typedef void (*push_output_t)(void* callbackReceiver, const RawOrchardActionOutput data); + +/** + * Trial-decrypts the specfied Orchard bundle, and uses the provided callbacks to pass + * `RawOrchardActionSpend` and `RawOrchardActionOutput` values (corresponding to the + * actions of that bundle) to the provided result receiver. + * + * Note that the callbacks can perform any necessary conversion from a + * `RawOrchardActionSpend` or `RawOrchardActionOutput` value in addition to modifying the + * provided result receiver. + * + * `raw_ovks` must be a pointer to an array of `unsigned char[32]`. + * + * The `addr` pointer on each `RawOrchardActionOutput` value must be freed using + * `orchard_address_free`. + */ +bool orchard_wallet_get_txdata( + const OrchardWalletPtr* wallet, + const OrchardBundlePtr* bundle, + const unsigned char* raw_ovks, + size_t raw_ovks_len, + void* callbackReceiver, + push_spend_t push_spend_cb, + push_output_t push_output_cb + ); typedef void (*push_txid_callback_t)(void* resultVector, unsigned char txid[32]); diff --git a/src/rust/src/wallet.rs b/src/rust/src/wallet.rs index 8e8bc9e1f..6d4670f47 100644 --- a/src/rust/src/wallet.rs +++ b/src/rust/src/wallet.rs @@ -16,7 +16,7 @@ use zcash_primitives::{ use orchard::{ bundle::Authorized, - keys::{FullViewingKey, IncomingViewingKey, SpendingKey}, + keys::{FullViewingKey, IncomingViewingKey, OutgoingViewingKey, SpendingKey}, note::Nullifier, tree::{MerkleHashOrchard, MerklePath}, Address, Bundle, Note, @@ -359,7 +359,7 @@ impl Wallet { .cloned() .collect::>(); - for (action_idx, ivk, note, recipient, memo) in bundle.decrypt_outputs_for_keys(&keys) { + for (action_idx, ivk, note, recipient, memo) in bundle.decrypt_outputs_with_keys(&keys) { assert!(self.add_decrypted_note( None, txid, @@ -991,6 +991,114 @@ pub extern "C" fn orchard_wallet_get_filtered_notes( } } +/// A type used to pass decrypted spend information across the FFI boundary. +/// This must have the same representation as `struct RawOrchardSpendData` +/// in `rust/include/rust/orchard/wallet.h`. +#[repr(C)] +pub struct FFIActionSpend { + spend_action_idx: u32, + outpoint_txid: [u8; 32], + outpoint_action_idx: u32, + received_at: *mut Address, + value: i64, +} + +/// A type used to pass decrypted output information across the FFI boundary. +/// This must have the same representation as `struct RawOrchardOutputData` +/// in `rust/include/rust/orchard/wallet.h`. +#[repr(C)] +pub struct FFIActionOutput { + action_idx: u32, + recipient: *mut Address, + value: i64, + memo: [u8; 512], + is_outgoing: bool, +} + +/// A C++-allocated function pointer that can send a FFIActionSpend value +/// to a receiver. +pub type SpendPushCB = unsafe extern "C" fn(obj: Option, data: FFIActionSpend); + +/// A C++-allocated function pointer that can send a FFIActionOutput value +/// to a receiver. +pub type OutputPushCB = + unsafe extern "C" fn(obj: Option, data: FFIActionOutput); + +#[no_mangle] +pub extern "C" fn orchard_wallet_get_txdata( + wallet: *const Wallet, + bundle: *const Bundle, + raw_ovks: *const [u8; 32], + raw_ovks_len: usize, + callback_receiver: Option, + spend_push_cb: Option, + output_push_cb: Option, +) -> bool { + let wallet = unsafe { wallet.as_ref() }.expect("Wallet pointer may not be null."); + let raw_ovks = unsafe { slice::from_raw_parts(raw_ovks, raw_ovks_len) }; + let ovks: Vec = raw_ovks + .iter() + .map(|k| OutgoingViewingKey::from(*k)) + .collect(); + if let Some(bundle) = unsafe { bundle.as_ref() } { + let ivks = wallet + .key_store + .viewing_keys + .keys() + .cloned() + .collect::>(); + + let incoming: BTreeMap = bundle + .decrypt_outputs_with_keys(&ivks) + .into_iter() + .map(|(idx, _, note, addr, memo)| (idx, (note, addr, memo))) + .collect(); + + let outgoing: BTreeMap = bundle + .recover_outputs_with_ovks(&ovks) + .into_iter() + .map(|(idx, _, note, addr, memo)| (idx, (note, addr, memo))) + .collect(); + + for (idx, action) in bundle.actions().iter().enumerate() { + let nf = action.nullifier(); + if let Some(spend) = wallet.nullifiers.get(nf).and_then(|outpoint| { + wallet + .wallet_received_notes + .get(&outpoint.txid) + .and_then(|txnotes| txnotes.decrypted_notes.get(&outpoint.action_idx)) + .map(|dnote| FFIActionSpend { + spend_action_idx: idx as u32, + outpoint_txid: *outpoint.txid.as_ref(), + 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, + }) + }) { + unsafe { (spend_push_cb.unwrap())(callback_receiver, spend) }; + } + + if let Some(((note, addr, memo), is_outgoing)) = incoming + .get(&idx) + .map(|n| (n, false)) + .or_else(|| outgoing.get(&idx).map(|n| (n, true))) + { + let output = FFIActionOutput { + action_idx: idx as u32, + recipient: Box::into_raw(Box::new(*addr)), + value: note.value().inner() as i64, + memo: *memo, + is_outgoing, + }; + unsafe { (output_push_cb.unwrap())(callback_receiver, output) }; + } + } + true + } else { + false + } +} + pub type PushTxId = unsafe extern "C" fn(obj: Option, txid: *const [u8; 32]); #[no_mangle] diff --git a/src/wallet/orchard.h b/src/wallet/orchard.h index 3ebc5e5cf..7812f7cc0 100644 --- a/src/wallet/orchard.h +++ b/src/wallet/orchard.h @@ -110,6 +110,79 @@ public: } }; +class OrchardActionSpend { +private: + OrchardOutPoint outPoint; + libzcash::OrchardRawAddress receivedAt; + CAmount noteValue; +public: + OrchardActionSpend(OrchardOutPoint outPoint, libzcash::OrchardRawAddress receivedAt, CAmount noteValue): + outPoint(outPoint), receivedAt(receivedAt), noteValue(noteValue) { } + + OrchardOutPoint GetOutPoint() const { + return outPoint; + } + + const libzcash::OrchardRawAddress& GetReceivedAt() const { + return receivedAt; + } + + CAmount GetNoteValue() const { + return noteValue; + } +}; + +class OrchardActionOutput { +private: + libzcash::OrchardRawAddress recipient; + CAmount noteValue; + std::array memo; + bool isOutgoing; +public: + OrchardActionOutput( + libzcash::OrchardRawAddress recipient, CAmount noteValue, std::array memo, bool isOutgoing): + recipient(recipient), noteValue(noteValue), memo(memo), isOutgoing(isOutgoing) { } + + const libzcash::OrchardRawAddress& GetRecipient() const { + return recipient; + } + + CAmount GetNoteValue() const { + return noteValue; + } + + const std::array& GetMemo() const { + return memo; + } + + bool IsOutgoing() const { + return isOutgoing; + } +}; + +class OrchardActions { +private: + std::map spends; + std::map outputs; +public: + OrchardActions() {} + + void AddSpend(uint32_t actionIdx, OrchardActionSpend spend) { + spends.insert({actionIdx, spend}); + } + + void AddOutput(uint32_t actionIdx, OrchardActionOutput output) { + outputs.insert({actionIdx, output}); + } + + const std::map& GetSpends() { + return spends; + } + + const std::map& GetOutputs() { + return outputs; + } +}; class OrchardWallet { @@ -355,6 +428,41 @@ public: void GarbageCollect() { orchard_wallet_gc_note_commitment_tree(inner.get()); } + + static void PushSpendAction(void* receiver, RawOrchardActionSpend rawSpend) { + uint256 txid; + std::move(std::begin(rawSpend.outpointTxId), std::end(rawSpend.outpointTxId), txid.begin()); + auto spend = OrchardActionSpend( + OrchardOutPoint(txid, rawSpend.outpointActionIdx), + libzcash::OrchardRawAddress(rawSpend.receivedAt), + rawSpend.noteValue); + reinterpret_cast(receiver)->AddSpend(rawSpend.spendActionIdx, spend); + } + + static void PushOutputAction(void* receiver, RawOrchardActionOutput rawOutput) { + std::array memo; + std::move(std::begin(rawOutput.memo), std::end(rawOutput.memo), memo.begin()); + auto output = OrchardActionOutput( + libzcash::OrchardRawAddress(rawOutput.addr), + rawOutput.noteValue, + memo, + rawOutput.isOutgoing); + + reinterpret_cast(receiver)->AddOutput(rawOutput.outputActionIdx, output); + } + + OrchardActions GetTxActions(const CTransaction& tx, const std::vector& ovks) const { + OrchardActions result; + orchard_wallet_get_txdata( + inner.get(), + tx.GetOrchardBundle().inner.get(), + reinterpret_cast(ovks.data()), + ovks.size(), + &result, + PushSpendAction, + PushOutputAction); + return result; + } }; class OrchardWalletNoteCommitmentTreeWriter diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 3db60c63a..10718599c 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -59,6 +59,7 @@ using namespace libzcash; const std::string ADDR_TYPE_SPROUT = "sprout"; const std::string ADDR_TYPE_SAPLING = "sapling"; +const std::string ADDR_TYPE_ORCHARD = "orchard"; extern UniValue TxJoinSplitToJSON(const CTransaction& tx); @@ -3135,7 +3136,7 @@ UniValue z_getaddressforaccount(const UniValue& params, bool fHelp) } if (receivers.empty()) { // Default is the best and second-best shielded pools, and the transparent pool. - receivers = {ReceiverType::P2PKH, ReceiverType::Sapling, ReceiverType::Orchard}; + receivers = CWallet::DefaultReceiverTypes(); } std::optional j = std::nullopt; @@ -4036,7 +4037,7 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp) " \"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" - " \"actionspend\" : n, (numeric, orchard) the index of the action within orchard bundle\n" + " \"action\" : n, (numeric, orchard) the index of the action within orchard bundle\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" @@ -4054,9 +4055,10 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp) " \"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" - " \"actionoutput\" : n, (numeric, orchard) the index of the action within the orchard bundle\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" + " \"action\" : n, (numeric, orchard) the index of the action within the orchard bundle\n" + " \"address\" : \"zcashaddress\", (string) The Zcash address involved in the transaction. Not included for change outputs.\n" + " \"outgoing\" : true|false (boolean) True if the output is not for an address in the wallet\n" + " \"walletInternal\" : true|false (boolean) True if this is a change output.\n" " \"value\" : x.xxx (numeric) The amount in " + CURRENCY_UNIT + "\n" " \"valueZat\" : xxxx (numeric) The amount in zatoshis\n" " \"memo\" : \"hexmemo\", (string) hexadecimal string representation of the memo field\n" @@ -4073,15 +4075,15 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp) LOCK2(cs_main, pwalletMain->cs_wallet); - uint256 hash; - hash.SetHex(params[0].get_str()); + uint256 txid; + txid.SetHex(params[0].get_str()); 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"); - 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 outputs(UniValue::VARR); @@ -4168,6 +4170,30 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp) auto legacyAcctOVKs = legacyKey.GetOVKsForShielding(); ovks.insert(legacyAcctOVKs.first); ovks.insert(legacyAcctOVKs.second); + + // Generate the OVKs for shielding for all unified key components + for (const auto& [_, ufvkid] : pwalletMain->mapUnifiedAccountKeys) { + auto ufvk = pwalletMain->GetUnifiedFullViewingKey(ufvkid); + if (ufvk.has_value()) { + auto tkey = ufvk.value().GetTransparentKey(); + if (tkey.has_value()) { + auto tovks = tkey.value().GetOVKsForShielding(); + ovks.insert(tovks.first); + ovks.insert(tovks.second); + } + auto skey = ufvk.value().GetSaplingKey(); + if (skey.has_value()) { + auto sovks = skey.value().GetOVKs(); + ovks.insert(sovks.first); + ovks.insert(sovks.second); + } + auto okey = ufvk.value().GetOrchardKey(); + if (okey.has_value()) { + ovks.insert(okey.value().ToExternalOutgoingViewingKey()); + ovks.insert(okey.value().ToInternalOutgoingViewingKey()); + } + } + } } // Sapling spends @@ -4196,23 +4222,22 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp) assert(pwalletMain->GetSaplingFullViewingKey(wtxPrev.mapSaplingNoteData.at(op).ivk, extfvk)); ovks.insert(extfvk.fvk.ovk); - // 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 = keyIO.EncodePaymentAddress([&]() { - auto ua = pwalletMain->FindUnifiedAddressByReceiver(pa); - if (ua.has_value()) { - return libzcash::PaymentAddress{ua.value()}; - } else { - return libzcash::PaymentAddress{pa}; - } - }()); + // Show the address that was cached at transaction construction as the + // recipient. + std::optional addrStr; + if (!pwalletMain->IsInternalRecipient(pa)) { + auto addr = pwalletMain->GetPaymentAddressForRecipient(txid, pa); + addrStr = keyIO.EncodePaymentAddress(addr); + } UniValue entry(UniValue::VOBJ); entry.pushKV("type", ADDR_TYPE_SAPLING); entry.pushKV("spend", (int)i); entry.pushKV("txidPrev", op.hash.GetHex()); entry.pushKV("outputPrev", (int)op.n); - entry.pushKV("address", address); + if (addrStr.has_value()) { + entry.pushKV("address", addrStr.value()); + } entry.pushKV("value", ValueFromAmount(notePt.value())); entry.pushKV("valueZat", notePt.value()); spends.push_back(entry); @@ -4220,7 +4245,7 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp) // Sapling outputs for (uint32_t i = 0; i < wtx.vShieldedOutput.size(); ++i) { - auto op = SaplingOutPoint(hash, i); + auto op = SaplingOutPoint(txid, i); SaplingNotePlaintext notePt; SaplingPaymentAddress pa; @@ -4250,28 +4275,88 @@ UniValue z_viewtransaction(const UniValue& params, bool fHelp) } auto memo = notePt.memo(); - // 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 = keyIO.EncodePaymentAddress([&]() { - auto ua = pwalletMain->FindUnifiedAddressByReceiver(pa); - if (ua.has_value()) { - return libzcash::PaymentAddress{ua.value()}; - } else { - return libzcash::PaymentAddress{pa}; - } - }()); + // Show the address that was cached at transaction construction as the + // recipient. + std::optional addrStr; + bool isInternal = pwalletMain->IsInternalRecipient(pa); + if (!isInternal) { + auto addr = pwalletMain->GetPaymentAddressForRecipient(txid, pa); + addrStr = keyIO.EncodePaymentAddress(addr); + } UniValue entry(UniValue::VOBJ); entry.pushKV("type", ADDR_TYPE_SAPLING); entry.pushKV("output", (int)op.n); entry.pushKV("outgoing", isOutgoing); - entry.pushKV("address", address); + entry.pushKV("walletInternal", isInternal); + if (addrStr.has_value()) { + entry.pushKV("address", addrStr.value()); + } entry.pushKV("value", ValueFromAmount(notePt.value())); entry.pushKV("valueZat", notePt.value()); addMemo(entry, memo); outputs.push_back(entry); } - // TODO unified addresses, orchard, see #5186 + + std::vector ovksVector(ovks.begin(), ovks.end()); + OrchardActions orchardActions = wtx.RecoverOrchardActions(ovksVector); + + // Orchard spends + for (auto & pair : orchardActions.GetSpends()) { + auto actionIdx = pair.first; + OrchardActionSpend orchardActionSpend = pair.second; + auto outpoint = orchardActionSpend.GetOutPoint(); + auto receivedAt = orchardActionSpend.GetReceivedAt(); + auto noteValue = orchardActionSpend.GetNoteValue(); + + std::optional addrStr; + if (!pwalletMain->IsInternalRecipient(receivedAt)) { + auto ua = pwalletMain->FindUnifiedAddressByReceiver(receivedAt); + assert(ua.has_value()); + addrStr = keyIO.EncodePaymentAddress(ua.value()); + } + + UniValue entry(UniValue::VOBJ); + entry.pushKV("type", ADDR_TYPE_ORCHARD); + entry.pushKV("action", (int) actionIdx); + entry.pushKV("txidPrev", outpoint.hash.GetHex()); + entry.pushKV("actionPrev", (int) outpoint.n); + if (addrStr.has_value()) { + entry.pushKV("address", addrStr.value()); + } + 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::optional addrStr; + bool isInternal = pwalletMain->IsInternalRecipient(recipient); + if (!isInternal) { + auto addr = pwalletMain->GetPaymentAddressForRecipient(txid, recipient); + addrStr = keyIO.EncodePaymentAddress(addr); + } + + UniValue entry(UniValue::VOBJ); + entry.pushKV("type", ADDR_TYPE_ORCHARD); + entry.pushKV("action", (int) actionIdx); + entry.pushKV("outgoing", orchardActionOutput.IsOutgoing()); + entry.pushKV("walletInternal", isInternal); + if (addrStr.has_value()) { + entry.pushKV("address", addrStr.value()); + } + entry.pushKV("value", ValueFromAmount(noteValue)); + entry.pushKV("valueZat", noteValue); + addMemo(entry, memo); + outputs.push_back(entry); + } entry.pushKV("spends", spends); entry.pushKV("outputs", outputs); @@ -4647,7 +4732,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp) } 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"); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 5c01d1792..28e8f03ad 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -61,6 +61,10 @@ const char * DEFAULT_WALLET_DAT = "wallet.dat"; */ CFeeRate CWallet::minTxFee = CFeeRate(DEFAULT_TRANSACTION_MINFEE); +std::set CWallet::DefaultReceiverTypes() { + return {ReceiverType::P2PKH, ReceiverType::Sapling, ReceiverType::Orchard}; +} + /** @defgroup mapWallet * * @{ @@ -853,6 +857,123 @@ bool CWallet::LoadUnifiedAddressMetadata(const ZcashdUnifiedAddressMetadata &add addrmeta.GetReceiverTypes()); } +PaymentAddress CWallet::GetPaymentAddressForRecipient( + const uint256& txid, + const libzcash::RecipientAddress& recipient) const +{ + auto self = this; + auto defaultAddress = [&]() -> PaymentAddress { + auto ufvk = self->GetUFVKForReceiver(RecipientAddressToReceiver(recipient)); + return std::visit(match { + [&](const CKeyID& addr) { + auto ua = self->FindUnifiedAddressByReceiver(addr); + if (ua.has_value()) { + return libzcash::PaymentAddress{ua.value()}; + } else { + return libzcash::PaymentAddress{addr}; + } + }, + [&](const CScriptID& addr) { + auto ua = self->FindUnifiedAddressByReceiver(addr); + if (ua.has_value()) { + return libzcash::PaymentAddress{ua.value()}; + } else { + return libzcash::PaymentAddress{addr}; + } + }, + [&](const SaplingPaymentAddress& addr) { + auto ua = self->FindUnifiedAddressByReceiver(addr); + if (ua.has_value()) { + return libzcash::PaymentAddress{ua.value()}; + } else if (ufvk.has_value() && ufvk->GetSaplingKey().has_value()) { + auto saplingKey = ufvk->GetSaplingKey().value(); + auto j = saplingKey.DecryptDiversifier(addr.d); + // std::get is safe here because we know we have a valid Sapling diversifier index + auto defaultUA = std::get>( + ufvk->Address(j, CWallet::DefaultReceiverTypes())); + return libzcash::PaymentAddress{defaultUA.first}; + } else { + return libzcash::PaymentAddress{addr}; + } + }, + [&](const OrchardRawAddress& addr) { + auto ua = self->FindUnifiedAddressByReceiver(addr); + if (ua.has_value()) { + return libzcash::PaymentAddress{ua.value()}; + } else if (ufvk.has_value() && ufvk->GetOrchardKey().has_value()) { + auto orchardKey = ufvk->GetOrchardKey().value(); + auto j = orchardKey.ToIncomingViewingKey().DecryptDiversifier(addr); + if (j.has_value()) { + auto genResult = ufvk->Address(j.value(), CWallet::DefaultReceiverTypes()); + auto defaultUA = std::get_if>(&genResult); + if (defaultUA != nullptr) { + return libzcash::PaymentAddress{defaultUA->first}; + } + } + } + + return libzcash::PaymentAddress{UnifiedAddress::ForSingleReceiver(addr)}; + } + }, recipient); + }; + + auto recipientsPtr = sendRecipients.find(txid); + if (recipientsPtr == sendRecipients.end()) { + // we don't know the recipient, so we just return the simplest type + return defaultAddress(); + } else { + // search the list of recipient mappings for one corresponding to + // our recipient, and return the known UA if it exists; otherwise + // just use the default. + for (const auto& mapping : recipientsPtr->second) { + if (mapping.address == recipient && mapping.ua.has_value()) { + return PaymentAddress{mapping.ua.value()}; + } + } + return defaultAddress(); + } +} + +bool CWallet::IsInternalRecipient(const libzcash::RecipientAddress& recipient) const +{ + auto self = this; + return std::visit(match { + [&](const CKeyID& addr) { + // we never send transparent change when sending to or from a + // unified address + return false; + }, + [&](const CScriptID& addr) { + // we never use P2SH for change or shielding + return false; + }, + [&](const SaplingPaymentAddress& addr) { + auto ufvk = self->GetUFVKForReceiver(addr); + if (ufvk.has_value()) { + auto changeAddr = ufvk->GetChangeAddress(SaplingChangeRequest()); + if (changeAddr.has_value()) { + return changeAddr.value() == recipient; + } + } + return false; + }, + [&](const OrchardRawAddress& addr) { + auto ufvk = self->GetUFVKForReceiver(addr); + if (ufvk.has_value()) { + auto changeAddr = ufvk->GetChangeAddress(OrchardChangeRequest()); + if (changeAddr.has_value()) { + return changeAddr.value() == recipient; + } + } + return false; + } + }, recipient); +} + +void CWallet::LoadRecipientMapping(const uint256& txid, const RecipientMapping& mapping) { + sendRecipients[txid].push_back(mapping); +} + bool CWallet::LoadCaches() { AssertLockHeld(cs_wallet); @@ -1486,7 +1607,7 @@ set CWallet::GetConflicts(const uint256& txid) const for (uint32_t i = 0; i < wtx.GetOrchardBundle().GetNumActions(); i++) { OrchardOutPoint op(wtx.GetHash(), i); - auto potential_spends = pwalletMain->orchardWallet.GetPotentialSpends(op); + auto potential_spends = orchardWallet.GetPotentialSpends(op); if (potential_spends.size() <= 1) { continue; // No conflict if zero or one spends @@ -1673,14 +1794,14 @@ std::optional CWallet::ZTXOSelectorForAddress( } }, [&](const libzcash::UnifiedAddress& ua) { - auto ufvkMeta = this->GetUFVKMetadataForAddress(ua); + auto ufvkMeta = GetUFVKMetadataForAddress(ua); if (ufvkMeta.has_value()) { // TODO: at present, the `false` value for the `requireSpendingKey` argument // is not respected for unified addresses, because we have no notion of // an account for which we do not control the spending key. An alternate // approach would be to use the UFVK directly in the case that we cannot // determine a local account. - auto accountId = this->GetUnifiedAccountId(ufvkMeta.value().first); + auto accountId = this->GetUnifiedAccountId(ufvkMeta.value().GetUFVKId()); if (accountId.has_value()) { if (allowAddressLinkability) { pattern = AccountZTXOPattern(accountId.value(), ua.GetKnownReceiverTypes()); @@ -1749,13 +1870,13 @@ std::optional CWallet::FindAccountForSelector(const ZTXOSel [&](const CKeyID& addr) { auto meta = self->GetUFVKMetadataForReceiver(addr); if (meta.has_value()) { - result = self->GetUnifiedAccountId(meta.value().first); + result = self->GetUnifiedAccountId(meta.value().GetUFVKId()); } }, [&](const CScriptID& addr) { auto meta = self->GetUFVKMetadataForReceiver(addr); if (meta.has_value()) { - result = self->GetUnifiedAccountId(meta.value().first); + result = self->GetUnifiedAccountId(meta.value().GetUFVKId()); } }, [&](const libzcash::SproutPaymentAddress& addr) { }, @@ -1763,7 +1884,7 @@ std::optional CWallet::FindAccountForSelector(const ZTXOSel [&](const libzcash::SaplingPaymentAddress& addr) { auto meta = GetUFVKMetadataForReceiver(addr); if (meta.has_value()) { - result = self->GetUnifiedAccountId(meta.value().first); + result = self->GetUnifiedAccountId(meta.value().GetUFVKId()); } }, [&](const libzcash::SaplingExtendedFullViewingKey& vk) { @@ -1775,7 +1896,7 @@ std::optional CWallet::FindAccountForSelector(const ZTXOSel [&](const libzcash::UnifiedAddress& addr) { auto meta = GetUFVKMetadataForAddress(addr); if (meta.has_value()) { - result = self->GetUnifiedAccountId(meta.value().first); + result = self->GetUnifiedAccountId(meta.value().GetUFVKId()); } }, [&](const libzcash::UnifiedFullViewingKey& vk) { @@ -1834,16 +1955,16 @@ bool CWallet::SelectorMatchesAddress( return false; }, [&](const libzcash::UnifiedFullViewingKey& ufvk) { - std::optional>> meta; + std::optional meta; std::visit(match { [&](const CNoDestination& none) { meta = std::nullopt; }, [&](const auto& addr) { meta = self->GetUFVKMetadataForReceiver(addr); } }, address); - return (meta.has_value() && meta.value().first == ufvk.GetKeyID(Params())); + return (meta.has_value() && meta.value().GetUFVKId() == ufvk.GetKeyID(Params())); }, [&](const AccountZTXOPattern& acct) { if (acct.IncludesP2PKH() || acct.IncludesP2SH()) { - std::optional>> meta; + std::optional meta; std::visit(match { [&](const CNoDestination& none) { meta = std::nullopt; }, [&](const auto& addr) { meta = self->GetUFVKMetadataForReceiver(addr); } @@ -1851,7 +1972,7 @@ bool CWallet::SelectorMatchesAddress( if (meta.has_value()) { // use the coin if the account id corresponding to the UFVK is // the payment source account. - return self->GetUnifiedAccountId(meta.value().first) == std::optional(acct.GetAccountId()); + return self->GetUnifiedAccountId(meta.value().GetUFVKId()) == std::optional(acct.GetAccountId()); } else { // The legacy account is treated as a single pool of // transparent funds, reproducing wallet behavior prior to @@ -1900,7 +2021,7 @@ bool CWallet::SelectorMatchesAddress( // the Sapling component of the unified address, we consider that a // match return a0Meta.has_value() && uaMeta.has_value() && - a0Meta.value().first == uaMeta.value().first; + a0Meta.value().GetUFVKId() == uaMeta.value().GetUFVKId(); } return false; }, @@ -1920,7 +2041,7 @@ bool CWallet::SelectorMatchesAddress( if (meta.has_value()) { // use the coin if the account id corresponding to the UFVK is // the payment source account. - return self->GetUnifiedAccountId(meta.value().first) == std::optional(acct.GetAccountId()); + return self->GetUnifiedAccountId(meta.value().GetUFVKId()) == std::optional(acct.GetAccountId()); } else { return false; } @@ -2122,7 +2243,7 @@ SpendableInputs CWallet::FindSpendableInputs( if (orchardReceiver.has_value()) { auto meta = GetUFVKMetadataForReceiver(orchardReceiver.value()); if (meta.has_value()) { - auto ufvk = GetUnifiedFullViewingKey(meta.value().first); + auto ufvk = GetUnifiedFullViewingKey(meta.value().GetUFVKId()); if (ufvk.has_value()) { auto fvk = ufvk->GetOrchardKey(); if (fvk.has_value()) { @@ -3594,7 +3715,7 @@ bool CWallet::MnemonicVerified() { } HDSeed CWallet::GetHDSeedForRPC() const { - auto seed = pwalletMain->GetMnemonicSeed(); + auto seed = GetMnemonicSeed(); if (!seed.has_value()) { throw JSONRPCError(RPC_WALLET_ERROR, "HD seed not found"); } @@ -3873,6 +3994,12 @@ std::optional& ovks) const +{ + return pwallet->orchardWallet.GetTxActions(*this, ovks); +} + + int64_t CWalletTx::GetTxTime() const { int64_t n = nTimeSmart; @@ -6706,7 +6833,7 @@ PaymentAddressSource GetSourceForPaymentAddress::GetUnifiedSource(const libzcash auto hdChain = m_wallet->GetMnemonicHDChain(); auto ufvkMeta = m_wallet->GetUFVKMetadataForReceiver(receiver); if (ufvkMeta.has_value()) { - auto ufvkid = ufvkMeta.value().first; + auto ufvkid = ufvkMeta.value().GetUFVKId(); // Look through the UFVKs that we have generated, and confirm that the // seed fingerprint for the key we find for the ufvkid corresponds to // the wallet's mnemonic seed. @@ -6789,13 +6916,13 @@ PaymentAddressSource GetSourceForPaymentAddress::operator()(const libzcash::Sapl PaymentAddressSource GetSourceForPaymentAddress::operator()(const libzcash::UnifiedAddress &uaddr) const { auto hdChain = m_wallet->GetMnemonicHDChain(); - auto ufvkid = m_wallet->GetUFVKMetadataForAddress(uaddr); - if (ufvkid.has_value()) { + auto ufvkMeta = m_wallet->GetUFVKMetadataForAddress(uaddr); + if (ufvkMeta.has_value()) { // Look through the UFVKs that we have generated, and confirm that the - // seed fingerprint for the key we find for the ufvkid corresponds to + // seed fingerprint for the key we find for the ufvkMeta corresponds to // the wallet's mnemonic seed. for (const auto& [k, v] : m_wallet->mapUnifiedAccountKeys) { - if (v == ufvkid.value().first && hdChain.has_value() && k.first == hdChain.value().GetSeedFingerprint()) { + if (v == ufvkMeta.value().GetUFVKId() && hdChain.has_value() && k.first == hdChain.value().GetSeedFingerprint()) { return PaymentAddressSource::MnemonicHDSeed; } } @@ -6945,9 +7072,9 @@ KeyAddResult AddSpendingKeyToWallet::operator()(const libzcash::SaplingExtendedS // UFVKForReceiver :: (CWallet&, Receiver) -> std::optional std::optional UFVKForReceiver::operator()(const libzcash::OrchardRawAddress& orchardAddr) const { - auto ufvkPair = wallet.GetUFVKMetadataForReceiver(orchardAddr); - if (ufvkPair.has_value()) { - auto ufvkid = ufvkPair.value().first; + auto ufvkMeta = wallet.GetUFVKMetadataForReceiver(orchardAddr); + if (ufvkMeta.has_value()) { + auto ufvkid = ufvkMeta.value().GetUFVKId(); auto ufvk = wallet.GetUnifiedFullViewingKey(ufvkid); // If we have UFVK metadata, `GetUnifiedFullViewingKey` should always // return a non-nullopt value, and since we obtained that metadata by @@ -6959,9 +7086,9 @@ std::optional UFVKForReceiver::operator() } } std::optional UFVKForReceiver::operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const { - auto ufvkPair = wallet.GetUFVKMetadataForReceiver(saplingAddr); - if (ufvkPair.has_value()) { - auto ufvkid = ufvkPair.value().first; + auto ufvkMeta = wallet.GetUFVKMetadataForReceiver(saplingAddr); + if (ufvkMeta.has_value()) { + auto ufvkid = ufvkMeta.value().GetUFVKId(); auto ufvk = wallet.GetUnifiedFullViewingKey(ufvkid); // If we have UFVK metadata, `GetUnifiedFullViewingKey` should always // return a non-nullopt value, and since we obtained that metadata by @@ -6978,12 +7105,12 @@ std::optional UFVKForReceiver::operator() return std::nullopt; } std::optional UFVKForReceiver::operator()(const CKeyID& keyId) const { - auto ufvkPair = wallet.GetUFVKMetadataForReceiver(keyId); - if (ufvkPair.has_value()) { - auto ufvkid = ufvkPair.value().first; + auto ufvkMeta = wallet.GetUFVKMetadataForReceiver(keyId); + if (ufvkMeta.has_value()) { + auto ufvkid = ufvkMeta.value().GetUFVKId(); // transparent address UFVK metadata is always accompanied by the child // index at which the address was produced - assert(ufvkPair.value().second.has_value()); + assert(ufvkMeta.value().GetDiversifierIndex().has_value()); auto ufvk = wallet.GetUnifiedFullViewingKey(ufvkid); assert(ufvk.has_value() && ufvk.value().GetTransparentKey().has_value()); return ufvk.value(); @@ -6999,9 +7126,9 @@ std::optional UFVKForReceiver::operator() std::optional UnifiedAddressForReceiver::operator()( const libzcash::OrchardRawAddress& orchardAddr) const { - auto ufvkPair = wallet.GetUFVKMetadataForReceiver(orchardAddr); - if (ufvkPair.has_value()) { - auto ufvkid = ufvkPair.value().first; + auto ufvkMeta = wallet.GetUFVKMetadataForReceiver(orchardAddr); + if (ufvkMeta.has_value()) { + auto ufvkid = ufvkMeta.value().GetUFVKId(); auto ufvk = wallet.GetUnifiedFullViewingKey(ufvkid); assert(ufvk.has_value()); @@ -7027,9 +7154,9 @@ std::optional UnifiedAddressForReceiver::operator()( } std::optional UnifiedAddressForReceiver::operator()(const libzcash::SaplingPaymentAddress& saplingAddr) const { - auto ufvkPair = wallet.GetUFVKMetadataForReceiver(saplingAddr); - if (ufvkPair.has_value()) { - auto ufvkid = ufvkPair.value().first; + auto ufvkMeta = wallet.GetUFVKMetadataForReceiver(saplingAddr); + if (ufvkMeta.has_value()) { + auto ufvkid = ufvkMeta.value().GetUFVKId(); auto ufvk = wallet.GetUnifiedFullViewingKey(ufvkid); assert(ufvk.has_value()); @@ -7057,13 +7184,13 @@ std::optional UnifiedAddressForReceiver::operator()(co return std::nullopt; } std::optional UnifiedAddressForReceiver::operator()(const CKeyID& keyId) const { - auto ufvkPair = wallet.GetUFVKMetadataForReceiver(keyId); - if (ufvkPair.has_value()) { - auto ufvkid = ufvkPair.value().first; + auto ufvkMeta = wallet.GetUFVKMetadataForReceiver(keyId); + if (ufvkMeta.has_value()) { + auto ufvkid = ufvkMeta.value().GetUFVKId(); // transparent address UFVK metadata is always accompanied by the child // index at which the address was produced - assert(ufvkPair.value().second.has_value()); - diversifier_index_t j = ufvkPair.value().second.value(); + assert(ufvkMeta.value().GetDiversifierIndex().has_value()); + diversifier_index_t j = ufvkMeta.value().GetDiversifierIndex().value(); auto ufvk = wallet.GetUnifiedFullViewingKey(ufvkid); if (!(ufvk.has_value() && ufvk.value().GetTransparentKey().has_value())) { throw std::runtime_error("CWallet::UnifiedAddressForReceiver(): UFVK has no P2PKH key part."); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 27c297ee0..53c8887d6 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -632,6 +632,7 @@ public: std::optional> RecoverSaplingNoteWithoutLeadByteCheck(SaplingOutPoint op, std::set& ovks) const; + OrchardActions RecoverOrchardActions(const std::vector& ovks) const; //! filter decides which addresses will count towards the debit CAmount GetDebit(const isminefilter& filter) const; @@ -1005,6 +1006,8 @@ public: class CWallet : public CCryptoKeyStore, public CValidationInterface { private: + friend class CWalletTx; + /** * Select a set of coins such that nValueRet >= nTargetValue and at least * all coins from coinControl are selected; Never select unconfirmed coins @@ -1291,6 +1294,8 @@ public: std::map mapWallet; + std::map> sendRecipients; + typedef std::multimap TxItems; TxItems wtxOrdered; @@ -1607,6 +1612,14 @@ public: bool LoadUnifiedAccountMetadata(const ZcashdUnifiedAccountMetadata &skmeta); bool LoadUnifiedAddressMetadata(const ZcashdUnifiedAddressMetadata &addrmeta); + libzcash::PaymentAddress GetPaymentAddressForRecipient( + const uint256& txid, + const libzcash::RecipientAddress& recipient) const; + bool IsInternalRecipient( + const libzcash::RecipientAddress& recipient) const; + + void LoadRecipientMapping(const uint256& txid, const RecipientMapping& mapping); + //! Reconstructs (in memory) caches and mappings for unified accounts, //! addresses and keying material. This should be called once, after the //! remainder of the on-disk wallet data has been loaded. @@ -1690,16 +1703,16 @@ public: bool SaveRecipientMappings(const uint256& txid, const std::vector& recipients) { LOCK2(cs_main, cs_wallet); - LogPrintf("SaveRecipientMappings:\n%s", txid.ToString()); for (const auto& recipient : recipients) { + sendRecipients[txid].push_back(recipient); if (recipient.ua.has_value()) { - CWalletDB(strWalletFile).WriteRecipientMapping( + assert(CWalletDB(strWalletFile).WriteRecipientMapping( txid, recipient.address, recipient.ua.value() - ); + )); } } @@ -1720,6 +1733,12 @@ public: */ static CAmount GetRequiredFee(unsigned int nTxBytes); + /** + * The current set of default receiver types used when the wallet generates + * unified addresses + */ + static std::set DefaultReceiverTypes(); + private: bool NewKeyPool(); public: diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index f885aa4ae..7ce281bf2 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -892,6 +892,8 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, strErr = "Error in wallet database: recipientmapping UA does not contain recipient"; return false; } + + pwallet->LoadRecipientMapping(txid, RecipientMapping(ua.value(), recipient)); } else if (strType == "orchard_note_commitment_tree") { diff --git a/src/zcash/Address.hpp b/src/zcash/Address.hpp index 3207e934e..c4d230daa 100644 --- a/src/zcash/Address.hpp +++ b/src/zcash/Address.hpp @@ -67,6 +67,12 @@ class UnifiedAddress { public: UnifiedAddress() {} + static UnifiedAddress ForSingleReceiver(Receiver receiver) { + UnifiedAddress ua; + ua.AddReceiver(receiver); + return ua; + } + static std::optional Parse(const KeyConstants& keyConstants, const std::string& str); ADD_SERIALIZE_METHODS; diff --git a/src/zcash/address/unified.cpp b/src/zcash/address/unified.cpp index b3ea37481..dd82feb51 100644 --- a/src/zcash/address/unified.cpp +++ b/src/zcash/address/unified.cpp @@ -2,9 +2,11 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or https://www.opensource.org/licenses/mit-license.php . +#include "tinyformat.h" #include "zcash/Address.hpp" #include "unified.h" #include "util/match.h" +#include "utilstrencodings.h" #include @@ -39,6 +41,37 @@ Receiver libzcash::RecipientAddressToReceiver(const RecipientAddress& recipient) }, recipient); } +std::string libzcash::DebugPrintReceiver(const Receiver& receiver) { + return std::visit(match { + [&](const OrchardRawAddress &zaddr) { + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << zaddr; + return tfm::format("Orchard(%s)", HexStr(ss.begin(), ss.end())); + }, + [&](const SaplingPaymentAddress &zaddr) { + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << zaddr; + return tfm::format("Sapling(%s)", HexStr(ss.begin(), ss.end())); + }, + [&](const CScriptID &p2sh) { + return tfm::format("P2SH(%s)", p2sh.GetHex()); + }, + [&](const CKeyID &p2pkh) { + return tfm::format("P2PKH(%s)", p2pkh.GetHex()); + }, + [&](const UnknownReceiver &unknown) { + return tfm::format( + "Unknown(%x, %s)", + unknown.typecode, + HexStr(unknown.data.begin(), unknown.data.end())); + } + }, receiver); +}; + +std::string libzcash::DebugPrintRecipientAddress(const RecipientAddress& addr) { + return DebugPrintReceiver(RecipientAddressToReceiver(addr)); +} + std::optional ZcashdUnifiedSpendingKey::ForAccount( const HDSeed& seed, uint32_t bip44CoinType, diff --git a/src/zcash/address/unified.h b/src/zcash/address/unified.h index 03a455915..b9e648e61 100644 --- a/src/zcash/address/unified.h +++ b/src/zcash/address/unified.h @@ -57,6 +57,8 @@ typedef std::variant< libzcash::SaplingPaymentAddress, libzcash::OrchardRawAddress> RecipientAddress; +std::string DebugPrintRecipientAddress(const RecipientAddress& add); + class TransparentChangeRequest { private: const diversifier_index_t& index; @@ -126,6 +128,8 @@ typedef std::variant< Receiver RecipientAddressToReceiver(const RecipientAddress& recipient); +std::string DebugPrintReceiver(const Receiver& receiver); + /** * An internal identifier for a unified full viewing key, derived as a * blake2b hash of the serialized form of the UFVK. diff --git a/src/zcash/address/zip32.h b/src/zcash/address/zip32.h index 7bdc5b890..d275d1cee 100644 --- a/src/zcash/address/zip32.h +++ b/src/zcash/address/zip32.h @@ -178,6 +178,17 @@ public: return j; } + // Attempts to construct a valid internal payment address with diversifier + // index `j`; returns std::nullopt if `j` does not result in a valid diversifier + // given this xfvk. + std::optional InternalAddress(diversifier_index_t j) const { + return GetInternalDFVK().Address(j); + } + + diversifier_index_t DecryptInternalDiversifier(const diversifier_t& d) const { + return GetInternalDFVK().DecryptDiversifier(d); + } + ADD_SERIALIZE_METHODS; template