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
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// cli/anchor-artifact.js — `vh anchor-artifact` / `vh verify-anchored` (T-70.2, EPIC-70).
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS IS
|
|
6
|
+
// The thin CLI bridge between the PURE anchor-binding core (T-70.1, cli/core/anchor-binding.js)
|
|
7
|
+
// and a live ContributionRegistry:
|
|
8
|
+
//
|
|
9
|
+
// vh anchor-artifact <sealed-file> --contract <addr> --rpc <url>
|
|
10
|
+
// (--key-env <VAR> | --key-file <p>)
|
|
11
|
+
// [--author-bound] [--uri <s>] [--out <receipt>] [--json]
|
|
12
|
+
// [--i-understand-mainnet]
|
|
13
|
+
// read + parse the sealed artifact, extract its ONE canonical digest via the closed T-70.1
|
|
14
|
+
// kind table (each leg re-validates through the artifact's own shipped validator), submit
|
|
15
|
+
// that digest as the registry contentHash, wait for the tx to mine, READ THE RECORD BACK
|
|
16
|
+
// (contributor / authorBound / blockNumber / block timestamp — the D-1 semantics surfaced
|
|
17
|
+
// from the chain, never re-implemented here), and emit the canonical
|
|
18
|
+
// kind:"vh-anchored-receipt@1" container.
|
|
19
|
+
// * default: the ONE-SHOT anchor() write path — the record is NOT author-bound (the
|
|
20
|
+
// contract records the first broadcaster; a mempool copier could have been first).
|
|
21
|
+
// * --author-bound: the commit-reveal claim (D-1): commit(keccak256(abi.encode(digest,
|
|
22
|
+
// committer, salt))) — the SHIPPED cli/claim.js computeCommitment/newSalt, reused
|
|
23
|
+
// verbatim — wait out MIN_REVEAL_DELAY, then reveal(digest, salt, uri). The resulting
|
|
24
|
+
// record reads back authorBound:true and cannot be redirected by a front-runner.
|
|
25
|
+
//
|
|
26
|
+
// vh verify-anchored <receipt> <sealed-file> [--rpc <url> --contract <addr>] [--json]
|
|
27
|
+
// OFFLINE by default: strict T-70.1 verifyAnchoredReceipt — validate the receipt container,
|
|
28
|
+
// RECOMPUTE the artifact's digest through the same closed table, and match the full
|
|
29
|
+
// {kind, digest, how} triple; every deviation is a SPECIFIC named reject. With BOTH --rpc
|
|
30
|
+
// and --contract it ADDITIONALLY (a) authenticates the registry through the EXISTING EPIC-11
|
|
31
|
+
// identity probe (cli/registry.js assertRegistry — no record is believed until the contract
|
|
32
|
+
// self-identifies on the receipt's chainId), then (b) re-checks the receipt's chain facts
|
|
33
|
+
// against the chain: the record for the digest must exist and its contributor / authorBound /
|
|
34
|
+
// blockNumber / block timestamp must equal the receipt's, and the receipt's txHash must be a
|
|
35
|
+
// real mined tx in the recorded block targeting the recorded contract. Each mismatch is a
|
|
36
|
+
// SPECIFIC named reject. verify-anchored NEVER signs and needs NO key.
|
|
37
|
+
//
|
|
38
|
+
// KEY HYGIENE (the house discipline, reused — not re-implemented)
|
|
39
|
+
// The signing key for anchor-artifact comes ONLY from --key-env <VAR> / --key-file <path> via the
|
|
40
|
+
// ONE shared read-used-discarded path, cli/core/attestation.js loadSigningWallet: EXACTLY ONE
|
|
41
|
+
// source (neither/both is a usage error BEFORE anything is read), a missing var / unreadable file /
|
|
42
|
+
// malformed or zero key hard-errors naming only the SOURCE (never echoing key material), and the
|
|
43
|
+
// raw key exists only inside the in-process Wallet. It is never generated, persisted, or logged.
|
|
44
|
+
//
|
|
45
|
+
// MAINNET GUARD (reused verbatim)
|
|
46
|
+
// The EXISTING cli/anchor.js isTestnetChainId set gates every submission: a chainId outside the
|
|
47
|
+
// known local/dev/testnet set refuses to write unless --i-understand-mainnet is passed. The guard
|
|
48
|
+
// runs BEFORE any transaction is built or sent.
|
|
49
|
+
//
|
|
50
|
+
// FREE SURFACE
|
|
51
|
+
// Both verbs are free: no paid gate is consulted anywhere in this module (the acceptance grep
|
|
52
|
+
// pins that), and verify-anchored is verify-only (no key, no signer, nothing written except what
|
|
53
|
+
// --out of the anchor verb explicitly asked for).
|
|
54
|
+
//
|
|
55
|
+
// OUTPUT / EXIT CONTRACT (stable; a future indexer/UI may depend on it)
|
|
56
|
+
// vh anchor-artifact: exit 0 anchored (receipt emitted; --json prints ONE machine object)
|
|
57
|
+
// exit 3 named reject — the artifact failed its own validator/binding, OR
|
|
58
|
+
// the registry itself reverted with a named error (e.g.
|
|
59
|
+
// AlreadyAnchored) — always a clean one-line error, never a stack
|
|
60
|
+
// exit 2 usage (bad flag, missing <sealed-file>/--contract/--rpc, neither or
|
|
61
|
+
// both key sources)
|
|
62
|
+
// exit 1 IO / network / key-source runtime error (unreadable file, RPC down,
|
|
63
|
+
// missing env var, malformed key, non-testnet refusal)
|
|
64
|
+
// vh verify-anchored: exit 0 ACCEPTED / 3 REJECTED (named) / 2 usage / 1 IO — the SHARED 0/3
|
|
65
|
+
// verify contract every vh verify-verb keeps.
|
|
66
|
+
//
|
|
67
|
+
// FILESYSTEM HYGIENE
|
|
68
|
+
// The only file either verb ever writes is the anchored receipt, and only when the caller passed
|
|
69
|
+
// an explicit --out <path> — never silently into cwd. Without --out the receipt is printed to
|
|
70
|
+
// stdout (its canonical one-line serialization) so the caller can redirect it wherever they want.
|
|
71
|
+
|
|
72
|
+
const fs = require("fs");
|
|
73
|
+
const path = require("path");
|
|
74
|
+
|
|
75
|
+
const { isTestnetChainId } = require("./anchor"); // the EXISTING mainnet guard, reused verbatim
|
|
76
|
+
const { computeCommitment, newSalt } = require("./claim"); // the SHIPPED commit-reveal building blocks
|
|
77
|
+
const { loadSigningWallet } = require("./core/attestation"); // the ONE read-used-discarded key path
|
|
78
|
+
const binding = require("./core/anchor-binding"); // T-70.1: the pure digest/receipt/verify core
|
|
79
|
+
const {
|
|
80
|
+
assertRegistry,
|
|
81
|
+
isGenuineRpcError,
|
|
82
|
+
formatRegistryLine,
|
|
83
|
+
jsonRegistryBlock,
|
|
84
|
+
} = require("./registry"); // EPIC-11: the EXISTING authenticated read path (identity probe)
|
|
85
|
+
|
|
86
|
+
const ARTIFACT = require("./core/registryArtifact");
|
|
87
|
+
const ABI = ARTIFACT.abi;
|
|
88
|
+
|
|
89
|
+
// The shared exit contract (matches the wider vh family).
|
|
90
|
+
const EXIT = Object.freeze({ OK: 0, IO: 1, USAGE: 2, REJECT: 3 });
|
|
91
|
+
|
|
92
|
+
const ANCHOR_ARTIFACT_USAGE =
|
|
93
|
+
"usage: vh anchor-artifact <sealed-file> --contract <addr> --rpc <url> " +
|
|
94
|
+
"(--key-env <VAR> | --key-file <p>) [--author-bound] [--uri <s>] [--out <receipt>] [--json] " +
|
|
95
|
+
"[--i-understand-mainnet]\n";
|
|
96
|
+
|
|
97
|
+
const VERIFY_ANCHORED_USAGE =
|
|
98
|
+
"usage: vh verify-anchored <receipt> <sealed-file> [--rpc <url> --contract <addr>] [--json]\n";
|
|
99
|
+
|
|
100
|
+
// The registry's own named custom errors (from the contract ABI). Used only as a last-resort
|
|
101
|
+
// textual fallback when a node surfaces a revert without decodable data.
|
|
102
|
+
const REGISTRY_ERROR_NAMES = Object.freeze([
|
|
103
|
+
"ZeroHash",
|
|
104
|
+
"AlreadyAnchored",
|
|
105
|
+
"NotAnchored",
|
|
106
|
+
"IndexOutOfRange",
|
|
107
|
+
"ZeroCommitment",
|
|
108
|
+
"CommitmentExists",
|
|
109
|
+
"NoSuchCommitment",
|
|
110
|
+
"RevealTooSoon",
|
|
111
|
+
"UnknownParent",
|
|
112
|
+
"SelfParent",
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------------------------------
|
|
116
|
+
// argv parsers — throw on unknown/incomplete flags so a typo never silently becomes a real submission.
|
|
117
|
+
// ---------------------------------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Parse `anchor-artifact` argv. One positional (<sealed-file>); throws on unknown flags, a flag
|
|
121
|
+
* missing its value, or extra positionals.
|
|
122
|
+
*/
|
|
123
|
+
function parseAnchorArtifactArgs(argv) {
|
|
124
|
+
const opts = {
|
|
125
|
+
artifact: undefined,
|
|
126
|
+
contract: undefined,
|
|
127
|
+
rpc: undefined,
|
|
128
|
+
keyEnv: undefined,
|
|
129
|
+
keyFile: undefined,
|
|
130
|
+
uri: undefined,
|
|
131
|
+
out: undefined,
|
|
132
|
+
authorBound: false,
|
|
133
|
+
json: false,
|
|
134
|
+
iUnderstandMainnet: false,
|
|
135
|
+
};
|
|
136
|
+
for (let i = 0; i < argv.length; i++) {
|
|
137
|
+
const a = argv[i];
|
|
138
|
+
switch (a) {
|
|
139
|
+
case "--author-bound":
|
|
140
|
+
opts.authorBound = true;
|
|
141
|
+
break;
|
|
142
|
+
case "--json":
|
|
143
|
+
opts.json = true;
|
|
144
|
+
break;
|
|
145
|
+
case "--i-understand-mainnet":
|
|
146
|
+
opts.iUnderstandMainnet = true;
|
|
147
|
+
break;
|
|
148
|
+
case "--uri":
|
|
149
|
+
opts.uri = argv[++i];
|
|
150
|
+
if (opts.uri === undefined) throw new Error("--uri requires a value");
|
|
151
|
+
break;
|
|
152
|
+
case "--out":
|
|
153
|
+
opts.out = argv[++i];
|
|
154
|
+
if (opts.out === undefined) throw new Error("--out requires a value");
|
|
155
|
+
break;
|
|
156
|
+
case "--contract":
|
|
157
|
+
opts.contract = argv[++i];
|
|
158
|
+
if (opts.contract === undefined) throw new Error("--contract requires a value");
|
|
159
|
+
break;
|
|
160
|
+
case "--rpc":
|
|
161
|
+
opts.rpc = argv[++i];
|
|
162
|
+
if (opts.rpc === undefined) throw new Error("--rpc requires a value");
|
|
163
|
+
break;
|
|
164
|
+
case "--key-env":
|
|
165
|
+
opts.keyEnv = argv[++i];
|
|
166
|
+
if (opts.keyEnv === undefined) throw new Error("--key-env requires a value");
|
|
167
|
+
break;
|
|
168
|
+
case "--key-file":
|
|
169
|
+
opts.keyFile = argv[++i];
|
|
170
|
+
if (opts.keyFile === undefined) throw new Error("--key-file requires a value");
|
|
171
|
+
break;
|
|
172
|
+
default:
|
|
173
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
174
|
+
if (opts.artifact !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
175
|
+
opts.artifact = a;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return opts;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Parse `verify-anchored` argv. TWO positionals, in order: <receipt> then <sealed-file>.
|
|
183
|
+
*/
|
|
184
|
+
function parseVerifyAnchoredArgs(argv) {
|
|
185
|
+
const opts = {
|
|
186
|
+
receipt: undefined,
|
|
187
|
+
artifact: undefined,
|
|
188
|
+
contract: undefined,
|
|
189
|
+
rpc: undefined,
|
|
190
|
+
json: false,
|
|
191
|
+
};
|
|
192
|
+
for (let i = 0; i < argv.length; i++) {
|
|
193
|
+
const a = argv[i];
|
|
194
|
+
switch (a) {
|
|
195
|
+
case "--json":
|
|
196
|
+
opts.json = true;
|
|
197
|
+
break;
|
|
198
|
+
case "--contract":
|
|
199
|
+
opts.contract = argv[++i];
|
|
200
|
+
if (opts.contract === undefined) throw new Error("--contract requires a value");
|
|
201
|
+
break;
|
|
202
|
+
case "--rpc":
|
|
203
|
+
opts.rpc = argv[++i];
|
|
204
|
+
if (opts.rpc === undefined) throw new Error("--rpc requires a value");
|
|
205
|
+
break;
|
|
206
|
+
default:
|
|
207
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
208
|
+
if (opts.receipt === undefined) opts.receipt = a;
|
|
209
|
+
else if (opts.artifact === undefined) opts.artifact = a;
|
|
210
|
+
else throw new Error(`unexpected extra argument: ${a}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return opts;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------------------------------
|
|
217
|
+
// Small shared helpers.
|
|
218
|
+
// ---------------------------------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
/** Read + JSON.parse a file; on failure write an actionable error and return null (caller exits 1). */
|
|
221
|
+
function readJson(label, filePath, writeErr) {
|
|
222
|
+
let text;
|
|
223
|
+
try {
|
|
224
|
+
text = fs.readFileSync(path.resolve(filePath), "utf8");
|
|
225
|
+
} catch (e) {
|
|
226
|
+
writeErr(`error: cannot read ${label} ${filePath}: ${e.message}\n`);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
return JSON.parse(text);
|
|
231
|
+
} catch (e) {
|
|
232
|
+
writeErr(`error: ${label} ${filePath} is not valid JSON: ${e.message}\n`);
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Pull raw revert data out of the several places ethers/hardhat nodes stash it. */
|
|
238
|
+
function extractRevertData(err) {
|
|
239
|
+
if (!err || typeof err !== "object") return null;
|
|
240
|
+
const candidates = [
|
|
241
|
+
err.data,
|
|
242
|
+
err.info && err.info.error && err.info.error.data,
|
|
243
|
+
err.info && err.info.error && err.info.error.data && err.info.error.data.data,
|
|
244
|
+
err.error && err.error.data,
|
|
245
|
+
];
|
|
246
|
+
for (const c of candidates) {
|
|
247
|
+
if (typeof c === "string" && c.startsWith("0x") && c.length >= 10) return c;
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Resolve a send/read failure to the REGISTRY'S OWN named error (e.g. "AlreadyAnchored(0x…, 0x…)"),
|
|
254
|
+
* or null when the failure is not a decodable contract revert. Tries, in order: ethers' decoded
|
|
255
|
+
* `err.revert`, parsing raw revert data against the registry ABI, ethers' `err.reason`, and finally
|
|
256
|
+
* a known-error-name match in the message (some nodes surface only "unknown custom error" text).
|
|
257
|
+
*/
|
|
258
|
+
function namedRegistryReject(err, ethersLib) {
|
|
259
|
+
if (err && err.revert && err.revert.name) {
|
|
260
|
+
const args = Array.isArray(err.revert.args) ? Array.from(err.revert.args).map(String).join(", ") : "";
|
|
261
|
+
return `${err.revert.name}(${args})`;
|
|
262
|
+
}
|
|
263
|
+
const data = extractRevertData(err);
|
|
264
|
+
if (data) {
|
|
265
|
+
try {
|
|
266
|
+
const parsed = new ethersLib.Interface(ABI).parseError(data);
|
|
267
|
+
if (parsed) return `${parsed.name}(${Array.from(parsed.args).map(String).join(", ")})`;
|
|
268
|
+
} catch (_) {
|
|
269
|
+
/* not one of the registry's errors */
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (err && typeof err.reason === "string" && /[A-Za-z]/.test(err.reason)) return err.reason;
|
|
273
|
+
const msg = err && err.message ? String(err.message) : "";
|
|
274
|
+
for (const name of REGISTRY_ERROR_NAMES) {
|
|
275
|
+
if (msg.includes(name)) return name;
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Wait until the chain has advanced past the MIN_REVEAL_DELAY window for a commit mined in
|
|
282
|
+
* `commitBlock` (a reveal needs `current > commitBlock + minDelay`). Mirrors the shipped
|
|
283
|
+
* cli/claim.js window wait: an injectable `waitForBlock` lets a test mine the blocks itself; the
|
|
284
|
+
* real path polls the node until blocks are produced.
|
|
285
|
+
*/
|
|
286
|
+
async function waitRevealWindow({ provider, commitBlock, minDelay, waitForBlock }) {
|
|
287
|
+
const revealAfter = commitBlock + minDelay;
|
|
288
|
+
if (waitForBlock) {
|
|
289
|
+
await waitForBlock(revealAfter + 1n);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
/* eslint-disable no-await-in-loop */
|
|
293
|
+
while (BigInt(await provider.getBlockNumber()) <= revealAfter) {
|
|
294
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
295
|
+
}
|
|
296
|
+
/* eslint-enable no-await-in-loop */
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------------------------------
|
|
300
|
+
// vh anchor-artifact
|
|
301
|
+
// ---------------------------------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Run `vh anchor-artifact` end to end. Returns the process exit code.
|
|
305
|
+
*
|
|
306
|
+
* @param {object} opts
|
|
307
|
+
* @param {string} opts.artifact path to the sealed artifact file (JSON)
|
|
308
|
+
* @param {string} opts.contract deployed ContributionRegistry address
|
|
309
|
+
* @param {string} [opts.rpc] RPC endpoint URL (or inject opts.provider)
|
|
310
|
+
* @param {string} [opts.keyEnv] env var NAME holding the key (EXACTLY ONE of keyEnv/keyFile)
|
|
311
|
+
* @param {string} [opts.keyFile] path to a key file the caller created
|
|
312
|
+
* @param {boolean}[opts.authorBound] use the commit-reveal claim (record reads back authorBound:true)
|
|
313
|
+
* @param {string} [opts.uri] optional untrusted off-chain pointer hint
|
|
314
|
+
* @param {string} [opts.out] write the anchored receipt to THIS explicit path (else stdout)
|
|
315
|
+
* @param {boolean}[opts.json] emit ONE machine-readable JSON object instead of human lines
|
|
316
|
+
* @param {boolean}[opts.iUnderstandMainnet] bypass the non-testnet refusal (the EXISTING guard)
|
|
317
|
+
* @param {object} [opts.provider] injected ethers Provider (tests; else built from opts.rpc)
|
|
318
|
+
* @param {bigint|number}[opts.chainId] override/short-circuit the chainId lookup (tests — same
|
|
319
|
+
* hook the shipped runAnchor exposes)
|
|
320
|
+
* @param {(target:bigint)=>Promise<void>}[opts.waitForBlock] test hook to advance/await blocks
|
|
321
|
+
* @param {object} [opts.ethers] ethers v6 module
|
|
322
|
+
* @param {{write?:Function, writeErr?:Function}} [io]
|
|
323
|
+
* @returns {Promise<number>} exit code (see the module-header contract)
|
|
324
|
+
*/
|
|
325
|
+
async function runAnchorArtifact(opts, io = {}) {
|
|
326
|
+
const write = io.write || ((s) => process.stdout.write(s));
|
|
327
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
328
|
+
const ethersLib = opts.ethers || require("ethers");
|
|
329
|
+
|
|
330
|
+
const reject = (reason, detail) => {
|
|
331
|
+
if (opts.json) {
|
|
332
|
+
write(JSON.stringify({ ok: false, verdict: "REJECTED", reason, detail }, null, 2) + "\n");
|
|
333
|
+
} else {
|
|
334
|
+
writeErr(`anchor-artifact: REJECTED (${reason}): ${detail}\n`);
|
|
335
|
+
}
|
|
336
|
+
return EXIT.REJECT;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// ---- usage-shape validation FIRST (nothing read, no key touched, no network) ----
|
|
340
|
+
if (!opts.artifact) {
|
|
341
|
+
writeErr("error: `vh anchor-artifact` requires a <sealed-file>\n" + ANCHOR_ARTIFACT_USAGE);
|
|
342
|
+
return EXIT.USAGE;
|
|
343
|
+
}
|
|
344
|
+
if (!opts.contract) {
|
|
345
|
+
writeErr(
|
|
346
|
+
"error: no contract address: pass --contract <address> or set VH_CONTRACT in the environment\n" +
|
|
347
|
+
ANCHOR_ARTIFACT_USAGE
|
|
348
|
+
);
|
|
349
|
+
return EXIT.USAGE;
|
|
350
|
+
}
|
|
351
|
+
if (!ethersLib.isAddress(opts.contract)) {
|
|
352
|
+
writeErr(`error: invalid contract address: ${opts.contract}\n`);
|
|
353
|
+
return EXIT.USAGE;
|
|
354
|
+
}
|
|
355
|
+
const hasEnv = opts.keyEnv !== undefined && opts.keyEnv !== null;
|
|
356
|
+
const hasFile = opts.keyFile !== undefined && opts.keyFile !== null;
|
|
357
|
+
if (!hasEnv && !hasFile) {
|
|
358
|
+
writeErr(
|
|
359
|
+
"error: `vh anchor-artifact` requires EXACTLY ONE signing-key source: --key-env <VAR> or " +
|
|
360
|
+
"--key-file <path>\n" +
|
|
361
|
+
ANCHOR_ARTIFACT_USAGE
|
|
362
|
+
);
|
|
363
|
+
return EXIT.USAGE;
|
|
364
|
+
}
|
|
365
|
+
if (hasEnv && hasFile) {
|
|
366
|
+
writeErr("error: --key-env and --key-file are mutually exclusive; pass EXACTLY ONE signing-key source\n");
|
|
367
|
+
return EXIT.USAGE;
|
|
368
|
+
}
|
|
369
|
+
if (!opts.provider && !opts.rpc) {
|
|
370
|
+
writeErr(
|
|
371
|
+
"error: no RPC endpoint; pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL in the environment\n" +
|
|
372
|
+
ANCHOR_ARTIFACT_USAGE
|
|
373
|
+
);
|
|
374
|
+
return EXIT.USAGE;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---- read the artifact + extract its ONE canonical digest (offline; the T-70.1 closed table) ----
|
|
378
|
+
const artifactPath = path.resolve(opts.artifact);
|
|
379
|
+
const artifact = readJson("artifact", opts.artifact, writeErr);
|
|
380
|
+
if (artifact === null) return EXIT.IO;
|
|
381
|
+
const d = binding.artifactDigest(artifact);
|
|
382
|
+
if (!d.ok) {
|
|
383
|
+
// The artifact's own named validation reject — nothing was signed or sent.
|
|
384
|
+
return reject(d.reason, d.detail || "the artifact failed its own validator; refusing to anchor it");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ---- signing key: the ONE house read-used-discarded path. Loaded only AFTER the artifact proved
|
|
388
|
+
// anchorable, and BEFORE any network use; errors name only the SOURCE, never key material. ----
|
|
389
|
+
let wallet;
|
|
390
|
+
try {
|
|
391
|
+
({ wallet } = loadSigningWallet({ keyEnv: opts.keyEnv, keyFile: opts.keyFile }));
|
|
392
|
+
} catch (e) {
|
|
393
|
+
writeErr(`error: ${e.message}\n`);
|
|
394
|
+
return EXIT.IO;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ---- chain resolution + the EXISTING mainnet guard (BEFORE any transaction is built/sent) ----
|
|
398
|
+
const provider = opts.provider || new ethersLib.JsonRpcProvider(opts.rpc);
|
|
399
|
+
let chainId = opts.chainId;
|
|
400
|
+
if (chainId == null) {
|
|
401
|
+
try {
|
|
402
|
+
chainId = (await provider.getNetwork()).chainId;
|
|
403
|
+
} catch (e) {
|
|
404
|
+
writeErr(`error: cannot reach the RPC endpoint to determine the chainId: ${e.message}\n`);
|
|
405
|
+
return EXIT.IO;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
chainId = BigInt(chainId);
|
|
409
|
+
if (!isTestnetChainId(chainId) && !opts.iUnderstandMainnet) {
|
|
410
|
+
writeErr(
|
|
411
|
+
`error: refusing to anchor on chainId ${chainId.toString()} (not a known testnet). ` +
|
|
412
|
+
"If you really mean to write to this chain, re-run with --i-understand-mainnet.\n"
|
|
413
|
+
);
|
|
414
|
+
return EXIT.IO;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// NonceManager keeps back-to-back sends (the --author-bound commit + reveal pair, possibly
|
|
418
|
+
// interleaved with externally mined blocks) from tripping ethers' briefly-cached nonce reads —
|
|
419
|
+
// the same wrapper the shipped commit-reveal test discipline uses.
|
|
420
|
+
const signer = new ethersLib.NonceManager(wallet.connect(provider));
|
|
421
|
+
const contractAddr = ethersLib.getAddress(opts.contract);
|
|
422
|
+
const contract = new ethersLib.Contract(contractAddr, ABI, signer);
|
|
423
|
+
const uri = opts.uri == null ? "" : String(opts.uri);
|
|
424
|
+
|
|
425
|
+
// ---- submit: one-shot anchor() by default; commit-reveal (D-1) with --author-bound ----
|
|
426
|
+
let txHash;
|
|
427
|
+
try {
|
|
428
|
+
if (opts.authorBound) {
|
|
429
|
+
const salt = newSalt(ethersLib); // fresh random 32-byte secret (public after reveal)
|
|
430
|
+
const committer = await signer.getAddress();
|
|
431
|
+
// The SHIPPED commitment construction, reused verbatim (sender-bound + salt-blinded).
|
|
432
|
+
const commitment = computeCommitment({ contentHash: d.digest, committer, salt, ethers: ethersLib });
|
|
433
|
+
if (!opts.json) {
|
|
434
|
+
write(`anchor-artifact: committing digest ${d.digest} (author-bound commit-reveal) as ${committer}...\n`);
|
|
435
|
+
}
|
|
436
|
+
const commitMined = await (await contract.commit(commitment)).wait();
|
|
437
|
+
const minDelay = BigInt(await contract.MIN_REVEAL_DELAY());
|
|
438
|
+
if (!opts.json) {
|
|
439
|
+
write(` commit tx: ${commitMined.hash} (block ${commitMined.blockNumber}); revealing after ${minDelay} block(s)...\n`);
|
|
440
|
+
}
|
|
441
|
+
await waitRevealWindow({
|
|
442
|
+
provider,
|
|
443
|
+
commitBlock: BigInt(commitMined.blockNumber),
|
|
444
|
+
minDelay,
|
|
445
|
+
waitForBlock: opts.waitForBlock,
|
|
446
|
+
});
|
|
447
|
+
const revealMined = await (await contract.reveal(d.digest, salt, uri)).wait();
|
|
448
|
+
txHash = revealMined.hash;
|
|
449
|
+
} else {
|
|
450
|
+
if (!opts.json) {
|
|
451
|
+
write(`anchor-artifact: anchoring digest ${d.digest} (one-shot; the record will NOT be author-bound)...\n`);
|
|
452
|
+
}
|
|
453
|
+
const mined = await (await contract.anchor(d.digest, uri)).wait();
|
|
454
|
+
txHash = mined.hash;
|
|
455
|
+
}
|
|
456
|
+
} catch (e) {
|
|
457
|
+
// The registry's OWN named revert (e.g. AlreadyAnchored) is a clean, named reject — never a
|
|
458
|
+
// stack trace. Anything else is a genuine runtime/network failure.
|
|
459
|
+
const named = namedRegistryReject(e, ethersLib);
|
|
460
|
+
if (named) {
|
|
461
|
+
return reject("registry-reject", `the registry rejected this write: ${named}`);
|
|
462
|
+
}
|
|
463
|
+
if (isGenuineRpcError(e)) {
|
|
464
|
+
writeErr(`error: RPC failure while anchoring: ${e.message}\n`);
|
|
465
|
+
return EXIT.IO;
|
|
466
|
+
}
|
|
467
|
+
writeErr(`error: ${e.message}\n`);
|
|
468
|
+
return EXIT.IO;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ---- read the record BACK from the chain (the D-1 semantics surfaced, not re-implemented):
|
|
472
|
+
// contributor / authorBound / blockNumber / block timestamp come from the registry itself ----
|
|
473
|
+
let rec;
|
|
474
|
+
try {
|
|
475
|
+
rec = await contract.getRecord(d.digest);
|
|
476
|
+
} catch (e) {
|
|
477
|
+
writeErr(`error: the anchor tx mined (${txHash}) but the record could not be read back: ${e.message}\n`);
|
|
478
|
+
return EXIT.IO;
|
|
479
|
+
}
|
|
480
|
+
const chain = {
|
|
481
|
+
authorBound: Boolean(rec.authorBound),
|
|
482
|
+
blockNumber: Number(rec.blockNumber),
|
|
483
|
+
blockTime: Number(rec.timestamp),
|
|
484
|
+
chainId: Number(chainId),
|
|
485
|
+
contract: contractAddr.toLowerCase(),
|
|
486
|
+
contributor: String(rec.contributor).toLowerCase(),
|
|
487
|
+
txHash: String(txHash).toLowerCase(),
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const built = binding.buildAnchoredReceipt({
|
|
491
|
+
digest: d.digest,
|
|
492
|
+
kind: d.kind,
|
|
493
|
+
how: d.how,
|
|
494
|
+
artifactLabel: path.basename(artifactPath),
|
|
495
|
+
chain,
|
|
496
|
+
});
|
|
497
|
+
if (!built.ok) {
|
|
498
|
+
writeErr(
|
|
499
|
+
`error: the anchor tx mined (${txHash}) but the anchored receipt could not be assembled ` +
|
|
500
|
+
`(${built.reason}): ${built.detail || ""}\n`
|
|
501
|
+
);
|
|
502
|
+
return EXIT.IO;
|
|
503
|
+
}
|
|
504
|
+
const receipt = built.receipt;
|
|
505
|
+
// The canonical byte serialization (the core's sorted-key container: stringify + newline IS it).
|
|
506
|
+
const receiptBytes = JSON.stringify(receipt) + "\n";
|
|
507
|
+
|
|
508
|
+
let outPath = null;
|
|
509
|
+
if (opts.out) {
|
|
510
|
+
outPath = path.resolve(opts.out); // explicit, caller-chosen path — never a silent cwd drop
|
|
511
|
+
try {
|
|
512
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
513
|
+
fs.writeFileSync(outPath, receiptBytes);
|
|
514
|
+
} catch (e) {
|
|
515
|
+
writeErr(`error: cannot write receipt ${opts.out}: ${e.message}\n`);
|
|
516
|
+
return EXIT.IO;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (opts.json) {
|
|
521
|
+
write(
|
|
522
|
+
JSON.stringify(
|
|
523
|
+
{
|
|
524
|
+
ok: true,
|
|
525
|
+
verdict: "ANCHORED",
|
|
526
|
+
artifact: opts.artifact,
|
|
527
|
+
digest: d.digest,
|
|
528
|
+
artifactKind: d.kind,
|
|
529
|
+
how: d.how,
|
|
530
|
+
chain,
|
|
531
|
+
receiptPath: outPath,
|
|
532
|
+
receipt,
|
|
533
|
+
},
|
|
534
|
+
null,
|
|
535
|
+
2
|
|
536
|
+
) + "\n"
|
|
537
|
+
);
|
|
538
|
+
} else {
|
|
539
|
+
write("anchor-artifact: ANCHORED\n");
|
|
540
|
+
write(` digest: ${d.digest}\n`);
|
|
541
|
+
write(` kind: ${d.kind}\n`);
|
|
542
|
+
write(` chainId: ${chain.chainId} contract: ${chain.contract}\n`);
|
|
543
|
+
write(` tx: ${chain.txHash} (block ${chain.blockNumber}, blockTime ${chain.blockTime})\n`);
|
|
544
|
+
write(` contributor: ${chain.contributor} authorBound: ${chain.authorBound}\n`);
|
|
545
|
+
if (outPath) {
|
|
546
|
+
write(` receipt written: ${outPath}\n`);
|
|
547
|
+
} else {
|
|
548
|
+
write(" receipt (NOT written to disk; pass --out <path> to save it):\n");
|
|
549
|
+
write(receiptBytes);
|
|
550
|
+
}
|
|
551
|
+
write(` NOTE: ${binding.ANCHOR_TRUST_NOTE}\n`);
|
|
552
|
+
}
|
|
553
|
+
return EXIT.OK;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ---------------------------------------------------------------------------------------------------
|
|
557
|
+
// vh verify-anchored
|
|
558
|
+
// ---------------------------------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Run `vh verify-anchored`. OFFLINE by default (pure T-70.1 binding verify); with BOTH an endpoint
|
|
562
|
+
* (--rpc, or an injected provider) AND --contract it additionally authenticates the registry (the
|
|
563
|
+
* EXISTING EPIC-11 identity probe) and re-checks the receipt's chain facts against the chain.
|
|
564
|
+
* Never signs; needs no key. Returns the process exit code (0 ACCEPTED / 3 REJECTED / 2 / 1).
|
|
565
|
+
*
|
|
566
|
+
* @param {object} opts { receipt, artifact, rpc?, contract?, json?, provider?, ethers? }
|
|
567
|
+
* @param {{write?:Function, writeErr?:Function}} [io]
|
|
568
|
+
* @returns {Promise<number>}
|
|
569
|
+
*/
|
|
570
|
+
async function runVerifyAnchored(opts, io = {}) {
|
|
571
|
+
const write = io.write || ((s) => process.stdout.write(s));
|
|
572
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
573
|
+
const ethersLib = opts.ethers || require("ethers");
|
|
574
|
+
|
|
575
|
+
const hasEndpoint = opts.provider !== undefined || opts.rpc !== undefined;
|
|
576
|
+
const hasContract = opts.contract !== undefined;
|
|
577
|
+
const mode = hasEndpoint || hasContract ? "rpc" : "offline";
|
|
578
|
+
|
|
579
|
+
const reject = (reason, detail, field) => {
|
|
580
|
+
if (opts.json) {
|
|
581
|
+
write(JSON.stringify({ ok: false, verdict: "REJECTED", mode, reason, field, detail }, null, 2) + "\n");
|
|
582
|
+
} else {
|
|
583
|
+
writeErr(`verify-anchored: REJECTED (${reason})${detail ? `: ${detail}` : ""}\n`);
|
|
584
|
+
}
|
|
585
|
+
return EXIT.REJECT;
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
if (!opts.receipt || !opts.artifact) {
|
|
589
|
+
writeErr("error: `vh verify-anchored` requires a <receipt> and a <sealed-file>\n" + VERIFY_ANCHORED_USAGE);
|
|
590
|
+
return EXIT.USAGE;
|
|
591
|
+
}
|
|
592
|
+
if (hasEndpoint !== hasContract) {
|
|
593
|
+
writeErr(
|
|
594
|
+
"error: the on-chain re-check needs BOTH --rpc <url> AND --contract <address> " +
|
|
595
|
+
"(omit both for the offline binding check)\n" +
|
|
596
|
+
VERIFY_ANCHORED_USAGE
|
|
597
|
+
);
|
|
598
|
+
return EXIT.USAGE;
|
|
599
|
+
}
|
|
600
|
+
if (hasContract && !ethersLib.isAddress(opts.contract)) {
|
|
601
|
+
writeErr(`error: invalid contract address: ${opts.contract}\n`);
|
|
602
|
+
return EXIT.USAGE;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const receipt = readJson("receipt", opts.receipt, writeErr);
|
|
606
|
+
if (receipt === null) return EXIT.IO;
|
|
607
|
+
const artifact = readJson("artifact", opts.artifact, writeErr);
|
|
608
|
+
if (artifact === null) return EXIT.IO;
|
|
609
|
+
|
|
610
|
+
// ---- leg 1 (always): the pure, offline binding verify — the T-70.1 core, reused verbatim ----
|
|
611
|
+
const v = binding.verifyAnchoredReceipt({ receipt, artifact });
|
|
612
|
+
if (!v.ok) return reject(v.reason, v.detail, v.field);
|
|
613
|
+
|
|
614
|
+
// ---- offline mode stops here: the binding holds; the chain facts remain the anchorer's CLAIM ----
|
|
615
|
+
if (!hasContract) {
|
|
616
|
+
if (opts.json) {
|
|
617
|
+
write(
|
|
618
|
+
JSON.stringify(
|
|
619
|
+
{
|
|
620
|
+
ok: true,
|
|
621
|
+
verdict: "ACCEPTED",
|
|
622
|
+
mode,
|
|
623
|
+
digest: v.digest,
|
|
624
|
+
artifactKind: receipt.artifactKind,
|
|
625
|
+
chain: v.chain,
|
|
626
|
+
registry: null,
|
|
627
|
+
note:
|
|
628
|
+
"OFFLINE verify: the receipt binds this exact artifact, but its chain facts were NOT " +
|
|
629
|
+
"re-checked. Pass --rpc <url> --contract <addr> to confirm them against the chain.",
|
|
630
|
+
},
|
|
631
|
+
null,
|
|
632
|
+
2
|
|
633
|
+
) + "\n"
|
|
634
|
+
);
|
|
635
|
+
} else {
|
|
636
|
+
write("verify-anchored: ACCEPTED (offline binding check)\n");
|
|
637
|
+
write(` digest: ${v.digest}\n`);
|
|
638
|
+
write(` kind: ${receipt.artifactKind}\n`);
|
|
639
|
+
write(
|
|
640
|
+
` chain CLAIM: chainId ${v.chain.chainId}, contract ${v.chain.contract}, tx ${v.chain.txHash}, ` +
|
|
641
|
+
`block ${v.chain.blockNumber}, blockTime ${v.chain.blockTime}, contributor ${v.chain.contributor}, ` +
|
|
642
|
+
`authorBound ${v.chain.authorBound}\n`
|
|
643
|
+
);
|
|
644
|
+
write(
|
|
645
|
+
" NOTE: offline mode did NOT re-check the chain facts — they are the anchorer's claim. " +
|
|
646
|
+
"Pass --rpc <url> --contract <addr> to confirm them against the chain.\n"
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return EXIT.OK;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ---- leg 2 (--rpc --contract): authenticate the registry FIRST (the EXISTING EPIC-11 identity
|
|
653
|
+
// probe — no record is believed until the contract self-identifies on the receipt's chain),
|
|
654
|
+
// then re-check every chain fact the receipt claims. ----
|
|
655
|
+
const provider = opts.provider || new ethersLib.JsonRpcProvider(opts.rpc);
|
|
656
|
+
let auth;
|
|
657
|
+
try {
|
|
658
|
+
auth = await assertRegistry({
|
|
659
|
+
provider,
|
|
660
|
+
contractAddress: opts.contract,
|
|
661
|
+
expectedChainId: v.chain.chainId,
|
|
662
|
+
ethers: ethersLib,
|
|
663
|
+
});
|
|
664
|
+
} catch (e) {
|
|
665
|
+
if (e && e.code === "REGISTRY_AUTH_FAILED") {
|
|
666
|
+
// The EXISTING identity-probe reject, surfaced verbatim (wrong address / non-registry / wrong chain).
|
|
667
|
+
return reject("registry-auth-failed", e.message);
|
|
668
|
+
}
|
|
669
|
+
if (isGenuineRpcError(e)) {
|
|
670
|
+
writeErr(`error: RPC failure during the registry identity check: ${e.message}\n`);
|
|
671
|
+
return EXIT.IO;
|
|
672
|
+
}
|
|
673
|
+
writeErr(`error: ${e.message}\n`);
|
|
674
|
+
return EXIT.IO;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const contractLc = ethersLib.getAddress(opts.contract).toLowerCase();
|
|
678
|
+
if (contractLc !== v.chain.contract) {
|
|
679
|
+
return reject(
|
|
680
|
+
"contract-mismatch",
|
|
681
|
+
`the receipt was anchored on contract ${v.chain.contract} but you passed --contract ${contractLc} — ` +
|
|
682
|
+
"a record on a different contract says nothing about this receipt"
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const contract = new ethersLib.Contract(ethersLib.getAddress(opts.contract), ABI, provider);
|
|
687
|
+
let rec;
|
|
688
|
+
try {
|
|
689
|
+
rec = await contract.getRecord(v.digest);
|
|
690
|
+
} catch (e) {
|
|
691
|
+
const named = namedRegistryReject(e, ethersLib);
|
|
692
|
+
if (named && named.startsWith("NotAnchored")) {
|
|
693
|
+
return reject(
|
|
694
|
+
"not-anchored-on-chain",
|
|
695
|
+
`the registry has NO record for digest ${v.digest} (${named}) — the receipt's chain facts are not real`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
if (isGenuineRpcError(e)) {
|
|
699
|
+
writeErr(`error: RPC failure while reading the record back: ${e.message}\n`);
|
|
700
|
+
return EXIT.IO;
|
|
701
|
+
}
|
|
702
|
+
writeErr(`error: ${e.message}\n`);
|
|
703
|
+
return EXIT.IO;
|
|
704
|
+
}
|
|
705
|
+
const onchain = {
|
|
706
|
+
contributor: String(rec.contributor).toLowerCase(),
|
|
707
|
+
authorBound: Boolean(rec.authorBound),
|
|
708
|
+
blockNumber: Number(rec.blockNumber),
|
|
709
|
+
blockTime: Number(rec.timestamp),
|
|
710
|
+
};
|
|
711
|
+
if (onchain.contributor !== v.chain.contributor) {
|
|
712
|
+
return reject(
|
|
713
|
+
"contributor-mismatch",
|
|
714
|
+
`on-chain contributor ${onchain.contributor} != receipt contributor ${v.chain.contributor}`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
if (onchain.authorBound !== v.chain.authorBound) {
|
|
718
|
+
return reject(
|
|
719
|
+
"author-bound-mismatch",
|
|
720
|
+
`on-chain authorBound ${onchain.authorBound} != receipt authorBound ${v.chain.authorBound}`
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
if (onchain.blockNumber !== v.chain.blockNumber) {
|
|
724
|
+
return reject(
|
|
725
|
+
"block-number-mismatch",
|
|
726
|
+
`on-chain record block ${onchain.blockNumber} != receipt blockNumber ${v.chain.blockNumber}`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
if (onchain.blockTime !== v.chain.blockTime) {
|
|
730
|
+
return reject(
|
|
731
|
+
"block-time-mismatch",
|
|
732
|
+
`on-chain record timestamp ${onchain.blockTime} != receipt blockTime ${v.chain.blockTime}`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// The receipt's txHash must be a REAL mined transaction, in the recorded block, targeting the
|
|
737
|
+
// recorded contract — an edited txHash cannot masquerade as the anchoring write.
|
|
738
|
+
let txr;
|
|
739
|
+
try {
|
|
740
|
+
txr = await provider.getTransactionReceipt(v.chain.txHash);
|
|
741
|
+
} catch (e) {
|
|
742
|
+
if (isGenuineRpcError(e)) {
|
|
743
|
+
writeErr(`error: RPC failure while re-checking the anchoring tx: ${e.message}\n`);
|
|
744
|
+
return EXIT.IO;
|
|
745
|
+
}
|
|
746
|
+
writeErr(`error: ${e.message}\n`);
|
|
747
|
+
return EXIT.IO;
|
|
748
|
+
}
|
|
749
|
+
if (!txr) {
|
|
750
|
+
return reject(
|
|
751
|
+
"tx-not-found",
|
|
752
|
+
`no transaction ${v.chain.txHash} exists on this chain — the receipt's txHash is not real`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
if (Number(txr.blockNumber) !== v.chain.blockNumber) {
|
|
756
|
+
return reject(
|
|
757
|
+
"tx-block-mismatch",
|
|
758
|
+
`tx ${v.chain.txHash} mined in block ${Number(txr.blockNumber)}, not the receipt's block ${v.chain.blockNumber}`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
if (txr.to && String(txr.to).toLowerCase() !== v.chain.contract) {
|
|
762
|
+
return reject(
|
|
763
|
+
"tx-target-mismatch",
|
|
764
|
+
`tx ${v.chain.txHash} targets ${String(txr.to).toLowerCase()}, not the receipt's contract ${v.chain.contract}`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (opts.json) {
|
|
769
|
+
write(
|
|
770
|
+
JSON.stringify(
|
|
771
|
+
{
|
|
772
|
+
ok: true,
|
|
773
|
+
verdict: "ACCEPTED",
|
|
774
|
+
mode,
|
|
775
|
+
digest: v.digest,
|
|
776
|
+
artifactKind: receipt.artifactKind,
|
|
777
|
+
chain: v.chain,
|
|
778
|
+
registry: jsonRegistryBlock(auth),
|
|
779
|
+
onchain,
|
|
780
|
+
note:
|
|
781
|
+
"The registry was authenticated (EPIC-11 identity probe) and every chain fact in the " +
|
|
782
|
+
"receipt matches the on-chain record and its mined transaction.",
|
|
783
|
+
},
|
|
784
|
+
null,
|
|
785
|
+
2
|
|
786
|
+
) + "\n"
|
|
787
|
+
);
|
|
788
|
+
} else {
|
|
789
|
+
write("verify-anchored: ACCEPTED (offline binding + on-chain re-check)\n");
|
|
790
|
+
write(formatRegistryLine(auth) + "\n");
|
|
791
|
+
write(` digest: ${v.digest}\n`);
|
|
792
|
+
write(` kind: ${receipt.artifactKind}\n`);
|
|
793
|
+
write(
|
|
794
|
+
` on-chain: contributor ${onchain.contributor}, authorBound ${onchain.authorBound}, ` +
|
|
795
|
+
`block ${onchain.blockNumber}, blockTime ${onchain.blockTime} — ALL match the receipt\n`
|
|
796
|
+
);
|
|
797
|
+
write(` tx: ${v.chain.txHash} found in block ${Number(txr.blockNumber)}, targeting the recorded contract\n`);
|
|
798
|
+
}
|
|
799
|
+
return EXIT.OK;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ---------------------------------------------------------------------------------------------------
|
|
803
|
+
// cmd wrappers (argv -> opts -> run). io is optional and defaults to the process streams.
|
|
804
|
+
// ---------------------------------------------------------------------------------------------------
|
|
805
|
+
|
|
806
|
+
async function cmdAnchorArtifact(argv, io = {}) {
|
|
807
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
808
|
+
let opts;
|
|
809
|
+
try {
|
|
810
|
+
opts = parseAnchorArtifactArgs(argv);
|
|
811
|
+
} catch (e) {
|
|
812
|
+
writeErr(`error: ${e.message}\n` + ANCHOR_ARTIFACT_USAGE);
|
|
813
|
+
return EXIT.USAGE;
|
|
814
|
+
}
|
|
815
|
+
return runAnchorArtifact(
|
|
816
|
+
{
|
|
817
|
+
...opts,
|
|
818
|
+
// The same env fallbacks the other write verbs honor — for the ADDRESS and ENDPOINT only.
|
|
819
|
+
// The signing key NEVER has an implicit env fallback: only --key-env/--key-file name it.
|
|
820
|
+
contract: opts.contract || process.env.VH_CONTRACT,
|
|
821
|
+
rpc: opts.rpc || process.env.VH_RPC_URL || process.env.AMOY_RPC_URL,
|
|
822
|
+
},
|
|
823
|
+
io
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async function cmdVerifyAnchored(argv, io = {}) {
|
|
828
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
829
|
+
let opts;
|
|
830
|
+
try {
|
|
831
|
+
opts = parseVerifyAnchoredArgs(argv);
|
|
832
|
+
} catch (e) {
|
|
833
|
+
writeErr(`error: ${e.message}\n` + VERIFY_ANCHORED_USAGE);
|
|
834
|
+
return EXIT.USAGE;
|
|
835
|
+
}
|
|
836
|
+
// No env fallbacks here on purpose: verify-anchored is OFFLINE unless the caller EXPLICITLY passes
|
|
837
|
+
// both --rpc and --contract (an env var must never silently flip a verify onto a network).
|
|
838
|
+
return runVerifyAnchored(opts, io);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
module.exports = {
|
|
842
|
+
EXIT,
|
|
843
|
+
ANCHOR_ARTIFACT_USAGE,
|
|
844
|
+
VERIFY_ANCHORED_USAGE,
|
|
845
|
+
parseAnchorArtifactArgs,
|
|
846
|
+
parseVerifyAnchoredArgs,
|
|
847
|
+
runAnchorArtifact,
|
|
848
|
+
runVerifyAnchored,
|
|
849
|
+
cmdAnchorArtifact,
|
|
850
|
+
cmdVerifyAnchored,
|
|
851
|
+
namedRegistryReject,
|
|
852
|
+
ABI,
|
|
853
|
+
};
|