#!/usr/bin/env bash
# gen_oversize_grandfather_table.sh — regenerate the empirical consensus
# oversize-grandfather table from the committed provenance list, verifying
# EVERY entry against a live zclassicd (the canonical C++ daemon) first.
#
# Why this exists: zclassicd's TEXT enforces tx serialized size
# > MAX_TX_SIZE_AFTER_SAPLING (102000) unconditionally in CheckTransaction
# (src/main.cpp:1196-1200), but the CANONICAL CHAIN contains 413 post-Sapling
# transactions above that cap (heights 478544..1968856) — legal at mine time
# when the effective cap was MAX_BLOCK_SIZE (2 MB); the constant was later
# tightened without grandfathering. Running zclassicd nodes accept those txs
# only because already-validated blocks are never re-checked; a from-genesis
# replay of zclassicd's own text FATALs at block 478544. zclassic23 matches
# zclassicd's LIVE BEHAVIOR: grandfather exactly those canonical txs
# (in-block only), enforce 102000 strictly everywhere else.
# See docs/CONSENSUS_PARITY_DOCTRINE.md "Empirical oversize grandfather".
#
# Inputs:
#   tools/data/oversize_grandfather_txids.txt   (height txid size; committed)
#   a running zclassicd with -txindex (RPC http://127.0.0.1:8232, creds from
#   ~/.zclassic/zclassic.conf rpcuser=/rpcpassword= lines)
#
# Outputs:
#   domain/consensus/src/oversize_grandfather_table.inc   (default)
#   lib/test/src/fixture_tx_oversize_478544.c             (--fixture)
#
# Per-entry verification against zclassicd before anything is emitted:
#   - getblockhash <height>       -> THE canonical main-chain hash at that
#     height today (in-main-chain proof)
#   - getblock <hash> 2           -> confirmations >= 1 AND the per-tx
#     "size" zclassicd computed from its stored raw tx == recorded size
#     (byte-exact). This is the same authority the 2026-06-11 scan used;
#     getrawtransaction/txindex is NOT used here because this zclassicd's
#     txindex returns "Database I/O error" for a subset of old txs while
#     the block-level read path is fine.
#   - recorded height within [476969, 1968856] (post-Sapling .. last
#     historical violation) and size within (102000, 2000000) (strict
#     structural ceiling)
#
# NEVER python. bash + curl + coreutils only.
set -euo pipefail

cd "$(dirname "$0")/../.."

RPC_URL="${ZCLASSICD_RPC_URL:-http://127.0.0.1:8232}"
CONF="${ZCLASSICD_CONF:-$HOME/.zclassic/zclassic.conf}"
DATA=tools/data/oversize_grandfather_txids.txt
OUT_INC=domain/consensus/src/oversize_grandfather_table.inc
OUT_FIXTURE=lib/test/src/fixture_tx_oversize_478544.c
FIXTURE_TXID=e3eeb123a79945cc74e6107422b124dc130ddd4b61fe5c74087317c256c79700
FIXTURE_SIZE=125811

SAPLING_ACTIVATION=476969
H_LAST_VIOLATION=1968856
CAP=102000
MAX_BLOCK_SIZE=2000000

MODE_FIXTURE=0
[ "${1:-}" = "--fixture" ] && MODE_FIXTURE=1

[ -f "$CONF" ] || { echo "FATAL: $CONF not found (need zclassicd RPC creds)" >&2; exit 1; }
RPCUSER=$(grep -m1 '^rpcuser=' "$CONF" | cut -d= -f2-)
RPCPASS=$(grep -m1 '^rpcpassword=' "$CONF" | cut -d= -f2-)
[ -n "$RPCUSER" ] && [ -n "$RPCPASS" ] || { echo "FATAL: rpcuser/rpcpassword missing in $CONF" >&2; exit 1; }

rpc() { # rpc <method> <params-json>
    curl -sf -u "$RPCUSER:$RPCPASS" -H 'content-type: text/plain' \
        -d "{\"jsonrpc\":\"1.0\",\"id\":\"gog\",\"method\":\"$1\",\"params\":$2}" \
        "$RPC_URL/"
}

