aere-contracts/contracts/raas/AereDACommittee.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

251 lines
9.3 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
/**
* @title AereDACommittee, M-of-N data availability attestation committee (validium-grade)
* @notice A data availability committee for AERE rollups / validiums. N member
* keys are registered; committee members sign availability attestations
* off-chain over (rollupId, epoch, dataCommitment); the contract records
* an (rollupId, epoch) as data-available only when at least M distinct
* registered members present valid ECDSA signatures over that tuple.
*
* Reuses the ECDSA-recover attestation pattern already used by the AERE
* RaaS contracts (AereRaaSFactory exit attestations): keccak256 of the
* encoded tuple, wrapped in the EIP-191 personal-sign prefix, ecrecovered
* against the registered member set.
*
* @dev HONEST SCOPE. This is VALIDIUM-GRADE data availability: a TRUSTED
* committee attests that data is available. It is NOT trustless data
* availability sampling (DAS) and provides NO cryptographic guarantee
* that the data actually exists off-chain. Availability is exactly as
* trustworthy as the committee is honest and live. It is "as centralized
* as the committee."
*
* There is NO admin backdoor to forge availability: the ONLY path that
* can set an epoch available is `attest`, which strictly requires >= M
* distinct registered-member signatures. The `admin` role can only manage
* committee membership / threshold (registrar duties, intended to be the
* Foundation multisig in production) and can be permanently frozen; it can
* never mark data available without member signatures.
*/
contract AereDACommittee {
/* --------------------------------- roles -------------------------------- */
/// @notice Committee registrar. Can add/remove members and set the threshold,
/// or freeze membership permanently. Cannot forge availability.
address public admin;
/// @notice Once true, membership and threshold are immutable forever.
bool public frozen;
/* -------------------------------- committee ----------------------------- */
uint256 public threshold; // M
uint256 public memberCount; // N
mapping(address => bool) public isMember;
address[] public members;
/* ------------------------------- attestations --------------------------- */
struct Attestation {
bool available;
bytes32 dataCommitment;
uint64 attestedAt;
uint256 signatures;
}
// rollupId => epoch => Attestation
mapping(bytes32 => mapping(uint256 => Attestation)) private _att;
/* --------------------------------- events ------------------------------- */
event AdminTransferred(address indexed prev, address indexed next);
event MemberAdded(address indexed member);
event MemberRemoved(address indexed member);
event ThresholdSet(uint256 threshold);
event Frozen();
event DataAvailable(
bytes32 indexed rollupId,
uint256 indexed epoch,
bytes32 dataCommitment,
uint256 signatures
);
/* --------------------------------- errors ------------------------------- */
error NotAdmin();
error IsFrozen();
error ZeroAddress();
error AlreadyMember();
error NotMember();
error BadThreshold();
error AlreadyRecorded();
error ThresholdNotMet(uint256 got, uint256 need);
error UnsortedOrDuplicate();
error NotACommitteeMember(address recovered);
modifier onlyAdmin() {
if (msg.sender != admin) revert NotAdmin();
_;
}
/* ----------------------------- constructor ------------------------------ */
constructor(address admin_, address[] memory members_, uint256 threshold_) {
if (admin_ == address(0)) revert ZeroAddress();
admin = admin_;
emit AdminTransferred(address(0), admin_);
for (uint256 i = 0; i < members_.length; i++) {
address m = members_[i];
if (m == address(0)) revert ZeroAddress();
if (isMember[m]) revert AlreadyMember();
isMember[m] = true;
members.push(m);
emit MemberAdded(m);
}
memberCount = members_.length;
if (threshold_ == 0 || threshold_ > memberCount) revert BadThreshold();
threshold = threshold_;
emit ThresholdSet(threshold_);
}
/* ------------------------ committee governance -------------------------- */
/* NOTE: none of these can mark data available. They only manage the */
/* member set / threshold, and are disabled forever once frozen. */
function addMember(address m) external onlyAdmin {
if (frozen) revert IsFrozen();
if (m == address(0)) revert ZeroAddress();
if (isMember[m]) revert AlreadyMember();
isMember[m] = true;
members.push(m);
memberCount++;
emit MemberAdded(m);
}
function removeMember(address m) external onlyAdmin {
if (frozen) revert IsFrozen();
if (!isMember[m]) revert NotMember();
isMember[m] = false;
uint256 n = members.length;
for (uint256 i = 0; i < n; i++) {
if (members[i] == m) {
members[i] = members[n - 1];
members.pop();
break;
}
}
memberCount--;
if (threshold > memberCount) threshold = memberCount; // keep threshold valid
emit MemberRemoved(m);
}
function setThreshold(uint256 t) external onlyAdmin {
if (frozen) revert IsFrozen();
if (t == 0 || t > memberCount) revert BadThreshold();
threshold = t;
emit ThresholdSet(t);
}
function transferAdmin(address next) external onlyAdmin {
if (next == address(0)) revert ZeroAddress();
emit AdminTransferred(admin, next);
admin = next;
}
/// @notice Permanently renounce all membership/threshold control. After this,
/// the committee is immutable and only `attest` can act.
function freeze() external onlyAdmin {
frozen = true;
emit Frozen();
}
/* ------------------------------ attestation ----------------------------- */
/// @notice The EIP-191 personal-sign digest committee members sign off-chain.
function attestationDigest(
bytes32 rollupId,
uint256 epoch,
bytes32 dataCommitment
) public view returns (bytes32) {
bytes32 h = keccak256(
abi.encode("AereDACommittee:attest", block.chainid, address(this), rollupId, epoch, dataCommitment)
);
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", h));
}
/// @notice Record (rollupId, epoch) as data-available. The ONLY path to
/// availability. Requires >= M distinct registered-member signatures
/// over (rollupId, epoch, dataCommitment). Signatures MUST be ordered
/// by strictly ascending recovered signer address, which guarantees
/// distinctness without an in-memory set.
function attest(
bytes32 rollupId,
uint256 epoch,
bytes32 dataCommitment,
bytes[] calldata sigs
) external returns (bool) {
Attestation storage a = _att[rollupId][epoch];
if (a.available) revert AlreadyRecorded();
bytes32 digest = attestationDigest(rollupId, epoch, dataCommitment);
uint256 valid;
address last = address(0);
for (uint256 i = 0; i < sigs.length; i++) {
address rec = _recover(digest, sigs[i]);
if (rec == address(0) || !isMember[rec]) revert NotACommitteeMember(rec);
if (rec <= last) revert UnsortedOrDuplicate(); // strictly ascending => distinct
last = rec;
valid++;
}
if (valid < threshold) revert ThresholdNotMet(valid, threshold);
a.available = true;
a.dataCommitment = dataCommitment;
a.attestedAt = uint64(block.timestamp);
a.signatures = valid;
emit DataAvailable(rollupId, epoch, dataCommitment, valid);
return true;
}
/* ---------------------------------- views ------------------------------- */
function isAvailable(bytes32 rollupId, uint256 epoch) external view returns (bool) {
return _att[rollupId][epoch].available;
}
function getAttestation(bytes32 rollupId, uint256 epoch)
external
view
returns (bool available, bytes32 dataCommitment, uint64 attestedAt, uint256 signatures)
{
Attestation storage a = _att[rollupId][epoch];
return (a.available, a.dataCommitment, a.attestedAt, a.signatures);
}
function getMembers() external view returns (address[] memory) {
return members;
}
/* ------------------------------- internal ------------------------------- */
function _recover(bytes32 hash, bytes calldata sig) internal pure returns (address) {
if (sig.length != 65) return address(0);
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := calldataload(sig.offset)
s := calldataload(add(sig.offset, 32))
v := byte(0, calldataload(add(sig.offset, 64)))
}
if (v < 27) v += 27;
if (v != 27 && v != 28) return address(0);
return ecrecover(hash, v, r, s);
}
}