]> Git Repo - VerusCoin.git/commitdiff
Fix intermediate vpub_new leakage in multi joinsplit tx (#1360)
authorSimon <[email protected]>
Mon, 12 Jun 2017 06:13:14 +0000 (23:13 -0700)
committerSimon <[email protected]>
Wed, 14 Jun 2017 06:21:53 +0000 (23:21 -0700)
src/wallet/asyncrpcoperation_sendmany.cpp

index 1691452b892299b360b00d1378edfa6b8b2c6df2..00915c68f17ac35ea9ab923f948fb04bfe6b8f23 100644 (file)
@@ -342,9 +342,14 @@ bool AsyncRPCOperation_sendmany::main_impl() {
     tx_ = CTransaction(mtx);
 
     // Copy zinputs and zoutputs to more flexible containers
-    std::deque<SendManyInputJSOP> zInputsDeque;
+    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_) {
@@ -442,283 +447,216 @@ bool AsyncRPCOperation_sendmany::main_impl() {
      * zaddr -> taddrs
      *       -> zaddrs
      * 
-     * Processing order:
-     * Part 1: taddrs and miners fee
-     * Part 2: zaddrs 
-     */
-    
-    /**
-     * SCENARIO #3
-     * Part 1: Add to the transparent value pool.
+     * 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 minersFeeProcessed = false;
-
+    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();
-        CAmount taddrTargetAmount = t_outputs_total + minersFee;
-        minersFeeProcessed = true;
-        while (zInputsDeque.size() > 0 && taddrTargetAmount > 0) {
-            AsyncJoinSplitInfo info;
-            info.vpub_old = 0;
-            info.vpub_new = 0;
-            std::vector<JSOutPoint> outPoints;
-            int n = 0;
-            while (n++ < ZC_NUM_JS_INPUTS && taddrTargetAmount > 0) {
-                SendManyInputJSOP o = zInputsDeque.front();
-                JSOutPoint outPoint = std::get<0>(o);
-                Note note = std::get<1>(o);
-                CAmount noteFunds = std::get<2>(o);
-                zInputsDeque.pop_front();
+        vpubNewTarget += t_outputs_total;
+    }
 
-                info.notes.push_back(note);
-                outPoints.push_back(outPoint);
-
-                int wtxHeight = -1;
-                int wtxDepth = -1;
-                {
-                    LOCK2(cs_main, pwalletMain->cs_wallet);
-                    const CWalletTx& wtx = pwalletMain->mapWallet[outPoint.hash];
-                    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(),
-                        outPoint.hash.ToString().substr(0, 10),
-                        outPoint.js,
-                        int(outPoint.n), // uint8_t
-                        FormatMoney(noteFunds),
-                        wtxHeight,
-                        wtxDepth
-                        );
+    // Keep track of treestate within this transaction
+    boost::unordered_map<uint256, ZCIncrementalMerkleTree, CCoinsKeyHasher> intermediates;
+    std::vector<uint256> previousCommitments;
 
-                
-                // Put value back into the value pool
-                if (noteFunds >= taddrTargetAmount) {
-                    jsChange = noteFunds - taddrTargetAmount;
-                    info.vpub_new += taddrTargetAmount;
-                } else {
-                    info.vpub_new += noteFunds;
-                }
+    while (!vpubNewProcessed) {
+        AsyncJoinSplitInfo info;
+        info.vpub_old = 0;
+        info.vpub_new = 0;
 
-                taddrTargetAmount -= noteFunds;
-                if (taddrTargetAmount <= 0) {
-                    break;
-                }
-            }
+        CAmount jsInputValue = 0;
+        uint256 jsAnchor;
+        std::vector<boost::optional<ZCIncrementalWitness>> witnesses;
 
-            if (jsChange > 0) {
-                info.vjsout.push_back(JSOutput());
-                info.vjsout.push_back(JSOutput(frompaymentaddress_, jsChange));
-                
-                LogPrint("zrpcunsafe", "%s: generating note for change (amount=%s)\n",
-                        getId(),
-                        FormatMoney(jsChange)
-                        );
-            }
+        JSDescription prevJoinSplit;
+
+        // Keep track of previous JoinSplit and its commitments
+        if (tx_.vjoinsplit.size() > 0) {
+            prevJoinSplit = tx_.vjoinsplit.back();
+        }
 
-            obj = perform_joinsplit(info, outPoints);
+        // 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();
+        }
 
-            if (jsChange > 0) {
-                changeOutputIndex = find_output(obj, 1);
+        //
+        // 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->GetAnchorAt(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(spendingkey_.viewing_key());
+            auto hSig = prevJoinSplit.h_sig(*pzcashParams, tx_.joinSplitPubKey);
+            try {
+                NotePlaintext plaintext = NotePlaintext::decrypt(
+                        decryptor,
+                        prevJoinSplit.ciphertexts[changeOutputIndex],
+                        prevJoinSplit.ephemeralKey,
+                        hSig,
+                        (unsigned char) changeOutputIndex);
+
+                Note note = plaintext.note(frompaymentaddress_);
+                info.notes.push_back(note);
 
-    /**
-     * SCENARIO #3
-     * Part 2: Send to zaddrs by chaining JoinSplits together and immediately consuming any change
-     */            
-    if (z_outputs_total>0) {
+                jsInputValue += plaintext.value;
 
-        // Keep track of treestate within this transaction 
-        boost::unordered_map<uint256, ZCIncrementalMerkleTree, CCoinsKeyHasher> intermediates;
-        std::vector<uint256> previousCommitments;
-        
-        while (zOutputsDeque.size() > 0) {
-            AsyncJoinSplitInfo info;
-            info.vpub_old = 0;
-            info.vpub_new = 0;
+                LogPrint("zrpcunsafe", "%s: spending change (amount=%s)\n",
+                    getId(),
+                    FormatMoney(plaintext.value)
+                    );
 
-            CAmount jsInputValue = 0;
-            uint256 jsAnchor;
-            std::vector<boost::optional<ZCIncrementalWitness>> witnesses;
+            } catch (const std::exception& e) {
+                throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Error decrypting output note of previous JoinSplit: %s", e.what()));
+            }
+        }
 
-            JSDescription prevJoinSplit;
 
-            // Keep track of previous JoinSplit and its commitments
-            if (tx_.vjoinsplit.size() > 0) {
-                prevJoinSplit = tx_.vjoinsplit.back();
+        //
+        // Consume spendable non-change notes
+        //
+        std::vector<Note> 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);
+            Note 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);
             
-            // 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();
-            }
+            jsInputValue += noteFunds;
             
-            //
-            // Consume change as the first input of the JoinSplit.
-            //
-            if (jsChange > 0) {
+            int wtxHeight = -1;
+            int wtxDepth = -1;
+            {
                 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->GetAnchorAt(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(spendingkey_.viewing_key());
-                auto hSig = prevJoinSplit.h_sig(*pzcashParams, tx_.joinSplitPubKey);
-                try {
-                    NotePlaintext plaintext = NotePlaintext::decrypt(
-                            decryptor,
-                            prevJoinSplit.ciphertexts[changeOutputIndex],
-                            prevJoinSplit.ephemeralKey,
-                            hSig,
-                            (unsigned char) changeOutputIndex);
-
-                    Note note = plaintext.note(frompaymentaddress_);
-                    info.notes.push_back(note);
-                    
-                    jsInputValue += plaintext.value;
+                const CWalletTx& wtx = pwalletMain->mapWallet[jso.hash];
+                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
+                    );
+        }
                     
-                    LogPrint("zrpcunsafe", "%s: spending change (amount=%s)\n",
-                        getId(),
-                        FormatMoney(plaintext.value)
-                        );
+        // Add history of previous commitments to witness
+        if (vInputNotes.size() > 0) {
 
-                } catch (const std::exception& e) {
-                    throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Error decrypting output note of previous JoinSplit: %s", e.what()));
-                }
+            if (vInputWitnesses.size()==0) {
+                throw JSONRPCError(RPC_WALLET_ERROR, "Could not find witness for note commitment");
             }
-
             
-            //
-            // Consume spendable non-change notes
-            //
-            std::vector<Note> 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);
-                Note 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");
+            for (auto & optionalWitness : vInputWitnesses) {
+                if (!optionalWitness) {
+                    throw JSONRPCError(RPC_WALLET_ERROR, "Witness for note commitment is null");
                 }
-
-                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];
-                    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);
                     }
