igneum/tools/reference-apps/oracle/lib.mjs

345 lines
25 KiB
JavaScript

// Shared helpers for the oracle scripts: byte layouts (header, coinbase transaction, segment record, coinbase
// payload), a JS mirror of the BLAKE2b driver the contract runs through the EIP-152 precompile, a small
// Merkle Patricia trie builder for synthetic vectors, and the RPC plumbing (viem, Sepolia, the deployer key).
import { readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { blake2b } from '@noble/hashes/blake2.js';
import { keccak_256 } from '@noble/hashes/sha3.js';
import { createPublicClient, createWalletClient, http, defineChain } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { headerHash, coinbaseTxHash, merkleRootFromPath, rlpEncode, trimBig, hexToBytes, bytesToHex, segmentRecordsOf } from '../../../site/lc/core.js';
export { blake2b, keccak_256, headerHash, coinbaseTxHash, merkleRootFromPath, rlpEncode, trimBig, hexToBytes, bytesToHex };
export const here = path.dirname(fileURLToPath(import.meta.url));
export const fixtures = path.resolve(here, '../fixtures');
export const RPC = 'https://ethereum-sepolia-rpc.publicnode.com';
export const EVM_CHAIN_ID = 0x116f; // Devnet 3's EVM chain id, from the receipt fixture
export const strip = s => String(s).replace(/^0x/i, '');
export const hex0x = b => '0x' + bytesToHex(b);
const te = new TextEncoder();
export function concat(parts) { const n = parts.reduce((a, p) => a + p.length, 0); const m = new Uint8Array(n); let o = 0; for (const p of parts) { m.set(p, o); o += p.length; } return m; }
export const u16le = v => { const b = new Uint8Array(2); new DataView(b.buffer).setUint16(0, Number(v), true); return b; };
export const u32le = v => { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, Number(v), true); return b; };
export const u64le = v => { const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, BigInt(v), true); return b; };
export const u32be = v => { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, Number(v), false); return b; };
export const u64be = v => { const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, BigInt(v), false); return b; };
export function randomBytes(n) { const b = new Uint8Array(n); for (let i = 0; i < n; i++) b[i] = Math.floor(Math.random() * 256); return b; }
// A seeded byte source (xorshift32) so a synthetic vector is the same on every run.
export function seeded(seed) { let x = seed >>> 0 || 1; return n => { const b = new Uint8Array(n); for (let i = 0; i < n; i++) { x ^= x << 13; x >>>= 0; x ^= x >>> 17; x ^= x << 5; x >>>= 0; b[i] = x & 0xff; } return b; }; }
// ---- the header, byte for byte as site/verify/core.js headerHash hashes it -----------------------------------
export function serializeHeader(h) {
const levels = h.parents_by_level || [];
const parts = [u16le(h.version), u64le(levels.length)];
for (const level of levels) { parts.push(u64le(level.length)); for (const p of level) parts.push(hexToBytes(strip(p))); }
parts.push(hexToBytes(strip(h.hash_merkle_root)), hexToBytes(strip(h.accepted_id_merkle_root)), hexToBytes(strip(h.utxo_commitment)),
u64le(h.timestamp), u32le(h.bits), u64le(h.nonce), u64le(h.daa_score), u64le(h.blue_score));
const bw = String(h.blue_work).replace(/^0+/, '');
const bwBytes = bw.length ? hexToBytes(bw.length % 2 ? '0' + bw : bw) : new Uint8Array(0);
parts.push(u64le(bwBytes.length), bwBytes, hexToBytes(strip(h.pruning_point)), hexToBytes(strip(h.vote_key_hash)));
return concat(parts);
}
// ---- the coinbase transaction, byte for byte as site/light/core.js coinbaseTxHash hashes it ---------------------
export function serializeCoinbase(tx, amountWireLen = 8) {
const version = Number(tx.version || 0);
const parts = [u16le(version), u64le((tx.inputs || []).length)];
for (const inp of tx.inputs || []) {
parts.push(hexToBytes(strip(inp.previousOutpoint.transactionId)), u32le(inp.previousOutpoint.index));
const sig = hexToBytes(strip(inp.signatureScript || ''));
parts.push(u64le(sig.length), sig);
if (version < 1) parts.push(new Uint8Array([Number(inp.sigOpCount || 0)]));
parts.push(u64le(inp.sequence));
if (version >= 1) parts.push(u16le(inp.computeBudget || 0));
}
parts.push(u64le((tx.outputs || []).length));
for (const out of tx.outputs || []) {
const v = new Uint8Array(amountWireLen); let x = BigInt(out.value);
for (let i = 0; i < amountWireLen; i++) { v[i] = Number(x & 0xffn); x >>= 8n; }
const spk = hexToBytes(strip(out.scriptPublicKey));
parts.push(v, spk.subarray(0, 2), u64le(spk.length - 2), spk.subarray(2));
if (version >= 1) parts.push(new Uint8Array([out.covenant ? 1 : 0]));
}
const payload = hexToBytes(strip(tx.payload || ''));
parts.push(u64le(tx.lockTime || 0), hexToBytes(strip(tx.subnetworkId)), u64le(tx.gas || 0), u64le(payload.length), payload);
const mass = BigInt(tx.mass || 0);
if (version < 1) { if (mass > 0n) parts.push(u64le(mass)); } else parts.push(u64le(mass));
return concat(parts);
}
// ---- the segment record (586 bytes) and the coinbase payload sections ----------------------------------------
export function encodeBlockStatement(s) {
return concat([u64be(s.chain_id), u64be(s.number), s.block_hash, s.parent_hash, u32be(s.shard_count), s.tx_commitment, s.pre_root, s.post_root, s.receipts,
u64be(s.gas_used), u64be(s.pgas_used), u32be(s.executed), u32be(s.skipped), s.provers, s.shard_vk, s.agg_vk, u64be(s.chain_len)]);
}
export function encodeSegmentRecord(r) {
const pv = encodeBlockStatement(r.statement);
if (pv.length !== 340) throw new Error('statement is ' + pv.length + ' bytes');
const b = concat([u16le(r.version), u64le(r.first), u64le(r.last), r.block, r.pubkey, r.payout, pv, r.proof_hash, r.signature]);
if (b.length !== 586) throw new Error('record is ' + b.length + ' bytes');
return b;
}
const section = (items, tag) => concat([items, u32le(items.length), te.encode(tag)]);
// blue_score || subsidy || script version || script len || script || IGNS || IGNP || IGNF
export function buildCoinbasePayload({ blueScore = 1n, subsidy = 0n, script = new Uint8Array(0), minerBytes = new Uint8Array(0), records = [], ignp = new Uint8Array(0), ignf = new Uint8Array(0) }) {
return concat([u64le(blueScore), u64le(subsidy), u16le(0), new Uint8Array([script.length]), script, minerBytes,
section(concat(records), 'IGNS'), section(ignp, 'IGNP'), section(ignf, 'IGNF')]);
}
// ---- keyed BLAKE2b-256 the way the contract drives the EIP-152 compression function -------------------------
const MASK = (1n << 64n) - 1n;
const IV = [0x6a09e667f3bcc908n, 0xbb67ae8584caa73bn, 0x3c6ef372fe94f82bn, 0xa54ff53a5f1d36f1n, 0x510e527fade682d1n, 0x9b05688c2b3e6c1fn, 0x1f83d9abfb41bd6bn, 0x5be0cd19137e2179n];
const SIGMA = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
[11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
[9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
[12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
[6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
];
const rotr = (x, n) => ((x >> n) | (x << (64n - n))) & MASK;
// The compression function F(h, m, t, f) with 12 rounds, exactly what the precompile computes.
export function blake2bF(h, m, t0, t1, f) {
const v = new Array(16);
for (let i = 0; i < 8; i++) { v[i] = h[i]; v[i + 8] = IV[i]; }
v[12] ^= t0; v[13] ^= t1; if (f) v[14] ^= MASK;
const G = (a, b, c, d, x, y) => {
v[a] = (v[a] + v[b] + x) & MASK; v[d] = rotr(v[d] ^ v[a], 32n); v[c] = (v[c] + v[d]) & MASK; v[b] = rotr(v[b] ^ v[c], 24n);
v[a] = (v[a] + v[b] + y) & MASK; v[d] = rotr(v[d] ^ v[a], 16n); v[c] = (v[c] + v[d]) & MASK; v[b] = rotr(v[b] ^ v[c], 63n);
};
for (let r = 0; r < 12; r++) {
const s = SIGMA[r % 10];
G(0, 4, 8, 12, m[s[0]], m[s[1]]); G(1, 5, 9, 13, m[s[2]], m[s[3]]); G(2, 6, 10, 14, m[s[4]], m[s[5]]); G(3, 7, 11, 15, m[s[6]], m[s[7]]);
G(0, 5, 10, 15, m[s[8]], m[s[9]]); G(1, 6, 11, 12, m[s[10]], m[s[11]]); G(2, 7, 8, 13, m[s[12]], m[s[13]]); G(3, 4, 9, 14, m[s[14]], m[s[15]]);
}
for (let i = 0; i < 8; i++) h[i] ^= v[i] ^ v[i + 8];
}
const wordsLE = block => { const dv = new DataView(block.buffer, block.byteOffset, 128); const m = []; for (let i = 0; i < 16; i++) m.push(dv.getBigUint64(i * 8, true)); return m; };
// Same driver as Blake2b.sol: parameter block, the key as the first block (t = 128), then the message blocks.
export function keyedBlake2b256(key, data) {
const h = IV.slice();
h[0] ^= 0x01010000n ^ (BigInt(key.length) << 8n) ^ 32n;
const kb = new Uint8Array(128); kb.set(key);
blake2bF(h, wordsLE(kb), 128n, 0n, data.length === 0);
let done = 0;
while (done < data.length) {
const take = Math.min(128, data.length - done);
const mb = new Uint8Array(128); mb.set(data.subarray(done, done + take));
done += take;
blake2bF(h, wordsLE(mb), 128n + BigInt(done), 0n, done === data.length);
}
const out = new Uint8Array(32); const dv = new DataView(out.buffer);
for (let i = 0; i < 4; i++) dv.setBigUint64(i * 8, h[i], true);
return out;
}
export const nobleKeyed = (key, data) => blake2b(data, { dkLen: 32, key });
// ---- a Merkle Patricia trie builder (secure trie, 32-byte keys) for synthetic vectors --------------------------
const nibblesOf = b => { const out = []; for (const x of b) out.push(x >> 4, x & 15); return out; };
function hp(nibs, leaf) {
const odd = nibs.length & 1; const flag = (leaf ? 2 : 0) | odd;
const out = []; let i = 0;
if (odd) { out.push((flag << 4) | nibs[0]); i = 1; } else out.push(flag << 4);
for (; i < nibs.length; i += 2) out.push((nibs[i] << 4) | nibs[i + 1]);
return new Uint8Array(out);
}
const common = (a, b) => { let i = 0; while (i < a.length && i < b.length && a[i] === b[i]) i++; return i; };
export class Trie {
constructor() { this.root = null; }
put(keyBytes, valueBytes) { this.root = this._put(this.root, nibblesOf(keyBytes), valueBytes); }
_put(n, path, value) {
if (!n) return { kind: 'leaf', path, value };
if (n.kind === 'leaf') {
const cp = common(n.path, path);
if (cp === n.path.length && cp === path.length) return { kind: 'leaf', path, value };
const br = { kind: 'branch', children: new Array(16).fill(null), value: null };
const place = (p, v) => { if (p.length === cp) br.value = v; else br.children[p[cp]] = { kind: 'leaf', path: p.slice(cp + 1), value: v }; };
place(n.path, n.value); place(path, value);
return cp ? { kind: 'ext', path: path.slice(0, cp), child: br } : br;
}
if (n.kind === 'ext') {
const cp = common(n.path, path);
if (cp === n.path.length) return { kind: 'ext', path: n.path, child: this._put(n.child, path.slice(cp), value) };
const br = { kind: 'branch', children: new Array(16).fill(null), value: null };
const rem = n.path.slice(cp);
br.children[rem[0]] = rem.length === 1 ? n.child : { kind: 'ext', path: rem.slice(1), child: n.child };
if (path.length === cp) br.value = value; else br.children[path[cp]] = { kind: 'leaf', path: path.slice(cp + 1), value };
return cp ? { kind: 'ext', path: path.slice(0, cp), child: br } : br;
}
if (path.length === 0) return { ...n, value };
const children = n.children.slice();
children[path[0]] = this._put(children[path[0]], path.slice(1), value);
return { kind: 'branch', children, value: n.value };
}
encode(n) {
if (n.kind === 'leaf') return rlpEncodeRaw([hp(n.path, true), n.value]);
if (n.kind === 'ext') return rlpEncodeRaw([hp(n.path, false), this.ref(n.child)]);
return rlpEncodeRaw([...n.children.map(c => (c ? this.ref(c) : new Uint8Array(0))), n.value || new Uint8Array(0)]);
}
// A child reference: the keccak of its encoding, or the encoding itself (spliced in raw) when shorter than 32 bytes.
ref(n) { const e = this.encode(n); return e.length < 32 ? { __raw: e } : keccak_256(e); }
rootHash() { return this.root ? keccak_256(this.encode(this.root)) : keccak_256(rlpEncode(new Uint8Array(0))); }
// The proof nodes for a key, root first: every hash-referenced node on the path (embedded ones travel inside their parent).
proof(keyBytes) {
const out = []; let n = this.root; let path = nibblesOf(keyBytes); let embedded = false;
while (n) {
const e = this.encode(n);
if (!embedded) out.push(e);
let c;
if (n.kind === 'leaf') break;
if (n.kind === 'ext') { if (common(n.path, path) !== n.path.length) break; path = path.slice(n.path.length); c = n.child; }
else { if (!path.length) break; c = n.children[path[0]]; path = path.slice(1); if (!c) break; }
embedded = this.encode(c).length < 32; n = c;
}
return out;
}
}
// rlpEncode in core.js knows only bytes and lists; this one also splices a {__raw} item in as it is.
export function rlpEncodeRaw(item) {
if (item && item.__raw) return item.__raw;
if (item instanceof Uint8Array) return rlpEncode(item);
const body = concat(item.map(rlpEncodeRaw));
const n = body.length;
const prefix = n < 56 ? new Uint8Array([0xc0 + n]) : (() => { const bytes = []; let x = n; while (x > 0) { bytes.unshift(x & 0xff); x = Math.floor(x / 256); } return new Uint8Array([0xf7 + bytes.length, ...bytes]); })();
return concat([prefix, body]);
}
export const EMPTY_ROOT = '56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421';
export const encodeAccount = a => rlpEncode([trimBig(a.nonce), trimBig(a.balance), hexToBytes(strip(a.storageRoot)), hexToBytes(strip(a.codeHash))]);
// ---- RPC plumbing ----------------------------------------------------------------------------------------------
export const sepolia = defineChain({ id: 0xaa36a7, name: 'Sepolia', nativeCurrency: { name: 'Sepolia Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: [RPC] } } });
export const publicClient = () => createPublicClient({ chain: sepolia, transport: http(RPC, { timeout: 120_000 }) });
export function deployerAccount() {
const k = JSON.parse(readFileSync(path.join(process.env.HOME, '.config/igneum/sepolia-deployer'), 'utf8'));
return privateKeyToAccount(k.private_key);
}
export const walletClient = account => createWalletClient({ account, chain: sepolia, transport: http(RPC, { timeout: 120_000 }) });
export const artifact = name => JSON.parse(readFileSync(path.join(here, 'artifacts', name + '.json'), 'utf8'));
export const deployment = () => JSON.parse(readFileSync(path.join(here, 'deployment.json'), 'utf8'));
export const ukTime = () => new Date().toLocaleTimeString('en-GB', { timeZone: 'Europe/London', hour: '2-digit', minute: '2-digit', second: '2-digit' });
// The revert reason out of a viem error, or the message's first line.
export function reasonOf(e) {
const m = String(e.shortMessage || e.message || e);
const r = /reason:\s*([^\n]+)/.exec(String(e.message || '')) || /reverted with the following reason:\s*([^\n]+)/.exec(String(e.message || ''));
return r ? r[1].trim() : m.split('\n')[0];
}
// ---- a synthetic balance proof: a state trie, a segment record, a coinbase, a merkle path and a two-header path --
// Everything the contract checks is real; only the aggregator's signature and the certificate are made up, which is
// what the stub verifier accepts. The checkpoint index is 9002 so it never collides with a real certificate (9001 holds an earlier synthetic vector).
export function buildSynthetic(evmChainId = EVM_CHAIN_ID, seed = 0x1a2b3c4d) {
const randomBytes = seeded(seed);
const address = randomBytes(20);
const storage = new Trie();
const slot1 = new Uint8Array(32); slot1[31] = 1;
const slot5 = new Uint8Array(32); slot5[31] = 5;
const v1 = hexToBytes('abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789');
storage.put(keccak_256(slot1), rlpEncode(v1));
storage.put(keccak_256(slot5), rlpEncode(trimBig(42n)));
for (let i = 0; i < 6; i++) storage.put(keccak_256(randomBytes(32)), rlpEncode(trimBig(BigInt(1000 + i))));
const account = { nonce: 7n, balance: 123456789n * 10n ** 15n, storageRoot: bytesToHex(storage.rootHash()), codeHash: bytesToHex(keccak_256(new Uint8Array(0))) };
const state = new Trie();
state.put(keccak_256(address), encodeAccount(account));
for (let i = 0; i < 9; i++) state.put(keccak_256(randomBytes(20)), encodeAccount({ nonce: BigInt(i), balance: BigInt(i) * 10n ** 18n, storageRoot: EMPTY_ROOT, codeHash: account.codeHash }));
const postRoot = state.rootHash();
const number = 9_000_027_039n; // far above any Devnet 3 block number, so it never collides with a real record
const blockHash = randomBytes(32);
const record = encodeSegmentRecord({
version: 2, first: number - 3n, last: number, block: blockHash, pubkey: randomBytes(48), payout: randomBytes(20),
statement: { chain_id: BigInt(evmChainId), number, block_hash: blockHash, parent_hash: randomBytes(32), shard_count: 1, tx_commitment: randomBytes(32), pre_root: randomBytes(32), post_root: postRoot, receipts: randomBytes(32), gas_used: 21000n, pgas_used: 0n, executed: 1, skipped: 0, provers: randomBytes(32), shard_vk: randomBytes(32), agg_vk: randomBytes(32), chain_len: 4n },
proof_hash: randomBytes(32), signature: randomBytes(96),
});
const payload = buildCoinbasePayload({ blueScore: 62371n, subsidy: 50000000n, script: hexToBytes('20' + bytesToHex(randomBytes(32)) + 'ac'), minerBytes: te.encode('synthetic'), records: [record], ignf: randomBytes(40) });
const coinbase = { version: 1, inputs: [], outputs: [{ value: '50000000', scriptPublicKey: '0000' + '20' + bytesToHex(randomBytes(32)) + 'ac' }], lockTime: 0, subnetworkId: '0100000000000000000000000000000000000000', gas: 0, payload: bytesToHex(payload), mass: 0 };
const cbBytes = serializeCoinbase(coinbase);
const leaf = coinbaseTxHash(coinbase, blake2b);
const leaves = [leaf, randomBytes(32), randomBytes(32)];
const H = d => blake2b(d, { dkLen: 32, key: te.encode('MerkleBranchHash') });
const siblings = [leaves[1], H(concat([leaves[2], new Uint8Array(32)]))];
const merkle = merkleRootFromPath(leaf, 0, siblings, blake2b);
const mk = (merkleRoot, parents, blueScore) => ({ version: 1538, parents_by_level: [parents, parents], hash_merkle_root: bytesToHex(merkleRoot), accepted_id_merkle_root: bytesToHex(randomBytes(32)), utxo_commitment: bytesToHex(randomBytes(32)), timestamp: '1759900000000', bits: 486805716, nonce: '1234567890123', daa_score: '62684', blue_work: '00000000000000000000000000000000000020dfbc77ac31', blue_score: String(blueScore), pruning_point: bytesToHex(randomBytes(32)), vote_key_hash: bytesToHex(randomBytes(32)) });
const carrier = mk(merkle, [bytesToHex(randomBytes(32))], 62371); carrier.hash = headerHash(carrier, blake2b);
const mid = mk(randomBytes(32), [carrier.hash, bytesToHex(randomBytes(32))], 62372); mid.hash = headerHash(mid, blake2b);
const top = mk(randomBytes(32), [bytesToHex(randomBytes(32)), mid.hash], 62373); top.hash = headerHash(top, blake2b);
return {
kind: 'synthetic', certIndex: 9002n, checkpoint: top.hash, headers: [carrier, mid, top], coinbase, coinbaseBytes: cbBytes, leafIndex: 0, siblings, recordIndex: 0,
number, postRoot, blockHash, address, account, accountProof: state.proof(keccak_256(address)),
slots: [{ slot: slot1, value: v1, proof: storage.proof(keccak_256(slot1)) }, { slot: slot5, value: trimBig(42n), proof: storage.proof(keccak_256(slot5)) }],
absent: (() => { const s = new Uint8Array(32); s[31] = 9; return { slot: s, proof: storage.proof(keccak_256(s)) }; })(),
};
}
// ---- contract arguments and the two writes --------------------------------------------------------------------
export function proofArgs(v) {
return [v.certIndex, v.headers.map(h => hex0x(serializeHeader(h))), hex0x(v.coinbaseBytes), BigInt(v.leafIndex), v.siblings.map(hex0x), BigInt(v.recordIndex)];
}
// Appends a write to deployment.json so every transaction is on record.
export function recordWrite(entry) {
const p = path.join(here, 'deployment.json');
const d = JSON.parse(readFileSync(p, 'utf8'));
d.writes = d.writes || [];
d.writes.push({ at: new Date().toISOString(), ...entry });
writeFileSync(p, JSON.stringify(d, null, 1) + '\n');
}
// The public RPC is a pool of nodes; a read right after a receipt can land on one that lags. Poll until it agrees.
async function untilVisible(check, what) {
for (let i = 0; i < 30; i++) { if (await check()) return; await new Promise(r => setTimeout(r, 1000)); }
throw new Error(what + ' is not visible on the RPC 30 s after its receipt');
}
export async function ensureCertificate(pub, wallet, d, index, checkpoint, bitmap = '0x', signature = '0x') {
const stubAbi = artifact('StubCertificateVerifier').abi;
const verifierAddr = await pub.readContract({ address: d.oracle.address, abi: artifact('IgneumStateOracle').abi, functionName: 'verifier' });
const same = async () => (await pub.readContract({ address: verifierAddr, abi: stubAbi, functionName: 'finalCheckpoint', args: [index] })).toLowerCase() === checkpoint.toLowerCase();
if (await same()) return { verifier: verifierAddr, already: true };
const hash = await wallet.writeContract({ address: verifierAddr, abi: stubAbi, functionName: 'submitCertificate', args: [index, checkpoint, bitmap, signature] });
const r = await pub.waitForTransactionReceipt({ hash, timeout: 180_000 });
if (r.status !== 'success') throw new Error('submitCertificate reverted in ' + hash);
await untilVisible(same, 'certificate ' + index);
const out = { verifier: verifierAddr, tx: hash, block: Number(r.blockNumber), gasUsed: Number(r.gasUsed) };
recordWrite({ what: 'submitCertificate', index: String(index), checkpoint, ...out });
return out;
}
export async function ensureStateRoot(pub, wallet, d, v) {
const abi = artifact('IgneumStateOracle').abi;
try {
const [root] = await pub.readContract({ address: d.oracle.address, abi, functionName: 'stateRoot', args: [v.number] });
if (root.toLowerCase() === hex0x(v.postRoot).toLowerCase()) return { already: true };
} catch {}
const args = proofArgs(v);
const gas = await pub.estimateContractGas({ address: d.oracle.address, abi, functionName: 'submitStateRoot', args, account: wallet.account });
const hash = await wallet.writeContract({ address: d.oracle.address, abi, functionName: 'submitStateRoot', args, gas: gas + gas / 5n });
const r = await pub.waitForTransactionReceipt({ hash, timeout: 180_000 });
if (r.status !== 'success') throw new Error('submitStateRoot reverted in ' + hash);
await untilVisible(async () => { try { const [root] = await pub.readContract({ address: d.oracle.address, abi, functionName: 'stateRoot', args: [v.number] }); return root.toLowerCase() === hex0x(v.postRoot).toLowerCase(); } catch { return false; } }, 'state root ' + v.number);
const out = { tx: hash, block: Number(r.blockNumber), gasUsed: Number(r.gasUsed), calldataBytes: args[1].reduce((a, h) => a + (h.length - 2) / 2, 0) + (args[2].length - 2) / 2, headers: args[1].length };
recordWrite({ what: 'submitStateRoot', vector: v.kind, number: String(v.number), postRoot: hex0x(v.postRoot), certIndex: String(v.certIndex), ...out });
return out;
}
// ---- the balance fixture (the light service's /balance body, with the certificate beside it) -------------------
// Accepts the flat /balance body, or {proof, certificate} / {balance, checkpoint} wrappers; the certificate's bitmap
// and signature come from the fixture when present, else from dn3-checkpoint.json when its index matches.
export function vectorFromBalanceFixture(fx) {
const p = fx.proof || fx.balance || fx;
let cert = fx.certificate || (p.checkpoint && p.checkpoint.certificate) || fx.checkpoint_certificate || null;
if (!cert) { try { const c = JSON.parse(readFileSync(path.join(fixtures, 'dn3-checkpoint.json'), 'utf8')); if (Number(c.index) === Number(p.checkpoint.index)) cert = c; } catch {} }
const c = cert && cert.certificate ? cert.certificate : cert || {};
const records = segmentRecordsOf(hexToBytes(strip(p.carrier.coinbase.payload)));
const recordIndex = records.findIndex(r => bytesToHex(r.bytes) === strip(p.segment_record_hex));
if (recordIndex < 0) throw new Error('the carrier coinbase carries ' + records.length + ' record(s), none is segment_record_hex');
const st = records[recordIndex].statement;
const acct = p.account;
return {
kind: 'devnet-3', certIndex: BigInt(p.checkpoint.index), checkpoint: strip(p.checkpoint.hash), headers: p.headers,
bitmap: c.bitmap_hex ? '0x' + strip(c.bitmap_hex) : '0x', signature: c.aggregate_signature_hex ? '0x' + strip(c.aggregate_signature_hex) : '0x',
coinbase: p.carrier.coinbase, coinbaseBytes: serializeCoinbase(p.carrier.coinbase, p.carrier.amount_wire_len || 8),
leafIndex: Number(p.carrier.leaf_index), siblings: p.carrier.merkle_siblings.map(x => hexToBytes(strip(x))), recordIndex,
number: st.number, postRoot: hexToBytes(st.post_root), blockHash: hexToBytes(st.block_hash), evmChainId: Number(st.chain_id),
address: hexToBytes(strip(p.address)), balance: BigInt(acct.balance), nonce: BigInt(acct.nonce || 0), accountProof: acct.accountProof.map(x => hexToBytes(strip(x))),
reportedStateRoot: acct.stateRoot ? strip(acct.stateRoot) : null, reportedBlock: acct.blockNumber != null ? Number(acct.blockNumber) : null,
storageProofs: (acct.storageProof || []).map(sp => ({ slot: hexToBytes(strip(sp.key).padStart(64, '0')), value: BigInt(sp.value), proof: sp.proof.map(x => hexToBytes(strip(x))) })),
};
}