Skip to content

Commit

Permalink
Merge #21206: refactor: Make CWalletTx sync state type-safe
Browse files Browse the repository at this point in the history
d8ee8f3 refactor: Make CWalletTx sync state type-safe (Russell Yanofsky)

Pull request description:

  Current `CWalletTx` state representation makes it possible to set inconsistent states that won't be handled correctly by wallet sync code or serialized & deserialized back into the same form.

  For example, it is possible to call `setConflicted` without setting a conflicting block hash, or `setConfirmed` with no transaction index. And it's possible update individual `m_confirm` and `fInMempool` data fields without setting an overall consistent state that can be serialized and handled correctly.

  Fix this without changing behavior by using `std::variant`, instead of an enum and collection of fields, to represent sync state, so state tracking code is safer and more legible.

  This is a first step to fixing state tracking bugs https://github.com/bitcoin-core/bitcoin-devwiki/wiki/Wallet-Transaction-Conflict-Tracking, by adding an extra margin of safety that can prevent new bugs from being introduced as existing bugs are fixed.

ACKs for top commit:
  laanwj:
    re-ACK d8ee8f3
  jonatack:
    Code review ACK d8ee8f3

Tree-SHA512: b9f15e9d99dbdbdd3ef7a76764e11f66949f50e6227e284126f209e4cb106af6d55e9a9e8c7d4aa216ddc92c6d5acc6f4aa4746f209bbd77f03831b51a2841c3
  • Loading branch information
laanwj committed Nov 25, 2021
2 parents 681b25e + d8ee8f3 commit cf24152
Show file tree
Hide file tree
Showing 16 changed files with 257 additions and 174 deletions.
1 change: 1 addition & 0 deletions src/Makefile.am
Expand Up @@ -246,6 +246,7 @@ BITCOIN_CORE_H = \
util/macros.h \
util/message.h \
util/moneystr.h \
util/overloaded.h \
util/rbf.h \
util/readwritefile.h \
util/serfloat.h \
Expand Down
1 change: 1 addition & 0 deletions src/Makefile.test.include
Expand Up @@ -161,6 +161,7 @@ BITCOIN_TESTS += \
wallet/test/wallet_tests.cpp \
wallet/test/walletdb_tests.cpp \
wallet/test/wallet_crypto_tests.cpp \
wallet/test/wallet_transaction_tests.cpp \
wallet/test/coinselector_tests.cpp \
wallet/test/init_tests.cpp \
wallet/test/ismine_tests.cpp \
Expand Down
2 changes: 1 addition & 1 deletion src/bench/coin_selection.cpp
Expand Up @@ -18,7 +18,7 @@ static void addCoin(const CAmount& nValue, const CWallet& wallet, std::vector<st
tx.nLockTime = nextLockTime++; // so all transactions get different hashes
tx.vout.resize(1);
tx.vout[0].nValue = nValue;
wtxs.push_back(std::make_unique<CWalletTx>(MakeTransactionRef(std::move(tx))));
wtxs.push_back(std::make_unique<CWalletTx>(MakeTransactionRef(std::move(tx)), TxStateInactive{}));
}

// Simple benchmark for wallet coin selection. Note that it maybe be necessary
Expand Down
22 changes: 22 additions & 0 deletions src/util/overloaded.h
@@ -0,0 +1,22 @@
// Copyright (c) 2021 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#ifndef BITCOIN_UTIL_OVERLOADED_H
#define BITCOIN_UTIL_OVERLOADED_H

namespace util {
//! Overloaded helper for std::visit. This helper and std::visit in general are
//! useful to write code that switches on a variant type. Unlike if/else-if and
//! switch/case statements, std::visit will trigger compile errors if there are
//! unhandled cases.
//!
//! Implementation comes from and example usage can be found at
//! https://en.cppreference.com/w/cpp/utility/variant/visit#Example
template<class... Ts> struct Overloaded : Ts... { using Ts::operator()...; };

//! Explicit deduction guide (not needed as of C++20)
template<class... Ts> Overloaded(Ts...) -> Overloaded<Ts...>;
} // namespace util

