diff --git a/src/impl/nmc/coin/header_chain.hpp b/src/impl/nmc/coin/header_chain.hpp index bf06ccb93..cc76b8e4b 100644 --- a/src/impl/nmc/coin/header_chain.hpp +++ b/src/impl/nmc/coin/header_chain.hpp @@ -627,10 +627,19 @@ class HeaderChain { /// Add a single plain header. /// P0-DEFER: NO difficulty validation, NO PoW check, NO connection check, /// NO persistence. Structural skeleton only — returns false. + /// Add a single plain (non-merge-mined) header. + /// P1e storage leg: in-memory connect path. On an empty chain the header + /// seeds the chain root (height 0, checkpoint anchor); otherwise it must + /// connect to a known parent via m_previous_block. The activation gate + /// decides admissibility from the connected height (a plain header is + /// admissible only BELOW activation). While the activation height is the + /// -1 sentinel the gate returns REJECT_UNPINNED, so in production nothing + /// connects -- the P1 leaf never builds chain off a placeholder constant. + /// NO difficulty/PoW validation and NO cumulative-work summation yet (the + /// next sub-leg); this leg pins connection topology + the activation gate. bool add_header(const BlockHeaderType& header) { - (void)header; - // P0-DEFER: header add/validate path not implemented. - return false; + std::lock_guard lock(m_mutex); + return connect_locked(header, std::nullopt); } /// Verify the AuxPow consensus GATE for an incoming merge-mined header. @@ -646,6 +655,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 @@ -661,9 +696,10 @@ class HeaderChain { << ": AuxPow check_proof != VALID"; return false; } - // P0-DEFER: proof verified, but header storage/connection + the - // is_auxpow_active(height) activation gate are not built yet. - return false; + // P1e storage leg: proof verified -- now connect into the in-memory + // chain, with the height-derived activation gate enforced inside. + std::lock_guard lock(m_mutex); + return connect_locked(header, auxpow); } /// Add a batch of plain headers. P0-DEFER: returns 0 (none accepted). @@ -676,6 +712,69 @@ class HeaderChain { const NMCChainParams& params() const { return m_params; } private: + // P1e storage leg: shared in-memory connect path for plain and merge-mined + // headers. Caller holds m_mutex. Derives the connected height from the + // parent (or 0 for the chain-root seed), runs the activation gate, and on + // ADMIT inserts the IndexEntry and advances the tip (height-proxy fork + // choice; work-based reorg is a later leg). Returns false -- nothing + // persisted -- on already-known / orphan / activation-gate rejection. + bool connect_locked(const BlockHeaderType& header, + const std::optional& auxpow) { + const uint256 bh = block_hash(header); + if (m_index.find(bh) != m_index.end()) + return false; // already known -- idempotent reject + + int32_t height; + uint256 parent_work; + if (m_index.empty()) { + height = 0; // chain-root seed (checkpoint anchor) + parent_work.SetNull(); + } else { + auto pit = m_index.find(header.m_previous_block); + if (pit == m_index.end()) { + LOG_WARNING << "[EMB-NMC] reject header " << bh.ToString() + << ": unknown parent " + << header.m_previous_block.ToString() << " (orphan)"; + return false; // no orphan headers + } + height = static_cast(pit->second.height) + 1; + parent_work = pit->second.chain_work; + } + + const bool has_auxpow = auxpow.has_value(); + switch (check_activation_gate(height, has_auxpow)) { + case AdmitResult::ADMIT: + break; + case AdmitResult::REJECT_PREMATURE_AUXPOW: + LOG_WARNING << "[EMB-NMC] reject header " << bh.ToString() + << ": premature AuxPow below activation (height " + << height << ")"; + return false; + case AdmitResult::REJECT_MISSING_AUXPOW: + LOG_WARNING << "[EMB-NMC] reject header " << bh.ToString() + << ": AuxPow required at/after activation (height " + << height << ")"; + return false; + case AdmitResult::REJECT_UNPINNED: + return false; // activation unpinned: refuse to build + } + + IndexEntry e; + e.header = header; + e.block_hash = bh; + e.height = static_cast(height); + e.chain_work = parent_work; // cumulative-work summation = next sub-leg + e.status = has_auxpow ? HEADER_VALID_CHAIN : HEADER_VALID_TREE; + e.auxpow = auxpow; + m_index.emplace(bh, std::move(e)); + + if (m_tip.IsNull() || height > static_cast(m_tip_height)) { + m_tip = bh; // height-proxy tip (no work reorg yet) + m_tip_height = static_cast(height); + } + return true; + } + NMCChainParams m_params; std::string m_db_path; diff --git a/src/impl/nmc/test/auxpow_merkle_test.cpp b/src/impl/nmc/test/auxpow_merkle_test.cpp index 5ebc00f5f..de21fd5f5 100644 --- a/src/impl/nmc/test/auxpow_merkle_test.cpp +++ b/src/impl/nmc/test/auxpow_merkle_test.cpp @@ -635,4 +635,171 @@ 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); +} + + +// --------------------------------------------------------------------------- +// P1e storage leg: in-memory connect path KATs (NmcP1eStore). Exercise +// HeaderChain::add_header / add_auxpow_header now that a passing header is +// CONNECTED: genesis-seed, prev-hash linkage, height derivation, the activation +// gate enforced from the connected height, and tip advance. Cumulative-work +// summation + work-based reorg stay a later sub-leg. +// --------------------------------------------------------------------------- +static BlockHeaderType plain_header(const uint256& prev, uint32_t bits, uint32_t nonce) +{ + BlockHeaderType h{}; + h.m_version = 1; + h.m_previous_block = prev; + h.m_bits = bits; + h.m_nonce = nonce; + return h; +} + +TEST(NmcP1eStore, EmptyChainSeedsRootAtHeightZero) +{ + HeaderChain chain_(params_activation(19200)); // pinned so plainblock_hash, block_hash(c)); +} + +TEST(NmcP1eStore, OrphanWithUnknownParentIsRejected) +{ + HeaderChain chain_(params_activation(19200)); + uint256 z; z.SetNull(); + BlockHeaderType g = plain_header(z, 0x1d00ffffu, 1); + ASSERT_TRUE(chain_.add_header(g)); + uint256 bogus = leaf_of(0xAB); // not g's hash + BlockHeaderType o = plain_header(bogus, 0x1d00ffffu, 3); + EXPECT_FALSE(chain_.add_header(o)); + EXPECT_FALSE(chain_.has_header(block_hash(o))); + EXPECT_EQ(chain_.size(), 1u); +} + +TEST(NmcP1eStore, UnpinnedActivationRefusesToConnect) +{ + HeaderChain chain_(params_chain_id_1()); // activation height == -1 + uint256 z; z.SetNull(); + BlockHeaderType g = plain_header(z, 0x1d00ffffu, 1); + EXPECT_FALSE(chain_.add_header(g)); // REJECT_UNPINNED: build nothing + EXPECT_EQ(chain_.size(), 0u); +} + +TEST(NmcP1eStore, VerifiedAuxPowConnectsAtActivationHeight) +{ + HeaderChain chain_(params_activation(1)); // activation at height 1 + uint256 z; z.SetNull(); + BlockHeaderType g = plain_header(z, 0x1d00ffffu, 1); + ASSERT_TRUE(chain_.add_header(g)); // genesis (height 0, plain) + + uint32_t aux_bits = 0x207fffffu; // easy aux target + BlockHeaderType h1 = plain_header(block_hash(g), aux_bits, 7); + uint256 aux = block_hash(h1); // production seam: aux == header hash + AuxPow ap = complete_proof(aux, /*parent_own_bits=*/0x1d00ffffu); + ASSERT_TRUE(mine_parent(ap, chain::bits_to_target(aux_bits))); + ASSERT_EQ(chain_.verify_auxpow_header(h1, ap), AuxPow::CheckResult::VALID); + + EXPECT_TRUE(chain_.add_auxpow_header(h1, ap)); // height 1 >= activation, proof VALID + EXPECT_TRUE(chain_.has_header(aux)); + EXPECT_EQ(chain_.height(), 1u); + ASSERT_TRUE(chain_.get_header(aux).has_value()); + EXPECT_TRUE(chain_.get_header(aux)->auxpow.has_value()); +} + +TEST(NmcP1eStore, PrematureAuxPowIsRejectedByStoreEvenWhenProofValid) +{ + HeaderChain chain_(params_activation(19200)); // activation far above height 1 + uint256 z; z.SetNull(); + BlockHeaderType g = plain_header(z, 0x1d00ffffu, 1); + ASSERT_TRUE(chain_.add_header(g)); + + uint32_t aux_bits = 0x207fffffu; + BlockHeaderType h1 = plain_header(block_hash(g), aux_bits, 9); + uint256 aux = block_hash(h1); + AuxPow ap = complete_proof(aux, 0x1d00ffffu); + ASSERT_TRUE(mine_parent(ap, chain::bits_to_target(aux_bits))); + ASSERT_EQ(chain_.verify_auxpow_header(h1, ap), AuxPow::CheckResult::VALID); + + // proof is VALID, but height 1 is below activation -> premature, NOT stored. + EXPECT_FALSE(chain_.add_auxpow_header(h1, ap)); + EXPECT_FALSE(chain_.has_header(aux)); + EXPECT_EQ(chain_.height(), 0u); +} + } // namespace