mirror of
https://github.com/Retropex/bitcoin.git
synced 2025-05-28 21:12:29 +02:00
Merge bitcoin/bitcoin#25272: wallet: guard and alert about a wallet invalid state during chain sync
9e04cfaa76
test: add coverage for wallet inconsistent state during sync (furszy)77de5c693f
wallet: guard and alert about a wallet invalid state during chain sync (furszy) Pull request description: Follow-up work to my comment in #25239. Guarding and alerting the user about a wallet invalid state during chain synchronization. #### Explanation if the `AddToWallet` tx write fails, the method returns a wtx `nullptr` without removing the recently added transaction from the wallet's map. Which makes that `AddToWalletIfInvolvingMe` return false (even when the tx is on the wallet's map already), --> which makes `SyncTransaction` skip the `MarkInputsDirty` call --> which leads to a wallet invalid state where the inputs of this new transaction are not marked dirty, while the transaction that spends them still exist on the in-memory wallet tx map. Plus, as we only store the arriving transaction inside `AddToWalletIfInvolvingMe` when we synchronize/scan block/s from the chain and nowhere else, it makes sense to treat the transaction db write error as a runtime error to notify the user about the problem. Otherwise, the user will lose all the not stored transactions after a wallet shutdown (without be able to recover them automatically on the next startup because the chain sync would be above the block where the txs arrived). Note: On purpose, the first commit adds test coverage for it. Showing how the wallet can end up in an invalid state. The second commit corrects it with the proposed solution. ACKs for top commit: achow101: re-ACK9e04cfaa76
jonatack: ACK9e04cfaa76
Tree-SHA512: 81f765eca40547d7764833d8ccfae686b67c7728c84271bc00dc51272de643dafc270014079dcc9727b47577ba67b340aeb5f981588b54e69a06abea6958aa96
This commit is contained in:
commit
de3c46c938
@ -859,5 +859,111 @@ BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup)
|
||||
TestUnloadWallet(std::move(wallet));
|
||||
}
|
||||
|
||||
/** RAII class that provides access to a FailDatabase. Which fails if needed. */
|
||||
class FailBatch : public DatabaseBatch
|
||||
{
|
||||
private:
|
||||
bool m_pass{true};
|
||||
bool ReadKey(CDataStream&& key, CDataStream& value) override { return m_pass; }
|
||||
bool WriteKey(CDataStream&& key, CDataStream&& value, bool overwrite=true) override { return m_pass; }
|
||||
bool EraseKey(CDataStream&& key) override { return m_pass; }
|
||||
bool HasKey(CDataStream&& key) override { return m_pass; }
|
||||
|
||||
public:
|
||||
explicit FailBatch(bool pass) : m_pass(pass) {}
|
||||
void Flush() override {}
|
||||
void Close() override {}
|
||||
|
||||
bool StartCursor() override { return true; }
|
||||
bool ReadAtCursor(CDataStream& ssKey, CDataStream& ssValue, bool& complete) override { return false; }
|
||||
void CloseCursor() override {}
|
||||
bool TxnBegin() override { return false; }
|
||||
bool TxnCommit() override { return false; }
|
||||
bool TxnAbort() override { return false; }
|
||||
};
|
||||
|
||||
/** A dummy WalletDatabase that does nothing, only fails if needed.**/
|
||||
class FailDatabase : public WalletDatabase
|
||||
{
|
||||
public:
|
||||
bool m_pass{true}; // false when this db should fail
|
||||
|
||||
void Open() override {};
|
||||
void AddRef() override {}
|
||||
void RemoveRef() override {}
|
||||
bool Rewrite(const char* pszSkip=nullptr) override { return true; }
|
||||
bool Backup(const std::string& strDest) const override { return true; }
|
||||
void Close() override {}
|
||||
void Flush() override {}
|
||||
bool PeriodicFlush() override { return true; }
|
||||
void IncrementUpdateCounter() override { ++nUpdateCounter; }
|
||||
void ReloadDbEnv() override {}
|
||||
std::string Filename() override { return "faildb"; }
|
||||
std::string Format() override { return "faildb"; }
|
||||
std::unique_ptr<DatabaseBatch> MakeBatch(bool flush_on_close = true) override { return std::make_unique<FailBatch>(m_pass); }
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks a wallet invalid state where the inputs (prev-txs) of a new arriving transaction are not marked dirty,
|
||||
* while the transaction that spends them exist inside the in-memory wallet tx map (not stored on db due a db write failure).
|
||||
*/
|
||||
BOOST_FIXTURE_TEST_CASE(wallet_sync_tx_invalid_state_test, TestingSetup)
|
||||
{
|
||||
CWallet wallet(m_node.chain.get(), "", m_args, std::make_unique<FailDatabase>());
|
||||
{
|
||||
LOCK(wallet.cs_wallet);
|
||||
wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
|
||||
wallet.SetupDescriptorScriptPubKeyMans();
|
||||
}
|
||||
|
||||
// Add tx to wallet
|
||||
const auto& op_dest = wallet.GetNewDestination(OutputType::BECH32M, "");
|
||||
BOOST_ASSERT(op_dest.HasRes());
|
||||
const CTxDestination& dest = op_dest.GetObj();
|
||||
|
||||
CMutableTransaction mtx;
|
||||
mtx.vout.push_back({COIN, GetScriptForDestination(dest)});
|
||||
mtx.vin.push_back(CTxIn(g_insecure_rand_ctx.rand256(), 0));
|
||||
const auto& tx_id_to_spend = wallet.AddToWallet(MakeTransactionRef(mtx), TxStateInMempool{})->GetHash();
|
||||
|
||||
{
|
||||
// Cache and verify available balance for the wtx
|
||||
LOCK(wallet.cs_wallet);
|
||||
const CWalletTx* wtx_to_spend = wallet.GetWalletTx(tx_id_to_spend);
|
||||
BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *wtx_to_spend), 1 * COIN);
|
||||
}
|
||||
|
||||
// Now the good case:
|
||||
// 1) Add a transaction that spends the previously created transaction
|
||||
// 2) Verify that the available balance of this new tx and the old one is updated (prev tx is marked dirty)
|
||||
|
||||
mtx.vin.clear();
|
||||
mtx.vin.push_back(CTxIn(tx_id_to_spend, 0));
|
||||
wallet.transactionAddedToMempool(MakeTransactionRef(mtx), 0);
|
||||
const uint256& good_tx_id = mtx.GetHash();
|
||||
|
||||
{
|
||||
// Verify balance update for the new tx and the old one
|
||||
LOCK(wallet.cs_wallet);
|
||||
const CWalletTx* new_wtx = wallet.GetWalletTx(good_tx_id);
|
||||
BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *new_wtx), 1 * COIN);
|
||||
|
||||
// Now the old wtx
|
||||
const CWalletTx* wtx_to_spend = wallet.GetWalletTx(tx_id_to_spend);
|
||||
BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *wtx_to_spend), 0 * COIN);
|
||||
}
|
||||
|
||||
// Now the bad case:
|
||||
// 1) Make db always fail
|
||||
// 2) Try to add a transaction that spends the previously created transaction and
|
||||
// verify that we are not moving forward if the wallet cannot store it
|
||||
static_cast<FailDatabase&>(wallet.GetDatabase()).m_pass = false;
|
||||
mtx.vin.clear();
|
||||
mtx.vin.push_back(CTxIn(good_tx_id, 0));
|
||||
BOOST_CHECK_EXCEPTION(wallet.transactionAddedToMempool(MakeTransactionRef(mtx), 0),
|
||||
std::runtime_error,
|
||||
HasReason("DB error adding transaction to wallet, write failed"));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
} // namespace wallet
|
||||
|
@ -1130,7 +1130,13 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, const SyncTxS
|
||||
// Block disconnection override an abandoned tx as unconfirmed
|
||||
// which means user may have to call abandontransaction again
|
||||
TxState tx_state = std::visit([](auto&& s) -> TxState { return s; }, state);
|
||||
return AddToWallet(MakeTransactionRef(tx), tx_state, /*update_wtx=*/nullptr, /*fFlushOnClose=*/false, rescanning_old_block);
|
||||
CWalletTx* wtx = AddToWallet(MakeTransactionRef(tx), tx_state, /*update_wtx=*/nullptr, /*fFlushOnClose=*/false, rescanning_old_block);
|
||||
if (!wtx) {
|
||||
// Can only be nullptr if there was a db write error (missing db, read-only db or a db engine internal writing error).
|
||||
// As we only store arriving transaction in this process, and we don't want an inconsistent state, let's throw an error.
|
||||
throw std::runtime_error("DB error adding transaction to wallet, write failed");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
@ -505,6 +505,10 @@ public:
|
||||
//! @return true if wtx is changed and needs to be saved to disk, otherwise false
|
||||
using UpdateWalletTxFn = std::function<bool(CWalletTx& wtx, bool new_tx)>;
|
||||
|
||||
/**
|
||||
* Add the transaction to the wallet, wrapping it up inside a CWalletTx
|
||||
* @return the recently added wtx pointer or nullptr if there was a db write error.
|
||||
*/
|
||||
CWalletTx* AddToWallet(CTransactionRef tx, const TxState& state, const UpdateWalletTxFn& update_wtx=nullptr, bool fFlushOnClose=true, bool rescanning_old_block = false);
|
||||
bool LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void transactionAddedToMempool(const CTransactionRef& tx, uint64_t mempool_sequence) override;
|
||||
|
Loading…
Reference in New Issue
Block a user