From d37a67aba621aeaeeb3f76f7a9b7b0c4ff2adb5e Mon Sep 17 00:00:00 2001 From: Ben Wilson Date: Tue, 27 Jun 2023 21:06:26 -0400 Subject: [PATCH] adjust client difficulty --- src/app.controller.spec.ts | 23 ----- src/app.controller.ts | 6 -- src/models/MiningJob.ts | 6 +- src/models/StratumV1Client.ts | 93 +++++++++++++++---- src/models/StratumV1ClientStatistics.ts | 56 ++++++++++- .../stratum-messages/AuthorizationMessage.ts | 6 +- .../validators/bitcoin-address.validator.ts | 27 ++++++ 7 files changed, 166 insertions(+), 51 deletions(-) delete mode 100644 src/app.controller.spec.ts create mode 100644 src/models/validators/bitcoin-address.validator.ts diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts deleted file mode 100644 index 8f2126c..0000000 --- a/src/app.controller.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - // expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/src/app.controller.ts b/src/app.controller.ts index e6e24ed..093587c 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -12,12 +12,6 @@ export class AppController { private readonly clientStatisticsService: ClientStatisticsService ) { } - @Get() - async getInfo() { - return { - clients: await this.clientService.connectedClientCount() - } - } @Get('client/:address') async getClientInfo(@Param('address') address: string) { diff --git a/src/models/MiningJob.ts b/src/models/MiningJob.ts index 6e76b63..d32bf62 100644 --- a/src/models/MiningJob.ts +++ b/src/models/MiningJob.ts @@ -158,13 +158,13 @@ export class MiningJob { return bitcoinjs.payments.p2wpkh({ address, network: bitcoinjs.networks.testnet }).output; } case AddressType.p2pkh: { - return bitcoinjs.payments.p2pkh({ address }).output;; + return bitcoinjs.payments.p2pkh({ address }).output; } case AddressType.p2sh: { - return bitcoinjs.payments.p2sh({ address }).output;; + return bitcoinjs.payments.p2sh({ address }).output; } case AddressType.p2tr: { - return bitcoinjs.payments.p2tr({ address }).output;; + return bitcoinjs.payments.p2tr({ address }).output; } case AddressType.p2wsh: { return bitcoinjs.payments.p2wsh({ address }).output; diff --git a/src/models/StratumV1Client.ts b/src/models/StratumV1Client.ts index c1c9e90..78acf16 100644 --- a/src/models/StratumV1Client.ts +++ b/src/models/StratumV1Client.ts @@ -3,7 +3,7 @@ import { plainToInstance } from 'class-transformer'; import { validate, ValidatorOptions } from 'class-validator'; import * as crypto from 'crypto'; import { Socket } from 'net'; -import { combineLatest, interval, startWith } from 'rxjs'; +import { combineLatest, interval, startWith, take } from 'rxjs'; import { ClientStatisticsService } from '../ORM/client-statistics/client-statistics.service'; import { ClientEntity } from '../ORM/client/client.entity'; @@ -12,7 +12,9 @@ import { BitcoinRpcService } from '../services/bitcoin-rpc.service'; import { BlockTemplateService } from '../services/block-template.service'; import { StratumV1JobsService } from '../services/stratum-v1-jobs.service'; import { EasyUnsubscribe } from '../utils/AutoUnsubscribe'; +import { IBlockTemplate } from './bitcoin-rpc/IBlockTemplate'; import { eRequestMethod } from './enums/eRequestMethod'; +import { eResponseMethod } from './enums/eResponseMethod'; import { MiningJob } from './MiningJob'; import { AuthorizationMessage } from './stratum-messages/AuthorizationMessage'; import { ConfigurationMessage } from './stratum-messages/ConfigurationMessage'; @@ -32,7 +34,8 @@ export class StratumV1Client extends EasyUnsubscribe { private statistics: StratumV1ClientStatistics; private stratumInitialized = false; - private clientDifficulty: number = 512; + private usedSuggestedDifficulty = false; + private sessionDifficulty: number = 524288; private entity: ClientEntity; public extraNonce: string; @@ -157,6 +160,9 @@ export class StratumV1Client extends EasyUnsubscribe { break; } case eRequestMethod.SUGGEST_DIFFICULTY: { + if (this.usedSuggestedDifficulty == true) { + return; + } const suggestDifficultyMessage = plainToInstance( SuggestDifficulty, @@ -173,8 +179,9 @@ export class StratumV1Client extends EasyUnsubscribe { if (errors.length === 0) { this.clientSuggestedDifficulty = suggestDifficultyMessage; - this.clientDifficulty = suggestDifficultyMessage.suggestedDifficulty; - socket.write(JSON.stringify(this.clientSuggestedDifficulty.response(this.clientDifficulty)) + '\n'); + this.sessionDifficulty = suggestDifficultyMessage.suggestedDifficulty; + socket.write(JSON.stringify(this.clientSuggestedDifficulty.response(this.sessionDifficulty)) + '\n'); + this.usedSuggestedDifficulty = true; } else { console.error(errors); } @@ -196,6 +203,8 @@ export class StratumV1Client extends EasyUnsubscribe { if (errors.length === 0) { await this.handleMiningSubmission(miningSubmitMessage); socket.write(JSON.stringify(miningSubmitMessage.response()) + '\n'); + + } else { console.error(errors); } @@ -227,22 +236,44 @@ export class StratumV1Client extends EasyUnsubscribe { } lastIntervalCount = interValCount; - const job = new MiningJob(this.stratumV1JobsService.getNextId(), [{ address: this.clientAuthorization.address, percent: 100 }], blockTemplate, clearJobs); + this.sendNewMiningJob(blockTemplate, clearJobs); - this.stratumV1JobsService.addJob(job, clearJobs); + this.checkDifficulty(); - this.socket.write(job.response + '\n'); - }) + + }); } } + private sendNewMiningJob(blockTemplate: IBlockTemplate, clearJobs: boolean) { + + // const payoutInformation = [ + // { address: 'bc1q99n3pu025yyu0jlywpmwzalyhm36tg5u37w20d', percent: 1.8 }, + // { address: this.clientAuthorization.address, percent: 98.2 } + // ]; + + const payoutInformation = [ + { address: this.clientAuthorization.address, percent: 100 } + ]; + + const job = new MiningJob(this.stratumV1JobsService.getNextId(), payoutInformation, blockTemplate, clearJobs); + + this.stratumV1JobsService.addJob(job, clearJobs); + + this.socket.write(job.response + '\n'); + + + + } + private async handleMiningSubmission(submission: MiningSubmitMessage) { const job = this.stratumV1JobsService.getJobById(submission.jobId); // a miner may submit a job that doesn't exist anymore if it was removed by a new block notification if (job == null) { + console.log('job not found') return; } const updatedJobBlock = job.copyAndUpdateBlock( @@ -251,26 +282,56 @@ export class StratumV1Client extends EasyUnsubscribe { this.extraNonce, submission.extraNonce2 ); - const diff = this.calculateDifficulty(updatedJobBlock.toBuffer(true)); - console.log(`DIFF: ${diff}`); + const submissionDifficulty = this.calculateDifficulty(updatedJobBlock.toBuffer(true)); - if (diff >= this.clientDifficulty) { + console.log(`DIFF: ${submissionDifficulty} of ${this.sessionDifficulty}`); - if (diff >= (job.networkDifficulty / 2)) { + if (submissionDifficulty >= this.sessionDifficulty) { + + if (submissionDifficulty >= (job.networkDifficulty / 2)) { console.log('!!! BOCK FOUND !!!'); const blockHex = updatedJobBlock.toHex(false); this.bitcoinRpcService.SUBMIT_BLOCK(blockHex); } - await this.statistics.addSubmission(this.entity, this.clientDifficulty); - if (diff > this.entity.bestDifficulty) { - await this.clientService.updateBestDifficulty(this.extraNonce, diff); - this.entity.bestDifficulty = diff; + await this.statistics.addSubmission(this.entity, this.sessionDifficulty); + if (submissionDifficulty > this.entity.bestDifficulty) { + await this.clientService.updateBestDifficulty(this.extraNonce, submissionDifficulty); + this.entity.bestDifficulty = submissionDifficulty; } } else { console.log(`Difficulty too low`); } + + this.checkDifficulty(); + + } + + private checkDifficulty() { + const targetDiff = this.statistics.getSuggestedDifficulty(this.sessionDifficulty); + if (targetDiff == null) { + return; + } + + if (targetDiff != this.sessionDifficulty) { + console.log(`Adjusting difficulty from ${this.sessionDifficulty} to ${targetDiff}`); + this.sessionDifficulty = targetDiff; + this.socket.write(JSON.stringify( + { + id: null, + method: eResponseMethod.SET_DIFFICULTY, + params: [targetDiff] + } + ) + '\n'); + + // we need to clear the jobs so that the difficulty set takes effect. Otherwise the different miner implementations can cause issues + this.blockTemplateService.currentBlockTemplate$.pipe(take(1)).subscribe(({ blockTemplate }) => { + this.sendNewMiningJob(blockTemplate, true); + }); + + + } } public calculateDifficulty(header: Buffer): number { diff --git a/src/models/StratumV1ClientStatistics.ts b/src/models/StratumV1ClientStatistics.ts index 8a39e7a..a62adfd 100644 --- a/src/models/StratumV1ClientStatistics.ts +++ b/src/models/StratumV1ClientStatistics.ts @@ -1,25 +1,77 @@ import { ClientStatisticsService } from '../ORM/client-statistics/client-statistics.service'; import { ClientEntity } from '../ORM/client/client.entity'; +const CACHE_SIZE = 30; +const TARGET_SUBMISSION_PER_SECOND = 30; export class StratumV1ClientStatistics { + private submissionCacheStart: Date; + private submissionCache = []; constructor(private readonly clientStatisticsService: ClientStatisticsService) { - + this.submissionCacheStart = new Date(); } public async addSubmission(client: ClientEntity, targetDifficulty: number) { + if (this.submissionCache.length > CACHE_SIZE) { + this.submissionCache.shift(); + } + + this.submissionCache.push({ + time: new Date(), + difficulty: targetDifficulty, + }); + await this.clientStatisticsService.save({ time: new Date(), difficulty: targetDifficulty, address: client.address, clientName: client.clientName, sessionId: client.sessionId, - }); } + public getSuggestedDifficulty(clientDifficulty: number) { + + // miner hasn't submitted shares in one minute + if (this.submissionCache.length == 0 && (new Date().getTime() - this.submissionCacheStart.getTime()) / 1000 > 60) { + return this.blpo2(clientDifficulty >> 1); + } + + if (this.submissionCache.length < CACHE_SIZE) { + return null; + } + + const sum = this.submissionCache.reduce((pre, cur) => { + pre += cur.difficulty; + return pre; + }, 0); + const diffSeconds = (this.submissionCache[this.submissionCache.length - 1].time.getTime() - this.submissionCache[0].time.getTime()) / 1000; + + const difficultyPerSecond = sum / diffSeconds; + + const targetDifficulty = difficultyPerSecond * TARGET_SUBMISSION_PER_SECOND; + + if (clientDifficulty << 1 < targetDifficulty || clientDifficulty >> 1 > targetDifficulty) { + return this.blpo2(targetDifficulty) + } + + return null; + + + } + + private blpo2(x) { + x = x | (x >> 1); + x = x | (x >> 2); + x = x | (x >> 4); + x = x | (x >> 8); + x = x | (x >> 16); + x = x | (x >> 32); + return x - (x >> 1); + } + } \ No newline at end of file diff --git a/src/models/stratum-messages/AuthorizationMessage.ts b/src/models/stratum-messages/AuthorizationMessage.ts index 19a1e1d..54f72fa 100644 --- a/src/models/stratum-messages/AuthorizationMessage.ts +++ b/src/models/stratum-messages/AuthorizationMessage.ts @@ -1,7 +1,8 @@ import { Expose, Transform } from 'class-transformer'; -import { ArrayMaxSize, ArrayMinSize, IsArray, IsString } from 'class-validator'; +import { ArrayMaxSize, ArrayMinSize, IsArray, IsString, MaxLength } from 'class-validator'; import { eRequestMethod } from '../enums/eRequestMethod'; +import { IsBitcoinAddress } from '../validators/bitcoin-address.validator'; import { StratumBaseMessage } from './StratumBaseMessage'; export class AuthorizationMessage extends StratumBaseMessage { @@ -16,10 +17,12 @@ export class AuthorizationMessage extends StratumBaseMessage { @Transform(({ value, key, obj, type }) => { return obj.params[0].split('.')[0]; }) + @IsBitcoinAddress() public address: string; @Expose() @IsString() + @MaxLength(64) @Transform(({ value, key, obj, type }) => { return obj.params[0].split('.')[1]; }) @@ -31,6 +34,7 @@ export class AuthorizationMessage extends StratumBaseMessage { @Transform(({ value, key, obj, type }) => { return obj.params[1]; }) + @MaxLength(64) public password: string; constructor() { diff --git a/src/models/validators/bitcoin-address.validator.ts b/src/models/validators/bitcoin-address.validator.ts new file mode 100644 index 0000000..ad247d9 --- /dev/null +++ b/src/models/validators/bitcoin-address.validator.ts @@ -0,0 +1,27 @@ +import { validate } from 'bitcoin-address-validation'; +import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator'; + +@ValidatorConstraint({ name: 'bitcoinAddress', async: false }) +export class BitcoinAddress implements ValidatorConstraintInterface { + validate(value: string): boolean { + // Implement your custom validation logic here + return validate(value); + } + + defaultMessage(): string { + return 'Must be a bitcoin address'; + } +} + +export function IsBitcoinAddress(validationOptions?: ValidationOptions) { + return function (object: Object, propertyName: string) { + registerDecorator({ + name: 'isBitcoinAddress', + target: object.constructor, + propertyName: propertyName, + constraints: [], + options: validationOptions, + validator: BitcoinAddress, + }); + }; +} \ No newline at end of file