mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 17:15:03 -07:00
Harden accounting and stratum production settings
This commit is contained in:
@@ -186,6 +186,17 @@ describe('StratumV1Client', () => {
|
||||
|
||||
});
|
||||
|
||||
it('should disable application idle timeout after Stratum initialization', async () => {
|
||||
const setTimeoutSpy = jest.spyOn(socket, 'setTimeout').mockImplementation(() => socket);
|
||||
jest.spyOn(client as any, 'write').mockImplementation(() => Promise.resolve(true));
|
||||
|
||||
emitMessage(MockRecording1.MINING_SUBSCRIBE);
|
||||
emitMessage(MockRecording1.MINING_AUTHORIZE);
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
expect(setTimeoutSpy).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('should block non-compliant user agents on subscribe without allocating a session', async () => {
|
||||
(configService.get as jest.Mock).mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
@@ -282,6 +293,26 @@ describe('StratumV1Client', () => {
|
||||
expect(socket.write).toHaveBeenCalledWith(`{"id":null,"method":"mining.set_difficulty","params":[512]}\n`, expect.any(Function));
|
||||
});
|
||||
|
||||
it('should clamp suggested difficulty to the configured minimum', async () => {
|
||||
(configService.get as jest.Mock).mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'STRATUM_MIN_DIFFICULTY':
|
||||
return '1';
|
||||
case 'DEV_FEE_ADDRESS':
|
||||
return 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4';
|
||||
case 'NETWORK':
|
||||
return 'testnet';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
jest.spyOn(socket, 'write').mockImplementation((data) => true);
|
||||
|
||||
emitMessage(`{"id":4,"method":"mining.suggest_difficulty","params":[0]}`);
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(socket.write).toHaveBeenCalledWith(`{"id":null,"method":"mining.set_difficulty","params":[1]}\n`, expect.any(Function));
|
||||
});
|
||||
|
||||
it('should set difficulty', async () => {
|
||||
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import { StratumV1ClientStatistics } from './StratumV1ClientStatistics';
|
||||
const TRUE_DIFF_ONE = 2.695953529101131e67;
|
||||
const BLOCKED_USER_AGENT_LOG_INTERVAL_MS = 60 * 1000;
|
||||
const VALIDATION_ERROR_LOG_INTERVAL_MS = 60 * 1000;
|
||||
const DEFAULT_MIN_DIFFICULTY = 0.001;
|
||||
|
||||
export class StratumV1Client {
|
||||
private static blockedUserAgentLogState = new Map<string, { nextLogAt: number, suppressed: number }>();
|
||||
@@ -159,7 +160,7 @@ export class StratumV1Client {
|
||||
|
||||
if (this.sessionStart == null) {
|
||||
this.sessionStart = new Date();
|
||||
this.statistics = new StratumV1ClientStatistics();
|
||||
this.statistics = new StratumV1ClientStatistics(this.getMinimumDifficulty());
|
||||
this.extraNonceAndSessionId = this.getRandomHexString();
|
||||
//console.log(`New client ID: : ${this.extraNonceAndSessionId}, ${this.socket.remoteAddress}:${this.socket.remotePort}`);
|
||||
}
|
||||
@@ -281,7 +282,7 @@ export class StratumV1Client {
|
||||
if (errors.length === 0) {
|
||||
|
||||
this.clientSuggestedDifficulty = suggestDifficultyMessage;
|
||||
this.sessionDifficulty = suggestDifficultyMessage.suggestedDifficulty;
|
||||
this.sessionDifficulty = this.clampDifficulty(suggestDifficultyMessage.suggestedDifficulty);
|
||||
const success = await this.write(JSON.stringify(this.clientSuggestedDifficulty.response(this.sessionDifficulty)) + '\n');
|
||||
if (!success) {
|
||||
return;
|
||||
@@ -369,6 +370,7 @@ export class StratumV1Client {
|
||||
|
||||
private async initStratum() {
|
||||
this.stratumInitialized = true;
|
||||
this.socket.setTimeout(0);
|
||||
|
||||
if (this.isBlockedUserAgent(this.clientSubscription.userAgent)) {
|
||||
this.logBlockedUserAgent(this.clientSubscription.userAgent);
|
||||
@@ -376,12 +378,6 @@ export class StratumV1Client {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.clientSubscription.userAgent) {
|
||||
case 'cpuminer': {
|
||||
this.sessionDifficulty = 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.clientSuggestedDifficulty == null) {
|
||||
//console.log(`Setting difficulty to ${this.sessionDifficulty}`)
|
||||
const setDifficulty = JSON.stringify(new SuggestDifficulty().response(this.sessionDifficulty));
|
||||
@@ -688,7 +684,7 @@ export class StratumV1Client {
|
||||
}
|
||||
|
||||
private async checkDifficulty() {
|
||||
const targetDiff = this.statistics.getSuggestedDifficulty(this.sessionDifficulty);
|
||||
const targetDiff = this.clampDifficulty(this.statistics.getSuggestedDifficulty(this.sessionDifficulty));
|
||||
if (targetDiff == null) {
|
||||
return;
|
||||
}
|
||||
@@ -824,6 +820,27 @@ export class StratumV1Client {
|
||||
return ` sample=${values.join(',')}`;
|
||||
}
|
||||
|
||||
private clampDifficulty(difficulty: number | null): number | null {
|
||||
if (difficulty == null || !Number.isFinite(difficulty)) {
|
||||
return null;
|
||||
}
|
||||
const configuredMinimum = this.getConfiguredMinimumDifficulty();
|
||||
return configuredMinimum == null ? difficulty : Math.max(difficulty, configuredMinimum);
|
||||
}
|
||||
|
||||
private getMinimumDifficulty(): number {
|
||||
return this.getConfiguredMinimumDifficulty() ?? DEFAULT_MIN_DIFFICULTY;
|
||||
}
|
||||
|
||||
private getConfiguredMinimumDifficulty(): number | null {
|
||||
const configured = parseFloat(
|
||||
this.configService.get<string>('STRATUM_MIN_DIFFICULTY')
|
||||
?? process.env.STRATUM_MIN_DIFFICULTY
|
||||
?? '',
|
||||
);
|
||||
return Number.isFinite(configured) && configured > 0 ? configured : null;
|
||||
}
|
||||
|
||||
private closeSocket() {
|
||||
this.connectionClosed = true;
|
||||
if (!this.socket.destroyed) {
|
||||
|
||||
@@ -62,4 +62,11 @@ describe('StratumV1ClientStatistics', () => {
|
||||
|
||||
expect(statistics.getSuggestedDifficulty(128)).toBe(16);
|
||||
});
|
||||
|
||||
it('should not suggest a difficulty below the configured minimum', () => {
|
||||
statistics = new StratumV1ClientStatistics(1);
|
||||
jest.setSystemTime(new Date('2026-05-06T12:06:00Z'));
|
||||
|
||||
expect(statistics.getSuggestedDifficulty(1)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ClientEntity } from '../ORM/client/client.entity';
|
||||
|
||||
const CACHE_SIZE = 30;
|
||||
const MIN_DIFF = 0.001;
|
||||
const DEFAULT_MIN_DIFF = 0.001;
|
||||
export class StratumV1ClientStatistics {
|
||||
|
||||
public targetSubmitShareEveryNSeconds: number = 30;
|
||||
@@ -11,7 +11,7 @@ export class StratumV1ClientStatistics {
|
||||
private submissionCache: { time: Date, difficulty: number }[] = [];
|
||||
private submissionCacheDifficultySum = 0;
|
||||
|
||||
constructor() {
|
||||
constructor(private readonly minDifficulty = DEFAULT_MIN_DIFF) {
|
||||
this.submissionCacheStart = new Date();
|
||||
}
|
||||
|
||||
@@ -67,8 +67,8 @@ export class StratumV1ClientStatistics {
|
||||
if (val === 0) {
|
||||
return null;
|
||||
}
|
||||
if (val < MIN_DIFF) {
|
||||
return MIN_DIFF;
|
||||
if (val < this.minDifficulty) {
|
||||
return this.minDifficulty;
|
||||
}
|
||||
let x = val | (val >> 1);
|
||||
x = x | (x >> 2);
|
||||
@@ -77,8 +77,8 @@ export class StratumV1ClientStatistics {
|
||||
x = x | (x >> 16);
|
||||
x = x | (x >> 32);
|
||||
const res = x - (x >> 1);
|
||||
if (res == 0 && val * 100 < MIN_DIFF) {
|
||||
return MIN_DIFF;
|
||||
if (res == 0 && val * 100 < this.minDifficulty) {
|
||||
return this.minDifficulty;
|
||||
}
|
||||
if (res == 0) {
|
||||
return this.nearestPowerOfTwo(val * 100) / 100;
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
import { Sv2NoiseSession } from './sv2/sv2-noise';
|
||||
|
||||
const DEFAULT_START_DIFFICULTY = 100000;
|
||||
const DEFAULT_MIN_DIFFICULTY = 0.001;
|
||||
const DEFAULT_TARGET_SHARES_PER_MINUTE = 2;
|
||||
const DEFAULT_DIFFICULTY_CHECK_INTERVAL_MS = 60 * 1000;
|
||||
const FIXED_STANDARD_EXTRANONCE2 = '0000000000000000';
|
||||
@@ -141,7 +142,7 @@ export class StratumV2Client {
|
||||
this.sessionDifficulty = this.getInitialDifficulty();
|
||||
this.targetSharesPerMinute = this.getTargetSharesPerMinute();
|
||||
this.difficultyCheckIntervalMs = this.getDifficultyCheckIntervalMs();
|
||||
this.statistics = new StratumV1ClientStatistics();
|
||||
this.statistics = new StratumV1ClientStatistics(this.getMinimumDifficulty());
|
||||
this.statistics.targetSubmitShareEveryNSeconds = 60 / this.targetSharesPerMinute;
|
||||
this.network = this.getNetwork();
|
||||
|
||||
@@ -357,10 +358,10 @@ export class StratumV2Client {
|
||||
this.targetSharesPerMinute,
|
||||
);
|
||||
if (Number.isFinite(calculatedDifficulty) && calculatedDifficulty > 0) {
|
||||
channelDifficulty = calculatedDifficulty;
|
||||
channelDifficulty = this.clampDifficulty(calculatedDifficulty);
|
||||
}
|
||||
}
|
||||
channelDifficulty = DifficultyUtils.clampDifficultyToMaxTarget(channelDifficulty, message.maxTarget);
|
||||
channelDifficulty = this.clampDifficulty(DifficultyUtils.clampDifficultyToMaxTarget(channelDifficulty, message.maxTarget));
|
||||
this.sessionDifficulty = channelDifficulty;
|
||||
|
||||
const channel: ChannelState = {
|
||||
@@ -385,6 +386,7 @@ export class StratumV2Client {
|
||||
}
|
||||
|
||||
await this.ensureClientEntity();
|
||||
this.disableApplicationIdleTimeout();
|
||||
await this.sendFrame(
|
||||
Sv2MsgType.OPEN_STANDARD_MINING_CHANNEL_SUCCESS,
|
||||
serializeOpenStandardMiningChannelSuccess({
|
||||
@@ -449,10 +451,10 @@ export class StratumV2Client {
|
||||
this.targetSharesPerMinute,
|
||||
);
|
||||
if (Number.isFinite(calculatedDifficulty) && calculatedDifficulty > 0) {
|
||||
channelDifficulty = calculatedDifficulty;
|
||||
channelDifficulty = this.clampDifficulty(calculatedDifficulty);
|
||||
}
|
||||
}
|
||||
channelDifficulty = DifficultyUtils.clampDifficultyToMaxTarget(channelDifficulty, message.maxTarget);
|
||||
channelDifficulty = this.clampDifficulty(DifficultyUtils.clampDifficultyToMaxTarget(channelDifficulty, message.maxTarget));
|
||||
this.sessionDifficulty = channelDifficulty;
|
||||
|
||||
const channel: ChannelState = {
|
||||
@@ -477,6 +479,7 @@ export class StratumV2Client {
|
||||
}
|
||||
|
||||
await this.ensureClientEntity();
|
||||
this.disableApplicationIdleTimeout();
|
||||
await this.sendFrame(
|
||||
Sv2MsgType.OPEN_EXTENDED_MINING_CHANNEL_SUCCESS,
|
||||
serializeOpenExtendedMiningChannelSuccess({
|
||||
@@ -797,10 +800,10 @@ export class StratumV2Client {
|
||||
this.targetSharesPerMinute,
|
||||
);
|
||||
if (Number.isFinite(nextDifficulty) && nextDifficulty > 0) {
|
||||
channel.sessionDifficulty = DifficultyUtils.clampDifficultyToMaxTarget(
|
||||
channel.sessionDifficulty = this.clampDifficulty(DifficultyUtils.clampDifficultyToMaxTarget(
|
||||
nextDifficulty,
|
||||
channel.declaredMaxTarget,
|
||||
);
|
||||
));
|
||||
await this.sendSetTarget(channel);
|
||||
}
|
||||
}
|
||||
@@ -876,17 +879,17 @@ export class StratumV2Client {
|
||||
}
|
||||
|
||||
private async checkDifficulty(): Promise<void> {
|
||||
const targetDiff = this.statistics.getSuggestedDifficulty(this.sessionDifficulty);
|
||||
const targetDiff = this.clampDifficulty(this.statistics.getSuggestedDifficulty(this.sessionDifficulty));
|
||||
if (targetDiff == null || targetDiff === this.sessionDifficulty || !Number.isFinite(targetDiff)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sessionDifficulty = targetDiff;
|
||||
for (const channel of this.channels.values()) {
|
||||
channel.sessionDifficulty = DifficultyUtils.clampDifficultyToMaxTarget(
|
||||
channel.sessionDifficulty = this.clampDifficulty(DifficultyUtils.clampDifficultyToMaxTarget(
|
||||
targetDiff,
|
||||
channel.declaredMaxTarget,
|
||||
);
|
||||
));
|
||||
await this.sendSetTarget(channel);
|
||||
}
|
||||
|
||||
@@ -1191,7 +1194,35 @@ export class StratumV2Client {
|
||||
?? this.configService.get<string>('STRATUM_START_DIFFICULTY')
|
||||
?? '',
|
||||
);
|
||||
return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_START_DIFFICULTY;
|
||||
const difficulty = Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_START_DIFFICULTY;
|
||||
return this.clampDifficulty(difficulty);
|
||||
}
|
||||
|
||||
private getMinimumDifficulty(): number {
|
||||
return this.getConfiguredMinimumDifficulty() ?? DEFAULT_MIN_DIFFICULTY;
|
||||
}
|
||||
|
||||
private getConfiguredMinimumDifficulty(): number | null {
|
||||
const configured = parseFloat(
|
||||
this.configService.get<string>('STRATUM_MIN_DIFFICULTY')
|
||||
?? process.env.STRATUM_MIN_DIFFICULTY
|
||||
?? '',
|
||||
);
|
||||
return Number.isFinite(configured) && configured > 0 ? configured : null;
|
||||
}
|
||||
|
||||
private clampDifficulty(difficulty: number | null): number | null {
|
||||
if (difficulty == null || !Number.isFinite(difficulty)) {
|
||||
return null;
|
||||
}
|
||||
const configuredMinimum = this.getConfiguredMinimumDifficulty();
|
||||
return configuredMinimum == null ? difficulty : Math.max(difficulty, configuredMinimum);
|
||||
}
|
||||
|
||||
private disableApplicationIdleTimeout(): void {
|
||||
if (typeof this.socket.setTimeout === 'function') {
|
||||
this.socket.setTimeout(0);
|
||||
}
|
||||
}
|
||||
|
||||
private getTargetSharesPerMinute(): number {
|
||||
|
||||
Reference in New Issue
Block a user