@sanning/proof 0.13.0 → 0.14.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.
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "rule": "jcs-reader",
3
- "ruleVersion": 2,
4
- "statement": "How a kernel reads bytes that claim to be JCS(value) (RFC 8785). Four checks, none of them left to a built-in decoder's defaults: strict UTF-8, no LEADING byte-order mark, no non-finite literal, and no UNPAIRED surrogate in the parsed value. The fourth is not the first one twice: a surrogate written as the ASCII escape `\\ud800` survives every byte-level check, because the bytes are ASCII, and the parser produces the malformed code unit afterwards. Everything else a JSON parser rejects is rejected. A reader that accepts any of the rejected cases here is reading a set of bytes some other reader will read differently, and a producer chooses those bytes: that is a split verdict on one pack, not a spelling preference. The accepted cases matter as much as the rejected ones -- an honest body carrying astral characters, noncharacters, a U+FEFF inside a string value or combining marks is conformant and must keep verifying.",
3
+ "ruleVersion": 3,
4
+ "statement": "How a kernel reads bytes that claim to be JCS(value) (RFC 8785). Five checks, none of them left to a built-in decoder's defaults: strict UTF-8, no LEADING byte-order mark, no non-finite literal, no object that names one member twice (I-JSON, RFC 7493; ruleVersion 3, proof#144), and no UNPAIRED surrogate in the parsed value. The fourth is not the first one twice: a surrogate written as the ASCII escape `\\ud800` survives every byte-level check, because the bytes are ASCII, and the parser produces the malformed code unit afterwards. Everything else a JSON parser rejects is rejected. A reader that accepts any of the rejected cases here is reading a set of bytes some other reader will read differently, and a producer chooses those bytes: that is a split verdict on one pack, not a spelling preference. The accepted cases matter as much as the rejected ones -- an honest body carrying astral characters, noncharacters, a U+FEFF inside a string value or combining marks is conformant and must keep verifying.",
5
5
  "home": "@sanning/proof -- ts/vectors/jcs-reader.json. This file is the RULE, not a test fixture of one implementation. TypeScript implements it in ts/src/verifier.ts as jcsParse and Python in src/sanning_proof/canonicalize.py as jcs_loads; ts/test/host-wording.test.ts and tests/test_host_wording.py hold BOTH kernels to this file, so neither can pass by agreeing with the other.",
6
- "howToUse": "Base64-decode `bytesB64`, hand the bytes to your kernel's JCS reader, and assert that it returns a value where `accept` is true and throws or raises where it is false. Never decode to a string first: half of these cases are ABOUT the decode, and a reader tested through a string has not been tested.",
6
+ "howToUse": "Base64-decode `bytesB64`, hand the bytes to your kernel's JCS reader, and assert that it returns a value where `accept` is true and throws or raises where it is false. Never decode to a string first: half of these cases are ABOUT the decode, and a reader tested through a string has not been tested. Where a case carries `error`, the refusal's message is that exact string in both kernels: a duplicate's message is kernel wording, and a finding carries it.",
7
7
  "whyItExists": "Round four of proof#140 put a strict decoder on the TypeScript side of three sites and left json.loads(<bytes>) on the Python side of two. CPython hands bytes to detect_encoding and decodes with errors='surrogatepass', so Python became the LENIENT kernel: a sanning.anchor.trace/v3 pack whose checkpoint record bytes were BOM + JCS(record), re-sealed so the pack verifies, read `verified` with zero exceptions in Python and `mixed` in TypeScript. The full ladder, the case leg and CI were green over it. Both guards were source assertions over different module sets, and the TypeScript one stated the false premise in its own comment. One table both kernels read is the fix for that.",
