mirror of
https://github.com/ElementsProject/elements.git
synced 2026-08-17 13:07:54 +02:00
Merge 09cb5ec6c8 into merged_master (Bitcoin PR bitcoin/bitcoin#23065)
This commit is contained in:
commit
c96554c071
11 changed files with 137 additions and 28 deletions
15
doc/release-notes-23065.md
Normal file
15
doc/release-notes-23065.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
Notable changes
|
||||
===============
|
||||
|
||||
Updated RPCs
|
||||
------------
|
||||
|
||||
- `lockunspent` now optionally takes a third parameter, `persistent`, which
|
||||
causes the lock to be written persistently to the wallet database. This
|
||||
allows UTXOs to remain locked even after node restarts or crashes.
|
||||
|
||||
GUI changes
|
||||
-----------
|
||||
|
||||
- UTXOs which are locked via the GUI are now stored persistently in the
|
||||
wallet database, so are not lost on node shutdown or crash.
|
||||
|
|
@ -124,10 +124,10 @@ public:
|
|||
virtual bool displayAddress(const CTxDestination& dest) = 0;
|
||||
|
||||
//! Lock coin.
|
||||
virtual void lockCoin(const COutPoint& output) = 0;
|
||||
virtual bool lockCoin(const COutPoint& output, const bool write_to_db) = 0;
|
||||
|
||||
//! Unlock coin.
|
||||
virtual void unlockCoin(const COutPoint& output) = 0;
|
||||
virtual bool unlockCoin(const COutPoint& output) = 0;
|
||||
|
||||
//! Return whether coin is locked.
|
||||
virtual bool isLockedCoin(const COutPoint& output) = 0;
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ void CoinControlDialog::lockCoin()
|
|||
contextMenuItem->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked);
|
||||
|
||||
COutPoint outpt(uint256S(contextMenuItem->data(COLUMN_ADDRESS, TxHashRole).toString().toStdString()), contextMenuItem->data(COLUMN_ADDRESS, VOutRole).toUInt());
|
||||
model->wallet().lockCoin(outpt);
|
||||
model->wallet().lockCoin(outpt, /* write_to_db = */ true);
|
||||
contextMenuItem->setDisabled(true);
|
||||
contextMenuItem->setIcon(COLUMN_CHECKBOX, platformStyle->SingleColorIcon(":/icons/lock_closed"));
|
||||
updateLabelLocked();
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||
{ "gettxoutsetinfo", 2, "use_index"},
|
||||
{ "lockunspent", 0, "unlock" },
|
||||
{ "lockunspent", 1, "transactions" },
|
||||
{ "lockunspent", 2, "persistent" },
|
||||
{ "send", 0, "outputs" },
|
||||
{ "send", 1, "conf_target" },
|
||||
{ "send", 3, "fee_rate"},
|
||||
|
|
|
|||
|
|
@ -226,15 +226,17 @@ public:
|
|||
LOCK(m_wallet->cs_wallet);
|
||||
return m_wallet->DisplayAddress(dest);
|
||||
}
|
||||
void lockCoin(const COutPoint& output) override
|
||||
bool lockCoin(const COutPoint& output, const bool write_to_db) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
return m_wallet->LockCoin(output);
|
||||
std::unique_ptr<WalletBatch> batch = write_to_db ? std::make_unique<WalletBatch>(m_wallet->GetDatabase()) : nullptr;
|
||||
return m_wallet->LockCoin(output, batch.get());
|
||||
}
|
||||
void unlockCoin(const COutPoint& output) override
|
||||
bool unlockCoin(const COutPoint& output) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
return m_wallet->UnlockCoin(output);
|
||||
std::unique_ptr<WalletBatch> batch = std::make_unique<WalletBatch>(m_wallet->GetDatabase());
|
||||
return m_wallet->UnlockCoin(output, batch.get());
|
||||
}
|
||||
bool isLockedCoin(const COutPoint& output) override
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2328,8 +2328,9 @@ static RPCHelpMan lockunspent()
|
|||
"If no transaction outputs are specified when unlocking then all current locked transaction outputs are unlocked.\n"
|
||||
"A locked transaction output will not be chosen by automatic coin selection, when spending bitcoins.\n"
|
||||
"Manually selected coins are automatically unlocked.\n"
|
||||
"Locks are stored in memory only. Nodes start with zero locked outputs, and the locked output list\n"
|
||||
"is always cleared (by virtue of process exit) when a node stops or fails.\n"
|
||||
"Locks are stored in memory only, unless persistent=true, in which case they will be written to the\n"
|
||||
"wallet database and loaded on node start. Unwritten (persistent=false) locks are always cleared\n"
|
||||
"(by virtue of process exit) when a node stops or fails. Unlocking will clear both persistent and not.\n"
|
||||
"Also see the listunspent call\n",
|
||||
{
|
||||
{"unlock", RPCArg::Type::BOOL, RPCArg::Optional::NO, "Whether to unlock (true) or lock (false) the specified transactions"},
|
||||
|
|
@ -2343,6 +2344,7 @@ static RPCHelpMan lockunspent()
|
|||
},
|
||||
},
|
||||
},
|
||||
{"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."},
|
||||
},
|
||||
RPCResult{
|
||||
RPCResult::Type::BOOL, "", "Whether the command was successful or not"
|
||||
|
|
@ -2356,6 +2358,8 @@ static RPCHelpMan lockunspent()
|
|||
+ HelpExampleCli("listlockunspent", "") +
|
||||
"\nUnlock the transaction again\n"
|
||||
+ HelpExampleCli("lockunspent", "true \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"") +
|
||||
"\nLock the transaction persistently in the wallet database\n"
|
||||
+ HelpExampleCli("lockunspent", "false \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\" true") +
|
||||
"\nAs a JSON-RPC call\n"
|
||||
+ HelpExampleRpc("lockunspent", "false, \"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":1}]\"")
|
||||
},
|
||||
|
|
@ -2374,9 +2378,13 @@ static RPCHelpMan lockunspent()
|
|||
|
||||
bool fUnlock = request.params[0].get_bool();
|
||||
|
||||
const bool persistent{request.params[2].isNull() ? false : request.params[2].get_bool()};
|
||||
|
||||
if (request.params[1].isNull()) {
|
||||
if (fUnlock)
|
||||
pwallet->UnlockAllCoins();
|
||||
if (fUnlock) {
|
||||
if (!pwallet->UnlockAllCoins())
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Unlocking coins failed");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -2427,17 +2435,24 @@ static RPCHelpMan lockunspent()
|
|||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected locked output");
|
||||
}
|
||||
|
||||
if (!fUnlock && is_locked) {
|
||||
if (!fUnlock && is_locked && !persistent) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output already locked");
|
||||
}
|
||||
|
||||
outputs.push_back(outpt);
|
||||
}
|
||||
|
||||
std::unique_ptr<WalletBatch> batch = nullptr;
|
||||
// Unlock is always persistent
|
||||
if (fUnlock || persistent) batch = std::make_unique<WalletBatch>(pwallet->GetDatabase());
|
||||
|
||||
// Atomically set (un)locked status for the outputs.
|
||||
for (const COutPoint& outpt : outputs) {
|
||||
if (fUnlock) pwallet->UnlockCoin(outpt);
|
||||
else pwallet->LockCoin(outpt);
|
||||
if (fUnlock) {
|
||||
if (!pwallet->UnlockCoin(outpt, batch.get())) throw JSONRPCError(RPC_WALLET_ERROR, "Unlocking coin failed");
|
||||
} else {
|
||||
if (!pwallet->LockCoin(outpt, batch.get())) throw JSONRPCError(RPC_WALLET_ERROR, "Locking coin failed");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -599,11 +599,16 @@ bool CWallet::IsSpent(const uint256& hash, unsigned int n) const
|
|||
return false;
|
||||
}
|
||||
|
||||
void CWallet::AddToSpends(const COutPoint& outpoint, const uint256& wtxid)
|
||||
void CWallet::AddToSpends(const COutPoint& outpoint, const uint256& wtxid, WalletBatch* batch)
|
||||
{
|
||||
mapTxSpends.insert(std::make_pair(outpoint, wtxid));
|
||||
|
||||
setLockedCoins.erase(outpoint);
|
||||
if (batch) {
|
||||
UnlockCoin(outpoint, batch);
|
||||
} else {
|
||||
WalletBatch temp_batch(GetDatabase());
|
||||
UnlockCoin(outpoint, &temp_batch);
|
||||
}
|
||||
|
||||
std::pair<TxSpends::iterator, TxSpends::iterator> range;
|
||||
range = mapTxSpends.equal_range(outpoint);
|
||||
|
|
@ -611,7 +616,7 @@ void CWallet::AddToSpends(const COutPoint& outpoint, const uint256& wtxid)
|
|||
}
|
||||
|
||||
|
||||
void CWallet::AddToSpends(const uint256& wtxid)
|
||||
void CWallet::AddToSpends(const uint256& wtxid, WalletBatch* batch)
|
||||
{
|
||||
auto it = mapWallet.find(wtxid);
|
||||
assert(it != mapWallet.end());
|
||||
|
|
@ -620,7 +625,7 @@ void CWallet::AddToSpends(const uint256& wtxid)
|
|||
return;
|
||||
|
||||
for (const CTxIn& txin : thisTx.tx->vin)
|
||||
AddToSpends(txin.prevout, wtxid);
|
||||
AddToSpends(txin.prevout, wtxid, batch);
|
||||
}
|
||||
|
||||
bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
|
||||
|
|
@ -920,7 +925,7 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const CWalletTx::Confirmatio
|
|||
wtx.nOrderPos = IncOrderPosNext(&batch);
|
||||
wtx.m_it_wtxOrdered = wtxOrdered.insert(std::make_pair(wtx.nOrderPos, &wtx));
|
||||
wtx.nTimeSmart = ComputeTimeSmart(wtx);
|
||||
AddToSpends(hash);
|
||||
AddToSpends(hash, &batch);
|
||||
}
|
||||
|
||||
if (!fInsertedNew)
|
||||
|
|
@ -2544,22 +2549,36 @@ bool CWallet::DisplayAddress(const CTxDestination& dest)
|
|||
return signer_spk_man->DisplayAddress(scriptPubKey, signer);
|
||||
}
|
||||
|
||||
void CWallet::LockCoin(const COutPoint& output)
|
||||
bool CWallet::LockCoin(const COutPoint& output, WalletBatch* batch)
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
setLockedCoins.insert(output);
|
||||
if (batch) {
|
||||
return batch->WriteLockedUTXO(output);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CWallet::UnlockCoin(const COutPoint& output)
|
||||
bool CWallet::UnlockCoin(const COutPoint& output, WalletBatch* batch)
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
setLockedCoins.erase(output);
|
||||
bool was_locked = setLockedCoins.erase(output);
|
||||
if (batch && was_locked) {
|
||||
return batch->EraseLockedUTXO(output);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CWallet::UnlockAllCoins()
|
||||
bool CWallet::UnlockAllCoins()
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
bool success = true;
|
||||
WalletBatch batch(GetDatabase());
|
||||
for (auto it = setLockedCoins.begin(); it != setLockedCoins.end(); ++it) {
|
||||
success &= batch.EraseLockedUTXO(*it);
|
||||
}
|
||||
setLockedCoins.clear();
|
||||
return success;
|
||||
}
|
||||
|
||||
bool CWallet::IsLockedCoin(uint256 hash, unsigned int n) const
|
||||
|
|
|
|||
|
|
@ -262,8 +262,8 @@ private:
|
|||
*/
|
||||
typedef std::multimap<COutPoint, uint256> TxSpends;
|
||||
TxSpends mapTxSpends GUARDED_BY(cs_wallet);
|
||||
void AddToSpends(const COutPoint& outpoint, const uint256& wtxid) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void AddToSpends(const uint256& wtxid) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void AddToSpends(const COutPoint& outpoint, const uint256& wtxid, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void AddToSpends(const uint256& wtxid, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
/**
|
||||
* Add a transaction to the wallet, or update it. pIndex and posInBlock should
|
||||
|
|
@ -476,9 +476,9 @@ public:
|
|||
bool DisplayAddress(const CTxDestination& dest) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
bool IsLockedCoin(uint256 hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void LockCoin(const COutPoint& output) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void UnlockCoin(const COutPoint& output) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void UnlockAllCoins() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
bool LockCoin(const COutPoint& output, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
bool UnlockCoin(const COutPoint& output, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
bool UnlockAllCoins() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void ListLockedCoins(std::vector<COutPoint>& vOutpts) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ const std::string FLAGS{"flags"};
|
|||
const std::string HDCHAIN{"hdchain"};
|
||||
const std::string KEYMETA{"keymeta"};
|
||||
const std::string KEY{"key"};
|
||||
const std::string LOCKED_UTXO{"lockedutxo"};
|
||||
const std::string MASTER_KEY{"mkey"};
|
||||
const std::string MINVERSION{"minversion"};
|
||||
const std::string NAME{"name"};
|
||||
|
|
@ -317,6 +318,16 @@ bool WalletBatch::WriteSpecificBlindingKey(const CScriptID& scriptid, const uint
|
|||
}
|
||||
// end ELEMENTS
|
||||
|
||||
bool WalletBatch::WriteLockedUTXO(const COutPoint& output)
|
||||
{
|
||||
return WriteIC(std::make_pair(DBKeys::LOCKED_UTXO, std::make_pair(output.hash, output.n)), uint8_t{'1'});
|
||||
}
|
||||
|
||||
bool WalletBatch::EraseLockedUTXO(const COutPoint& output)
|
||||
{
|
||||
return EraseIC(std::make_pair(DBKeys::LOCKED_UTXO, std::make_pair(output.hash, output.n)));
|
||||
}
|
||||
|
||||
class CWalletScanState {
|
||||
public:
|
||||
unsigned int nKeys{0};
|
||||
|
|
@ -778,6 +789,12 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
|
|||
|
||||
wss.m_descriptor_crypt_keys.insert(std::make_pair(std::make_pair(desc_id, pubkey.GetID()), std::make_pair(pubkey, privkey)));
|
||||
wss.fIsEncrypted = true;
|
||||
} else if (strType == DBKeys::LOCKED_UTXO) {
|
||||
uint256 hash;
|
||||
uint32_t n;
|
||||
ssKey >> hash;
|
||||
ssKey >> n;
|
||||
pwallet->LockCoin(COutPoint(hash, n));
|
||||
} else if (strType != DBKeys::BESTBLOCK && strType != DBKeys::BESTBLOCK_NOMERKLE &&
|
||||
strType != DBKeys::MINVERSION && strType != DBKeys::ACENTRY &&
|
||||
strType != DBKeys::VERSION && strType != DBKeys::SETTINGS &&
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ extern const std::string FLAGS;
|
|||
extern const std::string HDCHAIN;
|
||||
extern const std::string KEY;
|
||||
extern const std::string KEYMETA;
|
||||
extern const std::string LOCKED_UTXO;
|
||||
extern const std::string MASTER_KEY;
|
||||
extern const std::string MINVERSION;
|
||||
extern const std::string NAME;
|
||||
|
|
@ -250,6 +251,9 @@ public:
|
|||
bool WriteDescriptorLastHardenedCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index);
|
||||
bool WriteDescriptorCacheItems(const uint256& desc_id, const DescriptorCache& cache);
|
||||
|
||||
bool WriteLockedUTXO(const COutPoint& output);
|
||||
bool EraseLockedUTXO(const COutPoint& output);
|
||||
|
||||
/// Write destination data key,value tuple to database
|
||||
bool WriteDestData(const std::string &address, const std::string &key, const std::string &value);
|
||||
/// Erase destination data tuple from wallet database
|
||||
|
|
|
|||
|
|
@ -122,13 +122,49 @@ class WalletTest(BitcoinTestFramework):
|
|||
# Exercise locking of unspent outputs
|
||||
unspent_0 = self.nodes[2].listunspent()[0]
|
||||
unspent_0 = {"txid": unspent_0["txid"], "vout": unspent_0["vout"]}
|
||||
# Trying to unlock an output which isn't locked should error
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, expected locked output", self.nodes[2].lockunspent, True, [unspent_0])
|
||||
|
||||
# Locking an already-locked output should error
|
||||
self.nodes[2].lockunspent(False, [unspent_0])
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, output already locked", self.nodes[2].lockunspent, False, [unspent_0])
|
||||
|
||||
# Restarting the node should clear the lock
|
||||
self.restart_node(2)
|
||||
self.nodes[2].lockunspent(False, [unspent_0])
|
||||
|
||||
# Unloading and reloating the wallet should clear the lock
|
||||
assert_equal(self.nodes[0].listwallets(), [self.default_wallet_name])
|
||||
self.nodes[2].unloadwallet(self.default_wallet_name)
|
||||
self.nodes[2].loadwallet(self.default_wallet_name)
|
||||
assert_equal(len(self.nodes[2].listlockunspent()), 0)
|
||||
|
||||
# Locking non-persistently, then re-locking persistently, is allowed
|
||||
self.nodes[2].lockunspent(False, [unspent_0])
|
||||
self.nodes[2].lockunspent(False, [unspent_0], True)
|
||||
|
||||
# Restarting the node with the lock written to the wallet should keep the lock
|
||||
self.restart_node(2)
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, output already locked", self.nodes[2].lockunspent, False, [unspent_0])
|
||||
|
||||
# Unloading and reloading the wallet with a persistent lock should keep the lock
|
||||
self.nodes[2].unloadwallet(self.default_wallet_name)
|
||||
self.nodes[2].loadwallet(self.default_wallet_name)
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, output already locked", self.nodes[2].lockunspent, False, [unspent_0])
|
||||
|
||||
# Locked outputs should not be used, even if they are the only available funds
|
||||
assert_raises_rpc_error(-6, "Insufficient funds", self.nodes[2].sendtoaddress, self.nodes[2].getnewaddress(), 20)
|
||||
assert_equal([unspent_0], self.nodes[2].listlockunspent())
|
||||
|
||||
# Unlocking should remove the persistent lock
|
||||
self.nodes[2].lockunspent(True, [unspent_0])
|
||||
self.restart_node(2)
|
||||
assert_equal(len(self.nodes[2].listlockunspent()), 0)
|
||||
|
||||
# Reconnect node 2 after restarts
|
||||
self.connect_nodes(1, 2)
|
||||
self.connect_nodes(0, 2)
|
||||
|
||||
assert_raises_rpc_error(-8, "txid must be of length 64 (not 34, for '0000000000000000000000000000000000')",
|
||||
self.nodes[2].lockunspent, False,
|
||||
[{"txid": "0000000000000000000000000000000000", "vout": 0}])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue