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.
302 lines
14 KiB
Solidity
302 lines
14 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
|
|
|
/**
|
|
* @title AereConfidentialCompute, on-chain verifier for the AERE threshold-MPC
|
|
* confidential-compute layer (chain 2800).
|
|
*
|
|
* @notice HONEST SCOPE. This contract does NOT run any MPC. The confidential
|
|
* computation happens OFF chain, in a committee of reference nodes that
|
|
* jointly evaluate an agreed arithmetic circuit over the parties' PRIVATE
|
|
* inputs using real Shamir secret sharing and a real BGW multiplication
|
|
* gate (see foundation-ops/mpc-committee). No single node, and no
|
|
* colluding minority below the reconstruction threshold, ever learns any
|
|
* party's input; only the agreed public outputs are revealed. This
|
|
* contract's job is narrow and verifiable: it records a session's public
|
|
* outputs only if a quorum of the REGISTERED committee members has
|
|
* ECDSA-signed the canonical result. The chain does NOT re-execute the
|
|
* MPC and does NOT verify the confidentiality of the inputs; the
|
|
* confidentiality is provided by the secret sharing off chain. The chain
|
|
* verifies committee agreement on the output.
|
|
*
|
|
* THREAT MODEL of the off-chain MPC: SEMI-HONEST (honest but curious),
|
|
* secure against a colluding minority of nodes. It is NOT malicious
|
|
* secure: there is no SPDX-MAC / cheater detection, so a deviating node
|
|
* is not cryptographically caught by the protocol. The on-chain quorum of
|
|
* signatures is the trust anchor: a result is accepted only if at least
|
|
* `signatureThreshold` distinct registered members signed it.
|
|
*
|
|
* CENTRALIZATION. The committee is Foundation-run initially, so it is as
|
|
* centralized as AERE consensus is today, and it decentralizes as the
|
|
* committee grows. The owner can register or remove committee signing
|
|
* keys and set the quorum threshold. The owner CANNOT forge a session
|
|
* result: there is no function that records outputs without a real quorum
|
|
* of committee signatures, and the owner cannot produce those signatures
|
|
* for keys it does not hold. Once recorded, a session result is immutable.
|
|
*
|
|
* SUPPORTED CIRCUITS. This is a confidential-compute PRIMITIVE (private
|
|
* arithmetic tally over a fixed agreed circuit, for example a confidential
|
|
* quadratic-voting / funding tally that reveals only sum(v) and sum(v^2)).
|
|
* It is NOT a general-purpose confidential VM or fhEVM. `circuitId` binds
|
|
* the signed result to the exact circuit that was computed.
|
|
*
|
|
* TEE PATH NOT BUILT. A hardware-attested (SEV-SNP / TDX / SGX) execution
|
|
* path was intentionally not built: the AERE server is an AMD EPYC-Rome
|
|
* cloud guest with no usable trusted-execution attestation. The security
|
|
* here is pure-software secret-sharing MPC, which needs no trusted
|
|
* hardware.
|
|
*/
|
|
contract AereConfidentialCompute {
|
|
using ECDSA for bytes32;
|
|
|
|
// ----- ownership -----
|
|
address public owner;
|
|
|
|
// ----- committee registry -----
|
|
uint256 public committeeCount;
|
|
mapping(address => bool) public isCommittee;
|
|
address[] private committeeList;
|
|
|
|
// Minimum number of distinct registered committee signatures required to
|
|
// finalize a session. Set by the owner; must satisfy 1 <= threshold <= count.
|
|
uint256 public signatureThreshold;
|
|
|
|
// ----- EIP-712 -----
|
|
bytes32 public immutable DOMAIN_SEPARATOR;
|
|
bytes32 public constant EIP712DOMAIN_TYPEHASH =
|
|
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
|
|
bytes32 public constant SESSIONRESULT_TYPEHASH =
|
|
keccak256("SessionResult(uint256 sessionId,bytes32 circuitId,uint256[] outputs)");
|
|
|
|
// ----- session results -----
|
|
struct Session {
|
|
bool exists;
|
|
bytes32 circuitId;
|
|
bytes32 outputsHash; // keccak256(abi.encodePacked(outputs))
|
|
uint32 numOutputs;
|
|
uint32 numSigners;
|
|
uint256 finalizedAtBlock;
|
|
}
|
|
mapping(uint256 => Session) public sessions;
|
|
mapping(uint256 => uint256[]) private sessionOutputs;
|
|
|
|
// ----- optional input commitments (audit trail, inputs stay private) -----
|
|
// sessionId => partyId => commitment (e.g. keccak256(input || salt)). A party
|
|
// may post its commitment BEFORE the result is finalized, so the flow is
|
|
// tamper-evident without revealing the input. Commitments are write-once.
|
|
mapping(uint256 => mapping(bytes32 => bytes32)) public inputCommitment;
|
|
mapping(uint256 => uint256) public inputCommitmentCount;
|
|
|
|
// ----- events -----
|
|
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
|
|
event CommitteeMemberRegistered(address indexed member, uint256 committeeCount);
|
|
event CommitteeMemberRemoved(address indexed member, uint256 committeeCount);
|
|
event SignatureThresholdSet(uint256 threshold, uint256 committeeCount);
|
|
event InputCommitted(uint256 indexed sessionId, bytes32 indexed partyId, address indexed committer, bytes32 commitment);
|
|
event SessionFinalized(
|
|
uint256 indexed sessionId,
|
|
bytes32 indexed circuitId,
|
|
bytes32 outputsHash,
|
|
uint256 numOutputs,
|
|
uint256 numSigners,
|
|
uint256 threshold
|
|
);
|
|
|
|
modifier onlyOwner() {
|
|
require(msg.sender == owner, "not owner");
|
|
_;
|
|
}
|
|
|
|
constructor() {
|
|
owner = msg.sender;
|
|
emit OwnershipTransferred(address(0), msg.sender);
|
|
DOMAIN_SEPARATOR = keccak256(
|
|
abi.encode(
|
|
EIP712DOMAIN_TYPEHASH,
|
|
keccak256(bytes("AereConfidentialCompute")),
|
|
keccak256(bytes("1")),
|
|
block.chainid,
|
|
address(this)
|
|
)
|
|
);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Foundation: committee registry
|
|
// ==========================================================================
|
|
|
|
function registerCommitteeMember(address member) public onlyOwner {
|
|
require(member != address(0), "member=0");
|
|
require(!isCommittee[member], "already registered");
|
|
isCommittee[member] = true;
|
|
committeeList.push(member);
|
|
committeeCount += 1;
|
|
emit CommitteeMemberRegistered(member, committeeCount);
|
|
}
|
|
|
|
function registerCommittee(address[] calldata members) external onlyOwner {
|
|
for (uint256 i = 0; i < members.length; i++) {
|
|
registerCommitteeMember(members[i]);
|
|
}
|
|
}
|
|
|
|
function removeCommitteeMember(address member) external onlyOwner {
|
|
require(isCommittee[member], "not registered");
|
|
isCommittee[member] = false;
|
|
committeeCount -= 1;
|
|
// compact the enumeration array
|
|
uint256 len = committeeList.length;
|
|
for (uint256 i = 0; i < len; i++) {
|
|
if (committeeList[i] == member) {
|
|
committeeList[i] = committeeList[len - 1];
|
|
committeeList.pop();
|
|
break;
|
|
}
|
|
}
|
|
if (signatureThreshold > committeeCount) signatureThreshold = committeeCount;
|
|
emit CommitteeMemberRemoved(member, committeeCount);
|
|
}
|
|
|
|
/// @notice Set the quorum: how many distinct registered committee signatures a
|
|
/// session result must carry. Must be at least 1 and at most the
|
|
/// committee size. For a t-of-n MPC committee a sensible quorum is the
|
|
/// reconstruction threshold or higher.
|
|
function setSignatureThreshold(uint256 threshold) external onlyOwner {
|
|
require(threshold >= 1, "threshold < 1");
|
|
require(threshold <= committeeCount, "threshold > committee");
|
|
signatureThreshold = threshold;
|
|
emit SignatureThresholdSet(threshold, committeeCount);
|
|
}
|
|
|
|
function transferOwnership(address newOwner) external onlyOwner {
|
|
require(newOwner != address(0), "newOwner=0");
|
|
emit OwnershipTransferred(owner, newOwner);
|
|
owner = newOwner;
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Parties: optional input commitments
|
|
// ==========================================================================
|
|
|
|
/// @notice Post a write-once commitment to a private input before the session
|
|
/// is finalized. The input itself is never revealed on chain; the
|
|
/// commitment gives a tamper-evident audit trail. Permissionless: any
|
|
/// party may commit for its own partyId.
|
|
function commitInput(uint256 sessionId, bytes32 partyId, bytes32 commitment) external {
|
|
require(!sessions[sessionId].exists, "session finalized");
|
|
require(commitment != bytes32(0), "commitment=0");
|
|
require(inputCommitment[sessionId][partyId] == bytes32(0), "already committed");
|
|
inputCommitment[sessionId][partyId] = commitment;
|
|
inputCommitmentCount[sessionId] += 1;
|
|
emit InputCommitted(sessionId, partyId, msg.sender, commitment);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Anyone (relayer): submit a signed result
|
|
// ==========================================================================
|
|
|
|
/// @notice Finalize a confidential-compute session. Records `outputs` for
|
|
/// `sessionId` iff at least `signatureThreshold` DISTINCT registered
|
|
/// committee members signed the canonical EIP-712 SessionResult over
|
|
/// (sessionId, circuitId, outputs). msg.sender is only a gas-paying
|
|
/// relayer and gains no authority: it cannot forge committee
|
|
/// signatures, and there is no owner backdoor that records a result
|
|
/// without them.
|
|
/// @param signatures 65-byte ECDSA signatures, ORDERED BY SIGNER ADDRESS
|
|
/// STRICTLY ASCENDING (this both de-duplicates signers cheaply and
|
|
/// makes the quorum count unambiguous).
|
|
function submitSession(
|
|
uint256 sessionId,
|
|
bytes32 circuitId,
|
|
uint256[] calldata outputs,
|
|
bytes[] calldata signatures
|
|
) external {
|
|
require(committeeCount > 0, "no committee");
|
|
require(signatureThreshold > 0, "threshold unset");
|
|
require(!sessions[sessionId].exists, "already finalized");
|
|
require(signatures.length >= signatureThreshold, "insufficient signatures");
|
|
require(outputs.length <= type(uint32).max, "too many outputs");
|
|
|
|
bytes32 outputsHash = keccak256(abi.encodePacked(outputs));
|
|
bytes32 structHash = keccak256(
|
|
abi.encode(SESSIONRESULT_TYPEHASH, sessionId, circuitId, outputsHash)
|
|
);
|
|
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
|
|
|
|
// Count distinct registered signers. Requiring strictly ascending signer
|
|
// addresses rejects duplicates (a repeated key cannot inflate the quorum)
|
|
// and any non-committee (forged) signature fails the isCommittee check.
|
|
address last = address(0);
|
|
uint256 valid = 0;
|
|
for (uint256 i = 0; i < signatures.length; i++) {
|
|
address signer = digest.recover(signatures[i]);
|
|
require(signer > last, "sigs unsorted or duplicate");
|
|
require(isCommittee[signer], "signer not committee");
|
|
last = signer;
|
|
valid += 1;
|
|
}
|
|
require(valid >= signatureThreshold, "quorum not met");
|
|
|
|
sessions[sessionId] = Session({
|
|
exists: true,
|
|
circuitId: circuitId,
|
|
outputsHash: outputsHash,
|
|
numOutputs: uint32(outputs.length),
|
|
numSigners: uint32(valid),
|
|
finalizedAtBlock: block.number
|
|
});
|
|
sessionOutputs[sessionId] = outputs;
|
|
|
|
emit SessionFinalized(sessionId, circuitId, outputsHash, outputs.length, valid, signatureThreshold);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Views
|
|
// ==========================================================================
|
|
|
|
/// @notice The EIP-712 digest for a result (handy for off-chain signers/tests).
|
|
function digestFor(uint256 sessionId, bytes32 circuitId, uint256[] calldata outputs)
|
|
external
|
|
view
|
|
returns (bytes32)
|
|
{
|
|
bytes32 outputsHash = keccak256(abi.encodePacked(outputs));
|
|
bytes32 structHash = keccak256(
|
|
abi.encode(SESSIONRESULT_TYPEHASH, sessionId, circuitId, outputsHash)
|
|
);
|
|
return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
|
|
}
|
|
|
|
function isFinalized(uint256 sessionId) external view returns (bool) {
|
|
return sessions[sessionId].exists;
|
|
}
|
|
|
|
function getOutputs(uint256 sessionId) external view returns (uint256[] memory) {
|
|
require(sessions[sessionId].exists, "not finalized");
|
|
return sessionOutputs[sessionId];
|
|
}
|
|
|
|
function getCommittee() external view returns (address[] memory) {
|
|
return committeeList;
|
|
}
|
|
|
|
/// @notice Recover the signer of a signature over a session digest. View
|
|
/// helper used by the adversarial harness to show forged (non
|
|
/// committee) signatures are rejected.
|
|
function recoverSigner(
|
|
uint256 sessionId,
|
|
bytes32 circuitId,
|
|
uint256[] calldata outputs,
|
|
bytes calldata signature
|
|
) external view returns (address) {
|
|
bytes32 outputsHash = keccak256(abi.encodePacked(outputs));
|
|
bytes32 structHash = keccak256(
|
|
abi.encode(SESSIONRESULT_TYPEHASH, sessionId, circuitId, outputsHash)
|
|
);
|
|
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
|
|
return digest.recover(signature);
|
|
}
|
|
}
|