Skip to content

Commit 84f1466

Browse files
committed
dgb(test): weight-invariance KAT for PPLNS step-1 SSOT lift (#333)
Same bar as the #328 payout-split invariance KAT: prove the #333 lift of generate_share_transaction() step-1 into compute_pplns_weight_walk() moves ZERO weight. Embed the pre-refactor inline walk verbatim as legacy_inline_weight_walk() and assert the helper reproduces it weight-for- weight (per-script weights, total_weight, total_donation_weight) across a battery: V36 decay path, pre-V36 grandparent-start + max_weight cap, donation split, full-donation zero-addr-weight share, the insufficient-depth guard (both throw identically), null/absent parents, and a chain exceeding the real_chain_length window. Each weight map is then fed through the #328 payout-split SSOT and the outputs + donation asserted byte-identical, closing the end-to-end no-payout-satoshi-drifts proof shared by the emission and verification paths. dgb_share_test 28/28.
1 parent c4eac86 commit 84f1466

1 file changed

Lines changed: 208 additions & 0 deletions

File tree

src/impl/dgb/test/share_test.cpp

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <impl/dgb/share.hpp>
2020
#include <impl/dgb/share_tracker.hpp> // DensePPLNSWindow — V36 decayed-PPLNS SSOT
2121
#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)
2223
#include <impl/dgb/params.hpp> // make_coin_params — assembled CoinParams SSOT
2324
#include <impl/dgb/coin/rpc_conf.hpp> // #82 external-daemon RPC creds (digibyte.conf)
2425
#include <impl/dgb/auto_ratchet.hpp> // Phase B: mint-side share-version ratchet
@@ -630,3 +631,210 @@ TEST(DGB_run_loop_mint, AdapterFailsClosedOnShortHeader)
630631
uint256 h = dgb::mint_local_share_with_ratchet(in, tracker, params, ratchet);
631632
EXPECT_TRUE(h.IsNull());
632633
}
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

Comments
 (0)