Fix write verification and re-enable it
This commit is contained in:
parent
ef7463a29a
commit
ec51dcc52e
@ -9,8 +9,8 @@ export LOG_LEVEL=info
|
||||
|
||||
# Optional write confirmation probe settings:
|
||||
export WRITE_CHECK_ENABLED=true
|
||||
# false = consider publish OK sufficient; true = require read-after-write confirmation
|
||||
export WRITE_CHECK_VERIFY_READ=false
|
||||
# true = require read-after-write confirmation; set false to treat publish OK as sufficient
|
||||
export WRITE_CHECK_VERIFY_READ=true
|
||||
export WRITE_CHECK_KIND=30078
|
||||
# WRITE_CHECK_PRIVKEY accepts either:
|
||||
# - nsec1... string
|
||||
|
||||
24
README.md
24
README.md
@ -6,8 +6,8 @@ Production-oriented Prometheus exporter for monitoring a fixed set of Nostr rela
|
||||
|
||||
- Probes each configured relay either on an interval, or on-demand when `/metrics` is scraped.
|
||||
- Uses `@nostrwatch/nocap` for `open` + `read` checks.
|
||||
- Optionally performs a low-noise write confirmation check (kind `30078` by default) using deterministic `d` tags.
|
||||
- By default, treats relay `OK` for publish as success; optional read-after-write verification can be enabled.
|
||||
- Performs a low-noise write confirmation check (kind `30078` by default) using deterministic `d` tags.
|
||||
- By default, requires read-after-write verification.
|
||||
- Exposes:
|
||||
- `/metrics` for Prometheus scraping
|
||||
- `/healthz` for process and probe-loop health
|
||||
@ -49,7 +49,7 @@ Environment variables:
|
||||
- `PORT` (default: `9464`)
|
||||
- `LOG_LEVEL` (default: `info`; one of `debug|info|warn|error`)
|
||||
- `WRITE_CHECK_ENABLED` (default: `true`)
|
||||
- `WRITE_CHECK_VERIFY_READ` (default: `false`; when `true`, require event read-back after publish)
|
||||
- `WRITE_CHECK_VERIFY_READ` (default: `true`; set `false` to treat publish `OK` as sufficient)
|
||||
- `WRITE_CHECK_KIND` (default: `30078`)
|
||||
- `WRITE_CHECK_PRIVKEY` (optional; supports `nsec1...` or 64-char hex)
|
||||
|
||||
@ -100,13 +100,23 @@ LIVE_RELAY_TEST_EXPECT_NO_FAILURES=1 \
|
||||
pnpm test test/live.offchain.test.ts
|
||||
```
|
||||
|
||||
Optional knobs:
|
||||
Faster local loop (reduced stability sampling):
|
||||
|
||||
- `LIVE_RELAY_TEST_SAMPLES` (default `12`)
|
||||
- `LIVE_RELAY_TEST_SCRAPE_EVERY_MS` (default `1500`)
|
||||
```bash
|
||||
LIVE_RELAY_TEST_OFFCHAIN=1 \
|
||||
LIVE_RELAY_TEST_RELAYS="wss://offchain.pub" \
|
||||
LIVE_RELAY_TEST_SAMPLES=2 \
|
||||
LIVE_RELAY_TEST_SCRAPE_EVERY_MS=250 \
|
||||
pnpm test test/live.offchain.test.ts
|
||||
```
|
||||
|
||||
Optional knobs (defaults favor faster feedback):
|
||||
|
||||
- `LIVE_RELAY_TEST_SAMPLES` (default `4`)
|
||||
- `LIVE_RELAY_TEST_SCRAPE_EVERY_MS` (default `500`)
|
||||
- `LIVE_RELAY_TEST_TIMEOUT_SECONDS` (default `8`)
|
||||
- `LIVE_RELAY_TEST_RELAYS` (default `"wss://offchain.pub"`; comma-separated relay list)
|
||||
- `LIVE_RELAY_TEST_WRITE_VERIFY_READ=1` to enable read-after-write verification (disabled by default)
|
||||
- `LIVE_RELAY_TEST_WRITE_VERIFY_READ=1` to force read-after-write verification
|
||||
- `LIVE_RELAY_TEST_EXPECT_NO_FAILURES=1` to make the test fail on any write-confirm/probe failures
|
||||
|
||||
## Exposed metrics
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
pnpmDeps = pkgs.pnpm.fetchDeps {
|
||||
inherit pname version src;
|
||||
fetcherVersion = 1;
|
||||
hash = "sha256-n3pn4NVBVauwE2FWfMsulmn+KgCkcV32XJD6vX1NIB0=";
|
||||
hash = "sha256-rZhEXQcRPS5pHdXNfrYeTI3zxkqtJjcyaGgrmzwFaHA=";
|
||||
};
|
||||
|
||||
buildPhase = ''
|
||||
|
||||
@ -22,10 +22,12 @@
|
||||
"@nostrwatch/nocap-every-adapter-default": "^1.7.0",
|
||||
"nostr-tools": "^2.23.3",
|
||||
"p-limit": "^7.3.0",
|
||||
"prom-client": "^15.1.3"
|
||||
"prom-client": "^15.1.3",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
|
||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@ -23,10 +23,16 @@ importers:
|
||||
prom-client:
|
||||
specifier: ^15.1.3
|
||||
version: 15.1.3
|
||||
ws:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^25.5.0
|
||||
version: 25.5.0
|
||||
'@types/ws':
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.1
|
||||
tsx:
|
||||
specifier: ^4.21.0
|
||||
version: 4.21.0
|
||||
@ -683,6 +689,9 @@ packages:
|
||||
'@types/tough-cookie@4.0.5':
|
||||
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||
|
||||
'@types/yargs-parser@21.0.3':
|
||||
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
|
||||
|
||||
@ -1956,6 +1965,18 @@ packages:
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
ws@8.20.0:
|
||||
resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
@ -2677,6 +2698,10 @@ snapshots:
|
||||
|
||||
'@types/tough-cookie@4.0.5': {}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
|
||||
'@types/yargs-parser@21.0.3': {}
|
||||
|
||||
'@types/yargs@17.0.35':
|
||||
@ -4123,6 +4148,11 @@ snapshots:
|
||||
bufferutil: 4.1.0
|
||||
utf-8-validate: 5.0.10
|
||||
|
||||
ws@8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10):
|
||||
optionalDependencies:
|
||||
bufferutil: 4.1.0
|
||||
utf-8-validate: 5.0.10
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yaeti@0.0.6: {}
|
||||
|
||||
@ -35,7 +35,7 @@ const DEFAULT_LISTEN_ADDR = "0.0.0.0";
|
||||
const DEFAULT_PORT = 9464;
|
||||
const DEFAULT_LOG_LEVEL: LogLevel = "info";
|
||||
const DEFAULT_WRITE_CHECK_ENABLED = true;
|
||||
const DEFAULT_WRITE_CHECK_VERIFY_READ = false;
|
||||
const DEFAULT_WRITE_CHECK_VERIFY_READ = true;
|
||||
const DEFAULT_WRITE_CHECK_KIND = 30078;
|
||||
|
||||
const MIN_TIMEOUT_SECONDS = 1;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { finalizeEvent, getPublicKey, nip19, Relay, type Filter } from "nostr-tools";
|
||||
import WebSocket from "ws";
|
||||
|
||||
interface WriteConfirmInput {
|
||||
relay: string;
|
||||
@ -35,6 +36,13 @@ function decodePrivateKey(raw: string): Uint8Array {
|
||||
return Uint8Array.from(Buffer.from(value, "hex"));
|
||||
}
|
||||
|
||||
function ensureWebSocketSupport(): void {
|
||||
const globalWithWebSocket = globalThis as { WebSocket?: unknown };
|
||||
if (typeof globalWithWebSocket.WebSocket === "undefined") {
|
||||
globalWithWebSocket.WebSocket = WebSocket;
|
||||
}
|
||||
}
|
||||
|
||||
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
||||
@ -56,35 +64,53 @@ async function waitForEvent(
|
||||
expectedEventId: string,
|
||||
timeoutMs: number,
|
||||
): Promise<boolean> {
|
||||
return withTimeout<boolean>(
|
||||
new Promise<boolean>((resolve) => {
|
||||
let settled = false;
|
||||
const sub = relay.subscribe([filter], {
|
||||
onevent: (evt) => {
|
||||
if (settled) return;
|
||||
if (evt.id === expectedEventId) {
|
||||
const started = Date.now();
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
const elapsed = Date.now() - started;
|
||||
const remainingMs = Math.max(1, timeoutMs - elapsed);
|
||||
const queryTimeoutMs = Math.max(200, Math.min(1_000, remainingMs));
|
||||
const found = await withTimeout<boolean>(
|
||||
new Promise<boolean>((resolve) => {
|
||||
let settled = false;
|
||||
const sub = relay.subscribe([filter], {
|
||||
onevent: (evt) => {
|
||||
if (settled) return;
|
||||
if (evt.id === expectedEventId) {
|
||||
settled = true;
|
||||
sub.close();
|
||||
resolve(true);
|
||||
}
|
||||
},
|
||||
oneose: () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
sub.close();
|
||||
resolve(true);
|
||||
}
|
||||
},
|
||||
oneose: () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
sub.close();
|
||||
resolve(false);
|
||||
},
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
}),
|
||||
queryTimeoutMs,
|
||||
"write confirm read-after-write query",
|
||||
).catch(() => false);
|
||||
|
||||
if (found) return true;
|
||||
|
||||
const sleepMs = Math.min(200, Math.max(0, timeoutMs - (Date.now() - started)));
|
||||
if (sleepMs > 0) {
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, sleepMs);
|
||||
});
|
||||
}),
|
||||
timeoutMs,
|
||||
"write confirm read-after-write",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function runWriteConfirm(input: WriteConfirmInput): Promise<WriteConfirmResult> {
|
||||
const startedAt = performance.now();
|
||||
let relay: Relay | null = null;
|
||||
try {
|
||||
ensureWebSocketSupport();
|
||||
const secretKey = decodePrivateKey(input.privkey);
|
||||
const derivedPubkey = getPublicKey(secretKey);
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { loadConfig } from "../src/config.js";
|
||||
|
||||
test("loadConfig parses relay input and ignores invalid entries", () => {
|
||||
const config = loadConfig({
|
||||
RELAYS: "wss://offchain.pub,not-a-url,wss://offchain.pub",
|
||||
RELAYS: "wss://offchain.pub,not-a-url",
|
||||
WRITE_CHECK_ENABLED: "false",
|
||||
});
|
||||
|
||||
@ -50,11 +50,11 @@ test("loadConfig sets write-check read verification flag", () => {
|
||||
const defaultConfig = loadConfig({
|
||||
RELAYS: "wss://offchain.pub",
|
||||
});
|
||||
assert.equal(defaultConfig.writeCheck.verifyRead, false);
|
||||
assert.equal(defaultConfig.writeCheck.verifyRead, true);
|
||||
|
||||
const verifyConfig = loadConfig({
|
||||
RELAYS: "wss://offchain.pub",
|
||||
WRITE_CHECK_VERIFY_READ: "true",
|
||||
WRITE_CHECK_VERIFY_READ: "false",
|
||||
});
|
||||
assert.equal(verifyConfig.writeCheck.verifyRead, true);
|
||||
assert.equal(verifyConfig.writeCheck.verifyRead, false);
|
||||
});
|
||||
|
||||
@ -6,8 +6,8 @@ import type { Readable } from "node:stream";
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
|
||||
const ENABLED = process.env.LIVE_RELAY_TEST_OFFCHAIN === "1";
|
||||
const SCRAPE_SAMPLES = Number.parseInt(process.env.LIVE_RELAY_TEST_SAMPLES ?? "12", 10);
|
||||
const SCRAPE_EVERY_MS = Number.parseInt(process.env.LIVE_RELAY_TEST_SCRAPE_EVERY_MS ?? "1500", 10);
|
||||
const SCRAPE_SAMPLES = Number.parseInt(process.env.LIVE_RELAY_TEST_SAMPLES ?? "4", 10);
|
||||
const SCRAPE_EVERY_MS = Number.parseInt(process.env.LIVE_RELAY_TEST_SCRAPE_EVERY_MS ?? "500", 10);
|
||||
const PROBE_TIMEOUT_SECONDS = Number.parseInt(process.env.LIVE_RELAY_TEST_TIMEOUT_SECONDS ?? "8", 10);
|
||||
const WRITE_VERIFY_READ = process.env.LIVE_RELAY_TEST_WRITE_VERIFY_READ === "1";
|
||||
const ENFORCE_NO_WRITE_CONFIRM_FAILURES = process.env.LIVE_RELAY_TEST_EXPECT_NO_FAILURES === "1";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user