-
Notifications
You must be signed in to change notification settings - Fork 37.7k
coins: remove logic for spent-and-FRESH cache entries and writing non-DIRTY entries #30673
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code Coverage & BenchmarksFor details see: https://corecheck.dev/bitcoin/bitcoin/pulls/30673. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
3f032cf
to
b393b0d
Compare
🚧 At least one of the CI tasks failed. HintsMake sure to run all tests locally, according to the documentation. The failure may happen due to a number of reasons, for example:
Leave a comment here, if you need help tracking down a confusing failure. |
b393b0d
to
feaea74
Compare
feaea74
to
1f2f8e5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code became a lot simpler now - have a few suggestion I'd like us to consider, but I understand if you think it's out of scope.
Once we get to a state that is ACK-worthy, I'll enable hard assertions everywhere and run an IBD and check if it crashes anywhere.
I think for a change like this, spending time fuzzing would be more valuable. I believe it already runs in debug mode for that so |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dug deeper and added more relevant questions - bear with me if they're outside the scope of this PR.
82b6263
to
7d2b499
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The unreliability of many of our tests worries me, would you be open to fixing them in a PR before this one?
// we would remove it from this cache and would never flush spentness | ||
// to the parent cache. | ||
// A spent FRESH coin cannot exist in the cache because a FRESH coin | ||
// is simply erased when it is spent. | ||
// | ||
// Re-adding a spent coin can happen in the case of a re-org (the coin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this still happen during a reorg? The comment here is really scary, do we really need so much context here? I'd prefer a test that fails when the code is wrong instead of a really long description...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it can happen during a reorg. But we know we can't have a spent coin that is not DIRTY, so the comments being updated here are making this not be redundant. When the coin is spent in DisconnectBlock
, it is either erased if FRESH or set DIRTY. The only time we would get into this block if the coin is not DIRTY is if it was just inserted.
// DIRTY, then it can be marked FRESH. | ||
fresh = !it->second.IsDirty(); | ||
// If the coin doesn't exist in the current cache then it can be marked FRESH. | ||
fresh = inserted; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The freshness flag depend on inserted
, possible_overwrite
and it->second.coin.IsSpent()
, assigned and validated in different places.
Could we obviate the freshness flag calculation instead with something like:
bool fresh_flag = inserted && !possible_overwrite ? CCoinsCacheEntry::FRESH : 0;
Either here or in a follow-up we could simplify & modernize the method to something like:
void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possible_overwrite) {
assert(!coin.IsSpent());
if (coin.out.scriptPubKey.IsUnspendable()) return;
auto [it, inserted] = cacheCoins.try_emplace(outpoint);
if (!inserted) {
if (!possible_overwrite && !it->second.coin.IsSpent()) throw std::logic_error("Attempted to overwrite an unspent coin (when possible_overwrite is false)");
cachedCoinsUsage -= it->second.coin.DynamicMemoryUsage();
}
cachedCoinsUsage += coin.DynamicMemoryUsage();
it->second.coin = std::move(coin);
bool fresh_flag = inserted && !possible_overwrite ? CCoinsCacheEntry::FRESH : 0;
it->second.AddFlags(CCoinsCacheEntry::DIRTY | fresh_flag, *it, m_sentinel);
TRACE5(utxocache, add,
outpoint.hash.data(),
(uint32_t) outpoint.n,
(uint32_t) it->second.coin.nHeight,
(int64_t) it->second.coin.out.nValue,
(bool) it->second.coin.IsCoinBase());
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's leave this for a follow-up.
src/test/coins_tests.cpp
Outdated
@@ -793,7 +787,7 @@ BOOST_AUTO_TEST_CASE(ccoins_add) | |||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unrelated: could coins_cache_simulation_test
be moved into a fuzz test - or would it be too difficult to track all those states?.
If you think any of these recommendations can be done in a parallel PR, let me know and I'll do them myself before this is merged
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure. It might just be redundant to our current fuzz harness. It's also valuable to have these tests run on every CI.
7d2b499
to
34a101d
Compare
@@ -311,12 +311,13 @@ void CCoinsViewCache::SanityCheck() const | |||
size_t recomputed_usage = 0; | |||
size_t count_flagged = 0; | |||
for (const auto& [_, entry] : cacheCoins) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to me this can also be merged with the related SanityCheck commit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I merged it with aaef2d6 instead.
This is still important - @andrewtoth, how can I help? |
6956ee9
to
7f4a3bf
Compare
🚧 At least one of the CI tasks failed. HintsTry to run the tests locally, according to the documentation. However, a CI failure may still
Leave a comment here, if you need help tracking down a confusing failure. |
There are no code paths which add a non-DIRTY entry to the cursor in BatchWrite, so we can safely make this a logic error and test for it. There are no code paths which add a spent and FRESH coin to the cursor in BatchWrite, so we can safely make this a logic error and test for it.
It is no longer possible to get non-DIRTY entries in BatchWrite
It is not possible for an entry to be FRESH if not already DIRTY.
A spent coins must be DIRTY, so remove references to spent but not DIRTY coins. The only way a spent coin can be not DIRTY is when creating the CCoinsCacheEntry with an empty coin. This can be made more clear by setting fresh if inserted, instead of checking if an unspent coin is not DIRTY.
7f4a3bf
to
53dbebd
Compare
@l0rinc rebased. Thank you for your reviews! I have tried to address all your comments. There are some older comments that I'm unsure are still relevant. Please let me know if there is still anything outstanding that needs to be addressed. |
It is not possible to have a FRESH CCoinsCacheEntry that is not also DIRTY. Test code uses the SetFresh method out of order to simulate entries that are FRESH but not DIRTY. By removing the method entirely we can simplify the test code and make the production code easier to understand.
5f4095f
to
fd2338d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The src/coins.cpp
prod changes are a bit scary - while it's a very important change, based on the lack of enthusiasm by others, we may want to separate the tests and small refactors into PRs that will get us closer to this.
One could remove the invalid test states - without removing the invalid production code states, just potentially adding TODOs there in case it's missing.
The SetFresh
and flag removal could be next and if all of those are merged, we can continue with the scary FetchCoin
, AddCoin
, BatchWrite
and Uncache
.
Left a few new comments quickly, let me know what you think.
@@ -148,14 +146,13 @@ class CoinsViewBottom final : public CCoinsView | |||
public: | |||
std::optional<Coin> GetCoin(const COutPoint& outpoint) const final |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the class is already final + other overrides specify that explicitly for clarity
std::optional<Coin> GetCoin(const COutPoint& outpoint) const final | |
std::optional<Coin> GetCoin(const COutPoint& outpoint) const override |
and
bool HaveCoin(const COutPoint& outpoint) const override
And most other such methods in the file - but I understand if you'd prefer to do in a follow-up instead
static void SetDirty(CoinsCachePair& pair, CoinsCachePair& sentinel, bool fresh = false) noexcept | ||
{ | ||
AddFlags(fresh ? FRESH | DIRTY : DIRTY, pair, sentinel); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it's only called here, we could inline and simplify it now:
static void SetDirty(CoinsCachePair& pair, CoinsCachePair& sentinel, bool fresh = false) noexcept | |
{ | |
AddFlags(fresh ? FRESH | DIRTY : DIRTY, pair, sentinel); | |
} | |
static void SetDirty(CoinsCachePair& pair, CoinsCachePair& sentinel, bool fresh = false) noexcept | |
{ | |
if (!pair.second.m_flags) { | |
Assume(!pair.second.m_prev && !pair.second.m_next); | |
pair.second.m_prev = sentinel.second.m_prev; | |
pair.second.m_next = &sentinel; | |
sentinel.second.m_prev = &pair; | |
pair.second.m_prev->second.m_next = &pair; | |
} | |
Assume(pair.second.m_prev && pair.second.m_next); | |
pair.second.m_flags |= fresh ? FRESH | DIRTY : DIRTY; | |
} |
Not sure, but do the flags still make sense after this or would a bool is_fresh
also suffice? The dirtiness can be deduced from the m_next
/m_prev
pair so the only unknown is the freshness which doesn't necessitate a flag anymore - right?
@@ -188,17 +183,15 @@ struct CCoinsCacheEntry | |||
bool IsDirty() const noexcept { return m_flags & DIRTY; } | |||
bool IsFresh() const noexcept { return m_flags & FRESH; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't have Fresh anymore, only IsDirtyAndFresh
, right?
bool IsFresh() const noexcept { return m_flags & FRESH; } | |
bool IsDirtyAndFresh() const noexcept | |
{ | |
const bool is_fresh = m_flags & FRESH; | |
Assume(IsDirty() || !is_fresh); | |
return is_fresh; | |
} |
return it->second; // TODO spent coins shouldn't be returned | ||
} | ||
} | ||
if (auto it{map_.find(outpoint)}; it != map_.end() && !it->second.IsSpent()) return it->second; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the lack of reviews, we may want to split out the test cleanups (removing invalid states) and refactors to a separate PR, where this will be a tracking PR for where we want to get to in safer/smaller steps
CCoinsCacheEntry::SetFresh(n2, sentinel); | ||
BOOST_CHECK(n2.second.IsFresh() && !n2.second.IsDirty()); | ||
// Check that setting DIRTY and FRESH on new node inserts it after n1 | ||
CCoinsCacheEntry::SetDirty(n2, sentinel, true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CCoinsCacheEntry::SetDirty(n2, sentinel, true); | |
CCoinsCacheEntry::SetDirty(n2, sentinel, /*fresh=*/true); |
return std::nullopt; | ||
} | ||
|
||
bool HaveCoin(const COutPoint& outpoint) const final | ||
{ | ||
return m_data.count(outpoint); | ||
return GetCoin(outpoint).has_value(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should be able to do this safely in a prefactor PR regardless of the other changes
Closing in favor of #33018. |
Following up from #28280 (comment), which suggested a revival of #18746.
GetCoin
will never returntrue
for a spent entry, so we can safely assume that any entry we fetch will not be spent. This lets us remove the only non-test code path which adds a FRESH-but-not-DIRTY entry to the flagged linked list. This in turn ensures all entries being sent toBatchWrite
areDIRTY
entries.A corollary is that all spent coins must be DIRTY. The only time a coin can be spent and not DIRTY is when the
CCoinsViewCacheEntry
is created with an empty coin. The last commit makes this more clear by checking for freshness if inserted instead of if spent and not DIRTY.This is a pure refactor removing dead code which handles a non-existent corner case of a FRESH-but-not-DIRTY entry in the
CCoinsViewCache
or a spent entry inCCoinsViewDB
. There is a lot of test code which tries to exercise this corner case, which is also updated in this PR to behave like non-test code.