@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/dist/chain.js
CHANGED
|
@@ -143,7 +143,15 @@ function adapterLinkage(record) {
|
|
|
143
143
|
// here: nothing to resolve is a beginning.
|
|
144
144
|
const prev = str(fields.prev_event_id);
|
|
145
145
|
const seqRaw = fields.seq;
|
|
146
|
-
|
|
146
|
+
// SAFE integer, not merely integral. Beyond 2^53 a JSON number does not
|
|
147
|
+
// survive `JSON.parse` intact — `9007199254740993` arrives as
|
|
148
|
+
// `...992` — while Python's parser keeps it exactly, so the two kernels
|
|
149
|
+
// read DIFFERENT sequence numbers off identical bytes and report gaps of
|
|
150
|
+
// different widths. Measured at 2^53+1: a `chain-gap` count of
|
|
151
|
+
// 9007199254740990 here against 9007199254740992 there (proof#49). A value
|
|
152
|
+
// this kernel cannot represent is one it cannot read, which is the same
|
|
153
|
+
// answer it gives every other unreadable field: no sequence, take no part.
|
|
154
|
+
const seq = Number.isSafeInteger(seqRaw) ? seqRaw : null;
|
|
147
155
|
return { namespace, chainKey, prev, seq };
|
|
148
156
|
}
|
|
149
157
|
return null;
|
|
@@ -226,6 +234,20 @@ function byChainOrder(a, b) {
|
|
|
226
234
|
return a.seq - b.seq;
|
|
227
235
|
return a.order - b.order;
|
|
228
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* The smallest slot between the lowest and highest occupied ones that no record
|
|
239
|
+
* occupies, found by walking the SORTED occupants rather than the range they
|
|
240
|
+
* span. See the call site: the range's width is chosen by whoever wrote the
|
|
241
|
+
* bundle.
|
|
242
|
+
*/
|
|
243
|
+
function firstMissingSlot(present) {
|
|
244
|
+
const ordered = [...present].sort((a, b) => a - b);
|
|
245
|
+
for (let i = 0; i < ordered.length - 1; i++) {
|
|
246
|
+
if (ordered[i + 1] > ordered[i] + 1)
|
|
247
|
+
return ordered[i] + 1;
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
229
251
|
function findingsFor(group) {
|
|
230
252
|
const { members, chainKey, source, publicKey } = group;
|
|
231
253
|
const of = members.length;
|
|
@@ -283,18 +305,23 @@ function findingsFor(group) {
|
|
|
283
305
|
const present = new Set(sequenced.map((m) => m.seq));
|
|
284
306
|
const low = Math.min(...present);
|
|
285
307
|
const high = Math.max(...present);
|
|
286
|
-
|
|
287
|
-
for (let s = low; s <=
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
308
|
+
// Counted, never walked. `seq` is attacker-chosen, so a bundle presenting
|
|
309
|
+
// two records at seq 0 and seq 1e15 made the old `for (let s = low; s <=
|
|
310
|
+
// high; s++)` spin for days — a verifier that HANGS on adversarial input is
|
|
311
|
+
// worse than one that raises, and this reader promises neither (proof#49).
|
|
312
|
+
// The present set is a subset of [low, high], so the absent slots are the
|
|
313
|
+
// width minus the occupants, and the first of them is found by walking the
|
|
314
|
+
// occupants in order.
|
|
315
|
+
const missingCount = high - low + 1 - present.size;
|
|
316
|
+
const firstMissing = missingCount > 0 ? firstMissingSlot(present) : null;
|
|
317
|
+
if (firstMissing !== null) {
|
|
318
|
+
const after = sequenced.filter((m) => m.seq > firstMissing).sort(byChainOrder)[0];
|
|
292
319
|
findings.push({
|
|
293
320
|
kind: "chain-gap",
|
|
294
321
|
...base,
|
|
295
|
-
count:
|
|
322
|
+
count: missingCount,
|
|
296
323
|
eventIds: after === undefined ? [] : [after.eventId],
|
|
297
|
-
firstMissingSeq:
|
|
324
|
+
firstMissingSeq: firstMissing,
|
|
298
325
|
});
|
|
299
326
|
}
|
|
300
327
|
}
|
|
@@ -311,7 +338,18 @@ function findingsFor(group) {
|
|
|
311
338
|
export async function readChains(bundle) {
|
|
312
339
|
const groups = await groupChains(bundle);
|
|
313
340
|
const order = (g) => `${g.source} ${g.namespace ?? ""} ${g.chainKey}`;
|
|
314
|
-
|
|
341
|
+
// Ordered by UTF-16 code unit, NOT `localeCompare`. A chain key is caller
|
|
342
|
+
// text, and `localeCompare` with no locale argument answers from the host's
|
|
343
|
+
// ICU collation: measured here, it puts "chain-2" AFTER "chain_1" where every
|
|
344
|
+
// code-wise comparison puts it before, and it can differ between two
|
|
345
|
+
// TypeScript installs, let alone between kernels. The Python reader sorts the
|
|
346
|
+
// same key encoded as `utf-16-be`, which is exactly this comparison — so the
|
|
347
|
+
// two kernels emit findings in one order (proof#49).
|
|
348
|
+
groups.sort((a, b) => {
|
|
349
|
+
const x = order(a);
|
|
350
|
+
const y = order(b);
|
|
351
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
352
|
+
});
|
|
315
353
|
return {
|
|
316
354
|
chains: groups.map((g) => ({
|
|
317
355
|
chainKey: g.chainKey,
|
package/dist/cli.js
CHANGED
|
@@ -610,6 +610,31 @@ function reportEvidence(result, path, gateways, io, chains) {
|
|
|
610
610
|
io.out(paint(` - ${e}`, R));
|
|
611
611
|
}
|
|
612
612
|
}
|
|
613
|
+
// Stamps (evidence-bundle.md §5.2 / §5.3). Printed rather than summarised,
|
|
614
|
+
// because for a stamp body the WITNESS is the entire independent part
|
|
615
|
+
// (§5.2.4) — and until now this reporter showed nothing about it at all, so a
|
|
616
|
+
// §5.3 pack printed a rollup line claiming an on-chain scope with no visible
|
|
617
|
+
// on-chain result anywhere above it.
|
|
618
|
+
//
|
|
619
|
+
// The on-chain column distinguishes all four states in the same three
|
|
620
|
+
// characters the checkpoint block uses, plus its absence: `✓` confirmed,
|
|
621
|
+
// `✗` mismatched, `~` asked and unreachable, and NO COLUMN AT ALL when
|
|
622
|
+
// nothing was checked. A reader must never have to infer "not checked" from
|
|
623
|
+
// a symbol that also means "could not be reached".
|
|
624
|
+
if (result.stamps && result.stamps.length > 0) {
|
|
625
|
+
io.out("");
|
|
626
|
+
io.out(paint(" Stamps", DIM));
|
|
627
|
+
for (const st of result.stamps) {
|
|
628
|
+
const witness = st.anchorRef === null
|
|
629
|
+
? paint("~ no witness", Y)
|
|
630
|
+
: `witness ${st.anchorRef.kind}:${shortTx(st.anchorRef.id)}`;
|
|
631
|
+
const onChain = st.onChain === null ? "" : ` on-chain ${mark(st.onChainOk)} (${st.onChain.rollup})`;
|
|
632
|
+
io.out(` ${mark(st.ok)} ${shortTx(st.stampRef)} bytes ${mark(st.stampRefOk && st.bodyOk)}` +
|
|
633
|
+
` ranges ${mark(st.subtreesOk)} ${witness}${onChain}`);
|
|
634
|
+
for (const e of st.errors)
|
|
635
|
+
io.out(paint(` - ${e}`, R));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
613
638
|
if (result.events.length > 0) {
|
|
614
639
|
io.out("");
|
|
615
640
|
io.out(paint(" Events", DIM));
|
package/dist/evidence.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export declare const EXPORT_BODY_TYPE = "sanning.evidence.export/v1";
|
|
|
25
25
|
* unrecoverable once witnessed.
|
|
26
26
|
*/
|
|
27
27
|
export declare const STAMP_TRACE_BODY_TYPE = "sanning.stamp.trace/v1";
|
|
28
|
+
export declare const STAMP_EVIDENCE_BODY_TYPE = "sanning.stamp.evidence/v1";
|
|
28
29
|
/**
|
|
29
30
|
* The version string INSIDE a witnessed stamp body — a different string from
|
|
30
31
|
* the `body_type` above, and the two are easy to confuse.
|
|
@@ -162,6 +163,26 @@ export interface StampEntry {
|
|
|
162
163
|
export interface StampTraceBody {
|
|
163
164
|
stamps: StampEntry[];
|
|
164
165
|
}
|
|
166
|
+
/** §5.3's inclusion: §5.1's shape with `stamp_ref` as the join key. */
|
|
167
|
+
export interface StampEvidenceInclusion {
|
|
168
|
+
leaf_hash: string;
|
|
169
|
+
leaf_index: number;
|
|
170
|
+
leaf_count: number;
|
|
171
|
+
audit_path: string[];
|
|
172
|
+
/** Resolves against `stamps[].stamp_ref` in the SAME body. */
|
|
173
|
+
stamp_ref: string;
|
|
174
|
+
}
|
|
175
|
+
export interface StampEvidenceEvent {
|
|
176
|
+
envelope: Envelope;
|
|
177
|
+
record_bytes?: string;
|
|
178
|
+
content?: string;
|
|
179
|
+
inclusion: StampEvidenceInclusion;
|
|
180
|
+
}
|
|
181
|
+
export interface StampEvidenceBody {
|
|
182
|
+
/** Byte-identical to §5.2.2 — one definition, one place to change. */
|
|
183
|
+
stamps: StampEntry[];
|
|
184
|
+
events: StampEvidenceEvent[];
|
|
185
|
+
}
|
|
165
186
|
export interface EvidenceBundle {
|
|
166
187
|
spec_version: string;
|
|
167
188
|
body_type: string;
|
|
@@ -271,6 +292,35 @@ export interface StampResult {
|
|
|
271
292
|
* carries it.
|
|
272
293
|
*/
|
|
273
294
|
anchorRef: AnchorRef | null;
|
|
295
|
+
/**
|
|
296
|
+
* Per-gateway on-chain outcomes for this stamp's `anchor_ref` (§4.2's shape,
|
|
297
|
+
* `checkpoints[]`'s vocabulary), or `null` when NOTHING WAS CHECKED.
|
|
298
|
+
*
|
|
299
|
+
* ── The four states, and why none of them collapses ──
|
|
300
|
+
*
|
|
301
|
+
* `null` means not checked — no gateways were supplied, or this stamp names
|
|
302
|
+
* no witness to check. It is a statement about the RUN, never about the
|
|
303
|
+
* stamp. The other three are statements about the stamp and live in
|
|
304
|
+
* `onChain.rollup`: `confirm` (a gateway returned the witnessed bytes),
|
|
305
|
+
* `mismatch` (a gateway returned DIFFERENT bytes — a finding, and the only
|
|
306
|
+
* one of the four that fails anything), `unreachable` (we asked and nobody
|
|
307
|
+
* answered — an availability gap, never an accusation).
|
|
308
|
+
*
|
|
309
|
+
* Keeping "not checked" distinct from "unreachable" is the whole point. A
|
|
310
|
+
* surface handed a collapsed boolean cannot tell "no gateway could be
|
|
311
|
+
* reached" from "no gateway was tried", and reported the first when the
|
|
312
|
+
* second was true — a false accusation manufactured by a missing state.
|
|
313
|
+
*/
|
|
314
|
+
onChain: OnChainResult | null;
|
|
315
|
+
/**
|
|
316
|
+
* Derived collapse of `onChain`, mirroring `CheckpointResult.onChainOk` so a
|
|
317
|
+
* consumer reads one field the same way on both: `false` on mismatch, `true`
|
|
318
|
+
* on confirm, `null` on unreachable AND on not-checked.
|
|
319
|
+
*
|
|
320
|
+
* It is deliberately the LOSSY view — `null` is three-quarters of the state
|
|
321
|
+
* space here. Anything that needs to tell those apart reads `onChain`.
|
|
322
|
+
*/
|
|
323
|
+
onChainOk: boolean | null;
|
|
274
324
|
ok: boolean;
|
|
275
325
|
errors: string[];
|
|
276
326
|
}
|