Skip to content

Commit 451ffb1

Browse files
authored
Merge pull request #688 from frstrtr/bch/abla-msvc-portable-muldiv
bch/abla: MSVC-portable 128-bit muldiv (fixes c2pool-bch Windows build)
2 parents 51ba4f7 + 3299765 commit 451ffb1

4 files changed

Lines changed: 151 additions & 3 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ jobs:
366366
bch_embedded_getwork_test bch_embedded_seam_workview_test \
367367
bch_embedded_block_broadcast_test bch_coin_node_seam_test \
368368
bch_asert_kat_test \
369+
bch_muldiv_kat_test \
369370
bch_g1_oracle_byte_parity_test bch_g1_share_serialization_parity_test bch_g0_canonical_pin_test bch_g2_ratchet_gate_kat_test \
370371
-j8
371372
@@ -460,6 +461,7 @@ jobs:
460461
bch_embedded_getwork_test bch_embedded_seam_workview_test \
461462
bch_embedded_block_broadcast_test bch_coin_node_seam_test \
462463
bch_asert_kat_test \
464+
bch_muldiv_kat_test \
463465
bch_g1_oracle_byte_parity_test bch_g1_share_serialization_parity_test bch_g0_canonical_pin_test bch_g2_ratchet_gate_kat_test \
464466
-j8
465467

src/impl/bch/coin/abla.hpp

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
#include <cstdint>
2828
#include <limits>
2929

