From 3d6c8b31347fc19a7d7f5ce72c744c30df6fd804 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 5 Jun 2026 23:41:22 -0400 Subject: [PATCH] sv2 debug --- src/models/StratumV2Client.ts | 44 ++++++++++++++++++++++++- src/services/stratum-v1.service.spec.ts | 10 ++++++ src/services/stratum-v1.service.ts | 19 +++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/models/StratumV2Client.ts b/src/models/StratumV2Client.ts index b613bab..a29e07d 100644 --- a/src/models/StratumV2Client.ts +++ b/src/models/StratumV2Client.ts @@ -61,6 +61,7 @@ const DEFAULT_TARGET_SHARES_PER_MINUTE = 2; const DEFAULT_DIFFICULTY_CHECK_INTERVAL_MS = 60 * 1000; const FIXED_STANDARD_EXTRANONCE2 = '0000000000000000'; const RETIRED_EXTENDED_JOB_RETENTION_MS = 5 * 60 * 1000; +const SV2_AUTH_FAILURE_LOG_INTERVAL_MS = 60 * 1000; interface ExtendedJobData { coinbasePrefix: Buffer; @@ -93,6 +94,8 @@ interface ChannelState { } export class StratumV2Client { + private static authFailureLogState = new Map(); + private readonly sessionId = crypto.randomBytes(4).toString('hex'); private readonly noiseSession: Sv2NoiseSession; private readonly frameReader = new Sv2FrameReader(null); @@ -116,6 +119,7 @@ export class StratumV2Client { private sessionDifficulty: number; private clientEntity: ClientEntity = null; private creatingEntity: Promise = null; + private readonly firstChunkSummary: string; constructor( private readonly socket: Socket, @@ -130,6 +134,7 @@ export class StratumV2Client { private readonly configService: ConfigService, private readonly addressSettingsService: AddressSettingsService, ) { + this.firstChunkSummary = this.describeChunk(firstChunk); this.noiseSession = new Sv2NoiseSession(this.stratumV2Service.getNoiseConfig()); this.sessionDifficulty = this.getInitialDifficulty(); this.targetSharesPerMinute = this.getTargetSharesPerMinute(); @@ -182,7 +187,7 @@ export class StratumV2Client { await this.handleEncryptedData(data); } } catch (error) { - console.error(`[SV2 ${this.sessionId}] ${error.message}`); + this.logProtocolError(error); this.closeSocket(); } } @@ -1152,6 +1157,43 @@ export class StratumV2Client { throw new Error('Invalid network configuration'); } + private logProtocolError(error: Error): void { + if (!this.isNoisyAuthFailure(error)) { + console.error(`[SV2 ${this.sessionId}] ${error.message}`); + return; + } + + const remote = this.socket.remoteAddress ?? 'unknown'; + const key = `${remote}:${error.message}`; + const now = Date.now(); + const logState = StratumV2Client.authFailureLogState.get(key); + if (logState != null && now < logState.nextLogAt) { + logState.suppressed += 1; + return; + } + + const suppressed = logState?.suppressed ?? 0; + const suffix = suppressed > 0 ? ` (${suppressed} similar auth failures suppressed)` : ''; + console.warn(`[SV2 ${this.sessionId}] Authentication failed from ${remote}: ${error.message}; firstChunk=${this.firstChunkSummary}${suffix}`); + StratumV2Client.authFailureLogState.set(key, { + nextLogAt: now + SV2_AUTH_FAILURE_LOG_INTERVAL_MS, + suppressed: 0, + }); + } + + private isNoisyAuthFailure(error: Error): boolean { + return error.message.includes('Unsupported state or unable to authenticate data'); + } + + private describeChunk(chunk: Buffer): string { + const preview = chunk.subarray(0, 16); + const printable = Array.from(preview).every(byte => byte >= 0x20 && byte <= 0x7e); + const prefix = printable + ? preview.toString('ascii').replace(/\\/g, '\\\\').replace(/"/g, '\\"') + : preview.toString('hex'); + return `len=${chunk.length},${printable ? 'ascii' : 'hex'}=${prefix}`; + } + private closeSocket(): void { if (!this.socket.destroyed) { this.socket.destroy(); diff --git a/src/services/stratum-v1.service.spec.ts b/src/services/stratum-v1.service.spec.ts index fb9e879..39f861a 100644 --- a/src/services/stratum-v1.service.spec.ts +++ b/src/services/stratum-v1.service.spec.ts @@ -174,6 +174,16 @@ describe('StratumV1Service', () => { expect((service as any).detectProtocol(Buffer.from([0x16, 0xaa, 0xbb]))).toBe('v2'); }); + it('should not route plaintext PROXY protocol headers to Stratum V2', () => { + const firstChunk = Buffer.from('PROXY TCP4 203.0.113.10 192.0.2.10 54321 3333\\r\\n'); + + expect((service as any).detectProtocol(firstChunk)).toBeNull(); + }); + + it('should not route malformed plaintext to Stratum V2', () => { + expect((service as any).detectProtocol(Buffer.from('mining.subscribe\\n'))).toBeNull(); + }); + function restoreEnv(key: string, value: string | undefined) { if (value == null) { delete process.env[key]; diff --git a/src/services/stratum-v1.service.ts b/src/services/stratum-v1.service.ts index 793ba37..edad5b3 100644 --- a/src/services/stratum-v1.service.ts +++ b/src/services/stratum-v1.service.ts @@ -428,6 +428,13 @@ export class StratumV1Service implements OnModuleInit { return null; } + // Plaintext that is not JSON-RPC is not a valid SV2 Noise Act 1. This + // catches PROXY-protocol lines, SSH banners, and malformed SV1 clients + // before they get misrouted into the SV2 decrypt path. + if (this.looksLikePlaintext(firstChunk)) { + return null; + } + return 'v2'; } @@ -473,6 +480,18 @@ export class StratumV1Service implements OnModuleInit { || prefix.startsWith('OPTIONS '); } + private looksLikePlaintext(firstChunk: Buffer): boolean { + for (const byte of firstChunk) { + const isWhitespace = byte === 0x09 || byte === 0x0a || byte === 0x0d; + const isPrintableAscii = byte >= 0x20 && byte <= 0x7e; + if (!isWhitespace && !isPrintableAscii) { + return false; + } + } + + return true; + } + private getPositiveIntegerEnv(key: string, fallback: number) { const configured = parseInt(process.env[key], 10); if (Number.isFinite(configured) && configured > 0) {