Fixes #2667.
This is a simplified version of pull request #3574, which was abandoned by its author.
I added some tests as well.
2398 | @@ -2397,6 +2399,40 @@ UniValue lockunspent(const JSONRPCRequest& request) 2399 | 2400 | COutPoint outpt(uint256S(txid), nOutput); 2401 | 2402 | + const bool isLocked = pwallet->IsLockedCoin(outpt.hash, outpt.n);
bool is_locked = ...;
2398 | @@ -2397,6 +2399,40 @@ UniValue lockunspent(const JSONRPCRequest& request) 2399 | 2400 | COutPoint outpt(uint256S(txid), nOutput); 2401 | 2402 | + const bool isLocked = pwallet->IsLockedCoin(outpt.hash, outpt.n); 2403 | + 2404 | + if (fUnlock)
No need to chain:
if (fUnlock && !is_locked) {
throw ...;
}
if (!fUnlock && is_locked) {
throw ...;
}
...
Thanks for the review! Are you absolutely sure that attempting to lock an already locked output should be an error?
IMO yes, it indicates it was locked by other client and as such it is not yours to use?
Ok, makes sense!
2418 | + const CWalletTx& trans = it->second; 2419 | + 2420 | + if (outpt.n >= trans.tx->vout.size()) 2421 | + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout index out of bounds"); 2422 | + 2423 | + if (pwallet->IsMine(trans.tx->vout[outpt.n]) == ISMINE_NO)
I think is must be either ISMINE_SPENDABLE or ISMINE_WATCH_SOLVABLE?
Do we really have to test IsMine and all? In the wallet it's just a std::set<COutPoint>, so IMO is enough to check:
But if there is interest in doing these tests, then perform the cheapest first.
I'll remove the check for now. It can always be added later.
Read developer notes and update accordingly.
Amended to:
2398 | @@ -2397,6 +2399,38 @@ UniValue lockunspent(const JSONRPCRequest& request) 2399 | 2400 | COutPoint outpt(uint256S(txid), nOutput); 2401 | 2402 | + const bool is_locked = pwallet->IsLockedCoin(outpt.hash, outpt.n); 2403 | + 2404 | + if (fUnlock && !is_locked)
Missing {} and { is one the same line. Same below. See developer notes.
Will do! Thanks for caring about consistent style. :)
Amended to use curly brackets in accordance with developer notes.
92 | @@ -93,11 +93,17 @@ def run_test(self): 93 | # Exercise locking of unspent outputs 94 | unspent_0 = self.nodes[2].listunspent()[0] 95 | unspent_0 = {"txid": unspent_0["txid"], "vout": unspent_0["vout"]} 96 | + assert_raises_jsonrpc(-8, "expected locked output", self.nodes[2].lockunspent, True, [unspent_0])
Personally, I don't think the complete error message needs to be asserted, as long as the string is long enough to disambiguate. But also fine to change this to the complete string.
92 | @@ -93,11 +93,17 @@ def run_test(self): 93 | # Exercise locking of unspent outputs 94 | unspent_0 = self.nodes[2].listunspent()[0] 95 | unspent_0 = {"txid": unspent_0["txid"], "vout": unspent_0["vout"]} 96 | + assert_raises_jsonrpc(-8, "expected locked output", self.nodes[2].lockunspent, True, [unspent_0]) 97 | self.nodes[2].lockunspent(False, [unspent_0]) 98 | + assert_raises_jsonrpc(-8, "output already locked", self.nodes[2].lockunspent, False, [unspent_0])
Same as above.
98 | + assert_raises_jsonrpc(-8, "output already locked", self.nodes[2].lockunspent, False, [unspent_0]) 99 | assert_raises_jsonrpc(-4, "Insufficient funds", self.nodes[2].sendtoaddress, self.nodes[2].getnewaddress(), 20) 100 | assert_equal([unspent_0], self.nodes[2].listlockunspent()) 101 | self.nodes[2].lockunspent(True, [unspent_0]) 102 | assert_equal(len(self.nodes[2].listlockunspent()), 0) 103 | + assert_raises_jsonrpc(-8, "unknown transaction",
Same as above.
100 | assert_equal([unspent_0], self.nodes[2].listlockunspent()) 101 | self.nodes[2].lockunspent(True, [unspent_0]) 102 | assert_equal(len(self.nodes[2].listlockunspent()), 0) 103 | + assert_raises_jsonrpc(-8, "unknown transaction", 104 | + self.nodes[2].lockunspent, False, [{"txid": "0000000000000000000000000000000000", "vout": 0}]) 105 | + assert_raises_jsonrpc(-8, "vout index out of bounds",
Same as above.
141 | @@ -136,6 +142,10 @@ def run_test(self): 142 | assert_equal(self.nodes[2].getbalance(), 94) 143 | assert_equal(self.nodes[2].getbalance("from1"), 94-21) 144 | 145 | + # Verify that a spent output cannot be locked anymore 146 | + spent_0 = {"txid" : node0utxos[0]["txid"], "vout" : node0utxos[0]["vout"]} 147 | + assert_raises_jsonrpc(-8, "expected unspent output", self.nodes[2].lockunspent, False, [spent_0])
Same as above.
utACK 918b6db42b12d86a01bdb70a4fc50244aa2ce623
You could modernize the code a little more: adding a range for loop instead of an index loop, and using UniValue directly instead of adding adding new find_value calls. Also I'm not sure if it's useful to make locking an already locked output or unlocked an already unlocked one errors. But overall, this change seems like an improvement and looks good to merge.
2408 | + if (!fUnlock) { 2409 | + if (is_locked) { 2410 | + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output already locked"); 2411 | + } 2412 | + 2413 | + if (pwallet->IsSpent(outpt.hash, outpt.n)) {
Why are these three checks ("expected unspent", "unknown transaction" and "vout index out of bounds") only done when trying to lock? Isn't it equally invalid to try to unlock bad txouts? Can you place these three checks above the const bool is_locked = pwallet->IsLockedCoin(outpt.hash, outpt.n); line?
The idea was that in all three of those cases (already spent output, unknown transaction, and vout index out of bounds), IsLockedCoin should already have returned false, so checking !is_locked should be enough.
I agree that the function will correctly return an error, but I think that it's better to return the more precise error if possible. expected unspent output, unknown transaction and vout index out of bounds are better to return than expected locked output (the last could infer that the outpoint exists but is unlocked).
Makes sense, I'll make the errors more precise.
2426 | + } 2427 | + } 2428 | + } 2429 | + 2430 | + // Atomically set (un)locked status for the outputs. 2431 | + for (unsigned int idx = 0; idx < outputs.size(); idx++) {
You could save reparsing the json by saving a vector of COutPoints in the first pass.
These loops don't do any json parsing, because the data is already in a UniValue. The find_value calls find the desired value in UniValue's internal std::vector of values.
You're right. There's no reparsing the JSON, just rereading the string and reconstructing the COutPoint.
I think either way is fine. Whichever you prefer.
Looks good, and great test coverage. A couple of nits inline.
Maybe out of the scope of this PR, but this RPC returns true if called with false but no list of transactions. A user might expect that with that call all outputs have been locked, but in fact it's a no-op. I think that if (request.params.size() == 1 && !fUnlock) then we should return false or throw.
Amended to:
Thanks for the feedback!
2401 | + outputs.reserve(output_params.size()); 2402 | + 2403 | + for (unsigned int idx = 0; idx < output_params.size(); idx++) { 2404 | + const UniValue& param = output_params[idx]; 2405 | + 2406 | + if (!param.isObject()) {
You can remove this check entirely. Type checking is done by the UniValue.get_xxx() functions, and returns almost an identical error to that given by this explicit check.
With the explicit check:
→ bitcoin-cli -rpcwallet=w1 lockunspent true [\"string\"]
error code: -8
error message:
Invalid parameter, expected
without:
→ bitcoin-cli -rpcwallet=w1 lockunspent true [\"string\"]
error code: -1
error message:
JSON value is not an object as expected
The implicit get_xxx() check is used extensively in other RPCs.
You can also combine the line below with above:
const UniValue& o = request.params[1].get_obj();
2408 | - const UniValue& o = output.get_obj(); 2409 | + } 2410 | + 2411 | + const UniValue& o = param.get_obj(); 2412 | 2413 | RPCTypeCheckObj(o,
In contrast to my comment above, it makes sense to keep these. RPCTypeCheckObj provides slightly more information (both the type expected and type received)
149 | @@ -144,6 +150,10 @@ def run_test(self): 150 | assert_equal(self.nodes[2].getbalance(), 94) 151 | assert_equal(self.nodes[2].getbalance("from1"), 94-21) 152 | 153 | + # Verify that a spent output cannot be locked anymore 154 | + spent_0 = {"txid": node0utxos[0]["txid"], "vout": node0utxos[0]["vout"]} 155 | + assert_raises_jsonrpc(-8, "Invalid parameter, expected unspent output", self.nodes[2].lockunspent, False, [spent_0])
This is the bug. lockunspent() should be called on self.nodes[0], not self.nodes[2]
Thanks! This one stumped me because for some reason when I ran the wallet tests locally, they didn't fail..
Looks great! One bug in the test code and one nit in the RPC method.
107 | assert_raises_jsonrpc(-4, "Insufficient funds", self.nodes[2].sendtoaddress, self.nodes[2].getnewaddress(), 20) 108 | assert_equal([unspent_0], self.nodes[2].listlockunspent()) 109 | self.nodes[2].lockunspent(True, [unspent_0]) 110 | assert_equal(len(self.nodes[2].listlockunspent()), 0) 111 | + assert_raises_jsonrpc(-8, "Invalid parameter, unknown transaction", 112 | + self.nodes[2].lockunspent, False, [{"txid": "0000000000000000000000000000000000", "vout": 0}])
I personally don't like this indentation style. It looks like something between a new line and a new code block.
Would you mind aligning this continuation line with the opening parens:
assert_raises_jsonrpc(-8, "Invalid parameter, unknown transaction",
self.nodes[2].lockunspent, False, [{"txid": "0000000000000000000000000000000000", "vout": 0}])
same below
Amended to:
Tested ACK fe2c95bda22c21252709cc1088d7e61a8c8b1df8. Looks great. Thanks for being receptive to all my feedback!
151 | @@ -144,6 +152,10 @@ def run_test(self): 152 | assert_equal(self.nodes[2].getbalance(), 94) 153 | assert_equal(self.nodes[2].getbalance("from1"), 94-21) 154 | 155 | + # Verify that a spent output cannot be locked anymore 156 | + spent_0 = {"txid": node0utxos[0]["txid"], "vout": node0utxos[0]["vout"]} 157 | + assert_raises_jsonrpc(-8, "Invalid parameter, expected unspent output", self.nodes[0].lockunspent, False, [spent_0])
Needs rebase and replace with "assert_raises_rpc_error"
2393 | - for (unsigned int idx = 0; idx < outputs.size(); idx++) { 2394 | - const UniValue& output = outputs[idx]; 2395 | - if (!output.isObject()) 2396 | - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected object"); 2397 | - const UniValue& o = output.get_obj(); 2398 | + const UniValue output_params = request.params[1];
Could declare as const UniValue& to avoid copying params.
2411 | {"vout", UniValueType(UniValue::VNUM)},
2412 | });
2413 |
2414 | - std::string txid = find_value(o, "txid").get_str();
2415 | - if (!IsHex(txid))
2416 | + const std::string txid = find_value(o, "txid").get_str();
Could declare as const std::string& to avoid copying string.
utACK fe2c95bda22c21252709cc1088d7e61a8c8b1df8. No significant changes since last review. Left minor comments (feel free to ignore them), but this does need to be rebased.
Fixes #2667.
Amended/rebased to:
assert_raises_rpc_error;utACK 28f8b6657764c7746645a6e75dfb09ffc0597322. Only changes since last review are rebase & adding const refs.
2518 | + outputs.push_back(outpt); 2519 | + } 2520 | + 2521 | + // Atomically set (un)locked status for the outputs. 2522 | + for (const COutPoint& outpt : outputs) { 2523 | + if (fUnlock) pwallet->UnlockCoin(outpt);
Coding style nit; if with else always needs braces/indentation (I know the original code you're rewriting wasn't following this convention, but in new code we try to).
utACK 28f8b6657764c7746645a6e75dfb09ffc0597322