Skip to content

Commit 871a91d

Browse files
authored
Merge pull request #193 from frstrtr/nmc/p1e-storage-leg
nmc(P1e): activation-height gate + in-memory storage leg (re-target #190+#192 -> master)
2 parents 39ef837 + 9cfa9b3 commit 871a91d

2 files changed

Lines changed: 272 additions & 6 deletions

File tree

src/impl/nmc/coin/header_chain.hpp

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -627,10 +627,19 @@ class HeaderChain {
627627
/// Add a single plain header.
628628
/// P0-DEFER: NO difficulty validation, NO PoW check, NO connection check,
629629
/// NO persistence. Structural skeleton only — returns false.
630+
/// Add a single plain (non-merge-mined) header.
631+
/// P1e storage leg: in-memory connect path. On an empty chain the header
632+
/// seeds the chain root (height 0, checkpoint anchor); otherwise it must
633+
/// connect to a known parent via m_previous_block. The activation gate
634+
/// decides admissibility from the connected height (a plain header is
635+
/// admissible only BELOW activation). While the activation height is the
636+
/// -1 sentinel the gate returns REJECT_UNPINNED, so in production nothing
637+
/// connects -- the P1 leaf never builds chain off a placeholder constant.
638+
/// NO difficulty/PoW validation and NO cumulative-work summation yet (the
639+
/// next sub-leg); this leg pins connection topology + the activation gate.
630640
bool add_header(const BlockHeaderType& header) {
631-
(void)header;
632-
// P0-DEFER: header add/validate path not implemented.
633-
return false;
641+
std::lock_guard<std::mutex> lock(m_mutex);
642+
return connect_locked(header, std::nullopt);
634643
}
635644

636645
/// Verify the AuxPow consensus GATE for an incoming merge-mined header.
@@ -646,6 +655,32 @@ class HeaderChain {
646655
header.m_bits);
647656
}
648657

658+
/// P1e: verdict for the activation-height admission gate.
659+
enum class AdmitResult {
660+
ADMIT, // height / auxpow-presence consistent (proof checked separately)
661+
REJECT_PREMATURE_AUXPOW, // header carries an AuxPow below the activation height
662+
REJECT_MISSING_AUXPOW, // no AuxPow at/after activation height (mainnet requirement)
663+
REJECT_UNPINNED, // activation height still the -1 sentinel - refuse to judge
664+
};
665+
666+
/// P1e: the activation-height GATE, factored out for unit-testing
667+
/// independent of the (still-deferred) storage path - the same pattern as
668+
/// verify_auxpow_header(). Decides admissibility from the connected `height`
669+
/// and whether the header carries an AuxPow, BEFORE the four-leg proof is
670+
/// walked: below activation an AuxPow is premature, at/after activation
671+
/// (mainnet) one is mandatory. While auxpow_activation_height is the
672+
/// unpinned -1 sentinel it refuses to judge (REJECT_UNPINNED) so the P1 leaf
673+
/// never renders an activation verdict off a placeholder constant.
674+
AdmitResult check_activation_gate(int32_t height, bool has_auxpow) const {
675+
if (m_params.auxpow_activation_height < 0)
676+
return AdmitResult::REJECT_UNPINNED; // TO-CONFIRM unpinned
677+
if (!m_params.is_auxpow_active(height))
678+
return has_auxpow ? AdmitResult::REJECT_PREMATURE_AUXPOW
679+
: AdmitResult::ADMIT;
680+
return has_auxpow ? AdmitResult::ADMIT
681+
: AdmitResult::REJECT_MISSING_AUXPOW;
682+
}
683+
649684
/// Add a header that carries a merge-mining AuxPow proof.
650685
/// P1d: the AuxPow VERIFICATION GATE is now wired - check_proof() must
651686
/// return VALID or the header is rejected, so the (future) accept path can
@@ -661,9 +696,10 @@ class HeaderChain {
661696
<< ": AuxPow check_proof != VALID";
662697
return false;
663698
}
664-
// P0-DEFER: proof verified, but header storage/connection + the
665-
// is_auxpow_active(height) activation gate are not built yet.
666-
return false;
699+
// P1e storage leg: proof verified -- now connect into the in-memory
700+
// chain, with the height-derived activation gate enforced inside.
701+
std::lock_guard<std::mutex> lock(m_mutex);
702+
return connect_locked(header, auxpow);
667703
}
668704

