Skip to content

Commit de3c46c

Browse files
committed
Merge bitcoin/bitcoin#25272: wallet: guard and alert about a wallet invalid state during chain sync
9e04cfa test: add coverage for wallet inconsistent state during sync (furszy) 77de5c6 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-ACK 9e04cfa jonatack: ACK 9e04cfa Tree-SHA512: 81f765eca40547d7764833d8ccfae686b67c7728c84271bc00dc51272de643dafc270014079dcc9727b47577ba67b340aeb5f981588b54e69a06abea6958aa96
2 parents 0043ec4 + 9e04cfa commit de3c46c

File tree

3 files changed

+117
-1
lines changed

3 files changed

+117
-1
lines changed

src/wallet/test/wallet_tests.cpp

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,5 +859,111 @@ BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup)
859859
TestUnloadWallet(std::move(wallet));
860860
}
861861

862+
/** RAII class that provides access to a FailDatabase. Which fails if needed. */
863+
class FailBatch : public DatabaseBatch
864+
{
865+
private:
866+
bool m_pass{true};
867+
bool ReadKey(CDataStream&& key, CDataStream& value) override { return m_pass; }
868+
bool WriteKey(CDataStream&& key, CDataStream&& value, bool overwrite=true) override { return m_pass; }
869+
bool EraseKey(CDataStream&& key) override { return m_pass; }
870+
bool HasKey(CDataStream&& key) override { return m_pass; }
871+
872+
public:
873+
explicit FailBatch(bool pass) : m_pass(pass) {}
874+
void Flush() override {}
875+
void Close() override {}
876+
877+
bool StartCursor() override { return true; }
878+
bool ReadAtCursor(CDataStream& ssKey, CDataStream& ssValue, bool& complete) override { return false; }
879+
void CloseCursor() override {}
880+
bool TxnBegin() override { return false; }
881+
bool TxnCommit() override { return false; }
882+
bool TxnAbort() override { return false; }
883+
};
884+
885+
/** A dummy WalletDatabase that does nothing, only fails if needed.**/
886+
class FailDatabase : public WalletDatabase
887+
{
888+
public:
889+
bool m_pass{true}; // false when this db should fail
890+
891+
void Open() override {};
892+
void AddRef() override {}
893+
void RemoveRef() override {}
894+
bool Rewrite(const char* pszSkip=nullptr) override { return true; }
895+
bool Backup(const std::string& strDest) const override { return true; }
896+
void Close() override {}
897+
void Flush() override {}
898+
bool PeriodicFlush() override { return true; }
899+
void IncrementUpdateCounter() override { ++nUpdateCounter; }
900+
void ReloadDbEnv() override {}
901+
std::string Filename() override { return "faildb"; }
902+
std::string Format() override { return "faildb"; }
903+
std::unique_ptr<DatabaseBatch> MakeBatch(bool flush_on_close = true) override { return std::make_unique<FailBatch>(m_pass); }
904+
};
905+
906+
/**
907+
* Checks a wallet invalid state where the inputs (prev-txs) of a new arriving transaction are not marked dirty,
908+
* while the transaction that spends them exist inside the in-memory wallet tx map (not stored on db due a db write failure).
909+
*/
910+
BOOST_FIXTURE_TEST_CASE(wallet_sync_tx_invalid_state_test, TestingSetup)
911+
{
912+
CWallet wallet(m_node.chain.get(), "", m_args, std::make_unique<FailDatabase>());
913+
{
914+
LOCK(wallet.cs_wallet);
915+
wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
916+
wallet.SetupDescriptorScriptPubKeyMans();
917+
}
918+
919+
// Add tx to wallet
920+
const auto& op_dest = wallet.GetNewDestination(OutputType::BECH32M, "");
921+
BOOST_ASSERT(op_dest.HasRes());
922+
const CTxDestination& dest = op_dest.GetObj();
923+
924+
CMutableTransaction mtx;
925+
mtx.vout.push_back({COIN, GetScriptForDestination(dest)});
926+
mtx.vin.push_back(CTxIn(g_insecure_rand_ctx.rand256(), 0));
927+
const auto& tx_id_to_spend = wallet.AddToWallet(MakeTransactionRef(mtx), TxStateInMempool{})->GetHash();
928+
929+
{
930+
// Cache and verify available balance for the wtx
931+
LOCK(wallet.cs_wallet);
932+
const CWalletTx* wtx_to_spend = wallet.GetWalletTx(tx_id_to_spend);
933+
BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *wtx_to_spend), 1 * COIN);
934+
}
935+
936+
// Now the good case:
937+
// 1) Add a transaction that spends the previously created transaction
938+
// 2) Verify that the available balance of this new tx and the old one is updated (prev tx is marked dirty)
939+
940+
mtx.vin.clear();
941+
mtx.vin.push_back(CTxIn(tx_id_to_spend, 0));
942+
wallet.transactionAddedToMempool(MakeTransactionRef(mtx), 0);
943+
const uint256& good_tx_id = mtx.GetHash();
944+
945+
{
946+
// Verify balance update for the new tx and the old one
947+
LOCK(wallet.cs_wallet);
948+
const CWalletTx* new_wtx = wallet.GetWalletTx(good_tx_id);
949+
BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *new_wtx), 1 * COIN);
950+
951+
// Now the old wtx
952+
const CWalletTx* wtx_to_spend = wallet.GetWalletTx(tx_id_to_spend);
953+
BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *wtx_to_spend), 0 * COIN);
954+
}
955+
956+
// Now the bad case:
957+
// 1) Make db always fail
958+
// 2) Try to add a transaction that spends the previously created transaction and
959+
// verify that we are not moving forward if the wallet cannot store it
960+
static_cast<FailDatabase&>(wallet.GetDatabase()).m_pass = false;
961+
mtx.vin.clear();
962+
mtx.vin.push_back(CTxIn(good_tx_id, 0));
963+
BOOST_CHECK_EXCEPTION(wallet.transactionAddedToMempool(MakeTransactionRef(mtx), 0),
964+
std::runtime_error,
965+
HasReason("DB error adding transaction to wallet, write failed"));
966+
}
967+
862968
BOOST_AUTO_TEST_SUITE_END()
863969
} // namespace wallet

