292 lines
13 KiB
Solidity
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);
|
|
}
|
|
}
|