// Copyright (c) 2016-2024 The Hush developers // Copyright (c) 2019 Cryptoforge // Distributed under the GPLv3 software license, see the accompanying // file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html #include "init.h" #include "key_io.h" #include "rpc/server.h" #include "wallet.h" #include "rpchushwallet.h" #include "utilmoneystr.h" #include #include #include #include #include using namespace std; using namespace libzcash; bool EnsureWalletIsAvailable(bool avoidException); const std::vector& ZsTxJSONContext::keys() { if (!keysBuilt) { // The calls, order and filter of the per-output key loops this replaces. std::set addresses; pwalletMain->GetSaplingPaymentAddresses(addresses); for (std::set::const_iterator it = addresses.begin(); it != addresses.end(); ++it) { libzcash::SaplingExtendedSpendingKey extsk; if (!pwalletMain->GetSaplingExtendedSpendingKey(*it, extsk)) continue; // viewing-key-only, or encrypted and locked: skipped, as before libzcash::SaplingFullViewingKey fvk = extsk.expsk.full_viewing_key(); Key key; key.addr = *it; key.ivk = fvk.in_viewing_key(); key.ovk = fvk.ovk; vKeys.push_back(key); } keysBuilt = true; } return vKeys; } bool ZsTxJSONContext::hasNullifier(const libzcash::SaplingPaymentAddress& addr, const uint256& nullifier) { if (!nullifiersBuilt) { // The population GetNullifiersForAddresses() scans: every mapWallet note that has a nullifier. for (std::map::const_iterator it = pwalletMain->mapWallet.begin(); it != pwalletMain->mapWallet.end(); ++it) { for (mapSaplingNoteData_t::const_iterator nit = it->second.mapSaplingNoteData.begin(); nit != it->second.mapSaplingNoteData.end(); ++nit) { if (nit->second.nullifier) nfByIvk.insert(std::make_pair(nit->second.ivk, nit->second.nullifier.get())); } } nullifiersBuilt = true; } // GetNullifiersForAddresses() ignores the result of this lookup, so an address the wallet does not // know is matched against the default (null) ivk. Keep that. libzcash::SaplingIncomingViewingKey ivk; pwalletMain->GetSaplingIncomingViewingKey(addr, ivk); return nfByIvk.count(std::make_pair(ivk, nullifier)) > 0; } bool ZsTxJSONContext::seedOvk(uint256& ovkOut) { if (!seedOvkTried) { HDSeed seed; if (pwalletMain->GetHDSeedForDerivation(seed)) { seedOvkValue = ovkForShieldingFromTaddr(seed); seedOvkOk = true; } seedOvkTried = true; } if (seedOvkOk) ovkOut = seedOvkValue; return seedOvkOk; } void zsTxSpendsToJSON(const CWalletTx& wtx, UniValue& spends, CAmount& totalSpends, CAmount& filteredSpends, const std::string& strAddress, bool filterByAddress) { LOCK2(cs_main, pwalletMain->cs_wallet); //Used to identify incomplete key sets int vinCount = 0; int vinSpendCount = 0; int shieldedSpendCount = 0; int shieldedSpendsSpentCount = 0; //Check address bool isTAddress = false; bool isZsAddress = false; CTxDestination tAddress = DecodeDestination(strAddress); auto zAddress = DecodePaymentAddress(strAddress); SaplingPaymentAddress zsAddress; if (filterByAddress) { if (IsValidDestination(tAddress)) isTAddress = true; if (IsValidPaymentAddress(zAddress)) { if (boost::get(&zAddress) != nullptr) { zsAddress = boost::get(zAddress); isZsAddress = true; } } } // Transparent Inputs belonging to the wallet UniValue tSpends(UniValue::VARR); if (isTAddress || !filterByAddress) { for (int i = 0; i < wtx.vin.size(); i++) { vinCount++; const CTxIn& txin = wtx.vin[i]; UniValue obj(UniValue::VOBJ); CTxDestination address; const CWalletTx* parent = pwalletMain->GetWalletTx(txin.prevout.hash); if (parent != NULL) { const CTxOut& parentOut = parent->vout[txin.prevout.n]; ExtractDestination(parentOut.scriptPubKey, address); if(IsMine(*pwalletMain, address)){ vinSpendCount++; totalSpends += CAmount(-parentOut.nValue); obj.push_back(Pair("address",EncodeDestination(address))); obj.push_back(Pair("scriptPubKey",HexStr(parentOut.scriptPubKey.begin(), parentOut.scriptPubKey.end()))); obj.push_back(Pair("amount",ValueFromAmount(-parentOut.nValue))); obj.push_back(Pair("spendTxid",parent->GetHash().ToString())); obj.push_back(Pair("spendVout",(int)txin.prevout.n)); CTxDestination filterAddress = DecodeDestination(strAddress); if (address == tAddress || !filterByAddress) { filteredSpends += CAmount(-parentOut.nValue); tSpends.push_back(obj); } } } } } spends.push_back(Pair("transparentSpends",tSpends)); // Sapling Inputs belonging to the wallet UniValue zsSpends(UniValue::VARR); if (isZsAddress || !filterByAddress) { for (int i = 0; i < wtx.vShieldedSpend.size(); i++) { shieldedSpendCount++; const SpendDescription& spendDesc = wtx.vShieldedSpend[i]; UniValue obj(UniValue::VOBJ); SaplingOutPoint op = pwalletMain->mapSaplingNullifiersToNotes[spendDesc.nullifier]; if (pwalletMain->IsSaplingNullifierFromMe(spendDesc.nullifier)) { const CWalletTx* parent = pwalletMain->GetWalletTx(pwalletMain->mapSaplingNullifiersToNotes[spendDesc.nullifier].hash); const OutputDescription& output = parent->vShieldedOutput[op.n]; auto nd = pwalletMain->mapWallet[pwalletMain->mapSaplingNullifiersToNotes[spendDesc.nullifier].hash].mapSaplingNoteData[op]; auto pt = libzcash::SaplingNotePlaintext::decrypt(output.encCiphertext,nd.ivk,output.ephemeralKey,output.cm); if (pt) { auto note = pt.get(); auto pa = nd.ivk.address(note.d); auto address = pa.get(); shieldedSpendsSpentCount++; totalSpends += CAmount(-note.value()); obj.push_back(Pair("address",EncodePaymentAddress(address))); obj.push_back(Pair("amount", ValueFromAmount(CAmount(-note.value())))); obj.push_back(Pair("spendTxid",parent->GetHash().ToString())); obj.push_back(Pair("spendshieldedOutputIndex",(int)op.n)); if (address == zsAddress || !filterByAddress) { filteredSpends += CAmount(-note.value()); zsSpends.push_back(obj); } } } } } spends.push_back(Pair("saplingSpends",zsSpends)); spends.push_back(Pair("totalSpends",ValueFromAmount(filteredSpends))); if (!filterByAddress) { if (filteredSpends != 0 && (vinCount != vinSpendCount || shieldedSpendCount != shieldedSpendsSpentCount)) { spends.push_back(Pair("missingSpendingKeys", true)); spends.push_back(Pair("vinCount", vinCount)); spends.push_back(Pair("vinSpendCount", vinSpendCount)); spends.push_back(Pair("shieldedSpendCount", shieldedSpendCount)); spends.push_back(Pair("shieldedSpendsSpentCount", shieldedSpendsSpentCount)); } else { spends.push_back(Pair("missingSpendingKeys", false)); } } } void zsTxReceivedToJSON(const CWalletTx& wtx, UniValue& received, CAmount& totalReceived, const std::string& strAddress, bool filterByAddress, ZsTxJSONContext* ctx) { if(fZdebug) fprintf(stderr,"%s: txid=%s\n", __func__, wtx.GetHash().ToString().c_str() ); LOCK2(cs_main, pwalletMain->cs_wallet); ZsTxJSONContext localCtx; // used only when the caller passed no per-call memo ZsTxJSONContext& zctx = ctx ? *ctx : localCtx; //Check address bool isTAddress = false; bool isZsAddress = false; CTxDestination tAddress = DecodeDestination(strAddress); auto zAddress = DecodePaymentAddress(strAddress); SaplingPaymentAddress zsAddress; if (filterByAddress) { if (IsValidDestination(tAddress)) isTAddress = true; if (IsValidPaymentAddress(zAddress)) { if (boost::get(&zAddress) != nullptr) { zsAddress = boost::get(zAddress); isZsAddress = true; } } } //Transparent Received txos belonging to the wallet UniValue tReceived(UniValue::VARR); if (isTAddress || !filterByAddress) { for (int i = 0; i < wtx.vout.size(); i++) { const CTxOut& txout = wtx.vout[i]; UniValue obj(UniValue::VOBJ); CTxDestination address; ExtractDestination(txout.scriptPubKey, address); if(IsMine(*pwalletMain, address)){ obj.push_back(Pair("address",EncodeDestination(address))); obj.push_back(Pair("scriptPubKey",HexStr(txout.scriptPubKey.begin(), txout.scriptPubKey.end()))); obj.push_back(Pair("amount",ValueFromAmount(txout.nValue))); obj.push_back(Pair("vout", i)); if (address == tAddress || !filterByAddress) { totalReceived += CAmount(txout.nValue); tReceived.push_back(obj); } } } } received.push_back(Pair("transparentReceived",tReceived)); //Sapling Sends belonging to the wallet UniValue zsReceived(UniValue::VARR); if (isZsAddress || !filterByAddress) { for (int i = 0; i < wtx.vShieldedOutput.size(); i++) { const OutputDescription& outputDesc = wtx.vShieldedOutput[i]; UniValue obj(UniValue::VOBJ); bool changeTx = false; //Decrypt sapling incoming commitments using IVK const std::vector& keys = zctx.keys(); // An output opens under at most one ivk (decrypt() checks the note commitment, which binds // pk_d = [ivk]g_d), and FindMySaplingNotes records that ivk in mapSaplingNoteData. When the recorded // ivk is one of keys(), pass 0 tries only the keys with that ivk: every other key would fail, as in // the old single loop. If pass 0 opens nothing (stale note data), pass 1 tries the remaining keys in // the same order. Without a usable record only pass 1 runs, over every key, exactly as before. libzcash::SaplingIncomingViewingKey recordedIvk; bool narrow = false; { mapSaplingNoteData_t::const_iterator ndIt = wtx.mapSaplingNoteData.find(SaplingOutPoint(wtx.GetHash(), i)); if (ndIt != wtx.mapSaplingNoteData.end()) { for (size_t k = 0; k < keys.size() && !narrow; k++) { if (keys[k].ivk == ndIt->second.ivk) { recordedIvk = ndIt->second.ivk; narrow = true; } } } } bool openedByRecordedIvk = false; for (int pass = narrow ? 0 : 1; pass < 2 && !openedByRecordedIvk; pass++) { for (size_t k = 0; k < keys.size(); k++) { if (narrow && ((pass == 0) != (keys[k].ivk == recordedIvk))) continue; // pass 0: only the recorded ivk; pass 1: only the others const libzcash::SaplingPaymentAddress& addr = keys[k].addr; { // spending-key availability is resolved once per call, in ZsTxJSONContext::keys() auto pt = libzcash::SaplingNotePlaintext::decrypt( outputDesc.encCiphertext, keys[k].ivk, outputDesc.ephemeralKey, outputDesc.cm); if (pt && pass == 0) openedByRecordedIvk = true; if (pt) { auto note = pt.get(); auto pt_unwrapped = pt.get(); auto memo = pt_unwrapped.memo(); obj.push_back(Pair("address",EncodePaymentAddress(addr))); obj.push_back(Pair("amount", ValueFromAmount(CAmount(note.value())))); obj.push_back(Pair("memo", HexStr(memo))); if (memo[0] <= 0xf4) { 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)) { obj.push_back(Pair("memoStr", memoStr)); } } obj.push_back(Pair("shieldedOutputIndex",i)); //Check Change Status if (wtx.vShieldedSpend.size()!=0) { BOOST_FOREACH(const SpendDescription& spendDesc, wtx.vShieldedSpend) { if (zctx.hasNullifier(addr, spendDesc.nullifier)) { changeTx = true; } } } obj.push_back(Pair("change",changeTx)); if (addr == zsAddress || !filterByAddress) { totalReceived += CAmount(note.value()); zsReceived.push_back(obj); } } } } } // pass } } received.push_back(Pair("saplingReceived",zsReceived)); received.push_back(Pair("totalReceived",ValueFromAmount(totalReceived))); } void zsTxSendsToJSON(const CWalletTx& wtx, UniValue& sends, CAmount& totalSends, const std::string& strAddress, bool filterByAddress, ZsTxJSONContext* ctx) { LOCK2(cs_main, pwalletMain->cs_wallet); ZsTxJSONContext localCtx; // used only when the caller passed no per-call memo ZsTxJSONContext& zctx = ctx ? *ctx : localCtx; //Used to identify incomplete key sets int shieldedOutputCount = 0; int shieldedOutputDecryptedCount = 0; //Check address bool isTAddress = false; bool isZsAddress = false; CTxDestination tAddress = DecodeDestination(strAddress); auto zAddress = DecodePaymentAddress(strAddress); SaplingPaymentAddress zsAddress; if (filterByAddress) { if (IsValidDestination(tAddress)) isTAddress = true; if (IsValidPaymentAddress(zAddress)) { if (boost::get(&zAddress) != nullptr) { zsAddress = boost::get(zAddress); isZsAddress = true; } } } //All Transparent Sends in the transaction UniValue tSends(UniValue::VARR); if (isTAddress || !filterByAddress) { for (int i = 0; i < wtx.vout.size(); i++) { const CTxOut& txout = wtx.vout[i]; UniValue obj(UniValue::VOBJ); CTxDestination address; ExtractDestination(txout.scriptPubKey, address); obj.push_back(Pair("address",EncodeDestination(address))); obj.push_back(Pair("scriptPubKey",HexStr(txout.scriptPubKey.begin(), txout.scriptPubKey.end()))); obj.push_back(Pair("amount",ValueFromAmount(-txout.nValue))); obj.push_back(Pair("vout", i)); if (address == tAddress || !filterByAddress) { totalSends += CAmount(-txout.nValue); tSends.push_back(obj); } } } sends.push_back(Pair("transparentSends",tSends)); //All Shielded Sends in the transaction UniValue zsSends(UniValue::VARR); if (isZsAddress || !filterByAddress) { for (int i = 0; i < wtx.vShieldedOutput.size(); i++) { const OutputDescription& outputDesc = wtx.vShieldedOutput[i]; shieldedOutputCount++; UniValue obj(UniValue::VOBJ); bool changeTx = false; //Decrypt sapling outgoing t to z transaction using HDseed if (wtx.vShieldedSpend.size()==0) { uint256 taddrOvk; if (zctx.seedOvk(taddrOvk)) { auto opt = libzcash::SaplingOutgoingPlaintext::decrypt( outputDesc.outCiphertext,taddrOvk,outputDesc.cv,outputDesc.cm,outputDesc.ephemeralKey); if (opt) { auto opt_unwrapped = opt.get(); auto pt = libzcash::SaplingNotePlaintext::decrypt( outputDesc.encCiphertext,outputDesc.ephemeralKey,opt_unwrapped.esk,opt_unwrapped.pk_d,outputDesc.cm); if (pt) { shieldedOutputDecryptedCount++; auto pt_unwrapped = pt.get(); libzcash::SaplingPaymentAddress sentAddr(pt_unwrapped.d, opt_unwrapped.pk_d); obj.push_back(Pair("address",EncodePaymentAddress(sentAddr))); obj.push_back(Pair("amount", ValueFromAmount(CAmount(pt_unwrapped.value())))); obj.push_back(Pair("shieldedOutputIndex",i)); obj.push_back(Pair("change",false)); if (sentAddr == zsAddress || !filterByAddress) { totalSends += CAmount(-pt_unwrapped.value()); zsSends.push_back(obj); } } } } //attempt Decryption of Outgoing Sapling using wallet extended spending keys } else { const std::vector& keys = zctx.keys(); for (size_t k = 0; k < keys.size(); k++) { { // spending-key availability is resolved once per call, in ZsTxJSONContext::keys() auto opt = libzcash::SaplingOutgoingPlaintext::decrypt( outputDesc.outCiphertext,keys[k].ovk,outputDesc.cv,outputDesc.cm,outputDesc.ephemeralKey); if (opt) { auto opt_unwrapped = opt.get(); auto pt = libzcash::SaplingNotePlaintext::decrypt( outputDesc.encCiphertext,outputDesc.ephemeralKey,opt_unwrapped.esk,opt_unwrapped.pk_d,outputDesc.cm); if (pt) { auto pt_unwrapped = pt.get(); auto memo = pt_unwrapped.memo(); shieldedOutputDecryptedCount++; libzcash::SaplingPaymentAddress sentAddr(pt_unwrapped.d, opt_unwrapped.pk_d); obj.push_back(Pair("address",EncodePaymentAddress(sentAddr))); obj.push_back(Pair("amount", ValueFromAmount(CAmount(-pt_unwrapped.value())))); obj.push_back(Pair("memo", HexStr(memo))); if (memo[0] <= 0xf4) { 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)) { obj.push_back(Pair("memoStr", memoStr)); } } obj.push_back(Pair("shieldedOutputIndex",i)); //Check Change Status if (wtx.vShieldedSpend.size()!=0) { BOOST_FOREACH(const SpendDescription& spendDesc, wtx.vShieldedSpend) { if (zctx.hasNullifier(sentAddr, spendDesc.nullifier)) { changeTx = true; } } } obj.push_back(Pair("change",changeTx)); if (sentAddr == zsAddress || !filterByAddress) { totalSends += CAmount(-pt_unwrapped.value()); zsSends.push_back(obj); } } } } } } } } sends.push_back(Pair("saplingSends",zsSends)); if (shieldedOutputCount != shieldedOutputDecryptedCount) { sends.push_back(Pair("missingSaplingOVK", true)); } else { sends.push_back(Pair("missingSaplingOVK", false)); } sends.push_back(Pair("totalSends",ValueFromAmount(totalSends))); } void zsWalletTxJSON(const CWalletTx& wtx, UniValue& ret, const std::string strAddress, bool fBool, const int returnType, ZsTxJSONContext* ctx) { LOCK2(cs_main, pwalletMain->cs_wallet); //Track total wallet spend and received CAmount totalSpends = 0; CAmount filteredSpends = 0; CAmount totalReceived = 0; CAmount totalSends = 0; //Various Univalue to be added to the final transaction UniValue spends(UniValue::VOBJ); UniValue received(UniValue::VOBJ); UniValue sends(UniValue::VOBJ); UniValue tx(UniValue::VOBJ); //Begin Compiling the Decrypted Transaction tx.push_back(Pair("txid", wtx.GetHash().ToString())); if(fZdebug) fprintf(stderr,"%s: txid=%s\n", __func__, wtx.GetHash().ToString().c_str() ); if (wtx.IsCoinBase()) { tx.push_back(Pair("coinbase", true)); if (wtx.GetDepthInMainChain() < 1) tx.push_back(Pair("category", "orphan")); else if (wtx.GetBlocksToMaturity() > 0) tx.push_back(Pair("category", "immature")); else tx.push_back(Pair("category", "generate")); } else { tx.push_back(Pair("coinbase", false)); tx.push_back(Pair("category", "standard")); } tx.push_back(Pair("blockhash", wtx.hashBlock.GetHex())); tx.push_back(Pair("blockindex", wtx.nIndex)); int confirms = wtx.GetDepthInMainChain(); if(confirms > 0) { tx.push_back(Pair("blocktime", mapBlockIndex[wtx.hashBlock]->GetBlockTime())); } else { tx.push_back(Pair("blocktime", 0)); } tx.push_back(Pair("expiryheight", (int64_t)wtx.nExpiryHeight)); tx.push_back(Pair("confirmations", confirms)); tx.push_back(Pair("time", wtx.GetTxTime())); tx.push_back(Pair("size", static_cast(GetSerializeSize(static_cast(wtx), SER_NETWORK, PROTOCOL_VERSION)))); //Wallet Conflicts UniValue conflicts(UniValue::VARR); BOOST_FOREACH(const uint256& conflict, wtx.GetConflicts()) conflicts.push_back(conflict.GetHex()); tx.push_back(Pair("walletconflicts", conflicts)); // Return Type used to determine what is included in the transaction // 0 Spends, Received and spends // 1 Sends // 2 Received // 3 Spends // TODO - Setup Enum, maybe... // Add Spends retrieved earlier if (returnType != 2) { zsTxSpendsToJSON(wtx, spends, totalSpends, filteredSpends, strAddress, fBool); if ((!fBool || filteredSpends != 0) && (returnType == 0 || returnType == 1)) { tx.push_back(Pair("spends",spends)); } } // Get Received if (returnType == 0 || returnType == 2) { zsTxReceivedToJSON(wtx, received, totalReceived, strAddress, fBool, ctx); if (!fBool || totalReceived != 0) { tx.push_back(Pair("received",received)); } } // Get Sends if (returnType == 0 || returnType == 3) { //Only include sends if there are spends that belong to the wallet. if (totalSpends != 0 || fBool) { zsTxSendsToJSON(wtx, sends, totalSends, strAddress, fBool, ctx); } if (!fBool || totalSends != 0) { tx.push_back(Pair("sends",sends)); } } if ((returnType == 0 && (!fBool || filteredSpends != 0 || totalReceived != 0 || totalSends != 0)) || (returnType == 1 && (!fBool || filteredSpends != 0)) || (returnType == 2 && (!fBool || totalReceived != 0)) || (returnType == 3 && (!fBool || totalSends != 0))) { ret.push_back(tx); } }