669705
/// Add a batch of plain headers. P0-DEFER: returns 0 (none accepted).
@@ -676,6 +712,69 @@ class HeaderChain {
676712
const NMCChainParams& params() const { return m_params; }
677713

678714
private:
715+
// P1e storage leg: shared in-memory connect path for plain and merge-mined
716+
// headers. Caller holds m_mutex. Derives the connected height from the
717+
// parent (or 0 for the chain-root seed), runs the activation gate, and on
718+
// ADMIT inserts the IndexEntry and advances the tip (height-proxy fork
719+
// choice; work-based reorg is a later leg). Returns false -- nothing
720+
// persisted -- on already-known / orphan / activation-gate rejection.
721+
bool connect_locked(const BlockHeaderType& header,
722+
const std::optional<AuxPow>& auxpow) {
723+
const uint256 bh = block_hash(header);
724+
if (m_index.find(bh) != m_index.end())
725+
return false; // already known -- idempotent reject
726+
727+
int32_t height;
728+
uint256 parent_work;
729+
if (m_index.empty()) {
730+
height = 0; // chain-root seed (checkpoint anchor)
731+
parent_work.SetNull();
732+
} else {
733+
auto pit = m_index.find(header.m_previous_block);
734+
if (pit == m_index.end()) {
735+
LOG_WARNING << "[EMB-NMC] reject header " << bh.ToString()
736+
<< ": unknown parent "
737+
<< header.m_previous_block.ToString() << " (orphan)";
738+
return false; // no orphan headers
739+
}
740+
height = static_cast<int32_t>(pit->second.height) + 1;
741+
parent_work = pit->second.chain_work;
742+
}
743+
744+
const bool has_auxpow = auxpow.has_value();
745+
switch (check_activation_gate(height, has_auxpow)) {
746+
case AdmitResult::ADMIT:
747+
break;
748+
case AdmitResult::REJECT_PREMATURE_AUXPOW:
749+
LOG_WARNING << "[EMB-NMC] reject header " << bh.ToString()
750+
<< ": premature AuxPow below activation (height "
751+
<< height << ")";
752+
return false;
753+
case AdmitResult::REJECT_MISSING_AUXPOW:
754+
LOG_WARNING << "[EMB-NMC] reject header " << bh.ToString()
755+
<< ": AuxPow required at/after activation (height "
756+
<< height << ")";
757+
return false;
758+
case AdmitResult::REJECT_UNPINNED:
759+
return false; // activation unpinned: refuse to build
760+
}
761+
762+
IndexEntry e;
763+
e.header = header;
764+
e.block_hash = bh;
765+
e.height = static_cast<uint32_t>(height);
766+
e.chain_work = parent_work; // cumulative-work summation = next sub-leg
767+
e.status = has_auxpow ? HEADER_VALID_CHAIN : HEADER_VALID_TREE;
768+
e.auxpow = auxpow;
769+
m_index.emplace(bh, std::move(e));
770+
771+
if (m_tip.IsNull() || height > static_cast<int32_t>(m_tip_height)) {
772+
m_tip = bh; // height-proxy tip (no work reorg yet)
773+
m_tip_height = static_cast<uint32_t>(height);
774+
}
775+
return true;
776+
}
777+
679778
NMCChainParams m_params;
680779
std::string m_db_path;
681780

src/impl/nmc/test/auxpow_merkle_test.cpp

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,4 +635,171 @@ TEST(NmcP1dGate, MissingAuxBitsKeepsProofIncompleteAndRejected)
635635
EXPECT_FALSE(chain_.add_auxpow_header(h, ap));
636636
}
637637

