verifyhash 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +883 -0
- package/cli/abi/ContributionRegistry.json +881 -0
- package/cli/agent.js +2173 -0
- package/cli/anchor-artifact.js +853 -0
- package/cli/anchor.js +400 -0
- package/cli/claim.js +881 -0
- package/cli/core/agent-commit.js +448 -0
- package/cli/core/agent-session.js +598 -0
- package/cli/core/anchor-binding.js +663 -0
- package/cli/core/attestation.js +580 -0
- package/cli/core/evidence-plans.js +495 -0
- package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
- package/cli/core/fulfill-intake.js +1082 -0
- package/cli/core/go-live-preflight.js +481 -0
- package/cli/core/license.js +534 -0
- package/cli/core/manifest.js +243 -0
- package/cli/core/packetseal.js +591 -0
- package/cli/core/registryArtifact.js +49 -0
- package/cli/core/revocation.js +539 -0
- package/cli/core/rfc3161.js +389 -0
- package/cli/core/timestamp.js +482 -0
- package/cli/core/trust-asof.js +479 -0
- package/cli/dataset.js +2950 -0
- package/cli/evidence.js +2227 -0
- package/cli/fulfill-webhook-http.js +438 -0
- package/cli/git.js +220 -0
- package/cli/hash.js +550 -0
- package/cli/identity.js +1072 -0
- package/cli/journal-cli.js +1110 -0
- package/cli/journal-log.js +454 -0
- package/cli/journal.js +334 -0
- package/cli/lineage.js +447 -0
- package/cli/list.js +287 -0
- package/cli/parcel.js +1509 -0
- package/cli/proof.js +578 -0
- package/cli/prove.js +300 -0
- package/cli/receipt.js +631 -0
- package/cli/registry.js +331 -0
- package/cli/reputation.js +344 -0
- package/cli/revocation.js +495 -0
- package/cli/serve-verify-http.js +298 -0
- package/cli/serve-verify.js +333 -0
- package/cli/show.js +339 -0
- package/cli/verify.js +383 -0
- package/cli/vh.js +3927 -0
- package/docs/ADOPT.md +183 -0
- package/docs/ADOPTION.json +11 -0
- package/docs/AGENTTRACE.md +247 -0
- package/docs/ANCHORING.md +167 -0
- package/docs/AUDIT.md +55 -0
- package/docs/CONFORMANCE.md +107 -0
- package/docs/DATALEDGER.md +638 -0
- package/docs/DECIDE.md +47 -0
- package/docs/DECISIONS-PENDING.md +27 -0
- package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
- package/docs/ENGINE-LEDGER.json +12 -0
- package/docs/EVIDENCE.md +519 -0
- package/docs/GO-LIVE.md +66 -0
- package/docs/IDENTITY.md +123 -0
- package/docs/INDEPENDENT-VERIFICATION.md +377 -0
- package/docs/INTEGRITY-JOURNAL.md +337 -0
- package/docs/KEY-LIFECYCLE.md +179 -0
- package/docs/LICENSING.md +46 -0
- package/docs/LINEAGE.md +307 -0
- package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
- package/docs/LOOP-HARDENING-PLAN.md +44 -0
- package/docs/MERKLE-LEAVES.md +113 -0
- package/docs/METRICS.jsonl +31 -0
- package/docs/MORNING.md +204 -0
- package/docs/PILOT.md +444 -0
- package/docs/PROOFPARCEL.md +227 -0
- package/docs/PROOFS.md +262 -0
- package/docs/RECEIPTS.md +341 -0
- package/docs/REPUTATION.md +158 -0
- package/docs/SDK.md +301 -0
- package/docs/STRATEGY-ARCHIVE.md +5055 -0
- package/docs/SUPERVISOR-RUNBOOK.md +52 -0
- package/docs/TRUST-BOUNDARIES.md +335 -0
- package/docs/TRUSTLEDGER.md +1976 -0
- package/docs/USAGE-BUDGET.json +121 -0
- package/docs/VERIFY-SERVICE.md +168 -0
- package/index.js +160 -0
- package/package.json +41 -0
- package/trustledger/build-standalone.js +796 -0
- package/trustledger/cli.js +3179 -0
- package/trustledger/close.js +391 -0
- package/trustledger/corpus.js +159 -0
- package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
- package/trustledger/dist/trustledger-standalone.html +6197 -0
- package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
- package/trustledger/door-core.js +442 -0
- package/trustledger/fixtures/bank.csv +7 -0
- package/trustledger/fixtures/bank.malformed.csv +3 -0
- package/trustledger/fixtures/bank.noalias.csv +5 -0
- package/trustledger/fixtures/bank.ofx +34 -0
- package/trustledger/fixtures/bank.real.csv +5 -0
- package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
- package/trustledger/fixtures/e2e/bank.csv +4 -0
- package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
- package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
- package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.csv +6 -0
- package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
- package/trustledger/fixtures/plans/baseline.json +25 -0
- package/trustledger/fixtures/plans/price-binding.example.json +27 -0
- package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
- package/trustledger/fixtures/policy/baseline.json +19 -0
- package/trustledger/fixtures/policy/ca-example.json +12 -0
- package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
- package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
- package/trustledger/fixtures/quickbooks.csv +7 -0
- package/trustledger/fixtures/quickbooks.real.csv +5 -0
- package/trustledger/fixtures/rentroll.csv +6 -0
- package/trustledger/fixtures/rentroll.real.csv +4 -0
- package/trustledger/ingest.js +1163 -0
- package/trustledger/lib/policy-bundled-loader.js +44 -0
- package/trustledger/lib/sha256-vendored.js +227 -0
- package/trustledger/license.js +563 -0
- package/trustledger/match.js +551 -0
- package/trustledger/plans.js +551 -0
- package/trustledger/policy.js +398 -0
- package/trustledger/public/index.html +512 -0
- package/trustledger/reconcile.js +1486 -0
- package/trustledger/report.js +887 -0
- package/trustledger/seal.js +854 -0
- package/trustledger/server.js +391 -0
- package/trustledger/valueproof.js +350 -0
package/cli/proof.js
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// cli/proof.js — a versioned, strictly-validated, PORTABLE Merkle-proof artifact for verifyhash.
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS EXISTS
|
|
6
|
+
// `vh prove <file> --root <dir>` builds a genuine Merkle proof that a single file belongs to an
|
|
7
|
+
// anchored repository root, but historically it only PRINTED the proof or checked it on-chain
|
|
8
|
+
// in-process against the prover's own working tree. That makes the proof non-portable: a third
|
|
9
|
+
// party handed "file X is in anchored repo root R" cannot independently confirm it without
|
|
10
|
+
// re-running the prover against the prover's files. That directly contradicts the project's core
|
|
11
|
+
// promise — "anyone can later prove some content is byte-for-byte what was anchored, without
|
|
12
|
+
// trusting any server."
|
|
13
|
+
//
|
|
14
|
+
// This module closes that gap. `vh prove <file> --root <dir> --out <p>` writes a SELF-CONTAINED
|
|
15
|
+
// proof artifact: everything a verifier needs is in the file. `vh verify-proof <p>` then verifies
|
|
16
|
+
// it needing ONLY the artifact + an RPC URL — never the original repo or working tree:
|
|
17
|
+
// (a) it re-derives the leaf from `contentHash` + `relPath` and re-folds `proof` PURELY OFFLINE,
|
|
18
|
+
// using the SAME sorted-pair / domain-separated convention the contract's verifyLeaf uses
|
|
19
|
+
// (reusing hash.js's pathLeaf / leafHash / nodeHash — NOT a re-implementation), to confirm
|
|
20
|
+
// the proof folds back to `root`; then
|
|
21
|
+
// (b) it makes ONE read-only on-chain check that the root is ACTUALLY anchored (isAnchored) and
|
|
22
|
+
// that the contract's own verifyLeaf accepts the proof.
|
|
23
|
+
//
|
|
24
|
+
// TRUST POSTURE (consistent with docs/TRUST-BOUNDARIES.md)
|
|
25
|
+
// The artifact is an UNTRUSTED TRANSPORT CONTAINER. verify-proof never trusts the file's claims —
|
|
26
|
+
// it RE-DERIVES the leaf from contentHash+relPath (so a forged `leaf` field that doesn't match its
|
|
27
|
+
// own contentHash+relPath is rejected) and re-folds the proof itself; the `root` it checks on-chain
|
|
28
|
+
// is the one the offline fold produced from the proof, and a root that was never anchored reports
|
|
29
|
+
// NOT ANCHORED rather than a false ACCEPT. What this proves is SET-MEMBERSHIP: that the named file
|
|
30
|
+
// (path + bytes) is a leaf of an anchored repo root. It says NOTHING about authorship, the meaning
|
|
31
|
+
// of `contributor`, or any `uri` — exactly as the contract's verifyLeaf says nothing about those.
|
|
32
|
+
// A corrupt artifact must never be silently half-accepted: readProofArtifact validates strictly and
|
|
33
|
+
// throws on ANY deviation rather than filling defaults (mirroring cli/receipt.js's posture).
|
|
34
|
+
|
|
35
|
+
const fs = require("fs");
|
|
36
|
+
const { pathLeaf, leafHash, nodeHash } = require("./hash");
|
|
37
|
+
const {
|
|
38
|
+
assertRegistry,
|
|
39
|
+
formatRegistryLine,
|
|
40
|
+
formatSkippedLine,
|
|
41
|
+
jsonRegistryBlock,
|
|
42
|
+
jsonSkippedBlock,
|
|
43
|
+
} = require("./registry");
|
|
44
|
+
|
|
45
|
+
const ARTIFACT = require("./core/registryArtifact");
|
|
46
|
+
const ABI = ARTIFACT.abi;
|
|
47
|
+
|
|
48
|
+
// On-disk schema discriminators. A proof artifact carries its OWN kind + version (distinct from the
|
|
49
|
+
// receipt kinds in cli/receipt.js) so a random JSON file, a receipt, or a future/foreign artifact is
|
|
50
|
+
// never misread as a current proof artifact.
|
|
51
|
+
const PROOF_KIND = "verifyhash.merkle-proof";
|
|
52
|
+
const PROOF_SCHEMA_VERSION = 1;
|
|
53
|
+
const SUPPORTED_PROOF_SCHEMA_VERSIONS = Object.freeze([1]);
|
|
54
|
+
|
|
55
|
+
// Same hex/address shapes cli/receipt.js validates against, so the two modules never drift.
|
|
56
|
+
const HEX32_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
57
|
+
const ADDR_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
58
|
+
|
|
59
|
+
// The one-line trust boundary that LEADS every human-readable verify-proof run. It is load-bearing,
|
|
60
|
+
// not decorative: a reader must never mistake a set-membership proof for proof of authorship/URI.
|
|
61
|
+
const TRUST_CAVEAT = [
|
|
62
|
+
"NOTE: this proves SET-MEMBERSHIP only — that the named file (its path + bytes) is a leaf of an",
|
|
63
|
+
"anchored repo Merkle root. It does NOT prove authorship, who anchored the root, or anything about",
|
|
64
|
+
"any `uri`. The artifact is an UNTRUSTED transport container: verify-proof RE-DERIVES the leaf and",
|
|
65
|
+
"RE-FOLDS the proof itself (it never trusts the file's claims), then confirms the root is anchored",
|
|
66
|
+
"on-chain. Set-membership in an anchored root is exactly what the contract's verifyLeaf attests.",
|
|
67
|
+
].join("\n");
|
|
68
|
+
|
|
69
|
+
// Verify-proof outcomes. ACCEPTED requires BOTH the offline fold AND the on-chain checks to pass.
|
|
70
|
+
const STATUS = Object.freeze({
|
|
71
|
+
ACCEPTED: "ACCEPTED", // offline fold folds to root AND root is anchored AND on-chain verifyLeaf true
|
|
72
|
+
REJECTED: "REJECTED", // an offline or on-chain check failed (tampered proof, leaf, contentHash, …)
|
|
73
|
+
NOT_ANCHORED: "NOT_ANCHORED", // offline fold is fine, but the root was never anchored on-chain
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build a normalized, fully-validated portable proof artifact from a built proof (the object
|
|
78
|
+
* `buildProof` in cli/prove.js returns) plus optional on-chain context. Throws if any required field
|
|
79
|
+
* is missing or malformed, so a corrupt artifact is never even written.
|
|
80
|
+
*
|
|
81
|
+
* `relPath` is the file's repo-relative POSIX path — exactly what was bound into the leaf, so a
|
|
82
|
+
* verifier can RE-DERIVE the leaf from contentHash + relPath without the original tree. The optional
|
|
83
|
+
* `contractAddress` / `chainId` record WHERE the prover expects the root to be anchored; they are
|
|
84
|
+
* UNTRUSTED hints (the verifier may override with --contract/--rpc) but recording them makes the
|
|
85
|
+
* artifact more self-describing.
|
|
86
|
+
*
|
|
87
|
+
* @param {object} built a buildProof() result: { root, leaf, contentHash, proof, file }
|
|
88
|
+
* @param {object} [ctx]
|
|
89
|
+
* @param {string} [ctx.contractAddress] 0x 20-byte ContributionRegistry address the root is anchored at
|
|
90
|
+
* @param {number|string|bigint} [ctx.chainId] the chain the root is anchored on
|
|
91
|
+
* @returns {object} a validated proof-artifact object
|
|
92
|
+
*/
|
|
93
|
+
function buildProofArtifact(built, ctx = {}) {
|
|
94
|
+
if (!built || typeof built !== "object") {
|
|
95
|
+
throw new Error("buildProofArtifact requires the object buildProof() returns");
|
|
96
|
+
}
|
|
97
|
+
const artifact = {
|
|
98
|
+
kind: PROOF_KIND,
|
|
99
|
+
schemaVersion: PROOF_SCHEMA_VERSION,
|
|
100
|
+
root: built.root,
|
|
101
|
+
leaf: built.leaf,
|
|
102
|
+
contentHash: built.contentHash,
|
|
103
|
+
relPath: built.file,
|
|
104
|
+
proof: built.proof,
|
|
105
|
+
};
|
|
106
|
+
if (ctx.contractAddress != null) artifact.contractAddress = ctx.contractAddress;
|
|
107
|
+
if (ctx.chainId != null) artifact.chainId = _normChainId(ctx.chainId);
|
|
108
|
+
_validate(artifact);
|
|
109
|
+
return artifact;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Normalize a chainId (number|string|bigint) to a non-negative integer Number, like receipt.js. */
|
|
113
|
+
function _normChainId(v) {
|
|
114
|
+
let n;
|
|
115
|
+
try {
|
|
116
|
+
n = Number(BigInt(v));
|
|
117
|
+
} catch (_) {
|
|
118
|
+
throw new Error(`proof artifact chainId must be an integer, got: ${String(v)}`);
|
|
119
|
+
}
|
|
120
|
+
if (!Number.isSafeInteger(n) || n < 0) {
|
|
121
|
+
throw new Error(`proof artifact chainId must be a non-negative integer, got: ${String(v)}`);
|
|
122
|
+
}
|
|
123
|
+
return n;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Strictly validate a parsed proof-artifact object. Throws an Error describing the FIRST problem.
|
|
128
|
+
* Never mutates and never fills defaults — an artifact either is complete and well-formed or it is
|
|
129
|
+
* rejected outright (mirroring cli/receipt.js's _validate). A malformed/short hash or a `proof` that
|
|
130
|
+
* is not an array of 32-byte hex strings hard-errors here, so verify-proof can never silently accept
|
|
131
|
+
* a structurally bogus file.
|
|
132
|
+
* @param {any} obj
|
|
133
|
+
* @returns {object} the same object, if valid
|
|
134
|
+
*/
|
|
135
|
+
function _validate(obj) {
|
|
136
|
+
if (obj == null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
137
|
+
throw new Error("proof artifact must be a JSON object");
|
|
138
|
+
}
|
|
139
|
+
if (obj.kind !== PROOF_KIND) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`not a verifyhash proof artifact (kind: ${JSON.stringify(obj.kind)}; expected ${JSON.stringify(
|
|
142
|
+
PROOF_KIND
|
|
143
|
+
)})`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (!SUPPORTED_PROOF_SCHEMA_VERSIONS.includes(obj.schemaVersion)) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`unsupported proof artifact schemaVersion: ${JSON.stringify(obj.schemaVersion)} ` +
|
|
149
|
+
`(this build understands ${JSON.stringify(SUPPORTED_PROOF_SCHEMA_VERSIONS)})`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const f of ["root", "leaf", "contentHash"]) {
|
|
154
|
+
const v = obj[f];
|
|
155
|
+
if (v === undefined || v === null) throw new Error(`proof artifact missing required field: ${f}`);
|
|
156
|
+
if (typeof v !== "string" || !HEX32_RE.test(v)) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`proof artifact field ${f} must be a 0x-prefixed 32-byte hex string, got: ${String(v)}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (typeof obj.relPath !== "string" || obj.relPath.length === 0) {
|
|
164
|
+
throw new Error(`proof artifact relPath must be a non-empty string, got: ${String(obj.relPath)}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!Array.isArray(obj.proof)) {
|
|
168
|
+
throw new Error("proof artifact field proof must be an array of 0x 32-byte hex siblings");
|
|
169
|
+
}
|
|
170
|
+
obj.proof.forEach((sib, i) => {
|
|
171
|
+
if (typeof sib !== "string" || !HEX32_RE.test(sib)) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`proof artifact proof[${i}] must be a 0x-prefixed 32-byte hex string, got: ${String(sib)}`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Optional on-chain context. Validate SHAPE only when present (an artifact built with --dry-run and
|
|
179
|
+
// no chain context legitimately omits both).
|
|
180
|
+
if (obj.contractAddress !== undefined && obj.contractAddress !== null) {
|
|
181
|
+
if (typeof obj.contractAddress !== "string" || !ADDR_RE.test(obj.contractAddress)) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`proof artifact contractAddress must be a 0x-prefixed 20-byte address when present, got: ${String(
|
|
184
|
+
obj.contractAddress
|
|
185
|
+
)}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (obj.chainId !== undefined && obj.chainId !== null) {
|
|
190
|
+
if (!Number.isSafeInteger(obj.chainId) || obj.chainId < 0) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`proof artifact chainId must be a non-negative integer when present, got: ${String(obj.chainId)}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return obj;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Validate and write a proof artifact to `path` as pretty JSON. The only side effect is the file
|
|
202
|
+
* write, and it throws (before writing) if the object is not a valid artifact, so a corrupt artifact
|
|
203
|
+
* never lands on disk. Mirrors cli/receipt.js's writeReceipt.
|
|
204
|
+
* @param {object} obj a proof artifact (typically from buildProofArtifact)
|
|
205
|
+
* @param {string} path destination file path (caller-chosen — never silently the cwd)
|
|
206
|
+
* @returns {object} the validated object that was written
|
|
207
|
+
*/
|
|
208
|
+
function writeProofArtifact(obj, path) {
|
|
209
|
+
if (!path || typeof path !== "string") {
|
|
210
|
+
throw new Error("writeProofArtifact requires a destination path");
|
|
211
|
+
}
|
|
212
|
+
const valid = _validate(obj);
|
|
213
|
+
fs.writeFileSync(path, JSON.stringify(valid, null, 2) + "\n");
|
|
214
|
+
return valid;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Read, JSON-parse, and strictly validate a proof artifact from `path`. Throws a clear error if the
|
|
219
|
+
* file is missing, not JSON, or fails validation — it NEVER returns a partial/corrupt artifact.
|
|
220
|
+
* Mirrors cli/receipt.js's readReceipt.
|
|
221
|
+
* @param {string} path
|
|
222
|
+
* @returns {object} the validated artifact
|
|
223
|
+
*/
|
|
224
|
+
function readProofArtifact(path) {
|
|
225
|
+
if (!path || typeof path !== "string") {
|
|
226
|
+
throw new Error("readProofArtifact requires a path");
|
|
227
|
+
}
|
|
228
|
+
let raw;
|
|
229
|
+
try {
|
|
230
|
+
raw = fs.readFileSync(path, "utf8");
|
|
231
|
+
} catch (e) {
|
|
232
|
+
throw new Error(`cannot read proof artifact at ${path}: ${e.message}`);
|
|
233
|
+
}
|
|
234
|
+
let parsed;
|
|
235
|
+
try {
|
|
236
|
+
parsed = JSON.parse(raw);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
throw new Error(`proof artifact at ${path} is not valid JSON: ${e.message}`);
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
return _validate(parsed);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
throw new Error(`proof artifact at ${path} is invalid: ${e.message}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Re-fold a proof artifact PURELY OFFLINE — no network — to confirm it is internally consistent. This
|
|
249
|
+
* is the portability core: it re-derives the leaf from contentHash + relPath and replays the proof
|
|
250
|
+
* with the SAME convention the contract's verifyLeaf uses, reusing hash.js's pathLeaf / leafHash /
|
|
251
|
+
* nodeHash (NOT a re-implementation). Two independent things are checked:
|
|
252
|
+
*
|
|
253
|
+
* 1. leafMatches — the artifact's `leaf` actually equals pathLeaf(relPath, contentHash). A forged
|
|
254
|
+
* `leaf` that does not match its own claimed contentHash+relPath fails here, so verify-proof can
|
|
255
|
+
* never be fooled by swapping the leaf alone.
|
|
256
|
+
* 2. foldsToRoot — folding leafHash(leaf) up through `proof` with nodeHash reproduces `root` (the
|
|
257
|
+
* exact computation verifyLeaf does on-chain: it applies LEAF_TAG to the supplied value, then
|
|
258
|
+
* folds NODE_TAG sorted-pairs). The `computedRoot` is returned so the caller checks the SAME root
|
|
259
|
+
* on-chain that the offline fold produced — not merely the `root` field the file claims.
|
|
260
|
+
*
|
|
261
|
+
* @param {object} artifact a validated proof artifact (from readProofArtifact / buildProofArtifact)
|
|
262
|
+
* @returns {{
|
|
263
|
+
* derivedLeaf: string, // pathLeaf(relPath, contentHash) — what the leaf MUST be
|
|
264
|
+
* leafMatches: boolean, // artifact.leaf === derivedLeaf
|
|
265
|
+
* computedRoot: string, // fold of leafHash(leaf) through proof
|
|
266
|
+
* foldsToRoot: boolean, // computedRoot === artifact.root
|
|
267
|
+
* offlineOk: boolean, // leafMatches && foldsToRoot
|
|
268
|
+
* }}
|
|
269
|
+
*/
|
|
270
|
+
function recomputeFold(artifact) {
|
|
271
|
+
// Re-derive the leaf from the path + content digest the artifact carries. This is exactly the
|
|
272
|
+
// path-bound leaf hashDir/buildProof produced, so a genuine artifact's stored `leaf` equals it.
|
|
273
|
+
const derivedLeaf = pathLeaf(artifact.relPath, artifact.contentHash);
|
|
274
|
+
const leafMatches = derivedLeaf.toLowerCase() === artifact.leaf.toLowerCase();
|
|
275
|
+
|
|
276
|
+
// Fold the (tagged) leaf up through the proof, byte-identically to the on-chain verifyLeaf:
|
|
277
|
+
// computed = leafHash(leaf); for each sibling s: computed = nodeHash(computed, s)
|
|
278
|
+
// We fold the artifact's stored leaf (the value the contract is handed); leafMatches above
|
|
279
|
+
// independently guarantees that stored leaf is the genuine pathLeaf for (relPath, contentHash).
|
|
280
|
+
let computed = leafHash(artifact.leaf);
|
|
281
|
+
for (const sibling of artifact.proof) {
|
|
282
|
+
computed = nodeHash(computed, sibling);
|
|
283
|
+
}
|
|
284
|
+
const computedRoot = computed;
|
|
285
|
+
const foldsToRoot = computedRoot.toLowerCase() === artifact.root.toLowerCase();
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
derivedLeaf,
|
|
289
|
+
leafMatches,
|
|
290
|
+
computedRoot,
|
|
291
|
+
foldsToRoot,
|
|
292
|
+
offlineOk: leafMatches && foldsToRoot,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Render a verify-proof result as the human-readable block the CLI prints. Always LEADS with the
|
|
298
|
+
* trust-boundary one-liner (set-membership only, not authorship/uri), then the per-check breakdown,
|
|
299
|
+
* then the verdict and — on anything other than ACCEPTED — exactly which check failed.
|
|
300
|
+
*/
|
|
301
|
+
function formatVerifyProof(r) {
|
|
302
|
+
const yn = (b) => (b ? "yes" : "NO");
|
|
303
|
+
const lines = [
|
|
304
|
+
TRUST_CAVEAT,
|
|
305
|
+
"",
|
|
306
|
+
` proof artifact: ${r.artifactPath}`,
|
|
307
|
+
` relPath: ${r.relPath}`,
|
|
308
|
+
` contentHash: ${r.contentHash}`,
|
|
309
|
+
` leaf: ${r.leaf}`,
|
|
310
|
+
` root: ${r.root}`,
|
|
311
|
+
` proof siblings: ${r.proofLength}`,
|
|
312
|
+
"",
|
|
313
|
+
" offline recompute (no network):",
|
|
314
|
+
` leaf re-derived from contentHash+relPath: ${yn(r.leafMatches)}`,
|
|
315
|
+
` proof folds to the claimed root: ${yn(r.foldsToRoot)}`,
|
|
316
|
+
];
|
|
317
|
+
// T-11.2: the registry-authentication confirmation (or the loud skip warning), printed BEFORE the
|
|
318
|
+
// on-chain checks so a reader sees the contract+network were authenticated before believing them.
|
|
319
|
+
if (r.checkedChain || r.identitySkipped) {
|
|
320
|
+
if (r.identitySkipped) {
|
|
321
|
+
lines.push("", formatSkippedLine());
|
|
322
|
+
} else if (r.registry) {
|
|
323
|
+
lines.push("", formatRegistryLine(r.registry));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// On-chain checks are only meaningful once the offline fold holds; we still report what we did.
|
|
327
|
+
if (r.checkedChain) {
|
|
328
|
+
lines.push(
|
|
329
|
+
"",
|
|
330
|
+
" on-chain checks (one read-only call set):",
|
|
331
|
+
` root is anchored (isAnchored): ${yn(r.rootAnchored)}`,
|
|
332
|
+
` contract verifyLeaf accepts the proof: ${yn(r.onChainVerified)}`
|
|
333
|
+
);
|
|
334
|
+
} else if (r.offlineOk) {
|
|
335
|
+
lines.push("", " on-chain checks: SKIPPED (no provider) — offline fold only.");
|
|
336
|
+
}
|
|
337
|
+
lines.push("", ` result: ${r.status}`);
|
|
338
|
+
|
|
339
|
+
if (r.status === STATUS.ACCEPTED) {
|
|
340
|
+
lines.push(
|
|
341
|
+
" ACCEPTED: the file is a leaf of a Merkle root that is anchored on-chain (set-membership",
|
|
342
|
+
" proven offline AND confirmed on-chain). This binds the file's path + bytes to the anchored",
|
|
343
|
+
" root; it does NOT attest authorship or the meaning of `contributor`/`uri`."
|
|
344
|
+
);
|
|
345
|
+
} else if (r.status === STATUS.NOT_ANCHORED) {
|
|
346
|
+
lines.push(
|
|
347
|
+
" NOT ANCHORED: the proof folds to its root OFFLINE, but that root was never anchored on-chain.",
|
|
348
|
+
" There is nothing on-chain to prove the file against (it was never anchored, or you are pointed",
|
|
349
|
+
" at the wrong contract/chain). This is NOT an accept."
|
|
350
|
+
);
|
|
351
|
+
} else {
|
|
352
|
+
// REJECTED — name the first failed check so the reason is unambiguous.
|
|
353
|
+
if (!r.leafMatches) {
|
|
354
|
+
lines.push(
|
|
355
|
+
" REJECTED: the artifact's `leaf` does NOT equal pathLeaf(contentHash, relPath) — the leaf,",
|
|
356
|
+
" contentHash, or relPath was altered. A tampered leaf/contentHash is caught here offline."
|
|
357
|
+
);
|
|
358
|
+
} else if (!r.foldsToRoot) {
|
|
359
|
+
lines.push(
|
|
360
|
+
" REJECTED: the proof does NOT fold to the claimed root — a `proof` sibling (or the root) was",
|
|
361
|
+
" altered. The file is not a member of that root. Caught here offline, no network needed."
|
|
362
|
+
);
|
|
363
|
+
} else if (r.checkedChain && !r.onChainVerified) {
|
|
364
|
+
lines.push(
|
|
365
|
+
" REJECTED: the offline fold held, but the on-chain verifyLeaf rejected the proof against the",
|
|
366
|
+
" anchored root. (The on-chain root differs from the artifact's root, or the proof was altered.)"
|
|
367
|
+
);
|
|
368
|
+
} else {
|
|
369
|
+
lines.push(" REJECTED: a verification check failed.");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return lines.join("\n");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Shape a verify-proof result for `--json`. A machine consumer gets the same verdict + per-check
|
|
377
|
+
* booleans as the human block (so `--json` round-trips), plus the artifact's identifying hashes. The
|
|
378
|
+
* trust caveat is included verbatim so a JSON consumer can surface it too.
|
|
379
|
+
*/
|
|
380
|
+
function jsonVerifyProof(r) {
|
|
381
|
+
return {
|
|
382
|
+
kind: PROOF_KIND,
|
|
383
|
+
artifactPath: r.artifactPath,
|
|
384
|
+
relPath: r.relPath,
|
|
385
|
+
contentHash: r.contentHash,
|
|
386
|
+
leaf: r.leaf,
|
|
387
|
+
root: r.root,
|
|
388
|
+
proofLength: r.proofLength,
|
|
389
|
+
offline: {
|
|
390
|
+
leafMatches: r.leafMatches,
|
|
391
|
+
foldsToRoot: r.foldsToRoot,
|
|
392
|
+
ok: r.offlineOk,
|
|
393
|
+
},
|
|
394
|
+
onChain: r.checkedChain
|
|
395
|
+
? { checked: true, rootAnchored: r.rootAnchored, verifyLeaf: r.onChainVerified }
|
|
396
|
+
: { checked: false },
|
|
397
|
+
// T-11.2: the machine-readable registry block — proves the on-chain leg ran against an
|
|
398
|
+
// authenticated registry on the artifact's recorded chain (or that the check was skipped). null
|
|
399
|
+
// when no on-chain leg ran (offline-only / rejected before the chain check).
|
|
400
|
+
registry: r.identitySkipped
|
|
401
|
+
? jsonSkippedBlock()
|
|
402
|
+
: r.registry
|
|
403
|
+
? jsonRegistryBlock(r.registry)
|
|
404
|
+
: null,
|
|
405
|
+
accepted: r.status === STATUS.ACCEPTED,
|
|
406
|
+
status: r.status,
|
|
407
|
+
trustNote: TRUST_CAVEAT,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Verify a portable proof artifact. Read-only: needs ONLY the artifact + (for the on-chain leg) a
|
|
413
|
+
* provider. NEVER needs the original repo/working tree, and NEVER a signer or key — that is the
|
|
414
|
+
* portability property: hand someone the artifact and an RPC URL and they can independently confirm
|
|
415
|
+
* the file is in the anchored root with no trust in the prover.
|
|
416
|
+
*
|
|
417
|
+
* Flow:
|
|
418
|
+
* 1. Read + strictly validate the artifact (a malformed/short hash or non-hex proof hard-ERRORS).
|
|
419
|
+
* 2. Recompute the leaf from contentHash+relPath and re-fold the proof PURELY OFFLINE. If that fold
|
|
420
|
+
* fails (tampered leaf/contentHash/proof/root), the verdict is REJECTED immediately — no network.
|
|
421
|
+
* 3. If a provider is supplied, make the on-chain checks against the SAME root the offline fold
|
|
422
|
+
* produced: isAnchored(root) AND verifyLeaf(root, leaf, proof). A root that was never anchored is
|
|
423
|
+
* reported as NOT_ANCHORED (a distinct, non-accept outcome), distinguished from a genuine RPC
|
|
424
|
+
* error exactly as verify.js/show.js do (a real error is re-thrown, not masqueraded). ACCEPTED is
|
|
425
|
+
* printed ONLY when the offline fold AND both on-chain checks pass.
|
|
426
|
+
*
|
|
427
|
+
* @param {object} opts
|
|
428
|
+
* @param {string} opts.artifactPath path to the proof artifact JSON
|
|
429
|
+
* @param {string} [opts.contractAddress] override the artifact's contractAddress (else use it)
|
|
430
|
+
* @param {object} [opts.provider] ethers v6 Provider (read-only); omit for an offline-only run
|
|
431
|
+
* @param {boolean}[opts.json] emit a JSON object instead of the human block
|
|
432
|
+
* @param {object} [opts.ethers] ethers v6 module (defaults to the bundled one)
|
|
433
|
+
* @param {(s:string)=>void}[opts.log] sink for output (defaults to process.stdout)
|
|
434
|
+
* @returns {Promise<object>} the structured result
|
|
435
|
+
*/
|
|
436
|
+
async function runVerifyProof(opts) {
|
|
437
|
+
const ethersLib = opts.ethers || require("ethers");
|
|
438
|
+
const log = opts.log || ((s) => process.stdout.write(s));
|
|
439
|
+
|
|
440
|
+
const artifact = readProofArtifact(opts.artifactPath);
|
|
441
|
+
const fold = recomputeFold(artifact);
|
|
442
|
+
|
|
443
|
+
const result = {
|
|
444
|
+
artifactPath: opts.artifactPath,
|
|
445
|
+
relPath: artifact.relPath,
|
|
446
|
+
contentHash: artifact.contentHash,
|
|
447
|
+
leaf: artifact.leaf,
|
|
448
|
+
root: artifact.root,
|
|
449
|
+
proofLength: artifact.proof.length,
|
|
450
|
+
derivedLeaf: fold.derivedLeaf,
|
|
451
|
+
leafMatches: fold.leafMatches,
|
|
452
|
+
computedRoot: fold.computedRoot,
|
|
453
|
+
foldsToRoot: fold.foldsToRoot,
|
|
454
|
+
offlineOk: fold.offlineOk,
|
|
455
|
+
checkedChain: false,
|
|
456
|
+
rootAnchored: null,
|
|
457
|
+
onChainVerified: null,
|
|
458
|
+
contractAddress: null,
|
|
459
|
+
// T-11.2: the resolved registry identity (or null when not yet checked / skipped / offline-only).
|
|
460
|
+
registry: null,
|
|
461
|
+
identitySkipped: Boolean(opts.skipIdentityCheck),
|
|
462
|
+
artifactChainId: artifact.chainId != null ? artifact.chainId : null,
|
|
463
|
+
status: STATUS.REJECTED,
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// The offline fold is the gate: if the artifact is not internally consistent (tampered leaf/
|
|
467
|
+
// contentHash/proof/root), it is REJECTED before any network call. Membership in a root the proof
|
|
468
|
+
// does not even fold to is meaningless to check on-chain.
|
|
469
|
+
if (!fold.offlineOk) {
|
|
470
|
+
result.status = STATUS.REJECTED;
|
|
471
|
+
_emit(result, opts, log);
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const provider = opts.provider;
|
|
476
|
+
if (!provider) {
|
|
477
|
+
// No provider: the offline fold is the only thing we can assert. The acceptance criteria require
|
|
478
|
+
// the on-chain leg for an ACCEPTED verdict, so without a provider we do NOT claim ACCEPTED — we
|
|
479
|
+
// surface the offline-only result (status stays REJECTED so a script never reads it as a full
|
|
480
|
+
// pass). Callers that want an offline-only confirmation read result.offlineOk.
|
|
481
|
+
result.status = STATUS.REJECTED;
|
|
482
|
+
result.note = "no provider: offline fold passed but the on-chain anchored check was not performed";
|
|
483
|
+
_emit(result, opts, log);
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Resolve the contract address: explicit override > the artifact's recorded address. The artifact's
|
|
488
|
+
// address is an untrusted hint, so an explicit --contract always wins.
|
|
489
|
+
const contractAddress = opts.contractAddress || artifact.contractAddress;
|
|
490
|
+
if (!contractAddress) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
"no contract address: pass --contract <address> (or set VH_CONTRACT), " +
|
|
493
|
+
"or use an artifact that records its contractAddress"
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
if (!ethersLib.isAddress(contractAddress)) {
|
|
497
|
+
throw new Error(`invalid contract address: ${contractAddress}`);
|
|
498
|
+
}
|
|
499
|
+
result.contractAddress = ethersLib.getAddress(contractAddress);
|
|
500
|
+
|
|
501
|
+
// T-11.2: authenticate the registry BEFORE the on-chain checks — and cross-check the chainId. The
|
|
502
|
+
// artifact's recorded `chainId` (T-9.2) is passed as expectedChainId, so the offline fold + on-chain
|
|
503
|
+
// checks are believed ONLY once the provider is confirmed to be the right network AND the contract is
|
|
504
|
+
// the real registry. This is the portability promise made trustworthy: the consumer no longer has to
|
|
505
|
+
// trust the prover's RPC blindly. (A power user pointed at a known local/not-yet-deployed contract can
|
|
506
|
+
// opt out, loudly, via skipIdentityCheck.)
|
|
507
|
+
let registryAuth = null;
|
|
508
|
+
if (!opts.skipIdentityCheck) {
|
|
509
|
+
registryAuth = await assertRegistry({
|
|
510
|
+
provider,
|
|
511
|
+
contractAddress: result.contractAddress,
|
|
512
|
+
// The artifact's chainId is an UNTRUSTED hint we now ENFORCE: if it disagrees with the provider's
|
|
513
|
+
// chain, refuse to report a verdict against the wrong network.
|
|
514
|
+
expectedChainId: artifact.chainId,
|
|
515
|
+
ethers: ethersLib,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
result.registry = registryAuth;
|
|
519
|
+
result.identitySkipped = Boolean(opts.skipIdentityCheck);
|
|
520
|
+
|
|
521
|
+
const contract = new ethersLib.Contract(result.contractAddress, ABI, provider);
|
|
522
|
+
|
|
523
|
+
// ONE read-only on-chain check set: is the root anchored, and does the contract's verifyLeaf accept
|
|
524
|
+
// the proof. We check anchoring against the root the OFFLINE FOLD produced (computedRoot), which
|
|
525
|
+
// equals the artifact root here (foldsToRoot held) — so we never trust the file's root unchecked.
|
|
526
|
+
result.checkedChain = true;
|
|
527
|
+
result.rootAnchored = await contract.isAnchored(fold.computedRoot);
|
|
528
|
+
if (!result.rootAnchored) {
|
|
529
|
+
// The proof is internally valid but its root was never anchored — distinct from a tamper. This is
|
|
530
|
+
// NOT a false ACCEPT; the CLI exits non-zero on it.
|
|
531
|
+
result.status = STATUS.NOT_ANCHORED;
|
|
532
|
+
_emit(result, opts, log);
|
|
533
|
+
return result;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// The contract's own verdict (defense in depth: even if our offline fold had a bug, the chain
|
|
537
|
+
// decides). verifyLeaf takes the path-bound leaf as its `contentHash` argument and tags it itself.
|
|
538
|
+
result.onChainVerified = await contract.verifyLeaf(
|
|
539
|
+
fold.computedRoot,
|
|
540
|
+
artifact.leaf,
|
|
541
|
+
artifact.proof
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
result.status =
|
|
545
|
+
result.offlineOk && result.rootAnchored && result.onChainVerified
|
|
546
|
+
? STATUS.ACCEPTED
|
|
547
|
+
: STATUS.REJECTED;
|
|
548
|
+
|
|
549
|
+
_emit(result, opts, log);
|
|
550
|
+
return result;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Emit the result as JSON or the human block, per opts.json. */
|
|
554
|
+
function _emit(result, opts, log) {
|
|
555
|
+
if (opts.json) {
|
|
556
|
+
log(JSON.stringify(jsonVerifyProof(result), null, 2) + "\n");
|
|
557
|
+
} else {
|
|
558
|
+
log(formatVerifyProof(result) + "\n");
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
module.exports = {
|
|
563
|
+
PROOF_KIND,
|
|
564
|
+
PROOF_SCHEMA_VERSION,
|
|
565
|
+
SUPPORTED_PROOF_SCHEMA_VERSIONS,
|
|
566
|
+
STATUS,
|
|
567
|
+
TRUST_CAVEAT,
|
|
568
|
+
buildProofArtifact,
|
|
569
|
+
writeProofArtifact,
|
|
570
|
+
readProofArtifact,
|
|
571
|
+
recomputeFold,
|
|
572
|
+
runVerifyProof,
|
|
573
|
+
formatVerifyProof,
|
|
574
|
+
jsonVerifyProof,
|
|
575
|
+
ABI,
|
|
576
|
+
// Exported for unit tests that exercise validation directly.
|
|
577
|
+
_validate,
|
|
578
|
+
};
|