Skip to content

Commit a06c5b8

Browse files
laanwjvijaydasmp
authored andcommitted
Merge bitcoin#23065: Allow UTXO locks to be written to wallet DB
d96b000 Make GUI UTXO lock/unlock persistent (Samuel Dobson) 077154f Add release note for lockunspent change (Samuel Dobson) 719ae92 Update lockunspent tests for lock persistence (Samuel Dobson) f13fc16 Allow lockunspent to store the lock in the wallet DB (Samuel Dobson) c527893 Allow locked UTXOs to be store in the wallet database (Samuel Dobson) Pull request description: Addresses and closes bitcoin#22368 As per that issue (and its predecessor bitcoin#14907), there seems to be some interest in allowing unspent outputs to be locked persistently. This PR does so by adding a flag to lockunspent to store the change in the wallet database. Defaults to false, so there is no change in default behaviour. Edit: GUI commit changes default behaviour. UTXOs locked/unlocked via the GUI are now persistent. ACKs for top commit: achow101: ACK d96b000 kristapsk: ACK d96b000 lsilva01: Tested ACK bitcoin@d96b000 on Ubuntu 20.04 prayank23: ACK bitcoin@d96b000 Tree-SHA512: 957a5bbfe7f763036796906ccb1598feb6c14c5975838be1ba24a198840bf59e83233165cb112cebae909b6b25bf27275a4d7fa425923ef6c788ff671d7a89a8
1 parent 2d0e5d7 commit a06c5b8

File tree

11 files changed

+139
-29
lines changed

11 files changed

+139
-29
lines changed

doc/release-notes-23065.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Notable changes
2+
===============
3+
4+
Updated RPCs
5+
------------
6+
7+
- `lockunspent` now optionally takes a third parameter, `persistent`, which
8+
causes the lock to be written persistently to the wallet database. This
9+
allows UTXOs to remain locked even after node restarts or crashes.
10+
11+
GUI changes
12+
-----------
13+
14+
- UTXOs which are locked via the GUI are now stored persistently in the
15+
wallet database, so are not lost on node shutdown or crash.

src/interfaces/wallet.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,10 @@ class Wallet
136136
virtual bool setAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& value) = 0;
137137

138138
//! Lock coin.
139-
virtual void lockCoin(const COutPoint& output) = 0;
139+
virtual bool lockCoin(const COutPoint& output, const bool write_to_db) = 0;
140140

141141
//! Unlock coin.
142-
virtual void unlockCoin(const COutPoint& output) = 0;
142+
virtual bool unlockCoin(const COutPoint& output) = 0;
143143

144144
//! Return whether coin is locked.
145145
virtual bool isLockedCoin(const COutPoint& output) = 0;

src/qt/coincontroldialog.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ void CoinControlDialog::lockCoin()
300300
contextMenuItem->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked);
301301

302302
COutPoint outpt(uint256S(contextMenuItem->data(COLUMN_ADDRESS, TxHashRole).toString().toStdString()), contextMenuItem->data(COLUMN_ADDRESS, VOutRole).toUInt());
303-
model->wallet().lockCoin(outpt);
303+
model->wallet().lockCoin(outpt, /* write_to_db = */ true);
304304
contextMenuItem->setDisabled(true);
305305
contextMenuItem->setIcon(COLUMN_CHECKBOX, GUIUtil::getIcon("lock_closed", GUIUtil::ThemedColor::RED));
306306
updateLabelLocked();

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
152152
{ "gettxoutsetinfo", 2, "use_index"},
153153
{ "lockunspent", 0, "unlock" },
154154
{ "lockunspent", 1, "transactions" },
155+
{ "lockunspent", 2, "persistent" },
155156
{ "send", 0, "outputs" },
156157
{ "send", 1, "conf_target" },
157158
{ "send", 3, "fee_rate"},

