Skip to content

Commit 09cb5ec

Browse files
committed
Merge bitcoin/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 #22368 As per that issue (and its predecessor #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/bitcoin@d96b000 on Ubuntu 20.04 prayank23: ACK bitcoin/bitcoin@d96b000 Tree-SHA512: 957a5bbfe7f763036796906ccb1598feb6c14c5975838be1ba24a198840bf59e83233165cb112cebae909b6b25bf27275a4d7fa425923ef6c788ff671d7a89a8
2 parents 16ccb3a + d96b000 commit 09cb5ec

File tree

11 files changed

+137
-28
lines changed

11 files changed

+137
-28
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
@@ -122,10 +122,10 @@ class Wallet
122122
virtual bool displayAddress(const CTxDestination& dest) = 0;
123123

124124
//! Lock coin.
125-
virtual void lockCoin(const COutPoint& output) = 0;
125+
virtual bool lockCoin(const COutPoint& output, const bool write_to_db) = 0;
126126

127127
//! Unlock coin.
128-
virtual void unlockCoin(const COutPoint& output) = 0;
128+
virtual bool unlockCoin(const COutPoint& output) = 0;
129129

130130
//! Return whether coin is locked.
131131
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
@@ -241,7 +241,7 @@ void CoinControlDialog::lockCoin()
241241
contextMenuItem->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked);
242242

243243
COutPoint outpt(uint256S(contextMenuItem->data(COLUMN_ADDRESS, TxHashRole).toString().toStdString()), contextMenuItem->data(COLUMN_ADDRESS, VOutRole).toUInt());
244-
model->wallet().lockCoin(outpt);
244+
model->wallet().lockCoin(outpt, /* write_to_db = */ true);
245245
contextMenuItem->setDisabled(true);
246246
contextMenuItem->setIcon(COLUMN_CHECKBOX, platformStyle->SingleColorIcon(":/icons/lock_closed"));
247247
updateLabelLocked();

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
131131
{ "gettxoutsetinfo", 2, "use_index"},
132132
{ "lockunspent", 0, "unlock" },
133133
{ "lockunspent", 1, "transactions" },
134+
{ "lockunspent", 2, "persistent" },
134135
{ "send", 0, "outputs" },
135136
{ "send", 1, "conf_target" },
136137
{ "send", 3, "fee_rate"},

src/wallet/interfaces.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,17 @@ class WalletImpl : public Wallet
214214
LOCK(m_wallet->cs_wallet);
215215
return m_wallet->DisplayAddress(dest);
216216
}
217-
void lockCoin(const COutPoint& output) override
217+
bool lockCoin(const COutPoint& output, const bool write_to_db) override
218218
{
219219
LOCK(m_wallet->cs_wallet);
220-
return m_wallet->LockCoin(output);
220+
std::unique_ptr<WalletBatch> batch = write_to_db ? std::make_unique<WalletBatch>(m_wallet->GetDatabase()) : nullptr;
221+
return m_wallet->LockCoin(output, batch.get());
221222
}
222-
void unlockCoin(const COutPoint& output) override
223+
bool unlockCoin(const COutPoint& output) override
223224
{
224225
LOCK(m_wallet->cs_wallet);
225-
return m_wallet->UnlockCoin(output);
226+
std::unique_ptr<WalletBatch> batch = std::make_unique<WalletBatch>(m_wallet->GetDatabase());
227+
return m_wallet->UnlockCoin(output, batch.get());
226228
}
227229
bool isLockedCoin(const COutPoint& output) override
228230
{

src/wallet/rpcwallet.cpp

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2140,8 +2140,9 @@ static RPCHelpMan lockunspent()
21402140
"If no transaction outputs are specified when unlocking then all current locked transaction outputs are unlocked.\n"
21412141
"A locked transaction output will not be chosen by automatic coin selection, when spending bitcoins.\n"
21422142
"Manually selected coins are automatically unlocked.\n"
2143-
"Locks are stored in memory only. Nodes start with zero locked outputs, and the locked output list\n"
2144-
"is always cleared (by virtue of process exit) when a node stops or fails.\n"
2143+
"Locks are stored in memory only, unless persistent=true, in which case they will be written to the\n"
2144+
"wallet database and loaded on node start. Unwritten (persistent=false) locks are always cleared\n"
2145+
"(by virtue of process exit) when a node stops or fails. Unlocking will clear both persistent and not.\n"
21452146
"Also see the listunspent call\n",
21462147
{
21472148
{"unlock", RPCArg::Type::BOOL, RPCArg::Optional::NO, "Whether to unlock (true) or lock (false) the specified transactions"},
@@ -2155,6 +2156,7 @@ static RPCHelpMan lockunspent()
21552156
},
21562157
},
21572158
},
2159+
{"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."},
21582160
},
21592161
RPCResult{
21602162
RPCResult::Type::BOOL, "", "Whether the command was successful or not"
@@ -2168,6 +2170,8 @@ static RPCHelpMan lockunspent()
21682170
+ HelpExampleCli("listlockunspent", "") +
21692171
"\nUnlock the transaction again\n"
21702172
+ HelpExampleCli("lockunspent", "true \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"") +
2173+
"\nLock the transaction persistently in the wallet database\n"
2174+
+ HelpExampleCli("lockunspent", "false \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\" true") +
21712175
"\nAs a JSON-RPC call\n"
21722176
+ HelpExampleRpc("lockunspent", "false, \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"")
21732177
},
@@ -2186,9 +2190,13 @@ static RPCHelpMan lockunspent()
21862190

21872191
bool fUnlock = request.params[0].get_bool();
21882192

2193+
const bool persistent{request.params[2].isNull() ? false : request.params[2].get_bool()};
2194+
21892195
if (request.params[1].isNull()) {
2190-
if (fUnlock)
2191-
pwallet->UnlockAllCoins();
2196+
if (fUnlock) {
2197+
if (!pwallet->UnlockAllCoins())
2198+
throw JSONRPCError(RPC_WALLET_ERROR, "Unlocking coins failed");
2199+
}
21922200
return true;
21932201
}
21942202

@@ -2239,17 +2247,24 @@ static RPCHelpMan lockunspent()
22392247
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected locked output");
22402248
}
22412249

2242-
if (!fUnlock && is_locked) {
2250+
if (!fUnlock && is_locked && !persistent) {
22432251
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output already locked");
22442252
}
22452253

22462254
outputs.push_back(outpt);
22472255
}
22482256

2257+
std::unique_ptr<WalletBatch> batch = nullptr;
2258+
// Unlock is always persistent
2259+
if (fUnlock || persistent) batch = std::make_unique<WalletBatch>(pwallet->GetDatabase());
2260+
22492261
// Atomically set (un)locked status for the outputs.
22502262
for (const COutPoint& outpt : outputs) {
2251-
if (fUnlock) pwallet->UnlockCoin(outpt);
2252-
else pwallet->LockCoin(outpt);
2263+
if (fUnlock) {
2264+
if (!pwallet->UnlockCoin(outpt, batch.get())) throw JSONRPCError(RPC_WALLET_ERROR, "Unlocking coin failed");
2265+
} else {
2266+
if (!pwallet->LockCoin(outpt, batch.get())) throw JSONRPCError(RPC_WALLET_ERROR, "Locking coin failed");
2267+
}
22532268
}
22542269

22552270
return true;

src/wallet/wallet.cpp

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -589,19 +589,24 @@ bool CWallet::IsSpent(const uint256& hash, unsigned int n) const
589589
return false;
590590
}
591591

