Skip to content

nmc(P1d): wire AuxPow::check_proof gate into add_auxpow_header - #189

Merged
frstrtr merged 5 commits into
masterfrom
nmc/p1d-share-accept-gate
Jun 19, 2026
Merged

nmc(P1d): wire AuxPow::check_proof gate into add_auxpow_header#189
frstrtr merged 5 commits into
masterfrom
nmc/p1d-share-accept-gate

Conversation

@frstrtr

@frstrtr frstrtr commented Jun 19, 2026

Copy link
Copy Markdown
Owner

P1d - AuxPow verification gate in the header-accept path

Stacked on #185 (P1c-step4, full four-leg check_proof). Wires that verifier into the accept path so the share/header-accept gate can never admit an unproven merge-mined header.

Change

  • verify_auxpow_header(header, auxpow) - factors the four-leg AuxPow::check_proof into a unit-testable consensus gate: check_proof(block_hash(header), m_params.aux_chain_id, header.m_bits). Per Namecoin's aux-bits rule the parent PoW is checked against the AUX header's nBits.
  • add_auxpow_header - now rejects (warn + return false) on any non-VALID verdict. Header storage/connection and the height-derived is_auxpow_active() activation gate remain P0-DEFER (need a connected parent). P1d ships the rejection half: nothing unproven gets in.

Tests (+4 NmcP1dGate, suite 26->30, local green)

Bind the aux hash to a real header via the block_hash() production seam (not a bare leaf):

  • fully-verified proof -> VALID, passes the gate (storage deferred, not persisted)
  • wrong aux_chain_id -> rejected
  • insufficient parent work -> INVALID, rejected
  • missing aux_bits (leg-only sentinel) -> INCOMPLETE, rejected

Scope / fences

NMC-only under src/impl/nmc/; btc tree consumed READ-ONLY. nmc_auxpow_merkle_test already in CMake + build.yml allowlist. I do not self-merge - held for operator/integrator gating.

frstrtr added 5 commits June 19, 2026 01:05
check_proof() reconstructs the merged-mining merkle root from the aux
block hash through chain_merkle_branch/chain_merkle_index via the P1a
aux_merkle_root() primitive. New INCOMPLETE CheckResult state: a
structurally-consistent chain-merkle walk returns INCOMPLETE (never
VALID) because step 2 (MM-marker commitment + chain_id/slot binding),
step 3 (parent-coinbase tx-merkle leg) and step 4 (parent PoW) are not
built yet -- NMC still MUST NOT block-validate off this leaf. Malformed
slot indices (negative, or wider than branch depth) return INVALID.

4 KATs pin the INCOMPLETE-vs-INVALID boundary so the leg cannot silently
regress into asserting VALID. nmc_coin linked into the test (AuxPow ctor
pulls MutableTransaction). 9/9 PASS. NMC-local, btc tree READ-ONLY.
…p 3) + witness-stripped txid KAT

Adds nmc::coin::parent_coinbase_txid() (Hash over TX_NO_WITNESS serialization,
byte-identical to btc compute_txid() but NMC-local per the coin fence) and
wires AuxPow::check_proof step 3: reconstruct the parent block tx-merkle-root
from the witness-stripped coinbase txid through parent_coinbase_branch/index
via aux_merkle_root(). When a parent header is present the reconstructed root
MUST equal parent_header.m_merkle_root, else INVALID; malformed legs (negative
index, index wider than branch depth) are INVALID. Steps 2 (MM-marker
commitment) and 4 (parent PoW) remain unbuilt, so a structurally-consistent
proof still returns INCOMPLETE, never VALID -- NMC MUST NOT block-validate off
this leaf.

The load-bearing KAT asserts the exact byte serialization: the txid is hashed
over the LEGACY (no-witness) layout -- txid, NOT wtxid -- re-derived field-by-
field without SerializeTransaction's witness/marker path, and is shown to
differ from the wtxid for a coinbase carrying the BIP141 reserved value. Plus
4 check_proof boundary KATs (match=>INCOMPLETE, mismatch/negative/too-wide=>
INVALID). nmc_auxpow_merkle_test 14/14 PASS exit 0. NMC-local, btc tree
READ-ONLY, test target name unchanged (CI allowlist untouched).
…_id/slot binding

