@sanning/proof 0.7.0 → 0.8.1
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/dist/chain.js +48 -10
- package/dist/cli.js +25 -0
- package/dist/evidence.d.ts +50 -0
- package/dist/evidence.js +442 -91
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/chain.ts +48 -8
- package/src/cli.ts +29 -0
- package/src/evidence.ts +590 -97
- package/src/index.ts +4 -0
- package/vectors/README.md +18 -0
- package/vectors/stamp-on-chain.json +229 -0
package/src/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ export {
|
|
|
66
66
|
EXPORT_BODY_TYPE,
|
|
67
67
|
STAMP_BODY_VERSION,
|
|
68
68
|
STAMP_TRACE_BODY_TYPE,
|
|
69
|
+
STAMP_EVIDENCE_BODY_TYPE,
|
|
69
70
|
disclosedContent,
|
|
70
71
|
recomputeExportVerdict,
|
|
71
72
|
verifyEvidenceBundle,
|
|
@@ -98,6 +99,9 @@ export type {
|
|
|
98
99
|
StampSubtree,
|
|
99
100
|
StampSubtreeResult,
|
|
100
101
|
StampTraceBody,
|
|
102
|
+
StampEvidenceBody,
|
|
103
|
+
StampEvidenceEvent,
|
|
104
|
+
StampEvidenceInclusion,
|
|
101
105
|
TraceCheckpoint,
|
|
102
106
|
TraceCheckpointV1,
|
|
103
107
|
TraceEvent,
|
package/vectors/README.md
CHANGED
|
@@ -8,6 +8,24 @@ That distinction is the whole point. A rule that consumers must agree with needs
|
|
|
8
8
|
a statement they can *fetch*, not one they have to be trusted to have copied
|
|
9
9
|
correctly.
|
|
10
10
|
|
|
11
|
+
## `stamp-on-chain.json`
|
|
12
|
+
|
|
13
|
+
The rule that decides what re-fetching a **stamp's witness** from a gateway does
|
|
14
|
+
and does not settle — the optional, online half of `sanning.stamp.trace/v1` and
|
|
15
|
+
`sanning.stamp.evidence/v1` (`evidence-bundle.md` §5.2.4 / §5.3.4). Implemented
|
|
16
|
+
by [`../src/evidence.ts`](../src/evidence.ts) and asserted against this table by
|
|
17
|
+
[`../test/stamp-on-chain-vectors.test.ts`](../test/stamp-on-chain-vectors.test.ts)
|
|
18
|
+
— **and by the Python kernel against the same bytes**, in
|
|
19
|
+
`tests/test_stamp_on_chain_vectors.py`, so neither kernel can pass by agreeing
|
|
20
|
+
with the other.
|
|
21
|
+
|
|
22
|
+
**The one thing a consumer must not do with it: collapse the four states into a
|
|
23
|
+
boolean.** `not checked` · `confirm` · `mismatch` · `unreachable`. A reader that
|
|
24
|
+
cannot tell the first from the last will report that no gateway could be reached
|
|
25
|
+
on a run that contacted none — a false accusation manufactured by a missing
|
|
26
|
+
state, and the defect this table was written after. Vendoring rules are the same
|
|
27
|
+
three steps as below, against `vectors/stamp-on-chain.json`.
|
|
28
|
+
|
|
11
29
|
## `pack-grading.json`
|
|
12
30
|
|
|
13
31
|
The rule that decides whether a **pack** — a signed bundle plus whatever
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rule": "stamp-on-chain",
|
|
3
|
+
"ruleVersion": 1,
|
|
4
|
+
"statement": "What re-fetching a stamp's witness from a gateway DOES and DOES NOT settle. A stamp body carries no producer signature and no inclusion proof of its own, so the witness is the entire independent part of it (evidence-bundle.md §5.2.4): confirming that a gateway holds the exact bytes named by anchor_ref is the only check that turns Sanning's own assertion into evidence. It is an ONLINE act and always optional — an offline verdict is unchanged by it and never claims it happened. Four states, never three: not-checked (no gateway was asked, or the stamp names no witness), confirm (a gateway returned the witnessed bytes), mismatch (a gateway returned DIFFERENT bytes — the only outcome that fails anything), unreachable (we asked and nobody answered — an availability gap, never an accusation). The expected bytes are stamps[].stamp_bytes VERBATIM, because §5.2.2 defines that field as the exact bytes handed to the witness; re-canonicalizing them would compare the chain against bytes nobody signed.",
|
|
5
|
+
"home": "@sanning/proof — ts/vectors/stamp-on-chain.json. This file is the RULE, not a test fixture of one implementation. The TypeScript kernel implements it in ts/src/evidence.ts (refetchStamps / refetchStamp / probeGateway / rollupOnChain) and is held to it by ts/test/stamp-on-chain-vectors.test.ts; the Python kernel implements it in src/sanning_proof/evidence.py (_refetch_stamps / _refetch_stamp / _probe_gateway / _rollup_on_chain) and is held to the SAME file by tests/test_stamp_on_chain_vectors.py. Neither kernel can pass by agreeing with the other.",
|
|
6
|
+
"consumers": "A surface that re-fetches a stamp witness SHOULD call the kernel's verifyEvidenceBundle with `gateways` rather than derive the rule again. A surface that cannot yet (an older pinned kernel, another language) vendors this file byte-for-byte, asserts its own derivation against it, and runs a drift check against the copy shipped in the @sanning/proof tarball at vectors/stamp-on-chain.json — failing when it cannot find a copy to compare against. A vendored copy that is not byte-identical to this one is a defect, not a variant. In particular: a surface MUST NOT collapse the four states into a boolean. A reader that cannot distinguish not-checked from unreachable will report that no gateway could be reached when none was tried, which is a false accusation manufactured by a missing state, and is the defect this table was written after.",
|
|
7
|
+
"howToUse": "Each case describes one sanning.stamp.evidence/v1 pack and one set of scripted gateway responses. Build the pack from `packScaffold`: for each entry of `stamps` (index i, 0-based) sign ONE sanning.events/v1 envelope over the record {\"event\":{\"content_hash\":\"<64×a>\"},\"i\":i} with event_id `ev-<i>` and the fixed seed, take its leaf under RFC 9162 (SHA-256(0x00 ‖ JCS(envelope))), make a one-leaf tree, and build the stamp body from `stampBodyScaffold` with merkle_root = that root, leaf_count = 1 and one sub-tree covering [0,1). stamp_bytes is JCS(stamp body) hex-encoded; stamp_ref is SHA-256 of the DECODED bytes. When the entry's `witnessed` is true attach anchor_ref = {kind: \"arweave\", id: `<anchorIdPrefix><i>`}, otherwise omit anchor_ref entirely. Give each stamp one event whose inclusion is {stamp_ref, leaf_index: 0, leaf_count: 1, leaf_hash, audit_path: []}. Wrap the body per `wrapperScaffold`, set body_hash = SHA-256(JCS(body)) and sign the wrapper (minus `signature`) with the same seed. Then call the kernel with the case's `gateways` and a fetch seam that answers `{gateway}/{anchor id}` per the `returns` entry for that stamp at that gateway's index, and assert `expect`. A case with an empty `gateways` list is called with no gateway list at all — that is the not-checked state and it must be reachable without a fetch seam.",
|
|
8
|
+
"responseKinds": {
|
|
9
|
+
"witnessed": "The exact decoded stamp_bytes. The honest chain.",
|
|
10
|
+
"altered": "JCS of the same stamp body with interval_end changed to 2026-09-09T13:00:00.000Z — a well-formed, canonical, semantically different stamp. This is the forgery shape that matters: a pack built on it is fully self-consistent OFFLINE and lies only about what was witnessed.",
|
|
11
|
+
"reserialized": "The same JSON object re-serialized with 2-space indentation. Semantically identical, byte-different. MUST confirm: a gateway that pretty-prints is a delivery quirk, not a finding.",
|
|
12
|
+
"notJson": "The bytes `not json at all`. Neither byte-equal nor parseable, so there is nothing to compare structurally — a mismatch.",
|
|
13
|
+
"unreachable": "The fetch fails: a network error, a timeout, a non-ok response, or no such transaction. Never a mismatch."
|
|
14
|
+
},
|
|
15
|
+
"alsoAppliesTo": "sanning.stamp.trace/v1 — the same case, with body_type changed and events[] dropped. Every case's `expect` holds UNCHANGED for that body, and a grader MUST run all of them twice. §5.2 and §5.3 carry the same stamps[] array under the same §5.2.2 definition, so they must give the same on-chain answer: a stamp that can be re-fetched inside one wrapper and not the other forces every consumer to know which wrapper it is holding before it can read `onChain: null`, and that is the kind of per-body exception a reader eventually gets wrong. The one case-level difference is outside this rule: a §5.2 body with no stamps at all is `partial` for carrying nothing, which no case here exercises.",
|
|
16
|
+
"anchorIdPrefix": "stampOnChainFixtureAnchorId0000000000-",
|
|
17
|
+
"packScaffold": {
|
|
18
|
+
"seedHex": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
|
19
|
+
"eventIdPrefix": "ev-",
|
|
20
|
+
"contentHash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
21
|
+
"signedAt": "2026-09-09T12:00:00Z",
|
|
22
|
+
"environment": "production"
|
|
23
|
+
},
|
|
24
|
+
"stampBodyScaffold": {
|
|
25
|
+
"stamp_version": "sanning.stamp/v1",
|
|
26
|
+
"org_ref": "9999999999999999999999999999999999999999999999999999999999999999",
|
|
27
|
+
"interval_start": "2026-09-09T12:00:00.000Z",
|
|
28
|
+
"interval_end": "2026-09-09T12:10:00.000Z",
|
|
29
|
+
"subtreeRef": "1111111111111111111111111111111111111111111111111111111111111111"
|
|
30
|
+
},
|
|
31
|
+
"wrapperScaffold": {
|
|
32
|
+
"spec_version": "sanning.evidence/v1",
|
|
33
|
+
"body_type": "sanning.stamp.evidence/v1",
|
|
34
|
+
"issuer": { "kind": "sanning" },
|
|
35
|
+
"generated_at": "2026-09-09T12:20:00.000Z",
|
|
36
|
+
"gateway": null,
|
|
37
|
+
"previous_hash": "GENESIS",
|
|
38
|
+
"signature_alg": "ed25519"
|
|
39
|
+
},
|
|
40
|
+
"cases": [
|
|
41
|
+
{
|
|
42
|
+
"id": "not-checked-when-no-gateways",
|
|
43
|
+
"why": "The default, and the state the other three are defined against. No gateway list means no gateway was asked, so onChain is null — NOT an unreachable result over an empty list. An implementation that reported unreachable here would tell every offline auditor that the chain could not be reached, on a run that never intended to touch it.",
|
|
44
|
+
"gateways": [],
|
|
45
|
+
"stamps": [{ "witnessed": true, "returns": [] }],
|
|
46
|
+
"expect": {
|
|
47
|
+
"onChainChecked": false,
|
|
48
|
+
"status": "verified",
|
|
49
|
+
"exit": 0,
|
|
50
|
+
"stamps": [{ "rollup": null, "onChainOk": null, "ok": true }],
|
|
51
|
+
"bundleErrorsContain": []
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "confirm-single-gateway",
|
|
56
|
+
"why": "The gateway holds exactly the bytes the bundle says were witnessed. This is the only case in which a stamp bundle has been shown to be more than Sanning's own assertion, and it must still read `verified` — confirming a witness adds evidence, it does not change a verdict that was already sound offline.",
|
|
57
|
+
"gateways": ["https://g1.example"],
|
|
58
|
+
"stamps": [{ "witnessed": true, "returns": ["witnessed"] }],
|
|
59
|
+
"expect": {
|
|
60
|
+
"onChainChecked": true,
|
|
61
|
+
"status": "verified",
|
|
62
|
+
"exit": 0,
|
|
63
|
+
"stamps": [
|
|
64
|
+
{ "rollup": "confirm", "onChainOk": true, "ok": true, "perGateway": ["confirm"] }
|
|
65
|
+
],
|
|
66
|
+
"bundleErrorsContain": []
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "mismatch-fails-the-stamp",
|
|
71
|
+
"why": "THE case this capability exists for. The pack is internally perfect — wrapper signed, stamp_ref derived from its own bytes, those bytes JCS-canonical, the event's RFC 9162 path reconstructing the committed root — and verifies `verified` offline. The chain holds different bytes. Only a re-fetch can tell, and it must fail the stamp rather than note it: a stamp whose witness contradicts it is not partially witnessed, it is contradicted. An implementation whose byte comparison cannot fail passes every other case in this table and this one alone.",
|
|
72
|
+
"gateways": ["https://g1.example"],
|
|
73
|
+
"stamps": [{ "witnessed": true, "returns": ["altered"] }],
|
|
74
|
+
"expect": {
|
|
75
|
+
"onChainChecked": true,
|
|
76
|
+
"status": "failed",
|
|
77
|
+
"exit": 1,
|
|
78
|
+
"stamps": [
|
|
79
|
+
{
|
|
80
|
+
"rollup": "mismatch",
|
|
81
|
+
"onChainOk": false,
|
|
82
|
+
"ok": false,
|
|
83
|
+
"perGateway": ["mismatch"],
|
|
84
|
+
"errorContains": "do not match the stamp's witnessed bytes"
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"bundleErrorsContain": []
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "unreachable-is-undetermined-not-failed",
|
|
92
|
+
"why": "A gateway that does not answer has told us NOTHING about the evidence, and a verifier that turns silence into an accusation is the exact failure the anchor_ref vocabulary was designed around. Undetermined: the offline-sound stamp stays ok, the run drops to `partial`, and the exit code is 3 — a retry, not a finding.",
|
|
93
|
+
"gateways": ["https://g1.example"],
|
|
94
|
+
"stamps": [{ "witnessed": true, "returns": ["unreachable"] }],
|
|
95
|
+
"expect": {
|
|
96
|
+
"onChainChecked": true,
|
|
97
|
+
"status": "partial",
|
|
98
|
+
"exit": 3,
|
|
99
|
+
"stamps": [
|
|
100
|
+
{
|
|
101
|
+
"rollup": "unreachable",
|
|
102
|
+
"onChainOk": null,
|
|
103
|
+
"ok": true,
|
|
104
|
+
"perGateway": ["unreachable"],
|
|
105
|
+
"errorContains": "unreachable on all gateways"
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
"bundleErrorsContain": ["could not be re-fetched from the supplied gateways"]
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"id": "mismatch-beats-confirm",
|
|
113
|
+
"why": "Worst-finding-wins. One gateway serving the right bytes does not answer the other one serving different bytes — a disagreement between gateways about a permanent record is itself the finding, and the honest reading is the accusing one. The per-gateway array is retained so a reader can see WHICH gateway disagreed rather than only that one did.",
|
|
114
|
+
"gateways": ["https://g1.example", "https://g2.example"],
|
|
115
|
+
"stamps": [{ "witnessed": true, "returns": ["altered", "witnessed"] }],
|
|
116
|
+
"expect": {
|
|
117
|
+
"onChainChecked": true,
|
|
118
|
+
"status": "failed",
|
|
119
|
+
"exit": 1,
|
|
120
|
+
"stamps": [
|
|
121
|
+
{
|
|
122
|
+
"rollup": "mismatch",
|
|
123
|
+
"onChainOk": false,
|
|
124
|
+
"ok": false,
|
|
125
|
+
"perGateway": ["mismatch", "confirm"]
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"bundleErrorsContain": []
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"id": "confirm-beats-unreachable",
|
|
133
|
+
"why": "Best-evidence, once nothing has contradicted anything. One gateway being down is an availability fact about that gateway; another gateway returning the witnessed bytes is a fact about the record. The record wins, and the run is not downgraded for the outage — otherwise every auditor with one stale gateway in their list would read `partial` on fully witnessed evidence.",
|
|
134
|
+
"gateways": ["https://g1.example", "https://g2.example"],
|
|
135
|
+
"stamps": [{ "witnessed": true, "returns": ["unreachable", "witnessed"] }],
|
|
136
|
+
"expect": {
|
|
137
|
+
"onChainChecked": true,
|
|
138
|
+
"status": "verified",
|
|
139
|
+
"exit": 0,
|
|
140
|
+
"stamps": [
|
|
141
|
+
{
|
|
142
|
+
"rollup": "confirm",
|
|
143
|
+
"onChainOk": true,
|
|
144
|
+
"ok": true,
|
|
145
|
+
"perGateway": ["unreachable", "confirm"]
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
"bundleErrorsContain": []
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"id": "unwitnessed-stamp-is-not-checked-never-unreachable",
|
|
153
|
+
"why": "A stamp with no anchor_ref names no witness, so there is nothing to ask any gateway for. onChain stays null — not-checked — even though gateways were supplied and every one of them is reachable. Reporting all-unreachable here would invent a network event that did not occur, and would move this pack's exit code from 0 to 3 purely because the caller passed a gateway list. The bundle is already `partial` for the right reason, said in the right words, by §5.2.4.",
|
|
154
|
+
"gateways": ["https://g1.example"],
|
|
155
|
+
"stamps": [{ "witnessed": false, "returns": ["witnessed"] }],
|
|
156
|
+
"expect": {
|
|
157
|
+
"onChainChecked": true,
|
|
158
|
+
"status": "partial",
|
|
159
|
+
"exit": 0,
|
|
160
|
+
"stamps": [{ "rollup": null, "onChainOk": null, "ok": true }],
|
|
161
|
+
"bundleErrorsContain": ["sealed by Sanning, witnessed by nobody"]
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"id": "reserialized-body-still-confirms",
|
|
166
|
+
"why": "A gateway that re-serializes JSON on the way out has changed the delivery, not the record. The byte compare fails and the structural compare — JCS of what came back against the witnessed bytes — succeeds, so this is a confirm. Without the structural fallback every pretty-printing gateway would read as a tampered record, which is a false accusation with an operational cause.",
|
|
167
|
+
"gateways": ["https://g1.example"],
|
|
168
|
+
"stamps": [{ "witnessed": true, "returns": ["reserialized"] }],
|
|
169
|
+
"expect": {
|
|
170
|
+
"onChainChecked": true,
|
|
171
|
+
"status": "verified",
|
|
172
|
+
"exit": 0,
|
|
173
|
+
"stamps": [
|
|
174
|
+
{ "rollup": "confirm", "onChainOk": true, "ok": true, "perGateway": ["confirm"] }
|
|
175
|
+
],
|
|
176
|
+
"bundleErrorsContain": []
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"id": "not-json-is-mismatch-not-unreachable",
|
|
181
|
+
"why": "The gateway answered — so this is not an availability gap — and what it returned is neither the witnessed bytes nor anything that canonicalizes to them. The structural fallback must not swallow the failure to parse into a shrug: a witness id that resolves to something which is not this stamp is a finding, and filing it as unreachable would let a substituted record read as a temporary outage.",
|
|
182
|
+
"gateways": ["https://g1.example"],
|
|
183
|
+
"stamps": [{ "witnessed": true, "returns": ["notJson"] }],
|
|
184
|
+
"expect": {
|
|
185
|
+
"onChainChecked": true,
|
|
186
|
+
"status": "failed",
|
|
187
|
+
"exit": 1,
|
|
188
|
+
"stamps": [
|
|
189
|
+
{
|
|
190
|
+
"rollup": "mismatch",
|
|
191
|
+
"onChainOk": false,
|
|
192
|
+
"ok": false,
|
|
193
|
+
"perGateway": ["mismatch"],
|
|
194
|
+
"errorContains": "do not match the stamp's witnessed bytes"
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
"bundleErrorsContain": []
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"id": "two-gaps-are-two-notes",
|
|
202
|
+
"why": "One stamp witnessed but unreachable, one stamp never witnessed. Both hold the bundle at `partial` and both get SAID. They have different causes and different fixes — one waits for the network, the other for the interval to close — and an auditor told only about the gateway would read the unwitnessed stamp as fine. The per-stamp results keep them apart too: the first carries an unreachable rollup, the second carries null.",
|
|
203
|
+
"gateways": ["https://g1.example"],
|
|
204
|
+
"stamps": [
|
|
205
|
+
{ "witnessed": true, "returns": ["unreachable"] },
|
|
206
|
+
{ "witnessed": false, "returns": ["witnessed"] }
|
|
207
|
+
],
|
|
208
|
+
"expect": {
|
|
209
|
+
"onChainChecked": true,
|
|
210
|
+
"status": "partial",
|
|
211
|
+
"exit": 3,
|
|
212
|
+
"stamps": [
|
|
213
|
+
{
|
|
214
|
+
"rollup": "unreachable",
|
|
215
|
+
"onChainOk": null,
|
|
216
|
+
"ok": true,
|
|
217
|
+
"perGateway": ["unreachable"],
|
|
218
|
+
"errorContains": "unreachable on all gateways"
|
|
219
|
+
},
|
|
220
|
+
{ "rollup": null, "onChainOk": null, "ok": true }
|
|
221
|
+
],
|
|
222
|
+
"bundleErrorsContain": [
|
|
223
|
+
"could not be re-fetched from the supplied gateways",
|
|
224
|
+
"sealed by Sanning, witnessed by nobody"
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
}
|