Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 105 additions & 6 deletions src/impl/nmc/coin/header_chain.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::mutex> lock(m_mutex);
return connect_locked(header, std::nullopt);
}

/// Verify the AuxPow consensus GATE for an incoming merge-mined header.
Expand All @@ -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
Expand All @@ -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<std::mutex> lock(m_mutex);
return connect_locked(header, auxpow);
}

/// Add a batch of plain headers. P0-DEFER: returns 0 (none accepted).
Expand All @@ -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>& 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<int32_t>(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<uint32_t>(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<int32_t>(m_tip_height)) {
m_tip = bh; // height-proxy tip (no work reorg yet)
m_tip_height = static_cast<uint32_t>(height);
}
return true;
}

NMCChainParams m_params;
std::string m_db_path;

Expand Down
167 changes: 167 additions & 0 deletions src/impl/nmc/test/auxpow_merkle_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 plain<activation ADMITs
uint256 z; z.SetNull();
BlockHeaderType g = plain_header(z, 0x1d00ffffu, 1);
EXPECT_TRUE(chain_.add_header(g));
EXPECT_TRUE(chain_.has_header(block_hash(g)));
EXPECT_EQ(chain_.size(), 1u);
EXPECT_EQ(chain_.height(), 0u);
}

TEST(NmcP1eStore, ConnectsChildByPrevHashAndAdvancesTip)
{
HeaderChain chain_(params_activation(19200));
uint256 z; z.SetNull();
BlockHeaderType g = plain_header(z, 0x1d00ffffu, 1);
ASSERT_TRUE(chain_.add_header(g));
BlockHeaderType c = plain_header(block_hash(g), 0x1d00ffffu, 2);
EXPECT_TRUE(chain_.add_header(c));
EXPECT_EQ(chain_.size(), 2u);
EXPECT_EQ(chain_.height(), 1u);
ASSERT_TRUE(chain_.tip().has_value());
EXPECT_EQ(chain_.tip()->block_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
Loading