@secondlayer/shared 7.8.0 → 7.9.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/dist/src/archive/derived-stage-report.d.ts +99 -0
- package/dist/src/archive/derived-stage-report.js +64 -0
- package/dist/src/archive/derived-stage-report.js.map +10 -0
- package/dist/src/archive/disk-guard.d.ts +46 -0
- package/dist/src/archive/disk-guard.js +69 -0
- package/dist/src/archive/disk-guard.js.map +10 -0
- package/dist/src/archive/per-height-digest.d.ts +99 -0
- package/dist/src/archive/per-height-digest.js +194 -0
- package/dist/src/archive/per-height-digest.js.map +11 -0
- package/dist/src/archive/range-digest.d.ts +7 -0
- package/dist/src/archive/semantic-digest-builder.d.ts +1094 -0
- package/dist/src/archive/semantic-digest-builder.js +302 -0
- package/dist/src/archive/semantic-digest-builder.js.map +11 -0
- package/dist/src/archive/semantic-digest.d.ts +131 -0
- package/dist/src/archive/semantic-digest.js +143 -0
- package/dist/src/archive/semantic-digest.js.map +10 -0
- package/dist/src/db/index.d.ts +11 -1
- package/dist/src/db/index.js +26 -1
- package/dist/src/db/index.js.map +6 -5
- package/dist/src/db/queries/chain-reorgs.d.ts +7 -0
- package/dist/src/db/queries/chain-reorgs.js +25 -1
- package/dist/src/db/queries/chain-reorgs.js.map +6 -5
- package/dist/src/db/queries/contracts.d.ts +7 -0
- package/dist/src/db/queries/instance.d.ts +1038 -0
- package/dist/src/db/queries/instance.js +92 -0
- package/dist/src/db/queries/instance.js.map +10 -0
- package/dist/src/db/queries/integrity.d.ts +7 -0
- package/dist/src/db/queries/subgraph-gaps.d.ts +7 -0
- package/dist/src/db/queries/subgraph-operations.d.ts +7 -0
- package/dist/src/db/queries/subgraphs.d.ts +10 -1
- package/dist/src/db/queries/subgraphs.js +47 -23
- package/dist/src/db/queries/subgraphs.js.map +7 -7
- package/dist/src/db/queries/subscriptions.d.ts +7 -0
- package/dist/src/db/queries/subscriptions.js +47 -31
- package/dist/src/db/queries/subscriptions.js.map +5 -5
- package/dist/src/db/schema.d.ts +8 -1
- package/dist/src/index.d.ts +11 -1
- package/dist/src/index.js +26 -1
- package/dist/src/index.js.map +6 -5
- package/dist/src/node/client.d.ts +11 -1
- package/dist/src/node/client.js +42 -4
- package/dist/src/node/client.js.map +3 -3
- package/dist/src/node/local-client.d.ts +7 -0
- package/dist/src/node/nakamoto.d.ts +22 -7
- package/dist/src/node/nakamoto.js +46 -5
- package/dist/src/node/nakamoto.js.map +3 -3
- package/migrations/0115_instances.ts +33 -0
- package/package.json +25 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/archive/semantic-digest.ts
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
var SEMANTIC_DIGEST_SPEC_V1 = "sha256:semantic-v1";
|
|
20
|
+
var FIELD_SEP = "\x1F";
|
|
21
|
+
var NULL_MARKER = "\x00";
|
|
22
|
+
var textEncoder = new TextEncoder;
|
|
23
|
+
function canonicalJson(value) {
|
|
24
|
+
if (value === null)
|
|
25
|
+
return "null";
|
|
26
|
+
if (typeof value === "string" || typeof value === "boolean") {
|
|
27
|
+
return JSON.stringify(value);
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === "number") {
|
|
30
|
+
if (!Number.isFinite(value)) {
|
|
31
|
+
throw new Error("canonicalJson: non-finite number");
|
|
32
|
+
}
|
|
33
|
+
return JSON.stringify(value);
|
|
34
|
+
}
|
|
35
|
+
if (typeof value === "bigint")
|
|
36
|
+
return JSON.stringify(value.toString());
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === "object") {
|
|
41
|
+
const object = value;
|
|
42
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
|
|
43
|
+
}
|
|
44
|
+
return "null";
|
|
45
|
+
}
|
|
46
|
+
function scalar(value) {
|
|
47
|
+
if (value === null || value === undefined)
|
|
48
|
+
return NULL_MARKER;
|
|
49
|
+
if (typeof value === "number") {
|
|
50
|
+
if (!Number.isFinite(value)) {
|
|
51
|
+
throw new Error("semanticDigest: non-finite number");
|
|
52
|
+
}
|
|
53
|
+
if (!Number.isInteger(value)) {
|
|
54
|
+
throw new Error(`semanticDigest: non-integer scalar ${value}`);
|
|
55
|
+
}
|
|
56
|
+
return String(value);
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "bigint")
|
|
59
|
+
return value.toString();
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function jsonScalar(value) {
|
|
63
|
+
return value === null || value === undefined ? NULL_MARKER : canonicalJson(value);
|
|
64
|
+
}
|
|
65
|
+
function sha256Hex(input) {
|
|
66
|
+
return createHash("sha256").update(input).digest("hex");
|
|
67
|
+
}
|
|
68
|
+
function encodeBlock(row) {
|
|
69
|
+
return [
|
|
70
|
+
scalar(row.height),
|
|
71
|
+
scalar(row.hash),
|
|
72
|
+
scalar(row.parent_hash),
|
|
73
|
+
scalar(row.burn_block_height),
|
|
74
|
+
scalar(row.burn_block_hash),
|
|
75
|
+
scalar(row.index_block_hash),
|
|
76
|
+
scalar(row.timestamp)
|
|
77
|
+
].join(FIELD_SEP);
|
|
78
|
+
}
|
|
79
|
+
function encodeTransaction(row) {
|
|
80
|
+
return [
|
|
81
|
+
scalar(row.tx_id),
|
|
82
|
+
scalar(row.block_height),
|
|
83
|
+
scalar(row.tx_index),
|
|
84
|
+
scalar(row.type),
|
|
85
|
+
scalar(row.sender),
|
|
86
|
+
scalar(row.status),
|
|
87
|
+
scalar(row.contract_id),
|
|
88
|
+
scalar(row.function_name),
|
|
89
|
+
jsonScalar(row.function_args),
|
|
90
|
+
scalar(row.raw_result),
|
|
91
|
+
scalar(row.raw_tx)
|
|
92
|
+
].join(FIELD_SEP);
|
|
93
|
+
}
|
|
94
|
+
function encodeEvent(row) {
|
|
95
|
+
return [
|
|
96
|
+
scalar(row.tx_id),
|
|
97
|
+
scalar(row.block_height),
|
|
98
|
+
scalar(row.event_index),
|
|
99
|
+
scalar(row.type),
|
|
100
|
+
jsonScalar(row.data)
|
|
101
|
+
].join(FIELD_SEP);
|
|
102
|
+
}
|
|
103
|
+
var semanticDigest = {
|
|
104
|
+
v1: {
|
|
105
|
+
spec: SEMANTIC_DIGEST_SPEC_V1,
|
|
106
|
+
encodeBlock,
|
|
107
|
+
encodeTransaction,
|
|
108
|
+
encodeEvent,
|
|
109
|
+
block: (row) => sha256Hex(textEncoder.encode(encodeBlock(row))),
|
|
110
|
+
transaction: (row) => sha256Hex(textEncoder.encode(encodeTransaction(row))),
|
|
111
|
+
event: (row) => sha256Hex(textEncoder.encode(encodeEvent(row)))
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
class SemanticDigestRollup {
|
|
116
|
+
hasher = createHash("sha256");
|
|
117
|
+
count = 0;
|
|
118
|
+
static forDataset(_dataset) {
|
|
119
|
+
return new SemanticDigestRollup;
|
|
120
|
+
}
|
|
121
|
+
appendRowDigest(rowDigestHex) {
|
|
122
|
+
this.hasher.update(Buffer.from(rowDigestHex, "hex"));
|
|
123
|
+
this.count += 1;
|
|
124
|
+
}
|
|
125
|
+
rowCount() {
|
|
126
|
+
return this.count;
|
|
127
|
+
}
|
|
128
|
+
digest() {
|
|
129
|
+
return this.count === 0 ? null : this.hasher.copy().digest("hex");
|
|
130
|
+
}
|
|
131
|
+
spec() {
|
|
132
|
+
return SEMANTIC_DIGEST_SPEC_V1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/archive/semantic-digest-builder.ts
|
|
137
|
+
import { sql } from "kysely";
|
|
138
|
+
var DEFAULT_BATCH_ROWS = 20000;
|
|
139
|
+
async function* streamBlockDigests(db, fromBlock, toBlock, batchRows) {
|
|
140
|
+
let last = fromBlock - 1;
|
|
141
|
+
while (true) {
|
|
142
|
+
const rows = await db.selectFrom("blocks").select([
|
|
143
|
+
"height",
|
|
144
|
+
"hash",
|
|
145
|
+
"parent_hash",
|
|
146
|
+
"burn_block_height",
|
|
147
|
+
"burn_block_hash",
|
|
148
|
+
"index_block_hash",
|
|
149
|
+
"timestamp"
|
|
150
|
+
]).where("canonical", "=", true).where("height", ">", last).where("height", "<=", toBlock).orderBy("height", "asc").limit(batchRows).execute();
|
|
151
|
+
if (rows.length === 0)
|
|
152
|
+
return;
|
|
153
|
+
for (const row of rows) {
|
|
154
|
+
yield semanticDigest.v1.block({
|
|
155
|
+
height: Number(row.height),
|
|
156
|
+
hash: row.hash,
|
|
157
|
+
parent_hash: row.parent_hash,
|
|
158
|
+
burn_block_height: Number(row.burn_block_height),
|
|
159
|
+
burn_block_hash: row.burn_block_hash ?? null,
|
|
160
|
+
index_block_hash: row.index_block_hash ?? null,
|
|
161
|
+
timestamp: Number(row.timestamp)
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
last = Number(rows[rows.length - 1]?.height);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function* streamTransactionDigests(db, fromBlock, toBlock, batchRows) {
|
|
168
|
+
let cursor = null;
|
|
169
|
+
while (true) {
|
|
170
|
+
const result = await sql`
|
|
171
|
+
SELECT t.tx_id, t.block_height, t.tx_index, t.type, t.sender, t.status,
|
|
172
|
+
t.contract_id, t.function_name, t.function_args, t.raw_result, t.raw_tx
|
|
173
|
+
FROM transactions t
|
|
174
|
+
JOIN blocks b ON b.height = t.block_height AND b.canonical = true
|
|
175
|
+
WHERE t.block_height >= ${fromBlock} AND t.block_height <= ${toBlock}
|
|
176
|
+
AND (${cursor === null} OR (t.block_height, t.tx_index, t.tx_id) >
|
|
177
|
+
(${cursor?.height ?? 0}, ${cursor?.txIndex ?? 0}, ${cursor?.txId ?? ""}))
|
|
178
|
+
ORDER BY t.block_height, t.tx_index, t.tx_id
|
|
179
|
+
LIMIT ${batchRows}
|
|
180
|
+
`.execute(db);
|
|
181
|
+
const rows = result.rows;
|
|
182
|
+
if (rows.length === 0)
|
|
183
|
+
return;
|
|
184
|
+
for (const row of rows) {
|
|
185
|
+
yield semanticDigest.v1.transaction({
|
|
186
|
+
tx_id: row.tx_id,
|
|
187
|
+
block_height: Number(row.block_height),
|
|
188
|
+
tx_index: row.tx_index,
|
|
189
|
+
type: row.type,
|
|
190
|
+
sender: row.sender,
|
|
191
|
+
status: row.status,
|
|
192
|
+
contract_id: row.contract_id,
|
|
193
|
+
function_name: row.function_name,
|
|
194
|
+
function_args: row.function_args ?? null,
|
|
195
|
+
raw_result: row.raw_result,
|
|
196
|
+
raw_tx: row.raw_tx
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const tail = rows[rows.length - 1];
|
|
200
|
+
if (tail) {
|
|
201
|
+
cursor = {
|
|
202
|
+
height: Number(tail.block_height),
|
|
203
|
+
txIndex: tail.tx_index,
|
|
204
|
+
txId: tail.tx_id
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function* streamEventDigests(db, fromBlock, toBlock, batchRows) {
|
|
210
|
+
let cursor = null;
|
|
211
|
+
while (true) {
|
|
212
|
+
const result = await sql`
|
|
213
|
+
SELECT e.tx_id, e.block_height, e.event_index, e.type, e.data
|
|
214
|
+
FROM events e
|
|
215
|
+
JOIN blocks b ON b.height = e.block_height AND b.canonical = true
|
|
216
|
+
WHERE e.block_height >= ${fromBlock} AND e.block_height <= ${toBlock}
|
|
217
|
+
AND (${cursor === null} OR (e.block_height, e.event_index, e.tx_id) >
|
|
218
|
+
(${cursor?.height ?? 0}, ${cursor?.eventIndex ?? 0}, ${cursor?.txId ?? ""}))
|
|
219
|
+
ORDER BY e.block_height, e.event_index, e.tx_id
|
|
220
|
+
LIMIT ${batchRows}
|
|
221
|
+
`.execute(db);
|
|
222
|
+
const rows = result.rows;
|
|
223
|
+
if (rows.length === 0)
|
|
224
|
+
return;
|
|
225
|
+
for (const row of rows) {
|
|
226
|
+
yield semanticDigest.v1.event({
|
|
227
|
+
tx_id: row.tx_id,
|
|
228
|
+
block_height: Number(row.block_height),
|
|
229
|
+
event_index: row.event_index,
|
|
230
|
+
type: row.type,
|
|
231
|
+
data: row.data ?? null
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const tail = rows[rows.length - 1];
|
|
235
|
+
if (tail) {
|
|
236
|
+
cursor = {
|
|
237
|
+
height: Number(tail.block_height),
|
|
238
|
+
eventIndex: tail.event_index,
|
|
239
|
+
txId: tail.tx_id
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function digestStream(db, dataset, fromBlock, toBlock, batchRows) {
|
|
245
|
+
if (dataset === "blocks") {
|
|
246
|
+
return streamBlockDigests(db, fromBlock, toBlock, batchRows);
|
|
247
|
+
}
|
|
248
|
+
if (dataset === "transactions") {
|
|
249
|
+
return streamTransactionDigests(db, fromBlock, toBlock, batchRows);
|
|
250
|
+
}
|
|
251
|
+
return streamEventDigests(db, fromBlock, toBlock, batchRows);
|
|
252
|
+
}
|
|
253
|
+
async function computePartitionSemanticDigest(db, dataset, fromBlock, toBlock, options = {}) {
|
|
254
|
+
const rollup = SemanticDigestRollup.forDataset(dataset);
|
|
255
|
+
for await (const rowDigest of digestStream(db, dataset, fromBlock, toBlock, options.batchRows ?? DEFAULT_BATCH_ROWS)) {
|
|
256
|
+
rollup.appendRowDigest(rowDigest);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
dataset,
|
|
260
|
+
from_block: fromBlock,
|
|
261
|
+
to_block: toBlock,
|
|
262
|
+
row_count: rollup.rowCount(),
|
|
263
|
+
digest: rollup.digest(),
|
|
264
|
+
digest_spec: SEMANTIC_DIGEST_SPEC_V1
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function comparePartitionSemanticDigests(local, reference) {
|
|
268
|
+
const localByKey = new Map(local.map((d) => [`${d.dataset}:${d.from_block}-${d.to_block}`, d]));
|
|
269
|
+
return reference.map((expected) => {
|
|
270
|
+
const key = `${expected.dataset}:${expected.from_block}-${expected.to_block}`;
|
|
271
|
+
const actual = localByKey.get(key);
|
|
272
|
+
const base = {
|
|
273
|
+
dataset: expected.dataset,
|
|
274
|
+
from_block: expected.from_block,
|
|
275
|
+
to_block: expected.to_block,
|
|
276
|
+
expected_digest: expected.digest,
|
|
277
|
+
expected_rows: expected.row_count
|
|
278
|
+
};
|
|
279
|
+
if (!actual) {
|
|
280
|
+
return {
|
|
281
|
+
...base,
|
|
282
|
+
status: "missing-locally",
|
|
283
|
+
actual_digest: null,
|
|
284
|
+
actual_rows: 0
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const status = actual.row_count !== expected.row_count ? "count-mismatch" : actual.digest === expected.digest ? "match" : "digest-mismatch";
|
|
288
|
+
return {
|
|
289
|
+
...base,
|
|
290
|
+
status,
|
|
291
|
+
actual_digest: actual.digest,
|
|
292
|
+
actual_rows: actual.row_count
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
export {
|
|
297
|
+
computePartitionSemanticDigest,
|
|
298
|
+
comparePartitionSemanticDigests
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
//# debugId=DAABB1E8285EB5FE64756E2164756E21
|
|
302
|
+
//# sourceMappingURL=semantic-digest-builder.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/archive/semantic-digest.ts", "../src/archive/semantic-digest-builder.ts"],
|
|
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",
|
|
6
|
+
"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"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "DAABB1E8285EB5FE64756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v1 canonical byte encoding for chain identity — the shape that lets
|
|
3
|
+
* independently-run runtimes agree on the same block.
|
|
4
|
+
*
|
|
5
|
+
* The exact-body digest (`sha256:parquet-object`) proves capture fidelity to one
|
|
6
|
+
* source: replay the same bytes, get the same rows. It cannot prove two nodes
|
|
7
|
+
* observed the same chain, because their Parquet encoders legally differ in
|
|
8
|
+
* column padding, compression, JSON key order, and null placement — two
|
|
9
|
+
* operators processing the same block can ship byte-different Parquets that
|
|
10
|
+
* carry identical content.
|
|
11
|
+
*
|
|
12
|
+
* The semantic digest is the shape that lets independent runtimes agree. Every
|
|
13
|
+
* value on a block/transaction/event is normalized into a canonical byte string
|
|
14
|
+
* by rules that MUST NOT change without a version bump. Two v1 computations
|
|
15
|
+
* over the same range agree, or one is wrong about the chain — never about
|
|
16
|
+
* formatting.
|
|
17
|
+
*
|
|
18
|
+
* Encoding rules (v1):
|
|
19
|
+
* - Fields join with `\x1f` (ASCII US, unit separator — cannot legally
|
|
20
|
+
* appear in any chain value; a printable delimiter risks collision with
|
|
21
|
+
* contract-emitted strings).
|
|
22
|
+
* - Null encodes as `\x00` (single NUL). Distinct from empty string `""`.
|
|
23
|
+
* - Integers encode as their base-10 string. bigint → decimal string.
|
|
24
|
+
* - JSON payload columns (function_args, event data) encode as canonical
|
|
25
|
+
* JSON: object keys sorted lexicographically, no whitespace, bigint →
|
|
26
|
+
* quoted decimal string, non-finite numbers rejected.
|
|
27
|
+
* - Field order per record type is fixed and never reordered.
|
|
28
|
+
* - Digest = sha256(bytes), lowercase hex.
|
|
29
|
+
*
|
|
30
|
+
* Rollup rules (v1) — order MUST match the archive's stream order or an
|
|
31
|
+
* honest re-will disagree with the manifest:
|
|
32
|
+
* - blocks: (height asc)
|
|
33
|
+
* - transactions: (block_height asc, tx_index asc, tx_id asc)
|
|
34
|
+
* - events: (block_height asc, event_index asc, tx_id asc)
|
|
35
|
+
* Per-partition rollup is `sha256(concat(row_digest_bytes))` in stream order.
|
|
36
|
+
*
|
|
37
|
+
* Test vectors under `packages/shared/src/archive/__fixtures__/semantic-digest/`
|
|
38
|
+
* pin the encoding. Any change to bytes must bump the spec version.
|
|
39
|
+
*/
|
|
40
|
+
declare const SEMANTIC_DIGEST_SPEC_V1: "sha256:semantic-v1";
|
|
41
|
+
type SemanticDigestSpec = typeof SEMANTIC_DIGEST_SPEC_V1;
|
|
42
|
+
/**
|
|
43
|
+
* Stable JSON — sorted keys, no whitespace, bigint → decimal string. Distinct
|
|
44
|
+
* from `JSON.stringify(x)` which respects insertion order and rejects bigints.
|
|
45
|
+
* Also rejects non-finite numbers, since NaN/Infinity have no on-chain meaning
|
|
46
|
+
* and would silently hash as `null` under `JSON.stringify`.
|
|
47
|
+
*/
|
|
48
|
+
declare function canonicalJson(value: unknown): string;
|
|
49
|
+
interface BlockDigestInput {
|
|
50
|
+
height: number | bigint;
|
|
51
|
+
hash: string;
|
|
52
|
+
parent_hash: string;
|
|
53
|
+
burn_block_height: number | bigint;
|
|
54
|
+
burn_block_hash: string | null;
|
|
55
|
+
index_block_hash: string | null;
|
|
56
|
+
timestamp: number | bigint;
|
|
57
|
+
}
|
|
58
|
+
interface TransactionDigestInput {
|
|
59
|
+
tx_id: string;
|
|
60
|
+
block_height: number | bigint;
|
|
61
|
+
tx_index: number;
|
|
62
|
+
type: string;
|
|
63
|
+
sender: string;
|
|
64
|
+
status: string;
|
|
65
|
+
contract_id: string | null;
|
|
66
|
+
function_name: string | null;
|
|
67
|
+
function_args: unknown;
|
|
68
|
+
raw_result: string | null;
|
|
69
|
+
raw_tx: string;
|
|
70
|
+
}
|
|
71
|
+
interface EventDigestInput {
|
|
72
|
+
tx_id: string;
|
|
73
|
+
block_height: number | bigint;
|
|
74
|
+
event_index: number;
|
|
75
|
+
type: string;
|
|
76
|
+
data: unknown;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* v1 per-row digests. Each is `sha256(encoded_row_bytes)` in lowercase hex.
|
|
80
|
+
* The encoded byte form is stable across runtimes; the sha256 is stable
|
|
81
|
+
* across CPU architectures. Never mutate an encoder without a v-bump.
|
|
82
|
+
*/
|
|
83
|
+
interface SemanticDigestV1 {
|
|
84
|
+
readonly spec: SemanticDigestSpec;
|
|
85
|
+
readonly encodeBlock: (row: BlockDigestInput) => string;
|
|
86
|
+
readonly encodeTransaction: (row: TransactionDigestInput) => string;
|
|
87
|
+
readonly encodeEvent: (row: EventDigestInput) => string;
|
|
88
|
+
readonly block: (row: BlockDigestInput) => string;
|
|
89
|
+
readonly transaction: (row: TransactionDigestInput) => string;
|
|
90
|
+
readonly event: (row: EventDigestInput) => string;
|
|
91
|
+
}
|
|
92
|
+
interface SemanticDigestApi {
|
|
93
|
+
readonly v1: SemanticDigestV1;
|
|
94
|
+
}
|
|
95
|
+
declare const semanticDigest: SemanticDigestApi;
|
|
96
|
+
/**
|
|
97
|
+
* Streaming rollup — feed rows one at a time in their declared order, read the
|
|
98
|
+
* rolled digest at the end. Two callers passing the same rows in the same order
|
|
99
|
+
* produce the same rollup, regardless of batching.
|
|
100
|
+
*
|
|
101
|
+
* The rollup composes: partition_digest = sha256(concat(row_digest_bytes)),
|
|
102
|
+
* where each row_digest_bytes = sha256(encode(row)). This is why the streaming
|
|
103
|
+
* implementation can be constant-memory — the outer sha256 accepts row digests
|
|
104
|
+
* incrementally.
|
|
105
|
+
*/
|
|
106
|
+
declare class SemanticDigestRollup {
|
|
107
|
+
private readonly hasher;
|
|
108
|
+
private count;
|
|
109
|
+
static forDataset(_dataset: "blocks" | "transactions" | "events"): SemanticDigestRollup;
|
|
110
|
+
appendRowDigest(rowDigestHex: string): void;
|
|
111
|
+
rowCount(): number;
|
|
112
|
+
/** Empty rollups return `null`, not the sha256 of nothing, so an empty
|
|
113
|
+
* range never compares equal to another empty range on a different
|
|
114
|
+
* dataset. Callers assign `null` semantics deliberately. */
|
|
115
|
+
digest(): string | null;
|
|
116
|
+
spec(): SemanticDigestSpec;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Per-partition digest record — the shape that lands in the snapshot manifest.
|
|
120
|
+
* Complements the existing `sha256:parquet-object` per-partition digest by
|
|
121
|
+
* proving semantic identity independent of file encoding.
|
|
122
|
+
*/
|
|
123
|
+
interface PartitionSemanticDigest {
|
|
124
|
+
dataset: "blocks" | "transactions" | "events";
|
|
125
|
+
from_block: number;
|
|
126
|
+
to_block: number;
|
|
127
|
+
row_count: number;
|
|
128
|
+
digest: string | null;
|
|
129
|
+
digest_spec: SemanticDigestSpec;
|
|
130
|
+
}
|
|
131
|
+
export { semanticDigest, canonicalJson, TransactionDigestInput, SemanticDigestV1, SemanticDigestSpec, SemanticDigestRollup, SemanticDigestApi, SEMANTIC_DIGEST_SPEC_V1, PartitionSemanticDigest, EventDigestInput, BlockDigestInput };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/archive/semantic-digest.ts
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
var SEMANTIC_DIGEST_SPEC_V1 = "sha256:semantic-v1";
|
|
20
|
+
var FIELD_SEP = "\x1F";
|
|
21
|
+
var NULL_MARKER = "\x00";
|
|
22
|
+
var textEncoder = new TextEncoder;
|
|
23
|
+
function canonicalJson(value) {
|
|
24
|
+
if (value === null)
|
|
25
|
+
return "null";
|
|
26
|
+
if (typeof value === "string" || typeof value === "boolean") {
|
|
27
|
+
return JSON.stringify(value);
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === "number") {
|
|
30
|
+
if (!Number.isFinite(value)) {
|
|
31
|
+
throw new Error("canonicalJson: non-finite number");
|
|
32
|
+
}
|
|
33
|
+
return JSON.stringify(value);
|
|
34
|
+
}
|
|
35
|
+
if (typeof value === "bigint")
|
|
36
|
+
return JSON.stringify(value.toString());
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === "object") {
|
|
41
|
+
const object = value;
|
|
42
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
|
|
43
|
+
}
|
|
44
|
+
return "null";
|
|
45
|
+
}
|
|
46
|
+
function scalar(value) {
|
|
47
|
+
if (value === null || value === undefined)
|
|
48
|
+
return NULL_MARKER;
|
|
49
|
+
if (typeof value === "number") {
|
|
50
|
+
if (!Number.isFinite(value)) {
|
|
51
|
+
throw new Error("semanticDigest: non-finite number");
|
|
52
|
+
}
|
|
53
|
+
if (!Number.isInteger(value)) {
|
|
54
|
+
throw new Error(`semanticDigest: non-integer scalar ${value}`);
|
|
55
|
+
}
|
|
56
|
+
return String(value);
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "bigint")
|
|
59
|
+
return value.toString();
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function jsonScalar(value) {
|
|
63
|
+
return value === null || value === undefined ? NULL_MARKER : canonicalJson(value);
|
|
64
|
+
}
|
|
65
|
+
function sha256Hex(input) {
|
|
66
|
+
return createHash("sha256").update(input).digest("hex");
|
|
67
|
+
}
|
|
68
|
+
function encodeBlock(row) {
|
|
69
|
+
return [
|
|
70
|
+
scalar(row.height),
|
|
71
|
+
scalar(row.hash),
|
|
72
|
+
scalar(row.parent_hash),
|
|
73
|
+
scalar(row.burn_block_height),
|
|
74
|
+
scalar(row.burn_block_hash),
|
|
75
|
+
scalar(row.index_block_hash),
|
|
76
|
+
scalar(row.timestamp)
|
|
77
|
+
].join(FIELD_SEP);
|
|
78
|
+
}
|
|
79
|
+
function encodeTransaction(row) {
|
|
80
|
+
return [
|
|
81
|
+
scalar(row.tx_id),
|
|
82
|
+
scalar(row.block_height),
|
|
83
|
+
scalar(row.tx_index),
|
|
84
|
+
scalar(row.type),
|
|
85
|
+
scalar(row.sender),
|
|
86
|
+
scalar(row.status),
|
|
87
|
+
scalar(row.contract_id),
|
|
88
|
+
scalar(row.function_name),
|
|
89
|
+
jsonScalar(row.function_args),
|
|
90
|
+
scalar(row.raw_result),
|
|
91
|
+
scalar(row.raw_tx)
|
|
92
|
+
].join(FIELD_SEP);
|
|
93
|
+
}
|
|
94
|
+
function encodeEvent(row) {
|
|
95
|
+
return [
|
|
96
|
+
scalar(row.tx_id),
|
|
97
|
+
scalar(row.block_height),
|
|
98
|
+
scalar(row.event_index),
|
|
99
|
+
scalar(row.type),
|
|
100
|
+
jsonScalar(row.data)
|
|
101
|
+
].join(FIELD_SEP);
|
|
102
|
+
}
|
|
103
|
+
var semanticDigest = {
|
|
104
|
+
v1: {
|
|
105
|
+
spec: SEMANTIC_DIGEST_SPEC_V1,
|
|
106
|
+
encodeBlock,
|
|
107
|
+
encodeTransaction,
|
|
108
|
+
encodeEvent,
|
|
109
|
+
block: (row) => sha256Hex(textEncoder.encode(encodeBlock(row))),
|
|
110
|
+
transaction: (row) => sha256Hex(textEncoder.encode(encodeTransaction(row))),
|
|
111
|
+
event: (row) => sha256Hex(textEncoder.encode(encodeEvent(row)))
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
class SemanticDigestRollup {
|
|
116
|
+
hasher = createHash("sha256");
|
|
117
|
+
count = 0;
|
|
118
|
+
static forDataset(_dataset) {
|
|
119
|
+
return new SemanticDigestRollup;
|
|
120
|
+
}
|
|
121
|
+
appendRowDigest(rowDigestHex) {
|
|
122
|
+
this.hasher.update(Buffer.from(rowDigestHex, "hex"));
|
|
123
|
+
this.count += 1;
|
|
124
|
+
}
|
|
125
|
+
rowCount() {
|
|
126
|
+
return this.count;
|
|
127
|
+
}
|
|
128
|
+
digest() {
|
|
129
|
+
return this.count === 0 ? null : this.hasher.copy().digest("hex");
|
|
130
|
+
}
|
|
131
|
+
spec() {
|
|
132
|
+
return SEMANTIC_DIGEST_SPEC_V1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export {
|
|
136
|
+
semanticDigest,
|
|
137
|
+
canonicalJson,
|
|
138
|
+
SemanticDigestRollup,
|
|
139
|
+
SEMANTIC_DIGEST_SPEC_V1
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
//# debugId=85768A1D774EC06C64756E2164756E21
|
|
143
|
+
//# sourceMappingURL=semantic-digest.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/archive/semantic-digest.ts"],
|
|
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"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "85768A1D774EC06C64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|