src/wallet/interfaces.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,15 +238,17 @@ class WalletImpl : public Wallet
238238
WalletBatch batch{m_wallet->GetDatabase()};
239239
return m_wallet->SetAddressReceiveRequest(batch, dest, id, value);
240240
}
241-
void lockCoin(const COutPoint& output) override
241+
bool lockCoin(const COutPoint& output, const bool write_to_db) override
242242
{
243243
LOCK(m_wallet->cs_wallet);
244-
return m_wallet->LockCoin(output);
244+
std::unique_ptr<WalletBatch> batch = write_to_db ? std::make_unique<WalletBatch>(m_wallet->GetDatabase()) : nullptr;
245+
return m_wallet->LockCoin(output, batch.get());
245246
}
246-
void unlockCoin(const COutPoint& output) override
247+
bool unlockCoin(const COutPoint& output) override
247248
{
248249
LOCK(m_wallet->cs_wallet);
249-
return m_wallet->UnlockCoin(output);
250+
std::unique_ptr<WalletBatch> batch = std::make_unique<WalletBatch>(m_wallet->GetDatabase());
251+
return m_wallet->UnlockCoin(output, batch.get());
250252
}
251253
bool isLockedCoin(const COutPoint& output) override
252254
{

src/wallet/rpcwallet.cpp

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2204,8 +2204,9 @@ static RPCHelpMan lockunspent()
22042204
"If no transaction outputs are specified when unlocking then all current locked transaction outputs are unlocked.\n"
22052205
"A locked transaction output will not be chosen by automatic coin selection, when spending Dash.\n"
22062206
"Manually selected coins are automatically unlocked.\n"
2207-
"Locks are stored in memory only. Nodes start with zero locked outputs, and the locked output list\n"
2208-
"is always cleared (by virtue of process exit) when a node stops or fails.\n"
2207+
"Locks are stored in memory only, unless persistent=true, in which case they will be written to the\n"
2208+
"wallet database and loaded on node start. Unwritten (persistent=false) locks are always cleared\n"
2209+
"(by virtue of process exit) when a node stops or fails. Unlocking will clear both persistent and not.\n"
22092210
"Also see the listunspent call\n",
22102211
{
22112212
{"unlock", RPCArg::Type::BOOL, RPCArg::Optional::NO, "Whether to unlock (true) or lock (false) the specified transactions"},
@@ -2217,6 +2218,7 @@ static RPCHelpMan lockunspent()
22172218
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"},
22182219
},
22192220
},
2221+
{"persistent", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to write/erase this lock in the wallet database, or keep the change in memory only. Ignored for unlocking."},
22202222
},
22212223
},
22222224
},
@@ -2232,6 +2234,8 @@ static RPCHelpMan lockunspent()
22322234
+ HelpExampleCli("listlockunspent", "") +
22332235
"\nUnlock the transaction again\n"
22342236
+ HelpExampleCli("lockunspent", "true \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"") +
2237+
"\nLock the transaction persistently in the wallet database\n"
2238+
+ HelpExampleCli("lockunspent", "false \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\" true") +
22352239
"\nAs a JSON-RPC call\n"
22362240
+ HelpExampleRpc("lockunspent", "false, \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"")
22372241
},
@@ -2250,9 +2254,13 @@ static RPCHelpMan lockunspent()
22502254

22512255
bool fUnlock = request.params[0].get_bool();
22522256

2257+
const bool persistent{request.params[2].isNull() ? false : request.params[2].get_bool()};
2258+
22532259
if (request.params[1].isNull()) {
2254-
if (fUnlock)
2255-
pwallet->UnlockAllCoins();
2260+
if (fUnlock) {
2261+
if (!pwallet->UnlockAllCoins())
2262+
throw JSONRPCError(RPC_WALLET_ERROR, "Unlocking coins failed");
2263+
}
22562264
return true;
22572265
}
22582266

@@ -2303,17 +2311,24 @@ static RPCHelpMan lockunspent()
23032311
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected locked output");
23042312
}
23052313

2306-
if (!fUnlock && is_locked) {
2314+
if (!fUnlock && is_locked && !persistent) {
23072315
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output already locked");
23082316
}
23092317

23102318
outputs.push_back(outpt);
23112319
}
23122320

2321+
std::unique_ptr<WalletBatch> batch = nullptr;
2322+
// Unlock is always persistent
2323+
if (fUnlock || persistent) batch = std::make_unique<WalletBatch>(pwallet->GetDatabase());
2324+
23132325
// Atomically set (un)locked status for the outputs.
23142326
for (const COutPoint& outpt : outputs) {
2315-
if (fUnlock) pwallet->UnlockCoin(outpt);
2316-
else pwallet->LockCoin(outpt);
2327+
if (fUnlock) {
2328+
if (!pwallet->UnlockCoin(outpt, batch.get())) throw JSONRPCError(RPC_WALLET_ERROR, "Unlocking coin failed");
2329+
} else {
2330+
if (!pwallet->LockCoin(outpt, batch.get())) throw JSONRPCError(RPC_WALLET_ERROR, "Locking coin failed");
2331+
}
23172332
}
23182333

23192334
return true;

