Skip to content

Commit 9e04cfa

Browse files
committed
test: add coverage for wallet inconsistent state during sync
When a transaction arrives, the wallet mark its inputs (prev-txs) as dirty. Clearing the wallet transaction cache, triggering a balance recalculation. If this does not happen due a db write error during `AddToWallet`, the wallet will be in an invalid state: The transaction that spends certain wallet UTXO will exist inside the in-memory wallet tx map, having the credit/debit calculated, while its inputs will still have the old cached data (like if them were never spent).
1 parent 77de5c6 commit 9e04cfa

File tree

1 file changed

+106
-0
lines changed

1 file changed

+106
-0
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

0 commit comments

Comments
 (0)