@secondlayer/shared 11.14.0 → 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.
- package/dist/src/archive/range-digest.d.ts +3 -0
- package/dist/src/archive/semantic-digest-builder.d.ts +3 -0
- package/dist/src/coverage/index.d.ts +3 -0
- package/dist/src/db/index.d.ts +3 -0
- package/dist/src/db/queries/chain-reorgs.d.ts +3 -0
- package/dist/src/db/queries/contracts.d.ts +3 -0
- package/dist/src/db/queries/instance.d.ts +3 -0
- package/dist/src/db/queries/integrity.d.ts +28 -1
- package/dist/src/db/queries/integrity.js +25 -2
- package/dist/src/db/queries/integrity.js.map +3 -3
- package/dist/src/db/queries/subgraph-gaps.d.ts +3 -0
- package/dist/src/db/queries/subgraph-operations.d.ts +3 -0
- package/dist/src/db/queries/subgraphs.d.ts +3 -0
- package/dist/src/db/queries/sync-scope.d.ts +3 -0
- package/dist/src/db/queries/webhooks.d.ts +3 -0
- package/dist/src/db/schema.d.ts +3 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/node/local-client.d.ts +3 -0
- package/dist/src/types.d.ts +3 -0
- package/migrations/0142_pox5_events_signer_from_manager.ts +46 -0
- package/migrations/0143_blocks_tx_count.ts +24 -0
- package/package.json +1 -1
|
@@ -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 {
|
package/dist/src/db/index.d.ts
CHANGED
|
@@ -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=
|
|
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;
|
|
8
|
-
"debugId": "
|
|
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 {
|
package/dist/src/db/schema.d.ts
CHANGED
|
@@ -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 {
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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 {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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,46 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
import { onChainPlane } from "../src/db/migration-role.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Backfill: `claim-rewards`, `claim-staker-rewards-for-signer`,
|
|
6
|
+
* `grant-signer-key`, and `revoke-signer-grant` only ever printed
|
|
7
|
+
* `signer-manager` — never `signer` — even though the contract's own
|
|
8
|
+
* assertions make it the same principal (see the pox-5 decoder's
|
|
9
|
+
* `promoteTopicFields` comment). Rows decoded before that derivation shipped
|
|
10
|
+
* are missing `signer` on these four topics; fill it from `signer_manager` so
|
|
11
|
+
* `signer=` returns a pool's complete pox-5 activity for old rows too.
|
|
12
|
+
*
|
|
13
|
+
* Idempotent by its WHERE (only touches rows still missing `signer`), so it
|
|
14
|
+
* is safe to run more than once.
|
|
15
|
+
*/
|
|
16
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
17
|
+
await onChainPlane(async () => {
|
|
18
|
+
await sql`
|
|
19
|
+
UPDATE pox5_events
|
|
20
|
+
SET signer = signer_manager
|
|
21
|
+
WHERE signer IS NULL
|
|
22
|
+
AND signer_manager IS NOT NULL
|
|
23
|
+
AND topic IN (
|
|
24
|
+
'claim-rewards',
|
|
25
|
+
'claim-staker-rewards-for-signer',
|
|
26
|
+
'grant-signer-key',
|
|
27
|
+
'revoke-signer-grant'
|
|
28
|
+
)
|
|
29
|
+
`.execute(db);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
34
|
+
await onChainPlane(async () => {
|
|
35
|
+
await sql`
|
|
36
|
+
UPDATE pox5_events
|
|
37
|
+
SET signer = NULL
|
|
38
|
+
WHERE topic IN (
|
|
39
|
+
'claim-rewards',
|
|
40
|
+
'claim-staker-rewards-for-signer',
|
|
41
|
+
'grant-signer-key',
|
|
42
|
+
'revoke-signer-grant'
|
|
43
|
+
)
|
|
44
|
+
`.execute(db);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -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
|
+
}
|