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 printbitcoind: chain.h:265: uint256 CBlockIndex::GetBlockHash() const: Assertion 'phashBlock != nullptr' failed. Aborted
instead ofSegmentation fault
. - Remove the unused
CDiskBlockIndex#ToString()
class member, and mark the inheritedCBlockIndex#ToString()
public interface member as deleted to disallow calling it in the derived CDiskBlockIndex class. - Rename the
CDiskBlockIndex GetBlockHash()
class member toConstructBlockHash()
, which also makes sense as they perform different operations to return a blockhash, and mark the inheritedCBlockIndex#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 droppingtinyformat.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.