/* SPDX-License-Identifier: Apache-2.0
 * Copyright 2026 Rhett Creighton
 *
 * Unit tests for domain/consensus/tx_structural.{c,h}.
 *
 * These tests pin the PURE structural transaction-validation rules
 * extracted from lib/validation/src/check_transaction.c, and seal
 * the extraction with a side-by-side comparison against the legacy
 * `check_transaction()` wrapper for representative tx shapes:
 *   - empty (no vin / no vout)
 *   - single-in single-out non-coinbase
 *   - coinbase (with and without ill-formed scriptSig)
 *   - duplicate transparent inputs
 *   - oversized output values
 *
 * For every shape we assert:
 *   1. domain function returns the same {ok, reject_reason, dos} pair
 *      as the legacy wrapper populates on validation_state.
 *   2. reject_reason strings are byte-identical (literal pointer
 *      equality is not required, strcmp == 0 is).
 *
 * This is the regression seal: if anyone "improves" one side without
 * the other, the test shouts.
 */

#include "test/test_helpers.h"

#include "domain/consensus/tx_structural.h"
#include "validation/check_transaction.h"
#include "validation/accept_to_mempool.h"
#include "validation/txmempool.h"
#include "core/serialize.h"

/* The generated empirical oversize-grandfather table, included DIRECTLY so
 * this TU can assert invariants of the artifact itself (count, ordering,
 * size bounds) — independent of the predicate that consumes it. */
#include "../../../domain/consensus/src/oversize_grandfather_table.inc"

/* Real canonical mainnet tx e3eeb123... from block 478544 (125811 bytes —
 * the first canonical tx above the 102000 cap). Generated by
 * tools/scripts/gen_oversize_grandfather_table.sh --fixture. */
extern const unsigned char g_fixture_tx_oversize_478544[];
extern const size_t g_fixture_tx_oversize_478544_len;
/* consensus/consensus.h not included: its MAX_BLOCK_SIZE/MAX_BLOCK_SIGOPS
 * collide with validation/main_constants.h (auto-included via
 * test_helpers.h). The constants we need for these tests are pinned
 * locally below — they mirror the canonical definitions in
 * lib/consensus/include/consensus/consensus.h and are static across
 * the protocol. If they drift, the regression seal (verdicts_match)
 * still catches any divergence because the legacy wrapper IS compiled
 * against the canonical header. */
#include "consensus/validation.h"
#include "primitives/transaction.h"
#include "core/amount.h"
#include "core/uint256.h"

#include <stdio.h>
#include <string.h>

/* Mirror consensus/consensus.h constants — used only to build test
 * fixtures that exercise the rejection paths. */
#ifndef SPROUT_MIN_TX_VERSION
#define SPROUT_MIN_TX_VERSION      1
#endif
#ifndef OVERWINTER_MIN_TX_VERSION
#define OVERWINTER_MIN_TX_VERSION  3
#endif
#ifndef TX_EXPIRY_HEIGHT_THRESHOLD
#define TX_EXPIRY_HEIGHT_THRESHOLD 500000000U
#endif
/* ABSOLUTE pin to zclassicd's post-Sapling tx-size cap
 * (zclassic-cpp/src/consensus/consensus.h:27 = 102000). Intentionally a
 * literal, NOT a reference to the canonical macro: the boundary KAT below
 * builds transactions sized to exactly this value, so it pins the
 * canonical constant. If consensus/consensus.h ever drifts off 102000 the
 * accept/reject verdicts here diverge and the test shouts. */
#ifndef MAX_TX_SIZE_AFTER_SAPLING
#define MAX_TX_SIZE_AFTER_SAPLING  102000U
#endif

#define DCS_CHECK(name, expr) do { \
    printf("domain_consensus_tx_structural: %s... ", (name)); \
    if ((expr)) printf("OK\n"); \
    else { printf("FAIL\n"); failures++; } \
} while (0)

/* Construct a minimal non-coinbase tx with `n_in` inputs and `n_out`
 * outputs, each output value == `each_value`. Inputs each point at a
 * distinct (non-null) prevout so dedup checks pass. */
