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