diff --git a/src/lock-ops.mjs b/src/lock-ops.mjs index 3976724..50dbb9d 100644 --- a/src/lock-ops.mjs +++ b/src/lock-ops.mjs @@ -1,3 +1,4 @@ +import { randomInt } from 'node:crypto'; import { SimplePool } from 'nostr-tools/pool'; import { getQueryTimeoutMs, @@ -129,7 +130,13 @@ function isTransientPublishCategory(category) { ].includes(category); } -function calculateBackoffDelayMs(attemptNumber, baseMs, capMs, randomFn = Math.random) { +const MAX_RANDOM = 281474976710655; // 2**48 - 1 + +function secureRandom() { + return randomInt(0, MAX_RANDOM) / MAX_RANDOM; +} + +function calculateBackoffDelayMs(attemptNumber, baseMs, capMs, randomFn = secureRandom) { const maxDelay = Math.min(capMs, baseMs * (2 ** Math.max(0, attemptNumber - 1))); return Math.floor(randomFn() * maxDelay); } @@ -408,50 +415,22 @@ async function publishToRelays(pool, relays, event, publishTimeoutMs, phase) { }); } -class LockPublisher { - constructor(relays, event, deps) { - this.relays = relays; - this.event = event; - this.deps = deps; - - const { - poolFactory = () => new SimplePool(), - getPublishTimeoutMsFn = getPublishTimeoutMs, - getMinSuccessfulRelayPublishesFn = getMinSuccessfulRelayPublishes, - getRelayFallbacksFn = getRelayFallbacks, - getMinActiveRelayPoolFn = getMinActiveRelayPool, - retryAttempts = 4, - retryBaseDelayMs = 500, - retryCapDelayMs = 8_000, - sleepFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), - randomFn = Math.random, - telemetryLogger = console.error, - healthLogger = console.error, - diagnostics = {}, - } = deps; - - this.pool = poolFactory(); - this.publishTimeoutMs = getPublishTimeoutMsFn(); - this.minSuccesses = getMinSuccessfulRelayPublishesFn(); - this.fallbackRelays = getRelayFallbacksFn().filter((relay) => !relays.includes(relay)); - this.maxAttempts = Math.max(1, Math.floor(retryAttempts)); - this.healthConfig = buildRelayHealthConfig({ - ...deps, - minActiveRelayPool: getMinActiveRelayPoolFn(), - }); - - this.retryBaseDelayMs = retryBaseDelayMs; - this.retryCapDelayMs = retryCapDelayMs; - this.sleepFn = sleepFn; - this.randomFn = randomFn; - this.telemetryLogger = telemetryLogger; - this.healthLogger = healthLogger; - - this.correlationId = diagnostics.correlationId || process.env.SCHEDULER_LOCK_CORRELATION_ID || 'none'; - this.attemptId = diagnostics.attemptId || process.env.SCHEDULER_LOCK_ATTEMPT_ID || '1'; - - this.allRelays = mergeRelayList(relays, this.fallbackRelays); - } +export async function publishLock(relays, event, deps = {}) { + const { + poolFactory = () => new SimplePool(), + getPublishTimeoutMsFn = getPublishTimeoutMs, + getMinSuccessfulRelayPublishesFn = getMinSuccessfulRelayPublishes, + getRelayFallbacksFn = getRelayFallbacks, + getMinActiveRelayPoolFn = getMinActiveRelayPool, + retryAttempts = 4, + retryBaseDelayMs = 500, + retryCapDelayMs = 8_000, + sleepFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), + randomFn = secureRandom, + telemetryLogger = console.error, + healthLogger = console.error, + diagnostics = {}, + } = deps; async publish() { try { @@ -639,3 +618,5 @@ export async function publishLock(relays, event, deps = {}) { export function _resetRelayHealthState() { defaultHealthManager.reset(); } + +export const _secureRandom = secureRandom; diff --git a/test/secure-random.test.mjs b/test/secure-random.test.mjs new file mode 100644 index 0000000..af64c3b --- /dev/null +++ b/test/secure-random.test.mjs @@ -0,0 +1,21 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { _secureRandom } from '../src/lock-ops.mjs'; + +describe('secureRandom', () => { + it('returns values in [0, 1)', () => { + for (let i = 0; i < 1000; i++) { + const val = _secureRandom(); + assert.ok(val >= 0, `Value ${val} should be >= 0`); + assert.ok(val < 1, `Value ${val} should be < 1`); + } + }); + + it('returns varied values', () => { + const values = new Set(); + for (let i = 0; i < 100; i++) { + values.add(_secureRandom()); + } + assert.ok(values.size > 90, 'Values should be reasonably unique'); + }); +});