Skip to content

Commit 435e975

Browse files
authored
btc(stratum): integer-exact unconditional v35 finder fee in mined-block coinbase (D2) (#737)
The mined-block coinbase computed the block-finder fee as a float (coinbasevalue/200.0) and applied it only when the donation output could cover the full amount (all-or-nothing skip). Both diverge from the p2pool reference (data.py generate_transaction: amounts[finder] += subsidy//200, integer floor, unconditional) and were a latent satoshi-rounding divergence on the money path of a won block. The consensus share gentx was already integer-exact; this makes the block-coinbase path identical. Extract v35_finder_fee_split() (finder_fee.hpp SSOT): floor(cbv/200), capped at the donation available so the coinbase stays balanced (total == subsidy) and no output goes negative. No float in the split. Base stays coinbasevalue, matching core/web_server.cpp:1888 (p2pool subsidy = template coinbasevalue incl. fees). KAT finder_fee_integer_exact_test.cpp (btc_share_test): pins the split vs the jtoomim subsidy//200 formula at round and odd coinbase values, the sub-200 zero edge, the donation cap, and a float-vs-integer divergence witness. 4/4 green; full btc_share_test 50/50. Co-authored-by: frstrtr <frstrtr@users.noreply.github.com>
1 parent fcaf1e0 commit 435e975

4 files changed

Lines changed: 135 additions & 18 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
#pragma once
3+
4+
// btc::stratum::v35_finder_fee_split — integer-exact v35 block-finder fee.
5+
//
6+
// The mined-block coinbase moves 0.5% of the coinbase value from the donation
7+
// residual to the share-finder's payout output. This mirrors the p2pool
8+
// reference (data.py generate_transaction):
9+
// amounts[finder] += share_data['subsidy'] // 200 # 0.5%, floor, unconditional
10+
// where p2pool's `subsidy` is the block-template coinbasevalue (subsidy +
11+
// fees) — so the c2pool base is coinbasevalue (matching core/web_server.cpp).
12+
//
13+
// The consensus share gentx already computes this integer-exactly; this SSOT
14+
// keeps the block-coinbase money path identical: floor division, NO float, so
15+
// there is no sub-satoshi rounding divergence on a won block.
16+
//
17+
// Returns the satoshis actually moved: floor(coinbasevalue / 200), capped at
18+
// the donation available so the coinbase stays balanced (total == subsidy) and
19+
// no output goes negative. The cap only binds in a pathological rounding edge —
20+
// in canonical operation the donation residual (~0.5%) covers the fee (~0.5%),
21+
// so the full fee is always applied (unconditional; never all-or-nothing
22+
// skipped as the prior float path did when donation < fee).
23+
24+
#include <algorithm>
25+
#include <cstdint>
26+
27+
namespace btc::stratum {
28+
29+
inline uint64_t v35_finder_fee_split(uint64_t coinbasevalue, uint64_t donation_sats)
30+
{
31+
const uint64_t finder_fee = coinbasevalue / 200; // integer floor == subsidy//200
32+
return std::min(finder_fee, donation_sats);
33+
}
34+
35+
} // namespace btc::stratum

src/impl/btc/stratum/work_source.cpp

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
#include <impl/btc/stratum/work_source.hpp>
1515
#include <impl/btc/stratum/tx_data_memo.hpp> // H5 tx_data memo seam (work_source.cpp:634 churn fix)
16+
#include <impl/btc/stratum/finder_fee.hpp> // D2 v35_finder_fee_split integer-exact SSOT
1617
#include <memory>
1718

1819
#include <impl/btc/coin/header_chain.hpp>
@@ -446,23 +447,24 @@ core::stratum::CoinbaseResult BTCWorkSource::build_connection_coinbase(
446447
}
447448
}
448449

449-
// v35 finder fee: 0.5% of subsidy goes to the share-finder (this miner),
450-
// deducted from donation. Reference: c2pool_refactored.cpp wiring +
451-
// share_tracker.hpp v35 PPLNS docs ("amounts WITHOUT finder fee — caller
452-
// adds subsidy/200 to the share creator's script"). Conditional on having
453-
// a non-empty payout_script — otherwise we'd reintroduce the empty-output
454-
// bug we just filtered. And the deduction-from-donation must succeed
455-
// (donation must hold ≥ finder_fee), else we'd inflate total > subsidy.
456-
if (!payouts.empty() && coinbasevalue > 0 && !payout_script.empty()) {
457-
const double finder_fee = static_cast<double>(coinbasevalue) / 200.0;
458-
if (!donation_script.empty()) {
459-
auto it = payouts.find(donation_script);
460-
if (it != payouts.end() && it->second >= finder_fee) {
461-
it->second -= finder_fee;
462-
payouts[payout_script] += finder_fee;
463-
}
464-
// else: donation can't cover the fee — skip silently. Total stays
465-
// at subsidy (per get_v35_expected_payouts post-condition).
450+
// v35 finder fee: 0.5% of the coinbase value goes to the share-finder
451+
// (this miner), moved from the donation residual to the finder's output.
452+
// Integer floor division (subsidy//200) via the v35_finder_fee_split SSOT —
453+
// NO float on the money path (see finder_fee.hpp). Matches the p2pool
454+
// reference (data.py generate_transaction: amounts[finder] += subsidy//200,
455+
// unconditional) and the core/web_server.cpp preview path. The split is
456+
// capped at the donation available, so it is applied unconditionally
457+
// (never all-or-nothing skipped) while total stays == subsidy and no output
458+
// goes negative. The map holds doubles, but the moved value is an exact
459+
// integer satoshi count (<= 2^53, exact in double).
460+
if (!payouts.empty() && coinbasevalue > 0 && !payout_script.empty()
461+
&& !donation_script.empty()) {
462+
auto it = payouts.find(donation_script);
463+
if (it != payouts.end()) {
464+
const uint64_t donation_sats = static_cast<uint64_t>(it->second);
465+
const uint64_t taken = v35_finder_fee_split(coinbasevalue, donation_sats);
466+
it->second -= static_cast<double>(taken);
467+
payouts[payout_script] += static_cast<double>(taken);
466468
}
467469
}
468470

src/impl/btc/test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
if (BUILD_TESTING AND GTest_FOUND)
22
# btc twin of ltc share_test — uniquely named to avoid the CMP0002 target
33
# collision with src/impl/ltc/test (both subdirs build in the same tree).
4-
add_executable(btc_share_test share_test.cpp f11_donation_invariance_test.cpp f10b_version_punish_removal_test.cpp g01_share_format_parity_test.cpp auto_ratchet_sim_test.cpp block_broadcast_guard_test.cpp block_broadcast_connect_test.cpp regtest_sharechain_isolation_test.cpp stratum_merkle_branch_test.cpp witness_commitment_merkle_test.cpp)
4+
add_executable(btc_share_test share_test.cpp f11_donation_invariance_test.cpp f10b_version_punish_removal_test.cpp g01_share_format_parity_test.cpp auto_ratchet_sim_test.cpp block_broadcast_guard_test.cpp block_broadcast_connect_test.cpp regtest_sharechain_isolation_test.cpp stratum_merkle_branch_test.cpp witness_commitment_merkle_test.cpp finder_fee_integer_exact_test.cpp)
55
target_link_libraries(btc_share_test PRIVATE
66
GTest::gtest_main GTest::gtest
77
core btc
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
// D2 finder-fee integer-exactness KAT.
3+
//
4+
// Pins the mined-block coinbase block-finder fee split to the p2pool reference
5+
// as INTEGER floor division, closing the latent satoshi-rounding divergence
6+
// that the prior float path (coinbasevalue/200.0) carried onto the money path
7+
// of a won block in src/impl/btc/stratum/work_source.cpp.
8+
//
9+
// Reference (jtoomim p2pool data.py generate_transaction, verified against
10+
// ~/p2pool-jtoomim@ece15b0):
11+
// amounts = {script: subsidy*(199*w)//(200*W) ...} # 99.5% by weight
12+
// amounts[finder] += subsidy // 200 # 0.5% finder fee, floor, unconditional
13+
// amounts[donation] += subsidy - sum(amounts) # residual balances to subsidy
14+
// p2pool's `subsidy` is the block-template coinbasevalue (subsidy + fees), so
15+
// the c2pool base is coinbasevalue — matching src/core/web_server.cpp:1888.
16+
//
17+
// The consensus share gentx already matched this; D2 makes the block-coinbase
18+
// path identical (integer floor, no float, unconditional application).
19+
20+
#include <gtest/gtest.h>
21+
22+
#include <cmath>
23+
#include <cstdint>
24+
25+
#include <impl/btc/stratum/finder_fee.hpp>
26+
27+
using btc::stratum::v35_finder_fee_split;
28+
29+
namespace {
30+
31+
// The jtoomim reference operation: integer floor division, subsidy//200.
32+
uint64_t reference_finder_fee(uint64_t coinbasevalue) { return coinbasevalue / 200; }
33+
34+
} // namespace
35+
36+
// When the donation residual covers the fee (the canonical case: donation
37+
// ~0.5% >= finder fee ~0.5%), the moved amount equals the integer reference
38+
// exactly — for both round and odd coinbase values.
39+
TEST(BtcFinderFeeD2, MatchesJtoomimIntegerFormula) {
40+
const uint64_t big_donation = 1'000'000'000ULL; // always covers the fee
41+
for (uint64_t cbv : {0ULL, 199ULL, 200ULL, 312'500'000ULL, 312'500'100ULL,
42+
312'500'199ULL, 512'345'678ULL, 625'000'001ULL}) {
43+
EXPECT_EQ(v35_finder_fee_split(cbv, big_donation), reference_finder_fee(cbv))
44+
<< "cbv=" << cbv;
45+
}
46+
}
47+
48+
// Sub-200 coinbase -> floor(cbv/200)==0 -> finder gets nothing; no negative
49+
// output, no phantom satoshi.
50+
TEST(BtcFinderFeeD2, BelowThresholdIsZero) {
51+
EXPECT_EQ(v35_finder_fee_split(0ULL, 1000ULL), 0ULL);
52+
EXPECT_EQ(v35_finder_fee_split(199ULL, 1000ULL), 0ULL);
53+
EXPECT_EQ(v35_finder_fee_split(200ULL, 1000ULL), 1ULL);
54+
}
55+
56+
// Donation cap: never move more than the donation holds, so the coinbase stays
57+
// balanced (total == subsidy) and the donation output never goes negative.
58+
TEST(BtcFinderFeeD2, CappedByDonation) {
59+
// Fee would be 1'562'500 but the donation only holds 1'000.
60+
EXPECT_EQ(v35_finder_fee_split(312'500'199ULL, 1'000ULL), 1'000ULL);
61+
// Exact-cover boundary and one satoshi short of it.
62+
EXPECT_EQ(v35_finder_fee_split(312'500'000ULL, 1'562'500ULL), 1'562'500ULL);
63+
EXPECT_EQ(v35_finder_fee_split(312'500'000ULL, 1'562'499ULL), 1'562'499ULL);
64+
}
65+
66+
// Divergence witness: at odd coinbase values the OLD float path carried a
67+
// fractional satoshi (coinbasevalue/200.0) onto the money path, and naive
68+
// float-rounding disagreed with the integer floor by a full satoshi — exactly
69+
// the divergence D2 closes.
70+
TEST(BtcFinderFeeD2, FloatPathDivergesFromInteger) {
71+
const uint64_t cbv = 312'500'199ULL;
72+
const double float_fee = static_cast<double>(cbv) / 200.0; // 1562500.995
73+
// The old float fee carried a sub-satoshi fraction:
74+
EXPECT_NE(float_fee, std::trunc(float_fee));
75+
// Rounding that float disagrees with the integer floor by one satoshi:
76+
EXPECT_EQ(static_cast<uint64_t>(std::llround(float_fee)), 1'562'501ULL);
77+
EXPECT_EQ(v35_finder_fee_split(cbv, 1'000'000'000ULL), 1'562'500ULL);
78+
EXPECT_NE(static_cast<uint64_t>(std::llround(float_fee)),
79+
v35_finder_fee_split(cbv, 1'000'000'000ULL));
80+
}

0 commit comments

Comments
 (0)