@secondlayer/shared 11.14.1 → 11.14.2

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.
@@ -29,6 +29,9 @@ interface BlocksTable {
29
29
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
30
30
  timestamp: number;
31
31
  canonical: Generated<boolean>;
32
+ /** Tx count `persistBlock` was handed for this height. Null on rows
33
+ * ingested before this column existed. */
34
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
32
35
  created_at: Generated<Date>;
33
36
  }
34
37
  interface TransactionsTable {
@@ -29,6 +29,9 @@ interface BlocksTable {
29
29
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
30
30
  timestamp: number;
31
31
  canonical: Generated<boolean>;
32
+ /** Tx count `persistBlock` was handed for this height. Null on rows
33
+ * ingested before this column existed. */
34
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
32
35
  created_at: Generated<Date>;
33
36
  }
34
37
  interface TransactionsTable {
@@ -346,6 +346,9 @@ interface BlocksTable {
346
346
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
347
347
  timestamp: number;
348
348
  canonical: Generated<boolean>;
349
+ /** Tx count `persistBlock` was handed for this height. Null on rows
350
+ * ingested before this column existed. */
351
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
349
352
  created_at: Generated<Date>;
350
353
  }
351
354
  interface TransactionsTable {
@@ -30,6 +30,9 @@ interface BlocksTable {
30
30
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
31
31
  timestamp: number;
32
32
  canonical: Generated<boolean>;
33
+ /** Tx count `persistBlock` was handed for this height. Null on rows
34
+ * ingested before this column existed. */
35
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
33
36
  created_at: Generated<Date>;
34
37
  }
35
38
  interface TransactionsTable {
@@ -29,6 +29,9 @@ interface BlocksTable {
29
29
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
30
30
  timestamp: number;
31
31
  canonical: Generated<boolean>;
32
+ /** Tx count `persistBlock` was handed for this height. Null on rows
33
+ * ingested before this column existed. */
34
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
32
35
  created_at: Generated<Date>;
33
36
  }
34
37
  interface TransactionsTable {
@@ -29,6 +29,9 @@ interface BlocksTable {
29
29
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
30
30
  timestamp: number;
31
31
  canonical: Generated<boolean>;
32
+ /** Tx count `persistBlock` was handed for this height. Null on rows
33
+ * ingested before this column existed. */
34
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
32
35
  created_at: Generated<Date>;
33
36
  }
34
37
  interface TransactionsTable {
@@ -29,6 +29,9 @@ interface BlocksTable {
29
29
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
30
30
  timestamp: number;
31
31
  canonical: Generated<boolean>;
32
+ /** Tx count `persistBlock` was handed for this height. Null on rows
33
+ * ingested before this column existed. */
34
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
32
35
  created_at: Generated<Date>;
33
36
  }
34
37
  interface TransactionsTable {
@@ -29,6 +29,9 @@ interface BlocksTable {
29
29
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
30
30
  timestamp: number;
31
31
  canonical: Generated<boolean>;
32
+ /** Tx count `persistBlock` was handed for this height. Null on rows
33
+ * ingested before this column existed. */
34
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
32
35
  created_at: Generated<Date>;
33
36
  }
34
37
  interface TransactionsTable {
@@ -995,6 +998,30 @@ declare function checkChainDataIntegrity(db: Kysely<Database>, opts?: {
995
998
  lookback?: number
996
999
  }): Promise<ChainDataIntegrity>;
997
1000
  declare function computeContiguousTip(db: Kysely<Database>, fromHeight: number): Promise<number>;
1001
+ type ShortBlock = {
1002
+ height: number
1003
+ expectedTxCount: number
1004
+ actualTxCount: number
1005
+ };
1006
+ /**
1007
+ * Canonical heights where `transactions` holds fewer (or more) rows than the
1008
+ * block was supposed to. `blocks.tx_count` is set by `persistBlock` from the
1009
+ * incoming block, so a mismatch means a later write silently moved or dropped
1010
+ * a tx after that block was persisted — a reorg replaying the wrong height, a
1011
+ * same-height tx_id collision, or a bug in the persist path itself. Ingest
1012
+ * used to report the *parsed* count, not the inserted count, so this kind of
1013
+ * short block was invisible; a skipped insert looked identical to success.
1014
+ *
1015
+ * Scoped to heights with `tx_count IS NOT NULL` — rows ingested before this
1016
+ * column existed have no baseline to check against. Also scoped to a recent
1017
+ * window by default (like `findBrokenLinks`): every new block sets
1018
+ * `tx_count`, so the eligible range only grows, and a short block is an
1019
+ * active-repair concern, not something worth a full-table scan every cycle.
1020
+ */
1021
+ declare function findShortBlocks(db: Kysely<Database>, opts?: {
1022
+ window?: number | null
1023
+ limit?: number
1024
+ }): Promise<ShortBlock[]>;
998
1025
  type BrokenLink = {
999
1026
  height: number
1000
1027
  hash: string
@@ -1020,4 +1047,4 @@ declare function findBrokenLinks(db: Kysely<Database>, opts?: {
1020
1047
  window?: number | null
1021
1048
  limit?: number
1022
1049
  }): Promise<BrokenLink[]>;
1023
- export { BrokenLink, ChainDataIntegrity, Gap, checkChainDataIntegrity, computeContiguousTip, countMissingBlocks, findBrokenLinks, findGaps };
1050
+ export { BrokenLink, ChainDataIntegrity, Gap, ShortBlock, checkChainDataIntegrity, computeContiguousTip, countMissingBlocks, findBrokenLinks, findGaps, findShortBlocks };
@@ -76,6 +76,28 @@ async function computeContiguousTip(db, fromHeight) {
76
76
  `.execute(db);
77
77
  return Number(rows[0]?.tip ?? fromHeight);
78
78
  }
79
+ async function findShortBlocks(db, opts = {}) {
80
+ const windowSize = opts.window === undefined ? 1e4 : opts.window;
81
+ const limitClause = opts.limit ? sql`LIMIT ${opts.limit}` : sql``;
82
+ const windowClause = windowSize === null ? sql`` : sql`AND b.height > (SELECT MAX(height) - ${windowSize} FROM blocks WHERE canonical = true)`;
83
+ const { rows } = await sql`
84
+ SELECT b.height, b.tx_count, COUNT(t.tx_id) AS actual_count
85
+ FROM blocks b
86
+ LEFT JOIN transactions t ON t.block_height = b.height
87
+ WHERE b.canonical = true
88
+ AND b.tx_count IS NOT NULL
89
+ ${windowClause}
90
+ GROUP BY b.height, b.tx_count
91
+ HAVING COUNT(t.tx_id) <> b.tx_count
92
+ ORDER BY b.height
93
+ ${limitClause}
94
+ `.execute(db);
95
+ return rows.map((r) => ({
96
+ height: Number(r.height),
97
+ expectedTxCount: Number(r.tx_count),
98
+ actualTxCount: Number(r.actual_count)
99
+ }));
100
+ }
79
101
  async function findBrokenLinks(db, opts = {}) {
80
102
  const windowSize = opts.window === undefined ? 1e4 : opts.window;
81
103
  const limitClause = opts.limit ? sql`LIMIT ${opts.limit}` : sql``;
@@ -106,8 +128,9 @@ export {
106
128
  computeContiguousTip,
107
129
  countMissingBlocks,
108
130
  findBrokenLinks,
109
- findGaps
131
+ findGaps,
132
+ findShortBlocks
110
133
  };
111
134
 
112
- //# debugId=1E6E1376EB78163164756E2164756E21
135
+ //# debugId=E92283960DE6C55264756E2164756E21
113
136
  //# sourceMappingURL=integrity.js.map
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/db/queries/integrity.ts"],
4
4
  "sourcesContent": [
5
- "import { type Kysely, sql } from \"kysely\";\nimport type { Database } from \"../types.ts\";\n\nexport interface Gap {\n\tgapStart: number;\n\tgapEnd: number;\n\tsize: number;\n}\n\nexport async function findGaps(\n\tdb: Kysely<Database>,\n\tlimit?: number,\n): Promise<Gap[]> {\n\tconst limitClause = limit ? sql`LIMIT ${limit}` : sql``;\n\tconst { rows } = await sql<{\n\t\tgap_start: string;\n\t\tgap_end: string;\n\t\tsize: string;\n\t}>`\n SELECT gap_start, gap_end, gap_end - gap_start + 1 AS size\n FROM (\n SELECT height + 1 AS gap_start, next_height - 1 AS gap_end\n FROM (\n SELECT height, LEAD(height) OVER (ORDER BY height) AS next_height\n FROM blocks WHERE canonical = true\n ) sub\n WHERE next_height - height > 1\n ) gaps\n ORDER BY gap_start\n ${limitClause}\n `.execute(db);\n\n\treturn rows.map((r) => ({\n\t\tgapStart: Number(r.gap_start),\n\t\tgapEnd: Number(r.gap_end),\n\t\tsize: Number(r.size),\n\t}));\n}\n\nexport async function countMissingBlocks(\n\tdb: Kysely<Database>,\n): Promise<number> {\n\tconst { rows } = await sql<{ total: string }>`\n SELECT COALESCE(SUM(next_height - height - 1), 0) AS total\n FROM (\n SELECT height, LEAD(height) OVER (ORDER BY height) AS next_height\n FROM blocks WHERE canonical = true\n ) sub\n WHERE next_height - height > 1\n `.execute(db);\n\n\treturn Number(rows[0]?.total ?? 0);\n}\n\nexport interface ChainDataIntegrity {\n\tok: boolean;\n\tmaxHeight: number;\n\tsampleHeight: number | null;\n\tsampleBlocks: number;\n\treason: string | null;\n}\n\n/**\n * Cheap sanity check that the `blocks` table actually holds the history its tip\n * implies. Catches a wrong/empty Postgres volume — e.g. a container recreated\n * against a fresh volume — being silently served: if the tip is high but a\n * window of blocks well below it is missing, the data isn't what the tip claims.\n *\n * Below `checkFloor` we can't distinguish a fresh install from an empty volume,\n * so we report ok — a genuinely new DB legitimately has little history. Two\n * indexed lookups, safe to run on every startup / health poll.\n */\nexport async function checkChainDataIntegrity(\n\tdb: Kysely<Database>,\n\topts?: { checkFloor?: number; lookback?: number },\n): Promise<ChainDataIntegrity> {\n\tconst checkFloor = opts?.checkFloor ?? 1_000_000;\n\tconst lookback = opts?.lookback ?? 500_000;\n\n\tconst { rows: tipRows } = await sql<{ max: string | null }>`\n\t\tSELECT MAX(height) AS max FROM blocks WHERE canonical = true\n\t`.execute(db);\n\tconst maxHeight = Number(tipRows[0]?.max ?? 0);\n\n\tif (maxHeight < checkFloor) {\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmaxHeight,\n\t\t\tsampleHeight: null,\n\t\t\tsampleBlocks: 0,\n\t\t\treason: null,\n\t\t};\n\t}\n\n\tconst sampleHeight = maxHeight - lookback;\n\tconst { rows: sampleRows } = await sql<{ n: string }>`\n\t\tSELECT count(*) AS n FROM blocks\n\t\tWHERE canonical = true AND height >= ${sampleHeight} AND height < ${sampleHeight + 1000}\n\t`.execute(db);\n\tconst sampleBlocks = Number(sampleRows[0]?.n ?? 0);\n\n\treturn {\n\t\tok: sampleBlocks > 0,\n\t\tmaxHeight,\n\t\tsampleHeight,\n\t\tsampleBlocks,\n\t\treason:\n\t\t\tsampleBlocks > 0\n\t\t\t\t? null\n\t\t\t\t: `tip is ${maxHeight} but no canonical blocks near ${sampleHeight} — wrong or empty volume?`,\n\t};\n}\n\nexport async function computeContiguousTip(\n\tdb: Kysely<Database>,\n\tfromHeight: number,\n): Promise<number> {\n\tconst { rows } = await sql<{ tip: string }>`\n SELECT COALESCE(MAX(height), ${fromHeight}) AS tip\n FROM (\n SELECT height, height - ROW_NUMBER() OVER (ORDER BY height) AS grp\n FROM blocks WHERE canonical = true AND height >= ${fromHeight}\n ) sub\n WHERE grp = (\n SELECT height - ROW_NUMBER() OVER (ORDER BY height)\n FROM blocks WHERE canonical = true AND height = ${fromHeight}\n )\n `.execute(db);\n\n\treturn Number(rows[0]?.tip ?? fromHeight);\n}\n\nexport type BrokenLink = {\n\theight: number;\n\thash: string;\n\texpectedParent: string;\n\tstoredParent: string;\n};\n\n/**\n * Canonical heights whose block does not descend from the canonical block\n * below it.\n *\n * `findGaps` asks \"is every height present?\", which a chain sitting on a losing\n * fork answers yes to. On 2026-07-30 a reorg at 8,663,166 was resolved the\n * wrong way: we adopted a block that mainnet orphaned, its true successors were\n * swept non-canonical, and later blocks were inserted canonical on top. Every\n * height existed. Nothing was \"missing\". The chain simply did not join up, and\n * no check asked that question — so we served a broken chain for seventeen\n * hours and only noticed because subgraphs stopped moving.\n *\n * Scoped to a recent window by default: an unlinked height is an active-repair\n * concern, and scanning eight million rows every cycle is not.\n */\nexport async function findBrokenLinks(\n\tdb: Kysely<Database>,\n\t/** Heights below the tip to scan; `null` scans the whole chain. */\n\topts: { window?: number | null; limit?: number } = {},\n): Promise<BrokenLink[]> {\n\tconst windowSize = opts.window === undefined ? 10_000 : opts.window;\n\tconst limitClause = opts.limit ? sql`LIMIT ${opts.limit}` : sql``;\n\tconst windowClause =\n\t\twindowSize === null\n\t\t\t? sql``\n\t\t\t: sql`AND height > (SELECT MAX(height) - ${windowSize} FROM blocks WHERE canonical = true)`;\n\tconst { rows } = await sql<{\n\t\theight: string | number;\n\t\thash: string;\n\t\texpected_parent: string;\n\t\tstored_parent: string;\n\t}>`\n\t\tWITH recent AS (\n\t\t\tSELECT height, hash, parent_hash\n\t\t\tFROM blocks\n\t\t\tWHERE canonical = true\n\t\t\t\t${windowClause}\n\t\t)\n\t\tSELECT b.height, b.hash, p.hash AS expected_parent, b.parent_hash AS stored_parent\n\t\tFROM recent b\n\t\tJOIN recent p ON p.height = b.height - 1\n\t\tWHERE b.parent_hash <> p.hash\n\t\tORDER BY b.height\n\t\t${limitClause}\n\t`.execute(db);\n\treturn rows.map((r) => ({\n\t\theight: Number(r.height),\n\t\thash: r.hash,\n\t\texpectedParent: r.expected_parent,\n\t\tstoredParent: r.stored_parent,\n\t}));\n}\n"
5
+ "import { type Kysely, sql } from \"kysely\";\nimport type { Database } from \"../types.ts\";\n\nexport interface Gap {\n\tgapStart: number;\n\tgapEnd: number;\n\tsize: number;\n}\n\nexport async function findGaps(\n\tdb: Kysely<Database>,\n\tlimit?: number,\n): Promise<Gap[]> {\n\tconst limitClause = limit ? sql`LIMIT ${limit}` : sql``;\n\tconst { rows } = await sql<{\n\t\tgap_start: string;\n\t\tgap_end: string;\n\t\tsize: string;\n\t}>`\n SELECT gap_start, gap_end, gap_end - gap_start + 1 AS size\n FROM (\n SELECT height + 1 AS gap_start, next_height - 1 AS gap_end\n FROM (\n SELECT height, LEAD(height) OVER (ORDER BY height) AS next_height\n FROM blocks WHERE canonical = true\n ) sub\n WHERE next_height - height > 1\n ) gaps\n ORDER BY gap_start\n ${limitClause}\n `.execute(db);\n\n\treturn rows.map((r) => ({\n\t\tgapStart: Number(r.gap_start),\n\t\tgapEnd: Number(r.gap_end),\n\t\tsize: Number(r.size),\n\t}));\n}\n\nexport async function countMissingBlocks(\n\tdb: Kysely<Database>,\n): Promise<number> {\n\tconst { rows } = await sql<{ total: string }>`\n SELECT COALESCE(SUM(next_height - height - 1), 0) AS total\n FROM (\n SELECT height, LEAD(height) OVER (ORDER BY height) AS next_height\n FROM blocks WHERE canonical = true\n ) sub\n WHERE next_height - height > 1\n `.execute(db);\n\n\treturn Number(rows[0]?.total ?? 0);\n}\n\nexport interface ChainDataIntegrity {\n\tok: boolean;\n\tmaxHeight: number;\n\tsampleHeight: number | null;\n\tsampleBlocks: number;\n\treason: string | null;\n}\n\n/**\n * Cheap sanity check that the `blocks` table actually holds the history its tip\n * implies. Catches a wrong/empty Postgres volume — e.g. a container recreated\n * against a fresh volume — being silently served: if the tip is high but a\n * window of blocks well below it is missing, the data isn't what the tip claims.\n *\n * Below `checkFloor` we can't distinguish a fresh install from an empty volume,\n * so we report ok — a genuinely new DB legitimately has little history. Two\n * indexed lookups, safe to run on every startup / health poll.\n */\nexport async function checkChainDataIntegrity(\n\tdb: Kysely<Database>,\n\topts?: { checkFloor?: number; lookback?: number },\n): Promise<ChainDataIntegrity> {\n\tconst checkFloor = opts?.checkFloor ?? 1_000_000;\n\tconst lookback = opts?.lookback ?? 500_000;\n\n\tconst { rows: tipRows } = await sql<{ max: string | null }>`\n\t\tSELECT MAX(height) AS max FROM blocks WHERE canonical = true\n\t`.execute(db);\n\tconst maxHeight = Number(tipRows[0]?.max ?? 0);\n\n\tif (maxHeight < checkFloor) {\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmaxHeight,\n\t\t\tsampleHeight: null,\n\t\t\tsampleBlocks: 0,\n\t\t\treason: null,\n\t\t};\n\t}\n\n\tconst sampleHeight = maxHeight - lookback;\n\tconst { rows: sampleRows } = await sql<{ n: string }>`\n\t\tSELECT count(*) AS n FROM blocks\n\t\tWHERE canonical = true AND height >= ${sampleHeight} AND height < ${sampleHeight + 1000}\n\t`.execute(db);\n\tconst sampleBlocks = Number(sampleRows[0]?.n ?? 0);\n\n\treturn {\n\t\tok: sampleBlocks > 0,\n\t\tmaxHeight,\n\t\tsampleHeight,\n\t\tsampleBlocks,\n\t\treason:\n\t\t\tsampleBlocks > 0\n\t\t\t\t? null\n\t\t\t\t: `tip is ${maxHeight} but no canonical blocks near ${sampleHeight} — wrong or empty volume?`,\n\t};\n}\n\nexport async function computeContiguousTip(\n\tdb: Kysely<Database>,\n\tfromHeight: number,\n): Promise<number> {\n\tconst { rows } = await sql<{ tip: string }>`\n SELECT COALESCE(MAX(height), ${fromHeight}) AS tip\n FROM (\n SELECT height, height - ROW_NUMBER() OVER (ORDER BY height) AS grp\n FROM blocks WHERE canonical = true AND height >= ${fromHeight}\n ) sub\n WHERE grp = (\n SELECT height - ROW_NUMBER() OVER (ORDER BY height)\n FROM blocks WHERE canonical = true AND height = ${fromHeight}\n )\n `.execute(db);\n\n\treturn Number(rows[0]?.tip ?? fromHeight);\n}\n\nexport type ShortBlock = {\n\theight: number;\n\texpectedTxCount: number;\n\tactualTxCount: number;\n};\n\n/**\n * Canonical heights where `transactions` holds fewer (or more) rows than the\n * block was supposed to. `blocks.tx_count` is set by `persistBlock` from the\n * incoming block, so a mismatch means a later write silently moved or dropped\n * a tx after that block was persisted — a reorg replaying the wrong height, a\n * same-height tx_id collision, or a bug in the persist path itself. Ingest\n * used to report the *parsed* count, not the inserted count, so this kind of\n * short block was invisible; a skipped insert looked identical to success.\n *\n * Scoped to heights with `tx_count IS NOT NULL` — rows ingested before this\n * column existed have no baseline to check against. Also scoped to a recent\n * window by default (like `findBrokenLinks`): every new block sets\n * `tx_count`, so the eligible range only grows, and a short block is an\n * active-repair concern, not something worth a full-table scan every cycle.\n */\nexport async function findShortBlocks(\n\tdb: Kysely<Database>,\n\t/** Heights below the tip to scan; `null` scans the whole chain. */\n\topts: { window?: number | null; limit?: number } = {},\n): Promise<ShortBlock[]> {\n\tconst windowSize = opts.window === undefined ? 10_000 : opts.window;\n\tconst limitClause = opts.limit ? sql`LIMIT ${opts.limit}` : sql``;\n\tconst windowClause =\n\t\twindowSize === null\n\t\t\t? sql``\n\t\t\t: sql`AND b.height > (SELECT MAX(height) - ${windowSize} FROM blocks WHERE canonical = true)`;\n\tconst { rows } = await sql<{\n\t\theight: string | number;\n\t\ttx_count: number;\n\t\tactual_count: string | number;\n\t}>`\n\t\tSELECT b.height, b.tx_count, COUNT(t.tx_id) AS actual_count\n\t\t FROM blocks b\n\t\t LEFT JOIN transactions t ON t.block_height = b.height\n\t\t WHERE b.canonical = true\n\t\t AND b.tx_count IS NOT NULL\n\t\t ${windowClause}\n\t\t GROUP BY b.height, b.tx_count\n\t\tHAVING COUNT(t.tx_id) <> b.tx_count\n\t\t ORDER BY b.height\n\t\t ${limitClause}\n\t`.execute(db);\n\n\treturn rows.map((r) => ({\n\t\theight: Number(r.height),\n\t\texpectedTxCount: Number(r.tx_count),\n\t\tactualTxCount: Number(r.actual_count),\n\t}));\n}\n\nexport type BrokenLink = {\n\theight: number;\n\thash: string;\n\texpectedParent: string;\n\tstoredParent: string;\n};\n\n/**\n * Canonical heights whose block does not descend from the canonical block\n * below it.\n *\n * `findGaps` asks \"is every height present?\", which a chain sitting on a losing\n * fork answers yes to. On 2026-07-30 a reorg at 8,663,166 was resolved the\n * wrong way: we adopted a block that mainnet orphaned, its true successors were\n * swept non-canonical, and later blocks were inserted canonical on top. Every\n * height existed. Nothing was \"missing\". The chain simply did not join up, and\n * no check asked that question — so we served a broken chain for seventeen\n * hours and only noticed because subgraphs stopped moving.\n *\n * Scoped to a recent window by default: an unlinked height is an active-repair\n * concern, and scanning eight million rows every cycle is not.\n */\nexport async function findBrokenLinks(\n\tdb: Kysely<Database>,\n\t/** Heights below the tip to scan; `null` scans the whole chain. */\n\topts: { window?: number | null; limit?: number } = {},\n): Promise<BrokenLink[]> {\n\tconst windowSize = opts.window === undefined ? 10_000 : opts.window;\n\tconst limitClause = opts.limit ? sql`LIMIT ${opts.limit}` : sql``;\n\tconst windowClause =\n\t\twindowSize === null\n\t\t\t? sql``\n\t\t\t: sql`AND height > (SELECT MAX(height) - ${windowSize} FROM blocks WHERE canonical = true)`;\n\tconst { rows } = await sql<{\n\t\theight: string | number;\n\t\thash: string;\n\t\texpected_parent: string;\n\t\tstored_parent: string;\n\t}>`\n\t\tWITH recent AS (\n\t\t\tSELECT height, hash, parent_hash\n\t\t\tFROM blocks\n\t\t\tWHERE canonical = true\n\t\t\t\t${windowClause}\n\t\t)\n\t\tSELECT b.height, b.hash, p.hash AS expected_parent, b.parent_hash AS stored_parent\n\t\tFROM recent b\n\t\tJOIN recent p ON p.height = b.height - 1\n\t\tWHERE b.parent_hash <> p.hash\n\t\tORDER BY b.height\n\t\t${limitClause}\n\t`.execute(db);\n\treturn rows.map((r) => ({\n\t\theight: Number(r.height),\n\t\thash: r.hash,\n\t\texpectedParent: r.expected_parent,\n\t\tstoredParent: r.stored_parent,\n\t}));\n}\n"
6
6
  ],
7
- "mappings": ";AAAA;AASA,eAAsB,QAAQ,CAC7B,IACA,OACiB;AAAA,EACjB,MAAM,cAAc,QAAQ,YAAY,UAAU;AAAA,EAClD,QAAQ,SAAS,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAelB;AAAA,IACF,QAAQ,EAAE;AAAA,EAEb,OAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACvB,UAAU,OAAO,EAAE,SAAS;AAAA,IAC5B,QAAQ,OAAO,EAAE,OAAO;AAAA,IACxB,MAAM,OAAO,EAAE,IAAI;AAAA,EACpB,EAAE;AAAA;AAGH,eAAsB,kBAAkB,CACvC,IACkB;AAAA,EAClB,QAAQ,SAAS,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOpB,QAAQ,EAAE;AAAA,EAEb,OAAO,OAAO,KAAK,IAAI,SAAS,CAAC;AAAA;AAqBlC,eAAsB,uBAAuB,CAC5C,IACA,MAC8B;AAAA,EAC9B,MAAM,aAAa,MAAM,cAAc;AAAA,EACvC,MAAM,WAAW,MAAM,YAAY;AAAA,EAEnC,QAAQ,MAAM,YAAY,MAAM;AAAA;AAAA,GAE9B,QAAQ,EAAE;AAAA,EACZ,MAAM,YAAY,OAAO,QAAQ,IAAI,OAAO,CAAC;AAAA,EAE7C,IAAI,YAAY,YAAY;AAAA,IAC3B,OAAO;AAAA,MACN,IAAI;AAAA,MACJ;AAAA,MACA,cAAc;AAAA,MACd,cAAc;AAAA,MACd,QAAQ;AAAA,IACT;AAAA,EACD;AAAA,EAEA,MAAM,eAAe,YAAY;AAAA,EACjC,QAAQ,MAAM,eAAe,MAAM;AAAA;AAAA,yCAEK,6BAA6B,eAAe;AAAA,GAClF,QAAQ,EAAE;AAAA,EACZ,MAAM,eAAe,OAAO,WAAW,IAAI,KAAK,CAAC;AAAA,EAEjD,OAAO;AAAA,IACN,IAAI,eAAe;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA,QACC,eAAe,IACZ,OACA,UAAU,0CAA0C;AAAA,EACzD;AAAA;AAGD,eAAsB,oBAAoB,CACzC,IACA,YACkB;AAAA,EAClB,QAAQ,SAAS,MAAM;AAAA,mCACW;AAAA;AAAA;AAAA,yDAGsB;AAAA;AAAA;AAAA;AAAA,wDAID;AAAA;AAAA,IAEpD,QAAQ,EAAE;AAAA,EAEb,OAAO,OAAO,KAAK,IAAI,OAAO,UAAU;AAAA;AAyBzC,eAAsB,eAAe,CACpC,IAEA,OAAmD,CAAC,GAC5B;AAAA,EACxB,MAAM,aAAa,KAAK,WAAW,YAAY,MAAS,KAAK;AAAA,EAC7D,MAAM,cAAc,KAAK,QAAQ,YAAY,KAAK,UAAU;AAAA,EAC5D,MAAM,eACL,eAAe,OACZ,QACA,yCAAyC;AAAA,EAC7C,QAAQ,SAAS,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,MAUlB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOF;AAAA,GACD,QAAQ,EAAE;AAAA,EACZ,OAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACvB,QAAQ,OAAO,EAAE,MAAM;AAAA,IACvB,MAAM,EAAE;AAAA,IACR,gBAAgB,EAAE;AAAA,IAClB,cAAc,EAAE;AAAA,EACjB,EAAE;AAAA;",
8
- "debugId": "1E6E1376EB78163164756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "E92283960DE6C55264756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -29,6 +29,9 @@ interface BlocksTable {
29
29
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
30
30
  timestamp: number;
31
31
  canonical: Generated<boolean>;
32
+ /** Tx count `persistBlock` was handed for this height. Null on rows
33
+ * ingested before this column existed. */
34
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
32
35
  created_at: Generated<Date>;
33
36
  }
34
37
  interface TransactionsTable {
@@ -29,6 +29,9 @@ interface BlocksTable {
29
29
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
30
30
  timestamp: number;
31
31
  canonical: Generated<boolean>;
32
+ /** Tx count `persistBlock` was handed for this height. Null on rows
33
+ * ingested before this column existed. */
34
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
32
35
  created_at: Generated<Date>;
33
36
  }
34
37
  interface TransactionsTable {
@@ -29,6 +29,9 @@ interface BlocksTable {
29
29
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
30
30
  timestamp: number;
31
31
  canonical: Generated<boolean>;
32
+ /** Tx count `persistBlock` was handed for this height. Null on rows
33
+ * ingested before this column existed. */
34
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
32
35
  created_at: Generated<Date>;
33
36
  }
34
37
  interface TransactionsTable {
@@ -39,6 +39,9 @@ interface BlocksTable {
39
39
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
40
40
  timestamp: number;
41
41
  canonical: Generated<boolean>;
42
+ /** Tx count `persistBlock` was handed for this height. Null on rows
43
+ * ingested before this column existed. */
44
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
42
45
  created_at: Generated<Date>;
43
46
  }
44
47
  interface TransactionsTable {
@@ -29,6 +29,9 @@ interface BlocksTable {
29
29
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
30
30
  timestamp: number;
31
31
  canonical: Generated<boolean>;
32
+ /** Tx count `persistBlock` was handed for this height. Null on rows
33
+ * ingested before this column existed. */
34
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
32
35
  created_at: Generated<Date>;
33
36
  }
34
37
  interface TransactionsTable {
@@ -28,6 +28,9 @@ interface BlocksTable {
28
28
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
29
29
  timestamp: number;
30
30
  canonical: Generated<boolean>;
31
+ /** Tx count `persistBlock` was handed for this height. Null on rows
32
+ * ingested before this column existed. */
33
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
31
34
  created_at: Generated<Date>;
32
35
  }
33
36
  interface TransactionsTable {
@@ -28,6 +28,9 @@ interface BlocksTable {
28
28
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
29
29
  timestamp: number;
30
30
  canonical: Generated<boolean>;
31
+ /** Tx count `persistBlock` was handed for this height. Null on rows
32
+ * ingested before this column existed. */
33
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
31
34
  created_at: Generated<Date>;
32
35
  }
33
36
  interface TransactionsTable {
@@ -29,6 +29,9 @@ interface BlocksTable {
29
29
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
30
30
  timestamp: number;
31
31
  canonical: Generated<boolean>;
32
+ /** Tx count `persistBlock` was handed for this height. Null on rows
33
+ * ingested before this column existed. */
34
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
32
35
  created_at: Generated<Date>;
33
36
  }
34
37
  interface TransactionsTable {
@@ -9,6 +9,9 @@ interface BlocksTable {
9
9
  index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
10
10
  timestamp: number;
11
11
  canonical: Generated<boolean>;
12
+ /** Tx count `persistBlock` was handed for this height. Null on rows
13
+ * ingested before this column existed. */
14
+ tx_count: ColumnType<number | null, number | null | undefined, number | null>;
12
15
  created_at: Generated<Date>;
13
16
  }
14
17
  interface TransactionsTable {
@@ -0,0 +1,24 @@
1
+ import { type Kysely, sql } from "kysely";
2
+ import { onChainPlane } from "../src/db/migration-role.ts";
3
+
4
+ // The tx count `persistBlock` was actually handed for a height, set at write
5
+ // time from the incoming block. Nullable: historical rows backfill lazily /
6
+ // on demand. Lets a completeness check compare `count(transactions at h)`
7
+ // against what the block was supposed to hold, catching a persist that
8
+ // silently landed fewer rows than it received — see the ingest transaction
9
+ // completeness plan. `blocks` is a chain-plane table, so the DDL no-ops on
10
+ // the control DB under the source/target split.
11
+ export async function up(db: Kysely<unknown>): Promise<void> {
12
+ await onChainPlane(async () => {
13
+ await sql`SET lock_timeout = '30s'`.execute(db);
14
+ await sql`ALTER TABLE blocks ADD COLUMN IF NOT EXISTS tx_count INT`.execute(
15
+ db,
16
+ );
17
+ });
18
+ }
19
+
20
+ export async function down(db: Kysely<unknown>): Promise<void> {
21
+ await onChainPlane(async () => {
22
+ await sql`ALTER TABLE blocks DROP COLUMN IF EXISTS tx_count`.execute(db);
23
+ });
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secondlayer/shared",
3
- "version": "11.14.1",
3
+ "version": "11.14.2",
4
4
  "description": "Internal building blocks shared across Secondlayer packages.",
5
5
  "license": "MIT",
6
6
  "repository": {