Step 2 of AuxPow verification: confirm the chain-merkle root reconstructed in
step 1 is the one committed inside the parent (BTC) coinbase merged-mining
marker, and that this aux chain occupies the deterministic slot its
(nonce, chain_id) demands.

NMC-local port of Namecoin/Bitcoin auxpow.cpp CAuxPow::check coinbase scan
(coin fence: only std + core/* types, no btc include):
  * scan_mm_commitment(): the 4-byte pchMergedMiningHeader (fa be 6d 6d) must
    appear exactly once; the chain-merkle root must follow it immediately in
    reversed (big-endian display) byte order; the next LE uint32 tree size must
    equal 2^height; the following LE uint32 nonce must place the chain at
    chain_index == aux_expected_index(nonce, chain_id, height).
  * aux_expected_index(): byte-faithful CAuxPow::getExpectedIndex LCG.

Staged posture preserved: a coinbase carrying no marker (leg-only fixture or
empty parent coinbase) returns ABSENT -> step 2 skipped, mirroring the
null-parent-header gate in step 3. A marker that commits a different root,
wrong tree size, wrong slot, or is duplicated -> INVALID. The proof still never
reaches VALID -- step 4 (parent PoW vs target, btc tree READ-ONLY) is unbuilt,
so a structurally-consistent proof stays INCOMPLETE and NMC MUST NOT
block-validate off this leaf.

KATs: aux_expected_index pinned to offline-computed LCG values; the committed
root laid out by hand (magic|reversed-root|LE size|LE nonce) without calling
scan_mm_commitment, so a parse/scan bug is caught not mirrored.
nmc_auxpow_merkle_test 22/22 PASS exit 0 (was 14/14, +8).
…ep 4)

Complete the four-leg AuxPow verification walk: verify the parent (BTC)
block proof-of-work. Per Namecoin CheckAuxPowProofOfWork the parent block
SHA256d hash must satisfy the AUX (NMC) block difficulty target, NOT the
parent header own nBits, so check_proof() gains a uint32_t aux_bits param
(default 0). The PoW gate fires only with a real parent header and a
non-zero aux_bits: pow_hash(parent_header) > bits_to_target(aux_bits) is
INVALID (parent has insufficient work); otherwise the leg passes.

With all four legs satisfied -- chain-merkle, MM-marker (matched),
parent-coinbase tx-merkle, and parent PoW -- against a real parent header,
check_proof() now finally returns VALID. Anything short (null parent, no
matched marker, or aux_bits unset) stays INCOMPLETE so NMC never
block-validates off a partial proof; aux_bits == 0 is the leg-only-fixture
sentinel that preserves the staged posture.

No btc-tree dependency: chain::bits_to_target lives in src/core and the PoW
hash is the nmc-local pow_hash(); src/impl/btc is referenced read-only only.
NMC-local per fence #4. +4 KATs (sufficient->VALID, insufficient->INVALID,
no-aux_bits->INCOMPLETE, parent-own-bits-are-not-the-gate); the VALID KATs
mine the parent nonce against an easy target so the success path is
deterministic. nmc_auxpow_merkle_test 26/26 PASS exit 0.
verify_auxpow_header() factors the four-leg AuxPow::check_proof into a
unit-testable consensus gate: an incoming merge-mined header is admissible
only when check_proof(block_hash(header), aux_chain_id, header.m_bits)
returns VALID. add_auxpow_header() now rejects (warns + returns false) on
any non-VALID verdict; header storage/connection and the height-derived
is_auxpow_active() activation gate remain P0-DEFER (need a connected
parent). P1d ships the rejection half: nothing unproven gets in.

+4 KATs (NmcP1dGate) bind the aux hash to a real header via the
block_hash() production seam: fully-verified proof passes; wrong chain_id,
insufficient parent work, and missing aux_bits are each rejected. Suite
26->30. NMC-only under src/impl/nmc/; btc tree consumed READ-ONLY.
@frstrtr
frstrtr merged commit c888891 into master Jun 19, 2026
16 checks passed
frstrtr added a commit that referenced this pull request Jun 19, 2026
nmc(P1e): activation-height admission gate + 4 KATs (stacked on #189)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant