From 8ef102a035959a529905f1fa73a0ea9c9478b60f Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Tue, 25 Apr 2017 11:55:41 -0400 Subject: [PATCH] Make best-effort to blind desired outputs, fail or return depending on `ignoreblindfail` arguments. --- qa/rpc-tests/confidential_transactions.py | 11 +- src/rpc/client.cpp | 2 + src/rpc/rawtransaction.cpp | 116 ++++++++++++++++++---- src/wallet/rpcwallet.cpp | 34 +++++-- src/wallet/wallet.cpp | 79 +++++++++++---- src/wallet/wallet.h | 2 +- 6 files changed, 188 insertions(+), 56 deletions(-) diff --git a/qa/rpc-tests/confidential_transactions.py b/qa/rpc-tests/confidential_transactions.py index aabaa8ef4d..05b285e23f 100755 --- a/qa/rpc-tests/confidential_transactions.py +++ b/qa/rpc-tests/confidential_transactions.py @@ -145,12 +145,11 @@ class CTTest (BitcoinTestFramework): "nValue": unspent[0]["amount"]}], {unconfidential_address: unspent[0]["amount"] - fee, "fee":fee}); - # Test that blindrawtransaction returns an exception - try: - tx = self.nodes[0].blindrawtransaction(tx) - raise AssertionError("blindrawtransaction RPC should fail, but it doesn't") - except JSONRPCException: - pass + # Test that blindrawtransaction adds an OP_RETURN output to balance blinders + temptx = self.nodes[0].blindrawtransaction(tx) + decodedtx = self.nodes[0].decoderawtransaction(temptx) + assert_equal(decodedtx["vout"][-1]["scriptPubKey"]["asm"], "OP_RETURN") + assert_equal(len(decodedtx["vout"]), 3) # Create same transaction but with a change/dummy output. # It should pass the blinding step. diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 326a5254a4..e7cd7f2aee 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -41,6 +41,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "sendtoaddress", 1, "amount" }, { "sendtoaddress", 4, "subtractfeefromamount" }, { "settxfee", 0, "amount" }, + { "sendtoaddress", 6, "ignoreblindfail" }, { "getreceivedbyaddress", 1, "minconf" }, { "destroyamount", 1, "amount" }, { "getreceivedbyaccount", 1, "minconf" }, @@ -86,6 +87,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "getrawtransaction", 1, "verbose" }, { "rawblindrawtransaction", 1, "inputblinder" }, { "blindrawtransaction", 1, "assetcommitments" }, + { "blindrawtransaction", 2, "ignoreblindfail" }, { "createrawtransaction", 0, "inputs" }, { "createrawtransaction", 1, "outputs" }, { "dumpissuanceblindingkey", 1, "vin" }, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 5a6218a82f..17aac8c2ca 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -726,14 +726,14 @@ void FillBlinds(CMutableTransaction& tx, bool fUseWallet, std::vector& UniValue rawblindrawtransaction(const JSONRPCRequest& request) { - if (request.fHelp || (request.params.size() < 5 || request.params.size() > 6)) + if (request.fHelp || (request.params.size() < 5 || request.params.size() > 7)) throw std::runtime_error( - "rawblindrawtransaction \"hexstring\" [\"inputblinder\",...] [\"totalblinder\"]\n" + "rawblindrawtransaction \"hexstring\" [\"inputblinder\",...] [\"totalblinder\"] ignoreblindfail\n" "\nConvert one or more outputs of a raw transaction into confidential ones.\n" "Returns the hex-encoded raw transaction.\n" - "If at least one of the inputs is confidential, at least one of the outputs must be.\n" "The input raw transaction cannot have already-blinded outputs.\n" "The output keys used can be specified by using a confidential address in createrawtransaction.\n" + "If an additional blinded output is required to make a balanced blinding, a 0-value unspendable output will be added. Since there is no access to the wallet the blinding pubkey from the last output with blinding key will be repeated.\n" "\nArguments:\n" "1. \"hexstring\", (string, required) A hex-encoded raw transaction.\n" @@ -751,6 +751,7 @@ UniValue rawblindrawtransaction(const JSONRPCRequest& request) " \"inputassetblinder\" (string, required) A hex-encoded asset blinding factor, one for each input.\n" " ],\n" "6. \"totalblinder\" (string, optional) Ignored for now.\n" + "7. \"ignoreblindfail\"\" (bool, default=true) Return a transaction even when a blinding attempt fails due to number of blinded inputs/outputs.\n" "\nResult:\n" "\"transaction\" (string) hex string of the transaction\n" @@ -758,8 +759,10 @@ UniValue rawblindrawtransaction(const JSONRPCRequest& request) if (request.params.size() == 5) { RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VARR)(UniValue::VARR)(UniValue::VARR)(UniValue::VARR)); - } else { + } else if (request.params.size() == 6) { RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VARR)(UniValue::VARR)(UniValue::VARR)(UniValue::VARR)(UniValue::VSTR)); + } else { + RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VARR)(UniValue::VARR)(UniValue::VARR)(UniValue::VARR)(UniValue::VSTR)(UniValue::VBOOL)); } vector txData(ParseHexV(request.params[0], "argument 1")); @@ -776,6 +779,13 @@ UniValue rawblindrawtransaction(const JSONRPCRequest& request) UniValue inputAssets = request.params[3].get_array(); UniValue inputAssetBlinds = request.params[4].get_array(); + bool fIgnoreBlindFail = true; + if (request.params.size() > 6) { + fIgnoreBlindFail = request.params[6].get_bool(); + } + + int n_blinded_ins = 0; + if (inputBlinds.size() != tx.vin.size()) throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter: one (potentially empty) input blind for each input must be provided")); if (inputAmounts.size() != tx.vin.size()) throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter: one (potentially empty) input blind for each input must be provided")); if (inputAssets.size() != tx.vin.size()) throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter: one (potentially empty) input asset id for each input must be provided")); @@ -813,6 +823,10 @@ UniValue rawblindrawtransaction(const JSONRPCRequest& request) input_asset_blinds.push_back(uint256S(assetblind)); input_assets.push_back(CAsset(uint256S(asset))); input_amounts.push_back(inputAmounts[nIn].get_int64()); + + if (!input_blinds.back().IsNull()) { + n_blinded_ins++; + } } std::vector asset_keys; @@ -821,14 +835,40 @@ UniValue rawblindrawtransaction(const JSONRPCRequest& request) // How many are we trying to blind? int numPubKeys = 0; - for (auto&& key : output_pubkeys) { + unsigned int keyIndex = 0; + for (unsigned int i = 0; i < output_pubkeys.size(); i++) { + const CPubKey& key = output_pubkeys[i]; if (key.IsValid()) { numPubKeys++; + keyIndex = i; } } - if (numPubKeys == 0 || BlindTransaction(input_blinds, input_asset_blinds, input_assets, input_amounts, output_value_blinds, output_asset_blinds, output_pubkeys, std::vector(), std::vector(), tx) != numPubKeys) { - throw JSONRPCError(RPC_INVALID_PARAMETER, string("Unable to blind transaction: add an additional output with a blinding pubkey")); + if (numPubKeys == 0 && n_blinded_ins == 0) { + // Vacuous, just return the transaction + return EncodeHexTx(tx); + } else if (n_blinded_ins > 0 && numPubKeys == 0) { + // Blinded inputs need to balanced with something to be valid, make a dummy. + // No privacy lost because all outputs are explicit anyways. + CTxOut newTxOut(tx.vout.back().nAsset.GetAsset(), 0, CScript() << OP_RETURN); + tx.vout.push_back(newTxOut); + numPubKeys++; + // Just copy some non-zero key + output_pubkeys.push_back(output_pubkeys[keyIndex]); + } else if (n_blinded_ins == 0 && numPubKeys == 1) { + if (fIgnoreBlindFail) { + // Just get rid of the ECDH key in the nonce field and return + tx.vout[keyIndex].nNonce.SetNull(); + return EncodeHexTx(tx); + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, string("Unable to blind transaction: Add another output to blind in order to complete the blinding.")); + } + } + + if (BlindTransaction(input_blinds, input_asset_blinds, input_assets, input_amounts, output_value_blinds, output_asset_blinds, output_pubkeys, std::vector(), std::vector(), tx) != numPubKeys) { + // TODO Have more rich return values, communicating to user what has been blinded + // User may be ok not blinding something that for instance has no corresponding type on input + throw JSONRPCError(RPC_INVALID_PARAMETER, string("Unable to blind transaction: Are you sure each asset type to blind is represented in the inputs?")); } return EncodeHexTx(tx); @@ -837,21 +877,22 @@ UniValue rawblindrawtransaction(const JSONRPCRequest& request) #ifdef ENABLE_WALLET UniValue blindrawtransaction(const JSONRPCRequest& request) { - if (request.fHelp || (request.params.size() != 1 && request.params.size() != 2)) + if (request.fHelp || (request.params.size() < 1 || request.params.size() > 4)) throw runtime_error( - "blindrawtransaction \"hexstring\" [\"totalblinder\"]\n" + "blindrawtransaction \"hexstring\" [\"assetcommitments\"] [\"totalblinder\"] ignoreblindfail\n" "\nConvert one or more outputs of a raw transaction into confidential ones using only wallet inputs.\n" "Returns the hex-encoded raw transaction.\n" - "If at least one of the inputs is confidential, at least one of the outputs must be.\n" "The output keys used can be specified by using a confidential address in createrawtransaction.\n" + "This call may add an additional 0-value unspendable output in order to balance the blinders.\n" "\nArguments:\n" "1. \"hexstring\", (string, required) A hex-encoded raw transaction.\n" - "2. [ (array, optional) An array of input asset generators. If provided, this list must match the final input commitment list, including ordering, to make a valid surjection proof. This list does not include generators for issuances, as these assets are inherently unblinded.\n" + "2. \"ignoreblindfail\"\" (bool, default=true) Return a transaction even when a blinding attempt fails due to number of blinded inputs/outputs.\n" + "3. [ (array, optional) An array of input asset generators. If provided, this list must be empty, or match the final input commitment list, including ordering, to make a valid surjection proof. This list does not include generators for issuances, as these assets are inherently unblinded.\n" " \"assetcommitments\" (string, optional) A hex-encoded asset commitment, one for each input.\n" " Null commitments must be \"\".\n" " ],\n" - "3. \"totalblinder\" (string, optional) Ignored for now.\n" + "4. \"totalblinder\" (string, optional) Ignored for now.\n" "\nResult:\n" "\"transaction\" (string) hex string of the transaction\n" @@ -860,9 +901,11 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) if (request.params.size() == 1) { RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)); } else if (request.params.size() == 2){ - RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VARR)); + RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VBOOL)); } else if (request.params.size() == 3){ - RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VARR)(UniValue::VSTR)); + RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VBOOL)(UniValue::VARR)); + } else { + RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VBOOL)(UniValue::VARR)(UniValue::VSTR)); } vector txData(ParseHexV(request.params[0], "argument 1")); @@ -874,10 +917,15 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); } - std::vector > auxiliary_generators; + bool fIgnoreBlindFail = true; if (request.params.size() > 1) { - UniValue assetCommitments = request.params[1].get_array(); - if (assetCommitments.size() < tx.vin.size()) { + fIgnoreBlindFail = request.params[1].get_bool(); + } + + std::vector > auxiliary_generators; + if (request.params.size() > 2) { + UniValue assetCommitments = request.params[2].get_array(); + if (assetCommitments.size() != 0 && assetCommitments.size() < tx.vin.size()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Asset commitment array must have at least as many entries as transaction inputs."); } for (size_t nIn = 0; nIn < assetCommitments.size(); nIn++) { @@ -902,6 +950,7 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) std::vector output_asset_blinds; std::vector output_assets; std::vector output_pubkeys; + int n_blinded_ins = 0; for (size_t nIn = 0; nIn < tx.vin.size(); nIn++) { std::map::iterator it = pwalletMain->mapWallet.find(tx.vin[nIn].prevout.hash); @@ -933,6 +982,7 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) } else { input_amounts.push_back(it->second.GetOutputValueOut(tx.vin[nIn].prevout.n)); + n_blinded_ins += 1; } } @@ -942,15 +992,39 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) // How many are we trying to blind? int numPubKeys = 0; - for (auto&& key : output_pubkeys) { + unsigned int keyIndex = 0; + for (unsigned int i = 0; i < output_pubkeys.size(); i++) { + const CPubKey& key = output_pubkeys[i]; if (key.IsValid()) { numPubKeys++; + keyIndex = i; } } - // Something must become blinded, and all attempts must work - if (numPubKeys == 0 || BlindTransaction(input_blinds, input_asset_blinds, input_assets, input_amounts, output_blinds, output_asset_blinds, output_pubkeys, std::vector(), std::vector(), tx, (auxiliary_generators.size() ? &auxiliary_generators : NULL)) != numPubKeys) { - throw JSONRPCError(RPC_INVALID_PARAMETER, string("Unable to blind transaction: add an additional output with a blinding pubkey")); + if (numPubKeys == 0 && n_blinded_ins == 0) { + // Vacuous, just return the transaction + return EncodeHexTx(tx); + } else if (n_blinded_ins > 0 && numPubKeys == 0) { + // Blinded inputs need to balanced with something to be valid, make a dummy. + // No privacy lost because all outputs are explicit anyways. + CTxOut newTxOut(tx.vout.back().nAsset.GetAsset(), 0, CScript() << OP_RETURN); + tx.vout.push_back(newTxOut); + numPubKeys++; + output_pubkeys.push_back(pwalletMain->GetBlindingPubKey(newTxOut.scriptPubKey)); + } else if (n_blinded_ins == 0 && numPubKeys == 1) { + if (fIgnoreBlindFail) { + // Just get rid of the ECDH key in the nonce field and return + tx.vout[keyIndex].nNonce.SetNull(); + return EncodeHexTx(tx); + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, string("Unable to blind transaction: Add another output to blind in order to complete the blinding.")); + } + } + + if (BlindTransaction(input_blinds, input_asset_blinds, input_assets, input_amounts, output_blinds, output_asset_blinds, output_pubkeys, std::vector(), std::vector(), tx, (auxiliary_generators.size() ? &auxiliary_generators : NULL)) != numPubKeys) { + // TODO Have more rich return values, communicating to user what has been blinded + // User may be ok not blinding something that for instance has no corresponding type on input + throw JSONRPCError(RPC_INVALID_PARAMETER, string("Unable to blind transaction: Are you sure each asset type to blind is represented in the inputs?")); } return EncodeHexTx(tx); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index ab2c7fc7f5..f02bd70043 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -391,7 +391,7 @@ UniValue getaddressesbyaccount(const JSONRPCRequest& request) return ret; } -static void SendMoney(const CScript& scriptPubKey, CAmount nValue, CAsset asset, bool fSubtractFeeFromAmount, const CPubKey &confidentiality_key, CWalletTx& wtxNew) +static void SendMoney(const CScript& scriptPubKey, CAmount nValue, CAsset asset, bool fSubtractFeeFromAmount, const CPubKey &confidentiality_key, CWalletTx& wtxNew, bool fIgnoreBlindFail) { CAmount curBalance = pwalletMain->GetBalance()[asset]; @@ -422,7 +422,7 @@ static void SendMoney(const CScript& scriptPubKey, CAmount nValue, CAsset asset, int nChangePosRet = -1; CRecipient recipient = {scriptPubKey, nValue, asset, confidentiality_key, fSubtractFeeFromAmount}; vecSend.push_back(recipient); - if (!pwalletMain->CreateTransaction(vecSend, wtxNew, vpChangeKey, nFeeRequired, nChangePosRet, strError, NULL, true, NULL, true, NULL, NULL, NULL)) { + if (!pwalletMain->CreateTransaction(vecSend, wtxNew, vpChangeKey, nFeeRequired, nChangePosRet, strError, NULL, true, NULL, true, NULL, NULL, NULL, fIgnoreBlindFail)) { if (!fSubtractFeeFromAmount && nValue + nFeeRequired > curBalance) strError = strprintf("Error: This transaction requires a transaction fee of at least %s", FormatMoney(nFeeRequired)); throw JSONRPCError(RPC_WALLET_ERROR, strError); @@ -434,9 +434,9 @@ static void SendMoney(const CScript& scriptPubKey, CAmount nValue, CAsset asset, } } -static void SendMoney(const CTxDestination &address, CAmount nValue, CAsset asset, bool fSubtractFeeFromAmount, const CPubKey &confidentiality_key, CWalletTx& wtxNew) +static void SendMoney(const CTxDestination &address, CAmount nValue, CAsset asset, bool fSubtractFeeFromAmount, const CPubKey &confidentiality_key, CWalletTx& wtxNew, bool fIgnoreBlindFail) { - SendMoney(GetScriptForDestination(address), nValue, asset, fSubtractFeeFromAmount, confidentiality_key, wtxNew); + SendMoney(GetScriptForDestination(address), nValue, asset, fSubtractFeeFromAmount, confidentiality_key, wtxNew, fIgnoreBlindFail); } static void SendGenerationTransaction(const CScript& assetScriptPubKey, const CPubKey &assetKey, const CScript& tokenScriptPubKey, const CPubKey &tokenKey, CAmount nAmountAsset, CAmount nTokens, bool fBlindIssuances, uint256& entropy, CAsset& reissuanceAsset, CAsset& reissuanceToken, CWalletTx& wtxNew) @@ -495,9 +495,9 @@ UniValue sendtoaddress(const JSONRPCRequest& request) if (!EnsureWalletIsAvailable(request.fHelp)) return NullUniValue; - if (request.fHelp || request.params.size() < 2 || request.params.size() > 6) + if (request.fHelp || request.params.size() < 2 || request.params.size() > 7) throw runtime_error( - "sendtoaddress \"address\" amount ( \"comment\" \"comment_to\" subtractfeefromamount assetlabel)\n" + "sendtoaddress \"bitcoinaddress\" amount ( \"comment\" \"comment-to\" subtractfeefromamount assetlabel ignoreblindfail )\n" "\nSend an amount to a given address.\n" + HelpRequiringPassphrase() + "\nArguments:\n" @@ -509,8 +509,9 @@ UniValue sendtoaddress(const JSONRPCRequest& request) " to which you're sending the transaction. This is not part of the \n" " transaction, just kept in your wallet.\n" "5. subtractfeefromamount (boolean, optional, default=false) The fee will be deducted from the amount being sent.\n" - "6. \"assetlabel\" (string, optional) Hex asset id or asset label for balance.\n" " The recipient will receive less bitcoins than you enter in the amount field.\n" + "6. \"assetlabel\" (string, optional) Hex asset id or asset label for balance.\n" + "7. \"ignoreblindfail\"\" (bool, default=true) Return a transaction even when a blinding attempt fails due to number of blinded inputs/outputs.\n" "\nResult:\n" "\"txid\" (string) The transaction id.\n" "\nExamples:\n" @@ -553,11 +554,15 @@ UniValue sendtoaddress(const JSONRPCRequest& request) strasset = request.params[5].get_str(); } + bool fIgnoreBlindFail = true; + if (request.params.size() > 6) + fIgnoreBlindFail = request.params[6].get_bool(); + CAsset asset = GetAssetFromString(strasset); EnsureWalletIsUnlocked(); - SendMoney(address.Get(), nAmount, asset, fSubtractFeeFromAmount, confidentiality_pubkey, wtx); + SendMoney(address.Get(), nAmount, asset, fSubtractFeeFromAmount, confidentiality_pubkey, wtx, fIgnoreBlindFail); std::string blinds; for (unsigned int i=0; ivout.size(); i++) { @@ -610,7 +615,7 @@ UniValue destroyamount(const JSONRPCRequest& request) CPubKey confidentiality_pubkey; EnsureWalletIsUnlocked(); - SendMoney(destroyScript, nAmount, asset, false, confidentiality_pubkey, wtx); + SendMoney(destroyScript, nAmount, asset, false, confidentiality_pubkey, wtx, true); std::string blinds; for (unsigned int i=0; ivout.size(); i++) { @@ -1086,6 +1091,7 @@ UniValue sendmany(const JSONRPCRequest& request) " \"address\": \"hex\" \n" " ...\n" " }\n" + "7. \"ignoreblindfail\"\" (bool, default=true) Return a transaction even when a blinding attempt fails due to number of blinded inputs/outputs.\n" "\nResult:\n" "\"txid\" (string) The transaction id for the send. Only 1 transaction is created regardless of \n" " the number of addresses.\n" @@ -1127,6 +1133,11 @@ UniValue sendmany(const JSONRPCRequest& request) assets = request.params[5].get_obj(); } + bool fIgnoreBlindFail = true; + if (request.params.size() > 6) { + fIgnoreBlindFail = request.params[6].get_bool(); + } + set setAddress; vector vecSend; @@ -1200,7 +1211,7 @@ UniValue sendmany(const JSONRPCRequest& request) CAmount nFeeRequired = 0; int nChangePosRet = -1; string strFailReason; - bool fCreated = pwalletMain->CreateTransaction(vecSend, wtx, vpChangeKey, nFeeRequired, nChangePosRet, strFailReason); + bool fCreated = pwalletMain->CreateTransaction(vecSend, wtx, vpChangeKey, nFeeRequired, nChangePosRet, strFailReason, NULL, true, NULL, false, NULL, NULL, NULL, fIgnoreBlindFail); if (!fCreated) throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, strFailReason); CValidationState state; @@ -2862,6 +2873,7 @@ UniValue fundrawtransaction(const JSONRPCRequest& request) "in the wallet using importaddress or addmultisigaddress (to calculate fees).\n" "You can see whether this is the case by checking the \"solvable\" field in the listunspent output.\n" "Only pay-to-pubkey, multisig, and P2SH versions thereof are currently supported for watch-only\n" + "Note: Existing fee outputs will be dropped to aid fee estimation\n" "\nArguments:\n" "1. \"hexstring\" (string, required) The hex string of the raw transaction\n" "2. options (object, optional)\n" @@ -3564,7 +3576,7 @@ UniValue sendtomainchain(const JSONRPCRequest& request) EnsureWalletIsUnlocked(); CWalletTx wtxNew; - SendMoney(scriptPubKey, nAmount, BITCOINID, false, CPubKey(), wtxNew); + SendMoney(scriptPubKey, nAmount, BITCOINID, false, CPubKey(), wtxNew, true); std::string blinds; for (unsigned int i=0; ivout.size(); i++) { diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 0ff5e86a01..73980bd790 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2690,7 +2690,7 @@ bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool ov } bool CWallet::CreateTransaction(const vector& vecSend, CWalletTx& wtxNew, std::vector& vpChangeKey, CAmount& nFeeRet, - int& nChangePosInOut, std::string& strFailReason, const CCoinControl* coinControl, bool sign, std::vector *outAmounts, bool fBlindIssuances, const uint256* issuanceEntropy, const CAsset* reissuanceAsset, const CAsset* reissuanceToken) + int& nChangePosInOut, std::string& strFailReason, const CCoinControl* coinControl, bool sign, std::vector *outAmounts, bool fBlindIssuances, const uint256* issuanceEntropy, const CAsset* reissuanceAsset, const CAsset* reissuanceToken, bool fIgnoreBlindFail) { // TODO re-enable to support multiple assets in a logical fashion, since the number of possible // change positions are number of assets being spent. @@ -2778,6 +2778,12 @@ bool CWallet::CreateTransaction(const vector& vecSend, CWalletTx& wt nChangePosInOut = nChangePosRequest; std::vector output_pubkeys; int numToBlind = 0; + int changeToBlind = 0; + int numInputsBlinded = 0; + // Needed in case of one blinded output that is change and no blind inputs + int onlyChangePos = -1; + // Only used to strip blinding if its the only blind output in certain situations + int onlyRecipientBlindIndex = -1; txNew.vin.clear(); txNew.vout.clear(); @@ -2827,6 +2833,7 @@ bool CWallet::CreateTransaction(const vector& vecSend, CWalletTx& wt output_pubkeys.push_back(recipient.confidentiality_key); if (recipient.confidentiality_key != CPubKey()) { numToBlind++; + onlyRecipientBlindIndex = txNew.vout.size()-1; } } @@ -2942,7 +2949,9 @@ bool CWallet::CreateTransaction(const vector& vecSend, CWalletTx& wt output_pubkeys.insert(output_pubkeys.begin() + nChangePosInOut, pubkey); if (pubkey != CPubKey()) { numToBlind++; + changeToBlind++; } + onlyChangePos = nChangePosInOut; // reset nChangePosInOut for next asset nChangePosInOut = -1; } @@ -3082,6 +3091,9 @@ bool CWallet::CreateTransaction(const vector& vecSend, CWalletTx& wt input_assets.push_back(asset); CAmount amount = coin.first->GetOutputValueOut(coin.second); input_amounts.push_back(amount); + if (coin.first->tx->vout[coin.second].nValue.IsCommitment() || coin.first->tx->vout[coin.second].nAsset.IsCommitment()) { + numInputsBlinded++; + } } if(outAmounts) outAmounts->clear(); @@ -3094,30 +3106,63 @@ bool CWallet::CreateTransaction(const vector& vecSend, CWalletTx& wt output_assets.push_back(txNew.vout[nOut].nAsset.GetAsset()); } - // Keep a backup of transaction in case re-blinding necessary - txUnblindedAndUnsigned = txNew; - CMutableTransaction txBackup(txNew); - int ret = BlindTransaction(input_blinds, input_asset_blinds, input_assets, input_amounts, output_blinds, output_asset_blinds, output_pubkeys, vassetKeys, vtokenKeys, txNew); - // TODO remove? - assert(ret != -1); - if (ret != numToBlind) { - // We need a dummy output to put a non-zero blinding factor. - // TODO: if fBlindedOutputs, don't use an OP_RETURN but create an (extra) change output - // instead, as this does not actually provide better privacy. - + // There are a few edge-cases of blinding we need to take care of + // + // First, if there are blinded inputs but not outputs to blind + // We need this to go through, even though no privacy is gained. + if (numInputsBlinded > 0 && numToBlind == 0) { // We need to make sure to dupe an asset that is in input set + // TODO Have blinding do some extremely minimal rangeproof CTxOut newTxOut(output_assets.back(), 0, CScript() << OP_RETURN); - txBackup.vout.push_back(newTxOut); + txNew.vout.push_back(newTxOut); output_pubkeys.push_back(GetBlindingPubKey(newTxOut.scriptPubKey)); output_blinds.push_back(uint256()); output_asset_blinds.push_back(uint256()); output_assets.push_back(output_assets.back()); vAmounts.push_back(0); numToBlind++; - // Now it has to succeed - int ret = BlindTransaction(input_blinds, input_asset_blinds, input_assets, input_amounts, output_blinds, output_asset_blinds, output_pubkeys, vassetKeys, vtokenKeys, txBackup); - assert(ret == numToBlind); - txNew = txBackup; + + // No blinded inputs, but 1 blinded output + } else if (numInputsBlinded == 0 && numToBlind == 1) { + if (changeToBlind == 1) { + // Only 1 blinded change, unblinded the change + // TODO Split up change instead if possible + if (fIgnoreBlindFail) { + numToBlind--; + changeToBlind--; + txNew.vout[onlyChangePos].nNonce.SetNull(); + output_pubkeys[onlyChangePos] = CPubKey(); + output_blinds[onlyChangePos] = uint256(); + output_asset_blinds[onlyChangePos] = uint256(); + } else { + strFailReason = _("Change output could not be blinded as there are no blinded inputs and no other blinded outputs."); + return false; + } + } else { + // 1 blinded destination + // TODO Attempt to get a blinded input, OR add unblinded coin to make blinded change + assert(onlyRecipientBlindIndex != -1); + if (fIgnoreBlindFail) { + numToBlind--; + txNew.vout[onlyRecipientBlindIndex].nNonce.SetNull(); + output_pubkeys[onlyRecipientBlindIndex] = CPubKey(); + output_blinds[onlyRecipientBlindIndex] = uint256(); + output_asset_blinds[onlyRecipientBlindIndex] = uint256(); + } else { + strFailReason = _("Transaction output could not be blinded as there are no blinded inputs and no other blinded outputs."); + return false; + } + } + } + // All other combinations should work. + + // Keep a backup of transaction in case re-blinding necessary + CMutableTransaction txUnblindedAndUnsigned(txNew); + int ret = BlindTransaction(input_blinds, input_asset_blinds, input_assets, input_amounts, output_blinds, output_asset_blinds, output_pubkeys, vassetKeys, vtokenKeys, txNew); + assert(ret != -1); + if (ret != numToBlind) { + strFailReason = _("Unable to blind the transaction properly. This should not happen."); + return false; } // Fill in dummy signatures for fee calculation. diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index b497ba3e43..30fd5968ba 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -875,7 +875,7 @@ public: * @note passing nChangePosInOut as -1 will result in setting a random position */ bool CreateTransaction(const std::vector& vecSend, CWalletTx& wtxNew, std::vector& vpChangeKey, CAmount& nFeeRet, int& nChangePosInOut, - std::string& strFailReason, const CCoinControl *coinControl = NULL, bool sign = true, std::vector *outAmounts = NULL, bool fBlindIssuances = true, const uint256* issuanceEntropy = NULL, const CAsset* reissuanceAsset = NULL, const CAsset* reissuanceToken = NULL); + std::string& strFailReason, const CCoinControl *coinControl = NULL, bool sign = true, std::vector *outAmounts = NULL, bool fBlindIssuances = true, const uint256* issuanceEntropy = NULL, const CAsset* reissuanceAsset = NULL, const CAsset* reissuanceToken = NULL, bool fIgnoreBlindFail = true); bool CommitTransaction(CWalletTx& wtxNew, std::vector& reservekey, CConnman* connman, CValidationState& state); void ListAccountCreditDebit(const std::string& strAccount, std::list& entries);