#endif // BITCOIN_UTIL_OVERLOADED_H
5 changes: 4 additions & 1 deletion src/wallet/interfaces.cpp
Expand Up @@ -82,7 +82,10 @@ WalletTx MakeWalletTx(CWallet& wallet, const CWalletTx& wtx)
WalletTxStatus MakeWalletTxStatus(const CWallet& wallet, const CWalletTx& wtx)
{
WalletTxStatus result;
result.block_height = wtx.m_confirm.block_height > 0 ? wtx.m_confirm.block_height : std::numeric_limits<int>::max();
result.block_height =
wtx.state<TxStateConfirmed>() ? wtx.state<TxStateConfirmed>()->confirmed_block_height :
wtx.state<TxStateConflicted>() ? wtx.state<TxStateConflicted>()->conflicting_block_height :
std::numeric_limits<int>::max();
result.blocks_to_maturity = wallet.GetTxBlocksToMaturity(wtx);
result.depth_in_main_chain = wallet.GetTxDepthInMainChain(wtx);
result.time_received = wtx.nTimeReceived;
Expand Down
4 changes: 1 addition & 3 deletions src/wallet/rpcdump.cpp
Expand Up @@ -369,11 +369,9 @@ RPCHelpMan importprunedfunds()

unsigned int txnIndex = vIndex[it - vMatch.begin()];

CWalletTx::Confirmation confirm(CWalletTx::Status::CONFIRMED, height, merkleBlock.header.GetHash(), txnIndex);

CTransactionRef tx_ref = MakeTransactionRef(tx);
if (pwallet->IsMine(*tx_ref)) {
pwallet->AddToWallet(std::move(tx_ref), confirm);
pwallet->AddToWallet(std::move(tx_ref), TxStateConfirmed{merkleBlock.header.GetHash(), height, static_cast<int>(txnIndex)});
return NullUniValue;
}

Expand Down
10 changes: 5 additions & 5 deletions src/wallet/rpcwallet.cpp
Expand Up @@ -166,13 +166,13 @@ static void WalletTxToJSON(const CWallet& wallet, const CWalletTx& wtx, UniValue
entry.pushKV("confirmations", confirms);
if (wtx.IsCoinBase())
entry.pushKV("generated", true);
if (confirms > 0)
if (auto* conf = wtx.state<TxStateConfirmed>())
{
entry.pushKV("blockhash", wtx.m_confirm.hashBlock.GetHex());
entry.pushKV("blockheight", wtx.m_confirm.block_height);
entry.pushKV("blockindex", wtx.m_confirm.nIndex);
entry.pushKV("blockhash", conf->confirmed_block_hash.GetHex());
entry.pushKV("blockheight", conf->confirmed_block_height);
entry.pushKV("blockindex", conf->position_in_block);
int64_t block_time;
CHECK_NONFATAL(chain.findBlock(wtx.m_confirm.hashBlock, FoundBlock().time(block_time)));
CHECK_NONFATAL(chain.findBlock(conf->confirmed_block_hash, FoundBlock().time(block_time)));
entry.pushKV("blocktime", block_time);
} else {
entry.pushKV("trusted", CachedTxIsTrusted(wallet, wtx));
Expand Down
2 changes: 1 addition & 1 deletion src/wallet/test/coinselector_tests.cpp
Expand Up @@ -74,7 +74,7 @@ static void add_coin(std::vector<COutput>& coins, CWallet& wallet, const CAmount
uint256 txid = tx.GetHash();

LOCK(wallet.cs_wallet);
auto ret = wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(txid), std::forward_as_tuple(MakeTransactionRef(std::move(tx))));
auto ret = wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(txid), std::forward_as_tuple(MakeTransactionRef(std::move(tx)), TxStateInactive{}));
assert(ret.second);
CWalletTx& wtx = (*ret.first).second;
if (fIsFromMe)
Expand Down
4 changes: 2 additions & 2 deletions src/wallet/test/psbt_wallet_tests.cpp
Expand Up @@ -34,12 +34,12 @@ BOOST_AUTO_TEST_CASE(psbt_updater_test)
CDataStream s_prev_tx1(ParseHex("0200000000010158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88702483045022100a22edcc6e5bc511af4cc4ae0de0fcd75c7e04d8c1c3a8aa9d820ed4b967384ec02200642963597b9b1bc22c75e9f3e117284a962188bf5e8a74c895089046a20ad770121035509a48eb623e10aace8bfd0212fdb8a8e5af3c94b0b133b95e114cab89e4f7965000000"), SER_NETWORK, PROTOCOL_VERSION);
CTransactionRef prev_tx1;
s_prev_tx1 >> prev_tx1;
m_wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(prev_tx1->GetHash()), std::forward_as_tuple(prev_tx1));
m_wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(prev_tx1->GetHash()), std::forward_as_tuple(prev_tx1, TxStateInactive{}));

CDataStream s_prev_tx2(ParseHex("0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000"), SER_NETWORK, PROTOCOL_VERSION);
CTransactionRef prev_tx2;
s_prev_tx2 >> prev_tx2;
m_wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(prev_tx2->GetHash()), std::forward_as_tuple(prev_tx2));
m_wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(prev_tx2->GetHash()), std::forward_as_tuple(prev_tx2, TxStateInactive{}));

