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/claim.js
ADDED
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// `vh claim <path>` — front-running-resistant attribution via commit-reveal.
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS EXISTS
|
|
6
|
+
// The one-shot `vh anchor` puts the raw contentHash in the public mempool. Anyone watching can
|
|
7
|
+
// copy it and anchor it first, becoming the recorded `contributor` (audit findings F4/F14/F2/F5).
|
|
8
|
+
// So `anchor` records are only "first anchorer", never proven authorship.
|
|
9
|
+
//
|
|
10
|
+
// `vh claim` instead runs the contract's commit-reveal flow:
|
|
11
|
+
// 1. commit(commitment) where commitment = keccak256(abi.encode(contentHash, you, salt)).
|
|
12
|
+
// Only the opaque, sender-bound, salt-blinded hash goes on-chain — it leaks nothing about
|
|
13
|
+
// the contentHash and cannot be reused by anyone else.
|
|
14
|
+
// 2. ...wait MIN_REVEAL_DELAY blocks...
|
|
15
|
+
// 3. reveal(contentHash, salt, uri) — now the contentHash is public, but a mempool copier who
|
|
16
|
+
// resubmits this reveal as themselves recomputes a DIFFERENT commitment (bound to their
|
|
17
|
+
// address) that they never registered, so their reveal reverts. The committed claimant wins.
|
|
18
|
+
//
|
|
19
|
+
// The result is a record with authorBound = true and contributor = you, which front-running
|
|
20
|
+
// cannot redirect.
|
|
21
|
+
//
|
|
22
|
+
// The module is split into pure pieces (computeCommitment, buildCommitTx, buildRevealTx) plus an
|
|
23
|
+
// orchestration runner (runClaim) so the end-to-end test can drive both legs against a live hardhat
|
|
24
|
+
// node and prove a front-runner cannot steal the attribution.
|
|
25
|
+
|
|
26
|
+
const path = require("path");
|
|
27
|
+
const { hashPath, hashGit } = require("./hash");
|
|
28
|
+
const {
|
|
29
|
+
buildReceipt,
|
|
30
|
+
writeReceipt,
|
|
31
|
+
readReceipt,
|
|
32
|
+
defaultReceiptPath,
|
|
33
|
+
} = require("./receipt");
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve where a receipt file should be written from the caller's explicit choices, returning an
|
|
37
|
+
* ABSOLUTE path so the success line can name the exact file the user can see/relocate/delete.
|
|
38
|
+
*
|
|
39
|
+
* Precedence (all caller-opted-in; none of them silently default to cwd without telling the user):
|
|
40
|
+
* 1. `receiptPath` — an explicit full path (from `--receipt <path>`): used verbatim (resolved to
|
|
41
|
+
* absolute). The caller picked the exact file.
|
|
42
|
+
* 2. `receiptDir` + `contentHash` — an explicit destination directory (from `--receipt-dir <dir>`):
|
|
43
|
+
* `<dir>/<defaultName>`. The caller picked the folder; we pick the tidy default file name.
|
|
44
|
+
* 3. `contentHash` only — the documented default: `<baseDir>/<defaultName>` where `baseDir`
|
|
45
|
+
* defaults to `process.cwd()`. This is only reached for the DURABLE `vh commit` command, which
|
|
46
|
+
* MUST then print the exact resolved path (see runCommit) — never a silent cwd drop.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} args
|
|
49
|
+
* @param {string} [args.receiptPath] explicit full path
|
|
50
|
+
* @param {string} [args.receiptDir] explicit destination directory
|
|
51
|
+
* @param {string} [args.contentHash] 0x digest, used to derive the default file name
|
|
52
|
+
* @param {string} [args.baseDir] base for the bare default (defaults to process.cwd())
|
|
53
|
+
* @returns {string} an ABSOLUTE receipt path
|
|
54
|
+
*/
|
|
55
|
+
function resolveReceiptPath(args) {
|
|
56
|
+
if (args.receiptPath) return path.resolve(args.receiptPath);
|
|
57
|
+
const name = path.basename(defaultReceiptPath(args.contentHash)); // "<prefix>.vhclaim.json"
|
|
58
|
+
const base = args.receiptDir ? args.receiptDir : args.baseDir || process.cwd();
|
|
59
|
+
return path.resolve(base, name);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const ARTIFACT = require("./core/registryArtifact");
|
|
63
|
+
const ABI = ARTIFACT.abi;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compute the content hash to claim for a filesystem path (same convention as `vh anchor`):
|
|
67
|
+
* a file claims its keccak256 digest, a directory its sorted-leaf Merkle root. For a directory the
|
|
68
|
+
* per-file manifest (sorted `{ path, contentHash, leaf }`) is returned too, so the claim/commit
|
|
69
|
+
* receipt records it (letting a later `vh verify --receipt` localize a tamper).
|
|
70
|
+
*
|
|
71
|
+
* With `opts.git`, the root and manifest are computed over EXACTLY the files git tracks at `opts.ref`
|
|
72
|
+
* (default HEAD) — the same reproducible `vh hash --git` enumeration (T-8.1) — and a `git` provenance
|
|
73
|
+
* block `{ commit, scope }` is returned so the claim/commit receipt records the resolved commit oid
|
|
74
|
+
* and the repo-relative scope used to enumerate the tracked set (an UNTRUSTED convenience hint).
|
|
75
|
+
*
|
|
76
|
+
* @param {string} targetPath
|
|
77
|
+
* @param {{ git?: boolean, ref?: string }} [opts]
|
|
78
|
+
* @returns {{ contentHash: string, kind: "file"|"dir",
|
|
79
|
+
* manifest: Array<{path:string,contentHash:string,leaf:string}>|null,
|
|
80
|
+
* git: {commit:string,scope:string}|null }}
|
|
81
|
+
*/
|
|
82
|
+
function contentHashForPath(targetPath, opts = {}) {
|
|
83
|
+
if (opts.git) {
|
|
84
|
+
const res = hashGit(targetPath, { ref: opts.ref });
|
|
85
|
+
const manifest = res.leaves.map((l) => ({
|
|
86
|
+
path: l.path,
|
|
87
|
+
contentHash: l.contentHash,
|
|
88
|
+
leaf: l.leaf,
|
|
89
|
+
}));
|
|
90
|
+
return {
|
|
91
|
+
contentHash: res.root,
|
|
92
|
+
kind: "dir",
|
|
93
|
+
manifest,
|
|
94
|
+
git: { commit: res.commit, scope: res.scope },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const res = hashPath(targetPath);
|
|
98
|
+
const manifest =
|
|
99
|
+
res.kind === "dir" && Array.isArray(res.leaves)
|
|
100
|
+
? res.leaves.map((l) => ({ path: l.path, contentHash: l.contentHash, leaf: l.leaf }))
|
|
101
|
+
: null;
|
|
102
|
+
return { contentHash: res.root, kind: res.kind, manifest, git: null };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generate a fresh, cryptographically-random 32-byte salt (hex). The salt is the secret that, with
|
|
107
|
+
* the committer's address, blinds the commitment — it MUST be kept private until reveal.
|
|
108
|
+
* @param {object} [ethersLib] ethers v6 module
|
|
109
|
+
* @returns {string} 0x-prefixed 32-byte hex
|
|
110
|
+
*/
|
|
111
|
+
function newSalt(ethersLib) {
|
|
112
|
+
const e = ethersLib || require("ethers");
|
|
113
|
+
return e.hexlify(e.randomBytes(32));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Compute the commitment hash exactly as the contract's `commitmentOf` does:
|
|
118
|
+
* keccak256(abi.encode(contentHash, committer, salt)).
|
|
119
|
+
* Binding `committer` is what makes a stolen reveal resolve to a different, never-registered
|
|
120
|
+
* commitment, so a front-runner cannot claim someone else's content.
|
|
121
|
+
*
|
|
122
|
+
* @param {object} args
|
|
123
|
+
* @param {string} args.contentHash 0x 32-byte digest being claimed
|
|
124
|
+
* @param {string} args.committer the address that will reveal (== eventual msg.sender)
|
|
125
|
+
* @param {string} args.salt 0x 32-byte secret salt
|
|
126
|
+
* @param {object} [args.ethers] ethers v6 module
|
|
127
|
+
* @returns {string} 0x 32-byte commitment hash
|
|
128
|
+
*/
|
|
129
|
+
function computeCommitment(args) {
|
|
130
|
+
const e = args.ethers || require("ethers");
|
|
131
|
+
const { contentHash, committer, salt } = args;
|
|
132
|
+
if (!contentHash) throw new Error("computeCommitment requires contentHash");
|
|
133
|
+
if (!committer || !e.isAddress(committer)) {
|
|
134
|
+
throw new Error(`computeCommitment requires a valid committer address, got: ${committer}`);
|
|
135
|
+
}
|
|
136
|
+
if (!salt) throw new Error("computeCommitment requires a salt");
|
|
137
|
+
const encoded = e.AbiCoder.defaultAbiCoder().encode(
|
|
138
|
+
["bytes32", "address", "bytes32"],
|
|
139
|
+
[contentHash, e.getAddress(committer), salt]
|
|
140
|
+
);
|
|
141
|
+
return e.keccak256(encoded);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Internal: validate + normalize the shared inputs both legs need. */
|
|
145
|
+
function _resolveContext(opts) {
|
|
146
|
+
const ethersLib = opts.ethers || require("ethers");
|
|
147
|
+
const { contractAddress } = opts;
|
|
148
|
+
if (!contractAddress) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
"no contract address: pass --contract <address> or set VH_CONTRACT in the environment"
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (!ethersLib.isAddress(contractAddress)) {
|
|
154
|
+
throw new Error(`invalid contract address: ${contractAddress}`);
|
|
155
|
+
}
|
|
156
|
+
return { ethersLib, to: ethersLib.getAddress(contractAddress) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build (without sending) the `commit` transaction for a path. No network needed beyond knowing the
|
|
161
|
+
* committer's address. Generates a salt if one is not supplied; the caller MUST persist the returned
|
|
162
|
+
* salt (and contentHash + committer) to later reveal.
|
|
163
|
+
*
|
|
164
|
+
* @param {object} opts
|
|
165
|
+
* @param {string} opts.path file/dir to claim
|
|
166
|
+
* @param {string} opts.committer address that will commit & reveal
|
|
167
|
+
* @param {boolean}[opts.git] hash EXACTLY the git-tracked files (T-8.1 enumeration)
|
|
168
|
+
* @param {string} [opts.ref] with git: which commit's tracked set (default HEAD)
|
|
169
|
+
* @param {string} opts.contractAddress ContributionRegistry address (tx `to`)
|
|
170
|
+
* @param {string} [opts.salt] reuse a salt (else a fresh random one is generated)
|
|
171
|
+
* @param {string} [opts.parent] optional predecessor contentHash (B-10.1 lineage edge). The
|
|
172
|
+
* commit() tx itself NEVER carries a parent (the contract's
|
|
173
|
+
* commit takes only the commitment; the edge is recorded at
|
|
174
|
+
* REVEAL time via revealWithParent). We validate it here up
|
|
175
|
+
* front (parser parity with `vh anchor --parent`) and return
|
|
176
|
+
* the normalized value so runCommit can persist it for reveal.
|
|
177
|
+
* @param {object} [opts.ethers] ethers v6 module
|
|
178
|
+
* @returns {{
|
|
179
|
+
* to: string, data: string, value: string, functionName: "commit",
|
|
180
|
+
* contentHash: string, kind: "file"|"dir", path: string,
|
|
181
|
+
* manifest: Array|null, git: {commit:string,scope:string}|null,
|
|
182
|
+
* committer: string, salt: string, commitment: string, parent: string|null
|
|
183
|
+
* }}
|
|
184
|
+
*/
|
|
185
|
+
function buildCommitTx(opts) {
|
|
186
|
+
const { ethersLib, to } = _resolveContext(opts);
|
|
187
|
+
const { path: targetPath, committer } = opts;
|
|
188
|
+
if (!targetPath) throw new Error("claim requires a <path>");
|
|
189
|
+
if (!committer || !ethersLib.isAddress(committer)) {
|
|
190
|
+
throw new Error(`claim requires a valid committer address, got: ${committer}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const { contentHash, kind, manifest, git } = contentHashForPath(targetPath, {
|
|
194
|
+
git: opts.git,
|
|
195
|
+
ref: opts.ref,
|
|
196
|
+
});
|
|
197
|
+
if (/^0x0{64}$/i.test(contentHash)) {
|
|
198
|
+
throw new Error("refusing to claim the zero hash (contract rejects it)");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Validate the optional `--parent` lineage edge BEFORE building/sending anything (parser parity with
|
|
202
|
+
// `vh anchor --parent`, whose buildAnchorTx runs normalizeParent up front). A malformed/self-referential
|
|
203
|
+
// value is a typo the user must learn about immediately — never after commit() has already broadcast.
|
|
204
|
+
// The commit() tx is identical with or without a parent (the edge rides the REVEAL leg); we only carry
|
|
205
|
+
// the normalized parent on the built tx so runCommit can persist it into the receipt. normalizeParent
|
|
206
|
+
// maps missing/empty/zero -> null (a lineage root) and hard-errors on a malformed non-zero value.
|
|
207
|
+
const { normalizeParent } = require("./anchor");
|
|
208
|
+
const parent = normalizeParent(opts.parent, ethersLib);
|
|
209
|
+
if (parent !== null && parent.toLowerCase() === contentHash.toLowerCase()) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
"refusing to claim a record as its own parent (self-reference; the contract rejects it as SelfParent)"
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const salt = opts.salt || newSalt(ethersLib);
|
|
216
|
+
const committerAddr = ethersLib.getAddress(committer);
|
|
217
|
+
const commitment = computeCommitment({
|
|
218
|
+
contentHash,
|
|
219
|
+
committer: committerAddr,
|
|
220
|
+
salt,
|
|
221
|
+
ethers: ethersLib,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const iface = new ethersLib.Interface(ABI);
|
|
225
|
+
const data = iface.encodeFunctionData("commit", [commitment]);
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
to,
|
|
229
|
+
data,
|
|
230
|
+
value: "0x0", // commit() is non-payable.
|
|
231
|
+
functionName: "commit",
|
|
232
|
+
contentHash,
|
|
233
|
+
kind,
|
|
234
|
+
path: targetPath,
|
|
235
|
+
manifest, // per-file manifest for a dir target (null for a file); recorded into the receipt
|
|
236
|
+
git, // { commit, scope } when --git was used; null otherwise. Recorded into the receipt.
|
|
237
|
+
committer: committerAddr,
|
|
238
|
+
salt,
|
|
239
|
+
commitment,
|
|
240
|
+
parent, // null for a lineage root; the normalized predecessor hash when --parent was given.
|
|
241
|
+
// The commit() tx does NOT carry it (the edge rides the reveal leg); runCommit persists it.
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Build (without sending) the `reveal` transaction. Requires the salt produced at commit time.
|
|
247
|
+
*
|
|
248
|
+
* @param {object} opts
|
|
249
|
+
* @param {string} opts.contentHash the digest committed to
|
|
250
|
+
* @param {string} opts.salt the secret salt used to build the commitment
|
|
251
|
+
* @param {string} [opts.uri] optional untrusted off-chain pointer hint
|
|
252
|
+
* @param {string} [opts.parent] optional predecessor contentHash (T-10.1 lineage edge);
|
|
253
|
+
* non-zero routes to revealWithParent() and records the edge
|
|
254
|
+
* @param {string} opts.contractAddress ContributionRegistry address (tx `to`)
|
|
255
|
+
* @param {object} [opts.ethers] ethers v6 module
|
|
256
|
+
* @returns {{ to: string, data: string, value: string,
|
|
257
|
+
* functionName: "reveal"|"revealWithParent",
|
|
258
|
+
* contentHash: string, salt: string, uri: string, parent: string|null }}
|
|
259
|
+
*/
|
|
260
|
+
function buildRevealTx(opts) {
|
|
261
|
+
const { ethersLib, to } = _resolveContext(opts);
|
|
262
|
+
const { contentHash, salt } = opts;
|
|
263
|
+
if (!contentHash) throw new Error("reveal requires the committed contentHash");
|
|
264
|
+
if (!salt) throw new Error("reveal requires the secret salt from the commit step");
|
|
265
|
+
const uri = opts.uri == null ? "" : String(opts.uri);
|
|
266
|
+
|
|
267
|
+
// Resolve the optional lineage edge (same convention/validation as `vh anchor --parent`): a
|
|
268
|
+
// missing/zero parent is a root via the legacy reveal(); a non-zero 32-byte hash routes to
|
|
269
|
+
// revealWithParent(). Self-reference is rejected here; the contract enforces UnknownParent/SelfParent.
|
|
270
|
+
const { normalizeParent } = require("./anchor");
|
|
271
|
+
const parent = normalizeParent(opts.parent, ethersLib);
|
|
272
|
+
if (parent !== null && parent.toLowerCase() === contentHash.toLowerCase()) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
"refusing to reveal a record as its own parent (self-reference; the contract rejects it as SelfParent)"
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const iface = new ethersLib.Interface(ABI);
|
|
279
|
+
const functionName = parent === null ? "reveal" : "revealWithParent";
|
|
280
|
+
const data =
|
|
281
|
+
parent === null
|
|
282
|
+
? iface.encodeFunctionData("reveal", [contentHash, salt, uri])
|
|
283
|
+
: iface.encodeFunctionData("revealWithParent", [contentHash, salt, uri, parent]);
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
to,
|
|
287
|
+
data,
|
|
288
|
+
value: "0x0", // reveal()/revealWithParent() are non-payable.
|
|
289
|
+
functionName,
|
|
290
|
+
contentHash,
|
|
291
|
+
salt,
|
|
292
|
+
uri,
|
|
293
|
+
parent, // null for a lineage root; the predecessor hash when --parent was given.
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Render the commit/reveal plan a `--dry-run` claim prints (no key, no network).
|
|
299
|
+
*
|
|
300
|
+
* The optional `revealTx` (the built reveal leg from buildRevealTx) carries the lineage edge: when a
|
|
301
|
+
* `--parent` was given it routes the Step-2 reveal to `revealWithParent(contentHash, salt, uri, parent)`
|
|
302
|
+
* and the parent hash is shown so a user previewing a `vh claim --parent` write SEES the lineage edge
|
|
303
|
+
* they are about to record (parity with `vh anchor --dry-run`, which prints `parent:`). Without a
|
|
304
|
+
* parent the plan reads exactly as before — the legacy `reveal(contentHash, salt, uri)` line, byte for
|
|
305
|
+
* byte. A `revealTx` is always passed by runClaim; the parameter stays optional so an older caller that
|
|
306
|
+
* omits it degrades to the no-parent rendering rather than throwing.
|
|
307
|
+
*
|
|
308
|
+
* @param {object} commitTx the built commit leg (from buildCommitTx)
|
|
309
|
+
* @param {object} [revealTx] the built reveal leg (from buildRevealTx); carries `parent`/`functionName`
|
|
310
|
+
*/
|
|
311
|
+
function formatDryRun(commitTx, revealTx) {
|
|
312
|
+
// The lineage edge to preview: a non-null parent means this claim routes its reveal to
|
|
313
|
+
// revealWithParent() and records the edge; null/absent means a lineage root via the legacy reveal().
|
|
314
|
+
const parent = revealTx && revealTx.parent != null ? revealTx.parent : null;
|
|
315
|
+
const revealFn =
|
|
316
|
+
revealTx && revealTx.functionName ? revealTx.functionName : parent == null ? "reveal" : "revealWithParent";
|
|
317
|
+
|
|
318
|
+
const lines = [
|
|
319
|
+
"DRY RUN — no transaction will be sent (commit-reveal attribution).",
|
|
320
|
+
"",
|
|
321
|
+
` path: ${commitTx.path} (${commitTx.kind})`,
|
|
322
|
+
` contentHash: ${commitTx.contentHash}`,
|
|
323
|
+
` committer: ${commitTx.committer}`,
|
|
324
|
+
` salt: ${commitTx.salt} <-- SECRET: keep this to reveal later`,
|
|
325
|
+
` commitment: ${commitTx.commitment}`,
|
|
326
|
+
// Lineage edge (T-10.1): show whether this claim is a root or a child of `parent`, and which reveal
|
|
327
|
+
// path (reveal vs revealWithParent) it routes to — so a dry-run reader sees the edge they'd record.
|
|
328
|
+
` parent: ${parent == null ? "(none) — lineage root" : parent}`,
|
|
329
|
+
];
|
|
330
|
+
if (commitTx.git) {
|
|
331
|
+
lines.push(
|
|
332
|
+
` git commit: ${commitTx.git.commit} (untrusted provenance hint)`,
|
|
333
|
+
` git scope: ${commitTx.git.scope}`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
// The Step-2 line names the EXACT reveal function and (when parented) the predecessor hash, so the
|
|
337
|
+
// printed plan never silently omits a lineage edge the user is about to record.
|
|
338
|
+
const step2 =
|
|
339
|
+
parent == null
|
|
340
|
+
? ` Step 2 — after MIN_REVEAL_DELAY blocks, ${revealFn}(contentHash, salt, uri) is sent.`
|
|
341
|
+
: ` Step 2 — after MIN_REVEAL_DELAY blocks, ${revealFn}(contentHash, salt, uri, parent) is sent,\n` +
|
|
342
|
+
` recording the lineage edge -> parent ${parent}.`;
|
|
343
|
+
lines.push(
|
|
344
|
+
"",
|
|
345
|
+
" Step 1 — commit() that WOULD be sent:",
|
|
346
|
+
` to: ${commitTx.to}`,
|
|
347
|
+
` value: ${commitTx.value}`,
|
|
348
|
+
` data: ${commitTx.data}`,
|
|
349
|
+
"",
|
|
350
|
+
step2,
|
|
351
|
+
" A mempool copier who lifts your reveal cannot win: their commitment (bound to THEIR",
|
|
352
|
+
" address) was never registered, so their reveal reverts. Attribution stays yours.",
|
|
353
|
+
""
|
|
354
|
+
);
|
|
355
|
+
return lines.join("\n");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Resolve and guard the chain to submit to. Determines the chainId (from opts or the provider) and
|
|
360
|
+
* enforces the same testnet guard policy as `anchor`: refuse a non-testnet chain unless the caller
|
|
361
|
+
* explicitly passed `iUnderstandMainnet`.
|
|
362
|
+
* @param {object} opts {chainId?, provider, iUnderstandMainnet?, verb?}
|
|
363
|
+
* @returns {Promise<bigint>} the resolved chainId
|
|
364
|
+
*/
|
|
365
|
+
async function _resolveChainGuard(opts) {
|
|
366
|
+
let chainId = opts.chainId;
|
|
367
|
+
if (chainId == null && opts.provider) {
|
|
368
|
+
const net = await opts.provider.getNetwork();
|
|
369
|
+
chainId = net.chainId;
|
|
370
|
+
}
|
|
371
|
+
// Reuse the same testnet guard policy as anchor (imported lazily to avoid a cycle at load time).
|
|
372
|
+
const { isTestnetChainId } = require("./anchor");
|
|
373
|
+
if (chainId == null) {
|
|
374
|
+
throw new Error("cannot determine chainId; refusing to submit without knowing the network");
|
|
375
|
+
}
|
|
376
|
+
if (!isTestnetChainId(chainId) && !opts.iUnderstandMainnet) {
|
|
377
|
+
const verb = opts.verb || "claim";
|
|
378
|
+
throw new Error(
|
|
379
|
+
`refusing to ${verb} on chainId ${BigInt(chainId).toString()} (not a known testnet). ` +
|
|
380
|
+
"If you really mean to write to this chain, re-run with --i-understand-mainnet."
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
return BigInt(chainId);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Wait until the chain has advanced past the MIN_REVEAL_DELAY window for a commit that mined in
|
|
388
|
+
* `commitBlock`. A reveal requires `current > commitBlock + minDelay`, so we wait for
|
|
389
|
+
* `commitBlock + minDelay + 1`.
|
|
390
|
+
* @param {object} args {provider, commitBlock: bigint, minDelay: bigint, waitForBlock?}
|
|
391
|
+
*/
|
|
392
|
+
async function _waitRevealWindow(args) {
|
|
393
|
+
const { provider, commitBlock, minDelay } = args;
|
|
394
|
+
const revealAfter = commitBlock + minDelay;
|
|
395
|
+
if (args.waitForBlock) {
|
|
396
|
+
await args.waitForBlock(revealAfter + 1n);
|
|
397
|
+
} else if (provider) {
|
|
398
|
+
// Poll until the chain advances past the window. (On a live testnet this just waits for blocks
|
|
399
|
+
// to be produced.)
|
|
400
|
+
/* eslint-disable no-await-in-loop */
|
|
401
|
+
while (BigInt(await provider.getBlockNumber()) <= revealAfter) {
|
|
402
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
403
|
+
}
|
|
404
|
+
/* eslint-enable no-await-in-loop */
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Parse the first `Revealed` event out of a transaction receipt's logs, or return null. */
|
|
409
|
+
function _parseRevealed(receipt, ethersLib) {
|
|
410
|
+
const iface = new ethersLib.Interface(ABI);
|
|
411
|
+
for (const lg of receipt.logs) {
|
|
412
|
+
try {
|
|
413
|
+
const parsed = iface.parseLog({ topics: lg.topics, data: lg.data });
|
|
414
|
+
if (parsed && parsed.name === "Revealed") {
|
|
415
|
+
return {
|
|
416
|
+
contentHash: parsed.args.contentHash,
|
|
417
|
+
contributor: parsed.args.contributor,
|
|
418
|
+
index: parsed.args.index,
|
|
419
|
+
commitment: parsed.args.commitment,
|
|
420
|
+
timestamp: parsed.args.timestamp,
|
|
421
|
+
uri: parsed.args.uri,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
} catch (_) {
|
|
425
|
+
/* not our event */
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Run ONLY the commit leg of a resumable claim, persisting a durable receipt BEFORE it returns.
|
|
433
|
+
*
|
|
434
|
+
* This is the safe, restartable half of commit-reveal: it sends `commit()`, waits for it to mine,
|
|
435
|
+
* reads MIN_REVEAL_DELAY, then writes the receipt (salt + commitment + everything `reveal()` needs)
|
|
436
|
+
* to disk so a separate `runReveal` process can finish the claim even after a crash/restart.
|
|
437
|
+
*
|
|
438
|
+
* @param {object} opts
|
|
439
|
+
* @param {string} opts.path
|
|
440
|
+
* @param {string} [opts.uri]
|
|
441
|
+
* @param {string} [opts.parent] optional predecessor contentHash (B-10.1 lineage edge);
|
|
442
|
+
* validated up front and persisted into the receipt so the
|
|
443
|
+
* later `runReveal` routes to revealWithParent(). The commit()
|
|
444
|
+
* tx itself is unchanged (the edge is recorded at reveal time).
|
|
445
|
+
* @param {boolean}[opts.git] hash EXACTLY the git-tracked files (T-8.1 enumeration)
|
|
446
|
+
* @param {string} [opts.ref] with git: which commit's tracked set (default HEAD)
|
|
447
|
+
* @param {string} opts.contractAddress
|
|
448
|
+
* @param {string} [opts.receiptPath] explicit full path to write the receipt to (--receipt)
|
|
449
|
+
* @param {string} [opts.receiptDir] explicit destination DIRECTORY (--receipt-dir); the tidy
|
|
450
|
+
* default file name is used inside it
|
|
451
|
+
* @param {string} [opts.baseDir] base dir for the bare default name (default process.cwd())
|
|
452
|
+
* @param {boolean}[opts.iUnderstandMainnet]
|
|
453
|
+
* @param {object} opts.signer ethers Signer
|
|
454
|
+
* @param {object} [opts.provider]
|
|
455
|
+
* @param {bigint|number}[opts.chainId]
|
|
456
|
+
* @param {string} [opts.salt] reuse a salt (else random)
|
|
457
|
+
* @param {object} [opts.ethers]
|
|
458
|
+
* @param {(s:string)=>void}[opts.log]
|
|
459
|
+
* @returns {Promise<{commitTx, commitTxHash, commitBlockNumber, minRevealDelay, chainId, receiptPath, receipt}>}
|
|
460
|
+
*
|
|
461
|
+
* The receipt path is resolved to an ABSOLUTE path (see resolveReceiptPath) and the EXACT file is
|
|
462
|
+
* named in the success log so the user can always see/relocate/delete the secret-bearing receipt.
|
|
463
|
+
* When no `--receipt`/`--receipt-dir` is given the default lands in `baseDir` (cwd) — but only ever
|
|
464
|
+
* after the success line names that exact resolved file, so it is never a silent secret drop.
|
|
465
|
+
*/
|
|
466
|
+
async function runCommit(opts) {
|
|
467
|
+
const ethersLib = opts.ethers || require("ethers");
|
|
468
|
+
const log = opts.log || ((s) => process.stdout.write(s));
|
|
469
|
+
|
|
470
|
+
if (!opts.signer) {
|
|
471
|
+
throw new Error("no signer available to submit the commit (set PRIVATE_KEY?)");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let committer = opts.committer;
|
|
475
|
+
if (!committer) committer = await opts.signer.getAddress();
|
|
476
|
+
|
|
477
|
+
const commitTx = buildCommitTx({
|
|
478
|
+
path: opts.path,
|
|
479
|
+
committer,
|
|
480
|
+
git: opts.git,
|
|
481
|
+
ref: opts.ref,
|
|
482
|
+
contractAddress: opts.contractAddress,
|
|
483
|
+
salt: opts.salt,
|
|
484
|
+
parent: opts.parent, // validated up front in buildCommitTx (parity with anchor); persisted below
|
|
485
|
+
ethers: ethersLib,
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const provider = opts.provider || opts.signer.provider;
|
|
489
|
+
const chainId = await _resolveChainGuard({
|
|
490
|
+
chainId: opts.chainId,
|
|
491
|
+
provider,
|
|
492
|
+
iUnderstandMainnet: opts.iUnderstandMainnet,
|
|
493
|
+
verb: "commit",
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const contract = new ethersLib.Contract(commitTx.to, ABI, opts.signer);
|
|
497
|
+
|
|
498
|
+
// Name whether a lineage edge will be recorded at reveal time, so the operator SEES it from the
|
|
499
|
+
// commit step (the commit() tx is identical with or without a parent; the edge rides the reveal leg).
|
|
500
|
+
const parentNote = commitTx.parent ? ` (-> parent ${commitTx.parent} will be recorded at reveal)` : "";
|
|
501
|
+
log(
|
|
502
|
+
`commit: committing ${commitTx.path} (${commitTx.kind}) as ${commitTx.committer}${parentNote}...\n`
|
|
503
|
+
);
|
|
504
|
+
const commitSent = await contract.commit(commitTx.commitment);
|
|
505
|
+
log(` commit tx: ${commitSent.hash}\n`);
|
|
506
|
+
const commitReceiptTx = await commitSent.wait();
|
|
507
|
+
const commitBlock = BigInt(commitReceiptTx.blockNumber);
|
|
508
|
+
const minDelay = BigInt(await contract.MIN_REVEAL_DELAY());
|
|
509
|
+
|
|
510
|
+
// Persist the receipt BEFORE returning/waiting, so the salt survives a crash from here on.
|
|
511
|
+
// Resolve to an ABSOLUTE path from the caller's explicit choices; we name it exactly below so the
|
|
512
|
+
// secret-bearing file is never silently dropped somewhere the user can't find.
|
|
513
|
+
const receiptPath = resolveReceiptPath({
|
|
514
|
+
receiptPath: opts.receiptPath,
|
|
515
|
+
receiptDir: opts.receiptDir,
|
|
516
|
+
baseDir: opts.baseDir,
|
|
517
|
+
contentHash: commitTx.contentHash,
|
|
518
|
+
});
|
|
519
|
+
const receipt = buildReceipt({
|
|
520
|
+
contentHash: commitTx.contentHash,
|
|
521
|
+
committer: commitTx.committer,
|
|
522
|
+
salt: commitTx.salt,
|
|
523
|
+
commitment: commitTx.commitment,
|
|
524
|
+
contractAddress: commitTx.to,
|
|
525
|
+
chainId,
|
|
526
|
+
uri: opts.uri,
|
|
527
|
+
path: commitTx.path,
|
|
528
|
+
kind: commitTx.kind,
|
|
529
|
+
manifest: commitTx.manifest || undefined,
|
|
530
|
+
git: commitTx.git || undefined, // untrusted provenance hint: { commit, scope } when --git
|
|
531
|
+
parent: commitTx.parent || undefined, // B-10.1 lineage edge: recorded only when --parent was given
|
|
532
|
+
commitTxHash: commitReceiptTx.hash,
|
|
533
|
+
commitBlockNumber: commitBlock,
|
|
534
|
+
minRevealDelay: minDelay,
|
|
535
|
+
});
|
|
536
|
+
writeReceipt(receipt, receiptPath);
|
|
537
|
+
log(
|
|
538
|
+
` receipt written: ${receiptPath}\n` +
|
|
539
|
+
` KEEP THIS PRIVATE — it holds the secret salt. Resume with: vh reveal --receipt ${receiptPath}\n`
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
commitTx,
|
|
544
|
+
commitTxHash: commitReceiptTx.hash,
|
|
545
|
+
commitBlockNumber: commitBlock,
|
|
546
|
+
minRevealDelay: minDelay,
|
|
547
|
+
chainId,
|
|
548
|
+
receiptPath,
|
|
549
|
+
receipt,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Resume a claim from a persisted receipt and submit the reveal leg once the MIN_REVEAL_DELAY window
|
|
555
|
+
* has matured. Loads the salt/commitment/uri (and, B-10.1, the optional lineage `parent`) from the
|
|
556
|
+
* receipt — it needs NO information that wasn't durably written at commit time, so it works from a
|
|
557
|
+
* completely fresh process. When the receipt records a `parent` it routes to
|
|
558
|
+
* `revealWithParent(contentHash, salt, uri, parent)` (recording the lineage edge); otherwise it uses
|
|
559
|
+
* the legacy `reveal(contentHash, salt, uri)`, byte-for-byte unchanged.
|
|
560
|
+
*
|
|
561
|
+
* If the window has not yet matured the contract reverts with `RevealTooSoon`; if the receipt names a
|
|
562
|
+
* `parent` that was never anchored the contract reverts `UnknownParent`. In BOTH cases this function
|
|
563
|
+
* lets the error propagate and leaves the receipt file untouched so the user can simply retry later
|
|
564
|
+
* (the secret salt is never lost to a failed reveal).
|
|
565
|
+
*
|
|
566
|
+
* @param {object} opts
|
|
567
|
+
* @param {string} opts.receiptPath the receipt written by runCommit
|
|
568
|
+
* @param {object} opts.signer ethers Signer (must be the original committer)
|
|
569
|
+
* @param {object} [opts.provider]
|
|
570
|
+
* @param {bigint|number}[opts.chainId]
|
|
571
|
+
* @param {boolean}[opts.iUnderstandMainnet]
|
|
572
|
+
* @param {object} [opts.ethers]
|
|
573
|
+
* @param {(s:string)=>void}[opts.log]
|
|
574
|
+
* @param {(target:bigint)=>Promise<void>}[opts.waitForBlock] test hook to advance/await blocks
|
|
575
|
+
* @param {boolean}[opts.noWait] skip the maturation wait (let the contract enforce it)
|
|
576
|
+
* @returns {Promise<{revealed, revealTxHash, chainId, receiptPath, receipt}>}
|
|
577
|
+
*/
|
|
578
|
+
async function runReveal(opts) {
|
|
579
|
+
const ethersLib = opts.ethers || require("ethers");
|
|
580
|
+
const log = opts.log || ((s) => process.stdout.write(s));
|
|
581
|
+
|
|
582
|
+
if (!opts.receiptPath) throw new Error("runReveal requires a receiptPath");
|
|
583
|
+
if (!opts.signer) {
|
|
584
|
+
throw new Error("no signer available to submit the reveal (set PRIVATE_KEY?)");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Strict read: a corrupt/partial receipt throws here rather than producing a wrong reveal.
|
|
588
|
+
const receipt = readReceipt(opts.receiptPath);
|
|
589
|
+
|
|
590
|
+
const provider = opts.provider || opts.signer.provider;
|
|
591
|
+
const chainId = await _resolveChainGuard({
|
|
592
|
+
chainId: opts.chainId,
|
|
593
|
+
provider,
|
|
594
|
+
iUnderstandMainnet: opts.iUnderstandMainnet,
|
|
595
|
+
verb: "reveal",
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Sanity check: the signer must be the address bound into the commitment, else reveal would hit
|
|
599
|
+
// NoSuchCommitment. Fail fast with a clear message instead.
|
|
600
|
+
const signerAddr = ethersLib.getAddress(await opts.signer.getAddress());
|
|
601
|
+
if (ethersLib.getAddress(receipt.committer) !== signerAddr) {
|
|
602
|
+
throw new Error(
|
|
603
|
+
`signer ${signerAddr} is not the committer ${receipt.committer} bound in this receipt; ` +
|
|
604
|
+
"only the original committer can reveal it."
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const contract = new ethersLib.Contract(receipt.contractAddress, ABI, opts.signer);
|
|
609
|
+
|
|
610
|
+
// Wait out MIN_REVEAL_DELAY when we know the commit block (unless the caller opts out / handles it).
|
|
611
|
+
if (!opts.noWait && receipt.commitBlockNumber != null) {
|
|
612
|
+
const minDelay =
|
|
613
|
+
receipt.minRevealDelay != null
|
|
614
|
+
? BigInt(receipt.minRevealDelay)
|
|
615
|
+
: BigInt(await contract.MIN_REVEAL_DELAY());
|
|
616
|
+
await _waitRevealWindow({
|
|
617
|
+
provider,
|
|
618
|
+
commitBlock: BigInt(receipt.commitBlockNumber),
|
|
619
|
+
minDelay,
|
|
620
|
+
waitForBlock: opts.waitForBlock,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Route the reveal leg from what the receipt durably recorded at commit time (B-10.1). When the
|
|
625
|
+
// receipt carries a `parent` (a `vh commit --parent` claim), reuse buildRevealTx — which already
|
|
626
|
+
// supports a parent and routes to revealWithParent(contentHash, salt, uri, parent), recording the
|
|
627
|
+
// lineage edge. When absent it routes to the legacy reveal(), byte-for-byte unchanged (no regression).
|
|
628
|
+
// The contract checks the parent at REVEAL time: if the parent was never anchored it reverts
|
|
629
|
+
// UnknownParent and (since we let that propagate) the receipt is left intact for a later retry.
|
|
630
|
+
const revealTx = buildRevealTx({
|
|
631
|
+
contentHash: receipt.contentHash,
|
|
632
|
+
salt: receipt.salt,
|
|
633
|
+
uri: receipt.uri || "",
|
|
634
|
+
parent: receipt.parent, // null/undefined -> legacy reveal(); a hash -> revealWithParent()
|
|
635
|
+
contractAddress: receipt.contractAddress,
|
|
636
|
+
ethers: ethersLib,
|
|
637
|
+
});
|
|
638
|
+
const lineageNote = revealTx.parent ? ` with parent ${revealTx.parent}` : "";
|
|
639
|
+
log(`reveal: revealing ${receipt.contentHash}${lineageNote} as ${receipt.committer}...\n`);
|
|
640
|
+
const revealSent =
|
|
641
|
+
revealTx.parent == null
|
|
642
|
+
? await contract.reveal(receipt.contentHash, receipt.salt, receipt.uri || "")
|
|
643
|
+
: await contract.revealWithParent(
|
|
644
|
+
receipt.contentHash,
|
|
645
|
+
receipt.salt,
|
|
646
|
+
receipt.uri || "",
|
|
647
|
+
revealTx.parent
|
|
648
|
+
);
|
|
649
|
+
log(` reveal tx: ${revealSent.hash}\n`);
|
|
650
|
+
const revealReceiptTx = await revealSent.wait();
|
|
651
|
+
|
|
652
|
+
const revealed = _parseRevealed(revealReceiptTx, ethersLib);
|
|
653
|
+
if (revealed) {
|
|
654
|
+
log(
|
|
655
|
+
` Claimed (authorBound) at index ${revealed.index} by ${revealed.contributor} ` +
|
|
656
|
+
`in tx ${revealReceiptTx.hash}\n`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return {
|
|
661
|
+
revealed,
|
|
662
|
+
revealTxHash: revealReceiptTx.hash,
|
|
663
|
+
chainId,
|
|
664
|
+
receiptPath: opts.receiptPath,
|
|
665
|
+
receipt,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Run the full commit-reveal claim end to end (the one-shot convenience, both legs in one process).
|
|
671
|
+
*
|
|
672
|
+
* In `--dry-run` mode it only builds the commitment + both txs and returns them (no key, no
|
|
673
|
+
* network). Otherwise it: enforces the testnet guard, sends commit(), persists a durable receipt,
|
|
674
|
+
* waits for the MIN_REVEAL_DELAY window to pass, sends reveal(), and parses the Revealed event.
|
|
675
|
+
*
|
|
676
|
+
* RECEIPT POLICY (T-9.1). This one-shot helper NEVER silently drops a secret-bearing receipt into
|
|
677
|
+
* the current working directory. A claim receipt holds the secret `salt`, so persisting it is OPT-IN:
|
|
678
|
+
* - if an explicit `receiptPath` (or `receiptDir`) is given (and `writeReceiptFile !== false`), the
|
|
679
|
+
* receipt is written and the exact resolved file is named in the success log;
|
|
680
|
+
* - if NEITHER is given, NOTHING is written — the validated receipt object is returned in-memory on
|
|
681
|
+
* the result as `receipt` (and `receiptPath` stays undefined). The caller that wants a durable,
|
|
682
|
+
* resumable artifact should use `runCommit`/`vh commit` (the intended durable command, which
|
|
683
|
+
* resolves a documented default path), or pass an explicit `receiptPath`/`receiptDir` here.
|
|
684
|
+
* `writeReceiptFile: false` still hard-disables the write even when a destination is present.
|
|
685
|
+
*
|
|
686
|
+
* @param {object} opts
|
|
687
|
+
* @param {string} opts.path
|
|
688
|
+
* @param {string} [opts.uri]
|
|
689
|
+
* @param {string} [opts.parent] optional predecessor contentHash (T-10.1 lineage edge);
|
|
690
|
+
* non-zero routes the reveal leg to revealWithParent()
|
|
691
|
+
* @param {boolean}[opts.git] hash EXACTLY the git-tracked files (T-8.1 enumeration)
|
|
692
|
+
* @param {string} [opts.ref] with git: which commit's tracked set (default HEAD)
|
|
693
|
+
* @param {string} opts.contractAddress
|
|
694
|
+
* @param {boolean}[opts.dryRun]
|
|
695
|
+
* @param {boolean}[opts.iUnderstandMainnet]
|
|
696
|
+
* @param {object} [opts.signer] ethers Signer (required unless dryRun)
|
|
697
|
+
* @param {object} [opts.provider] ethers Provider (chainId + block waits)
|
|
698
|
+
* @param {bigint|number}[opts.chainId] override chainId lookup (tests)
|
|
699
|
+
* @param {string} [opts.salt] reuse a salt (else random)
|
|
700
|
+
* @param {string} [opts.receiptPath] explicit full path to persist the receipt (else nothing)
|
|
701
|
+
* @param {string} [opts.receiptDir] explicit destination DIR to persist the receipt into (else nothing)
|
|
702
|
+
* @param {boolean}[opts.writeReceiptFile] set false to hard-disable the write even with a destination
|
|
703
|
+
* @param {object} [opts.ethers]
|
|
704
|
+
* @param {(s:string)=>void}[opts.log]
|
|
705
|
+
* @param {(target:bigint)=>Promise<void>}[opts.waitForBlock] test hook to advance/await blocks
|
|
706
|
+
* @returns {Promise<object>} includes `receipt` (the in-memory receipt object) and `receiptPath`
|
|
707
|
+
* (the file written, or undefined when none was)
|
|
708
|
+
*/
|
|
709
|
+
async function runClaim(opts) {
|
|
710
|
+
const ethersLib = opts.ethers || require("ethers");
|
|
711
|
+
const log = opts.log || ((s) => process.stdout.write(s));
|
|
712
|
+
|
|
713
|
+
// Resolve who the committer is. For a dry run we may not have a signer; allow an explicit
|
|
714
|
+
// committer address so the plan can still be shown.
|
|
715
|
+
let committer = opts.committer;
|
|
716
|
+
if (!committer && opts.signer) {
|
|
717
|
+
committer = await opts.signer.getAddress();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const commitTx = buildCommitTx({
|
|
721
|
+
path: opts.path,
|
|
722
|
+
committer,
|
|
723
|
+
git: opts.git,
|
|
724
|
+
ref: opts.ref,
|
|
725
|
+
contractAddress: opts.contractAddress,
|
|
726
|
+
salt: opts.salt,
|
|
727
|
+
ethers: ethersLib,
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Validate the optional `--parent` lineage edge BEFORE any network call (parser parity with
|
|
731
|
+
// `vh anchor`, whose buildAnchorTx runs normalizeParent up front). The edge is recorded only on the
|
|
732
|
+
// REVEAL leg (revealWithParent), but a malformed/self-referential parent is a typo the user must
|
|
733
|
+
// learn about immediately — NOT after commit() has already been broadcast (a real gas-spending,
|
|
734
|
+
// MIN_REVEAL_DELAY-waiting write) only to have the reveal reject it. A typo never silently drops the
|
|
735
|
+
// parent into a no-op commit. `normalizeParent` maps missing/empty/zero -> null (a lineage root) and
|
|
736
|
+
// hard-errors on a malformed non-zero value; the self-reference is rejected here, the contract still
|
|
737
|
+
// enforces UnknownParent/SelfParent authoritatively on-chain. Reuses anchor.js, not a reimplementation.
|
|
738
|
+
const { normalizeParent } = require("./anchor");
|
|
739
|
+
const parent = normalizeParent(opts.parent, ethersLib);
|
|
740
|
+
if (parent !== null && parent.toLowerCase() === commitTx.contentHash.toLowerCase()) {
|
|
741
|
+
throw new Error(
|
|
742
|
+
"refusing to reveal a record as its own parent (self-reference; the contract rejects it as SelfParent)"
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (opts.dryRun) {
|
|
747
|
+
const revealTx = buildRevealTx({
|
|
748
|
+
contentHash: commitTx.contentHash,
|
|
749
|
+
salt: commitTx.salt,
|
|
750
|
+
uri: opts.uri,
|
|
751
|
+
parent, // already validated above (parity with the real submission path below)
|
|
752
|
+
contractAddress: opts.contractAddress,
|
|
753
|
+
ethers: ethersLib,
|
|
754
|
+
});
|
|
755
|
+
// Pass the built revealTx so the printed plan shows the lineage edge (parent + revealWithParent)
|
|
756
|
+
// it would record — without it the preview would silently omit a `--parent` the user passed.
|
|
757
|
+
log(formatDryRun(commitTx, revealTx) + "\n");
|
|
758
|
+
return { dryRun: true, commitTx, revealTx };
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Real submission from here on.
|
|
762
|
+
if (!opts.signer) {
|
|
763
|
+
throw new Error("no signer available to submit the claim (set PRIVATE_KEY?)");
|
|
764
|
+
}
|
|
765
|
+
const provider = opts.provider || opts.signer.provider;
|
|
766
|
+
|
|
767
|
+
const chainId = await _resolveChainGuard({
|
|
768
|
+
chainId: opts.chainId,
|
|
769
|
+
provider,
|
|
770
|
+
iUnderstandMainnet: opts.iUnderstandMainnet,
|
|
771
|
+
verb: "claim",
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
const contract = new ethersLib.Contract(commitTx.to, ABI, opts.signer);
|
|
775
|
+
|
|
776
|
+
// --- Step 1: commit ---
|
|
777
|
+
log(`claim: committing ${commitTx.path} (${commitTx.kind}) as ${commitTx.committer}...\n`);
|
|
778
|
+
const commitSent = await contract.commit(commitTx.commitment);
|
|
779
|
+
log(` commit tx: ${commitSent.hash}\n`);
|
|
780
|
+
const commitReceipt = await commitSent.wait();
|
|
781
|
+
const commitBlock = BigInt(commitReceipt.blockNumber);
|
|
782
|
+
const minDelay = BigInt(await contract.MIN_REVEAL_DELAY());
|
|
783
|
+
|
|
784
|
+
// Build the validated receipt object in memory regardless — it is always returned so a caller can
|
|
785
|
+
// persist it itself. We PERSIST it to disk only when the caller explicitly opted in with a
|
|
786
|
+
// `receiptPath` (and did not set writeReceiptFile:false). A claim receipt holds the secret salt, so
|
|
787
|
+
// this one-shot convenience never silently drops it into cwd; for a durable, resumable artifact use
|
|
788
|
+
// `runCommit`/`vh commit` (which resolves a documented default path and names the exact file).
|
|
789
|
+
let receiptPath;
|
|
790
|
+
const receipt = buildReceipt({
|
|
791
|
+
contentHash: commitTx.contentHash,
|
|
792
|
+
committer: commitTx.committer,
|
|
793
|
+
salt: commitTx.salt,
|
|
794
|
+
commitment: commitTx.commitment,
|
|
795
|
+
contractAddress: commitTx.to,
|
|
796
|
+
chainId,
|
|
797
|
+
uri: opts.uri,
|
|
798
|
+
path: commitTx.path,
|
|
799
|
+
kind: commitTx.kind,
|
|
800
|
+
manifest: commitTx.manifest || undefined,
|
|
801
|
+
git: commitTx.git || undefined, // untrusted provenance hint: { commit, scope } when --git
|
|
802
|
+
commitTxHash: commitReceipt.hash,
|
|
803
|
+
commitBlockNumber: commitBlock,
|
|
804
|
+
minRevealDelay: minDelay,
|
|
805
|
+
});
|
|
806
|
+
const persistOptIn = opts.receiptPath != null || opts.receiptDir != null;
|
|
807
|
+
if (opts.writeReceiptFile !== false && persistOptIn) {
|
|
808
|
+
receiptPath = resolveReceiptPath({
|
|
809
|
+
receiptPath: opts.receiptPath,
|
|
810
|
+
receiptDir: opts.receiptDir,
|
|
811
|
+
contentHash: commitTx.contentHash,
|
|
812
|
+
});
|
|
813
|
+
writeReceipt(receipt, receiptPath);
|
|
814
|
+
log(
|
|
815
|
+
` receipt written: ${receiptPath}\n` +
|
|
816
|
+
` KEEP THIS PRIVATE — it holds the secret salt.\n`
|
|
817
|
+
);
|
|
818
|
+
} else if (opts.writeReceiptFile !== false) {
|
|
819
|
+
// No explicit destination: do NOT silently write a secret receipt to cwd. Tell the user how to
|
|
820
|
+
// persist one if they want a resumable artifact.
|
|
821
|
+
log(
|
|
822
|
+
" (no --receipt given: not persisting a claim receipt. " +
|
|
823
|
+
"Pass --receipt <path> to persist a resumable receipt, or use `vh commit`.)\n"
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// --- Wait out MIN_REVEAL_DELAY ---
|
|
828
|
+
await _waitRevealWindow({
|
|
829
|
+
provider,
|
|
830
|
+
commitBlock,
|
|
831
|
+
minDelay,
|
|
832
|
+
waitForBlock: opts.waitForBlock,
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// --- Step 2: reveal ---
|
|
836
|
+
// Route to revealWithParent() iff a non-zero predecessor was given (T-10.1); otherwise the legacy
|
|
837
|
+
// reveal(), byte-for-byte unchanged. `parent` was validated up front (before any network call) so a
|
|
838
|
+
// malformed/self-referential value already hard-errored before commit() was ever broadcast.
|
|
839
|
+
const lineageNote = parent == null ? "" : ` with parent ${parent}`;
|
|
840
|
+
log(`claim: revealing ${commitTx.contentHash}${lineageNote}...\n`);
|
|
841
|
+
const revealUri = opts.uri == null ? "" : String(opts.uri);
|
|
842
|
+
const revealSent =
|
|
843
|
+
parent == null
|
|
844
|
+
? await contract.reveal(commitTx.contentHash, commitTx.salt, revealUri)
|
|
845
|
+
: await contract.revealWithParent(commitTx.contentHash, commitTx.salt, revealUri, parent);
|
|
846
|
+
log(` reveal tx: ${revealSent.hash}\n`);
|
|
847
|
+
const revealReceipt = await revealSent.wait();
|
|
848
|
+
|
|
849
|
+
const revealed = _parseRevealed(revealReceipt, ethersLib);
|
|
850
|
+
if (revealed) {
|
|
851
|
+
log(
|
|
852
|
+
` Claimed (authorBound) at index ${revealed.index} by ${revealed.contributor} ` +
|
|
853
|
+
`in tx ${revealReceipt.hash}\n`
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return {
|
|
858
|
+
dryRun: false,
|
|
859
|
+
chainId,
|
|
860
|
+
commitTx,
|
|
861
|
+
commitTxHash: commitReceipt.hash,
|
|
862
|
+
revealTxHash: revealReceipt.hash,
|
|
863
|
+
revealed,
|
|
864
|
+
receiptPath, // undefined when no receipt file was written (the default, safe behaviour)
|
|
865
|
+
receipt, // the validated receipt object, always returned in-memory for the caller to persist
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
module.exports = {
|
|
870
|
+
contentHashForPath,
|
|
871
|
+
newSalt,
|
|
872
|
+
computeCommitment,
|
|
873
|
+
buildCommitTx,
|
|
874
|
+
buildRevealTx,
|
|
875
|
+
formatDryRun,
|
|
876
|
+
resolveReceiptPath,
|
|
877
|
+
runClaim,
|
|
878
|
+
runCommit,
|
|
879
|
+
runReveal,
|
|
880
|
+
ABI,
|
|
881
|
+
};
|