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/lineage.js
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// `vh lineage <0xhash> [--contract a] [--rpc u] [--json] [--max-depth n]` — walk the immutable
|
|
4
|
+
// `parent` chain UP from a record to its lineage root, read-only.
|
|
5
|
+
//
|
|
6
|
+
// WHERE THIS FITS
|
|
7
|
+
// T-10.1 added an optional, immutable predecessor edge (`parent`) to every record, and
|
|
8
|
+
// `vh anchor/claim --parent <hash>` writes it. This is the read counterpart: given any record's
|
|
9
|
+
// contentHash, it follows `record.parent` from child -> parent -> ... until it reaches a lineage
|
|
10
|
+
// root (parent == bytes32(0)), printing each ancestor in order. It is the "what is the full history
|
|
11
|
+
// of this contribution, and who authored each step?" query.
|
|
12
|
+
//
|
|
13
|
+
// The walk is purely OFF-CHAIN: there is no on-chain loop (the contract deliberately never walks an
|
|
14
|
+
// unbounded set), so we issue one bounded `getRecord` per hop. The chain is acyclic by construction
|
|
15
|
+
// (a non-zero parent MUST already be anchored at write time), so a finite chain always terminates at
|
|
16
|
+
// a root — but a client must still cap the walk so a pathological/huge chain cannot hang it. That cap
|
|
17
|
+
// is `--max-depth` (default 256); reaching it prints a clear "deeper than --max-depth" note rather
|
|
18
|
+
// than looping forever.
|
|
19
|
+
//
|
|
20
|
+
// TRUST POSTURE (mirrors docs/TRUST-BOUNDARIES.md and the contract NatSpec). Two caveats lead every
|
|
21
|
+
// human run:
|
|
22
|
+
// 1. the shared record caveat (uri untrusted; contributor only proves authorship when authorBound);
|
|
23
|
+
// 2. a lineage-specific one: a `parent` edge is the CHILD author's CLAIM of a predecessor. It does
|
|
24
|
+
// NOT prove the predecessor's content is a genuine ancestor of the child's content (re-derive
|
|
25
|
+
// BOTH and reason about it yourself), and it does NOT transfer the parent's authorship to the
|
|
26
|
+
// child. Each record's contributor/authorBound stands on its own.
|
|
27
|
+
//
|
|
28
|
+
// Read-only by construction: it takes a PROVIDER only, never a signer and never a key. Walking a
|
|
29
|
+
// public, immutable lineage must never require the ability to write to it.
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
normalizeContentHash,
|
|
33
|
+
attributionFor,
|
|
34
|
+
isoFromUnix,
|
|
35
|
+
isRoot,
|
|
36
|
+
ZERO_HASH,
|
|
37
|
+
} = require("./show");
|
|
38
|
+
const { isNotAnchoredError } = require("./verify");
|
|
39
|
+
const {
|
|
40
|
+
assertRegistry,
|
|
41
|
+
formatRegistryLine,
|
|
42
|
+
formatSkippedLine,
|
|
43
|
+
jsonRegistryBlock,
|
|
44
|
+
jsonSkippedBlock,
|
|
45
|
+
} = require("./registry");
|
|
46
|
+
|
|
47
|
+
const ARTIFACT = require("./core/registryArtifact");
|
|
48
|
+
const ABI = ARTIFACT.abi;
|
|
49
|
+
|
|
50
|
+
// Default cap on how many ancestors the walk follows. A finite, acyclic chain always terminates at a
|
|
51
|
+
// root well before this; the cap exists only so a client can't be hung by a pathological/huge chain.
|
|
52
|
+
// 256 is generous for a real revision history yet bounds the worst case to 256 cheap eth_calls.
|
|
53
|
+
const DEFAULT_MAX_DEPTH = 256;
|
|
54
|
+
|
|
55
|
+
// Outcomes of a lineage run. WALKED == we read the start record and followed the chain (possibly to a
|
|
56
|
+
// root, possibly capped). NOT_ANCHORED == the START hash itself has no record (the contract reverted
|
|
57
|
+
// NotAnchored). A genuine RPC error is neither — it throws.
|
|
58
|
+
const STATUS = Object.freeze({
|
|
59
|
+
WALKED: "WALKED",
|
|
60
|
+
NOT_ANCHORED: "NOT_ANCHORED",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// The shared record trust caveat, kept consistent with cli/list.js / cli/show.js so the read commands
|
|
64
|
+
// never disagree about what `uri` / `contributor` are allowed to mean.
|
|
65
|
+
const RECORD_CAVEAT =
|
|
66
|
+
"NOTE: `uri` is an UNTRUSTED hint (never fetched/validated — re-fetch + re-hash yourself); " +
|
|
67
|
+
"`contributor` only means proven authorship when authorBound is true (commit-reveal), " +
|
|
68
|
+
"otherwise it is merely the first anchorer.";
|
|
69
|
+
|
|
70
|
+
// The lineage-specific caveat that ALSO leads every human run (acceptance #3). A parent edge is only a
|
|
71
|
+
// CLAIM by the child's author; spelling out what it does NOT prove keeps a reader from over-trusting an
|
|
72
|
+
// ancestry edge as proof of derivation or as a transfer of authorship.
|
|
73
|
+
const LINEAGE_CAVEAT =
|
|
74
|
+
"NOTE (lineage): a `parent` edge is the CHILD author's CLAIM of a predecessor. It does NOT prove " +
|
|
75
|
+
"the predecessor's content is a genuine ancestor of the child's content (re-derive BOTH yourself " +
|
|
76
|
+
"and reason about the relationship), and it does NOT transfer the parent's authorship to the child. " +
|
|
77
|
+
"Each record's contributor/authorBound stands on its own.";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validate and normalize the `--max-depth` value. A missing value means the default; anything present
|
|
81
|
+
* must be a positive integer (a zero/negative/non-integer cap is a usage error — a 0-depth walk could
|
|
82
|
+
* never even read the start record, and a typo must never silently change how far we walk).
|
|
83
|
+
*
|
|
84
|
+
* @param {number|string|undefined|null} value
|
|
85
|
+
* @returns {number} the resolved positive-integer cap
|
|
86
|
+
*/
|
|
87
|
+
function normalizeMaxDepth(value) {
|
|
88
|
+
if (value === undefined || value === null || value === "") return DEFAULT_MAX_DEPTH;
|
|
89
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
90
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
91
|
+
throw new Error(`invalid --max-depth: must be a positive integer, got: ${value}`);
|
|
92
|
+
}
|
|
93
|
+
return n;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Read ONE record by content hash, classifying a NotAnchored revert (an expected "no record") versus a
|
|
98
|
+
* genuine RPC/network failure (re-thrown), reusing the SAME `isNotAnchoredError` classifier verify.js /
|
|
99
|
+
* show.js use so the three commands never drift. Returns a normalized record object, or null when the
|
|
100
|
+
* hash has no record.
|
|
101
|
+
*
|
|
102
|
+
* @param {object} contract ethers v6 Contract bound to a provider
|
|
103
|
+
* @param {string} contentHash the 0x 32-byte hash to read
|
|
104
|
+
* @param {object} ethersLib ethers v6 module
|
|
105
|
+
* @param {string} notAnchoredSelector the NotAnchored 4-byte selector (for raw-data fallback)
|
|
106
|
+
* @returns {Promise<{
|
|
107
|
+
* contentHash:string, contributor:string, authorBound:boolean,
|
|
108
|
+
* timestamp:bigint, blockNumber:bigint, uri:string, parent:string
|
|
109
|
+
* }|null>}
|
|
110
|
+
*/
|
|
111
|
+
async function readOne(contract, contentHash, ethersLib, notAnchoredSelector) {
|
|
112
|
+
let record;
|
|
113
|
+
try {
|
|
114
|
+
record = await contract.getRecord(contentHash);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (isNotAnchoredError(err, ethersLib, notAnchoredSelector)) return null;
|
|
117
|
+
throw err; // genuine failure (network/address/etc.) — never masquerade as a missing record.
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
contentHash: contentHash.toLowerCase(),
|
|
121
|
+
contributor: record.contributor,
|
|
122
|
+
authorBound: Boolean(record.authorBound),
|
|
123
|
+
timestamp: BigInt(record.timestamp),
|
|
124
|
+
blockNumber: BigInt(record.blockNumber),
|
|
125
|
+
uri: record.uri,
|
|
126
|
+
// The immutable lineage edge. Normalize to a lowercase 0x string so isRoot()/equality is stable; a
|
|
127
|
+
// root reads back as the 32-byte zero hash.
|
|
128
|
+
parent: String(record.parent).toLowerCase(),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Walk the parent chain UP from `startHash`, bounded by `maxDepth`. Read-only: it issues at most
|
|
134
|
+
* `maxDepth` `getRecord` calls (one per hop), each through the shared NotAnchored classifier.
|
|
135
|
+
*
|
|
136
|
+
* Returns a structured result:
|
|
137
|
+
* - status NOT_ANCHORED + an empty ancestors[] when the START hash itself has no record;
|
|
138
|
+
* - status WALKED + ancestors[] in child->root order otherwise. `cappedAtDepth` is true iff we hit
|
|
139
|
+
* the cap before reaching a root (there is still an un-walked parent), in which case
|
|
140
|
+
* `nextParent` names the predecessor we stopped before so a caller could resume from there.
|
|
141
|
+
*
|
|
142
|
+
* The chain is acyclic by construction (a non-zero parent must already be anchored at write time), so
|
|
143
|
+
* a finite chain always terminates at a root before the cap; the cap only guards a pathological depth.
|
|
144
|
+
* A defensive in-walk visited-set still breaks on any repeat (it must never happen on a real chain),
|
|
145
|
+
* so even a (impossible) cycle can never spin forever.
|
|
146
|
+
*
|
|
147
|
+
* @param {object} contract ethers v6 Contract bound to a provider
|
|
148
|
+
* @param {string} startHash normalized 0x 32-byte start hash
|
|
149
|
+
* @param {object} opts { maxDepth:number, ethers:object }
|
|
150
|
+
* @returns {Promise<{
|
|
151
|
+
* status:"WALKED"|"NOT_ANCHORED",
|
|
152
|
+
* start:string,
|
|
153
|
+
* ancestors:Array<object>,
|
|
154
|
+
* cappedAtDepth:boolean,
|
|
155
|
+
* maxDepth:number,
|
|
156
|
+
* nextParent:string|null
|
|
157
|
+
* }>}
|
|
158
|
+
*/
|
|
159
|
+
async function walkLineage(contract, startHash, opts) {
|
|
160
|
+
const ethersLib = opts.ethers || require("ethers");
|
|
161
|
+
const maxDepth = opts.maxDepth || DEFAULT_MAX_DEPTH;
|
|
162
|
+
|
|
163
|
+
const iface = new ethersLib.Interface(ABI);
|
|
164
|
+
const notAnchoredSelector = iface.getError("NotAnchored").selector;
|
|
165
|
+
|
|
166
|
+
const ancestors = [];
|
|
167
|
+
const seen = new Set(); // defensive cycle guard (cannot trigger on a real acyclic chain)
|
|
168
|
+
let cursor = startHash.toLowerCase();
|
|
169
|
+
let cappedAtDepth = false;
|
|
170
|
+
let nextParent = null;
|
|
171
|
+
|
|
172
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
173
|
+
if (seen.has(cursor)) break; // impossible on an acyclic chain; never loop forever regardless
|
|
174
|
+
seen.add(cursor);
|
|
175
|
+
|
|
176
|
+
/* eslint-disable no-await-in-loop */
|
|
177
|
+
const rec = await readOne(contract, cursor, ethersLib, notAnchoredSelector);
|
|
178
|
+
/* eslint-enable no-await-in-loop */
|
|
179
|
+
|
|
180
|
+
if (rec === null) {
|
|
181
|
+
// Only the very FIRST hop can be NOT_ANCHORED: a non-zero parent is required to be anchored at
|
|
182
|
+
// write time, so an interior hash is always present. If the START hash is missing we report
|
|
183
|
+
// NOT_ANCHORED; an (impossible) missing interior just terminates the walk at what we have.
|
|
184
|
+
if (depth === 0) {
|
|
185
|
+
return {
|
|
186
|
+
status: STATUS.NOT_ANCHORED,
|
|
187
|
+
start: startHash.toLowerCase(),
|
|
188
|
+
ancestors: [],
|
|
189
|
+
cappedAtDepth: false,
|
|
190
|
+
maxDepth,
|
|
191
|
+
nextParent: null,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
ancestors.push({
|
|
198
|
+
depth,
|
|
199
|
+
contentHash: rec.contentHash,
|
|
200
|
+
contributor: rec.contributor,
|
|
201
|
+
authorBound: rec.authorBound,
|
|
202
|
+
attribution: attributionFor(rec.authorBound),
|
|
203
|
+
timestamp: rec.timestamp,
|
|
204
|
+
blockNumber: rec.blockNumber,
|
|
205
|
+
uri: rec.uri,
|
|
206
|
+
parent: rec.parent,
|
|
207
|
+
isRoot: isRoot(rec.parent),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (isRoot(rec.parent)) {
|
|
211
|
+
// Reached the lineage root: no predecessor. Done.
|
|
212
|
+
return {
|
|
213
|
+
status: STATUS.WALKED,
|
|
214
|
+
start: startHash.toLowerCase(),
|
|
215
|
+
ancestors,
|
|
216
|
+
cappedAtDepth: false,
|
|
217
|
+
maxDepth,
|
|
218
|
+
nextParent: null,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
cursor = rec.parent;
|
|
223
|
+
// If this was the last allowed iteration and the record still has a (non-root) parent, the walk is
|
|
224
|
+
// capped: there is an un-walked predecessor. Record it so the caller can resume from there.
|
|
225
|
+
if (depth === maxDepth - 1) {
|
|
226
|
+
cappedAtDepth = true;
|
|
227
|
+
nextParent = rec.parent;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
status: STATUS.WALKED,
|
|
233
|
+
start: startHash.toLowerCase(),
|
|
234
|
+
ancestors,
|
|
235
|
+
cappedAtDepth,
|
|
236
|
+
maxDepth,
|
|
237
|
+
nextParent,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Render one ancestor as the human-readable block printed per hop. Mirrors list.js/show.js fields. */
|
|
242
|
+
function formatAncestor(a, index) {
|
|
243
|
+
const rootTag = a.isRoot ? " <- lineage root (no predecessor)" : "";
|
|
244
|
+
const lines = [
|
|
245
|
+
`[${index}] ${a.contentHash}${rootTag}`,
|
|
246
|
+
` contributor: ${a.contributor}`,
|
|
247
|
+
` attribution: ${a.attribution}`,
|
|
248
|
+
` timestamp: ${a.timestamp} (${isoFromUnix(a.timestamp)})`,
|
|
249
|
+
` blockNumber: ${a.blockNumber}`,
|
|
250
|
+
` uri: ${a.uri ? a.uri : "(none)"}`,
|
|
251
|
+
];
|
|
252
|
+
// Show the edge to the next ancestor explicitly so a reader can see the chain links, not just nodes.
|
|
253
|
+
const parentLine = a.isRoot
|
|
254
|
+
? " parent: (none) — lineage root"
|
|
255
|
+
: ` parent: ${a.parent}`;
|
|
256
|
+
lines.push(parentLine);
|
|
257
|
+
return lines.join("\n");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Render a full lineage walk as the human-readable block the CLI prints. ALWAYS leads with both trust
|
|
262
|
+
* caveats (record + lineage-specific), then either the ordered ancestors or a NOT ANCHORED block, then
|
|
263
|
+
* a capped-walk note when the cap was hit.
|
|
264
|
+
*/
|
|
265
|
+
function formatLineage(r) {
|
|
266
|
+
const lines = [RECORD_CAVEAT, "", LINEAGE_CAVEAT, ""];
|
|
267
|
+
// T-11.2: the registry-authentication confirmation (or the loud skip warning), printed BEFORE the
|
|
268
|
+
// walk so a reader sees the contract was authenticated before believing any ancestor below.
|
|
269
|
+
if (r.identitySkipped) {
|
|
270
|
+
lines.push(formatSkippedLine(), "");
|
|
271
|
+
} else if (r.registry) {
|
|
272
|
+
lines.push(formatRegistryLine(r.registry), "");
|
|
273
|
+
}
|
|
274
|
+
lines.push(` start: ${r.start}`);
|
|
275
|
+
|
|
276
|
+
if (r.status === STATUS.NOT_ANCHORED) {
|
|
277
|
+
lines.push(
|
|
278
|
+
" result: NOT ANCHORED",
|
|
279
|
+
" No record exists for this content hash, so it has no lineage. It was never anchored (or you",
|
|
280
|
+
" mistyped the hash). `vh anchor <path> --parent <hash>` / `vh claim <path> --parent <hash>`",
|
|
281
|
+
" anchor a record AS a revision of an existing one; `vh verify <path>` recomputes a path's hash."
|
|
282
|
+
);
|
|
283
|
+
return lines.join("\n");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const n = r.ancestors.length;
|
|
287
|
+
lines.push(
|
|
288
|
+
` result: WALKED ${n} record${n === 1 ? "" : "s"} (child -> root order)`,
|
|
289
|
+
""
|
|
290
|
+
);
|
|
291
|
+
lines.push(r.ancestors.map((a, i) => formatAncestor(a, i)).join("\n\n"));
|
|
292
|
+
|
|
293
|
+
if (r.cappedAtDepth) {
|
|
294
|
+
lines.push(
|
|
295
|
+
"",
|
|
296
|
+
` NOTE: lineage deeper than --max-depth (${r.maxDepth}); the walk stopped before its root.`,
|
|
297
|
+
` The next un-walked predecessor is ${r.nextParent}.`,
|
|
298
|
+
` Re-run \`vh lineage ${r.nextParent} --max-depth <n>\` to continue from there.`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return lines.join("\n");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Shape a lineage result for `--json`: an ordered ancestor ARRAY carrying the same fields as the human
|
|
306
|
+
* block (BigInts -> Numbers so unix seconds / block heights pipe cleanly into CI). NOT_ANCHORED is a
|
|
307
|
+
* first-class value (anchored:false, empty ancestors), not an error object, so a script can branch on
|
|
308
|
+
* it without parsing stderr — while still seeing a non-zero exit from the CLI.
|
|
309
|
+
*/
|
|
310
|
+
function jsonLineage(r) {
|
|
311
|
+
// T-11.2: the machine-readable registry block — proves the walk was read from an authenticated
|
|
312
|
+
// registry (or that the check was skipped).
|
|
313
|
+
const registry = r.identitySkipped
|
|
314
|
+
? jsonSkippedBlock()
|
|
315
|
+
: r.registry
|
|
316
|
+
? jsonRegistryBlock(r.registry)
|
|
317
|
+
: null;
|
|
318
|
+
if (r.status === STATUS.NOT_ANCHORED) {
|
|
319
|
+
return {
|
|
320
|
+
start: r.start,
|
|
321
|
+
registry,
|
|
322
|
+
anchored: false,
|
|
323
|
+
ancestors: [],
|
|
324
|
+
note:
|
|
325
|
+
"NOT ANCHORED: no on-chain record for this hash, so it has no lineage. `lineage` only walks " +
|
|
326
|
+
"anchored records; run `vh verify <path>` to bind a record to real content.",
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
start: r.start,
|
|
331
|
+
registry,
|
|
332
|
+
anchored: true,
|
|
333
|
+
// The ordered ancestor array, child -> root. An indexer/UI can reconstruct the lineage path from
|
|
334
|
+
// this alone, mirroring the on-chain Linked(child, parent) logs.
|
|
335
|
+
ancestors: r.ancestors.map((a) => ({
|
|
336
|
+
depth: a.depth,
|
|
337
|
+
contentHash: a.contentHash,
|
|
338
|
+
contributor: a.contributor,
|
|
339
|
+
authorBound: a.authorBound,
|
|
340
|
+
attribution: a.attribution,
|
|
341
|
+
timestamp: Number(a.timestamp),
|
|
342
|
+
timestampISO: isoFromUnix(a.timestamp),
|
|
343
|
+
blockNumber: Number(a.blockNumber),
|
|
344
|
+
uri: a.uri ? a.uri : null,
|
|
345
|
+
// A root serializes parent:null + isRoot:true (distinguishable from a missing key); a parented
|
|
346
|
+
// record carries the predecessor hash + isRoot:false.
|
|
347
|
+
parent: a.isRoot ? null : a.parent,
|
|
348
|
+
isRoot: a.isRoot,
|
|
349
|
+
})),
|
|
350
|
+
// True iff the walk hit --max-depth before a root; `nextParent` is the un-walked predecessor.
|
|
351
|
+
cappedAtDepth: r.cappedAtDepth,
|
|
352
|
+
maxDepth: r.maxDepth,
|
|
353
|
+
nextParent: r.cappedAtDepth ? r.nextParent : null,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Walk a record's lineage by content hash. Read-only: requires a provider, never a signer.
|
|
359
|
+
*
|
|
360
|
+
* Validates the hash shape FIRST (a malformed/short hash throws BEFORE any network call, reusing
|
|
361
|
+
* show.js's normalizeContentHash so the same usage-grade error fires everywhere), then walks the chain.
|
|
362
|
+
* A NotAnchored revert on the START hash is the expected "no record" path (STATUS.NOT_ANCHORED); any
|
|
363
|
+
* other failure (bad RPC, wrong address, network down) is re-thrown rather than masqueraded — exactly as
|
|
364
|
+
* verify.js / show.js handle it, via the shared `isNotAnchoredError`.
|
|
365
|
+
*
|
|
366
|
+
* @param {object} opts
|
|
367
|
+
* @param {string} opts.contentHash the 0x 32-byte hash to start the walk from
|
|
368
|
+
* @param {string} opts.contractAddress deployed ContributionRegistry address to read from
|
|
369
|
+
* @param {object} opts.provider ethers v6 Provider (read-only RPC connection)
|
|
370
|
+
* @param {number|string} [opts.maxDepth] cap on how many ancestors to walk (default 256)
|
|
371
|
+
* @param {boolean} [opts.json] emit a JSON object instead of the human block
|
|
372
|
+
* @param {object} [opts.ethers] ethers v6 module (defaults to the bundled one)
|
|
373
|
+
* @param {(s:string)=>void} [opts.log] sink for output (defaults to process.stdout)
|
|
374
|
+
* @returns {Promise<{
|
|
375
|
+
* status:"WALKED"|"NOT_ANCHORED",
|
|
376
|
+
* start:string,
|
|
377
|
+
* ancestors:Array<object>,
|
|
378
|
+
* cappedAtDepth:boolean,
|
|
379
|
+
* maxDepth:number,
|
|
380
|
+
* nextParent:string|null
|
|
381
|
+
* }>}
|
|
382
|
+
*/
|
|
383
|
+
async function runLineage(opts) {
|
|
384
|
+
const ethersLib = opts.ethers || require("ethers");
|
|
385
|
+
const log = opts.log || ((s) => process.stdout.write(s));
|
|
386
|
+
|
|
387
|
+
// Validate the hash + the cap BEFORE touching the contract address / provider, so a bad input
|
|
388
|
+
// hard-errors with a usage-grade message and never reaches the network.
|
|
389
|
+
const contentHash = normalizeContentHash(opts.contentHash, ethersLib);
|
|
390
|
+
const maxDepth = normalizeMaxDepth(opts.maxDepth);
|
|
391
|
+
|
|
392
|
+
const { contractAddress, provider } = opts;
|
|
393
|
+
if (!contractAddress) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
"no contract address: pass --contract <address> or set VH_CONTRACT in the environment"
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
if (!ethersLib.isAddress(contractAddress)) {
|
|
399
|
+
throw new Error(`invalid contract address: ${contractAddress}`);
|
|
400
|
+
}
|
|
401
|
+
if (!provider) {
|
|
402
|
+
throw new Error("no provider: pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// T-11.2: authenticate the registry BEFORE walking the chain — no lineage is reported until we have
|
|
406
|
+
// confirmed there is a real verifyhash ContributionRegistry at this address (unless the caller
|
|
407
|
+
// explicitly, loudly opts out with skipIdentityCheck for a known not-yet-deployed/local-dev target).
|
|
408
|
+
let registryAuth = null;
|
|
409
|
+
if (!opts.skipIdentityCheck) {
|
|
410
|
+
registryAuth = await assertRegistry({ provider, contractAddress, ethers: ethersLib });
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const contract = new ethersLib.Contract(
|
|
414
|
+
ethersLib.getAddress(contractAddress),
|
|
415
|
+
ABI,
|
|
416
|
+
provider
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
const result = await walkLineage(contract, contentHash, { maxDepth, ethers: ethersLib });
|
|
420
|
+
// Attach the registry identity (or the skip marker) so the human block and --json both surface it.
|
|
421
|
+
result.registry = registryAuth;
|
|
422
|
+
result.identitySkipped = Boolean(opts.skipIdentityCheck);
|
|
423
|
+
|
|
424
|
+
if (opts.json) {
|
|
425
|
+
log(JSON.stringify(jsonLineage(result), null, 2) + "\n");
|
|
426
|
+
} else {
|
|
427
|
+
log(formatLineage(result) + "\n");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
module.exports = {
|
|
434
|
+
runLineage,
|
|
435
|
+
walkLineage,
|
|
436
|
+
readOne,
|
|
437
|
+
normalizeMaxDepth,
|
|
438
|
+
formatLineage,
|
|
439
|
+
formatAncestor,
|
|
440
|
+
jsonLineage,
|
|
441
|
+
STATUS,
|
|
442
|
+
RECORD_CAVEAT,
|
|
443
|
+
LINEAGE_CAVEAT,
|
|
444
|
+
DEFAULT_MAX_DEPTH,
|
|
445
|
+
ZERO_HASH,
|
|
446
|
+
ABI,
|
|
447
|
+
};
|