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

import "./RLPReader.sol";

/**
 * @title MerklePatriciaProof — hexary Merkle-Patricia trie inclusion/exclusion verifier.
 *
 * @notice Solidity port of the validated Rust verifier in
 *         `zk-interop/interop-core/src/mpt.rs` (used verbatim inside the AERE SP1
 *         interop guest). Given a trie `root`, a key as a nibble path, and the set of
 *         trie nodes along that path (as `eth_getProof` / a receipts-trie builder
 *         emits), it returns the trie value for an included key or the empty flag for a
 *         proven absent key. Byte-exact: a tampered node or wrong root fails rather
 *         than lies.
 *
 *         Node model (keccak-hashed refs, hex-prefix paths):
 *           leaf      [ HP(path, leaf), value ]
 *           extension [ HP(path, ext),  child_ref ]
 *           branch    [ c0, c1, ..., c15, value ]
 */
library MerklePatriciaProof {
    using RLPReader for bytes;

    /// @notice Verify inclusion of `key` under `root` given `nodes`.
    /// @param root  the 32-byte trie root (e.g. a header's receiptsRoot).
    /// @param key   the key bytes (for a receipts trie: rlp(transactionIndex)).
    /// @param nodes the proof node set; order irrelevant (resolved by hash).
    /// @return included true if the key is present, false if proven absent.
    /// @return value    the trie value bytes when included (empty otherwise).
    function verify(bytes32 root, bytes memory key, bytes[] memory nodes)
        internal
        pure
        returns (bool included, bytes memory value)
    {
        bytes memory path = _nibbles(key);

        // Empty trie: root = keccak(rlp("")) = keccak(0x80). Any key is absent.
        if (root == keccak256(hex"80")) {
            return (false, "");
        }

        bytes memory node = _findByHash(nodes, root);
        uint256 idx = 0;

        // Bounded loop: a valid path is at most 64 nibbles (+ node hops); 512 is safe.
        for (uint256 hop = 0; hop < 512; hop++) {
            bytes[] memory items = node.splitList();

            if (items.length == 2) {
                (bool isLeaf, bytes memory frag) = _hpDecode(items[0].readBytes());
                if (isLeaf) {
                    if (_eq(_slice(path, idx), frag)) {
                        return (true, items[1].readBytes());
                    }
                    return (false, ""); // diverges at leaf -> exclusion
                }
                // extension: remaining path must start with frag
                bytes memory rem = _slice(path, idx);
                if (rem.length < frag.length || !_startsWith(rem, frag)) {
                    return (false, "");
                }
                idx += frag.length;
                node = _resolve(items[1], nodes);
            } else if (items.length == 17) {
                if (idx >= path.length) {
                    // value in slot 16
                    if (_isEmptyRef(items[16])) return (false, "");
                    return (true, items[16].readBytes());
                }
                uint256 nib = uint8(path[idx]);
                idx += 1;
                bytes memory child = items[nib];
                if (_isEmptyRef(child)) return (false, "");
                node = _resolve(child, nodes);
            } else {
                revert("MPT:bad arity");
            }
        }
        revert("MPT:path too long");
    }

    // ---- node reference resolution ------------------------------------------

    /// @dev Resolve a child reference: an inline (embedded) list node IS the node;
    ///      a 32-byte string is a hash to look up in the node set.
    function _resolve(bytes memory ref, bytes[] memory nodes) private pure returns (bytes memory) {
        if (RLPReader.isList(ref)) {
            return ref; // embedded node
        }
        bytes memory content = ref.readBytes();
        require(content.length == 32, "MPT:bad ref");
        return _findByHash(nodes, _toBytes32(content));
    }

    function _findByHash(bytes[] memory nodes, bytes32 h) private pure returns (bytes memory) {
        for (uint256 i = 0; i < nodes.length; i++) {
            if (keccak256(nodes[i]) == h) return nodes[i];
        }
        revert("MPT:missing node");
    }

    function _isEmptyRef(bytes memory ref) private pure returns (bool) {
        return ref.length == 0 || (ref.length == 1 && ref[0] == 0x80);
    }

    // ---- path / nibble helpers ----------------------------------------------

    function _nibbles(bytes memory b) private pure returns (bytes memory out) {
        out = new bytes(b.length * 2);
        for (uint256 i = 0; i < b.length; i++) {
            out[2 * i] = bytes1(uint8(b[i]) >> 4);
            out[2 * i + 1] = bytes1(uint8(b[i]) & 0x0f);
        }
    }

    /// @dev Hex-prefix ("compact") decode. First nibble: bit1=leaf, bit0=odd.
    function _hpDecode(bytes memory enc) private pure returns (bool isLeaf, bytes memory nibbles) {
        require(enc.length != 0, "MPT:empty hp");
        uint8 flags = uint8(enc[0]) >> 4;
        isLeaf = (flags & 0x2) != 0;
        bool odd = (flags & 0x1) != 0;
        uint256 count = (enc.length - 1) * 2 + (odd ? 1 : 0);
        nibbles = new bytes(count);
        uint256 k = 0;
        if (odd) {
            nibbles[k++] = bytes1(uint8(enc[0]) & 0x0f);
        }
        for (uint256 i = 1; i < enc.length; i++) {
            nibbles[k++] = bytes1(uint8(enc[i]) >> 4);
            nibbles[k++] = bytes1(uint8(enc[i]) & 0x0f);
        }
    }

    function _slice(bytes memory b, uint256 start) private pure returns (bytes memory out) {
        if (start >= b.length) return "";
        out = new bytes(b.length - start);
        for (uint256 i = 0; i < out.length; i++) out[i] = b[start + i];
    }

    function _startsWith(bytes memory a, bytes memory prefix) private pure returns (bool) {
        if (a.length < prefix.length) return false;
        for (uint256 i = 0; i < prefix.length; i++) {
            if (a[i] != prefix[i]) return false;
        }
        return true;
    }

    function _eq(bytes memory a, bytes memory b) private pure returns (bool) {
        return a.length == b.length && keccak256(a) == keccak256(b);
    }

    function _toBytes32(bytes memory b) private pure returns (bytes32 r) {
        require(b.length == 32, "MPT:not b32");
        assembly {
            r := mload(add(b, 32))
        }
    }
}
