aere-contracts/contracts/zkverify/AereKZGVerifier.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

171 lines
6.3 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
/**
* @title AereKZGVerifier — on-chain anchor for EIP-4844 KZG point evaluations
* @notice Thin, admin-less registry over the POINT_EVALUATION precompile at
* address 0x0A (EIP-4844, live on AERE chain 2800: confirmed via the
* official go-ethereum known-answer test vector on 2026-07-10).
*
* The precompile takes a 192-byte input
* versioned_hash (32) | z (32) | y (32) | commitment (48) | proof (48)
* and, on a VALID proof that P(z) == y for the blob polynomial P
* committed to by `commitment`, returns exactly 64 bytes:
* uint256(FIELD_ELEMENTS_PER_BLOB = 4096) | uint256(BLS_MODULUS)
* On an INVALID proof the precompile call itself fails (Besu raises a
* precompile error, so the staticcall returns success=false).
*
* verifyPointEvaluation() forwards the raw input, validates BOTH the
* success flag and the canonical 64-byte return payload, and only then
* records (sender, versionedHash, z, y, block, time) and emits an
* event. checkPointEvaluation() is the gas-cheap view twin that
* records nothing.
*
* NOTE: precompiles have no code (extcodesize == 0), so this contract
* deliberately performs NO codesize check on the precompile address;
* correctness is enforced by the strict 64-byte canonical-constant
* return check, which no empty account and no accidental contract can
* satisfy by chance.
*
* IMMUTABILITY: no owner, no admin, no pause. Anyone may verify;
* records are append-only.
*/
contract AereKZGVerifier {
/// @notice EIP-4844 point-evaluation precompile address.
address public constant POINT_EVALUATION = address(0x0A);
/// @notice Exact input length the precompile accepts.
uint256 public constant INPUT_LENGTH = 192;
/// @notice First word of the canonical precompile return.
uint256 public constant FIELD_ELEMENTS_PER_BLOB = 4096;
/// @notice Second word of the canonical precompile return (BLS12-381 scalar
/// field modulus).
uint256 public constant BLS_MODULUS =
52435875175126190479447740508185965837690552500527637822603658699938581184513;
struct Verification {
address sender;
bytes32 versionedHash; // sha256(commitment) with first byte 0x01
bytes32 z; // evaluation point (field element)
bytes32 y; // claimed evaluation P(z) (field element)
uint64 blockNumber;
uint64 timestamp;
}
Verification[] private _verifications;
/// @notice Number of successful verifications recorded per versioned hash.
mapping(bytes32 => uint256) public verificationCountByHash;
/// @notice Index (+1) of the most recent verification for a versioned
/// hash; 0 means never verified.
mapping(bytes32 => uint256) public latestIndexPlusOneByHash;
event PointEvaluationVerified(
address indexed sender,
bytes32 indexed versionedHash,
bytes32 z,
bytes32 y,
uint256 index
);
error InvalidInputLength(uint256 provided);
error PrecompileCallFailed();
error InvalidPrecompileReturn(bytes returned);
/**
* @notice Verify an EIP-4844 KZG point-evaluation proof and record it.
* @param input Raw 192-byte precompile input:
* versioned_hash | z | y | commitment | proof.
* @return index Index of the stored verification record.
*/
function verifyPointEvaluation(bytes calldata input)
external
returns (uint256 index)
{
(bytes32 versionedHash, bytes32 z, bytes32 y) = _verify(input);
index = _verifications.length;
_verifications.push(Verification({
sender: msg.sender,
versionedHash: versionedHash,
z: z,
y: y,
blockNumber: uint64(block.number),
timestamp: uint64(block.timestamp)
}));
verificationCountByHash[versionedHash] += 1;
latestIndexPlusOneByHash[versionedHash] = index + 1;
emit PointEvaluationVerified(msg.sender, versionedHash, z, y, index);
}
/**
* @notice View-only twin of verifyPointEvaluation: verifies the proof via
* the precompile but records nothing. Reverts on any failure, so a
* `true` return means the proof is valid.
*/
function checkPointEvaluation(bytes calldata input)
external
view
returns (bool)
{
_verify(input);
return true;
}
/// @notice Total number of recorded verifications.
function verificationCount() external view returns (uint256) {
return _verifications.length;
}
/// @notice Full record at `index` (reverts if out of range).
function getVerification(uint256 index)
external
view
returns (Verification memory)
{
return _verifications[index];
}
/// @notice Most recent record for `versionedHash` (reverts if none).
function getLatestVerificationForHash(bytes32 versionedHash)
external
view
returns (Verification memory)
{
uint256 idxPlusOne = latestIndexPlusOneByHash[versionedHash];
require(idxPlusOne != 0, "AereKZGVerifier: hash never verified");
return _verifications[idxPlusOne - 1];
}
/// @dev Shared verification core: staticcall the precompile, enforce the
/// canonical 64-byte return, and split out the recorded fields.
function _verify(bytes calldata input)
internal
view
returns (bytes32 versionedHash, bytes32 z, bytes32 y)
{
if (input.length != INPUT_LENGTH) {
revert InvalidInputLength(input.length);
}
(bool ok, bytes memory ret) = POINT_EVALUATION.staticcall(input);
if (!ok) revert PrecompileCallFailed();
if (ret.length != 64) revert InvalidPrecompileReturn(ret);
(uint256 fieldElements, uint256 modulus) =
abi.decode(ret, (uint256, uint256));
if (fieldElements != FIELD_ELEMENTS_PER_BLOB || modulus != BLS_MODULUS) {
revert InvalidPrecompileReturn(ret);
}
versionedHash = bytes32(input[0:32]);
z = bytes32(input[32:64]);
y = bytes32(input[64:96]);
}
}