diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -34,6 +34,8 @@ #include +#include + static const std::string WALLET_ENDPOINT_BASE = "/wallet/"; static std::string urlDecode(const std::string &urlEncoded) { @@ -2652,7 +2654,7 @@ "\nArguments:\n" "1. \"passphrase\" (string, required) The wallet passphrase\n" "2. timeout (numeric, required) The time to keep the " - "decryption key in seconds.\n" + "decryption key in seconds; capped at 100000000 (~3 years).\n" "\nNote:\n" "Issuing the walletpassphrase command while the wallet is already " "unlocked will set a new unlock\n" @@ -2687,6 +2689,20 @@ // with. strWalletPass = request.params[0].get_str().c_str(); + // Get the timeout + int64_t nSleepTime = request.params[1].get_int64(); + // Timeout cannot be negative, otherwise it will relock immediately + if (nSleepTime < 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Timeout cannot be negative."); + } + // Clamp timeout + // larger values trigger a macos/libevent bug? + constexpr int64_t MAX_SLEEP_TIME = 100000000; + if (nSleepTime > MAX_SLEEP_TIME) { + nSleepTime = MAX_SLEEP_TIME; + } + if (strWalletPass.length() > 0) { if (!pwallet->Unlock(strWalletPass)) { throw JSONRPCError( @@ -2702,10 +2718,9 @@ pwallet->TopUpKeyPool(); - int64_t nSleepTime = request.params[1].get_int64(); pwallet->nRelockTime = GetTime() + nSleepTime; RPCRunLater(strprintf("lockwallet(%s)", pwallet->GetName()), - boost::bind(LockWallet, pwallet), nSleepTime); + std::bind(LockWallet, pwallet), nSleepTime); return NullUniValue; } diff --git a/test/functional/wallet_encryption.py b/test/functional/wallet_encryption.py --- a/test/functional/wallet_encryption.py +++ b/test/functional/wallet_encryption.py @@ -10,6 +10,8 @@ from test_framework.util import ( assert_equal, assert_raises_rpc_error, + assert_greater_than, + assert_greater_than_or_equal, ) @@ -56,7 +58,8 @@ self.nodes[0].walletpassphrase, passphrase + "wrong", 10) # Test walletlock and unlocked_until values - self.nodes[0].setmocktime(1) + self.mocktime = 1 + self.nodes[0].setmocktime(self.mocktime) self.nodes[0].walletpassphrase(passphrase, 84600) assert_equal(privkey, self.nodes[0].dumpprivkey(address)) assert_equal( @@ -73,6 +76,27 @@ self.nodes[0].walletpassphrase, passphrase, 10) self.nodes[0].walletpassphrase(passphrase2, 10) assert_equal(privkey, self.nodes[0].dumpprivkey(address)) + self.nodes[0].walletlock() + + # Test timeout bounds + assert_raises_rpc_error(-8, "Timeout cannot be negative.", + self.nodes[0].walletpassphrase, passphrase2, -10) + # Check the timeout + # Check a time less than the limit + MAX_VALUE = 100000000 + expected_time = self.mocktime + MAX_VALUE - 600 + self.nodes[0].walletpassphrase(passphrase2, MAX_VALUE - 600) + actual_time = self.nodes[0].getwalletinfo()['unlocked_until'] + assert_greater_than_or_equal(actual_time, expected_time) + # 5 second buffer + assert_greater_than(expected_time + 5, actual_time) + # Check a time greater than the limit + expected_time = self.mocktime + MAX_VALUE - 1 + self.nodes[0].walletpassphrase(passphrase2, MAX_VALUE + 1000) + actual_time = self.nodes[0].getwalletinfo()['unlocked_until'] + assert_greater_than_or_equal(actual_time, expected_time) + # 5 second buffer + assert_greater_than(expected_time + 5, actual_time) if __name__ == '__main__':