Skip to content

Conversation

@sdaftuar
Copy link
Member

@sdaftuar sdaftuar commented May 24, 2023

This PR proposes a clean up of the relationship between block storage and the chainstate objects, by moving the decision of whether to store a block on disk to something that is not chainstate-specific. Philosophically, the decision of whether to store a block on disk is related to validation rules that do not require any UTXO state; for anti-DoS reasons we were using some chainstate-specific heuristics, and those have been reworked here to achieve the proposed separation.

This PR also fixes a bug in how a chainstate's setBlockIndexCandidates was being initialized; it should always have all the HAVE_DATA block index entries that have more work than the chain tip. During startup, we were not fully populating setBlockIndexCandidates in some scenarios involving multiple chainstates.

Further, this PR establishes a concept that whenever we have 2 chainstates, that we always know the snapshotted chain's base block and the base block's hash must be an element of our block index. Given that, we can establish a new invariant that the background validation chainstate only needs to consider blocks leading to that snapshotted block entry as potential candidates for its tip. As a followup I would imagine that when writing net_processing logic to download blocks for the background chainstate, that we would use this concept to only download blocks towards the snapshotted entry as well.

@jamesob
Copy link
Contributor

jamesob commented May 24, 2023

Great! Thanks for this. I'm active in these neighborhoods today as well, since adding some -stopatheight/restart logic to the functional tests in #27596 has turned up some issues with CheckBlockIndex(), which are maybe related to some of the changes here. So I'll be looking at this shortly.

@sdaftuar sdaftuar force-pushed the 2023-05-assumeutxo-validation-improvements branch from beb5334 to 379467a Compare May 24, 2023 21:55
@DrahtBot
Copy link
Contributor

DrahtBot commented May 25, 2023

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK achow101, jamesob, Sjors, ryanofsky
Concept ACK dergoegge
Stale ACK fjahr, mzumsande

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #28052 (blockstorage: XOR blocksdir *.dat files by MarcoFalke)
  • #27866 (blockstorage: Return on fatal flush errors by TheCharlatan)
  • #27460 (rpc: Add importmempool RPC by MarcoFalke)

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.

@Sjors
Copy link
Member

Sjors commented May 31, 2023

The first two commits up to f50fa24 look good to me.

How do you see the division of labor between ChainstateManager and BlockManager? The ChainState doc currently says:

Anything that is contingent on the current tip of the chain is stored here, whereas block information and metadata independent of the current tip is kept in BlockManager.

In any case moving AcceptBlock from Chainstate to ChainstateManager seems an improvement.

Copy link
Member

@Sjors Sjors left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you can split 534b6f99a3779465678f39ed2704da6049c6469d in ~half that would make it a bit easier to review. Maybe start with anything that's trivial to move, and then do the more involved changes in the second commit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

534b6f99a3779465678f39ed2704da6049c6469d if it's broken in master, can we fix it before refactoring? Alternatively there could be a This will be fixed in the next commit comment.

I'm also confused by the comment. TryAddBlockIndexCandidate is only called from ReceivedBlockTransactions after it sets BLOCK_VALID_TRANSACTIONS. So are you saying we're currently too strict? Was there a regression somewhere?

Copy link
Member Author

@sdaftuar sdaftuar Jun 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry that comment was confusing. I dropped it, and instead change the behavior around managing setBlockIndexCandidates in the last two commits.

I think master has a couple bugs. When we start up, in LoadBlockIndex() we weren't adding blocks that have been downloaded and have more work than the background chainstate's tip to setBlockIndexCandidates, which prevents those blocks from being validated and connected on startup. Also, because AcceptBlock() is a member of Chainstate, it's at best confusing to reason about whether a block that would advance the tip of a chainstate would always be added to a chainstate's setBlockIndexCandidates (it depends on yet-more logic around where a block came from and guessing about which chainstate might need it).

The new code tries to simplify things by establishing that the background chainstate is only trying to connect blocks towards the snapshot block, and that all blocks for which we HAVE_DATA and have more work than the background chainstate's tip (and which are ancestors of the snapshot block) will be added to the background chainstate's setBlockIndexCandidates. Also, the logic bug in LoadBlockIndex should be fixed in the last commit here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #27746 (comment)

I think master has a couple bugs.

I'm curious if there is actually more than one bug. The one bug I know about is the bug introduced
0fd599a from #23174 where LoadBlockIndex is misinterpreting the BLOCK_ASSUMED_VALID flag, and setting first_assumed_valid_height to the height of the first assumed valid block, instead of the height of the first block after the last assumed valid block (i.e. the snapshot height). I think it's pretty bad that we didn't catch this bug in review, or push for a more realistic test that would have caught it, or followed up on the suggestion from Marco #23174 (comment) to just add blocks to setBlockIndexCandidates linearly, which is what this PR does, and is much more straightforward.

Are there other known bugs? Are the changes to AcceptBlock meant to fix a bug or more to prevent a potential bug? Does the change to ChainstateManager::ReceivedBlockTransactions fix a bug? It seems like maybe it could prevent inappropriate blocks being added as candidates to the background chainstate, but it's not clear.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the fairest thing to say is that the changes to AcceptBlock() are to prevent a future bug -- it's possible that we could write code that somehow gets it right, but the design of selectively adding blocks to one chainstate's block index, and not the other, is one that I found very confusing.

As an example, if you took #24008 (which proposed logic for determining which chainstate to invoke AcceptBlock() on) and combined that with changing the logic in AcceptBlock() so that the background chainstate wouldn't bother processing blocks that are past the snapshot base, then that would be problematic if we were to allow pruning the snapshot-based-chain while the background sync is running (because according to the logic in #24008, if the block is already on the active chain, then we give it to the background chainstate to process). That's 2 hypotheticals so certainly this is speculative, but I think that those changes would all make sense on their own, so the fact that they fail to compose with the code in master is a sign to me that we should take a different design.