-                    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");
-                        }
+                    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;                   
                 }
+                witnesses.push_back(w);
+            }
 
-                // Add spendable notes as inputs
-                std::copy(vInputNotes.begin(), vInputNotes.end(), std::back_inserter(info.notes));
+            // The jsAnchor is null if this JoinSplit is at the start of a new chain
+            if (jsAnchor.IsNull()) {
+                jsAnchor = inputAnchor;
             }
 
-            
-            //
-            // Find recipient to transfer funds to
-            //            
+            // 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();
-            std::string address = std::get<0>(smr);
-            CAmount value = std::get<1>(smr);
-            std::string hexMemo = std::get<2>(smr);
+            address = std::get<0>(smr);
+            value = std::get<1>(smr);
+            hexMemo = std::get<2>(smr);
             zOutputsDeque.pop_front();
+        }
 
-            // Will we have any change?  Has the miners fee been processed yet?
-            jsChange = 0;
-            CAmount outAmount = value;
-            if (!minersFeeProcessed) {
-                if (jsInputValue < minersFee) {
-                    throw JSONRPCError(RPC_WALLET_ERROR, "Not enough funds to pay miners fee");
-                }
-                outAmount += minersFee;
+        // 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) {
@@ -729,42 +667,44 @@ bool AsyncRPCOperation_sendmany::main_impl() {
 
                 // reduce the amount being sent right now to the value of all inputs
                 value = jsInputValue;
-                if (!minersFeeProcessed) {
-                    value -= minersFee;
-                }
-            }
-            
-            if (!minersFeeProcessed) {
-                minersFeeProcessed = true;
-                info.vpub_new += minersFee; // funds flowing back to public pool
             }
-            
-            // create output for recipient
+        }
+
+        // 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 = CZCPaymentAddress(address).Get();
             JSOutput jso = JSOutput(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(frompaymentaddress_, jsChange));
+        }
 
-                LogPrint("zrpcunsafe", "%s: generating note for change (amount=%s)\n",
-                        getId(),
-                        FormatMoney(jsChange)
-                        );
-            }
+        // create output for any change
+        if (jsChange>0) {
+            info.vjsout.push_back(JSOutput(frompaymentaddress_, jsChange));
 
-            obj = perform_joinsplit(info, witnesses, jsAnchor);
+            LogPrint("zrpcunsafe", "%s: generating note for change (amount=%s)\n",
+                    getId(),
+                    FormatMoney(jsChange)
+                    );
+        }
 
-            if (jsChange > 0) {
-                changeOutputIndex = find_output(obj, 1);
-            }
+        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;
 }
This page took 0.043624 seconds and 4 git commands to generate.