diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2f861275c..5dfbaec3c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -114,7 +114,7 @@ jobs: core_test sharechain_test share_test btc_share_test \ 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_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate test_dash_work_source test_dash_node_coin_state test_dash_coin_state_maintainer test_dash_node_embedded_wire test_dash_node_reception_wire test_dash_get_work \ + test_doge_chain test_compact_blocks test_dash_x11_kat test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_coinbase_muldiv test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate test_dash_work_source test_dash_node_coin_state test_dash_coin_state_maintainer test_dash_node_embedded_wire test_dash_node_reception_wire test_dash_get_work \ test_multiaddress_pplns test_pplns_stress \ test_hash_link test_decay_pplns \ test_pplns_consensus \ @@ -264,7 +264,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_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate test_dash_work_source test_dash_node_coin_state test_dash_coin_state_maintainer test_dash_node_embedded_wire test_dash_node_reception_wire test_dash_get_work \ + test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_coinbase_muldiv test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate test_dash_work_source test_dash_node_coin_state test_dash_coin_state_maintainer test_dash_node_embedded_wire test_dash_node_reception_wire test_dash_get_work \ test_hash_link test_decay_pplns \ test_pplns_consensus \ test_v36_script_sorting test_v36_cross_impl_refhash \ diff --git a/src/impl/dash/coinbase_builder.hpp b/src/impl/dash/coinbase_builder.hpp index c01bd542f..f4dfe9f47 100644 --- a/src/impl/dash/coinbase_builder.hpp +++ b/src/impl/dash/coinbase_builder.hpp @@ -38,6 +38,7 @@ #include "coin/transaction.hpp" #include "coin/rpc_data.hpp" #include "share_check.hpp" // decode_payee_script, pubkey_hash_to_script2, DONATION_SCRIPT +#include "payout_muldiv.hpp" // dash::payout::payout_share (MSVC-portable 128-bit muldiv) #include #include @@ -126,16 +127,14 @@ inline std::vector compute_dash_payouts( core::version_gate::is_v36_active(params.current_share_version); std::map amounts; if (total_weight > 0) { - // weights/total_weight fit uint64 at the DASH layer; __uint128_t covers - // the products (worker_payout < 2^50, 49 < 2^6, weight < 2^64). - __uint128_t den = v36 - ? static_cast<__uint128_t>(total_weight) - : static_cast<__uint128_t>(total_weight) * 50; + // weights/total_weight fit uint64 at the DASH layer, but the muldiv + // intermediate (worker_payout*weight*49 ~ 2^120, pre-v36 den > 2^64) + // needs a true 128-bit type. dash::payout::payout_share uses native + // __uint128_t on GCC/Clang and boost uint128 on MSVC (no __int128); + // the two are pinned bit-identical by test_dash_coinbase_muldiv. for (const auto& [script, w] : weights) { - __uint128_t num = static_cast<__uint128_t>(w) - * static_cast<__uint128_t>(worker_payout); - if (!v36) num *= 49; - amounts[script] = static_cast(num / den); + amounts[script] = + dash::payout::payout_share(w, worker_payout, total_weight, v36); } } diff --git a/src/impl/dash/payout_muldiv.hpp b/src/impl/dash/payout_muldiv.hpp new file mode 100644 index 000000000..3895b2202 --- /dev/null +++ b/src/impl/dash/payout_muldiv.hpp @@ -0,0 +1,85 @@ +#ifndef C2POOL_DASH_PAYOUT_MULDIV_HPP +#define C2POOL_DASH_PAYOUT_MULDIV_HPP + +// --------------------------------------------------------------------------- +// dash::payout -- MSVC-portable 128-bit intermediate for the PPLNS coinbase +// payout proportion (coinbase_builder.hpp step 3-4). +// +// The per-script amount is a muldiv evaluated in a 128-bit intermediate: +// +// v36 : amount = worker_payout * weight / total_weight +// pre-v36 : amount = worker_payout * 49 * weight / (total_weight * 50) +// +// The numerator worker_payout*weight*49 reaches ~2^120 (weight < 2^64, +// worker_payout < 2^50, 49 < 2^6) and the pre-v36 denominator total_weight*50 +// exceeds 2^64, so a true 128-bit intermediate is REQUIRED. +// +// GCC/Clang provide native unsigned __int128 -- that path is kept BYTE-EXACT +// for the shipping Linux/macOS c2pool-dash packages. MSVC has no __int128 +// (error C2065: '__uint128_t': undeclared identifier), which broke the +// c2pool-dash Windows v0.2.1 build; on any compiler without __int128 we fall +// back to boost::multiprecision::uint128_t, a fixed 128-bit unsigned type that +// yields BIT-IDENTICAL results. This is CONSENSUS PAYOUT math -- a one-satoshi +// divergence forks payouts -- so test_dash_coinbase_muldiv pins native == +// portable across the full DASH payout domain (the required guard). Mirrors the +// BCH abla (#688) and DGB arith256 (#690) MSVC-portability rework. +// --------------------------------------------------------------------------- + +#include +#include +#include + +// Portable 128-bit intermediate on compilers without __int128 (MSVC). +// Header-only; boost is already a c2pool dependency (conan + system libboost). +#include + +namespace dash { +namespace payout { + +// Portable path (MSVC and any non-__int128 compiler). Fixed 128-bit unsigned +// boost intermediate -> BIT-IDENTICAL to the native __int128 result. +inline uint64_t payout_share_portable(uint64_t weight, uint64_t worker_payout, + uint64_t total_weight, bool v36) { + assert(total_weight > 0); + using u128 = boost::multiprecision::uint128_t; + u128 num = u128(weight) * u128(worker_payout); + if (!v36) num *= 49u; + const u128 den = v36 ? u128(total_weight) : u128(total_weight) * 50u; + const u128 q = num / den; + assert(q <= u128(std::numeric_limits::max())); + return static_cast(q); +} + +#if defined(__SIZEOF_INT128__) +// Native path (GCC/Clang) -- BYTE-EXACT reproduction of the original +// coinbase_builder.hpp:131-138 block; this is what the merged Linux/macOS +// packages ship. Exposed by name so the KAT can diff it against the portable +// path on the trusted platform. +inline uint64_t payout_share_native(uint64_t weight, uint64_t worker_payout, + uint64_t total_weight, bool v36) { + assert(total_weight > 0); + const __uint128_t den = v36 + ? static_cast<__uint128_t>(total_weight) + : static_cast<__uint128_t>(total_weight) * 50; + __uint128_t num = static_cast<__uint128_t>(weight) + * static_cast<__uint128_t>(worker_payout); + if (!v36) num *= 49; + const __uint128_t q = num / den; + assert(q <= static_cast<__uint128_t>(std::numeric_limits::max())); + return static_cast(q); +} +inline uint64_t payout_share(uint64_t weight, uint64_t worker_payout, + uint64_t total_weight, bool v36) { + return payout_share_native(weight, worker_payout, total_weight, v36); +} +#else +inline uint64_t payout_share(uint64_t weight, uint64_t worker_payout, + uint64_t total_weight, bool v36) { + return payout_share_portable(weight, worker_payout, total_weight, v36); +} +#endif + +} // namespace payout +} // namespace dash + +#endif // C2POOL_DASH_PAYOUT_MULDIV_HPP diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 76a88f367..9cd894ed0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -828,6 +828,19 @@ if (BUILD_TESTING AND GTest_FOUND) target_link_libraries(test_dash_coinbase_parity PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39) gtest_add_tests(test_dash_coinbase_parity "" AUTO) + # PPLNS coinbase payout muldiv: native(__int128) == portable(boost uint128) + # byte-parity -- MSVC-portability guard for coinbase_builder.hpp step 3-4 + # (fixes c2pool-dash Windows v0.2.1 build). Pure header math; consensus. + add_executable(test_dash_coinbase_muldiv test_dash_coinbase_muldiv.cpp) + target_link_libraries(test_dash_coinbase_muldiv PRIVATE + GTest::gtest_main GTest::gtest + dash_x11 core + nlohmann_json::nlohmann_json + ${Boost_LIBRARIES} + ) + target_link_libraries(test_dash_coinbase_muldiv PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39) + gtest_add_tests(test_dash_coinbase_muldiv "" AUTO) + add_executable(test_dash_cb_payee test_dash_cb_payee.cpp) target_link_libraries(test_dash_cb_payee PRIVATE GTest::gtest_main GTest::gtest diff --git a/test/test_dash_coinbase_muldiv.cpp b/test/test_dash_coinbase_muldiv.cpp new file mode 100644 index 000000000..21ce5509d --- /dev/null +++ b/test/test_dash_coinbase_muldiv.cpp @@ -0,0 +1,137 @@ +// --------------------------------------------------------------------------- +// dash::payout 128-bit payout muldiv KAT -- the guard for the MSVC-portability +// rework of coinbase_builder.hpp (v0.2.1 c2pool-dash Windows build fix). +// +// coinbase_builder evaluates the PPLNS per-script coinbase amount in a 128-bit +// intermediate: +// v36 : amount = worker_payout * weight / total_weight +// pre-v36 : amount = worker_payout * 49 * weight / (total_weight * 50) +// GCC/Clang use native unsigned __int128; MSVC (no __int128, error C2065 on +// '__uint128_t') falls back to boost::multiprecision::uint128_t. This is +// CONSENSUS PAYOUT math -- a one-satoshi divergence forks payouts -- so the two +// paths MUST be BIT-IDENTICAL. +// +// Two layers (mirrors bch abla muldiv_kat / dgb arith256_muldiv_kat): +// A. cross-platform known answers: payout_share() / payout_share_portable() +// produce exact fixed results on EVERY compiler incl. MSVC (all MSVC can +// run -- there is no native to diff against there). Several exercise a +// product > 2^64, which a 64-bit multiply would wrap and fail. +// B. Linux-only equivalence sweep (guarded by __SIZEOF_INT128__): the +// portable path must equal the native path across the DASH payout operand +// space plus a deterministic wide fuzz, over BOTH the v36 and pre-v36 +// branches. Proving portable == native on the trusted platform certifies +// the MSVC path this CI cannot run. +// --------------------------------------------------------------------------- + +#include + +#include +#include + +#include + +namespace { + +using dash::payout::payout_share; +using dash::payout::payout_share_portable; + +constexpr uint64_t U64MAX = std::numeric_limits::max(); + +// Deterministic 64-bit LCG (no , no Date/rand) -- reproducible vectors. +struct Lcg { + uint64_t s; + explicit Lcg(uint64_t seed) : s(seed) {} + uint64_t next() { s = s * 6364136223846793005ULL + 1442695040888963407ULL; return s; } +}; + +} // namespace + +// ---- A. cross-platform known answers (run on ALL compilers incl. MSVC) ------ +TEST(DashCoinbaseMuldiv, KnownAnswersV36) { + // v36: amount = worker_payout * weight / total_weight + EXPECT_EQ(payout_share(1, 100, 4, true), 25ULL); + EXPECT_EQ(payout_share(3, 100, 4, true), 75ULL); + EXPECT_EQ(payout_share(4, 100, 4, true), 100ULL); // whole weight -> full payout + EXPECT_EQ(payout_share(0, 100, 4, true), 0ULL); + // product > 2^64 (weight*worker_payout = 2^80): a 64-bit multiply would wrap. + EXPECT_EQ(payout_share(1099511627776ULL, 1099511627776ULL, + 1099511627776ULL, true), 1099511627776ULL); +} + +TEST(DashCoinbaseMuldiv, KnownAnswersPreV36) { + // pre-v36: amount = worker_payout * 49 * weight / (total_weight * 50) + EXPECT_EQ(payout_share(50, 100, 50, false), 98ULL); // whole share = 98% (2% finder fee added separately) + EXPECT_EQ(payout_share(1, 100, 2, false), 49ULL); + EXPECT_EQ(payout_share(0, 100, 50, false), 0ULL); + // numerator worker_payout*49*weight ~ 2^75 (> 2^64) and den = total*50 > 2^64. + EXPECT_EQ(payout_share(1099511627776ULL, 1000000ULL, + 1099511627776ULL, false), 980000ULL); +} + +TEST(DashCoinbaseMuldiv, PortablePathSatisfiesKnownAnswers) { + // the MSVC fallback must independently produce the same answers. + EXPECT_EQ(payout_share_portable(1, 100, 4, true), 25ULL); + EXPECT_EQ(payout_share_portable(4, 100, 4, true), 100ULL); + EXPECT_EQ(payout_share_portable(1099511627776ULL, 1099511627776ULL, + 1099511627776ULL, true), 1099511627776ULL); + EXPECT_EQ(payout_share_portable(50, 100, 50, false), 98ULL); + EXPECT_EQ(payout_share_portable(1099511627776ULL, 1000000ULL, + 1099511627776ULL, false), 980000ULL); +} + +#if defined(__SIZEOF_INT128__) +// ---- B. native == portable across the DASH payout domain (trusted platform) - +TEST(DashCoinbaseMuldiv, NativeEqualsPortableGrid) { + using dash::payout::payout_share_native; + // operand shapes from the coinbase_builder call site: weights are subsets of + // total_weight (so quotient <= worker_payout), worker_payout < 2^50. + const uint64_t totals[] = { + 1, 2, 50, 100, 37938, 1000000ULL, + (1ULL << 30), (1ULL << 40), (1ULL << 62), U64MAX + }; + const uint64_t payouts[] = { + 0, 1, 100, 5000000000ULL, (1ULL << 40), (1ULL << 50) - 1 + }; + for (uint64_t total : totals) { + // weight sampled across [0, total]: 0, 1, total/2, total-1, total. + const uint64_t weights[] = { + 0, 1, total / 2, total > 0 ? total - 1 : 0, total + }; + for (uint64_t w : weights) + for (uint64_t wp : payouts) + for (bool v36 : {false, true}) { + EXPECT_EQ(payout_share_native(w, wp, total, v36), + payout_share_portable(w, wp, total, v36)) + << "w=" << w << " wp=" << wp + << " total=" << total << " v36=" << v36; + } + } +} + +TEST(DashCoinbaseMuldiv, NativeEqualsPortableFuzz) { + using dash::payout::payout_share_native; + Lcg rng(0x9E3779B97F4A7C15ULL); + int compared = 0; + for (int i = 0; i < 200000; ++i) { + uint64_t total = rng.next(); + if (total == 0) total = 1; + const uint64_t w = rng.next() % total; // weight in [0, total) + const uint64_t wp = rng.next() & ((1ULL << 50) - 1); // worker_payout < 2^50 + const bool v36 = (rng.next() & 1) != 0; + ++compared; + ASSERT_EQ(payout_share_native(w, wp, total, v36), + payout_share_portable(w, wp, total, v36)) + << "fuzz i=" << i << " w=" << w << " wp=" << wp + << " total=" << total << " v36=" << v36; + } + EXPECT_GT(compared, 1000); + std::cerr << "dash coinbase muldiv KAT: native==portable over " + << compared << " fuzz vectors\n"; +} +#else +TEST(DashCoinbaseMuldiv, NoInt128KnownAnswerLayerOnly) { + std::cerr << "dash coinbase muldiv KAT: no __int128 (MSVC) -- " + "known-answer layer only\n"; + SUCCEED(); +} +#endif