aere-contracts/contracts/confidential/AereConfidentialCompute.sol
Aere Network acac2f00a6
Some checks failed
contracts-ci / Install (lockfile) → compile → full test suite (push) Has been cancelled
contracts-ci / Ethereum interop (EIP-2537 BLS, prague hardfork) (push) Has been cancelled
contracts-ci / PQC known-answer tests (NIST vectors) (push) Has been cancelled
contracts-ci / Coverage (scoped, with artifacts) (push) Has been cancelled
The unpublished line of work joins the sanitized public line
The published line and the local line had no common ancestor: the public one
carried the redaction pass, the local one carried three weeks of corrections
that never shipped. This commit ports the local work onto the public line,
keeps every public redaction, and extends the same discretion to seven client
mentions that were still named in published comments.

Carried: LICENSE year and LICENSING.md; the measured burn figures replacing
the deflation claim (the vault holds ~0.137 AERE of 2.8 billion, and burn is
a share of validator coinbase revenue, which is zero today); 'audited' removed
from next to Bouncy Castle; citation paths rewritten to published form with
CITATIONS-UNRESOLVED.md remeasured 2026-08-11; VERIFY-POLICY.md; slashing and
ownership comments brought down to what the code does; the AerePyth repair;
the shutter test helper the tests cite; runnable package.json entries; the CI
file split into a GitHub/Gitea twin pair with a real measured test-run status;
and the .gitignore hardening written after a compiled artifact leaked a local
path in a sibling repository. A false '2-of-3 multisig' description of the
owner account is corrected to what the chain measures: an externally owned
account. The self-audit findings catalog stays unpublished pending an explicit
decision.
2026-08-15 13:59:30 +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 a 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);
}
}