diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2973,34 +2973,71 @@ return true; } - UniValue outputs = request.params[1].get_array(); - for (size_t idx = 0; idx < outputs.size(); idx++) { - const UniValue &output = outputs[idx]; - if (!output.isObject()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, - "Invalid parameter, expected object"); - } - const UniValue &o = output.get_obj(); + const UniValue &output_params = request.params[1]; + + // Create and validate the COutPoints first. + + std::vector outputs; + outputs.reserve(output_params.size()); + + for (size_t idx = 0; idx < output_params.size(); idx++) { + const UniValue &o = output_params[idx].get_obj(); RPCTypeCheckObj(o, { {"txid", UniValueType(UniValue::VSTR)}, {"vout", UniValueType(UniValue::VNUM)}, }); - std::string txid = find_value(o, "txid").get_str(); + const std::string &txid = find_value(o, "txid").get_str(); if (!IsHex(txid)) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected hex txid"); } - int nOutput = find_value(o, "vout").get_int(); + const int nOutput = find_value(o, "vout").get_int(); if (nOutput < 0) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout must be positive"); } - COutPoint outpt(uint256S(txid), nOutput); + const COutPoint outpt(uint256S(txid), nOutput); + + const auto it = pwallet->mapWallet.find(outpt.GetTxId()); + if (it == pwallet->mapWallet.end()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Invalid parameter, unknown transaction"); + } + + const CWalletTx &trans = it->second; + + if (outpt.GetN() >= trans.tx->vout.size()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Invalid parameter, vout index out of bounds"); + } + + if (pwallet->IsSpent(outpt.GetTxId(), outpt.GetN())) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Invalid parameter, expected unspent output"); + } + + const bool is_locked = + pwallet->IsLockedCoin(outpt.GetTxId(), outpt.GetN()); + + if (fUnlock && !is_locked) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Invalid parameter, expected locked output"); + } + + if (!fUnlock && is_locked) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Invalid parameter, output already locked"); + } + + outputs.push_back(outpt); + } + // Atomically set (un)locked status for the outputs. + for (const COutPoint &outpt : outputs) { if (fUnlock) { pwallet->UnlockCoin(outpt); } else { diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -119,12 +119,20 @@ # Exercise locking of unspent outputs unspent_0 = self.nodes[2].listunspent()[0] unspent_0 = {"txid": unspent_0["txid"], "vout": unspent_0["vout"]} + assert_raises_rpc_error(-8, "Invalid parameter, expected locked output", + self.nodes[2].lockunspent, True, [unspent_0]) self.nodes[2].lockunspent(False, [unspent_0]) + assert_raises_rpc_error(-8, "Invalid parameter, output already locked", + self.nodes[2].lockunspent, False, [unspent_0]) assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].sendtoaddress, self.nodes[2].getnewaddress(), 20) assert_equal([unspent_0], self.nodes[2].listlockunspent()) self.nodes[2].lockunspent(True, [unspent_0]) assert_equal(len(self.nodes[2].listlockunspent()), 0) + assert_raises_rpc_error(-8, "Invalid parameter, unknown transaction", self.nodes[2].lockunspent, False, [ + {"txid": "0000000000000000000000000000000000", "vout": 0}]) + assert_raises_rpc_error(-8, "Invalid parameter, vout index out of bounds", + self.nodes[2].lockunspent, False, [{"txid": unspent_0["txid"], "vout": 999}]) # Have node1 generate 100 blocks (so node0 can recover the fee) self.nodes[1].generate(100) @@ -164,6 +172,12 @@ assert_equal(self.nodes[2].getbalance(), 94) assert_equal(self.nodes[2].getbalance("from1"), 94 - 21) + # Verify that a spent output cannot be locked anymore + spent_0 = {"txid": node0utxos[0]["txid"], + "vout": node0utxos[0]["vout"]} + assert_raises_rpc_error(-8, "Invalid parameter, expected unspent output", + self.nodes[0].lockunspent, False, [spent_0]) + # Send 10 BTC normal old_balance = self.nodes[2].getbalance() address = self.nodes[0].getnewaddress("test")