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

/**
 * @title AereShutterMempool — threshold-encrypted mempool ingress (Shutter playbook)
 * @notice MEV at its worst lives in the gap between transaction broadcast and
 *         block inclusion. AereShutterMempool closes that gap by accepting
 *         ENCRYPTED tx envelopes that a Shutter-style threshold keypers
 *         committee decrypts only at block N+1 (configurable delay), AFTER
 *         the block ordering is finalised.
 *
 *         Phase 1 (this contract): the on-chain commitment + reveal logic.
 *         An off-chain Shutter keyper committee handles the actual
 *         threshold encryption + key release. The on-chain side just
 *         tracks:
 *
 *           1. Active keyper committee (Foundation-rotated per epoch).
 *           2. Per-epoch committed ciphertext envelopes (anyone can
 *              submit).
 *           3. Per-epoch decryption-key release (keypers post the
 *              aggregated decryption key once the epoch has finalised).
 *           4. Reveal-and-execute path: anyone can present a plaintext
 *              tx whose ciphertext was committed in epoch E, prove the
 *              decryption key was released, and have it MARKED as
 *              decrypt-eligible. The actual EVM execution happens via
 *              the receiver contract's own logic (this contract is
 *              orchestration, not an executor).
 *
 *         INVARIANT — sequencer cannot front-run:
 *           - At block N, ciphertexts are visible but undecipherable.
 *           - Block N's ordering is finalised by consensus.
 *           - At block N+REVEAL_DELAY, keypers release the decryption
 *             key. Now the ciphertexts can be decrypted in the order
 *             they were committed.
 *           - Reordering after the fact is impossible because the
 *             ordering was already in block N.
 *
 *         IMMUTABILITY:
 *           - REVEAL_DELAY_BLOCKS constant.
 *           - FOUNDATION immutable.
 *           - Keyper threshold parameters set at deploy; rotation is
 *             one-shot per epoch (no in-epoch swaps).
 *
 *         WHAT'S NOT IN V0:
 *           - Slashing of keypers that fail to release on time (Phase 2).
 *           - Actual on-chain decryption (Phase 2 precompile at 0x102).
 *           - Per-tx fee accounting / priority (Phase 2; we ship plumbing
 *             first).
 *           - MEV redistribution (separate contract).
 */
