134 lines
13 KiB
JavaScript
134 lines
13 KiB
JavaScript
// The oracle's tests against Sepolia. Every negative runs through eth_call (no gas). The only transactions are the
|
|
// one-time stub certificate for the synthetic vector and its submitStateRoot, skipped when already on chain.
|
|
// Part A: the receipt fixture (real Devnet 3 headers, a real merkle path with an EVM transaction leaf)
|
|
// Part B: the synthetic vector (segment record, coinbase, two-header path, MPT account and storage proofs)
|
|
// Part C: the balance fixture when it exists (real segment record and eth_getProof)
|
|
import { readFileSync, existsSync } from 'node:fs';
|
|
import * as L from './lib.mjs';
|
|
|
|
const d = L.deployment();
|
|
const pub = L.publicClient();
|
|
const abi = L.artifact('IgneumStateOracle').abi;
|
|
const oracle = d.oracle.address;
|
|
const read = (functionName, args) => pub.readContract({ address: oracle, abi, functionName, args });
|
|
let pass = 0, fail = 0;
|
|
const ok = (name, cond, detail = '') => { cond ? pass++ : fail++; console.log((cond ? 'PASS ' : 'FAIL ') + name + (detail ? ' (' + detail + ')' : '')); };
|
|
async function expectRevert(name, fn, want) {
|
|
try { await fn(); ok(name, false, 'did not revert'); }
|
|
catch (e) { const r = L.reasonOf(e); ok(name, !want || r.includes(want), 'reverted: ' + r); }
|
|
}
|
|
const clone = x => JSON.parse(JSON.stringify(x));
|
|
const flipByte = (hex, i) => { const b = L.hexToBytes(L.strip(hex)); b[i] ^= 0x01; return L.hex0x(b); };
|
|
console.log(L.ukTime(), 'UK oracle', oracle, 'verifier', await read('verifier'));
|
|
|
|
// ---- Part A: the receipt fixture --------------------------------------------------------------------------------
|
|
const f = JSON.parse(readFileSync(L.fixtures + '/dn3-receipt.json'));
|
|
const H = f.headers.map(h => L.hex0x(L.serializeHeader(h)));
|
|
const cp = '0x' + L.strip(f.checkpoint.hash);
|
|
{
|
|
let same = 0;
|
|
for (let i = 0; i < H.length; i += 8) {
|
|
const got = await Promise.all(H.slice(i, i + 8).map(h => read('headerHash', [h])));
|
|
got.forEach((g, k) => { if (L.strip(g) === f.headers[i + k].hash) same++; });
|
|
}
|
|
ok('A1 headerHash on chain matches all fixture headers', same === H.length, same + ' of ' + H.length);
|
|
const [carrier, merkle] = await read('checkHeaderPath', [H, cp]);
|
|
ok('A2 checkHeaderPath accepts the 62-header path to checkpoint ' + f.checkpoint.index, L.strip(carrier) === f.headers[0].hash && L.strip(merkle) === f.headers[0].hash_merkle_root);
|
|
console.log(' gas estimate for the view', await pub.estimateContractGas({ address: oracle, abi, functionName: 'checkHeaderPath', args: [H, cp], account: d.deployer }));
|
|
await expectRevert('A3 a header removed from the path', () => read('checkHeaderPath', [H.filter((_, i) => i !== 30), cp]), 'does not name the previous one');
|
|
const altered = clone(f.headers[5]); altered.nonce = String(BigInt(altered.nonce) + 1n);
|
|
await expectRevert('A4 a header with its nonce altered', () => read('checkHeaderPath', [H.map((h, i) => (i === 5 ? L.hex0x(L.serializeHeader(altered)) : h)), cp]), 'does not name the previous one');
|
|
await expectRevert('A5 the right path to the wrong checkpoint', () => read('checkHeaderPath', [H, flipByte(cp, 0)]), 'not the certified checkpoint');
|
|
await expectRevert('A6 the path reversed', () => read('checkHeaderPath', [H.slice().reverse(), cp]), 'does not name the previous one');
|
|
const ib = f.including_block; const sib = ib.merkle_siblings.map(s => '0x' + s);
|
|
const root = await read('merkleRoot', ['0x' + f.tx_hash, BigInt(ib.leaf_index), sib]);
|
|
ok('A7 merkleRoot reaches hash_merkle_root (leaf ' + ib.leaf_index + ' of ' + ib.leaf_count + ')', L.strip(root) === f.headers[0].hash_merkle_root);
|
|
const root2 = await read('merkleRoot', ['0x' + f.tx_hash, BigInt(ib.leaf_index), sib.map((s, i) => (i === 2 ? flipByte(s, 3) : s))]);
|
|
ok('A8 a sibling altered gives another root', L.strip(root2) !== f.headers[0].hash_merkle_root);
|
|
const root3 = await read('merkleRoot', ['0x' + f.tx_hash, BigInt(ib.leaf_index ^ 1), sib]);
|
|
ok('A9 the wrong leaf index gives another root', L.strip(root3) !== f.headers[0].hash_merkle_root);
|
|
await expectRevert('A10 a leaf index beyond the path', () => read('merkleRoot', ['0x' + f.tx_hash, 1n << 40n, sib]), 'beyond the path');
|
|
ok('A11 keccak of the raw transaction is the leaf', L.bytesToHex(L.keccak_256(L.hexToBytes(f.raw_tx_hex))) === f.tx_hash);
|
|
}
|
|
|
|
// ---- Part B: the synthetic vector -----------------------------------------------------------------------------
|
|
// The synthetic certificate is accepted by the stub verifier only; with the shared verifier installed (a real BLS check)
|
|
// part B's positive case cannot be recorded, so the part is skipped and part C (the real proof) carries the positive.
|
|
const S = L.buildSynthetic(L.EVM_CHAIN_ID);
|
|
const wallet = L.walletClient(L.deployerAccount());
|
|
const installedVerifier = String(await read('verifier')).toLowerCase();
|
|
const stubAddress = String((d.stub && d.stub.address) || (d.verifier && d.verifier.address) || '').toLowerCase();
|
|
if (installedVerifier !== stubAddress) console.log(' Part B skipped: the installed verifier', installedVerifier, 'is not the stub (synthetic certificates are refused by a real BLS check)');
|
|
else {
|
|
const c = await L.ensureCertificate(pub, wallet, d, S.certIndex, L.hex0x(L.hexToBytes(S.checkpoint)));
|
|
console.log(' stub certificate', S.certIndex, c.already ? 'already recorded' : 'recorded in ' + c.tx + ' block ' + c.block + ' gas ' + c.gasUsed);
|
|
const args = L.proofArgs(S);
|
|
const [number, postRoot, blockHash] = await read('verifyStateRoot', args);
|
|
ok('B1 verifyStateRoot accepts the synthetic proof', number === S.number && L.strip(postRoot) === L.bytesToHex(S.postRoot) && L.strip(blockHash) === L.bytesToHex(S.blockHash), 'block ' + number + ' post_root ' + postRoot.slice(0, 14));
|
|
console.log(' gas estimate for the view', await pub.estimateContractGas({ address: oracle, abi, functionName: 'verifyStateRoot', args, account: d.deployer }));
|
|
const withArgs = (patch) => { const a = args.slice(); patch(a); return read('verifyStateRoot', a); };
|
|
await expectRevert('B2 a certificate index the verifier does not hold', () => withArgs(a => { a[0] = 9999n; }), 'no certificate');
|
|
await expectRevert('B3 the middle header removed', () => withArgs(a => { a[1] = [a[1][0], a[1][2]]; }), 'does not name the previous one');
|
|
const carrierAltered = clone(S.headers[0]); carrierAltered.nonce = '1';
|
|
await expectRevert('B4 the carrier header with its nonce altered', () => withArgs(a => { a[1] = [L.hex0x(L.serializeHeader(carrierAltered)), a[1][1], a[1][2]]; }), 'does not name the previous one');
|
|
const topAltered = clone(S.headers[2]); topAltered.timestamp = String(BigInt(topAltered.timestamp) + 1n);
|
|
await expectRevert('B5 the checkpoint header altered', () => withArgs(a => { a[1] = [a[1][0], a[1][1], L.hex0x(L.serializeHeader(topAltered))]; }), 'not the certified checkpoint');
|
|
await expectRevert('B6 a merkle sibling altered', () => withArgs(a => { a[4] = [flipByte(a[4][0], 0), a[4][1]]; }), 'hash_merkle_root');
|
|
await expectRevert('B7 the wrong leaf index', () => withArgs(a => { a[3] = 1n; }), 'hash_merkle_root');
|
|
const cb = L.hexToBytes(L.strip(args[2]));
|
|
const postRootOff = (() => { const want = L.bytesToHex(S.postRoot); const h = L.bytesToHex(cb); return h.indexOf(want) / 2; })();
|
|
const tampered = cb.slice(); tampered[postRootOff + 5] ^= 0xff;
|
|
await expectRevert('B8 the record\'s post_root altered inside the coinbase (offset ' + postRootOff + ')', () => withArgs(a => { a[2] = L.hex0x(tampered); }), 'hash_merkle_root');
|
|
await expectRevert('B9 a record index beyond the IGNS section', () => withArgs(a => { a[5] = 1n; }), 'beyond the section');
|
|
const tamperedNumber = cb.slice(); tamperedNumber[postRootOff - 148 + 8 + 7] ^= 0x01;
|
|
await expectRevert('B10 the statement number altered', () => withArgs(a => { a[2] = L.hex0x(tamperedNumber); }), 'hash_merkle_root');
|
|
const r = await L.ensureStateRoot(pub, wallet, d, S);
|
|
console.log(' synthetic root for block', S.number, r.already ? 'already stored' : 'stored in ' + r.tx + ' block ' + r.block + ' gas ' + r.gasUsed + ' calldata ' + r.calldataBytes + ' bytes');
|
|
const [root, bh, carrier, certIndex] = await read('stateRoot', [S.number]);
|
|
ok('B11 stateRoot stored', L.strip(root) === L.bytesToHex(S.postRoot) && certIndex === S.certIndex && L.strip(carrier) === S.headers[0].hash);
|
|
const addr = L.hex0x(S.address); const ap = S.accountProof.map(L.hex0x);
|
|
const bal = await read('provenBalance', [S.number, addr, ap]);
|
|
ok('B12 provenBalance returns the trie balance', bal === S.account.balance, bal + ' wei');
|
|
console.log(' gas estimate for provenBalance', await pub.estimateContractGas({ address: oracle, abi, functionName: 'provenBalance', args: [S.number, addr, ap], account: d.deployer }));
|
|
const acct = await read('provenAccount', [S.number, addr, ap]);
|
|
ok('B13 provenAccount nonce, storage root and code hash', acct[0] === true && acct[1] === 7n && L.strip(acct[3]) === S.account.storageRoot && L.strip(acct[4]) === S.account.codeHash);
|
|
for (const s of S.slots) {
|
|
const v = await read('provenStorage', [S.number, addr, L.hex0x(s.slot), ap, s.proof.map(L.hex0x)]);
|
|
ok('B14 provenStorage slot ' + s.slot[31], L.strip(v).replace(/^0+/, '') === L.bytesToHex(s.value).replace(/^0+/, ''), v);
|
|
}
|
|
const absent = await read('provenStorage', [S.number, addr, L.hex0x(S.absent.slot), ap, S.absent.proof.map(L.hex0x)]);
|
|
ok('B15 provenStorage of an unset slot reads zero (exclusion proof)', BigInt(absent) === 0n);
|
|
await expectRevert('B16 a flipped byte in the account proof\'s last node', () => read('provenBalance', [S.number, addr, ap.map((n, i) => (i === ap.length - 1 ? flipByte(n, 10) : n))]), 'mpt');
|
|
await expectRevert('B17 a flipped byte in the root node', () => read('provenBalance', [S.number, addr, ap.map((n, i) => (i === 0 ? flipByte(n, 40) : n))]), 'not the root');
|
|
await expectRevert('B18 the wrong block number', () => read('provenBalance', [S.number + 1n, addr, ap]), 'no proven root');
|
|
await expectRevert('B19 the proof with its nodes reordered', () => read('provenBalance', [S.number, addr, ap.slice().reverse()]), 'not the root');
|
|
await expectRevert('B20 a storage proof with a flipped node', () => read('provenStorage', [S.number, addr, L.hex0x(S.slots[0].slot), ap, S.slots[0].proof.map((n, i) => (i === 1 ? flipByte(L.hex0x(n), 5) : L.hex0x(n)))]), 'mpt');
|
|
await expectRevert('B21 the account proof used as a storage proof', () => read('provenStorage', [S.number, addr, L.hex0x(S.slots[0].slot), ap, ap]), 'not the root');
|
|
const other = L.hex0x(L.seeded(99)(20));
|
|
await expectRevert('B22 another address under this account proof', () => read('provenBalance', [S.number, other, ap]), 'mpt');
|
|
}
|
|
|
|
// ---- Part C: the balance fixture ------------------------------------------------------------------------------
|
|
if (existsSync(L.fixtures + '/dn3-balance.json')) {
|
|
const V = L.vectorFromBalanceFixture(JSON.parse(readFileSync(L.fixtures + '/dn3-balance.json')));
|
|
const c = await L.ensureCertificate(pub, wallet, d, V.certIndex, L.hex0x(L.hexToBytes(V.checkpoint)), V.bitmap, V.signature);
|
|
console.log(' certificate', V.certIndex, c.already ? 'already recorded' : 'recorded in ' + c.tx);
|
|
const args = L.proofArgs(V);
|
|
const [number, postRoot] = await read('verifyStateRoot', args);
|
|
ok('C1 verifyStateRoot accepts the real Devnet 3 proof', number === V.number && L.strip(postRoot) === L.bytesToHex(V.postRoot), 'block ' + number);
|
|
const r = await L.ensureStateRoot(pub, wallet, d, V);
|
|
console.log(' real root for block', V.number, r.already ? 'already stored' : 'stored in ' + r.tx + ' gas ' + r.gasUsed);
|
|
const bal = await read('provenBalance', [V.number, L.hex0x(V.address), V.accountProof.map(L.hex0x)]);
|
|
ok('C2 provenBalance equals the node\'s eth_getProof balance', bal === V.balance, bal + ' wei');
|
|
await expectRevert('C3 a flipped byte in the real account proof', () => read('provenBalance', [V.number, L.hex0x(V.address), V.accountProof.map((n, i) => (i === V.accountProof.length - 1 ? flipByte(L.hex0x(n), 10) : L.hex0x(n)))]), 'mpt');
|
|
const realAltered = clone(V.headers[0]); realAltered.nonce = String(BigInt(realAltered.nonce) + 1n);
|
|
await expectRevert('C4 the real carrier header with its nonce altered', () => read('verifyStateRoot', [args[0], [L.hex0x(L.serializeHeader(realAltered)), ...args[1].slice(1)], ...args.slice(2)]), V.headers.length === 1 ? 'not the certified checkpoint' : 'does not name');
|
|
await expectRevert('C5 the real path emptied', () => read('verifyStateRoot', [args[0], [], ...args.slice(2)]), 'headers: none');
|
|
await expectRevert('C6 the real certificate index off by one', () => read('verifyStateRoot', [args[0] + 1n, ...args.slice(1)]), 'no certificate');
|
|
const cbReal = L.hexToBytes(L.strip(args[2])); const off = L.bytesToHex(cbReal).indexOf(L.bytesToHex(V.postRoot)) / 2; cbReal[off + 3] ^= 0x80;
|
|
await expectRevert('C7 the real record\'s post_root altered inside the coinbase (offset ' + off + ')', () => read('verifyStateRoot', [...args.slice(0, 2), L.hex0x(cbReal), ...args.slice(3)]), 'hash_merkle_root');
|
|
} else {
|
|
console.log(' Part C skipped: fixtures/dn3-balance.json is not there yet');
|
|
}
|
|
console.log(L.ukTime(), 'UK', pass, 'passed,', fail, 'failed');
|
|
process.exit(fail ? 1 : 0);
|