// Notes: // 1. #1159 Currently there is no limit set on the number of joinsplits, so size of tx could be invalid. // 2. #1360 Note selection is not optimal // 3. #1277 Spendable notes are not locked, so an operation running in parallel could also try to use them bool AsyncRPCOperation_sendmany::main_impl() { assert(isfromtaddr_ != isfromzaddr_); bool isSingleZaddrOutput = (t_outputs_.size()==0 && z_outputs_.size()==1); bool isMultipleZaddrOutput = (t_outputs_.size()==0 && z_outputs_.size()>=1); bool isPureTaddrOnlyTx = (isfromtaddr_ && z_outputs_.size() == 0); CAmount minersFee = fee_; // When spending coinbase utxos, you can only specify a single zaddr as the change must go somewhere // and if there are multiple zaddrs, we don't know where to send it. if (isfromtaddr_) { if (isSingleZaddrOutput) { bool b = find_utxos(true); if (!b) { throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds, no UTXOs found for taddr from address."); } } else { bool b = find_utxos(false); if (!b) { if (isMultipleZaddrOutput) { throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Could not find any non-coinbase UTXOs to spend. Coinbase UTXOs can only be sent to a single zaddr recipient."); } else { throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Could not find any non-coinbase UTXOs to spend."); } } } } if (isfromzaddr_ && !find_unspent_notes()) { throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds, no unspent notes found for zaddr from address."); } CAmount t_inputs_total = 0; for (SendManyInputUTXO & t : t_inputs_) { t_inputs_total += std::get<2>(t); } CAmount z_inputs_total = 0; for (SendManyInputJSOP & t : z_inputs_) { z_inputs_total += std::get<2>(t); } CAmount t_outputs_total = 0; for (SendManyRecipient & t : t_outputs_) { t_outputs_total += std::get<1>(t); } CAmount z_outputs_total = 0; for (SendManyRecipient & t : z_outputs_) { z_outputs_total += std::get<1>(t); } CAmount sendAmount = z_outputs_total + t_outputs_total; CAmount targetAmount = sendAmount + minersFee; assert(!isfromtaddr_ || z_inputs_total == 0); assert(!isfromzaddr_ || t_inputs_total == 0); if (isfromtaddr_ && (t_inputs_total < targetAmount)) { throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, strprintf("Insufficient transparent funds, have %s, need %s", FormatMoney(t_inputs_total), FormatMoney(targetAmount))); } if (isfromzaddr_ && (z_inputs_total < targetAmount)) { throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, strprintf("Insufficient protected funds, have %s, need %s", FormatMoney(z_inputs_total), FormatMoney(targetAmount))); } // If from address is a taddr, select UTXOs to spend CAmount selectedUTXOAmount = 0; bool selectedUTXOCoinbase = false; if (isfromtaddr_) { // Get dust threshold CKey secret; secret.MakeNewKey(true); CScript scriptPubKey = GetScriptForDestination(secret.GetPubKey().GetID()); CTxOut out(CAmount(1), scriptPubKey); CAmount dustThreshold = out.GetDustThreshold(minRelayTxFee); CAmount dustChange = -1; std::vector<SendManyInputUTXO> selectedTInputs; for (SendManyInputUTXO & t : t_inputs_) { bool b = std::get<3>(t); if (b) { selectedUTXOCoinbase = true; } selectedUTXOAmount += std::get<2>(t); selectedTInputs.push_back(t); if (selectedUTXOAmount >= targetAmount) { // Select another utxo if there is change less than the dust threshold. dustChange = selectedUTXOAmount - targetAmount; if (dustChange == 0 || dustChange >= dustThreshold) { break; } } } // If there is transparent change, is it valid or is it dust? if (dustChange < dustThreshold && dustChange != 0) { throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, strprintf("Insufficient transparent funds, have %s, need %s more to avoid creating invalid change output %s (dust threshold is %s)", FormatMoney(t_inputs_total), FormatMoney(dustThreshold - dustChange), FormatMoney(dustChange), FormatMoney(dustThreshold))); } t_inputs_ = selectedTInputs; t_inputs_total = selectedUTXOAmount; // Check mempooltxinputlimit to avoid creating a transaction which the local mempool rejects size_t limit = (size_t)GetArg("-mempooltxinputlimit", 0); { LOCK(cs_main); if (NetworkUpgradeActive(chainActive.Height() + 1, Params().GetConsensus(), Consensus::UPGRADE_OVERWINTER)) { limit = 0; } } if (limit > 0) { size_t n = t_inputs_.size(); if (n > limit) { throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Too many transparent inputs %zu > limit %zu", n, limit)); } } // update the transaction with these inputs CMutableTransaction rawTx(tx_); for (SendManyInputUTXO & t : t_inputs_) { uint256 txid = std::get<0>(t); int vout = std::get<1>(t); CAmount amount = std::get<2>(t); CTxIn in(COutPoint(txid, vout)); rawTx.vin.push_back(in); } tx_ = CTransaction(rawTx); } LogPrint((isfromtaddr_) ? "zrpc" : "zrpcunsafe", "%s: spending %s to send %s with fee %s\n", getId(), FormatMoney(targetAmount), FormatMoney(sendAmount), FormatMoney(minersFee)); LogPrint("zrpc", "%s: transparent input: %s (to choose from)\n", getId(), FormatMoney(t_inputs_total)); LogPrint("zrpcunsafe", "%s: private input: %s (to choose from)\n", getId(), FormatMoney(z_inputs_total)); LogPrint("zrpc", "%s: transparent output: %s\n", getId(), FormatMoney(t_outputs_total)); LogPrint("zrpcunsafe", "%s: private output: %s\n", getId(), FormatMoney(z_outputs_total)); LogPrint("zrpc", "%s: fee: %s\n", getId(), FormatMoney(minersFee)); // Grab the current consensus branch ID { LOCK(cs_main); consensusBranchId_ = CurrentEpochBranchId(chainActive.Height() + 1, Params().GetConsensus()); } /** * SCENARIO #1 * * taddr -> taddrs * * There are no zaddrs or joinsplits involved. */ if (isPureTaddrOnlyTx) { add_taddr_outputs_to_tx(); CAmount funds = selectedUTXOAmount; CAmount fundsSpent = t_outputs_total + minersFee; CAmount change = funds - fundsSpent; if (change > 0) { add_taddr_change_output_to_tx(change); LogPrint("zrpc", "%s: transparent change in transaction output (amount=%s)\n", getId(), FormatMoney(change) ); } UniValue obj(UniValue::VOBJ); obj.push_back(Pair("rawtxn", EncodeHexTx(tx_))); sign_send_raw_transaction(obj); return true; } /** * END SCENARIO #1 */ // Prepare raw transaction to handle JoinSplits CMutableTransaction mtx(tx_); crypto_sign_keypair(joinSplitPubKey_.begin(), joinSplitPrivKey_); mtx.joinSplitPubKey = joinSplitPubKey_; tx_ = CTransaction(mtx); // Copy zinputs and zoutputs to more flexible containers std::deque<SendManyInputJSOP> zInputsDeque; // zInputsDeque stores minimum numbers of notes for target amount CAmount tmp = 0; for (auto o : z_inputs_) { zInputsDeque.push_back(o); tmp += std::get<2>(o); if (tmp >= targetAmount) { break; } } std::deque<SendManyRecipient> zOutputsDeque; for (auto o : z_outputs_) { // TODO: Add Sapling support. For now, ensure we can later convert freely. auto addr = DecodePaymentAddress(std::get<0>(o)); assert(boost::get<libzcash::SproutPaymentAddress>(&addr) != nullptr); zOutputsDeque.push_back(o); } // When spending notes, take a snapshot of note witnesses and anchors as the treestate will // change upon arrival of new blocks which contain joinsplit transactions. This is likely // to happen as creating a chained joinsplit transaction can take longer than the block interval. if (z_inputs_.size() > 0) { LOCK2(cs_main, pwalletMain->cs_wallet); for (auto t : z_inputs_) { JSOutPoint jso = std::get<0>(t); std::vector<JSOutPoint> vOutPoints = { jso }; uint256 inputAnchor; std::vector<boost::optional<ZCIncrementalWitness>> vInputWitnesses; pwalletMain->GetNoteWitnesses(vOutPoints, vInputWitnesses, inputAnchor); jsopWitnessAnchorMap[ jso.ToString() ] = WitnessAnchorData{ vInputWitnesses[0], inputAnchor }; } } /** * SCENARIO #2 * * taddr -> taddrs * -> zaddrs * * Note: Consensus rule states that coinbase utxos can only be sent to a zaddr. * Local wallet rule does not allow any change when sending coinbase utxos * since there is currently no way to specify a change address and we don't * want users accidentally sending excess funds to a recipient. */ if (isfromtaddr_) { add_taddr_outputs_to_tx(); CAmount funds = selectedUTXOAmount; CAmount fundsSpent = t_outputs_total + minersFee + z_outputs_total; CAmount change = funds - fundsSpent; if (change > 0) { if (selectedUTXOCoinbase) { assert(isSingleZaddrOutput); throw JSONRPCError(RPC_WALLET_ERROR, strprintf( "Change %s not allowed. When protecting coinbase funds, the wallet does not " "allow any change as there is currently no way to specify a change address " "in z_sendmany.", FormatMoney(change))); } else { add_taddr_change_output_to_tx(change); LogPrint("zrpc", "%s: transparent change in transaction output (amount=%s)\n", getId(), FormatMoney(change) ); } } // Create joinsplits, where each output represents a zaddr recipient. UniValue obj(UniValue::VOBJ); while (zOutputsDeque.size() > 0) { AsyncJoinSplitInfo info; info.vpub_old = 0; info.vpub_new = 0; int n = 0; while (n++<ZC_NUM_JS_OUTPUTS && zOutputsDeque.size() > 0) { SendManyRecipient smr = zOutputsDeque.front(); std::string address = std::get<0>(smr); CAmount value = std::get<1>(smr); std::string hexMemo = std::get<2>(smr); zOutputsDeque.pop_front(); PaymentAddress pa = DecodePaymentAddress(address); JSOutput jso = JSOutput(boost::get<libzcash::SproutPaymentAddress>(pa), value); if (hexMemo.size() > 0) { jso.memo = get_memo_from_hex_string(hexMemo); } info.vjsout.push_back(jso); // Funds are removed from the value pool and enter the private pool info.vpub_old += value; } obj = perform_joinsplit(info); } sign_send_raw_transaction(obj); return true; } /** * END SCENARIO #2 */ /** * SCENARIO #3 * * zaddr -> taddrs * -> zaddrs * * Send to zaddrs by chaining JoinSplits together and immediately consuming any change * Send to taddrs by creating dummy z outputs and accumulating value in a change note * which is used to set vpub_new in the last chained joinsplit. */ UniValue obj(UniValue::VOBJ); CAmount jsChange = 0; // this is updated after each joinsplit int changeOutputIndex = -1; // this is updated after each joinsplit if jsChange > 0 bool vpubNewProcessed = false; // updated when vpub_new for miner fee and taddr outputs is set in last joinsplit CAmount vpubNewTarget = minersFee; if (t_outputs_total > 0) { add_taddr_outputs_to_tx(); vpubNewTarget += t_outputs_total; } // Keep track of treestate within this transaction boost::unordered_map<uint256, ZCIncrementalMerkleTree, CCoinsKeyHasher> intermediates; std::vector<uint256> previousCommitments; while (!vpubNewProcessed) { AsyncJoinSplitInfo info; info.vpub_old = 0; info.vpub_new = 0; CAmount jsInputValue = 0; uint256 jsAnchor; std::vector<boost::optional<ZCIncrementalWitness>> witnesses; JSDescription prevJoinSplit; // Keep track of previous JoinSplit and its commitments if (tx_.vjoinsplit.size() > 0) { prevJoinSplit = tx_.vjoinsplit.back(); } // If there is no change, the chain has terminated so we can reset the tracked treestate. if (jsChange==0 && tx_.vjoinsplit.size() > 0) { intermediates.clear(); previousCommitments.clear(); } // // Consume change as the first input of the JoinSplit. // if (jsChange > 0) { LOCK2(cs_main, pwalletMain->cs_wallet); // Update tree state with previous joinsplit ZCIncrementalMerkleTree tree; auto it = intermediates.find(prevJoinSplit.anchor); if (it != intermediates.end()) { tree = it->second; } else if (!pcoinsTip->GetSproutAnchorAt(prevJoinSplit.anchor, tree)) { throw JSONRPCError(RPC_WALLET_ERROR, "Could not find previous JoinSplit anchor"); } assert(changeOutputIndex != -1); boost::optional<ZCIncrementalWitness> changeWitness; int n = 0; for (const uint256& commitment : prevJoinSplit.commitments) { tree.append(commitment); previousCommitments.push_back(commitment); if (!changeWitness && changeOutputIndex == n++) { changeWitness = tree.witness(); } else if (changeWitness) { changeWitness.get().append(commitment); } } if (changeWitness) { witnesses.push_back(changeWitness); } jsAnchor = tree.root(); intermediates.insert(std::make_pair(tree.root(), tree)); // chained js are interstitial (found in between block boundaries) // Decrypt the change note's ciphertext to retrieve some data we need ZCNoteDecryption decryptor(boost::get<libzcash::SproutSpendingKey>(spendingkey_).receiving_key()); auto hSig = prevJoinSplit.h_sig(*pzcashParams, tx_.joinSplitPubKey); try { SproutNotePlaintext plaintext = SproutNotePlaintext::decrypt( decryptor, prevJoinSplit.ciphertexts[changeOutputIndex], prevJoinSplit.ephemeralKey, hSig, (unsigned char) changeOutputIndex); SproutNote note = plaintext.note(boost::get<libzcash::SproutPaymentAddress>(frompaymentaddress_)); info.notes.push_back(note); jsInputValue += plaintext.value(); LogPrint("zrpcunsafe", "%s: spending change (amount=%s)\n", getId(), FormatMoney(plaintext.value()) ); } catch (const std::exception& e) { throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Error decrypting output note of previous JoinSplit: %s", e.what())); } } // // Consume spendable non-change notes // std::vector<SproutNote> vInputNotes; std::vector<JSOutPoint> vOutPoints; std::vector<boost::optional<ZCIncrementalWitness>> vInputWitnesses; uint256 inputAnchor; int numInputsNeeded = (jsChange>0) ? 1 : 0; while (numInputsNeeded++ < ZC_NUM_JS_INPUTS && zInputsDeque.size() > 0) { SendManyInputJSOP t = zInputsDeque.front(); JSOutPoint jso = std::get<0>(t); SproutNote note = std::get<1>(t); CAmount noteFunds = std::get<2>(t); zInputsDeque.pop_front(); WitnessAnchorData wad = jsopWitnessAnchorMap[ jso.ToString() ]; vInputWitnesses.push_back(wad.witness); if (inputAnchor.IsNull()) { inputAnchor = wad.anchor; } else if (inputAnchor != wad.anchor) { throw JSONRPCError(RPC_WALLET_ERROR, "Selected input notes do not share the same anchor"); } vOutPoints.push_back(jso); vInputNotes.push_back(note); jsInputValue += noteFunds; int wtxHeight = -1; int wtxDepth = -1; { LOCK2(cs_main, pwalletMain->cs_wallet); const CWalletTx& wtx = pwalletMain->mapWallet[jso.hash]; // Zero-confirmation notes belong to transactions which have not yet been mined if (mapBlockIndex.find(wtx.hashBlock) == mapBlockIndex.end()) { throw JSONRPCError(RPC_WALLET_ERROR, strprintf("mapBlockIndex does not contain block hash %s", wtx.hashBlock.ToString())); } wtxHeight = mapBlockIndex[wtx.hashBlock]->nHeight; wtxDepth = wtx.GetDepthInMainChain(); } LogPrint("zrpcunsafe", "%s: spending note (txid=%s, vjoinsplit=%d, ciphertext=%d, amount=%s, height=%d, confirmations=%d)\n", getId(), jso.hash.ToString().substr(0, 10), jso.js, int(jso.n), // uint8_t FormatMoney(noteFunds), wtxHeight, wtxDepth ); } // Add history of previous commitments to witness if (vInputNotes.size() > 0) { if (vInputWitnesses.size()==0) { throw JSONRPCError(RPC_WALLET_ERROR, "Could not find witness for note commitment"); } for (auto & optionalWitness : vInputWitnesses) { if (!optionalWitness) { throw JSONRPCError(RPC_WALLET_ERROR, "Witness for note commitment is null"); } ZCIncrementalWitness w = *optionalWitness; // could use .get(); if (jsChange > 0) { for (const uint256& commitment : previousCommitments) { w.append(commitment); } if (jsAnchor != w.root()) { throw JSONRPCError(RPC_WALLET_ERROR, "Witness for spendable note does not have same anchor as change input"); } } witnesses.push_back(w); } // The jsAnchor is null if this JoinSplit is at the start of a new chain if (jsAnchor.IsNull()) { jsAnchor = inputAnchor; } // Add spendable notes as inputs std::copy(vInputNotes.begin(), vInputNotes.end(), std::back_inserter(info.notes)); } // Find recipient to transfer funds to std::string address, hexMemo; CAmount value = 0; if (zOutputsDeque.size() > 0) { SendManyRecipient smr = zOutputsDeque.front(); address = std::get<0>(smr); value = std::get<1>(smr); hexMemo = std::get<2>(smr); zOutputsDeque.pop_front(); } // Reset change jsChange = 0; CAmount outAmount = value; // Set vpub_new in the last joinsplit (when there are no more notes to spend or zaddr outputs to satisfy) if (zOutputsDeque.size() == 0 && zInputsDeque.size() == 0) { assert(!vpubNewProcessed); if (jsInputValue < vpubNewTarget) { throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Insufficient funds for vpub_new %s (miners fee %s, taddr outputs %s)", FormatMoney(vpubNewTarget), FormatMoney(minersFee), FormatMoney(t_outputs_total))); } outAmount += vpubNewTarget; info.vpub_new += vpubNewTarget; // funds flowing back to public pool vpubNewProcessed = true; jsChange = jsInputValue - outAmount; assert(jsChange >= 0); } else { // This is not the last joinsplit, so compute change and any amount still due to the recipient if (jsInputValue > outAmount) { jsChange = jsInputValue - outAmount; } else if (outAmount > jsInputValue) { // Any amount due is owed to the recipient. Let the miners fee get paid first. CAmount due = outAmount - jsInputValue; SendManyRecipient r = SendManyRecipient(address, due, hexMemo); zOutputsDeque.push_front(r); // reduce the amount being sent right now to the value of all inputs value = jsInputValue; } } // create output for recipient if (address.empty()) { assert(value==0); info.vjsout.push_back(JSOutput()); // dummy output while we accumulate funds into a change note for vpub_new } else { PaymentAddress pa = DecodePaymentAddress(address); JSOutput jso = JSOutput(boost::get<libzcash::SproutPaymentAddress>(pa), value); if (hexMemo.size() > 0) { jso.memo = get_memo_from_hex_string(hexMemo); } info.vjsout.push_back(jso); } // create output for any change if (jsChange>0) { info.vjsout.push_back(JSOutput(boost::get<libzcash::SproutPaymentAddress>(frompaymentaddress_), jsChange)); LogPrint("zrpcunsafe", "%s: generating note for change (amount=%s)\n", getId(), FormatMoney(jsChange) ); } obj = perform_joinsplit(info, witnesses, jsAnchor); if (jsChange > 0) { changeOutputIndex = find_output(obj, 1); } } // Sanity check in case changes to code block above exits loop by invoking 'break' assert(zInputsDeque.size() == 0); assert(zOutputsDeque.size() == 0); assert(vpubNewProcessed); sign_send_raw_transaction(obj); return true; }
// Notes: // 1. #1359 Currently there is no limit set on the number of joinsplits, so size of tx could be invalid. // 2. #1277 Spendable notes are not locked, so an operation running in parallel could also try to use them. bool AsyncRPCOperation_mergetoaddress::main_impl() { assert(isToTaddr_ != isToZaddr_); bool isPureTaddrOnlyTx = (sproutNoteInputs_.empty() && saplingNoteInputs_.empty() && isToTaddr_); CAmount minersFee = fee_; size_t numInputs = utxoInputs_.size(); // Check mempooltxinputlimit to avoid creating a transaction which the local mempool rejects size_t limit = (size_t)GetArg("-mempooltxinputlimit", 0); { LOCK(cs_main); if (NetworkUpgradeActive(chainActive.Height() + 1, Params().GetConsensus(), Consensus::UPGRADE_OVERWINTER)) { limit = 0; } } if (limit > 0 && numInputs > limit) { throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Number of transparent inputs %d is greater than mempooltxinputlimit of %d", numInputs, limit)); } CAmount t_inputs_total = 0; for (MergeToAddressInputUTXO& t : utxoInputs_) { t_inputs_total += std::get<1>(t); } CAmount z_inputs_total = 0; for (const MergeToAddressInputSproutNote& t : sproutNoteInputs_) { z_inputs_total += std::get<2>(t); } for (const MergeToAddressInputSaplingNote& t : saplingNoteInputs_) { z_inputs_total += std::get<2>(t); } CAmount targetAmount = z_inputs_total + t_inputs_total; if (targetAmount <= minersFee) { throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, strprintf("Insufficient funds, have %s and miners fee is %s", FormatMoney(targetAmount), FormatMoney(minersFee))); } CAmount sendAmount = targetAmount - minersFee; // update the transaction with the UTXO inputs and output (if any) if (!isUsingBuilder_) { CMutableTransaction rawTx(tx_); for (const MergeToAddressInputUTXO& t : utxoInputs_) { CTxIn in(std::get<0>(t)); rawTx.vin.push_back(in); } if (isToTaddr_) { CScript scriptPubKey = GetScriptForDestination(toTaddr_); CTxOut out(sendAmount, scriptPubKey); rawTx.vout.push_back(out); } tx_ = CTransaction(rawTx); } LogPrint(isPureTaddrOnlyTx ? "zrpc" : "zrpcunsafe", "%s: spending %s to send %s with fee %s\n", getId(), FormatMoney(targetAmount), FormatMoney(sendAmount), FormatMoney(minersFee)); LogPrint("zrpc", "%s: transparent input: %s\n", getId(), FormatMoney(t_inputs_total)); LogPrint("zrpcunsafe", "%s: private input: %s\n", getId(), FormatMoney(z_inputs_total)); if (isToTaddr_) { LogPrint("zrpc", "%s: transparent output: %s\n", getId(), FormatMoney(sendAmount)); } else { LogPrint("zrpcunsafe", "%s: private output: %s\n", getId(), FormatMoney(sendAmount)); } LogPrint("zrpc", "%s: fee: %s\n", getId(), FormatMoney(minersFee)); // Grab the current consensus branch ID { LOCK(cs_main); consensusBranchId_ = CurrentEpochBranchId(chainActive.Height() + 1, Params().GetConsensus()); } /** * SCENARIO #0 * * Sprout not involved, so we just use the TransactionBuilder and we're done. * * This is based on code from AsyncRPCOperation_sendmany::main_impl() and should be refactored. */ if (isUsingBuilder_) { builder_.SetFee(minersFee); for (const MergeToAddressInputUTXO& t : utxoInputs_) { COutPoint outPoint = std::get<0>(t); CAmount amount = std::get<1>(t); CScript scriptPubKey = std::get<2>(t); builder_.AddTransparentInput(outPoint, scriptPubKey, amount); } boost::optional<uint256> ovk; // Select Sapling notes std::vector<SaplingOutPoint> saplingOPs; std::vector<SaplingNote> saplingNotes; std::vector<SaplingExpandedSpendingKey> expsks; for (const MergeToAddressInputSaplingNote& saplingNoteInput: saplingNoteInputs_) { saplingOPs.push_back(std::get<0>(saplingNoteInput)); saplingNotes.push_back(std::get<1>(saplingNoteInput)); auto expsk = std::get<3>(saplingNoteInput); expsks.push_back(expsk); if (!ovk) { ovk = expsk.full_viewing_key().ovk; } } // Fetch Sapling anchor and witnesses uint256 anchor; std::vector<boost::optional<SaplingWitness>> witnesses; { LOCK2(cs_main, pwalletMain->cs_wallet); pwalletMain->GetSaplingNoteWitnesses(saplingOPs, witnesses, anchor); } // Add Sapling spends for (size_t i = 0; i < saplingNotes.size(); i++) { if (!witnesses[i]) { throw JSONRPCError(RPC_WALLET_ERROR, "Missing witness for Sapling note"); } builder_.AddSaplingSpend(expsks[i], saplingNotes[i], anchor, witnesses[i].get()); } if (isToTaddr_) { builder_.AddTransparentOutput(toTaddr_, sendAmount); } else { std::string zaddr = std::get<0>(recipient_); std::string memo = std::get<1>(recipient_); std::array<unsigned char, ZC_MEMO_SIZE> hexMemo = get_memo_from_hex_string(memo); auto saplingPaymentAddress = boost::get<libzcash::SaplingPaymentAddress>(&toPaymentAddress_); if (saplingPaymentAddress == nullptr) { // This should never happen as we have already determined that the payment is to sapling throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Could not get Sapling payment address."); } if (saplingNoteInputs_.size() == 0 && utxoInputs_.size() > 0) { // Sending from t-addresses, which we don't have ovks for. Instead, // generate a common one from the HD seed. This ensures the data is // recoverable, while keeping it logically separate from the ZIP 32 // Sapling key hierarchy, which the user might not be using. HDSeed seed = pwalletMain->GetHDSeedForRPC(); ovk = ovkForShieldingFromTaddr(seed); } if (!ovk) { throw JSONRPCError(RPC_WALLET_ERROR, "Sending to a Sapling address requires an ovk."); } builder_.AddSaplingOutput(ovk.get(), *saplingPaymentAddress, sendAmount, hexMemo); } // Build the transaction tx_ = builder_.Build().GetTxOrThrow(); // Send the transaction // TODO: Use CWallet::CommitTransaction instead of sendrawtransaction auto signedtxn = EncodeHexTx(tx_); if (!testmode) { UniValue params = UniValue(UniValue::VARR); params.push_back(signedtxn); UniValue sendResultValue = sendrawtransaction(params, false); if (sendResultValue.isNull()) { throw JSONRPCError(RPC_WALLET_ERROR, "sendrawtransaction did not return an error or a txid."); } auto txid = sendResultValue.get_str(); UniValue o(UniValue::VOBJ); o.push_back(Pair("txid", txid)); set_result(o); } else { // Test mode does not send the transaction to the network. UniValue o(UniValue::VOBJ); o.push_back(Pair("test", 1)); o.push_back(Pair("txid", tx_.GetHash().ToString())); o.push_back(Pair("hex", signedtxn)); set_result(o); } return true; } /** * END SCENARIO #0 */ /** * SCENARIO #1 * * taddrs -> taddr * * There are no zaddrs or joinsplits involved. */ if (isPureTaddrOnlyTx) { UniValue obj(UniValue::VOBJ); obj.push_back(Pair("rawtxn", EncodeHexTx(tx_))); sign_send_raw_transaction(obj); return true; } /** * END SCENARIO #1 */ // Prepare raw transaction to handle JoinSplits CMutableTransaction mtx(tx_); crypto_sign_keypair(joinSplitPubKey_.begin(), joinSplitPrivKey_); mtx.joinSplitPubKey = joinSplitPubKey_; tx_ = CTransaction(mtx); std::string hexMemo = std::get<1>(recipient_); /** * SCENARIO #2 * * taddrs -> zaddr * * We only need a single JoinSplit. */ if (sproutNoteInputs_.empty() && isToZaddr_) { // Create JoinSplit to target z-addr. MergeToAddressJSInfo info; info.vpub_old = sendAmount; info.vpub_new = 0; JSOutput jso = JSOutput(boost::get<libzcash::SproutPaymentAddress>(toPaymentAddress_), sendAmount); if (hexMemo.size() > 0) { jso.memo = get_memo_from_hex_string(hexMemo); } info.vjsout.push_back(jso); UniValue obj(UniValue::VOBJ); obj = perform_joinsplit(info); sign_send_raw_transaction(obj); return true; } /** * END SCENARIO #2 */ // Copy zinputs to more flexible containers std::deque<MergeToAddressInputSproutNote> zInputsDeque; for (const auto& o : sproutNoteInputs_) { zInputsDeque.push_back(o); } // When spending notes, take a snapshot of note witnesses and anchors as the treestate will // change upon arrival of new blocks which contain joinsplit transactions. This is likely // to happen as creating a chained joinsplit transaction can take longer than the block interval. { LOCK2(cs_main, pwalletMain->cs_wallet); for (auto t : sproutNoteInputs_) { JSOutPoint jso = std::get<0>(t); std::vector<JSOutPoint> vOutPoints = {jso}; uint256 inputAnchor; std::vector<boost::optional<SproutWitness>> vInputWitnesses; pwalletMain->GetSproutNoteWitnesses(vOutPoints, vInputWitnesses, inputAnchor); jsopWitnessAnchorMap[jso.ToString()] = MergeToAddressWitnessAnchorData{vInputWitnesses[0], inputAnchor}; } } /** * SCENARIO #3 * * zaddrs -> zaddr * taddrs -> * * zaddrs -> * taddrs -> taddr * * Send to zaddr by chaining JoinSplits together and immediately consuming any change * Send to taddr by creating dummy z outputs and accumulating value in a change note * which is used to set vpub_new in the last chained joinsplit. */ UniValue obj(UniValue::VOBJ); CAmount jsChange = 0; // this is updated after each joinsplit int changeOutputIndex = -1; // this is updated after each joinsplit if jsChange > 0 bool vpubOldProcessed = false; // updated when vpub_old for taddr inputs is set in first joinsplit bool vpubNewProcessed = false; // updated when vpub_new for miner fee and taddr outputs is set in last joinsplit // At this point, we are guaranteed to have at least one input note. // Use address of first input note as the temporary change address. SproutSpendingKey changeKey = std::get<3>(zInputsDeque.front()); SproutPaymentAddress changeAddress = changeKey.address(); CAmount vpubOldTarget = 0; CAmount vpubNewTarget = 0; if (isToTaddr_) { vpubNewTarget = z_inputs_total; } else { if (utxoInputs_.empty()) { vpubNewTarget = minersFee; } else { vpubOldTarget = t_inputs_total - minersFee; } } // Keep track of treestate within this transaction boost::unordered_map<uint256, SproutMerkleTree, CCoinsKeyHasher> intermediates; std::vector<uint256> previousCommitments; while (!vpubNewProcessed) { MergeToAddressJSInfo info; info.vpub_old = 0; info.vpub_new = 0; // Set vpub_old in the first joinsplit if (!vpubOldProcessed) { if (t_inputs_total < vpubOldTarget) { throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Insufficient transparent funds for vpub_old %s (miners fee %s, taddr inputs %s)", FormatMoney(vpubOldTarget), FormatMoney(minersFee), FormatMoney(t_inputs_total))); } info.vpub_old += vpubOldTarget; // funds flowing from public pool vpubOldProcessed = true; } CAmount jsInputValue = 0; uint256 jsAnchor; std::vector<boost::optional<SproutWitness>> witnesses; JSDescription prevJoinSplit; // Keep track of previous JoinSplit and its commitments if (tx_.vjoinsplit.size() > 0) { prevJoinSplit = tx_.vjoinsplit.back(); } // If there is no change, the chain has terminated so we can reset the tracked treestate. if (jsChange == 0 && tx_.vjoinsplit.size() > 0) { intermediates.clear(); previousCommitments.clear(); } // // Consume change as the first input of the JoinSplit. // if (jsChange > 0) { LOCK2(cs_main, pwalletMain->cs_wallet); // Update tree state with previous joinsplit SproutMerkleTree tree; auto it = intermediates.find(prevJoinSplit.anchor); if (it != intermediates.end()) { tree = it->second; } else if (!pcoinsTip->GetSproutAnchorAt(prevJoinSplit.anchor, tree)) { throw JSONRPCError(RPC_WALLET_ERROR, "Could not find previous JoinSplit anchor"); } assert(changeOutputIndex != -1); boost::optional<SproutWitness> changeWitness; int n = 0; for (const uint256& commitment : prevJoinSplit.commitments) { tree.append(commitment); previousCommitments.push_back(commitment); if (!changeWitness && changeOutputIndex == n++) { changeWitness = tree.witness(); } else if (changeWitness) { changeWitness.get().append(commitment); } } if (changeWitness) { witnesses.push_back(changeWitness); } jsAnchor = tree.root(); intermediates.insert(std::make_pair(tree.root(), tree)); // chained js are interstitial (found in between block boundaries) // Decrypt the change note's ciphertext to retrieve some data we need ZCNoteDecryption decryptor(changeKey.receiving_key()); auto hSig = prevJoinSplit.h_sig(*pzcashParams, tx_.joinSplitPubKey); try { SproutNotePlaintext plaintext = SproutNotePlaintext::decrypt( decryptor, prevJoinSplit.ciphertexts[changeOutputIndex], prevJoinSplit.ephemeralKey, hSig, (unsigned char)changeOutputIndex); SproutNote note = plaintext.note(changeAddress); info.notes.push_back(note); info.zkeys.push_back(changeKey); jsInputValue += plaintext.value(); LogPrint("zrpcunsafe", "%s: spending change (amount=%s)\n", getId(), FormatMoney(plaintext.value())); } catch (const std::exception& e) { throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Error decrypting output note of previous JoinSplit: %s", e.what())); } } // // Consume spendable non-change notes // std::vector<SproutNote> vInputNotes; std::vector<SproutSpendingKey> vInputZKeys; std::vector<JSOutPoint> vOutPoints; std::vector<boost::optional<SproutWitness>> vInputWitnesses; uint256 inputAnchor; int numInputsNeeded = (jsChange > 0) ? 1 : 0; while (numInputsNeeded++ < ZC_NUM_JS_INPUTS && zInputsDeque.size() > 0) { MergeToAddressInputSproutNote t = zInputsDeque.front(); JSOutPoint jso = std::get<0>(t); SproutNote note = std::get<1>(t); CAmount noteFunds = std::get<2>(t); SproutSpendingKey zkey = std::get<3>(t); zInputsDeque.pop_front(); MergeToAddressWitnessAnchorData wad = jsopWitnessAnchorMap[jso.ToString()]; vInputWitnesses.push_back(wad.witness); if (inputAnchor.IsNull()) { inputAnchor = wad.anchor; } else if (inputAnchor != wad.anchor) { throw JSONRPCError(RPC_WALLET_ERROR, "Selected input notes do not share the same anchor"); } vOutPoints.push_back(jso); vInputNotes.push_back(note); vInputZKeys.push_back(zkey); jsInputValue += noteFunds; int wtxHeight = -1; int wtxDepth = -1; { LOCK2(cs_main, pwalletMain->cs_wallet); const CWalletTx& wtx = pwalletMain->mapWallet[jso.hash]; // Zero confirmation notes belong to transactions which have not yet been mined if (mapBlockIndex.find(wtx.hashBlock) == mapBlockIndex.end()) { throw JSONRPCError(RPC_WALLET_ERROR, strprintf("mapBlockIndex does not contain block hash %s", wtx.hashBlock.ToString())); } wtxHeight = mapBlockIndex[wtx.hashBlock]->nHeight; wtxDepth = wtx.GetDepthInMainChain(); } LogPrint("zrpcunsafe", "%s: spending note (txid=%s, vjoinsplit=%d, ciphertext=%d, amount=%s, height=%d, confirmations=%d)\n", getId(), jso.hash.ToString().substr(0, 10), jso.js, int(jso.n), // uint8_t FormatMoney(noteFunds), wtxHeight, wtxDepth); } // Add history of previous commitments to witness if (vInputNotes.size() > 0) { if (vInputWitnesses.size() == 0) { throw JSONRPCError(RPC_WALLET_ERROR, "Could not find witness for note commitment"); } for (auto& optionalWitness : vInputWitnesses) { if (!optionalWitness) { throw JSONRPCError(RPC_WALLET_ERROR, "Witness for note commitment is null"); } SproutWitness w = *optionalWitness; // could use .get(); if (jsChange > 0) { for (const uint256& commitment : previousCommitments) { w.append(commitment); } if (jsAnchor != w.root()) { throw JSONRPCError(RPC_WALLET_ERROR, "Witness for spendable note does not have same anchor as change input"); } } witnesses.push_back(w); } // The jsAnchor is null if this JoinSplit is at the start of a new chain if (jsAnchor.IsNull()) { jsAnchor = inputAnchor; } // Add spendable notes as inputs std::copy(vInputNotes.begin(), vInputNotes.end(), std::back_inserter(info.notes)); std::copy(vInputZKeys.begin(), vInputZKeys.end(), std::back_inserter(info.zkeys)); } // Accumulate change jsChange = jsInputValue + info.vpub_old; // Set vpub_new in the last joinsplit (when there are no more notes to spend) if (zInputsDeque.empty()) { assert(!vpubNewProcessed); if (jsInputValue < vpubNewTarget) { throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Insufficient funds for vpub_new %s (miners fee %s, taddr inputs %s)", FormatMoney(vpubNewTarget), FormatMoney(minersFee), FormatMoney(t_inputs_total))); } info.vpub_new += vpubNewTarget; // funds flowing back to public pool vpubNewProcessed = true; jsChange -= vpubNewTarget; // If we are merging to a t-addr, there should be no change if (isToTaddr_) assert(jsChange == 0); } // create dummy output info.vjsout.push_back(JSOutput()); // dummy output while we accumulate funds into a change note for vpub_new // create output for any change if (jsChange > 0) { std::string outputType = "change"; auto jso = JSOutput(changeAddress, jsChange); // If this is the final output, set the target and memo if (isToZaddr_ && vpubNewProcessed) { outputType = "target"; jso.addr = boost::get<libzcash::SproutPaymentAddress>(toPaymentAddress_); if (!hexMemo.empty()) { jso.memo = get_memo_from_hex_string(hexMemo); } } info.vjsout.push_back(jso); LogPrint("zrpcunsafe", "%s: generating note for %s (amount=%s)\n", getId(), outputType, FormatMoney(jsChange)); } obj = perform_joinsplit(info, witnesses, jsAnchor); if (jsChange > 0) { changeOutputIndex = mta_find_output(obj, 1); } } // Sanity check in case changes to code block above exits loop by invoking 'break' assert(zInputsDeque.size() == 0); assert(vpubNewProcessed); sign_send_raw_transaction(obj); return true; }