Reference apps F04: the one-file verifier rebuilt on the box from the lock-state source; the oracle README names the refused recovery locks

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 19:00:18 +00:00
parent bd39a7c18a
commit 9e689c3051
2 changed files with 20 additions and 2 deletions

View file

@ -1,5 +1,5 @@
#!/usr/bin/env node
// 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
// Igneum transaction inclusion receipt verifier, 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
@ -5226,6 +5226,13 @@ function verifyHeaderPath(headers, blake2b2, fromHash, toHash) {
if (toHash && strip(headers[headers.length - 1].hash) !== strip(toHash)) throw new Error("the last header is not the certified checkpoint");
return headers.length;
}
function lockStateOf(cp) {
const v = cp && (cp.lock_state ?? cp.lockKind ?? (cp.recoveryLock === true ? "recovery" : cp.recoveryLock === false ? "final" : void 0));
if (v === void 0 || v === null || v === "") return null;
if (v !== "final" && v !== "recovery") throw new Error(`unknown lock state "${v}" reported for checkpoint ${cp.index}; refused`);
return v;
}
var lockStateDetail = (st) => st === "recovery" ? "RECOVERY LOCK as the node reports it: a lock under the recovery rule, not the final rule; nothing under it is final" : st === "final" ? "final lock as the node reports it" : "no lock state reported by this node (a node before the field)";
function verifyReceipt(receipt2, deps2) {
const { blake2b: blake2b2, bls: bls2, keccak } = deps2;
const steps = [];
@ -5249,6 +5256,11 @@ function verifyReceipt(receipt2, deps2) {
if (strip(cp.hash) !== strip(receipt2.checkpoint.hash) || receipt2.chain_id !== cp.chain_id) throw new Error("the receipt names another checkpoint or chain than its certificate");
return `checkpoint ${r2.index} on ${cp.chain_id}, ${r2.signers} of ${r2.voters} voters, ${(r2.weight_fraction_total * 100).toFixed(1)}% of total weight`;
});
const lockState = step("lock state: the node's report beside the certificate (the certificate bytes carry none)", () => {
const st = lockStateOf(cp);
if (receipt2.lock_state !== void 0 && receipt2.lock_state !== null && receipt2.lock_state !== (st || "final")) throw new Error(`the receipt says lock_state "${receipt2.lock_state}" but its certificate is reported as ${st || "final (no state field)"}; refused`);
return lockStateDetail(st);
}) && lockStateOf(cp);
let tx = null;
step("the transaction hash is keccak256 of the raw signed transaction", () => {
const raw = hexToBytes3(strip(receipt2.raw_tx_hex));
@ -5265,7 +5277,7 @@ function verifyReceipt(receipt2, deps2) {
if (bytesToHex3(root) !== strip(receipt2.headers[0].hash_merkle_root)) throw new Error("the merkle path does not reach the including block's hash_merkle_root");
return `leaf ${ib.leaf_index} of ${ib.leaf_count}`;
});
return done({ verified: true, tx, headers: receipt2.headers.length, checkpoint: Number(cp.index), certificate: cert, block: strip(receipt2.headers[0].hash), block_daa: receipt2.headers[0].daa_score, block_time: receipt2.headers[0].timestamp });
return done({ verified: true, tx, headers: receipt2.headers.length, checkpoint: Number(cp.index), lock_state: lockState, certificate: cert, block: strip(receipt2.headers[0].hash), block_daa: receipt2.headers[0].daa_score, block_time: receipt2.headers[0].timestamp });
} catch (e) {
return done({ verified: false, reason: String(e.message || e) });
}
@ -5316,12 +5328,14 @@ if (args.includes("--tamper")) {
}
var r = verifyReceipt(receipt, deps);
console.log(`TRANSACTION INCLUSION RECEIPT 0x${receipt.tx_hash} on ${receipt.chain_id} (devnet, no value)`);
if (receipt.lock_state === "recovery") console.log("RECOVERY LOCK: the node reported this checkpoint locked under the recovery rule (more than half of the anchored weight after a full window with no lock), not the final rule. Nothing in this file is final.");
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.");
print(r);
if (!r.verified) {
console.log(`REFUSED: ${r.reason}`);
process.exit(1);
}
if (r.lock_state === "recovery") console.log("LOCK STATE: recovery, as the node reported it; this receipt is included under a recovery lock, not a final one.");
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.");

View file

@ -50,3 +50,7 @@ node demo.mjs
The deployer key is read from `~/.config/igneum/sepolia-deployer`. Sepolia's gas schedule is repriced (a plain
transfer estimates 12,000 gas), so the numbers in `deployment.json` are what this network charges.
## Recovery locks (Review B F04, 8 October 2026)
Recovery locks are not accepted by this verifier: a certificate under half of the installed table's weight reverts in `submitCertificate` (both Sepolia verifiers apply the two-thirds rule only), so every stored root passed the final rule and nothing the oracle answers can read final for a recovery lock. `trust()` gains this sentence at the next redeploy; the page carries it now.