Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
36 changes: 33 additions & 3 deletions src/impl/bch/coin/abla.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
#include <cstdint>
#include <limits>

// Portable 128-bit intermediate for muldiv on compilers without __int128 (MSVC).
// Header-only; boost is already a c2pool dependency (conan + system libboost).
#include <boost/multiprecision/cpp_int.hpp>

namespace bch {
namespace coin {
namespace abla {
Expand All @@ -39,16 +43,42 @@ 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<uint64_t>::max()));
return static_cast<uint64_t>(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<unsigned __int128>(x) * static_cast<unsigned __int128>(y))
/ static_cast<unsigned __int128>(z);
assert(res <= static_cast<unsigned __int128>(std::numeric_limits<uint64_t>::max()));
return static_cast<uint64_t>(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 {
Expand Down
1 change: 1 addition & 0 deletions src/impl/bch/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
115 changes: 115 additions & 0 deletions src/impl/bch/test/muldiv_kat_test.cpp
Original file line number Diff line number Diff line change
@@ -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 <cstdint>
#include <iostream>
#include <limits>

#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<uint64_t>::max();

// Deterministic 64-bit LCG (no <random>, 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<unsigned __int128>(x) * y) / z;
if (q > static_cast<unsigned __int128>(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<unsigned __int128>(x) * y) / z;
if (q > static_cast<unsigned __int128>(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;
}
Loading