@secondlayer/shared 13.13.1 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/archive/copy-loader.d.ts +11 -12
- package/dist/src/archive/copy-loader.js +100 -39
- package/dist/src/archive/copy-loader.js.map +5 -4
- package/dist/src/archive/datasets.d.ts +37 -0
- package/dist/src/archive/datasets.js +50 -0
- package/dist/src/archive/datasets.js.map +10 -0
- package/dist/src/archive/derived-stage-report.d.ts +12 -26
- package/dist/src/archive/derived-stage-report.js +42 -6
- package/dist/src/archive/derived-stage-report.js.map +5 -4
- package/dist/src/archive/per-height-digest.d.ts +1 -0
- package/dist/src/archive/per-height-digest.js +13 -2
- package/dist/src/archive/per-height-digest.js.map +3 -3
- package/dist/src/archive/range-digest.d.ts +16 -1
- package/dist/src/archive/range-digest.js +107 -41
- package/dist/src/archive/range-digest.js.map +5 -4
- package/dist/src/archive/reference.d.ts +18 -2
- package/dist/src/archive/semantic-digest-builder.d.ts +30 -3
- package/dist/src/archive/semantic-digest-builder.js +143 -39
- package/dist/src/archive/semantic-digest-builder.js.map +6 -5
- package/dist/src/archive/semantic-digest.d.ts +29 -3
- package/dist/src/archive/semantic-digest.js +13 -2
- package/dist/src/archive/semantic-digest.js.map +3 -3
- package/dist/src/coverage/index.d.ts +0 -4
- package/dist/src/coverage/index.js +42 -2
- package/dist/src/coverage/index.js.map +5 -4
- package/dist/src/node/hiro-pg-client.d.ts +9 -1
- package/dist/src/node/hiro-pg-client.js +7 -4
- package/dist/src/node/hiro-pg-client.js.map +3 -3
- package/dist/src/node/local-client.js +8 -7
- package/dist/src/node/local-client.js.map +3 -3
- package/package.json +6 -6
- package/dist/src/node/archive-client.d.ts +0 -39
- package/dist/src/node/archive-client.js +0 -399
- package/dist/src/node/archive-client.js.map +0 -12
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"sources": ["../src/archive/per-height-digest.ts", "../src/archive/semantic-digest.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import { createHash } from \"node:crypto\";\nimport { SEMANTIC_DIGEST_SPEC_V1 } from \"./semantic-digest.ts\";\n\n/**\n * Per-height semantic digest sidecars — enable \"verify one block\" without\n * downloading a whole 50k-block partition.\n *\n * The partition-level semantic digest is the shape a byte-level comparator\n * wants: one string per partition, cheap to compare in bulk. But a caller who\n * only cares about block 8_500_017 has to pull an entire Parquet file to check\n * one row. The per-height sidecar closes that gap: one 32-byte block digest and\n * two roll-up digests per height (transactions and events), addressable by\n * height without touching the raw datasets.\n *\n * Rollup rules (v1) — MUST match the exporter's stream order or an honest\n * re-derivation will disagree:\n * - transactions_rollup: sha256(concat(tx_digest_bytes, tx_index asc, tx_id asc))\n * `null` when the height has no transactions.\n * - events_rollup: sha256(concat(event_digest_bytes, event_index asc, tx_id asc))\n * `null` when the height has no events.\n * - block_digest: the semantic-v1 block digest itself (one row per\n * height, so no rollup needed).\n *\n * `null` is deliberate: an empty rollup is not the sha256 of nothing (which\n * would collide across two empty datasets); an absent-here-but-present-elsewhere\n * dataset must never compare equal to a truly-empty one.\n */\n\nexport interface PerHeightDigestRow {\n\theight: number;\n\tblock_digest: string;\n\ttransactions_rollup: string | null;\n\tevents_rollup: string | null;\n}\n\ninterface Accumulator {\n\tblock_digest: string | null;\n\ttx: ReturnType<typeof createHash>;\n\ttx_count: number;\n\tevent: ReturnType<typeof createHash>;\n\tevent_count: number;\n}\n\nexport class PerHeightDigestAccumulator {\n\tprivate readonly rows = new Map<number, Accumulator>();\n\n\tsetBlockDigest(height: number, blockDigestHex: string): void {\n\t\tthis.ensure(height).block_digest = blockDigestHex;\n\t}\n\n\tappendTransactionDigest(height: number, digestHex: string): void {\n\t\tconst a = this.ensure(height);\n\t\ta.tx.update(Buffer.from(digestHex, \"hex\"));\n\t\ta.tx_count += 1;\n\t}\n\n\tappendEventDigest(height: number, digestHex: string): void {\n\t\tconst a = this.ensure(height);\n\t\ta.event.update(Buffer.from(digestHex, \"hex\"));\n\t\ta.event_count += 1;\n\t}\n\n\t/**\n\t * Drain the accumulator into height-ascending rows. Heights with no block\n\t * digest are dropped with a thrown error — a canonical partition MUST have\n\t * a block at every height it covers, and silently emitting a row without\n\t * one would hide the missing block from downstream verifiers.\n\t */\n\tdrain(fromBlock: number, toBlock: number): PerHeightDigestRow[] {\n\t\tconst rows: PerHeightDigestRow[] = [];\n\t\tfor (let h = fromBlock; h <= toBlock; h++) {\n\t\t\tconst a = this.rows.get(h);\n\t\t\tif (!a) continue; // Missing entirely — surfaced by canonical audit, not here.\n\t\t\tif (!a.block_digest) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`per-height digest at ${h} has tx/event rollups but no block digest`,\n\t\t\t\t);\n\t\t\t}\n\t\t\trows.push({\n\t\t\t\theight: h,\n\t\t\t\tblock_digest: a.block_digest,\n\t\t\t\ttransactions_rollup:\n\t\t\t\t\ta.tx_count === 0 ? null : a.tx.copy().digest(\"hex\"),\n\t\t\t\tevents_rollup:\n\t\t\t\t\ta.event_count === 0 ? null : a.event.copy().digest(\"hex\"),\n\t\t\t});\n\t\t}\n\t\treturn rows;\n\t}\n\n\tspec(): typeof SEMANTIC_DIGEST_SPEC_V1 {\n\t\treturn SEMANTIC_DIGEST_SPEC_V1;\n\t}\n\n\tprivate ensure(height: number): Accumulator {\n\t\tlet a = this.rows.get(height);\n\t\tif (!a) {\n\t\t\ta = {\n\t\t\t\tblock_digest: null,\n\t\t\t\ttx: createHash(\"sha256\"),\n\t\t\t\ttx_count: 0,\n\t\t\t\tevent: createHash(\"sha256\"),\n\t\t\t\tevent_count: 0,\n\t\t\t};\n\t\t\tthis.rows.set(height, a);\n\t\t}\n\t\treturn a;\n\t}\n}\n\n/**\n * Registered in the snapshot manifest under `digest_index[]`. One entry per\n * partition, addressable at height granularity without downloading raw data.\n */\nexport interface DigestIndexPartition {\n\tfrom_block: number;\n\tto_block: number;\n\tpath: string;\n\trow_count: number;\n\tbyte_size: number;\n\tsha256: string;\n\tdigest_spec: typeof SEMANTIC_DIGEST_SPEC_V1;\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"
|
|
6
|
+
"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
7
|
],
|
|
8
|
-
"mappings": ";AAAA,uBAAS;;;ACAT;
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "93A25676A491D51564756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -1374,6 +1374,21 @@ interface FeedbackTicketsTable {
|
|
|
1374
1374
|
created_at: Generated<Date>;
|
|
1375
1375
|
}
|
|
1376
1376
|
/**
|
|
1377
|
+
* The canonical archive's datasets — the one list every exporter, loader,
|
|
1378
|
+
* verifier and fetch gate derives from.
|
|
1379
|
+
*
|
|
1380
|
+
* Order is load order: each dataset's foreign keys point only at datasets
|
|
1381
|
+
* earlier in the list, so loading in this order never violates a constraint.
|
|
1382
|
+
*
|
|
1383
|
+
* Per-dataset code branches with an exhaustive `switch` ending in
|
|
1384
|
+
* `assertNeverDataset`, never a trailing default: adding a dataset here must
|
|
1385
|
+
* fail to compile everywhere it needs its own schema, SQL or table, instead of
|
|
1386
|
+
* silently inheriting another dataset's.
|
|
1387
|
+
*/
|
|
1388
|
+
declare const ARCHIVE_DATASETS: readonly ["blocks", "transactions", "events", "state_writes"];
|
|
1389
|
+
/** One archive dataset: blocks, transactions, events or state writes. */
|
|
1390
|
+
type ArchiveDataset = (typeof ARCHIVE_DATASETS)[number];
|
|
1391
|
+
/**
|
|
1377
1392
|
* Cheap, SQL-computed digests over a height range — the basis of a verify that
|
|
1378
1393
|
* runs in seconds instead of hours.
|
|
1379
1394
|
*
|
|
@@ -1400,7 +1415,7 @@ interface FeedbackTicketsTable {
|
|
|
1400
1415
|
* (timestamps with timezone rendering, generated ids, `created_at`).
|
|
1401
1416
|
*/
|
|
1402
1417
|
declare const RANGE_DIGEST_SPEC: "md5:sql-identity-v1";
|
|
1403
|
-
type RangeDigestDataset =
|
|
1418
|
+
type RangeDigestDataset = ArchiveDataset;
|
|
1404
1419
|
type RangeDigest = {
|
|
1405
1420
|
dataset: RangeDigestDataset
|
|
1406
1421
|
from_block: number
|
|
@@ -1,48 +1,118 @@
|
|
|
1
1
|
// src/archive/range-digest.ts
|
|
2
2
|
import { sql } from "kysely";
|
|
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
|
+
|
|
44
|
+
// src/archive/range-digest.ts
|
|
3
45
|
var RANGE_DIGEST_SPEC = "md5:sql-identity-v1";
|
|
4
46
|
function identityExpression(dataset) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
47
|
+
switch (dataset) {
|
|
48
|
+
case "blocks":
|
|
49
|
+
return sql`md5(
|
|
50
|
+
height::text || '|' ||
|
|
51
|
+
hash || '|' ||
|
|
52
|
+
parent_hash || '|' ||
|
|
53
|
+
burn_block_height::text || '|' ||
|
|
54
|
+
coalesce(burn_block_hash, '~') || '|' ||
|
|
55
|
+
coalesce(index_block_hash, '~')
|
|
56
|
+
)`;
|
|
57
|
+
case "transactions":
|
|
58
|
+
return sql`md5(
|
|
59
|
+
tx_id || '|' ||
|
|
60
|
+
block_height::text || '|' ||
|
|
61
|
+
tx_index::text || '|' ||
|
|
62
|
+
type || '|' ||
|
|
63
|
+
sender || '|' ||
|
|
64
|
+
status || '|' ||
|
|
65
|
+
coalesce(contract_id, '~') || '|' ||
|
|
66
|
+
coalesce(function_name, '~')
|
|
67
|
+
)`;
|
|
68
|
+
case "events":
|
|
69
|
+
return sql`md5(
|
|
70
|
+
tx_id || '|' ||
|
|
71
|
+
block_height::text || '|' ||
|
|
72
|
+
event_index::text || '|' ||
|
|
73
|
+
type
|
|
74
|
+
)`;
|
|
75
|
+
case "state_writes":
|
|
76
|
+
return sql`md5(
|
|
77
|
+
block_height::text || '|' ||
|
|
78
|
+
ordinal::text || '|' ||
|
|
79
|
+
coalesce(tx_index::text, '~') || '|' ||
|
|
80
|
+
key || '|' ||
|
|
81
|
+
md5(value_hex)
|
|
82
|
+
)`;
|
|
83
|
+
default:
|
|
84
|
+
return assertNeverDataset(dataset);
|
|
26
85
|
}
|
|
27
|
-
return sql`md5(
|
|
28
|
-
tx_id || '|' ||
|
|
29
|
-
block_height::text || '|' ||
|
|
30
|
-
event_index::text || '|' ||
|
|
31
|
-
type
|
|
32
|
-
)`;
|
|
33
86
|
}
|
|
34
87
|
function orderExpression(dataset) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
88
|
+
switch (dataset) {
|
|
89
|
+
case "blocks":
|
|
90
|
+
return sql`height`;
|
|
91
|
+
case "transactions":
|
|
92
|
+
return sql`block_height, tx_index, tx_id`;
|
|
93
|
+
case "events":
|
|
94
|
+
return sql`block_height, event_index, tx_id`;
|
|
95
|
+
case "state_writes":
|
|
96
|
+
return sql`block_height, ordinal`;
|
|
97
|
+
default:
|
|
98
|
+
return assertNeverDataset(dataset);
|
|
99
|
+
}
|
|
40
100
|
}
|
|
41
101
|
function fromExpression(dataset) {
|
|
42
|
-
|
|
43
|
-
|
|
102
|
+
switch (dataset) {
|
|
103
|
+
case "blocks":
|
|
104
|
+
return sql`FROM blocks WHERE canonical = true`;
|
|
105
|
+
case "transactions":
|
|
106
|
+
return childFrom(sql`transactions`);
|
|
107
|
+
case "events":
|
|
108
|
+
return childFrom(sql`events`);
|
|
109
|
+
case "state_writes":
|
|
110
|
+
return childFrom(sql`state_writes`);
|
|
111
|
+
default:
|
|
112
|
+
return assertNeverDataset(dataset);
|
|
44
113
|
}
|
|
45
|
-
|
|
114
|
+
}
|
|
115
|
+
function childFrom(table) {
|
|
46
116
|
return sql`FROM ${table} AS child
|
|
47
117
|
JOIN blocks AS b ON b.height = child.block_height AND b.canonical = true
|
|
48
118
|
WHERE true`;
|
|
@@ -75,11 +145,7 @@ async function computeRangeDigest(db, dataset, fromBlock, toBlock) {
|
|
|
75
145
|
digest_spec: RANGE_DIGEST_SPEC
|
|
76
146
|
};
|
|
77
147
|
}
|
|
78
|
-
async function computeRangeDigests(db, fromBlock, toBlock, datasets =
|
|
79
|
-
"blocks",
|
|
80
|
-
"transactions",
|
|
81
|
-
"events"
|
|
82
|
-
]) {
|
|
148
|
+
async function computeRangeDigests(db, fromBlock, toBlock, datasets = ARCHIVE_DATASETS) {
|
|
83
149
|
const digests = [];
|
|
84
150
|
for (const dataset of datasets) {
|
|
85
151
|
digests.push(await computeRangeDigest(db, dataset, fromBlock, toBlock));
|
|
@@ -122,5 +188,5 @@ export {
|
|
|
122
188
|
computeRangeDigests
|
|
123
189
|
};
|
|
124
190
|
|
|
125
|
-
//# debugId=
|
|
191
|
+
//# debugId=E7DFA890F7D5EB2E64756E2164756E21
|
|
126
192
|
//# sourceMappingURL=range-digest.js.map
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/archive/range-digest.ts"],
|
|
3
|
+
"sources": ["../src/archive/range-digest.ts", "../src/archive/datasets.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import { sql } from \"kysely\";\nimport type { Kysely } from \"kysely\";\nimport type { Database } from \"../db/schema.ts\";\n\n/**\n * Cheap, SQL-computed digests over a height range — the basis of a verify that\n * runs in seconds instead of hours.\n *\n * The archive's per-object `sha256:parquet-object` digests can only be checked\n * by regenerating the Parquet locally, which costs a full export. That is a\n * reasonable paranoid check and a terrible first-run experience: nobody will\n * spend two hours to discover whether they are broken. These digests are\n * computed by the database itself, over the columns that actually carry chain\n * identity, so an operator gets an answer before they lose interest.\n *\n * What this catches is not a subset chosen for convenience — it is precisely\n * the defect class seen in production: missing heights, a canonical child whose\n * `parent_hash` names a block we no longer hold, duplicate canonical rows at one\n * height, and any altered block/transaction identity. What it deliberately does\n * NOT catch is a difference in bulky payload columns (`raw_tx`, event `data`)\n * that leaves identity intact; `--deep` re-export exists for that.\n *\n * Determinism rules, so two independent databases agree:\n * - Aggregate over an explicit ORDER BY, never table order.\n * - Hash each row to a fixed-width digest FIRST, then aggregate those, so the\n * intermediate string is 32 bytes/row rather than the whole row.\n * - Normalize NULL to a sentinel that cannot collide with a real value.\n * - Never include a column whose representation is environment-dependent\n * (timestamps with timezone rendering, generated ids, `created_at`).\n */\n\nexport const RANGE_DIGEST_SPEC = \"md5:sql-identity-v1\" as const;\n\nexport type RangeDigestDataset =
|
|
5
|
+
"import { sql } from \"kysely\";\nimport type { Kysely, RawBuilder } from \"kysely\";\nimport type { Database } from \"../db/schema.ts\";\nimport {\n\tARCHIVE_DATASETS,\n\ttype ArchiveDataset,\n\tassertNeverDataset,\n} from \"./datasets.ts\";\n\n/**\n * Cheap, SQL-computed digests over a height range — the basis of a verify that\n * runs in seconds instead of hours.\n *\n * The archive's per-object `sha256:parquet-object` digests can only be checked\n * by regenerating the Parquet locally, which costs a full export. That is a\n * reasonable paranoid check and a terrible first-run experience: nobody will\n * spend two hours to discover whether they are broken. These digests are\n * computed by the database itself, over the columns that actually carry chain\n * identity, so an operator gets an answer before they lose interest.\n *\n * What this catches is not a subset chosen for convenience — it is precisely\n * the defect class seen in production: missing heights, a canonical child whose\n * `parent_hash` names a block we no longer hold, duplicate canonical rows at one\n * height, and any altered block/transaction identity. What it deliberately does\n * NOT catch is a difference in bulky payload columns (`raw_tx`, event `data`)\n * that leaves identity intact; `--deep` re-export exists for that.\n *\n * Determinism rules, so two independent databases agree:\n * - Aggregate over an explicit ORDER BY, never table order.\n * - Hash each row to a fixed-width digest FIRST, then aggregate those, so the\n * intermediate string is 32 bytes/row rather than the whole row.\n * - Normalize NULL to a sentinel that cannot collide with a real value.\n * - Never include a column whose representation is environment-dependent\n * (timestamps with timezone rendering, generated ids, `created_at`).\n */\n\nexport const RANGE_DIGEST_SPEC = \"md5:sql-identity-v1\" as const;\n\nexport type RangeDigestDataset = ArchiveDataset;\n\nexport type RangeDigest = {\n\tdataset: RangeDigestDataset;\n\tfrom_block: number;\n\tto_block: number;\n\trow_count: number;\n\t/** null when the range holds no rows — an empty range has no digest, and\n\t * that is materially different from \"a digest that happens to be of zero\n\t * rows\", which would compare equal across datasets. */\n\tdigest: string | null;\n\tdigest_spec: typeof RANGE_DIGEST_SPEC;\n};\n\n/**\n * Identity columns per dataset. `blocks` carries the whole corruption surface\n * we care about, which is why quick mode leans on it.\n */\nfunction identityExpression(dataset: RangeDigestDataset) {\n\tswitch (dataset) {\n\t\tcase \"blocks\":\n\t\t\treturn sql`md5(\n\t\t\t\theight::text || '|' ||\n\t\t\t\thash || '|' ||\n\t\t\t\tparent_hash || '|' ||\n\t\t\t\tburn_block_height::text || '|' ||\n\t\t\t\tcoalesce(burn_block_hash, '~') || '|' ||\n\t\t\t\tcoalesce(index_block_hash, '~')\n\t\t\t)`;\n\t\tcase \"transactions\":\n\t\t\treturn sql`md5(\n\t\t\t\ttx_id || '|' ||\n\t\t\t\tblock_height::text || '|' ||\n\t\t\t\ttx_index::text || '|' ||\n\t\t\t\ttype || '|' ||\n\t\t\t\tsender || '|' ||\n\t\t\t\tstatus || '|' ||\n\t\t\t\tcoalesce(contract_id, '~') || '|' ||\n\t\t\t\tcoalesce(function_name, '~')\n\t\t\t)`;\n\t\tcase \"events\":\n\t\t\treturn sql`md5(\n\t\t\t\ttx_id || '|' ||\n\t\t\t\tblock_height::text || '|' ||\n\t\t\t\tevent_index::text || '|' ||\n\t\t\t\ttype\n\t\t\t)`;\n\t\tcase \"state_writes\":\n\t\t\t// The value is folded to its own md5 so a large write does not\n\t\t\t// widen the per-row string; the digest still moves with it.\n\t\t\treturn sql`md5(\n\t\t\t\tblock_height::text || '|' ||\n\t\t\t\tordinal::text || '|' ||\n\t\t\t\tcoalesce(tx_index::text, '~') || '|' ||\n\t\t\t\tkey || '|' ||\n\t\t\t\tmd5(value_hex)\n\t\t\t)`;\n\t\tdefault:\n\t\t\treturn assertNeverDataset(dataset);\n\t}\n}\n\nfunction orderExpression(dataset: RangeDigestDataset) {\n\tswitch (dataset) {\n\t\tcase \"blocks\":\n\t\t\treturn sql`height`;\n\t\tcase \"transactions\":\n\t\t\treturn sql`block_height, tx_index, tx_id`;\n\t\tcase \"events\":\n\t\t\treturn sql`block_height, event_index, tx_id`;\n\t\tcase \"state_writes\":\n\t\t\treturn sql`block_height, ordinal`;\n\t\tdefault:\n\t\t\treturn assertNeverDataset(dataset);\n\t}\n}\n\n/**\n * Rows are restricted to CANONICAL blocks in every dataset — a non-canonical\n * row at a height must not change the digest, or a database that merely retains\n * more reorg history than another would report as diverging.\n */\nfunction fromExpression(dataset: RangeDigestDataset) {\n\tswitch (dataset) {\n\t\tcase \"blocks\":\n\t\t\treturn sql`FROM blocks WHERE canonical = true`;\n\t\tcase \"transactions\":\n\t\t\treturn childFrom(sql`transactions`);\n\t\tcase \"events\":\n\t\t\treturn childFrom(sql`events`);\n\t\tcase \"state_writes\":\n\t\t\treturn childFrom(sql`state_writes`);\n\t\tdefault:\n\t\t\treturn assertNeverDataset(dataset);\n\t}\n}\n\nfunction childFrom(table: RawBuilder<unknown>) {\n\treturn sql`FROM ${table} AS child\n\t\tJOIN blocks AS b ON b.height = child.block_height AND b.canonical = true\n\t\tWHERE true`;\n}\n\nfunction heightColumn(dataset: RangeDigestDataset) {\n\treturn dataset === \"blocks\" ? sql`height` : sql`child.block_height`;\n}\n\nexport async function computeRangeDigest(\n\tdb: Kysely<Database>,\n\tdataset: RangeDigestDataset,\n\tfromBlock: number,\n\ttoBlock: number,\n): Promise<RangeDigest> {\n\tconst { rows } = await sql<{ digest: string | null; row_count: string }>`\n\t\tSELECT\n\t\t\tmd5(string_agg(row_digest, '' ORDER BY ord)) AS digest,\n\t\t\tCOUNT(*)::text AS row_count\n\t\tFROM (\n\t\t\tSELECT\n\t\t\t\t${identityExpression(dataset)} AS row_digest,\n\t\t\t\trow_number() OVER (ORDER BY ${orderExpression(dataset)}) AS ord\n\t\t\t${fromExpression(dataset)}\n\t\t\t\tAND ${heightColumn(dataset)} >= ${fromBlock}\n\t\t\t\tAND ${heightColumn(dataset)} <= ${toBlock}\n\t\t) AS identity_rows\n\t`.execute(db);\n\n\tconst row = rows[0];\n\tconst rowCount = Number(row?.row_count ?? 0);\n\treturn {\n\t\tdataset,\n\t\tfrom_block: fromBlock,\n\t\tto_block: toBlock,\n\t\trow_count: rowCount,\n\t\tdigest: rowCount === 0 ? null : (row?.digest ?? null),\n\t\tdigest_spec: RANGE_DIGEST_SPEC,\n\t};\n}\n\nexport async function computeRangeDigests(\n\tdb: Kysely<Database>,\n\tfromBlock: number,\n\ttoBlock: number,\n\tdatasets: readonly RangeDigestDataset[] = ARCHIVE_DATASETS,\n): Promise<RangeDigest[]> {\n\tconst digests: RangeDigest[] = [];\n\tfor (const dataset of datasets) {\n\t\tdigests.push(await computeRangeDigest(db, dataset, fromBlock, toBlock));\n\t}\n\treturn digests;\n}\n\nexport type RangeComparison = {\n\tdataset: RangeDigestDataset;\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\n/**\n * Compare locally computed digests against a reference set.\n *\n * Row count is reported separately from digest because the two failures mean\n * different things to an operator: a count difference is missing or extra data\n * (usually a gap or an un-swept reorg), while an equal count with a different\n * digest means the same number of rows carry different identities — the\n * fork-point corruption signature.\n */\nexport function compareRangeDigests(\n\tlocal: readonly RangeDigest[],\n\treference: readonly RangeDigest[],\n): RangeComparison[] {\n\tconst localByKey = new Map(\n\t\tlocal.map((d) => [`${d.dataset}:${d.from_block}-${d.to_block}`, d]),\n\t);\n\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"
|
|
6
7
|
],
|
|
7
|
-
"mappings": ";AAAA;
|
|
8
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "E7DFA890F7D5EB2E64756E2164756E21",
|
|
9
10
|
"names": []
|
|
10
11
|
}
|
|
@@ -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
|
* Cheap, SQL-computed digests over a height range — the basis of a verify that
|
|
3
18
|
* runs in seconds instead of hours.
|
|
4
19
|
*
|
|
@@ -25,7 +40,7 @@
|
|
|
25
40
|
* (timestamps with timezone rendering, generated ids, `created_at`).
|
|
26
41
|
*/
|
|
27
42
|
declare const RANGE_DIGEST_SPEC: "md5:sql-identity-v1";
|
|
28
|
-
type RangeDigestDataset =
|
|
43
|
+
type RangeDigestDataset = ArchiveDataset;
|
|
29
44
|
type RangeDigest = {
|
|
30
45
|
dataset: RangeDigestDataset
|
|
31
46
|
from_block: number
|
|
@@ -71,6 +86,7 @@ type RangeDigest = {
|
|
|
71
86
|
* - blocks: (height asc)
|
|
72
87
|
* - transactions: (block_height asc, tx_index asc, tx_id asc)
|
|
73
88
|
* - events: (block_height asc, event_index asc, tx_id asc)
|
|
89
|
+
* - state_writes: (block_height asc, ordinal asc)
|
|
74
90
|
* Per-partition rollup is `sha256(concat(row_digest_bytes))` in stream order.
|
|
75
91
|
*
|
|
76
92
|
* Test vectors under `packages/shared/src/archive/__fixtures__/semantic-digest/`
|
|
@@ -84,7 +100,7 @@ type SemanticDigestSpec = typeof SEMANTIC_DIGEST_SPEC_V1;
|
|
|
84
100
|
* proving semantic identity independent of file encoding.
|
|
85
101
|
*/
|
|
86
102
|
interface PartitionSemanticDigest {
|
|
87
|
-
dataset:
|
|
103
|
+
dataset: ArchiveDataset;
|
|
88
104
|
from_block: number;
|
|
89
105
|
to_block: number;
|
|
90
106
|
row_count: number;
|
|
@@ -1374,6 +1374,21 @@ interface FeedbackTicketsTable {
|
|
|
1374
1374
|
created_at: Generated<Date>;
|
|
1375
1375
|
}
|
|
1376
1376
|
/**
|
|
1377
|
+
* The canonical archive's datasets — the one list every exporter, loader,
|
|
1378
|
+
* verifier and fetch gate derives from.
|
|
1379
|
+
*
|
|
1380
|
+
* Order is load order: each dataset's foreign keys point only at datasets
|
|
1381
|
+
* earlier in the list, so loading in this order never violates a constraint.
|
|
1382
|
+
*
|
|
1383
|
+
* Per-dataset code branches with an exhaustive `switch` ending in
|
|
1384
|
+
* `assertNeverDataset`, never a trailing default: adding a dataset here must
|
|
1385
|
+
* fail to compile everywhere it needs its own schema, SQL or table, instead of
|
|
1386
|
+
* silently inheriting another dataset's.
|
|
1387
|
+
*/
|
|
1388
|
+
declare const ARCHIVE_DATASETS: readonly ["blocks", "transactions", "events", "state_writes"];
|
|
1389
|
+
/** One archive dataset: blocks, transactions, events or state writes. */
|
|
1390
|
+
type ArchiveDataset = (typeof ARCHIVE_DATASETS)[number];
|
|
1391
|
+
/**
|
|
1377
1392
|
* v1 canonical byte encoding for chain identity — the shape that lets
|
|
1378
1393
|
* independently-run runtimes agree on the same block.
|
|
1379
1394
|
*
|
|
@@ -1407,6 +1422,7 @@ interface FeedbackTicketsTable {
|
|
|
1407
1422
|
* - blocks: (height asc)
|
|
1408
1423
|
* - transactions: (block_height asc, tx_index asc, tx_id asc)
|
|
1409
1424
|
* - events: (block_height asc, event_index asc, tx_id asc)
|
|
1425
|
+
* - state_writes: (block_height asc, ordinal asc)
|
|
1410
1426
|
* Per-partition rollup is `sha256(concat(row_digest_bytes))` in stream order.
|
|
1411
1427
|
*
|
|
1412
1428
|
* Test vectors under `packages/shared/src/archive/__fixtures__/semantic-digest/`
|
|
@@ -1420,7 +1436,7 @@ type SemanticDigestSpec = typeof SEMANTIC_DIGEST_SPEC_V1;
|
|
|
1420
1436
|
* proving semantic identity independent of file encoding.
|
|
1421
1437
|
*/
|
|
1422
1438
|
interface PartitionSemanticDigest {
|
|
1423
|
-
dataset:
|
|
1439
|
+
dataset: ArchiveDataset;
|
|
1424
1440
|
from_block: number;
|
|
1425
1441
|
to_block: number;
|
|
1426
1442
|
row_count: number;
|
|
@@ -1438,11 +1454,22 @@ interface PartitionSemanticDigest {
|
|
|
1438
1454
|
* correct — and there is no way to tell which is wrong. Any change here needs
|
|
1439
1455
|
* a matching change there.
|
|
1440
1456
|
*/
|
|
1441
|
-
type CanonicalDataset =
|
|
1457
|
+
type CanonicalDataset = ArchiveDataset;
|
|
1458
|
+
/** One row's semantic-v1 digest with the height it belongs to. */
|
|
1459
|
+
type HeightDigest = {
|
|
1460
|
+
height: number
|
|
1461
|
+
digest: string
|
|
1462
|
+
};
|
|
1442
1463
|
interface StreamOptions {
|
|
1443
1464
|
batchRows?: number;
|
|
1444
1465
|
}
|
|
1445
1466
|
/**
|
|
1467
|
+
* Row digests with their heights, in the exporter's total order. Additive:
|
|
1468
|
+
* the per-height sidecar rebuild needs the height the exporter fed its
|
|
1469
|
+
* accumulator, which the digest string alone does not carry.
|
|
1470
|
+
*/
|
|
1471
|
+
declare function streamHeightDigests(db: Kysely<Database>, dataset: CanonicalDataset, fromBlock: number, toBlock: number, options?: StreamOptions): AsyncGenerator<HeightDigest>;
|
|
1472
|
+
/**
|
|
1446
1473
|
* Compute one partition's semantic-v1 digest. Empty ranges return `null` — an
|
|
1447
1474
|
* empty range has no digest, distinct from "digest of zero rows" which would
|
|
1448
1475
|
* compare equal across datasets.
|
|
@@ -1459,4 +1486,4 @@ type PartitionSemanticComparison = {
|
|
|
1459
1486
|
actual_rows: number
|
|
1460
1487
|
};
|
|
1461
1488
|
declare function comparePartitionSemanticDigests(local: readonly PartitionSemanticDigest[], reference: readonly PartitionSemanticDigest[]): PartitionSemanticComparison[];
|
|
1462
|
-
export { CanonicalDataset, PartitionSemanticComparison, comparePartitionSemanticDigests, computePartitionSemanticDigest };
|
|
1489
|
+
export { CanonicalDataset, HeightDigest, PartitionSemanticComparison, comparePartitionSemanticDigests, computePartitionSemanticDigest, streamHeightDigests };
|