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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
test_threading test_weights \
test_header_chain test_mempool test_template_builder \
test_doge_chain test_compact_blocks test_dash_x11_kat \
test_dash_header_chain test_dash_block_replay test_dash_conformance \
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy \
test_multiaddress_pplns test_pplns_stress \
test_hash_link test_decay_pplns \
test_pplns_consensus \
Expand Down Expand Up @@ -190,7 +190,7 @@ jobs:
test_threading test_weights \
test_header_chain test_mempool test_template_builder \
test_doge_chain test_compact_blocks test_dash_x11_kat \
test_dash_header_chain test_dash_block_replay test_dash_conformance \
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy \
test_hash_link test_decay_pplns \
test_pplns_consensus \
test_v36_script_sorting test_v36_cross_impl_refhash \
Expand Down
176 changes: 176 additions & 0 deletions src/impl/dash/coin/subsidy.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#pragma once

/// Phase C-TEMPLATE step 1: block-subsidy + MN-payment formula vendor.
///
/// Direct port of dashcore validation.cpp::GetBlockSubsidyHelper +
/// GetMasternodePayment @ cfad414. The functions are pure (no chain
/// state needed), so we can call them on every observed block to do
/// shadow validation against the actual coinbase outputs.
///
/// We DELIBERATELY simplify by hard-coding the post-V20 / post-BRR
/// path: c2pool-dash's header checkpoint is at h=2,400,000, well past
/// V20Height (1,987,776) and BRRHeight (1,374,912), so we will never
/// observe a block where these aren't active. The pre-V20 / pre-BRR
/// paths (with the 9-step historical MN-payment-increase ladder + the
/// difficulty-driven nSubsidyBase formula) are NOT vendored. If we
/// ever observe a block at h<V20Height we'd log a SHADOW-SKIP and
/// move on; in practice it can't happen post-checkpoint.
///
/// Mainnet constants (chainparams.cpp:162-194):
/// nSubsidyHalvingInterval = 210240
/// nBudgetPaymentsStartBlock = 328008 (always passed by 2.4M)
/// BRRHeight = 1,374,912
/// V20Height = 1,987,776
/// nSuperblockCycle = 16616 (~30 days on mainnet)
///
/// Reference flow at block height H (post-V20 always true):
/// nSubsidyBase = 5 (fixed since V20)
/// nSubsidy = nSubsidyBase * COIN (= 500_000_000 sat)
/// for i = 210240; i <= H-1; i += 210240:
/// nSubsidy -= nSubsidy / 14 // ~7.14% yearly decline
/// nSuperblockPart = nSubsidy / 5 // 20% to treasury (V20 active)
/// block_reward = nSubsidy - nSuperblockPart
/// mn_payment = block_reward * 3 / 4 // 75% (post-realloc + V20)
/// miner_share = block_reward / 4 // 25% + tx fees

#include <cstdint>

namespace dash {
namespace coin {

inline constexpr int64_t COIN_SAT = 100'000'000LL;

inline constexpr int DASH_SUBSIDY_HALVING_INTERVAL = 210240;
inline constexpr int DASH_V20_HEIGHT_MAINNET = 1'987'776;
inline constexpr int DASH_BRR_HEIGHT_MAINNET = 1'374'912;
inline constexpr int DASH_MN_RR_HEIGHT_MAINNET = 2'128'896;
inline constexpr int DASH_SUPERBLOCK_CYCLE_MAINNET = 16616;

/// Returns the BLOCK reward (excluding tx fees) for a block at height
/// `height` on Dash mainnet. Equivalent to dashcore's
/// GetBlockSubsidyInner @ height when V20 is active.
inline int64_t compute_dash_block_reward_post_v20(uint32_t height)
{
// Mirror dashcore's prev_height-based loop.
int prev_height = static_cast<int>(height) - 1;
int64_t nSubsidy = 5 * COIN_SAT;
for (int i = DASH_SUBSIDY_HALVING_INTERVAL;
i <= prev_height;
i += DASH_SUBSIDY_HALVING_INTERVAL) {
nSubsidy -= nSubsidy / 14;
}
int64_t nSuperblockPart = nSubsidy / 5;
return nSubsidy - nSuperblockPart;
}

/// Returns the MN payment amount given the block reward (excluding
/// fees). Equivalent to dashcore's GetMasternodePayment @ post-realloc
/// + V20-active path: blockValue * 3 / 4.
///
/// Note: dashcore passes blockValue = block_reward + tx_fees (i.e.,
/// MN gets 75% of subsidy + 75% of fees). For shadow validation
/// against observed coinbases we must include fees the same way.
inline int64_t compute_dash_mn_payment_post_v20(int64_t block_value)
{
return block_value * 3 / 4;
}

/// Platform Credit Pool burn (DIP-0027 / Asset Lock) per-block share.
/// Activates when both V20 AND MN_RR are deployed (mainnet h>=MN_RR=2,128,896,
/// our checkpoint h=2.4M is always past). Mirror of dashcore PlatformShare()
/// at masternode/payments.cpp: PlatformShare(GetMasternodePayment(h, subsidy, true))
/// = (subsidy * 3/4) * 375/1000. Integer-arithmetic order matters: dashcore
/// truncates between the two divisions, so we replicate that order exactly.
/// The result is added as an OP_RETURN coinbase output and DEDUCTED from the
/// MN's portion (miner share is unaffected).
inline int64_t compute_dash_platform_reward_post_v20_mn_rr(uint32_t height)
{
if (static_cast<int>(height) < DASH_MN_RR_HEIGHT_MAINNET) return 0;
int64_t mn_subsidy_share = compute_dash_block_reward_post_v20(height) * 3 / 4;
return mn_subsidy_share * 375 / 1000;
}

/// True if `height` is a superblock height (treasury budget payout).
inline bool is_superblock_height(uint32_t height,
int cycle = DASH_SUPERBLOCK_CYCLE_MAINNET)
{
return cycle > 0 && (height % static_cast<uint32_t>(cycle)) == 0;
}

} // namespace coin
} // namespace dash