// Import descriptors for keys and scripts
import_descriptor(m_wallet, "sh(multi(2,xprv9s21ZrQH143K2LE7W4Xf3jATf9jECxSb7wj91ZnmY4qEJrS66Qru9RFqq8xbkgT32ya6HqYJweFdJUEDf5Q6JFV7jMiUws7kQfe6Tv4RbfN/0h/0h/0h,xprv9s21ZrQH143K2LE7W4Xf3jATf9jECxSb7wj91ZnmY4qEJrS66Qru9RFqq8xbkgT32ya6HqYJweFdJUEDf5Q6JFV7jMiUws7kQfe6Tv4RbfN/0h/0h/1h))");
Expand Down
22 changes: 9 additions & 13 deletions src/wallet/test/wallet_tests.cpp
Expand Up @@ -330,17 +330,14 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup)
BOOST_FIXTURE_TEST_CASE(coin_mark_dirty_immature_credit, TestChain100Setup)
{
CWallet wallet(m_node.chain.get(), "", m_args, CreateDummyWalletDatabase());
CWalletTx wtx(m_coinbase_txns.back());
CWalletTx wtx{m_coinbase_txns.back(), TxStateConfirmed{m_node.chainman->ActiveChain().Tip()->GetBlockHash(), m_node.chainman->ActiveChain().Height(), /*position_in_block=*/0}};

LOCK(wallet.cs_wallet);
wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
wallet.SetupDescriptorScriptPubKeyMans();

wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());

CWalletTx::Confirmation confirm(CWalletTx::Status::CONFIRMED, m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash(), 0);
wtx.m_confirm = confirm;

// Call GetImmatureCredit() once before adding the key to the wallet to
// cache the current immature credit amount, which is 0.
BOOST_CHECK_EQUAL(CachedTxGetImmatureCredit(wallet, wtx), 0);
Expand All @@ -355,7 +352,7 @@ BOOST_FIXTURE_TEST_CASE(coin_mark_dirty_immature_credit, TestChain100Setup)
static int64_t AddTx(ChainstateManager& chainman, CWallet& wallet, uint32_t lockTime, int64_t mockTime, int64_t blockTime)
{
CMutableTransaction tx;
CWalletTx::Confirmation confirm;
TxState state = TxStateInactive{};
tx.nLockTime = lockTime;
SetMockTime(mockTime);
CBlockIndex* block = nullptr;
Expand All @@ -367,13 +364,13 @@ static int64_t AddTx(ChainstateManager& chainman, CWallet& wallet, uint32_t lock
block = inserted.first->second;
block->nTime = blockTime;
block->phashBlock = &hash;
confirm = {CWalletTx::Status::CONFIRMED, block->nHeight, hash, 0};
state = TxStateConfirmed{hash, block->nHeight, /*position_in_block=*/0};
}

// If transaction is already in map, to avoid inconsistencies, unconfirmation
// is needed before confirm again with different block.
return wallet.AddToWallet(MakeTransactionRef(tx), confirm, [&](CWalletTx& wtx, bool /* new_tx */) {
wtx.setUnconfirmed();
return wallet.AddToWallet(MakeTransactionRef(tx), state, [&](CWalletTx& wtx, bool /* new_tx */) {
// Assign wtx.m_state to simplify test and avoid the need to simulate
// reorg events. Without this, AddToWallet asserts false when the same
// transaction is confirmed in different blocks.
wtx.m_state = state;
return true;
})->nTimeSmart;
}
Expand Down Expand Up @@ -534,8 +531,7 @@ class ListCoinsTestingSetup : public TestChain100Setup
wallet->SetLastBlockProcessed(wallet->GetLastBlockHeight() + 1, m_node.chainman->ActiveChain().Tip()->GetBlockHash());
auto it = wallet->mapWallet.find(tx->GetHash());
BOOST_CHECK(it != wallet->mapWallet.end());
CWalletTx::Confirmation confirm(CWalletTx::Status::CONFIRMED, m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash(), 1);
it->second.m_confirm = confirm;
it->second.m_state = TxStateConfirmed{m_node.chainman->ActiveChain().Tip()->GetBlockHash(), m_node.chainman->ActiveChain().Height(), /*position_in_block=*/1};
return it->second;
}

Expand Down
24 changes: 24 additions & 0 deletions src/wallet/test/wallet_transaction_tests.cpp
@@ -0,0 +1,24 @@
// Copyright (c) 2021 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <wallet/transaction.h>

#include <wallet/test/wallet_test_fixture.h>

#include <boost/test/unit_test.hpp>

BOOST_FIXTURE_TEST_SUITE(wallet_transaction_tests, WalletTestingSetup)

BOOST_AUTO_TEST_CASE(roundtrip)
{
for (uint8_t hash = 0; hash < 5; ++hash) {
for (int index = -2; index < 3; ++index) {
TxState state = TxStateInterpretSerialized(TxStateUnrecognized{uint256{hash}, index});
BOOST_CHECK_EQUAL(TxStateSerializedBlockHash(state), uint256{hash});
BOOST_CHECK_EQUAL(TxStateSerializedIndex(state), index);
}
}
}

BOOST_AUTO_TEST_SUITE_END()
2 changes: 1 addition & 1 deletion src/wallet/transaction.cpp
Expand Up @@ -15,7 +15,7 @@ bool CWalletTx::IsEquivalentTo(const CWalletTx& _tx) const

bool CWalletTx::InMempool() const
{
return fInMempool;
return state<TxStateInMempool>();
}

int64_t CWalletTx::GetTxTime() const
Expand Down

0 comments on commit cf24152

Please sign in to comment.