// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

/// @title AereDaFraudProofVerifier
/// @notice On-chain verifier for the AERE Data Availability layer's
///         bad-encoding fraud proofs and availability samples. It recomputes,
///         entirely on-chain, the Namespaced-Merkle-Tree (NMT) commitment and
///         the 2D Reed-Solomon re-encoding used by the off-chain `aere-da`
///         Rust crate (aerenew/da), and CONVICTS a badly-encoded block while
///         REJECTING spurious challenges.
///
///         Commitment layout (Celestia-style, identical to the crate):
///           * each row and each column of the 2k x 2k extended data square is
///             committed with an NMT  -> row_root / col_root;
///           * an outer NMT over row_roots ++ col_roots gives the data root.
///
///         A bad-encoding fraud proof reveals the k committed basis shares of a
///         row/column plus one committed "disputed" share. The verifier:
///           1. checks the challenged line's root is committed under the data
///              root (outer NMT inclusion), and
///           2. checks every revealed share is committed under that root (inner
///              NMT inclusion), and
///           3. recomputes the RS value at the disputed position from the k
///              basis shares and CONVICTS iff it disagrees with the committed
///              disputed share (the RS-parity-break check).
///         All three must hold for a conviction; otherwise the challenge is
///         rejected.
///
///         Hash: this verifier uses the EVM `sha256` precompile (0x02) and so
///         commits to the crate's SHA-256 hasher. The crate also offers a
///         FIPS-202 SHAKE256 hasher matching AERE mainnet's live 0x0AE5
///         precompile; standard EVM exposes no SHAKE256 precompile, so the
///         SHA-256 variant is the on-chain path here. Both are hash-based and
///         carry no pairing / discrete-log assumption.
///
///         Scope: verification logic only. No block production, no economic
///         slashing, no deployment wired here. This is the settlement-side
///         adjudicator a DA dispute would call.
contract AereDaFraudProofVerifier {
    // ---------------------------------------------------------------------
    // Types (mirror the crate's serialization; see aerenew/da/src/nmt.rs,
    // rs2d.rs, das.rs).
    // ---------------------------------------------------------------------

    /// An NMT node: the namespace range it spans and its 32-byte hash.
    struct Node {
        bytes8 min;
        bytes8 max;
        bytes32 hash;
    }

    /// A single-leaf NMT inclusion proof: siblings from leaf level upward.
    struct InclusionProof {
        uint256 index;
        Node[] siblings;
    }

    /// A revealed share at a line position, with its inclusion proof.
    struct ShareProof {
        uint256 pos;
        bytes8 ns;
        bytes data;
        InclusionProof proof;
    }

    /// A bad-encoding fraud proof for one row (axis 0) or column (axis 1).
    struct BadEncoding {
        uint8 axis; // 0 = row ('r'), 1 = column ('c')
        uint256 index;
        Node root;
        InclusionProof outerProof;
        ShareProof[] basis; // k basis shares
        ShareProof disputed; // the committed share that breaks RS parity
    }

    /// A verifiable availability sample (light-node path).
    struct Sample {
        uint256 row;
        uint256 col;
        bytes8 ns;
        bytes data;
        InclusionProof rowProof;
        Node rowRoot;
        InclusionProof outerProof;
    }

    bytes1 private constant LEAF_PREFIX = 0x00;
    bytes1 private constant NODE_PREFIX = 0x01;

    /// @dev GF(2^8) exp/log tables over the AES field x^8+x^4+x^3+x^2+1 (0x11b),
    ///      generator 0x03. Built once in the constructor; identical to the
    ///      crate's `Gf256::new`. `expTable` is length 512 so multiply needs no
    ///      modulo.
    bytes private expTable;
    bytes private logTable;

    event BadBlockConvicted(bytes32 indexed dataRootHash, uint8 axis, uint256 index, uint256 disputedPos);

    constructor() {
        bytes memory e = new bytes(512);
        bytes memory l = new bytes(256);
        uint16 x = 1;
        for (uint256 i = 0; i < 255; i++) {
            e[i] = bytes1(uint8(x));
            l[uint256(x)] = bytes1(uint8(i));
            // multiply by generator 0x03 == x + 1: x*3 = (x<<1) ^ x
            x ^= x << 1;
            if (x & 0x100 != 0) {
                x ^= 0x11b;
            }
            x &= 0xff;
        }
        for (uint256 i = 255; i < 512; i++) {
            e[i] = e[i - 255];
        }
        expTable = e;
        logTable = l;
    }

    // ---------------------------------------------------------------------
    // NMT recomputation (SHA-256), byte-for-byte matching aerenew/da/src/nmt.rs.
    // ---------------------------------------------------------------------

    function _leaf(bytes8 ns, bytes memory data) internal pure returns (Node memory) {
        bytes32 h = sha256(abi.encodePacked(LEAF_PREFIX, ns, data));
        return Node({min: ns, max: ns, hash: h});
    }

    function _inner(Node memory a, Node memory b) internal pure returns (Node memory) {
        bytes32 h = sha256(
            abi.encodePacked(NODE_PREFIX, a.min, a.max, a.hash, b.min, b.max, b.hash)
        );
        return Node({min: a.min, max: b.max, hash: h});
    }

    function _eq(Node memory a, Node memory b) internal pure returns (bool) {
        return a.min == b.min && a.max == b.max && a.hash == b.hash;
    }

    /// @dev outer_ns(i): 8-byte big-endian encoding of i (matches das.rs).
    function _outerNs(uint256 i) internal pure returns (bytes8) {
        return bytes8(uint64(i));
    }

    /// @dev node_bytes(node) = min || max || hash (48 bytes), the outer-tree
    ///      leaf payload for a row/column root.
    function _nodeBytes(Node memory node) internal pure returns (bytes memory) {
        return abi.encodePacked(node.min, node.max, node.hash);
    }

    /// @dev Verify a single-leaf NMT inclusion proof against `root`.
    function _verifyInclusion(
        Node memory root,
        InclusionProof memory proof,
        bytes8 ns,
        bytes memory data
    ) internal pure returns (bool) {
        Node memory node = _leaf(ns, data);
        uint256 idx = proof.index;
        uint256 len = proof.siblings.length;
        for (uint256 i = 0; i < len; i++) {
            if (idx & 1 == 0) {
                node = _inner(node, proof.siblings[i]);
            } else {
                node = _inner(proof.siblings[i], node);
            }
            idx >>= 1;
        }
        return _eq(node, root);
    }

    // ---------------------------------------------------------------------
    // GF(256) Reed-Solomon (matches aerenew/da/src/gf256.rs).
    // ---------------------------------------------------------------------

    function _mul(uint8 a, uint8 b) internal view returns (uint8) {
        if (a == 0 || b == 0) return 0;
        uint256 idx = uint256(uint8(logTable[a])) + uint256(uint8(logTable[b]));
        return uint8(expTable[idx]);
    }

    function _div(uint8 a, uint8 b) internal view returns (uint8) {
        // Caller guarantees b != 0 (interpolation nodes are distinct).
        if (a == 0) return 0;
        uint256 idx = (uint256(uint8(logTable[a])) + 255 - uint256(uint8(logTable[b]))) % 255;
        return uint8(expTable[idx]);
    }

    /// @dev Evaluate at `x` the unique degree<m polynomial through (xs[i],ys[i]),
    ///      via the Lagrange form. Identical to `Gf256::interpolate_eval`.
    function _interp(uint8[] memory xs, uint8[] memory ys, uint8 x) internal view returns (uint8) {
        uint8 acc = 0;
        uint256 m = xs.length;
        for (uint256 i = 0; i < m; i++) {
            if (x == xs[i]) {
                return ys[i];
            }
            uint8 num = 1;
            uint8 den = 1;
            for (uint256 j = 0; j < m; j++) {
                if (i == j) continue;
                num = _mul(num, x ^ xs[j]);
                den = _mul(den, xs[i] ^ xs[j]);
            }
            acc ^= _mul(ys[i], _div(num, den));
        }
        return acc;
    }

    // ---------------------------------------------------------------------
    // Public verification API.
    // ---------------------------------------------------------------------

    /// @notice Verify a bad-encoding fraud proof. Returns true iff it is a
    ///         VALID proof that the committed square is badly encoded: the RS
    ///         re-encoding of the k committed basis shares disagrees with the
    ///         committed disputed share, and every revealed share carries a
    ///         valid inclusion proof against the committed line root, which is
    ///         itself committed under `dataRoot`.
    /// @dev View (reads the GF tables). Pure with respect to persistent state.
    function verifyBadEncoding(
        Node calldata dataRoot,
        uint256 k,
        uint256 n,
        BadEncoding calldata p
    ) public view returns (bool) {
        if (p.basis.length != k) {
            return false;
        }
        // The challenged line root must be committed under the data root at the
        // right outer index: row r sits at index r, column c at index n + c.
        uint256 outerIdx = p.axis == 0 ? p.index : n + p.index;
        if (p.outerProof.index != outerIdx) {
            return false;
        }
        if (!_verifyInclusion(_copyNode(dataRoot), _copyProof(p.outerProof), _outerNs(outerIdx), _nodeBytes(_copyNode(p.root)))) {
            return false;
        }

        Node memory rootMem = _copyNode(p.root);

        // Every basis share is committed under the line root at its position.
        for (uint256 i = 0; i < k; i++) {
            ShareProof calldata b = p.basis[i];
            if (b.proof.index != b.pos) {
                return false;
            }
            if (!_verifyInclusion(rootMem, _copyProof(b.proof), b.ns, b.data)) {
                return false;
            }
        }

        // The disputed share is committed under the line root at its position.
        ShareProof calldata d = p.disputed;
        if (d.proof.index != d.pos) {
            return false;
        }
        if (!_verifyInclusion(rootMem, _copyProof(d.proof), d.ns, d.data)) {
            return false;
        }

        // Recompute the RS value at the disputed position from the k basis
        // shares, symbol-wise over each byte offset. CONVICT iff any byte of the
        // committed disputed share disagrees with the re-encoding.
        uint256 sl = d.data.length;
        uint8[] memory xs = new uint8[](k);
        for (uint256 i = 0; i < k; i++) {
            xs[i] = uint8(p.basis[i].pos);
        }
        uint8 xj = uint8(d.pos);
        for (uint256 bpos = 0; bpos < sl; bpos++) {
            uint8[] memory ys = new uint8[](k);
            for (uint256 i = 0; i < k; i++) {
                ys[i] = uint8(p.basis[i].data[bpos]);
            }
            uint8 rec = _interp(xs, ys, xj);
            if (rec != uint8(d.data[bpos])) {
                return true; // RS parity break -> badly encoded -> convict
            }
        }
        return false; // re-encoding matches -> spurious challenge -> reject
    }

    /// @notice Same as {verifyBadEncoding} but state-changing: emits
    ///         {BadBlockConvicted} on a genuine bad-encoding proof and reverts
    ///         on a spurious challenge. This is the entry point a DA slashing
    ///         module would call.
    function challenge(
        Node calldata dataRoot,
        uint256 k,
        uint256 n,
        BadEncoding calldata p
    ) external returns (bool) {
        require(verifyBadEncoding(dataRoot, k, n, p), "AereDA: not a valid bad-encoding fraud proof");
        emit BadBlockConvicted(dataRoot.hash, p.axis, p.index, p.disputed.pos);
        return true;
    }

    /// @notice Verify a single availability sample against the data root: the
    ///         cell is committed under its row root, and the row root is
    ///         committed under the data root. This is the NMT-inclusion leg a
    ///         sampling light node checks.
    function verifySample(
        Node calldata dataRoot,
        uint256 n,
        Sample calldata s
    ) external pure returns (bool) {
        Node memory rowRoot = _copyNode(s.rowRoot);
        if (!_verifyInclusion(rowRoot, _copyProof(s.rowProof), s.ns, s.data)) {
            return false;
        }
        if (s.rowProof.index != s.col) {
            return false;
        }
        if (s.outerProof.index != s.row) {
            return false;
        }
        if (!_verifyInclusion(_copyNode(dataRoot), _copyProof(s.outerProof), _outerNs(s.row), _nodeBytes(rowRoot))) {
            return false;
        }
        return s.row < n;
    }

    // ---------------------------------------------------------------------
    // Small calldata -> memory copy helpers (viaIR keeps these cheap).
    // ---------------------------------------------------------------------

    function _copyNode(Node calldata nd) internal pure returns (Node memory) {
        return Node({min: nd.min, max: nd.max, hash: nd.hash});
    }

    function _copyProof(InclusionProof calldata pr) internal pure returns (InclusionProof memory out) {
        out.index = pr.index;
        out.siblings = new Node[](pr.siblings.length);
        for (uint256 i = 0; i < pr.siblings.length; i++) {
            out.siblings[i] = _copyNode(pr.siblings[i]);
        }
    }
}