592-
void CWallet::AddToSpends(const COutPoint& outpoint, const uint256& wtxid)
592+
void CWallet::AddToSpends(const COutPoint& outpoint, const uint256& wtxid, WalletBatch* batch)
593593
{
594594
mapTxSpends.insert(std::make_pair(outpoint, wtxid));
595595

596-
setLockedCoins.erase(outpoint);
596+
if (batch) {
597+
UnlockCoin(outpoint, batch);
598+
} else {
599+
WalletBatch temp_batch(GetDatabase());
600+
UnlockCoin(outpoint, &temp_batch);
601+
}
597602

598603
std::pair<TxSpends::iterator, TxSpends::iterator> range;
599604
range = mapTxSpends.equal_range(outpoint);
600605
SyncMetaData(range);
601606
}
602607

603608

604-
void CWallet::AddToSpends(const uint256& wtxid)
609+
void CWallet::AddToSpends(const uint256& wtxid, WalletBatch* batch)
605610
{
606611
auto it = mapWallet.find(wtxid);
607612
assert(it != mapWallet.end());
@@ -610,7 +615,7 @@ void CWallet::AddToSpends(const uint256& wtxid)
610615
return;
611616

612617
for (const CTxIn& txin : thisTx.tx->vin)
613-
AddToSpends(txin.prevout, wtxid);
618+
AddToSpends(txin.prevout, wtxid, batch);
614619
}
615620

616621
bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
@@ -910,7 +915,7 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const CWalletTx::Confirmatio
910915
wtx.nOrderPos = IncOrderPosNext(&batch);
911916
wtx.m_it_wtxOrdered = wtxOrdered.insert(std::make_pair(wtx.nOrderPos, &wtx));
912917
wtx.nTimeSmart = ComputeTimeSmart(wtx);
913-
AddToSpends(hash);
918+
AddToSpends(hash, &batch);
914919
}
915920

916921
if (!fInsertedNew)
@@ -2260,22 +2265,36 @@ bool CWallet::DisplayAddress(const CTxDestination& dest)
22602265
return signer_spk_man->DisplayAddress(scriptPubKey, signer);
22612266
}
22622267

2263-
void CWallet::LockCoin(const COutPoint& output)
2268+
bool CWallet::LockCoin(const COutPoint& output, WalletBatch* batch)
22642269
{
22652270
AssertLockHeld(cs_wallet);
22662271
setLockedCoins.insert(output);
2272+
if (batch) {
2273+
return batch->WriteLockedUTXO(output);
2274+
}
2275+
return true;
22672276
}
22682277

