345 lines
25 KiB
JavaScript
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))) })),
|
|
};
|
|
}
|
|
|