#include "block.hpp"
#include "transaction.hpp"
#include <core/coin/utxo_view_cache.hpp>
#include <map>
#include <optional>

namespace dash {
namespace coin {

/// Sum of all coinbase output values. = block_reward + total_block_fees
/// for any accepted block. Used as one half of the cross-check in
/// Phase C-TEMPLATE step 2 shadow validation.
inline int64_t observed_coinbase_value(const dash::coin::BlockType& block)
{
if (block.m_txs.empty()) return 0;
int64_t sum = 0;
for (const auto& vo : block.m_txs[0].vout) sum += vo.value;
return sum;
}

/// Compute total block fees from UTXO + same-block parent outputs.
/// Walks non-coinbase txs; for each, sums input values via UTXO
/// lookups (with same-block parent-tx output fallback for chained
/// txs inside the block) and subtracts output values.
///
/// Returns nullopt when ANY input is missing from UTXO + same-block
/// parent. That happens during cold start (rolling-288 UTXO window
/// doesn't cover deeper inputs) or when the block contains txs
/// spending outputs from blocks we've never observed. Caller treats
/// nullopt as "skip the cross-check this block, retry next block".
inline std::optional<int64_t> computed_block_fees(
const dash::coin::BlockType& block,
::core::coin::UTXOViewCache* utxo)
{
if (!utxo || block.m_txs.size() <= 1) return int64_t{0};

// Pre-index this block's outputs so chained txs (parent + child
// in same block) can fall back to the parent's vout values when
// the parent isn't yet in UTXO.
std::map<uint256, const dash::coin::MutableTransaction*> in_block_txs;
for (size_t i = 1; i < block.m_txs.size(); ++i) {
in_block_txs[dash::coin::dash_txid(block.m_txs[i])] = &block.m_txs[i];
}

int64_t total_fees = 0;
for (size_t i = 1; i < block.m_txs.size(); ++i) {
const auto& tx = block.m_txs[i];
int64_t in_sum = 0, out_sum = 0;
for (const auto& vin : tx.vin) {
::core::coin::Outpoint op(vin.prevout.hash, vin.prevout.index);
::core::coin::Coin coin;
if (utxo->get_coin(op, coin)) {
in_sum += coin.value;
continue;
}
// Same-block parent fallback.
auto pit = in_block_txs.find(vin.prevout.hash);
if (pit != in_block_txs.end()
&& vin.prevout.index < pit->second->vout.size()) {
in_sum += pit->second->vout[vin.prevout.index].value;
continue;
}
// Input missing → can't compute fees this block.
return std::nullopt;
}
for (const auto& vo : tx.vout) out_sum += vo.value;
if (in_sum < out_sum) return std::nullopt;
total_fees += (in_sum - out_sum);
}
return total_fees;
}

} // namespace coin
} // namespace dash
105 changes: 105 additions & 0 deletions src/impl/dash/coin/utxo_adapter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#pragma once

/// Dash UTXO adapter layer — Phase U step 1.
///
/// Plumbs `dash::coin::BlockType` and `dash::coin::MutableTransaction` into
/// the chain-agnostic `core::coin::UTXOViewCache` / `UTXOViewDB` machinery
/// (already used by LTC and DOGE). This file is the **type-compatibility
/// shim only** — no live UTXOViewCache instance is constructed yet; that
/// wiring lands in later Phase U steps (connect_block on new-tip arrival,
/// LevelDB persistence, block-bootstrapper cold-start).
///
/// Why adapter-only first: lets us ship the Dash-side types + constants +
/// txid helper in a standalone commit so the larger Phase U integration
/// stages (which touch main_dash.cpp + broadcaster event plumbing) land
/// against a stable base.
///
/// References:
/// - `dashcore/src/consensus/consensus.h` (COINBASE_MATURITY = 100)
/// - `dashcore/src/consensus/amount.h` (MAX_MONEY = 21M * COIN)
/// - `dashcore/src/validation.h` (MIN_BLOCKS_TO_KEEP = 288)

