Skip to content

Commit 99bc0b4

Browse files
committed
Merge #11087: Diagnose unsuitable outputs in lockunspent().
28f8b66 Diagnose unsuitable outputs in lockunspent(). (Eelis) Pull request description: Fixes #2667. This is a simplified version of pull request #3574, which was abandoned by its author. I added some tests as well. Tree-SHA512: e63e00dec8b1b232079380183805cb0b0b18c78ea6bea769837949aab984689d7f68b2ccfe66b1873517b040b9e616ce0eb058575c3d4382aa8c26eebcf1f14e
2 parents 084f52f + 28f8b66 commit 99bc0b4

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
@@ -2531,33 +2531,66 @@ UniValue lockunspent(const JSONRPCRequest& request)
25312531

25322532
RPCTypeCheckArgument(request.params[1], UniValue::VARR);
25332533

2534-
UniValue outputs = request.params[1].get_array();
2535-
for (unsigned int idx = 0; idx < outputs.size(); idx++) {
2536-
const UniValue& output = outputs[idx];
2537-
if (!output.isObject())
2538-
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected object");
2539-
const UniValue& o = output.get_obj();
2534+
const UniValue& output_params = request.params[1];
2535+
2536+
// Create and validate the COutPoints first.
2537+
2538+
std::vector<COutPoint> outputs;
2539+
outputs.reserve(output_params.size());
2540+
2541+
for (unsigned int idx = 0; idx < output_params.size(); idx++) {
2542+
const UniValue& o = output_params[idx].get_obj();
25402543

25412544
RPCTypeCheckObj(o,
25422545
{
25432546
{"txid", UniValueType(UniValue::VSTR)},
25442547
{"vout", UniValueType(UniValue::VNUM)},
25452548
});
25462549

2547-
std::string txid = find_value(o, "txid").get_str();
2548-
if (!IsHex(txid))
2550+
const std::string& txid = find_value(o, "txid").get_str();
2551+
if (!IsHex(txid)) {
25492552
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected hex txid");
2553+
}
25502554

2551-
int nOutput = find_value(o, "vout").get_int();
2552-
if (nOutput < 0)
2555+
const int nOutput = find_value(o, "vout").get_int();
2556+
if (nOutput < 0) {
25532557
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout must be positive");
2558+
}
25542559

2555-
COutPoint outpt(uint256S(txid), nOutput);
2560+
const COutPoint outpt(uint256S(txid), nOutput);
25562561

2557-
if (fUnlock)
2558-
pwallet->UnlockCoin(outpt);
2559-
else
2560-
pwallet->LockCoin(outpt);
2562+
const auto it = pwallet->mapWallet.find(outpt.hash);
2563+
if (it == pwallet->mapWallet.end()) {
2564+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, unknown transaction");
2565+
}
2566+
2567+
const CWalletTx& trans = it->second;
2568+
2569+
if (outpt.n >= trans.tx->vout.size()) {
2570+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout index out of bounds");
2571+
}
2572+
2573+
if (pwallet->IsSpent(outpt.hash, outpt.n)) {
2574+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected unspent output");
2575+
}
2576+
2577+
const bool is_locked = pwallet->IsLockedCoin(outpt.hash, outpt.n);
2578+
2579+
if (fUnlock && !is_locked) {
2580+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected locked output");
2581+
}
2582+
2583+
if (!fUnlock && is_locked) {
2584+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output already locked");
2585+
}
2586+
2587+
outputs.push_back(outpt);
2588+
}
2589+
2590+
// Atomically set (un)locked status for the outputs.
2591+
for (const COutPoint& outpt : outputs) {
2592+
if (fUnlock) pwallet->UnlockCoin(outpt);
2593+
else pwallet->LockCoin(outpt);
25612594
}
25622595

25632596
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)