Skip to content

Commit dc9d581

Browse files
committed
Dash Phase C-PAY: account for DIP-0027 platform reward in MN payment
Live-discovered 2026-05-14 on .41 PAY soak: [MN-PAY] MISMATCH at every post-MN_RR block with constant diff=-49,787,579 sat. Tracked to block 2,470,904 having three coinbase outputs: 49,787,579 OP_RETURN (platform credit pool burn) + 83,056,309 MN payee + 44,281,297 miner. Total = 177,125,185 = subsidy(177,022,505) + fees(102,680). c2pool was using the pre-DIP-0027 formula `expected = block_value × 3/4` which assumes the MN gets 75% of (subsidy + fees). Real chain (post both V20 h=1,987,776 AND MN_RR h=2,128,896) burns 9/32 of subsidy at L1 to mint Platform credit at L2, with the burn coming entirely from the MN's portion — miner share unchanged. Mirror dashcore PlatformShare() at masternode/payments.cpp:28-58: mn_subsidy_share = subsidy × 3/4 platform_reward = mn_subsidy_share × 375/1000 (= 9/32 of subsidy) mn_payment -= platform_reward The integer-truncation order matters; 9/32 alone isn't bit-exact. Three sites updated: - subsidy.hpp: add DASH_MN_RR_HEIGHT_MAINNET + helper, dashcore order - main_dash.cpp [MN-PAY] check: subtract platform from expected - embedded_gbt.hpp: subtract from MN payment + emit OP_RETURN entry FIRST in m_packed_payments (matches dashcore vout ordering) The third site is consensus-critical for the future embedded-as-primary cutover; without it every template c2pool builds would be rejected by mainnet. Currently [GBT-XCHECK] would mismatch on every post-MN_RR block; this commit also fixes that. 3 new tests pin the formula bit-exact against block 2,470,904.
1 parent c00e87c commit dc9d581

4 files changed

Lines changed: 84 additions & 5 deletions

File tree

src/impl/dash/coin/embedded_gbt.hpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ inline DashWorkData build_embedded_workdata(
7676
constexpr uint32_t MAX_BLOCK_BYTES = 1'990'000; // leave headroom for cb
7777
auto [selected, total_fees] =
7878
mempool.get_sorted_txs_with_fees(MAX_BLOCK_BYTES);
79-
int64_t block_value = reward + total_fees;
80-
int64_t mn_payment = compute_dash_mn_payment_post_v20(block_value);
79+
int64_t block_value = reward + total_fees;
80+
int64_t platform_reward = compute_dash_platform_reward_post_v20_mn_rr(w.m_height);
81+
int64_t mn_payment = compute_dash_mn_payment_post_v20(block_value) - platform_reward;
8182

8283
w.m_coinbase_value = static_cast<uint64_t>(block_value);
8384
w.m_payment_amount = static_cast<uint64_t>(mn_payment);
@@ -93,6 +94,16 @@ inline DashWorkData build_embedded_workdata(
9394
w.m_tx_fees.push_back(s.fee);
9495
}
9596

97+
// Platform Credit Pool burn (DIP-0027): emit OP_RETURN payment
98+
// FIRST (matches dashcore GetBlockTxOuts ordering at payments.cpp:55).
99+
// Payee uses the "!hex" raw-script convention; OP_RETURN single byte = 0x6a.
100+
if (platform_reward > 0) {
101+
PackedPayment burn;
102+
burn.payee = "!6a";
103+
burn.amount = static_cast<uint64_t>(platform_reward);
104+
w.m_packed_payments.push_back(std::move(burn));
105+
}
106+
96107
// MN payee → packed_payment. dashcore GBT returns "payee" as a
97108
// base58 address. We use script_to_address() to produce the same
98109
// wire form for standard P2PKH/P2SH scripts. Unrecognized script

src/impl/dash/coin/subsidy.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ inline constexpr int64_t COIN_SAT = 100'000'000LL;
4343
inline constexpr int DASH_SUBSIDY_HALVING_INTERVAL = 210240;
4444
inline constexpr int DASH_V20_HEIGHT_MAINNET = 1'987'776;
4545
inline constexpr int DASH_BRR_HEIGHT_MAINNET = 1'374'912;
46+
inline constexpr int DASH_MN_RR_HEIGHT_MAINNET = 2'128'896;
4647
inline constexpr int DASH_SUPERBLOCK_CYCLE_MAINNET = 16616;
4748

4849
/// Returns the BLOCK reward (excluding tx fees) for a block at height
@@ -74,6 +75,21 @@ inline int64_t compute_dash_mn_payment_post_v20(int64_t block_value)
7475
return block_value * 3 / 4;
7576
}
7677

78+
/// Platform Credit Pool burn (DIP-0027 / Asset Lock) per-block share.
79+
/// Activates when both V20 AND MN_RR are deployed (mainnet h>=MN_RR=2,128,896,
80+
/// our checkpoint h=2.4M is always past). Mirror of dashcore PlatformShare()
81+
/// at masternode/payments.cpp: PlatformShare(GetMasternodePayment(h, subsidy, true))
82+
/// = (subsidy * 3/4) * 375/1000. Integer-arithmetic order matters: dashcore
83+
/// truncates between the two divisions, so we replicate that order exactly.
84+
/// The result is added as an OP_RETURN coinbase output and DEDUCTED from the
85+
/// MN's portion (miner share is unaffected).
86+
inline int64_t compute_dash_platform_reward_post_v20_mn_rr(uint32_t height)
87+
{
88+
if (static_cast<int>(height) < DASH_MN_RR_HEIGHT_MAINNET) return 0;
89+
int64_t mn_subsidy_share = compute_dash_block_reward_post_v20(height) * 3 / 4;
90+
return mn_subsidy_share * 375 / 1000;
91+
}
92+
7793
/// True if `height` is a superblock height (treasury budget payout).
7894
inline bool is_superblock_height(uint32_t height,
7995
int cycle = DASH_SUPERBLOCK_CYCLE_MAINNET)

