Skip to content

Commit 28f8b66

Browse files
committed
Diagnose unsuitable outputs in lockunspent().
Fixes #2667.
1 parent f74459d commit 28f8b66

File tree

2 files changed

+60
-15
lines changed

2 files changed

+60
-15
lines changed

src/wallet/rpcwallet.cpp

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2447,33 +2447,66 @@ UniValue lockunspent(const JSONRPCRequest& request)
24472447

24482448
RPCTypeCheckArgument(request.params[1], UniValue::VARR);
24492449

2450-
UniValue outputs = request.params[1].get_array();
2451-
for (unsigned int idx = 0; idx < outputs.size(); idx++) {
2452-
const UniValue& output = outputs[idx];
2453-
if (!output.isObject())
2454-
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected object");
2455-
const UniValue& o = output.get_obj();
2450+
const UniValue& output_params = request.params[1];
2451+
2452+
// Create and validate the COutPoints first.
2453+
2454+
std::vector<COutPoint> outputs;
2455+
outputs.reserve(output_params.size());
2456+
2457+
for (unsigned int idx = 0; idx < output_params.size(); idx++) {
2458+
const UniValue& o = output_params[idx].get_obj();
24562459

24572460
RPCTypeCheckObj(o,
24582461
{
24592462
{"txid", UniValueType(UniValue::VSTR)},
24602463
{"vout", UniValueType(UniValue::VNUM)},
24612464
});
24622465

2463-
std::string txid = find_value(o, "txid").get_str();
2464-
if (!IsHex(txid))
2466+
const std::string& txid = find_value(o, "txid").get_str();
2467+
if (!IsHex(txid)) {
24652468
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected hex txid");
2469+
}
24662470

2467-
int nOutput = find_value(o, "vout").get_int();
2468-
if (nOutput < 0)
2471+
const int nOutput = find_value(o, "vout").get_int();
2472+
if (nOutput < 0) {
24692473
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout must be positive");
2474+
}
24702475

2471-
COutPoint outpt(uint256S(txid), nOutput);
2476+
const COutPoint outpt(uint256S(txid), nOutput);
24722477

2473-
if (fUnlock)
2474-
pwallet->UnlockCoin(outpt);
2475-
else
2476-
pwallet->LockCoin(outpt);
2478+
const auto it = pwallet->mapWallet.find(outpt.hash);
2479+
if (it == pwallet->mapWallet.end()) {
2480+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, unknown transaction");
2481+
}
2482+
2483+
const CWalletTx& trans = it->second;
2484+
2485+
if (outpt.n >= trans.tx->vout.size()) {
2486+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout index out of bounds");
2487+
}
2488+
2489+
if (pwallet->IsSpent(outpt.hash, outpt.n)) {
2490+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected unspent output");
2491+
}
2492+
2493+
const bool is_locked = pwallet->IsLockedCoin(outpt.hash, outpt.n);
2494+
2495+
if (fUnlock && !is_locked) {
2496+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected locked output");
2497+
}
2498+
2499+
if (!fUnlock && is_locked) {
2500+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output already locked");
2501+
}
2502+
2503+
outputs.push_back(outpt);
2504+
}
2505+
2506+
// Atomically set (un)locked status for the outputs.
2507+
for (const COutPoint& outpt : outputs) {
2508+
if (fUnlock) pwallet->UnlockCoin(outpt);
2509+
else pwallet->LockCoin(outpt);
24772510
}
24782511

24792512
return true;

test/functional/wallet.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,19 @@ def run_test(self):
100100
# Exercise locking of unspent outputs
101101
unspent_0 = self.nodes[2].listunspent()[0]
102102
unspent_0 = {"txid": unspent_0["txid"], "vout": unspent_0["vout"]}
103+
assert_raises_rpc_error(-8, "Invalid parameter, expected locked output", self.nodes[2].lockunspent, True, [unspent_0])
103104
self.nodes[2].lockunspent(False, [unspent_0])
105+
assert_raises_rpc_error(-8, "Invalid parameter, output already locked", self.nodes[2].lockunspent, False, [unspent_0])
104106
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].sendtoaddress, self.nodes[2].getnewaddress(), 20)
105107
assert_equal([unspent_0], self.nodes[2].listlockunspent())
106108
self.nodes[2].lockunspent(True, [unspent_0])
107109
assert_equal(len(self.nodes[2].listlockunspent()), 0)
110+
assert_raises_rpc_error(-8, "Invalid parameter, unknown transaction",
111+
self.nodes[2].lockunspent, False,
112+
[{"txid": "0000000000000000000000000000000000", "vout": 0}])
113+
assert_raises_rpc_error(-8, "Invalid parameter, vout index out of bounds",
114+
self.nodes[2].lockunspent, False,
115+
[{"txid": unspent_0["txid"], "vout": 999}])
108116

109117
# Have node1 generate 100 blocks (so node0 can recover the fee)
110118
self.nodes[1].generate(100)
@@ -143,6 +151,10 @@ def run_test(self):
143151
assert_equal(self.nodes[2].getbalance(), 94)
144152
assert_equal(self.nodes[2].getbalance("from1"), 94-21)
145153

154+
# Verify that a spent output cannot be locked anymore
155+
spent_0 = {"txid": node0utxos[0]["txid"], "vout": node0utxos[0]["vout"]}
156+
assert_raises_rpc_error(-8, "Invalid parameter, expected unspent output", self.nodes[0].lockunspent, False, [spent_0])
157+
146158
# Send 10 BTC normal
147159
address = self.nodes[0].getnewaddress("test")
148160
fee_per_byte = Decimal('0.001') / 1000

0 commit comments

Comments
 (0)