diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 421bd1404..8749cf3d3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -366,7 +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_g1_oracle_byte_parity_test bch_g0_canonical_pin_test bch_g2_ratchet_gate_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 - name: Run BCH tests @@ -460,7 +460,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_g1_oracle_byte_parity_test bch_g0_canonical_pin_test bch_g2_ratchet_gate_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 - name: Run BCH tests under sanitizers diff --git a/src/impl/bch/config_pool.hpp b/src/impl/bch/config_pool.hpp index 83962b986..c0d5c4819 100644 --- a/src/impl/bch/config_pool.hpp +++ b/src/impl/bch/config_pool.hpp @@ -67,6 +67,17 @@ class PoolConfig : protected core::Fileconfig static constexpr uint32_t MINIMUM_PROTOCOL_VERSION = 3301; // Our capability: V36 shares (matches LTC/DOGE advertised version). static constexpr uint32_t ADVERTISED_PROTOCOL_VERSION = 3600; + // ----------------------------------------------------------------------- + // CROSSING-FLOOR SHARE VERSION (v36 HYBRID accept-both floor, SSOT). + // The exact share version the LIVE jtoomim-BCH sharechain currently mints. + // BCH is a CROSSING coin (not greenfield): shares author at the ratchet- + // current tip version, whose cold-start / empty-chain floor is THIS value. + // author, ref_hash, donation and the gentx-rebuild ALL feed from here so + // the miner-hashed template and the verify-side rebuild read ONE version + // -- never a scattered 35/36 literal (that inconsistency was the recompute/ + // GENTX byte-offset-104 root). The 60%-by-work auto-ratchet (#326/#577) + // advances the tip past this floor to 36; bumping this to 36 forks the net. + static constexpr int64_t CROSSING_FLOOR_VERSION = 35; // NOTE: NO SEGWIT_ACTIVATION_VERSION on BCH -- SegWit was rejected at the // Aug-2017 fork. (BTC carries one; intentionally absent here.) static constexpr uint32_t BLOCK_MAX_SIZE = 32000000; // 32 MB (BCH) diff --git a/src/impl/bch/pool_entrypoint.hpp b/src/impl/bch/pool_entrypoint.hpp index 82e15f1dd..4bf0888e7 100644 --- a/src/impl/bch/pool_entrypoint.hpp +++ b/src/impl/bch/pool_entrypoint.hpp @@ -164,10 +164,16 @@ inline void standup_pool_run(boost::asio::io_context& ioc, work_source->set_best_share_hash_fn( [&node]() -> uint256 { return node.best_share_hash(); }); - // Initial donation matches the cold-start create version (35 -> P2PK). A - // ratchet-driven refresh to the COMBINED P2SH on v36 activation is the same - // follow-up slice as pplns_fn/ref_hash_fn. - work_source->set_donation_script(PoolConfig::get_donation_script(35)); + // Work-source donation MUST match the authored share version, and both must + // match the LIVE jtoomim-BCH net -- BCH is a CROSSING coin. Shares author at + // CROSSING_FLOOR_VERSION (what the live net mints), and the donation served + // in the miner-hashed template is the floor script (sub-36 -> forrestv P2PK). + // generate_share_transaction rebuilds the coinbase donation under the SAME + // authored version, so template-donation and verify-rebuild are byte-identical + // -- closing the byte-offset-104 recompute/GENTX divergence WITHOUT forking + // off the live net (authoring v36 from genesis would). The 60%-by-work auto- + // ratchet (#326/#577) advances author + donation together once the tip>=36. + work_source->set_donation_script(PoolConfig::get_donation_script(PoolConfig::CROSSING_FLOOR_VERSION)); // -- ref_hash_fn: peer-verifiable share commitment (G2 conform) -------- // Without this the local-author coinbase carries NO p2pool OP_RETURN @@ -191,18 +197,18 @@ inline void standup_pool_run(boost::asio::io_context& ioc, -> core::stratum::RefHashResult { core::stratum::RefHashResult result; - result.share_version = 35; - result.desired_version = 36; + result.share_version = PoolConfig::CROSSING_FLOOR_VERSION; // crossing-floor SSOT (ratchet-current genesis default) + result.desired_version = core::version_gate::V36_ACTIVATION_VERSION; // vote to ratchet toward v36 bch::RefHashParams p; - p.share_version = 35; + p.share_version = PoolConfig::CROSSING_FLOOR_VERSION; // crossing-floor author version (SSOT) p.prev_share = prev_share_hash; p.coinbase_scriptSig = scriptSig; p.share_nonce = 0; p.subsidy = subsidy; p.donation = 50; // 0.5% finder fee (matches create side) p.stale_info = 0; - p.desired_version = 36; + p.desired_version = core::version_gate::V36_ACTIVATION_VERSION; // vote to ratchet toward v36 p.has_segwit = false; // BCH: never segwit p.timestamp = timestamp; @@ -505,7 +511,7 @@ inline void standup_pool_run(boost::asio::io_context& ioc, // reconstruction is byte-exact; fall back to the prev-tip version // (cold start: v35 voting v36) when ref_hash_fn produced no frozen // data (bits == 0). - int64_t create_ver = 35; + int64_t create_ver = PoolConfig::CROSSING_FLOOR_VERSION; // crossing-floor default; tip version overrides below (ratchet-current) if (job.frozen_ref.bits != 0) { create_ver = job.frozen_ref.share_version; } else if (!job.prev_share_hash.IsNull() && diff --git a/src/impl/bch/share_check.hpp b/src/impl/bch/share_check.hpp index f4e4403f7..b1bea105c 100644 --- a/src/impl/bch/share_check.hpp +++ b/src/impl/bch/share_check.hpp @@ -170,6 +170,19 @@ inline HashLinkType prefix_to_hash_link_v35( const std::vector& const_ending) { auto v36_link = prefix_to_hash_link(prefix, const_ending); + // FAIL-LOUD (v36 verify-preimage): a V35 HashLinkType (FixedStrType(0)) has + // NO extra_data field, so any coinbase whose trailing partial SHA-256 block + // exceeds the const_ending tail (bufsize > len(const_ending)) leaves residual + // bytes a V35 hash_link cannot carry. Silently dropping them makes + // check_hash_link recompute a DIFFERENT coinbase txid than the author + // committed, so the share fails verify against a v36 peer. This is exactly + // the case the oracle's V36 hash_link (variable-length extra_data) exists + // for: such a coinbase MUST be authored at V36. Refuse to forge an + // unrepresentable V35 hash_link rather than emit a mismatching preimage. + if (!v36_link.m_extra_data.m_data.empty()) + throw std::runtime_error( + "prefix_to_hash_link_v35: coinbase requires V36 hash_link " + "(extra_data non-empty; V35 FixedStrType(0) cannot represent it)"); HashLinkType result; result.m_state = v36_link.m_state; result.m_length = v36_link.m_length; diff --git a/src/impl/bch/test/CMakeLists.txt b/src/impl/bch/test/CMakeLists.txt index 05c81eeea..26abfe09d 100644 --- a/src/impl/bch/test/CMakeLists.txt +++ b/src/impl/bch/test/CMakeLists.txt @@ -21,6 +21,7 @@ if(BUILD_TESTING) 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) 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) g2_ratchet_gate_kat_test # G2: 60%-by-WORK version-switch accept gate + mint-cannot-outrun-accept (pure) coinbase_author_kat_test # KAT G2: v36 coinbase output-assembly byte-parity vs p2pool-merged-v36 oracle (pure) diff --git a/src/impl/bch/test/g1_share_serialization_parity_test.cpp b/src/impl/bch/test/g1_share_serialization_parity_test.cpp new file mode 100644 index 000000000..4b7d5aeed --- /dev/null +++ b/src/impl/bch/test/g1_share_serialization_parity_test.cpp @@ -0,0 +1,159 @@ +// --------------------------------------------------------------------------- +// bch G1 share-serialization byte-parity KAT (greenlight gate G1, share layer). +// +// FENCED conformance test (no production code touched). Complements +// g1_oracle_byte_parity_test (which pins net/consensus constants): this KAT +// pins the on-the-wire SHARE serialization byte layout of the c2pool-bch SUT +// against the wire format hand-transcribed from the BCH crossing oracle +// jtoomim / frstrtr/p2pool-merged-v36 p2pool/data.py: +// +// share_type = ComposedType([('type', VarIntType()), +// ('contents', VarStrType())]) (data.py:98) +// hash_link_type = ComposedType([('state', FixedStrType(32)), +// ('extra_data', FixedStrType(0)), +// ('length', VarIntType())]) (data.py:32) +// v36_hash_link_type= ComposedType([('state', FixedStrType(32)), +// ('extra_data', VarStrType()), +// ('length', VarIntType())]) (data.py:41) +// +// NON-CIRCULAR: the expected side below is literal wire bytes typed from the +// oracle python ComposedType field order + P2Pool's VarIntType (== Bitcoin +// CompactSize), VarStrType (CompactSize-prefixed bytes) and FixedStrType(N) +// (N raw bytes, no prefix) semantics -- NOT a second read of the same C++ SUT +// serializer. A field-order or encoding drift in share_types.hpp / RawShare +// that silently diverges from the oracle fails HERE even though every other bch +// share test (which sources the same SUT serializer on both sides) stays green. +// +// PINNED LIVE SHARE VERSION / accept-both crossing floor: +// CROSSING_FLOOR_VERSION == 35 is the version the live jtoomim BCH net mints +// today; it is the SSOT feeding author/ref_hash/donation and the accept-both +// floor until the 60%-by-work auto-ratchet (#326/#577) flips to v36. Pinned +// here so a silent bump of the crossing floor (which would fork the live +// sharechain) reddens this gate. +// +// 3-bucket posture (operator 2026-06-17): +// - the share wire FORMAT is bucket-2 v36-native shared structure -> pinned +// toward the canonical jtoomim layout (this KAT). +// - CROSSING_FLOOR_VERSION is a bucket-3 transition value (accept-both floor), +// dropped after the ratchet crossing-soak; pinned here for the crossing only. +// +// MUST appear in BOTH the ctest registration (this dir CMakeLists.txt) AND the +// build.yml COIN_BCH --target allowlist, or it becomes a #143-style NOT_BUILT +// sentinel that silently never builds. +// --------------------------------------------------------------------------- + +#include +#include +#include +#include +#include + +#include +#include +#include "../config_pool.hpp" +#include "../share_types.hpp" + +static int failures = 0; +#define CHECK_EQ(label, got, want) do { \ + if ((got) != (want)) { \ + std::cerr << "FAIL: " << (label) << "\n want: " << (want) \ + << "\n got : " << (got) << "\n"; \ + ++failures; \ + } \ +} while (0) + +static std::string to_hex(std::span sp) +{ + static const char* d = "0123456789abcdef"; + std::string s; + s.reserve(sp.size() * 2); + for (std::byte b : sp) { + auto v = static_cast(b); + s.push_back(d[(v >> 4) & 0xf]); + s.push_back(d[v & 0xf]); + } + return s; +} + +// 32-byte state: 0x00,0x01,...,0x1f (transcribed as the oracle FixedStrType(32) +// operand). Same value reused for the v35 and v36 hash_link vectors. +static const std::string STATE_HEX = + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; + +static std::string state_raw() +{ + std::string s; + for (int i = 0; i < 32; ++i) s.push_back(static_cast(i)); + return s; +} + +int main() +{ + using PC = bch::PoolConfig; + + // -- 0. pinned live share version (accept-both crossing floor) ------------ + // Oracle: live jtoomim BCH net mints v35 today; ratchet-current SSOT. + CHECK_EQ("CROSSING_FLOOR_VERSION == 35 (live jtoomim mint / accept floor)", + (int)PC::CROSSING_FLOOR_VERSION, 35); + + // -- 1. hash_link_type (v35): state(32) + FixedStr(0) empty + VarInt(len) -- + // Oracle wire = <32-byte state> || . extra_data FixedStr(0) + // contributes ZERO bytes. length = 300 -> CompactSize "fd2c01". + { + bch::HashLinkType hl; + hl.m_state = FixedStrType<32>(state_raw()); + hl.m_length = 300; + + PackStream ss; + ss << hl; + const std::string want = STATE_HEX + "fd2c01"; + CHECK_EQ("v35 hash_link_type wire (state||VarInt len, no extra_data)", + to_hex(ss.get_span()), want); + } + + // -- 2. v36_hash_link_type: state(32) + VarStr(extra_data) + VarInt(len) --- + // Oracle wire = <32-byte state> || || . + // extra_data = aa bb cc -> VarStr "03aabbcc"; length = 300 -> "fd2c01". + { + bch::V36HashLinkType hl; + hl.m_state = FixedStrType<32>(state_raw()); + hl.m_extra_data = BaseScript(std::vector{0xaa, 0xbb, 0xcc}); + hl.m_length = 300; + + PackStream ss; + ss << hl; + const std::string want = STATE_HEX + "03aabbcc" + "fd2c01"; + CHECK_EQ("v36_hash_link_type wire (state||VarStr extra_data||VarInt len)", + to_hex(ss.get_span()), want); + } + + // -- 3. share_type outer wrapper: VarInt(type) + VarStr(contents) ---------- + // Oracle share_type = ('type', VarIntType()), ('contents', VarStrType()). + // type=35 -> "23"; contents = de ad be ef -> VarStr "04deadbeef". + { + chain::RawShare rs(35, BaseScript(std::vector{0xde, 0xad, 0xbe, 0xef})); + PackStream ss; + ss << rs; + const std::string want = std::string("23") + "04deadbeef"; + CHECK_EQ("share_type wrapper wire v35 (VarInt type || VarStr contents)", + to_hex(ss.get_span()), want); + } + { + // post-ratchet v36 type tag still single-byte CompactSize -> "24". + chain::RawShare rs(36, BaseScript(std::vector{0xde, 0xad, 0xbe, 0xef})); + PackStream ss; + ss << rs; + const std::string want = std::string("24") + "04deadbeef"; + CHECK_EQ("share_type wrapper wire v36 (VarInt type || VarStr contents)", + to_hex(ss.get_span()), want); + } + + if (failures) { + std::cerr << "\nbch G1 share-serialization byte-parity KAT: " + << failures << " MISMATCH(es) vs jtoomim oracle wire format\n"; + return 1; + } + std::cout << "bch G1 share-serialization byte-parity KAT: PASS " + "(share_type + hash_link v35/v36 + crossing-floor v35 pinned)\n"; + return 0; +}