Merge the mirror's master into counter-asic-4 before the third landing

This commit is contained in:
igneum-labs 2026-10-08 15:28:43 +00:00
commit 03029526da
9 changed files with 528 additions and 25 deletions

File diff suppressed because one or more lines are too long

View file

@ -6,7 +6,7 @@
import { blake2b } from 'https://cdn.jsdelivr.net/npm/@noble/hashes@2.4.0/blake2.js/+esm';
import { keccak_256 } from 'https://cdn.jsdelivr.net/npm/@noble/hashes@2.4.0/sha3.js/+esm';
import { bls12_381 } from 'https://cdn.jsdelivr.net/npm/@noble/curves@2.4.0/bls12-381.js/+esm';
import { verifyBalance, verifyReceipt, formatIgn } from './core.js';
import { verifyBalance, verifyReceipt, verifyPaymentReceipt, formatIgn } from './core.js';
export const LIBRARIES = { '@noble/hashes': '2.4.0', '@noble/curves': '2.4.0' };
const deps = { blake2b, bls: bls12_381, keccak: keccak_256 };
@ -76,24 +76,28 @@ export async function runReceipt(tx) {
await new Promise(resolve => setTimeout(resolve, 20));
const bad = clone(receipt); bad.raw_tx_hex = flipHex(bad.raw_tx_hex, 40);
const neg = verifyReceipt(bad, deps);
const res = verifyReceipt(receipt, deps);
const res = verifyPaymentReceipt(receipt, deps);
if (res.payment) { receipt.kind = 'payment receipt'; receipt.authenticates = 'inclusion of the signed transaction in a finalised block and its successful execution outcome (status and logs) through the proven segment\'s receipts commitment'; }
let negPay = null;
if (res.payment) { const b2 = clone(receipt); const r0 = b2.segment.receipts[Number(b2.segment.receipt_position)]; r0.status = '0x0'; negPay = verifyPaymentReceipt(b2, deps); }
window.__igneumReceipt = { receipt, neg, res };
const t = res.tx || {};
const when = new Date(Number(res.block_time || 0)).toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
const top = res.verified
? `<div class="headline"><div class="eyebrow ember">Transaction inclusion receipt · included and finalised · Devnet 3, no value</div><div class="big">${esc(formatIgn(t.value || '0'))} <span>IGN</span></div>
? `<div class="headline"><div class="eyebrow ember">${res.payment ? 'Payment receipt · included, executed, proven and finalised' : 'Transaction inclusion receipt · included and finalised'} · Devnet 3, no value</div><div class="big">${esc(formatIgn(t.value || '0'))} <span>IGN</span></div>
${res.payment ? `<p class="pt"><b>Outcome authenticated:</b> ${esc(res.outcome.asset)}, ${esc(formatIgn(res.outcome.amount_wei))} IGN to ${esc(res.outcome.recipient || 'contract creation')}, executed with status success, through the receipts commitment of the proven segment ending at chain block ${esc(String(receipt.execution.chain_block))}.</p>` : `<p class="pt"><b>This is the inclusion receipt.</b> ${esc(res.payment_unavailable || receipt.payment_unavailable || 'the outcome is not authenticated')}${res.receipt_status === 'failed' ? ' The authenticated status is FAILED: no transfer took place.' : ''}</p>`}
<div class="kv"><div class="k">To</div><div class="mono">${esc(t.to || 'contract creation')}</div><div class="k">From</div><div class="mono">${esc(receipt.tx_as_reported.from)} <small>(as the node reports it; the signature is the chain's check)</small></div>
<div class="k">Transaction</div><div class="mono">0x${esc(receipt.tx_hash)}</div><div class="k">Block</div><div class="mono">${esc(res.block)} <small>at ${esc(when)}, DAA ${esc(res.block_daa)}</small></div>
<div class="k">Finality</div><div>checkpoint ${res.checkpoint}, ${esc(res.certificate)}; ${res.headers} headers from the block to the checkpoint, verified here in ${res.ms} ms</div>
${receipt.execution ? `<div class="k">Executed</div><div>status ${receipt.execution.status === '0x1' ? 'success' : 'failed'}, gas ${parseInt(receipt.execution.gas_used, 16)}, ${receipt.execution.logs} log(s) <small>(as the node reports it: executed is reported, not authenticated by this receipt; a payment receipt would authenticate the outcome)</small></div>` : ''}</div></div>`
${receipt.execution ? `<div class="k">Executed</div><div>status ${receipt.execution.status === '0x1' ? 'success' : 'failed'}, gas ${parseInt(receipt.execution.gas_used, 16)}, ${receipt.execution.logs} log(s) <small>${res.payment ? '(authenticated: the receipt sits in the shard receipts trie whose root the proven segment statement commits to)' : '(as the node reports it: executed is reported, not authenticated by this receipt; a payment receipt would authenticate the outcome)'}</small></div>` : ''}</div></div>`
: `<div class="headline bad"><div class="eyebrow">Not verified · Devnet 3, no value</div><p class="pt">${esc(res.reason)}</p></div>`;
out.innerHTML = top + negativeHtml('a copy of this receipt with one nibble of the raw transaction altered', neg) + stepsHtml(res)
+ (res.verified ? `<p><button class="btn" type="button" data-download>Download the inclusion receipt (JSON, ${Math.round(JSON.stringify(receipt).length / 1024)} KB)</button></p>
out.innerHTML = top + negativeHtml('a copy of this receipt with one nibble of the raw transaction altered', neg) + (negPay ? negativeHtml('a copy of this payment receipt with the receipt status flipped to failed', { verified: negPay.verified && negPay.payment, reason: negPay.reason }) : '') + stepsHtml(res)
+ (res.verified ? `<p><button class="btn" type="button" data-download>Download the ${res.payment ? 'payment' : 'inclusion'} receipt (JSON, ${Math.round(JSON.stringify(receipt).length / 1024)} KB)</button></p>
<p class="note">The file carries the raw transaction, the including block's header and merkle path, every header up to the certified checkpoint, the certificate and the voter table. Anyone re-verifies it offline with the one-file verifier: <code>node verify-receipt.js receipt.json</code> (<a href="/lc/verify-receipt.js" download>verify-receipt.js</a>, plain JavaScript, no npm, no network). A tampered file fails there the same way the copy above failed here.</p>` : '');
const dl = $('[data-download]');
if (dl) dl.addEventListener('click', () => {
const blob = new Blob([JSON.stringify(receipt, null, 1)], { type: 'application/json' });
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `igneum-inclusion-receipt-${receipt.tx_hash.slice(0, 12)}.json`; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 5000);
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `igneum-${res.payment ? 'payment' : 'inclusion'}-receipt-${receipt.tx_hash.slice(0, 12)}.json`; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 5000);
});
setStatus(res.verified ? 'verified' : 'refused', res.verified ? 'ok' : 'bad');
return res;

View file

@ -379,6 +379,138 @@ export function parseTx(raw) {
return { type: 0, nonce: bigOf(body[0]).toString(), to: body[3].length ? hex(body[3]) : null, value: bigOf(body[4]).toString(), data: hex(body[5]), gas: bigOf(body[2]).toString() };
}
// ---- the ordered receipts trie (reth's layout: key = RLP(index), value = the EIP-2718 receipt envelope) ----------
// Built in full from the shard's receipts, so the root is recomputed rather than proven: a shard is bounded by its pgas
// budget and holds at most a few hundred receipts.
function hpEncode(nibs, leaf) {
const odd = nibs.length % 2; const flag = (leaf ? 2 : 0) + odd;
const out = []; if (odd) out.push((flag << 4) | nibs[0]); else out.push(flag << 4, );
for (let i = odd; i < nibs.length; i += 2) out.push((nibs[i] << 4) | nibs[i + 1]);
return new Uint8Array(out);
}
function trieNode(items, keccak) {
// items: [{nibs, value}] sorted by nibs; returns the RLP of the node (inline when under 32 bytes, else its hash)
if (items.length === 0) return new Uint8Array(0);
const encode = node => { const enc = rlpEncode(node); return enc.length < 32 ? enc : keccak(enc); };
if (items.length === 1) return encode([hpEncode(items[0].nibs, true), items[0].value]);
let prefix = 0;
while (items.every(it => it.nibs.length > prefix && it.nibs[prefix] === items[0].nibs[prefix])) prefix++;
if (prefix > 0) {
const child = trieNode(items.map(it => ({ nibs: it.nibs.slice(prefix), value: it.value })), keccak);
return encode([hpEncode(items[0].nibs.slice(0, prefix), false), child.length === 32 && rlpEncode(child).length > 32 ? child : child]);
}
const branch = []; let value = new Uint8Array(0);
for (let b = 0; b < 16; b++) {
const sub = items.filter(it => it.nibs.length > 0 && it.nibs[0] === b).map(it => ({ nibs: it.nibs.slice(1), value: it.value }));
branch.push(sub.length ? trieNode(sub, keccak) : new Uint8Array(0));
}
const here = items.find(it => it.nibs.length === 0); if (here) value = here.value;
branch.push(value);
return encode(branch);
}
// Children are RLP-embedded when their encoding is under 32 bytes, else referenced by hash; trieNode returns either
// the short RLP or the 32-byte hash, and rlpEncode treats a Uint8Array as a string item: a short child must be
// spliced in raw, so the list encoder below handles both forms.
export function orderedTrieRoot(values, keccak) {
const items = values.map((v, i) => ({ nibs: nibblesOf(rlpEncode(trimBig(BigInt(i)))), value: v }));
items.sort((a, b) => { const n = Math.min(a.nibs.length, b.nibs.length); for (let k = 0; k < n; k++) if (a.nibs[k] !== b.nibs[k]) return a.nibs[k] - b.nibs[k]; return a.nibs.length - b.nibs.length; });
if (!items.length) return keccak(rlpEncode(new Uint8Array(0)));
const root = trieNodeTop(items, keccak);
return root;
}
function nibblesOf(b) { const out = []; for (const x of b) out.push(x >> 4, x & 15); return out; }
// the node forms: an Embedded child is spliced raw into the parent's list; a Hashed child is a 32-byte string item
class Raw { constructor(bytes) { this.bytes = bytes; } }
function rlpList(parts) {
const body = concat(parts.map(p => p instanceof Raw ? p.bytes : rlpEncode(p)));
const len = body.length; const head = len < 56 ? new Uint8Array([0xc0 + len]) : (() => { const b = []; let x = len; while (x > 0) { b.unshift(x & 0xff); x = Math.floor(x / 256); } return new Uint8Array([0xf7 + b.length, ...b]); })();
return concat([head, body]);
}
function build(items, keccak) { // returns { enc } the node's RLP
if (items.length === 1) return rlpList([hpEncode(items[0].nibs, true), items[0].value]);
let prefix = 0;
while (items.every(it => it.nibs.length > prefix && it.nibs[prefix] === items[0].nibs[prefix])) prefix++;
if (prefix > 0) {
const childEnc = build(items.map(it => ({ nibs: it.nibs.slice(prefix), value: it.value })), keccak);
return rlpList([hpEncode(items[0].nibs.slice(0, prefix), false), childEnc.length < 32 ? new Raw(childEnc) : keccak(childEnc)]);
}
const parts = [];
for (let b = 0; b < 16; b++) {
const sub = items.filter(it => it.nibs.length > 0 && it.nibs[0] === b).map(it => ({ nibs: it.nibs.slice(1), value: it.value }));
if (!sub.length) { parts.push(new Uint8Array(0)); continue; }
const enc = build(sub, keccak); parts.push(enc.length < 32 ? new Raw(enc) : keccak(enc));
}
const here = items.find(it => it.nibs.length === 0); parts.push(here ? here.value : new Uint8Array(0));
return rlpList(parts);
}
function trieNodeTop(items, keccak) { return keccak(build(items, keccak)); }
void trieNode;
// A receipt as the node encodes it for the trie (alloy ReceiptEnvelope::encode_2718): type byte for 1 and 2, then
// RLP([status, cumulative_gas_used, logs_bloom, [[address, [topics], data]...]]). Fields from eth_getBlockReceipts.
export function receiptEnvelope(r) {
const h = x => hexToBytes(strip(x));
const status = Number(r.status) === 1 ? new Uint8Array([1]) : new Uint8Array(0);
const logs = (r.logs || []).map(l => [h(l.address), (l.topics || []).map(h), h(l.data || '0x')]);
const body = rlpEncode([status, trimBig(BigInt(r.cumulativeGasUsed)), h(r.logsBloom), logs]);
const type = Number(r.type || 0);
return type === 1 || type === 2 ? concat([new Uint8Array([type]), body]) : body;
}
// ---- the payment receipt: the outcome authenticated through the proven segment's receipts commitment ------------
// `pr` = the inclusion receipt's fields plus: segment: { record_hex, carrier: {...as /balance}, headers: [carrier..checkpoint],
// shard_receipts_roots: [hex...], shard_index, receipts: [eth_getBlockReceipts rows of that shard, in order],
// receipt_position }. The transaction's chain block must be the segment's last block (the statement commits that
// block's receipts); the verifier says so when it is not.
export function verifyPaymentReceipt(pr, deps) {
const { blake2b, bls, keccak } = deps;
const base = verifyReceipt(pr, deps);
if (!base.verified) return { ...base, payment: false };
const steps = base.steps.slice(); const t0 = now();
const step = (name, fn) => { try { const d = fn(); steps.push({ name, ok: true, detail: d }); return d; } catch (e) { steps.push({ name, ok: false, detail: String(e.message || e) }); throw e; } };
const done = extra => ({ ...base, ...extra, steps, ms: Math.round((base.ms + now() - t0) * 10) / 10 });
try {
const seg = pr.segment;
if (!seg) throw Object.assign(new Error(pr.payment_unavailable || 'no proven segment record covers the chain block that executed this transaction yet'), { soft: true });
const cp = pr.checkpoint.certificate;
step('header chain from the record\'s carrier block up to the checkpoint', () => `${verifyHeaderPath(seg.headers, blake2b, seg.headers[0].hash, cp.hash)} headers`);
const record = step('the segment record is in the carrier\'s coinbase (hash recomputed, merkle path) and signed by its aggregator', () => {
const cb = coinbaseTxHash(seg.carrier.coinbase, blake2b, seg.carrier.amount_wire_len || 8);
const root = merkleRootFromPath(cb, Number(seg.carrier.leaf_index), seg.carrier.merkle_siblings.map(x => hexToBytes(strip(x))), blake2b);
if (bytesToHex(root) !== strip(seg.headers[0].hash_merkle_root)) throw new Error('the merkle path does not reach the carrier\'s hash_merkle_root');
const rec = segmentRecordsOf(hexToBytes(strip(seg.carrier.coinbase.payload))).find(r => bytesToHex(r.bytes) === strip(seg.record_hex));
if (!rec) throw new Error('the named segment record is not in the carrier\'s coinbase');
if (!verifySegmentRecordSignature(cp.chain_id, rec, bls)) throw new Error('the aggregator\'s signature does not verify');
return `record for blocks ${rec.first} to ${rec.last}, aggregator ${voteKeyHash(rec.pubkey, blake2b).slice(0, 12)}`;
}) && segmentRecordsOf(hexToBytes(strip(seg.carrier.coinbase.payload))).find(r => bytesToHex(r.bytes) === strip(seg.record_hex));
const st = record.statement;
step('the statement is for the chain block that executed the transaction', () => {
if (Number(st.number) !== Number(pr.execution.chain_block) || st.block_hash !== strip(pr.execution.chain_block_hash)) throw new Error(`the statement is for chain block ${st.number}, the transaction executed at ${pr.execution.chain_block}`);
return `chain block ${st.number}, ${st.shard_count} shard(s), receipts commitment ${st.receipts.slice(0, 12)}`;
});
step('the shard receipts roots hash to the statement\'s receipts commitment', () => {
const roots = seg.shard_receipts_roots.map(x => hexToBytes(strip(x)));
if (roots.length !== Number(st.shard_count)) throw new Error(`${roots.length} roots given, the statement names ${st.shard_count} shards`);
if (bytesToHex(keccak(concat(roots))) !== st.receipts) throw new Error('keccak over the shard receipts roots is not the statement\'s commitment');
return `${roots.length} root(s), shard ${seg.shard_index} is ${strip(seg.shard_receipts_roots[seg.shard_index]).slice(0, 12)}`;
});
const mine = step('the shard\'s receipts rebuild its receipts trie root, and the transaction\'s receipt sits in it', () => {
const root = bytesToHex(orderedTrieRoot(seg.receipts.map(receiptEnvelope), keccak));
if (root !== strip(seg.shard_receipts_roots[seg.shard_index])) throw new Error(`the ${seg.receipts.length} receipts rebuild root ${root.slice(0, 12)}, the committed root is ${strip(seg.shard_receipts_roots[seg.shard_index]).slice(0, 12)}`);
const r = seg.receipts[Number(seg.receipt_position)];
if (!r || strip(r.transactionHash) !== strip(pr.tx_hash)) throw new Error('the receipt at the named position is not this transaction\'s');
return `${seg.receipts.length} receipt(s), this one at position ${seg.receipt_position}, status ${Number(r.status) === 1 ? 'success' : 'failed'}, gas ${parseInt(r.gasUsed, 16)}, ${(r.logs || []).length} log(s)`;
}) && seg.receipts[Number(seg.receipt_position)];
const ok = Number(mine.status) === 1;
const outcome = ok ? { asset: 'IGN (the native coin)', recipient: base.tx.to, amount_wei: base.tx.value, logs: (mine.logs || []).map(l => ({ address: l.address, topics: l.topics, data: l.data })) } : null;
return done({ verified: true, payment: ok, outcome, receipt_status: ok ? 'success' : 'failed', note: ok ? 'the transfer outcome is authenticated through the proven segment\'s receipts commitment' : 'the transaction executed and FAILED: no transfer took place; authenticated the same way' });
} catch (e) {
if (e.soft) return done({ verified: true, payment: false, payment_unavailable: String(e.message || e) });
return done({ verified: false, payment: false, reason: String(e.message || e) });
}
}
export function formatIgn(wei, decimals = 18) {
const v = BigInt(wei); const base = 10n ** BigInt(decimals);
const whole = v / base; let frac = (v % base).toString().padStart(decimals, '0').replace(/0+$/, '');

View file

@ -1,5 +1,5 @@
#!/usr/bin/env node
// Igneum transaction inclusion receipt verifier, one file, offline. Source: tools/reference-apps/receipt/verify-receipt.src.mjs and site/lc/core.js
// Igneum receipt verifier (transaction inclusion receipt or payment receipt), one file, offline. Source: tools/reference-apps/receipt/verify-receipt.src.mjs and site/lc/core.js
// (the same checks the browser page runs), bundled with @noble/hashes 2.4.0 and @noble/curves 2.4.0 (MIT). Usage: node verify-receipt.js receipt.json [--tamper]
// tools/reference-apps/receipt/verify-receipt.src.mjs
@ -5142,10 +5142,26 @@ function verifyCheckpoint(data, deps2) {
}
// site/lc/core.js
var DST_SEGMENT = "IGNEUM_SEGMENT_RECORD_V1_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_";
var SEGMENT_RECORD_LEN = 2 + 8 + 8 + 32 + 48 + 20 + 340 + 32 + 96;
var ZERO32 = new Uint8Array(32);
var te2 = new TextEncoder();
var strip = (s) => String(s).replace(/^0x/i, "");
var u64le = (v) => {
const b = new Uint8Array(8);
new DataView(b.buffer).setBigUint64(0, BigInt(v), true);
return b;
};
var u16le = (v) => {
const b = new Uint8Array(2);
new DataView(b.buffer).setUint16(0, Number(v), true);
return b;
};
var u32le = (v) => {
const b = new Uint8Array(4);
new DataView(b.buffer).setUint32(0, Number(v), true);
return b;
};
function concat2(parts) {
const n = parts.reduce((a, p) => a + p.length, 0);
const m = new Uint8Array(n);
@ -5171,6 +5187,114 @@ function merkleRootFromPath(leaf, index, siblings, blake2b2) {
}
return h;
}
function coinbaseTxHash(tx, blake2b2, amountWireLen = 8) {
const version = Number(tx.version || 0);
const parts = [u16le(version), u64le((tx.inputs || []).length)];
for (const inp of tx.inputs || []) {
parts.push(hexToBytes3(strip(inp.previousOutpoint.transactionId)), u32le(inp.previousOutpoint.index));
const sig = hexToBytes3(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 = hexToBytes3(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 = hexToBytes3(strip(tx.payload || ""));
parts.push(u64le(tx.lockTime || 0), hexToBytes3(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 keyed(blake2b2, "TransactionHash")(concat2(parts));
}
function coinbaseExtraData(payload) {
if (payload.length < 19) return new Uint8Array(0);
const scriptLen = payload[18];
return payload.subarray(19 + scriptLen);
}
function takeSection(extra, tag) {
const n = extra.length;
if (n < 8) return { items: null, before: extra };
const t2 = String.fromCharCode(...extra.subarray(n - 4));
if (t2 !== tag) return { items: null, before: extra };
const len = new DataView(extra.buffer, extra.byteOffset + n - 8, 4).getUint32(0, true);
if (len + 8 > n) return { items: null, before: extra };
return { items: extra.subarray(n - 8 - len, n - 8), before: extra.subarray(0, n - 8 - len) };
}
function segmentRecordsOf(payload) {
const extra = coinbaseExtraData(payload);
const f = takeSection(extra, "IGNF");
const p = takeSection(f.before, "IGNP");
const s = takeSection(p.before, "IGNS");
const out = [];
if (!s.items) return out;
for (let o = 0; o + SEGMENT_RECORD_LEN <= s.items.length; o += SEGMENT_RECORD_LEN) out.push(parseSegmentRecord(s.items.subarray(o, o + SEGMENT_RECORD_LEN)));
return out;
}
function parseSegmentRecord(b) {
if (b.length !== SEGMENT_RECORD_LEN) throw new Error(`segment record is ${b.length} bytes, not ${SEGMENT_RECORD_LEN}`);
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
let o = 0;
const version = dv.getUint16(o, true);
o += 2;
const first = dv.getBigUint64(o, true);
o += 8;
const last = dv.getBigUint64(o, true);
o += 8;
const block = b.subarray(o, o + 32);
o += 32;
const pubkey = b.subarray(o, o + 48);
o += 48;
const payout = b.subarray(o, o + 20);
o += 20;
const publicValues = b.subarray(o, o + 340);
o += 340;
const proofHash = b.subarray(o, o + 32);
o += 32;
const signature = b.subarray(o, o + 96);
return { bytes: b, version, first, last, block, pubkey, payout, publicValues, proofHash, signature, statement: parseBlockStatement(publicValues) };
}
function parseBlockStatement(b) {
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
const h = (o) => bytesToHex3(b.subarray(o, o + 32));
return {
chain_id: dv.getBigUint64(0),
number: dv.getBigUint64(8),
block_hash: h(16),
parent_hash: h(48),
shard_count: dv.getUint32(80),
tx_commitment: h(84),
pre_root: h(116),
post_root: h(148),
receipts: h(180),
gas_used: dv.getBigUint64(212),
pgas_used: dv.getBigUint64(220),
executed: dv.getUint32(228),
skipped: dv.getUint32(232),
provers: h(236),
shard_vk: h(268),
agg_vk: h(300),
chain_len: dv.getBigUint64(332)
};
}
function segmentRecordMessage(chainId, r2) {
return concat2([te2.encode("igneum-segment-record-v1/" + chainId), new Uint8Array([0]), u64le(r2.first), u64le(r2.last), r2.block, r2.payout, r2.publicValues, r2.proofHash]);
}
function verifySegmentRecordSignature(chainId, r2, bls2) {
const L = bls2.longSignatures;
return L.verify(r2.signature, L.hash(segmentRecordMessage(chainId, r2), DST_SEGMENT), r2.pubkey);
}
function rlpDecode(b) {
const [item, rest] = rlpItem(b, 0);
if (rest !== b.length) throw new Error("rlp: trailing bytes");
@ -5209,6 +5333,31 @@ function rlpItem(b, o) {
}
return [items, end];
}
function rlpEncode(item) {
if (item instanceof Uint8Array) {
if (item.length === 1 && item[0] < 128) return item;
return concat2([rlpLen(item.length, 128), item]);
}
const body = concat2(item.map(rlpEncode));
return concat2([rlpLen(body.length, 192), body]);
}
function rlpLen(n, base) {
if (n < 56) return new Uint8Array([base + n]);
const bytes = [];
let x = n;
while (x > 0) {
bytes.unshift(x & 255);
x = Math.floor(x / 256);
}
return new Uint8Array([base + 55 + bytes.length, ...bytes]);
}
var trimBig = (v) => {
v = BigInt(v);
if (v === 0n) return new Uint8Array(0);
let h = v.toString(16);
if (h.length % 2) h = "0" + h;
return hexToBytes3(h);
};
function verifyHeaderPath(headers, blake2b2, fromHash, toHash) {
if (!headers.length) throw new Error("no headers");
for (let i = 0; i < headers.length; i++) {
@ -5276,6 +5425,140 @@ function parseTx(raw) {
if (type === 1) return { type, chain_id: bigOf(body[0]).toString(), nonce: bigOf(body[1]).toString(), to: body[4].length ? hex(body[4]) : null, value: bigOf(body[5]).toString(), data: hex(body[6]), gas: bigOf(body[3]).toString() };
return { type: 0, nonce: bigOf(body[0]).toString(), to: body[3].length ? hex(body[3]) : null, value: bigOf(body[4]).toString(), data: hex(body[5]), gas: bigOf(body[2]).toString() };
}
function hpEncode(nibs, leaf) {
const odd = nibs.length % 2;
const flag = (leaf ? 2 : 0) + odd;
const out = [];
if (odd) out.push(flag << 4 | nibs[0]);
else out.push(flag << 4);
for (let i = odd; i < nibs.length; i += 2) out.push(nibs[i] << 4 | nibs[i + 1]);
return new Uint8Array(out);
}
function orderedTrieRoot(values, keccak) {
const items = values.map((v, i) => ({ nibs: nibblesOf(rlpEncode(trimBig(BigInt(i)))), value: v }));
items.sort((a, b) => {
const n = Math.min(a.nibs.length, b.nibs.length);
for (let k = 0; k < n; k++) if (a.nibs[k] !== b.nibs[k]) return a.nibs[k] - b.nibs[k];
return a.nibs.length - b.nibs.length;
});
if (!items.length) return keccak(rlpEncode(new Uint8Array(0)));
const root = trieNodeTop(items, keccak);
return root;
}
function nibblesOf(b) {
const out = [];
for (const x of b) out.push(x >> 4, x & 15);
return out;
}
var Raw = class {
constructor(bytes) {
this.bytes = bytes;
}
};
function rlpList(parts) {
const body = concat2(parts.map((p) => p instanceof Raw ? p.bytes : rlpEncode(p)));
const len = body.length;
const head = len < 56 ? new Uint8Array([192 + len]) : (() => {
const b = [];
let x = len;
while (x > 0) {
b.unshift(x & 255);
x = Math.floor(x / 256);
}
return new Uint8Array([247 + b.length, ...b]);
})();
return concat2([head, body]);
}
function build(items, keccak) {
if (items.length === 1) return rlpList([hpEncode(items[0].nibs, true), items[0].value]);
let prefix = 0;
while (items.every((it) => it.nibs.length > prefix && it.nibs[prefix] === items[0].nibs[prefix])) prefix++;
if (prefix > 0) {
const childEnc = build(items.map((it) => ({ nibs: it.nibs.slice(prefix), value: it.value })), keccak);
return rlpList([hpEncode(items[0].nibs.slice(0, prefix), false), childEnc.length < 32 ? new Raw(childEnc) : keccak(childEnc)]);
}
const parts = [];
for (let b = 0; b < 16; b++) {
const sub = items.filter((it) => it.nibs.length > 0 && it.nibs[0] === b).map((it) => ({ nibs: it.nibs.slice(1), value: it.value }));
if (!sub.length) {
parts.push(new Uint8Array(0));
continue;
}
const enc = build(sub, keccak);
parts.push(enc.length < 32 ? new Raw(enc) : keccak(enc));
}
const here = items.find((it) => it.nibs.length === 0);
parts.push(here ? here.value : new Uint8Array(0));
return rlpList(parts);
}
function trieNodeTop(items, keccak) {
return keccak(build(items, keccak));
}
function receiptEnvelope(r2) {
const h = (x) => hexToBytes3(strip(x));
const status = Number(r2.status) === 1 ? new Uint8Array([1]) : new Uint8Array(0);
const logs = (r2.logs || []).map((l) => [h(l.address), (l.topics || []).map(h), h(l.data || "0x")]);
const body = rlpEncode([status, trimBig(BigInt(r2.cumulativeGasUsed)), h(r2.logsBloom), logs]);
const type = Number(r2.type || 0);
return type === 1 || type === 2 ? concat2([new Uint8Array([type]), body]) : body;
}
function verifyPaymentReceipt(pr, deps2) {
const { blake2b: blake2b2, bls: bls2, keccak } = deps2;
const base = verifyReceipt(pr, deps2);
if (!base.verified) return { ...base, payment: false };
const steps = base.steps.slice();
const t0 = now();
const step = (name, fn) => {
try {
const d = fn();
steps.push({ name, ok: true, detail: d });
return d;
} catch (e) {
steps.push({ name, ok: false, detail: String(e.message || e) });
throw e;
}
};
const done = (extra) => ({ ...base, ...extra, steps, ms: Math.round((base.ms + now() - t0) * 10) / 10 });
try {
const seg = pr.segment;
if (!seg) throw Object.assign(new Error(pr.payment_unavailable || "no proven segment record covers the chain block that executed this transaction yet"), { soft: true });
const cp = pr.checkpoint.certificate;
step("header chain from the record's carrier block up to the checkpoint", () => `${verifyHeaderPath(seg.headers, blake2b2, seg.headers[0].hash, cp.hash)} headers`);
const record = step("the segment record is in the carrier's coinbase (hash recomputed, merkle path) and signed by its aggregator", () => {
const cb = coinbaseTxHash(seg.carrier.coinbase, blake2b2, seg.carrier.amount_wire_len || 8);
const root = merkleRootFromPath(cb, Number(seg.carrier.leaf_index), seg.carrier.merkle_siblings.map((x) => hexToBytes3(strip(x))), blake2b2);
if (bytesToHex3(root) !== strip(seg.headers[0].hash_merkle_root)) throw new Error("the merkle path does not reach the carrier's hash_merkle_root");
const rec = segmentRecordsOf(hexToBytes3(strip(seg.carrier.coinbase.payload))).find((r2) => bytesToHex3(r2.bytes) === strip(seg.record_hex));
if (!rec) throw new Error("the named segment record is not in the carrier's coinbase");
if (!verifySegmentRecordSignature(cp.chain_id, rec, bls2)) throw new Error("the aggregator's signature does not verify");
return `record for blocks ${rec.first} to ${rec.last}, aggregator ${voteKeyHash(rec.pubkey, blake2b2).slice(0, 12)}`;
}) && segmentRecordsOf(hexToBytes3(strip(seg.carrier.coinbase.payload))).find((r2) => bytesToHex3(r2.bytes) === strip(seg.record_hex));
const st = record.statement;
step("the statement is for the chain block that executed the transaction", () => {
if (Number(st.number) !== Number(pr.execution.chain_block) || st.block_hash !== strip(pr.execution.chain_block_hash)) throw new Error(`the statement is for chain block ${st.number}, the transaction executed at ${pr.execution.chain_block}`);
return `chain block ${st.number}, ${st.shard_count} shard(s), receipts commitment ${st.receipts.slice(0, 12)}`;
});
step("the shard receipts roots hash to the statement's receipts commitment", () => {
const roots = seg.shard_receipts_roots.map((x) => hexToBytes3(strip(x)));
if (roots.length !== Number(st.shard_count)) throw new Error(`${roots.length} roots given, the statement names ${st.shard_count} shards`);
if (bytesToHex3(keccak(concat2(roots))) !== st.receipts) throw new Error("keccak over the shard receipts roots is not the statement's commitment");
return `${roots.length} root(s), shard ${seg.shard_index} is ${strip(seg.shard_receipts_roots[seg.shard_index]).slice(0, 12)}`;
});
const mine = step("the shard's receipts rebuild its receipts trie root, and the transaction's receipt sits in it", () => {
const root = bytesToHex3(orderedTrieRoot(seg.receipts.map(receiptEnvelope), keccak));
if (root !== strip(seg.shard_receipts_roots[seg.shard_index])) throw new Error(`the ${seg.receipts.length} receipts rebuild root ${root.slice(0, 12)}, the committed root is ${strip(seg.shard_receipts_roots[seg.shard_index]).slice(0, 12)}`);
const r2 = seg.receipts[Number(seg.receipt_position)];
if (!r2 || strip(r2.transactionHash) !== strip(pr.tx_hash)) throw new Error("the receipt at the named position is not this transaction's");
return `${seg.receipts.length} receipt(s), this one at position ${seg.receipt_position}, status ${Number(r2.status) === 1 ? "success" : "failed"}, gas ${parseInt(r2.gasUsed, 16)}, ${(r2.logs || []).length} log(s)`;
}) && seg.receipts[Number(seg.receipt_position)];
const ok = Number(mine.status) === 1;
const outcome = ok ? { asset: "IGN (the native coin)", recipient: base.tx.to, amount_wei: base.tx.value, logs: (mine.logs || []).map((l) => ({ address: l.address, topics: l.topics, data: l.data })) } : null;
return done({ verified: true, payment: ok, outcome, receipt_status: ok ? "success" : "failed", note: ok ? "the transfer outcome is authenticated through the proven segment's receipts commitment" : "the transaction executed and FAILED: no transfer took place; authenticated the same way" });
} catch (e) {
if (e.soft) return done({ verified: true, payment: false, payment_unavailable: String(e.message || e) });
return done({ verified: false, payment: false, reason: String(e.message || e) });
}
}
function formatIgn(wei, decimals = 18) {
const v = BigInt(wei);
const base = 10n ** BigInt(decimals);
@ -5312,14 +5595,16 @@ if (args.includes("--tamper")) {
console.log(`tampered copy (one nibble of the raw transaction): ${t2.verified ? "NOT REFUSED, this verifier is broken" : "REFUSED"}${t2.reason ? " :: " + t2.reason : ""}`);
if (t2.verified) process.exit(1);
}
var r = verifyReceipt(receipt, deps);
console.log(`TRANSACTION INCLUSION RECEIPT 0x${receipt.tx_hash} on ${receipt.chain_id} (Devnet 3, no value)`);
console.log("What this file authenticates: that the signed transaction is included in a block that is finalised. What it does not: the execution outcome (status, gas), which is carried as the node reported it. A payment receipt, which authenticates the transfer outcome, is a different file.");
var r = receipt.segment ? verifyPaymentReceipt(receipt, deps) : verifyReceipt(receipt, deps);
var payment = !!(r.verified && r.payment);
console.log(`${payment ? "PAYMENT RECEIPT" : "TRANSACTION INCLUSION RECEIPT"} 0x${receipt.tx_hash} on ${receipt.chain_id} (Devnet 3, no value)`);
console.log(payment ? "What this file authenticates: that the signed transaction is included in a finalised block and executed with status success, its receipt sitting in the shard receipts trie whose root the proven segment statement commits to. Trusted: the voter table from the node; the SP1 proof behind the statement is verified by nodes, not here." : "What this file authenticates: that the signed transaction is included in a block that is finalised. What it does not: the execution outcome (status, gas), which is carried as the node reported it." + (receipt.payment_unavailable ? " Why no payment receipt: " + receipt.payment_unavailable : ""));
print(r);
if (!r.verified) {
console.log(`REFUSED: ${r.reason}`);
process.exit(1);
}
var t = r.tx;
console.log(`INCLUSION VERIFIED in ${r.ms} ms: a signed transaction of ${formatIgn(t.value)} IGN to ${t.to || "contract creation"} (${t.value} wei) is included in block ${r.block.slice(0, 16)} at DAA ${r.block_daa} (${new Date(Number(r.block_time)).toISOString()}), finalised under checkpoint ${r.checkpoint}; ${r.headers} headers checked; ${r.certificate}.`);
console.log("Reported by the node, not authenticated by this file: from " + (receipt.tx_as_reported && receipt.tx_as_reported.from) + (receipt.execution ? `, executed with status ${receipt.execution.status === "0x1" ? "success" : "failed"}` : "") + ". The voter table with weights came from the node (spec 10.1). In the four words: included and finalised are authenticated, executed is reported, proven is not claimed.");
if (payment) console.log(`PAYMENT VERIFIED in ${r.ms} ms: ${formatIgn(t.value)} IGN (${t.value} wei of the native coin) to ${t.to || "contract creation"}, executed with status success (${r.outcome.logs.length} log(s)) at chain block ${receipt.execution.chain_block}, in block ${r.block.slice(0, 16)} at DAA ${r.block_daa}, finalised under checkpoint ${r.checkpoint}; ${r.certificate}.`);
else console.log(`INCLUSION VERIFIED in ${r.ms} ms: a signed transaction of ${formatIgn(t.value)} IGN to ${t.to || "contract creation"} (${t.value} wei) is included in block ${r.block.slice(0, 16)} at DAA ${r.block_daa} (${new Date(Number(r.block_time)).toISOString()}), finalised under checkpoint ${r.checkpoint}; ${r.headers} headers checked; ${r.certificate}.`);
console.log("Reported by the node, not authenticated by this file: from " + (receipt.tx_as_reported && receipt.tx_as_reported.from) + (payment ? "" : receipt.execution ? `, executed with status ${receipt.execution.status === "0x1" ? "success" : "failed"}` : "") + ". The voter table with weights came from the node (spec 10.1). In the four words: " + (payment ? "included, executed, proven and finalised are authenticated under the stated trust." : "included and finalised are authenticated, executed is reported, proven is not claimed."));

View file

@ -249,7 +249,12 @@
<div class="eyebrow"><span class="line"></span>Reference app 2 · Devnet 3, no value</div>
<h1>A receipt any third party re-verifies<span class="accent">.</span></h1>
</div>
<p class="note">Paste a Devnet 3 transaction hash. This tab fetches the raw transaction, the including block's merkle path, every header up to the certified checkpoint and the certificate, then proves inclusion and finality itself. Download the receipt as JSON. Anyone checks it later with a one-file verifier, offline, with no node and no network.</p>
<p class="note">Paste a Devnet 3 transaction hash. This tab fetches the raw transaction, the including block's merkle path, every header up to the certified checkpoint and the certificate, then proves inclusion and finality itself. Download the receipt as JSON. Anyone checks it later with a one-file verifier, offline, with no node and no network. Test with the negative cases: <code>tools/reference-apps/light-service/verify.test.mjs</code> (eight payment cases, nine inclusion cases).</p>
<div class="trust"><h3>Which receipt this is</h3><ul>
<li>The page issues one of two receipts and names it on the result, in the file (field <code>kind</code>) and in the offline verifier's output.</li>
<li>A <b>transaction inclusion receipt</b> authenticates that the signed transaction (recipient, amount and data as signed) is included in a block that is finalised. The execution result (status, gas, logs) is carried as the node reported it and labelled so; a transaction can be included and finalised and still have failed.</li>
<li>A <b>payment receipt</b> also authenticates the execution outcome: the transaction's receipt (status success, its logs) sits in the shard receipts trie whose root the proven segment's statement commits to (<code>receipts</code> = keccak over the shard roots, signed by the aggregator inside the carrier block, under the certificate). Asset, recipient and amount are the signed transaction's; the outcome is the authenticated receipt's. It is issued when the transaction executed at a paid segment's last block, which is where the statement commits that block's receipts; otherwise the page says why it issues the inclusion receipt. Per-shard records would cover every block and are the next step once they are paid again.</li>
</ul></div>
<div class="why">
<p><b>Why this only works on a proven chain.</b> A merchant's receipt is only worth something if the payment cannot be undone and the proof of that fits in a file: here the certificate is a weighted BLS signature by the miners over a checkpoint, and the block holding the payment hashes into that checkpoint's past. On a chain with probabilistic finality a receipt is a guess that ages well; here it is a fact that a file carries.</p>
@ -268,14 +273,26 @@
<h2 id="offline">Re-verify offline</h2>
<p class="note">The verifier is one file of plain JavaScript with the hashing and curve code inside it: <a href="/lc/verify-receipt.js" download>verify-receipt.js</a>. It runs under Node 18 or later (<code>node verify-receipt.js receipt.json</code>) and prints each check, then <code>VERIFIED</code> or <code>REFUSED</code> with the reason. It fetches nothing. Flip one byte of the file and it is refused: <code>node verify-receipt.js receipt.json --tamper</code> runs that case first so you see the refusal before the verdict.</p>
<h2 id="checked">What the receipt proves</h2>
<h2 id="checked">What the receipts prove</h2>
<ol>
<li>The transaction hash is keccak256 of the raw signed transaction in the file, so the recipient, the amount and the data are the signed ones.</li>
<li>The transaction is a leaf of the including block's <code>hash_merkle_root</code> (BLAKE2b-256 keyed MerkleBranchHash up the path).</li>
<li>Every header from the including block to the certified checkpoint recomputes and links to the next by a direct parent.</li>
<li>The certificate over that checkpoint verifies: the aggregate BLS signature of the signers, the voter order, two thirds of active weight and 17/30 of total.</li>
</ol>
<p class="note">Not proven by the file: the execution result (status, gas) is carried as the node reported it and labelled so; the voter table with weights comes from the node (spec 10.1). Both are named on the result.</p>
<p class="note">The payment receipt adds: the record's carrier block up to the checkpoint, the segment record's signature, the shard receipts roots hashing to the statement's commitment, the shard's receipts rebuilding that root with this transaction's receipt at its position. Not proven by either file: the voter table with weights comes from the node (spec 10.1), and the SP1 proof behind the statement is verified by nodes, not here. In the four words below: the inclusion receipt authenticates included and finalised, reports executed and claims nothing about proven; the payment receipt authenticates included, executed, proven and finalised under the trust assumptions stated.</p>
<!-- terms:start -->
<section class="terms" aria-labelledby="terms-head">
<h2 id="terms-head">Four words, used exactly</h2>
<div class="kv">
<div class="k">Included</div><div>The transaction is in a block's body: its hash is a leaf under the block header's <code>hash_merkle_root</code>. Proves the block carries it, nothing about what it did.</div>
<div class="k">Executed</div><div>A node ran it at a chain block and reports a result (status, gas, logs). On these pages an execution result is reported by the node, not authenticated, unless the page says it is.</div>
<div class="k">Proven</div><div>An aggregator's segment record, carried in a block's coinbase and signed with its vote key, commits to the state root after that chain block; nodes check the statement against their own execution before paying it. The SP1 proof behind the statement is verified by nodes, not in the browser or on Sepolia.</div>
<div class="k">Finalised</div><div>A certified checkpoint has the block in its past: an aggregate BLS signature by voters holding two thirds of active weight and at least 17/30 of total weight over the checkpoint, checked here against the voter table the node supplies.</div>
</div>
<p class="note">The same four definitions sit on <a href="/light">/light</a>, <a href="/receipt">/receipt</a> and <a href="/oracle">/oracle</a> (one source: <code>site/partials/terms.html</code>). Devnet 3, no value.</p>
</section>
<!-- terms:end -->
<p class="asof">Devnet 3, no value. The chain may reset. A demonstration of the verification path, not a product.</p>
</main>
<script type="module" src="/lc/app.js"></script>

File diff suppressed because one or more lines are too long

View file

@ -221,8 +221,17 @@ async function receipt(q) {
const chainNumber = Number(t.blockNumber);
// the smallest proof: the earliest certified checkpoint at or above the block (from the Devnet 3 observer's rows), else
// the checkpoint the caller named; the certificate used is returned so the page verifies against that one
// the paid segment record for the transaction's segment, when its last block is the transaction's chain block: the
// certificate then has to sit above the record's carrier, so the lookup comes first
const segFirst = Math.floor(chainNumber / SEGMENT_BLOCKS) * SEGMENT_BLOCKS, segLast = segFirst + SEGMENT_BLOCKS - 1;
let paidRecord = null, paidSr = null;
if (chainNumber === segLast) {
paidSr = await exec('igneum_getSegmentRecords', ['0x' + segFirst.toString(16)]).catch(() => null);
paidRecord = paidSr && paidSr.paid ? (paidSr.carried || []).find(x => x.valid && Number(x.carrierNumber) === Number(paidSr.paid.carrierNumber) && strip(x.keyHash) === strip(paidSr.paid.keyHash)) || null : null;
}
const mustCover = paidRecord ? Math.max(chainNumber, Number(paidRecord.carrierNumber)) : chainNumber;
let certificate = null;
if (process.env.DATABASE_URL) certificate = await earliestCheckpointAbove(neon(), chainNumber, chainNumberOf).catch(() => null);
if (process.env.DATABASE_URL) certificate = await earliestCheckpointAbove(neon(), mustCover, chainNumberOf).catch(() => null);
if (certificate) { cpHash = certificate.hash; cpIndex = Number(certificate.index); }
const cpNumber = await chainNumberOf(cpHash);
if (chainNumber > cpNumber) throw httpError(409, `not final yet: executed at chain block ${chainNumber}, the latest certified checkpoint is chain block ${cpNumber}; try again in about ${chainNumber - cpNumber + 30} s`);
@ -235,8 +244,44 @@ async function receipt(q) {
const root = bytesToHex(merkleRoot(leaves, blake2b));
if (root !== strip(b.header.hashMerkleRoot)) throw httpError(500, `the including block's body root recomputes to ${root.slice(0, 12)}, the header says ${strip(b.header.hashMerkleRoot).slice(0, 12)}`);
const headers = await headerPath(including, chainNumber, cpNumber, cpHash);
// the payment receipt's data: when a paid segment record's last block is the chain block that executed the transaction,
// the record's statement commits to that block's shard receipts roots; the shard's receipts rebuild the root
let segment = null, paymentUnavailable = null;
try {
const first = segFirst, last = segLast;
if (chainNumber !== last) paymentUnavailable = `the transaction executed at chain block ${chainNumber}; a segment statement commits the receipts of its last block only (${last} for this segment), so a payment receipt is available for transactions executed at a paid segment's last block; this one has the inclusion receipt`;
else {
const sr = paidSr, c = paidRecord;
if (!c) paymentUnavailable = `segment ${first} to ${last} has no paid record yet (${sr && sr.carried ? sr.carried.length : 0} carried, pending or unproven); the inclusion receipt stands`;
else {
const carrierNumber = Number(c.carrierNumber);
if (carrierNumber > cpNumber) paymentUnavailable = `the paid record is carried at chain block ${carrierNumber}, above the certified checkpoint ${cpNumber}; try again after the next lock`;
else {
const carrier = await body(c.carrier);
const coinbase = carrier.transactions[0];
const evmHashes = (carrier.evmTransactions || []).map(raw => keccak_256(hexToBytes(strip(raw))));
const leaves = [coinbaseTxHash(coinbase, blake2b), ...evmHashes];
const rec = segmentRecordsOf(hexToBytes(strip(coinbase.payload))).find(r => Number(r.first) === first && Number(r.last) === last);
if (!rec) throw new Error('the carrier\'s coinbase holds no record for the segment');
const plan = await exec('igneum_getShardPlan', ['0x' + chainNumber.toString(16)]);
const all = await exec('eth_getBlockReceipts', ['0x' + chainNumber.toString(16)]);
const txIndex = Number(t.transactionIndex);
let start = 0, shardIndex = -1, mine = null;
for (const sh of plan.shards) { const n = Number(sh.executed); if (txIndex >= start && txIndex < start + n) { shardIndex = Number(sh.index); mine = all.slice(start, start + n); break; } start += n; }
if (shardIndex < 0) throw new Error('the transaction index falls in no shard of the plan');
segment = {
first, last, record_hex: bytesToHex(rec.bytes), headers: await headerPath(strip(c.carrier), carrierNumber, cpNumber, cpHash),
carrier: { coinbase, evm_tx_hashes: evmHashes.map(bytesToHex), leaf_index: 0, merkle_siblings: siblings(leaves, 0), amount_wire_len: 8 },
shard_receipts_roots: plan.shards.map(sh => strip(sh.receiptsRoot)), shard_index: shardIndex, receipts: mine, receipt_position: txIndex - start,
aggregator_key_hash: c.keyHash, paid_wei: sr.paid.wei,
};
}
}
}
} catch (e) { paymentUnavailable = 'payment receipt data could not be assembled: ' + String(e.message || e); }
return {
ok: true, now: new Date().toISOString(), chain_id: 'igneum-devnet-3', tx_hash: strip(tx), raw_tx_hex: strip(raws[idx]),
segment: segment || undefined, payment_unavailable: paymentUnavailable || undefined,
checkpoint: { hash: strip(cpHash), index: cpIndex, chain_block: cpNumber, certificate: certificate ? { ok: true, ...certificate } : undefined },
including_block: { header: headers[0], leaf_index: idx + 1, leaf_count: leaves.length, merkle_siblings: siblings(leaves, idx + 1) },
headers,

View file

@ -7,7 +7,7 @@ import { readFileSync } from 'node:fs';
import { blake2b } from '@noble/hashes/blake2.js';
import { keccak_256 } from '@noble/hashes/sha3.js';
import { bls12_381 } from '@noble/curves/bls12-381.js';
import { verifyReceipt, verifyBalance } from '../../../site/lc/core.js';
import { verifyReceipt, verifyBalance, verifyPaymentReceipt } from '../../../site/lc/core.js';
const deps = { blake2b, bls: bls12_381, keccak: keccak_256 };
const here = new URL('.', import.meta.url).pathname;
@ -60,5 +60,20 @@ if (balance) {
expect(`genuine balance (${b.ms} ms)`, b, true);
if (b.verified) { console.log(` ${b.balance_wei} wei at chain block ${b.block}, ${b.headers} headers, ${b.aggregator}`); for (const s of b.steps) console.log(' ' + (s.ok ? 'ok ' : 'no ') + s.name + ': ' + s.detail); }
}
// the payment receipt: the transaction's execution outcome through the proven segment's receipts commitment
let pay = null; try { pay = load(here + '../fixtures/dn3-payment-receipt.json'); } catch { console.log('no payment receipt fixture yet, skipping the payment cases'); }
if (pay && pay.segment) {
const P = (name, mutate, want) => { const d = clone(pay); mutate(d); const r = verifyPaymentReceipt(d, deps); const ok = (r.verified && r.payment) === want; console.log(`${ok ? 'ok ' : 'FAIL'} ${want ? 'payment verifies' : 'payment refused '} ${name}${r.verified && r.payment ? '' : ' :: ' + (r.reason || r.payment_unavailable || 'status ' + r.receipt_status)}`); if (!ok) failed++; };
console.log(`payment receipt ${pay.tx_hash.slice(0, 12)} at chain block ${pay.execution.chain_block}, segment ${pay.segment.first} to ${pay.segment.last}, ${pay.segment.receipts.length} receipts in shard ${pay.segment.shard_index}`);
P('the receipt status flipped to failed', d => { d.segment.receipts[Number(d.segment.receipt_position)].status = '0x0'; }, false);
P('a receipt removed from the shard', d => { d.segment.receipts.splice(d.segment.receipts.length - 1, 1); }, false);
P('the shard receipts root altered', d => { d.segment.shard_receipts_roots[d.segment.shard_index] = flipHex(d.segment.shard_receipts_roots[d.segment.shard_index], 5); }, false);
P('the receipts commitment altered inside the segment record', d => { d.segment.record_hex = flipHex(d.segment.record_hex, 2 * (2 + 8 + 8 + 32 + 48 + 20 + 180) + 3); }, false);
P('the receipt position moved', d => { d.segment.receipt_position = Number(d.segment.receipt_position) === 0 ? 1 : 0; }, false);
P('a log address altered in the receipt', d => { const r = d.segment.receipts[Number(d.segment.receipt_position)]; if (r.logs.length) r.logs[0].address = flipHex(r.logs[0].address, 3); else r.cumulativeGasUsed = '0x' + (BigInt(r.cumulativeGasUsed) + 1n).toString(16); }, false);
P('a header removed from the record\'s path', d => { if (d.segment.headers.length > 1) d.segment.headers.splice(0, 1); else d.segment.headers[0].nonce = String(BigInt(d.segment.headers[0].nonce) ^ 1n); }, false);
P('genuine payment receipt', d => {}, true);
const g = verifyPaymentReceipt(pay, deps); if (g.payment) console.log(` outcome: ${g.outcome.amount_wei} wei of ${g.outcome.asset} to ${g.outcome.recipient}, ${g.outcome.logs.length} log(s), ${g.ms} ms`);
} else if (pay) console.log('payment fixture has no segment data: ' + pay.payment_unavailable);
console.log(failed ? `FAILED ${failed}` : 'RESULT every case behaved');
process.exit(failed ? 1 : 0);

View file

@ -6,7 +6,7 @@ import { readFileSync } from 'node:fs';
import { blake2b } from '@noble/hashes/blake2.js';
import { keccak_256 } from '@noble/hashes/sha3.js';
import { bls12_381 } from '@noble/curves/bls12-381.js';
import { verifyReceipt, formatIgn } from '../../../site/lc/core.js';
import { verifyReceipt, verifyPaymentReceipt, formatIgn } from '../../../site/lc/core.js';
const args = process.argv.slice(2);
const file = args.find(a => !a.startsWith('--'));
@ -24,11 +24,15 @@ if (args.includes('--tamper')) {
console.log(`tampered copy (one nibble of the raw transaction): ${t.verified ? 'NOT REFUSED, this verifier is broken' : 'REFUSED'}${t.reason ? ' :: ' + t.reason : ''}`);
if (t.verified) process.exit(1);
}
const r = verifyReceipt(receipt, deps);
console.log(`TRANSACTION INCLUSION RECEIPT 0x${receipt.tx_hash} on ${receipt.chain_id} (Devnet 3, no value)`);
console.log('What this file authenticates: that the signed transaction is included in a block that is finalised. What it does not: the execution outcome (status, gas), which is carried as the node reported it. A payment receipt, which authenticates the transfer outcome, is a different file.');
const r = receipt.segment ? verifyPaymentReceipt(receipt, deps) : verifyReceipt(receipt, deps);
const payment = !!(r.verified && r.payment);
console.log(`${payment ? 'PAYMENT RECEIPT' : 'TRANSACTION INCLUSION RECEIPT'} 0x${receipt.tx_hash} on ${receipt.chain_id} (Devnet 3, no value)`);
console.log(payment
? 'What this file authenticates: that the signed transaction is included in a finalised block and executed with status success, its receipt sitting in the shard receipts trie whose root the proven segment statement commits to. Trusted: the voter table from the node; the SP1 proof behind the statement is verified by nodes, not here.'
: 'What this file authenticates: that the signed transaction is included in a block that is finalised. What it does not: the execution outcome (status, gas), which is carried as the node reported it.' + (receipt.payment_unavailable ? ' Why no payment receipt: ' + receipt.payment_unavailable : ''));
print(r);
if (!r.verified) { console.log(`REFUSED: ${r.reason}`); process.exit(1); }
const t = r.tx;
console.log(`INCLUSION VERIFIED in ${r.ms} ms: a signed transaction of ${formatIgn(t.value)} IGN to ${t.to || 'contract creation'} (${t.value} wei) is included in block ${r.block.slice(0, 16)} at DAA ${r.block_daa} (${new Date(Number(r.block_time)).toISOString()}), finalised under checkpoint ${r.checkpoint}; ${r.headers} headers checked; ${r.certificate}.`);
console.log('Reported by the node, not authenticated by this file: from ' + (receipt.tx_as_reported && receipt.tx_as_reported.from) + (receipt.execution ? `, executed with status ${receipt.execution.status === '0x1' ? 'success' : 'failed'}` : '') + '. The voter table with weights came from the node (spec 10.1). In the four words: included and finalised are authenticated, executed is reported, proven is not claimed.');
if (payment) console.log(`PAYMENT VERIFIED in ${r.ms} ms: ${formatIgn(t.value)} IGN (${t.value} wei of the native coin) to ${t.to || 'contract creation'}, executed with status success (${r.outcome.logs.length} log(s)) at chain block ${receipt.execution.chain_block}, in block ${r.block.slice(0, 16)} at DAA ${r.block_daa}, finalised under checkpoint ${r.checkpoint}; ${r.certificate}.`);
else console.log(`INCLUSION VERIFIED in ${r.ms} ms: a signed transaction of ${formatIgn(t.value)} IGN to ${t.to || 'contract creation'} (${t.value} wei) is included in block ${r.block.slice(0, 16)} at DAA ${r.block_daa} (${new Date(Number(r.block_time)).toISOString()}), finalised under checkpoint ${r.checkpoint}; ${r.headers} headers checked; ${r.certificate}.`);
console.log('Reported by the node, not authenticated by this file: from ' + (receipt.tx_as_reported && receipt.tx_as_reported.from) + (payment ? '' : (receipt.execution ? `, executed with status ${receipt.execution.status === '0x1' ? 'success' : 'failed'}` : '')) + '. The voter table with weights came from the node (spec 10.1). In the four words: ' + (payment ? 'included, executed, proven and finalised are authenticated under the stated trust.' : 'included and finalised are authenticated, executed is reported, proven is not claimed.'));