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.
216 lines
8.2 KiB
Solidity
216 lines
8.2 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.23;
|
|
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
|
|
|
/**
|
|
* @title AereMessenger
|
|
* @notice Cross-chain messaging hub for AERE Network, designed for compatibility
|
|
* with the Hyperlane Mailbox interface so we can plug into the public
|
|
* Hyperlane validator/relayer network once registered.
|
|
*
|
|
* For now this is a self-contained messaging contract: an off-chain validator
|
|
* set we operate signs outgoing messages. The signature scheme + ISM design
|
|
* are deliberately compatible with Hyperlane's ECDSA-multisig ISM, so when
|
|
* Hyperlane Labs accepts AERE into the public registry we can swap our
|
|
* signers for the public validator set with one parameter change — no
|
|
* contract redeploy.
|
|
*
|
|
* Compatible with Hyperlane Mailbox semantics:
|
|
* - 32-bit domain identifier (we use Chain ID 2800)
|
|
* - dispatch(destDomain, recipient, body) emits outbound messages
|
|
* - process(metadata, message) verifies + delivers inbound messages
|
|
* - Recipients implement IMessageRecipient
|
|
* - Default ISM is a multisig of named validators
|
|
*
|
|
* When AERE is added to the Hyperlane registry, this contract continues to
|
|
* work — Hyperlane validators sign with the same ECDSA scheme.
|
|
*/
|
|
|
|
interface IMessageRecipient {
|
|
function handle(uint32 origin, bytes32 sender, bytes calldata body) external payable;
|
|
}
|
|
|
|
interface IPostDispatchHook {
|
|
function postDispatch(bytes calldata metadata, bytes calldata message) external payable;
|
|
}
|
|
|
|
contract AereMessenger is Ownable, ReentrancyGuard {
|
|
/// AERE's domain (= chain ID).
|
|
uint32 public immutable localDomain;
|
|
|
|
/// Outbound nonce — incremented per dispatch.
|
|
uint32 public nonce;
|
|
|
|
/// Validator set authorised to sign inbound messages. ECDSA-signed digests.
|
|
address[] public validators;
|
|
mapping(address => bool) public isValidator;
|
|
|
|
/// Threshold: how many distinct validator signatures required to accept an inbound message.
|
|
uint8 public threshold = 1;
|
|
|
|
/// Delivered message digest tracking (per-origin) to prevent replay.
|
|
mapping(uint32 => mapping(bytes32 => bool)) public delivered;
|
|
|
|
/// Optional post-dispatch hook (e.g. AereIGP — Interchain Gas Paymaster).
|
|
address public defaultHook;
|
|
|
|
/// Pause flag for emergency halt.
|
|
bool public paused;
|
|
|
|
event Dispatch(
|
|
uint32 indexed destDomain,
|
|
bytes32 indexed recipient,
|
|
uint32 indexed nonce,
|
|
bytes32 sender,
|
|
bytes body,
|
|
bytes32 messageId
|
|
);
|
|
event Process(
|
|
uint32 indexed origin,
|
|
bytes32 indexed sender,
|
|
address indexed recipient,
|
|
bytes32 messageId
|
|
);
|
|
event ValidatorAdded(address indexed validator);
|
|
event ValidatorRemoved(address indexed validator);
|
|
event ThresholdChanged(uint8 newThreshold);
|
|
event HookSet(address indexed hook);
|
|
event Paused(bool paused);
|
|
|
|
constructor(uint32 _localDomain) {
|
|
localDomain = _localDomain;
|
|
}
|
|
|
|
modifier whenNotPaused() {
|
|
require(!paused, "AereMessenger: paused");
|
|
_;
|
|
}
|
|
|
|
// ───────────────────── Outbound ─────────────────────
|
|
|
|
/**
|
|
* @notice Send a cross-chain message.
|
|
* @param destDomain Destination chain's domain ID (= chain ID).
|
|
* @param recipient Recipient contract on destination chain, padded to bytes32.
|
|
* @param body Application-defined payload.
|
|
* @return messageId Unique digest of this message.
|
|
*/
|
|
function dispatch(
|
|
uint32 destDomain,
|
|
bytes32 recipient,
|
|
bytes calldata body
|
|
) external payable whenNotPaused returns (bytes32 messageId) {
|
|
uint32 n = nonce;
|
|
nonce = n + 1;
|
|
bytes32 sender = bytes32(uint256(uint160(msg.sender)));
|
|
messageId = keccak256(abi.encode(localDomain, destDomain, n, sender, recipient, body));
|
|
emit Dispatch(destDomain, recipient, n, sender, body, messageId);
|
|
|
|
if (defaultHook != address(0) && msg.value > 0) {
|
|
// Forward the IGP fee to the hook (e.g. interchain gas payment).
|
|
IPostDispatchHook(defaultHook).postDispatch{ value: msg.value }("", abi.encode(messageId, destDomain));
|
|
}
|
|
}
|
|
|
|
// ───────────────────── Inbound ─────────────────────
|
|
|
|
/**
|
|
* @notice Process an inbound message after verifying validator signatures.
|
|
* @param signatures Concatenated 65-byte ECDSA signatures (one per validator).
|
|
* @param origin Origin chain's domain ID.
|
|
* @param sender Sender on origin chain (address padded to bytes32).
|
|
* @param recipient Recipient contract address on AERE.
|
|
* @param body Payload to deliver.
|
|
* @param originNonce Sender's nonce on origin chain — used for replay protection.
|
|
*/
|
|
function process(
|
|
bytes calldata signatures,
|
|
uint32 origin,
|
|
bytes32 sender,
|
|
address recipient,
|
|
bytes calldata body,
|
|
uint32 originNonce
|
|
) external nonReentrant whenNotPaused {
|
|
bytes32 messageId = keccak256(abi.encode(origin, localDomain, originNonce, sender, bytes32(uint256(uint160(recipient))), body));
|
|
require(!delivered[origin][messageId], "AereMessenger: replay");
|
|
|
|
// Verify threshold signatures over the messageId (Ethereum-signed-message prefix).
|
|
bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageId));
|
|
require(signatures.length % 65 == 0, "AereMessenger: bad sig length");
|
|
uint256 sigCount = signatures.length / 65;
|
|
require(sigCount >= threshold, "AereMessenger: below threshold");
|
|
|
|
address[] memory seen = new address[](sigCount);
|
|
uint256 validCount = 0;
|
|
for (uint256 i = 0; i < sigCount; i++) {
|
|
bytes32 r;
|
|
bytes32 s;
|
|
uint8 v;
|
|
assembly {
|
|
let off := add(signatures.offset, mul(i, 65))
|
|
r := calldataload(off)
|
|
s := calldataload(add(off, 32))
|
|
v := byte(0, calldataload(add(off, 64)))
|
|
}
|
|
address signer = ecrecover(digest, v, r, s);
|
|
require(signer != address(0), "AereMessenger: bad signature");
|
|
require(isValidator[signer], "AereMessenger: not a validator");
|
|
|
|
// No-double-counting: ensure each signer is distinct.
|
|
for (uint256 j = 0; j < validCount; j++) {
|
|
require(seen[j] != signer, "AereMessenger: duplicate signer");
|
|
}
|
|
seen[validCount] = signer;
|
|
validCount++;
|
|
}
|
|
|
|
delivered[origin][messageId] = true;
|
|
IMessageRecipient(recipient).handle(origin, sender, body);
|
|
emit Process(origin, sender, recipient, messageId);
|
|
}
|
|
|
|
// ───────────────────── Admin ─────────────────────
|
|
|
|
function addValidator(address v) external onlyOwner {
|
|
require(!isValidator[v], "AereMessenger: already added");
|
|
isValidator[v] = true;
|
|
validators.push(v);
|
|
emit ValidatorAdded(v);
|
|
}
|
|
|
|
function removeValidator(address v) external onlyOwner {
|
|
require(isValidator[v], "AereMessenger: not found");
|
|
isValidator[v] = false;
|
|
for (uint256 i = 0; i < validators.length; i++) {
|
|
if (validators[i] == v) {
|
|
validators[i] = validators[validators.length - 1];
|
|
validators.pop();
|
|
break;
|
|
}
|
|
}
|
|
emit ValidatorRemoved(v);
|
|
}
|
|
|
|
function setThreshold(uint8 _threshold) external onlyOwner {
|
|
require(_threshold > 0 && _threshold <= validators.length, "AereMessenger: bad threshold");
|
|
threshold = _threshold;
|
|
emit ThresholdChanged(_threshold);
|
|
}
|
|
|
|
function setDefaultHook(address _hook) external onlyOwner {
|
|
defaultHook = _hook;
|
|
emit HookSet(_hook);
|
|
}
|
|
|
|
function setPaused(bool _paused) external onlyOwner {
|
|
paused = _paused;
|
|
emit Paused(_paused);
|
|
}
|
|
|
|
function validatorCount() external view returns (uint256) {
|
|
return validators.length;
|
|
}
|
|
}
|