8
8
  "cases": [
9
9
  {
@@ -161,6 +161,85 @@
161
161
  "bytesB64": "WzEsMiwzXQ==",
162
162
  "accept": true,
163
163
  "why": "a JSON array at the root. The reader returns it; whether a given site accepts a non-object is that site's check, not this one's"
164
+ },
165
+ {
166
+ "id": "duplicate-top-level",
167
+ "bytesB64": "eyJzcGVjX3ZlcnNpb24iOiJqdW5rIiwic3BlY192ZXJzaW9uIjoic2FubmluZy5ldmVudHMvdjIifQ==",
168
+ "accept": false,
169
+ "error": "duplicate key \"spec_version\"",
170
+ "why": "an object that names one member twice (proof#144). I-JSON (RFC 7493 section 2.3), which RFC 8785 is defined over, forbids it, and RFC 8785 cannot emit it. `json.loads` and `JSON.parse` both keep the LAST value and say nothing, so a reader that looks at the first sees junk while the kernel verifies the second"
171
+ },
172
+ {
173
+ "id": "duplicate-nested",
174
+ "bytesB64": "eyJldmVudCI6eyJtZXJrbGVfcm9vdCI6ImFhIiwibWVya2xlX3Jvb3QiOiJiYiJ9LCJwcmV2aW91c19oYXNoIjoiR0VORVNJUyJ9",
175
+ "accept": false,
176
+ "error": "duplicate key \"merkle_root\"",
177
+ "why": "the repeat is inside a nested object. Every level is checked, not only the root"
178
+ },
179
+ {
180
+ "id": "duplicate-escaped-spelling",
181
+ "bytesB64": "eyJhIjoxLCJcdTAwNjEiOjJ9",
182
+ "accept": false,
183
+ "error": "duplicate key \"a\"",
184
+ "why": "`\\u0061` is `a`. Names compare after unescaping, as both parsers compare them, so a second spelling of one name is still one name"
185
+ },
186
+ {
187
+ "id": "duplicate-innermost-first",
188
+ "bytesB64": "eyJhIjoxLCJhIjoyLCJiIjp7ImMiOjEsImMiOjJ9fQ==",
189
+ "accept": false,
190
+ "error": "duplicate key \"c\"",
191
+ "why": "two objects repeat a name. Objects are checked as they CLOSE, innermost first, so both kernels name `c` and not `a`: the rule for WHICH duplicate a message names, pinned because a finding carries it"
192
+ },
193
+ {
194
+ "id": "duplicate-name-rendered",
195
+ "bytesB64": "eyJcdTAwMDFcdWQ4M2RcdWRlMDBcIiI6MSwiXHUwMDAxXHVkODNkXHVkZTAwXCIiOjJ9",
196
+ "accept": false,
197
+ "error": "duplicate key \"\\u0001\\ud83d\\ude00\\\"\"",
198
+ "why": "the name in the message is rendered pure ASCII in both kernels: printable ASCII kept, a quote and a backslash escaped, every other UTF-16 code unit written \\uXXXX in lower case, so an astral character is its surrogate pair"
199
+ },
200
+ {
201
+ "id": "duplicate-name-truncated",
202
+ "bytesB64": "eyJra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2trayI6MSwia2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2siOjJ9",
203
+ "accept": false,
204
+ "error": "duplicate key \"kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk\"...",
205
+ "why": "a rendered name longer than 64 characters is cut there and `...` follows the closing quote, so a hostile name cannot flood a finding"
206
+ },
207
+ {
208
+ "id": "duplicate-backslash-at-string-end",
209
+ "bytesB64": "eyJrIjoiXFwiLCJrIjoxfQ==",
210
+ "accept": false,
211
+ "error": "duplicate key \"k\"",
212
+ "why": "a string value ending in an escaped backslash. A scanner that reads `\\\\\"` as an escaped quote runs past the end of the string and misses the repeat"
213
+ },
214
+ {
215
+ "id": "duplicate-then-non-finite",
216
+ "bytesB64": "eyJ4Ijp7ImEiOjEsImEiOjJ9LCJiIjpOYU59",
217
+ "accept": false,
218
+ "why": "a repeat AND a non-finite literal. The parse runs to the end before any repeat is reported, so these bytes fail as JSON in both kernels and never as a duplicate: `JSON.parse` has to finish before a scan can start, and a Python hook that raised on the repeat would name a duplicate where TypeScript names a syntax error"
219
+ },
220
+ {
221
+ "id": "same-name-sibling-objects",
222
+ "bytesB64": "W3siYSI6MX0seyJhIjoyfV0=",
223
+ "accept": true,
224
+ "why": "one name in two different objects is not a repeat"
225
+ },
226
+ {
227
+ "id": "same-name-nested",
228
+ "bytesB64": "eyJhIjp7ImEiOjF9fQ==",
229
+ "accept": true,
230
+ "why": "one name at two depths is not a repeat"
231
+ },
232
+ {
233
+ "id": "braces-and-quotes-inside-strings",
234
+ "bytesB64": "eyJhXCJ9eyI6MSwiYiI6IntcImFcIjoxLFwiYVwiOjJ9In0=",
235
+ "accept": true,
236
+ "why": "a name holding a quote and braces, and a string VALUE holding JSON text with a repeat in it. Neither is an object, so neither is a repeat"
237
+ },
238
+ {
239
+ "id": "case-variant-names",
240
+ "bytesB64": "eyJzcGVjX3ZlcnNpb24iOiJzYW5uaW5nLmV2ZW50cy92MiIsIlNwZWNfVmVyc2lvbiI6Imp1bmsiLCJcdTAxN2ZwZWNfdmVyc2lvbiI6Imp1bmsifQ==",
241
+ "accept": true,
242
+ "why": "names that differ only in case, or by a look-alike such as the long s U+017F, are DIFFERENT names to I-JSON and to both parsers, and this rule does not refuse them. No spec in this repository pins how a reader treats a key that folds to a defined one; to every reader here it is an unknown member, and unknown members are ignored. The Go bundle reader in sanning-io/agent refuses these at the top level of a bundle, which is a recorded Go divergence (proof#144)"
164
243
  }
165
244
  ]