json_int() { # json_int <json> <key>  — first integer value of "key"
    echo "$1" | grep -o "\"$2\":[0-9-]*" | head -1 | cut -d: -f2
}

json_str() { # json_str <json> <key>  — first string value of "key"
    echo "$1" | grep -o "\"$2\":\"[^\"]*\"" | head -1 | cut -d'"' -f4
}

reverse_hex() { # display txid hex -> internal uint256 byte order
    echo "$1" | fold -w2 | tac | tr -d '\n'
}

# ── Fixture mode ────────────────────────────────────────────────────────
if [ "$MODE_FIXTURE" = 1 ]; then
    raw=$(rpc getrawtransaction "[\"$FIXTURE_TXID\", 0]")
    hex=$(json_str "$raw" result || true)
    [ -n "$hex" ] || hex=$(echo "$raw" | sed -n 's/^{"result":"\([0-9a-f]*\)".*/\1/p')
    [ -n "$hex" ] || { echo "FATAL: could not fetch raw tx $FIXTURE_TXID" >&2; exit 1; }
    got=$(( ${#hex} / 2 ))
    [ "$got" = "$FIXTURE_SIZE" ] || { echo "FATAL: fixture size $got != $FIXTURE_SIZE" >&2; exit 1; }
    {
        echo "/* GENERATED by tools/scripts/gen_oversize_grandfather_table.sh --fixture"
        echo " * — DO NOT EDIT. Raw canonical mainnet transaction"
        echo " * $FIXTURE_TXID"
        echo " * (block 478544, $FIXTURE_SIZE bytes serialized — the first canonical tx"
        echo " * above MAX_TX_SIZE_AFTER_SAPLING; fetched from zclassicd getrawtransaction)."
        echo " * Test-binary-only KAT input for the empirical oversize grandfather. */"
        echo "#include <stddef.h>"
        echo ""
        echo "const unsigned char g_fixture_tx_oversize_478544[$FIXTURE_SIZE] = {"
        # Each token is exactly 5 chars ("0xab,"); fold width must be a
        # multiple of 5 or tokens get split across lines (invalid C).
        echo "$hex" | sed 's/../0x&,/g' | fold -w70
        echo "};"
        echo "const size_t g_fixture_tx_oversize_478544_len = $FIXTURE_SIZE;"
    } > "$OUT_FIXTURE"
    echo "wrote $OUT_FIXTURE ($(wc -c < "$OUT_FIXTURE") bytes)"
    exit 0
fi

# ── Table mode ──────────────────────────────────────────────────────────
[ -f "$DATA" ] || { echo "FATAL: $DATA not found" >&2; exit 1; }

tmp=$(mktemp)
blktmp=$(mktemp)
trap 'rm -f "$tmp" "$blktmp"' EXIT

n=0
for h in $(grep -v '^#' "$DATA" | awk 'NF==3 { print $1 }' | sort -un); do
    bh=$(json_str "$(rpc getblockhash "[$h]")" result)
    [ "${#bh}" = 64 ] || { echo "FATAL: getblockhash $h -> '$bh'" >&2; exit 1; }
    rpc getblock "[\"$bh\", 2]" > "$blktmp"
    conf=$(json_int "$(head -c 512 "$blktmp")" confirmations)
    [ -n "$conf" ] && [ "$conf" -ge 1 ] \
        || { echo "FATAL: block $h not confirmed (confirmations='$conf')" >&2; exit 1; }
    bheight=$(json_int "$(head -c 512 "$blktmp")" height)
    [ "$bheight" = "$h" ] \
        || { echo "FATAL: getblock height $bheight != requested $h" >&2; exit 1; }

    while read -r height txid size; do
        [ "${#txid}" = 64 ] || { echo "FATAL: bad txid '$txid'" >&2; exit 1; }
        [ "$size" -gt "$CAP" ] && [ "$size" -lt "$MAX_BLOCK_SIZE" ] \
            || { echo "FATAL: $txid size $size outside ($CAP, $MAX_BLOCK_SIZE)" >&2; exit 1; }
        [ "$height" -ge "$SAPLING_ACTIVATION" ] && [ "$height" -le "$H_LAST_VIOLATION" ] \
            || { echo "FATAL: $txid height $height outside [$SAPLING_ACTIVATION, $H_LAST_VIOLATION]" >&2; exit 1; }

        # Top-level tx objects (NOT vin prevouts) carry exactly this key
        # order in zclassicd's TxToJSON: txid, overwintered, version, size.
        m=$(grep -o "\"txid\":\"$txid\",\"overwintered\":[a-z]*,\"version\":[0-9]*,\"size\":[0-9]*" "$blktmp" | head -1)
        [ -n "$m" ] \
            || { echo "FATAL: $txid not found as a top-level tx in canonical block $h" >&2; exit 1; }
        gotsize=${m##*:}
        [ "$gotsize" = "$size" ] \
            || { echo "FATAL: $txid zclassicd size $gotsize != recorded $size" >&2; exit 1; }

        printf '%s %s %s %s\n' "$(reverse_hex "$txid")" "$size" "$height" "$txid" >> "$tmp"
        n=$((n + 1))
    done < <(grep -v '^#' "$DATA" | awk -v hh="$h" 'NF==3 && $1==hh { print }')
done

[ "$n" = 413 ] || { echo "FATAL: expected 413 entries, verified $n" >&2; exit 1; }

# Lowercase-hex lexical sort of the INTERNAL byte order == memcmp order of
# the 32 raw bytes (each byte maps monotonically to two hex chars).
sort "$tmp" -o "$tmp"
dups=$(cut -d' ' -f1 "$tmp" | uniq -d | wc -l)
[ "$dups" = 0 ] || { echo "FATAL: duplicate txids in table" >&2; exit 1; }

{
    echo "/* GENERATED by tools/scripts/gen_oversize_grandfather_table.sh — DO NOT EDIT."
    echo " * Source list: tools/data/oversize_grandfather_txids.txt (committed provenance)."
    echo " *"
    echo " * COMPLETE empirical scan of the canonical ZClassic chain, heights 0..3143532"
    echo " * (2026-06-11; raw blk*.dat frame walk + per-height hash and per-tx size"
    echo " * cross-check against a live zclassicd with txindex). Every canonical"
    echo " * transaction with serialized size > MAX_TX_SIZE_AFTER_SAPLING (102000)."
    echo " * All 413 lie in heights 478544..1968856 — post-Sapling (act. 476969), legal"
    echo " * at mine time when the effective cap was MAX_BLOCK_SIZE (2000000); zclassicd"
    echo " * later tightened the constant without grandfathering, so its TEXT cannot"
    echo " * resync its own chain (proven: from-genesis replay FATALs at block 478544,"
    echo " * tx e3eeb123..., bad-txns-oversize). Running zclassicd nodes accept these"
    echo " * txs only because already-validated blocks are never re-checked, and they"
    echo " * enforce 102000 on every NEW block — which is exactly the live behavior the"
    echo " * predicate in tx_structural.c reproduces. Max entry 1922197 < 2000000."
    echo " * See docs/CONSENSUS_PARITY_DOCTRINE.md \"Empirical oversize grandfather\"."
    echo " *"
    echo " * txids are in INTERNAL uint256 byte order (reversed display hex), sorted"
    echo " * ascending by memcmp so the predicate can bsearch. Every entry was verified"
    echo " * against zclassicd at generation time: confirmations>=1, byte-exact size,"
    echo " * height match. */"
    echo ""
    echo "struct oversize_grandfather_entry {"
    echo "    unsigned char txid[32];   /* INTERNAL byte order */"
    echo "    uint32_t      size;       /* exact serialized size, bytes */"
    echo "};"
    echo ""
    echo "#define OVERSIZE_GRANDFATHER_COUNT 413u"
    echo ""
    echo "static const struct oversize_grandfather_entry"
    echo "g_oversize_grandfather[OVERSIZE_GRANDFATHER_COUNT] = {"
    while read -r internal size height display; do
        bytes=$(echo "$internal" | sed 's/../0x&,/g' | sed 's/,$//')
        echo "    { {$bytes},"
        echo "      $size },  /* h=$height $display */"
    done < "$tmp"
    echo "};"
} > "$OUT_INC"

echo "verified $n/413 entries against zclassicd; wrote $OUT_INC ($(wc -l < "$OUT_INC") lines)"
