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.
340 lines
13 KiB
Solidity
340 lines
13 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
/// @title AereDaFraudProofVerifier
|
|
/// @notice On-chain verifier for the AERE Data Availability layer's
|
|
/// bad-encoding fraud proofs and availability samples. It recomputes,
|
|
/// entirely on-chain, the Namespaced-Merkle-Tree (NMT) commitment and
|
|
/// the 2D Reed-Solomon re-encoding used by the off-chain `aere-da`
|
|
/// Rust crate (aerenew/da), and CONVICTS a badly-encoded block while
|
|
/// REJECTING spurious challenges.
|
|
///
|
|
/// Commitment layout (Celestia-style, identical to the crate):
|
|
/// * each row and each column of the 2k x 2k extended data square is
|
|
/// committed with an NMT -> row_root / col_root;
|
|
/// * an outer NMT over row_roots ++ col_roots gives the data root.
|
|
///
|
|
/// A bad-encoding fraud proof reveals the k committed basis shares of a
|
|
/// row/column plus one committed "disputed" share. The verifier:
|
|
/// 1. checks the challenged line's root is committed under the data
|
|
/// root (outer NMT inclusion), and
|
|
/// 2. checks every revealed share is committed under that root (inner
|
|
/// NMT inclusion), and
|
|
/// 3. recomputes the RS value at the disputed position from the k
|
|
/// basis shares and CONVICTS iff it disagrees with the committed
|
|
/// disputed share (the RS-parity-break check).
|
|
/// All three must hold for a conviction; otherwise the challenge is
|
|
/// rejected.
|
|
///
|
|
/// Hash: this verifier uses the EVM `sha256` precompile (0x02) and so
|
|
/// commits to the crate's SHA-256 hasher. The crate also offers a
|
|
/// FIPS-202 SHAKE256 hasher matching AERE mainnet's live 0x0AE5
|
|
/// precompile; standard EVM exposes no SHAKE256 precompile, so the
|
|
/// SHA-256 variant is the on-chain path here. Both are hash-based and
|
|
/// carry no pairing / discrete-log assumption.
|
|
///
|
|
/// Scope: verification logic only. No block production, no economic
|
|
/// slashing, no deployment wired here. This is the settlement-side
|
|
/// adjudicator a DA dispute would call.
|
|
contract AereDaFraudProofVerifier {
|
|
// ---------------------------------------------------------------------
|
|
// Types (mirror the crate's serialization; see aerenew/da/src/nmt.rs,
|
|
// rs2d.rs, das.rs).
|
|
// ---------------------------------------------------------------------
|
|
|
|
/// An NMT node: the namespace range it spans and its 32-byte hash.
|
|
struct Node {
|
|
bytes8 min;
|
|
bytes8 max;
|
|
bytes32 hash;
|
|
}
|
|
|
|
/// A single-leaf NMT inclusion proof: siblings from leaf level upward.
|
|
struct InclusionProof {
|
|
uint256 index;
|
|
Node[] siblings;
|
|
}
|
|
|
|
/// A revealed share at a line position, with its inclusion proof.
|
|
struct ShareProof {
|
|
uint256 pos;
|
|
bytes8 ns;
|
|
bytes data;
|
|
InclusionProof proof;
|
|
}
|
|
|
|
/// A bad-encoding fraud proof for one row (axis 0) or column (axis 1).
|
|
struct BadEncoding {
|
|
uint8 axis; // 0 = row ('r'), 1 = column ('c')
|
|
uint256 index;
|
|
Node root;
|
|
InclusionProof outerProof;
|
|
ShareProof[] basis; // k basis shares
|
|
ShareProof disputed; // the committed share that breaks RS parity
|
|
}
|
|
|
|
/// A verifiable availability sample (light-node path).
|
|
struct Sample {
|
|
uint256 row;
|
|
uint256 col;
|
|
bytes8 ns;
|
|
bytes data;
|
|
InclusionProof rowProof;
|
|
Node rowRoot;
|
|
InclusionProof outerProof;
|
|
}
|
|
|
|
bytes1 private constant LEAF_PREFIX = 0x00;
|
|
bytes1 private constant NODE_PREFIX = 0x01;
|
|
|
|
/// @dev GF(2^8) exp/log tables over the AES field x^8+x^4+x^3+x^2+1 (0x11b),
|
|
/// generator 0x03. Built once in the constructor; identical to the
|
|
/// crate's `Gf256::new`. `expTable` is length 512 so multiply needs no
|
|
/// modulo.
|
|
bytes private expTable;
|
|
bytes private logTable;
|
|
|
|
event BadBlockConvicted(bytes32 indexed dataRootHash, uint8 axis, uint256 index, uint256 disputedPos);
|
|
|
|
constructor() {
|
|
bytes memory e = new bytes(512);
|
|
bytes memory l = new bytes(256);
|
|
uint16 x = 1;
|
|
for (uint256 i = 0; i < 255; i++) {
|
|
e[i] = bytes1(uint8(x));
|
|
l[uint256(x)] = bytes1(uint8(i));
|
|
// multiply by generator 0x03 == x + 1: x*3 = (x<<1) ^ x
|
|
x ^= x << 1;
|
|
if (x & 0x100 != 0) {
|
|
x ^= 0x11b;
|
|
}
|
|
x &= 0xff;
|
|
}
|
|
for (uint256 i = 255; i < 512; i++) {
|
|
e[i] = e[i - 255];
|
|
}
|
|
expTable = e;
|
|
logTable = l;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// NMT recomputation (SHA-256), byte-for-byte matching aerenew/da/src/nmt.rs.
|
|
// ---------------------------------------------------------------------
|
|
|
|
function _leaf(bytes8 ns, bytes memory data) internal pure returns (Node memory) {
|
|
bytes32 h = sha256(abi.encodePacked(LEAF_PREFIX, ns, data));
|
|
return Node({min: ns, max: ns, hash: h});
|
|
}
|
|
|
|
function _inner(Node memory a, Node memory b) internal pure returns (Node memory) {
|
|
bytes32 h = sha256(
|
|
abi.encodePacked(NODE_PREFIX, a.min, a.max, a.hash, b.min, b.max, b.hash)
|
|
);
|
|
return Node({min: a.min, max: b.max, hash: h});
|
|
}
|
|
|
|
function _eq(Node memory a, Node memory b) internal pure returns (bool) {
|
|
return a.min == b.min && a.max == b.max && a.hash == b.hash;
|
|
}
|
|
|
|
/// @dev outer_ns(i): 8-byte big-endian encoding of i (matches das.rs).
|
|
function _outerNs(uint256 i) internal pure returns (bytes8) {
|
|
return bytes8(uint64(i));
|
|
}
|
|
|
|
/// @dev node_bytes(node) = min || max || hash (48 bytes), the outer-tree
|
|
/// leaf payload for a row/column root.
|
|
function _nodeBytes(Node memory node) internal pure returns (bytes memory) {
|
|
return abi.encodePacked(node.min, node.max, node.hash);
|
|
}
|
|
|
|
/// @dev Verify a single-leaf NMT inclusion proof against `root`.
|
|
function _verifyInclusion(
|
|
Node memory root,
|
|
InclusionProof memory proof,
|
|
bytes8 ns,
|
|
bytes memory data
|
|
) internal pure returns (bool) {
|
|
Node memory node = _leaf(ns, data);
|
|
uint256 idx = proof.index;
|
|
uint256 len = proof.siblings.length;
|
|
for (uint256 i = 0; i < len; i++) {
|
|
if (idx & 1 == 0) {
|
|
node = _inner(node, proof.siblings[i]);
|
|
} else {
|
|
node = _inner(proof.siblings[i], node);
|
|
}
|
|
idx >>= 1;
|
|
}
|
|
return _eq(node, root);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// GF(256) Reed-Solomon (matches aerenew/da/src/gf256.rs).
|
|
// ---------------------------------------------------------------------
|
|
|
|
function _mul(uint8 a, uint8 b) internal view returns (uint8) {
|
|
if (a == 0 || b == 0) return 0;
|
|
uint256 idx = uint256(uint8(logTable[a])) + uint256(uint8(logTable[b]));
|
|
return uint8(expTable[idx]);
|
|
}
|
|
|
|
function _div(uint8 a, uint8 b) internal view returns (uint8) {
|
|
// Caller guarantees b != 0 (interpolation nodes are distinct).
|
|
if (a == 0) return 0;
|
|
uint256 idx = (uint256(uint8(logTable[a])) + 255 - uint256(uint8(logTable[b]))) % 255;
|
|
return uint8(expTable[idx]);
|
|
}
|
|
|
|
/// @dev Evaluate at `x` the unique degree<m polynomial through (xs[i],ys[i]),
|
|
/// via the Lagrange form. Identical to `Gf256::interpolate_eval`.
|
|
function _interp(uint8[] memory xs, uint8[] memory ys, uint8 x) internal view returns (uint8) {
|
|
uint8 acc = 0;
|
|
uint256 m = xs.length;
|
|
for (uint256 i = 0; i < m; i++) {
|
|
if (x == xs[i]) {
|
|
return ys[i];
|
|
}
|
|
uint8 num = 1;
|
|
uint8 den = 1;
|
|
for (uint256 j = 0; j < m; j++) {
|
|
if (i == j) continue;
|
|
num = _mul(num, x ^ xs[j]);
|
|
den = _mul(den, xs[i] ^ xs[j]);
|
|
}
|
|
acc ^= _mul(ys[i], _div(num, den));
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Public verification API.
|
|
// ---------------------------------------------------------------------
|
|
|
|
/// @notice Verify a bad-encoding fraud proof. Returns true iff it is a
|
|
/// VALID proof that the committed square is badly encoded: the RS
|
|
/// re-encoding of the k committed basis shares disagrees with the
|
|
/// committed disputed share, and every revealed share carries a
|
|
/// valid inclusion proof against the committed line root, which is
|
|
/// itself committed under `dataRoot`.
|
|
/// @dev View (reads the GF tables). Pure with respect to persistent state.
|
|
function verifyBadEncoding(
|
|
Node calldata dataRoot,
|
|
uint256 k,
|
|
uint256 n,
|
|
BadEncoding calldata p
|
|
) public view returns (bool) {
|
|
if (p.basis.length != k) {
|
|
return false;
|
|
}
|
|
// The challenged line root must be committed under the data root at the
|
|
// right outer index: row r sits at index r, column c at index n + c.
|
|
uint256 outerIdx = p.axis == 0 ? p.index : n + p.index;
|
|
if (p.outerProof.index != outerIdx) {
|
|
return false;
|
|
}
|
|
if (!_verifyInclusion(_copyNode(dataRoot), _copyProof(p.outerProof), _outerNs(outerIdx), _nodeBytes(_copyNode(p.root)))) {
|
|
return false;
|
|
}
|
|
|
|
Node memory rootMem = _copyNode(p.root);
|
|
|
|
// Every basis share is committed under the line root at its position.
|
|
for (uint256 i = 0; i < k; i++) {
|
|
ShareProof calldata b = p.basis[i];
|
|
if (b.proof.index != b.pos) {
|
|
return false;
|
|
}
|
|
if (!_verifyInclusion(rootMem, _copyProof(b.proof), b.ns, b.data)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// The disputed share is committed under the line root at its position.
|
|
ShareProof calldata d = p.disputed;
|
|
if (d.proof.index != d.pos) {
|
|
return false;
|
|
}
|
|
if (!_verifyInclusion(rootMem, _copyProof(d.proof), d.ns, d.data)) {
|
|
return false;
|
|
}
|
|
|
|
// Recompute the RS value at the disputed position from the k basis
|
|
// shares, symbol-wise over each byte offset. CONVICT iff any byte of the
|
|
// committed disputed share disagrees with the re-encoding.
|
|
uint256 sl = d.data.length;
|
|
uint8[] memory xs = new uint8[](k);
|
|
for (uint256 i = 0; i < k; i++) {
|
|
xs[i] = uint8(p.basis[i].pos);
|
|
}
|
|
uint8 xj = uint8(d.pos);
|
|
for (uint256 bpos = 0; bpos < sl; bpos++) {
|
|
uint8[] memory ys = new uint8[](k);
|
|
for (uint256 i = 0; i < k; i++) {
|
|
ys[i] = uint8(p.basis[i].data[bpos]);
|
|
}
|
|
uint8 rec = _interp(xs, ys, xj);
|
|
if (rec != uint8(d.data[bpos])) {
|
|
return true; // RS parity break -> badly encoded -> convict
|
|
}
|
|
}
|
|
return false; // re-encoding matches -> spurious challenge -> reject
|
|
}
|
|
|
|
/// @notice Same as {verifyBadEncoding} but state-changing: emits
|
|
/// {BadBlockConvicted} on a genuine bad-encoding proof and reverts
|
|
/// on a spurious challenge. This is the entry point a DA slashing
|
|
/// module would call.
|
|
function challenge(
|
|
Node calldata dataRoot,
|
|
uint256 k,
|
|
uint256 n,
|
|
BadEncoding calldata p
|
|
) external returns (bool) {
|
|
require(verifyBadEncoding(dataRoot, k, n, p), "AereDA: not a valid bad-encoding fraud proof");
|
|
emit BadBlockConvicted(dataRoot.hash, p.axis, p.index, p.disputed.pos);
|
|
return true;
|
|
}
|
|
|
|
/// @notice Verify a single availability sample against the data root: the
|
|
/// cell is committed under its row root, and the row root is
|
|
/// committed under the data root. This is the NMT-inclusion leg a
|
|
/// sampling light node checks.
|
|
function verifySample(
|
|
Node calldata dataRoot,
|
|
uint256 n,
|
|
Sample calldata s
|
|
) external pure returns (bool) {
|
|
Node memory rowRoot = _copyNode(s.rowRoot);
|
|
if (!_verifyInclusion(rowRoot, _copyProof(s.rowProof), s.ns, s.data)) {
|
|
return false;
|
|
}
|
|
if (s.rowProof.index != s.col) {
|
|
return false;
|
|
}
|
|
if (s.outerProof.index != s.row) {
|
|
return false;
|
|
}
|
|
if (!_verifyInclusion(_copyNode(dataRoot), _copyProof(s.outerProof), _outerNs(s.row), _nodeBytes(rowRoot))) {
|
|
return false;
|
|
}
|
|
return s.row < n;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Small calldata -> memory copy helpers (viaIR keeps these cheap).
|
|
// ---------------------------------------------------------------------
|
|
|
|
function _copyNode(Node calldata nd) internal pure returns (Node memory) {
|
|
return Node({min: nd.min, max: nd.max, hash: nd.hash});
|
|
}
|
|
|
|
function _copyProof(InclusionProof calldata pr) internal pure returns (InclusionProof memory out) {
|
|
out.index = pr.index;
|
|
out.siblings = new Node[](pr.siblings.length);
|
|
for (uint256 i = 0; i < pr.siblings.length; i++) {
|
|
out.siblings[i] = _copyNode(pr.siblings[i]);
|
|
}
|
|
}
|
|
}
|