contract AereShutterMempool {

    /* ================================ config ================================ */

    address public immutable FOUNDATION;
    uint64  public immutable REVEAL_DELAY_BLOCKS;   // e.g. 2 for ~1s on 0.5s blocks
    uint16  public immutable KEYPER_THRESHOLD;       // e.g. 9 of 13
    uint16  public immutable KEYPER_SIZE;

    /* ================================= types ================================ */

    struct KeyperEpoch {
        bytes32 committeeRoot;       // Merkle root of keyper pubkeys (BLS12-381)
        bytes32 pubkeyAggregateHash; // hash of the aggregated public key
        uint64  startBlock;
        uint64  endBlock;
        bool    exists;
    }

    struct CipherEnvelope {
        address sender;              // submitter (pays gas; not necessarily the tx originator)
        bytes32 ciphertextHash;      // SHA256 of the raw ciphertext blob
        string  ciphertextUri;       // ipfs:// or https:// (encrypted blob lives off-chain)
        uint64  committedAtBlock;
        uint64  epochId;
        bool    revealed;
    }

    /* ================================ storage ================================ */

    /// @notice epochId → KeyperEpoch.
    mapping(uint64 => KeyperEpoch) public epochs;
    uint64 public currentEpochId;

    /// @notice epochId → decryption key commitment posted by keypers.
    mapping(uint64 => bytes32) public decryptionKeyHash;
    mapping(uint64 => uint64)  public decryptionKeyReleasedAt;
    mapping(uint64 => string)  public decryptionKeyUri;

    /// @notice envelopeId → CipherEnvelope.
    mapping(bytes32 => CipherEnvelope) internal _envelopes;
    /// @notice per-epoch monotonic ingestion counter.
    mapping(uint64 => uint256) public envelopeCount;

    /* ================================ events ================================ */

    event EpochOpened(uint64 indexed epochId, bytes32 committeeRoot, bytes32 pubkeyAggregateHash, uint64 startBlock);
    event EpochClosed(uint64 indexed epochId, uint64 endBlock);
    event Committed(bytes32 indexed envelopeId, uint64 indexed epochId, address indexed sender, bytes32 ciphertextHash, string ciphertextUri);
    event DecryptionKeyReleased(uint64 indexed epochId, bytes32 keyHash, string keyUri);
    event Revealed(bytes32 indexed envelopeId, bytes32 plaintextHash);

    /* ================================ errors ================================ */

    error NotFoundation();
    error EpochClosedAlready();
    error EpochNotOpen();
    error EmptyCommitment();
    error EnvelopeUnknown();
    error EnvelopeAlreadyCommitted();
    error KeyNotReleased();
    error AlreadyRevealed();
    error PlaintextMismatch();
    error KeyAlreadyReleased();
    error EpochTransitionRace(uint64 currentEpoch, uint64 expectedEpoch);
    error RevealTooEarly(uint64 nowBlock, uint64 earliest);

    /* =============================== modifiers ============================== */

    modifier onlyFoundation() {
        if (msg.sender != FOUNDATION) revert NotFoundation();
        _;
    }

    /* ============================== constructor ============================= */

    constructor(address foundation, uint64 revealDelayBlocks, uint16 keyperSize, uint16 keyperThreshold) {
        require(foundation != address(0), "zero-foundation");
        require(keyperThreshold > 0 && keyperThreshold <= keyperSize, "bad-threshold");
        // AUDIT FIX (MED #31): MEV protection requires at least 1 block of delay.
        require(revealDelayBlocks >= 1, "reveal-delay-zero");
        FOUNDATION = foundation;
        REVEAL_DELAY_BLOCKS = revealDelayBlocks;
        KEYPER_SIZE = keyperSize;
        KEYPER_THRESHOLD = keyperThreshold;
    }

    /* ============================== epoch ops ============================== */

    /// @notice Foundation opens a new keyper epoch. Implicitly closes the
    ///         previous one. Open at most one epoch at a time.
    function openEpoch(uint64 epochId, bytes32 committeeRoot, bytes32 pubkeyAggregateHash) external onlyFoundation {
        if (epochs[epochId].exists) revert EpochClosedAlready();
        // close prior
        KeyperEpoch storage prev = epochs[currentEpochId];
        if (prev.exists && prev.endBlock == 0) {
            prev.endBlock = uint64(block.number);
            emit EpochClosed(currentEpochId, prev.endBlock);
        }
        epochs[epochId] = KeyperEpoch({
            committeeRoot: committeeRoot,
            pubkeyAggregateHash: pubkeyAggregateHash,
            startBlock: uint64(block.number),
            endBlock: 0,
            exists: true
        });
        currentEpochId = epochId;
        emit EpochOpened(epochId, committeeRoot, pubkeyAggregateHash, uint64(block.number));
    }

    /* ============================ commit ciphertext ========================== */

    /// @notice Submit an encrypted tx envelope for the current epoch.
    ///         envelopeId = keccak256(epochId, ciphertextHash, sender).
    function commit(bytes32 ciphertextHash, string calldata ciphertextUri) external returns (bytes32 envelopeId) {
        return _commit(ciphertextHash, ciphertextUri, currentEpochId);
    }

    /// AUDIT FIX (HIGH #6): caller can pin the expected epoch to avoid an
    /// epoch transition race where a sequencer reorders openEpoch before
    /// this commit, attaching the ciphertext to the wrong keyper pubkey
    /// (permanently undecryptable).
    function commitForEpoch(bytes32 ciphertextHash, string calldata ciphertextUri, uint64 expectedEpochId)
        external returns (bytes32 envelopeId)
    {
        return _commit(ciphertextHash, ciphertextUri, expectedEpochId);
    }

    function _commit(bytes32 ciphertextHash, string calldata ciphertextUri, uint64 expectedEpochId)
        internal returns (bytes32 envelopeId)
    {
        if (ciphertextHash == bytes32(0)) revert EmptyCommitment();
        if (expectedEpochId != currentEpochId) revert EpochTransitionRace(currentEpochId, expectedEpochId);
        KeyperEpoch storage e = epochs[currentEpochId];
        if (!e.exists || e.endBlock != 0) revert EpochNotOpen();

        envelopeId = keccak256(abi.encode(currentEpochId, ciphertextHash, msg.sender));
        // AUDIT FIX (HIGH #4): refuse to overwrite an existing envelope slot;
        // double-reveal via re-commit is now impossible.
        if (_envelopes[envelopeId].sender != address(0)) revert EnvelopeAlreadyCommitted();
        _envelopes[envelopeId] = CipherEnvelope({
            sender: msg.sender,
            ciphertextHash: ciphertextHash,
            ciphertextUri: ciphertextUri,
            committedAtBlock: uint64(block.number),
            epochId: currentEpochId,
            revealed: false
        });
        envelopeCount[currentEpochId]++;
        emit Committed(envelopeId, currentEpochId, msg.sender, ciphertextHash, ciphertextUri);
    }

    /* ============================= release key =============================== */

    /// @notice Foundation (proxying for keypers post quorum) releases the
    ///         decryption key for an epoch. Releasable from
    ///         epoch.startBlock + REVEAL_DELAY_BLOCKS onward.
    function releaseDecryptionKey(uint64 epochId, bytes32 keyHash, string calldata keyUri) external onlyFoundation {
        KeyperEpoch storage e = epochs[epochId];
        if (!e.exists) revert EpochNotOpen();
        uint64 earliest = e.startBlock + REVEAL_DELAY_BLOCKS;
        if (block.number < earliest) revert RevealTooEarly(uint64(block.number), earliest);
        // AUDIT FIX (HIGH #5): one-shot per epoch. Previously Foundation could
        // overwrite a released key arbitrarily, defeating the commitment trail.
        if (decryptionKeyHash[epochId] != bytes32(0)) revert KeyAlreadyReleased();
        decryptionKeyHash[epochId] = keyHash;
        decryptionKeyReleasedAt[epochId] = uint64(block.number);
        decryptionKeyUri[epochId] = keyUri;
        emit DecryptionKeyReleased(epochId, keyHash, keyUri);
    }

    /* ============================ reveal + execute ========================== */

    /// @notice Mark an envelope as decrypt-eligible (the key was released).
    ///         The consumer contract (e.g. AereCoreBookV0, AERE402, the
    ///         settlement layer) reads `revealed` to gate execution of the
    ///         decoded plaintext tx.
    /// AUDIT FIX (HIGH #3): gated to Foundation in Phase 1 (Phase 2 will use
    /// keyper-signed reveals against the BLS aggregate pubkey). Previously
    /// anyone could forge a Revealed event with any plaintextHash, defeating
    /// the entire anti-MEV invariant. Also: Foundation must submit the actual
    /// plaintext bytes so the contract can check H(plaintext, releasedKey)
    /// matches the ciphertextHash — proving the reveal is consistent with
    /// the released key + the committed ciphertext.
    function markRevealed(bytes32 envelopeId, bytes calldata plaintext) external onlyFoundation returns (bool) {
        CipherEnvelope storage env = _envelopes[envelopeId];
        if (env.sender == address(0)) revert EnvelopeUnknown();
        if (env.revealed) revert AlreadyRevealed();
        bytes32 releasedKey = decryptionKeyHash[env.epochId];
        if (releasedKey == bytes32(0)) revert KeyNotReleased();
        // Verify the plaintext binds to (ciphertextHash, releasedKey).
        // The off-chain protocol: ciphertext = encrypt(key, plaintext);
        // so H(plaintext, releasedKey) must equal the committed ciphertextHash.
        bytes32 expected = keccak256(abi.encodePacked(plaintext, releasedKey));
        if (expected != env.ciphertextHash) revert PlaintextMismatch();

        env.revealed = true;
        emit Revealed(envelopeId, keccak256(plaintext));
        return true;
    }

    /* ================================ views ================================ */

    function envelopeOf(bytes32 id) external view returns (CipherEnvelope memory) {
        return _envelopes[id];
    }

    function isRevealed(bytes32 id) external view returns (bool) {
        return _envelopes[id].revealed;
    }

    function isKeyReleased(uint64 epochId) external view returns (bool) {
        return decryptionKeyHash[epochId] != bytes32(0);
    }
}
