diff options
author | furszy <matiasfurszyfer@protonmail.com> | 2022-06-03 12:26:56 -0300 |
---|---|---|
committer | furszy <matiasfurszyfer@protonmail.com> | 2022-07-18 12:04:48 -0300 |
commit | 9e04cfaa76cf9dda27f10359dd43e78dd3268e09 (patch) | |
tree | 3004e185800124d06ddcb0111f548b5162cd417b /src/wallet/test | |
parent | 77de5c693ffe8dc0afa5e40126e9b0e9cc547e04 (diff) | |
download | bitcoin-9e04cfaa76cf9dda27f10359dd43e78dd3268e09.tar.xz |
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).
Diffstat (limited to 'src/wallet/test')
-rw-r--r-- | src/wallet/test/wallet_tests.cpp | 106 |
1 files changed, 106 insertions, 0 deletions
diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 81c01e5023..744ac99b4b 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -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 |