@secondlayer/shared 6.22.0 → 6.24.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/crypto/ed25519.d.ts +22 -0
- package/dist/src/crypto/ed25519.js +80 -0
- package/dist/src/crypto/ed25519.js.map +10 -0
- package/dist/src/crypto/secondlayer-webhook.d.ts +40 -0
- package/dist/src/crypto/secondlayer-webhook.js +130 -0
- package/dist/src/crypto/secondlayer-webhook.js.map +11 -0
- package/dist/src/db/index.d.ts +2 -0
- package/dist/src/db/queries/chain-reorgs.d.ts +2 -0
- package/dist/src/db/queries/contracts.d.ts +2 -0
- package/dist/src/db/queries/integrity.d.ts +2 -0
- package/dist/src/db/queries/subgraph-gaps.d.ts +2 -0
- package/dist/src/db/queries/subgraph-operations.d.ts +2 -0
- package/dist/src/db/queries/subgraphs.d.ts +2 -0
- package/dist/src/db/queries/subscriptions.d.ts +2 -0
- package/dist/src/db/schema.d.ts +2 -0
- package/dist/src/index.d.ts +26 -24
- package/dist/src/index.js +54 -53
- package/dist/src/index.js.map +5 -5
- package/dist/src/leader.d.ts +53 -0
- package/dist/src/leader.js +257 -0
- package/dist/src/leader.js.map +12 -0
- package/dist/src/node/client.d.ts +52 -0
- package/dist/src/node/client.js +188 -1
- package/dist/src/node/client.js.map +5 -4
- package/dist/src/node/consensus.d.ts +38 -0
- package/dist/src/node/consensus.js +67 -0
- package/dist/src/node/consensus.js.map +10 -0
- package/dist/src/node/local-client.d.ts +2 -0
- package/dist/src/node/nakamoto.d.ts +90 -0
- package/dist/src/node/nakamoto.js +177 -0
- package/dist/src/node/nakamoto.js.map +10 -0
- package/dist/src/streams-bulk-manifest.d.ts +33 -0
- package/dist/src/streams-bulk-manifest.js +104 -0
- package/dist/src/streams-bulk-manifest.js.map +11 -0
- package/dist/src/types.d.ts +2 -0
- package/migrations/0089_blocks_index_block_hash.ts +25 -0
- package/package.json +25 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/env.ts
|
|
18
|
+
import { z } from "zod/v4";
|
|
19
|
+
var networksSchema = z.string().transform((val) => {
|
|
20
|
+
const networks = val.split(",").map((n) => n.trim()).filter(Boolean);
|
|
21
|
+
const valid = ["mainnet", "testnet"];
|
|
22
|
+
for (const n of networks) {
|
|
23
|
+
if (!valid.includes(n)) {
|
|
24
|
+
throw new Error(`Invalid network: ${n}. Must be one of: ${valid.join(", ")}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return networks;
|
|
28
|
+
});
|
|
29
|
+
var envSchema = z.object({
|
|
30
|
+
DATABASE_URL: z.preprocess((val) => typeof val === "string" && val.length === 0 ? undefined : val, z.string().url().optional()),
|
|
31
|
+
SOURCE_DATABASE_URL: z.preprocess((val) => typeof val === "string" && val.length === 0 ? undefined : val, z.string().url().optional()),
|
|
32
|
+
TARGET_DATABASE_URL: z.preprocess((val) => typeof val === "string" && val.length === 0 ? undefined : val, z.string().url().optional()),
|
|
33
|
+
NETWORK: z.enum(["mainnet", "testnet"]).optional(),
|
|
34
|
+
NETWORKS: networksSchema.optional(),
|
|
35
|
+
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
36
|
+
NODE_ENV: z.enum(["development", "production", "test"]).default("development")
|
|
37
|
+
});
|
|
38
|
+
var cachedEnv = null;
|
|
39
|
+
function getEnv() {
|
|
40
|
+
if (cachedEnv) {
|
|
41
|
+
return cachedEnv;
|
|
42
|
+
}
|
|
43
|
+
const result = envSchema.safeParse(process.env);
|
|
44
|
+
if (!result.success) {
|
|
45
|
+
console.error("❌ Invalid environment configuration:");
|
|
46
|
+
console.error(z.treeifyError(result.error));
|
|
47
|
+
throw new Error("Invalid environment configuration");
|
|
48
|
+
}
|
|
49
|
+
let enabledNetworks;
|
|
50
|
+
if (result.data.NETWORKS && result.data.NETWORKS.length > 0) {
|
|
51
|
+
enabledNetworks = result.data.NETWORKS;
|
|
52
|
+
} else if (result.data.NETWORK) {
|
|
53
|
+
enabledNetworks = [result.data.NETWORK];
|
|
54
|
+
} else {
|
|
55
|
+
enabledNetworks = ["mainnet"];
|
|
56
|
+
}
|
|
57
|
+
cachedEnv = { ...result.data, enabledNetworks };
|
|
58
|
+
return cachedEnv;
|
|
59
|
+
}
|
|
60
|
+
function isPox4DecoderEnabled() {
|
|
61
|
+
return process.env.POX4_DECODER_ENABLED !== "false";
|
|
62
|
+
}
|
|
63
|
+
// src/logger.ts
|
|
64
|
+
var LOG_LEVELS = {
|
|
65
|
+
debug: 0,
|
|
66
|
+
info: 1,
|
|
67
|
+
warn: 2,
|
|
68
|
+
error: 3
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
class Logger {
|
|
72
|
+
_level;
|
|
73
|
+
_isProduction;
|
|
74
|
+
_initialized = false;
|
|
75
|
+
init() {
|
|
76
|
+
if (this._initialized)
|
|
77
|
+
return;
|
|
78
|
+
this._initialized = true;
|
|
79
|
+
try {
|
|
80
|
+
const env = getEnv();
|
|
81
|
+
this._level = env.LOG_LEVEL;
|
|
82
|
+
this._isProduction = env.NODE_ENV === "production";
|
|
83
|
+
} catch {
|
|
84
|
+
this._level = "info";
|
|
85
|
+
this._isProduction = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
get level() {
|
|
89
|
+
this.init();
|
|
90
|
+
return this._level;
|
|
91
|
+
}
|
|
92
|
+
get isProduction() {
|
|
93
|
+
this.init();
|
|
94
|
+
return this._isProduction;
|
|
95
|
+
}
|
|
96
|
+
shouldLog(level) {
|
|
97
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
|
|
98
|
+
}
|
|
99
|
+
formatMessage(level, message, meta) {
|
|
100
|
+
const timestamp = new Date().toISOString();
|
|
101
|
+
if (this.isProduction) {
|
|
102
|
+
return JSON.stringify({
|
|
103
|
+
timestamp,
|
|
104
|
+
level,
|
|
105
|
+
message,
|
|
106
|
+
...meta
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
|
|
110
|
+
return `[${timestamp}] ${level.toUpperCase()}: ${message}${metaStr}`;
|
|
111
|
+
}
|
|
112
|
+
debug(message, meta) {
|
|
113
|
+
if (this.shouldLog("debug")) {
|
|
114
|
+
console.debug(this.formatMessage("debug", message, meta));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
info(message, meta) {
|
|
118
|
+
if (this.shouldLog("info")) {
|
|
119
|
+
console.info(this.formatMessage("info", message, meta));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
warn(message, meta) {
|
|
123
|
+
if (this.shouldLog("warn")) {
|
|
124
|
+
console.warn(this.formatMessage("warn", message, meta));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
error(message, meta) {
|
|
128
|
+
if (this.shouldLog("error")) {
|
|
129
|
+
console.error(this.formatMessage("error", message, meta));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
var logger = new Logger;
|
|
134
|
+
|
|
135
|
+
// src/leader.ts
|
|
136
|
+
import postgres from "postgres";
|
|
137
|
+
var INDEXER_LEADER_LOCK_KEY = 7702026;
|
|
138
|
+
var SUBSCRIPTION_EVALUATOR_LOCK_KEY = 7702027;
|
|
139
|
+
var SUBGRAPH_CATCHUP_LOCK_KEY = 7702028;
|
|
140
|
+
function leaderDatabaseUrl() {
|
|
141
|
+
return process.env.SOURCE_DATABASE_URL || process.env.DATABASE_URL || "postgres://localhost:5432/secondlayer";
|
|
142
|
+
}
|
|
143
|
+
function createPostgresLeaderBackend(url) {
|
|
144
|
+
const resolvedUrl = url ?? leaderDatabaseUrl();
|
|
145
|
+
const host = (() => {
|
|
146
|
+
try {
|
|
147
|
+
return new URL(resolvedUrl).hostname;
|
|
148
|
+
} catch {
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
})();
|
|
152
|
+
const isLocal = host === "localhost" || host === "127.0.0.1" || !host.includes(".");
|
|
153
|
+
const sql = postgres(resolvedUrl, {
|
|
154
|
+
max: 1,
|
|
155
|
+
idle_timeout: 0,
|
|
156
|
+
ssl: isLocal ? undefined : {
|
|
157
|
+
rejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== "0"
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
async tryAcquire(lockKey) {
|
|
162
|
+
const rows = await sql`
|
|
163
|
+
SELECT pg_try_advisory_lock(${lockKey}) AS locked
|
|
164
|
+
`;
|
|
165
|
+
return rows[0]?.locked === true;
|
|
166
|
+
},
|
|
167
|
+
async ping() {
|
|
168
|
+
const rows = await sql`
|
|
169
|
+
SELECT count(*)::int AS held
|
|
170
|
+
FROM pg_locks
|
|
171
|
+
WHERE locktype = 'advisory' AND pid = pg_backend_pid()
|
|
172
|
+
`;
|
|
173
|
+
if ((rows[0]?.held ?? 0) === 0) {
|
|
174
|
+
throw new Error("advisory lock no longer held (connection reset?)");
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
async close() {
|
|
178
|
+
await sql.end({ timeout: 5 });
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function withLeaderLock(lockKey, startWork, opts = {}) {
|
|
183
|
+
const pollMs = opts.pollMs ?? 15000;
|
|
184
|
+
const heartbeatMs = opts.heartbeatMs ?? 1e4;
|
|
185
|
+
const backend = (opts.createBackend ?? createPostgresLeaderBackend)();
|
|
186
|
+
let stopped = false;
|
|
187
|
+
let isLeader = false;
|
|
188
|
+
let stopWork = null;
|
|
189
|
+
let pollTimer = null;
|
|
190
|
+
let heartbeatTimer = null;
|
|
191
|
+
async function relinquish() {
|
|
192
|
+
isLeader = false;
|
|
193
|
+
if (heartbeatTimer) {
|
|
194
|
+
clearInterval(heartbeatTimer);
|
|
195
|
+
heartbeatTimer = null;
|
|
196
|
+
}
|
|
197
|
+
if (stopWork) {
|
|
198
|
+
try {
|
|
199
|
+
await stopWork();
|
|
200
|
+
} catch (err) {
|
|
201
|
+
logger.warn("Leader work stop failed", { error: String(err) });
|
|
202
|
+
}
|
|
203
|
+
stopWork = null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function startHeartbeat() {
|
|
207
|
+
heartbeatTimer = setInterval(async () => {
|
|
208
|
+
if (stopped || !isLeader)
|
|
209
|
+
return;
|
|
210
|
+
try {
|
|
211
|
+
await backend.ping();
|
|
212
|
+
} catch (err) {
|
|
213
|
+
logger.warn("Leader heartbeat failed; relinquishing", {
|
|
214
|
+
lockKey,
|
|
215
|
+
error: String(err)
|
|
216
|
+
});
|
|
217
|
+
await relinquish();
|
|
218
|
+
}
|
|
219
|
+
}, heartbeatMs);
|
|
220
|
+
}
|
|
221
|
+
async function tryAcquire() {
|
|
222
|
+
if (stopped || isLeader)
|
|
223
|
+
return;
|
|
224
|
+
try {
|
|
225
|
+
if (await backend.tryAcquire(lockKey)) {
|
|
226
|
+
isLeader = true;
|
|
227
|
+
logger.info("Acquired leader lock", { lockKey });
|
|
228
|
+
stopWork = await startWork();
|
|
229
|
+
startHeartbeat();
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
logger.warn("Leader lock acquire failed", {
|
|
233
|
+
lockKey,
|
|
234
|
+
error: String(err)
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
pollTimer = setInterval(tryAcquire, pollMs);
|
|
239
|
+
tryAcquire();
|
|
240
|
+
return async () => {
|
|
241
|
+
stopped = true;
|
|
242
|
+
if (pollTimer)
|
|
243
|
+
clearInterval(pollTimer);
|
|
244
|
+
await relinquish();
|
|
245
|
+
await backend.close().catch(() => {});
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
export {
|
|
249
|
+
withLeaderLock,
|
|
250
|
+
createPostgresLeaderBackend,
|
|
251
|
+
SUBSCRIPTION_EVALUATOR_LOCK_KEY,
|
|
252
|
+
SUBGRAPH_CATCHUP_LOCK_KEY,
|
|
253
|
+
INDEXER_LEADER_LOCK_KEY
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
//# debugId=84CDFC6FBB0B0AD464756E2164756E21
|
|
257
|
+
//# sourceMappingURL=leader.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/env.ts", "../src/logger.ts", "../src/leader.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { z } from \"zod/v4\";\n\n// Parse comma-separated networks\nconst networksSchema = z.string().transform((val) => {\n\tconst networks = val\n\t\t.split(\",\")\n\t\t.map((n) => n.trim())\n\t\t.filter(Boolean);\n\tconst valid = [\"mainnet\", \"testnet\"];\n\tfor (const n of networks) {\n\t\tif (!valid.includes(n)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Invalid network: ${n}. Must be one of: ${valid.join(\", \")}`,\n\t\t\t);\n\t\t}\n\t}\n\treturn networks as (\"mainnet\" | \"testnet\")[];\n});\n\ninterface EnvSchemaOutput {\n\tDATABASE_URL?: string;\n\t/**\n\t * Shared indexer DB (blocks/txs/events). Falls back to DATABASE_URL.\n\t * Set this alongside TARGET_DATABASE_URL to enable dual-DB mode.\n\t */\n\tSOURCE_DATABASE_URL?: string;\n\t/**\n\t * Tenant DB (subgraph schemas + subgraphs table). Falls back to DATABASE_URL.\n\t * Set this alongside SOURCE_DATABASE_URL to enable dual-DB mode.\n\t */\n\tTARGET_DATABASE_URL?: string;\n\tNETWORK?: \"mainnet\" | \"testnet\";\n\tNETWORKS?: (\"mainnet\" | \"testnet\")[];\n\tLOG_LEVEL: \"debug\" | \"info\" | \"warn\" | \"error\";\n\tNODE_ENV: \"development\" | \"production\" | \"test\";\n}\n\n// Cast needed: z.preprocess / z.default create different _input vs _output types\n// that z.ZodType<T> can't represent without explicit input type param\nconst envSchema: z.ZodType<EnvSchemaOutput> = z.object({\n\tDATABASE_URL: z.preprocess(\n\t\t(val) => (typeof val === \"string\" && val.length === 0 ? undefined : val),\n\t\tz.string().url().optional(),\n\t),\n\tSOURCE_DATABASE_URL: z.preprocess(\n\t\t(val) => (typeof val === \"string\" && val.length === 0 ? undefined : val),\n\t\tz.string().url().optional(),\n\t),\n\tTARGET_DATABASE_URL: z.preprocess(\n\t\t(val) => (typeof val === \"string\" && val.length === 0 ? undefined : val),\n\t\tz.string().url().optional(),\n\t),\n\tNETWORK: z.enum([\"mainnet\", \"testnet\"]).optional(),\n\tNETWORKS: networksSchema.optional(),\n\tLOG_LEVEL: z.enum([\"debug\", \"info\", \"warn\", \"error\"]).default(\"info\"),\n\tNODE_ENV: z\n\t\t.enum([\"development\", \"production\", \"test\"])\n\t\t.default(\"development\"),\n}) as unknown as z.ZodType<EnvSchemaOutput>;\n\nexport type Env = EnvSchemaOutput & {\n\tenabledNetworks: (\"mainnet\" | \"testnet\")[];\n};\n\nlet cachedEnv: Env | null = null;\n\nexport function getEnv(): Env {\n\tif (cachedEnv) {\n\t\treturn cachedEnv;\n\t}\n\n\tconst result = envSchema.safeParse(process.env);\n\n\tif (!result.success) {\n\t\tconsole.error(\"❌ Invalid environment configuration:\");\n\t\tconsole.error(z.treeifyError(result.error));\n\t\tthrow new Error(\"Invalid environment configuration\");\n\t}\n\n\t// Compute enabled networks from NETWORKS or NETWORK\n\tlet enabledNetworks: (\"mainnet\" | \"testnet\")[];\n\tif (result.data.NETWORKS && result.data.NETWORKS.length > 0) {\n\t\tenabledNetworks = result.data.NETWORKS;\n\t} else if (result.data.NETWORK) {\n\t\tenabledNetworks = [result.data.NETWORK];\n\t} else {\n\t\tenabledNetworks = [\"mainnet\"]; // Default\n\t}\n\n\tcachedEnv = { ...result.data, enabledNetworks };\n\treturn cachedEnv;\n}\n\n/**\n * PoX-4 stacking decoder is ON by default — `/v1/index/stacking` is part of the\n * public surface, so the decoder that fills `pox4_calls` runs unless explicitly\n * opted out with `POX4_DECODER_ENABLED=false` (mirrors the sBTC decoder policy).\n */\nexport function isPox4DecoderEnabled(): boolean {\n\treturn process.env.POX4_DECODER_ENABLED !== \"false\";\n}\n\n// Export for testing\nexport { envSchema };\n",
|
|
6
|
+
"import { getEnv } from \"./env.ts\";\n\ntype LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nconst LOG_LEVELS: Record<LogLevel, number> = {\n\tdebug: 0,\n\tinfo: 1,\n\twarn: 2,\n\terror: 3,\n};\n\nclass Logger {\n\tprivate _level?: LogLevel;\n\tprivate _isProduction?: boolean;\n\tprivate _initialized = false;\n\n\tprivate init() {\n\t\tif (this._initialized) return;\n\t\tthis._initialized = true;\n\t\ttry {\n\t\t\tconst env = getEnv();\n\t\t\tthis._level = env.LOG_LEVEL;\n\t\t\tthis._isProduction = env.NODE_ENV === \"production\";\n\t\t} catch {\n\t\t\t// Fallback when env is unavailable (e.g. tests without DATABASE_URL)\n\t\t\tthis._level = \"info\";\n\t\t\tthis._isProduction = false;\n\t\t}\n\t}\n\n\tprivate get level(): LogLevel {\n\t\tthis.init();\n\t\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\t\treturn this._level!;\n\t}\n\n\tprivate get isProduction(): boolean {\n\t\tthis.init();\n\t\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\t\treturn this._isProduction!;\n\t}\n\n\tprivate shouldLog(level: LogLevel): boolean {\n\t\treturn LOG_LEVELS[level] >= LOG_LEVELS[this.level];\n\t}\n\n\tprivate formatMessage(\n\t\tlevel: LogLevel,\n\t\tmessage: string,\n\t\t// biome-ignore lint/suspicious/noExplicitAny: interop boundary or dynamic-shape value where typing adds friction without runtime safety\n\t\tmeta?: Record<string, any>,\n\t) {\n\t\tconst timestamp = new Date().toISOString();\n\n\t\tif (this.isProduction) {\n\t\t\t// JSON output for production\n\t\t\treturn JSON.stringify({\n\t\t\t\ttimestamp,\n\t\t\t\tlevel,\n\t\t\t\tmessage,\n\t\t\t\t...meta,\n\t\t\t});\n\t\t}\n\n\t\t// Human-readable output for development\n\t\tconst metaStr = meta ? ` ${JSON.stringify(meta)}` : \"\";\n\t\treturn `[${timestamp}] ${level.toUpperCase()}: ${message}${metaStr}`;\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: interop boundary or dynamic-shape value where typing adds friction without runtime safety\n\tdebug(message: string, meta?: Record<string, any>): void {\n\t\tif (this.shouldLog(\"debug\")) {\n\t\t\tconsole.debug(this.formatMessage(\"debug\", message, meta));\n\t\t}\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: interop boundary or dynamic-shape value where typing adds friction without runtime safety\n\tinfo(message: string, meta?: Record<string, any>): void {\n\t\tif (this.shouldLog(\"info\")) {\n\t\t\tconsole.info(this.formatMessage(\"info\", message, meta));\n\t\t}\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: interop boundary or dynamic-shape value where typing adds friction without runtime safety\n\twarn(message: string, meta?: Record<string, any>): void {\n\t\tif (this.shouldLog(\"warn\")) {\n\t\t\tconsole.warn(this.formatMessage(\"warn\", message, meta));\n\t\t}\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: interop boundary or dynamic-shape value where typing adds friction without runtime safety\n\terror(message: string, meta?: Record<string, any>): void {\n\t\tif (this.shouldLog(\"error\")) {\n\t\t\tconsole.error(this.formatMessage(\"error\", message, meta));\n\t\t}\n\t}\n}\n\n// Export singleton instance\nexport const logger: Logger = new Logger();\n",
|
|
7
|
+
"import postgres from \"postgres\";\nimport { logger } from \"./logger.ts\";\n\n/**\n * Single-leader election via a Postgres session advisory lock.\n *\n * Exactly one process across the fleet holds `lockKey` and runs the leader-only\n * work. Others poll and take over if the leader exits or its connection dies.\n * The lock lives on a dedicated long-lived connection — a pooled connection\n * would silently drop a session lock — and is released by closing it.\n *\n * Lock keys are centralized here so the fleet-wide set stays distinct: a\n * collision would let two unrelated singletons exclude each other.\n */\n\n/** Advisory lock key for the indexer's singleton loops. */\nexport const INDEXER_LEADER_LOCK_KEY = 770_2026;\n/** Advisory lock key for the chain-subscription trigger evaluator (+ its\n * chain-reorg cursor rewind — they mutate the same `trigger_evaluator_state`\n * row and so must share one lock). */\nexport const SUBSCRIPTION_EVALUATOR_LOCK_KEY = 770_2027;\n/** Advisory lock key for the subgraph catch-up driver. */\nexport const SUBGRAPH_CATCHUP_LOCK_KEY = 770_2028;\n\nexport type StopFn = () => void | Promise<void>;\n\n/**\n * Backend for the advisory lock. Abstracted so the election logic is testable\n * without a database; the default is Postgres-backed.\n */\nexport type LeaderBackend = {\n\t/** Try to grab the lock without blocking. */\n\ttryAcquire(lockKey: number): Promise<boolean>;\n\t/** Liveness check; throws if the lock-holding connection is gone. */\n\tping(): Promise<void>;\n\t/** Release the lock (closes the dedicated connection). */\n\tclose(): Promise<void>;\n};\n\nfunction leaderDatabaseUrl(): string {\n\treturn (\n\t\tprocess.env.SOURCE_DATABASE_URL ||\n\t\tprocess.env.DATABASE_URL ||\n\t\t\"postgres://localhost:5432/secondlayer\"\n\t);\n}\n\n/**\n * Postgres-backed advisory lock on a dedicated connection.\n *\n * Pass an explicit `url` to pin the lock to the DB that holds the serialized\n * row — after the source/target split, control-plane state (subscriptions,\n * subgraphs) lives on the target DB, so a lock on the default source DB would\n * guard nothing.\n */\nexport function createPostgresLeaderBackend(url?: string): LeaderBackend {\n\tconst resolvedUrl = url ?? leaderDatabaseUrl();\n\tconst host = (() => {\n\t\ttry {\n\t\t\treturn new URL(resolvedUrl).hostname;\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t})();\n\tconst isLocal =\n\t\thost === \"localhost\" || host === \"127.0.0.1\" || !host.includes(\".\");\n\t// max:1 + idle_timeout:0 keeps one connection open so the session lock holds.\n\tconst sql = postgres(resolvedUrl, {\n\t\tmax: 1,\n\t\tidle_timeout: 0,\n\t\tssl: isLocal\n\t\t\t? undefined\n\t\t\t: {\n\t\t\t\t\trejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== \"0\",\n\t\t\t\t},\n\t});\n\treturn {\n\t\tasync tryAcquire(lockKey) {\n\t\t\tconst rows = await sql<{ locked: boolean }[]>`\n\t\t\t\tSELECT pg_try_advisory_lock(${lockKey}) AS locked\n\t\t\t`;\n\t\t\treturn rows[0]?.locked === true;\n\t\t},\n\t\tasync ping() {\n\t\t\t// Verify we STILL hold the advisory lock — not just that the connection\n\t\t\t// is alive. The driver auto-reconnects transparently; a reconnect starts\n\t\t\t// a new backend session, which silently drops the session-scoped lock. A\n\t\t\t// plain `SELECT 1` would succeed on the new session and never notice, so\n\t\t\t// two instances could both believe they're leader. The lock is held on\n\t\t\t// this (max:1) connection, so count advisory locks on the current\n\t\t\t// backend; 0 means we lost it (reconnect) → relinquish + re-elect.\n\t\t\tconst rows = await sql<{ held: number }[]>`\n\t\t\t\tSELECT count(*)::int AS held\n\t\t\t\tFROM pg_locks\n\t\t\t\tWHERE locktype = 'advisory' AND pid = pg_backend_pid()\n\t\t\t`;\n\t\t\tif ((rows[0]?.held ?? 0) === 0) {\n\t\t\t\tthrow new Error(\"advisory lock no longer held (connection reset?)\");\n\t\t\t}\n\t\t},\n\t\tasync close() {\n\t\t\t// Closing the session releases all advisory locks it held.\n\t\t\tawait sql.end({ timeout: 5 });\n\t\t},\n\t};\n}\n\nexport type WithLeaderLockOptions = {\n\tpollMs?: number;\n\theartbeatMs?: number;\n\t/** Injectable for tests; defaults to the Postgres backend. */\n\tcreateBackend?: () => LeaderBackend;\n};\n\n/**\n * Run `startWork` only while this process is leader. Returns a stop function\n * that ends election, stops the work, and releases the lock.\n */\nexport function withLeaderLock(\n\tlockKey: number,\n\tstartWork: () => StopFn | Promise<StopFn>,\n\topts: WithLeaderLockOptions = {},\n): () => Promise<void> {\n\tconst pollMs = opts.pollMs ?? 15_000;\n\tconst heartbeatMs = opts.heartbeatMs ?? 10_000;\n\tconst backend = (opts.createBackend ?? createPostgresLeaderBackend)();\n\n\tlet stopped = false;\n\tlet isLeader = false;\n\tlet stopWork: StopFn | null = null;\n\tlet pollTimer: ReturnType<typeof setInterval> | null = null;\n\tlet heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n\n\tasync function relinquish() {\n\t\tisLeader = false;\n\t\tif (heartbeatTimer) {\n\t\t\tclearInterval(heartbeatTimer);\n\t\t\theartbeatTimer = null;\n\t\t}\n\t\tif (stopWork) {\n\t\t\ttry {\n\t\t\t\tawait stopWork();\n\t\t\t} catch (err) {\n\t\t\t\tlogger.warn(\"Leader work stop failed\", { error: String(err) });\n\t\t\t}\n\t\t\tstopWork = null;\n\t\t}\n\t}\n\n\tfunction startHeartbeat() {\n\t\theartbeatTimer = setInterval(async () => {\n\t\t\tif (stopped || !isLeader) return;\n\t\t\ttry {\n\t\t\t\tawait backend.ping();\n\t\t\t} catch (err) {\n\t\t\t\tlogger.warn(\"Leader heartbeat failed; relinquishing\", {\n\t\t\t\t\tlockKey,\n\t\t\t\t\terror: String(err),\n\t\t\t\t});\n\t\t\t\tawait relinquish();\n\t\t\t}\n\t\t}, heartbeatMs);\n\t}\n\n\tasync function tryAcquire() {\n\t\tif (stopped || isLeader) return;\n\t\ttry {\n\t\t\tif (await backend.tryAcquire(lockKey)) {\n\t\t\t\tisLeader = true;\n\t\t\t\tlogger.info(\"Acquired leader lock\", { lockKey });\n\t\t\t\tstopWork = await startWork();\n\t\t\t\tstartHeartbeat();\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlogger.warn(\"Leader lock acquire failed\", {\n\t\t\t\tlockKey,\n\t\t\t\terror: String(err),\n\t\t\t});\n\t\t}\n\t}\n\n\tpollTimer = setInterval(tryAcquire, pollMs);\n\tvoid tryAcquire();\n\n\treturn async () => {\n\t\tstopped = true;\n\t\tif (pollTimer) clearInterval(pollTimer);\n\t\tawait relinquish();\n\t\tawait backend.close().catch(() => {});\n\t};\n}\n"
|
|
8
|
+
],
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "84CDFC6FBB0B0AD464756E2164756E21",
|
|
11
|
+
"names": []
|
|
12
|
+
}
|
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
interface RewardSetSigner {
|
|
2
|
+
/** 33-byte compressed secp256k1 public key (hex, no 0x). */
|
|
3
|
+
signing_key: string;
|
|
4
|
+
weight: number;
|
|
5
|
+
}
|
|
6
|
+
interface RewardSet {
|
|
7
|
+
signers: RewardSetSigner[];
|
|
8
|
+
total_weight: number;
|
|
9
|
+
}
|
|
10
|
+
interface NakamotoBlockHeader {
|
|
11
|
+
version: number;
|
|
12
|
+
chainLength: bigint;
|
|
13
|
+
burnSpent: bigint;
|
|
14
|
+
/** 20-byte consensus hash (hex). */
|
|
15
|
+
consensusHash: string;
|
|
16
|
+
/** 32-byte parent StacksBlockId (hex). */
|
|
17
|
+
parentBlockId: string;
|
|
18
|
+
/** 32-byte SHA512/256 merkle root over the block's txids (hex). */
|
|
19
|
+
txMerkleRoot: string;
|
|
20
|
+
/** 32-byte MARF root after applying this block (hex). */
|
|
21
|
+
stateIndexRoot: string;
|
|
22
|
+
timestamp: bigint;
|
|
23
|
+
/** 65-byte recoverable ECDSA miner signature (hex). */
|
|
24
|
+
minerSignature: string;
|
|
25
|
+
/** Per-signer recoverable ECDSA signatures, reward-set order (hex, 65B each). */
|
|
26
|
+
signerSignatures: string[];
|
|
27
|
+
/** Full serialized pox_treatment BitVec bytes (u16 bits ‖ u32 len ‖ data). */
|
|
28
|
+
poxTreatment: Uint8Array;
|
|
29
|
+
/**
|
|
30
|
+
* Exact bytes whose SHA512/256 IS the block_hash / signer_signature_hash:
|
|
31
|
+
* the header with the signer_signature vector omitted (header[0:206] ‖ pox).
|
|
32
|
+
*/
|
|
33
|
+
signerSignatureHashPreimage: Uint8Array;
|
|
34
|
+
/** Offset at which the tx `Vec` begins (= total header byte length). */
|
|
35
|
+
headerByteLength: number;
|
|
36
|
+
}
|
|
1
37
|
interface NodeInfo {
|
|
2
38
|
peer_version: number;
|
|
3
39
|
pox_consensus: string;
|
|
@@ -29,6 +65,22 @@ declare class StacksNodeClient {
|
|
|
29
65
|
constructor(rpcUrl?: string);
|
|
30
66
|
getInfo(): Promise<NodeInfo>;
|
|
31
67
|
getBlock(height: number): Promise<BlockResponse | null>;
|
|
68
|
+
/**
|
|
69
|
+
* Fetch + parse a Nakamoto block by its index_block_hash. Returns the raw
|
|
70
|
+
* bytes, the parsed header, and the recomputed block_hash / index_block_hash
|
|
71
|
+
* (so a caller can cross-check the node's answer). Null on 404.
|
|
72
|
+
*/
|
|
73
|
+
getNakamotoBlock(blockId: string): Promise<{
|
|
74
|
+
raw: Uint8Array
|
|
75
|
+
header: NakamotoBlockHeader
|
|
76
|
+
blockHash: string
|
|
77
|
+
indexBlockHash: string
|
|
78
|
+
} | null>;
|
|
79
|
+
/**
|
|
80
|
+
* Fetch the reward set (signer keys + weights) for a reward cycle from
|
|
81
|
+
* `/v3/stacker_set/{cycle}`. Null on 404 (cycle not yet computed).
|
|
82
|
+
*/
|
|
83
|
+
getRewardSet(cycle: number): Promise<RewardSet | null>;
|
|
32
84
|
isHealthy(): Promise<boolean>;
|
|
33
85
|
getContractAbi(contractId: string): Promise<unknown>;
|
|
34
86
|
/** Fetch a contract's Clarity source — used to parse declared `impl-trait`s. */
|
package/dist/src/node/client.js
CHANGED
|
@@ -14,6 +14,154 @@ var __export = (target, all) => {
|
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// src/node/nakamoto.ts
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
function sha512_256(bytes) {
|
|
20
|
+
return createHash("sha512-256").update(bytes).digest();
|
|
21
|
+
}
|
|
22
|
+
var toHex = (b) => Buffer.from(b).toString("hex");
|
|
23
|
+
var fromHex = (h) => Uint8Array.from(Buffer.from(h.startsWith("0x") ? h.slice(2) : h, "hex"));
|
|
24
|
+
var PREFIX_LEN = 206;
|
|
25
|
+
var CONSENSUS_HASH_OFF = 17;
|
|
26
|
+
var TX_MERKLE_ROOT_OFF = 69;
|
|
27
|
+
var STATE_INDEX_ROOT_OFF = 101;
|
|
28
|
+
var TIMESTAMP_OFF = 133;
|
|
29
|
+
var MINER_SIG_OFF = 141;
|
|
30
|
+
var SIGNER_VEC_OFF = 206;
|
|
31
|
+
var SIG_LEN = 65;
|
|
32
|
+
function u32(b, off) {
|
|
33
|
+
return new DataView(b.buffer, b.byteOffset, b.byteLength).getUint32(off);
|
|
34
|
+
}
|
|
35
|
+
function u64(b, off) {
|
|
36
|
+
return new DataView(b.buffer, b.byteOffset, b.byteLength).getBigUint64(off);
|
|
37
|
+
}
|
|
38
|
+
function parseNakamotoBlockHeader(raw) {
|
|
39
|
+
if (raw.length < PREFIX_LEN + 4) {
|
|
40
|
+
throw new Error("raw block too short for a Nakamoto header");
|
|
41
|
+
}
|
|
42
|
+
const signerCount = u32(raw, SIGNER_VEC_OFF);
|
|
43
|
+
const sigsStart = SIGNER_VEC_OFF + 4;
|
|
44
|
+
const signerSignatures = [];
|
|
45
|
+
for (let i = 0;i < signerCount; i++) {
|
|
46
|
+
const off = sigsStart + i * SIG_LEN;
|
|
47
|
+
signerSignatures.push(toHex(raw.subarray(off, off + SIG_LEN)));
|
|
48
|
+
}
|
|
49
|
+
const poxOff = sigsStart + signerCount * SIG_LEN;
|
|
50
|
+
const poxDataLen = u32(raw, poxOff + 2);
|
|
51
|
+
const poxEnd = poxOff + 6 + poxDataLen;
|
|
52
|
+
const poxTreatment = raw.subarray(poxOff, poxEnd);
|
|
53
|
+
const preimage = new Uint8Array(PREFIX_LEN + poxTreatment.length);
|
|
54
|
+
preimage.set(raw.subarray(0, PREFIX_LEN), 0);
|
|
55
|
+
preimage.set(poxTreatment, PREFIX_LEN);
|
|
56
|
+
return {
|
|
57
|
+
version: raw[0],
|
|
58
|
+
chainLength: u64(raw, 1),
|
|
59
|
+
burnSpent: u64(raw, 9),
|
|
60
|
+
consensusHash: toHex(raw.subarray(CONSENSUS_HASH_OFF, CONSENSUS_HASH_OFF + 20)),
|
|
61
|
+
parentBlockId: toHex(raw.subarray(37, 69)),
|
|
62
|
+
txMerkleRoot: toHex(raw.subarray(TX_MERKLE_ROOT_OFF, TX_MERKLE_ROOT_OFF + 32)),
|
|
63
|
+
stateIndexRoot: toHex(raw.subarray(STATE_INDEX_ROOT_OFF, STATE_INDEX_ROOT_OFF + 32)),
|
|
64
|
+
timestamp: u64(raw, TIMESTAMP_OFF),
|
|
65
|
+
minerSignature: toHex(raw.subarray(MINER_SIG_OFF, MINER_SIG_OFF + SIG_LEN)),
|
|
66
|
+
signerSignatures,
|
|
67
|
+
poxTreatment,
|
|
68
|
+
signerSignatureHashPreimage: preimage,
|
|
69
|
+
headerByteLength: poxEnd
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function nakamotoBlockHash(header) {
|
|
73
|
+
return toHex(sha512_256(header.signerSignatureHashPreimage));
|
|
74
|
+
}
|
|
75
|
+
function nakamotoBlockId(blockHashHex, consensusHashHex) {
|
|
76
|
+
const a = fromHex(blockHashHex);
|
|
77
|
+
const b = fromHex(consensusHashHex);
|
|
78
|
+
const buf = new Uint8Array(a.length + b.length);
|
|
79
|
+
buf.set(a, 0);
|
|
80
|
+
buf.set(b, a.length);
|
|
81
|
+
return toHex(sha512_256(buf));
|
|
82
|
+
}
|
|
83
|
+
function stacksTxid(rawTx) {
|
|
84
|
+
return toHex(sha512_256(rawTx));
|
|
85
|
+
}
|
|
86
|
+
var LEAF_TAG = 0;
|
|
87
|
+
var NODE_TAG = 1;
|
|
88
|
+
function tagged(tag, ...parts) {
|
|
89
|
+
const len = parts.reduce((n, p) => n + p.length, 1);
|
|
90
|
+
const buf = new Uint8Array(len);
|
|
91
|
+
buf[0] = tag;
|
|
92
|
+
let o = 1;
|
|
93
|
+
for (const p of parts) {
|
|
94
|
+
buf.set(p, o);
|
|
95
|
+
o += p.length;
|
|
96
|
+
}
|
|
97
|
+
return sha512_256(buf);
|
|
98
|
+
}
|
|
99
|
+
function txMerkleRoot(txidsHex) {
|
|
100
|
+
if (txidsHex.length === 0)
|
|
101
|
+
throw new Error("no transactions");
|
|
102
|
+
let level = txidsHex.map((t) => tagged(LEAF_TAG, fromHex(t)));
|
|
103
|
+
while (level.length > 1) {
|
|
104
|
+
if (level.length % 2 === 1)
|
|
105
|
+
level.push(level[level.length - 1]);
|
|
106
|
+
const next = [];
|
|
107
|
+
for (let i = 0;i < level.length; i += 2) {
|
|
108
|
+
next.push(tagged(NODE_TAG, level[i], level[i + 1]));
|
|
109
|
+
}
|
|
110
|
+
level = next;
|
|
111
|
+
}
|
|
112
|
+
return toHex(level[0]);
|
|
113
|
+
}
|
|
114
|
+
function txMerkleProof(txidsHex, index) {
|
|
115
|
+
if (index < 0 || index >= txidsHex.length) {
|
|
116
|
+
throw new Error("index out of range");
|
|
117
|
+
}
|
|
118
|
+
let level = txidsHex.map((t) => tagged(LEAF_TAG, fromHex(t)));
|
|
119
|
+
let idx = index;
|
|
120
|
+
const path = [];
|
|
121
|
+
while (level.length > 1) {
|
|
122
|
+
if (level.length % 2 === 1)
|
|
123
|
+
level.push(level[level.length - 1]);
|
|
124
|
+
const siblingIdx = idx % 2 === 0 ? idx + 1 : idx - 1;
|
|
125
|
+
path.push({
|
|
126
|
+
position: idx % 2 === 0 ? "right" : "left",
|
|
127
|
+
hash: toHex(level[siblingIdx])
|
|
128
|
+
});
|
|
129
|
+
const next = [];
|
|
130
|
+
for (let i = 0;i < level.length; i += 2) {
|
|
131
|
+
next.push(tagged(NODE_TAG, level[i], level[i + 1]));
|
|
132
|
+
}
|
|
133
|
+
level = next;
|
|
134
|
+
idx = Math.floor(idx / 2);
|
|
135
|
+
}
|
|
136
|
+
return path;
|
|
137
|
+
}
|
|
138
|
+
function verifyTxMerkleProof(txidHex, path, txMerkleRootHex) {
|
|
139
|
+
let acc = tagged(LEAF_TAG, fromHex(txidHex));
|
|
140
|
+
for (const step of path) {
|
|
141
|
+
const sib = fromHex(step.hash);
|
|
142
|
+
acc = step.position === "right" ? tagged(NODE_TAG, acc, sib) : tagged(NODE_TAG, sib, acc);
|
|
143
|
+
}
|
|
144
|
+
const root = txMerkleRootHex.startsWith("0x") ? txMerkleRootHex.slice(2) : txMerkleRootHex;
|
|
145
|
+
return toHex(acc) === root;
|
|
146
|
+
}
|
|
147
|
+
async function fetchNakamotoBlock(opts) {
|
|
148
|
+
const id = opts.blockId.startsWith("0x") ? opts.blockId.slice(2) : opts.blockId;
|
|
149
|
+
const f = opts.fetchImpl ?? fetch;
|
|
150
|
+
const res = await f(`${opts.nodeUrl.replace(/\/+$/, "")}/v3/blocks/${id}`);
|
|
151
|
+
if (!res.ok) {
|
|
152
|
+
throw new Error(`/v3/blocks/${id} returned ${res.status}`);
|
|
153
|
+
}
|
|
154
|
+
const raw = new Uint8Array(await res.arrayBuffer());
|
|
155
|
+
const header = parseNakamotoBlockHeader(raw);
|
|
156
|
+
const blockHash = nakamotoBlockHash(header);
|
|
157
|
+
return {
|
|
158
|
+
raw,
|
|
159
|
+
header,
|
|
160
|
+
blockHash,
|
|
161
|
+
indexBlockHash: nakamotoBlockId(blockHash, header.consensusHash)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
17
165
|
// src/node/client.ts
|
|
18
166
|
class StacksNodeClient {
|
|
19
167
|
rpcUrl;
|
|
@@ -40,6 +188,45 @@ class StacksNodeClient {
|
|
|
40
188
|
}
|
|
41
189
|
return res.json();
|
|
42
190
|
}
|
|
191
|
+
async getNakamotoBlock(blockId) {
|
|
192
|
+
const id = blockId.startsWith("0x") ? blockId.slice(2) : blockId;
|
|
193
|
+
const res = await fetch(`${this.rpcUrl}/v3/blocks/${id}`, {
|
|
194
|
+
signal: AbortSignal.timeout(30000)
|
|
195
|
+
});
|
|
196
|
+
if (res.status === 404)
|
|
197
|
+
return null;
|
|
198
|
+
if (!res.ok) {
|
|
199
|
+
throw new Error(`Node RPC /v3/blocks/${id} returned ${res.status}`);
|
|
200
|
+
}
|
|
201
|
+
const raw = new Uint8Array(await res.arrayBuffer());
|
|
202
|
+
const header = parseNakamotoBlockHeader(raw);
|
|
203
|
+
const blockHash = nakamotoBlockHash(header);
|
|
204
|
+
return {
|
|
205
|
+
raw,
|
|
206
|
+
header,
|
|
207
|
+
blockHash,
|
|
208
|
+
indexBlockHash: nakamotoBlockId(blockHash, header.consensusHash)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async getRewardSet(cycle) {
|
|
212
|
+
const res = await fetch(`${this.rpcUrl}/v3/stacker_set/${cycle}`, {
|
|
213
|
+
signal: AbortSignal.timeout(15000)
|
|
214
|
+
});
|
|
215
|
+
if (res.status === 404)
|
|
216
|
+
return null;
|
|
217
|
+
if (!res.ok) {
|
|
218
|
+
throw new Error(`Node RPC /v3/stacker_set/${cycle} returned ${res.status}`);
|
|
219
|
+
}
|
|
220
|
+
const body = await res.json();
|
|
221
|
+
const signers = body.stacker_set.signers.map((s) => ({
|
|
222
|
+
signing_key: s.signing_key.startsWith("0x") ? s.signing_key.slice(2) : s.signing_key,
|
|
223
|
+
weight: s.weight
|
|
224
|
+
}));
|
|
225
|
+
return {
|
|
226
|
+
signers,
|
|
227
|
+
total_weight: signers.reduce((sum, s) => sum + s.weight, 0)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
43
230
|
async isHealthy() {
|
|
44
231
|
try {
|
|
45
232
|
const info = await this.getInfo();
|
|
@@ -78,5 +265,5 @@ export {
|
|
|
78
265
|
StacksNodeClient
|
|
79
266
|
};
|
|
80
267
|
|
|
81
|
-
//# debugId=
|
|
268
|
+
//# debugId=26D700D33629603864756E2164756E21
|
|
82
269
|
//# sourceMappingURL=client.js.map
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/node/client.ts"],
|
|
3
|
+
"sources": ["../src/node/nakamoto.ts", "../src/node/client.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"
|
|
5
|
+
"import { createHash } from \"node:crypto\";\n\n/**\n * Nakamoto block-header parsing + consensus hashing for trustless verification.\n *\n * Every constant here is verified bit-exact against mainnet `stacks-node\n * 3.4.0.0.3` (see docs/design/trustless-verification-proofs.md, Appendix). This\n * Building block for transaction-inclusion / block-canonicity proofs: fetch a\n * raw `/v3/blocks/{id}` body, parse the header, and recompute the block_hash,\n * index_block_hash, and tx_merkle_root the chain itself commits to.\n */\n\n/** SHA-512/256 — the hash Stacks uses everywhere (NOT truncated SHA-512). */\nexport function sha512_256(bytes: Uint8Array): Uint8Array {\n\treturn createHash(\"sha512-256\").update(bytes).digest();\n}\n\nconst toHex = (b: Uint8Array): string => Buffer.from(b).toString(\"hex\");\nconst fromHex = (h: string): Uint8Array =>\n\tUint8Array.from(Buffer.from(h.startsWith(\"0x\") ? h.slice(2) : h, \"hex\"));\n\n// Fixed-size header prefix: version..miner_signature, before signer_signature.\nconst PREFIX_LEN = 206;\nconst CONSENSUS_HASH_OFF = 17;\nconst TX_MERKLE_ROOT_OFF = 69;\nconst STATE_INDEX_ROOT_OFF = 101;\nconst TIMESTAMP_OFF = 133;\nconst MINER_SIG_OFF = 141;\nconst SIGNER_VEC_OFF = 206; // u32 count, then 65 bytes per signer\nconst SIG_LEN = 65;\n\nexport interface NakamotoBlockHeader {\n\tversion: number;\n\tchainLength: bigint;\n\tburnSpent: bigint;\n\t/** 20-byte consensus hash (hex). */\n\tconsensusHash: string;\n\t/** 32-byte parent StacksBlockId (hex). */\n\tparentBlockId: string;\n\t/** 32-byte SHA512/256 merkle root over the block's txids (hex). */\n\ttxMerkleRoot: string;\n\t/** 32-byte MARF root after applying this block (hex). */\n\tstateIndexRoot: string;\n\ttimestamp: bigint;\n\t/** 65-byte recoverable ECDSA miner signature (hex). */\n\tminerSignature: string;\n\t/** Per-signer recoverable ECDSA signatures, reward-set order (hex, 65B each). */\n\tsignerSignatures: string[];\n\t/** Full serialized pox_treatment BitVec bytes (u16 bits ‖ u32 len ‖ data). */\n\tpoxTreatment: Uint8Array;\n\t/**\n\t * Exact bytes whose SHA512/256 IS the block_hash / signer_signature_hash:\n\t * the header with the signer_signature vector omitted (header[0:206] ‖ pox).\n\t */\n\tsignerSignatureHashPreimage: Uint8Array;\n\t/** Offset at which the tx `Vec` begins (= total header byte length). */\n\theaderByteLength: number;\n}\n\nfunction u32(b: Uint8Array, off: number): number {\n\treturn new DataView(b.buffer, b.byteOffset, b.byteLength).getUint32(off);\n}\nfunction u64(b: Uint8Array, off: number): bigint {\n\treturn new DataView(b.buffer, b.byteOffset, b.byteLength).getBigUint64(off);\n}\n\n/** Parse the Nakamoto block header from the raw `/v3/blocks` body. */\nexport function parseNakamotoBlockHeader(raw: Uint8Array): NakamotoBlockHeader {\n\tif (raw.length < PREFIX_LEN + 4) {\n\t\tthrow new Error(\"raw block too short for a Nakamoto header\");\n\t}\n\tconst signerCount = u32(raw, SIGNER_VEC_OFF);\n\tconst sigsStart = SIGNER_VEC_OFF + 4;\n\tconst signerSignatures: string[] = [];\n\tfor (let i = 0; i < signerCount; i++) {\n\t\tconst off = sigsStart + i * SIG_LEN;\n\t\tsignerSignatures.push(toHex(raw.subarray(off, off + SIG_LEN)));\n\t}\n\t// pox_treatment: u16 num_bits ‖ u32 data_len ‖ data[data_len].\n\tconst poxOff = sigsStart + signerCount * SIG_LEN;\n\tconst poxDataLen = u32(raw, poxOff + 2);\n\tconst poxEnd = poxOff + 6 + poxDataLen;\n\tconst poxTreatment = raw.subarray(poxOff, poxEnd);\n\n\t// block_hash preimage = header minus signer_signature = prefix[0:206] ‖ pox.\n\tconst preimage = new Uint8Array(PREFIX_LEN + poxTreatment.length);\n\tpreimage.set(raw.subarray(0, PREFIX_LEN), 0);\n\tpreimage.set(poxTreatment, PREFIX_LEN);\n\n\treturn {\n\t\tversion: raw[0],\n\t\tchainLength: u64(raw, 1),\n\t\tburnSpent: u64(raw, 9),\n\t\tconsensusHash: toHex(\n\t\t\traw.subarray(CONSENSUS_HASH_OFF, CONSENSUS_HASH_OFF + 20),\n\t\t),\n\t\tparentBlockId: toHex(raw.subarray(37, 69)),\n\t\ttxMerkleRoot: toHex(\n\t\t\traw.subarray(TX_MERKLE_ROOT_OFF, TX_MERKLE_ROOT_OFF + 32),\n\t\t),\n\t\tstateIndexRoot: toHex(\n\t\t\traw.subarray(STATE_INDEX_ROOT_OFF, STATE_INDEX_ROOT_OFF + 32),\n\t\t),\n\t\ttimestamp: u64(raw, TIMESTAMP_OFF),\n\t\tminerSignature: toHex(raw.subarray(MINER_SIG_OFF, MINER_SIG_OFF + SIG_LEN)),\n\t\tsignerSignatures,\n\t\tpoxTreatment,\n\t\tsignerSignatureHashPreimage: preimage,\n\t\theaderByteLength: poxEnd,\n\t};\n}\n\n/**\n * block_hash (== signer_signature_hash): SHA512/256 over the header with the\n * signer_signature vector omitted. This is what each signer signs.\n */\nexport function nakamotoBlockHash(header: NakamotoBlockHeader): string {\n\treturn toHex(sha512_256(header.signerSignatureHashPreimage));\n}\n\n/** index_block_hash (StacksBlockId) = SHA512/256(block_hash ‖ consensus_hash). */\nexport function nakamotoBlockId(\n\tblockHashHex: string,\n\tconsensusHashHex: string,\n): string {\n\tconst a = fromHex(blockHashHex);\n\tconst b = fromHex(consensusHashHex);\n\tconst buf = new Uint8Array(a.length + b.length);\n\tbuf.set(a, 0);\n\tbuf.set(b, a.length);\n\treturn toHex(sha512_256(buf));\n}\n\n/** A Stacks txid = SHA512/256 of the transaction's consensus serialization. */\nexport function stacksTxid(rawTx: Uint8Array): string {\n\treturn toHex(sha512_256(rawTx));\n}\n\nconst LEAF_TAG = 0x00;\nconst NODE_TAG = 0x01;\n\nfunction tagged(tag: number, ...parts: Uint8Array[]): Uint8Array {\n\tconst len = parts.reduce((n, p) => n + p.length, 1);\n\tconst buf = new Uint8Array(len);\n\tbuf[0] = tag;\n\tlet o = 1;\n\tfor (const p of parts) {\n\t\tbuf.set(p, o);\n\t\to += p.length;\n\t}\n\treturn sha512_256(buf);\n}\n\n/**\n * tx_merkle_root over the block's txids (hex), reproducing the consensus rule:\n * leaf = H(0x00 ‖ txid), node = H(0x01 ‖ left ‖ right), odd level duplicates the\n * last node. Returns the root hex; throws on an empty tx list.\n */\nexport function txMerkleRoot(txidsHex: string[]): string {\n\tif (txidsHex.length === 0) throw new Error(\"no transactions\");\n\tlet level = txidsHex.map((t) => tagged(LEAF_TAG, fromHex(t)));\n\twhile (level.length > 1) {\n\t\tif (level.length % 2 === 1) level.push(level[level.length - 1]);\n\t\tconst next: Uint8Array[] = [];\n\t\tfor (let i = 0; i < level.length; i += 2) {\n\t\t\tnext.push(tagged(NODE_TAG, level[i], level[i + 1]));\n\t\t}\n\t\tlevel = next;\n\t}\n\treturn toHex(level[0]);\n}\n\n/** One authentication-path step: the sibling hash and which side it's on. */\nexport interface MerkleProofStep {\n\t/** Side the SIBLING is on relative to the accumulator. */\n\tposition: \"left\" | \"right\";\n\t/** Sibling node hash (hex). */\n\thash: string;\n}\n\n/**\n * Build the tx-inclusion authentication path for the tx at `index` in a block,\n * reproducing the consensus merkle tree (incl. duplicate-last-on-odd). The path\n * lets a verifier recompute `tx_merkle_root` from just the target txid.\n */\nexport function txMerkleProof(\n\ttxidsHex: string[],\n\tindex: number,\n): MerkleProofStep[] {\n\tif (index < 0 || index >= txidsHex.length) {\n\t\tthrow new Error(\"index out of range\");\n\t}\n\tlet level = txidsHex.map((t) => tagged(LEAF_TAG, fromHex(t)));\n\tlet idx = index;\n\tconst path: MerkleProofStep[] = [];\n\twhile (level.length > 1) {\n\t\tif (level.length % 2 === 1) level.push(level[level.length - 1]);\n\t\tconst siblingIdx = idx % 2 === 0 ? idx + 1 : idx - 1;\n\t\tpath.push({\n\t\t\tposition: idx % 2 === 0 ? \"right\" : \"left\",\n\t\t\thash: toHex(level[siblingIdx]),\n\t\t});\n\t\tconst next: Uint8Array[] = [];\n\t\tfor (let i = 0; i < level.length; i += 2) {\n\t\t\tnext.push(tagged(NODE_TAG, level[i], level[i + 1]));\n\t\t}\n\t\tlevel = next;\n\t\tidx = Math.floor(idx / 2);\n\t}\n\treturn path;\n}\n\n/**\n * Verify a tx-inclusion proof: fold the target `txid` (hex) up through `path`\n * and check it equals `txMerkleRoot` (hex). The verifier recomputes the txid\n * itself from the raw tx bytes, so nothing here is trusted.\n */\nexport function verifyTxMerkleProof(\n\ttxidHex: string,\n\tpath: MerkleProofStep[],\n\ttxMerkleRootHex: string,\n): boolean {\n\tlet acc = tagged(LEAF_TAG, fromHex(txidHex));\n\tfor (const step of path) {\n\t\tconst sib = fromHex(step.hash);\n\t\tacc =\n\t\t\tstep.position === \"right\"\n\t\t\t\t? tagged(NODE_TAG, acc, sib)\n\t\t\t\t: tagged(NODE_TAG, sib, acc);\n\t}\n\tconst root = txMerkleRootHex.startsWith(\"0x\")\n\t\t? txMerkleRootHex.slice(2)\n\t\t: txMerkleRootHex;\n\treturn toHex(acc) === root;\n}\n\n/**\n * Fetch and parse a Nakamoto block from a stacks-node. `blockId` is the\n * index_block_hash (with or without 0x). Returns the raw bytes + parsed header +\n * the recomputed block_hash / index_block_hash so a caller can cross-check.\n */\nexport async function fetchNakamotoBlock(opts: {\n\tnodeUrl: string;\n\tblockId: string;\n\tfetchImpl?: typeof fetch;\n}): Promise<{\n\traw: Uint8Array;\n\theader: NakamotoBlockHeader;\n\tblockHash: string;\n\tindexBlockHash: string;\n}> {\n\tconst id = opts.blockId.startsWith(\"0x\")\n\t\t? opts.blockId.slice(2)\n\t\t: opts.blockId;\n\tconst f = opts.fetchImpl ?? fetch;\n\tconst res = await f(`${opts.nodeUrl.replace(/\\/+$/, \"\")}/v3/blocks/${id}`);\n\tif (!res.ok) {\n\t\tthrow new Error(`/v3/blocks/${id} returned ${res.status}`);\n\t}\n\tconst raw = new Uint8Array(await res.arrayBuffer());\n\tconst header = parseNakamotoBlockHeader(raw);\n\tconst blockHash = nakamotoBlockHash(header);\n\treturn {\n\t\traw,\n\t\theader,\n\t\tblockHash,\n\t\tindexBlockHash: nakamotoBlockId(blockHash, header.consensusHash),\n\t};\n}\n",
|
|
6
|
+
"import type { RewardSet } from \"./consensus.ts\";\nimport {\n\ttype NakamotoBlockHeader,\n\tnakamotoBlockHash,\n\tnakamotoBlockId,\n\tparseNakamotoBlockHeader,\n} from \"./nakamoto.ts\";\n\nexport interface NodeInfo {\n\tpeer_version: number;\n\tpox_consensus: string;\n\tburn_block_height: number;\n\tstable_pox_consensus: string;\n\tstable_burn_block_height: number;\n\tserver_version: string;\n\tnetwork_id: number;\n\tparent_network_id: number;\n\tstacks_tip_height: number;\n\tstacks_tip: string;\n\tstacks_tip_consensus_hash: string;\n\tgenesis_chainstate_hash: string;\n}\n\nexport interface BlockResponse {\n\thash: string;\n\theight: number;\n\tparent_block_hash: string;\n\tburn_block_height: number;\n\tburn_block_hash: string;\n\tburn_block_time: number;\n\tindex_block_hash: string;\n\tparent_index_block_hash: string;\n\tminer_txid: string;\n\ttxs: string[];\n\t// Full block data varies by endpoint — kept minimal for fetch use case\n}\n\nexport class StacksNodeClient {\n\tprivate rpcUrl: string;\n\n\tconstructor(rpcUrl?: string) {\n\t\tthis.rpcUrl =\n\t\t\trpcUrl || process.env.STACKS_NODE_RPC_URL || \"http://localhost:20443\";\n\t}\n\n\tasync getInfo(): Promise<NodeInfo> {\n\t\tconst res = await fetch(`${this.rpcUrl}/v2/info`, {\n\t\t\tsignal: AbortSignal.timeout(10_000),\n\t\t});\n\t\tif (!res.ok) {\n\t\t\tthrow new Error(`Node RPC /v2/info returned ${res.status}`);\n\t\t}\n\t\treturn res.json() as Promise<NodeInfo>;\n\t}\n\n\tasync getBlock(height: number): Promise<BlockResponse | null> {\n\t\t// Stacks API v2 block-by-height endpoint\n\t\tconst res = await fetch(`${this.rpcUrl}/v2/blocks/${height}`, {\n\t\t\tsignal: AbortSignal.timeout(30_000),\n\t\t});\n\t\tif (res.status === 404) return null;\n\t\tif (!res.ok) {\n\t\t\tthrow new Error(`Node RPC /v2/blocks/${height} returned ${res.status}`);\n\t\t}\n\t\treturn res.json() as Promise<BlockResponse>;\n\t}\n\n\t/**\n\t * Fetch + parse a Nakamoto block by its index_block_hash. Returns the raw\n\t * bytes, the parsed header, and the recomputed block_hash / index_block_hash\n\t * (so a caller can cross-check the node's answer). Null on 404.\n\t */\n\tasync getNakamotoBlock(blockId: string): Promise<{\n\t\traw: Uint8Array;\n\t\theader: NakamotoBlockHeader;\n\t\tblockHash: string;\n\t\tindexBlockHash: string;\n\t} | null> {\n\t\tconst id = blockId.startsWith(\"0x\") ? blockId.slice(2) : blockId;\n\t\tconst res = await fetch(`${this.rpcUrl}/v3/blocks/${id}`, {\n\t\t\tsignal: AbortSignal.timeout(30_000),\n\t\t});\n\t\tif (res.status === 404) return null;\n\t\tif (!res.ok) {\n\t\t\tthrow new Error(`Node RPC /v3/blocks/${id} returned ${res.status}`);\n\t\t}\n\t\tconst raw = new Uint8Array(await res.arrayBuffer());\n\t\tconst header = parseNakamotoBlockHeader(raw);\n\t\tconst blockHash = nakamotoBlockHash(header);\n\t\treturn {\n\t\t\traw,\n\t\t\theader,\n\t\t\tblockHash,\n\t\t\tindexBlockHash: nakamotoBlockId(blockHash, header.consensusHash),\n\t\t};\n\t}\n\n\t/**\n\t * Fetch the reward set (signer keys + weights) for a reward cycle from\n\t * `/v3/stacker_set/{cycle}`. Null on 404 (cycle not yet computed).\n\t */\n\tasync getRewardSet(cycle: number): Promise<RewardSet | null> {\n\t\tconst res = await fetch(`${this.rpcUrl}/v3/stacker_set/${cycle}`, {\n\t\t\tsignal: AbortSignal.timeout(15_000),\n\t\t});\n\t\tif (res.status === 404) return null;\n\t\tif (!res.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Node RPC /v3/stacker_set/${cycle} returned ${res.status}`,\n\t\t\t);\n\t\t}\n\t\tconst body = (await res.json()) as {\n\t\t\tstacker_set: { signers: { signing_key: string; weight: number }[] };\n\t\t};\n\t\tconst signers = body.stacker_set.signers.map((s) => ({\n\t\t\tsigning_key: s.signing_key.startsWith(\"0x\")\n\t\t\t\t? s.signing_key.slice(2)\n\t\t\t\t: s.signing_key,\n\t\t\tweight: s.weight,\n\t\t}));\n\t\treturn {\n\t\t\tsigners,\n\t\t\ttotal_weight: signers.reduce((sum, s) => sum + s.weight, 0),\n\t\t};\n\t}\n\n\tasync isHealthy(): Promise<boolean> {\n\t\ttry {\n\t\t\tconst info = await this.getInfo();\n\t\t\treturn info.stacks_tip_height > 0;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tasync getContractAbi(contractId: string): Promise<unknown> {\n\t\tconst dotIdx = contractId.indexOf(\".\");\n\t\tconst address = contractId.slice(0, dotIdx);\n\t\tconst name = contractId.slice(dotIdx + 1);\n\t\tconst res = await fetch(\n\t\t\t`${this.rpcUrl}/v2/contracts/interface/${address}/${name}`,\n\t\t\t{\n\t\t\t\tsignal: AbortSignal.timeout(30_000),\n\t\t\t},\n\t\t);\n\t\tif (!res.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Node RPC /v2/contracts/interface/${address}/${name} returned ${res.status}`,\n\t\t\t);\n\t\t}\n\t\treturn res.json();\n\t}\n\n\t/** Fetch a contract's Clarity source — used to parse declared `impl-trait`s. */\n\tasync getContractSource(contractId: string): Promise<string | null> {\n\t\tconst dotIdx = contractId.indexOf(\".\");\n\t\tconst address = contractId.slice(0, dotIdx);\n\t\tconst name = contractId.slice(dotIdx + 1);\n\t\tconst res = await fetch(\n\t\t\t`${this.rpcUrl}/v2/contracts/source/${address}/${name}`,\n\t\t\t{ signal: AbortSignal.timeout(30_000) },\n\t\t);\n\t\tif (!res.ok) return null;\n\t\tconst body = (await res.json()) as { source?: string };\n\t\treturn body.source ?? null;\n\t}\n\n\tgetRpcUrl(): string {\n\t\treturn this.rpcUrl;\n\t}\n}\n"
|
|
6
7
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;
|
|
8
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "26D700D33629603864756E2164756E21",
|
|
9
10
|
"names": []
|
|
10
11
|
}
|