static void build_simple_tx(struct transaction *tx, size_t n_in, size_t n_out,
                            int64_t each_value)
{
    transaction_init(tx);
    transaction_alloc(tx, n_in, n_out);
    for (size_t i = 0; i < n_in; i++) {
        uint256_set_null(&tx->vin[i].prevout.hash);
        tx->vin[i].prevout.hash.data[0] = (unsigned char)(i + 1);
        tx->vin[i].prevout.n = (uint32_t)i;
        tx->vin[i].sequence = 0xFFFFFFFEu;
        tx->vin[i].script_sig.size = 0;
    }
    for (size_t i = 0; i < n_out; i++) {
        tx->vout[i].value = each_value;
        tx->vout[i].script_pub_key.size = 0;
    }
    tx->lock_time = 0;
}

/* Construct a minimal coinbase tx (1 vin with null prevout, scriptSig
 * size in [2,100], 1 vout). */
static void build_coinbase(struct transaction *tx, size_t scriptsig_size)
{
    transaction_init(tx);
    transaction_alloc(tx, 1, 1);
    outpoint_set_null(&tx->vin[0].prevout);
    tx->vin[0].script_sig.size = scriptsig_size;
    if (scriptsig_size > 0) {
        for (size_t i = 0; i < scriptsig_size && i < sizeof(tx->vin[0].script_sig.data); i++)
            tx->vin[0].script_sig.data[i] = 0;
    }
    tx->vin[0].sequence = 0xFFFFFFFFu;
    tx->vout[0].value = 12 * COIN;
    tx->vout[0].script_pub_key.size = 0;
}

/* Run the legacy wrapper against `tx` and return {ok, reason copy, dos}. */
struct legacy_verdict {
    bool ok;
    char reason[MAX_REJECT_REASON];
    int dos;
};

static struct legacy_verdict run_legacy(const struct transaction *tx)
{
    struct validation_state st;
    validation_state_init(&st);
    bool ok = check_transaction(tx, &st);
    struct legacy_verdict v;
    v.ok = ok;
    v.dos = st.dos;
    strncpy(v.reason, st.reject_reason, sizeof(v.reason) - 1);
    v.reason[sizeof(v.reason) - 1] = '\0';
    return v;
}

/* Run the domain function against `tx` and return {ok, reason, dos}. */
struct domain_verdict {
    bool ok;
    const char *reason;  /* may be NULL on ok */
    int dos;
    int code;
};

static struct domain_verdict run_domain_ctx(const struct transaction *tx,
                                            enum domain_tx_check_context ctx)
{
    struct domain_consensus_tx_structural_failure f = {0};
    struct zcl_result r =
        domain_consensus_check_transaction_structural(tx, ctx, &f);
    struct domain_verdict v;
    v.ok = r.ok;
    v.reason = f.reject_reason;
    v.dos = f.dos;
    v.code = r.code;
    return v;
}

static struct domain_verdict run_domain(const struct transaction *tx)
{
    return run_domain_ctx(tx, DOMAIN_TX_CTX_STANDALONE);
}

static struct domain_verdict run_domain_block(const struct transaction *tx)
{
    return run_domain_ctx(tx, DOMAIN_TX_CTX_BLOCK);
}

/* Compare domain output with legacy. Both must agree on:
 *  - ok bit
 *  - reject_reason (byte-identical)
 *  - dos score
 */
static bool verdicts_match(const struct domain_verdict *d,
                           const struct legacy_verdict *l)
{
    if (d->ok != l->ok) return false;
    if (d->ok) return true;
    const char *dr = d->reason ? d->reason : "";
    if (strcmp(dr, l->reason) != 0) return false;
    if (d->dos != l->dos) return false;
    return true;
}

