CBlockIndex/CDiskBlockIndex improvements for safety, consistent behavior #25349

pull jonatack wants to merge 4 commits into bitcoin:master from jonatack:2022-06-CDiskBlockIndex-class-design changing 5 files +14 −23
  1. jonatack commented at 2:20 pm on June 12, 2022: contributor

    Fix a few design issues, potential footguns and inconsistent behavior in the CBlockIndex and CDiskBlockIndex classes.

    • Ensure phashBlock in CBlockIndex#GetBlockHash() is not nullptr before dereferencing and remove a now-redundant assert preceding a GetBlockHash() caller. This protects against UB here, and in case of failure (which would indicate a consensus bug), the debug log will print bitcoind: chain.h:265: uint256 CBlockIndex::GetBlockHash() const: Assertion 'phashBlock != nullptr' failed. Aborted instead of Segmentation fault.
    • Remove the unused CDiskBlockIndex#ToString() class member, and mark the inherited CBlockIndex#ToString() public interface member as deleted to disallow calling it in the derived CDiskBlockIndex class.
    • Rename the CDiskBlockIndex GetBlockHash() class member to ConstructBlockHash(), which also makes sense as they perform different operations to return a blockhash, and mark the inherited CBlockIndex#GetBlockHash() public interface member as deleted to disallow calling it in the derived CDiskBlockIndex class.
    • Move CBlockIndex#ToString() from header to implementation, which also allows dropping tinyformat.h from the header file.

    Rationale and discussion regarding the CDiskBlockIndex changes:

    Here is a failing test on master that demonstrates the inconsistent behavior of the current design: calling the same inherited public interface functions on the same CDiskBlockIndex object should yield identical behavior, but does not.

     0diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp
     1index 6dc522b421..dac3840f32 100644
     2--- a/src/test/validation_chainstatemanager_tests.cpp
     3+++ b/src/test/validation_chainstatemanager_tests.cpp
     4@@ -240,6 +240,15 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_activate_snapshot, TestChain100Setup)
     5
     6     const CBlockIndex* tip = chainman.ActiveTip();
     7
     8     BOOST_CHECK_EQUAL(tip->nChainTx, au_data.nChainTx);
     9
    10+    // CDiskBlockIndex "is a" CBlockIndex, as it publicly inherits from it.
    11+    // Test that calling the same inherited interface functions on the same
    12+    // object yields identical behavior.
    13+    CDiskBlockIndex index{tip};
    14+    CBlockIndex *pB = &index;
    15+    CDiskBlockIndex *pD = &index;
    16+    BOOST_CHECK_EQUAL(pB->GetBlockHash(), pD->GetBlockHash());
    17+    BOOST_CHECK_EQUAL(pB->ToString(), pD->ToString());
    

    (build and run: $ ./src/test/test_bitcoin -t validation_chainstatemanager_tests)

    The GetBlockHash() test assertion only passes on master because the different methods invoked by the current design happen to return the same result. If one of the two is changed, it fails like the ToString() assertion does.

    Redefining inherited non-virtual functions is well-documented as incorrect design to avoid inconsistent behavior (see Scott Meyers, Effective C++, Item 36). Class usage is confusing when the behavior depends on the pointer definition instead of the object definition (static binding happening where dynamic binding was expected). This can lead to unsuspected or hard-to-track bugs.

    Outside of critical hot spots, correctness usually comes before optimisation, but the current design dates back to main.cpp and it may possibly have been chosen to avoid the overhead of dynamic dispatch. This solution does the same: the class sizes are unchanged and no vptr or vtbl is added.

    There are better designs for doing this that use composition instead of inheritance, or that separate the public interface from the private implementations. One example of the latter would be a non-virtual public interface that calls private virtual implementation methods, i.e. the Template pattern via the Non-Virtual Interface (NVI) idiom.

  2. jonatack renamed this:
    refactor: CDiskBlockIndex ToString() and GetBlockHash() for consistent behavior and safety
    refactor: CDiskBlockIndex ToString() and GetBlockHash()
    on Jun 12, 2022
  3. jonatack force-pushed on Jun 12, 2022
  4. DrahtBot added the label Refactoring on Jun 12, 2022
  5. jonatack renamed this:
    refactor: CDiskBlockIndex ToString() and GetBlockHash()
    CDiskBlockIndex: remove ToString and rename GetBlockHash for consistent behavior
    on Jun 13, 2022
  6. jonatack renamed this:
    CDiskBlockIndex: remove ToString and rename GetBlockHash for consistent behavior
    CDiskBlockIndex: remove ToString() and rename GetBlockHash() for consistent behavior
    on Jun 13, 2022
  7. in src/chain.h:418 in 905a4d1b2c outdated
    413@@ -414,16 +414,9 @@ class CDiskBlockIndex : public CBlockIndex
    414         return block.GetHash();
    415     }
    416 
    417-
    418-    std::string ToString() const
    


    laanwj commented at 6:48 am on June 14, 2022:
    I’m somewhat ambivalent about removing this. The superclass has it so it can be considered part of the API, and it can be useful to show the contents of the structure when debugging/troubleshooting.

    jonatack commented at 7:06 am on June 14, 2022:

    Should we rename it instead?

    (Noting that the safe and correct way would be to use dynamic dispatch, like in https://github.com/jonatack/bitcoin/commits/CBlockIndex-CDiskBlockIndex-class-design, if the base and derived classes aren’t critical hotspots.)


    laanwj commented at 8:02 am on June 14, 2022:
    I agree it’s not worth it to set that up just for a debugging function.

    jonatack commented at 8:06 am on June 14, 2022:
    Right.

    jonatack commented at 6:00 pm on June 23, 2022:
    Resolving as the method is unused and can have inconsistent behavior as described in the OP, but please let me know if a change should be made.
  8. DrahtBot commented at 8:43 am on June 15, 2022: contributor

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

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #25311 (remove CBlockIndex copy construction by jamesob)
    • #25023 (Remove unused SetTip(nullptr) code 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.

  9. jonatack renamed this:
    CDiskBlockIndex: remove ToString() and rename GetBlockHash() for consistent behavior
    CDiskBlockIndex: remove unused ToString(), rename GetBlockHash() for consistent behavior
    on Jun 23, 2022
  10. jonatack commented at 6:03 pm on June 23, 2022: contributor
    The same approach (and a further design refactoring) were also suggested in #25311 (review).
  11. in src/chain.h:419 in 905a4d1b2c outdated
    424-            hashPrev.ToString());
    425-        return str;
    426-    }
    427+    // To be called only by the base class.
    428+    uint256 GetBlockHash() = delete;
    429+    uint256 ToString() = delete;
    


    vasild commented at 1:46 pm on July 13, 2022:

    Why return uint256 from ToString()? It is irrelevant in this case, but better have the same return type as the parent CBlockIndex::ToString() in order to avoid people asking this question :)

    0    std::string ToString() = delete;
    

    MarcoFalke commented at 1:58 pm on July 13, 2022:
    Deleted functions may also return void

    vasild commented at 2:26 pm on July 13, 2022:
    It may return anything because it is irrelevant, but if it is going to be different than the return type of the base method, then I would prefer it to return std::vector<double>.

    MarcoFalke commented at 2:45 pm on July 13, 2022:

    std::vector.

    I mentioned it because the standard library seems to be using void, but I couldn’t find the reference, so I didn’t mention the reason. I’d advise against using non-built in types, as this increases the include dependencies.


    jonatack commented at 6:23 am on July 15, 2022:

    Why return uint256 from ToString()? It is irrelevant in this case, but better have the same return type as the parent CBlockIndex::ToString() in order to avoid people asking this question :)

    Agree, fixed (thanks!)

  12. vasild approved
  13. vasild commented at 1:55 pm on July 13, 2022: contributor

    ACK 905a4d1b2c9ed6b28f2bfbb156bbb95b1d99c44e

    Removing unused code is always good IMO. If needed it can be resurrected from git history or re-implemented.

    Some non-blocker questions:

    Why is overriding non-virtual functions incorrect design?

    I think inheritance implies that the derived class wants to have all the properties of the base class. If it wants just some of them, then at some point maybe composition would be more appropriate than inheritance.

    The test from the commit message is expected to fail. If you access a Derived object through a pointer to Base, and both have the same method, then the Base’s method is to be called.

    Edit: now I see that the alternative approach in #25311 (review) uses composition instead of inheritance. If indeed the users of CDiskBlockIndex do not use all the stuff provided by CBlockIndex, then that is a better approach.

  14. jonatack force-pushed on Jul 15, 2022
  15. jonatack commented at 6:39 am on July 15, 2022: contributor

    Updated per git range-diff 02ede4f 905a4d1 7866c85 to take review feedback (thanks!)

    Some non-blocker questions:

    Why is overriding non-virtual functions incorrect design?

    To avoid inconsistent behavior. Public inheritance establishes an invariant over specialization for the derived class. Class usage is confusing when the behavior depends on the pointer definition instead of the object definition (static binding happening where dynamic binding was expected). This can lead to unsuspected or hard-to-track bugs. See Scott Meyers, Effective C++, Item 36: Inherited non-virtual functions should never be redefined, rephrased in urls like https://programmersought.com/article/50007468749/ and https://www.programminghunter.com/article/96792264397/. I updated the PR description with part of this paragraph.

    Edit: now I see that the alternative approach in #25311 (comment) uses composition instead of inheritance. If indeed the users of CDiskBlockIndex do not use all the stuff provided by CBlockIndex, then that is a better approach.

    I agree it could potentially be a better design as a follow-up / next step (it is a somewhat more invasive refactoring).

  16. vasild approved
  17. vasild commented at 1:00 pm on July 15, 2022: contributor

    ACK 7866c85387c3fa553437be1e004986b889fcd2de

    Thanks for the explanation and links!

    Now I have justification to clarify the naming of CNetAddr and CService methods which has been confusing me for a long time: https://github.com/bitcoin/bitcoin/pull/25619

  18. in src/chain.h:417 in 7866c85387 outdated
    422-        str += strprintf("\n                hashBlock=%s, hashPrev=%s)",
    423-            GetBlockHash().ToString(),
    424-            hashPrev.ToString());
    425-        return str;
    426-    }
    427+    // To be called only by the base class.
    


    MarcoFalke commented at 1:20 pm on July 15, 2022:
    I am pretty sure that constructing a CDiskBlockIndex, then calling the base class’s method on the constructed object will result in dereferencing a nullptr (UB)

    vasild commented at 4:24 pm on July 15, 2022:

    I don’t get it. Do you mean this?

     0class B
     1{
     2public:
     3    void f() { std::cout << "B::f()\n"; }
     4};
     5
     6class D : public B
     7{
     8public:
     9    void f() = delete;
    10};
    11
    12int main(int, char**)
    13{
    14    D d;
    15    static_cast<B*>(&d)->f();
    16    return 0;
    17}
    

    (it prints “B::f()”)


    jonatack commented at 4:41 pm on July 15, 2022:

    Running mainnet with the following patch:

     0diff --git a/src/txdb.cpp b/src/txdb.cpp
     1index bad3bb80a9..5078ddb6bf 100644
     2--- a/src/txdb.cpp
     3+++ b/src/txdb.cpp
     4@@ -310,7 +310,9 @@ bool CBlockTreeDB::LoadBlockIndexGuts(const Consensus::Params& consensusParams,
     5             CDiskBlockIndex diskindex;
     6             if (pcursor->GetValue(diskindex)) {
     7                 // Construct block index object
     8                 CBlockIndex* pindexNew = insertBlockIndex(diskindex.ConstructBlockHash());
     9+                assert(pindexNew);
    10+                LogPrintf("CBI 1: %s\n", pindexNew->ToString());
    11                 pindexNew->pprev          = insertBlockIndex(diskindex.hashPrev);
    12                 pindexNew->nHeight        = diskindex.nHeight;
    13                 pindexNew->nFile          = diskindex.nFile;
    14@@ -323,6 +325,7 @@ bool CBlockTreeDB::LoadBlockIndexGuts(const Consensus::Params& consensusParams,
    15                 pindexNew->nNonce         = diskindex.nNonce;
    16                 pindexNew->nStatus        = diskindex.nStatus;
    17                 pindexNew->nTx            = diskindex.nTx;
    18+                LogPrintf("CBI 2: %s\n", pindexNew->ToString());
    19 
    20                 if (!CheckProofOfWork(pindexNew->GetBlockHash(), pindexNew->nBits, consensusParams)) {
    21                     return error("%s: CheckProofOfWork failed: %s", __func__, pindexNew->ToString());
    

    prints (many) valid-looking objects.

    02022-07-15T16:34:08Z CBI 1: CBlockIndex(pprev=0, nHeight=0, merkle=0000000000000000000000000000000000000000000000000000000000000000, hashBlock=000000000000000000597281aa1ec7e99f0fff6eeda0db8707f77709951ba61a)
    12022-07-15T16:34:08Z CBI 2: CBlockIndex(pprev=0x55d02f11c778, nHeight=478600, merkle=40e759d1ea04b507003f1177a66c39ec2783eb464f7d867c3d7f401fb168f2aa, hashBlock=000000000000000000597281aa1ec7e99f0fff6eeda0db8707f77709951ba61a)
    2
    32022-07-15T16:34:08Z CBI 1: CBlockIndex(pprev=0, nHeight=0, merkle=0000000000000000000000000000000000000000000000000000000000000000, hashBlock=00000000000000000506e18c023143c5c3421399d3582be8aa4b263b8f69a61a)
    42022-07-15T16:34:08Z CBI 2: CBlockIndex(pprev=0x55d02f11c918, nHeight=427311, merkle=29af1206ec66690b1a0e51c2b07e6ead88056e7c11746c4d448f47e161cbbe39, hashBlock=00000000000000000506e18c023143c5c3421399d3582be8aa4b263b8f69a61a)
    

    and with this patch calling the base function from the child class that is deleted in the child class:

     0diff --git a/src/txdb.cpp b/src/txdb.cpp
     1index bad3bb80a9..c048c2d92a 100644
     2--- a/src/txdb.cpp
     3+++ b/src/txdb.cpp
     4@@ -310,7 +310,7 @@ bool CBlockTreeDB::LoadBlockIndexGuts(const Consensus::Params& consensusParams,
     5             CDiskBlockIndex diskindex;
     6             if (pcursor->GetValue(diskindex)) {
     7                 // Construct block index object
     8-                CBlockIndex* pindexNew = insertBlockIndex(diskindex.ConstructBlockHash());
     9+                CBlockIndex* pindexNew = insertBlockIndex(diskindex.GetBlockHash());
    10                 pindexNew->pprev          = insertBlockIndex(diskindex.hashPrev);
    11                 pindexNew->nHeight        = diskindex.nHeight;
    

    I see the following build error:

    0txdb.cpp:313:69: error: attempt to use a deleted function
    1                CBlockIndex* pindexNew = insertBlockIndex(diskindex.GetBlockHash());
    2                                                                    ^
    3./chain.h:418:13: note: 'GetBlockHash' has been explicitly marked deleted here
    4    uint256 GetBlockHash() = delete;
    5            ^
    61 error generated.
    

    Let me know if I’m misunderstanding or missing something.


    MarcoFalke commented at 8:04 am on July 16, 2022:

    I mean that calling the base class method on a disk index object is UB:

     0diff --git a/src/txdb.cpp b/src/txdb.cpp
     1index c048c2d92a..de1384fb61 100644
     2--- a/src/txdb.cpp
     3+++ b/src/txdb.cpp
     4@@ -309,6 +309,7 @@ bool CBlockTreeDB::LoadBlockIndexGuts(const Consensus::Params& consensusParams,
     5         if (pcursor->GetKey(key) && key.first == DB_BLOCK_INDEX) {
     6             CDiskBlockIndex diskindex;
     7             if (pcursor->GetValue(diskindex)) {
     8+                static_cast<CBlockIndex&>(diskindex).ToString();
     9                 // Construct block index object
    10                 CBlockIndex* pindexNew = insertBlockIndex(diskindex.GetBlockHash());
    11                 pindexNew->pprev          = insertBlockIndex(diskindex.hashPrev);
    
     0==563291== Invalid read of size 16
     1==563291==    at 0x2192B0: GetBlockHash (chain.h:266)
     2==563291==    by 0x2192B0: CBlockIndex::ToString[abi:cxx11]() const (chain.h:309)
     3==563291==    by 0x3CD954: CBlockTreeDB::LoadBlockIndexGuts(Consensus::Params const&, std::function<CBlockIndex* (uint256 const&)>) (txdb.cpp:312)
     4==563291==    by 0x210BBF: node::BlockManager::LoadBlockIndex(Consensus::Params const&) (blockstorage.cpp:258)
     5==563291==    by 0x2114AD: node::BlockManager::LoadBlockIndexDB(Consensus::Params const&) (blockstorage.cpp:323)
     6==563291==    by 0x410C0B: ChainstateManager::LoadBlockIndex() (validation.cpp:4148)
     7==563291==    by 0x222EEB: node::LoadChainstate(bool, ChainstateManager&, CTxMemPool*, bool, bool, long, long, long, bool, bool, std::function<bool ()>, std::function<void ()>) (chainstate.cpp:53)
     8==563291==    by 0x1749A4: AppInitMain(node::NodeContext&, interfaces::BlockAndHeaderTipInfo*) (init.cpp:1456)
     9==563291==    by 0x159A31: AppInit (bitcoind.cpp:234)
    10==563291==    by 0x159A31: main (bitcoind.cpp:278)
    11==563291==  Address 0x0 is not stack'd, malloc'd or (recently) free'd
    12==563291== 
    13==563291== 
    14==563291== Process terminating with default action of signal 11 (SIGSEGV): dumping core
    15==563291==  Access not within mapped region at address 0x0
    16==563291==    at 0x2192B0: GetBlockHash (chain.h:266)
    17==563291==    by 0x2192B0: CBlockIndex::ToString[abi:cxx11]() const (chain.h:309)
    18==563291==    by 0x3CD954: CBlockTreeDB::LoadBlockIndexGuts(Consensus::Params const&, std::function<CBlockIndex* (uint256 const&)>) (txdb.cpp:312)
    19==563291==    by 0x210BBF: node::BlockManager::LoadBlockIndex(Consensus::Params const&) (blockstorage.cpp:258)
    20==563291==    by 0x2114AD: node::BlockManager::LoadBlockIndexDB(Consensus::Params const&) (blockstorage.cpp:323)
    21==563291==    by 0x410C0B: ChainstateManager::LoadBlockIndex() (validation.cpp:4148)
    22==563291==    by 0x222EEB: node::LoadChainstate(bool, ChainstateManager&, CTxMemPool*, bool, bool, long, long, long, bool, bool, std::function<bool ()>, std::function<void ()>) (chainstate.cpp:53)
    23==563291==    by 0x1749A4: AppInitMain(node::NodeContext&, interfaces::BlockAndHeaderTipInfo*) (init.cpp:1456)
    24==563291==    by 0x159A31: AppInit (bitcoind.cpp:234)
    25==563291==    by 0x159A31: main (bitcoind.cpp:278)
    

    So I’d say the comment should be clarified that it is wholly illegal to call any such method on this object (whether through itself or a class it inherits from).


    jonatack commented at 9:33 am on July 16, 2022:

    Added the following belt-and-suspenders check to ensure we don’t dereference nullptr.

    0    std::string ToString() const
    1    {
    2        return strprintf("CBlockIndex(pprev=%p, nHeight=%d, merkle=%s, hashBlock=%s)",
    3            pprev, nHeight,
    4            hashMerkleRoot.ToString(),
    5-            GetBlockHash().ToString());
    6+            phashBlock ? GetBlockHash().ToString() : "");
    7    }
    

    vasild commented at 10:27 am on July 20, 2022:

    I am pretty sure that constructing a CDiskBlockIndex, then calling the base class’s method on the constructed object will result in dereferencing a nullptr (UB)

    I see your point now.

    0CDiskBlockIndex diskindex;
    1// Will crash because `phashBlock` is nullptr.
    2static_cast<CBlockIndex&>(diskindex).ToString();
    

    But then, the following will also crash for the same reason:

    0CBlockIndex index;
    1index.ToString();
    

    The crash is not related to the inheritance or the deleted methods from this PR.


    MarcoFalke commented at 11:52 am on July 20, 2022:

    The crash is not related to the inheritance or the deleted methods from this PR.

    Correct, this is why I commented on the documentation, which seemed to encourage calling the deleted method on the base class instead.


    jonatack commented at 6:29 pm on July 21, 2022:
    Dropped the documentation and moved the fix that ensures phashBlock in CBlockIndex#ToString() is not nullptr before dereferencing to the first commit of this changeset. Don’t hesitate to propose a code comment if you think one is still a good idea.
  19. in src/chain.cpp:19 in 2ececec41e outdated
    14 
    15+std::string CBlockIndex::ToString() const
    16+{
    17+    return strprintf("CBlockIndex(pprev=%p, nHeight=%d, merkle=%s, hashBlock=%s)",
    18+                     pprev, nHeight, hashMerkleRoot.ToString(),
    19+                     phashBlock ? GetBlockHash().ToString() : "");
    


    vasild commented at 9:49 am on July 20, 2022:

    nit1: This could print hashBlock=). Maybe better to omit hashBlock altogether:

    0return strprintf("CBlockIndex(pprev=%p, nHeight=%d, merkle=%s%s)",
    1                 pprev, nHeight, hashMerkleRoot.ToString(),
    2                 phashBlock ? ", hashBlock=" + GetBlockHash().ToString() : "");
    

    nit2: This assumes that GetBlockHash() will dereference phashBlock - it kind of “knows” what GetBlockHash() will do, which defeats the purpose of isolating/abstracting some code into that method. Given that this is happening internally in CBlockIndex I think it would better be:

    0phashBlock ? phashBlock->ToString() : ""
    

    jonatack commented at 6:30 pm on July 21, 2022:
    Agree, done both, thanks!
  20. in src/chain.h:266 in 2ececec41e outdated
    262@@ -263,7 +263,7 @@ class CBlockIndex
    263 
    264     uint256 GetBlockHash() const
    265     {
    266-        return *phashBlock;
    267+        return *Assert(phashBlock);
    


    vasild commented at 10:21 am on July 20, 2022:

    I think this should be turned into assert(phashBlock); (or even better assert(phashBlock != nullptr)). It will crash for sure even in release builds where Assert() is a noop. Better provide some meaningful message in that case (from the assert), before crashing. Then this assert can be removed:

    https://github.com/bitcoin/bitcoin/blob/895937edb2cca3046640dc016827a216e0b6d1c1/src/validation.cpp#L2269-L2271


    jonatack commented at 6:30 pm on July 21, 2022:
    Done and done, thanks!
  21. jonatack renamed this:
    CDiskBlockIndex: remove unused ToString(), rename GetBlockHash() for consistent behavior
    CBlockIndex and CDiskBlockIndex improvements for safety and consistent behavior
    on Jul 21, 2022
  22. jonatack renamed this:
    CBlockIndex and CDiskBlockIndex improvements for safety and consistent behavior
    CBlockIndex/CDiskBlockIndex improvements for safety, consistent behavior
    on Jul 21, 2022
  23. jonatack force-pushed on Jul 21, 2022
  24. jonatack commented at 6:36 pm on July 21, 2022: contributor
    Updated per review feedback by @vasild and @MarcoFalke (thanks!)
  25. in src/chain.h:309 in 0ddad240dc outdated
    302@@ -303,10 +303,10 @@ class CBlockIndex
    303 
    304     std::string ToString() const
    305     {
    306-        return strprintf("CBlockIndex(pprev=%p, nHeight=%d, merkle=%s, hashBlock=%s)",
    307+        return strprintf("CBlockIndex(pprev=%p, nHeight=%d, merkle=%s%s)",
    308             pprev, nHeight,
    309             hashMerkleRoot.ToString(),
    310-            GetBlockHash().ToString());
    311+            phashBlock ? ", hashBlock=" + phashBlock->ToString() : "");
    


    MarcoFalke commented at 6:43 pm on July 21, 2022:

    0ddad240dcd43b46ee70ccfb112d66d655b5c34f:

    Missing the hash is a critical consensus bug. I think it is better to assert than to silently continue when there is a bug.


    jonatack commented at 7:00 pm on July 21, 2022:
    Good idea, updating.
  26. jonatack force-pushed on Jul 21, 2022
  27. in src/chain.cpp:17 in 306cd9defe outdated
    12     return strprintf("CBlockFileInfo(blocks=%u, size=%u, heights=%u...%u, time=%s...%s)", nBlocks, nSize, nHeightFirst, nHeightLast, FormatISO8601Date(nTimeFirst), FormatISO8601Date(nTimeLast));
    13 }
    14 
    15+std::string CBlockIndex::ToString() const
    16+{
    17+    assert(phashBlock != nullptr);
    


    MarcoFalke commented at 6:06 am on July 22, 2022:
    no need to call the assert twice in this function?

    vasild commented at 10:50 am on July 22, 2022:
    Right, the first commit bc7d14164b CBlockIndex: ensure phashBlock is not nullptr before dereferencing seems unnecessary.

    jonatack commented at 11:04 am on July 22, 2022:
    Indeed. Done.
  28. CBlockIndex: ensure phashBlock is not nullptr before dereferencing
    and remove a now-redundant assert preceding a GetBlockHash() caller.
    
    This protects against UB here, and in case of failure (which would
    indicate a consensus bug), the debug log will print
    
    bitcoind: chain.h:265: uint256 CBlockIndex::GetBlockHash() const: Assertion `phashBlock != nullptr' failed.
    Aborted
    
    instead of
    
    Segmentation fault
    14aeece462
  29. CDiskBlockIndex: remove unused ToString() class member
    and mark its inherited CBlockIndex#ToString public interface member
    as deleted, to disallow calling it in the derived CDiskBlockIndex class.
    99e8ec8721
  30. CDiskBlockIndex: rename GetBlockHash() to ConstructBlockHash()
    and mark the inherited CBlockIndex#GetBlockHash public interface member
    as deleted, to disallow calling it in the derived CDiskBlockIndex class.
    
    Here is a failing test on master demonstrating the inconsistent behavior of the
    current design: calling the same inherited public interface functions on the
    same CDiskBlockIndex object should yield identical behavior.
    
    ```diff
    diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp
    index 6dc522b421..dac3840f32 100644
    --- a/src/test/validation_chainstatemanager_tests.cpp
    +++ b/src/test/validation_chainstatemanager_tests.cpp
    @@ -240,6 +240,15 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_activate_snapshot, TestChain100Setup)
    
         const CBlockIndex* tip = chainman.ActiveTip();
    
         BOOST_CHECK_EQUAL(tip->nChainTx, au_data.nChainTx);
    
    +    // CDiskBlockIndex "is a" CBlockIndex, as it publicly inherits from it.
    +    // Test that calling the same inherited interface functions on the same
    +    // object yields identical behavior.
    +    CDiskBlockIndex index{tip};
    +    CBlockIndex *pB = &index;
    +    CDiskBlockIndex *pD = &index;
    +    BOOST_CHECK_EQUAL(pB->GetBlockHash(), pD->GetBlockHash());
    +    BOOST_CHECK_EQUAL(pB->ToString(), pD->ToString());
    +
    ```
    
    The GetBlockHash() test assertion only passes on master because the different
    methods invoked by the current design happen to return the same result.  If one
    of the two is changed, it fails like the ToString() assertion does.
    
    Redefining inherited non-virtual functions is well-documented as incorrect
    design to avoid inconsistent behavior (see Scott Meyers, "Effective C++", Item
    36).  Class usage is confusing when the behavior depends on the pointer
    definition instead of the object definition (static binding happening where
    dynamic binding was expected).  This can lead to unsuspected or hard-to-track
    bugs.
    
    Outside of critical hot spots, correctness usually comes before optimisation,
    but the current design dates back to main.cpp and it may possibly have been
    chosen to avoid the overhead of dynamic dispatch.  This solution does the same:
    the class sizes are unchanged and no vptr or vtbl is added.
    
    There are better designs for doing this that use composition instead of
    inheritance or that separate the public interface from the private
    implementations.  One example of the latter would be a non-virtual public
    interface that calls private virtual implementation methods, i.e. the Template
    pattern via the Non-Virtual Interface (NVI) idiom.
    57865eb512
  31. refactor: move CBlockIndex#ToString() from header to implementation
    which allows dropping tinyformat.h from the header file.
    3a61fc56a0
  32. jonatack force-pushed on Jul 22, 2022
  33. jonatack commented at 11:05 am on July 22, 2022: contributor
    Updated per review feedback by @MarcoFalke (thanks!)
  34. vasild approved
  35. vasild commented at 11:17 am on July 22, 2022: contributor
    ACK 3a61fc56a0ad6ed58570350dcfd9ed2d10239b48
  36. MarcoFalke merged this on Jul 25, 2022
  37. MarcoFalke closed this on Jul 25, 2022

  38. jonatack deleted the branch on Jul 25, 2022
  39. sidhujag referenced this in commit 3af0f9a84c on Jul 25, 2022
  40. achow101 referenced this in commit 35fbc97208 on Feb 17, 2023
  41. bitcoin locked this on Jul 25, 2023

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2024-07-05 19:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me