#include <impl/dash/coin/block.hpp>
#include <impl/dash/coin/transaction.hpp>

#include <core/coin/utxo.hpp>
#include <core/coin/utxo_view_cache.hpp>
#include <core/coin/utxo_view_db.hpp>
#include <core/hash.hpp>
#include <core/pack.hpp>
#include <core/uint256.hpp>

#include <cstdint>

namespace dash {
namespace coin {

// ── ChainLimits ────────────────────────────────────────────────────────────
//
// Dash consensus constants, mirroring dashcore/src/consensus. Values
// chosen to match the generic UTXO machinery's ChainLimits struct
// (max_money / coinbase_maturity / pegout_maturity).
//
// - max_money : 21_000_000 * COIN (dashcore's validation bound;
// Dash's circulating supply is ~21 M DASH but
// dashcore inherits Bitcoin's 21 M MoneyRange check).
// - coinbase_maturity : 100 blocks (standard Bitcoin-family value;
// dashcore/src/consensus/consensus.h COINBASE_MATURITY).
// - pegout_maturity : 0 (Dash has no MWEB, so no pegout concept).
inline constexpr ::core::coin::ChainLimits DASH_LIMITS = {
/* max_money */ 2'100'000'000'000'000LL,
/* coinbase_maturity */ 100,
/* pegout_maturity */ 0,
};

// Undo-data retention: at least 288 blocks, same as dashcore's
// MIN_BLOCKS_TO_KEEP (validation.h). Keeps reorg-repair data around
// long enough to handle deep reorganisations.
inline constexpr uint32_t DASH_MIN_BLOCKS_TO_KEEP = 288;

// Minimum chain depth before embedded mining can fully trust the UTXO
// set: coinbase_maturity + a small reorg buffer. 100 + 6 = 106, in
// line with LTC's approach (LTC uses coinbase_maturity + pegout_maturity
// = 106; Dash has no pegout so we add an equivalent 6-block reorg
// buffer explicitly).
inline constexpr uint32_t DASH_MINING_GATE_DEPTH =
DASH_LIMITS.coinbase_maturity + 6;

// ── Dash txid helper ───────────────────────────────────────────────────────
//
// Double-SHA256 of the transaction's canonical serialization. DIP3 /
// DIP4 special transactions (type != 0) carry an `extra_payload` field
// that `MutableTransaction::Serialize` already appends; the resulting
// byte stream is what dashcore hashes into the txid, so we mirror
// exactly. Plain (type=0) transactions serialize identically to
// pre-DIP3 Bitcoin-family txs.
//
// UTXOViewCache::connect_block<BlockType> takes a TxidFn parameter —
// this function matches that signature.
inline ::uint256 dash_txid(const MutableTransaction& tx)
{
auto packed = ::pack(tx);
return ::Hash(packed.get_span());
}

// ── Alias so call sites read naturally ─────────────────────────────────────
using UtxoViewCache = ::core::coin::UTXOViewCache;
using UtxoViewDB = ::core::coin::UTXOViewDB;
using Outpoint = ::core::coin::Outpoint;
using Coin = ::core::coin::Coin;
using BlockUndo = ::core::coin::BlockUndo;

// ── Compile-time shape check ──────────────────────────────────────────────
//
// Confirms BlockType + MutableTransaction + the adapter constants are
// mutually consistent with the generic UTXOViewCache template interface.
// No runtime cost; just a type-lookup assertion that fails-to-compile if
// any of the expected fields are missing or renamed.
static_assert(sizeof(DASH_LIMITS) > 0,
"DASH_LIMITS must be a valid ChainLimits instance");
static_assert(MutableTransaction::m_hogEx == false,
"Dash transactions never mark as MWEB HogEx (m_hogEx=false is "
"required by the shared UTXOViewCache::connect_block template)");

} // namespace coin
} // namespace dash
15 changes: 15 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,21 @@ if (BUILD_TESTING AND GTest_FOUND)
target_link_libraries(test_dash_conformance PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39): core stratum/web_server TUs
gtest_add_tests(test_dash_conformance "" AUTO)

# DASH block-subsidy + UTXO adapter (S7 foundation slice): GetBlockSubsidy /
# masternode + platform reward split (subsidy.hpp) consumed through the
# header-only utxo_adapter view shim. Both are header-only over
# block/transaction + core/coin (utxo_view_cache/db); no ltc/pool SCC
# dependency. Mirrors the test_dash_conformance link set.
add_executable(test_dash_subsidy test_dash_subsidy.cpp)
target_link_libraries(test_dash_subsidy PRIVATE
GTest::gtest_main GTest::gtest
dash_x11 core
nlohmann_json::nlohmann_json
${Boost_LIBRARIES}
)
target_link_libraries(test_dash_subsidy PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39): core stratum/web_server TUs
gtest_add_tests(test_dash_subsidy "" AUTO)

add_executable(test_compact_blocks test_compact_blocks.cpp)
target_link_libraries(test_compact_blocks PRIVATE
GTest::gtest_main GTest::gtest
Expand Down
Loading
Loading