igneum/tools/reference-apps/oracle/contracts/IgneumStateOracle.sol

292 lines
13 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {IIgneumCertificateVerifier} from "./IIgneumCertificateVerifier.sol";
import {Blake2b} from "./Blake2b.sol";
import {Mpt} from "./Mpt.sol";
// Stores proven Igneum Devnet 3 state roots on Sepolia and answers balance and storage reads under them.
//
// A state root enters through submitStateRoot with:
// headers the serialized headers from the carrier block up to a certified checkpoint, carrier first
// coinbaseTx the carrier's serialized coinbase transaction
// leafIndex, siblings the merkle path from the coinbase hash to the carrier's hash_merkle_root
// recordIndex which segment record inside the coinbase payload's IGNS section to take
// Every header is hashed with keyed BLAKE2b-256 ("BlockHash") on chain and parsed out of the same bytes.
// Each next header must list the previous hash among its level-0 parents. The last hash must be the
// checkpoint the verifier holds for the given certificate index. The coinbase hash ("TransactionHash")
// must reach the carrier's hash_merkle_root through the path ("MerkleBranchHash"). The record's
// statement then gives (number, block hash, post state root), which is stored under the number.
contract IgneumStateOracle {
struct Root {
bytes32 postRoot;
bytes32 blockHash;
bytes32 carrier;
uint64 certIndex;
}
IIgneumCertificateVerifier public verifier;
address public owner;
uint64 public immutable evmChainId; // the statement's chain id, 0 to skip the check
mapping(uint64 => Root) private _roots;
event StateRoot(uint64 indexed number, bytes32 postRoot, bytes32 blockHash, uint64 certIndex, bytes32 carrier);
event VerifierSet(address verifier);
uint256 private constant RECORD_LEN = 586;
constructor(address verifier_, uint64 evmChainId_) {
verifier = IIgneumCertificateVerifier(verifier_);
owner = msg.sender;
evmChainId = evmChainId_;
}
function setVerifier(address v) external {
require(msg.sender == owner, "oracle: owner only");
verifier = IIgneumCertificateVerifier(v);
emit VerifierSet(v);
}
function trust() external pure returns (string memory) {
return "Checked on chain: header hashes and parent links to a certified checkpoint, the coinbase merkle path, the segment record layout, and the account and storage proofs under post_root. Not checked on chain in this slice: the aggregator's BLS signature over the segment record and the ZK proof behind it.";
}
// ---- hashes -------------------------------------------------------------------------------------
function headerHash(bytes memory header) public view returns (bytes32) {
return Blake2b.hash256("BlockHash", header);
}
function transactionHash(bytes memory tx_) public view returns (bytes32) {
return Blake2b.hash256("TransactionHash", tx_);
}
function merkleRoot(bytes32 leaf, uint256 index, bytes32[] calldata siblings) public view returns (bytes32 h) {
h = leaf;
for (uint256 i = 0; i < siblings.length; i++) {
h = (index & 1) == 0
? Blake2b.hash256("MerkleBranchHash", abi.encodePacked(h, siblings[i]))
: Blake2b.hash256("MerkleBranchHash", abi.encodePacked(siblings[i], h));
index >>= 1;
}
require(index == 0, "merkle: leaf index beyond the path");
}
// ---- headers ------------------------------------------------------------------------------------
function le64(bytes memory b, uint256 off) private pure returns (uint64) {
require(off + 8 <= b.length, "header: short");
uint256 w;
assembly { w := mload(add(add(b, 32), off)) }
return Blake2b.swap64(uint64(w >> 192));
}
function word(bytes memory b, uint256 off) private pure returns (bytes32 w) {
require(off + 32 <= b.length, "header: short");
assembly { w := mload(add(add(b, 32), off)) }
}
// Parses the level-0 parents and hash_merkle_root out of a serialized header.
function parseHeader(bytes memory h) public pure returns (uint256 parentsOff, uint256 parentCount, bytes32 merkle) {
uint64 levels = le64(h, 2);
require(levels >= 1, "header: no parent levels");
uint256 off = 10;
parentCount = le64(h, off);
parentsOff = off + 8;
off = parentsOff + 32 * parentCount;
for (uint256 l = 1; l < levels; l++) {
uint256 cnt = le64(h, off);
off += 8 + 32 * cnt;
}
merkle = word(h, off);
}
function listsParent(bytes memory h, uint256 parentsOff, uint256 parentCount, bytes32 x) private pure returns (bool) {
for (uint256 i = 0; i < parentCount; i++) {
if (word(h, parentsOff + 32 * i) == x) return true;
}
return false;
}
// Hashes every header, checks each parent link, and that the last hash is `checkpoint`.
// Returns the carrier's hash and hash_merkle_root.
function checkHeaderPath(bytes[] calldata headers, bytes32 checkpoint) public view returns (bytes32 carrier, bytes32 merkle) {
require(headers.length > 0, "headers: none");
bytes32 prev;
for (uint256 i = 0; i < headers.length; i++) {
bytes memory h = headers[i];
(uint256 pOff, uint256 pCount, bytes32 m) = parseHeader(h);
if (i == 0) {
merkle = m;
} else {
require(listsParent(h, pOff, pCount, prev), "headers: a header does not name the previous one as a parent");
}
prev = headerHash(h);
if (i == 0) carrier = prev;
}
require(prev == checkpoint, "headers: the last header is not the certified checkpoint");
}
// ---- the coinbase transaction and its segment record ---------------------------------------------
// Walks the serialized transaction to the payload. Amounts are 8 bytes on the wire.
function payloadOf(bytes memory t) public pure returns (uint256 off, uint256 len) {
uint16 version = uint16(le64(t, 0) & 0xffff);
uint256 p = 2;
uint256 nIn = le64(t, p);
p += 8;
for (uint256 i = 0; i < nIn; i++) {
p += 36;
uint256 sigLen = le64(t, p);
p += 8 + sigLen;
if (version < 1) p += 1;
p += 8;
if (version >= 1) p += 2;
}
uint256 nOut = le64(t, p);
p += 8;
for (uint256 i = 0; i < nOut; i++) {
p += 8 + 2;
uint256 spkLen = le64(t, p);
p += 8 + spkLen;
if (version >= 1) {
require(p < t.length, "coinbase: short");
if (uint8(t[p]) != 0) p += 34;
p += 1;
}
}
p += 8 + 20 + 8;
len = le64(t, p);
off = p + 8;
require(off + len <= t.length, "coinbase: payload overruns");
}
// The nested sections at the end of the extra data: items || len_le32 || TAG. Returns the IGNS items.
function segmentSection(bytes memory t, uint256 payloadOff, uint256 payloadLen) public pure returns (uint256 off, uint256 len) {
require(payloadLen >= 19, "coinbase: payload too short");
uint256 scriptLen = uint8(t[payloadOff + 18]);
require(19 + scriptLen <= payloadLen, "coinbase: script overruns");
uint256 start = payloadOff + 19 + scriptLen;
uint256 end = payloadOff + payloadLen;
end = takeSection(t, start, end, "IGNF");
end = takeSection(t, start, end, "IGNP");
uint256 before = takeSection(t, start, end, "IGNS");
require(before != end, "coinbase: no IGNS section");
off = before;
len = end - 8 - before;
}
// If the bytes in [start, end) end with `tag`, returns the start of that section; else returns `end`.
function takeSection(bytes memory t, uint256 start, uint256 end, bytes4 tag) private pure returns (uint256) {
if (end < start + 8) return end;
bytes4 have;
assembly { have := mload(add(add(t, 32), sub(end, 4))) }
if (have != tag) return end;
uint256 len = le32(t, end - 8);
if (len + 8 > end - start) return end;
return end - 8 - len;
}
function le32(bytes memory b, uint256 off) private pure returns (uint32) {
uint256 w;
assembly { w := mload(add(add(b, 32), off)) }
uint32 be = uint32(w >> 224);
return ((be & 0xff) << 24) | ((be & 0xff00) << 8) | ((be & 0xff0000) >> 8) | (be >> 24);
}
// Reads record `recordIndex` of the IGNS section: (last block number, block hash, post state root, chain id).
function readRecord(bytes memory t, uint256 recordIndex)
public
pure
returns (uint64 number, bytes32 blockHash, bytes32 postRoot, uint64 chainId)
{
(uint256 pOff, uint256 pLen) = payloadOf(t);
(uint256 sOff, uint256 sLen) = segmentSection(t, pOff, pLen);
require(sLen % RECORD_LEN == 0, "record: section length");
require((recordIndex + 1) * RECORD_LEN <= sLen, "record: index beyond the section");
uint256 r = sOff + recordIndex * RECORD_LEN;
uint64 last = le64(t, r + 10);
bytes32 block_ = word(t, r + 18);
uint256 pv = r + 118;
chainId = uint64(uint256(word(t, pv)) >> 192);
number = uint64(uint256(word(t, pv + 8)) >> 192);
blockHash = word(t, pv + 16);
postRoot = word(t, pv + 148);
require(number == last, "record: statement is not for the segment's last block");
require(blockHash == block_, "record: block hash is not the statement's");
require(postRoot != bytes32(0), "record: zero post_root");
}
// ---- the whole check ----------------------------------------------------------------------------
function verifyStateRoot(
uint64 certIndex,
bytes[] calldata headers,
bytes calldata coinbaseTx,
uint256 leafIndex,
bytes32[] calldata siblings,
uint256 recordIndex
) public view returns (uint64 number, bytes32 postRoot, bytes32 blockHash, bytes32 carrier) {
bytes32 checkpoint = verifier.finalCheckpoint(certIndex);
require(checkpoint != bytes32(0), "oracle: no certificate at that index");
bytes32 merkle;
(carrier, merkle) = checkHeaderPath(headers, checkpoint);
bytes memory t = coinbaseTx;
bytes32 leaf = transactionHash(t);
require(merkleRoot(leaf, leafIndex, siblings) == merkle, "merkle: path does not reach the carrier's hash_merkle_root");
uint64 chainId;
(number, blockHash, postRoot, chainId) = readRecord(t, recordIndex);
require(evmChainId == 0 || chainId == evmChainId, "record: statement chain id");
}
function submitStateRoot(
uint64 certIndex,
bytes[] calldata headers,
bytes calldata coinbaseTx,
uint256 leafIndex,
bytes32[] calldata siblings,
uint256 recordIndex
) external returns (uint64 number, bytes32 postRoot) {
bytes32 blockHash;
bytes32 carrier;
(number, postRoot, blockHash, carrier) = verifyStateRoot(certIndex, headers, coinbaseTx, leafIndex, siblings, recordIndex);
Root storage r = _roots[number];
require(r.postRoot == bytes32(0) || r.postRoot == postRoot, "oracle: another root is stored for that block");
_roots[number] = Root(postRoot, blockHash, carrier, certIndex);
emit StateRoot(number, postRoot, blockHash, certIndex, carrier);
}
// ---- reads --------------------------------------------------------------------------------------
function stateRoot(uint64 number) public view returns (bytes32 postRoot, bytes32 blockHash, bytes32 carrier, uint64 certIndex) {
Root storage r = _roots[number];
require(r.postRoot != bytes32(0), "oracle: no proven root for that block");
return (r.postRoot, r.blockHash, r.carrier, r.certIndex);
}
function provenAccount(uint64 number, address a, bytes[] calldata accountProof)
public
view
returns (bool exists, uint256 nonce, uint256 balance, bytes32 storageRoot, bytes32 codeHash)
{
(bytes32 root,,,) = stateRoot(number);
return Mpt.account(root, a, accountProof);
}
function provenBalance(uint64 number, address a, bytes[] calldata accountProof) external view returns (uint256 balance) {
(,, balance,,) = provenAccount(number, a, accountProof);
}
function provenStorage(uint64 number, address a, bytes32 slot, bytes[] calldata accountProof, bytes[] calldata storageProof)
external
view
returns (bytes32)
{
(bool exists,,, bytes32 storageRoot,) = provenAccount(number, a, accountProof);
if (!exists) {
require(storageProof.length == 0, "mpt: storage proof for an absent account");
return bytes32(0);
}
return Mpt.storageSlot(storageRoot, slot, storageProof);
}
}