2269-
void CWallet::UnlockCoin(const COutPoint& output)
2278+
bool CWallet::UnlockCoin(const COutPoint& output, WalletBatch* batch)
22702279
{
22712280
AssertLockHeld(cs_wallet);
2272-
setLockedCoins.erase(output);
2281+
bool was_locked = setLockedCoins.erase(output);
2282+
if (batch && was_locked) {
2283+
return batch->EraseLockedUTXO(output);
2284+
}
2285+
return true;
22732286
}
22742287

2275-
void CWallet::UnlockAllCoins()
2288+
bool CWallet::UnlockAllCoins()
22762289
{
22772290
AssertLockHeld(cs_wallet);
2291+
bool success = true;
2292+
WalletBatch batch(GetDatabase());
2293+
for (auto it = setLockedCoins.begin(); it != setLockedCoins.end(); ++it) {
2294+
success &= batch.EraseLockedUTXO(*it);
2295+
}
22782296
setLockedCoins.clear();
2297+
return success;
22792298
}
22802299

22812300
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
@@ -256,8 +256,8 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
256256
*/
257257
typedef std::multimap<COutPoint, uint256> TxSpends;
258258
TxSpends mapTxSpends GUARDED_BY(cs_wallet);
259-
void AddToSpends(const COutPoint& outpoint, const uint256& wtxid) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
260-
void AddToSpends(const uint256& wtxid) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
259+
void AddToSpends(const COutPoint& outpoint, const uint256& wtxid, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
260+
void AddToSpends(const uint256& wtxid, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
261261

262262
/**
263263
* Add a transaction to the wallet, or update it. pIndex and posInBlock should
@@ -449,9 +449,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
449449
bool DisplayAddress(const CTxDestination& dest) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
450450

451451
bool IsLockedCoin(uint256 hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
452-
void LockCoin(const COutPoint& output) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
453-
void UnlockCoin(const COutPoint& output) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
454-
void UnlockAllCoins() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
452+
bool LockCoin(const COutPoint& output, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
453+
bool UnlockCoin(const COutPoint& output, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
454+
bool UnlockAllCoins() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
455455
void ListLockedCoins(std::vector<COutPoint>& vOutpts) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
456456

457457
/*

src/wallet/walletdb.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const std::string FLAGS{"flags"};
4040
const std::string HDCHAIN{"hdchain"};
4141
const std::string KEYMETA{"keymeta"};
4242
const std::string KEY{"key"};
43+
const std::string LOCKED_UTXO{"lockedutxo"};
4344
const std::string MASTER_KEY{"mkey"};
4445
const std::string MINVERSION{"minversion"};
4546
const std::string NAME{"name"};
@@ -284,6 +285,16 @@ bool WalletBatch::WriteDescriptorCacheItems(const uint256& desc_id, const Descri
284285
return true;
285286
}
286287

288+
bool WalletBatch::WriteLockedUTXO(const COutPoint& output)
289+
{
290+
return WriteIC(std::make_pair(DBKeys::LOCKED_UTXO, std::make_pair(output.hash, output.n)), uint8_t{'1'});
291+
}
292+
293+
bool WalletBatch::EraseLockedUTXO(const COutPoint& output)
294+
{
295+
return EraseIC(std::make_pair(DBKeys::LOCKED_UTXO, std::make_pair(output.hash, output.n)));
296+
}
297+
287298
class CWalletScanState {
288299
public:
289300
unsigned int nKeys{0};
@@ -701,6 +712,12 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
701712

702713
wss.m_descriptor_crypt_keys.insert(std::make_pair(std::make_pair(desc_id, pubkey.GetID()), std::make_pair(pubkey, privkey)));
703714
wss.fIsEncrypted = true;
715+
} else if (strType == DBKeys::LOCKED_UTXO) {
716+
uint256 hash;
717+
uint32_t n;
718+
ssKey >> hash;
719+
ssKey >> n;
720+
pwallet->LockCoin(COutPoint(hash, n));
704721
} else if (strType != DBKeys::BESTBLOCK && strType != DBKeys::BESTBLOCK_NOMERKLE &&
705722
strType != DBKeys::MINVERSION && strType != DBKeys::ACENTRY &&
706723
strType != DBKeys::VERSION && strType != DBKeys::SETTINGS &&

src/wallet/walletdb.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ extern const std::string FLAGS;
6565
extern const std::string HDCHAIN;
6666
extern const std::string KEY;
6767
extern const std::string KEYMETA;
68+
extern const std::string LOCKED_UTXO;
6869
extern const std::string MASTER_KEY;
6970
extern const std::string MINVERSION;
7071
extern const std::string NAME;
@@ -250,6 +251,9 @@ class WalletBatch
250251
bool WriteDescriptorLastHardenedCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index);
251252
bool WriteDescriptorCacheItems(const uint256& desc_id, const DescriptorCache& cache);
252253

254+
bool WriteLockedUTXO(const COutPoint& output);
255+
bool EraseLockedUTXO(const COutPoint& output);
256+
253257
/// Write destination data key,value tuple to database
254258
bool WriteDestData(const std::string &address, const std::string &key, const std::string &value);
255259
/// Erase destination data tuple from wallet database

0 commit comments

Comments
 (0)