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.
266 lines
12 KiB
Solidity
266 lines
12 KiB
Solidity
// 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);
|
|
}
|
|
}
|