Harden accounting and stratum production settings

This commit is contained in:
Ben
2026-06-08 13:27:24 -04:00
parent deaf3e56bd
commit 8252111c32
27 changed files with 414 additions and 183 deletions
+31
View File
@@ -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));
+26 -9
View File
@@ -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);
});
});
+6 -6
View File
@@ -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;
+42 -11
View File
@@ -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 {