// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; /** * @title AereShutterMempoolV2 - threshold-BLS anti-MEV encrypted mempool (PoC) * @notice APPLICATION-LEVEL, OPT-IN Shutter-style encrypted mempool. This is NOT * an L1 base-layer change: a Hyperledger Besu QBFT chain cannot gain a * protocol-level encrypted mempool without a client fork. Instead this * contract implements the commit -> order -> threshold-decrypt pipeline * at the application layer, so a proposer/sequencer cannot see plaintext * transactions BEFORE the ordering for an epoch is fixed. * * Unlike the V0 orchestration contract (which used a keccak placeholder * for the "decryption key"), V2 verifies a REAL (t,N) threshold-BLS * scheme on-chain using the EIP-2537 BLS12-381 pairing precompile at * address 0x0f (PROBED LIVE on AERE chain 2800: a valid pairing identity * returns 1, garbage input reverts). * * Cryptography (Boldyreva threshold BLS over BLS12-381): * - Committee master secret s is Shamir (t,N)-shared off-chain by a dealer. * - Group public key PK = s * g2 (G2) : the epoch encryption pubkey. * - Keyper pubkeys P_i = s_i * g2 (G2) : published VSS commitments. * - Epoch identity H1 = hashToCurve(epoch tag) in G1. * - Epoch decrypt key DK = s * H1 (G1) : the threshold signature. * - Keyper share sig_i = s_i * H1 (G1). * - DK is reconstructed off-chain from t shares via Lagrange interpolation. * * On-chain pairing checks (all via 0x0f): * share valid : e(sig_i, g2) == e(H1, P_i) <=> e(sig_i,g2)*e(-H1,P_i)==1 * DK valid : e(DK, g2) == e(H1, PK) <=> e(DK, g2)*e(-H1,PK )==1 * A share or a reconstructed DK that does not satisfy its pairing REVERTS, * so a keyper cannot post an inconsistent share and no forged DK is accepted. * * Anti-MEV invariant: * 1. Users submit ciphertexts + a hiding/binding commitment while the epoch * is Open. The sequencer sees only ciphertexts. * 2. The sequencer commits an ordering (Merkle root over positions) BEFORE * any decryption share exists. submitDecryptionShare REVERTS until then. * 3. Keypers post t valid shares; anyone reconstructs DK off-chain; the * reconstruction is pairing-verified on-chain at finalizeEpoch. * 4. Reveal-and-execute walks the committed ordering strictly in sequence; * a plaintext that does not open its committed value REVERTS, and an * out-of-order reveal REVERTS. * * HONEST SCOPE / ASSUMPTIONS: * - The symmetric unmasking step (hashing the GT pairing element to a byte * mask for XOR) is performed OFF-CHAIN by the executor: the 0x0f precompile * returns only a pairing-equality boolean, it cannot output a GT element to * hash. On-chain, plaintext<->commitment binding is a keccak commitment. * DK correctness itself is fully pairing-verified on-chain, so decryption is * deterministic and cannot be produced before t keypers act. * - PK, P_i, H1 and its negation H1neg are published by the committee dealer * at registration/epoch start (the "VSS commitments"). A malformed published * point only breaks LIVENESS (an honest DK/share would fail its pairing); it * cannot let a non-committee actor forge DK, which still requires the master * secret s that only t-of-N shares reconstruct. * - Trusted-dealer Shamir setup is used for the PoC; a production system would * run a DKG so no single party ever holds s. */ contract AereShutterMempoolV2 { /* ================================ roles ================================ */ address public coordinator; // committee dealer / admin address public sequencer; // commits the epoch ordering address public executor; // optional consumer that executes revealed txs /* ============================== precompile ============================= */ address internal constant BLS_PAIRING = address(0x0f); // EIP-2537 pairing check /* =============================== committee ============================= */ bool public committeeSet; uint16 public threshold; // t uint16 public keyperCount; // N mapping(uint16 => address) public keyperAddr; // 0-based mapping(uint16 => bytes) public keyperPub; // P_i = s_i*g2, 256-byte G2 bytes public G2_GEN; // canonical BLS12-381 G2 generator (256-byte EIP-2537) /* ================================ epochs ============================== */ enum Phase { None, Open, Ordered, Finalized } struct Epoch { Phase phase; uint64 startBlock; bytes pk; // 256-byte G2 group/encryption pubkey bytes h1; // 128-byte G1 epoch identity bytes h1neg; // 128-byte G1 -h1 bytes32 orderingRoot; // Merkle root over committed execution positions bytes dk; // 128-byte G1 reconstructed decryption key uint256 envelopeCount; uint256 shareCount; uint256 nextExecIndex; // strict sequential execution cursor } mapping(uint64 => Epoch) internal _epochs; mapping(uint64 => mapping(uint16 => bytes)) internal _shareOf; mapping(uint64 => mapping(uint16 => bool)) public shareSubmitted; mapping(uint64 => mapping(bytes32 => bool)) public committedCommitment; mapping(uint64 => mapping(bytes32 => bool)) public executedCommitment; /* ================================ events ============================== */ event CommitteeRegistered(uint16 threshold, uint16 keyperCount); event SequencerSet(address indexed sequencer); event ExecutorSet(address indexed executor); event EpochStarted(uint64 indexed epochId, bytes pk, bytes h1); event Encrypted(uint64 indexed epochId, uint256 indexed index, address indexed sender, bytes32 commitment, bytes ciphertext); event OrderingCommitted(uint64 indexed epochId, bytes32 orderingRoot, uint256 envelopeCount); event ShareSubmitted(uint64 indexed epochId, uint16 indexed keyperIndex, bytes32 shareHash); event EpochFinalized(uint64 indexed epochId, bytes32 dkHash, uint256 shareCount); event Executed(uint64 indexed epochId, uint256 indexed index, bytes32 plaintextHash, bytes32 commitment); event KeyperFlagged(uint64 indexed epochId, uint16 indexed keyperIndex, address indexed keyper); /* ================================ errors ============================== */ error NotCoordinator(); error NotSequencer(); error CommitteeAlreadySet(); error CommitteeNotSet(); error BadArgs(); error BadPointLength(); error EpochExists(); error EpochNotOpen(); error OrderingNotCommitted(); error OrderingAlreadyCommitted(); error NoEnvelopes(); error NotFinalized(); error AlreadyFinalized(); error DuplicateShare(); error BadShare(); // share fails on-chain pairing check error NotEnoughShares(); error BadReconstruction(); // DK fails on-chain pairing check error KeyperIndexRange(); error DuplicateCommitment(); error UnknownCommitment(); error AlreadyExecuted(); error OutOfOrder(); error BadMerkleProof(); error PlaintextMismatch(); error ShareIsConsistent(); /* ============================== modifiers ============================= */ modifier onlyCoordinator() { if (msg.sender != coordinator) revert NotCoordinator(); _; } modifier onlySequencer() { if (msg.sender != sequencer) revert NotSequencer(); _; } /* ============================= constructor ============================ */ constructor(address coordinator_, bytes memory g2Gen) { if (coordinator_ == address(0) || g2Gen.length != 256) revert BadArgs(); coordinator = coordinator_; sequencer = coordinator_; G2_GEN = g2Gen; } /* ============================== admin ops ============================= */ function setSequencer(address s) external onlyCoordinator { sequencer = s; emit SequencerSet(s); } function setExecutor(address e) external onlyCoordinator { executor = e; emit ExecutorSet(e); } /// @notice Register the (t,N) keyper committee with their BLS12-381 G2 /// public keys P_i = s_i*g2 (the on-chain VSS commitments). One-shot. function registerKeypers(address[] calldata addrs, bytes[] calldata pubkeys, uint16 t) external onlyCoordinator { if (committeeSet) revert CommitteeAlreadySet(); uint256 n = addrs.length; if (n == 0 || pubkeys.length != n || t == 0 || t > n || n > type(uint16).max) revert BadArgs(); for (uint256 i = 0; i < n; i++) { if (pubkeys[i].length != 256) revert BadPointLength(); keyperAddr[uint16(i)] = addrs[i]; keyperPub[uint16(i)] = pubkeys[i]; } threshold = t; keyperCount = uint16(n); committeeSet = true; emit CommitteeRegistered(t, uint16(n)); } /* ============================== epoch ops ============================= */ /// @notice Open an epoch by publishing the epoch encryption pubkey `pk` /// (= the committee group key PK), the epoch identity point `h1` /// (= hashToCurve(epoch tag) in G1) and its negation `h1neg`. function startEpoch(uint64 epochId, bytes calldata pk, bytes calldata h1, bytes calldata h1neg) external onlyCoordinator { if (!committeeSet) revert CommitteeNotSet(); if (_epochs[epochId].phase != Phase.None) revert EpochExists(); if (pk.length != 256 || h1.length != 128 || h1neg.length != 128) revert BadPointLength(); Epoch storage e = _epochs[epochId]; e.phase = Phase.Open; e.startBlock = uint64(block.number); e.pk = pk; e.h1 = h1; e.h1neg = h1neg; emit EpochStarted(epochId, pk, h1); } /// @notice Submit an encrypted tx envelope while the epoch is Open. /// `ciphertext` = U(256-byte G2) || C(payload XOR mask); the full /// blob is emitted for off-chain availability, only its binding /// `commitment` = keccak256(abi.encode(plaintext, opening)) is stored. function submitEncrypted(uint64 epochId, bytes calldata ciphertext, bytes32 commitment) external returns (uint256 index) { Epoch storage e = _epochs[epochId]; if (e.phase != Phase.Open) revert EpochNotOpen(); if (commitment == bytes32(0) || ciphertext.length < 256) revert BadArgs(); if (committedCommitment[epochId][commitment]) revert DuplicateCommitment(); committedCommitment[epochId][commitment] = true; index = e.envelopeCount++; emit Encrypted(epochId, index, msg.sender, commitment, ciphertext); } /// @notice Sequencer commits the epoch ordering as a Merkle root over /// execution positions. MUST happen before any decryption share. function commitOrdering(uint64 epochId, bytes32 orderingRoot) external onlySequencer { Epoch storage e = _epochs[epochId]; if (e.phase != Phase.Open) { if (e.phase == Phase.None) revert EpochNotOpen(); revert OrderingAlreadyCommitted(); } if (e.envelopeCount == 0) revert NoEnvelopes(); if (orderingRoot == bytes32(0)) revert BadArgs(); e.orderingRoot = orderingRoot; e.phase = Phase.Ordered; emit OrderingCommitted(epochId, orderingRoot, e.envelopeCount); } /// @notice A keyper (or a relayer on its behalf) posts sig_i = s_i*H1. The /// share is REJECTED unless e(sig_i,g2)==e(H1,P_i) verifies on-chain. /// Reverts before the ordering is committed (prevents early decrypt). function submitDecryptionShare(uint64 epochId, uint16 keyperIndex, bytes calldata share) external { Epoch storage e = _epochs[epochId]; if (e.phase == Phase.Open || e.phase == Phase.None) revert OrderingNotCommitted(); if (keyperIndex >= keyperCount) revert KeyperIndexRange(); if (shareSubmitted[epochId][keyperIndex]) revert DuplicateShare(); if (share.length != 128) revert BadPointLength(); // e(sig_i, g2) * e(-H1, P_i) == 1 if (!_pairingCheck2(share, G2_GEN, e.h1neg, keyperPub[keyperIndex])) revert BadShare(); _shareOf[epochId][keyperIndex] = share; shareSubmitted[epochId][keyperIndex] = true; e.shareCount++; emit ShareSubmitted(epochId, keyperIndex, keccak256(share)); } /// @notice Once >= t valid shares exist, submit the off-chain Lagrange /// reconstruction DK. Accepted only if e(DK,g2)==e(H1,PK) verifies. function finalizeEpoch(uint64 epochId, bytes calldata dk) external { Epoch storage e = _epochs[epochId]; if (e.phase == Phase.Finalized) revert AlreadyFinalized(); if (e.phase != Phase.Ordered) revert OrderingNotCommitted(); if (e.shareCount < threshold) revert NotEnoughShares(); if (dk.length != 128) revert BadPointLength(); // e(DK, g2) * e(-H1, PK) == 1 if (!_pairingCheck2(dk, G2_GEN, e.h1neg, e.pk)) revert BadReconstruction(); e.dk = dk; e.phase = Phase.Finalized; emit EpochFinalized(epochId, keccak256(dk), e.shareCount); } /// @notice Reveal a plaintext at its committed execution position and mark it /// executed. Execution positions MUST be revealed strictly in order. /// Verifies: (a) sequential position, (b) the commitment was actually /// submitted, (c) Merkle membership of (epochId,index,commitment) in /// the committed ordering, (d) the plaintext opens the commitment. function revealAndExecute( uint64 epochId, uint256 index, bytes calldata plaintext, bytes32 opening, bytes32[] calldata proof ) external { Epoch storage e = _epochs[epochId]; if (e.phase != Phase.Finalized) revert NotFinalized(); if (index != e.nextExecIndex) revert OutOfOrder(); bytes32 commitment = keccak256(abi.encode(plaintext, opening)); if (!committedCommitment[epochId][commitment]) revert UnknownCommitment(); if (executedCommitment[epochId][commitment]) revert AlreadyExecuted(); // Merkle leaf = keccak(bytes.concat(keccak(abi.encode(epochId,index,commitment)))) (OZ sorted-pair) bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(epochId, index, commitment)))); if (!MerkleProof.verify(proof, e.orderingRoot, leaf)) revert BadMerkleProof(); executedCommitment[epochId][commitment] = true; e.nextExecIndex = index + 1; emit Executed(epochId, index, keccak256(plaintext), commitment); // Optional: hand the decoded plaintext to a consumer executor. address ex = executor; if (ex != address(0)) { (bool ok, ) = ex.call(abi.encodeWithSignature("execute(uint64,uint256,bytes)", epochId, index, plaintext)); ok; // best-effort; failure of the inner consumer does not roll back the reveal record } } /* ============================ slashing flag =========================== */ /// @notice Flag a keyper who published (off-chain) a share that is /// INCONSISTENT with its on-chain VSS commitment P_i. Reverts if the /// alleged share is actually valid (no false accusations). function reportInconsistentShare(uint64 epochId, uint16 keyperIndex, bytes calldata allegedShare) external { Epoch storage e = _epochs[epochId]; if (e.phase == Phase.None) revert EpochNotOpen(); if (keyperIndex >= keyperCount) revert KeyperIndexRange(); if (allegedShare.length != 128) revert BadPointLength(); if (_pairingCheck2(allegedShare, G2_GEN, e.h1neg, keyperPub[keyperIndex])) revert ShareIsConsistent(); emit KeyperFlagged(epochId, keyperIndex, keyperAddr[keyperIndex]); } /* ============================ pairing helper ========================== */ /// @dev Returns true iff e(a1,b1) * e(a2,b2) == 1 via the EIP-2537 pairing /// precompile at 0x0f. a* are 128-byte G1, b* are 256-byte G2. A revert /// inside the precompile (e.g. a non-subgroup point) yields false. function _pairingCheck2(bytes memory a1, bytes memory b1, bytes memory a2, bytes memory b2) internal view returns (bool) { if (a1.length != 128 || a2.length != 128 || b1.length != 256 || b2.length != 256) revert BadPointLength(); bytes memory input = bytes.concat(a1, b1, a2, b2); // 2 pairs * 384 = 768 bytes (bool ok, bytes memory out) = BLS_PAIRING.staticcall(input); if (!ok || out.length != 32) return false; return abi.decode(out, (uint256)) == 1; } /* =============================== views =============================== */ function getEpoch(uint64 epochId) external view returns (Phase phase, uint64 startBlock, bytes32 orderingRoot, uint256 envelopeCount, uint256 shareCount, uint256 nextExecIndex) { Epoch storage e = _epochs[epochId]; return (e.phase, e.startBlock, e.orderingRoot, e.envelopeCount, e.shareCount, e.nextExecIndex); } function epochPk(uint64 epochId) external view returns (bytes memory) { return _epochs[epochId].pk; } function epochH1(uint64 epochId) external view returns (bytes memory) { return _epochs[epochId].h1; } function epochDk(uint64 epochId) external view returns (bytes memory) { return _epochs[epochId].dk; } function shareOf(uint64 epochId, uint16 i) external view returns (bytes memory) { return _shareOf[epochId][i]; } function isFinalized(uint64 epochId) external view returns (bool) { return _epochs[epochId].phase == Phase.Finalized; } function verifyShareView(uint64 epochId, uint16 keyperIndex, bytes calldata share) external view returns (bool) { if (keyperIndex >= keyperCount || share.length != 128) return false; return _pairingCheck2(share, G2_GEN, _epochs[epochId].h1neg, keyperPub[keyperIndex]); } }