aere-contracts/contracts/mempool/AereShutterMempoolV2.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

339 lines
18 KiB
Solidity

// 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]);
}
}