diff --git a/RELEASE_v9.26.2.md b/RELEASE_v9.26.2.md index c09028bb6c..39f9a2fa48 100644 --- a/RELEASE_v9.26.2.md +++ b/RELEASE_v9.26.2.md @@ -35,6 +35,102 @@ Mainnet is normal mainnet: Do not use the old pre-mainnet test ports, temporary data directories, or modified activation settings with this release. +## Critical Consensus Fix: Retired-Algorithm Enforcement (Groestl) + +Separately from DigiDollar, v9.26.2 ships an urgent consensus security fix. +**Every node on the DigiByte network must upgrade to v9.26.2.** This is not +optional and is independent of whether you use DigiDollar. + +### What Happened + +DigiByte secures the chain with five mining algorithms (SHA256d, Scrypt, Skein, +Qubit, Odocrypt). A sixth algorithm, Groestl, was retired in 2019 at the Odocrypt +fork. The rule that rejected retired-algorithm blocks existed in the v7.17.3-era +software, but it was accidentally dropped during the v8 Bitcoin Core rebase in +2021/2022. The function that knew Groestl was retired still existed and was used +for difficulty and display, but the single line that enforced it when accepting a +block was gone. Because nobody mined Groestl, its difficulty fell to the lowest +possible setting and the gap stayed dormant for years. + +Starting 2026-06-28 16:40:05 UTC, at block 23,751,096, an actor — using AI to +analyze DigiByte's consensus rules — reactivated Groestl and began mining a sixth +algorithm at floor difficulty. + +No coins were stolen and no confirmed transactions were reversed. There is no +evidence of a successful 51% attack, though the cheap mining is consistent with an +attempt: across every competing branch the network has seen, none ever accumulated +more total work than the honest chain, and the deepest reorganization of the +active chain was 4 blocks. The damage was instability and a network split. Block +times dropped from the 15-second target to roughly 12-13 seconds, and the network +divided, because v8/v9 software accepted the Groestl blocks while old v7.17.3 +software rejected them and forked onto a separate, slower chain. + +### The Fix + +v9.26.2 restores retired-algorithm enforcement, the right way: + +- Blocks using a retired (Groestl) or unknown mining algorithm are rejected. +- The rule is enforced in both header validation and block connection, so that + `-reindex` and `-reindex-chainstate` also enforce it. A node cannot carry a + post-activation retired-algorithm block forward after upgrading. +- Existing Groestl blocks already buried in the chain are grandfathered (kept). + No history is rewritten and no transactions are reversed. + +### Mainnet Activation Parameters (Groestl Deactivation) + +| Field | Value | +| --- | --- | +| Deployment name | `algolock` | +| Versionbit (readiness signal) | `0` | +| Signaling start | June 29, 2026 | +| Activation height (backstop) | `23,808,000` (~7 days after release) | +| Enforcement | reject retired (Groestl) and unknown algorithms | +| Existing Groestl blocks | grandfathered below the activation height | + +The `algolock` versionbit is a readiness signal that lets miners advertise they +have upgraded; you can watch adoption with `getdeploymentinfo`. The rule activates +at block 23,808,000 regardless of signaling, giving the network roughly seven days +to upgrade before retired-algorithm blocks are rejected. + +### All Nodes Must Upgrade + +Every full node, miner, pool, exchange, explorer, wallet, and service must upgrade +to v9.26.2. The majority of mining power is currently on the v8 line. That is fine, +but all of it must move to v9.26.2 so that the upgraded chain is the strongest +chain at activation and the network converges back onto a single chain. + +### v7.17.3 And Older Nodes Must Reindex On Upgrade + +Nodes running the ~7-year-old v7.17.3 line reject every post-Odocrypt Groestl +block, including the grandfathered blocks the healed chain keeps. They therefore +cannot follow the unified chain on their own. Operators on v7.17.3 (or older) must: + +1. Upgrade to v9.26.2. +2. Reindex or resync (start with `-reindex`, or perform a fresh sync) so the node + accepts the existing chain history and reorganizes onto the correct chain. + +Upgrading also provides Taproot, DigiDollar, and every other improvement added +since v7.17.3. + +### Miners And Pools: The 7-Day Window + +- Upgrade to v9.26.2 within the ~7-day window before block 23,808,000. +- Use the block version returned by DigiByte Core and do not strip the `algolock` + readiness signal. +- Once the majority of mining power is upgraded, retired-algorithm blocks are + orphaned, the attack ends, and the network heals into one chain. +- Optional, during the window only: miners who wish to actively push back can point + hash power at Groestl themselves. This captures block rewards that would + otherwise go to the attacker and drives Groestl difficulty up, removing the + cheap-mining advantage. This is secondary to upgrading and keeps block times fast + while it lasts; after activation, Groestl is rejected regardless. + +Forensic detail (as of this release, mining was still ongoing): the Groestl blocks +account for roughly 14-15% of all blocks since onset and are paid to two attacker +payout addresses — `dgb1qy5epvfs535a96tygn945a3a85lauh3ddu9v63y` (coinbase tag +`SORG`) and `D8S5JWaCrpFsryGG1c9AzWKhbS7e7VZ4r8`. Exchanges and custodians should +flag deposits originating from these addresses until the network has healed. + ## Who Should Upgrade All full nodes, miners, mining pools, exchanges, explorers, wallets, service diff --git a/src/consensus/params.h b/src/consensus/params.h index 76e3c2398f..3b324d8b33 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -41,6 +41,7 @@ enum DeploymentPos : uint16_t { DEPLOYMENT_TESTDUMMY, DEPLOYMENT_TAPROOT, // Deployment of Schnorr/Taproot (BIPs 340-342) DEPLOYMENT_DIGIDOLLAR, // Deployment of DigiDollar stablecoin features + DEPLOYMENT_ALGOLOCK, // Reject blocks mined with a deactivated (e.g. retired Groestl) or unknown algorithm // NOTE: Also add new deployments to VersionBitsDeploymentInfo in deploymentinfo.cpp MAX_VERSION_BITS_DEPLOYMENTS }; @@ -110,6 +111,10 @@ struct Params { /** * Block height at which Odocrypt got activated */ int OdoHeight; + /** + * Block height at which blocks using a deactivated mining algorithm + * (e.g. the retired Groestl) or an unknown algorithm are rejected. */ + int nGroestlDeactivationHeight{std::numeric_limits::max()}; /** Don't warn about unknown BIP 9 activations below this height. * This prevents us from warning about the CSV and segwit activations. */ int MinBIP9WarningHeight; diff --git a/src/deploymentinfo.cpp b/src/deploymentinfo.cpp index 8fd0cea5c8..c74efc47a6 100644 --- a/src/deploymentinfo.cpp +++ b/src/deploymentinfo.cpp @@ -24,6 +24,10 @@ const struct VBDeploymentInfo VersionBitsDeploymentInfo[Consensus::MAX_VERSION_B /*.name =*/ "digidollar", /*.gbt_force =*/ true, }, + { + /*.name =*/ "algolock", + /*.gbt_force =*/ true, + }, }; std::string DeploymentName(Consensus::BuriedDeployment dep) diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index d13fef1a97..333fb0520e 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -124,6 +124,7 @@ class CMainParams : public CChainParams { consensus.workComputationChangeTarget = 1430000; // Block 1,430,000 DigiSpeed Hard Fork consensus.algoSwapChangeTarget = 9100000; // Block 9,100,000 Odo PoW Hard Fork consensus.OdoHeight = 9112320; // 906b712a7b1f54f10b0faf86111e832ddb7b8ce86ac71a4edd2c61e5ccfe9428 + consensus.nGroestlDeactivationHeight = 23808000; // v9.26.2 activation (~7 days for miner upgrade): reject reactivated Groestl / unknown algos consensus.ReserveAlgoBitsHeight = 8547840; // d2c03966aeef35f739b222c8332b68df2676204d49c390b3a2544b967c46163f // DigiByte-specific difficulty adjustment parameters @@ -178,6 +179,13 @@ class CMainParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_DIGIDOLLAR].nStartTime = 1780272000; // June 1, 2026 consensus.vDeployments[Consensus::DEPLOYMENT_DIGIDOLLAR].nTimeout = 1811808000; // June 1, 2027 consensus.vDeployments[Consensus::DEPLOYMENT_DIGIDOLLAR].min_activation_height = 23627520; // Aligned to confirmation window (586 * 40320) + // ALGOLOCK: reject reactivated Groestl / unknown-algo blocks. BIP9 bit 0, signalling + // starts immediately so miners can lock in early; nGroestlDeactivationHeight is the + // mandatory unconditional backstop so activation cannot be vetoed or stalled. + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].bit = 0; + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].nStartTime = 1782691200; // June 29, 2026 (signalling open) + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].nTimeout = 1814227200; // June 29, 2027 + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].min_activation_height = 0; // may activate as soon as it locks in // The best chain should have at least this much work. consensus.nMinimumChainWork = uint256S("0x0000000000000000000000000000000000000000001cae290ed41eb2efd4804c"); @@ -217,6 +225,7 @@ class CMainParams : public CChainParams { vSeeds.emplace_back("seed.diginode.tools"); // Olly Stedall @saltedlolly vSeeds.emplace_back("seed.digibyte.link"); // Bastian Driessen @bastiandriessen vSeeds.emplace_back("seed.aroundtheblock.app"); // Mark McNiel @JohnnyLawDGB + vSeeds.emplace_back("seed.tuyul.cc"); // Mbah Jambon @mbah_jambon base58Prefixes[PUBKEY_ADDRESS] = std::vector(1,30); base58Prefixes[SCRIPT_ADDRESS_OLD] = std::vector(1,5); @@ -294,6 +303,18 @@ class CMainParams : public CChainParams { // Initialize DigiDollar Oracle Nodes (35 active slots) InitializeOracleNodes(); + // Public mainnet peers that oracle operators can use to bootstrap + // DigiDollar P2P connectivity. These are operational seed peers, not + // consensus trust anchors; oracle security comes from the hardcoded + // public keys and 7-of-35 MuSig2 validation. + vOracleSeedPeers = { + "oracle1.digibyte.io:12024", // DigiByte.io / Jared Tate + "digihash.digibyte.io:12024", // DigiHash Mining Pool + "oracleseed.digibyte.link:12024", // Bastian Driessen + "digiscope.me:12024", // DigiScope / JohnnyLawDGB + "oracle.dgbmaxi.com:12024", // digibyte-maxi / Ycagel + }; + // Mainnet-specific oracle and activation settings consensus.nDDOracleEpochBlocks = 40; // Rotate oracle signing epochs every 40 blocks (~10 minutes) consensus.nDDOracleUpdateInterval = 4; // Update price every 4 blocks (~1 minute) @@ -511,6 +532,10 @@ class CTestNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_DIGIDOLLAR].nStartTime = 1780156800; // testnet26 genesis timestamp consensus.vDeployments[Consensus::DEPLOYMENT_DIGIDOLLAR].nTimeout = 1830297600; // Jan 1, 2028 consensus.vDeployments[Consensus::DEPLOYMENT_DIGIDOLLAR].min_activation_height = 600; // Activation delayed until block 600 + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].bit = 0; + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE; + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].min_activation_height = 0; consensus.nMinimumChainWork = uint256S("0x00"); consensus.defaultAssumeValid = uint256S("0x00"); //1079274 @@ -963,6 +988,10 @@ class SigNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_DIGIDOLLAR].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE; consensus.vDeployments[Consensus::DEPLOYMENT_DIGIDOLLAR].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; consensus.vDeployments[Consensus::DEPLOYMENT_DIGIDOLLAR].min_activation_height = 0; // No activation delay for ALWAYS_ACTIVE + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].bit = 0; + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE; + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].min_activation_height = 0; // message start is defined as the first 4 bytes of the sha256d of the block script HashWriter h{}; @@ -1024,6 +1053,7 @@ class CRegTestParams : public CChainParams consensus.SegwitHeight = 0; // Always active unless overridden consensus.ReserveAlgoBitsHeight = 0; // DigiByte ReserveAlgoBits consensus.OdoHeight = 600; // DigiByte Odocrypt height + consensus.nGroestlDeactivationHeight = 0; // enforce deactivated-algo rejection from genesis on regtest consensus.MinBIP9WarningHeight = 0; consensus.powLimit = uint256S("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // Set initial targets for all algorithms (easy difficulty for regtest) @@ -1093,6 +1123,10 @@ class CRegTestParams : public CChainParams consensus.vDeployments[Consensus::DEPLOYMENT_DIGIDOLLAR].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE; consensus.vDeployments[Consensus::DEPLOYMENT_DIGIDOLLAR].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; consensus.vDeployments[Consensus::DEPLOYMENT_DIGIDOLLAR].min_activation_height = 0; // No activation delay for ALWAYS_ACTIVE + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].bit = 0; + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE; + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_ALGOLOCK].min_activation_height = 0; consensus.nMinimumChainWork = uint256{}; consensus.defaultAssumeValid = uint256{}; diff --git a/src/kernel/chainparams.h b/src/kernel/chainparams.h index 7e252da845..8db0c9b3a5 100644 --- a/src/kernel/chainparams.h +++ b/src/kernel/chainparams.h @@ -121,6 +121,8 @@ class CChainParams ChainType GetChainType() const { return m_chain_type; } /** Return the list of hostnames to look up for DNS seeds */ const std::vector& DNSSeeds() const { return vSeeds; } + /** Return public mainnet peers operators can use to bootstrap DigiDollar oracle P2P traffic */ + const std::vector& OracleSeedPeers() const { return vOracleSeedPeers; } const std::vector& Base58Prefix(Base58Type type) const { return base58Prefixes[type]; } const std::string& Bech32HRP() const { return bech32_hrp; } const std::vector& FixedSeeds() const { return vFixedSeeds; } @@ -200,6 +202,7 @@ class CChainParams uint64_t m_assumed_blockchain_size; uint64_t m_assumed_chain_state_size; std::vector vSeeds; + std::vector vOracleSeedPeers; std::vector base58Prefixes[MAX_BASE58_TYPES]; std::string bech32_hrp; ChainType m_chain_type; diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index c7e8c737bf..503ef99764 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1396,6 +1396,7 @@ UniValue DeploymentInfo(const CBlockIndex* blockindex, const ChainstateManager& SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_TESTDUMMY); SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_TAPROOT); SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_DIGIDOLLAR); + SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_ALGOLOCK); return softforks; } } // anon namespace diff --git a/src/rpc/digidollar.cpp b/src/rpc/digidollar.cpp index 11c1c1bc21..5bc910f023 100644 --- a/src/rpc/digidollar.cpp +++ b/src/rpc/digidollar.cpp @@ -1116,6 +1116,11 @@ static RPCHelpMan getdigidollardeploymentinfo() {RPCResult::Type::NUM, "oracle_pubkey_count", "Number of consensus oracle public keys configured for MuSig2 (nOraclePubkeyCount)"}, {RPCResult::Type::NUM, "oracle_consensus_required", "MuSig2 quorum size required to satisfy a v0x03 bundle (nOracleConsensusRequired)"}, {RPCResult::Type::NUM, "oracle_total_slots", "Total oracle slots configured in chainparams (vOracleNodes.size); oracle_id values must be below oracle_pubkey_count to vote"}, + {RPCResult::Type::ARR, "oracle_seed_peers", "Public mainnet peers operators can use to bootstrap DigiDollar oracle P2P connectivity", + { + {RPCResult::Type::STR, "peer", "Host:port seed peer"}, + } + }, {RPCResult::Type::OBJ, "musig2_session", "Current MuSig2 signing session status (operator diagnostic)", { {RPCResult::Type::NUM, "epoch", "Current epoch number (block_height / nDDOracleEpochBlocks)"}, @@ -1203,6 +1208,12 @@ static RPCHelpMan getdigidollardeploymentinfo() result.pushKV("oracle_consensus_required", consensusParams.nOracleConsensusRequired); result.pushKV("oracle_total_slots", static_cast(Params().GetOracleNodes().size())); + UniValue oracle_seed_peers(UniValue::VARR); + for (const auto& peer : Params().OracleSeedPeers()) { + oracle_seed_peers.push_back(peer); + } + result.pushKV("oracle_seed_peers", oracle_seed_peers); + // Wave 10 (Agent C): expose the orchestrator's MuSig2 session // status for the current epoch so operators can diagnose stuck // sessions (timeout / sub-quorum / liveness drift). The state diff --git a/src/test/oracle_config_tests.cpp b/src/test/oracle_config_tests.cpp index 085df8167c..e4edd7c039 100644 --- a/src/test/oracle_config_tests.cpp +++ b/src/test/oracle_config_tests.cpp @@ -14,6 +14,8 @@ #include #include +#include + BOOST_FIXTURE_TEST_SUITE(oracle_config_tests, BasicTestingSetup) /** @@ -386,6 +388,32 @@ BOOST_AUTO_TEST_CASE(mainnet_oracle_endpoints_use_mainnet_p2p_port) SelectParams(ChainType::MAIN); } +BOOST_AUTO_TEST_CASE(mainnet_oracle_seed_peers_include_launch_bootstrap_nodes) +{ + SelectParams(ChainType::MAIN); + const auto& seed_peers = Params().OracleSeedPeers(); + + BOOST_REQUIRE(!seed_peers.empty()); + + const std::vector expected_peers{ + "oracle1.digibyte.io:12024", + "digihash.digibyte.io:12024", + "oracleseed.digibyte.link:12024", + "digiscope.me:12024", + "oracle.dgbmaxi.com:12024", + }; + + for (const auto& expected_peer : expected_peers) { + BOOST_CHECK_MESSAGE(std::find(seed_peers.begin(), seed_peers.end(), expected_peer) != seed_peers.end(), + "Missing mainnet oracle seed peer " << expected_peer); + } + + for (const auto& peer : seed_peers) { + BOOST_CHECK_MESSAGE(peer.size() >= 6 && peer.rfind(":12024") == peer.size() - 6, + "Mainnet oracle seed peer must use P2P port 12024, got " << peer); + } +} + // ============================================================================ // PART 4: Network-Specific Configuration Tests (BONUS) // ============================================================================ diff --git a/src/test/pow_tests.cpp b/src/test/pow_tests.cpp index 4003c28331..5ff932b9e9 100644 --- a/src/test/pow_tests.cpp +++ b/src/test/pow_tests.cpp @@ -11,6 +11,7 @@ #include #include // For GetVersionForAlgo +#include // For IsAlgoActive BOOST_FIXTURE_TEST_SUITE(pow_tests, BasicTestingSetup) @@ -294,4 +295,43 @@ BOOST_AUTO_TEST_CASE(digibyte_difficulty_versions_test) } } +BOOST_AUTO_TEST_CASE(digibyte_isalgoactive_matrix) +{ + // The set of mining algorithms accepted at a given height. Groestl is part of + // the original MultiAlgo set but is deactivated at the Odocrypt fork; the + // consensus rule rejecting deactivated algorithms relies on this predicate. + const auto chainParams = CreateChainParams(*m_node.args, ChainType::REGTEST); + const auto& params = chainParams->GetConsensus(); + + // regtest fork heights: MultiAlgo at 100, Odocrypt/Groestl-swap at 600. + auto is_active = [&](int prev_height, int algo) { + CBlockIndex prev; + prev.nHeight = prev_height; + return IsAlgoActive(&prev, params, algo); + }; + + // Before the MultiAlgo fork: Scrypt only. + BOOST_CHECK(is_active(50, ALGO_SCRYPT)); + for (int algo : {ALGO_SHA256D, ALGO_GROESTL, ALGO_SKEIN, ALGO_QUBIT, ALGO_ODO}) { + BOOST_CHECK(!is_active(50, algo)); + } + + // MultiAlgo era (100..599): five algorithms including Groestl, excluding Odocrypt. + for (int algo : {ALGO_SHA256D, ALGO_SCRYPT, ALGO_GROESTL, ALGO_SKEIN, ALGO_QUBIT}) { + BOOST_CHECK(is_active(150, algo)); + } + BOOST_CHECK(!is_active(150, ALGO_ODO)); + BOOST_CHECK(is_active(599, ALGO_GROESTL)); // last block before the swap + + // Odocrypt era (>=600): Groestl is deactivated, Odocrypt is active. + BOOST_CHECK(!is_active(600, ALGO_GROESTL)); + BOOST_CHECK(!is_active(700, ALGO_GROESTL)); + for (int algo : {ALGO_SHA256D, ALGO_SCRYPT, ALGO_SKEIN, ALGO_QUBIT, ALGO_ODO}) { + BOOST_CHECK(is_active(700, algo)); + } + + // An unknown algorithm is never active. + BOOST_CHECK(!is_active(700, ALGO_UNKNOWN)); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/validation.cpp b/src/validation.cpp index ac4f088e03..bdef1863c0 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2849,6 +2849,24 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, return error("%s: Consensus::CheckBlock: %s", __func__, state.ToString()); } + // Reject blocks mined with a deactivated (e.g. retired Groestl) or unknown + // algorithm once ALGOLOCK is in force. This mirrors the ContextualCheckBlockHeader + // gate and is repeated here, at connection time, precisely to close the upgrade + // gap described above: -reindex-chainstate skips the header check, and a node that + // accepted a post-activation deactivated-algo block while running older software + // must not carry it forward on replay/reconnection. Blocks below the activation + // point are grandfathered identically to the header-path check. + if (pindex->pprev) { + const Consensus::Params& algo_consensus = params.GetConsensus(); + const int algo = block.GetAlgo(); + if ((DeploymentActiveAfter(pindex->pprev, m_chainman, Consensus::DEPLOYMENT_ALGOLOCK) || + pindex->nHeight >= algo_consensus.nGroestlDeactivationHeight) && + (algo == ALGO_UNKNOWN || !IsAlgoActive(pindex->pprev, algo_consensus, algo))) { + return state.Invalid(BlockValidationResult::BLOCK_INVALID_ALGO, "bad-algo", + "block uses a deactivated or unknown mining algorithm"); + } + } + if (!OracleDataValidator::ValidateBlockOracleData(block, pindex->pprev, params.GetConsensus(), state)) { return error("%s: OracleDataValidator::ValidateBlockOracleData: %s", __func__, state.ToString()); } @@ -4798,6 +4816,15 @@ static bool ContextualCheckBlockHeader(const CBlockHeader& block, BlockValidatio // Check proof of work const Consensus::Params& consensusParams = chainman.GetConsensus(); int algo = block.GetAlgo(); + + if (DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_ALGOLOCK) || + nHeight >= consensusParams.nGroestlDeactivationHeight) { + if (algo == ALGO_UNKNOWN || !IsAlgoActive(pindexPrev, consensusParams, algo)) { + return state.Invalid(BlockValidationResult::BLOCK_INVALID_ALGO, "bad-algo", + "block uses a deactivated or unknown mining algorithm"); + } + } + unsigned int nBitsExpected = GetNextWorkRequired(pindexPrev, &block, consensusParams, algo); // Debug logging for early blocks diff --git a/test/functional/feature_digibyte_groestl_deactivation.py b/test/functional/feature_digibyte_groestl_deactivation.py new file mode 100755 index 0000000000..c63e9afe67 --- /dev/null +++ b/test/functional/feature_digibyte_groestl_deactivation.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 The DigiByte Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Validator-level enforcement of active mining algorithms. + +A block mined with a deactivated algorithm (Groestl, retired at the Odocrypt +fork) must be rejected at block acceptance, not merely refused by the local +miner. This exercises the submit/validate path with externally-crafted blocks, +which is the path the local miner does not cover. + +On regtest the deactivated-algorithm rule is enforced from genesis +(nGroestlDeactivationHeight = 0) and Odocrypt activates at height 600, so above +height 600 Groestl is deactivated and crafted Groestl blocks must be rejected; +below it Groestl is still active and accepted (grandfathered). + +The rule is enforced both in ContextualCheckBlockHeader (header acceptance) and in +ConnectBlock (connection/replay). The latter is exercised by reindexing: a block +that was valid when mined (a pre-Odocrypt Groestl block) must survive -reindex and +-reindex-chainstate, proving the connection-time guard grandfathers by height +rather than rejecting the algorithm outright. +""" + +from test_framework.test_framework import DigiByteTestFramework +from test_framework.blocktools import create_block, create_coinbase +from test_framework.util import assert_equal + +ODOCRYPT_HEIGHT = 600 # Groestl deactivation / Odocrypt activation (regtest) + +BLOCK_VERSION_GROESTL = 0x20000402 +BLOCK_VERSION_SHA256D = 0x20000202 + + +class GroestlDeactivationTest(DigiByteTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [["-easypow=1"]] + self.mining_address = "dgbrt1qtmp74ayg7p24uslctssvjm06q5phz4yrgndnyh" + + def skip_test_if_missing_module(self): + pass + + def craft(self, node, version): + tip = node.getbestblockhash() + hdr = node.getblockheader(tip) + height = node.getblockcount() + 1 + return create_block(int(tip, 16), create_coinbase(height, nValue=0), + hdr["time"] + 1, version=version) + + def submit_until_pow_ok(self, node, block): + # -easypow keeps the target at powLimit, so ~half of nonces satisfy any + # algorithm's PoW. The node computes the algo-specific hash, so no Python + # implementation of Groestl is needed: grind until PoW passes, then return + # whatever the validator says next. + for nonce in range(2000): + block.nNonce = nonce + res = node.submitblock(block.serialize().hex()) + if res != "high-hash": + return res + raise AssertionError("could not satisfy easypow PoW") + + def run_test(self): + node = self.nodes[0] + + self.log.info("Below Odocrypt: Groestl is active, crafted Groestl block is accepted") + self.generatetoaddress(node, 150, self.mining_address, 1000000, "scrypt") + assert_equal(node.getblockcount(), 150) + accepted = self.craft(node, BLOCK_VERSION_GROESTL) + assert_equal(self.submit_until_pow_ok(node, accepted), None) + assert_equal(node.getblockcount(), 151) + + # Pin the exact boundary. IsAlgoActive() keys off pindexPrev->nHeight, so the + # Groestl block AT the Odocrypt height (600, prev 599 < 600) is still active and + # accepted; rejection begins one block later, at height 601 (prev 600). Asserting + # only "rejected somewhere above 600" would let an off-by-one in the height math + # slip through, so we exercise both edges. + self.log.info("At Odocrypt height: Groestl block 600 is still accepted (prev 599 < 600)") + self.generatetoaddress(node, ODOCRYPT_HEIGHT - 1 - node.getblockcount(), + self.mining_address, 1000000, "scrypt") + assert_equal(node.getblockcount(), ODOCRYPT_HEIGHT - 1) # tip 599 + boundary = self.craft(node, BLOCK_VERSION_GROESTL) + assert_equal(self.submit_until_pow_ok(node, boundary), None) # block 600 accepted + assert_equal(node.getblockcount(), ODOCRYPT_HEIGHT) + + self.log.info("One past Odocrypt height: the first rejected Groestl block is 601") + boundary_tip = node.getbestblockhash() + rejected601 = self.craft(node, BLOCK_VERSION_GROESTL) + assert_equal(self.submit_until_pow_ok(node, rejected601), "bad-algo") # block 601 rejected + assert_equal(node.getbestblockhash(), boundary_tip) + + self.log.info("Above Odocrypt: Groestl is deactivated, crafted Groestl block is rejected") + self.generatetoaddress(node, ODOCRYPT_HEIGHT + 5 - node.getblockcount(), + self.mining_address, 1000000, "scrypt") + assert node.getblockcount() > ODOCRYPT_HEIGHT + tip = node.getbestblockhash() + rejected = self.craft(node, BLOCK_VERSION_GROESTL) + assert_equal(self.submit_until_pow_ok(node, rejected), "bad-algo") + assert_equal(node.getbestblockhash(), tip) + + self.log.info("Control: an active algorithm via the same submit path is accepted") + control = self.craft(node, BLOCK_VERSION_SHA256D) + assert_equal(self.submit_until_pow_ok(node, control), None) + assert node.getbestblockhash() != tip + + self.log.info("Signalling flag: the algolock BIP9 deployment is exposed for tracking") + deployments = node.getdeploymentinfo()["deployments"] + assert "algolock" in deployments, "algolock deployment must be visible via getdeploymentinfo" + + self.log.info("Reindex-safety: the pre-Odocrypt Groestl block is grandfathered through replay") + # The chain contains a Groestl block at height 151 that was valid when mined + # (pre-Odocrypt). The ConnectBlock guard must grandfather it by height, so a + # full replay must reproduce the exact same tip and height. + final_tip = node.getbestblockhash() + final_height = node.getblockcount() + # Confirm height 151 really is the grandfathered Groestl block. + assert_equal(node.getblockheader(node.getblockhash(151))["version"], BLOCK_VERSION_GROESTL) + + self.restart_node(0, extra_args=["-easypow=1", "-reindex=1"]) + assert_equal(node.getbestblockhash(), final_tip) + assert_equal(node.getblockcount(), final_height) + + self.restart_node(0, extra_args=["-easypow=1", "-reindex-chainstate=1"]) + assert_equal(node.getbestblockhash(), final_tip) + assert_equal(node.getblockcount(), final_height) + + +if __name__ == '__main__': + GroestlDeactivationTest().main() diff --git a/test/functional/mining_basic.py b/test/functional/mining_basic.py index 12ddcbb471..a3cc37fb31 100755 --- a/test/functional/mining_basic.py +++ b/test/functional/mining_basic.py @@ -37,6 +37,9 @@ VERSIONBITS_TOP_BITS = 0x20000000 VERSIONBITS_DEPLOYMENT_TESTDUMMY_BIT = 27 VERSIONBITS_DEPLOYMENT_TAPROOT_BIT = 0x02 # DigiByte taproot bit +# DigiByte encodes the mining algorithm in version bits 8-11, so a block version +# must select a valid algorithm. Use a version whose algo nibble is SHA256D. +BLOCKVERSION_OVERRIDE = 0x20000202 DEFAULT_BLOCK_MIN_TX_FEE = 100000 # default `-blockmintxfee` setting [sat/kvB] - DigiByte uses higher fees @@ -73,9 +76,9 @@ def mine_chain(self): self.log.info('test blockversion') mock_time = TIME_GENESIS_BLOCK + block_count * 15 - self.restart_node(0, extra_args=[f'-mocktime={mock_time}', '-blockversion=1337', '-dandelion=0']) + self.restart_node(0, extra_args=[f'-mocktime={mock_time}', f'-blockversion={BLOCKVERSION_OVERRIDE}', '-dandelion=0']) self.connect_nodes(0, 1) - assert_equal(1337, self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)['version']) + assert_equal(BLOCKVERSION_OVERRIDE, self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)['version']) self.restart_node(0, extra_args=[f'-mocktime={mock_time}', '-dandelion=0']) self.connect_nodes(0, 1) assert_equal(VERSIONBITS_TOP_BITS + (1 << VERSIONBITS_DEPLOYMENT_TESTDUMMY_BIT) + VERSIONBITS_DEPLOYMENT_TAPROOT_BIT, self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)['version']) diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index c9e2d354ee..0e8f25b055 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -250,6 +250,19 @@ def check_signalling_deploymentinfo_result(self, gdi_result, height, blockhash, }, 'height': 0, 'active': True + }, + 'algolock': { + 'type': 'bip9', + 'bip9': { + 'start_time': -1, + 'timeout': 9223372036854775807, + 'min_activation_height': 0, + 'status': 'active', + 'status_next': 'active', + 'since': 0, + }, + 'height': 0, + 'active': True } } }) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 7968ce2f2d..c9e0359dfc 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -104,6 +104,7 @@ # DigiByte: Multi-Algorithm Mining Tests 'feature_digibyte_multialgo_mining.py', + 'feature_digibyte_groestl_deactivation.py', # vv Tests less than 5m vv 'feature_fee_estimation.py',