@secondlayer/shared 13.13.1 → 13.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,6 +1,46 @@
1
1
  // src/archive/semantic-digest-builder.ts
2
2
  import { sql } from "kysely";
3
3
 
4
+ // src/archive/datasets.ts
5
+ var ARCHIVE_DATASETS = [
6
+ "blocks",
7
+ "transactions",
8
+ "events",
9
+ "state_writes"
10
+ ];
11
+ function isArchiveDataset(value) {
12
+ return ARCHIVE_DATASETS.includes(value);
13
+ }
14
+ var OPTIONAL_DATASET = {
15
+ blocks: false,
16
+ transactions: false,
17
+ events: false,
18
+ state_writes: true
19
+ };
20
+ function isOptionalDataset(dataset) {
21
+ return OPTIONAL_DATASET[dataset];
22
+ }
23
+ function manifestPublishes(manifest, dataset) {
24
+ if (!isOptionalDataset(dataset))
25
+ return true;
26
+ return (manifest.partitions ?? []).some((p) => p.dataset === dataset) || (manifest.zero_record_ranges ?? []).some((z) => z.dataset === dataset);
27
+ }
28
+ function archiveHeightColumn(dataset) {
29
+ switch (dataset) {
30
+ case "blocks":
31
+ return "height";
32
+ case "transactions":
33
+ case "events":
34
+ case "state_writes":
35
+ return "block_height";
36
+ default:
37
+ return assertNeverDataset(dataset);
38
+ }
39
+ }
40
+ function assertNeverDataset(dataset) {
41
+ throw new Error(`unhandled archive dataset: ${String(dataset)}`);
42
+ }
43
+
4
44
  // src/archive/semantic-digest.ts
5
45
  import { createHash } from "node:crypto";
6
46
  var SEMANTIC_DIGEST_SPEC_V1 = "sha256:semantic-v1";
@@ -87,15 +127,26 @@ function encodeEvent(row) {
87
127
  jsonScalar(row.data)
88
128
  ].join(FIELD_SEP);
89
129
  }
130
+ function encodeStateWrite(row) {
131
+ return [
132
+ scalar(row.block_height),
133
+ scalar(row.ordinal),
134
+ scalar(row.tx_index),
135
+ scalar(row.key),
136
+ scalar(row.value_hex)
137
+ ].join(FIELD_SEP);
138
+ }
90
139
  var semanticDigest = {
91
140
  v1: {
92
141
  spec: SEMANTIC_DIGEST_SPEC_V1,
93
142
  encodeBlock,
94
143
  encodeTransaction,
95
144
  encodeEvent,
145
+ encodeStateWrite,
96
146
  block: (row) => sha256Hex(textEncoder.encode(encodeBlock(row))),
97
147
  transaction: (row) => sha256Hex(textEncoder.encode(encodeTransaction(row))),
98
- event: (row) => sha256Hex(textEncoder.encode(encodeEvent(row)))
148
+ event: (row) => sha256Hex(textEncoder.encode(encodeEvent(row))),
149
+ stateWrite: (row) => sha256Hex(textEncoder.encode(encodeStateWrite(row)))
99
150
  }
100
151
  };
101
152
 
@@ -137,15 +188,19 @@ async function* streamBlockDigests(db, fromBlock, toBlock, batchRows) {
137
188
  if (rows.length === 0)
138
189
  return;
139
190
  for (const row of rows) {
140
- yield semanticDigest.v1.block({
141
- height: Number(row.height),
142
- hash: row.hash,
143
- parent_hash: row.parent_hash,
144
- burn_block_height: Number(row.burn_block_height),
145
- burn_block_hash: row.burn_block_hash ?? null,
146
- index_block_hash: row.index_block_hash ?? null,
147
- timestamp: Number(row.timestamp)
148
- });
191
+ const height = Number(row.height);
192
+ yield {
193
+ height,
194
+ digest: semanticDigest.v1.block({
195
+ height,
196
+ hash: row.hash,
197
+ parent_hash: row.parent_hash,
198
+ burn_block_height: Number(row.burn_block_height),
199
+ burn_block_hash: row.burn_block_hash ?? null,
200
+ index_block_hash: row.index_block_hash ?? null,
201
+ timestamp: Number(row.timestamp)
202
+ })
203
+ };
149
204
  }
150
205
  last = Number(rows[rows.length - 1]?.height);
151
206
  }
