aere-contracts/contracts/interop/AereEthMessageInbox.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

206 lines
8.1 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import "./lib/MerklePatriciaProof.sol";
import "./lib/RLPReader.sol";
interface IAereEthLightClient {
function verifyReceiptsRootAgainstFinalized(
bytes32 receiptsRoot,
bytes32 bodyRoot,
bytes32[] calldata branch,
uint256 depth,
uint256 subtreeIndex
) external view returns (bool);
function finalizedSlot() external view returns (uint64);
}
/**
* @title AereEthMessageInbox — deliver an Ethereum event to AERE, trust-minimized.
*
* @notice A thin inbox over {AereEthLightClient}. It accepts (proof, message) and
* delivers a message ONLY if:
* 1. the light client attests `receiptsRoot` is committed under the current
* FINALIZED Ethereum beacon header (execution-payload SSZ branch), and
* 2. the referenced transaction receipt is included in that `receiptsRoot`
* via a Merkle-Patricia proof (key = rlp(txIndex)), and
* 3. the chosen log inside that receipt was emitted by the registered source
* contract on Ethereum.
* Each (receiptsRoot, txIndex, logIndex) is delivered at most once.
*
* The message payload is the log itself: (emitter, topics, data). No bridge
* operator can inject a message; forging one requires forging Ethereum
* sync-committee finality OR a keccak/MPT collision.
*
* @dev SECURITY. Inherits the light client's model: >= 2/3 of Ethereum's 512-member
* sync committee (classical BLS12-381, quantum-vulnerable). This is Ethereum's
* trust model, not AERE's. See AereEthLightClient for the full statement.
*
* The execution-payload generalized index (depth, subtreeIndex) locating
* receipts_root under the beacon body root is FORK-DEPENDENT and supplied by
* the caller; the inbox pins the expected values it was configured with so a
* caller cannot point the branch at the wrong field.
*/
contract AereEthMessageInbox {
using RLPReader for bytes;
IAereEthLightClient public immutable lightClient;
/// @notice Registered Ethereum source contract whose logs this inbox accepts.
address public immutable sourceContract;
/// @notice Pinned execution-payload branch shape (receipts_root under body_root).
uint256 public immutable execBranchDepth;
uint256 public immutable execBranchIndex;
mapping(bytes32 => bool) public delivered;
event MessageDelivered(
bytes32 indexed receiptsRoot,
uint256 indexed txIndex,
uint256 indexed logIndex,
address emitter,
bytes32[] topics,
bytes data
);
constructor(
address lightClient_,
address sourceContract_,
uint256 execBranchDepth_,
uint256 execBranchIndex_
) {
require(lightClient_ != address(0), "Inbox:zero lc");
lightClient = IAereEthLightClient(lightClient_);
sourceContract = sourceContract_;
execBranchDepth = execBranchDepth_;
execBranchIndex = execBranchIndex_;
}
struct ReceiptProof {
bytes32 receiptsRoot;
bytes32 finalizedBodyRoot; // the beacon body root the light client finalized
bytes32[] execBranch; // receipts_root -> finalized body_root
uint256 txIndex; // transaction index in the block
bytes[] mptNodes; // receipts-trie nodes along rlp(txIndex)
uint256 logIndex; // which log within the receipt to deliver
}
/// @notice Verify and deliver the log referenced by `p`. Reverts on any failure.
/// @return emitter the log's emitting address (== sourceContract)
/// @return topics the log topics
/// @return data the log data
function deliver(ReceiptProof calldata p)
external
returns (address emitter, bytes32[] memory topics, bytes memory data)
{
// 1. finality: receiptsRoot committed under the finalized beacon body.
require(
lightClient.verifyReceiptsRootAgainstFinalized(
p.receiptsRoot, p.finalizedBodyRoot, p.execBranch, execBranchDepth, execBranchIndex
),
"Inbox:not finalized"
);
// 2. inclusion: the receipt is in receiptsRoot at key rlp(txIndex).
bytes memory key = _rlpUint(p.txIndex);
(bool included, bytes memory receiptValue) =
MerklePatriciaProof.verify(p.receiptsRoot, key, p.mptNodes);
require(included, "Inbox:receipt not included");
// 3. extract the chosen log and check the emitter.
(emitter, topics, data) = _parseLog(receiptValue, p.logIndex);
require(emitter == sourceContract, "Inbox:wrong source");
// 4. replay protection.
bytes32 id = keccak256(abi.encodePacked(p.receiptsRoot, p.txIndex, p.logIndex));
require(!delivered[id], "Inbox:already delivered");
delivered[id] = true;
emit MessageDelivered(p.receiptsRoot, p.txIndex, p.logIndex, emitter, topics, data);
}
// -------------------------------------------------------------------------
// receipt / log parsing
// -------------------------------------------------------------------------
/// @notice Parse the `logIndex`-th log out of an EIP-2718 receipt value.
/// Handles both legacy (rlp list) and typed (type byte || rlp list) receipts.
function parseLog(bytes memory receiptValue, uint256 logIndex)
external
pure
returns (address emitter, bytes32[] memory topics, bytes memory data)
{
return _parseLog(receiptValue, logIndex);
}
function _parseLog(bytes memory receiptValue, uint256 logIndex)
internal
pure
returns (address emitter, bytes32[] memory topics, bytes memory data)
{
// Strip the EIP-2718 type prefix if present (first byte < 0xc0 => typed).
bytes memory rlpList = receiptValue;
if (receiptValue.length != 0 && uint8(receiptValue[0]) < 0xc0) {
rlpList = new bytes(receiptValue.length - 1);
for (uint256 i = 0; i < rlpList.length; i++) rlpList[i] = receiptValue[i + 1];
}
// receipt = [status, cumulativeGasUsed, logsBloom, logs]
bytes[] memory fields = rlpList.splitList();
require(fields.length == 4, "Inbox:bad receipt");
bytes[] memory logs = fields[3].splitList();
require(logIndex < logs.length, "Inbox:log index oob");
// log = [address, topics[], data]
bytes[] memory log = logs[logIndex].splitList();
require(log.length == 3, "Inbox:bad log");
emitter = _toAddress(log[0].readBytes());
bytes[] memory rawTopics = log[1].splitList();
topics = new bytes32[](rawTopics.length);
for (uint256 i = 0; i < rawTopics.length; i++) {
topics[i] = _toBytes32(rawTopics[i].readBytes());
}
data = log[2].readBytes();
}
// -------------------------------------------------------------------------
// helpers
// -------------------------------------------------------------------------
/// @dev RLP-encode a non-negative integer (transaction index key).
function _rlpUint(uint256 v) internal pure returns (bytes memory) {
if (v == 0) return hex"80";
// minimal big-endian bytes
uint256 len = 0;
uint256 t = v;
while (t != 0) {
len++;
t >>= 8;
}
bytes memory be = new bytes(len);
t = v;
for (uint256 i = 0; i < len; i++) {
be[len - 1 - i] = bytes1(uint8(t & 0xff));
t >>= 8;
}
if (len == 1 && uint8(be[0]) < 0x80) {
return be; // single small byte encodes as itself
}
return bytes.concat(bytes1(uint8(0x80 + len)), be);
}
function _toAddress(bytes memory b) internal pure returns (address a) {
require(b.length == 20, "Inbox:addr len");
assembly {
a := shr(96, mload(add(b, 32)))
}
}
function _toBytes32(bytes memory b) internal pure returns (bytes32 r) {
require(b.length == 32, "Inbox:b32 len");
assembly {
r := mload(add(b, 32))
}
}
}