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.
206 lines
8.1 KiB
Solidity
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))
|
|
}
|
|
}
|
|
}
|