diff --git a/src/impl/nmc/coin/header_chain.hpp b/src/impl/nmc/coin/header_chain.hpp index bf06ccb93..d161e1e6f 100644 --- a/src/impl/nmc/coin/header_chain.hpp +++ b/src/impl/nmc/coin/header_chain.hpp @@ -646,6 +646,32 @@ class HeaderChain { header.m_bits); } + /// P1e: verdict for the activation-height admission gate. + enum class AdmitResult { + ADMIT, // height / auxpow-presence consistent (proof checked separately) + REJECT_PREMATURE_AUXPOW, // header carries an AuxPow below the activation height + REJECT_MISSING_AUXPOW, // no AuxPow at/after activation height (mainnet requirement) + REJECT_UNPINNED, // activation height still the -1 sentinel - refuse to judge + }; + + /// P1e: the activation-height GATE, factored out for unit-testing + /// independent of the (still-deferred) storage path - the same pattern as + /// verify_auxpow_header(). Decides admissibility from the connected `height` + /// and whether the header carries an AuxPow, BEFORE the four-leg proof is + /// walked: below activation an AuxPow is premature, at/after activation + /// (mainnet) one is mandatory. While auxpow_activation_height is the + /// unpinned -1 sentinel it refuses to judge (REJECT_UNPINNED) so the P1 leaf + /// never renders an activation verdict off a placeholder constant. + AdmitResult check_activation_gate(int32_t height, bool has_auxpow) const { + if (m_params.auxpow_activation_height < 0) + return AdmitResult::REJECT_UNPINNED; // TO-CONFIRM unpinned + if (!m_params.is_auxpow_active(height)) + return has_auxpow ? AdmitResult::REJECT_PREMATURE_AUXPOW + : AdmitResult::ADMIT; + return has_auxpow ? AdmitResult::ADMIT + : AdmitResult::REJECT_MISSING_AUXPOW; + } + /// Add a header that carries a merge-mining AuxPow proof. /// P1d: the AuxPow VERIFICATION GATE is now wired - check_proof() must /// return VALID or the header is rejected, so the (future) accept path can diff --git a/src/impl/nmc/test/auxpow_merkle_test.cpp b/src/impl/nmc/test/auxpow_merkle_test.cpp index 5ebc00f5f..dafadcb99 100644 --- a/src/impl/nmc/test/auxpow_merkle_test.cpp +++ b/src/impl/nmc/test/auxpow_merkle_test.cpp @@ -635,4 +635,65 @@ TEST(NmcP1dGate, MissingAuxBitsKeepsProofIncompleteAndRejected) EXPECT_FALSE(chain_.add_auxpow_header(h, ap)); } +// --------------------------------------------------------------------------- +// P1e: activation-height admission gate KATs (NmcP1eActivationGate). +// Exercise HeaderChain::check_activation_gate() - the height-derived MM +// activation gate, factored like the P1d proof gate so it is testable without +// the deferred storage path. The production activation height stays the -1 +// sentinel; these fixtures pin a TEST-only height (19200, the historically +// cited NMC mainnet value) purely to drive the gate - no consensus promotion. +// --------------------------------------------------------------------------- +static NMCChainParams params_activation(int32_t act_height) +{ + NMCChainParams p = NMCChainParams::mainnet(); + p.aux_chain_id = 1; + p.auxpow_activation_height = act_height; // TEST-only pin; production stays -1 + return p; +} + +TEST(NmcP1eActivationGate, UnpinnedActivationRefusesToJudge) +{ + // mainnet() leaves auxpow_activation_height at -1: the gate must refuse + // rather than guess, regardless of whether the header carries an AuxPow. + HeaderChain chain_(params_chain_id_1()); + EXPECT_EQ(chain_.check_activation_gate(50000, /*has_auxpow=*/true), + HeaderChain::AdmitResult::REJECT_UNPINNED); + EXPECT_EQ(chain_.check_activation_gate(50000, /*has_auxpow=*/false), + HeaderChain::AdmitResult::REJECT_UNPINNED); +} + +TEST(NmcP1eActivationGate, AuxPowBelowActivationIsPremature) +{ + HeaderChain chain_(params_activation(19200)); + EXPECT_EQ(chain_.check_activation_gate(19199, /*has_auxpow=*/true), + HeaderChain::AdmitResult::REJECT_PREMATURE_AUXPOW); + // a plain (non-MM) header below activation is admissible. + EXPECT_EQ(chain_.check_activation_gate(19199, /*has_auxpow=*/false), + HeaderChain::AdmitResult::ADMIT); +} + +TEST(NmcP1eActivationGate, AuxPowRequiredAtAndAfterActivation) +{ + HeaderChain chain_(params_activation(19200)); + // exactly at the activation height the AuxPow becomes mandatory. + EXPECT_EQ(chain_.check_activation_gate(19200, /*has_auxpow=*/false), + HeaderChain::AdmitResult::REJECT_MISSING_AUXPOW); + EXPECT_EQ(chain_.check_activation_gate(19200, /*has_auxpow=*/true), + HeaderChain::AdmitResult::ADMIT); + // and well past it. + EXPECT_EQ(chain_.check_activation_gate(250000, /*has_auxpow=*/true), + HeaderChain::AdmitResult::ADMIT); +} + +TEST(NmcP1eActivationGate, ActivationBoundaryIsInclusive) +{ + HeaderChain chain_(params_activation(19200)); + // height == activation-1 is still pre-activation (MM not yet required); + // height == activation flips MM to mandatory. + EXPECT_EQ(chain_.check_activation_gate(19199, /*has_auxpow=*/false), + HeaderChain::AdmitResult::ADMIT); + EXPECT_EQ(chain_.check_activation_gate(19200, /*has_auxpow=*/false), + HeaderChain::AdmitResult::REJECT_MISSING_AUXPOW); +} + } // namespace