src/impl/dash/main_dash.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2779,22 +2779,25 @@ int main(int argc, char* argv[])
27792779
auto fees_opt = dash::coin::computed_block_fees(
27802780
block, utxo);
27812781
if (fees_opt.has_value()) {
2782+
int64_t platform =
2783+
dash::coin::compute_dash_platform_reward_post_v20_mn_rr(
2784+
height);
27822785
int64_t expected_mn =
27832786
dash::coin::compute_dash_mn_payment_post_v20(
2784-
reward + *fees_opt);
2787+
reward + *fees_opt) - platform;
27852788
if (observed_mn_amount == expected_mn) {
27862789
LOG_INFO << "[MN-PAY] match h=" << height
27872790
<< " mn_payment=" << observed_mn_amount
27882791
<< " (reward=" << reward
27892792
<< " + fees=" << *fees_opt
2790-
<< " ×3/4)";
2793+
<< " ×3/4 - platform=" << platform << ")";
27912794
} else {
27922795
LOG_WARNING << "[MN-PAY] MISMATCH h=" << height
27932796
<< " observed=" << observed_mn_amount
27942797
<< " expected=" << expected_mn
27952798
<< " (reward=" << reward
27962799
<< " + fees=" << *fees_opt
2797-
<< " ×3/4)"
2800+
<< " ×3/4 - platform=" << platform << ")"
27982801
<< " diff=" << (observed_mn_amount - expected_mn)
27992802
<< " — log-only at MVP";
28002803
}

test/test_dash_subsidy.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
using dash::coin::compute_dash_block_reward_post_v20;
3535
using dash::coin::compute_dash_mn_payment_post_v20;
36+
using dash::coin::compute_dash_platform_reward_post_v20_mn_rr;
3637

3738
// ─── Tests ──────────────────────────────────────────────────────────────────
3839

@@ -84,3 +85,51 @@ TEST(DashSubsidy, SuperblockHeightDetectionMainnet)
8485
EXPECT_FALSE(dash::coin::is_superblock_height(16615, 16616));
8586
EXPECT_FALSE(dash::coin::is_superblock_height(16617, 16616));
8687
}
88+
89+
TEST(DashSubsidy, PlatformRewardZeroBeforeMN_RR)
90+
{
91+
// MN_RR mainnet activation = h=2,128,896. Before that, platform_reward
92+
// must be zero (pre-DIP-0027 split with no asset-lock burn output).
93+
EXPECT_EQ(compute_dash_platform_reward_post_v20_mn_rr(2'128'895), 0);
94+
EXPECT_EQ(compute_dash_platform_reward_post_v20_mn_rr(1'987'776), 0);
95+
EXPECT_EQ(compute_dash_platform_reward_post_v20_mn_rr(0), 0);
96+
}
97+
98+
TEST(DashSubsidy, PlatformRewardMatchesMainnetH2470904)
99+
{
100+
// Live-validated against Dash mainnet block 2470904 (2026-05-13):
101+
// coinbase vout[0] value=0.49787579 DASH = 49,787,579 sat (OP_RETURN burn)
102+
// coinbase vout[1] value=0.83056309 DASH = 83,056,309 sat (MN payee)
103+
// coinbase vout[2] value=0.44281297 DASH = 44,281,297 sat (miner)
104+
// block reward = 177,022,505 sat (this height has 11 halvings applied)
105+
// fees = 102,680 sat (computed from in-block tx fees)
106+
//
107+
// Formula (dashcore masternode/payments.cpp:28-58):
108+
// mn_subsidy_share = block_reward × 3/4 = 132,766,878 (floored)
109+
// platform_reward = mn_subsidy_share × 375/1000 = 49,787,579 (floored)
110+
constexpr uint32_t H = 2'470'904;
111+
EXPECT_EQ(compute_dash_block_reward_post_v20(H), 177'022'505LL);
112+
EXPECT_EQ(compute_dash_platform_reward_post_v20_mn_rr(H), 49'787'579LL);
113+
114+
// expected_mn (for [MN-PAY] shadow check) = (subsidy+fees) × 3/4 - platform
115+
int64_t reward = compute_dash_block_reward_post_v20(H);
116+
int64_t fees = 102'680LL;
117+
int64_t platform = compute_dash_platform_reward_post_v20_mn_rr(H);
118+
int64_t expected_mn = compute_dash_mn_payment_post_v20(reward + fees) - platform;
119+
EXPECT_EQ(expected_mn, 83'056'309LL)
120+
<< "must match observed MN coinbase output at h=2,470,904";
121+
}
122+
123+
TEST(DashSubsidy, PlatformReward28125PercentOfBlockReward)
124+
{
125+
// Per dashcore: platform = (block_reward × 3/4) × 375/1000.
126+
// For any block_reward where the math is exact (no truncation),
127+
// platform = block_reward × 9/32 = 28.125%. At h=2,400,000 (post-MN_RR)
128+
// the live values produce small truncation noise but the proportion
129+
// should hold within 1 sat.
130+
int64_t r = compute_dash_block_reward_post_v20(2'400'000);
131+
int64_t p = compute_dash_platform_reward_post_v20_mn_rr(2'400'000);
132+
int64_t expected_exact = (r * 9) / 32;
133+
EXPECT_LE(std::abs(p - expected_exact), 1LL)
134+
<< "platform should match 9/32 of block_reward within int-truncation";
135+
}

0 commit comments

Comments
 (0)