638+
// ---------------------------------------------------------------------------
639+
// P1e: activation-height admission gate KATs (NmcP1eActivationGate).
640+
// Exercise HeaderChain::check_activation_gate() - the height-derived MM
641+
// activation gate, factored like the P1d proof gate so it is testable without
642+
// the deferred storage path. The production activation height stays the -1
643+
// sentinel; these fixtures pin a TEST-only height (19200, the historically
644+
// cited NMC mainnet value) purely to drive the gate - no consensus promotion.
645+
// ---------------------------------------------------------------------------
646+
static NMCChainParams params_activation(int32_t act_height)
647+
{
648+
NMCChainParams p = NMCChainParams::mainnet();
649+
p.aux_chain_id = 1;
650+
p.auxpow_activation_height = act_height; // TEST-only pin; production stays -1
651+
return p;
652+
}
653+
654+
TEST(NmcP1eActivationGate, UnpinnedActivationRefusesToJudge)
655+
{
656+
// mainnet() leaves auxpow_activation_height at -1: the gate must refuse
657+
// rather than guess, regardless of whether the header carries an AuxPow.
658+
HeaderChain chain_(params_chain_id_1());
659+
EXPECT_EQ(chain_.check_activation_gate(50000, /*has_auxpow=*/true),
660+
HeaderChain::AdmitResult::REJECT_UNPINNED);
661+
EXPECT_EQ(chain_.check_activation_gate(50000, /*has_auxpow=*/false),
662+
HeaderChain::AdmitResult::REJECT_UNPINNED);
663+
}
664+
665+
TEST(NmcP1eActivationGate, AuxPowBelowActivationIsPremature)
666+
{
667+
HeaderChain chain_(params_activation(19200));
668+
EXPECT_EQ(chain_.check_activation_gate(19199, /*has_auxpow=*/true),
669+
HeaderChain::AdmitResult::REJECT_PREMATURE_AUXPOW);
670+
// a plain (non-MM) header below activation is admissible.
671+
EXPECT_EQ(chain_.check_activation_gate(19199, /*has_auxpow=*/false),
672+
HeaderChain::AdmitResult::ADMIT);
673+
}
674+
675+
TEST(NmcP1eActivationGate, AuxPowRequiredAtAndAfterActivation)
676+
{
677+
HeaderChain chain_(params_activation(19200));
678+
// exactly at the activation height the AuxPow becomes mandatory.
679+
EXPECT_EQ(chain_.check_activation_gate(19200, /*has_auxpow=*/false),
680+
HeaderChain::AdmitResult::REJECT_MISSING_AUXPOW);
681+
EXPECT_EQ(chain_.check_activation_gate(19200, /*has_auxpow=*/true),
682+
HeaderChain::AdmitResult::ADMIT);
683+
// and well past it.
684+
EXPECT_EQ(chain_.check_activation_gate(250000, /*has_auxpow=*/true),
685+
HeaderChain::AdmitResult::ADMIT);
686+
}
687+
688+
TEST(NmcP1eActivationGate, ActivationBoundaryIsInclusive)
689+
{
690+
HeaderChain chain_(params_activation(19200));
691+
// height == activation-1 is still pre-activation (MM not yet required);
692+
// height == activation flips MM to mandatory.
693+
EXPECT_EQ(chain_.check_activation_gate(19199, /*has_auxpow=*/false),
694+
HeaderChain::AdmitResult::ADMIT);
695+
EXPECT_EQ(chain_.check_activation_gate(19200, /*has_auxpow=*/false),
696+
HeaderChain::AdmitResult::REJECT_MISSING_AUXPOW);
697+
}
698+
699+
700+
// ---------------------------------------------------------------------------
701+
// P1e storage leg: in-memory connect path KATs (NmcP1eStore). Exercise
702+
// HeaderChain::add_header / add_auxpow_header now that a passing header is
703+
// CONNECTED: genesis-seed, prev-hash linkage, height derivation, the activation
704+
// gate enforced from the connected height, and tip advance. Cumulative-work
705+
// summation + work-based reorg stay a later sub-leg.
706+
// ---------------------------------------------------------------------------
707+
static BlockHeaderType plain_header(const uint256& prev, uint32_t bits, uint32_t nonce)
708+
{
709+
BlockHeaderType h{};
710+
h.m_version = 1;
711+
h.m_previous_block = prev;
712+
h.m_bits = bits;
713+
h.m_nonce = nonce;
714+
return h;
715+
}
716+
717+
TEST(NmcP1eStore, EmptyChainSeedsRootAtHeightZero)
718+
{
719+
HeaderChain chain_(params_activation(19200)); // pinned so plain<activation ADMITs
720+
uint256 z; z.SetNull();
721+
BlockHeaderType g = plain_header(z, 0x1d00ffffu, 1);
722+
EXPECT_TRUE(chain_.add_header(g));
723+
EXPECT_TRUE(chain_.has_header(block_hash(g)));
724+
EXPECT_EQ(chain_.size(), 1u);
725+
EXPECT_EQ(chain_.height(), 0u);
726+
}
727+
728+
TEST(NmcP1eStore, ConnectsChildByPrevHashAndAdvancesTip)
729+
{
730+
HeaderChain chain_(params_activation(19200));
731+
uint256 z; z.SetNull();
732+
BlockHeaderType g = plain_header(z, 0x1d00ffffu, 1);
733+
ASSERT_TRUE(chain_.add_header(g));
734+
BlockHeaderType c = plain_header(block_hash(g), 0x1d00ffffu, 2);
735+
EXPECT_TRUE(chain_.add_header(c));
736+
EXPECT_EQ(chain_.size(), 2u);
737+
EXPECT_EQ(chain_.height(), 1u);
738+
ASSERT_TRUE(chain_.tip().has_value());
739+
EXPECT_EQ(chain_.tip()->block_hash, block_hash(c));
740+
}
741+
742+
TEST(NmcP1eStore, OrphanWithUnknownParentIsRejected)
743+
{
744+
HeaderChain chain_(params_activation(19200));
745+
uint256 z; z.SetNull();
746+
BlockHeaderType g = plain_header(z, 0x1d00ffffu, 1);
747+
ASSERT_TRUE(chain_.add_header(g));
748+
uint256 bogus = leaf_of(0xAB); // not g's hash
749+
BlockHeaderType o = plain_header(bogus, 0x1d00ffffu, 3);
750+
EXPECT_FALSE(chain_.add_header(o));
751+
EXPECT_FALSE(chain_.has_header(block_hash(o)));
752+
EXPECT_EQ(chain_.size(), 1u);
753+
}
754+
755+
TEST(NmcP1eStore, UnpinnedActivationRefusesToConnect)
756+
{
757+
HeaderChain chain_(params_chain_id_1()); // activation height == -1
758+
uint256 z; z.SetNull();
759+
BlockHeaderType g = plain_header(z, 0x1d00ffffu, 1);
760+
EXPECT_FALSE(chain_.add_header(g)); // REJECT_UNPINNED: build nothing
761+
EXPECT_EQ(chain_.size(), 0u);
762+
}
763+
764+
TEST(NmcP1eStore, VerifiedAuxPowConnectsAtActivationHeight)
765+
{
766+
HeaderChain chain_(params_activation(1)); // activation at height 1
767+
uint256 z; z.SetNull();
768+
BlockHeaderType g = plain_header(z, 0x1d00ffffu, 1);
769+
ASSERT_TRUE(chain_.add_header(g)); // genesis (height 0, plain)
770+
771+
uint32_t aux_bits = 0x207fffffu; // easy aux target
772+
BlockHeaderType h1 = plain_header(block_hash(g), aux_bits, 7);
773+
uint256 aux = block_hash(h1); // production seam: aux == header hash
774+
AuxPow ap = complete_proof(aux, /*parent_own_bits=*/0x1d00ffffu);
775+
ASSERT_TRUE(mine_parent(ap, chain::bits_to_target(aux_bits)));
776+
ASSERT_EQ(chain_.verify_auxpow_header(h1, ap), AuxPow::CheckResult::VALID);
777+
778+
EXPECT_TRUE(chain_.add_auxpow_header(h1, ap)); // height 1 >= activation, proof VALID
779+
EXPECT_TRUE(chain_.has_header(aux));
780+
EXPECT_EQ(chain_.height(), 1u);
781+
ASSERT_TRUE(chain_.get_header(aux).has_value());
782+
EXPECT_TRUE(chain_.get_header(aux)->auxpow.has_value());
783+
}
784+
785+
TEST(NmcP1eStore, PrematureAuxPowIsRejectedByStoreEvenWhenProofValid)
786+
{
787+
HeaderChain chain_(params_activation(19200)); // activation far above height 1
788+
uint256 z; z.SetNull();
789+
BlockHeaderType g = plain_header(z, 0x1d00ffffu, 1);
790+
ASSERT_TRUE(chain_.add_header(g));
791+
792+
uint32_t aux_bits = 0x207fffffu;
793+
BlockHeaderType h1 = plain_header(block_hash(g), aux_bits, 9);
794+
uint256 aux = block_hash(h1);
795+
AuxPow ap = complete_proof(aux, 0x1d00ffffu);
796+
ASSERT_TRUE(mine_parent(ap, chain::bits_to_target(aux_bits)));
797+
ASSERT_EQ(chain_.verify_auxpow_header(h1, ap), AuxPow::CheckResult::VALID);
798+
799+
// proof is VALID, but height 1 is below activation -> premature, NOT stored.
800+
EXPECT_FALSE(chain_.add_auxpow_header(h1, ap));
801+
EXPECT_FALSE(chain_.has_header(aux));
802+
EXPECT_EQ(chain_.height(), 0u);
803+
}
804+
638805
} // namespace

0 commit comments

Comments
 (0)