src/wallet/wallet.cpp

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -625,20 +625,25 @@ bool CWallet::IsSpent(const uint256& hash, unsigned int n) const
625625
return false;
626626
}
627627

628-
void CWallet::AddToSpends(const COutPoint& outpoint, const uint256& wtxid)
628+
void CWallet::AddToSpends(const COutPoint& outpoint, const uint256& wtxid, WalletBatch* batch)
629629
{
630630
mapTxSpends.insert(std::make_pair(outpoint, wtxid));
631631
setWalletUTXO.erase(outpoint);
632632

633-
setLockedCoins.erase(outpoint);
633+
if (batch) {
634+
UnlockCoin(outpoint, batch);
635+
} else {
636+
WalletBatch temp_batch(GetDatabase());
637+
UnlockCoin(outpoint, &temp_batch);
638+
}
634639

635640
std::pair<TxSpends::iterator, TxSpends::iterator> range;
636641
range = mapTxSpends.equal_range(outpoint);
637642
SyncMetaData(range);
638643
}
639644

640645

641-
void CWallet::AddToSpends(const uint256& wtxid)
646+
void CWallet::AddToSpends(const uint256& wtxid, WalletBatch* batch)
642647
{
643648
auto it = mapWallet.find(wtxid);
644649
assert(it != mapWallet.end());
@@ -647,7 +652,7 @@ void CWallet::AddToSpends(const uint256& wtxid)
647652
return;
648653

649654
for (const CTxIn& txin : thisTx.tx->vin)
650-
AddToSpends(txin.prevout, wtxid);
655+
AddToSpends(txin.prevout, wtxid, batch);
651656
}
652657

653658
bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
@@ -915,7 +920,7 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const CWalletTx::Confirmatio
915920
wtx.nOrderPos = IncOrderPosNext(&batch);
916921
wtx.m_it_wtxOrdered = wtxOrdered.insert(std::make_pair(wtx.nOrderPos, &wtx));
917922
wtx.nTimeSmart = ComputeTimeSmart(wtx);
918-
AddToSpends(hash);
923+
AddToSpends(hash, &batch);
919924

920925
std::vector<std::pair<const CTransactionRef&, unsigned int>> outputs;
921926
for(unsigned int i = 0; i < wtx.tx->vout.size(); ++i) {
@@ -4443,7 +4448,7 @@ void ReserveDestination::ReturnDestination()
44434448
address = CNoDestination();
44444449
}
44454450

4446-
void CWallet::LockCoin(const COutPoint& output)
4451+
bool CWallet::LockCoin(const COutPoint& output, WalletBatch* batch)
44474452
{
44484453
AssertLockHeld(cs_wallet);
44494454
setLockedCoins.insert(output);
@@ -4452,23 +4457,38 @@ void CWallet::LockCoin(const COutPoint& output)
44524457

44534458
fAnonymizableTallyCached = false;
44544459
fAnonymizableTallyCachedNonDenom = false;
4460+
if (batch) {
4461+
return batch->WriteLockedUTXO(output);
4462+
}
4463+
return true;
44554464
}
44564465

4457-
void CWallet::UnlockCoin(const COutPoint& output)
4466+
bool CWallet::UnlockCoin(const COutPoint& output, WalletBatch* batch)
44584467
{
44594468
AssertLockHeld(cs_wallet);
4460-
setLockedCoins.erase(output);
44614469
std::map<uint256, CWalletTx>::iterator it = mapWallet.find(output.hash);
44624470
if (it != mapWallet.end()) it->second.MarkDirty(); // recalculate all credits for this tx
44634471

44644472
fAnonymizableTallyCached = false;
44654473
fAnonymizableTallyCachedNonDenom = false;
4474+
4475+
bool was_locked = setLockedCoins.erase(output);
4476+
if (batch && was_locked) {
4477+
return batch->EraseLockedUTXO(output);
4478+
}
4479+
return true;
44664480
}
44674481

4468-
void CWallet::UnlockAllCoins()
4482+
bool CWallet::UnlockAllCoins()
44694483
{
44704484
AssertLockHeld(cs_wallet);
4485+
bool success = true;
4486+
WalletBatch batch(GetDatabase());
4487+
for (auto it = setLockedCoins.begin(); it != setLockedCoins.end(); ++it) {
4488+
success &= batch.EraseLockedUTXO(*it);
4489+
}
44714490
setLockedCoins.clear();
4491+
return success;
44724492
}
44734493

44744494
bool CWallet::IsLockedCoin(uint256 hash, unsigned int n) const