@@ -168,19 +223,22 @@ async function* streamTransactionDigests(db, fromBlock, toBlock, batchRows) {
168
223
  if (rows.length === 0)
169
224
  return;
170
225
  for (const row of rows) {
171
- yield semanticDigest.v1.transaction({
172
- tx_id: row.tx_id,
173
- block_height: Number(row.block_height),
174
- tx_index: row.tx_index,
175
- type: row.type,
176
- sender: row.sender,
177
- status: row.status,
178
- contract_id: row.contract_id,
179
- function_name: row.function_name,
180
- function_args: row.function_args ?? null,
181
- raw_result: row.raw_result,
182
- raw_tx: row.raw_tx
183
- });
226
+ yield {
227
+ height: Number(row.block_height),
228
+ digest: semanticDigest.v1.transaction({
229
+ tx_id: row.tx_id,
230
+ block_height: Number(row.block_height),
231
+ tx_index: row.tx_index,
232
+ type: row.type,
233
+ sender: row.sender,
234
+ status: row.status,
235
+ contract_id: row.contract_id,
236
+ function_name: row.function_name,
237
+ function_args: row.function_args ?? null,
238
+ raw_result: row.raw_result,
239
+ raw_tx: row.raw_tx
240
+ })
241
+ };
184
242
  }
185
243
  const tail = rows[rows.length - 1];
186
244
  if (tail) {
@@ -209,13 +267,16 @@ async function* streamEventDigests(db, fromBlock, toBlock, batchRows) {
209
267
  if (rows.length === 0)
210
268
  return;
211
269
  for (const row of rows) {
212
- yield semanticDigest.v1.event({
213
- tx_id: row.tx_id,
214
- block_height: Number(row.block_height),
215
- event_index: row.event_index,
216
- type: row.type,
217
- data: row.data ?? null
218
- });
270
+ yield {
271
+ height: Number(row.block_height),
272
+ digest: semanticDigest.v1.event({
273
+ tx_id: row.tx_id,
274
+ block_height: Number(row.block_height),
275
+ event_index: row.event_index,
276
+ type: row.type,
277
+ data: row.data ?? null
278
+ })
279
+ };
219
280
  }
220
281
  const tail = rows[rows.length - 1];
221
282
  if (tail) {
@@ -227,18 +288,60 @@ async function* streamEventDigests(db, fromBlock, toBlock, batchRows) {
227
288
  }
228
289
  }
229
290
  }
230
- function digestStream(db, dataset, fromBlock, toBlock, batchRows) {
231
- if (dataset === "blocks") {
232
- return streamBlockDigests(db, fromBlock, toBlock, batchRows);
291
+ async function* streamStateWriteDigests(db, fromBlock, toBlock, batchRows) {
292
+ let cursor = null;
293
+ while (true) {
294
+ const result = await sql`
295
+ SELECT w.block_height, w.ordinal, w.tx_index, w.key, w.value_hex
296
+ FROM state_writes w
297
+ JOIN blocks b ON b.height = w.block_height AND b.canonical = true
298
+ WHERE w.block_height >= ${fromBlock} AND w.block_height <= ${toBlock}
299
+ AND (${cursor === null} OR (w.block_height, w.ordinal) >
300
+ (${cursor?.height ?? 0}, ${cursor?.ordinal ?? 0}))
301
+ ORDER BY w.block_height, w.ordinal
302
+ LIMIT ${batchRows}
303
+ `.execute(db);
304
+ const rows = result.rows;
305
+ if (rows.length === 0)
306
+ return;
307
+ for (const row of rows) {
308
+ yield {
309
+ height: Number(row.block_height),
310
+ digest: semanticDigest.v1.stateWrite({
311
+ block_height: Number(row.block_height),
312
+ ordinal: row.ordinal,
313
+ tx_index: row.tx_index,
314
+ key: row.key,
315
+ value_hex: row.value_hex
316
+ })
317
+ };
318
+ }
319
+ const tail = rows[rows.length - 1];
320
+ if (tail) {
321
+ cursor = { height: Number(tail.block_height), ordinal: tail.ordinal };
322
+ }
233
323
  }
234
- if (dataset === "transactions") {
235
- return streamTransactionDigests(db, fromBlock, toBlock, batchRows);
324
+ }
325
+ function streamHeightDigests(db, dataset, fromBlock, toBlock, options = {}) {
326
+ return digestStream(db, dataset, fromBlock, toBlock, options.batchRows ?? DEFAULT_BATCH_ROWS);
327
+ }
328
+ function digestStream(db, dataset, fromBlock, toBlock, batchRows) {
329
+ switch (dataset) {
330
+ case "blocks":
331
+ return streamBlockDigests(db, fromBlock, toBlock, batchRows);
332
+ case "transactions":
333
+ return streamTransactionDigests(db, fromBlock, toBlock, batchRows);
334
+ case "events":
335
+ return streamEventDigests(db, fromBlock, toBlock, batchRows);
336
+ case "state_writes":
337
+ return streamStateWriteDigests(db, fromBlock, toBlock, batchRows);
338
+ default:
339
+ return assertNeverDataset(dataset);
236
340
  }
237
- return streamEventDigests(db, fromBlock, toBlock, batchRows);
238
341
  }
239
342
  async function computePartitionSemanticDigest(db, dataset, fromBlock, toBlock, options = {}) {
240
343
  const rollup = SemanticDigestRollup.forDataset(dataset);
241
- for await (const rowDigest of digestStream(db, dataset, fromBlock, toBlock, options.batchRows ?? DEFAULT_BATCH_ROWS)) {
344
+ for await (const { digest: rowDigest } of digestStream(db, dataset, fromBlock, toBlock, options.batchRows ?? DEFAULT_BATCH_ROWS)) {
242
345
  rollup.appendRowDigest(rowDigest);
243
346
  }
244
347
  return {
@@ -281,8 +384,9 @@ function comparePartitionSemanticDigests(local, reference) {
281
384
  }
282
385
  export {
283
386
  comparePartitionSemanticDigests,
284
- computePartitionSemanticDigest
387
+ computePartitionSemanticDigest,
388
+ streamHeightDigests
285
389
  };
286
390
 
287
- //# debugId=0559E7178C9550A164756E2164756E21
391
+ //# debugId=474576EF266272F564756E2164756E21
288
392
  //# sourceMappingURL=semantic-digest-builder.js.map
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/archive/semantic-digest-builder.ts", "../src/archive/semantic-digest.ts"],
3
+ "sources": ["../src/archive/semantic-digest-builder.ts", "../src/archive/datasets.ts", "../src/archive/semantic-digest.ts"],
4
4
  "sourcesContent": [
5
- "import { type Kysely, sql } from \"kysely\";\nimport type { Database } from \"../db/schema.ts\";\nimport {\n\ttype PartitionSemanticDigest,\n\tSEMANTIC_DIGEST_SPEC_V1,\n\tSemanticDigestRollup,\n\tsemanticDigest,\n} from \"./semantic-digest.ts\";\n\n/**\n * Compute per-partition semantic-v1 digests directly from a database — the same\n * bytes an exporter would produce, without writing Parquet. Callers: the CLI\n * verifier's `--semantic` pass, and (in future) the node replay auditor.\n *\n * SQL and ordering MUST match the archive exporter's stream (see\n * `packages/indexer/src/archive/export-snapshot.ts`). If either drifts, an\n * honest re-computation will disagree with the manifest even though both are\n * correct — and there is no way to tell which is wrong. Any change here needs\n * a matching change there.\n */\n\nexport type CanonicalDataset = \"blocks\" | \"transactions\" | \"events\";\n\nconst DEFAULT_BATCH_ROWS = 20_000;\n\ninterface StreamOptions {\n\tbatchRows?: number;\n}\n\nasync function* streamBlockDigests(\n\tdb: Kysely<Database>,\n\tfromBlock: number,\n\ttoBlock: number,\n\tbatchRows: number,\n): AsyncGenerator<string> {\n\tlet last = fromBlock - 1;\n\twhile (true) {\n\t\tconst rows = await db\n\t\t\t.selectFrom(\"blocks\")\n\t\t\t.select([\n\t\t\t\t\"height\",\n\t\t\t\t\"hash\",\n\t\t\t\t\"parent_hash\",\n\t\t\t\t\"burn_block_height\",\n\t\t\t\t\"burn_block_hash\",\n\t\t\t\t\"index_block_hash\",\n\t\t\t\t\"timestamp\",\n\t\t\t])\n\t\t\t.where(\"canonical\", \"=\", true)\n\t\t\t.where(\"height\", \">\", last)\n\t\t\t.where(\"height\", \"<=\", toBlock)\n\t\t\t.orderBy(\"height\", \"asc\")\n\t\t\t.limit(batchRows)\n\t\t\t.execute();\n\t\tif (rows.length === 0) return;\n\t\tfor (const row of rows) {\n\t\t\tyield semanticDigest.v1.block({\n\t\t\t\theight: Number(row.height),\n\t\t\t\thash: row.hash,\n\t\t\t\tparent_hash: row.parent_hash,\n\t\t\t\tburn_block_height: Number(row.burn_block_height),\n\t\t\t\tburn_block_hash: row.burn_block_hash ?? null,\n\t\t\t\tindex_block_hash: row.index_block_hash ?? null,\n\t\t\t\ttimestamp: Number(row.timestamp),\n\t\t\t});\n\t\t}\n\t\tlast = Number(rows[rows.length - 1]?.height);\n\t}\n}\n\nasync function* streamTransactionDigests(\n\tdb: Kysely<Database>,\n\tfromBlock: number,\n\ttoBlock: number,\n\tbatchRows: number,\n): AsyncGenerator<string> {\n\ttype TxRow = {\n\t\ttx_id: string;\n\t\tblock_height: string | number;\n\t\ttx_index: number;\n\t\ttype: string;\n\t\tsender: string;\n\t\tstatus: string;\n\t\tcontract_id: string | null;\n\t\tfunction_name: string | null;\n\t\tfunction_args: unknown;\n\t\traw_result: string | null;\n\t\traw_tx: string;\n\t};\n\tlet cursor: { height: number; txIndex: number; txId: string } | null = null;\n\twhile (true) {\n\t\tconst result = await sql<TxRow>`\n\t\t\tSELECT t.tx_id, t.block_height, t.tx_index, t.type, t.sender, t.status,\n\t\t\t t.contract_id, t.function_name, t.function_args, t.raw_result, t.raw_tx\n\t\t\t FROM transactions t\n\t\t\t JOIN blocks b ON b.height = t.block_height AND b.canonical = true\n\t\t\t WHERE t.block_height >= ${fromBlock} AND t.block_height <= ${toBlock}\n\t\t\t AND (${cursor === null} OR (t.block_height, t.tx_index, t.tx_id) >\n\t\t\t (${cursor?.height ?? 0}, ${cursor?.txIndex ?? 0}, ${cursor?.txId ?? \"\"}))\n\t\t\t ORDER BY t.block_height, t.tx_index, t.tx_id\n\t\t\t LIMIT ${batchRows}\n\t\t`.execute(db);\n\t\tconst rows: TxRow[] = result.rows;\n\t\tif (rows.length === 0) return;\n\t\tfor (const row of rows) {\n\t\t\tyield semanticDigest.v1.transaction({\n\t\t\t\ttx_id: row.tx_id,\n\t\t\t\tblock_height: Number(row.block_height),\n\t\t\t\ttx_index: row.tx_index,\n\t\t\t\ttype: row.type,\n\t\t\t\tsender: row.sender,\n\t\t\t\tstatus: row.status,\n\t\t\t\tcontract_id: row.contract_id,\n\t\t\t\tfunction_name: row.function_name,\n\t\t\t\tfunction_args: row.function_args ?? null,\n\t\t\t\traw_result: row.raw_result,\n\t\t\t\traw_tx: row.raw_tx,\n\t\t\t});\n\t\t}\n\t\tconst tail = rows[rows.length - 1];\n\t\tif (tail) {\n\t\t\tcursor = {\n\t\t\t\theight: Number(tail.block_height),\n\t\t\t\ttxIndex: tail.tx_index,\n\t\t\t\ttxId: tail.tx_id,\n\t\t\t};\n\t\t}\n\t}\n}\n\nasync function* streamEventDigests(\n\tdb: Kysely<Database>,\n\tfromBlock: number,\n\ttoBlock: number,\n\tbatchRows: number,\n): AsyncGenerator<string> {\n\ttype EventRow = {\n\t\ttx_id: string;\n\t\tblock_height: string | number;\n\t\tevent_index: number;\n\t\ttype: string;\n\t\tdata: unknown;\n\t};\n\tlet cursor: { height: number; eventIndex: number; txId: string } | null =\n\t\tnull;\n\twhile (true) {\n\t\tconst result = await sql<EventRow>`\n\t\t\tSELECT e.tx_id, e.block_height, e.event_index, e.type, e.data\n\t\t\t FROM events e\n\t\t\t JOIN blocks b ON b.height = e.block_height AND b.canonical = true\n\t\t\t WHERE e.block_height >= ${fromBlock} AND e.block_height <= ${toBlock}\n\t\t\t AND (${cursor === null} OR (e.block_height, e.event_index, e.tx_id) >\n\t\t\t (${cursor?.height ?? 0}, ${cursor?.eventIndex ?? 0}, ${cursor?.txId ?? \"\"}))\n\t\t\t ORDER BY e.block_height, e.event_index, e.tx_id\n\t\t\t LIMIT ${batchRows}\n\t\t`.execute(db);\n\t\tconst rows: EventRow[] = result.rows;\n\t\tif (rows.length === 0) return;\n\t\tfor (const row of rows) {\n\t\t\tyield semanticDigest.v1.event({\n\t\t\t\ttx_id: row.tx_id,\n\t\t\t\tblock_height: Number(row.block_height),\n\t\t\t\tevent_index: row.event_index,\n\t\t\t\ttype: row.type,\n\t\t\t\tdata: row.data ?? null,\n\t\t\t});\n\t\t}\n\t\tconst tail = rows[rows.length - 1];\n\t\tif (tail) {\n\t\t\tcursor = {\n\t\t\t\theight: Number(tail.block_height),\n\t\t\t\teventIndex: tail.event_index,\n\t\t\t\ttxId: tail.tx_id,\n\t\t\t};\n\t\t}\n\t}\n}\n\nfunction digestStream(\n\tdb: Kysely<Database>,\n\tdataset: CanonicalDataset,\n\tfromBlock: number,\n\ttoBlock: number,\n\tbatchRows: number,\n): AsyncGenerator<string> {\n\tif (dataset === \"blocks\") {\n\t\treturn streamBlockDigests(db, fromBlock, toBlock, batchRows);\n\t}\n\tif (dataset === \"transactions\") {\n\t\treturn streamTransactionDigests(db, fromBlock, toBlock, batchRows);\n\t}\n\treturn streamEventDigests(db, fromBlock, toBlock, batchRows);\n}\n\n/**\n * Compute one partition's semantic-v1 digest. Empty ranges return `null` — an\n * empty range has no digest, distinct from \"digest of zero rows\" which would\n * compare equal across datasets.\n */\nexport async function computePartitionSemanticDigest(\n\tdb: Kysely<Database>,\n\tdataset: CanonicalDataset,\n\tfromBlock: number,\n\ttoBlock: number,\n\toptions: StreamOptions = {},\n): Promise<PartitionSemanticDigest> {\n\tconst rollup = SemanticDigestRollup.forDataset(dataset);\n\tfor await (const rowDigest of digestStream(\n\t\tdb,\n\t\tdataset,\n\t\tfromBlock,\n\t\ttoBlock,\n\t\toptions.batchRows ?? DEFAULT_BATCH_ROWS,\n\t)) {\n\t\trollup.appendRowDigest(rowDigest);\n\t}\n\treturn {\n\t\tdataset,\n\t\tfrom_block: fromBlock,\n\t\tto_block: toBlock,\n\t\trow_count: rollup.rowCount(),\n\t\tdigest: rollup.digest(),\n\t\tdigest_spec: SEMANTIC_DIGEST_SPEC_V1,\n\t};\n}\n\nexport type PartitionSemanticComparison = {\n\tdataset: CanonicalDataset;\n\tfrom_block: number;\n\tto_block: number;\n\tstatus: \"match\" | \"digest-mismatch\" | \"count-mismatch\" | \"missing-locally\";\n\texpected_digest: string | null;\n\tactual_digest: string | null;\n\texpected_rows: number;\n\tactual_rows: number;\n};\n\nexport function comparePartitionSemanticDigests(\n\tlocal: readonly PartitionSemanticDigest[],\n\treference: readonly PartitionSemanticDigest[],\n): PartitionSemanticComparison[] {\n\tconst localByKey = new Map(\n\t\tlocal.map((d) => [`${d.dataset}:${d.from_block}-${d.to_block}`, d]),\n\t);\n\treturn reference.map((expected) => {\n\t\tconst key = `${expected.dataset}:${expected.from_block}-${expected.to_block}`;\n\t\tconst actual = localByKey.get(key);\n\t\tconst base = {\n\t\t\tdataset: expected.dataset,\n\t\t\tfrom_block: expected.from_block,\n\t\t\tto_block: expected.to_block,\n\t\t\texpected_digest: expected.digest,\n\t\t\texpected_rows: expected.row_count,\n\t\t};\n\t\tif (!actual) {\n\t\t\treturn {\n\t\t\t\t...base,\n\t\t\t\tstatus: \"missing-locally\" as const,\n\t\t\t\tactual_digest: null,\n\t\t\t\tactual_rows: 0,\n\t\t\t};\n\t\t}\n\t\tconst status =\n\t\t\tactual.row_count !== expected.row_count\n\t\t\t\t? (\"count-mismatch\" as const)\n\t\t\t\t: actual.digest === expected.digest\n\t\t\t\t\t? (\"match\" as const)\n\t\t\t\t\t: (\"digest-mismatch\" as const);\n\t\treturn {\n\t\t\t...base,\n\t\t\tstatus,\n\t\t\tactual_digest: actual.digest,\n\t\t\tactual_rows: actual.row_count,\n\t\t};\n\t});\n}\n",
6
- "import { createHash } from \"node:crypto\";\n\n/**\n * v1 canonical byte encoding for chain identity — the shape that lets\n * independently-run runtimes agree on the same block.\n *\n * The exact-body digest (`sha256:parquet-object`) proves capture fidelity to one\n * source: replay the same bytes, get the same rows. It cannot prove two nodes\n * observed the same chain, because their Parquet encoders legally differ in\n * column padding, compression, JSON key order, and null placement — two\n * operators processing the same block can ship byte-different Parquets that\n * carry identical content.\n *\n * The semantic digest is the shape that lets independent runtimes agree. Every\n * value on a block/transaction/event is normalized into a canonical byte string\n * by rules that MUST NOT change without a version bump. Two v1 computations\n * over the same range agree, or one is wrong about the chain — never about\n * formatting.\n *\n * Encoding rules (v1):\n * - Fields join with `\\x1f` (ASCII US, unit separator — cannot legally\n * appear in any chain value; a printable delimiter risks collision with\n * contract-emitted strings).\n * - Null encodes as `\\x00` (single NUL). Distinct from empty string `\"\"`.\n * - Integers encode as their base-10 string. bigint → decimal string.\n * - JSON payload columns (function_args, event data) encode as canonical\n * JSON: object keys sorted lexicographically, no whitespace, bigint →\n * quoted decimal string, non-finite numbers rejected.\n * - Field order per record type is fixed and never reordered.\n * - Digest = sha256(bytes), lowercase hex.\n *\n * Rollup rules (v1) — order MUST match the archive's stream order or an\n * honest re-export will disagree with the manifest:\n * - blocks: (height asc)\n * - transactions: (block_height asc, tx_index asc, tx_id asc)\n * - events: (block_height asc, event_index asc, tx_id asc)\n * Per-partition rollup is `sha256(concat(row_digest_bytes))` in stream order.\n *\n * Test vectors under `packages/shared/src/archive/__fixtures__/semantic-digest/`\n * pin the encoding. Any change to bytes must bump the spec version.\n */\n\nexport const SEMANTIC_DIGEST_SPEC_V1 = \"sha256:semantic-v1\" as const;\nexport type SemanticDigestSpec = typeof SEMANTIC_DIGEST_SPEC_V1;\n\nconst FIELD_SEP = \"\\x1f\";\nconst NULL_MARKER = \"\\x00\";\n\nconst textEncoder = new TextEncoder();\n\n/**\n * Stable JSON — sorted keys, no whitespace, bigint → decimal string. Distinct\n * from `JSON.stringify(x)` which respects insertion order and rejects bigints.\n * Also rejects non-finite numbers, since NaN/Infinity have no on-chain meaning\n * and would silently hash as `null` under `JSON.stringify`.\n */\nexport function canonicalJson(value: unknown): string {\n\tif (value === null) return \"null\";\n\tif (typeof value === \"string\" || typeof value === \"boolean\") {\n\t\treturn JSON.stringify(value);\n\t}\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) {\n\t\t\tthrow new Error(\"canonicalJson: non-finite number\");\n\t\t}\n\t\treturn JSON.stringify(value);\n\t}\n\tif (typeof value === \"bigint\") return JSON.stringify(value.toString());\n\tif (Array.isArray(value)) {\n\t\treturn `[${value.map((item) => canonicalJson(item)).join(\",\")}]`;\n\t}\n\tif (typeof value === \"object\") {\n\t\tconst object = value as Record<string, unknown>;\n\t\treturn `{${Object.keys(object)\n\t\t\t.sort()\n\t\t\t.map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`)\n\t\t\t.join(\",\")}}`;\n\t}\n\t// undefined / function / symbol — never present in decoded chain rows.\n\t// Fall through as null so tests catch unexpected shapes.\n\treturn \"null\";\n}\n\nfunction scalar(value: string | number | bigint | null | undefined): string {\n\tif (value === null || value === undefined) return NULL_MARKER;\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) {\n\t\t\tthrow new Error(\"semanticDigest: non-finite number\");\n\t\t}\n\t\tif (!Number.isInteger(value)) {\n\t\t\t// Chain heights, indexes, timestamps are all integer; a fractional\n\t\t\t// value here is a schema bug we want to surface loudly, not silently\n\t\t\t// serialize with float precision that varies by runtime.\n\t\t\tthrow new Error(`semanticDigest: non-integer scalar ${value}`);\n\t\t}\n\t\treturn String(value);\n\t}\n\tif (typeof value === \"bigint\") return value.toString();\n\treturn value;\n}\n\nfunction jsonScalar(value: unknown): string {\n\treturn value === null || value === undefined\n\t\t? NULL_MARKER\n\t\t: canonicalJson(value);\n}\n\nfunction sha256Hex(input: Uint8Array | string): string {\n\treturn createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\nexport interface BlockDigestInput {\n\theight: number | bigint;\n\thash: string;\n\tparent_hash: string;\n\tburn_block_height: number | bigint;\n\tburn_block_hash: string | null;\n\tindex_block_hash: string | null;\n\ttimestamp: number | bigint;\n}\n\nexport interface TransactionDigestInput {\n\ttx_id: string;\n\tblock_height: number | bigint;\n\ttx_index: number;\n\ttype: string;\n\tsender: string;\n\tstatus: string;\n\tcontract_id: string | null;\n\tfunction_name: string | null;\n\tfunction_args: unknown;\n\traw_result: string | null;\n\traw_tx: string;\n}\n\nexport interface EventDigestInput {\n\ttx_id: string;\n\tblock_height: number | bigint;\n\tevent_index: number;\n\ttype: string;\n\tdata: unknown;\n}\n\nfunction encodeBlock(row: BlockDigestInput): string {\n\treturn [\n\t\tscalar(row.height),\n\t\tscalar(row.hash),\n\t\tscalar(row.parent_hash),\n\t\tscalar(row.burn_block_height),\n\t\tscalar(row.burn_block_hash),\n\t\tscalar(row.index_block_hash),\n\t\tscalar(row.timestamp),\n\t].join(FIELD_SEP);\n}\n\nfunction encodeTransaction(row: TransactionDigestInput): string {\n\treturn [\n\t\tscalar(row.tx_id),\n\t\tscalar(row.block_height),\n\t\tscalar(row.tx_index),\n\t\tscalar(row.type),\n\t\tscalar(row.sender),\n\t\tscalar(row.status),\n\t\tscalar(row.contract_id),\n\t\tscalar(row.function_name),\n\t\tjsonScalar(row.function_args),\n\t\tscalar(row.raw_result),\n\t\tscalar(row.raw_tx),\n\t].join(FIELD_SEP);\n}\n\nfunction encodeEvent(row: EventDigestInput): string {\n\treturn [\n\t\tscalar(row.tx_id),\n\t\tscalar(row.block_height),\n\t\tscalar(row.event_index),\n\t\tscalar(row.type),\n\t\tjsonScalar(row.data),\n\t].join(FIELD_SEP);\n}\n\n/**\n * v1 per-row digests. Each is `sha256(encoded_row_bytes)` in lowercase hex.\n * The encoded byte form is stable across runtimes; the sha256 is stable\n * across CPU architectures. Never mutate an encoder without a v-bump.\n */\nexport interface SemanticDigestV1 {\n\treadonly spec: SemanticDigestSpec;\n\treadonly encodeBlock: (row: BlockDigestInput) => string;\n\treadonly encodeTransaction: (row: TransactionDigestInput) => string;\n\treadonly encodeEvent: (row: EventDigestInput) => string;\n\treadonly block: (row: BlockDigestInput) => string;\n\treadonly transaction: (row: TransactionDigestInput) => string;\n\treadonly event: (row: EventDigestInput) => string;\n}\n\nexport interface SemanticDigestApi {\n\treadonly v1: SemanticDigestV1;\n}\n\nexport const semanticDigest: SemanticDigestApi = {\n\tv1: {\n\t\tspec: SEMANTIC_DIGEST_SPEC_V1,\n\t\tencodeBlock,\n\t\tencodeTransaction,\n\t\tencodeEvent,\n\t\tblock: (row: BlockDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeBlock(row))),\n\t\ttransaction: (row: TransactionDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeTransaction(row))),\n\t\tevent: (row: EventDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeEvent(row))),\n\t},\n};\n\n/**\n * Streaming rollup — feed rows one at a time in their declared order, read the\n * rolled digest at the end. Two callers passing the same rows in the same order\n * produce the same rollup, regardless of batching.\n *\n * The rollup composes: partition_digest = sha256(concat(row_digest_bytes)),\n * where each row_digest_bytes = sha256(encode(row)). This is why the streaming\n * implementation can be constant-memory — the outer sha256 accepts row digests\n * incrementally.\n */\nexport class SemanticDigestRollup {\n\tprivate readonly hasher = createHash(\"sha256\");\n\tprivate count = 0;\n\n\tstatic forDataset(\n\t\t_dataset: \"blocks\" | \"transactions\" | \"events\",\n\t): SemanticDigestRollup {\n\t\treturn new SemanticDigestRollup();\n\t}\n\n\tappendRowDigest(rowDigestHex: string): void {\n\t\tthis.hasher.update(Buffer.from(rowDigestHex, \"hex\"));\n\t\tthis.count += 1;\n\t}\n\n\trowCount(): number {\n\t\treturn this.count;\n\t}\n\n\t/** Empty rollups return `null`, not the sha256 of nothing, so an empty\n\t * range never compares equal to another empty range on a different\n\t * dataset. Callers assign `null` semantics deliberately. */\n\tdigest(): string | null {\n\t\treturn this.count === 0 ? null : this.hasher.copy().digest(\"hex\");\n\t}\n\n\tspec(): SemanticDigestSpec {\n\t\treturn SEMANTIC_DIGEST_SPEC_V1;\n\t}\n}\n\n/**\n * Per-partition digest record — the shape that lands in the snapshot manifest.\n * Complements the existing `sha256:parquet-object` per-partition digest by\n * proving semantic identity independent of file encoding.\n */\nexport interface PartitionSemanticDigest {\n\tdataset: \"blocks\" | \"transactions\" | \"events\";\n\tfrom_block: number;\n\tto_block: number;\n\trow_count: number;\n\tdigest: string | null;\n\tdigest_spec: SemanticDigestSpec;\n}\n"
5
+ "import { type Kysely, sql } from \"kysely\";\nimport type { Database } from \"../db/schema.ts\";\nimport { type ArchiveDataset, assertNeverDataset } from \"./datasets.ts\";\nimport {\n\ttype PartitionSemanticDigest,\n\tSEMANTIC_DIGEST_SPEC_V1,\n\tSemanticDigestRollup,\n\tsemanticDigest,\n} from \"./semantic-digest.ts\";\n\n/**\n * Compute per-partition semantic-v1 digests directly from a database — the same\n * bytes an exporter would produce, without writing Parquet. Callers: the CLI\n * verifier's `--semantic` pass, and (in future) the node replay auditor.\n *\n * SQL and ordering MUST match the archive exporter's stream (see\n * `packages/indexer/src/archive/export-snapshot.ts`). If either drifts, an\n * honest re-computation will disagree with the manifest even though both are\n * correct — and there is no way to tell which is wrong. Any change here needs\n * a matching change there.\n */\n\nexport type CanonicalDataset = ArchiveDataset;\n\nconst DEFAULT_BATCH_ROWS = 20_000;\n\n/** One row's semantic-v1 digest with the height it belongs to. */\nexport type HeightDigest = { height: number; digest: string };\n\ninterface StreamOptions {\n\tbatchRows?: number;\n}\n\nasync function* streamBlockDigests(\n\tdb: Kysely<Database>,\n\tfromBlock: number,\n\ttoBlock: number,\n\tbatchRows: number,\n): AsyncGenerator<HeightDigest> {\n\tlet last = fromBlock - 1;\n\twhile (true) {\n\t\tconst rows = await db\n\t\t\t.selectFrom(\"blocks\")\n\t\t\t.select([\n\t\t\t\t\"height\",\n\t\t\t\t\"hash\",\n\t\t\t\t\"parent_hash\",\n\t\t\t\t\"burn_block_height\",\n\t\t\t\t\"burn_block_hash\",\n\t\t\t\t\"index_block_hash\",\n\t\t\t\t\"timestamp\",\n\t\t\t])\n\t\t\t.where(\"canonical\", \"=\", true)\n\t\t\t.where(\"height\", \">\", last)\n\t\t\t.where(\"height\", \"<=\", toBlock)\n\t\t\t.orderBy(\"height\", \"asc\")\n\t\t\t.limit(batchRows)\n\t\t\t.execute();\n\t\tif (rows.length === 0) return;\n\t\tfor (const row of rows) {\n\t\t\tconst height = Number(row.height);\n\t\t\tyield {\n\t\t\t\theight,\n\t\t\t\tdigest: semanticDigest.v1.block({\n\t\t\t\t\theight,\n\t\t\t\t\thash: row.hash,\n\t\t\t\t\tparent_hash: row.parent_hash,\n\t\t\t\t\tburn_block_height: Number(row.burn_block_height),\n\t\t\t\t\tburn_block_hash: row.burn_block_hash ?? null,\n\t\t\t\t\tindex_block_hash: row.index_block_hash ?? null,\n\t\t\t\t\ttimestamp: Number(row.timestamp),\n\t\t\t\t}),\n\t\t\t};\n\t\t}\n\t\tlast = Number(rows[rows.length - 1]?.height);\n\t}\n}\n\nasync function* streamTransactionDigests(\n\tdb: Kysely<Database>,\n\tfromBlock: number,\n\ttoBlock: number,\n\tbatchRows: number,\n): AsyncGenerator<HeightDigest> {\n\ttype TxRow = {\n\t\ttx_id: string;\n\t\tblock_height: string | number;\n\t\ttx_index: number;\n\t\ttype: string;\n\t\tsender: string;\n\t\tstatus: string;\n\t\tcontract_id: string | null;\n\t\tfunction_name: string | null;\n\t\tfunction_args: unknown;\n\t\traw_result: string | null;\n\t\traw_tx: string;\n\t};\n\tlet cursor: { height: number; txIndex: number; txId: string } | null = null;\n\twhile (true) {\n\t\tconst result = await sql<TxRow>`\n\t\t\tSELECT t.tx_id, t.block_height, t.tx_index, t.type, t.sender, t.status,\n\t\t\t t.contract_id, t.function_name, t.function_args, t.raw_result, t.raw_tx\n\t\t\t FROM transactions t\n\t\t\t JOIN blocks b ON b.height = t.block_height AND b.canonical = true\n\t\t\t WHERE t.block_height >= ${fromBlock} AND t.block_height <= ${toBlock}\n\t\t\t AND (${cursor === null} OR (t.block_height, t.tx_index, t.tx_id) >\n\t\t\t (${cursor?.height ?? 0}, ${cursor?.txIndex ?? 0}, ${cursor?.txId ?? \"\"}))\n\t\t\t ORDER BY t.block_height, t.tx_index, t.tx_id\n\t\t\t LIMIT ${batchRows}\n\t\t`.execute(db);\n\t\tconst rows: TxRow[] = result.rows;\n\t\tif (rows.length === 0) return;\n\t\tfor (const row of rows) {\n\t\t\tyield {\n\t\t\t\theight: Number(row.block_height),\n\t\t\t\tdigest: semanticDigest.v1.transaction({\n\t\t\t\t\ttx_id: row.tx_id,\n\t\t\t\t\tblock_height: Number(row.block_height),\n\t\t\t\t\ttx_index: row.tx_index,\n\t\t\t\t\ttype: row.type,\n\t\t\t\t\tsender: row.sender,\n\t\t\t\t\tstatus: row.status,\n\t\t\t\t\tcontract_id: row.contract_id,\n\t\t\t\t\tfunction_name: row.function_name,\n\t\t\t\t\tfunction_args: row.function_args ?? null,\n\t\t\t\t\traw_result: row.raw_result,\n\t\t\t\t\traw_tx: row.raw_tx,\n\t\t\t\t}),\n\t\t\t};\n\t\t}\n\t\tconst tail = rows[rows.length - 1];\n\t\tif (tail) {\n\t\t\tcursor = {\n\t\t\t\theight: Number(tail.block_height),\n\t\t\t\ttxIndex: tail.tx_index,\n\t\t\t\ttxId: tail.tx_id,\n\t\t\t};\n\t\t}\n\t}\n}\n\nasync function* streamEventDigests(\n\tdb: Kysely<Database>,\n\tfromBlock: number,\n\ttoBlock: number,\n\tbatchRows: number,\n): AsyncGenerator<HeightDigest> {\n\ttype EventRow = {\n\t\ttx_id: string;\n\t\tblock_height: string | number;\n\t\tevent_index: number;\n\t\ttype: string;\n\t\tdata: unknown;\n\t};\n\tlet cursor: { height: number; eventIndex: number; txId: string } | null =\n\t\tnull;\n\twhile (true) {\n\t\tconst result = await sql<EventRow>`\n\t\t\tSELECT e.tx_id, e.block_height, e.event_index, e.type, e.data\n\t\t\t FROM events e\n\t\t\t JOIN blocks b ON b.height = e.block_height AND b.canonical = true\n\t\t\t WHERE e.block_height >= ${fromBlock} AND e.block_height <= ${toBlock}\n\t\t\t AND (${cursor === null} OR (e.block_height, e.event_index, e.tx_id) >\n\t\t\t (${cursor?.height ?? 0}, ${cursor?.eventIndex ?? 0}, ${cursor?.txId ?? \"\"}))\n\t\t\t ORDER BY e.block_height, e.event_index, e.tx_id\n\t\t\t LIMIT ${batchRows}\n\t\t`.execute(db);\n\t\tconst rows: EventRow[] = result.rows;\n\t\tif (rows.length === 0) return;\n\t\tfor (const row of rows) {\n\t\t\tyield {\n\t\t\t\theight: Number(row.block_height),\n\t\t\t\tdigest: semanticDigest.v1.event({\n\t\t\t\t\ttx_id: row.tx_id,\n\t\t\t\t\tblock_height: Number(row.block_height),\n\t\t\t\t\tevent_index: row.event_index,\n\t\t\t\t\ttype: row.type,\n\t\t\t\t\tdata: row.data ?? null,\n\t\t\t\t}),\n\t\t\t};\n\t\t}\n\t\tconst tail = rows[rows.length - 1];\n\t\tif (tail) {\n\t\t\tcursor = {\n\t\t\t\theight: Number(tail.block_height),\n\t\t\t\teventIndex: tail.event_index,\n\t\t\t\ttxId: tail.tx_id,\n\t\t\t};\n\t\t}\n\t}\n}\n\nasync function* streamStateWriteDigests(\n\tdb: Kysely<Database>,\n\tfromBlock: number,\n\ttoBlock: number,\n\tbatchRows: number,\n): AsyncGenerator<HeightDigest> {\n\ttype StateWriteRow = {\n\t\tblock_height: string | number;\n\t\tordinal: number;\n\t\ttx_index: number | null;\n\t\tkey: string;\n\t\tvalue_hex: string;\n\t};\n\tlet cursor: { height: number; ordinal: number } | null = null;\n\twhile (true) {\n\t\tconst result = await sql<StateWriteRow>`\n\t\t\tSELECT w.block_height, w.ordinal, w.tx_index, w.key, w.value_hex\n\t\t\t FROM state_writes w\n\t\t\t JOIN blocks b ON b.height = w.block_height AND b.canonical = true\n\t\t\t WHERE w.block_height >= ${fromBlock} AND w.block_height <= ${toBlock}\n\t\t\t AND (${cursor === null} OR (w.block_height, w.ordinal) >\n\t\t\t (${cursor?.height ?? 0}, ${cursor?.ordinal ?? 0}))\n\t\t\t ORDER BY w.block_height, w.ordinal\n\t\t\t LIMIT ${batchRows}\n\t\t`.execute(db);\n\t\tconst rows: StateWriteRow[] = result.rows;\n\t\tif (rows.length === 0) return;\n\t\tfor (const row of rows) {\n\t\t\tyield {\n\t\t\t\theight: Number(row.block_height),\n\t\t\t\tdigest: semanticDigest.v1.stateWrite({\n\t\t\t\t\tblock_height: Number(row.block_height),\n\t\t\t\t\tordinal: row.ordinal,\n\t\t\t\t\ttx_index: row.tx_index,\n\t\t\t\t\tkey: row.key,\n\t\t\t\t\tvalue_hex: row.value_hex,\n\t\t\t\t}),\n\t\t\t};\n\t\t}\n\t\tconst tail = rows[rows.length - 1];\n\t\tif (tail) {\n\t\t\tcursor = { height: Number(tail.block_height), ordinal: tail.ordinal };\n\t\t}\n\t}\n}\n\n/**\n * Row digests with their heights, in the exporter's total order. Additive:\n * the per-height sidecar rebuild needs the height the exporter fed its\n * accumulator, which the digest string alone does not carry.\n */\nexport function streamHeightDigests(\n\tdb: Kysely<Database>,\n\tdataset: CanonicalDataset,\n\tfromBlock: number,\n\ttoBlock: number,\n\toptions: StreamOptions = {},\n): AsyncGenerator<HeightDigest> {\n\treturn digestStream(\n\t\tdb,\n\t\tdataset,\n\t\tfromBlock,\n\t\ttoBlock,\n\t\toptions.batchRows ?? DEFAULT_BATCH_ROWS,\n\t);\n}\n\nfunction digestStream(\n\tdb: Kysely<Database>,\n\tdataset: CanonicalDataset,\n\tfromBlock: number,\n\ttoBlock: number,\n\tbatchRows: number,\n): AsyncGenerator<HeightDigest> {\n\tswitch (dataset) {\n\t\tcase \"blocks\":\n\t\t\treturn streamBlockDigests(db, fromBlock, toBlock, batchRows);\n\t\tcase \"transactions\":\n\t\t\treturn streamTransactionDigests(db, fromBlock, toBlock, batchRows);\n\t\tcase \"events\":\n\t\t\treturn streamEventDigests(db, fromBlock, toBlock, batchRows);\n\t\tcase \"state_writes\":\n\t\t\treturn streamStateWriteDigests(db, fromBlock, toBlock, batchRows);\n\t\tdefault:\n\t\t\treturn assertNeverDataset(dataset);\n\t}\n}\n\n/**\n * Compute one partition's semantic-v1 digest. Empty ranges return `null` — an\n * empty range has no digest, distinct from \"digest of zero rows\" which would\n * compare equal across datasets.\n */\nexport async function computePartitionSemanticDigest(\n\tdb: Kysely<Database>,\n\tdataset: CanonicalDataset,\n\tfromBlock: number,\n\ttoBlock: number,\n\toptions: StreamOptions = {},\n): Promise<PartitionSemanticDigest> {\n\tconst rollup = SemanticDigestRollup.forDataset(dataset);\n\tfor await (const { digest: rowDigest } of digestStream(\n\t\tdb,\n\t\tdataset,\n\t\tfromBlock,\n\t\ttoBlock,\n\t\toptions.batchRows ?? DEFAULT_BATCH_ROWS,\n\t)) {\n\t\trollup.appendRowDigest(rowDigest);\n\t}\n\treturn {\n\t\tdataset,\n\t\tfrom_block: fromBlock,\n\t\tto_block: toBlock,\n\t\trow_count: rollup.rowCount(),\n\t\tdigest: rollup.digest(),\n\t\tdigest_spec: SEMANTIC_DIGEST_SPEC_V1,\n\t};\n}\n\nexport type PartitionSemanticComparison = {\n\tdataset: CanonicalDataset;\n\tfrom_block: number;\n\tto_block: number;\n\tstatus: \"match\" | \"digest-mismatch\" | \"count-mismatch\" | \"missing-locally\";\n\texpected_digest: string | null;\n\tactual_digest: string | null;\n\texpected_rows: number;\n\tactual_rows: number;\n};\n\nexport function comparePartitionSemanticDigests(\n\tlocal: readonly PartitionSemanticDigest[],\n\treference: readonly PartitionSemanticDigest[],\n): PartitionSemanticComparison[] {\n\tconst localByKey = new Map(\n\t\tlocal.map((d) => [`${d.dataset}:${d.from_block}-${d.to_block}`, d]),\n\t);\n\treturn reference.map((expected) => {\n\t\tconst key = `${expected.dataset}:${expected.from_block}-${expected.to_block}`;\n\t\tconst actual = localByKey.get(key);\n\t\tconst base = {\n\t\t\tdataset: expected.dataset,\n\t\t\tfrom_block: expected.from_block,\n\t\t\tto_block: expected.to_block,\n\t\t\texpected_digest: expected.digest,\n\t\t\texpected_rows: expected.row_count,\n\t\t};\n\t\tif (!actual) {\n\t\t\treturn {\n\t\t\t\t...base,\n\t\t\t\tstatus: \"missing-locally\" as const,\n\t\t\t\tactual_digest: null,\n\t\t\t\tactual_rows: 0,\n\t\t\t};\n\t\t}\n\t\tconst status =\n\t\t\tactual.row_count !== expected.row_count\n\t\t\t\t? (\"count-mismatch\" as const)\n\t\t\t\t: actual.digest === expected.digest\n\t\t\t\t\t? (\"match\" as const)\n\t\t\t\t\t: (\"digest-mismatch\" as const);\n\t\treturn {\n\t\t\t...base,\n\t\t\tstatus,\n\t\t\tactual_digest: actual.digest,\n\t\t\tactual_rows: actual.row_count,\n\t\t};\n\t});\n}\n",
6
+ "/**\n * The canonical archive's datasets — the one list every exporter, loader,\n * verifier and fetch gate derives from.\n *\n * Order is load order: each dataset's foreign keys point only at datasets\n * earlier in the list, so loading in this order never violates a constraint.\n *\n * Per-dataset code branches with an exhaustive `switch` ending in\n * `assertNeverDataset`, never a trailing default: adding a dataset here must\n * fail to compile everywhere it needs its own schema, SQL or table, instead of\n * silently inheriting another dataset's.\n */\nexport const ARCHIVE_DATASETS = [\n\t\"blocks\",\n\t\"transactions\",\n\t\"events\",\n\t\"state_writes\",\n] as const;\n\n/** One archive dataset: blocks, transactions, events or state writes. */\nexport type ArchiveDataset = (typeof ARCHIVE_DATASETS)[number];\n\nexport function isArchiveDataset(value: string): value is ArchiveDataset {\n\treturn (ARCHIVE_DATASETS as readonly string[]).includes(value);\n}\n\n/**\n * Datasets a manifest may leave out. The exporter publishes `state_writes`\n * only from an instance that holds it from the start of the exported range,\n * so a manifest without it means \"not published\", never \"empty\": consumers\n * must not treat its absence as zero rows, or as rows missing locally.\n * The core datasets are always published; a manifest that lacks one of them\n * is incomplete, and consumers say so.\n */\nconst OPTIONAL_DATASET: Record<ArchiveDataset, boolean> = {\n\tblocks: false,\n\ttransactions: false,\n\tevents: false,\n\tstate_writes: true,\n};\n\nexport function isOptionalDataset(dataset: ArchiveDataset): boolean {\n\treturn OPTIONAL_DATASET[dataset];\n}\n\n/**\n * Whether a manifest publishes a dataset: always for a core dataset, and for\n * an optional one only when it names at least one partition or zero-record\n * range of it.\n */\nexport function manifestPublishes(\n\tmanifest: {\n\t\tpartitions?: ReadonlyArray<{ dataset: string }>;\n\t\tzero_record_ranges?: ReadonlyArray<{ dataset: string }>;\n\t},\n\tdataset: ArchiveDataset,\n): boolean {\n\tif (!isOptionalDataset(dataset)) return true;\n\treturn (\n\t\t(manifest.partitions ?? []).some((p) => p.dataset === dataset) ||\n\t\t(manifest.zero_record_ranges ?? []).some((z) => z.dataset === dataset)\n\t);\n}\n\n/**\n * The column a dataset's rows are placed at a height by. Each dataset is\n * stored in the table of the same name.\n */\nexport function archiveHeightColumn(\n\tdataset: ArchiveDataset,\n): \"height\" | \"block_height\" {\n\tswitch (dataset) {\n\t\tcase \"blocks\":\n\t\t\treturn \"height\";\n\t\tcase \"transactions\":\n\t\tcase \"events\":\n\t\tcase \"state_writes\":\n\t\t\treturn \"block_height\";\n\t\tdefault:\n\t\t\treturn assertNeverDataset(dataset);\n\t}\n}\n\nexport function assertNeverDataset(dataset: never): never {\n\tthrow new Error(`unhandled archive dataset: ${String(dataset)}`);\n}\n",
7
+ "import { createHash } from \"node:crypto\";\nimport type { ArchiveDataset } from \"./datasets.ts\";\n\n/**\n * v1 canonical byte encoding for chain identity — the shape that lets\n * independently-run runtimes agree on the same block.\n *\n * The exact-body digest (`sha256:parquet-object`) proves capture fidelity to one\n * source: replay the same bytes, get the same rows. It cannot prove two nodes\n * observed the same chain, because their Parquet encoders legally differ in\n * column padding, compression, JSON key order, and null placement — two\n * operators processing the same block can ship byte-different Parquets that\n * carry identical content.\n *\n * The semantic digest is the shape that lets independent runtimes agree. Every\n * value on a block/transaction/event is normalized into a canonical byte string\n * by rules that MUST NOT change without a version bump. Two v1 computations\n * over the same range agree, or one is wrong about the chain — never about\n * formatting.\n *\n * Encoding rules (v1):\n * - Fields join with `\\x1f` (ASCII US, unit separator — cannot legally\n * appear in any chain value; a printable delimiter risks collision with\n * contract-emitted strings).\n * - Null encodes as `\\x00` (single NUL). Distinct from empty string `\"\"`.\n * - Integers encode as their base-10 string. bigint → decimal string.\n * - JSON payload columns (function_args, event data) encode as canonical\n * JSON: object keys sorted lexicographically, no whitespace, bigint →\n * quoted decimal string, non-finite numbers rejected.\n * - Field order per record type is fixed and never reordered.\n * - Digest = sha256(bytes), lowercase hex.\n *\n * Rollup rules (v1) — order MUST match the archive's stream order or an\n * honest re-export will disagree with the manifest:\n * - blocks: (height asc)\n * - transactions: (block_height asc, tx_index asc, tx_id asc)\n * - events: (block_height asc, event_index asc, tx_id asc)\n * - state_writes: (block_height asc, ordinal asc)\n * Per-partition rollup is `sha256(concat(row_digest_bytes))` in stream order.\n *\n * Test vectors under `packages/shared/src/archive/__fixtures__/semantic-digest/`\n * pin the encoding. Any change to bytes must bump the spec version.\n */\n\nexport const SEMANTIC_DIGEST_SPEC_V1 = \"sha256:semantic-v1\" as const;\nexport type SemanticDigestSpec = typeof SEMANTIC_DIGEST_SPEC_V1;\n\nconst FIELD_SEP = \"\\x1f\";\nconst NULL_MARKER = \"\\x00\";\n\nconst textEncoder = new TextEncoder();\n\n/**\n * Stable JSON — sorted keys, no whitespace, bigint → decimal string. Distinct\n * from `JSON.stringify(x)` which respects insertion order and rejects bigints.\n * Also rejects non-finite numbers, since NaN/Infinity have no on-chain meaning\n * and would silently hash as `null` under `JSON.stringify`.\n */\nexport function canonicalJson(value: unknown): string {\n\tif (value === null) return \"null\";\n\tif (typeof value === \"string\" || typeof value === \"boolean\") {\n\t\treturn JSON.stringify(value);\n\t}\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) {\n\t\t\tthrow new Error(\"canonicalJson: non-finite number\");\n\t\t}\n\t\treturn JSON.stringify(value);\n\t}\n\tif (typeof value === \"bigint\") return JSON.stringify(value.toString());\n\tif (Array.isArray(value)) {\n\t\treturn `[${value.map((item) => canonicalJson(item)).join(\",\")}]`;\n\t}\n\tif (typeof value === \"object\") {\n\t\tconst object = value as Record<string, unknown>;\n\t\treturn `{${Object.keys(object)\n\t\t\t.sort()\n\t\t\t.map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`)\n\t\t\t.join(\",\")}}`;\n\t}\n\t// undefined / function / symbol — never present in decoded chain rows.\n\t// Fall through as null so tests catch unexpected shapes.\n\treturn \"null\";\n}\n\nfunction scalar(value: string | number | bigint | null | undefined): string {\n\tif (value === null || value === undefined) return NULL_MARKER;\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) {\n\t\t\tthrow new Error(\"semanticDigest: non-finite number\");\n\t\t}\n\t\tif (!Number.isInteger(value)) {\n\t\t\t// Chain heights, indexes, timestamps are all integer; a fractional\n\t\t\t// value here is a schema bug we want to surface loudly, not silently\n\t\t\t// serialize with float precision that varies by runtime.\n\t\t\tthrow new Error(`semanticDigest: non-integer scalar ${value}`);\n\t\t}\n\t\treturn String(value);\n\t}\n\tif (typeof value === \"bigint\") return value.toString();\n\treturn value;\n}\n\nfunction jsonScalar(value: unknown): string {\n\treturn value === null || value === undefined\n\t\t? NULL_MARKER\n\t\t: canonicalJson(value);\n}\n\nfunction sha256Hex(input: Uint8Array | string): string {\n\treturn createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\nexport interface BlockDigestInput {\n\theight: number | bigint;\n\thash: string;\n\tparent_hash: string;\n\tburn_block_height: number | bigint;\n\tburn_block_hash: string | null;\n\tindex_block_hash: string | null;\n\ttimestamp: number | bigint;\n}\n\nexport interface TransactionDigestInput {\n\ttx_id: string;\n\tblock_height: number | bigint;\n\ttx_index: number;\n\ttype: string;\n\tsender: string;\n\tstatus: string;\n\tcontract_id: string | null;\n\tfunction_name: string | null;\n\tfunction_args: unknown;\n\traw_result: string | null;\n\traw_tx: string;\n}\n\nexport interface EventDigestInput {\n\ttx_id: string;\n\tblock_height: number | bigint;\n\tevent_index: number;\n\ttype: string;\n\tdata: unknown;\n}\n\nexport interface StateWriteDigestInput {\n\tblock_height: number | bigint;\n\tordinal: number;\n\t/** null for a block-level write (no transaction). */\n\ttx_index: number | null;\n\tkey: string;\n\tvalue_hex: string;\n}\n\nfunction encodeBlock(row: BlockDigestInput): string {\n\treturn [\n\t\tscalar(row.height),\n\t\tscalar(row.hash),\n\t\tscalar(row.parent_hash),\n\t\tscalar(row.burn_block_height),\n\t\tscalar(row.burn_block_hash),\n\t\tscalar(row.index_block_hash),\n\t\tscalar(row.timestamp),\n\t].join(FIELD_SEP);\n}\n\nfunction encodeTransaction(row: TransactionDigestInput): string {\n\treturn [\n\t\tscalar(row.tx_id),\n\t\tscalar(row.block_height),\n\t\tscalar(row.tx_index),\n\t\tscalar(row.type),\n\t\tscalar(row.sender),\n\t\tscalar(row.status),\n\t\tscalar(row.contract_id),\n\t\tscalar(row.function_name),\n\t\tjsonScalar(row.function_args),\n\t\tscalar(row.raw_result),\n\t\tscalar(row.raw_tx),\n\t].join(FIELD_SEP);\n}\n\nfunction encodeEvent(row: EventDigestInput): string {\n\treturn [\n\t\tscalar(row.tx_id),\n\t\tscalar(row.block_height),\n\t\tscalar(row.event_index),\n\t\tscalar(row.type),\n\t\tjsonScalar(row.data),\n\t].join(FIELD_SEP);\n}\n\nfunction encodeStateWrite(row: StateWriteDigestInput): string {\n\treturn [\n\t\tscalar(row.block_height),\n\t\tscalar(row.ordinal),\n\t\tscalar(row.tx_index),\n\t\tscalar(row.key),\n\t\tscalar(row.value_hex),\n\t].join(FIELD_SEP);\n}\n\n/**\n * v1 per-row digests. Each is `sha256(encoded_row_bytes)` in lowercase hex.\n * The encoded byte form is stable across runtimes; the sha256 is stable\n * across CPU architectures. Never mutate an encoder without a v-bump.\n */\nexport interface SemanticDigestV1 {\n\treadonly spec: SemanticDigestSpec;\n\treadonly encodeBlock: (row: BlockDigestInput) => string;\n\treadonly encodeTransaction: (row: TransactionDigestInput) => string;\n\treadonly encodeEvent: (row: EventDigestInput) => string;\n\treadonly encodeStateWrite: (row: StateWriteDigestInput) => string;\n\treadonly block: (row: BlockDigestInput) => string;\n\treadonly transaction: (row: TransactionDigestInput) => string;\n\treadonly event: (row: EventDigestInput) => string;\n\treadonly stateWrite: (row: StateWriteDigestInput) => string;\n}\n\nexport interface SemanticDigestApi {\n\treadonly v1: SemanticDigestV1;\n}\n\nexport const semanticDigest: SemanticDigestApi = {\n\tv1: {\n\t\tspec: SEMANTIC_DIGEST_SPEC_V1,\n\t\tencodeBlock,\n\t\tencodeTransaction,\n\t\tencodeEvent,\n\t\tencodeStateWrite,\n\t\tblock: (row: BlockDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeBlock(row))),\n\t\ttransaction: (row: TransactionDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeTransaction(row))),\n\t\tevent: (row: EventDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeEvent(row))),\n\t\tstateWrite: (row: StateWriteDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeStateWrite(row))),\n\t},\n};\n\n/**\n * Streaming rollup — feed rows one at a time in their declared order, read the\n * rolled digest at the end. Two callers passing the same rows in the same order\n * produce the same rollup, regardless of batching.\n *\n * The rollup composes: partition_digest = sha256(concat(row_digest_bytes)),\n * where each row_digest_bytes = sha256(encode(row)). This is why the streaming\n * implementation can be constant-memory — the outer sha256 accepts row digests\n * incrementally.\n */\nexport class SemanticDigestRollup {\n\tprivate readonly hasher = createHash(\"sha256\");\n\tprivate count = 0;\n\n\tstatic forDataset(_dataset: ArchiveDataset): SemanticDigestRollup {\n\t\treturn new SemanticDigestRollup();\n\t}\n\n\tappendRowDigest(rowDigestHex: string): void {\n\t\tthis.hasher.update(Buffer.from(rowDigestHex, \"hex\"));\n\t\tthis.count += 1;\n\t}\n\n\trowCount(): number {\n\t\treturn this.count;\n\t}\n\n\t/** Empty rollups return `null`, not the sha256 of nothing, so an empty\n\t * range never compares equal to another empty range on a different\n\t * dataset. Callers assign `null` semantics deliberately. */\n\tdigest(): string | null {\n\t\treturn this.count === 0 ? null : this.hasher.copy().digest(\"hex\");\n\t}\n\n\tspec(): SemanticDigestSpec {\n\t\treturn SEMANTIC_DIGEST_SPEC_V1;\n\t}\n}\n\n/**\n * Per-partition digest record — the shape that lands in the snapshot manifest.\n * Complements the existing `sha256:parquet-object` per-partition digest by\n * proving semantic identity independent of file encoding.\n */\nexport interface PartitionSemanticDigest {\n\tdataset: ArchiveDataset;\n\tfrom_block: number;\n\tto_block: number;\n\trow_count: number;\n\tdigest: string | null;\n\tdigest_spec: SemanticDigestSpec;\n}\n"
7
8
  ],
8
- "mappings": "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",
9
- "debugId": "0559E7178C9550A164756E2164756E21",
9
+ "mappings": "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",
10
+ "debugId": "474576EF266272F564756E2164756E21",
10
11
  "names": []
11
12
  }
@@ -1,4 +1,19 @@
1
1
  /**
2
+ * The canonical archive's datasets — the one list every exporter, loader,
3
+ * verifier and fetch gate derives from.
4
+ *
5
+ * Order is load order: each dataset's foreign keys point only at datasets
6
+ * earlier in the list, so loading in this order never violates a constraint.
7
+ *
8
+ * Per-dataset code branches with an exhaustive `switch` ending in
9
+ * `assertNeverDataset`, never a trailing default: adding a dataset here must
10
+ * fail to compile everywhere it needs its own schema, SQL or table, instead of
11
+ * silently inheriting another dataset's.
12
+ */
13
+ declare const ARCHIVE_DATASETS: readonly ["blocks", "transactions", "events", "state_writes"];
14
+ /** One archive dataset: blocks, transactions, events or state writes. */
15
+ type ArchiveDataset = (typeof ARCHIVE_DATASETS)[number];
16
+ /**
2
17
  * v1 canonical byte encoding for chain identity — the shape that lets
3
18
  * independently-run runtimes agree on the same block.
4
19
  *
@@ -32,6 +47,7 @@
32
47
  * - blocks: (height asc)
33
48
  * - transactions: (block_height asc, tx_index asc, tx_id asc)
34
49
  * - events: (block_height asc, event_index asc, tx_id asc)
50
+ * - state_writes: (block_height asc, ordinal asc)
35
51
  * Per-partition rollup is `sha256(concat(row_digest_bytes))` in stream order.
36
52
  *
37
53
  * Test vectors under `packages/shared/src/archive/__fixtures__/semantic-digest/`
@@ -75,6 +91,14 @@ interface EventDigestInput {
75
91
  type: string;
76
92
  data: unknown;
77
93
  }
94
+ interface StateWriteDigestInput {
95
+ block_height: number | bigint;
96
+ ordinal: number;
97
+ /** null for a block-level write (no transaction). */
98
+ tx_index: number | null;
99
+ key: string;
100
+ value_hex: string;
101
+ }
78
102
  /**
79
103
  * v1 per-row digests. Each is `sha256(encoded_row_bytes)` in lowercase hex.
80
104
  * The encoded byte form is stable across runtimes; the sha256 is stable
@@ -85,9 +109,11 @@ interface SemanticDigestV1 {
85
109
  readonly encodeBlock: (row: BlockDigestInput) => string;
86
110
  readonly encodeTransaction: (row: TransactionDigestInput) => string;
87
111
  readonly encodeEvent: (row: EventDigestInput) => string;
112
+ readonly encodeStateWrite: (row: StateWriteDigestInput) => string;
88
113
  readonly block: (row: BlockDigestInput) => string;
89
114
  readonly transaction: (row: TransactionDigestInput) => string;
90
115
  readonly event: (row: EventDigestInput) => string;
116
+ readonly stateWrite: (row: StateWriteDigestInput) => string;
91
117
  }
92
118
  interface SemanticDigestApi {
93
119
  readonly v1: SemanticDigestV1;
@@ -106,7 +132,7 @@ declare const semanticDigest: SemanticDigestApi;
106
132
  declare class SemanticDigestRollup {
107
133
  private readonly hasher;
108
134
  private count;
109
- static forDataset(_dataset: "blocks" | "transactions" | "events"): SemanticDigestRollup;
135
+ static forDataset(_dataset: ArchiveDataset): SemanticDigestRollup;
110
136
  appendRowDigest(rowDigestHex: string): void;
111
137
  rowCount(): number;
112
138
  /** Empty rollups return `null`, not the sha256 of nothing, so an empty
@@ -121,11 +147,11 @@ declare class SemanticDigestRollup {
121
147
  * proving semantic identity independent of file encoding.
122
148
  */
123
149
  interface PartitionSemanticDigest {
124
- dataset: "blocks" | "transactions" | "events";
150
+ dataset: ArchiveDataset;
125
151
  from_block: number;
126
152
  to_block: number;
127
153
  row_count: number;
128
154
  digest: string | null;
129
155
  digest_spec: SemanticDigestSpec;
130
156
  }
131
- export { BlockDigestInput, EventDigestInput, PartitionSemanticDigest, SEMANTIC_DIGEST_SPEC_V1, SemanticDigestApi, SemanticDigestRollup, SemanticDigestSpec, SemanticDigestV1, TransactionDigestInput, canonicalJson, semanticDigest };
157
+ export { BlockDigestInput, EventDigestInput, PartitionSemanticDigest, SEMANTIC_DIGEST_SPEC_V1, SemanticDigestApi, SemanticDigestRollup, SemanticDigestSpec, SemanticDigestV1, StateWriteDigestInput, TransactionDigestInput, canonicalJson, semanticDigest };
@@ -84,15 +84,26 @@ function encodeEvent(row) {
84
84
  jsonScalar(row.data)
85
85
  ].join(FIELD_SEP);
86
86
  }
87
+ function encodeStateWrite(row) {
88
+ return [
89
+ scalar(row.block_height),
90
+ scalar(row.ordinal),
91
+ scalar(row.tx_index),
92
+ scalar(row.key),
93
+ scalar(row.value_hex)
94
+ ].join(FIELD_SEP);
95
+ }
87
96
  var semanticDigest = {
88
97
  v1: {
89
98
  spec: SEMANTIC_DIGEST_SPEC_V1,
90
99
  encodeBlock,
91
100
  encodeTransaction,
92
101
  encodeEvent,
102
+ encodeStateWrite,
93
103
  block: (row) => sha256Hex(textEncoder.encode(encodeBlock(row))),
94
104
  transaction: (row) => sha256Hex(textEncoder.encode(encodeTransaction(row))),
95
- event: (row) => sha256Hex(textEncoder.encode(encodeEvent(row)))
105
+ event: (row) => sha256Hex(textEncoder.encode(encodeEvent(row))),
106
+ stateWrite: (row) => sha256Hex(textEncoder.encode(encodeStateWrite(row)))
96
107
  }
97
108
  };
98
109
 
@@ -123,5 +134,5 @@ export {
123
134
  semanticDigest
124
135
  };
125
136
 
126
- //# debugId=875B24339EB4B9CB64756E2164756E21
137
+ //# debugId=72B0800E38B4905164756E2164756E21
127
138
  //# sourceMappingURL=semantic-digest.js.map
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/archive/semantic-digest.ts"],
4
4
  "sourcesContent": [
5
- "import { createHash } from \"node:crypto\";\n\n/**\n * v1 canonical byte encoding for chain identity — the shape that lets\n * independently-run runtimes agree on the same block.\n *\n * The exact-body digest (`sha256:parquet-object`) proves capture fidelity to one\n * source: replay the same bytes, get the same rows. It cannot prove two nodes\n * observed the same chain, because their Parquet encoders legally differ in\n * column padding, compression, JSON key order, and null placement — two\n * operators processing the same block can ship byte-different Parquets that\n * carry identical content.\n *\n * The semantic digest is the shape that lets independent runtimes agree. Every\n * value on a block/transaction/event is normalized into a canonical byte string\n * by rules that MUST NOT change without a version bump. Two v1 computations\n * over the same range agree, or one is wrong about the chain — never about\n * formatting.\n *\n * Encoding rules (v1):\n * - Fields join with `\\x1f` (ASCII US, unit separator — cannot legally\n * appear in any chain value; a printable delimiter risks collision with\n * contract-emitted strings).\n * - Null encodes as `\\x00` (single NUL). Distinct from empty string `\"\"`.\n * - Integers encode as their base-10 string. bigint → decimal string.\n * - JSON payload columns (function_args, event data) encode as canonical\n * JSON: object keys sorted lexicographically, no whitespace, bigint →\n * quoted decimal string, non-finite numbers rejected.\n * - Field order per record type is fixed and never reordered.\n * - Digest = sha256(bytes), lowercase hex.\n *\n * Rollup rules (v1) — order MUST match the archive's stream order or an\n * honest re-export will disagree with the manifest:\n * - blocks: (height asc)\n * - transactions: (block_height asc, tx_index asc, tx_id asc)\n * - events: (block_height asc, event_index asc, tx_id asc)\n * Per-partition rollup is `sha256(concat(row_digest_bytes))` in stream order.\n *\n * Test vectors under `packages/shared/src/archive/__fixtures__/semantic-digest/`\n * pin the encoding. Any change to bytes must bump the spec version.\n */\n\nexport const SEMANTIC_DIGEST_SPEC_V1 = \"sha256:semantic-v1\" as const;\nexport type SemanticDigestSpec = typeof SEMANTIC_DIGEST_SPEC_V1;\n\nconst FIELD_SEP = \"\\x1f\";\nconst NULL_MARKER = \"\\x00\";\n\nconst textEncoder = new TextEncoder();\n\n/**\n * Stable JSON — sorted keys, no whitespace, bigint → decimal string. Distinct\n * from `JSON.stringify(x)` which respects insertion order and rejects bigints.\n * Also rejects non-finite numbers, since NaN/Infinity have no on-chain meaning\n * and would silently hash as `null` under `JSON.stringify`.\n */\nexport function canonicalJson(value: unknown): string {\n\tif (value === null) return \"null\";\n\tif (typeof value === \"string\" || typeof value === \"boolean\") {\n\t\treturn JSON.stringify(value);\n\t}\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) {\n\t\t\tthrow new Error(\"canonicalJson: non-finite number\");\n\t\t}\n\t\treturn JSON.stringify(value);\n\t}\n\tif (typeof value === \"bigint\") return JSON.stringify(value.toString());\n\tif (Array.isArray(value)) {\n\t\treturn `[${value.map((item) => canonicalJson(item)).join(\",\")}]`;\n\t}\n\tif (typeof value === \"object\") {\n\t\tconst object = value as Record<string, unknown>;\n\t\treturn `{${Object.keys(object)\n\t\t\t.sort()\n\t\t\t.map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`)\n\t\t\t.join(\",\")}}`;\n\t}\n\t// undefined / function / symbol — never present in decoded chain rows.\n\t// Fall through as null so tests catch unexpected shapes.\n\treturn \"null\";\n}\n\nfunction scalar(value: string | number | bigint | null | undefined): string {\n\tif (value === null || value === undefined) return NULL_MARKER;\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) {\n\t\t\tthrow new Error(\"semanticDigest: non-finite number\");\n\t\t}\n\t\tif (!Number.isInteger(value)) {\n\t\t\t// Chain heights, indexes, timestamps are all integer; a fractional\n\t\t\t// value here is a schema bug we want to surface loudly, not silently\n\t\t\t// serialize with float precision that varies by runtime.\n\t\t\tthrow new Error(`semanticDigest: non-integer scalar ${value}`);\n\t\t}\n\t\treturn String(value);\n\t}\n\tif (typeof value === \"bigint\") return value.toString();\n\treturn value;\n}\n\nfunction jsonScalar(value: unknown): string {\n\treturn value === null || value === undefined\n\t\t? NULL_MARKER\n\t\t: canonicalJson(value);\n}\n\nfunction sha256Hex(input: Uint8Array | string): string {\n\treturn createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\nexport interface BlockDigestInput {\n\theight: number | bigint;\n\thash: string;\n\tparent_hash: string;\n\tburn_block_height: number | bigint;\n\tburn_block_hash: string | null;\n\tindex_block_hash: string | null;\n\ttimestamp: number | bigint;\n}\n\nexport interface TransactionDigestInput {\n\ttx_id: string;\n\tblock_height: number | bigint;\n\ttx_index: number;\n\ttype: string;\n\tsender: string;\n\tstatus: string;\n\tcontract_id: string | null;\n\tfunction_name: string | null;\n\tfunction_args: unknown;\n\traw_result: string | null;\n\traw_tx: string;\n}\n\nexport interface EventDigestInput {\n\ttx_id: string;\n\tblock_height: number | bigint;\n\tevent_index: number;\n\ttype: string;\n\tdata: unknown;\n}\n\nfunction encodeBlock(row: BlockDigestInput): string {\n\treturn [\n\t\tscalar(row.height),\n\t\tscalar(row.hash),\n\t\tscalar(row.parent_hash),\n\t\tscalar(row.burn_block_height),\n\t\tscalar(row.burn_block_hash),\n\t\tscalar(row.index_block_hash),\n\t\tscalar(row.timestamp),\n\t].join(FIELD_SEP);\n}\n\nfunction encodeTransaction(row: TransactionDigestInput): string {\n\treturn [\n\t\tscalar(row.tx_id),\n\t\tscalar(row.block_height),\n\t\tscalar(row.tx_index),\n\t\tscalar(row.type),\n\t\tscalar(row.sender),\n\t\tscalar(row.status),\n\t\tscalar(row.contract_id),\n\t\tscalar(row.function_name),\n\t\tjsonScalar(row.function_args),\n\t\tscalar(row.raw_result),\n\t\tscalar(row.raw_tx),\n\t].join(FIELD_SEP);\n}\n\nfunction encodeEvent(row: EventDigestInput): string {\n\treturn [\n\t\tscalar(row.tx_id),\n\t\tscalar(row.block_height),\n\t\tscalar(row.event_index),\n\t\tscalar(row.type),\n\t\tjsonScalar(row.data),\n\t].join(FIELD_SEP);\n}\n\n/**\n * v1 per-row digests. Each is `sha256(encoded_row_bytes)` in lowercase hex.\n * The encoded byte form is stable across runtimes; the sha256 is stable\n * across CPU architectures. Never mutate an encoder without a v-bump.\n */\nexport interface SemanticDigestV1 {\n\treadonly spec: SemanticDigestSpec;\n\treadonly encodeBlock: (row: BlockDigestInput) => string;\n\treadonly encodeTransaction: (row: TransactionDigestInput) => string;\n\treadonly encodeEvent: (row: EventDigestInput) => string;\n\treadonly block: (row: BlockDigestInput) => string;\n\treadonly transaction: (row: TransactionDigestInput) => string;\n\treadonly event: (row: EventDigestInput) => string;\n}\n\nexport interface SemanticDigestApi {\n\treadonly v1: SemanticDigestV1;\n}\n\nexport const semanticDigest: SemanticDigestApi = {\n\tv1: {\n\t\tspec: SEMANTIC_DIGEST_SPEC_V1,\n\t\tencodeBlock,\n\t\tencodeTransaction,\n\t\tencodeEvent,\n\t\tblock: (row: BlockDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeBlock(row))),\n\t\ttransaction: (row: TransactionDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeTransaction(row))),\n\t\tevent: (row: EventDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeEvent(row))),\n\t},\n};\n\n/**\n * Streaming rollup — feed rows one at a time in their declared order, read the\n * rolled digest at the end. Two callers passing the same rows in the same order\n * produce the same rollup, regardless of batching.\n *\n * The rollup composes: partition_digest = sha256(concat(row_digest_bytes)),\n * where each row_digest_bytes = sha256(encode(row)). This is why the streaming\n * implementation can be constant-memory — the outer sha256 accepts row digests\n * incrementally.\n */\nexport class SemanticDigestRollup {\n\tprivate readonly hasher = createHash(\"sha256\");\n\tprivate count = 0;\n\n\tstatic forDataset(\n\t\t_dataset: \"blocks\" | \"transactions\" | \"events\",\n\t): SemanticDigestRollup {\n\t\treturn new SemanticDigestRollup();\n\t}\n\n\tappendRowDigest(rowDigestHex: string): void {\n\t\tthis.hasher.update(Buffer.from(rowDigestHex, \"hex\"));\n\t\tthis.count += 1;\n\t}\n\n\trowCount(): number {\n\t\treturn this.count;\n\t}\n\n\t/** Empty rollups return `null`, not the sha256 of nothing, so an empty\n\t * range never compares equal to another empty range on a different\n\t * dataset. Callers assign `null` semantics deliberately. */\n\tdigest(): string | null {\n\t\treturn this.count === 0 ? null : this.hasher.copy().digest(\"hex\");\n\t}\n\n\tspec(): SemanticDigestSpec {\n\t\treturn SEMANTIC_DIGEST_SPEC_V1;\n\t}\n}\n\n/**\n * Per-partition digest record — the shape that lands in the snapshot manifest.\n * Complements the existing `sha256:parquet-object` per-partition digest by\n * proving semantic identity independent of file encoding.\n */\nexport interface PartitionSemanticDigest {\n\tdataset: \"blocks\" | \"transactions\" | \"events\";\n\tfrom_block: number;\n\tto_block: number;\n\trow_count: number;\n\tdigest: string | null;\n\tdigest_spec: SemanticDigestSpec;\n}\n"
5
+ "import { createHash } from \"node:crypto\";\nimport type { ArchiveDataset } from \"./datasets.ts\";\n\n/**\n * v1 canonical byte encoding for chain identity — the shape that lets\n * independently-run runtimes agree on the same block.\n *\n * The exact-body digest (`sha256:parquet-object`) proves capture fidelity to one\n * source: replay the same bytes, get the same rows. It cannot prove two nodes\n * observed the same chain, because their Parquet encoders legally differ in\n * column padding, compression, JSON key order, and null placement — two\n * operators processing the same block can ship byte-different Parquets that\n * carry identical content.\n *\n * The semantic digest is the shape that lets independent runtimes agree. Every\n * value on a block/transaction/event is normalized into a canonical byte string\n * by rules that MUST NOT change without a version bump. Two v1 computations\n * over the same range agree, or one is wrong about the chain — never about\n * formatting.\n *\n * Encoding rules (v1):\n * - Fields join with `\\x1f` (ASCII US, unit separator — cannot legally\n * appear in any chain value; a printable delimiter risks collision with\n * contract-emitted strings).\n * - Null encodes as `\\x00` (single NUL). Distinct from empty string `\"\"`.\n * - Integers encode as their base-10 string. bigint → decimal string.\n * - JSON payload columns (function_args, event data) encode as canonical\n * JSON: object keys sorted lexicographically, no whitespace, bigint →\n * quoted decimal string, non-finite numbers rejected.\n * - Field order per record type is fixed and never reordered.\n * - Digest = sha256(bytes), lowercase hex.\n *\n * Rollup rules (v1) — order MUST match the archive's stream order or an\n * honest re-export will disagree with the manifest:\n * - blocks: (height asc)\n * - transactions: (block_height asc, tx_index asc, tx_id asc)\n * - events: (block_height asc, event_index asc, tx_id asc)\n * - state_writes: (block_height asc, ordinal asc)\n * Per-partition rollup is `sha256(concat(row_digest_bytes))` in stream order.\n *\n * Test vectors under `packages/shared/src/archive/__fixtures__/semantic-digest/`\n * pin the encoding. Any change to bytes must bump the spec version.\n */\n\nexport const SEMANTIC_DIGEST_SPEC_V1 = \"sha256:semantic-v1\" as const;\nexport type SemanticDigestSpec = typeof SEMANTIC_DIGEST_SPEC_V1;\n\nconst FIELD_SEP = \"\\x1f\";\nconst NULL_MARKER = \"\\x00\";\n\nconst textEncoder = new TextEncoder();\n\n/**\n * Stable JSON — sorted keys, no whitespace, bigint → decimal string. Distinct\n * from `JSON.stringify(x)` which respects insertion order and rejects bigints.\n * Also rejects non-finite numbers, since NaN/Infinity have no on-chain meaning\n * and would silently hash as `null` under `JSON.stringify`.\n */\nexport function canonicalJson(value: unknown): string {\n\tif (value === null) return \"null\";\n\tif (typeof value === \"string\" || typeof value === \"boolean\") {\n\t\treturn JSON.stringify(value);\n\t}\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) {\n\t\t\tthrow new Error(\"canonicalJson: non-finite number\");\n\t\t}\n\t\treturn JSON.stringify(value);\n\t}\n\tif (typeof value === \"bigint\") return JSON.stringify(value.toString());\n\tif (Array.isArray(value)) {\n\t\treturn `[${value.map((item) => canonicalJson(item)).join(\",\")}]`;\n\t}\n\tif (typeof value === \"object\") {\n\t\tconst object = value as Record<string, unknown>;\n\t\treturn `{${Object.keys(object)\n\t\t\t.sort()\n\t\t\t.map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`)\n\t\t\t.join(\",\")}}`;\n\t}\n\t// undefined / function / symbol — never present in decoded chain rows.\n\t// Fall through as null so tests catch unexpected shapes.\n\treturn \"null\";\n}\n\nfunction scalar(value: string | number | bigint | null | undefined): string {\n\tif (value === null || value === undefined) return NULL_MARKER;\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) {\n\t\t\tthrow new Error(\"semanticDigest: non-finite number\");\n\t\t}\n\t\tif (!Number.isInteger(value)) {\n\t\t\t// Chain heights, indexes, timestamps are all integer; a fractional\n\t\t\t// value here is a schema bug we want to surface loudly, not silently\n\t\t\t// serialize with float precision that varies by runtime.\n\t\t\tthrow new Error(`semanticDigest: non-integer scalar ${value}`);\n\t\t}\n\t\treturn String(value);\n\t}\n\tif (typeof value === \"bigint\") return value.toString();\n\treturn value;\n}\n\nfunction jsonScalar(value: unknown): string {\n\treturn value === null || value === undefined\n\t\t? NULL_MARKER\n\t\t: canonicalJson(value);\n}\n\nfunction sha256Hex(input: Uint8Array | string): string {\n\treturn createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\nexport interface BlockDigestInput {\n\theight: number | bigint;\n\thash: string;\n\tparent_hash: string;\n\tburn_block_height: number | bigint;\n\tburn_block_hash: string | null;\n\tindex_block_hash: string | null;\n\ttimestamp: number | bigint;\n}\n\nexport interface TransactionDigestInput {\n\ttx_id: string;\n\tblock_height: number | bigint;\n\ttx_index: number;\n\ttype: string;\n\tsender: string;\n\tstatus: string;\n\tcontract_id: string | null;\n\tfunction_name: string | null;\n\tfunction_args: unknown;\n\traw_result: string | null;\n\traw_tx: string;\n}\n\nexport interface EventDigestInput {\n\ttx_id: string;\n\tblock_height: number | bigint;\n\tevent_index: number;\n\ttype: string;\n\tdata: unknown;\n}\n\nexport interface StateWriteDigestInput {\n\tblock_height: number | bigint;\n\tordinal: number;\n\t/** null for a block-level write (no transaction). */\n\ttx_index: number | null;\n\tkey: string;\n\tvalue_hex: string;\n}\n\nfunction encodeBlock(row: BlockDigestInput): string {\n\treturn [\n\t\tscalar(row.height),\n\t\tscalar(row.hash),\n\t\tscalar(row.parent_hash),\n\t\tscalar(row.burn_block_height),\n\t\tscalar(row.burn_block_hash),\n\t\tscalar(row.index_block_hash),\n\t\tscalar(row.timestamp),\n\t].join(FIELD_SEP);\n}\n\nfunction encodeTransaction(row: TransactionDigestInput): string {\n\treturn [\n\t\tscalar(row.tx_id),\n\t\tscalar(row.block_height),\n\t\tscalar(row.tx_index),\n\t\tscalar(row.type),\n\t\tscalar(row.sender),\n\t\tscalar(row.status),\n\t\tscalar(row.contract_id),\n\t\tscalar(row.function_name),\n\t\tjsonScalar(row.function_args),\n\t\tscalar(row.raw_result),\n\t\tscalar(row.raw_tx),\n\t].join(FIELD_SEP);\n}\n\nfunction encodeEvent(row: EventDigestInput): string {\n\treturn [\n\t\tscalar(row.tx_id),\n\t\tscalar(row.block_height),\n\t\tscalar(row.event_index),\n\t\tscalar(row.type),\n\t\tjsonScalar(row.data),\n\t].join(FIELD_SEP);\n}\n\nfunction encodeStateWrite(row: StateWriteDigestInput): string {\n\treturn [\n\t\tscalar(row.block_height),\n\t\tscalar(row.ordinal),\n\t\tscalar(row.tx_index),\n\t\tscalar(row.key),\n\t\tscalar(row.value_hex),\n\t].join(FIELD_SEP);\n}\n\n/**\n * v1 per-row digests. Each is `sha256(encoded_row_bytes)` in lowercase hex.\n * The encoded byte form is stable across runtimes; the sha256 is stable\n * across CPU architectures. Never mutate an encoder without a v-bump.\n */\nexport interface SemanticDigestV1 {\n\treadonly spec: SemanticDigestSpec;\n\treadonly encodeBlock: (row: BlockDigestInput) => string;\n\treadonly encodeTransaction: (row: TransactionDigestInput) => string;\n\treadonly encodeEvent: (row: EventDigestInput) => string;\n\treadonly encodeStateWrite: (row: StateWriteDigestInput) => string;\n\treadonly block: (row: BlockDigestInput) => string;\n\treadonly transaction: (row: TransactionDigestInput) => string;\n\treadonly event: (row: EventDigestInput) => string;\n\treadonly stateWrite: (row: StateWriteDigestInput) => string;\n}\n\nexport interface SemanticDigestApi {\n\treadonly v1: SemanticDigestV1;\n}\n\nexport const semanticDigest: SemanticDigestApi = {\n\tv1: {\n\t\tspec: SEMANTIC_DIGEST_SPEC_V1,\n\t\tencodeBlock,\n\t\tencodeTransaction,\n\t\tencodeEvent,\n\t\tencodeStateWrite,\n\t\tblock: (row: BlockDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeBlock(row))),\n\t\ttransaction: (row: TransactionDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeTransaction(row))),\n\t\tevent: (row: EventDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeEvent(row))),\n\t\tstateWrite: (row: StateWriteDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeStateWrite(row))),\n\t},\n};\n\n/**\n * Streaming rollup — feed rows one at a time in their declared order, read the\n * rolled digest at the end. Two callers passing the same rows in the same order\n * produce the same rollup, regardless of batching.\n *\n * The rollup composes: partition_digest = sha256(concat(row_digest_bytes)),\n * where each row_digest_bytes = sha256(encode(row)). This is why the streaming\n * implementation can be constant-memory — the outer sha256 accepts row digests\n * incrementally.\n */\nexport class SemanticDigestRollup {\n\tprivate readonly hasher = createHash(\"sha256\");\n\tprivate count = 0;\n\n\tstatic forDataset(_dataset: ArchiveDataset): SemanticDigestRollup {\n\t\treturn new SemanticDigestRollup();\n\t}\n\n\tappendRowDigest(rowDigestHex: string): void {\n\t\tthis.hasher.update(Buffer.from(rowDigestHex, \"hex\"));\n\t\tthis.count += 1;\n\t}\n\n\trowCount(): number {\n\t\treturn this.count;\n\t}\n\n\t/** Empty rollups return `null`, not the sha256 of nothing, so an empty\n\t * range never compares equal to another empty range on a different\n\t * dataset. Callers assign `null` semantics deliberately. */\n\tdigest(): string | null {\n\t\treturn this.count === 0 ? null : this.hasher.copy().digest(\"hex\");\n\t}\n\n\tspec(): SemanticDigestSpec {\n\t\treturn SEMANTIC_DIGEST_SPEC_V1;\n\t}\n}\n\n/**\n * Per-partition digest record — the shape that lands in the snapshot manifest.\n * Complements the existing `sha256:parquet-object` per-partition digest by\n * proving semantic identity independent of file encoding.\n */\nexport interface PartitionSemanticDigest {\n\tdataset: ArchiveDataset;\n\tfrom_block: number;\n\tto_block: number;\n\trow_count: number;\n\tdigest: string | null;\n\tdigest_spec: SemanticDigestSpec;\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "875B24339EB4B9CB64756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "72B0800E38B4905164756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1912,10 +1912,6 @@ declare function planRepair(input: {
1912
1912
  defect: RepairDefect
1913
1913
  range_safe_proven?: boolean
1914
1914
  }): RepairPlan;
1915
- /**
1916
- * Unified verify targets — library used by CLI (and REST later).
1917
- * Exit codes match `sl verify`: 0 clean, 1 diverged, 2 unanchored.
1918
- */
1919
1915
  declare const VERIFY_EXIT: {
1920
1916
  readonly CLEAN: 0
1921
1917
  readonly DIVERGED: 1