Skip to content

Commit

Permalink
Merge bitcoin/bitcoin#30849: refactor: migrate bool GetCoin to retu…
Browse files Browse the repository at this point in the history
…rn `optional<Coin>`

4feaa28 refactor: Rely on returned value of GetCoin instead of parameter (Lőrinc)
46dfbf1 refactor: Return optional of Coin in GetCoin (Lőrinc)
e31bfb2 refactor: Remove unrealistic simulation state (Lőrinc)

Pull request description:

  While reviewing [the removal of the unreachable combinations from the Coin cache logic](bitcoin/bitcoin#30673 (comment)), we've noticed that the related tests often [reflect impossible states](https://github.com/bitcoin/bitcoin/pull/30673/files#r1740154464).

  Browsing the Coin cache refactoring history revealed that migrating `bool GetCoin` to `optional<Coin> GetCoin` was [already proposed a few times before](bitcoin/bitcoin#18746 (comment)).

  This refactor makes certain invalid states impossible, reducing the possibility of errors and making the code easier to understand. This will let us remove test code that exercises the impossible states as well.
  The PR is done in multiple small steps, first swapping the new `optional` return value, slowly strangling out the usages of the return parameter, followed by the removal of the parameter.

  Most of the invalid test states were still kept, except for https://github.com/bitcoin/bitcoin/pull/30673/files#r1748087322, where the new design prohibits invalid usage and https://github.com/bitcoin/bitcoin/pull/30673/files#r1749350258 was just marked with a TODO, will be removed in a follow-up PR.

ACKs for top commit:
  andrewtoth:
    re-ACK 4feaa28
  achow101:
    ACK 4feaa28
  laanwj:
    Code review ACK 4feaa28
  theStack:
    Code-review ACK 4feaa28

Tree-SHA512: 818d60b2e97f58c489a61120fe761fb67a08dffbefe7a3fce712d362fc9eb8c2cced23074f1bec55fe71c616a3561b5a8737919ad6ffb2635467ec4711683df7
  • Loading branch information
achow101 committed Oct 24, 2024
2 parents c16e909 + 4feaa28 commit 74fb193
Show file tree
Hide file tree
Showing 16 changed files with 99 additions and 129 deletions.
46 changes: 23 additions & 23 deletions src/coins.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,19 @@
#include <random.h>
#include <util/trace.h>

bool CCoinsView::GetCoin(const COutPoint &outpoint, Coin &coin) const { return false; }
std::optional<Coin> CCoinsView::GetCoin(const COutPoint& outpoint) const { return std::nullopt; }
uint256 CCoinsView::GetBestBlock() const { return uint256(); }
std::vector<uint256> CCoinsView::GetHeadBlocks() const { return std::vector<uint256>(); }
bool CCoinsView::BatchWrite(CoinsViewCacheCursor& cursor, const uint256 &hashBlock) { return false; }
std::unique_ptr<CCoinsViewCursor> CCoinsView::Cursor() const { return nullptr; }

bool CCoinsView::HaveCoin(const COutPoint &outpoint) const
{
Coin coin;
return GetCoin(outpoint, coin);
return GetCoin(outpoint).has_value();
}

CCoinsViewBacked::CCoinsViewBacked(CCoinsView *viewIn) : base(viewIn) { }
bool CCoinsViewBacked::GetCoin(const COutPoint &outpoint, Coin &coin) const { return base->GetCoin(outpoint, coin); }
std::optional<Coin> CCoinsViewBacked::GetCoin(const COutPoint& outpoint) const { return base->GetCoin(outpoint); }
bool CCoinsViewBacked::HaveCoin(const COutPoint &outpoint) const { return base->HaveCoin(outpoint); }
uint256 CCoinsViewBacked::GetBestBlock() const { return base->GetBestBlock(); }
std::vector<uint256> CCoinsViewBacked::GetHeadBlocks() const { return base->GetHeadBlocks(); }
Expand All @@ -45,26 +44,25 @@ size_t CCoinsViewCache::DynamicMemoryUsage() const {
CCoinsMap::iterator CCoinsViewCache::FetchCoin(const COutPoint &outpoint) const {
const auto [ret, inserted] = cacheCoins.try_emplace(outpoint);
if (inserted) {
if (!base->GetCoin(outpoint, ret->second.coin)) {
if (auto coin{base->GetCoin(outpoint)}) {
ret->second.coin = std::move(*coin);
cachedCoinsUsage += ret->second.coin.DynamicMemoryUsage();
if (ret->second.coin.IsSpent()) { // TODO GetCoin cannot return spent coins
// The parent only has an empty entry for this outpoint; we can consider our version as fresh.
ret->second.AddFlags(CCoinsCacheEntry::FRESH, *ret, m_sentinel);
}
} else {
cacheCoins.erase(ret);
return cacheCoins.end();
}
if (ret->second.coin.IsSpent()) {
// The parent only has an empty entry for this outpoint; we can consider our version as fresh.
ret->second.AddFlags(CCoinsCacheEntry::FRESH, *ret, m_sentinel);
}
cachedCoinsUsage += ret->second.coin.DynamicMemoryUsage();
}
return ret;
}

bool CCoinsViewCache::GetCoin(const COutPoint &outpoint, Coin &coin) const {
CCoinsMap::const_iterator it = FetchCoin(outpoint);
if (it != cacheCoins.end()) {
coin = it->second.coin;
return !coin.IsSpent();
}
return false;
std::optional<Coin> CCoinsViewCache::GetCoin(const COutPoint& outpoint) const
{
if (auto it{FetchCoin(outpoint)}; it != cacheCoins.end() && !it->second.coin.IsSpent()) return it->second.coin;
return std::nullopt;
}

void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possible_overwrite) {
Expand Down Expand Up @@ -363,8 +361,8 @@ const Coin& AccessByTxid(const CCoinsViewCache& view, const Txid& txid)
return coinEmpty;
}

template <typename Func>
static bool ExecuteBackedWrapper(Func func, const std::vector<std::function<void()>>& err_callbacks)
template <typename ReturnType, typename Func>
static ReturnType ExecuteBackedWrapper(Func func, const std::vector<std::function<void()>>& err_callbacks)
{
try {
return func();
Expand All @@ -381,10 +379,12 @@ static bool ExecuteBackedWrapper(Func func, const std::vector<std::function<void
}
}

bool CCoinsViewErrorCatcher::GetCoin(const COutPoint &outpoint, Coin &coin) const {
return ExecuteBackedWrapper([&]() { return CCoinsViewBacked::GetCoin(outpoint, coin); }, m_err_callbacks);
std::optional<Coin> CCoinsViewErrorCatcher::GetCoin(const COutPoint& outpoint) const
{
return ExecuteBackedWrapper<std::optional<Coin>>([&]() { return CCoinsViewBacked::GetCoin(outpoint); }, m_err_callbacks);
}

bool CCoinsViewErrorCatcher::HaveCoin(const COutPoint &outpoint) const {
return ExecuteBackedWrapper([&]() { return CCoinsViewBacked::HaveCoin(outpoint); }, m_err_callbacks);
bool CCoinsViewErrorCatcher::HaveCoin(const COutPoint& outpoint) const
{
return ExecuteBackedWrapper<bool>([&]() { return CCoinsViewBacked::HaveCoin(outpoint); }, m_err_callbacks);
}
13 changes: 5 additions & 8 deletions src/coins.h
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,8 @@ struct CoinsViewCacheCursor
class CCoinsView
{
public:
/** Retrieve the Coin (unspent transaction output) for a given outpoint.
* Returns true only when an unspent coin was found, which is returned in coin.
* When false is returned, coin's value is unspecified.
*/
virtual bool GetCoin(const COutPoint &outpoint, Coin &coin) const;
//! Retrieve the Coin (unspent transaction output) for a given outpoint.
virtual std::optional<Coin> GetCoin(const COutPoint& outpoint) const;

//! Just check whether a given outpoint is unspent.
virtual bool HaveCoin(const COutPoint &outpoint) const;
Expand Down Expand Up @@ -344,7 +341,7 @@ class CCoinsViewBacked : public CCoinsView

public:
CCoinsViewBacked(CCoinsView *viewIn);
bool GetCoin(const COutPoint &outpoint, Coin &coin) const override;
std::optional<Coin> GetCoin(const COutPoint& outpoint) const override;
bool HaveCoin(const COutPoint &outpoint) const override;
uint256 GetBestBlock() const override;
std::vector<uint256> GetHeadBlocks() const override;
Expand Down Expand Up @@ -384,7 +381,7 @@ class CCoinsViewCache : public CCoinsViewBacked
CCoinsViewCache(const CCoinsViewCache &) = delete;

// Standard CCoinsView methods
bool GetCoin(const COutPoint &outpoint, Coin &coin) const override;
std::optional<Coin> GetCoin(const COutPoint& outpoint) const override;
bool HaveCoin(const COutPoint &outpoint) const override;
uint256 GetBestBlock() const override;
void SetBestBlock(const uint256 &hashBlock);
Expand Down Expand Up @@ -514,7 +511,7 @@ class CCoinsViewErrorCatcher final : public CCoinsViewBacked
m_err_callbacks.emplace_back(std::move(f));
}

bool GetCoin(const COutPoint &outpoint, Coin &coin) const override;
std::optional<Coin> GetCoin(const COutPoint& outpoint) const override;
bool HaveCoin(const COutPoint &outpoint) const override;

private:
Expand Down
9 changes: 5 additions & 4 deletions src/node/coin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ void FindCoins(const NodeContext& node, std::map<COutPoint, Coin>& coins)
LOCK2(cs_main, node.mempool->cs);
CCoinsViewCache& chain_view = node.chainman->ActiveChainstate().CoinsTip();
CCoinsViewMemPool mempool_view(&chain_view, *node.mempool);
for (auto& coin : coins) {
if (!mempool_view.GetCoin(coin.first, coin.second)) {
// Either the coin is not in the CCoinsViewCache or is spent. Clear it.
coin.second.Clear();
for (auto& [outpoint, coin] : coins) {
if (auto c{mempool_view.GetCoin(outpoint)}) {
coin = std::move(*c);
} else {
coin.Clear(); // Either the coin is not in the CCoinsViewCache or is spent
}
}
}
Expand Down
4 changes: 1 addition & 3 deletions src/node/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -358,9 +358,7 @@ class NodeImpl : public Node
std::optional<Coin> getUnspentOutput(const COutPoint& output) override
{
LOCK(::cs_main);
Coin coin;
if (chainman().ActiveChainstate().CoinsTip().GetCoin(output, coin)) return coin;
return {};
return chainman().ActiveChainstate().CoinsTip().GetCoin(output);
}
TransactionError broadcastTransaction(CTransactionRef tx, CAmount max_tx_fee, std::string& err_string) override
{
Expand Down
7 changes: 3 additions & 4 deletions src/rest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -870,10 +870,9 @@ static bool rest_getutxos(const std::any& context, HTTPRequest* req, const std::
{
auto process_utxos = [&vOutPoints, &outs, &hits, &active_height, &active_hash, &chainman](const CCoinsView& view, const CTxMemPool* mempool) EXCLUSIVE_LOCKS_REQUIRED(chainman.GetMutex()) {
for (const COutPoint& vOutPoint : vOutPoints) {
Coin coin;
bool hit = (!mempool || !mempool->isSpent(vOutPoint)) && view.GetCoin(vOutPoint, coin);
hits.push_back(hit);
if (hit) outs.emplace_back(std::move(coin));
auto coin = !mempool || !mempool->isSpent(vOutPoint) ? view.GetCoin(vOutPoint) : std::nullopt;
hits.push_back(coin.has_value());
if (coin) outs.emplace_back(std::move(*coin));
}
active_height = chainman.ActiveHeight();
active_hash = chainman.ActiveTip()->GetBlockHash();
Expand Down
21 changes: 9 additions & 12 deletions src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1133,35 +1133,32 @@ static RPCHelpMan gettxout()
if (!request.params[2].isNull())
fMempool = request.params[2].get_bool();

Coin coin;
Chainstate& active_chainstate = chainman.ActiveChainstate();
CCoinsViewCache* coins_view = &active_chainstate.CoinsTip();

std::optional<Coin> coin;
if (fMempool) {
const CTxMemPool& mempool = EnsureMemPool(node);
LOCK(mempool.cs);
CCoinsViewMemPool view(coins_view, mempool);
if (!view.GetCoin(out, coin) || mempool.isSpent(out)) {
return UniValue::VNULL;
}
if (!mempool.isSpent(out)) coin = view.GetCoin(out);
} else {
if (!coins_view->GetCoin(out, coin)) {
return UniValue::VNULL;
}
coin = coins_view->GetCoin(out);
}
if (!coin) return UniValue::VNULL;

const CBlockIndex* pindex = active_chainstate.m_blockman.LookupBlockIndex(coins_view->GetBestBlock());
ret.pushKV("bestblock", pindex->GetBlockHash().GetHex());
if (coin.nHeight == MEMPOOL_HEIGHT) {
if (coin->nHeight == MEMPOOL_HEIGHT) {
ret.pushKV("confirmations", 0);
} else {
ret.pushKV("confirmations", (int64_t)(pindex->nHeight - coin.nHeight + 1));
ret.pushKV("confirmations", (int64_t)(pindex->nHeight - coin->nHeight + 1));
}
ret.pushKV("value", ValueFromAmount(coin.out.nValue));
ret.pushKV("value", ValueFromAmount(coin->out.nValue));
UniValue o(UniValue::VOBJ);
ScriptToUniv(coin.out.scriptPubKey, /*out=*/o, /*include_hex=*/true, /*include_address=*/true);
ScriptToUniv(coin->out.scriptPubKey, /*out=*/o, /*include_hex=*/true, /*include_address=*/true);
ret.pushKV("scriptPubKey", std::move(o));
ret.pushKV("coinbase", (bool)coin.fCoinBase);
ret.pushKV("coinbase", (bool)coin->fCoinBase);

return ret;
},
Expand Down
16 changes: 6 additions & 10 deletions src/test/coins_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,14 @@ class CCoinsViewTest : public CCoinsView
public:
CCoinsViewTest(FastRandomContext& rng) : m_rng{rng} {}

[[nodiscard]] bool GetCoin(const COutPoint& outpoint, Coin& coin) const override
std::optional<Coin> GetCoin(const COutPoint& outpoint) const override
{
std::map<COutPoint, Coin>::const_iterator it = map_.find(outpoint);
if (it == map_.end()) {
return false;
}
coin = it->second;
if (coin.IsSpent() && m_rng.randbool() == 0) {
// Randomly return false in case of an empty entry.
return false;
if (auto it{map_.find(outpoint)}; it != map_.end()) {
if (!it->second.IsSpent() || m_rng.randbool()) {
return it->second; // TODO spent coins shouldn't be returned
}
}
return true;
return std::nullopt;
}

uint256 GetBestBlock() const override { return hashBestBlock_; }
Expand Down
16 changes: 7 additions & 9 deletions src/test/fuzz/coins_view.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -162,22 +162,20 @@ FUZZ_TARGET(coins_view, .init = initialize_coins_view)
const bool exists_using_access_coin = !(coin_using_access_coin == EMPTY_COIN);
const bool exists_using_have_coin = coins_view_cache.HaveCoin(random_out_point);
const bool exists_using_have_coin_in_cache = coins_view_cache.HaveCoinInCache(random_out_point);
Coin coin_using_get_coin;
const bool exists_using_get_coin = coins_view_cache.GetCoin(random_out_point, coin_using_get_coin);
if (exists_using_get_coin) {
assert(coin_using_get_coin == coin_using_access_coin);
if (auto coin{coins_view_cache.GetCoin(random_out_point)}) {
assert(*coin == coin_using_access_coin);
assert(exists_using_access_coin && exists_using_have_coin_in_cache && exists_using_have_coin);
} else {
assert(!exists_using_access_coin && !exists_using_have_coin_in_cache && !exists_using_have_coin);
}
assert((exists_using_access_coin && exists_using_have_coin_in_cache && exists_using_have_coin && exists_using_get_coin) ||
(!exists_using_access_coin && !exists_using_have_coin_in_cache && !exists_using_have_coin && !exists_using_get_coin));
// If HaveCoin on the backend is true, it must also be on the cache if the coin wasn't spent.
const bool exists_using_have_coin_in_backend = backend_coins_view.HaveCoin(random_out_point);
if (!coin_using_access_coin.IsSpent() && exists_using_have_coin_in_backend) {
assert(exists_using_have_coin);
}
Coin coin_using_backend_get_coin;
if (backend_coins_view.GetCoin(random_out_point, coin_using_backend_get_coin)) {
if (auto coin{backend_coins_view.GetCoin(random_out_point)}) {
assert(exists_using_have_coin_in_backend);
// Note we can't assert that `coin_using_get_coin == coin_using_backend_get_coin` because the coin in
// Note we can't assert that `coin_using_get_coin == *coin` because the coin in
// the cache may have been modified but not yet flushed.
} else {
assert(!exists_using_have_coin_in_backend);
Expand Down
47 changes: 18 additions & 29 deletions src/test/fuzz/coinscache_sim.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,29 +137,20 @@ struct CacheLevel

/** Class for the base of the hierarchy (roughly simulating a memory-backed CCoinsViewDB).
*
* The initial state consists of the empty UTXO set, though coins whose output index
* is 3 (mod 5) always have GetCoin() succeed (but returning an IsSpent() coin unless a UTXO
* exists). Coins whose output index is 4 (mod 5) have GetCoin() always succeed after being spent.
* The initial state consists of the empty UTXO set.
* Coins whose output index is 4 (mod 5) have GetCoin() always succeed after being spent.
* This exercises code paths with spent, non-DIRTY cache entries.
*/
class CoinsViewBottom final : public CCoinsView
{
std::map<COutPoint, Coin> m_data;

public:
bool GetCoin(const COutPoint& outpoint, Coin& coin) const final
std::optional<Coin> GetCoin(const COutPoint& outpoint) const final
{
auto it = m_data.find(outpoint);
if (it == m_data.end()) {
if ((outpoint.n % 5) == 3) {
coin.Clear();
return true;
}
return false;
} else {
coin = it->second;
return true;
}
// TODO GetCoin shouldn't return spent coins
if (auto it = m_data.find(outpoint); it != m_data.end()) return it->second;
return std::nullopt;
}

bool HaveCoin(const COutPoint& outpoint) const final
Expand Down Expand Up @@ -270,17 +261,16 @@ FUZZ_TARGET(coinscache_sim)
// Look up in simulation data.
auto sim = lookup(outpointidx);
// Look up in real caches.
Coin realcoin;
auto real = caches.back()->GetCoin(data.outpoints[outpointidx], realcoin);
auto realcoin = caches.back()->GetCoin(data.outpoints[outpointidx]);
// Compare results.
if (!sim.has_value()) {
assert(!real || realcoin.IsSpent());
assert(!realcoin || realcoin->IsSpent());
} else {
assert(real && !realcoin.IsSpent());
assert(realcoin && !realcoin->IsSpent());
const auto& simcoin = data.coins[sim->first];
assert(realcoin.out == simcoin.out);
assert(realcoin.fCoinBase == simcoin.fCoinBase);
assert(realcoin.nHeight == sim->second);
assert(realcoin->out == simcoin.out);
assert(realcoin->fCoinBase == simcoin.fCoinBase);
assert(realcoin->nHeight == sim->second);
}
},

Expand Down Expand Up @@ -465,16 +455,15 @@ FUZZ_TARGET(coinscache_sim)

// Compare the bottom coinsview (not a CCoinsViewCache) with sim_cache[0].
for (uint32_t outpointidx = 0; outpointidx < NUM_OUTPOINTS; ++outpointidx) {
Coin realcoin;
bool real = bottom.GetCoin(data.outpoints[outpointidx], realcoin);
auto realcoin = bottom.GetCoin(data.outpoints[outpointidx]);
auto sim = lookup(outpointidx, 0);
if (!sim.has_value()) {
assert(!real || realcoin.IsSpent());
assert(!realcoin || realcoin->IsSpent());
} else {
assert(real && !realcoin.IsSpent());
assert(realcoin.out == data.coins[sim->first].out);
assert(realcoin.fCoinBase == data.coins[sim->first].fCoinBase);
assert(realcoin.nHeight == sim->second);
assert(realcoin && !realcoin->IsSpent());
assert(realcoin->out == data.coins[sim->first].out);
assert(realcoin->fCoinBase == data.coins[sim->first].fCoinBase);
assert(realcoin->nHeight == sim->second);
}
}
}
5 changes: 2 additions & 3 deletions src/test/fuzz/tx_pool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,8 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool)
// Helper to query an amount
const CCoinsViewMemPool amount_view{WITH_LOCK(::cs_main, return &chainstate.CoinsTip()), tx_pool};
const auto GetAmount = [&](const COutPoint& outpoint) {
Coin c;
Assert(amount_view.GetCoin(outpoint, c));
return c.out.nValue;
auto coin{amount_view.GetCoin(outpoint).value()};
return coin.out.nValue;
};

LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 300)
Expand Down
5 changes: 2 additions & 3 deletions src/test/util/setup_common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -433,9 +433,8 @@ std::pair<CMutableTransaction, CAmount> TestChain100Setup::CreateValidTransactio
std::map<COutPoint, Coin> input_coins;
CAmount inputs_amount{0};
for (const auto& outpoint_to_spend : inputs) {
// - Use GetCoin to properly populate utxo_to_spend,
Coin utxo_to_spend;
assert(coins_cache.GetCoin(outpoint_to_spend, utxo_to_spend));
// Use GetCoin to properly populate utxo_to_spend
auto utxo_to_spend{coins_cache.GetCoin(outpoint_to_spend).value()};
input_coins.insert({outpoint_to_spend, utxo_to_spend});
inputs_amount += utxo_to_spend.out.nValue;
}
Expand Down
6 changes: 4 additions & 2 deletions src/txdb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ void CCoinsViewDB::ResizeCache(size_t new_cache_size)
}
}

bool CCoinsViewDB::GetCoin(const COutPoint &outpoint, Coin &coin) const {
return m_db->Read(CoinEntry(&outpoint), coin);
std::optional<Coin> CCoinsViewDB::GetCoin(const COutPoint& outpoint) const
{
if (Coin coin; m_db->Read(CoinEntry(&outpoint), coin)) return coin;
return std::nullopt;
}

bool CCoinsViewDB::HaveCoin(const COutPoint &outpoint) const {
Expand Down
Loading

0 comments on commit 74fb193

Please sign in to comment.