aere-contracts/contracts/confidential/AereConfidentialCompute.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

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);
}
}