diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08166f105..137c3093e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -283,6 +283,7 @@ jobs: bch_genesis_conformance_test bch_abla_growth_soak_test \ bch_coinbase_kat_segwit_predicate_test \ bch_coinbase_kat_bytevector_test \ + bch_cashtokens_transparency_test \ bch_block_connector_test bch_block_ordering_test \ bch_utxo_connect_test bch_reorg_connect_test bch_reorg_rerequest_test \ bch_compact_block_connector_test \ @@ -357,6 +358,7 @@ jobs: bch_genesis_conformance_test bch_abla_growth_soak_test \ bch_coinbase_kat_segwit_predicate_test \ bch_coinbase_kat_bytevector_test \ + bch_cashtokens_transparency_test \ bch_block_connector_test bch_block_ordering_test \ bch_utxo_connect_test bch_reorg_connect_test bch_reorg_rerequest_test \ bch_compact_block_connector_test \ diff --git a/src/impl/bch/test/CMakeLists.txt b/src/impl/bch/test/CMakeLists.txt index 782a09acf..385ff2f66 100644 --- a/src/impl/bch/test/CMakeLists.txt +++ b/src/impl/bch/test/CMakeLists.txt @@ -73,6 +73,22 @@ if(BUILD_TESTING) btclibs) add_test(NAME bch_coinbase_kat_bytevector_test COMMAND bch_coinbase_kat_bytevector_test) + # cashtokens_transparency_test: M5 conform pass -- pins the CHIP-2022-02 + # CashTokens "transparent carry" invariant (M1 s4). Builds a token-prefixed + # (0xef) TxOut, proves the SHA256d serializer treats the prefix as opaque + # locking bytecode: lossless serialize/deserialize round-trip, verbatim byte + # survival, and binding into the tx_id pre-image. Same MutableTransaction TU + # + link set as the byte-vector KAT. + add_executable(bch_cashtokens_transparency_test + cashtokens_transparency_test.cpp + ${CMAKE_SOURCE_DIR}/src/impl/bch/coin/transaction.cpp) + target_include_directories(bch_cashtokens_transparency_test PRIVATE ${CMAKE_SOURCE_DIR}/src) + target_link_libraries(bch_cashtokens_transparency_test PRIVATE + core pool sharechain + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage c2pool_difficulty + btclibs) + add_test(NAME bch_cashtokens_transparency_test COMMAND bch_cashtokens_transparency_test) + # utxo_connect_test: M5 (a) -- the now-live UTXO-connect leg. connect_block() # applies a best-chain block to a wired UTXO view, then revalidate_inputs() # (Phase 4) sweeps mempool txs whose inputs the connect left unspendable diff --git a/src/impl/bch/test/cashtokens_transparency_test.cpp b/src/impl/bch/test/cashtokens_transparency_test.cpp new file mode 100644 index 000000000..9ff2eda21 --- /dev/null +++ b/src/impl/bch/test/cashtokens_transparency_test.cpp @@ -0,0 +1,161 @@ +// --------------------------------------------------------------------------- +// bch::coin CashTokens transparency KAT (CHIP-2022-02, May 2023). +// +// Pins the M1 ยง4 invariant that the embedded daemon carries CashToken txs +// UNCHANGED: the token prefix (0xef PREFIX_TOKEN) lives INSIDE the +// CompactSize-length-prefixed TxOut scriptPubKey region, so the SHA256d tx +// serializer treats it as opaque locking bytecode -- NO token-aware parsing, +// NO field rewrite. This is the byte-level corollary of transaction.hpp:19-22 +// ("round-trips transparently through OPScript") and template_builder.hpp:11 +// ("they live in the tx bytes and round-trip unchanged"), which until now were +// asserted only by comment. +// +// Asserts: +// 1. A token-prefixed scriptPubKey serializes as , +// the length == the WHOLE blob (token prefix + locking bytecode), proving +// the 0xef prefix gets no special-casing. +// 2. The exact token-prefix bytes (0xef || category || bitfield || amount) +// appear VERBATIM in the serialized tx -- nothing stripped or reordered. +// 3. Full deserialize -> re-serialize is byte-identical (lossless round-trip), +// so a relayed CashToken tx re-emits into the block template unchanged. +// 4. Mutating one token-prefix byte changes the serialized bytes (the token +// data is bound into the tx_id pre-image -- relevant for CTOR ordering and +// the merkle-root accept gate; tokens are not invisible to consensus). +// +// The vector is a representative CHIP-2022-02 FUNGIBLE-only token output; this +// test pins SERIALIZATION TRANSPARENCY, not token validity (consensus token +// rules live in the vendored BCHN slice, not here). +// +// p2pool-merged-v36 surface: NONE (BCH-internal consensus; adds no wire field). +// per-coin isolation: src/impl/bch/ only. Over coin/transaction.hpp + +// core/pack.hpp -- no peer, socket, or live coin lib. +// --------------------------------------------------------------------------- + +#include +#include +#include +#include +#include + +#include +#include "../coin/transaction.hpp" + +using bch::coin::MutableTransaction; +using bch::coin::TxIn; +using bch::coin::TxOut; + +static std::vector from_hex(const std::string& h) +{ + std::vector out; + out.reserve(h.size() / 2); + for (std::size_t i = 0; i + 1 < h.size(); i += 2) + out.push_back(static_cast(std::stoul(h.substr(i, 2), nullptr, 16))); + return out; +} + +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; +} + +// CHIP-2022-02 fungible-token-prefixed locking bytecode, decomposed: +// ef PREFIX_TOKEN +// 1122..ff (32 bytes) category id (genesis txid, wire order) +// 40 bitfield: HAS_AMOUNT (0x40), no NFT +// fde803 amount = 1000 (CompactSize) +// 76a914<20-byte hash160>88ac the actual P2PKH locking bytecode +static const std::string TOKEN_PREFIX_HEX = + "ef" + "112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00" + "40" + "fde803"; +static const std::string LOCKING_HEX = + "76a91400112233445566778899aabbccddeeff0011223388ac"; + +int main() +{ + const std::string blob_hex = TOKEN_PREFIX_HEX + LOCKING_HEX; + const auto blob = from_hex(blob_hex); + const std::size_t blob_len = blob.size(); // 1+32+1+3+25 = 62 (0x3e) + + // Build a 1-in / 1-out tx whose sole output carries the token blob. + TxIn vin; + vin.prevout.hash = uint256{}; + vin.prevout.index = 0x00000000u; + { + auto ss = from_hex("ab"); // 1-byte dummy scriptSig + vin.scriptSig = OPScript(ss.data(), ss.data() + ss.size()); + } + vin.sequence = 0xffffffffu; + + TxOut vout; + vout.value = 0; // token-only output (0 sats) + vout.scriptPubKey = OPScript(blob.data(), blob.data() + blob.size()); + + MutableTransaction tx; + tx.version = 2; + tx.locktime = 0; + tx.vin.push_back(vin); + tx.vout.push_back(vout); + + PackStream ss; + ss << tx; + const std::string got = to_hex(ss.get_span()); + + // (1) the scriptPubKey serializes as , len == whole + // blob (token prefix is NOT split out from the locking bytecode). + assert(blob_len == 62 && "token blob length sanity"); + const std::string len_then_blob = "3e" + blob_hex; // 0x3e == 62 + assert(got.find(len_then_blob) != std::string::npos + && "token scriptPubKey must serialize as verbatim"); + + // (2) the token-prefix bytes appear verbatim (nothing stripped/reordered). + assert(got.find(TOKEN_PREFIX_HEX) != std::string::npos + && "0xef token prefix must survive serialization byte-for-byte"); + + // (3) deserialize -> re-serialize is byte-identical (lossless carry). + { + std::vector raw; + for (std::byte b : ss.get_span()) raw.push_back(static_cast(b)); + PackStream in2(raw); + MutableTransaction tx2; + in2 >> tx2; + PackStream re; + re << tx2; + const std::string round = to_hex(re.get_span()); + if (round != got) { + std::cerr << "CashToken tx round-trip MISMATCH\n" + << " in : " << got << "\n" + << " out: " << round << "\n"; + return 1; + } + } + + // (4) the token data is bound into the serialized pre-image: flip one prefix + // byte and the bytes must change (tokens are not invisible to tx_id). + { + auto mutated = blob; + mutated[0] ^= 0x01; // 0xef -> 0xee + TxOut vout2 = vout; + vout2.scriptPubKey = OPScript(mutated.data(), mutated.data() + mutated.size()); + MutableTransaction txm = tx; + txm.vout[0] = vout2; + PackStream sm; + sm << txm; + assert(to_hex(sm.get_span()) != got + && "token-prefix mutation must change the tx bytes (bound into tx_id)"); + } + + std::cout << "bch CashTokens transparency KAT: OK (" + << (got.size() / 2) << "-byte tx; 0xef token prefix carried opaque, " + << "lossless round-trip, bound into tx_id pre-image)\n"; + return 0; +}