int test_domain_consensus_tx_structural(void)
{
    int failures = 0;

    /* --- contract: NULL tx -> typed error --- */
    {
        struct domain_consensus_tx_structural_failure f = {0};
        struct zcl_result r =
            domain_consensus_check_transaction_structural(
                    NULL, DOMAIN_TX_CTX_STANDALONE, &f);
        DCS_CHECK("null tx -> ERR_NULL_TX",
                  !r.ok && r.code == DOMAIN_CONSENSUS_TX_STRUCTURAL_ERR_NULL_TX);
    }

    /* --- contract: NULL out_failure is allowed --- */
    {
        struct transaction tx;
        build_simple_tx(&tx, 1, 1, 1 * COIN);
        struct zcl_result r =
            domain_consensus_check_transaction_structural(
                    &tx, DOMAIN_TX_CTX_STANDALONE, NULL);
        DCS_CHECK("null out_failure tolerated on success",
                  r.ok && r.code == 0);
        transaction_free(&tx);
    }

    /* --- shape: simple 1-in 1-out tx is structurally valid --- */
    {
        struct transaction tx;
        build_simple_tx(&tx, 1, 1, 1 * COIN);
        struct domain_verdict d = run_domain(&tx);
        struct legacy_verdict l = run_legacy(&tx);
        DCS_CHECK("simple 1in1out: domain accepts", d.ok);
        DCS_CHECK("simple 1in1out: legacy accepts", l.ok);
        DCS_CHECK("simple 1in1out: verdicts agree",
                  verdicts_match(&d, &l));
        transaction_free(&tx);
    }

    /* --- shape: empty tx (no vin, no vout, no joinsplit/shielded)
     *      legacy first rejects vin-empty (vin == 0 and no shielded). */
    {
        struct transaction tx;
        transaction_init(&tx);
        transaction_alloc(&tx, 0, 0);
        struct domain_verdict d = run_domain(&tx);
        struct legacy_verdict l = run_legacy(&tx);
        DCS_CHECK("empty tx: domain rejects with vin-empty",
                  !d.ok && d.reason && strcmp(d.reason, "bad-txns-vin-empty") == 0);
        DCS_CHECK("empty tx: dos=10",
                  d.dos == 10);
        DCS_CHECK("empty tx: verdicts agree byte-identically",
                  verdicts_match(&d, &l));
        transaction_free(&tx);
    }

    /* --- shape: vin present but no vout (and no joinsplit/shielded) --- */
    {
        struct transaction tx;
        build_simple_tx(&tx, 1, 0, 0);
        struct domain_verdict d = run_domain(&tx);
        struct legacy_verdict l = run_legacy(&tx);
        DCS_CHECK("vin only: domain rejects vout-empty",
                  !d.ok && d.reason && strcmp(d.reason, "bad-txns-vout-empty") == 0);
        DCS_CHECK("vin only: verdicts agree", verdicts_match(&d, &l));
        transaction_free(&tx);
    }

    /* --- shape: negative output value --- */
    {
        struct transaction tx;
        build_simple_tx(&tx, 1, 1, 1 * COIN);
        tx.vout[0].value = -1;
        struct domain_verdict d = run_domain(&tx);
        struct legacy_verdict l = run_legacy(&tx);
        DCS_CHECK("negative vout: domain rejects vout-negative",
                  !d.ok && d.reason &&
                  strcmp(d.reason, "bad-txns-vout-negative") == 0);
        DCS_CHECK("negative vout: dos=100", d.dos == 100);
        DCS_CHECK("negative vout: verdicts agree",
                  verdicts_match(&d, &l));
        transaction_free(&tx);
    }

    /* --- shape: oversized vout value --- */
    {
        struct transaction tx;
        build_simple_tx(&tx, 1, 1, 1 * COIN);
        tx.vout[0].value = MAX_MONEY + 1;
        struct domain_verdict d = run_domain(&tx);
        struct legacy_verdict l = run_legacy(&tx);
        DCS_CHECK("vout > MAX_MONEY: domain rejects vout-toolarge",
                  !d.ok && d.reason &&
                  strcmp(d.reason, "bad-txns-vout-toolarge") == 0);
        DCS_CHECK("vout > MAX_MONEY: verdicts agree",
                  verdicts_match(&d, &l));
        transaction_free(&tx);
    }

    /* --- shape: vout running total exceeds MAX_MONEY --- */
    {
        struct transaction tx;
        build_simple_tx(&tx, 1, 2, MAX_MONEY);
        /* each output is exactly MAX_MONEY -> running total overflows
         * after second iteration */
        struct domain_verdict d = run_domain(&tx);
        struct legacy_verdict l = run_legacy(&tx);
        DCS_CHECK("vout total > MAX_MONEY: domain rejects txouttotal-toolarge",
                  !d.ok && d.reason &&
                  strcmp(d.reason, "bad-txns-txouttotal-toolarge") == 0);
        DCS_CHECK("vout total > MAX_MONEY: verdicts agree",
                  verdicts_match(&d, &l));
        transaction_free(&tx);
    }

    /* --- shape: duplicate transparent inputs --- */
    {
        struct transaction tx;
        build_simple_tx(&tx, 2, 1, 1 * COIN);
        /* Force inputs to share the same prevout. */
        tx.vin[1].prevout = tx.vin[0].prevout;
        struct domain_verdict d = run_domain(&tx);
        struct legacy_verdict l = run_legacy(&tx);
        DCS_CHECK("dup inputs: domain rejects inputs-duplicate",
                  !d.ok && d.reason &&
                  strcmp(d.reason, "bad-txns-inputs-duplicate") == 0);
        DCS_CHECK("dup inputs: verdicts agree",
                  verdicts_match(&d, &l));
        transaction_free(&tx);
    }

    /* --- shape: coinbase with valid scriptSig length (2..100) --- */
    {
        struct transaction tx;
        build_coinbase(&tx, 5);
        struct domain_verdict d = run_domain(&tx);
        struct legacy_verdict l = run_legacy(&tx);
        DCS_CHECK("coinbase scriptSig=5: accepted", d.ok && l.ok);
        DCS_CHECK("coinbase scriptSig=5: verdicts agree",
                  verdicts_match(&d, &l));
        transaction_free(&tx);
    }

    /* --- shape: coinbase with too-short scriptSig --- */
    {
        struct transaction tx;
        build_coinbase(&tx, 1);  /* below 2 -> bad-cb-length */
        struct domain_verdict d = run_domain(&tx);
        struct legacy_verdict l = run_legacy(&tx);
        DCS_CHECK("coinbase scriptSig=1: domain rejects bad-cb-length",
                  !d.ok && d.reason && strcmp(d.reason, "bad-cb-length") == 0);
        DCS_CHECK("coinbase scriptSig=1: verdicts agree",
                  verdicts_match(&d, &l));
        transaction_free(&tx);
    }

    /* --- shape: coinbase with too-long scriptSig --- */
    {
        struct transaction tx;
        build_coinbase(&tx, 101);  /* above 100 -> bad-cb-length */
        struct domain_verdict d = run_domain(&tx);
        struct legacy_verdict l = run_legacy(&tx);
        DCS_CHECK("coinbase scriptSig=101: domain rejects bad-cb-length",
                  !d.ok && d.reason && strcmp(d.reason, "bad-cb-length") == 0);
        DCS_CHECK("coinbase scriptSig=101: verdicts agree",
                  verdicts_match(&d, &l));
        transaction_free(&tx);
    }

    /* --- shape: non-coinbase with null prevout (legacy: bad-txns-prevout-null) --- */
    {
        struct transaction tx;
        build_simple_tx(&tx, 2, 1, 1 * COIN);
        /* Null the second prevout — but inputs must NOT match each
         * other, so we need vin[0] not null and vin[1] null. The
         * non-null/non-coinbase predicate (transaction_is_coinbase
         * requires num_vin==1) means the loop runs. */
        outpoint_set_null(&tx.vin[1].prevout);
        struct domain_verdict d = run_domain(&tx);
        struct legacy_verdict l = run_legacy(&tx);
        DCS_CHECK("non-cb null prevout: domain rejects prevout-null",
                  !d.ok && d.reason &&
                  strcmp(d.reason, "bad-txns-prevout-null") == 0);
        DCS_CHECK("non-cb null prevout: dos=10", d.dos == 10);
        DCS_CHECK("non-cb null prevout: verdicts agree",
                  verdicts_match(&d, &l));
        transaction_free(&tx);
    }

    /* --- shape: version too low (non-overwintered v=0) --- */
    {
        struct transaction tx;
        build_simple_tx(&tx, 1, 1, 1 * COIN);
        tx.overwintered = false;
        tx.version = 0;  /* < SPROUT_MIN_TX_VERSION (1) */
        struct domain_verdict d = run_domain(&tx);
        struct legacy_verdict l = run_legacy(&tx);
        DCS_CHECK("v=0 non-overwintered: domain rejects version-too-low",
                  !d.ok && d.reason &&
                  strcmp(d.reason, "bad-txns-version-too-low") == 0);
        DCS_CHECK("v=0 non-overwintered: verdicts agree",
                  verdicts_match(&d, &l));
        transaction_free(&tx);
    }

    /* --- shape: overwintered with bad version-group-id --- */
    {
        struct transaction tx;
        build_simple_tx(&tx, 1, 1, 1 * COIN);
        tx.overwintered = true;
        tx.version = OVERWINTER_MIN_TX_VERSION;
        tx.version_group_id = 0xDEADBEEFu;  /* not OVERWINTER nor SAPLING */
        tx.expiry_height = 0;
        struct domain_verdict d = run_domain(&tx);
        struct legacy_verdict l = run_legacy(&tx);
        DCS_CHECK("bad version_group_id: domain rejects version-group-id",
                  !d.ok && d.reason &&
                  strcmp(d.reason, "bad-tx-version-group-id") == 0);
        DCS_CHECK("bad version_group_id: verdicts agree",
                  verdicts_match(&d, &l));
        transaction_free(&tx);
    }

    /* --- shape: overwintered with too-high expiry_height --- */
    {
        struct transaction tx;
        build_simple_tx(&tx, 1, 1, 1 * COIN);
        tx.overwintered = true;
        tx.version = OVERWINTER_MIN_TX_VERSION;
        tx.version_group_id = OVERWINTER_VERSION_GROUP_ID;
        tx.expiry_height = TX_EXPIRY_HEIGHT_THRESHOLD;  /* == threshold */
        struct domain_verdict d = run_domain(&tx);
        struct legacy_verdict l = run_legacy(&tx);
        DCS_CHECK("expiry_height == threshold: domain rejects expiry-too-high",
                  !d.ok && d.reason &&
                  strcmp(d.reason, "bad-tx-expiry-height-too-high") == 0);
        DCS_CHECK("expiry_height == threshold: verdicts agree",
                  verdicts_match(&d, &l));
        transaction_free(&tx);
    }

    /* --- shape: valueBalance nonzero but no shielded io --- */
    {
        struct transaction tx;
        build_simple_tx(&tx, 1, 1, 1 * COIN);
        tx.value_balance = 1000;
        struct domain_verdict d = run_domain(&tx);
        struct legacy_verdict l = run_legacy(&tx);
        DCS_CHECK("valueBalance nonzero, no shielded: domain rejects valuebalance-nonzero",
                  !d.ok && d.reason &&
                  strcmp(d.reason, "bad-txns-valuebalance-nonzero") == 0);
        DCS_CHECK("valueBalance nonzero, no shielded: verdicts agree",
                  verdicts_match(&d, &l));
        transaction_free(&tx);
    }

    /* --- shape: tx serialize-size boundary (MAX_TX_SIZE_AFTER_SAPLING) ---
     *
     * zclassicd rejects GetSerializeSize(tx) > MAX_TX_SIZE_AFTER_SAPLING
     * (102000) as a CONSENSUS error ("bad-txns-oversize", DoS 100) on the
     * block-connect path: ContextualCheckBlock -> ContextualCheckTransaction
     * (zclassic-cpp/src/main.cpp:4079 then 1198). The comparison is a strict
     * '>', so a tx serializing to EXACTLY 102000 is accepted and 102001 is
     * rejected — the reference pins this same two-sided boundary in its own
     * gtest (src/gtest/test_checktransaction.cpp:234/245). c23 previously
     * capped here at 2e6 (~20x looser); this seals the tightened value.
     *
     * We size the tx with the REAL serializer as ground truth, so the KAT
     * is agnostic to base/per-output wire overhead. */
    {
        struct transaction tx;
        build_simple_tx(&tx, 1, 11, 1 * COIN);
        /* First 10 outputs carry maximum-size scripts (~10 KB each); the
         * 11th is the tunable remainder used to hit the cap exactly. */
        for (size_t i = 0; i < 10; i++) {
            memset(tx.vout[i].script_pub_key.data, 0x51, MAX_SCRIPT_SIZE);
            tx.vout[i].script_pub_key.size = MAX_SCRIPT_SIZE;
        }
        memset(tx.vout[10].script_pub_key.data, 0x51, MAX_SCRIPT_SIZE);
        tx.vout[10].script_pub_key.size = 0;

        /* Grow the final script until the whole tx serializes to EXACTLY
         * the cap. Seed near the target (a >=253-byte script adds its
         * length + a 2-byte compact-size prefix), then converge against
         * the serializer to absorb any off-by-one. */
        size_t base = transaction_serialize_size(&tx);   /* < cap */
        size_t need = (size_t)MAX_TX_SIZE_AFTER_SAPLING - base;
        tx.vout[10].script_pub_key.size = (need > 2) ? need - 2 : 0;
        while (transaction_serialize_size(&tx) < MAX_TX_SIZE_AFTER_SAPLING)
            tx.vout[10].script_pub_key.size++;
        while (transaction_serialize_size(&tx) > MAX_TX_SIZE_AFTER_SAPLING)
            tx.vout[10].script_pub_key.size--;

        DCS_CHECK("tx-size: serializer lands exactly on the 102000 cap",
                  transaction_serialize_size(&tx) == MAX_TX_SIZE_AFTER_SAPLING);
        {
            struct domain_verdict d = run_domain(&tx);
            struct legacy_verdict l = run_legacy(&tx);
            DCS_CHECK("tx-size == 102000: domain accepts (strict GT, matches zclassicd)",
                      d.ok);
            DCS_CHECK("tx-size == 102000: verdicts agree", verdicts_match(&d, &l));
        }

        /* One byte over the cap -> consensus reject. */
        tx.vout[10].script_pub_key.size++;
        DCS_CHECK("tx-size: serializer one byte past the cap",
                  transaction_serialize_size(&tx) ==
                      (size_t)MAX_TX_SIZE_AFTER_SAPLING + 1);
        {
            struct domain_verdict d = run_domain(&tx);
            struct legacy_verdict l = run_legacy(&tx);
            DCS_CHECK("tx-size == 102001: domain rejects bad-txns-oversize",
                      !d.ok && d.reason &&
                      strcmp(d.reason, "bad-txns-oversize") == 0);
            DCS_CHECK("tx-size == 102001: verdicts agree", verdicts_match(&d, &l));
        }
        /* BLOCK context is NOT a blanket loosening: a synthetic 102001 tx
         * (not one of the 413 canonical grandfathered txids) is rejected
         * in BOTH contexts. */
        {
            struct domain_verdict db = run_domain_block(&tx);
            DCS_CHECK("tx-size == 102001: BLOCK context also rejects (not grandfathered)",
                      !db.ok && db.reason &&
                      strcmp(db.reason, "bad-txns-oversize") == 0);
        }
        transaction_free(&tx);
    }

    /* === Empirical oversize grandfather (zclassicd live-behavior parity) ===
     *
     * The canonical chain carries 413 post-Sapling txs above the 102000 cap
     * (heights 478544..1968856) — legal at mine time; zclassicd later
     * tightened the constant without grandfathering, so a from-genesis
     * replay of its own text FATALs at block 478544 (proven live
     * 2026-06-11). zclassic23 matches the LIVE network behavior: exactly
     * those txs are excused, in-block only. */

    /* --- table invariants: the generated artifact itself --- */
    {
        DCS_CHECK("grandfather table: count == 413",
                  OVERSIZE_GRANDFATHER_COUNT == 413u);
        bool sorted = true, sizes_ok = true;
        for (size_t i = 0; i < OVERSIZE_GRANDFATHER_COUNT; i++) {
            uint32_t sz = g_oversize_grandfather[i].size;
            if (sz <= 102000u || sz >= 2000000u || sz > 1922197u)
                sizes_ok = false;
            if (i > 0 &&
                memcmp(g_oversize_grandfather[i - 1].txid,
                       g_oversize_grandfather[i].txid, 32) >= 0)
                sorted = false;
        }
        DCS_CHECK("grandfather table: strictly ascending by memcmp (bsearch-able, no dups)",
                  sorted);
        DCS_CHECK("grandfather table: every size in (102000, 2000000) and <= max 1922197",
                  sizes_ok);
    }

    /* --- predicate unit: exact {txid, size} match semantics --- */
    {
        struct uint256 first, last;
        uint256_set_hex(&first,
            "e3eeb123a79945cc74e6107422b124dc130ddd4b61fe5c74087317c256c79700");
        uint256_set_hex(&last,
            "e28d84f48ac642252519ad7d1ca6009cc2eb0ad2c5aaf5d0761c9105518a1db9");
        DCS_CHECK("predicate: (e3eeb123, 125811) -> true (first violation, h=478544)",
                  domain_consensus_tx_oversize_grandfathered(&first, 125811));
        DCS_CHECK("predicate: (e3eeb123, 125810) -> false (size must match exactly)",
                  !domain_consensus_tx_oversize_grandfathered(&first, 125810));
        DCS_CHECK("predicate: (e28d84f4, 108629) -> true (last violation, h=1968856)",
                  domain_consensus_tx_oversize_grandfathered(&last, 108629));
        struct uint256 flipped = first;
        flipped.data[0] ^= 0x01;
        DCS_CHECK("predicate: one-bit-different txid -> false",
                  !domain_consensus_tx_oversize_grandfathered(&flipped, 125811));
        DCS_CHECK("predicate: NULL txid -> false",
                  !domain_consensus_tx_oversize_grandfathered(NULL, 125811));
    }

    /* --- golden KAT: the REAL canonical tx from block 478544 --- */
    {
        struct byte_stream s;
        stream_init_from_data(&s, g_fixture_tx_oversize_478544,
                              g_fixture_tx_oversize_478544_len);
        struct transaction tx;
        bool des = transaction_deserialize(&tx, &s);
        DCS_CHECK("478544 KAT: canonical fixture deserializes", des);
        if (des) {
            DCS_CHECK("478544 KAT: serialize_size == 125811",
                      transaction_serialize_size(&tx) == 125811u);
            char hex[65];
            uint256_get_hex(&tx.hash, hex);
            DCS_CHECK("478544 KAT: computed txid == e3eeb123... (pins fixture identity)",
                      strcmp(hex,
                             "e3eeb123a79945cc74e6107422b124dc130ddd4b6"
                             "1fe5c74087317c256c79700") == 0);

            /* In-block: accepted (the false-reject that FATALed the live
             * reindex at block 478544 is fixed). */
            {
                struct domain_verdict d = run_domain_block(&tx);
                DCS_CHECK("478544 KAT: BLOCK context accepts", d.ok);
            }
            /* Standalone (mempool/relay): still strictly rejected, like
             * zclassicd would reject it as a NEW tx today. */
            {
                struct domain_verdict d = run_domain(&tx);
                DCS_CHECK("478544 KAT: STANDALONE context rejects bad-txns-oversize dos=100",
                          !d.ok && d.dos == 100 && d.reason &&
                          strcmp(d.reason, "bad-txns-oversize") == 0);
            }
            /* Wrapper parity: lib/validation mirrors both contexts. */
            {
                struct validation_state st;
                validation_state_init(&st);
                DCS_CHECK("478544 KAT: check_transaction (standalone wrapper) rejects",
                          !check_transaction(&tx, &st));
                struct validation_state st2;
                validation_state_init(&st2);
                DCS_CHECK("478544 KAT: check_transaction_in_block accepts",
                          check_transaction_in_block(&tx, &st2));
            }
            /* Mempool gate: the structural check fires FIRST, so the
             * grandfathered tx still cannot enter the mempool as new. */
            {
                struct tx_mempool pool;
                tx_mempool_init(&pool, 0);
                enum mempool_accept_result r =
                    accept_to_mempool(&pool, NULL, NULL, NULL, &tx);
                DCS_CHECK("478544 KAT: accept_to_mempool -> INVALID (strict gate first)",
                          r == MEMPOOL_ACCEPT_INVALID &&
                          tx_mempool_size(&pool) == 0);
                tx_mempool_free(&pool);
            }
            /* Tamper KAT: flip one serialized field WITHOUT recomputing
             * tx->hash — the domain must derive the txid from the actual
             * bytes (never trust a stale tx->hash), so the tampered tx
             * no longer matches the allowlist and is rejected in-block. */
            {
                tx.lock_time ^= 1u;
                struct domain_verdict d = run_domain_block(&tx);
                DCS_CHECK("478544 KAT: tampered bytes (stale tx->hash) -> BLOCK context rejects",
                          !d.ok && d.reason &&
                          strcmp(d.reason, "bad-txns-oversize") == 0);
                tx.lock_time ^= 1u;
            }
        }
        transaction_free(&tx);
    }

    return failures;
}
