@secondlayer/shared 7.3.1 → 7.5.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/db/index.d.ts +18 -1
- package/dist/src/db/index.js +2 -1
- package/dist/src/db/index.js.map +3 -3
- package/dist/src/db/queries/chain-reorgs.d.ts +16 -0
- package/dist/src/db/queries/chain-reorgs.js +2 -1
- package/dist/src/db/queries/chain-reorgs.js.map +3 -3
- package/dist/src/db/queries/contracts.d.ts +16 -0
- package/dist/src/db/queries/integrity.d.ts +42 -1
- package/dist/src/db/queries/integrity.js +26 -1
- package/dist/src/db/queries/integrity.js.map +3 -3
- package/dist/src/db/queries/subgraph-gaps.d.ts +16 -0
- package/dist/src/db/queries/subgraph-operations.d.ts +16 -0
- package/dist/src/db/queries/subgraphs.d.ts +78 -13
- package/dist/src/db/queries/subgraphs.js +20 -1
- package/dist/src/db/queries/subgraphs.js.map +4 -4
- package/dist/src/db/queries/subscriptions.d.ts +16 -0
- package/dist/src/db/schema.d.ts +17 -1
- package/dist/src/index.d.ts +18 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +3 -3
- package/dist/src/node/local-client.d.ts +16 -0
- package/migrations/0112_pending_fork_blocks.ts +52 -0
- package/package.json +2 -5
|
@@ -682,6 +682,21 @@ interface ChainReadCacheTable {
|
|
|
682
682
|
result_hex: string;
|
|
683
683
|
created_at: Generated<Date>;
|
|
684
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* A block that arrived for a height we already hold, staged until a later
|
|
687
|
+
* block's `parent_hash` says which of the two the chain kept. See migration
|
|
688
|
+
* 0112 — adopting on first sight picked the losing fork twice.
|
|
689
|
+
*/
|
|
690
|
+
interface PendingForkBlocksTable {
|
|
691
|
+
height: number;
|
|
692
|
+
block_hash: string;
|
|
693
|
+
parent_hash: string;
|
|
694
|
+
/** The canonical hash we held at this height when the contender arrived. */
|
|
695
|
+
incumbent_hash: string;
|
|
696
|
+
/** The raw observer payload, replayed verbatim if this fork wins. */
|
|
697
|
+
payload: ColumnType<unknown, unknown, unknown>;
|
|
698
|
+
received_at: Generated<Date>;
|
|
699
|
+
}
|
|
685
700
|
interface Database {
|
|
686
701
|
blocks: BlocksTable;
|
|
687
702
|
transactions: TransactionsTable;
|
|
@@ -744,6 +759,7 @@ interface Database {
|
|
|
744
759
|
x402_payments: X402PaymentsTable;
|
|
745
760
|
x402_balances: X402BalancesTable;
|
|
746
761
|
chain_read_cache: ChainReadCacheTable;
|
|
762
|
+
pending_fork_blocks: PendingForkBlocksTable;
|
|
747
763
|
}
|
|
748
764
|
/** Prepaid x402 credit — one running USD-micros balance per payer principal. */
|
|
749
765
|
interface X402BalancesTable {
|
|
@@ -986,4 +1002,29 @@ declare function checkChainDataIntegrity(db: Kysely<Database>, opts?: {
|
|
|
986
1002
|
lookback?: number
|
|
987
1003
|
}): Promise<ChainDataIntegrity>;
|
|
988
1004
|
declare function computeContiguousTip(db: Kysely<Database>, fromHeight: number): Promise<number>;
|
|
989
|
-
|
|
1005
|
+
type BrokenLink = {
|
|
1006
|
+
height: number
|
|
1007
|
+
hash: string
|
|
1008
|
+
expectedParent: string
|
|
1009
|
+
storedParent: string
|
|
1010
|
+
};
|
|
1011
|
+
/**
|
|
1012
|
+
* Canonical heights whose block does not descend from the canonical block
|
|
1013
|
+
* below it.
|
|
1014
|
+
*
|
|
1015
|
+
* `findGaps` asks "is every height present?", which a chain sitting on a losing
|
|
1016
|
+
* fork answers yes to. On 2026-07-30 a reorg at 8,663,166 was resolved the
|
|
1017
|
+
* wrong way: we adopted a block that mainnet orphaned, its true successors were
|
|
1018
|
+
* swept non-canonical, and later blocks were inserted canonical on top. Every
|
|
1019
|
+
* height existed. Nothing was "missing". The chain simply did not join up, and
|
|
1020
|
+
* no check asked that question — so we served a broken chain for seventeen
|
|
1021
|
+
* hours and only noticed because subgraphs stopped moving.
|
|
1022
|
+
*
|
|
1023
|
+
* Scoped to a recent window by default: an unlinked height is an active-repair
|
|
1024
|
+
* concern, and scanning eight million rows every cycle is not.
|
|
1025
|
+
*/
|
|
1026
|
+
declare function findBrokenLinks(db: Kysely<Database>, opts?: {
|
|
1027
|
+
window?: number
|
|
1028
|
+
limit?: number
|
|
1029
|
+
}): Promise<BrokenLink[]>;
|
|
1030
|
+
export { findGaps, findBrokenLinks, countMissingBlocks, computeContiguousTip, checkChainDataIntegrity, Gap, ChainDataIntegrity, BrokenLink };
|
|
@@ -92,12 +92,37 @@ async function computeContiguousTip(db, fromHeight) {
|
|
|
92
92
|
`.execute(db);
|
|
93
93
|
return Number(rows[0]?.tip ?? fromHeight);
|
|
94
94
|
}
|
|
95
|
+
async function findBrokenLinks(db, opts = {}) {
|
|
96
|
+
const windowSize = opts.window ?? 1e4;
|
|
97
|
+
const limitClause = opts.limit ? sql`LIMIT ${opts.limit}` : sql``;
|
|
98
|
+
const { rows } = await sql`
|
|
99
|
+
WITH recent AS (
|
|
100
|
+
SELECT height, hash, parent_hash
|
|
101
|
+
FROM blocks
|
|
102
|
+
WHERE canonical = true
|
|
103
|
+
AND height > (SELECT MAX(height) - ${windowSize} FROM blocks WHERE canonical = true)
|
|
104
|
+
)
|
|
105
|
+
SELECT b.height, b.hash, p.hash AS expected_parent, b.parent_hash AS stored_parent
|
|
106
|
+
FROM recent b
|
|
107
|
+
JOIN recent p ON p.height = b.height - 1
|
|
108
|
+
WHERE b.parent_hash <> p.hash
|
|
109
|
+
ORDER BY b.height
|
|
110
|
+
${limitClause}
|
|
111
|
+
`.execute(db);
|
|
112
|
+
return rows.map((r) => ({
|
|
113
|
+
height: Number(r.height),
|
|
114
|
+
hash: r.hash,
|
|
115
|
+
expectedParent: r.expected_parent,
|
|
116
|
+
storedParent: r.stored_parent
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
95
119
|
export {
|
|
96
120
|
findGaps,
|
|
121
|
+
findBrokenLinks,
|
|
97
122
|
countMissingBlocks,
|
|
98
123
|
computeContiguousTip,
|
|
99
124
|
checkChainDataIntegrity
|
|
100
125
|
};
|
|
101
126
|
|
|
102
|
-
//# debugId=
|
|
127
|
+
//# debugId=FFA2BEF866239EA364756E2164756E21
|
|
103
128
|
//# 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"
|
|
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\topts: { window?: number; limit?: number } = {},\n): Promise<BrokenLink[]> {\n\tconst windowSize = opts.window ?? 10_000;\n\tconst limitClause = opts.limit ? sql`LIMIT ${opts.limit}` : sql``;\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\tAND height > (SELECT MAX(height) - ${windowSize} FROM blocks WHERE canonical = true)\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": ";;;;;;;;;;;;;;;;;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,IACA,OAA4C,CAAC,GACrB;AAAA,EACxB,MAAM,aAAa,KAAK,UAAU;AAAA,EAClC,MAAM,cAAc,KAAK,QAAQ,YAAY,KAAK,UAAU;AAAA,EAC5D,QAAQ,SAAS,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,yCAUiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOrC;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": "FFA2BEF866239EA364756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -682,6 +682,21 @@ interface ChainReadCacheTable {
|
|
|
682
682
|
result_hex: string;
|
|
683
683
|
created_at: Generated<Date>;
|
|
684
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* A block that arrived for a height we already hold, staged until a later
|
|
687
|
+
* block's `parent_hash` says which of the two the chain kept. See migration
|
|
688
|
+
* 0112 — adopting on first sight picked the losing fork twice.
|
|
689
|
+
*/
|
|
690
|
+
interface PendingForkBlocksTable {
|
|
691
|
+
height: number;
|
|
692
|
+
block_hash: string;
|
|
693
|
+
parent_hash: string;
|
|
694
|
+
/** The canonical hash we held at this height when the contender arrived. */
|
|
695
|
+
incumbent_hash: string;
|
|
696
|
+
/** The raw observer payload, replayed verbatim if this fork wins. */
|
|
697
|
+
payload: ColumnType<unknown, unknown, unknown>;
|
|
698
|
+
received_at: Generated<Date>;
|
|
699
|
+
}
|
|
685
700
|
interface Database {
|
|
686
701
|
blocks: BlocksTable;
|
|
687
702
|
transactions: TransactionsTable;
|
|
@@ -744,6 +759,7 @@ interface Database {
|
|
|
744
759
|
x402_payments: X402PaymentsTable;
|
|
745
760
|
x402_balances: X402BalancesTable;
|
|
746
761
|
chain_read_cache: ChainReadCacheTable;
|
|
762
|
+
pending_fork_blocks: PendingForkBlocksTable;
|
|
747
763
|
}
|
|
748
764
|
/** Prepaid x402 credit — one running USD-micros balance per payer principal. */
|
|
749
765
|
interface X402BalancesTable {
|
|
@@ -682,6 +682,21 @@ interface ChainReadCacheTable {
|
|
|
682
682
|
result_hex: string;
|
|
683
683
|
created_at: Generated<Date>;
|
|
684
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* A block that arrived for a height we already hold, staged until a later
|
|
687
|
+
* block's `parent_hash` says which of the two the chain kept. See migration
|
|
688
|
+
* 0112 — adopting on first sight picked the losing fork twice.
|
|
689
|
+
*/
|
|
690
|
+
interface PendingForkBlocksTable {
|
|
691
|
+
height: number;
|
|
692
|
+
block_hash: string;
|
|
693
|
+
parent_hash: string;
|
|
694
|
+
/** The canonical hash we held at this height when the contender arrived. */
|
|
695
|
+
incumbent_hash: string;
|
|
696
|
+
/** The raw observer payload, replayed verbatim if this fork wins. */
|
|
697
|
+
payload: ColumnType<unknown, unknown, unknown>;
|
|
698
|
+
received_at: Generated<Date>;
|
|
699
|
+
}
|
|
685
700
|
interface Database {
|
|
686
701
|
blocks: BlocksTable;
|
|
687
702
|
transactions: TransactionsTable;
|
|
@@ -744,6 +759,7 @@ interface Database {
|
|
|
744
759
|
x402_payments: X402PaymentsTable;
|
|
745
760
|
x402_balances: X402BalancesTable;
|
|
746
761
|
chain_read_cache: ChainReadCacheTable;
|
|
762
|
+
pending_fork_blocks: PendingForkBlocksTable;
|
|
747
763
|
}
|
|
748
764
|
/** Prepaid x402 credit — one running USD-micros balance per payer principal. */
|
|
749
765
|
interface X402BalancesTable {
|
|
@@ -683,6 +683,21 @@ interface ChainReadCacheTable {
|
|
|
683
683
|
result_hex: string;
|
|
684
684
|
created_at: Generated<Date>;
|
|
685
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* A block that arrived for a height we already hold, staged until a later
|
|
688
|
+
* block's `parent_hash` says which of the two the chain kept. See migration
|
|
689
|
+
* 0112 — adopting on first sight picked the losing fork twice.
|
|
690
|
+
*/
|
|
691
|
+
interface PendingForkBlocksTable {
|
|
692
|
+
height: number;
|
|
693
|
+
block_hash: string;
|
|
694
|
+
parent_hash: string;
|
|
695
|
+
/** The canonical hash we held at this height when the contender arrived. */
|
|
696
|
+
incumbent_hash: string;
|
|
697
|
+
/** The raw observer payload, replayed verbatim if this fork wins. */
|
|
698
|
+
payload: ColumnType<unknown, unknown, unknown>;
|
|
699
|
+
received_at: Generated<Date>;
|
|
700
|
+
}
|
|
686
701
|
interface Database {
|
|
687
702
|
blocks: BlocksTable;
|
|
688
703
|
transactions: TransactionsTable;
|
|
@@ -745,6 +760,7 @@ interface Database {
|
|
|
745
760
|
x402_payments: X402PaymentsTable;
|
|
746
761
|
x402_balances: X402BalancesTable;
|
|
747
762
|
chain_read_cache: ChainReadCacheTable;
|
|
763
|
+
pending_fork_blocks: PendingForkBlocksTable;
|
|
748
764
|
}
|
|
749
765
|
/** Prepaid x402 credit — one running USD-micros balance per payer principal. */
|
|
750
766
|
interface X402BalancesTable {
|
|
@@ -1015,6 +1031,22 @@ declare function listSubgraphs(db: Kysely<Database>, accountId?: string): Promis
|
|
|
1015
1031
|
declare function findPublicSubgraphByName(db: Kysely<Database>, name: string): Promise<Subgraph | null>;
|
|
1016
1032
|
declare function updateSubgraphVisibility(db: Kysely<Database>, name: string, accountId: string, visibility: "public" | "private"): Promise<void>;
|
|
1017
1033
|
declare function updateSubgraphStatus(db: Kysely<Database>, name: string, status: string, lastProcessedBlock?: number): Promise<void>;
|
|
1034
|
+
/** Live-walk ERROR write (f069): a block whose handlers ALL failed
|
|
1035
|
+
* (`processed === 0 && errors > 0`, `block-processor.ts`'s `applyProgress`)
|
|
1036
|
+
* is stamped 'error' and its height is recorded as processed, same as
|
|
1037
|
+
* `updateSubgraphStatus(db, name, "error", blockHeight)` — except the
|
|
1038
|
+
* cursor half of that write now goes through the same conditional `<`
|
|
1039
|
+
* guard as {@link recordLiveProgress}. Marking a subgraph 'error' is not
|
|
1040
|
+
* itself replay-sensitive, but it shares the `last_processed_block` column
|
|
1041
|
+
* with the replay guard: an unconditional write here could regress the
|
|
1042
|
+
* cursor a racing (successful) writer already advanced past, which is
|
|
1043
|
+
* exactly the regress-then-re-walk loop this guard exists to close. In
|
|
1044
|
+
* practice `ctx.rollbackTo` (`runner.ts`) means an all-failed block never
|
|
1045
|
+
* flushes writes, so a lost race here is always a silent no-op, never a
|
|
1046
|
+
* case that needs `CursorRaceLostError`-style tx abort. Returns whether
|
|
1047
|
+
* the write applied. Live path only — reindex/backfill's error stamping
|
|
1048
|
+
* goes through {@link updateSubgraphStatus} unconditionally, as before. */
|
|
1049
|
+
declare function recordLiveError(db: Kysely<Database>, name: string, blockHeight: number): Promise<boolean>;
|
|
1018
1050
|
declare function recordSubgraphProcessed(db: Kysely<Database>, name: string, processed: number, errors: number, lastError?: string): Promise<void>;
|
|
1019
1051
|
declare function updateSubgraphHandlerPath(db: Kysely<Database>, name: string, handlerPath: string, opts?: {
|
|
1020
1052
|
handlerCode?: string
|
|
@@ -1029,16 +1061,49 @@ declare function updateSubgraphExpiry(db: Kysely<Database>, name: string, accoun
|
|
|
1029
1061
|
* status stamping let catch-up flap a parked subgraph back into its own path
|
|
1030
1062
|
* per block, fighting the queued reindex op.
|
|
1031
1063
|
*
|
|
1032
|
-
* `last_processed_block`
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
* in-process per-subgraph lock +
|
|
1039
|
-
* (`packages/subgraphs/src/runtime/catchup.ts`,
|
|
1040
|
-
* `
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1043
|
-
|
|
1044
|
-
|
|
1064
|
+
* `last_processed_block` is now written CONDITIONALLY — `WHERE
|
|
1065
|
+
* last_processed_block < lastProcessedBlock`, mirroring
|
|
1066
|
+
* `advanceOperationCursor`'s (`subgraph-operations.ts`) comparison shape —
|
|
1067
|
+
* and returns whether THIS call actually advanced it. This used to be
|
|
1068
|
+
* unconditional by design, on the premise that the only two writers able to
|
|
1069
|
+
* advance/rewind a given subgraph's cursor concurrently (the catch-up walk
|
|
1070
|
+
* and the reorg handler) serialize on an in-process per-subgraph lock +
|
|
1071
|
+
* reorg epoch guard (`packages/subgraphs/src/runtime/catchup.ts`,
|
|
1072
|
+
* `subgraphBlockLockHeld` / `reorgEpoch`; f057). That guard is in-process
|
|
1073
|
+
* only. The f068 investigation
|
|
1074
|
+
* (`docs/internal/audits/asset-holdings-unreproducible-balances-f068.md`)
|
|
1075
|
+
* found a cross-process sequence it does not cover: a catch-up leader that
|
|
1076
|
+
* loses its advisory-lock connection mid-walk keeps writing (leadership is
|
|
1077
|
+
* checked only at walk entry, not per-iteration) while a new leader takes
|
|
1078
|
+
* over and walks the same range — both commit increments for overlapping
|
|
1079
|
+
* heights, and every regressed cursor write from the laggard re-triggers a
|
|
1080
|
+
* full re-walk, growing per-height application without bound. The
|
|
1081
|
+
* conditional advance here (paired with the fast-path replay guard armed on
|
|
1082
|
+
* the live path in `block-processor.ts`) is now the cross-process
|
|
1083
|
+
* guarantee: a block at/below the current cursor can never re-advance it,
|
|
1084
|
+
* so a laggard's commits can no longer regress-then-retrigger.
|
|
1085
|
+
*
|
|
1086
|
+
* This conditionality is why the reorg rewind can no longer go through this
|
|
1087
|
+
* function — a rewind's whole point is to move the cursor *backward*, which
|
|
1088
|
+
* the `<` guard would silently refuse. Reorg mode calls
|
|
1089
|
+
* {@link rewindLiveProgress} instead, which keeps the old unconditional
|
|
1090
|
+
* semantics and is safe specifically because the reorg path takes a
|
|
1091
|
+
* transaction-scoped advisory lock (`reorg.ts`) other writers can't cross. */
|
|
1092
|
+
declare function recordLiveProgress(db: Kysely<Database>, name: string, lastProcessedBlock: number): Promise<boolean>;
|
|
1093
|
+
/** Reorg-rewind progress write: the OLD unconditional `recordLiveProgress`
|
|
1094
|
+
* semantics, verbatim — moves `last_processed_block` to the fork height
|
|
1095
|
+
* regardless of its current value, because that is by definition a
|
|
1096
|
+
* backward move the conditional `<` guard in `recordLiveProgress` would
|
|
1097
|
+
* refuse. Same status-promotion CASE (never unparks an explicit
|
|
1098
|
+
* 'reindexing').
|
|
1099
|
+
*
|
|
1100
|
+
* Callers: exclusively the subgraph-reorg path
|
|
1101
|
+
* (`packages/subgraphs/src/runtime/reorg.ts`, via `block-processor.ts`'s
|
|
1102
|
+
* `reorgRewind` mode), which is safe from the cross-process race this
|
|
1103
|
+
* function reintroduces because it holds a transaction-scoped Postgres
|
|
1104
|
+
* advisory lock (`pg_advisory_xact_lock`) for the whole
|
|
1105
|
+
* delete+reprocess+rewind sequence — no other writer can interleave a
|
|
1106
|
+
* forward write between the rewind and the fork block's reprocess. Do not
|
|
1107
|
+
* call this from the live catch-up path. */
|
|
1108
|
+
declare function rewindLiveProgress(db: Kysely<Database>, name: string, lastProcessedBlock: number): Promise<void>;
|
|
1109
|
+
export { updateSubgraphVisibility, updateSubgraphStatus, updateSubgraphHandlerPath, updateSubgraphExpiry, subgraphDatabaseUrl, rewindLiveProgress, resolveSubgraphRawClient, resolveSubgraphDb, registerSubgraph, recordSubgraphProcessed, recordLiveProgress, recordLiveError, pgSchemaNameFor, pgSchemaName, listSubgraphs, isByoSubgraph, getSubgraph, findPublicSubgraphByName, encryptDatabaseUrl, deleteSubgraph };
|
|
@@ -657,6 +657,7 @@ var TABLE_TO_DB = {
|
|
|
657
657
|
contracts: "source",
|
|
658
658
|
chain_reorgs: "source",
|
|
659
659
|
chain_read_cache: "source",
|
|
660
|
+
pending_fork_blocks: "source",
|
|
660
661
|
decoded_events: "source",
|
|
661
662
|
decoder_checkpoints: "source",
|
|
662
663
|
pox4_calls: "source",
|
|
@@ -1092,6 +1093,14 @@ async function updateSubgraphStatus(db, name, status, lastProcessedBlock) {
|
|
|
1092
1093
|
updated_at: new Date
|
|
1093
1094
|
}).where("name", "=", name).execute();
|
|
1094
1095
|
}
|
|
1096
|
+
async function recordLiveError(db, name, blockHeight) {
|
|
1097
|
+
const result = await db.updateTable("subgraphs").set({
|
|
1098
|
+
status: "error",
|
|
1099
|
+
last_processed_block: blockHeight,
|
|
1100
|
+
updated_at: new Date
|
|
1101
|
+
}).where("name", "=", name).where("last_processed_block", "<", blockHeight).executeTakeFirst();
|
|
1102
|
+
return Number(result.numUpdatedRows ?? 0n) > 0;
|
|
1103
|
+
}
|
|
1095
1104
|
async function recordSubgraphProcessed(db, name, processed, errors, lastError) {
|
|
1096
1105
|
await db.updateTable("subgraphs").set({
|
|
1097
1106
|
total_processed: sql3`total_processed + ${processed}`,
|
|
@@ -1129,6 +1138,14 @@ async function updateSubgraphExpiry(db, name, accountId, expiresAt) {
|
|
|
1129
1138
|
await db.updateTable("subgraphs").set({ expires_at: expiresAt }).where("name", "=", name).where("account_id", "=", accountId).execute();
|
|
1130
1139
|
}
|
|
1131
1140
|
async function recordLiveProgress(db, name, lastProcessedBlock) {
|
|
1141
|
+
const result = await db.updateTable("subgraphs").set({
|
|
1142
|
+
last_processed_block: lastProcessedBlock,
|
|
1143
|
+
status: sql3`CASE WHEN status = 'reindexing' THEN status ELSE 'active' END`,
|
|
1144
|
+
updated_at: new Date
|
|
1145
|
+
}).where("name", "=", name).where("last_processed_block", "<", lastProcessedBlock).executeTakeFirst();
|
|
1146
|
+
return Number(result.numUpdatedRows ?? 0n) > 0;
|
|
1147
|
+
}
|
|
1148
|
+
async function rewindLiveProgress(db, name, lastProcessedBlock) {
|
|
1132
1149
|
await db.updateTable("subgraphs").set({
|
|
1133
1150
|
last_processed_block: lastProcessedBlock,
|
|
1134
1151
|
status: sql3`CASE WHEN status = 'reindexing' THEN status ELSE 'active' END`,
|
|
@@ -1141,11 +1158,13 @@ export {
|
|
|
1141
1158
|
updateSubgraphHandlerPath,
|
|
1142
1159
|
updateSubgraphExpiry,
|
|
1143
1160
|
subgraphDatabaseUrl,
|
|
1161
|
+
rewindLiveProgress,
|
|
1144
1162
|
resolveSubgraphRawClient,
|
|
1145
1163
|
resolveSubgraphDb,
|
|
1146
1164
|
registerSubgraph,
|
|
1147
1165
|
recordSubgraphProcessed,
|
|
1148
1166
|
recordLiveProgress,
|
|
1167
|
+
recordLiveError,
|
|
1149
1168
|
pgSchemaNameFor,
|
|
1150
1169
|
pgSchemaName,
|
|
1151
1170
|
listSubgraphs,
|
|
@@ -1156,5 +1175,5 @@ export {
|
|
|
1156
1175
|
deleteSubgraph
|
|
1157
1176
|
};
|
|
1158
1177
|
|
|
1159
|
-
//# debugId=
|
|
1178
|
+
//# debugId=3C4CE031C9F5B5D364756E2164756E21
|
|
1160
1179
|
//# sourceMappingURL=subgraphs.js.map
|