From 0d15d4fb702c4220d6546aadb254355c9547ec27 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 6 May 2026 20:15:27 -0400 Subject: [PATCH] reduce overhead --- src/models/StratumV1Client.spec.ts | 78 ++++++++++++++++++++++++++++++ src/models/StratumV1Client.ts | 54 ++++++++++++++++++--- 2 files changed, 124 insertions(+), 8 deletions(-) diff --git a/src/models/StratumV1Client.spec.ts b/src/models/StratumV1Client.spec.ts index bff87ec..9e9c720 100644 --- a/src/models/StratumV1Client.spec.ts +++ b/src/models/StratumV1Client.spec.ts @@ -120,6 +120,16 @@ describe('StratumV1Client', () => { clientStatisticsService = moduleRef.get(ClientStatisticsService); configService = moduleRef.get(ConfigService); + (configService.get as jest.Mock).mockImplementation((key: string) => { + switch (key) { + case 'DEV_FEE_ADDRESS': + return 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4'; + case 'NETWORK': + return 'testnet'; + } + return null; + }); + (StratumV1Client as any).blockedUserAgentLogState.clear(); bitcoinRpcService = { newBlockTemplate$: newBlockEmitter.asObservable(), @@ -141,6 +151,7 @@ describe('StratumV1Client', () => { }); socket.end = jest.fn(); + jest.spyOn(socket, 'destroy').mockImplementation(() => socket); const addressSettings = moduleRef.get(AddressSettingsService); notificationService = { @@ -220,6 +231,73 @@ describe('StratumV1Client', () => { }); + it('should block non-compliant user agents on subscribe without allocating a session', async () => { + (configService.get as jest.Mock).mockImplementation((key: string) => { + switch (key) { + case 'NON_COMPLIANT_USER_AGENTS': + return 'NMMiner'; + case 'DEV_FEE_ADDRESS': + return 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4'; + case 'NETWORK': + return 'testnet'; + } + return null; + }); + jest.spyOn(socket, 'write').mockImplementation((data) => true); + + emitMessage(`{"id":1,"method":"mining.subscribe","params":["NMMiner/1.0"]}`); + await new Promise((r) => setTimeout(r, 1)); + + expect(socket.destroy).toHaveBeenCalled(); + expect(socket.write).not.toHaveBeenCalled(); + expect((client as any).statistics).toBeUndefined(); + expect(consoleLogSpy).toHaveBeenCalledWith('Blocked non-compliant connection from userAgent: NMMiner'); + }); + + it('should throttle repeated non-compliant user agent logs', async () => { + (configService.get as jest.Mock).mockImplementation((key: string) => { + switch (key) { + case 'NON_COMPLIANT_USER_AGENTS': + return 'NMMiner'; + case 'DEV_FEE_ADDRESS': + return 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4'; + case 'NETWORK': + return 'testnet'; + } + return null; + }); + jest.spyOn(socket, 'write').mockImplementation((data) => true); + + emitMessage(`{"id":1,"method":"mining.subscribe","params":["NMMiner/1.0"]}`); + await new Promise((r) => setTimeout(r, 1)); + + const secondSocket = new Socket(); + jest.spyOn(secondSocket, 'on').mockImplementation((event: string, listener: (...args: any[]) => void) => { + socketEmitter = listener; + return secondSocket; + }); + secondSocket.end = jest.fn(); + jest.spyOn(secondSocket, 'destroy').mockImplementation(() => secondSocket); + const secondClient = new StratumV1Client( + secondSocket, + stratumV1JobsService, + bitcoinRpcService, + clientService, + clientStatisticsService, + notificationService, + blocksService, + configService, + moduleRef.get(AddressSettingsService) + ); + + socketEmitter(Buffer.from(`{"id":1,"method":"mining.subscribe","params":["NMMiner/1.0"]}\n`)); + await new Promise((r) => setTimeout(r, 1)); + + expect(secondSocket.destroy).toHaveBeenCalled(); + expect(consoleLogSpy.mock.calls.filter(call => call[0]?.startsWith('Blocked non-compliant connection'))).toHaveLength(1); + await secondClient.destroy(); + }); + it('should respond to mining.configure', async () => { diff --git a/src/models/StratumV1Client.ts b/src/models/StratumV1Client.ts index 9330f72..33e7b21 100644 --- a/src/models/StratumV1Client.ts +++ b/src/models/StratumV1Client.ts @@ -29,6 +29,7 @@ import { SuggestDifficulty } from './stratum-messages/SuggestDifficultyMessage'; import { StratumV1ClientStatistics } from './StratumV1ClientStatistics'; const TRUE_DIFF_ONE = 2.695953529101131e67; +const BLOCKED_USER_AGENT_LOG_INTERVAL_MS = 60 * 1000; export function effectiveJobDifficulty( jobIdInt: number, @@ -47,6 +48,7 @@ export function effectiveJobDifficulty( export class StratumV1Client { + private static blockedUserAgentLogState = new Map(); public clientSubscription: SubscriptionMessage; private clientConfiguration: ConfigurationMessage; @@ -71,6 +73,7 @@ export class StratumV1Client { //public hashRate: number = 0; private buffer: string = ''; + private connectionClosed = false; private miningSubmissionHashes = new Set() @@ -93,6 +96,9 @@ export class StratumV1Client { (async () => { for (const m of lines.filter(l => l.length > 0)) { + if (this.connectionClosed || this.socket.destroyed || this.socket.writableEnded) { + break; + } try { await this.handleMessage(m); } catch (e) { @@ -157,6 +163,11 @@ export class StratumV1Client { const errors = await validate(subscriptionMessage, validatorOptions); if (errors.length === 0) { + if (this.isBlockedUserAgent(subscriptionMessage.userAgent)) { + this.logBlockedUserAgent(subscriptionMessage.userAgent); + this.closeSocket(); + return; + } if (this.sessionStart == null) { this.sessionStart = new Date(); @@ -372,9 +383,9 @@ export class StratumV1Client { private async initStratum() { this.stratumInitialized = true; - if (this.validateHeaderCompliance(this.clientSubscription.userAgent)) { - console.log(`Non compliant connection from userAgent: ${this.clientSubscription.userAgent}`); - await this.socket.end(); + if (this.isBlockedUserAgent(this.clientSubscription.userAgent)) { + this.logBlockedUserAgent(this.clientSubscription.userAgent); + this.closeSocket(); return; } @@ -722,16 +733,43 @@ export class StratumV1Client { return number; } - private validateHeaderCompliance(userAgent: string): boolean { - const headerCompliance = this.configService.get('COMPLIANT_HEADERS'); - if (!headerCompliance || headerCompliance.trim() === '') { + private isBlockedUserAgent(userAgent: string): boolean { + const blockedUserAgents = this.configService.get('NON_COMPLIANT_USER_AGENTS') + || this.configService.get('BLOCKED_USER_AGENTS') + || this.configService.get('COMPLIANT_HEADERS'); + if (!blockedUserAgents || blockedUserAgents.trim() === '') { return false; } - const complianceList = headerCompliance.split(',').map(ua => ua.trim().toLowerCase()); + const blockedList = blockedUserAgents.split(',').map(ua => ua.trim().toLowerCase()); const userAgentLower = userAgent.toLowerCase(); - return complianceList.some(compliant => compliant.length > 0 && userAgentLower.includes(compliant)); + return blockedList.some(blocked => blocked.length > 0 && userAgentLower.includes(blocked)); + } + + private logBlockedUserAgent(userAgent: string) { + const now = Date.now(); + const logState = StratumV1Client.blockedUserAgentLogState.get(userAgent); + + if (logState != null && now < logState.nextLogAt) { + logState.suppressed += 1; + return; + } + + const suppressed = logState?.suppressed ?? 0; + const suffix = suppressed > 0 ? ` (${suppressed} similar connections suppressed)` : ''; + console.log(`Blocked non-compliant connection from userAgent: ${userAgent}${suffix}`); + StratumV1Client.blockedUserAgentLogState.set(userAgent, { + nextLogAt: now + BLOCKED_USER_AGENT_LOG_INTERVAL_MS, + suppressed: 0 + }); + } + + private closeSocket() { + this.connectionClosed = true; + if (!this.socket.destroyed) { + this.socket.destroy(); + } } private async write(message: string): Promise {