@sdaftuar
Copy link
Member Author

sdaftuar commented Jun 5, 2023

How do you see the division of labor between ChainstateManager and BlockManager? The ChainState doc currently says:

Anything that is contingent on the current tip of the chain is stored here, whereas block information and metadata independent of the current tip is kept in BlockManager.

I think of BlockManager as being a generic backend database of block (and undo) data, while any validation-related logic should live in Chainstate (if it relies on chainstate-specific information) or somewhere else (either a static function in validation or in ChainstateManager as I propose here) if not.

I think AcceptBlock could also be a static function in validation.cpp; I mostly just wanted to get it out of Chainstate because I think that having block storage be coupled to a particular chainstate is a confusing idea, but I can go either way on whether it belongs in ChainstateManager or not.

@sdaftuar
Copy link
Member Author

@Sjors @achow101 Thanks for the review! I split up the big commit as @Sjors suggested, and with the test fixes I think this is ready to be moved out of Draft status.

@sdaftuar sdaftuar marked this pull request as ready for review June 13, 2023 17:54
@sdaftuar sdaftuar force-pushed the 2023-05-assumeutxo-validation-improvements branch from bccd618 to 48de8dc Compare June 13, 2023 17:59
@sdaftuar sdaftuar changed the title Draft: rework validation logic for assumeutxo Rework validation logic for assumeutxo Jun 13, 2023
@sdaftuar sdaftuar force-pushed the 2023-05-assumeutxo-validation-improvements branch from 48de8dc to 89e8826 Compare June 14, 2023 15:15
Copy link
Contributor

@mzumsande mzumsande left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about inserting and returning here in the !is_bg_chainstate case, so that we can skip the GetAncestor() call when dealing with the snapshot chainstate?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, good idea.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

55e27ee

If you retouch, I think you can simply say bool is_bg_chainstate = this != &ActiveChainstate();

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea -- and it looks like my code here is wrong anyway, because if we're a disabled background chainstate, then we'll be adding blocks to setBlockIndexCandidates as long as they have more work than the tip!

@sdaftuar sdaftuar force-pushed the 2023-05-assumeutxo-validation-improvements branch from 89e8826 to f02125f Compare June 15, 2023 18:22
Copy link
Member

@achow101 achow101 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK f02125f

Copy link
Contributor

@ryanofsky ryanofsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial code review ACK f02125f. I've looked at most of this, but not the whole thing yet, and all the changes seem very nice. This should help avoid bugs and make assumeutxo code more maintainable. I think it would be good to clean up documentation as part of this too. Here are documentation changes I would suggest:

--- a/doc/design/assumeutxo.md
+++ b/doc/design/assumeutxo.md
@@ -17,10 +17,9 @@ respectively generate and load UTXO snapshots. The utility script
 
 - A new block index `nStatus` flag is introduced, `BLOCK_ASSUMED_VALID`, to mark block
   index entries that are required to be assumed-valid by a chainstate created
-  from a UTXO snapshot. This flag is mostly used as a way to modify certain
+  from a UTXO snapshot. This flag is used as a way to modify certain
   CheckBlockIndex() logic to account for index entries that are pending validation by a
-  chainstate running asynchronously in the background. We also use this flag to control
-  which index entries are added to setBlockIndexCandidates during LoadBlockIndex().
+  chainstate running asynchronously in the background.
 
 - The concept of UTXO snapshots is treated as an implementation detail that lives
   behind the ChainstateManager interface. The external presentation of the changes
