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.
442 lines
25 KiB
Solidity
442 lines
25 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
|
|
|
|
/**
|
|
* @title AereShutterMempoolV3 - 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.
|
|
*
|
|
* V3 is byte-for-byte the V2 threshold-BLS design (same 3-of-5 committee,
|
|
* same commitment binding, same out-of-order reject, same no-early-decrypt
|
|
* guarantee) PLUS one liveness fix. See "V3 LIVENESS FIX" below.
|
|
*
|
|
* ------------------------------------------------------------------------
|
|
* V3 LIVENESS FIX (audit finding: reveal-timeout DoS)
|
|
* ------------------------------------------------------------------------
|
|
* PROBLEM in V2: revealAndExecute requires index == nextExecIndex and the
|
|
* cursor only advances on a SUCCESSFUL reveal. A single committed position
|
|
* whose plaintext/opening is never revealed (griefed, lost, or malformed)
|
|
* therefore stalls EVERY later position in that epoch forever. There is no
|
|
* skip and no timeout, so one stuck position denies liveness to the rest of
|
|
* the ordering.
|
|
*
|
|
* FIX in V3:
|
|
* 1. finalizeEpoch stamps a per-epoch reveal DEADLINE:
|
|
* revealDeadline = block.timestamp + REVEAL_WINDOW.
|
|
* The whole ordering has until this deadline to be revealed in order.
|
|
* 2. skipUnrevealed(epochId, index) advances nextExecIndex PAST the current
|
|
* head position, but ONLY when
|
|
* (a) the epoch is Finalized,
|
|
* (b) index == nextExecIndex (skip strictly at the head),
|
|
* (c) index < envelopeCount (there is a position to skip),
|
|
* (d) block.timestamp > revealDeadline (the window has closed).
|
|
* The skip is recorded (skipped[epochId][index] = true) and emits
|
|
* PositionSkipped, so later positions can execute.
|
|
*
|
|
* Because skip only ever acts on the head (index == nextExecIndex) and a
|
|
* successful revealAndExecute advances the head atomically, the head is by
|
|
* construction the still-unrevealed position: a position that WAS revealed is
|
|
* already behind the cursor, so skipping it reverts OutOfOrder. Revealing is
|
|
* never blocked by the deadline: revealAndExecute keeps working after the
|
|
* window closes, so a late-but-honest opener can still land its position; the
|
|
* skip is only an escape hatch for a position that nobody reveals.
|
|
*
|
|
* PERMISSIONLESS REVEAL REQUIREMENT (protocol invariant):
|
|
* The ciphertext MUST embed its own opening, i.e. the encrypted blob decrypts
|
|
* (under the epoch decryption key DK) to (plaintext || opening) such that
|
|
* keccak256(abi.encode(plaintext, opening)) == the committed commitment.
|
|
* This makes reveal PERMISSIONLESS: once DK is reconstructed, ANY holder of
|
|
* the decryption key can open ANY committed position and call
|
|
* revealAndExecute. A well-formed position is therefore always revealable by
|
|
* anyone, so skipUnrevealed only ever fires on a position whose opening was
|
|
* genuinely withheld/lost/malformed, never to censor a revealable tx.
|
|
* ------------------------------------------------------------------------
|
|
*
|
|
* Unlike the V0 orchestration contract (which used a keccak placeholder
|
|
* for the "decryption key"), V2/V3 verify 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. After the reveal deadline, an unrevealed
|
|
* head position may be skipped (V3) so liveness never stalls.
|
|
*
|
|
* 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.
|
|
* - The reveal window trades a little worst-case latency (a stuck position is
|
|
* only skippable after REVEAL_WINDOW) for guaranteed liveness. Since every
|
|
* well-formed position is permissionlessly revealable (see the invariant
|
|
* above), the window only ever elapses for genuinely un-openable positions.
|
|
*/
|
|
contract AereShutterMempoolV3 {
|
|
/* ================================ 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
|
|
|
|
/* =============================== timeout ============================== */
|
|
/// @notice Seconds after finalizeEpoch during which the committed ordering
|
|
/// must be revealed strictly in order. Once elapsed, an unrevealed
|
|
/// head position becomes skippable via skipUnrevealed. Immutable.
|
|
uint64 public immutable REVEAL_WINDOW;
|
|
|
|
/* =============================== 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
|
|
uint64 revealDeadline; // V3: block.timestamp deadline set at finalize
|
|
}
|
|
|
|
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;
|
|
/// @notice V3: epochId => position index => was skipped past (unrevealed).
|
|
mapping(uint64 => mapping(uint256 => bool)) public skippedPosition;
|
|
|
|
/* ================================ 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, uint64 revealDeadline);
|
|
event Executed(uint64 indexed epochId, uint256 indexed index, bytes32 plaintextHash, bytes32 commitment);
|
|
event PositionSkipped(uint64 indexed epochId, uint256 indexed index, uint64 deadline, address caller);
|
|
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();
|
|
error SkipTooEarly(); // V3: reveal window has not elapsed yet
|
|
error NothingToSkip(); // V3: nextExecIndex is already past the last position
|
|
|
|
/* ============================== modifiers ============================= */
|
|
modifier onlyCoordinator() { if (msg.sender != coordinator) revert NotCoordinator(); _; }
|
|
modifier onlySequencer() { if (msg.sender != sequencer) revert NotSequencer(); _; }
|
|
|
|
/* ============================= constructor ============================ */
|
|
/// @param coordinator_ committee dealer / admin (also the initial sequencer).
|
|
/// @param g2Gen canonical BLS12-381 G2 generator (256-byte EIP-2537).
|
|
/// @param revealWindow_ seconds after finalize before an unrevealed head
|
|
/// position may be skipped. MUST be > 0.
|
|
constructor(address coordinator_, bytes memory g2Gen, uint64 revealWindow_) {
|
|
if (coordinator_ == address(0) || g2Gen.length != 256 || revealWindow_ == 0) revert BadArgs();
|
|
coordinator = coordinator_;
|
|
sequencer = coordinator_;
|
|
G2_GEN = g2Gen;
|
|
REVEAL_WINDOW = revealWindow_;
|
|
}
|
|
|
|
/* ============================== 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.
|
|
/// @dev PERMISSIONLESS-REVEAL INVARIANT: the plaintext payload MUST itself
|
|
/// carry the `opening`, i.e. the ciphertext decrypts under DK to
|
|
/// (plaintext || opening) that reproduces `commitment`. Then any DK
|
|
/// holder can open this position and call revealAndExecute, so a
|
|
/// well-formed position is always revealable and never wrongly skipped.
|
|
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.
|
|
/// V3: stamps the reveal deadline = block.timestamp + REVEAL_WINDOW.
|
|
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;
|
|
uint64 deadline = uint64(block.timestamp) + REVEAL_WINDOW;
|
|
e.revealDeadline = deadline;
|
|
emit EpochFinalized(epochId, keccak256(dk), e.shareCount, deadline);
|
|
}
|
|
|
|
/// @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.
|
|
/// NOTE: reveal is never gated by the deadline; a late-but-honest
|
|
/// opener can still land its position after the window closes.
|
|
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
|
|
}
|
|
}
|
|
|
|
/// @notice V3 LIVENESS FIX. Skip the current head position when it was never
|
|
/// revealed and its reveal window has closed, so the epoch's later
|
|
/// positions can execute. Permissionless: anyone may call it once the
|
|
/// conditions hold.
|
|
/// @param epochId the finalized epoch.
|
|
/// @param index MUST equal nextExecIndex (skip strictly at the head). This
|
|
/// guarantees the position is still unrevealed: a revealed position is
|
|
/// already behind the cursor, so skipping it reverts OutOfOrder.
|
|
/// @dev Reverts unless: epoch Finalized (NotFinalized); index == nextExecIndex
|
|
/// (OutOfOrder); index < envelopeCount (NothingToSkip); and the reveal
|
|
/// deadline has passed (SkipTooEarly). Records skippedPosition and emits
|
|
/// PositionSkipped, then advances nextExecIndex by one.
|
|
function skipUnrevealed(uint64 epochId, uint256 index) external {
|
|
Epoch storage e = _epochs[epochId];
|
|
if (e.phase != Phase.Finalized) revert NotFinalized();
|
|
if (index != e.nextExecIndex) revert OutOfOrder();
|
|
if (index >= e.envelopeCount) revert NothingToSkip();
|
|
if (block.timestamp <= e.revealDeadline) revert SkipTooEarly();
|
|
|
|
skippedPosition[epochId][index] = true;
|
|
e.nextExecIndex = index + 1;
|
|
emit PositionSkipped(epochId, index, e.revealDeadline, msg.sender);
|
|
}
|
|
|
|
/* ============================ 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, uint64 revealDeadline)
|
|
{
|
|
Epoch storage e = _epochs[epochId];
|
|
return (e.phase, e.startBlock, e.orderingRoot, e.envelopeCount, e.shareCount, e.nextExecIndex, e.revealDeadline);
|
|
}
|
|
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 revealDeadlineOf(uint64 epochId) external view returns (uint64) { return _epochs[epochId].revealDeadline; }
|
|
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]);
|
|
}
|
|
}
|