From ff3a5c22cd4a756c7d73b405de7a63eb71b89b61 Mon Sep 17 00:00:00 2001 From: lidarbtc Date: Wed, 24 Dec 2025 21:45:40 +0900 Subject: [PATCH 01/10] tss_api, key_share_node: add key share node telemetry and slack alerts --- .../oko_api/server/oko_api_server.env.example | 5 +- backend/oko_api/server/src/bin/launch.ts | 5 + backend/oko_api/server/src/envs.ts | 3 + .../server/src/runtime/ks_node_monitor.ts | 63 +++++++++ .../20251224070753_add_ks_node_telemetry.ts | 42 ++++++ .../oko_pg_interface/src/ks_nodes/index.ts | 121 ++++++++++++++++++ backend/openapi/src/tss/index.ts | 1 + backend/openapi/src/tss/ks_node.ts | 29 +++++ backend/tss_api/src/api/ks_node/telemetry.ts | 75 +++++++++++ backend/tss_api/src/index.ts | 1 + backend/tss_api/src/routes/index.ts | 2 + .../tss_api/src/routes/ks_node_telemetry.ts | 112 ++++++++++++++++ backend/tss_api/src/utils/slack.ts | 27 ++++ common/oko_types/src/tss/ks_node.ts | 14 ++ key_share_node/ksn_interface/src/status.ts | 1 + .../migrations/20251224071629_add_meta.ts | 27 ++++ key_share_node/pg_interface/package.json | 1 + key_share_node/pg_interface/src/index.ts | 1 + .../pg_interface/src/key_shares/index.ts | 12 ++ key_share_node/pg_interface/src/meta/index.ts | 52 ++++++++ .../server/key_share_node.env.example | 2 + .../server/key_share_node_2.env.example | 2 + .../server/key_share_node_3.env.example | 2 + key_share_node/server/src/bin/launch/index.ts | 8 ++ key_share_node/server/src/envs.ts | 4 + key_share_node/server/src/routes/status.ts | 20 ++- .../server/src/runtime/telemetry_reporter.ts | 89 +++++++++++++ 27 files changed, 718 insertions(+), 3 deletions(-) create mode 100644 backend/oko_api/server/src/runtime/ks_node_monitor.ts create mode 100644 backend/oko_pg_interface/migrations/20251224070753_add_ks_node_telemetry.ts create mode 100644 backend/openapi/src/tss/ks_node.ts create mode 100644 backend/tss_api/src/api/ks_node/telemetry.ts create mode 100644 backend/tss_api/src/routes/ks_node_telemetry.ts create mode 100644 backend/tss_api/src/utils/slack.ts create mode 100644 key_share_node/pg_interface/migrations/20251224071629_add_meta.ts create mode 100644 key_share_node/pg_interface/src/meta/index.ts create mode 100644 key_share_node/server/src/runtime/telemetry_reporter.ts diff --git a/backend/oko_api/server/oko_api_server.env.example b/backend/oko_api/server/oko_api_server.env.example index 22a450141..720f251d2 100644 --- a/backend/oko_api/server/oko_api_server.env.example +++ b/backend/oko_api/server/oko_api_server.env.example @@ -33,4 +33,7 @@ ES_USERNAME="username" ES_PASSWORD="pw" TYPEFORM_WEBHOOK_SECRET="typeform-webhook-secret" -TELEGRAM_BOT_TOKEN="telegram-bot-token" \ No newline at end of file +TELEGRAM_BOT_TOKEN="telegram-bot-token" + +SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..." +KS_NODE_REPORT_PASSWORD="ks-node-report-password" diff --git a/backend/oko_api/server/src/bin/launch.ts b/backend/oko_api/server/src/bin/launch.ts index a8b50a47c..96312816f 100644 --- a/backend/oko_api/server/src/bin/launch.ts +++ b/backend/oko_api/server/src/bin/launch.ts @@ -15,6 +15,7 @@ import { makeApp } from "@oko-wallet-api/app"; import { ENV_FILE_NAME, envSchema } from "@oko-wallet-api/envs"; import { getCommitHash } from "@oko-wallet-api/git"; import { startKSNodeHealthCheckRuntime } from "@oko-wallet-api/runtime/health_check_node"; +import { startKSNodeHeartbeatRuntime } from "@oko-wallet-api/runtime/ks_node_monitor"; async function main() { console.log("NODE_ENV: %s", process.env.NODE_ENV); @@ -90,6 +91,10 @@ async function main() { intervalSeconds: 10 * 60, // 10 minutes }); + startKSNodeHeartbeatRuntime(state.db, state.logger, { + intervalSeconds: 60, // 1 minute + }); + startInactiveCustomerUserReminderRuntime(state.db, state.logger, { intervalSeconds: 60 * 60, // 1 hour timeUntilInactiveMs: 7 * 24 * 60 * 60 * 1000, // 7 days in milliseconds diff --git a/backend/oko_api/server/src/envs.ts b/backend/oko_api/server/src/envs.ts index 6278f7adc..4009e2dac 100644 --- a/backend/oko_api/server/src/envs.ts +++ b/backend/oko_api/server/src/envs.ts @@ -39,4 +39,7 @@ export const envSchema = z.object({ TYPEFORM_WEBHOOK_SECRET: z.string(), TELEGRAM_BOT_TOKEN: z.string(), + + SLACK_WEBHOOK_URL: z.string().optional(), + KS_NODE_REPORT_PASSWORD: z.string(), }); diff --git a/backend/oko_api/server/src/runtime/ks_node_monitor.ts b/backend/oko_api/server/src/runtime/ks_node_monitor.ts new file mode 100644 index 000000000..66286c9fa --- /dev/null +++ b/backend/oko_api/server/src/runtime/ks_node_monitor.ts @@ -0,0 +1,63 @@ +import type { Pool } from "pg"; +import type { Logger } from "winston"; +import { + getLatestKSNodeTelemetries, + getKSNodeByTelemetryId, +} from "@oko-wallet/oko-pg-interface/ks_nodes"; +import { sendSlackAlert } from "@oko-wallet/tss-api"; +import dayjs from "dayjs"; + +const HEARTBEAT_THRESHOLD_MINUTES = 10; + +export function startKSNodeHeartbeatRuntime( + db: Pool, + logger: Logger, + options: { intervalSeconds: number }, +) { + logger.info("Starting KS Node heartbeat runtime"); + + const run = async () => { + try { + await checkKSNodeHeartbeats(db, logger); + } catch (err) { + logger.error("KS Node heartbeat runtime error: %s", err); + } + }; + + run().then(); + setInterval(run, options.intervalSeconds * 1000); +} + +async function checkKSNodeHeartbeats(db: Pool, logger: Logger) { + const latestTelemetriesRes = await getLatestKSNodeTelemetries(db); + if (!latestTelemetriesRes.success) { + logger.error( + "Failed to get latest KS node telemetries: %s", + latestTelemetriesRes.err, + ); + return; + } + + const now = dayjs(); + const threshold = now.subtract(HEARTBEAT_THRESHOLD_MINUTES, "minute"); + + for (const telemetry of latestTelemetriesRes.data) { + const lastUpdate = dayjs(telemetry.created_at); + + if (lastUpdate.isBefore(threshold)) { + // Node is unresponsive + const telemetryNodeId = telemetry.telemetry_node_id; + + // Get node name + const nodeRes = await getKSNodeByTelemetryId(db, telemetryNodeId); + const nodeName = + nodeRes.success && nodeRes.data + ? `${nodeRes.data.node_name} (${telemetryNodeId})` + : telemetryNodeId; + + await sendSlackAlert( + `[KS Node Alert] Node ${nodeName} has not reported telemetry for over ${HEARTBEAT_THRESHOLD_MINUTES} minutes. Last seen: ${lastUpdate.toISOString()}`, + ); + } + } +} diff --git a/backend/oko_pg_interface/migrations/20251224070753_add_ks_node_telemetry.ts b/backend/oko_pg_interface/migrations/20251224070753_add_ks_node_telemetry.ts new file mode 100644 index 000000000..b99e69243 --- /dev/null +++ b/backend/oko_pg_interface/migrations/20251224070753_add_ks_node_telemetry.ts @@ -0,0 +1,42 @@ +import type { Knex } from "knex"; + +export async function up(knex: Knex): Promise { + await knex.schema + .withSchema("public") + .alterTable("key_share_nodes", (table) => { + table.string("telemetry_node_id", 255).unique(); + }); + + await knex.schema + .withSchema("public") + .createTable("ks_node_telemetry", (table) => { + table + .uuid("log_id") + .notNullable() + .defaultTo(knex.raw("gen_random_uuid()")) + .primary({ constraintName: "ks_node_telemetry_pkey" }); + table.string("telemetry_node_id", 255).notNullable(); + table.integer("key_share_count").notNullable(); + table.jsonb("payload").notNullable(); + table + .timestamp("created_at", { useTz: true }) + .notNullable() + .defaultTo(knex.fn.now()); + + table.index( + ["telemetry_node_id"], + "idx_ks_node_telemetry_telemetry_node_id", + ); + table.index(["created_at"], "idx_ks_node_telemetry_created_at"); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.withSchema("public").dropTableIfExists("ks_node_telemetry"); + + await knex.schema + .withSchema("public") + .alterTable("key_share_nodes", (table) => { + table.dropColumn("telemetry_node_id"); + }); +} diff --git a/backend/oko_pg_interface/src/ks_nodes/index.ts b/backend/oko_pg_interface/src/ks_nodes/index.ts index 2d790cdf3..876db2816 100644 --- a/backend/oko_pg_interface/src/ks_nodes/index.ts +++ b/backend/oko_pg_interface/src/ks_nodes/index.ts @@ -8,9 +8,130 @@ import { type WalletKSNodeWithNodeNameAndServerUrl, type WalletKSNodeStatus, type KSNodeHealthCheck, + type KSNodeTelemetry, } from "@oko-wallet/oko-types/tss"; import type { WithPagination, WithTime } from "@oko-wallet-types/aux_types"; +export async function insertKSNodeTelemetry( + db: Pool | PoolClient, + telemetryNodeId: string, + key_share_count: number, + payload: any, +): Promise> { + const query = ` +INSERT INTO ks_node_telemetry ( + log_id, telemetry_node_id, key_share_count, payload +) +VALUES ( + $1, $2, $3, $4 +) +`; + + try { + await db.query(query, [ + uuidv4(), + telemetryNodeId, + key_share_count, + payload, + ]); + + return { + success: true, + data: void 0, + }; + } catch (error) { + return { + success: false, + err: String(error), + }; + } +} + +export async function getLastKSNodeTelemetry( + db: Pool | PoolClient, + telemetryNodeId: string, +): Promise> { + const query = ` +SELECT * +FROM ks_node_telemetry +WHERE telemetry_node_id = $1 +ORDER BY created_at DESC +LIMIT 1 +`; + + try { + const result = await db.query(query, [telemetryNodeId]); + const row = result.rows[0]; + + if (!row) { + return { success: true, data: null }; + } + + return { + success: true, + data: { + log_id: row.log_id, + telemetry_node_id: row.telemetry_node_id, + key_share_count: row.key_share_count, + payload: row.payload, + created_at: row.created_at, + }, + }; + } catch (error) { + return { + success: false, + err: String(error), + }; + } +} + +export async function getLatestKSNodeTelemetries( + db: Pool | PoolClient, +): Promise> { + const query = ` +SELECT DISTINCT ON (telemetry_node_id) telemetry_node_id, created_at +FROM ks_node_telemetry +ORDER BY telemetry_node_id, created_at DESC +`; + + try { + const result = await db.query(query); + return { + success: true, + data: result.rows, + }; + } catch (error) { + return { + success: false, + err: String(error), + }; + } +} + +export async function getKSNodeByTelemetryId( + db: Pool | PoolClient, + telemetryNodeId: string, +): Promise> { + const query = ` +SELECT * +FROM key_share_nodes +WHERE telemetry_node_id = $1 AND deleted_at IS NULL +`; + + try { + const result = await db.query(query, [telemetryNodeId]); + return { + success: true, + data: result.rows[0] || null, + }; + } catch (error) { + return { + success: false, + err: String(error), + }; + } +} + export async function getKSNodeById( db: Pool | PoolClient, nodeId: string, diff --git a/backend/openapi/src/tss/index.ts b/backend/openapi/src/tss/index.ts index aad6af32a..73d63a3f1 100644 --- a/backend/openapi/src/tss/index.ts +++ b/backend/openapi/src/tss/index.ts @@ -4,3 +4,4 @@ export * from "./sign"; export * from "./tss_session"; export * from "./triples"; export * from "./user"; +export * from "./ks_node"; diff --git a/backend/openapi/src/tss/ks_node.ts b/backend/openapi/src/tss/ks_node.ts new file mode 100644 index 000000000..0313bdc0a --- /dev/null +++ b/backend/openapi/src/tss/ks_node.ts @@ -0,0 +1,29 @@ +import { z } from "zod"; + +import { registry } from "../registry"; + +export const KSNodeTelemetryRequestSchema = registry.register( + "TssKSNodeTelemetryRequest", + z.object({ + telemetry_node_id: z.string().openapi({ + description: "Unique identifier for the key share node telemetry", + }), + key_share_count: z.number().openapi({ + description: "Current number of key shares stored in the node", + }), + payload: z.object({}).loose().openapi({ + description: + "Additional telemetry data (e.g., db status, error messages)", + }), + }), +); + +export const KSNodeTelemetryResponseSchema = registry.register( + "TssKSNodeTelemetryResponse", + z.object({ + success: z.boolean().openapi({ + description: "Indicates if the telemetry was successfully processed", + }), + data: z.null().optional(), + }), +); diff --git a/backend/tss_api/src/api/ks_node/telemetry.ts b/backend/tss_api/src/api/ks_node/telemetry.ts new file mode 100644 index 000000000..145bea04a --- /dev/null +++ b/backend/tss_api/src/api/ks_node/telemetry.ts @@ -0,0 +1,75 @@ +import type { Pool } from "pg"; +import type { Result } from "@oko-wallet/stdlib-js"; +import { + insertKSNodeTelemetry, + getLastKSNodeTelemetry, + getKSNodeByTelemetryId, +} from "@oko-wallet/oko-pg-interface/ks_nodes"; + +import { sendSlackAlert } from "@oko-wallet-tss-api/utils/slack"; + +export interface KSNodeTelemetryPayload { + telemetry_node_id: string; + key_share_count: number; + payload: any; +} + +export async function processKSNodeTelemetry( + db: Pool, + input: KSNodeTelemetryPayload, + password: string, +): Promise> { + // 1. Verify Password + if (password !== process.env.KS_NODE_REPORT_PASSWORD) { + return { + success: false, + err: "Invalid password", + }; + } + + const { telemetry_node_id, key_share_count, payload } = input; + + // 2. Get previous telemetry for comparison + const lastTelemetryRes = await getLastKSNodeTelemetry(db, telemetry_node_id); + if (!lastTelemetryRes.success) { + await sendSlackAlert( + `[TSS API Error] Failed to get last telemetry for node ${telemetry_node_id}: ${lastTelemetryRes.err}`, + ); + return { success: false, err: lastTelemetryRes.err }; + } + + const lastTelemetry = lastTelemetryRes.data; + + // 3. Insert new telemetry + const insertRes = await insertKSNodeTelemetry( + db, + telemetry_node_id, + key_share_count, + payload, + ); + if (!insertRes.success) { + await sendSlackAlert( + `[TSS API Error] Failed to insert telemetry for node ${telemetry_node_id}: ${insertRes.err}`, + ); + return { success: false, err: insertRes.err }; + } + + // 4. Check for anomalies + const nodeRes = await getKSNodeByTelemetryId(db, telemetry_node_id); + const nodeName = + nodeRes.success && nodeRes.data + ? `${nodeRes.data.node_name} (${telemetry_node_id})` + : telemetry_node_id; + + if (key_share_count === 0) { + await sendSlackAlert( + `[KS Node Alert] Key share count is 0 for node: ${nodeName}`, + ); + } else if (lastTelemetry && key_share_count < lastTelemetry.key_share_count) { + await sendSlackAlert( + `[KS Node Alert] Key share count decreased for node: ${nodeName}. Previous: ${lastTelemetry.key_share_count}, Current: ${key_share_count}`, + ); + } + + return { success: true, data: void 0 }; +} diff --git a/backend/tss_api/src/index.ts b/backend/tss_api/src/index.ts index 9bf9b1b68..ccc87e2f0 100644 --- a/backend/tss_api/src/index.ts +++ b/backend/tss_api/src/index.ts @@ -1 +1,2 @@ export * from "./routes"; +export * from "./utils/slack"; diff --git a/backend/tss_api/src/routes/index.ts b/backend/tss_api/src/routes/index.ts index 07149b6e5..f85533984 100644 --- a/backend/tss_api/src/routes/index.ts +++ b/backend/tss_api/src/routes/index.ts @@ -6,6 +6,7 @@ import { setPresignRoutes } from "./presign"; import { setSignRoutes } from "./sign"; import { setUserRoutes } from "./user"; import { setTssSessionRoutes } from "./tss_session"; +import { setKSNodeTelemetryRoutes } from "./ks_node_telemetry"; export function makeTssRouter() { const router = express.Router(); @@ -16,6 +17,7 @@ export function makeTssRouter() { setSignRoutes(router); setUserRoutes(router); setTssSessionRoutes(router); + setKSNodeTelemetryRoutes(router); return router; } diff --git a/backend/tss_api/src/routes/ks_node_telemetry.ts b/backend/tss_api/src/routes/ks_node_telemetry.ts new file mode 100644 index 000000000..fb4555e6f --- /dev/null +++ b/backend/tss_api/src/routes/ks_node_telemetry.ts @@ -0,0 +1,112 @@ +import { Router, type Response } from "express"; +import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; +import type { KSNodeTelemetryRequest } from "@oko-wallet/oko-types/tss"; +import { + KSNodeTelemetryRequestSchema, + KSNodeTelemetryResponseSchema, +} from "@oko-wallet/oko-api-openapi/tss"; +import { registry } from "@oko-wallet/oko-api-openapi"; +import { z } from "zod"; + +import { processKSNodeTelemetry } from "@oko-wallet-tss-api/api/ks_node/telemetry"; + +export function setKSNodeTelemetryRoutes(router: Router) { + registry.registerPath({ + method: "post", + path: "/tss/v1/ks_node/telemetry", + tags: ["TSS"], + summary: "Report Key Share Node Telemetry", + description: + "Allows Key Share Nodes to report their status and statistics (telemetry) to the OKO API.", + request: { + headers: z.object({ + "x-ks-node-password": z.string().openapi({ + description: "Password for authenticating the Key Share Node", + }), + }), + body: { + content: { + "application/json": { + schema: KSNodeTelemetryRequestSchema, + }, + }, + }, + }, + responses: { + 200: { + description: "Telemetry reported successfully", + content: { + "application/json": { + schema: KSNodeTelemetryResponseSchema, + }, + }, + }, + 400: { + description: "Invalid request body", + }, + 401: { + description: "Unauthorized (invalid password)", + }, + 500: { + description: "Internal server error", + }, + }, + }); + router.post( + "/ks_node/telemetry", + async (req, res: Response>) => { + const password = req.headers["x-ks-node-password"]; + if (typeof password !== "string") { + res.status(401).json({ + success: false, + code: "UNAUTHORIZED", + msg: "Missing or invalid password header", + }); + return; + } + + const payload: KSNodeTelemetryRequest = req.body; + if ( + !payload || + !payload.telemetry_node_id || + payload.key_share_count === undefined + ) { + res.status(400).json({ + success: false, + code: "INVALID_REQUEST", + msg: "Missing required fields: telemetry_node_id, key_share_count", + }); + return; + } + + const result = await processKSNodeTelemetry( + req.app.locals.db, + payload, + password, + ); + + if (!result.success) { + if (result.err === "Invalid password") { + res.status(401).json({ + success: false, + code: "UNAUTHORIZED", + msg: "Invalid password", + }); + return; + } + + res.status(500).json({ + success: false, + code: "UNKNOWN_ERROR", + msg: result.err, + }); + return; + } + + res.status(200).json({ + success: true, + data: void 0, + }); + }, + ); +} diff --git a/backend/tss_api/src/utils/slack.ts b/backend/tss_api/src/utils/slack.ts new file mode 100644 index 000000000..2fc2b1d74 --- /dev/null +++ b/backend/tss_api/src/utils/slack.ts @@ -0,0 +1,27 @@ +export async function sendSlackAlert(message: string): Promise { + const webhookUrl = process.env.SLACK_WEBHOOK_URL; + if (!webhookUrl) { + console.warn("SLACK_WEBHOOK_URL is not set. Skipping Slack alert."); + return; + } + + try { + const response = await fetch(webhookUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text: message, + }), + }); + + if (!response.ok) { + console.error( + `Failed to send Slack alert: ${response.status} ${response.statusText}`, + ); + } + } catch (error) { + console.error("Error sending Slack alert:", error); + } +} diff --git a/common/oko_types/src/tss/ks_node.ts b/common/oko_types/src/tss/ks_node.ts index 23add1b98..21d23d01a 100644 --- a/common/oko_types/src/tss/ks_node.ts +++ b/common/oko_types/src/tss/ks_node.ts @@ -72,3 +72,17 @@ export type KSNodeWithHealthCheck = { health_check_status: KSNodeHealthCheckStatus | null; health_checked_at: string | null; }; + +export interface KSNodeTelemetry { + log_id: string; + telemetry_node_id: string; + key_share_count: number; + payload: any; + created_at: Date; +} + +export interface KSNodeTelemetryRequest { + telemetry_node_id: string; + key_share_count: number; + payload: Record; +} diff --git a/key_share_node/ksn_interface/src/status.ts b/key_share_node/ksn_interface/src/status.ts index 1e77ec553..8f925325c 100644 --- a/key_share_node/ksn_interface/src/status.ts +++ b/key_share_node/ksn_interface/src/status.ts @@ -6,4 +6,5 @@ export interface ServerStatus { launch_time: string; git_hash: string | null; version: string; + telemetry_node_id: string | null; } diff --git a/key_share_node/pg_interface/migrations/20251224071629_add_meta.ts b/key_share_node/pg_interface/migrations/20251224071629_add_meta.ts new file mode 100644 index 000000000..ba245ffdb --- /dev/null +++ b/key_share_node/pg_interface/migrations/20251224071629_add_meta.ts @@ -0,0 +1,27 @@ +import type { Knex } from "knex"; + +export async function up(knex: Knex): Promise { + await knex.schema.withSchema("public").createTable("meta", (table) => { + table + .uuid("meta_id") + .notNullable() + .defaultTo(knex.raw("gen_random_uuid()")) + .primary({ constraintName: "meta_pkey" }); + table + .string("telemetry_node_id", 255) + .notNullable() + .unique({ indexName: "meta_telemetry_node_id_key" }); + table + .timestamp("created_at", { useTz: true }) + .notNullable() + .defaultTo(knex.fn.now()); + table + .timestamp("updated_at", { useTz: true }) + .notNullable() + .defaultTo(knex.fn.now()); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.withSchema("public").dropTableIfExists("meta"); +} diff --git a/key_share_node/pg_interface/package.json b/key_share_node/pg_interface/package.json index a3f4ae08d..304fe4b9e 100644 --- a/key_share_node/pg_interface/package.json +++ b/key_share_node/pg_interface/package.json @@ -13,6 +13,7 @@ "migrate_2": "MIGRATE_MODE=one NODE_ID=2 tsx ./src/bin/migrate/index.ts", "db_backup": "tsx ./src/bin/db_backup.ts", "db_restore": "tsx ./src/bin/db_restore.ts", + "schema_make": "tsx ../../node_modules/knex/bin/cli.js migrate:make -x ts", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/key_share_node/pg_interface/src/index.ts b/key_share_node/pg_interface/src/index.ts index 657415e91..d9e0f0d8f 100644 --- a/key_share_node/pg_interface/src/index.ts +++ b/key_share_node/pg_interface/src/index.ts @@ -5,3 +5,4 @@ export * from "./pg_dumps"; export * from "./dump"; export * from "./postgres"; export * from "./server_keypairs"; +export * from "./meta"; diff --git a/key_share_node/pg_interface/src/key_shares/index.ts b/key_share_node/pg_interface/src/key_shares/index.ts index bf60ba078..951e40ba5 100644 --- a/key_share_node/pg_interface/src/key_shares/index.ts +++ b/key_share_node/pg_interface/src/key_shares/index.ts @@ -120,3 +120,15 @@ RETURNING * return { success: false, err: String(error) }; } } + +export async function countKeyShares( + db: Pool | PoolClient, +): Promise> { + try { + const query = `SELECT count(*) FROM "2_key_shares"`; + const result = await db.query<{ count: string }>(query); + return { success: true, data: parseInt(result.rows[0].count, 10) }; + } catch (error) { + return { success: false, err: String(error) }; + } +} diff --git a/key_share_node/pg_interface/src/meta/index.ts b/key_share_node/pg_interface/src/meta/index.ts new file mode 100644 index 000000000..72c8e1b8f --- /dev/null +++ b/key_share_node/pg_interface/src/meta/index.ts @@ -0,0 +1,52 @@ +import type { Pool, PoolClient } from "pg"; +import { v4 as uuidv4 } from "uuid"; +import type { Result } from "@oko-wallet/stdlib-js"; + +export interface NodeMeta { + meta_id: string; + analytics_id: string; + created_at: Date; + updated_at: Date; +} + +export async function getTelemetryId( + db: Pool | PoolClient, +): Promise> { + try { + const query = ` +SELECT telemetry_node_id FROM "meta" +LIMIT 1 +`; + const result = await db.query<{ telemetry_node_id: string }>(query); + + const row = result.rows[0]; + if (!row) { + return { success: true, data: null }; + } + + return { success: true, data: row.telemetry_node_id }; + } catch (error) { + return { success: false, err: String(error) }; + } +} + +export async function setTelemetryId( + db: Pool | PoolClient, + telemetryNodeId: string, +): Promise> { + try { + const query = ` +INSERT INTO "meta" ( + meta_id, telemetry_node_id +) +VALUES ( + $1, $2 +) +`; + await db.query(query, [uuidv4(), telemetryNodeId]); + + return { success: true, data: void 0 }; + } catch (error) { + return { success: false, err: String(error) }; + } +} diff --git a/key_share_node/server/key_share_node.env.example b/key_share_node/server/key_share_node.env.example index 7ebe52d8d..6d22ea110 100644 --- a/key_share_node/server/key_share_node.env.example +++ b/key_share_node/server/key_share_node.env.example @@ -9,3 +9,5 @@ ENCRYPTION_SECRET_PATH="~/.oko/encryption_secret.txt" ADMIN_PASSWORD="admin_password" DUMP_DIR="~/key_share_node/dump" TELEGRAM_BOT_TOKEN="telegram-bot-token" +OKO_API_BASE_URL="http://localhost:4200" +KS_NODE_REPORT_PASSWORD="ks-node-report-password" diff --git a/key_share_node/server/key_share_node_2.env.example b/key_share_node/server/key_share_node_2.env.example index 3f21b296d..6a4473db7 100644 --- a/key_share_node/server/key_share_node_2.env.example +++ b/key_share_node/server/key_share_node_2.env.example @@ -9,3 +9,5 @@ ENCRYPTION_SECRET_PATH="~/.oko/encryption_secret.txt" ADMIN_PASSWORD="admin_password" DUMP_DIR="~/key_share_node/dump" TELEGRAM_BOT_TOKEN="telegram-bot-token" +OKO_API_BASE_URL="http://localhost:4200" +KS_NODE_REPORT_PASSWORD="ks-node-report-password" diff --git a/key_share_node/server/key_share_node_3.env.example b/key_share_node/server/key_share_node_3.env.example index 91d301eb2..5299c27e8 100644 --- a/key_share_node/server/key_share_node_3.env.example +++ b/key_share_node/server/key_share_node_3.env.example @@ -9,3 +9,5 @@ ENCRYPTION_SECRET_PATH="~/.oko/encryption_secret.txt" ADMIN_PASSWORD="admin_password" DUMP_DIR="~/key_share_node/dump" TELEGRAM_BOT_TOKEN="telegram-bot-token" +OKO_API_BASE_URL="http://localhost:4200" +KS_NODE_REPORT_PASSWORD="ks-node-report-password" diff --git a/key_share_node/server/src/bin/launch/index.ts b/key_share_node/server/src/bin/launch/index.ts index cdea28e77..7971f9cd2 100644 --- a/key_share_node/server/src/bin/launch/index.ts +++ b/key_share_node/server/src/bin/launch/index.ts @@ -7,6 +7,7 @@ import { connectPG } from "@oko-wallet-ksn-server/database"; import { makeApp } from "@oko-wallet-ksn-server/app"; import { loadEnv, verifyEnv } from "@oko-wallet-ksn-server/envs"; import { startPgDumpRuntime } from "@oko-wallet-ksn-server/pg_dump/runtime"; +import { startTelemetryReporterRuntime } from "@oko-wallet-ksn-server/runtime/telemetry_reporter"; import { loadEncSecret } from "./load_enc_secret"; import { checkDBBackup } from "./check_db_backup"; import { parseCLIArgs } from "./cli_args"; @@ -148,6 +149,13 @@ async function main() { }, ); + startTelemetryReporterRuntime( + app.locals.db, + process.env.OKO_API_BASE_URL!, + process.env.KS_NODE_REPORT_PASSWORD!, + 180, + ); + app.listen(process.env.PORT, () => { logger.info("Start server, listening on port: %s", process.env.PORT); }); diff --git a/key_share_node/server/src/envs.ts b/key_share_node/server/src/envs.ts index 09701e6fa..5817e6047 100644 --- a/key_share_node/server/src/envs.ts +++ b/key_share_node/server/src/envs.ts @@ -30,6 +30,8 @@ interface Env { ADMIN_PASSWORD: string; DUMP_DIR: string; TELEGRAM_BOT_TOKEN: string; + OKO_API_BASE_URL: string; + KS_NODE_REPORT_PASSWORD: string; } const envSchema = z.object({ @@ -44,6 +46,8 @@ const envSchema = z.object({ ADMIN_PASSWORD: z.string(), DUMP_DIR: z.string(), TELEGRAM_BOT_TOKEN: z.string(), + OKO_API_BASE_URL: z.string(), + KS_NODE_REPORT_PASSWORD: z.string(), }); export function loadEnv(nodeId: string): Result { diff --git a/key_share_node/server/src/routes/status.ts b/key_share_node/server/src/routes/status.ts index a75b8cc4c..6f3f45174 100644 --- a/key_share_node/server/src/routes/status.ts +++ b/key_share_node/server/src/routes/status.ts @@ -1,6 +1,9 @@ import type { Express, Response } from "express"; import type { ServerStatus } from "@oko-wallet/ksn-interface/status"; -import { getLatestCompletedPgDump } from "@oko-wallet/ksn-pg-interface"; +import { + getLatestCompletedPgDump, + getTelemetryId, +} from "@oko-wallet/ksn-pg-interface"; import dayjs from "dayjs"; import { logger } from "@oko-wallet-ksn-server/logger"; @@ -28,12 +31,24 @@ export function addStatusRoutes(app: Express) { ).toISOString(); } } else { - console.error("Failed to get latest dump:", getLatestDumpRes.err); + logger.error("Failed to get latest dump:", getLatestDumpRes.err); } } catch (err: any) { logger.error("Get latest pg dump, err: %s", err); } + let telemetryNodeId: string | null = null; + try { + const getTelemetryIdRes = await getTelemetryId(db); + if (getTelemetryIdRes.success) { + telemetryNodeId = getTelemetryIdRes.data; + } else { + logger.error("Failed to get telemetry id: %s", getTelemetryIdRes.err); + } + } catch (err) { + logger.error("Get telemetry id error: %s", err); + } + const status: ServerStatus = { is_db_connected: isDbConnected, is_db_backup_checked: state.is_db_backup_checked, @@ -42,6 +57,7 @@ export function addStatusRoutes(app: Express) { launch_time: state.launch_time, git_hash: state.git_hash, version: state.version, + telemetry_node_id: telemetryNodeId, }; res.status(200).json(status); diff --git a/key_share_node/server/src/runtime/telemetry_reporter.ts b/key_share_node/server/src/runtime/telemetry_reporter.ts new file mode 100644 index 000000000..ffef2f8e5 --- /dev/null +++ b/key_share_node/server/src/runtime/telemetry_reporter.ts @@ -0,0 +1,89 @@ +import { + getTelemetryId, + setTelemetryId, + countKeyShares, +} from "@oko-wallet/ksn-pg-interface"; +import { v4 as uuidv4 } from "uuid"; +import type { Pool } from "pg"; + +import { logger } from "@oko-wallet-ksn-server/logger"; + +export async function startTelemetryReporterRuntime( + db: Pool, + okoApiBaseUrl: string, + reportPassword: string, + intervalSeconds: number, +) { + logger.info( + "Starting Telemetry Reporter Runtime, interval: %ds", + intervalSeconds, + ); + + const run = async () => { + try { + await reportTelemetry(db, okoApiBaseUrl, reportPassword); + } catch (err) { + logger.error("Telemetry reporter runtime error: %s", err); + } + }; + + // Initial run + run(); + + setInterval(run, intervalSeconds * 1000); +} + +async function reportTelemetry(db: Pool, baseUrl: string, password: string) { + // 1. Get or Create Telemetry ID + let telemetryIdRes = await getTelemetryId(db); + if (!telemetryIdRes.success) { + logger.error("Failed to get telemetry ID: %s", telemetryIdRes.err); + return; + } + + let telemetryNodeId = telemetryIdRes.data; + if (!telemetryNodeId) { + telemetryNodeId = uuidv4(); + logger.info("Generating new telemetry ID: %s", telemetryNodeId); + const setRes = await setTelemetryId(db, telemetryNodeId); + if (!setRes.success) { + logger.error("Failed to set telemetry ID: %s", setRes.err); + return; + } + } + + // 2. Count Key Shares + const countRes = await countKeyShares(db); + const keyShareCount = countRes.success ? countRes.data : -1; + const payload = { + db_status: countRes.success ? "OK" : "ERROR", + error_msg: countRes.success ? null : countRes.err, + }; + + // 3. Send Report + const url = `${baseUrl}/tss/v1/ks_node/telemetry`; + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-KS-NODE-PASSWORD": password, + }, + body: JSON.stringify({ + telemetry_node_id: telemetryNodeId, + key_share_count: keyShareCount, + payload: payload, + }), + }); + + if (!response.ok) { + logger.error( + "Failed to report telemetry to OKO API: %s %s", + response.status, + response.statusText, + ); + } + } catch (error) { + logger.error("Error reporting telemetry: %s", error); + } +} From 5706a8555007586fb64d9ee918d7bfbaa69a80f8 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Wed, 24 Dec 2025 23:09:41 +0900 Subject: [PATCH 02/10] ks_node: fix migrate.sql --- .../pg_interface/src/bin/migrate/migrate.sql | 18 +++++++++--------- .../pg_interface/src/postgres/index.ts | 5 ++--- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/key_share_node/pg_interface/src/bin/migrate/migrate.sql b/key_share_node/pg_interface/src/bin/migrate/migrate.sql index 285c5764a..669ca90b3 100644 --- a/key_share_node/pg_interface/src/bin/migrate/migrate.sql +++ b/key_share_node/pg_interface/src/bin/migrate/migrate.sql @@ -13,8 +13,8 @@ CREATE TABLE public."2_key_shares" ( created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL, aux jsonb NULL, - CONSTRAINT 2_key_shares_pkey PRIMARY KEY (share_id), - CONSTRAINT 2_key_shares_unique UNIQUE (wallet_id) + CONSTRAINT "2_key_shares_pkey" PRIMARY KEY (share_id), + CONSTRAINT "2_key_shares_unique" UNIQUE (wallet_id) ); @@ -31,7 +31,7 @@ CREATE TABLE public."2_pg_dumps" ( meta jsonb NULL, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL, - CONSTRAINT 2_pg_dumps_pkey PRIMARY KEY (dump_id) + CONSTRAINT "2_pg_dumps_pkey" PRIMARY KEY (dump_id) ); @@ -49,8 +49,8 @@ CREATE TABLE public."2_users" ( created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL, aux jsonb NULL, - CONSTRAINT 2_users_pkey PRIMARY KEY (user_id), - CONSTRAINT 2_users_auth_type_user_auth_id_key UNIQUE (auth_type, user_auth_id) + CONSTRAINT "2_users_pkey" PRIMARY KEY (user_id), + CONSTRAINT "2_users_auth_type_user_auth_id_key" UNIQUE (auth_type, user_auth_id) ); @@ -68,8 +68,8 @@ CREATE TABLE public."2_wallets" ( created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL, aux jsonb NULL, - CONSTRAINT 2_wallets_pkey PRIMARY KEY (wallet_id), - CONSTRAINT 2_wallets_public_key_key UNIQUE (public_key) + CONSTRAINT "2_wallets_pkey" PRIMARY KEY (wallet_id), + CONSTRAINT "2_wallets_public_key_key" UNIQUE (public_key) ); @@ -88,7 +88,7 @@ CREATE TABLE public."2_server_keypairs" ( created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL, rotated_at timestamptz NULL, - CONSTRAINT 2_server_keypairs_pkey PRIMARY KEY (keypair_id), - CONSTRAINT 2_server_keypairs_version_key UNIQUE (version) + CONSTRAINT "2_server_keypairs_pkey" PRIMARY KEY (keypair_id), + CONSTRAINT "2_server_keypairs_version_key" UNIQUE (version) ); CREATE INDEX idx_2_server_keypairs_is_active ON public."2_server_keypairs" USING btree (is_active) WHERE (is_active = true); diff --git a/key_share_node/pg_interface/src/postgres/index.ts b/key_share_node/pg_interface/src/postgres/index.ts index 09d2f969c..a10cfa352 100644 --- a/key_share_node/pg_interface/src/postgres/index.ts +++ b/key_share_node/pg_interface/src/postgres/index.ts @@ -18,10 +18,9 @@ WHERE table_schema='public' console.log("Existing tables: %j", tableNames); for (let idx = 0; idx < tableNames.length; idx += 1) { + const tableName = `"${tableNames[idx]}"`; await pool.query<{ table_name: string }>( - ` -DROP TABLE IF EXISTS ${tableNames[idx]} CASCADE -`, + `DROP TABLE IF EXISTS ${tableName} CASCADE`, [], ); } From 5f04866fa92c8554b6f2782c63420eaa2b710c3c Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 29 Dec 2025 14:25:49 +0900 Subject: [PATCH 03/10] ks_node: add telemetry reporting configuration to docker compose --- key_share_node/docker/docker-compose.yml | 2 ++ key_share_node/docker/env.example | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/key_share_node/docker/docker-compose.yml b/key_share_node/docker/docker-compose.yml index 9ed504d03..4e3b61f1f 100644 --- a/key_share_node/docker/docker-compose.yml +++ b/key_share_node/docker/docker-compose.yml @@ -36,6 +36,8 @@ services: # Container path is fixed for consistency; you may change the host path in the volume mapping DUMP_DIR: /home/node/key_share_node/dump TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN} + OKO_API_BASE_URL: ${OKO_API_BASE_URL} + KS_NODE_REPORT_PASSWORD: ${KS_NODE_REPORT_PASSWORD} volumes: - ${DUMP_DIR}:/home/node/key_share_node/dump diff --git a/key_share_node/docker/env.example b/key_share_node/docker/env.example index 15deaf069..070d86941 100644 --- a/key_share_node/docker/env.example +++ b/key_share_node/docker/env.example @@ -28,4 +28,7 @@ ADMIN_PASSWORD=admin_password ENCRYPTION_SECRET_FILE_PATH=/opt/key_share_node/encryption_secret.txt ## Telegram bot token for Telegram login authentication TELEGRAM_BOT_TOKEN=telegram_bot_token - +## Base URL of the OKO API server for telemetry reporting +OKO_API_BASE_URL="http://localhost:4200" +## Password for authenticating telemetry reports sent to OKO API +KS_NODE_REPORT_PASSWORD="ks-node-report-password" From 6fe3da45dc00ab737b20a11d782bceeb8afb2b36 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 29 Dec 2025 14:42:51 +0900 Subject: [PATCH 04/10] ks_node: update docs --- documentation/key_share_node.md | 6 ++++++ key_share_node/docker/env.example | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/documentation/key_share_node.md b/documentation/key_share_node.md index 8d5d7cae8..f73bdfcc1 100644 --- a/documentation/key_share_node.md +++ b/documentation/key_share_node.md @@ -89,6 +89,12 @@ SERVER_PORT=4201 ADMIN_PASSWORD=admin_password ## Host file path to encryption secret file (used to create docker secret) ENCRYPTION_SECRET_FILE_PATH=/opt/key_share_node/encryption_secret.txt +## Telegram bot token for Telegram login authentication +TELEGRAM_BOT_TOKEN=telegram_bot_token +## Base URL of the Oko API server for telemetry reporting +OKO_API_BASE_URL="http://localhost:4200" +## Password for authenticating telemetry reports sent to Oko API +KS_NODE_REPORT_PASSWORD="ks-node-report-password" ``` ### Starting the Services diff --git a/key_share_node/docker/env.example b/key_share_node/docker/env.example index 070d86941..984beb779 100644 --- a/key_share_node/docker/env.example +++ b/key_share_node/docker/env.example @@ -28,7 +28,7 @@ ADMIN_PASSWORD=admin_password ENCRYPTION_SECRET_FILE_PATH=/opt/key_share_node/encryption_secret.txt ## Telegram bot token for Telegram login authentication TELEGRAM_BOT_TOKEN=telegram_bot_token -## Base URL of the OKO API server for telemetry reporting +## Base URL of the Oko API server for telemetry reporting OKO_API_BASE_URL="http://localhost:4200" -## Password for authenticating telemetry reports sent to OKO API +## Password for authenticating telemetry reports sent to Oko API KS_NODE_REPORT_PASSWORD="ks-node-report-password" From 8cb84be7fcf77ab9e019b033ec74d03e5af47852 Mon Sep 17 00:00:00 2001 From: lidarbtc Date: Mon, 29 Dec 2025 14:51:17 +0900 Subject: [PATCH 05/10] oko_api, key_share_node: change telemetry_node_id to ks node pubkey --- .../server/src/runtime/ks_node_monitor.ts | 10 ++-- .../20251224070753_add_ks_node_telemetry.ts | 16 +++--- .../oko_pg_interface/src/ks_nodes/index.ts | 38 +++++++------- backend/openapi/src/tss/ks_node.ts | 5 +- backend/tss_api/src/api/ks_node/telemetry.ts | 39 +++++++++----- .../tss_api/src/routes/ks_node_telemetry.ts | 4 +- common/oko_types/src/tss/ks_node.ts | 4 +- key_share_node/ksn_interface/src/status.ts | 1 - .../migrations/20251224071629_add_meta.ts | 27 ---------- key_share_node/pg_interface/src/index.ts | 1 - key_share_node/pg_interface/src/meta/index.ts | 52 ------------------- key_share_node/server/src/bin/launch/index.ts | 1 + key_share_node/server/src/routes/status.ts | 18 +------ .../server/src/runtime/telemetry_reporter.ts | 41 +++++---------- 14 files changed, 79 insertions(+), 178 deletions(-) delete mode 100644 key_share_node/pg_interface/migrations/20251224071629_add_meta.ts delete mode 100644 key_share_node/pg_interface/src/meta/index.ts diff --git a/backend/oko_api/server/src/runtime/ks_node_monitor.ts b/backend/oko_api/server/src/runtime/ks_node_monitor.ts index 66286c9fa..54c38b38a 100644 --- a/backend/oko_api/server/src/runtime/ks_node_monitor.ts +++ b/backend/oko_api/server/src/runtime/ks_node_monitor.ts @@ -2,7 +2,7 @@ import type { Pool } from "pg"; import type { Logger } from "winston"; import { getLatestKSNodeTelemetries, - getKSNodeByTelemetryId, + getKSNodeByPublicKey, } from "@oko-wallet/oko-pg-interface/ks_nodes"; import { sendSlackAlert } from "@oko-wallet/tss-api"; import dayjs from "dayjs"; @@ -46,14 +46,14 @@ async function checkKSNodeHeartbeats(db: Pool, logger: Logger) { if (lastUpdate.isBefore(threshold)) { // Node is unresponsive - const telemetryNodeId = telemetry.telemetry_node_id; + const publicKey = telemetry.public_key; // Get node name - const nodeRes = await getKSNodeByTelemetryId(db, telemetryNodeId); + const nodeRes = await getKSNodeByPublicKey(db, publicKey); const nodeName = nodeRes.success && nodeRes.data - ? `${nodeRes.data.node_name} (${telemetryNodeId})` - : telemetryNodeId; + ? `${nodeRes.data.node_name} (${publicKey})` + : publicKey; await sendSlackAlert( `[KS Node Alert] Node ${nodeName} has not reported telemetry for over ${HEARTBEAT_THRESHOLD_MINUTES} minutes. Last seen: ${lastUpdate.toISOString()}`, diff --git a/backend/oko_pg_interface/migrations/20251224070753_add_ks_node_telemetry.ts b/backend/oko_pg_interface/migrations/20251224070753_add_ks_node_telemetry.ts index b99e69243..3df7d736a 100644 --- a/backend/oko_pg_interface/migrations/20251224070753_add_ks_node_telemetry.ts +++ b/backend/oko_pg_interface/migrations/20251224070753_add_ks_node_telemetry.ts @@ -4,7 +4,7 @@ export async function up(knex: Knex): Promise { await knex.schema .withSchema("public") .alterTable("key_share_nodes", (table) => { - table.string("telemetry_node_id", 255).unique(); + table.string("public_key", 255).unique(); }); await knex.schema @@ -15,7 +15,12 @@ export async function up(knex: Knex): Promise { .notNullable() .defaultTo(knex.raw("gen_random_uuid()")) .primary({ constraintName: "ks_node_telemetry_pkey" }); - table.string("telemetry_node_id", 255).notNullable(); + table + .string("public_key", 255) + .notNullable() + .references("public_key") + .inTable("key_share_nodes") + .onDelete("CASCADE"); table.integer("key_share_count").notNullable(); table.jsonb("payload").notNullable(); table @@ -23,10 +28,7 @@ export async function up(knex: Knex): Promise { .notNullable() .defaultTo(knex.fn.now()); - table.index( - ["telemetry_node_id"], - "idx_ks_node_telemetry_telemetry_node_id", - ); + table.index(["public_key"], "idx_ks_node_telemetry_public_key"); table.index(["created_at"], "idx_ks_node_telemetry_created_at"); }); } @@ -37,6 +39,6 @@ export async function down(knex: Knex): Promise { await knex.schema .withSchema("public") .alterTable("key_share_nodes", (table) => { - table.dropColumn("telemetry_node_id"); + table.dropColumn("public_key"); }); } diff --git a/backend/oko_pg_interface/src/ks_nodes/index.ts b/backend/oko_pg_interface/src/ks_nodes/index.ts index 876db2816..4916b5428 100644 --- a/backend/oko_pg_interface/src/ks_nodes/index.ts +++ b/backend/oko_pg_interface/src/ks_nodes/index.ts @@ -14,13 +14,13 @@ import type { WithPagination, WithTime } from "@oko-wallet-types/aux_types"; export async function insertKSNodeTelemetry( db: Pool | PoolClient, - telemetryNodeId: string, + public_key: string, key_share_count: number, payload: any, ): Promise> { const query = ` INSERT INTO ks_node_telemetry ( - log_id, telemetry_node_id, key_share_count, payload + log_id, public_key, key_share_count, payload ) VALUES ( $1, $2, $3, $4 @@ -28,12 +28,7 @@ VALUES ( `; try { - await db.query(query, [ - uuidv4(), - telemetryNodeId, - key_share_count, - payload, - ]); + await db.query(query, [uuidv4(), public_key, key_share_count, payload]); return { success: true, @@ -49,18 +44,18 @@ VALUES ( export async function getLastKSNodeTelemetry( db: Pool | PoolClient, - telemetryNodeId: string, + public_key: string, ): Promise> { const query = ` SELECT * FROM ks_node_telemetry -WHERE telemetry_node_id = $1 +WHERE public_key = $1 ORDER BY created_at DESC LIMIT 1 `; try { - const result = await db.query(query, [telemetryNodeId]); + const result = await db.query(query, [public_key]); const row = result.rows[0]; if (!row) { @@ -71,7 +66,7 @@ LIMIT 1 success: true, data: { log_id: row.log_id, - telemetry_node_id: row.telemetry_node_id, + public_key: row.public_key, key_share_count: row.key_share_count, payload: row.payload, created_at: row.created_at, @@ -87,18 +82,21 @@ LIMIT 1 export async function getLatestKSNodeTelemetries( db: Pool | PoolClient, -): Promise> { +): Promise> { const query = ` -SELECT DISTINCT ON (telemetry_node_id) telemetry_node_id, created_at +SELECT DISTINCT ON (public_key) public_key, created_at FROM ks_node_telemetry -ORDER BY telemetry_node_id, created_at DESC +ORDER BY public_key, created_at DESC `; try { const result = await db.query(query); return { success: true, - data: result.rows, + data: result.rows.map((row) => ({ + public_key: row.public_key, + created_at: row.created_at, + })), }; } catch (error) { return { @@ -108,18 +106,18 @@ ORDER BY telemetry_node_id, created_at DESC } } -export async function getKSNodeByTelemetryId( +export async function getKSNodeByPublicKey( db: Pool | PoolClient, - telemetryNodeId: string, + public_key: string, ): Promise> { const query = ` SELECT * FROM key_share_nodes -WHERE telemetry_node_id = $1 AND deleted_at IS NULL +WHERE public_key = $1 AND deleted_at IS NULL `; try { - const result = await db.query(query, [telemetryNodeId]); + const result = await db.query(query, [public_key]); return { success: true, data: result.rows[0] || null, diff --git a/backend/openapi/src/tss/ks_node.ts b/backend/openapi/src/tss/ks_node.ts index 0313bdc0a..2451104d2 100644 --- a/backend/openapi/src/tss/ks_node.ts +++ b/backend/openapi/src/tss/ks_node.ts @@ -5,8 +5,9 @@ import { registry } from "../registry"; export const KSNodeTelemetryRequestSchema = registry.register( "TssKSNodeTelemetryRequest", z.object({ - telemetry_node_id: z.string().openapi({ - description: "Unique identifier for the key share node telemetry", + public_key: z.string().openapi({ + description: + "Unique identifier for the key share node telemetry (public key)", }), key_share_count: z.number().openapi({ description: "Current number of key shares stored in the node", diff --git a/backend/tss_api/src/api/ks_node/telemetry.ts b/backend/tss_api/src/api/ks_node/telemetry.ts index 145bea04a..b9ddd4732 100644 --- a/backend/tss_api/src/api/ks_node/telemetry.ts +++ b/backend/tss_api/src/api/ks_node/telemetry.ts @@ -3,13 +3,13 @@ import type { Result } from "@oko-wallet/stdlib-js"; import { insertKSNodeTelemetry, getLastKSNodeTelemetry, - getKSNodeByTelemetryId, + getKSNodeByPublicKey, } from "@oko-wallet/oko-pg-interface/ks_nodes"; import { sendSlackAlert } from "@oko-wallet-tss-api/utils/slack"; export interface KSNodeTelemetryPayload { - telemetry_node_id: string; + public_key: string; key_share_count: number; payload: any; } @@ -27,13 +27,30 @@ export async function processKSNodeTelemetry( }; } - const { telemetry_node_id, key_share_count, payload } = input; + const { public_key, key_share_count, payload } = input; - // 2. Get previous telemetry for comparison - const lastTelemetryRes = await getLastKSNodeTelemetry(db, telemetry_node_id); + // 2. Check if node exists + const nodeRes = await getKSNodeByPublicKey(db, public_key); + if (!nodeRes.success) { + await sendSlackAlert( + `[TSS API Error] Failed to check if node exists ${public_key}: ${nodeRes.err}`, + ); + return { success: false, err: nodeRes.err }; + } + + if (!nodeRes.data) { + // Node not registered, ignore telemetry + console.warn( + `[KS Node Telemetry] Ignored telemetry from unregistered node: ${public_key}`, + ); + return { success: true, data: void 0 }; + } + + // 3. Get previous telemetry for comparison + const lastTelemetryRes = await getLastKSNodeTelemetry(db, public_key); if (!lastTelemetryRes.success) { await sendSlackAlert( - `[TSS API Error] Failed to get last telemetry for node ${telemetry_node_id}: ${lastTelemetryRes.err}`, + `[TSS API Error] Failed to get last telemetry for node ${public_key}: ${lastTelemetryRes.err}`, ); return { success: false, err: lastTelemetryRes.err }; } @@ -43,23 +60,19 @@ export async function processKSNodeTelemetry( // 3. Insert new telemetry const insertRes = await insertKSNodeTelemetry( db, - telemetry_node_id, + public_key, key_share_count, payload, ); if (!insertRes.success) { await sendSlackAlert( - `[TSS API Error] Failed to insert telemetry for node ${telemetry_node_id}: ${insertRes.err}`, + `[TSS API Error] Failed to insert telemetry for node ${public_key}: ${insertRes.err}`, ); return { success: false, err: insertRes.err }; } // 4. Check for anomalies - const nodeRes = await getKSNodeByTelemetryId(db, telemetry_node_id); - const nodeName = - nodeRes.success && nodeRes.data - ? `${nodeRes.data.node_name} (${telemetry_node_id})` - : telemetry_node_id; + const nodeName = `${nodeRes.data!.node_name} (${public_key})`; if (key_share_count === 0) { await sendSlackAlert( diff --git a/backend/tss_api/src/routes/ks_node_telemetry.ts b/backend/tss_api/src/routes/ks_node_telemetry.ts index fb4555e6f..9346437ab 100644 --- a/backend/tss_api/src/routes/ks_node_telemetry.ts +++ b/backend/tss_api/src/routes/ks_node_telemetry.ts @@ -68,13 +68,13 @@ export function setKSNodeTelemetryRoutes(router: Router) { const payload: KSNodeTelemetryRequest = req.body; if ( !payload || - !payload.telemetry_node_id || + !payload.public_key || payload.key_share_count === undefined ) { res.status(400).json({ success: false, code: "INVALID_REQUEST", - msg: "Missing required fields: telemetry_node_id, key_share_count", + msg: "Missing required fields: public_key, key_share_count", }); return; } diff --git a/common/oko_types/src/tss/ks_node.ts b/common/oko_types/src/tss/ks_node.ts index 21d23d01a..13620476e 100644 --- a/common/oko_types/src/tss/ks_node.ts +++ b/common/oko_types/src/tss/ks_node.ts @@ -75,14 +75,14 @@ export type KSNodeWithHealthCheck = { export interface KSNodeTelemetry { log_id: string; - telemetry_node_id: string; + public_key: string; key_share_count: number; payload: any; created_at: Date; } export interface KSNodeTelemetryRequest { - telemetry_node_id: string; + public_key: string; key_share_count: number; payload: Record; } diff --git a/key_share_node/ksn_interface/src/status.ts b/key_share_node/ksn_interface/src/status.ts index 8f925325c..1e77ec553 100644 --- a/key_share_node/ksn_interface/src/status.ts +++ b/key_share_node/ksn_interface/src/status.ts @@ -6,5 +6,4 @@ export interface ServerStatus { launch_time: string; git_hash: string | null; version: string; - telemetry_node_id: string | null; } diff --git a/key_share_node/pg_interface/migrations/20251224071629_add_meta.ts b/key_share_node/pg_interface/migrations/20251224071629_add_meta.ts deleted file mode 100644 index ba245ffdb..000000000 --- a/key_share_node/pg_interface/migrations/20251224071629_add_meta.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - await knex.schema.withSchema("public").createTable("meta", (table) => { - table - .uuid("meta_id") - .notNullable() - .defaultTo(knex.raw("gen_random_uuid()")) - .primary({ constraintName: "meta_pkey" }); - table - .string("telemetry_node_id", 255) - .notNullable() - .unique({ indexName: "meta_telemetry_node_id_key" }); - table - .timestamp("created_at", { useTz: true }) - .notNullable() - .defaultTo(knex.fn.now()); - table - .timestamp("updated_at", { useTz: true }) - .notNullable() - .defaultTo(knex.fn.now()); - }); -} - -export async function down(knex: Knex): Promise { - await knex.schema.withSchema("public").dropTableIfExists("meta"); -} diff --git a/key_share_node/pg_interface/src/index.ts b/key_share_node/pg_interface/src/index.ts index d9e0f0d8f..657415e91 100644 --- a/key_share_node/pg_interface/src/index.ts +++ b/key_share_node/pg_interface/src/index.ts @@ -5,4 +5,3 @@ export * from "./pg_dumps"; export * from "./dump"; export * from "./postgres"; export * from "./server_keypairs"; -export * from "./meta"; diff --git a/key_share_node/pg_interface/src/meta/index.ts b/key_share_node/pg_interface/src/meta/index.ts deleted file mode 100644 index 72c8e1b8f..000000000 --- a/key_share_node/pg_interface/src/meta/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Pool, PoolClient } from "pg"; -import { v4 as uuidv4 } from "uuid"; -import type { Result } from "@oko-wallet/stdlib-js"; - -export interface NodeMeta { - meta_id: string; - analytics_id: string; - created_at: Date; - updated_at: Date; -} - -export async function getTelemetryId( - db: Pool | PoolClient, -): Promise> { - try { - const query = ` -SELECT telemetry_node_id FROM "meta" -LIMIT 1 -`; - const result = await db.query<{ telemetry_node_id: string }>(query); - - const row = result.rows[0]; - if (!row) { - return { success: true, data: null }; - } - - return { success: true, data: row.telemetry_node_id }; - } catch (error) { - return { success: false, err: String(error) }; - } -} - -export async function setTelemetryId( - db: Pool | PoolClient, - telemetryNodeId: string, -): Promise> { - try { - const query = ` -INSERT INTO "meta" ( - meta_id, telemetry_node_id -) -VALUES ( - $1, $2 -) -`; - await db.query(query, [uuidv4(), telemetryNodeId]); - - return { success: true, data: void 0 }; - } catch (error) { - return { success: false, err: String(error) }; - } -} diff --git a/key_share_node/server/src/bin/launch/index.ts b/key_share_node/server/src/bin/launch/index.ts index 7971f9cd2..a8d0c4606 100644 --- a/key_share_node/server/src/bin/launch/index.ts +++ b/key_share_node/server/src/bin/launch/index.ts @@ -151,6 +151,7 @@ async function main() { startTelemetryReporterRuntime( app.locals.db, + serverKeypair.publicKey.toHex(), process.env.OKO_API_BASE_URL!, process.env.KS_NODE_REPORT_PASSWORD!, 180, diff --git a/key_share_node/server/src/routes/status.ts b/key_share_node/server/src/routes/status.ts index 6f3f45174..46cdf6c8d 100644 --- a/key_share_node/server/src/routes/status.ts +++ b/key_share_node/server/src/routes/status.ts @@ -1,9 +1,6 @@ import type { Express, Response } from "express"; import type { ServerStatus } from "@oko-wallet/ksn-interface/status"; -import { - getLatestCompletedPgDump, - getTelemetryId, -} from "@oko-wallet/ksn-pg-interface"; +import { getLatestCompletedPgDump } from "@oko-wallet/ksn-pg-interface"; import dayjs from "dayjs"; import { logger } from "@oko-wallet-ksn-server/logger"; @@ -37,18 +34,6 @@ export function addStatusRoutes(app: Express) { logger.error("Get latest pg dump, err: %s", err); } - let telemetryNodeId: string | null = null; - try { - const getTelemetryIdRes = await getTelemetryId(db); - if (getTelemetryIdRes.success) { - telemetryNodeId = getTelemetryIdRes.data; - } else { - logger.error("Failed to get telemetry id: %s", getTelemetryIdRes.err); - } - } catch (err) { - logger.error("Get telemetry id error: %s", err); - } - const status: ServerStatus = { is_db_connected: isDbConnected, is_db_backup_checked: state.is_db_backup_checked, @@ -57,7 +42,6 @@ export function addStatusRoutes(app: Express) { launch_time: state.launch_time, git_hash: state.git_hash, version: state.version, - telemetry_node_id: telemetryNodeId, }; res.status(200).json(status); diff --git a/key_share_node/server/src/runtime/telemetry_reporter.ts b/key_share_node/server/src/runtime/telemetry_reporter.ts index ffef2f8e5..56889a8e0 100644 --- a/key_share_node/server/src/runtime/telemetry_reporter.ts +++ b/key_share_node/server/src/runtime/telemetry_reporter.ts @@ -1,15 +1,11 @@ -import { - getTelemetryId, - setTelemetryId, - countKeyShares, -} from "@oko-wallet/ksn-pg-interface"; -import { v4 as uuidv4 } from "uuid"; +import { countKeyShares } from "@oko-wallet/ksn-pg-interface"; import type { Pool } from "pg"; import { logger } from "@oko-wallet-ksn-server/logger"; export async function startTelemetryReporterRuntime( db: Pool, + publicKey: string, okoApiBaseUrl: string, reportPassword: string, intervalSeconds: number, @@ -21,7 +17,7 @@ export async function startTelemetryReporterRuntime( const run = async () => { try { - await reportTelemetry(db, okoApiBaseUrl, reportPassword); + await reportTelemetry(db, publicKey, okoApiBaseUrl, reportPassword); } catch (err) { logger.error("Telemetry reporter runtime error: %s", err); } @@ -33,26 +29,13 @@ export async function startTelemetryReporterRuntime( setInterval(run, intervalSeconds * 1000); } -async function reportTelemetry(db: Pool, baseUrl: string, password: string) { - // 1. Get or Create Telemetry ID - let telemetryIdRes = await getTelemetryId(db); - if (!telemetryIdRes.success) { - logger.error("Failed to get telemetry ID: %s", telemetryIdRes.err); - return; - } - - let telemetryNodeId = telemetryIdRes.data; - if (!telemetryNodeId) { - telemetryNodeId = uuidv4(); - logger.info("Generating new telemetry ID: %s", telemetryNodeId); - const setRes = await setTelemetryId(db, telemetryNodeId); - if (!setRes.success) { - logger.error("Failed to set telemetry ID: %s", setRes.err); - return; - } - } - - // 2. Count Key Shares +async function reportTelemetry( + db: Pool, + publicKey: string, + baseUrl: string, + password: string, +) { + // 1. Count Key Shares const countRes = await countKeyShares(db); const keyShareCount = countRes.success ? countRes.data : -1; const payload = { @@ -60,7 +43,7 @@ async function reportTelemetry(db: Pool, baseUrl: string, password: string) { error_msg: countRes.success ? null : countRes.err, }; - // 3. Send Report + // 2. Send Report const url = `${baseUrl}/tss/v1/ks_node/telemetry`; try { const response = await fetch(url, { @@ -70,7 +53,7 @@ async function reportTelemetry(db: Pool, baseUrl: string, password: string) { "X-KS-NODE-PASSWORD": password, }, body: JSON.stringify({ - telemetry_node_id: telemetryNodeId, + public_key: publicKey, key_share_count: keyShareCount, payload: payload, }), From b0682be623c804b0da76d79fb8d4152a7dc75035 Mon Sep 17 00:00:00 2001 From: lidarbtc Date: Mon, 29 Dec 2025 15:28:18 +0900 Subject: [PATCH 06/10] tss_api: Stop sending alert for zero key count (only for decreased) --- backend/tss_api/src/api/ks_node/telemetry.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/tss_api/src/api/ks_node/telemetry.ts b/backend/tss_api/src/api/ks_node/telemetry.ts index b9ddd4732..bd4690efe 100644 --- a/backend/tss_api/src/api/ks_node/telemetry.ts +++ b/backend/tss_api/src/api/ks_node/telemetry.ts @@ -74,11 +74,7 @@ export async function processKSNodeTelemetry( // 4. Check for anomalies const nodeName = `${nodeRes.data!.node_name} (${public_key})`; - if (key_share_count === 0) { - await sendSlackAlert( - `[KS Node Alert] Key share count is 0 for node: ${nodeName}`, - ); - } else if (lastTelemetry && key_share_count < lastTelemetry.key_share_count) { + if (lastTelemetry && key_share_count < lastTelemetry.key_share_count) { await sendSlackAlert( `[KS Node Alert] Key share count decreased for node: ${nodeName}. Previous: ${lastTelemetry.key_share_count}, Current: ${key_share_count}`, ); From a09cf9d6e4aa94b2d4ee1d51e9865b32d8cf423d Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 29 Dec 2025 15:37:51 +0900 Subject: [PATCH 07/10] ks_node: move telemetry config to server state --- backend/oko_api/server/src/bin/launch.ts | 3 +++ .../server/src/runtime/ks_node_monitor.ts | 11 ++++++++--- backend/oko_api_server_state/src/index.ts | 4 ++++ backend/tss_api/src/api/ks_node/telemetry.ts | 18 +++++++----------- .../tss_api/src/routes/ks_node_telemetry.ts | 12 +++++++++++- backend/tss_api/src/utils/slack.ts | 6 ++++-- 6 files changed, 37 insertions(+), 17 deletions(-) diff --git a/backend/oko_api/server/src/bin/launch.ts b/backend/oko_api/server/src/bin/launch.ts index 96312816f..caecf88da 100644 --- a/backend/oko_api/server/src/bin/launch.ts +++ b/backend/oko_api/server/src/bin/launch.ts @@ -60,6 +60,8 @@ async function main() { encryption_secret: envs.ENCRYPTION_SECRET!, typeform_webhook_secret: envs.TYPEFORM_WEBHOOK_SECRET!, telegram_bot_token: envs.TELEGRAM_BOT_TOKEN!, + slack_webhook_url: envs.SLACK_WEBHOOK_URL ?? null, + ks_node_report_password: envs.KS_NODE_REPORT_PASSWORD!, }); state.logger.info("Running database migrations..."); @@ -93,6 +95,7 @@ async function main() { startKSNodeHeartbeatRuntime(state.db, state.logger, { intervalSeconds: 60, // 1 minute + slackWebhookUrl: state.slack_webhook_url, }); startInactiveCustomerUserReminderRuntime(state.db, state.logger, { diff --git a/backend/oko_api/server/src/runtime/ks_node_monitor.ts b/backend/oko_api/server/src/runtime/ks_node_monitor.ts index 54c38b38a..f7bfb7f72 100644 --- a/backend/oko_api/server/src/runtime/ks_node_monitor.ts +++ b/backend/oko_api/server/src/runtime/ks_node_monitor.ts @@ -12,13 +12,13 @@ const HEARTBEAT_THRESHOLD_MINUTES = 10; export function startKSNodeHeartbeatRuntime( db: Pool, logger: Logger, - options: { intervalSeconds: number }, + options: { intervalSeconds: number; slackWebhookUrl: string | null }, ) { logger.info("Starting KS Node heartbeat runtime"); const run = async () => { try { - await checkKSNodeHeartbeats(db, logger); + await checkKSNodeHeartbeats(db, logger, options.slackWebhookUrl); } catch (err) { logger.error("KS Node heartbeat runtime error: %s", err); } @@ -28,7 +28,11 @@ export function startKSNodeHeartbeatRuntime( setInterval(run, options.intervalSeconds * 1000); } -async function checkKSNodeHeartbeats(db: Pool, logger: Logger) { +async function checkKSNodeHeartbeats( + db: Pool, + logger: Logger, + slackWebhookUrl: string | null, +) { const latestTelemetriesRes = await getLatestKSNodeTelemetries(db); if (!latestTelemetriesRes.success) { logger.error( @@ -57,6 +61,7 @@ async function checkKSNodeHeartbeats(db: Pool, logger: Logger) { await sendSlackAlert( `[KS Node Alert] Node ${nodeName} has not reported telemetry for over ${HEARTBEAT_THRESHOLD_MINUTES} minutes. Last seen: ${lastUpdate.toISOString()}`, + slackWebhookUrl, ); } } diff --git a/backend/oko_api_server_state/src/index.ts b/backend/oko_api_server_state/src/index.ts index 3c6d28b24..5e0ed3aae 100644 --- a/backend/oko_api_server_state/src/index.ts +++ b/backend/oko_api_server_state/src/index.ts @@ -100,6 +100,8 @@ export interface ServerState { encryption_secret: string; typeform_webhook_secret: string; telegram_bot_token: string; + slack_webhook_url: string | null; + ks_node_report_password: string; server_keypair: EddsaKeypair; } @@ -131,6 +133,8 @@ export interface InitStateArgs { encryption_secret: string; typeform_webhook_secret: string; telegram_bot_token: string; + slack_webhook_url: string | null; + ks_node_report_password: string; } async function initializeServerKeypair( diff --git a/backend/tss_api/src/api/ks_node/telemetry.ts b/backend/tss_api/src/api/ks_node/telemetry.ts index bd4690efe..79da5c9eb 100644 --- a/backend/tss_api/src/api/ks_node/telemetry.ts +++ b/backend/tss_api/src/api/ks_node/telemetry.ts @@ -17,23 +17,16 @@ export interface KSNodeTelemetryPayload { export async function processKSNodeTelemetry( db: Pool, input: KSNodeTelemetryPayload, - password: string, + slackWebhookUrl: string | null, ): Promise> { - // 1. Verify Password - if (password !== process.env.KS_NODE_REPORT_PASSWORD) { - return { - success: false, - err: "Invalid password", - }; - } - const { public_key, key_share_count, payload } = input; - // 2. Check if node exists + // 1. Check if node exists const nodeRes = await getKSNodeByPublicKey(db, public_key); if (!nodeRes.success) { await sendSlackAlert( `[TSS API Error] Failed to check if node exists ${public_key}: ${nodeRes.err}`, + slackWebhookUrl, ); return { success: false, err: nodeRes.err }; } @@ -46,11 +39,12 @@ export async function processKSNodeTelemetry( return { success: true, data: void 0 }; } - // 3. Get previous telemetry for comparison + // 2. Get previous telemetry for comparison const lastTelemetryRes = await getLastKSNodeTelemetry(db, public_key); if (!lastTelemetryRes.success) { await sendSlackAlert( `[TSS API Error] Failed to get last telemetry for node ${public_key}: ${lastTelemetryRes.err}`, + slackWebhookUrl, ); return { success: false, err: lastTelemetryRes.err }; } @@ -67,6 +61,7 @@ export async function processKSNodeTelemetry( if (!insertRes.success) { await sendSlackAlert( `[TSS API Error] Failed to insert telemetry for node ${public_key}: ${insertRes.err}`, + slackWebhookUrl, ); return { success: false, err: insertRes.err }; } @@ -77,6 +72,7 @@ export async function processKSNodeTelemetry( if (lastTelemetry && key_share_count < lastTelemetry.key_share_count) { await sendSlackAlert( `[KS Node Alert] Key share count decreased for node: ${nodeName}. Previous: ${lastTelemetry.key_share_count}, Current: ${key_share_count}`, + slackWebhookUrl, ); } diff --git a/backend/tss_api/src/routes/ks_node_telemetry.ts b/backend/tss_api/src/routes/ks_node_telemetry.ts index 9346437ab..acd2ed10e 100644 --- a/backend/tss_api/src/routes/ks_node_telemetry.ts +++ b/backend/tss_api/src/routes/ks_node_telemetry.ts @@ -65,6 +65,16 @@ export function setKSNodeTelemetryRoutes(router: Router) { return; } + const expectedPassword = req.app.locals.ks_node_report_password; + if (password !== expectedPassword) { + res.status(401).json({ + success: false, + code: "UNAUTHORIZED", + msg: "Invalid password", + }); + return; + } + const payload: KSNodeTelemetryRequest = req.body; if ( !payload || @@ -82,7 +92,7 @@ export function setKSNodeTelemetryRoutes(router: Router) { const result = await processKSNodeTelemetry( req.app.locals.db, payload, - password, + req.app.locals.slack_webhook_url, ); if (!result.success) { diff --git a/backend/tss_api/src/utils/slack.ts b/backend/tss_api/src/utils/slack.ts index 2fc2b1d74..543a1d49f 100644 --- a/backend/tss_api/src/utils/slack.ts +++ b/backend/tss_api/src/utils/slack.ts @@ -1,5 +1,7 @@ -export async function sendSlackAlert(message: string): Promise { - const webhookUrl = process.env.SLACK_WEBHOOK_URL; +export async function sendSlackAlert( + message: string, + webhookUrl: string | null, +): Promise { if (!webhookUrl) { console.warn("SLACK_WEBHOOK_URL is not set. Skipping Slack alert."); return; From 7a8d39b539644da416ab064a4b8232c589e2be30 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 29 Dec 2025 15:43:03 +0900 Subject: [PATCH 08/10] o --- backend/tss_api/src/routes/ks_node_telemetry.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/backend/tss_api/src/routes/ks_node_telemetry.ts b/backend/tss_api/src/routes/ks_node_telemetry.ts index acd2ed10e..901708d54 100644 --- a/backend/tss_api/src/routes/ks_node_telemetry.ts +++ b/backend/tss_api/src/routes/ks_node_telemetry.ts @@ -96,15 +96,6 @@ export function setKSNodeTelemetryRoutes(router: Router) { ); if (!result.success) { - if (result.err === "Invalid password") { - res.status(401).json({ - success: false, - code: "UNAUTHORIZED", - msg: "Invalid password", - }); - return; - } - res.status(500).json({ success: false, code: "UNKNOWN_ERROR", From 17f47cc5d3aae1479938d3bfe4e49955a880e4a3 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 29 Dec 2025 15:57:01 +0900 Subject: [PATCH 09/10] o --- backend/oko_pg_interface/src/ks_nodes/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/oko_pg_interface/src/ks_nodes/index.ts b/backend/oko_pg_interface/src/ks_nodes/index.ts index 4916b5428..fbe206a03 100644 --- a/backend/oko_pg_interface/src/ks_nodes/index.ts +++ b/backend/oko_pg_interface/src/ks_nodes/index.ts @@ -20,10 +20,12 @@ export async function insertKSNodeTelemetry( ): Promise> { const query = ` INSERT INTO ks_node_telemetry ( - log_id, public_key, key_share_count, payload + log_id, public_key, key_share_count, + payload ) VALUES ( - $1, $2, $3, $4 + $1, $2, $3, + $4 ) `; From 54ec6c9c3e6d31dd4523dbc72354edbe0c3fb17f Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 29 Dec 2025 16:08:34 +0900 Subject: [PATCH 10/10] oko_api: remove FK from ks_node_telemetry table --- .../migrations/20251224070753_add_ks_node_telemetry.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/backend/oko_pg_interface/migrations/20251224070753_add_ks_node_telemetry.ts b/backend/oko_pg_interface/migrations/20251224070753_add_ks_node_telemetry.ts index 3df7d736a..5b2f97f9f 100644 --- a/backend/oko_pg_interface/migrations/20251224070753_add_ks_node_telemetry.ts +++ b/backend/oko_pg_interface/migrations/20251224070753_add_ks_node_telemetry.ts @@ -15,12 +15,7 @@ export async function up(knex: Knex): Promise { .notNullable() .defaultTo(knex.raw("gen_random_uuid()")) .primary({ constraintName: "ks_node_telemetry_pkey" }); - table - .string("public_key", 255) - .notNullable() - .references("public_key") - .inTable("key_share_nodes") - .onDelete("CASCADE"); + table.string("public_key", 255).notNullable(); table.integer("key_share_count").notNullable(); table.jsonb("payload").notNullable(); table