diff --git a/site/lc/verify-receipt.js b/site/lc/verify-receipt.js index 72a549134..6c930420a 100644 --- a/site/lc/verify-receipt.js +++ b/site/lc/verify-receipt.js @@ -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."); diff --git a/tools/reference-apps/oracle/README.md b/tools/reference-apps/oracle/README.md index 8acd8eb60..a1cea76b2 100644 --- a/tools/reference-apps/oracle/README.md +++ b/tools/reference-apps/oracle/README.md @@ -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.