diff --git a/src/checkpoints.cpp b/src/checkpoints.cpp index 5d09f7dbee2..78b5c011367 100644 --- a/src/checkpoints.cpp +++ b/src/checkpoints.cpp @@ -81,6 +81,16 @@ namespace Checkpoints { return NULL; } + bool CheckBlock(const CCheckpointData& data, int nHeight, const uint256& hash) + { + const MapCheckpoints& checkpoints = data.mapCheckpoints; + + MapCheckpoints::const_iterator i = checkpoints.find(nHeight); + if (i == checkpoints.end()) + return true; + return hash == i->second; + } + static std::string FastSyncAnchorPayload(const CChainParams& chainparams, const CFastSyncAnchorData& anchor) { return strprintf("zclassic-fastsync-anchor-v1|%s|%d|%s", diff --git a/src/checkpoints.h b/src/checkpoints.h index 5b65ba5736a..baa410e6932 100644 --- a/src/checkpoints.h +++ b/src/checkpoints.h @@ -27,6 +27,11 @@ int GetTotalBlocksEstimate(const CCheckpointData& data); //! Returns last CBlockIndex* in mapBlockIndex that is a checkpoint CBlockIndex* GetLastCheckpoint(const CCheckpointData& data); +//! Returns false only if there is a checkpoint at nHeight and hash does not +//! match it. A block presented at a checkpoint height with a different hash is +//! a forgery and must be rejected (eclipse / bootstrap lock-in). +bool CheckBlock(const CCheckpointData& data, int nHeight, const uint256& hash); + double GuessVerificationProgress(const CCheckpointData& data, CBlockIndex* pindex, bool fSigchecks = true); //! Validate the compiled fast-sync anchor against the checkpoint set and digest fields. diff --git a/src/main.cpp b/src/main.cpp index eb98a68c72d..e9dce0340ec 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4389,6 +4389,17 @@ bool ContextualCheckBlockHeader(const CBlockHeader& block, CValidationState& sta CBlockIndex* pcheckpoint = Checkpoints::GetLastCheckpoint(chainParams.Checkpoints()); if (pcheckpoint && nHeight < pcheckpoint->nHeight) return state.DoS(100, error("%s: forked chain older than last checkpoint (height %d)", __func__, nHeight)); + + // Enforce the exact hash at checkpoint heights. A header presented at a + // checkpoint height with a different hash is a forgery. The forked-chain + // check above only covers heights strictly below the last checkpoint + // *present in mapBlockIndex*; this closes the gap at the checkpoint + // height itself and is independent of mapBlockIndex state, so it also + // protects a fresh/eclipsed node during bootstrap. + if (!Checkpoints::CheckBlock(chainParams.Checkpoints(), nHeight, hash)) + return state.DoS(100, error("%s: rejected by checkpoint lock-in at height %d (hash %s)", + __func__, nHeight, hash.ToString()), + REJECT_CHECKPOINT, "bad-fork-checkpoint"); } // Reject block.nVersion < 4 blocks diff --git a/src/test/Checkpoints_tests.cpp b/src/test/Checkpoints_tests.cpp index e31c8903cf9..755bfbb91e1 100644 --- a/src/test/Checkpoints_tests.cpp +++ b/src/test/Checkpoints_tests.cpp @@ -97,4 +97,26 @@ BOOST_AUTO_TEST_CASE(fast_sync_anchor_negative_branches) } } +BOOST_AUTO_TEST_CASE(checkpoint_hash_lockin) +{ + const uint256 hashA = uint256S("0x00000000000000000000000000000000000000000000000000000000000000aa"); + const uint256 hashB = uint256S("0x00000000000000000000000000000000000000000000000000000000000000bb"); + + CCheckpointData data{}; + data.mapCheckpoints[30000] = hashA; + data.mapCheckpoints[160000] = hashB; + + // No checkpoint at this height -> always accepted, regardless of hash. + BOOST_CHECK(Checkpoints::CheckBlock(data, 12345, hashA)); + BOOST_CHECK(Checkpoints::CheckBlock(data, 12345, hashB)); + + // Correct hash at a checkpoint height -> accepted. + BOOST_CHECK(Checkpoints::CheckBlock(data, 30000, hashA)); + BOOST_CHECK(Checkpoints::CheckBlock(data, 160000, hashB)); + + // Wrong hash at a checkpoint height -> rejected (the forgery case #2 closes). + BOOST_CHECK(!Checkpoints::CheckBlock(data, 30000, hashB)); + BOOST_CHECK(!Checkpoints::CheckBlock(data, 160000, hashA)); +} + BOOST_AUTO_TEST_SUITE_END()