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/docs/RECEIPTS.md
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# verifyhash receipts — schema, lifecycle, and diff semantics
|
|
2
|
+
|
|
3
|
+
This is the canonical spec for the on-disk **receipt** artifacts that `cli/receipt.js` reads and
|
|
4
|
+
writes (tasks **T-6.1** and **T-6.2**). A receipt is a versioned, strictly-validated JSON file that
|
|
5
|
+
makes two CLI flows durable and operable:
|
|
6
|
+
|
|
7
|
+
- a **claim receipt** (`kind: "verifyhash.claim-receipt"`) persists everything `reveal()` needs so a
|
|
8
|
+
crashed/interrupted commit-reveal claim can be **resumed** from a fresh process (T-6.1);
|
|
9
|
+
- an **anchor receipt** (`kind: "verifyhash.anchor-receipt"`) records the per-file **manifest** of a
|
|
10
|
+
directory so a later `vh verify <dir> --receipt <p>` can **localize** which file diverged (T-6.2).
|
|
11
|
+
|
|
12
|
+
> **Trust posture (read this first).** A receipt is an **UNTRUSTED local convenience**, exactly as
|
|
13
|
+
> stated in [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md). The authoritative result always comes
|
|
14
|
+
> from the on-chain record — for `verify`, from re-deriving the Merkle root and comparing it to that
|
|
15
|
+
> record. A receipt's `manifest` only **localizes** which file diverged; it can never, by itself, make
|
|
16
|
+
> content "verified". Rule of thumb: **the on-chain root decides MATCH/MISMATCH; the receipt manifest
|
|
17
|
+
> only points at the file.** The one field that is operationally load-bearing is the claim receipt's
|
|
18
|
+
> secret `salt` — see [Trust vs hints](#trust-vs-hints).
|
|
19
|
+
|
|
20
|
+
The receipt is **never** consumed by the contract and is **never** uploaded anywhere. **A claim receipt
|
|
21
|
+
holds the SECRET `salt`**, so where it is written is always something you opt into — `vh` never silently
|
|
22
|
+
drops it into your repo unless you ask:
|
|
23
|
+
|
|
24
|
+
- the durable **`vh commit`** writes a claim receipt to `--receipt <path>` (exact file) or
|
|
25
|
+
`--receipt-dir <dir>` (into that folder under a tidy default file name), or — with neither — defaults
|
|
26
|
+
to `<cwd>/<contentHashPrefix>.vhclaim.json` **and prints the EXACT absolute path it wrote**
|
|
27
|
+
(`receipt written: <abs path>`), so you can always see, move, or delete it. It is never written
|
|
28
|
+
somewhere you can't find;
|
|
29
|
+
- the one-shot **`vh claim`** persists a receipt **only if you pass `--receipt`/`--receipt-dir`**; with
|
|
30
|
+
neither it writes **nothing** (the in-memory receipt is just returned to the caller). Use `vh commit`
|
|
31
|
+
for a durable, resumable claim;
|
|
32
|
+
- the pure helper `defaultReceiptPath(contentHash)` only computes a **relative** file name
|
|
33
|
+
(`./<prefix>.vhclaim.json`); the caller is responsible for resolving it against a safe base.
|
|
34
|
+
|
|
35
|
+
Receipt files are also git-ignored (`*.vhclaim.json`). **Keep a claim receipt private until you reveal:**
|
|
36
|
+
anyone holding the `salt` before reveal could front-run the open (after a successful reveal the
|
|
37
|
+
commitment is single-use and spent, so the receipt is no longer sensitive). This reuses the trust posture
|
|
38
|
+
in [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md): the receipt is an **untrusted local convenience**;
|
|
39
|
+
the authoritative result always comes from the on-chain record.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Schema
|
|
44
|
+
|
|
45
|
+
Two `kind`s share a common header. `readReceipt` validates **strictly** and throws on ANY deviation
|
|
46
|
+
rather than filling defaults: a partial claim receipt could make you re-derive a wrong commitment or
|
|
47
|
+
reveal with the wrong salt and waste (or burn) a transaction, so a corrupt receipt is rejected outright,
|
|
48
|
+
never silently half-accepted.
|
|
49
|
+
|
|
50
|
+
### Common header (every receipt)
|
|
51
|
+
|
|
52
|
+
| Field | Type | Required | Trust | Meaning |
|
|
53
|
+
|-------|------|----------|-------|---------|
|
|
54
|
+
| `kind` | string | yes | structural | `"verifyhash.claim-receipt"` or `"verifyhash.anchor-receipt"`. A discriminator so a random JSON file is never mistaken for a receipt. |
|
|
55
|
+
| `schemaVersion` | integer | yes | structural | On-disk schema version. This build **writes** `4` and **reads** `1`, `2`, `3`, or `4`. Any other version is rejected, so a future/foreign file is never misread. (`1` → base, `2` added the optional `manifest`, `3` added the optional `git` block, `4` added the optional `parent` on a CLAIM receipt — all additive.) |
|
|
56
|
+
| `contentHash` | `0x`+64 hex (32 bytes) | yes | **trusted-as-target** | The digest being claimed/anchored: a file's `keccak256`, or a directory's Merkle **root** (see [`docs/MERKLE-LEAVES.md`](MERKLE-LEAVES.md)). This is the only thing the chain attests to; everything else is metadata. |
|
|
57
|
+
| `contractAddress` | `0x`+40 hex (address) | yes | hint | The `ContributionRegistry` the receipt is about. Used to target the right contract on resume. |
|
|
58
|
+
| `chainId` | non-negative integer | yes | hint | Chain the commit/anchor was sent to (e.g. `31337` local, `80002` Amoy). |
|
|
59
|
+
| `uri` | string | yes (may be `""`) | **UNTRUSTED hint** | Off-chain pointer (IPFS CID, commit URL, …). The contract **never fetches, validates, or hashes it**; consumers must re-fetch + re-hash and compare to `contentHash`. Defaulted to `""`, never `undefined`. |
|
|
60
|
+
| `path` | string | optional | informational | The source path that was hashed. For humans only. |
|
|
61
|
+
| `targetKind` | `"file"` \| `"dir"` | optional | informational | Whether the target was a single file or a directory. |
|
|
62
|
+
| `manifest` | array | optional (v2+) | **UNTRUSTED hint** | Per-file breakdown of a directory target; see [Manifest](#the-manifest-directory-targets). A v1 receipt that carries a manifest is rejected (the version must not lie). |
|
|
63
|
+
| `git` | object | optional (v3+) | **UNTRUSTED hint** | Git provenance `{ commit, scope }` recorded by a `--git` anchor/claim; see [Git provenance](#the-git-provenance-block-git-scoped-targets). A v1/v2 receipt that carries a git block is rejected (the version must not lie). |
|
|
64
|
+
|
|
65
|
+
### Claim receipt — additional fields (`kind: "verifyhash.claim-receipt"`)
|
|
66
|
+
|
|
67
|
+
A claim receipt carries the **secret material** that lets a separate process finish a commit-reveal claim.
|
|
68
|
+
|
|
69
|
+
| Field | Type | Required | Trust | Meaning |
|
|
70
|
+
|-------|------|----------|-------|---------|
|
|
71
|
+
| `salt` | `0x`+64 hex (32 bytes) | yes | **SECRET — keep private** | The blinding salt bound into the commitment. `reveal()` needs this exact value; lose it and the claim is **unrevealable by anyone**. |
|
|
72
|
+
| `commitment` | `0x`+64 hex (32 bytes) | yes | trusted-as-derived | `keccak256(abi.encode(contentHash, committer, salt))` — the blinded value that went on-chain in `commit()`. |
|
|
73
|
+
| `committer` | `0x`+40 hex (address) | yes | trusted-as-target | The address that committed and is the only one that can reveal (it is hashed into the commitment). |
|
|
74
|
+
| `commitTxHash` | `0x`+64 hex (32 bytes) | optional | informational | The `commit()` transaction hash. |
|
|
75
|
+
| `commitBlockNumber` | non-negative integer | optional | operational | Block the commit mined in; used to compute when the reveal window matures. |
|
|
76
|
+
| `minRevealDelay` | non-negative integer | optional | operational | `MIN_REVEAL_DELAY` read from the contract at commit time; how many blocks must pass before `reveal()`. |
|
|
77
|
+
| `parent` | `0x`+64 hex (32 bytes) | optional (v4+) | **UNTRUSTED hint** | The lineage edge (B-10.1): an **already-anchored** predecessor's `contentHash`, recorded by `vh commit --parent`. Present only for a revision; **omitted entirely** for a lineage root (the all-zero hash is rejected, never recorded). On resume, `vh reveal` routes to `revealWithParent(contentHash, salt, uri, parent)` and records the edge; the **authoritative** edge is what that on-chain call records, not this field. It is a *claim* of a predecessor — never proof of content ancestry or any transfer of the parent's authorship. **Back-compat is total:** this field is purely additive, so a v1/v2/v3 receipt simply has no `parent` and is read **unchanged** (and reveals via the legacy `reveal`, as a lineage root). Rejected on an anchor receipt, on a receipt below v4, when malformed/zero, or when equal to `contentHash` (`SelfParent`). |
|
|
78
|
+
|
|
79
|
+
### Anchor receipt — additional fields (`kind: "verifyhash.anchor-receipt"`)
|
|
80
|
+
|
|
81
|
+
An anchor receipt has **no secret material at all** (anchoring needs none). Its only reason to exist
|
|
82
|
+
beyond the header is the optional directory `manifest`.
|
|
83
|
+
|
|
84
|
+
| Field | Type | Required | Trust | Meaning |
|
|
85
|
+
|-------|------|----------|-------|---------|
|
|
86
|
+
| `anchorTxHash` | `0x`+64 hex (32 bytes) | optional | informational | The `anchor()` transaction hash, when one was sent. |
|
|
87
|
+
| `anchorBlockNumber` | non-negative integer | optional | informational | Block the anchor mined in. |
|
|
88
|
+
|
|
89
|
+
> An anchor receipt deliberately has **no `salt`, `commitment`, or `committer`** — there is no secret to
|
|
90
|
+
> protect and no signer needed to verify a hash you already know. `readReceipt` rejects an anchor receipt
|
|
91
|
+
> that smuggles those in, and a claim receipt missing any of them.
|
|
92
|
+
|
|
93
|
+
### The manifest (directory targets)
|
|
94
|
+
|
|
95
|
+
For a directory target the receipt may carry a `manifest` (schemaVersion ≥ 2): the **sorted list of
|
|
96
|
+
every file's `{ path, contentHash, leaf }`** — exactly what `vh hash <dir>` / `hashDir()` computes and
|
|
97
|
+
then would otherwise discard. Each entry:
|
|
98
|
+
|
|
99
|
+
| Field | Type | Meaning |
|
|
100
|
+
|-------|------|---------|
|
|
101
|
+
| `path` | non-empty string | the file's POSIX relative path inside the directory |
|
|
102
|
+
| `contentHash` | `0x`+64 hex | `keccak256` of the file's bytes (the bare content digest `c` in [`docs/MERKLE-LEAVES.md`](MERKLE-LEAVES.md)) |
|
|
103
|
+
| `leaf` | `0x`+64 hex | the **path-bound** leaf `keccak256(DIR_LEAF_DOMAIN ‖ relPath ‖ 0x00 ‖ c)`, which is what the tree is actually built from |
|
|
104
|
+
|
|
105
|
+
The manifest is stored **sorted ascending by `leaf` value** (the same total order `hashDir` uses to
|
|
106
|
+
build the tree), so a written manifest is deterministic regardless of input enumeration order. Because
|
|
107
|
+
the `leaf` binds the path, two files at different paths can never collide, and a leaf change with the
|
|
108
|
+
same path is unambiguously a **content** change.
|
|
109
|
+
|
|
110
|
+
### The git provenance block (`--git`-scoped targets)
|
|
111
|
+
|
|
112
|
+
When a directory is anchored/claimed with **`--git`** (T-8.2), the root and `manifest` are computed over
|
|
113
|
+
**exactly the files git tracks** at a commit — the same reproducible, untracked-junk-ignoring enumeration
|
|
114
|
+
as `vh hash <dir> --git` (see [`docs/MERKLE-LEAVES.md`](MERKLE-LEAVES.md)). The receipt then records a
|
|
115
|
+
`git` block (schemaVersion ≥ 3):
|
|
116
|
+
|
|
117
|
+
| Field | Type | Meaning |
|
|
118
|
+
|-------|------|---------|
|
|
119
|
+
| `commit` | 40-hex (bare, no `0x`) | the resolved commit object id the tracked set was enumerated from (paste straight into `git show <oid>`) |
|
|
120
|
+
| `scope` | non-empty string | the repo-relative POSIX path the operator pointed `vh` at (`"."` for the repo root) — *how* the tracked set was scoped |
|
|
121
|
+
|
|
122
|
+
The `git` block is an **UNTRUSTED convenience hint**, consistent with
|
|
123
|
+
[`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md): it records *how* a root was produced so a reader can
|
|
124
|
+
reproduce the `--git --ref <oid>` enumeration, but the authoritative verdict is still the **recomputed
|
|
125
|
+
root vs the on-chain record**. A receipt's `git.commit` is never re-checked against the chain — the chain
|
|
126
|
+
attests only to `contentHash`. `vh verify <dir> --git [--ref <ref>]` re-derives the root over the tracked
|
|
127
|
+
set at that ref and reports MATCH/MISMATCH; with `--receipt` it localizes ADDED/REMOVED/CHANGED over the
|
|
128
|
+
tracked set. Untracked junk in the work tree never affects the verdict.
|
|
129
|
+
|
|
130
|
+
### Trust vs hints
|
|
131
|
+
|
|
132
|
+
Summarizing the columns above, in the same spirit as the [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md)
|
|
133
|
+
one-liner table:
|
|
134
|
+
|
|
135
|
+
| Field(s) | Trust it for | Do NOT trust it for |
|
|
136
|
+
|----------|--------------|---------------------|
|
|
137
|
+
| `contentHash` | the exact digest/root the on-chain record is keyed by | being "valid" without the on-chain lookup + (for dirs) a recomputed root |
|
|
138
|
+
| `salt` (claim only) | finishing **your** reveal — **keep it secret until revealed** | sharing; anyone with it before reveal could front-run the open (and after a successful reveal it is spent and harmless) |
|
|
139
|
+
| `commitment`, `committer` | knowing who can reveal and what was committed | proving authorship by themselves — that comes from the on-chain `Record.authorBound` |
|
|
140
|
+
| `uri` | a human hint of where the content might be | anything security-relevant — re-fetch + re-hash |
|
|
141
|
+
| `manifest` | **localizing** which file diverged (ADDED/REMOVED/CHANGED) | deciding MATCH/MISMATCH — the recomputed root vs the on-chain record decides that |
|
|
142
|
+
| `git` (`{ commit, scope }`) | reproducing *how* a `--git` root was enumerated (which commit/scope) | proving the root — it is never re-checked against the chain; the chain attests only to `contentHash` |
|
|
143
|
+
| `*TxHash`, `*BlockNumber`, `path`, `targetKind` | operational convenience (resume timing, display) | any security claim |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Commit → reveal resume lifecycle (claim receipts, T-6.1)
|
|
148
|
+
|
|
149
|
+
The front-running-resistant claim (`vh claim`) is a **two-transaction** commit-reveal flow separated by
|
|
150
|
+
a maturation window of `MIN_REVEAL_DELAY` blocks. The commitment is
|
|
151
|
+
`keccak256(abi.encode(contentHash, committer, salt))`; only that opaque hash goes on-chain first, so a
|
|
152
|
+
mempool watcher cannot copy your content hash. After the window the committer reveals `(contentHash,
|
|
153
|
+
salt)`; an attacker who replays the revealed values as themselves recomputes a **different** commitment
|
|
154
|
+
they never registered, so their reveal reverts with `NoSuchCommitment` and `contributor` stays the
|
|
155
|
+
original committer (full threat model: [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md)).
|
|
156
|
+
|
|
157
|
+
**The durability problem (why the receipt exists).** On a live testnet the maturation window is minutes.
|
|
158
|
+
The single-process `vh claim` holds the secret `salt` only in memory while it waits. If that process
|
|
159
|
+
crashes or is interrupted between the two legs, the salt is lost — and since `reveal()` needs that exact
|
|
160
|
+
salt, the `contentHash` becomes **committed-but-unrevealable by anyone**, permanently burning the
|
|
161
|
+
attribution. The receipt fixes this by persisting the salt (and everything `reveal()` needs) to disk
|
|
162
|
+
**before** the commit step returns, so a separate process can finish later.
|
|
163
|
+
|
|
164
|
+
### The split: `vh commit` then `vh reveal`
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
vh commit ./src --uri ipfs://cid # sends commit(), writes the receipt, PRINTS its exact path, exits
|
|
168
|
+
# ...wait out MIN_REVEAL_DELAY (a few blocks)...
|
|
169
|
+
vh reveal --receipt <that exact path> # resumes from the receipt and reveals
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
1. **`vh commit <path>`** (`runCommit`) hashes the target, derives `(salt, commitment)`, sends
|
|
173
|
+
`commit(commitment)`, reads `MIN_REVEAL_DELAY`, then **writes the claim receipt before it returns**.
|
|
174
|
+
For a directory it also records the `manifest`. The receipt destination is **always opted into**:
|
|
175
|
+
`--receipt <path>` (exact file), `--receipt-dir <dir>` (default name inside that folder), or — with
|
|
176
|
+
neither — `<cwd>/<contentHashPrefix>.vhclaim.json`; in every case the success line names the **exact
|
|
177
|
+
absolute path written** (`receipt written: <abs path>`), so the secret-bearing file is never dropped
|
|
178
|
+
silently. From this point on, a crash is survivable: the salt is durable.
|
|
179
|
+
2. **Wait** out `MIN_REVEAL_DELAY` blocks. The commit-block height and the delay are in the receipt, so
|
|
180
|
+
any process can compute when the window matures.
|
|
181
|
+
3. **`vh reveal --receipt <p>`** (`runReveal`) `readReceipt`s the file (strict — a corrupt receipt throws
|
|
182
|
+
here rather than producing a wrong reveal), checks the signer **is** the receipt's `committer` (else
|
|
183
|
+
the reveal would hit `NoSuchCommitment`; it fails fast with a clear message instead), waits out the
|
|
184
|
+
window, then sends `reveal(contentHash, salt, uri)` — **or, if the receipt carries a `parent` (v4),
|
|
185
|
+
`revealWithParent(contentHash, salt, uri, parent)`** so the resumed reveal records the lineage edge
|
|
186
|
+
(B-10.1). This needs **no** information that was not durably written at commit time, so it works from a
|
|
187
|
+
completely fresh process — even after a reboot.
|
|
188
|
+
|
|
189
|
+
**Resumable lineage edge (`vh commit --parent`, schema v4).** `vh commit --parent <hash>` validates the
|
|
190
|
+
parent's *shape* locally and persists it into the claim receipt (the additive v4 `parent` field above);
|
|
191
|
+
`commit()` itself never carries a parent (it encodes only the opaque commitment). The edge is therefore
|
|
192
|
+
checked **on-chain at reveal time, not at commit time**: a separate `vh reveal --receipt <p>` reads
|
|
193
|
+
`parent` back and routes to `revealWithParent`. If the parent is stale (never anchored, or anchored only
|
|
194
|
+
after the commit) the **reveal** reverts `UnknownParent` while the **commit stays valid** — so the receipt
|
|
195
|
+
is left untouched and reusable: anchor the missing parent and re-run the same `vh reveal --receipt <p>`,
|
|
196
|
+
with no lost salt. A receipt with **no** `parent` reveals exactly as before (legacy `reveal`, a lineage
|
|
197
|
+
root). See [`docs/LINEAGE.md`](LINEAGE.md) for the full write/read flow.
|
|
198
|
+
|
|
199
|
+
**Retry semantics.** If you reveal before the window matures the contract reverts with `RevealTooSoon`;
|
|
200
|
+
`runReveal` lets that propagate and **leaves the receipt file untouched**, so you simply retry later. The
|
|
201
|
+
receipt is also unaffected by an unrelated crash, so resume is idempotent up to the single successful
|
|
202
|
+
reveal.
|
|
203
|
+
|
|
204
|
+
**`vh claim` is still the one-shot convenience** (commit + reveal in one process). To keep it safe by
|
|
205
|
+
default it persists a receipt **only if you ask** — pass `--receipt <path>` or `--receipt-dir <dir>` and
|
|
206
|
+
it writes the secret-bearing receipt to that exact, named location (so even the one-shot path is then
|
|
207
|
+
crash-recoverable: resume with `vh reveal --receipt <p>`). With **neither**, `vh claim` writes
|
|
208
|
+
**nothing** to disk (the in-memory `runClaim` helper just returns the receipt object); use `vh commit`
|
|
209
|
+
for a durable, resumable claim rather than relying on a silent cwd drop.
|
|
210
|
+
|
|
211
|
+
> **Keep the receipt private until you reveal:** it contains the secret `salt`. After a successful reveal
|
|
212
|
+
> the commitment is single-use and spent, so the receipt is no longer sensitive.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Directory-manifest diff semantics (anchor receipts, T-6.2)
|
|
217
|
+
|
|
218
|
+
A one-shot `vh anchor <dir>` records only the Merkle **root** on-chain. So plain `vh verify <dir>` can
|
|
219
|
+
only ever say "the whole tree's root matches / does not match" — it cannot say WHICH file diverged.
|
|
220
|
+
`vh anchor <dir> --receipt <p>` records the directory's `manifest` (every `{ path, contentHash, leaf }`),
|
|
221
|
+
and `vh verify <dir> --receipt <p>` then prints a precise per-file diff.
|
|
222
|
+
|
|
223
|
+
### How the diff is computed (`diffManifest`)
|
|
224
|
+
|
|
225
|
+
`diffManifest(recordedManifest, currentLeaves)` is a **pure** localizer. It keys both sides by `path` and
|
|
226
|
+
compares the path-bound `leaf`:
|
|
227
|
+
|
|
228
|
+
- **ADDED** — a path present in the current tree but not in the receipt's manifest.
|
|
229
|
+
- **REMOVED** — a path in the receipt's manifest, gone from the current tree.
|
|
230
|
+
- **CHANGED** — same `path`, different `leaf`. Because the path is bound into the leaf, an identical key
|
|
231
|
+
with a different leaf is unambiguously a **content** change; the diff reports `oldContentHash` → `newContentHash`.
|
|
232
|
+
- **unchanged** — same `path`, same `leaf`.
|
|
233
|
+
- `identical: true` iff there are zero added, removed, or changed entries.
|
|
234
|
+
|
|
235
|
+
### What decides the verdict (and what does not)
|
|
236
|
+
|
|
237
|
+
The diff **does not** decide MATCH/MISMATCH. The authoritative verdict is the same re-derive-and-compare
|
|
238
|
+
check the trust model requires: `vh verify` recomputes the directory's Merkle **root** from the files on
|
|
239
|
+
disk and compares that root to the on-chain record. **MATCH/MISMATCH comes only from that comparison.**
|
|
240
|
+
The manifest never participates in the verdict.
|
|
241
|
+
|
|
242
|
+
A malicious or stale receipt can at worst mislabel which file moved, and even that is caught: `verify`
|
|
243
|
+
flags a receipt whose recorded root does not match the recomputed root (`receiptHashMismatch`) and reports
|
|
244
|
+
it as a **different directory snapshot** rather than silently pretending the files line up. The verify
|
|
245
|
+
output leads with the caveat:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
--- receipt manifest diff (UNTRUSTED hint) ---
|
|
249
|
+
NOTE: the receipt is an untrusted convenience. The authoritative verdict is the
|
|
250
|
+
MATCH/MISMATCH above (recomputed root vs the on-chain record). This diff only localizes
|
|
251
|
+
WHICH file diverged; it cannot make content valid or invalid on its own.
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
If you pass `--receipt` for a **file** target (not a directory), the manifest diff is simply ignored with
|
|
255
|
+
a note — there are no per-file leaves to localize.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Worked example
|
|
260
|
+
|
|
261
|
+
### A. Resumable claim (claim receipt)
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
$ vh commit ./src --uri ipfs://bafy... # step 1
|
|
265
|
+
commit: committing ./src (file) as 0x7099...79C8...
|
|
266
|
+
commit tx: 0x...
|
|
267
|
+
receipt written: /work/0c271a48a26d075d.vhclaim.json
|
|
268
|
+
KEEP THIS PRIVATE — it holds the secret salt. Resume with: vh reveal --receipt /work/0c271a48a26d075d.vhclaim.json
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The `receipt written:` line names the **exact absolute path** so you can see, relocate, or delete the
|
|
272
|
+
secret-bearing file. Choose where it goes with `--receipt <path>` or `--receipt-dir <dir>`.
|
|
273
|
+
|
|
274
|
+
The receipt on disk (a real v1 claim receipt; v2 adds an optional `manifest` for a directory target):
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"kind": "verifyhash.claim-receipt",
|
|
279
|
+
"schemaVersion": 1,
|
|
280
|
+
"contentHash": "0x0c271a48a26d075dabf24d6d9474fe3dde105ed15d05638972142c1c5a2a02b5",
|
|
281
|
+
"committer": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
|
|
282
|
+
"salt": "0x59b7ae3c7fba3c5420517d1897b82bbd17d6d568aa303ebf3dbd5347cf6df1b6",
|
|
283
|
+
"commitment": "0x0cf4ffdbdc94d8fab10bceb57af65c02c9daa109cc080c54d49ca7481271dd43",
|
|
284
|
+
"contractAddress": "0xD0141E899a65C95a556fE2B27e5982A6DE7fDD7A",
|
|
285
|
+
"chainId": 31337,
|
|
286
|
+
"uri": "ipfs://cid-alice",
|
|
287
|
+
"path": "/work/src",
|
|
288
|
+
"targetKind": "file",
|
|
289
|
+
"commitTxHash": "0x7470198b01e39a7e11270e826f6305cd15fc1ee34f6d28ee11d80c1f38e50831",
|
|
290
|
+
"commitBlockNumber": 649,
|
|
291
|
+
"minRevealDelay": 1
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Later — even after a reboot, from a fresh process:
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
$ vh reveal --receipt ./0c271a48a26d075d.vhclaim.json # step 2
|
|
299
|
+
reveal: revealing 0x0c271a48...02b5 as 0x7099...79C8...
|
|
300
|
+
Claimed (authorBound) at index 3 by 0x7099...79C8 in tx 0x...
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### B. Localized directory verify (anchor receipt + manifest)
|
|
304
|
+
|
|
305
|
+
After `vh anchor ./repo --receipt ./repo.vhclaim.json`, the receipt records the manifest. If `src/b.js`
|
|
306
|
+
is later edited and `src/new.js` is added, `vh verify ./repo --receipt ./repo.vhclaim.json` prints:
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
MISMATCH: recomputed root is NOT the anchored record.
|
|
310
|
+
...
|
|
311
|
+
--- receipt manifest diff (UNTRUSTED hint) ---
|
|
312
|
+
NOTE: the receipt is an untrusted convenience. The authoritative verdict is the
|
|
313
|
+
MATCH/MISMATCH above (recomputed root vs the on-chain record). This diff only localizes
|
|
314
|
+
WHICH file diverged; it cannot make content valid or invalid on its own.
|
|
315
|
+
files: 1 CHANGED, 1 ADDED, 0 REMOVED (1 unchanged)
|
|
316
|
+
CHANGED src/b.js
|
|
317
|
+
old: 0x0202...0202
|
|
318
|
+
new: 0x0909...0909
|
|
319
|
+
ADDED src/new.js (0x0303...0303) present now, not in the receipt
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The `MISMATCH` is decided by the recomputed root vs the on-chain record; the diff only tells you it was
|
|
323
|
+
`src/b.js` (and the new file) that moved the root.
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Tests
|
|
328
|
+
|
|
329
|
+
- `test/cli.receipt.test.js` round-trips both receipt kinds, proves strict validation (rejects wrong
|
|
330
|
+
version/kind, missing/malformed fields, a v1 receipt smuggling a v2 manifest), and exercises
|
|
331
|
+
`diffManifest`'s ADDED/REMOVED/CHANGED localization.
|
|
332
|
+
- `test/cli.claim.test.js` covers the `commit`/`reveal` split end-to-end against a live node, including a
|
|
333
|
+
resume-from-a-fresh-process path and the front-run-resistance proof.
|
|
334
|
+
- `test/cli.verify.test.js` covers `vh verify <dir> --receipt` localization and the `receiptHashMismatch`
|
|
335
|
+
caveat.
|
|
336
|
+
- `test/cli.receipt.docs.test.js` is a docs-rot guard: it asserts this file and the README keep the
|
|
337
|
+
schema, the resume lifecycle, and the untrusted/localizes-not-decides caveats in sync with the code.
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
<sub>© 2026 verifyhash.com · Licensed under Apache-2.0 (SPDX-License-Identifier: Apache-2.0) — see the [LICENSE](https://verifyhash.com/LICENSE) and [NOTICE](https://verifyhash.com/NOTICE) served with this file.</sub>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# verifyhash contribution score — what it is, what it does and does NOT prove
|
|
2
|
+
|
|
3
|
+
This is the canonical spec for the **contribution score** surfaced by `vh reputation <addr>` (task
|
|
4
|
+
**T-12.2**) over the bounded per-contributor on-chain index (task **T-12.1**). It is **pure
|
|
5
|
+
documentation** of behaviour that already ships; no new runtime behaviour is introduced here.
|
|
6
|
+
|
|
7
|
+
The single sentence to keep in mind before everything below:
|
|
8
|
+
|
|
9
|
+
> **The score is a NON-TRANSFERABLE DERIVED VIEW over records that already exist on-chain. It is
|
|
10
|
+
> re-derivable by anyone from the same registry. It is NOT a token, holds no value, grants no rights,
|
|
11
|
+
> and is only as meaningful as the `authorBound` bar.**
|
|
12
|
+
|
|
13
|
+
It exists to answer one question — *"who are the real contributors, and how much have they verifiably
|
|
14
|
+
contributed?"* — without inventing any new on-chain object. It groups the registry's existing,
|
|
15
|
+
immutable records by address and counts them, separating the strong signal (commit-reveal,
|
|
16
|
+
front-running-resistant claims) from the weak one (front-runnable plain anchors).
|
|
17
|
+
|
|
18
|
+
> **Read [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md) first.** Every caveat here is the same
|
|
19
|
+
> caveat the record fields already carry; this doc reuses that wording verbatim so the boundaries stay
|
|
20
|
+
> consistent. If the two ever drift, TRUST-BOUNDARIES (and the contract NatSpec it mirrors) is
|
|
21
|
+
> authoritative.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## EXACT definition — which on-chain read it aggregates
|
|
26
|
+
|
|
27
|
+
The score is computed entirely off-chain from **a single ownerless `view` read** on
|
|
28
|
+
`contracts/ContributionRegistry.sol` (the T-12.1 per-contributor index): the paged
|
|
29
|
+
`getRecordsByContributor` walk. It reads **nothing else** and re-derives everything — there is no
|
|
30
|
+
stored "score" on-chain.
|
|
31
|
+
|
|
32
|
+
| Read | What it returns | Used for |
|
|
33
|
+
|------|-----------------|----------|
|
|
34
|
+
| `getRecordsByContributor(addr, start, count)` | a clamped, forgiving page of `{ contentHash, Record }` for `addr`'s own records, in insertion order | **the only read the command issues** — `total` and every breakdown below are derived from the records this walk returns |
|
|
35
|
+
| `contributorRecordCount(addr)` | how many records carry `addr` (0 for an unknown address) | **companion read, NOT issued by `vh reputation`.** The T-12.1 O(1) count an external consumer can call to get the same `total` *without* paging; it equals the CLI's `total` because both count the same records |
|
|
36
|
+
|
|
37
|
+
`getRecordsByContributor` is paged in fixed-size chunks (`cli/reputation.js` walks pages of
|
|
38
|
+
`DEFAULT_PAGE = 100`, stopping on a **short/empty page** — that short-page stop, not any count read, is
|
|
39
|
+
the page-walk's bound). Because the contract clamps an out-of-range window to empty (it **never** reverts
|
|
40
|
+
on a tail), enumerating one address is **O(that address's own records), never O(total)** — that is the
|
|
41
|
+
whole point of the T-12.1 index.
|
|
42
|
+
|
|
43
|
+
So `vh reputation` makes exactly **one read shape** (`getRecordsByContributor`), not two.
|
|
44
|
+
`contributorRecordCount` is the matching O(1) count an indexer/UI may call independently; the command
|
|
45
|
+
itself never calls it (`computeScore` sets `total = records.length` from the walked page set).
|
|
46
|
+
|
|
47
|
+
From that page-walk, `computeScore` (pure, no I/O, fully re-derivable from the same input) produces:
|
|
48
|
+
|
|
49
|
+
- **`total`** — `records.length` from the page-walk: the number of records the walk returned for the
|
|
50
|
+
address (which equals what `contributorRecordCount` would return, since both count the same records).
|
|
51
|
+
- **the attribution breakdown — reported SEPARATELY, never summed into one number:**
|
|
52
|
+
- **`authorBound`** — records with `record.authorBound == true`: written via the commit-reveal path
|
|
53
|
+
(`vh claim` / `vh commit`+`vh reveal`). The **proven first claimant** — front-running-resistant.
|
|
54
|
+
- **`anchorOnly`** — records with `record.authorBound == false`: written via the one-shot `anchor()`
|
|
55
|
+
path (`vh anchor`). The **first anchorer only — NOT authorship** (see anti-sybil below).
|
|
56
|
+
- **the lineage breakdown** (orthogonal to attribution; uses `record.parent`):
|
|
57
|
+
- **`lineageRoots`** — records whose `parent == bytes32(0)` (a lineage root; `cli/show.js › isRoot`).
|
|
58
|
+
- **`revisions`** — records whose `parent != bytes32(0)` (a **CLAIMED** predecessor edge — see
|
|
59
|
+
`parent` in TRUST-BOUNDARIES; it is a claim, not proof of ancestry).
|
|
60
|
+
- **the block/time bounds** — the **earliest** and **latest** `{ blockNumber, timestamp }` seen across
|
|
61
|
+
the address's records. These are the same `block.number` / `block.timestamp` the records carry, with
|
|
62
|
+
the same meaning: on-chain ordering + an **UPPER BOUND on existence time**, never authorship time,
|
|
63
|
+
and `timestamp` is validator-influenced (prefer `blockNumber` for hard ordering).
|
|
64
|
+
|
|
65
|
+
The attribution counts are kept SEPARATE on purpose. `authorBound` and `anchorOnly` are **never**
|
|
66
|
+
collapsed into a single opaque number that would hide the difference between a front-running-resistant
|
|
67
|
+
claim and a cheap first-anchor.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## It is a NON-TRANSFERABLE DERIVED VIEW — NOT a token
|
|
72
|
+
|
|
73
|
+
The score is a **read**, not an asset:
|
|
74
|
+
|
|
75
|
+
- **Re-derivable by anyone.** Hand someone the same `(rpc, address)` and they recompute the identical
|
|
76
|
+
numbers. There is no privileged issuer, no per-address balance stored on-chain, nothing to mint, hold,
|
|
77
|
+
or move. `vh reputation` takes a **provider only — never a signer, never a key.**
|
|
78
|
+
- **Non-transferable.** There is nothing to transfer. The "score" is just `count`s over immutable
|
|
79
|
+
records; it cannot be sent, sold, or assigned. It confers no rights and holds no value.
|
|
80
|
+
- **NOT a token, NOT a security.** Issuing a transferable/tradeable reputation **token** on top of this
|
|
81
|
+
view is a **separate, human-gated decision** — proposal **D-2 / P-1** in
|
|
82
|
+
[`STRATEGY.md`](../STRATEGY.md), tagged `needs-human`, and **NOT built here**. This document and the
|
|
83
|
+
`vh reputation` command stay strictly on the non-transferable derived-view side of that line.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## What the score does NOT prove
|
|
88
|
+
|
|
89
|
+
The score inherits every limit of the records it counts. It adds **no** trust beyond them.
|
|
90
|
+
|
|
91
|
+
1. **It does NOT validate record CONTENT.** "This address has N records" says nothing about whether
|
|
92
|
+
those records correspond to real, untampered bytes. A record only ever attested to a `contentHash`;
|
|
93
|
+
the `uri` is an **UNTRUSTED hint** the contract never fetched or validated. To bind any record to
|
|
94
|
+
actual content you must independently obtain it, **re-derive its hash** (`vh hash`), and run
|
|
95
|
+
`vh verify <path>` (re-derive-and-compare). The score never does this and never claims to.
|
|
96
|
+
2. **It does NOT upgrade a front-runnable anchor's attribution.** Grouping records by `contributor` is
|
|
97
|
+
a **RAW ENUMERATION, NOT AN ENDORSEMENT** (the contract's own NatSpec on
|
|
98
|
+
`getRecordsByContributor` / `contributorRecordCount`). A record written via the front-runnable
|
|
99
|
+
`anchor()` is counted under its writer's address while staying `authorBound == false` — still only
|
|
100
|
+
"first anchorer", never proven authorship. Counting it does not make it stronger.
|
|
101
|
+
3. **For anchor-only records, the grouping address is merely "first anchorer".** When
|
|
102
|
+
`authorBound == false`, `contributor` is whoever broadcast the `anchor()` transaction first — anyone
|
|
103
|
+
who learned a `contentHash` (e.g. from the public mempool) could have anchored it. So the address an
|
|
104
|
+
anchor-only record is grouped under is **not** a proven author; it is the first broadcaster. Only the
|
|
105
|
+
`authorBound` count groups under a **proven first claimant**.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Anti-sybil: the meaningful signal is the `authorBound` count
|
|
110
|
+
|
|
111
|
+
Addresses are free to create and one-shot `anchor()` calls are cheap, so any metric that treats every
|
|
112
|
+
record equally is trivially **sybil-inflatable** — a single actor can spin up many addresses and anchor
|
|
113
|
+
many hashes (including hashes copied from someone else's mempool) at near-zero cost. None of that
|
|
114
|
+
proves authorship of anything.
|
|
115
|
+
|
|
116
|
+
The defense is **not** a gate or a stake; it is **reading the breakdown correctly**:
|
|
117
|
+
|
|
118
|
+
> **The meaningful signal is the `authorBound` (commit-reveal) count.** Producing a
|
|
119
|
+
> front-running-resistant claim has a real, irreducible cost: you must `commit` a sender-bound,
|
|
120
|
+
> salt-blinded commitment, wait out the `MIN_REVEAL_DELAY` maturation window, and then `reveal` — and
|
|
121
|
+
> only the original committer can ever reveal it (a copier who lifts the revealed values recomputes a
|
|
122
|
+
> commitment they never registered and reverts). That is the only count that reflects a proven,
|
|
123
|
+
> front-running-resistant claim of authorship.
|
|
124
|
+
|
|
125
|
+
By contrast, the `anchorOnly` count and the raw `total` are **cheap to inflate** (free address creation
|
|
126
|
+
+ front-runnable single-tx anchors) and prove only order-of-anchoring. That is exactly why
|
|
127
|
+
`vh reputation` reports `authorBound` and `anchorOnly` **separately and never sums them**: a consumer
|
|
128
|
+
who wants a sybil-resistant reading should weight (or restrict to) `authorBound`, and treat `anchorOnly`
|
|
129
|
+
/ `total` as the weak, inflatable figures they are. The score makes the distinction visible; it does not
|
|
130
|
+
make the weak signal strong.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## One-line summary
|
|
135
|
+
|
|
136
|
+
| Field | What it is | Do NOT read it as |
|
|
137
|
+
|-------|-----------|-------------------|
|
|
138
|
+
| `total` | how many records carry this address | a sybil-resistant measure (cheap to inflate) |
|
|
139
|
+
| `authorBound` | proven first-claimant (commit-reveal) records — the **meaningful, costly** signal | content validation (re-derive + `vh verify`) |
|
|
140
|
+
| `anchorOnly` | first-anchorer-only records — front-runnable, **weak**, cheap to inflate | proven authorship |
|
|
141
|
+
| `lineageRoots` / `revisions` | `parent == 0x0` vs a CLAIMED predecessor edge | proof of genuine content ancestry |
|
|
142
|
+
| `earliest` / `latest` block+ts | on-chain ordering + upper bound on existence time | authorship time; a precise wall clock |
|
|
143
|
+
| the whole score | a non-transferable, re-derivable DERIVED VIEW | a token, an asset, an endorsement, or content validation |
|
|
144
|
+
|
|
145
|
+
## Tests
|
|
146
|
+
|
|
147
|
+
`test/cli.reputation.docs.test.js` is a docs-rot guard (pure: no chain, no fixtures). It asserts that
|
|
148
|
+
this file and README.md keep documenting the score the way `cli/reputation.js` actually behaves — that
|
|
149
|
+
the single read it aggregates is the paged `getRecordsByContributor` walk (with `contributorRecordCount`
|
|
150
|
+
named as the companion O(1) count, not a read the command issues), the authorBound vs
|
|
151
|
+
anchor-only and root vs revision breakdowns, that it is a non-transferable derived view (NOT a token;
|
|
152
|
+
any tradeable layer is D-2/P-1), what it does NOT prove, and the anti-sybil note that the meaningful
|
|
153
|
+
signal is the `authorBound` count — pinned to the caveats `cli/reputation.js` / `cli/list.js` export so
|
|
154
|
+
the prose can't silently drift from the implementation.
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
<sub>© 2026 verifyhash.com · Licensed under Apache-2.0 (SPDX-License-Identifier: Apache-2.0) — see the [LICENSE](https://verifyhash.com/LICENSE) and [NOTICE](https://verifyhash.com/NOTICE) served with this file.</sub>
|