mirror of
https://github.com/dashpay/dash.git
synced 2024-12-24 19:42:46 +01:00
Merge bitcoin/bitcoin#23065: Allow UTXO locks to be written to wallet DB
d96b000e94d72d041689c5c47e374df2ebc0e011 Make GUI UTXO lock/unlock persistent (Samuel Dobson) 077154fe698f5556ad6e26ef49c9024c2f07ff68 Add release note for lockunspent change (Samuel Dobson) 719ae927dcdb60c0f9902fa79796256035228c4e Update lockunspent tests for lock persistence (Samuel Dobson) f13fc16295c19a156f2974d2d73fba56d52fc161 Allow lockunspent to store the lock in the wallet DB (Samuel Dobson) c52789365e5dbcb25aa5f1775de4d318da79e5a7 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 d96b000e94d72d041689c5c47e374df2ebc0e011 kristapsk: ACK d96b000e94d72d041689c5c47e374df2ebc0e011 lsilva01: Tested ACKd96b000e94
on Ubuntu 20.04 prayank23: ACKd96b000e94
Tree-SHA512: 957a5bbfe7f763036796906ccb1598feb6c14c5975838be1ba24a198840bf59e83233165cb112cebae909b6b25bf27275a4d7fa425923ef6c788ff671d7a89a8
This commit is contained in:
parent
2d0e5d7370
commit
84deba5456
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.
|
@ -136,10 +136,10 @@ public:
|
||||
virtual bool setAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& value) = 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;
|
||||
|
@ -207,7 +207,7 @@ void CoinControlDialog::buttonToggleLockClicked()
|
||||
item->setIcon(COLUMN_CHECKBOX, QIcon());
|
||||
}
|
||||
else{
|
||||
model->wallet().lockCoin(outpt);
|
||||
model->wallet().lockCoin(outpt, /* write_to_db = */ true);
|
||||
item->setDisabled(true);
|
||||
item->setIcon(COLUMN_CHECKBOX, GUIUtil::getIcon("lock_closed", GUIUtil::ThemedColor::RED));
|
||||
}
|
||||
@ -300,7 +300,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, GUIUtil::getIcon("lock_closed", GUIUtil::ThemedColor::RED));
|
||||
updateLabelLocked();
|
||||
|
@ -152,6 +152,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"},
|
||||
|
@ -238,15 +238,17 @@ public:
|
||||
WalletBatch batch{m_wallet->GetDatabase()};
|
||||
return m_wallet->SetAddressReceiveRequest(batch, dest, id, value);
|
||||
}
|
||||
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
|
||||
{
|
||||
|
@ -2204,8 +2204,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 Dash.\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"},
|
||||
@ -2217,6 +2218,7 @@ static RPCHelpMan lockunspent()
|
||||
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"},
|
||||
},
|
||||
},
|
||||
{"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."},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -2232,6 +2234,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}]\"")
|
||||
},
|
||||
@ -2250,9 +2254,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;
|
||||
}
|
||||
|
||||
@ -2303,17 +2311,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;
|
||||
|
@ -625,12 +625,17 @@ 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));
|
||||
setWalletUTXO.erase(outpoint);
|
||||
|
||||
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);
|
||||
@ -638,7 +643,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());
|
||||
@ -647,7 +652,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)
|
||||
@ -915,7 +920,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);
|
||||
|
||||
std::vector<std::pair<const CTransactionRef&, unsigned int>> outputs;
|
||||
for(unsigned int i = 0; i < wtx.tx->vout.size(); ++i) {
|
||||
@ -4443,7 +4448,7 @@ void ReserveDestination::ReturnDestination()
|
||||
address = CNoDestination();
|
||||
}
|
||||
|
||||
void CWallet::LockCoin(const COutPoint& output)
|
||||
bool CWallet::LockCoin(const COutPoint& output, WalletBatch* batch)
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
setLockedCoins.insert(output);
|
||||
@ -4452,23 +4457,38 @@ void CWallet::LockCoin(const COutPoint& output)
|
||||
|
||||
fAnonymizableTallyCached = false;
|
||||
fAnonymizableTallyCachedNonDenom = false;
|
||||
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);
|
||||
std::map<uint256, CWalletTx>::iterator it = mapWallet.find(output.hash);
|
||||
if (it != mapWallet.end()) it->second.MarkDirty(); // recalculate all credits for this tx
|
||||
|
||||
fAnonymizableTallyCached = false;
|
||||
fAnonymizableTallyCachedNonDenom = false;
|
||||
|
||||
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
|
||||
|
@ -759,8 +759,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);
|
||||
|
||||
std::set<COutPoint> setWalletUTXO;
|
||||
mutable std::map<COutPoint, int> mapOutpointRoundsCache GUARDED_BY(cs_wallet);
|
||||
@ -1033,9 +1033,9 @@ public:
|
||||
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;
|
||||
|
||||
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);
|
||||
void ListProTxCoins(std::vector<COutPoint>& vOutpts) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
|
@ -47,6 +47,7 @@ const std::string HDCHAIN{"hdchain"};
|
||||
const std::string HDPUBKEY{"hdpubkey"};
|
||||
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"};
|
||||
@ -308,6 +309,16 @@ bool WalletBatch::WriteDescriptorCacheItems(const uint256& desc_id, const Descri
|
||||
return true;
|
||||
}
|
||||
|
||||
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};
|
||||
@ -709,6 +720,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 &&
|
||||
|
@ -74,6 +74,7 @@ extern const std::string HDCHAIN;
|
||||
extern const std::string HDPUBKEY;
|
||||
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;
|
||||
@ -219,6 +220,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
|
||||
|
@ -128,13 +128,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])
|
||||
assert_raises_rpc_error(-6, "Insufficient funds", self.nodes[2].sendtoaddress, self.nodes[2].getnewaddress(), 200)
|
||||
assert_equal([unspent_0], self.nodes[2].listlockunspent())
|
||||
self.nodes[2].lockunspent(True, [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…
Reference in New Issue
Block a user