294 lines
14 KiB
Solidity
294 lines
14 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 before the network's class v5 floor (Devnet 3: 4463), 0 to skip the check
|
|
uint64 public immutable evmChainIdAfterFloor; // the id after the floor (Devnet 3: 4464); a record names the id in force at its block
|
|
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_, uint64 evmChainIdAfterFloor_) {
|
|
verifier = IIgneumCertificateVerifier(verifier_);
|
|
owner = msg.sender;
|
|
evmChainId = evmChainId_;
|
|
evmChainIdAfterFloor = evmChainIdAfterFloor_;
|
|
}
|
|
|
|
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: the certificate through the installed verifier, header hashes and parent links to the certified checkpoint, the coinbase merkle path, the segment record layout, and the account and storage proofs under post_root. Trusted, not checked on chain: the verifier's voter table and weights are installed by its deployer (a stated input, not read from the chain); the aggregator's BLS signature over the segment record and the SP1 proof behind its statement are not verified here, so a root rests on the aggregator's statement as the nodes check and pay it. A stored root is proven state under those assumptions; a balance read from it is executed and finalised state, not a payment outcome.";
|
|
}
|
|
|
|
// ---- 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 || chainId == evmChainIdAfterFloor, "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);
|
|
}
|
|
}
|