Skip to content

Commit a3cdd5b

Browse files
authored
Merge pull request #622 from frstrtr/dash/g1-curtime-seam
dash: injectable curtime seam + G1 golden deterministic template KAT
2 parents 3b5267d + bae5302 commit a3cdd5b

2 files changed

Lines changed: 96 additions & 2 deletions

File tree

src/impl/dash/coin/embedded_gbt.hpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,17 @@ inline DashWorkData build_embedded_workdata(
6262
uint32_t bits_for_next,
6363
uint32_t mtp_at_tip,
6464
uint8_t address_version,
65-
uint8_t address_p2sh_version)
65+
uint8_t address_p2sh_version,
66+
// Step 8 seam: injectable block time. Defaults to std::time(nullptr) so
67+
// every existing caller is byte-for-byte unchanged (SAFE-ADDITIVE); the
68+
// G1 golden KAT pins it for a deterministic template+coinbase vector.
69+
uint32_t curtime = static_cast<uint32_t>(std::time(nullptr)))
6670
{
6771
DashWorkData w;
6872
w.m_height = prev_height + 1;
6973
w.m_previous_block = prev_hash;
7074
w.m_bits = bits_for_next;
71-
w.m_curtime = static_cast<uint32_t>(std::time(nullptr));
75+
w.m_curtime = curtime;
7276
// Step 8: real median-time-past from header_chain. dashcore
7377
// requires curtime > MTP for the candidate block to be valid;
7478
// GBT returns MTP+1 as mintime so miners don't accidentally

test/test_dash_embedded_gbt.cpp

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,3 +408,93 @@ TEST(DashEmbeddedGbt, GbtXcheckMatchesIdenticalDetectsFieldDrift) {
408408
rpc_count.m_packed_payments.pop_back();
409409
EXPECT_FALSE(gbt_xcheck(w, rpc_count));
410410
}
411+
412+
// ════════════════════════════════════════════════════════════════════════
413+
// G1 golden: deterministic template+coinbase vector via the curtime seam.
414+
//
415+
// The sole non-determinism in build_embedded_workdata was the live
416+
// std::time(nullptr) read for m_curtime. With the trailing defaulted curtime
417+
// param, a fixed-input call yields a byte-for-byte reproducible header
418+
// projection — the G1 deliverable (fixed-input template+coinbase golden) in
419+
// the per-coin block-production gate.
420+
//
421+
// Oracle discipline (non-circular, mirrors the params KAT): bits is pinned to
422+
// the oracle GBT nBits and passed through; every value/split number is
423+
// re-derived here from the SAME closed-form dashcore formulas, not copied from
424+
// a build_embedded_workdata run. Only curtime is a pinned input we assert the
425+
// seam faithfully echoes.
426+
// ════════════════════════════════════════════════════════════════════════
427+
428+
TEST(DashEmbeddedGbt, G1GoldenDeterministicTemplateViaCurtimeSeam) {
429+
UTXOViewCache utxo(nullptr);
430+
uint256 prev = mint_hash(77);
431+
utxo.add_coin(Outpoint(prev, 0), Coin(100'000, {}, /*height=*/1, /*cb=*/false));
432+
Mempool mp;
433+
mp.set_utxo(&utxo);
434+
auto tx = make_spend(prev, 0, 90'000, /*salt=*/7); // fee = 10'000
435+
ASSERT_TRUE(mp.add_tx(tx));
436+
437+
auto payout = p2pkh_script(0x55);
438+
auto mnstates = single_mn(payout);
439+
440+
uint256 prev_hash = raw256(0xC3);
441+
const uint32_t bits = 0x1b104be3u; // pinned oracle GBT nBits
442+
const uint32_t mtp = 1'700'000'000u;
443+
const uint32_t PINNED_CURTIME = 1'700'000'123u; // fixed block time
444+
445+
// Same fixed inputs → identical WorkData every run (the golden property).
446+
auto a = build_embedded_workdata(
447+
H - 1, prev_hash, mnstates, mp,
448+
bits, mtp, DASH_PUBKEY_VER, DASH_P2SH_VER, PINNED_CURTIME);
449+
auto b = build_embedded_workdata(
450+
H - 1, prev_hash, mnstates, mp,
451+
bits, mtp, DASH_PUBKEY_VER, DASH_P2SH_VER, PINNED_CURTIME);
452+
453+
// The seam echoes the pinned time exactly — no wall-clock leak.
454+
EXPECT_EQ(a.m_curtime, PINNED_CURTIME);
455+
EXPECT_EQ(b.m_curtime, PINNED_CURTIME);
456+
457+
// Full header projection is deterministic field-for-field.
458+
EXPECT_EQ(a.m_height, b.m_height);
459+
EXPECT_EQ(a.m_previous_block, b.m_previous_block);
460+
EXPECT_EQ(a.m_bits, b.m_bits);
461+
EXPECT_EQ(a.m_mintime, b.m_mintime);
462+
EXPECT_EQ(a.m_version, b.m_version);
463+
EXPECT_EQ(a.m_coinbase_value, b.m_coinbase_value);
464+
EXPECT_EQ(a.m_payment_amount, b.m_payment_amount);
465+
466+
// Independent re-derivation of the golden values (non-circular oracle).
467+
int64_t reward = compute_dash_block_reward_post_v20(H);
468+
int64_t total_fees = 10'000;
469+
int64_t block_value = reward + total_fees;
470+
int64_t platform_reward = compute_dash_platform_reward_post_v20_mn_rr(H);
471+
int64_t mn_payment = compute_dash_mn_payment_post_v20(block_value)
472+
- platform_reward;
473+
474+
EXPECT_EQ(a.m_height, H);
475+
EXPECT_EQ(a.m_previous_block, prev_hash);
476+
EXPECT_EQ(a.m_bits, bits); // oracle nBits passthrough
477+
EXPECT_EQ(a.m_mintime, mtp + 1u); // GBT returns MTP+1
478+
EXPECT_EQ(a.m_version, 0x20000000u);
479+
EXPECT_EQ(a.m_coinbase_value, static_cast<uint64_t>(block_value));
480+
EXPECT_EQ(a.m_payment_amount, static_cast<uint64_t>(mn_payment));
481+
482+
// Coinbase payee side is deterministic: platform OP_RETURN burn FIRST,
483+
// then the MN base58 payee — same ordering the shadow path asserts.
484+
ASSERT_EQ(a.m_packed_payments.size(), 2u);
485+
EXPECT_EQ(a.m_packed_payments[0].payee, "!6a"); // DIP-0027 burn
486+
EXPECT_EQ(a.m_packed_payments[0].amount,
487+
static_cast<uint64_t>(platform_reward));
488+
EXPECT_EQ(a.m_packed_payments[1].amount,
489+
static_cast<uint64_t>(mn_payment));
490+
EXPECT_FALSE(a.m_packed_payments[1].payee.empty());
491+
EXPECT_NE(a.m_packed_payments[1].payee.front(), '!'); // base58, not !hex
492+
493+
// The default arg preserves the prior live-read behavior: omitting curtime
494+
// still reads the wall clock (post-2023 epoch), so no caller is changed.
495+
auto live = build_embedded_workdata(
496+
H - 1, prev_hash, mnstates, mp,
497+
bits, mtp, DASH_PUBKEY_VER, DASH_P2SH_VER);
498+
EXPECT_GE(live.m_curtime, 1'700'000'000u)
499+
<< "default curtime must still read std::time(nullptr)";
500+
}

0 commit comments

Comments
 (0)