diff options
author | Andrew Chow <achow101-github@achow101.com> | 2022-08-02 13:55:24 -0400 |
---|---|---|
committer | Andrew Chow <achow101-github@achow101.com> | 2022-08-02 14:06:03 -0400 |
commit | de3c46c93818c6e4000175bcdbf7dd332f54768d (patch) | |
tree | 219d3232db3e080ca65162c5e4acce5f3b0b3197 /src/wallet/test | |
parent | 0043ec4e1310e860150e5789064789377e5a6273 (diff) | |
parent | 9e04cfaa76cf9dda27f10359dd43e78dd3268e09 (diff) | |
download | bitcoin-de3c46c93818c6e4000175bcdbf7dd332f54768d.tar.xz |
Merge bitcoin/bitcoin#25272: wallet: guard and alert about a wallet invalid state during chain sync
9e04cfaa76cf9dda27f10359dd43e78dd3268e09 test: add coverage for wallet inconsistent state during sync (furszy)
77de5c693ffe8dc0afa5e40126e9b0e9cc547e04 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 9e04cfaa76cf9dda27f10359dd43e78dd3268e09
jonatack:
ACK 9e04cfaa76cf9dda27f10359dd43e78dd3268e09
Tree-SHA512: 81f765eca40547d7764833d8ccfae686b67c7728c84271bc00dc51272de643dafc270014079dcc9727b47577ba67b340aeb5f981588b54e69a06abea6958aa96
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 81183aa738..d3a760742d 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 |