diff --git a/src/blindpsbt.cpp b/src/blindpsbt.cpp index b4e55b7fd0..a84dfff292 100644 --- a/src/blindpsbt.cpp +++ b/src/blindpsbt.cpp @@ -263,6 +263,7 @@ bool SubtractScalars(uint256& a, const uint256& b) // Compute the scalar offset used for the final blinder computation // value * asset_blinder + value_blinder +// FIXME this method should be in libsecp, as should `ComputeAndAddToScalarOffset` bool CalculateScalarOffset(uint256& out, CAmount value, const uint256& asset_blinder, const uint256& value_blinder) { // If the asset_blinder is 0, then the equation resolves to just the value_blinder @@ -276,8 +277,24 @@ bool CalculateScalarOffset(uint256& out, CAmount value, const uint256& asset_bli // tweak_mul expects a 32 byte, big endian tweak. // We need to pack the 8 byte CAmount into a uint256 with the correct padding, so start it at 24 bytes from the front WriteBE64(val.begin() + 24, value); - if (secp256k1_ec_privkey_tweak_mul(secp256k1_blind_context, out.begin(), val.begin()) != 1) return false; - if (!value_blinder.IsNull() && secp256k1_ec_privkey_tweak_add(secp256k1_blind_context, out.begin(), value_blinder.begin()) != 1) return false; + if (value > 0) { + if (secp256k1_ec_privkey_tweak_mul(secp256k1_blind_context, out.begin(), val.begin()) != 1) return false; + } else { + out = value_blinder; + return true; + } + if (!value_blinder.IsNull()) { + uint256 value_negated = value_blinder; + if (secp256k1_ec_seckey_negate(secp256k1_blind_context, value_negated.begin()) != 1) { + return false; + } + // Special-case zero, which would otherwise cause `secp256k1_ec_privkey_tweak_add` to fail + if (value_negated == out) { + out = uint256{}; + return true; + } + if (secp256k1_ec_privkey_tweak_add(secp256k1_blind_context, out.begin(), value_blinder.begin()) != 1) return false; + } return true; } @@ -294,8 +311,17 @@ bool ComputeAndAddToScalarOffset(uint256& a, CAmount value, const uint256& asset if (a.IsNull()) { a = scalar; } else { - // If we have a, then add the scalar to it. - if (secp256k1_ec_privkey_tweak_add(secp256k1_blind_context, a.begin(), scalar.begin()) != 1) return false; + uint256 scalar_negated = scalar; + if (secp256k1_ec_seckey_negate(secp256k1_blind_context, scalar_negated.begin()) != 1) { + return false; + } + // Special-case zero, which would otherwise cause `secp256k1_ec_privkey_tweak_add` to fail + if (scalar_negated == a) { + a = uint256{}; + } else { + // If we have a, then add the scalar to it. + if (secp256k1_ec_privkey_tweak_add(secp256k1_blind_context, a.begin(), scalar.begin()) != 1) return false; + } } return true; } diff --git a/src/util/error.cpp b/src/util/error.cpp index 9d4bb2947e..0aa8e7edc5 100644 --- a/src/util/error.cpp +++ b/src/util/error.cpp @@ -45,6 +45,10 @@ bilingual_str TransactionErrorString(const TransactionError err) return Untranslated("Proof of blinded value is invalid"); case TransactionError::INVALID_ASSET_PROOF: return Untranslated("Proof of blinded asset is invalid"); + case TransactionError::MISSING_BLINDING_KEY: + return Untranslated("Wallet does not have necessary blinding key"); + case TransactionError::MISSING_SIDECHANNEL_DATA: + return Untranslated("A rangeproof did not encode necessary blinding data"); // no default case, so the compiler can warn about missing cases } assert(false); diff --git a/src/util/error.h b/src/util/error.h index ceb39eb313..4b798b84a6 100644 --- a/src/util/error.h +++ b/src/util/error.h @@ -37,6 +37,8 @@ enum class TransactionError { UTXOS_MISSING_BALANCE_CHECK, INVALID_VALUE_PROOF, INVALID_ASSET_PROOF, + MISSING_BLINDING_KEY, + MISSING_SIDECHANNEL_DATA, }; bilingual_str TransactionErrorString(const TransactionError error); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 1360f52514..f1c5dd4943 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5025,8 +5025,16 @@ static RPCHelpMan walletcreatefundedpsbt() // Automatically select coins, unless at least one is manually selected. Can // be overridden by options.add_inputs. coin_control.m_add_inputs = rawTx.vin.size() == 0; + // FundTransaction expects blinding keys, if present, to appear in the output nonces + for (CTxOut& txout : rawTx.vout) { + auto search_it = psbt_outs.find(txout); + assert (search_it != psbt_outs.end()); + CPubKey& blind_pub = search_it->second.m_blinding_pubkey; + if (blind_pub.IsFullyValid()) { + txout.nNonce.vchCommitment = std::vector(blind_pub.begin(), blind_pub.end()); + } + } FundTransaction(wallet, rawTx, fee, change_position, request.params[3], coin_control, /* solving_data */ request.params[5], /* override_min_fee */ true); - PartiallySignedTransaction psbtx(rawTx, psbt_version); // Find an input that is ours unsigned int blinder_index = 0; { @@ -5039,6 +5047,17 @@ static RPCHelpMan walletcreatefundedpsbt() } } assert(blinder_index < rawTx.vin.size()); // We added inputs, or existing inputs are ours, we should have a blinder index at this point. + // It may add outputs (change, and in some edge case OP_RETURN) which need to be + // blinded. So pull these into `psbt_outs`. + for (const CTxOut& txout : rawTx.vout) { + if (!txout.nNonce.IsNull() && !psbt_outs.count(txout)) { + PSBTOutput new_out{2}; // psbtv2 output + new_out.m_blinding_pubkey.Set(txout.nNonce.vchCommitment.begin(), txout.nNonce.vchCommitment.end()); + new_out.m_blinder_index = blinder_index; + psbt_outs.insert(std::make_pair(txout, new_out)); + } + } + PartiallySignedTransaction psbtx(rawTx, psbt_version); for (unsigned int i = 0; i < rawTx.vout.size(); ++i) { PSBTOutput& output = psbtx.outputs[i]; auto it = psbt_outs.find(rawTx.vout.at(i)); diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index ac8a6b1e3e..ba9c739e10 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -7,6 +7,7 @@ #include #include // ELEMENTS: for GenerateAssetEntropy and others #include +#include // for GetDestinationBlindingKey and IsBlindDestination #include #include #include @@ -765,6 +766,8 @@ bool fillBlindDetails(BlindDetails* det, CWallet* wallet, CMutableTransaction& t // We need to make sure to dupe an asset that is in input set //TODO Have blinding do some extremely minimal rangeproof CTxOut newTxOut(det->o_assets.back(), 0, CScript() << OP_RETURN); + CPubKey blind_pub = wallet->GetBlindingPubKey(newTxOut.scriptPubKey); // irrelevent, just needs to be non-null + newTxOut.nNonce.vchCommitment = std::vector(blind_pub.begin(), blind_pub.end()); txNew.vout.push_back(newTxOut); det->o_pubkeys.push_back(wallet->GetBlindingPubKey(newTxOut.scriptPubKey)); det->o_amount_blinds.push_back(uint256()); @@ -878,12 +881,20 @@ bool CWallet::CreateTransactionInternal( // ELEMENTS: A map that keeps track of the change script for each asset and also // the index of the reservedest used for that script (-1 if none). std::map> mapScriptChange; + // For manually set change, we need to use the blinding pubkey associated + // with the manually-set address rather than generating one from the wallet + std::map > mapBlindingKeyChange; // coin control: send change to custom address if (coin_control.destChange.size() > 0) { for (const auto& dest : coin_control.destChange) { // No need to test we cover all assets. We produce error for that later. mapScriptChange[dest.first] = std::pair(-1, GetScriptForDestination(dest.second)); + if (IsBlindDestination(dest.second)) { + mapBlindingKeyChange[dest.first] = GetDestinationBlindingKey(dest.second); + } else { + mapBlindingKeyChange[dest.first] = std::nullopt; + } } } else { // no coin control: send change to newly generated address // Note: We use a new key here to keep it from being obvious which side is the change. @@ -1138,19 +1149,34 @@ bool CWallet::CreateTransactionInternal( CTxOut newTxOut(asset, change_and_fee, itScript->second.second); if (blind_details) { + std::optional blind_pub = std::nullopt; + // We cannot blind zero-valued outputs, and anyway they will be dropped + // later in this function during the dust check if (change_and_fee > 0) { - CPubKey blind_pub = GetBlindingPubKey(itScript->second.second); - blind_details->o_pubkeys.insert(blind_details->o_pubkeys.begin() + i, blind_pub); - assert(blind_pub.IsFullyValid()); + const auto itBlindingKey = mapBlindingKeyChange.find(asset); + if (itBlindingKey != mapBlindingKeyChange.end()) { + // If the change output was specified, use the blinding key that + // came with the specified address (if any) + blind_pub = itBlindingKey->second; + } else { + // Otherwise, we generated it from our own wallet, so get the + // blinding key from our own wallet. + blind_pub = GetBlindingPubKey(itScript->second.second); + } + } else { + assert(asset == policyAsset); + } + + if (blind_pub) { + blind_details->o_pubkeys.insert(blind_details->o_pubkeys.begin() + i, *blind_pub); + assert(blind_pub->IsFullyValid()); + blind_details->num_to_blind++; blind_details->change_to_blind++; blind_details->only_change_pos = i; // Place the blinding pubkey here in case of fundraw calls - newTxOut.nNonce.vchCommitment = std::vector(blind_pub.begin(), blind_pub.end()); + newTxOut.nNonce.vchCommitment = std::vector(blind_pub->begin(), blind_pub->end()); } else { - // We cannot blind zero-valued outputs, and anyway they will be dropped - // later in this function during the dust check - assert(asset == policyAsset); blind_details->o_pubkeys.insert(blind_details->o_pubkeys.begin() + i, CPubKey()); } } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 5cf548ab3d..0615a69417 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1963,7 +1963,7 @@ TransactionError CWallet::SignPSBT(PartiallySignedTransaction& psbtx, bool& comp CConfidentialNonce nonce; nonce.vchCommitment.insert(nonce.vchCommitment.end(), o.m_ecdh_pubkey.begin(), o.m_ecdh_pubkey.end()); - if (!UnblindConfidentialPair(blinding_key, o.m_value_commitment, o.m_asset_commitment, nonce, *o.script, o.m_value_rangeproof, value, value_factor, asset, asset_factor)) { + if (UnblindConfidentialPair(blinding_key, o.m_value_commitment, o.m_asset_commitment, nonce, *o.script, o.m_value_rangeproof, value, value_factor, asset, asset_factor)) { // These assertions are cryptographically impossible to trigger, as we // checked the proofs above, and then `UnblindConfidentialPair` checks // the extracted value/asset against the commitments. @@ -1973,10 +1973,11 @@ TransactionError CWallet::SignPSBT(PartiallySignedTransaction& psbtx, bool& comp if (!o.m_asset.IsNull()) { assert(CAsset(o.m_asset) == asset); } - return TransactionError::INVALID_ASSET_PROOF; // FIXME + } else { + return TransactionError::MISSING_SIDECHANNEL_DATA; } } else { - return TransactionError::INVALID_ASSET_PROOF; // FIXME + return TransactionError::MISSING_BLINDING_KEY; } } } diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index d69338a634..c2bc1cf7d9 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -26,6 +26,17 @@ import os MAX_BIP125_RBF_SEQUENCE = 0xfffffffd +# Utility function to extract info from outputs for use in assert_equal +# Returns a set of tuples (address, is blinded, is OP_RETURN). (It would +# feel nicer to return a set of dicts, but you can't do that in Python.) +def outputs_info(outputs): + return {( + x["script"].get("address"), + x.get("blinding_pubkey") is not None, + x["script"]["asm"] == "OP_RETURN", + ) for x in outputs} + + # Create one-input, one-output, no-fee transaction: class PSBTTest(BitcoinTestFramework): @@ -324,13 +335,49 @@ class PSBTTest(BitcoinTestFramework): assert_raises_rpc_error(-4, msg, self.nodes[1].walletcreatefundedpsbt, inputs, outputs_array, 0, {"feeRate": 1, "add_inputs": bool_add}) self.log.info("Test various PSBT operations") + addr = self.get_address(confidential, 1) + unconf_addr = self.nodes[1].getaddressinfo(addr)['unconfidential'] + change_addr = self.nodes[1].getrawchangeaddress() + conf_change_addr = self.nodes[1].getaddressinfo(change_addr)['confidential'] + unconf_change_addr = self.nodes[1].getaddressinfo(change_addr)['unconfidential'] # partially sign multisig things with node 1 - psbtx = wmulti.walletcreatefundedpsbt(inputs=[{"txid":txid,"vout":p2wsh_pos},{"txid":txid,"vout":p2sh_pos},{"txid":txid,"vout":p2sh_p2wsh_pos}], outputs=[{self.get_address(confidential, 1):29.99}], options={'changeAddress': self.nodes[1].getrawchangeaddress()})['psbt'] + psbtx = wmulti.walletcreatefundedpsbt(inputs=[{"txid":txid,"vout":p2wsh_pos},{"txid":txid,"vout":p2sh_pos},{"txid":txid,"vout":p2sh_p2wsh_pos}], outputs=[{addr:29.99}], options={'changeAddress': unconf_change_addr})['psbt'] filled = wmulti.walletprocesspsbt(psbtx) # have both nodes fill before we try to blind and sign walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(filled["psbt"]) psbtx = walletprocesspsbt_out['psbt'] assert_equal(walletprocesspsbt_out['complete'], False) + # check that the unblinded change address led to unblinded change + assert_equal( + outputs_info(self.nodes[1].decodepsbt(psbtx)["outputs"]), + { + (unconf_addr, confidential, False), + (unconf_change_addr, False, False), + (None, False, False), # fee + }, + ) + + # Repeat the above, with a confidential change address + psbtx = wmulti.walletcreatefundedpsbt(inputs=[{"txid":txid,"vout":p2wsh_pos},{"txid":txid,"vout":p2sh_pos},{"txid":txid,"vout":p2sh_p2wsh_pos}], outputs=[{addr:29.99}], options={'changeAddress': conf_change_addr})['psbt'] + filled = wmulti.walletprocesspsbt(psbtx) + # have both nodes fill before we try to blind and sign + walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(filled["psbt"]) + psbtx = walletprocesspsbt_out['psbt'] + assert_equal(walletprocesspsbt_out['complete'], False) + # check that the blinded change address led to blinded change (and below, + # when we call `walletprocesspsbt` with nodes[2], it will make sure that + # node 2 is able to unblind this change, even though wmulti created it). + # Notice that if `confidential` is False, the change is not blinded. This + # is a quirk of the wallet.cpp blinding logic and will go away when we + # overhaul this. + assert_equal( + outputs_info(self.nodes[1].decodepsbt(psbtx)["outputs"]), + { + (unconf_addr, confidential, False), + (unconf_change_addr, confidential, False), + (None, False, False), # fee + }, + ) # Unload wmulti, we don't need it anymore wmulti.unloadwallet() @@ -735,6 +782,40 @@ class PSBTTest(BitcoinTestFramework): self.nodes[0].generate(1) self.sync_all() + # Regression for #1049 + # 1. Create a one-blinded-output PSET and check that it is blinded correctly + addr = self.nodes[0].getnewaddress() + conf_addr = self.nodes[0].getaddressinfo(addr)['confidential'] + unconf_addr = self.nodes[0].getaddressinfo(addr)['unconfidential'] + # 1a. Funding should succeed and *not* add a OP_RETURN output + funded = self.nodes[1].walletcreatefundedpsbt([], [{conf_addr: self.nodes[1].getbalance()['bitcoin']}], 0, {"subtractFeeFromOutputs": [0]})["psbt"] + assert_equal( + outputs_info(self.nodes[1].decodepsbt(funded)["outputs"]), + { + (unconf_addr, True, False), + (None, False, False), # fee + }, + ) + # 1b. `walletprocesspsbt` should then succeed in creating a full transaction + signed = self.nodes[1].walletprocesspsbt(funded)["psbt"] + tx = self.nodes[1].finalizepsbt(signed)["hex"] + assert self.nodes[1].testmempoolaccept([tx])[0]['allowed'] + # 2. Create a one-unblinded-output PSET and check that it is blinded correctly + # 2a. Funding should succeed and add a OP_RETURN output + funded = self.nodes[1].walletcreatefundedpsbt([], [{unconf_addr: self.nodes[1].getbalance()['bitcoin']}], 0, {"subtractFeeFromOutputs": [0]})["psbt"] + assert_equal( + outputs_info(self.nodes[1].decodepsbt(funded)["outputs"]), + { + (unconf_addr, False, False), + (None, True, True), # blinded OP_RETURN + (None, False, False), # fee + }, + ) + # 2b. `walletprocesspsbt` should then succeed in creating a full transaction + signed = self.nodes[1].walletprocesspsbt(funded)["psbt"] + tx = self.nodes[1].finalizepsbt(signed)["hex"] + assert self.nodes[1].testmempoolaccept([tx])[0]['allowed'] + def pset_confidential_proofs(self): UNBLINDED = "cHNldP8BAgQCAAAAAQMEAAAAAAEEAQEBBQECAfsEAgAAAAABAP1UAQIAAAAAASopobdl5W15RSedscp/8bxEXKuKIMOZw+JTqgD8qJEKBAAAAAD9////Awrye7Xu4kI5VnpTDeGaq8sYdXP3qdzYaHrLDRzaC8y51ggl1U8hJxSo+8GcTzHv926wsqTTkOrdBnJo8qcLwLQauQKktt71EJU7HTH5HsgG4kJV/tC32F992/WgieIPRkUkmxYAFPrs/iioimRS5hoJKl/hua83d7rwC1uuuLvfuQh38wHS+0Vg2ecXzypsUabYofOFaGSrICByCKvjgTF6TdHNp2el7Cwi+94dy4qMDrEh/25Aqnc+5qABAqWPEY9ZNCz7m64pANrr04bVgPxaWCr7LvvWGH5FLzvRFgAU96wAzcLFRah7B8gq17sVY9Uso18BIw9PXUt8b6hFgG7k9ncTRZ4baejmD87i5JQMeg1d4bIBAAAAAAAAKfQAAAAAAAABAXoK8nu17uJCOVZ6Uw3hmqvLGHVz96nc2Gh6yw0c2gvMudYIJdVPIScUqPvBnE8x7/dusLKk05Dq3QZyaPKnC8C0GrkCpLbe9RCVOx0x+R7IBuJCVf7Qt9hffdv1oIniD0ZFJJsWABT67P4oqIpkUuYaCSpf4bmvN3e68CIGA3pgD7iheh1WkyCWvviXQBa9KOJk6JBeYxEpPuxiRBOvEElHIxkAAACAAQAAgAMAAIABDiB25bQww62kp1L1uQVb7MxEVoem8kCzSmM5DW09I9V6DQEPBAAAAAABEAT/////B/wEcHNldA79CwFgAgAHY7+9IRzxAXWemL7C9M7CBAqQoSrXRoxI5/YnMLV6nV/GBMEhmvoDFJcNzRXI/LrIRMLZFvNrP5IupN8OZ+4q+++aJTnuYCZIDR1pssb0JHA0z2UXkEYdHv26qoW26RbLf2LNh29yVIOHG3jqqc7+L7F4UELZmjlEs6R1sulqQ0ePCUUgAsqURkdnNKtl0nORiyLN/9JfqGGTC30WhsdXifWRmqOfkWil0Va1bDYumMU7zJdW/go83ODuZ5VZVWFsBLFSn9HxF1SaFCGt197qo8dr+vhPZwb72k13A72D+5Lx7UKoYqamRJsoAZdUZ/oVd9GRlPbAmRPV7iOxmPYf+t9AQiEd0Z4AIgICuujF5+Lk/uCeX9+RWtJ8ioG51rogGduwt+iY1tZFtjUQSUcjGQAAAIAAAACACwAAgAEEFgAUg+8ATSQ8VvNg+WJAuweXm6kXlFkBAwgAv3xIGAkAAAf8BHBzZXQCICMPT11LfG+oRYBu5PZ3E0WeG2no5g/O4uSUDHoNXeGyB/wEcHNldAYhAwxmNPa94Vg9u/nZBWC/8IYTgnp85V5TMOEFWTTAcF2pB/wEcHNldAgEAAAAAAABBAABAwgA4fUFAAAAAAf8BHBzZXQCICMPT11LfG+oRYBu5PZ3E0WeG2no5g/O4uSUDHoNXeGyB/wEcHNldAgEAAAAAAA=" BLINDED = "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"