166
245
  }
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "rule": "stamp-on-chain",
3
- "ruleVersion": 2,
3
+ "ruleVersion": 3,
4
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
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
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/v2 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/v2 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.",
7
+ "howToUse": "Each case describes one sanning.stamp.evidence/v2 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/v2 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: entry's `kind` (default \"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. Only an `arweave` witness whose id is a 43-character base64url string is ever fetched (proof#115): a stamp naming any other `kind` is never probed, whatever its id looks like, and stays `ok` with `onChain: null`.",
8
8
  "responseKinds": {
9
9
  "witnessed": "The exact decoded stamp_bytes. The honest chain.",
10
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.",
@@ -16,7 +16,7 @@
16
16
  "anchorIdPrefix": "stampOnChainFixtureAnchorId00000000000000-",
17
17
  "packScaffold": {
18
18
  "seedHex": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
19
- "eventIdPrefix": "ev-",
19
+ "eventIdPrefix": "00000000-0000-4000-8000-00000000000",
20
20
  "contentHash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
21
21
  "signedAt": "2026-09-09T12:00:00Z",
22
22
  "environment": "production"
@@ -224,6 +224,21 @@
224
224
  "sealed by Sanning, witnessed by nobody"
225
225
  ]
226
226
  }
227
+ },
228
+ {
229
+ "id": "unknown-kind-witness-is-not-checked-never-mismatch",
230
+ "why": "proof#115: a witness whose kind names no adapter this verifier has is never fetched, whatever gateways were passed and whatever its id looks like. Fetching it anyway, and comparing the response against stamp_bytes, is how a gateway that has never heard of the kind gets read as a mismatch or a 404 gets read as unreachable — both an accusation this verifier has no grounds to make. The bundle stays partial, said in the kind note, and the stamp itself stays ok with no on-chain result at all: not-checked, not unreachable.",
231
+ "gateways": ["https://g1.example"],
232
+ "stamps": [{ "witnessed": true, "kind": "memory", "returns": ["witnessed"] }],
233
+ "expect": {
234
+ "onChainChecked": true,
235
+ "status": "partial",
236
+ "exit": 3,
237
+ "stamps": [{ "rollup": null, "onChainOk": null, "ok": true }],
238
+ "bundleErrorsContain": [
239
+ "one or more stamps name a witness kind this verifier has no adapter for, so it could not be re-fetched"
240
+ ]
241
+ }
227
242
  }
228
243
  ]
229
244
  }