30+
// Portable 128-bit intermediate for muldiv on compilers without __int128 (MSVC).
31+
// Header-only; boost is already a c2pool dependency (conan + system libboost).
32+
#include <boost/multiprecision/cpp_int.hpp>
33+
3034
namespace bch {
3135
namespace coin {
3236
namespace abla {
@@ -39,16 +43,42 @@ inline constexpr uint64_t MAX_CONSENSUS_BLOCK_SIZE = uint64_t(2000) * ONE_ME
3943
// 2^7 fixed precision for the "asymmetry factor" (zeta). BCHN abla.h B7.
4044
inline constexpr uint64_t B7 = 1u << 7u;
4145

42-
// muldiv(x,y,z) = x*y/z in 128-bit intermediate (BCHN abla.cpp). The platform
43-
// is gcc/clang C++20 on linux -> use the native __int128 path BCHN selects.
44-
inline uint64_t muldiv(uint64_t x, uint64_t y, uint64_t z) {
46+
// muldiv(x,y,z) = x*y/z evaluated in a 128-bit intermediate (BCHN abla.cpp).
47+
// Consensus-critical: the product x*y can reach ~2^128, so the intermediate MUST
48+
// be a true 128-bit type; the ABLA control function guarantees the final
49+
// quotient fits uint64_t (asserted below).
50+
//
51+
// GCC/Clang provide the native unsigned __int128 that BCHN selects -- that path
52+
// is kept BYTE-EXACT for the shipping Linux/macOS builds. MSVC has no __int128
53+
// (error C4235), which broke the c2pool-bch Windows package; on any compiler
54+
// without __int128 we fall back to boost::multiprecision::uint128_t, a fixed
55+
// 128-bit unsigned type that yields BIT-IDENTICAL results. The two paths are
56+
// pinned equal on Linux by muldiv_kat_test (native == portable across the full
57+
// ABLA operand space) -- the KAT guard required for this consensus math.
58+
inline uint64_t muldiv_portable(uint64_t x, uint64_t y, uint64_t z) {
59+
assert(z != 0);
60+
using u128 = boost::multiprecision::uint128_t;
61+
const u128 res = (u128(x) * u128(y)) / u128(z);
62+
assert(res <= u128(std::numeric_limits<uint64_t>::max()));
63+
return static_cast<uint64_t>(res);
64+
}
65+
66+
#if defined(__SIZEOF_INT128__)
67+
// Native path (GCC/Clang) -- unchanged; this is what the merged Linux/macOS
68+
// packages have always shipped. Exposed by name so the KAT can diff it against
69+
// muldiv_portable on the trusted platform.
70+
inline uint64_t muldiv_native(uint64_t x, uint64_t y, uint64_t z) {
4571
assert(z != 0);
4672
const unsigned __int128 res =
4773
(static_cast<unsigned __int128>(x) * static_cast<unsigned __int128>(y))
4874
/ static_cast<unsigned __int128>(z);
4975
assert(res <= static_cast<unsigned __int128>(std::numeric_limits<uint64_t>::max()));
5076
return static_cast<uint64_t>(res);
5177
}
78+
inline uint64_t muldiv(uint64_t x, uint64_t y, uint64_t z) { return muldiv_native(x, y, z); }
79+
#else
80+
inline uint64_t muldiv(uint64_t x, uint64_t y, uint64_t z) { return muldiv_portable(x, y, z); }
81+
#endif
5282

5383
// Algorithm configuration -- part of a chain's consensus params (BCHN abla.h).
5484
struct Config {

src/impl/bch/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ if(BUILD_TESTING)
2020
abla_growth_soak_test # M5: dynamic ABLA budget-GROWTH proof above 32 MB floor (pure)
2121
coinbase_kat_segwit_predicate_test # KAT: DGB<->BCH coinbase pairing -- gated-segwit-OFF predicate pin (pure)
2222
asert_kat_test # M5: ASERT DAA known-answer vs BCHN @89a591f gold properties + mainnet anchor (pure)
23+
muldiv_kat_test # ABLA: 128-bit muldiv native(__int128)==portable(boost uint128) equivalence -- MSVC-portability guard (pure)
2324
g1_oracle_byte_parity_test # G1: oracle byte-parity vs p2pool-merged-v36 bitcoincash[_testnet].py (pure)
2425
g1_share_serialization_parity_test # G1: share-serialization byte-parity vs jtoomim oracle wire (pure)
2526
g0_canonical_pin_test # G0: canonical-pin vs p2poolBCH @6603b79 baseline + v35->v36 transition rule (pure)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// ---------------------------------------------------------------------------
2+
// bch::abla muldiv 128-bit KAT -- the guard for the MSVC-portability rework.
3+
//
4+
// abla.hpp evaluates x*y/z (BCHN control function) in a 128-bit intermediate.
5+
// GCC/Clang use native unsigned __int128; MSVC (no __int128, error C4235) falls
6+
// back to boost::multiprecision::uint128_t. Both must be BIT-IDENTICAL or the
7+
// Windows package would silently diverge from the shipping Linux/macOS ABLA.
8+
//
9+
// This test pins that in two layers:
10+
// A. cross-platform known-answers: muldiv() must produce exact fixed results
11+
// on EVERY compiler (this is all MSVC can run -- no native to diff).
12+
// B. Linux-only equivalence sweep (guarded by __SIZEOF_INT128__): the portable
13+
// path must equal the native path across the full ABLA operand space plus a
14+
// deterministic wide-operand fuzz. Proving portable == native on the trusted
15+
// platform is what certifies the MSVC path we cannot run here.
16+
//
17+
// Build-INERT wrt consensus surface: pure header math, no node/RPC/boost-graph.
18+
// p2pool-merged-v36 surface: NONE (ABLA is a LOCAL build-time byte budget only).
19+
// ---------------------------------------------------------------------------
20+
21+
#include <cstdint>
22+
#include <iostream>
23+
#include <limits>
24+
25+
#include "../coin/abla.hpp"
26+
27+
namespace {
28+
29+
int failures = 0;
30+
#define CHECK(cond) do { if (!(cond)) { \
31+
std::cerr << "FAIL: " #cond " @ line " << __LINE__ << "\n"; ++failures; } } while (0)
32+
33+
using bch::coin::abla::muldiv;
34+
using bch::coin::abla::muldiv_portable;
35+
using bch::coin::abla::B7;
36+
37+
constexpr uint64_t U64MAX = std::numeric_limits<uint64_t>::max();
38+
39+
// Deterministic 64-bit LCG (no <random>, no Date/rand) -- reproducible vectors.
40+
struct Lcg {
41+
uint64_t s;
42+
explicit Lcg(uint64_t seed) : s(seed) {}
43+
uint64_t next() { s = s * 6364136223846793005ULL + 1442695040888963407ULL; return s; }
44+
};
45+
46+
} // namespace
47+
48+
int main() {
49+
// ---- A. cross-platform known answers (run on ALL compilers incl. MSVC) ----
50+
// hand-computed; several exercise a product > 2^64 so the true 128-bit
51+
// intermediate is required (a 64-bit multiply would wrap and fail these).
52+
CHECK(muldiv(192, 16000000ULL, 128) == 24000000ULL); // ABLA amplify shape
53+
CHECK(muldiv(6, 7, 2) == 21ULL);
54+
CHECK(muldiv(0, 12345, 7) == 0ULL);
55+
CHECK(muldiv(U64MAX, 1, 1) == U64MAX);
56+
CHECK(muldiv(U64MAX, U64MAX, U64MAX) == U64MAX); // product ~2^128
57+
CHECK(muldiv(1000000000000000000ULL, 1000000000ULL,
58+
1000000000000ULL) == 1000000000000000ULL); // 1e27 intermediate
59+
CHECK(muldiv(B7, 32ULL * 1000000ULL, B7) == 32ULL * 1000000ULL);// identity via B7
60+
61+
// portable path must independently satisfy the same known answers
62+
CHECK(muldiv_portable(192, 16000000ULL, 128) == 24000000ULL);
63+
CHECK(muldiv_portable(U64MAX, U64MAX, U64MAX) == U64MAX);
64+
CHECK(muldiv_portable(1000000000000000000ULL, 1000000000ULL,
65+
1000000000000ULL) == 1000000000000000ULL);
66+
67+
#if defined(__SIZEOF_INT128__)
68+
using bch::coin::abla::muldiv_native;
69+
70+
// ---- B1. exact ABLA-domain vectors: native == portable ----
71+
// operand shapes taken straight from State::NextBlockState call sites.
72+
const uint64_t sizes[] = {
73+
0, 1, B7, 192, 37938, 1000000ULL, 16ULL * 1000000ULL,
74+
32ULL * 1000000ULL, 2000ULL * 1000000ULL, (1ULL << 40), (1ULL << 62)
75+
};
76+
for (uint64_t x : sizes)
77+
for (uint64_t y : sizes)
78+
for (uint64_t z : sizes) {
79+
if (z == 0) continue;
80+
// only compare where the true quotient fits uint64_t (the ABLA
81+
// invariant); otherwise both paths take low-64 of an out-of-range
82+
// value and the comparison is not meaningful.
83+
const unsigned __int128 q =
84+
(static_cast<unsigned __int128>(x) * y) / z;
85+
if (q > static_cast<unsigned __int128>(U64MAX)) continue;
86+
CHECK(muldiv_native(x, y, z) == muldiv_portable(x, y, z));
87+
}
88+
89+
// ---- B2. wide deterministic fuzz: native == portable ----
90+
Lcg rng(0x9E3779B97F4A7C15ULL);
91+
int compared = 0;
92+
for (int i = 0; i < 200000; ++i) {
93+
const uint64_t x = rng.next();
94+
const uint64_t y = rng.next();
95+
uint64_t z = rng.next();
96+
if (z == 0) z = 1;
97+
const unsigned __int128 q =
98+
(static_cast<unsigned __int128>(x) * y) / z;
99+
if (q > static_cast<unsigned __int128>(U64MAX)) continue; // keep in-domain
100+
++compared;
101+
if (muldiv_native(x, y, z) != muldiv_portable(x, y, z)) {
102+
std::cerr << "FAIL fuzz: x=" << x << " y=" << y << " z=" << z << "\n";
103+
++failures;
104+
}
105+
}
106+
CHECK(compared > 1000); // sanity: the domain filter did not reject everything
107+
std::cerr << "muldiv KAT: native==portable over " << compared << " fuzz vectors\n";
108+
#else
109+
std::cerr << "muldiv KAT: no __int128 (MSVC) -- known-answer layer only\n";
110+
#endif
111+
112+
if (failures) { std::cerr << failures << " CHECK(s) FAILED\n"; return 1; }
113+
std::cerr << "muldiv_kat_test: ALL PASS\n";
114+
return 0;
115+
}

0 commit comments

Comments
 (0)