Skip to content

Commit bc15976

Browse files
authored
nmc: PD-prep golden fixture pinning dual-target MM-commitment byte layout (#409)
Pin PD acceptance target ahead of the build code (#408 build_mm_commitment): hand-compute the 44-byte merged-mining marker from the Namecoin/Bitcoin auxpow.cpp reference layout (magic | reversed root | LE size | LE nonce) and assert it as an explicit byte literal. Does NOT import or invoke build_mm_commitment, so it is an independent oracle rather than a mirror of the build side. Binds to the already-merged verify SSOT via scan_mm_commitment (MATCH on the live single-slot posture and a height-2 multi-slot case, plus a wrong-slot MISMATCH negative) and cross-checks aux_expected_index. Fenced to the nmc test target (additive, +3 KATs 77->80), rides the already-allowlisted nmc_auxpow_merkle_test exe; no build.yml or consensus-value change. Co-authored-by: frstrtr <frstrtr@users.noreply.github.com>
1 parent e6e0d6d commit bc15976

1 file changed

Lines changed: 152 additions & 0 deletions

File tree

src/impl/nmc/test/auxpow_merkle_test.cpp

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,4 +1569,156 @@ TEST(NmcAuxChainIdConformance, BuildVerifySymmetryOnFactoryDefault)
15691569
EXPECT_EQ(static_cast<uint32_t>(p.aux_chain_id), 1u); // build-side AuxWork.chain_id
15701570
}
15711571

1572+
// ---------------------------------------------------------------------------
1573+
// PD-prep GOLDEN FIXTURE: dual-target coinbase merged-mining commitment layout.
1574+
//
1575+
// Pins PD's acceptance target AHEAD of the build code. PD's dual-target coinbase
1576+
// path (PR #408's nmc::coin::build_mm_commitment) must emit the merged-mining
1577+
// marker in EXACTLY the Namecoin/Bitcoin auxpow.cpp reference layout:
1578+
//
1579+
// [ fa be 6d 6d ] 4-byte MM magic ("\xfa\xbe" + "mm")
1580+
// [ 32 bytes ] chain-merkle root, REVERSED (big-endian display order)
1581+
// [ 4 bytes LE ] chain-merkle tree size = 2^chain_height
1582+
// [ 4 bytes LE ] nonce
1583+
//
1584+
// The expected 44-byte marker is hand-computed here from that reference oracle
1585+
// and asserted as an explicit byte LITERAL. This fixture DELIBERATELY does NOT
1586+
// import or invoke build_mm_commitment: that is PD's surface, and calling it
1587+
// would re-introduce the #408 dependency and merely mirror a build bug instead
1588+
// of catching it. The literal is the independent oracle PD's output reproduces.
1589+
//
1590+
// Binding to the already-merged consensus surface (build-independent):
1591+
// * the literal is fed through scan_mm_commitment (the verify-side SSOT) and
1592+
// must return MMScan::MATCH;
1593+
// * the deterministic slot is cross-checked against aux_expected_index, and a
1594+
// wrong slot is shown to be rejected (the slot field is load-bearing);
1595+
// * the local hand layout (root_reversed + mm_script, NOT build_mm_commitment)
1596+
// is asserted byte-equal to the literal so a layout drift on either side reds.
1597+
//
1598+
// Per-coin isolation (P0 fence #4): src/impl/nmc/ test target only; additive;
1599+
// rides the already-allowlisted nmc_auxpow_merkle_test exe (no build.yml change,
1600+
// no consensus-value change). Pure layout/byte-order pin, no other-coin tree.
1601+
// ---------------------------------------------------------------------------
1602+
1603+
// uint256 whose 32 INTERNAL (little-endian) bytes ascend from `start`.
1604+
static uint256 root_ascending(unsigned char start)
1605+
{
1606+
uint256 u; u.SetNull();
1607+
// begin() is a typed (word-strided) iterator; cast to raw bytes like
1608+
// root_reversed does so we set 32 CONTIGUOUS internal bytes, not every 4th.
1609+
unsigned char* p = reinterpret_cast<unsigned char*>(u.begin());
1610+
for (size_t i = 0; i < uint256::BYTES; ++i)
1611+
p[i] = static_cast<unsigned char>(start + i);
1612+
return u;
1613+
}
1614+
1615+
TEST(NmcPdGoldenFixture, SingleSlotMarkerMatchesHandComputedLiteral)
1616+
{
1617+
// Live NMC-under-BTC posture: a length-1 chain tree (height 0 => size 1),
1618+
// so the aux chain always occupies slot 0 regardless of nonce. A legible
1619+
// non-zero nonce exercises the little-endian nonce field.
1620+
uint256 root = root_ascending(0x01); // internal LE = 01 02 .. 20
1621+
const unsigned h = 0;
1622+
const int32_t cid = 1;
1623+
const uint32_t nonce = 0x12345678u;
1624+
1625+
// Hand-computed golden marker (44 bytes), reference layout above.
1626+
const std::vector<unsigned char> golden = {
1627+
0xfa, 0xbe, 0x6d, 0x6d, // MM magic
1628+
// chain-merkle root, reversed (big-endian display order):
1629+
0x20, 0x1f, 0x1e, 0x1d, 0x1c, 0x1b, 0x1a, 0x19,
1630+
0x18, 0x17, 0x16, 0x15, 0x14, 0x13, 0x12, 0x11,
1631+
0x10, 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09,
1632+
0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01,
1633+
0x01, 0x00, 0x00, 0x00, // size = 2^0 = 1 (LE)
1634+
0x78, 0x56, 0x34, 0x12, // nonce 0x12345678 (LE)
1635+
};
1636+
ASSERT_EQ(golden.size(), 4u + uint256::BYTES + 8u);
1637+
1638+
// Independent hand layout (NOT build_mm_commitment) reproduces the literal.
1639+
// mm_script() frames the marker with a 4-byte dummy coinbase-height prefix
1640+
// (build_mm_commitment emits ONLY the marker payload; the caller prepends the
1641+
// scriptSig framing), so strip that prefix before the byte-equality check.
1642+
std::vector<unsigned char> framed = mm_script(root_reversed(root), 1u << h, nonce);
1643+
std::vector<unsigned char> payload(framed.begin() + 4, framed.end());
1644+
EXPECT_EQ(payload, golden);
1645+
1646+
// Deterministic slot is 0 and is what the verify-side arithmetic demands.
1647+
const uint32_t slot = aux_expected_index(nonce, cid, h);
1648+
EXPECT_EQ(slot, 0u);
1649+
1650+
// The merged verify-side scanner accepts exactly this layout, both as the
1651+
// bare marker and embedded after scriptSig framing (found by search).
1652+
EXPECT_EQ(scan_mm_commitment(golden, root, h, cid, slot), MMScan::MATCH);
1653+
EXPECT_EQ(scan_mm_commitment(framed, root, h, cid, slot), MMScan::MATCH);
1654+
}
1655+
1656+
TEST(NmcPdGoldenFixture, MultiSlotMarkerPinsDeterministicSlotAndLiteral)
1657+
{
1658+
// height 2 => 4-leaf chain tree; nonce 2 pins the aux chain to slot 1
1659+
// (aux_expected_index(2, 1, 2) == 1), exercising both the LE tree-size
1660+
// field (= 4) and the deterministic-slot binding the single-slot case cannot.
1661+
uint256 root = root_ascending(0x41); // internal LE = 41 42 .. 60
1662+
const unsigned h = 2;
1663+
const int32_t cid = 1;
1664+
const uint32_t nonce = 2u;
1665+
1666+
const std::vector<unsigned char> golden = {
1667+
0xfa, 0xbe, 0x6d, 0x6d, // MM magic
1668+
0x60, 0x5f, 0x5e, 0x5d, 0x5c, 0x5b, 0x5a, 0x59,
1669+
0x58, 0x57, 0x56, 0x55, 0x54, 0x53, 0x52, 0x51,
1670+
0x50, 0x4f, 0x4e, 0x4d, 0x4c, 0x4b, 0x4a, 0x49,
1671+
0x48, 0x47, 0x46, 0x45, 0x44, 0x43, 0x42, 0x41,
1672+
0x04, 0x00, 0x00, 0x00, // size = 2^2 = 4 (LE)
1673+
0x02, 0x00, 0x00, 0x00, // nonce 2 (LE)
1674+
};
1675+
ASSERT_EQ(golden.size(), 44u);
1676+
1677+
std::vector<unsigned char> framed = mm_script(root_reversed(root), 1u << h, nonce);
1678+
std::vector<unsigned char> payload(framed.begin() + 4, framed.end());
1679+
EXPECT_EQ(payload, golden);
1680+
1681+
const uint32_t slot = aux_expected_index(nonce, cid, h);
1682+
EXPECT_EQ(slot, 1u); // hand-pinned: nonce 2, chain_id 1, height 2 => slot 1
1683+
1684+
EXPECT_EQ(scan_mm_commitment(golden, root, h, cid, slot), MMScan::MATCH);
1685+
1686+
// Negative pin: the SAME marker presented for the WRONG slot is rejected,
1687+
// proving the slot field is load-bearing, not decorative.
1688+
EXPECT_EQ(scan_mm_commitment(golden, root, h, cid, slot ^ 1u), MMScan::MISMATCH);
1689+
}
1690+
1691+
TEST(NmcPdGoldenFixture, MarkerFieldOffsetsAreThePinnedLayout)
1692+
{
1693+
// Pin the exact field offsets PD's build path must emit. Slicing the marker
1694+
// proves WHERE each field sits; each slice is checked against an independent
1695+
// literal (not against the builder), so a field-order/width drift reds.
1696+
uint256 root = root_ascending(0x01);
1697+
std::vector<unsigned char> framed =
1698+
mm_script(root_reversed(root), /*size=*/1u, /*nonce=*/0x12345678u);
1699+
// Drop the 4-byte dummy coinbase-height prefix; index the bare marker from 0.
1700+
const std::vector<unsigned char> marker(framed.begin() + 4, framed.end());
1701+
ASSERT_EQ(marker.size(), 44u);
1702+
1703+
// [0,4) MM magic.
1704+
EXPECT_EQ((std::vector<unsigned char>(marker.begin(), marker.begin() + 4)),
1705+
(std::vector<unsigned char>{0xfa, 0xbe, 0x6d, 0x6d}));
1706+
1707+
// [4,36) reversed root == big-endian display order of the internal bytes,
1708+
// re-derived here by an independent reverse of the ascending internal bytes.
1709+
std::vector<unsigned char> expect_root(uint256::BYTES);
1710+
for (size_t i = 0; i < uint256::BYTES; ++i)
1711+
expect_root[i] = static_cast<unsigned char>(0x01 + (uint256::BYTES - 1 - i));
1712+
EXPECT_EQ((std::vector<unsigned char>(marker.begin() + 4, marker.begin() + 36)),
1713+
expect_root);
1714+
1715+
// [36,40) tree size = 1 (LE).
1716+
EXPECT_EQ((std::vector<unsigned char>(marker.begin() + 36, marker.begin() + 40)),
1717+
(std::vector<unsigned char>{0x01, 0x00, 0x00, 0x00}));
1718+
1719+
// [40,44) nonce 0x12345678 (LE).
1720+
EXPECT_EQ((std::vector<unsigned char>(marker.begin() + 40, marker.begin() + 44)),
1721+
(std::vector<unsigned char>{0x78, 0x56, 0x34, 0x12}));
1722+
}
1723+
15721724
} // namespace

0 commit comments

Comments
 (0)