mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 09:05:06 -07:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb367ac5f0 | ||
|
|
3d6c8b3134 |
@@ -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<string, { nextLogAt: number; suppressed: number }>();
|
||||
|
||||
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<void> = 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();
|
||||
|
||||
@@ -157,6 +157,15 @@ describe('StratumV1Service', () => {
|
||||
expect((service as any).detectProtocol(firstChunk)).toBe('v1');
|
||||
});
|
||||
|
||||
it('should detect JSON-RPC as Stratum V1 even with non-printable trailing bytes', () => {
|
||||
const firstChunk = Buffer.concat([
|
||||
Buffer.from('{"id": 1, "method": "mining.subscribe", "params": []}\n'),
|
||||
Buffer.from([0x00, 0xff])
|
||||
]);
|
||||
|
||||
expect((service as any).detectProtocol(firstChunk)).toBe('v1');
|
||||
});
|
||||
|
||||
it('should detect binary Noise traffic as Stratum V2', () => {
|
||||
const firstChunk = Buffer.concat([
|
||||
Buffer.from([0x01, 0x02, 0x03, 0x04]),
|
||||
@@ -174,6 +183,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];
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -446,6 +453,13 @@ export class StratumV1Service implements OnModuleInit {
|
||||
return false;
|
||||
}
|
||||
|
||||
const jsonPrefix = firstChunk
|
||||
.subarray(firstNonWhitespace, Math.min(firstChunk.length, firstNonWhitespace + 256))
|
||||
.toString('utf8');
|
||||
if (jsonPrefix.includes('"method"') || jsonPrefix.includes('"id"')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const byte of firstChunk) {
|
||||
const isWhitespace = byte === 0x09 || byte === 0x0a || byte === 0x0d;
|
||||
const isPrintableAscii = byte >= 0x20 && byte <= 0x7e;
|
||||
@@ -473,6 +487,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) {
|
||||
|
||||
Reference in New Issue
Block a user