src/wallet/wallet.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1130,7 +1130,13 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, const SyncTxS
11301130
// Block disconnection override an abandoned tx as unconfirmed
11311131
// which means user may have to call abandontransaction again
11321132
TxState tx_state = std::visit([](auto&& s) -> TxState { return s; }, state);
1133-
return AddToWallet(MakeTransactionRef(tx), tx_state, /*update_wtx=*/nullptr, /*fFlushOnClose=*/false, rescanning_old_block);
1133+
CWalletTx* wtx = AddToWallet(MakeTransactionRef(tx), tx_state, /*update_wtx=*/nullptr, /*fFlushOnClose=*/false, rescanning_old_block);
1134+
if (!wtx) {
1135+
// Can only be nullptr if there was a db write error (missing db, read-only db or a db engine internal writing error).
1136+
// As we only store arriving transaction in this process, and we don't want an inconsistent state, let's throw an error.
1137+
throw std::runtime_error("DB error adding transaction to wallet, write failed");
1138+
}
1139+
return true;
11341140
}
11351141
}
11361142
return false;

src/wallet/wallet.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,10 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
505505
//! @return true if wtx is changed and needs to be saved to disk, otherwise false
506506
using UpdateWalletTxFn = std::function<bool(CWalletTx& wtx, bool new_tx)>;
507507

508+
/**
509+
* Add the transaction to the wallet, wrapping it up inside a CWalletTx
510+
* @return the recently added wtx pointer or nullptr if there was a db write error.
511+
*/
508512
CWalletTx* AddToWallet(CTransactionRef tx, const TxState& state, const UpdateWalletTxFn& update_wtx=nullptr, bool fFlushOnClose=true, bool rescanning_old_block = false);
509513
bool LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
510514
void transactionAddedToMempool(const CTransactionRef& tx, uint64_t mempool_sequence) override;

0 commit comments

Comments
 (0)