mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 17:15:03 -07:00
245 lines
10 KiB
TypeScript
245 lines
10 KiB
TypeScript
import { StratumV1Service } from './stratum-v1.service';
|
|
|
|
describe('StratumV1Service', () => {
|
|
const originalMaster = process.env.MASTER;
|
|
const originalStratumPorts = process.env.STRATUM_PORTS;
|
|
const originalStratumSecure = process.env.STRATUM_SECURE;
|
|
const originalSecureStratumPorts = process.env.SECURE_STRATUM_PORTS;
|
|
const originalBackpressureEnabled = process.env.STRATUM_BACKPRESSURE_ENABLED;
|
|
const originalMaxConnectionsPerListener = process.env.STRATUM_MAX_CONNECTIONS_PER_LISTENER;
|
|
const originalTlsHandshakeTimeoutMs = process.env.STRATUM_TLS_HANDSHAKE_TIMEOUT_MS;
|
|
const originalSocketTimeoutMs = process.env.STRATUM_SOCKET_TIMEOUT_MS;
|
|
const originalTcpKeepAliveInitialDelayMs = process.env.STRATUM_TCP_KEEPALIVE_INITIAL_DELAY_MS;
|
|
|
|
let service: StratumV1Service;
|
|
let clientService;
|
|
let userAgentReportService;
|
|
let stratumV2Service;
|
|
let redisMessagingService;
|
|
let consoleLogSpy: jest.SpyInstance;
|
|
let consoleWarnSpy: jest.SpyInstance;
|
|
|
|
beforeEach(() => {
|
|
jest.useFakeTimers();
|
|
clientService = {
|
|
deleteAll: jest.fn().mockResolvedValue(undefined)
|
|
};
|
|
userAgentReportService = {
|
|
refreshReport: jest.fn().mockResolvedValue(undefined)
|
|
};
|
|
stratumV2Service = {
|
|
ensureInitialized: jest.fn().mockResolvedValue(undefined),
|
|
createClient: jest.fn()
|
|
};
|
|
redisMessagingService = {
|
|
clearClientPresence: jest.fn().mockResolvedValue(undefined)
|
|
};
|
|
service = new StratumV1Service(
|
|
{} as any,
|
|
clientService,
|
|
{} as any,
|
|
{} as any,
|
|
{} as any,
|
|
{} as any,
|
|
{} as any,
|
|
stratumV2Service as any,
|
|
userAgentReportService as any,
|
|
undefined,
|
|
redisMessagingService as any
|
|
);
|
|
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
});
|
|
|
|
afterEach(() => {
|
|
restoreEnv('MASTER', originalMaster);
|
|
restoreEnv('STRATUM_PORTS', originalStratumPorts);
|
|
restoreEnv('STRATUM_SECURE', originalStratumSecure);
|
|
restoreEnv('SECURE_STRATUM_PORTS', originalSecureStratumPorts);
|
|
restoreEnv('STRATUM_BACKPRESSURE_ENABLED', originalBackpressureEnabled);
|
|
restoreEnv('STRATUM_MAX_CONNECTIONS_PER_LISTENER', originalMaxConnectionsPerListener);
|
|
restoreEnv('STRATUM_TLS_HANDSHAKE_TIMEOUT_MS', originalTlsHandshakeTimeoutMs);
|
|
restoreEnv('STRATUM_SOCKET_TIMEOUT_MS', originalSocketTimeoutMs);
|
|
restoreEnv('STRATUM_TCP_KEEPALIVE_INITIAL_DELAY_MS', originalTcpKeepAliveInitialDelayMs);
|
|
consoleLogSpy.mockRestore();
|
|
consoleWarnSpy.mockRestore();
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it('should skip Stratum listeners in the master process', async () => {
|
|
process.env.MASTER = 'true';
|
|
const startSocketServerSpy = jest.spyOn(service as any, 'startSocketServer');
|
|
const startSecureSocketServerSpy = jest.spyOn(service as any, 'startSecureSocketServer');
|
|
|
|
await service.onModuleInit();
|
|
jest.runOnlyPendingTimers();
|
|
|
|
expect(clientService.deleteAll).toHaveBeenCalled();
|
|
expect(redisMessagingService.clearClientPresence).toHaveBeenCalled();
|
|
expect(userAgentReportService.refreshReport).toHaveBeenCalled();
|
|
expect(startSocketServerSpy).not.toHaveBeenCalled();
|
|
expect(startSecureSocketServerSpy).not.toHaveBeenCalled();
|
|
expect(consoleLogSpy).toHaveBeenCalledWith('Master process skipping Stratum socket listeners');
|
|
});
|
|
|
|
it('should start Stratum listeners in worker processes', async () => {
|
|
process.env.MASTER = 'false';
|
|
process.env.STRATUM_PORTS = '3333,3334';
|
|
process.env.STRATUM_SECURE = 'true';
|
|
process.env.SECURE_STRATUM_PORTS = '4333';
|
|
const startSocketServerSpy = jest.spyOn(service as any, 'startSocketServer').mockImplementation(() => undefined);
|
|
const startSecureSocketServerSpy = jest.spyOn(service as any, 'startSecureSocketServer').mockImplementation(() => undefined);
|
|
|
|
await service.onModuleInit();
|
|
jest.advanceTimersByTime(10000);
|
|
|
|
expect(clientService.deleteAll).not.toHaveBeenCalled();
|
|
expect(userAgentReportService.refreshReport).not.toHaveBeenCalled();
|
|
expect(startSocketServerSpy).toHaveBeenCalledWith(3333);
|
|
expect(startSocketServerSpy).toHaveBeenCalledWith(3334);
|
|
expect(startSecureSocketServerSpy).toHaveBeenCalledWith(4333);
|
|
});
|
|
|
|
it('should pause listeners when worker backpressure is high', () => {
|
|
const close = jest.fn((callback?: (error?: Error) => void) => callback?.());
|
|
(service as any).listeners.push({
|
|
port: 3333,
|
|
secure: false,
|
|
server: { close },
|
|
paused: false
|
|
});
|
|
jest.spyOn(service as any, 'getEventLoopP95Ms').mockReturnValue(5000);
|
|
jest.spyOn(service as any, 'getBackpressureEventLoopP95Ms').mockReturnValue(2000);
|
|
|
|
(service as any).checkBackpressure();
|
|
|
|
expect(close).toHaveBeenCalled();
|
|
expect((service as any).listeners[0].paused).toBe(true);
|
|
expect((service as any).listeners[0].server).toBeNull();
|
|
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Pausing Stratum accepts'));
|
|
});
|
|
|
|
it('should resume listeners after consecutive healthy backpressure checks', () => {
|
|
(service as any).listeners.push({
|
|
port: 3333,
|
|
secure: false,
|
|
server: null,
|
|
paused: true
|
|
});
|
|
jest.spyOn(service as any, 'getEventLoopP95Ms').mockReturnValue(50);
|
|
jest.spyOn(service as any, 'getBackpressureEventLoopP95Ms').mockReturnValue(2000);
|
|
jest.spyOn(service as any, 'getBackpressureResumeEventLoopP95Ms').mockReturnValue(250);
|
|
jest.spyOn(service as any, 'getBackpressureResumeRssMb').mockReturnValue(Number.MAX_SAFE_INTEGER);
|
|
jest.spyOn(service as any, 'getBackpressureHealthyChecks').mockReturnValue(2);
|
|
const listenSpy = jest.spyOn(service as any, 'listen').mockImplementation((listener: any) => {
|
|
listener.server = {};
|
|
listener.paused = false;
|
|
});
|
|
|
|
(service as any).checkBackpressure();
|
|
expect(listenSpy).not.toHaveBeenCalled();
|
|
|
|
(service as any).checkBackpressure();
|
|
|
|
expect(listenSpy).toHaveBeenCalledWith((service as any).listeners[0]);
|
|
expect((service as any).listeners[0].paused).toBe(false);
|
|
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Resuming Stratum accepts'));
|
|
});
|
|
|
|
it('should cap listener connections and drop excess cluster connections', () => {
|
|
process.env.STRATUM_MAX_CONNECTIONS_PER_LISTENER = '250';
|
|
const server = {} as any;
|
|
|
|
(service as any).configureConnectionLimit(server);
|
|
|
|
expect(server.maxConnections).toBe(250);
|
|
expect(server.dropMaxConnection).toBe(true);
|
|
});
|
|
|
|
it('should allow high-volume pools by default', () => {
|
|
delete process.env.STRATUM_MAX_CONNECTIONS_PER_LISTENER;
|
|
|
|
expect((service as any).getMaxConnectionsPerListener()).toBe(10000);
|
|
});
|
|
|
|
it('should use an explicit TLS handshake timeout', () => {
|
|
process.env.STRATUM_TLS_HANDSHAKE_TIMEOUT_MS = '5000';
|
|
|
|
expect((service as any).getTlsHandshakeTimeoutMs()).toBe(5000);
|
|
});
|
|
|
|
it('should keep quiet miners connected for one hour by default', () => {
|
|
delete process.env.STRATUM_SOCKET_TIMEOUT_MS;
|
|
|
|
expect((service as any).getSocketTimeoutMs()).toBe(1000 * 60 * 60);
|
|
});
|
|
|
|
it('should allow configuring Stratum socket idle timeout', () => {
|
|
process.env.STRATUM_SOCKET_TIMEOUT_MS = '7200000';
|
|
|
|
expect((service as any).getSocketTimeoutMs()).toBe(7200000);
|
|
});
|
|
|
|
it('should enable TCP keepalive quickly by default', () => {
|
|
delete process.env.STRATUM_TCP_KEEPALIVE_INITIAL_DELAY_MS;
|
|
|
|
expect((service as any).getTcpKeepAliveInitialDelayMs()).toBe(60000);
|
|
});
|
|
|
|
it('should allow configuring TCP keepalive initial delay', () => {
|
|
process.env.STRATUM_TCP_KEEPALIVE_INITIAL_DELAY_MS = '30000';
|
|
|
|
expect((service as any).getTcpKeepAliveInitialDelayMs()).toBe(30000);
|
|
});
|
|
|
|
it('should detect JSON-RPC as Stratum V1', () => {
|
|
const firstChunk = Buffer.from('{"id":1,"method":"mining.subscribe","params":[]}\n');
|
|
|
|
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]),
|
|
Buffer.alloc(60, 0xaa)
|
|
]);
|
|
|
|
expect((service as any).detectProtocol(firstChunk)).toBe('v2');
|
|
});
|
|
|
|
it('should reject recognizable TLS client hello on the unified plain stratum port', () => {
|
|
expect((service as any).detectProtocol(Buffer.from([0x16, 0x03, 0x01]))).toBeNull();
|
|
});
|
|
|
|
it('should route binary data that only shares a TLS first byte to Stratum V2', () => {
|
|
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];
|
|
return;
|
|
}
|
|
process.env[key] = value;
|
|
}
|
|
});
|