diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 3746b654aeeac..2fc3b780fd807 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -174,9 +174,13 @@ static const CRPCConvertParam vRPCConvertParams[] = { "rawblindrawtransaction", 3, "inputasset" }, { "rawblindrawtransaction", 4, "inputassetblinder" }, { "rawblindrawtransaction", 6, "ignoreblindfail" }, + { "blindrawtransaction", 1, "assetcommitments" }, + { "blindrawtransaction", 2, "ignoreblindfail" }, + { "blindrawtransaction", 3, "blind_issuances" }, { "sendmany", 7 , "output_assets" }, { "sendmany", 8 , "ignoreblindfail" }, { "sendtoaddress", 9 , "ignoreblindfail" }, + { "importblindingkey", 2, "key_is_master"}, { "createrawtransaction", 4, "output_assets" }, }; diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 58881687adb01..7cb3cac26f5a4 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -1333,3 +1333,113 @@ UniValue getwalletpakinfo(const JSONRPCRequest& request) ret.pushKV("address_lookahead", address_list); return ret; } + +UniValue importblindingkey(const JSONRPCRequest& request) +{ + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + CWallet* const pwallet = wallet.get(); + + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() < 2 || request.params.size() > 3) + throw std::runtime_error( + "importblindingkey \"address\" \"blindinghex\"\n" + "\nImports a private blinding key in hex for a CT address." + "\nArguments:\n" + "1. \"address\" (string, required) The CT address\n" + "2. \"hexkey\" (string, required) The blinding key in hex\n" + "3. \"key_is_master\" (bool, optional, default=false) If the `hexkey` is a master blinding key. Note: wallets can only have one master blinding key at a time. Funds could be permanently lost if user doesn't know what they are doing. Recommended use is only for wallet recovery using this in conjunction with `sethdseed`.\n" + "\nExample:\n" + + HelpExampleCli("importblindingkey", "\"my blinded CT address\" ") + ); + + LOCK2(cs_main, pwallet->cs_wallet); + + CTxDestination dest = DecodeDestination(request.params[0].get_str()); + if (!IsValidDestination(dest)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address."); + } + if (!IsBlindDestination(dest)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Address is not confidential."); + } + + if (!IsHex(request.params[1].get_str())) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid hexadecimal for key"); + } + std::vector keydata = ParseHex(request.params[1].get_str()); + if (keydata.size() != 32) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid hexadecimal key length"); + } + + bool key_is_master = false; + if (!request.params[2].isNull()) { + key_is_master = request.params[2].get_bool(); + } + + CKey key; + key.Set(keydata.begin(), keydata.end(), true); + if (!key.IsValid() || key.GetPubKey() != GetDestinationBlindingKey(dest)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Address and key do not match"); + } + + uint256 keyval; + memcpy(keyval.begin(), &keydata[0], 32); + if (key_is_master) { + if (pwallet->SetMasterBlindingKey(keyval)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Failed to import master blinding key"); + } + } else { + if (!pwallet->AddSpecificBlindingKey(CScriptID(GetScriptForDestination(dest)), keyval)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Failed to import blinding key"); + } + } + pwallet->MarkDirty(); + + return NullUniValue; +} + +UniValue dumpblindingkey(const JSONRPCRequest& request) +{ + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + CWallet* const pwallet = wallet.get(); + + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() != 1) + throw std::runtime_error( + "dumpblindingkey \"address\"\n" + "\nDumps the private blinding key for a CT address in hex." + "\nArguments:\n" + "1. \"address\" (string, required) The CT address\n" + "\nResult:\n" + "\"blindingkey\" (string) The blinding key\n" + "\nExample:\n" + + HelpExampleCli("dumpblindingkey", "\"my address\"") + ); + + LOCK2(cs_main, pwallet->cs_wallet); + + + CTxDestination dest = DecodeDestination(request.params[0].get_str()); + if (!IsValidDestination(dest)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address"); + } + if (!IsBlindDestination(dest)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Not a CT address"); + } + CScript script = GetScriptForDestination(dest); + CKey key; + key = pwallet->GetBlindingKey(&script); + if (key.IsValid()) { + CPubKey pubkey(key.GetPubKey()); + if (pubkey == GetDestinationBlindingKey(dest)) { + return HexStr(key.begin(), key.end()); + } + } + + throw JSONRPCError(RPC_WALLET_ERROR, "Blinding key for address is unknown"); +} diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 11393d3a32206..a1573e175205d 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5334,11 +5334,328 @@ UniValue claimpegin(const JSONRPCRequest& request) return mtx.GetHash().GetHex(); } +// Rewind the outputs to unblinded, and push placeholders for blinding info. Not exported. +void FillBlinds(CWallet* pwallet, CMutableTransaction& tx, std::vector& output_value_blinds, std::vector& output_asset_blinds, std::vector& output_pubkeys, std::vector& asset_keys, std::vector& token_keys) { + + // Resize witness before doing anything + tx.witness.vtxinwit.resize(tx.vin.size()); + tx.witness.vtxoutwit.resize(tx.vout.size()); + + for (size_t nOut = 0; nOut < tx.vout.size(); ++nOut) { + CTxOut& out = tx.vout[nOut]; + if (out.nValue.IsExplicit()) { + CPubKey pubkey(out.nNonce.vchCommitment); + if (!pubkey.IsFullyValid()) { + output_pubkeys.push_back(CPubKey()); + } else { + output_pubkeys.push_back(pubkey); + } + output_value_blinds.push_back(uint256()); + output_asset_blinds.push_back(uint256()); + } else if (out.nValue.IsCommitment()) { + CTxOutWitness* ptxoutwit = &tx.witness.vtxoutwit[nOut]; + uint256 blinding_factor; + uint256 asset_blinding_factor; + CAsset asset; + CAmount amount; + // This can only be used to recover things like change addresses and self-sends. + if (UnblindConfidentialPair(pwallet->GetBlindingKey(&out.scriptPubKey), out.nValue, out.nAsset, out.nNonce, out.scriptPubKey, ptxoutwit->vchRangeproof, amount, blinding_factor, asset, asset_blinding_factor) != 0) { + // Wipe out confidential info from output and output witness + CScript scriptPubKey = tx.vout[nOut].scriptPubKey; + CTxOut newOut(asset, amount, scriptPubKey); + tx.vout[nOut] = newOut; + ptxoutwit->SetNull(); + + // Mark for re-blinding with same key that deblinded it + CPubKey pubkey(pwallet->GetBlindingKey(&out.scriptPubKey).GetPubKey()); + output_pubkeys.push_back(pubkey); + output_value_blinds.push_back(uint256()); + output_asset_blinds.push_back(uint256()); + } else { + output_pubkeys.push_back(CPubKey()); + output_value_blinds.push_back(uint256()); + output_asset_blinds.push_back(uint256()); + } + } else { + // Null or invalid, do nothing for that output + output_pubkeys.push_back(CPubKey()); + output_value_blinds.push_back(uint256()); + output_asset_blinds.push_back(uint256()); + } + } + + // Fill out issuance blinding keys to be used directly as nonce for rangeproof + for (size_t nIn = 0; nIn < tx.vin.size(); ++nIn) { + CAssetIssuance& issuance = tx.vin[nIn].assetIssuance; + if (issuance.IsNull()) { + asset_keys.push_back(CKey()); + token_keys.push_back(CKey()); + continue; + } + + // Calculate underlying asset for use as blinding key script + CAsset asset; + // New issuance, compute the asset ids + if (issuance.assetBlindingNonce.IsNull()) { + uint256 entropy; + GenerateAssetEntropy(entropy, tx.vin[nIn].prevout, issuance.assetEntropy); + CalculateAsset(asset, entropy); + } + // Re-issuance + else { + // TODO Give option to skip blinding when the reissuance token was derived without blinding ability. For now we assume blinded + // hashAssetIdentifier doubles as the entropy on reissuance + CalculateAsset(asset, issuance.assetEntropy); + } + + // Special format for issuance blinding keys, unique for each transaction + CScript blindingScript = CScript() << OP_RETURN << std::vector(tx.vin[nIn].prevout.hash.begin(), tx.vin[nIn].prevout.hash.end()) << tx.vin[nIn].prevout.n; + + for (size_t nPseudo = 0; nPseudo < 2; nPseudo++) { + bool issuance_asset = (nPseudo == 0); + std::vector& issuance_blinding_keys = issuance_asset ? asset_keys : token_keys; + CConfidentialValue& conf_value = issuance_asset ? issuance.nAmount : issuance.nInflationKeys; + if (conf_value.IsCommitment()) { + // Rangeproof must exist + if (tx.witness.vtxinwit.size() <= nIn) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Transaction issuance is already blinded but has no attached rangeproof."); + } + CTxInWitness& txinwit = tx.witness.vtxinwit[nIn]; + std::vector& vchRangeproof = issuance_asset ? txinwit.vchIssuanceAmountRangeproof : txinwit.vchInflationKeysRangeproof; + uint256 blinding_factor; + uint256 asset_blinding_factor; + CAmount amount; + if (UnblindConfidentialPair(pwallet->GetBlindingKey(&blindingScript), conf_value, CConfidentialAsset(asset), CConfidentialNonce(), CScript(), vchRangeproof, amount, blinding_factor, asset, asset_blinding_factor) != 0) { + // Wipe out confidential info from issuance + vchRangeproof.clear(); + conf_value = CConfidentialValue(amount); + // One key blinds both values, single key needed for issuance reveal + issuance_blinding_keys.push_back(pwallet->GetBlindingKey(&blindingScript)); + } else { + // If unable to unblind, leave it alone in next blinding step + issuance_blinding_keys.push_back(CKey()); + } + } else if (conf_value.IsExplicit()) { + // Use wallet to generate blindingkey used directly as nonce + // as user is not "sending" to anyone. + // Always assumed we want to blind here. + // TODO Signal intent for all blinding via API including replacing nonce commitment + issuance_blinding_keys.push_back(pwallet->GetBlindingKey(&blindingScript)); + } else { + // Null or invalid, don't try anything but append an empty key + issuance_blinding_keys.push_back(CKey()); + } + } + } +} + +UniValue blindrawtransaction(const JSONRPCRequest& request) +{ + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + CWallet* const pwallet = wallet.get(); + + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) + return NullUniValue; + + if (request.fHelp || (request.params.size() < 1 || request.params.size() > 5)) + throw std::runtime_error( + "blindrawtransaction \"hexstring\" ( ignoreblindfail [\"assetcommitment,...\"] blind_issuances \"totalblinder\" )\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" + "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. \"ignoreblindfail\"\" (bool, optional, default=true) Return a transaction even when a blinding attempt fails due to number of blinded inputs/outputs.\n" + "3. \"asset_commitments\" \n" + " [ (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" + " \"assetcommitment\" (string, optional) A hex-encoded asset commitment, one for each input.\n" + " Null commitments must be \"\".\n" + " ],\n" + "4. \"blind_issuances\" (bool, optional, default=true) Blind the issuances found in the raw transaction or not. All issuances will be blinded if true. \n" + "5. \"totalblinder\" (string, optional) Ignored for now.\n" + + "\nResult:\n" + "\"transaction\" (string) hex string of the transaction\n" + ); + + std::vector txData(ParseHexV(request.params[0], "argument 1")); + CDataStream ssData(txData, SER_NETWORK, PROTOCOL_VERSION); + CMutableTransaction tx; + try { + ssData >> tx; + } catch (const std::exception &) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); + } + + bool ignore_blind_fail = true; + if (request.params.size() > 1) { + ignore_blind_fail = 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 exactly as many entries as transaction inputs."); + } + for (size_t nIn = 0; nIn < assetCommitments.size(); nIn++) { + if (assetCommitments[nIn].isStr()) { + std::string assetcommitment = assetCommitments[nIn].get_str(); + if (IsHex(assetcommitment) && assetcommitment.size() == 66) { + auxiliary_generators.push_back(ParseHex(assetcommitment)); + continue; + } + } + throw JSONRPCError(RPC_INVALID_PARAMETER, "Asset commitments must be a hex encoded string of length 66."); + } + } + + bool blind_issuances = request.params[3].isNull() || request.params[3].get_bool(); + + LOCK(pwallet->cs_wallet); + + std::vector input_blinds; + std::vector input_asset_blinds; + std::vector input_assets; + std::vector input_amounts; + int n_blinded_ins = 0; + for (size_t nIn = 0; nIn < tx.vin.size(); ++nIn) { + COutPoint prevout = tx.vin[nIn].prevout; + + std::map::iterator it = pwallet->mapWallet.find(prevout.hash); + if (it == pwallet->mapWallet.end()) { + // For inputs we don't own input assetcommitments for the surjection must be supplied + if (auxiliary_generators.size() > 0) { + input_blinds.push_back(uint256()); + input_asset_blinds.push_back(uint256()); + input_assets.push_back(CAsset()); + input_amounts.push_back(-1); + continue; + } + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter: transaction spends from non-wallet output and no assetcommitment list was given."); + } + + if (prevout.n >= it->second.tx->vout.size()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter: transaction spends non-existing output"); + } + input_blinds.push_back(it->second.GetOutputAmountBlindingFactor(prevout.n)); + input_asset_blinds.push_back(it->second.GetOutputAssetBlindingFactor(prevout.n)); + input_assets.push_back(it->second.GetOutputAsset(prevout.n)); + input_amounts.push_back(it->second.GetOutputValueOut(prevout.n)); + if (it->second.tx->vout[prevout.n].nValue.IsCommitment()) { + n_blinded_ins += 1; + } + } + + std::vector output_blinds; + std::vector output_asset_blinds; + std::vector output_pubkeys; + std::vector asset_keys; + std::vector token_keys; + // This fills out issuance blinding data for you from the wallet itself + FillBlinds(pwallet, tx, output_blinds, output_asset_blinds, output_pubkeys, asset_keys, token_keys); + + if (!blind_issuances) { + asset_keys.clear(); + token_keys.clear(); + } + + // How many are we trying to blind? + int num_pubkeys = 0; + unsigned int key_index = 0; + for (unsigned int i = 0; i < output_pubkeys.size(); i++) { + const CPubKey& key = output_pubkeys[i]; + if (key.IsValid()) { + num_pubkeys++; + key_index = i; + } + } + for (const CKey& key : asset_keys) { + if (key.IsValid()) num_pubkeys++; + } + for (const CKey& key : token_keys) { + if (key.IsValid()) num_pubkeys++; + } + + if (num_pubkeys == 0 && n_blinded_ins == 0) { + // Vacuous, just return the transaction + return EncodeHexTx(tx); + } else if (n_blinded_ins > 0 && num_pubkeys == 0) { + // Blinded inputs need to balanced with something to be valid, make a dummy. + CTxOut newTxOut(tx.vout.back().nAsset.GetAsset(), 0, CScript() << OP_RETURN); + tx.vout.push_back(newTxOut); + num_pubkeys++; + output_pubkeys.push_back(pwallet->GetBlindingPubKey(newTxOut.scriptPubKey)); + } else if (n_blinded_ins == 0 && num_pubkeys == 1) { + if (ignore_blind_fail) { + // Just get rid of the ECDH key in the nonce field and return + tx.vout[key_index].nNonce.SetNull(); + return EncodeHexTx(tx); + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, "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, asset_keys, token_keys, tx, (auxiliary_generators.size() ? &auxiliary_generators : NULL)) != num_pubkeys) { + // 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, "Unable to blind transaction: Are you sure each asset type to blind is represented in the inputs?"); + } + + return EncodeHexTx(tx); +} + +static UniValue unblindrawtransaction(const JSONRPCRequest& request) +{ + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + CWallet* const pwallet = wallet.get(); + + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() != 1) + throw std::runtime_error( + "unblindrawtransaction \"hex\"\n" + "\nRecovers unblinded transaction outputs from blinded outputs and issuance inputs when possible using wallet's known blinding keys, and strips related witness data.\n" + "\nArguments:\n" + "1. \"hex\" (string, required) The hex string of the raw transaction\n" + "\nResult:\n" + "{\n" + " \"hex\": \"value\", (string) The resulting unblinded raw transaction (hex-encoded string)\n" + "}\n" + "\nExamples:\n" + + HelpExampleCli("unblindrawtransaction", "\"blindedtransactionhex\"") + ); + + RPCTypeCheck(request.params, {UniValue::VSTR}); + + CMutableTransaction tx; + if (!DecodeHexTx(tx, request.params[0].get_str())) + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); + + std::vector output_value_blinds; + std::vector output_asset_blinds; + std::vector output_pubkeys; + std::vector asset_keys; + std::vector token_keys; + FillBlinds(pwallet, tx, output_value_blinds, output_asset_blinds, output_pubkeys, asset_keys, token_keys); + + UniValue result(UniValue::VOBJ); + result.pushKV("hex", EncodeHexTx(tx)); + return result; +} + + // END ELEMENTS commands // UniValue abortrescan(const JSONRPCRequest& request); // in rpcdump.cpp UniValue dumpprivkey(const JSONRPCRequest& request); // in rpcdump.cpp +UniValue importblindingkey(const JSONRPCRequest& request); // in rpcdump.cpp +UniValue dumpblindingkey(const JSONRPCRequest& request); // in rpcdump.cpp UniValue importprivkey(const JSONRPCRequest& request); UniValue importaddress(const JSONRPCRequest& request); UniValue importpubkey(const JSONRPCRequest& request); @@ -5413,10 +5730,13 @@ static const CRPCCommand commands[] = { "wallet", "getpeginaddress", &getpeginaddress, {} }, { "wallet", "claimpegin", &claimpegin, {"bitcoin_tx", "txoutproof", "claim_script"} }, { "wallet", "createrawpegin", &createrawpegin, {"bitcoin_tx", "txoutproof", "claim_script"} }, + { "wallet", "blindrawtransaction", &blindrawtransaction, {"hexstring", "ignoreblindfail", "assetcommitment", "blind_issuances", "totalblinder"} }, + { "wallet", "unblindrawtransaction", &unblindrawtransaction, {"hex"} }, { "wallet", "sendtomainchain", &sendtomainchain, {"address", "amount", "subtractfeefromamount"} }, { "wallet", "initpegoutwallet", &initpegoutwallet, {"bitcoin_descriptor", "bip32_counter", "liquid_pak"} }, { "wallet", "getwalletpakinfo", &getwalletpakinfo, {} }, - + { "wallet", "importblindingkey", &importblindingkey, {"address", "hexkey", "key_is_master"}}, + { "wallet", "dumpblindingkey", &dumpblindingkey, {"address"}}, { "wallet", "signblock", &signblock, {"blockhex"}}, }; // clang-format on