diff --git a/src/chain.h b/src/chain.h
index f5dd0fd31514..cb8bc7f04792 100644
--- a/src/chain.h
+++ b/src/chain.h
@@ -113,10 +113,10 @@ enum BlockStatus : uint32_t {
     BLOCK_VALID_TRANSACTIONS =    3,
 
     //! Outputs do not overspend inputs, no double spends, coinbase output ok, no immature coinbase spends, BIP30.
-    //! Implies all parents are also at least CHAIN.
+    //! Implies all parents are either at least VALID_CHAIN, or are ASSUMED_VALID
     BLOCK_VALID_CHAIN        =    4,
 
-    //! Scripts & signatures ok. Implies all parents are also at least SCRIPTS.
+    //! Scripts & signatures ok. Implies all parents are either at least VALID_SCRIPTS, or are ASSUMED_VALID.
     BLOCK_VALID_SCRIPTS      =    5,
 
     //! All validity bits.
@@ -134,10 +134,18 @@ enum BlockStatus : uint32_t {
     BLOCK_OPT_WITNESS        =   128, //!< block data in blk*.dat was received with a witness-enforcing client
 
     /**
-     * If set, this indicates that the block index entry is assumed-valid.
-     * Certain diagnostics will be skipped in e.g. CheckBlockIndex().
-     * It almost certainly means that the block's full validation is pending
-     * on a background chainstate. See `doc/design/assumeutxo.md`.
+     * If ASSUMED_VALID is set, it means that this block has not been validated
+     * and has validity status less than VALID_SCRIPTS. Also that it has
+     * descendant blocks with VALID_SCRIPTS set, because they were validated
+     * based on an assumeutxo snapshot.
+     *
+     * When an assumeutxo snapshot is loaded, the ASSUMED_VALID flag is added to
+     * unvalidated blocks below the snapshot height. Then, as the background
+     * validation progresses, and these blocks are validated, the ASSUMED_VALID
+     * flags are removed. See `doc/design/assumeutxo.md` for details.
+     *
+     * This flag is only used to implement checks in CheckBlockIndex() and
+     * should not be used elsewhere.
      */
     BLOCK_ASSUMED_VALID      =   256,
 };

re: PR description #27746 (comment)

base block is: the block hash

Grammar here seems off, maybe replace with "base block, and the base block hash"


re: PR description #27746 (comment)

This code needs a lot of cleanup and test fixes and so I've marked it as a draft -- help from reviewers would be welcome!

Could drop this part since PR seems to be a good state to review and merge.


re: #27746 (comment)

I think AcceptBlock could also be a static function in validation.cpp

IMO, would be nice make it a standalone function (static or not). Unless a function is really tied to one particular class or needs access to its private state, better for it just be standalone and use the public interface.


// Create a snapshot-based chainstate.
//
Chainstate& c2 = WITH_LOCK(cs_main, return manager.ActivateExistingSnapshot(&mempool, GetRandHash()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "Tighten requirements for adding elements to setBlockIndexCandidates" (b0b48b3)

Note for reviewers: A real hash is being passed instead of GetRandHash() now, because of discussion #27746 (comment), because ActivateExistingSnapshot should not be expected to work when called with an invalid blockhash even if it does work now.

src/validation.h Outdated
void UpdateSnapshotBaseEntry(const CBlockIndex *entry) EXCLUSIVE_LOCKS_REQUIRED(::cs_main)
{ m_snapshot_entry = entry; }

//! Track the snapshot entry
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "Explicitly track snapshot block header when using multiple chainstates" (d2ccbd3)

I don't think adding this Chainstate::m_snapshot_entry variable is a good idea. I don't like how it has different meanings for different chainstates (starting point of validation for the snapshot chainstate, and ending point for the background chainstate), and the lack of documentation, and the complicated initialization which happens in the Chainstate constructor for the snapshot chainstate, but also in ActivateSnapshot for both chainstates, and then again in LoadBlockIndex for both chainstates, and pointedly not in ActivateExistingSnapshot according to a new comment there saying it will be initialized later... It just seems like a lot of complexity for new variable which is redundant with the existing m_from_snapshot_blockhash variable and is only used one place, in Chainstate::TryAddBlockIndexCandidate.

I think it would be better if Chainstate::TryAddBlockIndexCandidate just used snapshot chainstate's m_from_snapshot_blockhash value directly. Or if that is too inefficient, we could add a m_from_snapshot_blockindex member that mirrors m_from_snapshot_blockhash and is const and could be initialized straightforwardly in the ChainState constructor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I concur with @ryanofsky here. I'll have to double check, but in actual usage in #27596, I think I found that the one usage of this had another (existing) way of getting at this data. I'm afk for a few days, but will verify when I get back.

Copy link
Member Author

@sdaftuar sdaftuar Jun 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your criticisms that the way this is written is pretty confusing (it took me a while to get it right in the first place, and I found this hard to reason about).

I wanted to capture the idea that the hash m_from_snapshot_blockhash must always be in our block index, so that we never have to worry when we look it up in the block index that it might be missing, and we have to deal with a nullptr case.

However, I couldn't see how to make it a const that is straightforwardly initialized in Chainstate, because there are two different cases to consider (activating a snapshot for the first time, versus detecting that we have a snapshot on restart).

Also, while this PR introduces one place where this CBlockIndex is used, I expect that we'll use it again in the net_processing code which will download blocks toward the snapshot base.

Here are a few options I can think of to improve this:

  • just look the hash up every time we need to use it (both in TryAddBlockIndexCandidate and wherever it is needed in net_processing, likely one additional place), avoiding the storage of redundant information
  • lazily fill it in so that we only look it up once, and then use the looked-up value for future requests
  • move the knowledge of the snapshot base to ChainstateManager, so that we don't store redundant information in both chainstates (this could be done along with either of the first two ideas, I think?)

EDIT: Gah, I just noticed ChainstateManager::GetSnapshotBaseBlock(), didn't realize we already had that. I think I know what to do now to fix this!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #27746 (comment)

EDIT: Gah, I just noticed ChainstateManager::GetSnapshotBaseBlock(), didn't realize we already had that. I think I know what to do now to fix this!

Thanks, I didn't realize how clunky ChainstateManager initialization was when I made that suggestion. Particularly how it allowed construction with that snapshot block that wasn't actually in the index. I think initialization can definitely be simplified later. But even without doing that I experimented a little and came up with a change that adds CS::SnapshotBase() and CSM::ActiveSnapshotBase() methods that I think should provide a good interface for external code to use, while allowing us to clean up the internal representation and init process later:

--- a/src/validation.h
+++ b/src/validation.h
@@ -528,12 +528,8 @@ protected:
     //! is set to true on the snapshot chainstate.
     bool m_disabled GUARDED_BY(::cs_main) {false};
 
-    void UpdateSnapshotBaseEntry(const CBlockIndex *entry) EXCLUSIVE_LOCKS_REQUIRED(::cs_main)
-        { m_snapshot_entry = entry; }
-
-    //! Track the snapshot entry
-    const CBlockIndex* m_snapshot_entry GUARDED_BY(::cs_main) {nullptr};
-
+    //! Cached result of LookupBlockIndex(*m_from_snapshot_blockhash)
+    const CBlockIndex* m_cached_snapshot_base GUARDED_BY(::cs_main) {nullptr};
 
 public:
     //! Reference to a BlockManager instance which itself is shared across all
@@ -586,6 +582,13 @@ public:
      */
     const std::optional<uint256> m_from_snapshot_blockhash;
 
+    /**
+     * The base of the snapshot this chainstate was created from.
+     *
+     * nullptr if this chainstate was not created from a snapshot.
+     */
+    const CBlockIndex* SnapshotBase() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
+
     //! Return true if this chainstate relies on blocks that are assumed-valid. In
     //! practice this means it was created based on a UTXO snapshot.
     bool reliesOnAssumedValid() { return m_from_snapshot_blockhash.has_value(); }
@@ -1093,6 +1096,11 @@ public:
         return m_snapshot_chainstate && m_ibd_chainstate && m_ibd_chainstate->m_disabled;
     }
 
+    const CBlockIndex* ActiveSnapshotBase() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main)
+    {
+        return m_active_chainstate ? m_active_chainstate->SnapshotBase() : nullptr;
+    }
+
     /**
      * Import blocks from an external file
      *
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -1579,9 +1579,13 @@ Chainstate::Chainstate(
       m_chainman(chainman),
       m_from_snapshot_blockhash(from_snapshot_blockhash)
 {
-    if (m_from_snapshot_blockhash) {
-        m_snapshot_entry = WITH_LOCK(::cs_main, return m_chainman.m_blockman.LookupBlockIndex(*m_from_snapshot_blockhash));
-    }
+}
+
+const CBlockIndex* Chainstate::SnapshotBase()
+{
+    if (!m_from_snapshot_blockhash) return nullptr;
+    if (!m_cached_snapshot_base) m_cached_snapshot_base = Assert(m_chainman.m_blockman.LookupBlockIndex(*m_from_snapshot_blockhash));
+    return m_cached_snapshot_base;
 }
 
 void Chainstate::InitCoinsDB(
@@ -3423,8 +3427,9 @@ void Chainstate::TryAddBlockIndexCandidate(CBlockIndex* pindex)
             // For the background chainstate, we only consider connecting blocks
             // towards the snapshot base (which can't be nullptr or else we'll
             // never make progress).
-            assert(m_snapshot_entry != nullptr);
-            if (m_snapshot_entry->GetAncestor(pindex->nHeight) == pindex) {
+            const CBlockIndex* snapshot_base{m_chainman.ActiveSnapshotBase()};
+            assert(snapshot_base != nullptr);
+            if (snapshot_base->GetAncestor(pindex->nHeight) == pindex) {
                 setBlockIndexCandidates.insert(pindex);
             }
         }
@@ -4416,14 +4421,6 @@ bool ChainstateManager::LoadBlockIndex()
         bool ret{m_blockman.LoadBlockIndexDB()};
         if (!ret) return false;
 
-        // If we already have 2 chainstates, then we need to update the
-        // snapshot base to point to the correct block entry.
-        if (IsSnapshotActive()) {
-            CBlockIndex *entry = m_blockman.LookupBlockIndex(*SnapshotBlockhash());
-            m_ibd_chainstate->UpdateSnapshotBaseEntry(entry);
-            m_snapshot_chainstate->UpdateSnapshotBaseEntry(entry);
-        }
-
         m_blockman.ScanAndUnlinkAlreadyPrunedFiles();
 
         std::vector<CBlockIndex*> vSortedByHeight{m_blockman.GetAllBlockIndices()};
@@ -5142,8 +5139,6 @@ bool ChainstateManager::ActivateSnapshot(
             m_snapshot_chainstate->CoinsTip().DynamicMemoryUsage() / (1000 * 1000));
 
         this->MaybeRebalanceCaches();
-
-        m_ibd_chainstate->m_snapshot_entry = m_snapshot_chainstate->m_snapshot_entry;
     }
     return true;
 }
@@ -5623,8 +5618,6 @@ Chainstate& ChainstateManager::ActivateExistingSnapshot(CTxMemPool* mempool, uin
         std::make_unique<Chainstate>(mempool, m_blockman, *this, base_blockhash);
     LogPrintf("[snapshot] switching active chainstate to %s\n", m_snapshot_chainstate->ToString());
     m_active_chainstate = m_snapshot_chainstate.get();
-    // Note: m_snapshot_entry will be populated after the block index is
-    // loaded; see ChainstateManager::LoadBlockIndex.
     return *m_snapshot_chainstate;
 }
 

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #27746 (comment)

I think master has a couple bugs.

I'm curious if there is actually more than one bug. The one bug I know about is the bug introduced
0fd599a from #23174 where LoadBlockIndex is misinterpreting the BLOCK_ASSUMED_VALID flag, and setting first_assumed_valid_height to the height of the first assumed valid block, instead of the height of the first block after the last assumed valid block (i.e. the snapshot height). I think it's pretty bad that we didn't catch this bug in review, or push for a more realistic test that would have caught it, or followed up on the suggestion from Marco #23174 (comment) to just add blocks to setBlockIndexCandidates linearly, which is what this PR does, and is much more straightforward.

Are there other known bugs? Are the changes to AcceptBlock meant to fix a bug or more to prevent a potential bug? Does the change to ChainstateManager::ReceivedBlockTransactions fix a bug? It seems like maybe it could prevent inappropriate blocks being added as candidates to the background chainstate, but it's not clear.

Copy link
Contributor

@ryanofsky ryanofsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review ACK a733dd7. Just suggested changes since the last review. There are various small things that could be followed up on, but I think this is ready for merge.

// If we have an assumeutxo-based chainstate, then the snapshot
// block will be a candidate for the tip, but it may not be
// VALID_TRANSACTIONS (eg if we haven't yet downloaded the block),
// so we special-case the snapshot block as a potential candidate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #27746 (comment)

GetSnapshotBaseBlock() is nullptr so the special-case won't apply

I may be misunderstanding, but I don't think it's true that GetSnapshotBaseBlock is null for the background chainstate. This is a ChainstateManager method, so GetSnapshotBaseBlock is going to be non-null if any snapshot is loaded, and this code will try to add the snapshot block to both chainstates, even though it's not needed for the background chainstate. It might be clearer if the code narrowed the special case, and only added the snapshot block to the snapshot chainstate where it was actually needed, not to both chainstates. But that might make the code more complicated.

I do think it might be better if the comment said "assumeutxo snapshot chainstate" instead of "assumeutxo-based chainstate" to be more specific about when this special case is needed, but I don't think it need to go into detail about other cases where the check is harmless and not needed.

// The assumed-valid tolerant chain has the assumed valid base as a
// candidate, but otherwise has none of the assumed-valid (which do not
// HAVE_DATA) blocks as candidates.
BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(validated_tip), 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "Fix initialization of setBlockIndexCandidates when working with multiple chainstates" (768690b)

Would be good to directly verify the snapshot block is included:

// block before the snapshot is excluded (as well as earlier blocks)
BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(assumed_base->pprev), 0); 
// snapshot block is included (as well as later blocks)
BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(assumed_base), 1);

// candidate, but otherwise has none of the assumed-valid (which do not
// HAVE_DATA) blocks as candidates.
BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(validated_tip), 0);
BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(assumed_tip), 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #27746 (comment)

768690b: this had me confused, but it's because the test considers a scenario where the snapshot base block has been downloaded. It could be nice to include the moment before it's downloaded where this should be 0.

This review comment does not match my understanding. The snapshot block is assumed_base at height 39 and it does not have data. This line is checking assumed_tip block at height 100 which does have data.

I think all the variable names in this test are pretty confusing, and it would probably be clearer if blocks were referred to directly by height, but that's the was the test style, so it's not changing.

//! that will tolerate assumed-valid blocks. Run LoadBlockIndex() and ensure that the first
//! chainstate only contains fully validated blocks and the other chainstate contains all blocks,
//! even those assumed-valid.
//! except those marked assume-valid, because those entries don't HAVE_DATA.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "Fix initialization of setBlockIndexCandidates when working with multiple chainstates" (768690b)

I think it's a little misleading to say the other chainstate contains all block except those marked assume-valid because:

  • It actually does contain one block marked assumed-valid, which is the snapshot base (block 39)
  • It also excludes other blocks which are not assumed-valid because they don't have more work than the tip (blocks 1-19)

I think this comment could incorporate sjors suggestion above and just say "Run LoadBlockIndex() and ensure that setBlockIndexCandidates of the first chainstate only contains blocks leading to the snapshot base, and that setBlockIndexCandidates of the second chainstate only contains blocks following the snapshot base."

Or could leave out the details and say "Run LoadBlockIndex() and check setBlockIndexCandidates of both chainstates."

* sigops, size, merkle root. Implies all parents are at least TREE but not necessarily TRANSACTIONS. When all
* parent blocks also have TRANSACTIONS, CBlockIndex::nChainTx will be set.
*/
BLOCK_VALID_TRANSACTIONS = 3,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #27746 (comment)

0ce805b: why isn't BLOCK_VALID_TRANSACTIONS also alternatively ASSUMED_VALID?

I'm not sure what this review comment is asking. The code comment above still seems accurate to me, but it maybe it could be improved.

@ryanofsky ryanofsky merged commit f4f1d6d into bitcoin:master Jul 31, 2023
@sedited
Copy link
Contributor

sedited commented Aug 1, 2023

Post-merge ACK a733dd7

Very happy with the separation of concerns between blockstorage and validation.

@fjahr
Copy link
Contributor

fjahr commented Aug 1, 2023

Post-merge code review ACK a733dd7

I also synced and kept a node running for a few days with the status of my previous ACK and didn't see anything unusual.

ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Aug 4, 2023
…ChainstateManager

This change makes IsInitialBlockDownload and NotifyHeaderTip functions no
longer tied to individual Chainstate objects. It makes them work with the
ChainstateManager object instead so code is simpler and it is no longer
possible to call them incorrectly with an inactive Chainstate.

This change also makes m_cached_finished_ibd caching easier to reason about,
because now there is only one cached value instead of two (for background and
snapshot chainstates) so the cached IBD state now no longer gets reset when a
snapshot is loaded.

There should be no change in behavior because these functions were always
called on the active ChainState objects.

These changes were discussed previously
bitcoin#27746 (comment) and
bitcoin#27746 (comment) as
possible followups for that PR.
ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Aug 4, 2023
…ChainstateManager

This change makes IsInitialBlockDownload and NotifyHeaderTip functions no
longer tied to individual Chainstate objects. It makes them work with the
ChainstateManager object instead so code is simpler and it is no longer
possible to call them incorrectly with an inactive Chainstate.

This change also makes m_cached_finished_ibd caching easier to reason about,
because now there is only one cached value instead of two (for background and
snapshot chainstates) so the cached IBD state now no longer gets reset when a
snapshot is loaded.

There should be no change in behavior because these functions were always
called on the active ChainState objects.

These changes were discussed previously
bitcoin#27746 (comment) and
bitcoin#27746 (comment) as
possible followups for that PR.
ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Aug 8, 2023
…ChainstateManager

This change makes IsInitialBlockDownload and NotifyHeaderTip functions no
longer tied to individual Chainstate objects. It makes them work with the
ChainstateManager object instead so code is simpler and it is no longer
possible to call them incorrectly with an inactive Chainstate.

This change also makes m_cached_finished_ibd caching easier to reason about,
because now there is only one cached value instead of two (for background and
snapshot chainstates) so the cached IBD state now no longer gets reset when a
snapshot is loaded.

There should be no change in behavior because these functions were always
called on the active ChainState objects.

These changes were discussed previously
bitcoin#27746 (comment) and
bitcoin#27746 (comment) as
possible followups for that PR.
sidhujag pushed a commit to syscoin/syscoin that referenced this pull request Aug 9, 2023
ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Aug 18, 2023
…ChainstateManager

This change makes IsInitialBlockDownload and NotifyHeaderTip functions no
longer tied to individual Chainstate objects. It makes them work with the
ChainstateManager object instead so code is simpler and it is no longer
possible to call them incorrectly with an inactive Chainstate.

This change also makes m_cached_finished_ibd caching easier to reason about,
because now there is only one cached value instead of two (for background and
snapshot chainstates) so the cached IBD state now no longer gets reset when a
snapshot is loaded.

There should be no change in behavior because these functions were always
called on the active ChainState objects.

These changes were discussed previously
bitcoin#27746 (comment) and
bitcoin#27746 (comment) as
possible followups for that PR.
ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Aug 18, 2023
…ChainstateManager

This change makes IsInitialBlockDownload and NotifyHeaderTip functions no
longer tied to individual Chainstate objects. It makes them work with the
ChainstateManager object instead so code is simpler and it is no longer
possible to call them incorrectly with an inactive Chainstate.

This change also makes m_cached_finished_ibd caching easier to reason about,
because now there is only one cached value instead of two (for background and
snapshot chainstates) so the cached IBD state now no longer gets reset when a
snapshot is loaded.

There should be no change in behavior because these functions were always
called on the active ChainState objects.

These changes were discussed previously
bitcoin#27746 (comment) and
bitcoin#27746 (comment) as
possible followups for that PR.
fanquake added a commit to bitcoin-core/gui that referenced this pull request Aug 21, 2023
…NotifyHeaderTip not require a Chainstate

94a98fb assumeutxo cleanup: Move IsInitialBlockDownload & NotifyHeaderTip to ChainstateManager (Ryan Ofsky)

Pull request description:

  This change makes `IsInitialBlockDownload` and `NotifyHeaderTip` functions no longer tied to individual `Chainstate` objects. It makes them work with the `ChainstateManager` object instead so code is simpler and it is no longer possible to call them incorrectly with an inactive `Chainstate`.

  This change also makes `m_cached_finished_ibd` caching easier to reason about, because now there is only one cached value instead of two (for background and snapshot chainstates) so the cached IBD state now no longer gets reset when a snapshot is loaded.

  There should be no change in behavior because these functions were always called on the active `ChainState` objects.

  These changes were discussed previously bitcoin/bitcoin#27746 (comment) and bitcoin/bitcoin#27746 (comment) as possible followups for that PR.

ACKs for top commit:
  MarcoFalke:
    re-ACK 94a98fb 🐺
  naumenkogs:
    ACK 94a98fb
  dergoegge:
    reACK 94a98fb

Tree-SHA512: 374d6e5c9bbc7564c143f634bd709a4e8f4a42c8d77e7a8554c832acdcf60fa2a134f3ea10827db1a1e0191006496329c0ebf5c64f3ab868398c3722bb7ff56f
janus pushed a commit to BitgesellOfficial/bitgesell that referenced this pull request Sep 11, 2023
…ChainstateManager

This change makes IsInitialBlockDownload and NotifyHeaderTip functions no
longer tied to individual Chainstate objects. It makes them work with the
ChainstateManager object instead so code is simpler and it is no longer
possible to call them incorrectly with an inactive Chainstate.

This change also makes m_cached_finished_ibd caching easier to reason about,
because now there is only one cached value instead of two (for background and
snapshot chainstates) so the cached IBD state now no longer gets reset when a
snapshot is loaded.

There should be no change in behavior because these functions were always
called on the active ChainState objects.

These changes were discussed previously
bitcoin/bitcoin#27746 (comment) and
bitcoin/bitcoin#27746 (comment) as
possible followups for that PR.
// VALID_TRANSACTIONS (eg if we haven't yet downloaded the block),
// so we special-case the snapshot block as a potential candidate
// here.
if (pindex == GetSnapshotBaseBlock() ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "Fix initialization of setBlockIndexCandidates when working with multiple chainstates" (768690b)

It looks like there may be a bug in this code because this pindex == GetSnapshotBaseBlock() special case will add the snapshot block to setBlockIndexCandidates whether or not it has data, but there is also an assert in FindMostWorkChain which asserts that blocks in setBlockIndexCandidates do have data:

assert(pindexTest->HaveTxsDownloaded() || pindexTest->nHeight == 0);

I think it's not possible to hit this assert for the snapshot chainstate, but it may be possible to hit this assert for the background chainstate.

For the snapshot chainstate, I think the assert won't be reached because after the snapshot is loaded the Chainstate::LoadChainTip function should immediately set the chain tip to the snapshot block index:

m_chain.SetTip(*pindex);

so the m_chain.Contains(pindexTest) check before the assert will be false and the assert will not run:

bitcoin/src/validation.cpp

Lines 2913 to 2914 in c5a63ea

while (pindexTest && !m_chain.Contains(pindexTest)) {
assert(pindexTest->HaveTxsDownloaded() || pindexTest->nHeight == 0);

But for the background chainstate, it seems possible assert could be hit if FindMostWorkChain is called before the snapshot block is downloaded.

In any case, this special pindex == GetSnapshotBaseBlock() case seems overbroad. It seems like it should only apply to the snapshot chainstate, not the background chainstate, and I'm not actually sure why it is necessary to have at all if Chainstate::LoadChainTip is already going to set the snapshot block as the snapshot chainstate tip.

I think the interaction between these checks and the checks inside the TryAddBlockIndexCandidate are also unnecessarily confusing, and it would be nice whether there is a bug or not to move all the move all the checks inside TryAddBlockIndexCandidate and call it unconditionally. This should make it easeir to avoid adding the snapshot block the background chainstate candidate set before it is downloaded. It would also be great to drop the snapshot block special case entirely if it is not needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like there may be a bug in this code because this pindex == GetSnapshotBaseBlock() special case will add the snapshot block to setBlockIndexCandidates whether or not it has data, but there is also an assert in FindMostWorkChain which asserts that blocks in setBlockIndexCandidates do have data:

bitcoin/src/validation.cpp
Line 2914 in c5a63ea

assert(pindexTest->HaveTxsDownloaded() || pindexTest->nHeight == 0); 

I think it's not possible to hit this assert for the snapshot chainstate, but it may be possible to hit this assert for the background chainstate.

If I'm reading FindMostWorkChain() correctly, this assert won't be hit for a background chainstate, because the second clause of this condition

while (pindexTest && !m_chain.Contains(pindexTest)) {

will exclude the snapshot base block from consideration without data (since it is not in the background chainstate's m_chain until it is actually downloaded, and so has data). So as far as I can tell, there's no bug here - but I'm glad you're double checking this; the setBlockIndexCandidates code is very confusing indeed, and I'm amenable to any way we can make it more easily understandable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #27746 (comment)

The way I'm reading e code:

bitcoin/src/validation.cpp

Lines 2913 to 2914 in c5a63ea

while (pindexTest && !m_chain.Contains(pindexTest)) {
assert(pindexTest->HaveTxsDownloaded() || pindexTest->nHeight == 0);

If a snapshot is loaded and pindexTest points to the snapshot block and the snapshot block is not yet downloaded, and m_chain is the background chain, then m_chain.Contains(pindexTest) will be false, so the while loop condition will be true, and the assert could be be hit.

You seem to be saying that m_chain.Contains(pindexTest) will be false, so the while condition won't be hit?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry, my previous comment was more or less incorrect. Another reason why the assert you mention doesn't pose a problem is because, confusingly, HaveTxsDownloaded() is just an alias for nChainTx != 0. Since we manually set nChainTx for the snapshot base block (315586f) this check (again, very confusingly) passes. At some point long ago I made a note somewhere about fixing that method's misleading name...

So the long story short is that FindMostWorkChain() eventually works for the background chainstate because it detects, correctly, that there are blocks with missing data between the IBD tip and the snapshot base, and correctly rewinds m_chain back to the actual IBD tip. I think when @sdaftuar first proposed this change I was confused about why the LoadBlockIndex() simplification could actually work, and this is the reason.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the long story short is that FindMostWorkChain() eventually works for the background chainstate because it detects, correctly, that there are blocks with missing data between the IBD tip and the snapshot base, and correctly rewinds m_chain back to the actual IBD tip.

Again I'm wrong: snapshot base block never makes it into background chainstate's m_chain because LoadChainTip() (when called from the init code in src/node/chainstate.cpp) sets the background chainstate's tip to the correct IBD tip (on the basis of the coinscache GetBestBlock()), which is before the snapshot base block.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I missed that HaveTxsDownloaded was checking nChainTx not nTx.

I guess the main thing I'm confused about now is why this pindex == GetSnapshotBaseBlock() || special case is needed at all? If LoadChainTip() already sets the snapshot block to be the snapshot tip, why does the snapshot block need to be added to the candidate set?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the main thing I'm confused about now is why this pindex == GetSnapshotBaseBlock() || special case is needed at all? If LoadChainTip() already sets the snapshot block to be the snapshot tip, why does the snapshot block need to be added to the candidate set?

I'm not sure if there's a more fundamental reason, but various assertions fail if this case is stripped out of LoadBlockIndex(); e.g. the !setBlockIndexCandidates.empty() assertion fails during PruneBlockIndexCandidates() if this special case is not included.

The true purpose of setBlockIndexCandidates has always been somewhat mysterious to me, but the most essential use seems to be in FindMostWorkChain(), to supply candidates for activation in ActivateBestChain().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some documentation for HaveTxsDownloaded() (9f9ffb6), and I'm happy to add more for the confusing process discussed above.

ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Oct 6, 2023
… flat list of chainstates

Also add m_validated and m_target_block members to Chainstate class it is
possible to determine chainstate properties directly from Chainstate objects
and it is not neccessary for validation code make calls to ChainstateManager
and decide what to do by looking at assumeutxo download state.

Goal is to remove hardcoded logic handling assumeutxo snapshots from most
validation code. Also to make it easy to fix locking issues like the fact that
cs_main is currently held unnecessarily validating snapshots, and the fact that
background chainstate is not immediately deleted when it is no longer used,
wasting disk space and adding a long startup delay next time the node is
restarted.

This follows up on some previous discussions:

bitcoin#24232 (comment)
bitcoin#27746 (comment)
bitcoin#28562 (comment)
ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Oct 10, 2023
… flat list of chainstates

Also add m_validity and m_target_block members to Chainstate class it is
possible to determine chainstate properties directly from Chainstate objects
and it is not neccessary for validation code make calls to ChainstateManager
and decide what to do by looking at assumeutxo download state.

Goal is to remove hardcoded logic handling assumeutxo snapshots from most
validation code. Also to make it easy to fix locking issues like the fact that
cs_main is currently held unnecessarily validating snapshots, and the fact that
background chainstate is not immediately deleted when it is no longer used,
wasting disk space and adding a long startup delay next time the node is
restarted.

This follows up on some previous discussions:

bitcoin#24232 (comment)
bitcoin#27746 (comment)
bitcoin#28562 (comment)
ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Oct 11, 2023
… flat list of chainstates

Also add m_validity and m_target_block members to Chainstate class it is
possible to determine chainstate properties directly from Chainstate objects
and it is not neccessary for validation code make calls to ChainstateManager
and decide what to do by looking at assumeutxo download state.

Goal is to remove hardcoded logic handling assumeutxo snapshots from most
validation code. Also to make it easy to fix locking issues like the fact that
cs_main is currently held unnecessarily validating snapshots, and the fact that
background chainstate is not immediately deleted when it is no longer used,
wasting disk space and adding a long startup delay next time the node is
restarted.

This follows up on some previous discussions:

bitcoin#24232 (comment)
bitcoin#27746 (comment)
bitcoin#28562 (comment)
ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Oct 12, 2023
… flat list of chainstates

Also add m_validity and m_target_block members to Chainstate class it is
possible to determine chainstate properties directly from Chainstate objects
and it is not neccessary for validation code make calls to ChainstateManager
and decide what to do by looking at assumeutxo download state.

Goal is to remove hardcoded logic handling assumeutxo snapshots from most
validation code. Also to make it easy to fix locking issues like the fact that
cs_main is currently held unnecessarily validating snapshots, and the fact that
background chainstate is not immediately deleted when it is no longer used,
wasting disk space and adding a long startup delay next time the node is
restarted.

This follows up on some previous discussions:

bitcoin#24232 (comment)
bitcoin#27746 (comment)
bitcoin#28562 (comment)
ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Oct 12, 2023
… flat list of chainstates

Also add m_validity and m_target_block members to Chainstate class it is
possible to determine chainstate properties directly from Chainstate objects
and it is not neccessary for validation code make calls to ChainstateManager
and decide what to do by looking at assumeutxo download state.

Goal is to remove hardcoded logic handling assumeutxo snapshots from most
validation code. Also to make it easy to fix locking issues like the fact that
cs_main is currently held unnecessarily validating snapshots, and the fact that
background chainstate is not immediately deleted when it is no longer used,
wasting disk space and adding a long startup delay next time the node is
restarted.

This follows up on some previous discussions:

bitcoin#24232 (comment)
bitcoin#27746 (comment)
bitcoin#28562 (comment)
ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Oct 12, 2023
… flat list of chainstates

Also add m_validity and m_target_block members to Chainstate class it is
possible to determine chainstate properties directly from Chainstate objects
and it is not neccessary for validation code make calls to ChainstateManager
and decide what to do by looking at assumeutxo download state.

Goal is to remove hardcoded logic handling assumeutxo snapshots from most
validation code. Also to make it easy to fix locking issues like the fact that
cs_main is currently held unnecessarily validating snapshots, and the fact that
background chainstate is not immediately deleted when it is no longer used,
wasting disk space and adding a long startup delay next time the node is
restarted.

This follows up on some previous discussions:

bitcoin#24232 (comment)
bitcoin#27746 (comment)
bitcoin#28562 (comment)
ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Oct 12, 2023
… flat list of chainstates

Also add m_validity and m_target_block members to Chainstate class it is
possible to determine chainstate properties directly from Chainstate objects
and it is not neccessary for validation code make calls to ChainstateManager
and decide what to do by looking at assumeutxo download state.

Goal is to remove hardcoded logic handling assumeutxo snapshots from most
validation code. Also to make it easy to fix locking issues like the fact that
cs_main is currently held unnecessarily validating snapshots, and the fact that
background chainstate is not immediately deleted when it is no longer used,
wasting disk space and adding a long startup delay next time the node is
restarted.

This follows up on some previous discussions:

bitcoin#24232 (comment)
bitcoin#27746 (comment)
bitcoin#28562 (comment)
ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Feb 23, 2024
… flat list of chainstates

Also add m_validity and m_target_block members to Chainstate class it is
possible to determine chainstate properties directly from Chainstate objects
and it is not neccessary for validation code make calls to ChainstateManager
and decide what to do by looking at assumeutxo download state.

Goal is to remove hardcoded logic handling assumeutxo snapshots from most
validation code. Also to make it easy to fix locking issues like the fact that
cs_main is currently held unnecessarily validating snapshots, and the fact that
background chainstate is not immediately deleted when it is no longer used,
wasting disk space and adding a long startup delay next time the node is
restarted.

This follows up on some previous discussions:

bitcoin#24232 (comment)
bitcoin#27746 (comment)
bitcoin#28562 (comment)
ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Feb 23, 2024
… flat list of chainstates

Also add m_validity and m_target_block members to Chainstate class it is
possible to determine chainstate properties directly from Chainstate objects
and it is not neccessary for validation code make calls to ChainstateManager
and decide what to do by looking at assumeutxo download state.

Goal is to remove hardcoded logic handling assumeutxo snapshots from most
validation code. Also to make it easy to fix locking issues like the fact that
cs_main is currently held unnecessarily validating snapshots, and the fact that
background chainstate is not immediately deleted when it is no longer used,
wasting disk space and adding a long startup delay next time the node is
restarted.

This follows up on some previous discussions:

bitcoin#24232 (comment)
bitcoin#27746 (comment)
bitcoin#28562 (comment)
@bitcoin bitcoin locked and limited conversation to collaborators Sep 10, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.