src/wallet/wallet.h

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -759,8 +759,8 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
759759
*/
760760
typedef std::multimap<COutPoint, uint256> TxSpends;
761761
TxSpends mapTxSpends GUARDED_BY(cs_wallet);
762-
void AddToSpends(const COutPoint& outpoint, const uint256& wtxid) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
763-
void AddToSpends(const uint256& wtxid) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
762+
void AddToSpends(const COutPoint& outpoint, const uint256& wtxid, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
763+
void AddToSpends(const uint256& wtxid, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
764764

765765
std::set<COutPoint> setWalletUTXO;
766766
mutable std::map<COutPoint, int> mapOutpointRoundsCache GUARDED_BY(cs_wallet);
@@ -1033,9 +1033,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
10331033
std::vector<OutputGroup> GroupOutputs(const std::vector<COutput>& outputs, bool separate_coins, const CFeeRate& effective_feerate, const CFeeRate& long_term_feerate, const CoinEligibilityFilter& filter, bool positive_only) const;
10341034

10351035
bool IsLockedCoin(uint256 hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
1036-
void LockCoin(const COutPoint& output) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
1037-
void UnlockCoin(const COutPoint& output) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
1038-
void UnlockAllCoins() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
1036+
bool LockCoin(const COutPoint& output, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
1037+
bool UnlockCoin(const COutPoint& output, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
1038+
bool UnlockAllCoins() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
10391039
void ListLockedCoins(std::vector<COutPoint>& vOutpts) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
10401040
void ListProTxCoins(std::vector<COutPoint>& vOutpts) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
10411041

src/wallet/walletdb.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const std::string HDCHAIN{"hdchain"};
4747
const std::string HDPUBKEY{"hdpubkey"};
4848
const std::string KEYMETA{"keymeta"};
4949
const std::string KEY{"key"};
50+
const std::string LOCKED_UTXO{"lockedutxo"};
5051
const std::string MASTER_KEY{"mkey"};
5152
const std::string MINVERSION{"minversion"};
5253
const std::string NAME{"name"};
@@ -308,6 +309,16 @@ bool WalletBatch::WriteDescriptorCacheItems(const uint256& desc_id, const Descri
308309
return true;
309310
}
310311

312+
bool WalletBatch::WriteLockedUTXO(const COutPoint& output)
313+
{
314+
return WriteIC(std::make_pair(DBKeys::LOCKED_UTXO, std::make_pair(output.hash, output.n)), uint8_t{'1'});
315+
}
316+
317+
bool WalletBatch::EraseLockedUTXO(const COutPoint& output)
318+
{
319+
return EraseIC(std::make_pair(DBKeys::LOCKED_UTXO, std::make_pair(output.hash, output.n)));
320+
}
321+
311322
class CWalletScanState {
312323
public:
313324
unsigned int nKeys{0};
@@ -709,6 +720,12 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
709720

710721
wss.m_descriptor_crypt_keys.insert(std::make_pair(std::make_pair(desc_id, pubkey.GetID()), std::make_pair(pubkey, privkey)));
711722
wss.fIsEncrypted = true;
723+
} else if (strType == DBKeys::LOCKED_UTXO) {
724+
uint256 hash;
725+
uint32_t n;
726+
ssKey >> hash;
727+
ssKey >> n;
728+
pwallet->LockCoin(COutPoint(hash, n));
712729
} else if (strType != DBKeys::BESTBLOCK && strType != DBKeys::BESTBLOCK_NOMERKLE &&
713730
strType != DBKeys::MINVERSION && strType != DBKeys::ACENTRY &&
714731
strType != DBKeys::VERSION && strType != DBKeys::SETTINGS &&

src/wallet/walletdb.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ extern const std::string HDCHAIN;
7474
extern const std::string HDPUBKEY;
7575
extern const std::string KEY;
7676
extern const std::string KEYMETA;
77+
extern const std::string LOCKED_UTXO;
7778
extern const std::string MASTER_KEY;
7879
extern const std::string MINVERSION;
7980
extern const std::string NAME;
@@ -219,6 +220,9 @@ class WalletBatch
219220
bool WriteDescriptorLastHardenedCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index);
220221
bool WriteDescriptorCacheItems(const uint256& desc_id, const DescriptorCache& cache);
221222

223+
bool WriteLockedUTXO(const COutPoint& output);
224+
bool EraseLockedUTXO(const COutPoint& output);
225+
222226
/// Write destination data key,value tuple to database
223227
bool WriteDestData(const std::string &address, const std::string &key, const std::string &value);
224228
/// Erase destination data tuple from wallet database

0 commit comments

Comments
 (0)