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