|
19 | 19 | #include <impl/dgb/share.hpp> |
20 | 20 | #include <impl/dgb/share_tracker.hpp> // DensePPLNSWindow — V36 decayed-PPLNS SSOT |
21 | 21 | #include <impl/dgb/coin/pplns_weight_walk.hpp> // SSOT: PPLNS step-1 tracker walk (shared emission/verify) |
| 22 | +#include <impl/dgb/coin/pplns_payout_split.hpp> // #328 SSOT: weights -> payout outputs (invariance tie-in) |
22 | 23 | #include <impl/dgb/params.hpp> // make_coin_params — assembled CoinParams SSOT |
23 | 24 | #include <impl/dgb/coin/rpc_conf.hpp> // #82 external-daemon RPC creds (digibyte.conf) |
24 | 25 | #include <impl/dgb/auto_ratchet.hpp> // Phase B: mint-side share-version ratchet |
@@ -630,3 +631,210 @@ TEST(DGB_run_loop_mint, AdapterFailsClosedOnShortHeader) |
630 | 631 | uint256 h = dgb::mint_local_share_with_ratchet(in, tracker, params, ratchet); |
631 | 632 | EXPECT_TRUE(h.IsNull()); |
632 | 633 | } |
| 634 | + |
| 635 | +// ── PPLNS weight-walk VALUE-INVARIANCE (before-vs-after) differential KAT ──── |
| 636 | +// SAME BAR as the #328 payout-split invariance KAT: prove the #333 lift of |
| 637 | +// generate_share_transaction() step-1 into dgb::coin::compute_pplns_weight_walk() |
| 638 | +// moves ZERO weight. We embed the PRE-REFACTOR inline walk here VERBATIM |
| 639 | +// (transcribed line-for-line from the share_check.hpp generate_share_transaction() |
| 640 | +// body that commit a5c23b7fb removed) as legacy_inline_weight_walk(), and assert |
| 641 | +// the helper reproduces it weight-for-weight — per-script weights, total_weight |
| 642 | +// AND total_donation_weight — across a battery exercising BOTH PPLNS branches |
| 643 | +// (V36 decay / pre-V36 grandparent-start + max_weight cap), the donation split, |
| 644 | +// zero-addr-weight shares (full-donation), the insufficient-depth guard (throw), |
| 645 | +// null/absent parents, and a chain that EXCEEDS the real_chain_length window. |
| 646 | +// Then BOTH weight maps are fed through the #328 payout-split SSOT and the |
| 647 | +// resulting outputs + donation asserted byte-identical — closing the end-to-end |
| 648 | +// "no payout satoshi drifts between emission and verification" proof. |
| 649 | +// |
| 650 | +// COVERAGE NOTE (no silent cap): the >4000-distinct-OUTPUT truncation |
| 651 | +// (PPLNS_MAX_OUTPUTS) is proven on the payout-split side by the #328 KAT |
| 652 | +// (PplnsPayoutSplitInvariance, case n=4096). Here the weight-walk "window" is |
| 653 | +// the real_chain_length share cap; the window case below drives the walk past it |
| 654 | +// on testnet params (window=400) to prove the cap truncates the walk identically |
| 655 | +// before vs after the lift. |
| 656 | + |
| 657 | +// Verbatim transcription of share_check.hpp generate_share_transaction() step 1 |
| 658 | +// as it stood BEFORE commit a5c23b7fb (the #333 weight-walk lift). The only |
| 659 | +// adaptation: block_bits is taken as a parameter (the helper extracted exactly |
| 660 | +// share.m_min_header.m_bits into this same parameter) and the three result |
| 661 | +// fields are returned in the deduced CumulativeWeights instead of assigned to |
| 662 | +// generate_share_transaction()'s locals. |
| 663 | +template <typename TrackerT> |
| 664 | +static auto legacy_inline_weight_walk( |
| 665 | + TrackerT& tracker, const uint256& prev_hash, uint32_t block_bits, |
| 666 | + const core::CoinParams& params, bool use_v36_pplns) |
| 667 | + -> decltype(tracker.get_v36_decayed_cumulative_weights( |
| 668 | + prev_hash, std::int32_t{0}, std::declval<const uint288&>())) |
| 669 | +{ |
| 670 | + using Result = decltype(tracker.get_v36_decayed_cumulative_weights( |
| 671 | + prev_hash, std::int32_t{0}, std::declval<const uint288&>())); |
| 672 | + Result out{}; |
| 673 | + |
| 674 | + if (!prev_hash.IsNull() && tracker.chain.contains(prev_hash)) |
| 675 | + { |
| 676 | + auto chain_len = static_cast<int32_t>(params.real_chain_length); |
| 677 | + { |
| 678 | + auto pplns_height = tracker.chain.get_height(prev_hash); |
| 679 | + auto pplns_last = tracker.chain.get_last(prev_hash); |
| 680 | + if (!(pplns_height >= chain_len || pplns_last.IsNull())) |
| 681 | + throw std::invalid_argument( |
| 682 | + "share chain not long enough for PPLNS verification (height=" |
| 683 | + + std::to_string(pplns_height) + " need=" |
| 684 | + + std::to_string(chain_len) + ")"); |
| 685 | + } |
| 686 | + |
| 687 | + auto block_target = chain::bits_to_target(block_bits); |
| 688 | + auto max_weight = chain::target_to_average_attempts(block_target) |
| 689 | + * params.spread * 65535; |
| 690 | + |
| 691 | + if (use_v36_pplns) { |
| 692 | + uint288 unlimited_weight; |
| 693 | + unlimited_weight.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); |
| 694 | + auto result = tracker.get_v36_decayed_cumulative_weights(prev_hash, chain_len, unlimited_weight); |
| 695 | + out.weights = std::move(result.weights); |
| 696 | + out.total_weight = result.total_weight; |
| 697 | + out.total_donation_weight = result.total_donation_weight; |
| 698 | + } else { |
| 699 | + uint256 pplns_start; |
| 700 | + tracker.chain.get(prev_hash).share.invoke([&](auto* s) { |
| 701 | + pplns_start = s->m_prev_hash; // grandparent |
| 702 | + }); |
| 703 | + auto available = tracker.chain.get_height(prev_hash); |
| 704 | + auto walk_count = static_cast<int32_t>( |
| 705 | + std::max(0, std::min(chain_len, available) - 1)); |
| 706 | + |
| 707 | + if (!pplns_start.IsNull() && tracker.chain.contains(pplns_start) && walk_count > 0) { |
| 708 | + auto result = tracker.get_cumulative_weights(pplns_start, walk_count, max_weight); |
| 709 | + out.weights = std::move(result.weights); |
| 710 | + out.total_weight = result.total_weight; |
| 711 | + out.total_donation_weight = result.total_donation_weight; |
| 712 | + } |
| 713 | + } |
| 714 | + } |
| 715 | + return out; |
| 716 | +} |
| 717 | + |
| 718 | +namespace { |
| 719 | +// Build a share into the tracker. prev==nullptr => null parent (resolved chain |
| 720 | +// genesis). bits drives per-share work (target_to_average_attempts); donation in |
| 721 | +// [0,65535] drives the addr/donation weight split (65535 => zero addr weight). |
| 722 | +inline void add_walk_share(dgb::ShareTracker& tracker, const uint256& hh, |
| 723 | + const uint256* ph, uint32_t bits, uint16_t donation) |
| 724 | +{ |
| 725 | + auto* s = new dgb::MergedMiningShare(); |
| 726 | + s->m_hash = hh; |
| 727 | + if (ph) s->m_prev_hash = *ph; else s->m_prev_hash.SetNull(); |
| 728 | + s->m_bits = bits; |
| 729 | + s->m_max_bits = bits; |
| 730 | + s->m_min_header.m_bits = bits; |
| 731 | + s->m_donation = donation; |
| 732 | + dgb::ShareType st; st = s; |
| 733 | + tracker.add(st); |
| 734 | +} |
| 735 | +inline uint256 hx(const std::string& tail) { |
| 736 | + uint256 v; v.SetHex(std::string(64 - tail.size(), '0') + tail); return v; |
| 737 | +} |
| 738 | +} // namespace |
| 739 | + |
| 740 | +TEST(DGB_share_test, WeightWalkValueInvarianceBattery) |
| 741 | +{ |
| 742 | + const core::CoinParams params = dgb::make_coin_params(/*testnet=*/false); |
| 743 | + const uint32_t bb = 0x1e0fffff; // block-header bits feeding the pre-V36 cap |
| 744 | + |
| 745 | + auto assert_walk_identical = |
| 746 | + [&](dgb::ShareTracker& tk, const uint256& prev, bool v36, const char* tag) { |
| 747 | + const auto legacy = legacy_inline_weight_walk(tk, prev, bb, params, v36); |
| 748 | + const auto helper = dgb::coin::compute_pplns_weight_walk(tk, prev, bb, params, v36); |
| 749 | + EXPECT_EQ(helper.weights, legacy.weights) << tag << " weights"; |
| 750 | + EXPECT_EQ(helper.total_weight, legacy.total_weight) << tag << " total_weight"; |
| 751 | + EXPECT_EQ(helper.total_donation_weight, legacy.total_donation_weight) << tag << " donation_weight"; |
| 752 | + |
| 753 | + // End-to-end: identical weights => identical payout split (the #328 |
| 754 | + // SSOT), proven on the SAME inputs both paths feed. |
| 755 | + std::vector<unsigned char> finder = helper.weights.empty() ? std::vector<unsigned char>{} : helper.weights.begin()->first; |
| 756 | + for (uint64_t subsidy : {uint64_t(0), uint64_t(625000000)}) { |
| 757 | + auto sp_h = dgb::coin::compute_pplns_payout_split( |
| 758 | + helper.weights, helper.total_weight, subsidy, v36, finder); |
| 759 | + auto sp_l = dgb::coin::compute_pplns_payout_split( |
| 760 | + legacy.weights, legacy.total_weight, subsidy, v36, finder); |
| 761 | + EXPECT_EQ(sp_h.payout_outputs, sp_l.payout_outputs) << tag << " payout_outputs s=" << subsidy; |
| 762 | + EXPECT_EQ(sp_h.donation_amount, sp_l.donation_amount) << tag << " donation s=" << subsidy; |
| 763 | + } |
| 764 | + }; |
| 765 | + |
| 766 | + // Resolved chain g <- a <- b <- c (g.prev null). Mixed donation incl. 0 and |
| 767 | + // full-donation (65535 => that share contributes ZERO addr weight) and |
| 768 | + // varied bits (varied per-share work). |
| 769 | + { |
| 770 | + dgb::ShareTracker tk; |
| 771 | + uint256 g = hx("c0"), a = hx("c1"), b = hx("c2"), c = hx("c3"); |
| 772 | + add_walk_share(tk, g, nullptr, 0x1e0fffff, 0); |
| 773 | + add_walk_share(tk, a, &g, 0x1e07ffff, 32767); |
| 774 | + add_walk_share(tk, b, &a, 0x1e0fffff, 65535); // zero addr-weight share |
| 775 | + add_walk_share(tk, c, &b, 0x1d00ffff, 100); |
| 776 | + assert_walk_identical(tk, c, /*v36=*/true, "v36-basic"); |
| 777 | + assert_walk_identical(tk, c, /*v36=*/false, "preV36-basic(grandparent+cap)"); |
| 778 | + |
| 779 | + // Donation weight is genuinely exercised (>0) so the donation field is |
| 780 | + // not a trivial zero on both sides. |
| 781 | + const auto w = dgb::coin::compute_pplns_weight_walk(tk, c, bb, params, true); |
| 782 | + EXPECT_FALSE(w.total_donation_weight.IsNull()) << "battery must exercise non-zero donation weight"; |
| 783 | + EXPECT_FALSE(w.total_weight.IsNull()) << "battery must exercise non-zero total weight"; |
| 784 | + } |
| 785 | + |
| 786 | + // prev points at genesis directly => pre-V36 grandparent is null => empty |
| 787 | + // (both), even though prev IS in the chain. |
| 788 | + { |
| 789 | + dgb::ShareTracker tk; |
| 790 | + uint256 g = hx("d0"), a = hx("d1"); |
| 791 | + add_walk_share(tk, g, nullptr, 0x1e0fffff, 0); |
| 792 | + add_walk_share(tk, a, &g, 0x1e0fffff, 0); |
| 793 | + assert_walk_identical(tk, g, /*v36=*/false, "preV36-grandparent-null"); |
| 794 | + } |
| 795 | + |
| 796 | + // null parent / absent parent => empty, no throw (both). |
| 797 | + { |
| 798 | + dgb::ShareTracker tk; |
| 799 | + uint256 g = hx("e0"); |
| 800 | + add_walk_share(tk, g, nullptr, 0x1e0fffff, 0); |
| 801 | + assert_walk_identical(tk, uint256::ZERO, true, "null-parent"); |
| 802 | + assert_walk_identical(tk, hx("deadbeef"), true, "absent-parent"); |
| 803 | + } |
| 804 | + |
| 805 | + // Insufficient-depth guard: a chain whose oldest share points at a NON-null |
| 806 | + // hash absent from the tracker (so chain.get_last(prev) is non-null) and |
| 807 | + // whose height < real_chain_length MUST throw — identically, both paths. |
| 808 | + { |
| 809 | + dgb::ShareTracker tk; |
| 810 | + uint256 root_missing = hx("beef"), a = hx("f1"), b = hx("f2"); |
| 811 | + add_walk_share(tk, a, &root_missing, 0x1e0fffff, 0); // a.prev absent => get_last non-null |
| 812 | + add_walk_share(tk, b, &a, 0x1e0fffff, 0); |
| 813 | + EXPECT_THROW(legacy_inline_weight_walk(tk, b, bb, params, true), std::invalid_argument) |
| 814 | + << "legacy guard must throw on insufficient depth"; |
| 815 | + EXPECT_THROW(dgb::coin::compute_pplns_weight_walk(tk, b, bb, params, true), std::invalid_argument) |
| 816 | + << "helper guard must throw identically"; |
| 817 | + } |
| 818 | + |
| 819 | + // Exceeds the real_chain_length window: testnet params (window=400); build a |
| 820 | + // resolved chain LONGER than the window and prove the walk caps identically. |
| 821 | + { |
| 822 | + const core::CoinParams tn = dgb::make_coin_params(/*testnet=*/true); |
| 823 | + const int32_t window = static_cast<int32_t>(tn.real_chain_length); // 400 |
| 824 | + dgb::ShareTracker tk; |
| 825 | + uint256 prev; prev.SetNull(); |
| 826 | + uint256 tip; |
| 827 | + for (int i = 0; i < window + 5; ++i) { |
| 828 | + char buf[16]; std::snprintf(buf, sizeof(buf), "%x", 0x9000 + i); |
| 829 | + uint256 h = hx(buf); |
| 830 | + add_walk_share(tk, h, (i == 0 ? nullptr : &prev), |
| 831 | + 0x1e0fffff, static_cast<uint16_t>((i * 7919) % 65535)); |
| 832 | + prev = h; tip = h; |
| 833 | + } |
| 834 | + const auto legacy = legacy_inline_weight_walk(tk, tip, bb, tn, /*v36=*/true); |
| 835 | + const auto helper = dgb::coin::compute_pplns_weight_walk(tk, tip, bb, tn, /*v36=*/true); |
| 836 | + EXPECT_EQ(helper.weights, legacy.weights) << "window weights"; |
| 837 | + EXPECT_EQ(helper.total_weight, legacy.total_weight) << "window total_weight"; |
| 838 | + EXPECT_EQ(helper.total_donation_weight, legacy.total_donation_weight) << "window donation_weight"; |
| 839 | + } |
| 840 | +} |
0 commit comments