From 3299765fa84dcd85a8478546fd52bbf78f772624 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Mon, 13 Jul 2026 05:29:13 +0000 Subject: [PATCH] bch/abla: make 128-bit muldiv MSVC-portable (fixes c2pool-bch Windows build) abla.hpp evaluated the ABLA control-function muldiv(x,y,z)=x*y/z in a native unsigned __int128 intermediate. __int128 is a GCC/Clang extension MSVC does not provide (error C4235 __int128 keyword not supported on this architecture), so c2pool-bch failed to compile on Windows while Linux/macOS passed -- blocking the v0.2.1 c2pool-bch-windows package. Keep the native __int128 path BYTE-EXACT for GCC/Clang (the shipping Linux/macOS builds are unchanged) under #if defined(__SIZEOF_INT128__). For any compiler without __int128 (MSVC), fall back to boost::multiprecision::uint128_t -- a fixed 128-bit unsigned type, header-only, already a project dependency (conan + system libboost). ABLA math stays exact. Guard: new bch_muldiv_kat_test pins the two paths equal. Layer A is a set of cross-platform known answers (several with >2^64 products, so a 64-bit multiply would fail) that muldiv must satisfy on every compiler incl. MSVC. Layer B, on platforms with __int128, asserts muldiv_native == muldiv_portable across the full ABLA operand space plus a deterministic 200k-vector wide fuzz -- proving the portable arithmetic is bit-identical to native on the trusted platform, which certifies the MSVC path we cannot run in this CI. Verified locally: KAT PASS (native==portable over 150260 in-domain fuzz vectors, release+debug); ABLA growth soak unchanged (mainnet final=41305530). Wired bch_muldiv_kat_test into src/impl/bch/test/CMakeLists.txt and both build.yml COIN_BCH target lists. p2pool-merged-v36 surface: NONE (ABLA is a local build-time block-size budget only; no PoW/share/coinbase/PPLNS math touched). --- .github/workflows/build.yml | 2 + src/impl/bch/coin/abla.hpp | 36 +++++++- src/impl/bch/test/CMakeLists.txt | 1 + src/impl/bch/test/muldiv_kat_test.cpp | 115 ++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 src/impl/bch/test/muldiv_kat_test.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 250d89bdd..eda47e2d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -366,6 +366,7 @@ jobs: bch_embedded_getwork_test bch_embedded_seam_workview_test \ bch_embedded_block_broadcast_test bch_coin_node_seam_test \ bch_asert_kat_test \ + bch_muldiv_kat_test \ bch_g1_oracle_byte_parity_test bch_g1_share_serialization_parity_test bch_g0_canonical_pin_test bch_g2_ratchet_gate_kat_test \ -j8 @@ -460,6 +461,7 @@ jobs: bch_embedded_getwork_test bch_embedded_seam_workview_test \ bch_embedded_block_broadcast_test bch_coin_node_seam_test \ bch_asert_kat_test \ + bch_muldiv_kat_test \ bch_g1_oracle_byte_parity_test bch_g1_share_serialization_parity_test bch_g0_canonical_pin_test bch_g2_ratchet_gate_kat_test \ -j8 diff --git a/src/impl/bch/coin/abla.hpp b/src/impl/bch/coin/abla.hpp index 20e1bdf8b..8237aabd4 100644 --- a/src/impl/bch/coin/abla.hpp +++ b/src/impl/bch/coin/abla.hpp @@ -27,6 +27,10 @@ #include #include +// Portable 128-bit intermediate for muldiv on compilers without __int128 (MSVC). +// Header-only; boost is already a c2pool dependency (conan + system libboost). +#include + namespace bch { namespace coin { namespace abla { @@ -39,9 +43,31 @@ inline constexpr uint64_t MAX_CONSENSUS_BLOCK_SIZE = uint64_t(2000) * ONE_ME // 2^7 fixed precision for the "asymmetry factor" (zeta). BCHN abla.h B7. inline constexpr uint64_t B7 = 1u << 7u; -// muldiv(x,y,z) = x*y/z in 128-bit intermediate (BCHN abla.cpp). The platform -// is gcc/clang C++20 on linux -> use the native __int128 path BCHN selects. -inline uint64_t muldiv(uint64_t x, uint64_t y, uint64_t z) { +// muldiv(x,y,z) = x*y/z evaluated in a 128-bit intermediate (BCHN abla.cpp). +// Consensus-critical: the product x*y can reach ~2^128, so the intermediate MUST +// be a true 128-bit type; the ABLA control function guarantees the final +// quotient fits uint64_t (asserted below). +// +// GCC/Clang provide the native unsigned __int128 that BCHN selects -- that path +// is kept BYTE-EXACT for the shipping Linux/macOS builds. MSVC has no __int128 +// (error C4235), which broke the c2pool-bch Windows package; on any compiler +// without __int128 we fall back to boost::multiprecision::uint128_t, a fixed +// 128-bit unsigned type that yields BIT-IDENTICAL results. The two paths are +// pinned equal on Linux by muldiv_kat_test (native == portable across the full +// ABLA operand space) -- the KAT guard required for this consensus math. +inline uint64_t muldiv_portable(uint64_t x, uint64_t y, uint64_t z) { + assert(z != 0); + using u128 = boost::multiprecision::uint128_t; + const u128 res = (u128(x) * u128(y)) / u128(z); + assert(res <= u128(std::numeric_limits::max())); + return static_cast(res); +} + +#if defined(__SIZEOF_INT128__) +// Native path (GCC/Clang) -- unchanged; this is what the merged Linux/macOS +// packages have always shipped. Exposed by name so the KAT can diff it against +// muldiv_portable on the trusted platform. +inline uint64_t muldiv_native(uint64_t x, uint64_t y, uint64_t z) { assert(z != 0); const unsigned __int128 res = (static_cast(x) * static_cast(y)) @@ -49,6 +75,10 @@ inline uint64_t muldiv(uint64_t x, uint64_t y, uint64_t z) { assert(res <= static_cast(std::numeric_limits::max())); return static_cast(res); } +inline uint64_t muldiv(uint64_t x, uint64_t y, uint64_t z) { return muldiv_native(x, y, z); } +#else +inline uint64_t muldiv(uint64_t x, uint64_t y, uint64_t z) { return muldiv_portable(x, y, z); } +#endif // Algorithm configuration -- part of a chain's consensus params (BCHN abla.h). struct Config { diff --git a/src/impl/bch/test/CMakeLists.txt b/src/impl/bch/test/CMakeLists.txt index 26abfe09d..1ea6b2362 100644 --- a/src/impl/bch/test/CMakeLists.txt +++ b/src/impl/bch/test/CMakeLists.txt @@ -20,6 +20,7 @@ if(BUILD_TESTING) abla_growth_soak_test # M5: dynamic ABLA budget-GROWTH proof above 32 MB floor (pure) coinbase_kat_segwit_predicate_test # KAT: DGB<->BCH coinbase pairing -- gated-segwit-OFF predicate pin (pure) asert_kat_test # M5: ASERT DAA known-answer vs BCHN @89a591f gold properties + mainnet anchor (pure) + muldiv_kat_test # ABLA: 128-bit muldiv native(__int128)==portable(boost uint128) equivalence -- MSVC-portability guard (pure) g1_oracle_byte_parity_test # G1: oracle byte-parity vs p2pool-merged-v36 bitcoincash[_testnet].py (pure) g1_share_serialization_parity_test # G1: share-serialization byte-parity vs jtoomim oracle wire (pure) g0_canonical_pin_test # G0: canonical-pin vs p2poolBCH @6603b79 baseline + v35->v36 transition rule (pure) diff --git a/src/impl/bch/test/muldiv_kat_test.cpp b/src/impl/bch/test/muldiv_kat_test.cpp new file mode 100644 index 000000000..7d52730a5 --- /dev/null +++ b/src/impl/bch/test/muldiv_kat_test.cpp @@ -0,0 +1,115 @@ +// --------------------------------------------------------------------------- +// bch::abla muldiv 128-bit KAT -- the guard for the MSVC-portability rework. +// +// abla.hpp evaluates x*y/z (BCHN control function) in a 128-bit intermediate. +// GCC/Clang use native unsigned __int128; MSVC (no __int128, error C4235) falls +// back to boost::multiprecision::uint128_t. Both must be BIT-IDENTICAL or the +// Windows package would silently diverge from the shipping Linux/macOS ABLA. +// +// This test pins that in two layers: +// A. cross-platform known-answers: muldiv() must produce exact fixed results +// on EVERY compiler (this is all MSVC can run -- no native to diff). +// B. Linux-only equivalence sweep (guarded by __SIZEOF_INT128__): the portable +// path must equal the native path across the full ABLA operand space plus a +// deterministic wide-operand fuzz. Proving portable == native on the trusted +// platform is what certifies the MSVC path we cannot run here. +// +// Build-INERT wrt consensus surface: pure header math, no node/RPC/boost-graph. +// p2pool-merged-v36 surface: NONE (ABLA is a LOCAL build-time byte budget only). +// --------------------------------------------------------------------------- + +#include +#include +#include + +#include "../coin/abla.hpp" + +namespace { + +int failures = 0; +#define CHECK(cond) do { if (!(cond)) { \ + std::cerr << "FAIL: " #cond " @ line " << __LINE__ << "\n"; ++failures; } } while (0) + +using bch::coin::abla::muldiv; +using bch::coin::abla::muldiv_portable; +using bch::coin::abla::B7; + +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 + +int main() { + // ---- A. cross-platform known answers (run on ALL compilers incl. MSVC) ---- + // hand-computed; several exercise a product > 2^64 so the true 128-bit + // intermediate is required (a 64-bit multiply would wrap and fail these). + CHECK(muldiv(192, 16000000ULL, 128) == 24000000ULL); // ABLA amplify shape + CHECK(muldiv(6, 7, 2) == 21ULL); + CHECK(muldiv(0, 12345, 7) == 0ULL); + CHECK(muldiv(U64MAX, 1, 1) == U64MAX); + CHECK(muldiv(U64MAX, U64MAX, U64MAX) == U64MAX); // product ~2^128 + CHECK(muldiv(1000000000000000000ULL, 1000000000ULL, + 1000000000000ULL) == 1000000000000000ULL); // 1e27 intermediate + CHECK(muldiv(B7, 32ULL * 1000000ULL, B7) == 32ULL * 1000000ULL);// identity via B7 + + // portable path must independently satisfy the same known answers + CHECK(muldiv_portable(192, 16000000ULL, 128) == 24000000ULL); + CHECK(muldiv_portable(U64MAX, U64MAX, U64MAX) == U64MAX); + CHECK(muldiv_portable(1000000000000000000ULL, 1000000000ULL, + 1000000000000ULL) == 1000000000000000ULL); + +#if defined(__SIZEOF_INT128__) + using bch::coin::abla::muldiv_native; + + // ---- B1. exact ABLA-domain vectors: native == portable ---- + // operand shapes taken straight from State::NextBlockState call sites. + const uint64_t sizes[] = { + 0, 1, B7, 192, 37938, 1000000ULL, 16ULL * 1000000ULL, + 32ULL * 1000000ULL, 2000ULL * 1000000ULL, (1ULL << 40), (1ULL << 62) + }; + for (uint64_t x : sizes) + for (uint64_t y : sizes) + for (uint64_t z : sizes) { + if (z == 0) continue; + // only compare where the true quotient fits uint64_t (the ABLA + // invariant); otherwise both paths take low-64 of an out-of-range + // value and the comparison is not meaningful. + const unsigned __int128 q = + (static_cast(x) * y) / z; + if (q > static_cast(U64MAX)) continue; + CHECK(muldiv_native(x, y, z) == muldiv_portable(x, y, z)); + } + + // ---- B2. wide deterministic fuzz: native == portable ---- + Lcg rng(0x9E3779B97F4A7C15ULL); + int compared = 0; + for (int i = 0; i < 200000; ++i) { + const uint64_t x = rng.next(); + const uint64_t y = rng.next(); + uint64_t z = rng.next(); + if (z == 0) z = 1; + const unsigned __int128 q = + (static_cast(x) * y) / z; + if (q > static_cast(U64MAX)) continue; // keep in-domain + ++compared; + if (muldiv_native(x, y, z) != muldiv_portable(x, y, z)) { + std::cerr << "FAIL fuzz: x=" << x << " y=" << y << " z=" << z << "\n"; + ++failures; + } + } + CHECK(compared > 1000); // sanity: the domain filter did not reject everything + std::cerr << "muldiv KAT: native==portable over " << compared << " fuzz vectors\n"; +#else + std::cerr << "muldiv KAT: no __int128 (MSVC) -- known-answer layer only\n"; +#endif + + if (failures) { std::cerr << failures << " CHECK(s) FAILED\n"; return 1; } + std::cerr << "muldiv_kat_test: ALL PASS\n"; + return 0; +}