From fda3f21b7189f95e40cafb24967202d973362711 Mon Sep 17 00:00:00 2001 From: mr-vcrypto Date: Sun, 23 Feb 2025 14:43:01 -0600 Subject: [PATCH 1/3] feat: external pool shares (#89) * feat:external shares --------- Co-authored-by: mrv777 Co-authored-by: Benjamin Wilson --- .../external-shares/external-shares.entity.ts | 30 ++++++++ .../external-shares/external-shares.module.ts | 13 ++++ .../external-shares.service.ts | 52 ++++++++++++++ src/app.module.ts | 13 ++-- .../external-share.controller.ts | 72 +++++++++++++++++++ src/models/ExternalPoolShare.ts | 27 +++++++ src/models/StratumV1Client.ts | 43 +++++------ src/services/external-shares.service.ts | 31 ++++++++ src/services/stratum-v1.service.ts | 7 +- src/utils/difficulty.utils.ts | 23 ++++++ 10 files changed, 279 insertions(+), 32 deletions(-) create mode 100644 src/ORM/external-shares/external-shares.entity.ts create mode 100644 src/ORM/external-shares/external-shares.module.ts create mode 100644 src/ORM/external-shares/external-shares.service.ts create mode 100644 src/controllers/external-share/external-share.controller.ts create mode 100644 src/models/ExternalPoolShare.ts create mode 100644 src/services/external-shares.service.ts create mode 100644 src/utils/difficulty.utils.ts diff --git a/src/ORM/external-shares/external-shares.entity.ts b/src/ORM/external-shares/external-shares.entity.ts new file mode 100644 index 0000000..a498d69 --- /dev/null +++ b/src/ORM/external-shares/external-shares.entity.ts @@ -0,0 +1,30 @@ +import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; +import { TrackedEntity } from '../utils/TrackedEntity.entity'; + +@Entity() +@Index(['address', 'time']) +export class ExternalSharesEntity extends TrackedEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ length: 62, type: 'varchar' }) + address: string; + + @Column() + clientName: string; + + @Column({ type: 'integer' }) + time: number; + + @Column({ type: 'real' }) + difficulty: number; + + @Column({ length: 128, type: 'varchar', nullable: true }) + userAgent: string; + + @Column({ length: 128, type: 'varchar', nullable: true }) + externalPoolName: string; + + @Column() + header: string; +} diff --git a/src/ORM/external-shares/external-shares.module.ts b/src/ORM/external-shares/external-shares.module.ts new file mode 100644 index 0000000..812c232 --- /dev/null +++ b/src/ORM/external-shares/external-shares.module.ts @@ -0,0 +1,13 @@ +import { Global, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { ExternalSharesEntity } from './external-shares.entity'; +import { ExternalSharesService } from './external-shares.service'; + +@Global() +@Module({ + imports: [TypeOrmModule.forFeature([ExternalSharesEntity])], + providers: [ExternalSharesService], + exports: [TypeOrmModule, ExternalSharesService], +}) +export class ExternalSharesModule { } diff --git a/src/ORM/external-shares/external-shares.service.ts b/src/ORM/external-shares/external-shares.service.ts new file mode 100644 index 0000000..071e5aa --- /dev/null +++ b/src/ORM/external-shares/external-shares.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ExternalSharesEntity } from './external-shares.entity'; + +@Injectable() +export class ExternalSharesService { + constructor( + @InjectRepository(ExternalSharesEntity) + private externalSharesRepository: Repository + ) {} + + public async insert(externalShare: Partial) { + return await this.externalSharesRepository.insert(externalShare); + } + + public async getTopDifficulties(): Promise> { + return await this.externalSharesRepository + .createQueryBuilder('share') + .select('share.userAgent', 'userAgent') + .addSelect('share.time', 'time') + .addSelect('share.externalPoolName', 'externalPoolName') + .addSelect('MAX(share.difficulty)', 'difficulty') + .groupBy('share.address') + .orderBy('MAX(share.difficulty)', 'DESC') + .limit(10) + .getRawMany(); + } + + public async getAddressBestDifficulty(address: string): Promise { + const result = await this.externalSharesRepository + .createQueryBuilder() + .select('MAX(difficulty)', 'maxDifficulty') + .where('address = :address', { address }) + .getRawOne(); + return result?.maxDifficulty || 0; + } + + public async deleteOldShares() { + const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); + return await this.externalSharesRepository + .createQueryBuilder() + .delete() + .from(ExternalSharesEntity) + .where('time < :time', { time: oneDayAgo.getTime() }) + .execute(); + } + + public async deleteAll() { + return await this.externalSharesRepository.delete({}); + } +} diff --git a/src/app.module.ts b/src/app.module.ts index e1a94c1..aac711e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -24,7 +24,9 @@ import { NotificationService } from './services/notification.service'; import { StratumV1JobsService } from './services/stratum-v1-jobs.service'; import { StratumV1Service } from './services/stratum-v1.service'; import { TelegramService } from './services/telegram.service'; - +import { ExternalSharesService } from './services/external-shares.service'; +import { ExternalShareController } from './controllers/external-share/external-share.controller'; +import { ExternalSharesModule } from './ORM/external-shares/external-shares.module'; const ORMModules = [ ClientStatisticsModule, @@ -32,7 +34,8 @@ const ORMModules = [ AddressSettingsModule, TelegramSubscriptionsModule, BlocksModule, - RpcBlocksModule + RpcBlocksModule, + ExternalSharesModule ] @Module({ @@ -56,7 +59,8 @@ const ORMModules = [ controllers: [ AppController, ClientController, - AddressController + AddressController, + ExternalShareController ], providers: [ DiscordService, @@ -68,7 +72,8 @@ const ORMModules = [ BitcoinAddressValidator, StratumV1JobsService, BTCPayService, - BraiinsService + BraiinsService, + ExternalSharesService, ], }) export class AppModule { diff --git a/src/controllers/external-share/external-share.controller.ts b/src/controllers/external-share/external-share.controller.ts new file mode 100644 index 0000000..08f832b --- /dev/null +++ b/src/controllers/external-share/external-share.controller.ts @@ -0,0 +1,72 @@ +import { Body, Controller, Post, Get, UnauthorizedException, Headers } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ExternalPoolShare } from '../../models/ExternalPoolShare'; +import { ExternalSharesService } from '../../ORM/external-shares/external-shares.service'; +import { DifficultyUtils } from '../../utils/difficulty.utils'; +import * as bitcoinjs from 'bitcoinjs-lib'; + +@Controller('share') +export class ExternalShareController { + private readonly apiKey: string; + private readonly minimumDifficulty: number; + + constructor( + private readonly configService: ConfigService, + private readonly externalSharesService: ExternalSharesService, + ) { + this.apiKey = this.configService.get('SHARE_SUBMISSION_API_KEY'); + this.minimumDifficulty = this.configService.get('MINIMUM_DIFFICULTY') || 1000000000000; // 1T + } + + @Get('top-difficulties') + async getTopDifficulties() { + const topDifficulties = await this.externalSharesService.getTopDifficulties(); + return topDifficulties; + } + + @Post() + async submitExternalShare( + @Body() externalShare: ExternalPoolShare, + @Headers('x-api-key') apiKey: string, + ) { + // Only validate API key if one is configured + if (this.apiKey && apiKey !== this.apiKey) { + throw new UnauthorizedException('Invalid API key'); + } + + // Validate the header hash matches claimed difficulty + const headerBuffer = Buffer.from(externalShare.header, 'hex'); + const { submissionDifficulty: difficulty } = DifficultyUtils.calculateDifficulty(headerBuffer); + + // Verify the calculated difficulty matches or exceeds minimum difficulty + if (difficulty < this.minimumDifficulty) { + throw new UnauthorizedException('Share difficulty too low'); + } + + const block = bitcoinjs.Block.fromBuffer(headerBuffer); + + const tenMinutesAgo = Math.floor(Date.now() / 1000) - (10 * 60); + + if (block.timestamp < tenMinutesAgo) { + throw new UnauthorizedException('Share timestamp too old - must be within last 10 minutes'); + } + + // Store share submission + await this.externalSharesService.insert({ + address: externalShare.address, + clientName: externalShare.worker, + time: new Date().getTime(), + difficulty: difficulty, + userAgent: externalShare.userAgent, + externalPoolName: externalShare.externalPoolName, + header: externalShare.header + }); + + console.log(`Accepted external share. ${difficulty}`); + + return { + success: true, + calculatedDifficulty: difficulty, + }; + } +} diff --git a/src/models/ExternalPoolShare.ts b/src/models/ExternalPoolShare.ts new file mode 100644 index 0000000..a0041ff --- /dev/null +++ b/src/models/ExternalPoolShare.ts @@ -0,0 +1,27 @@ +import { IsString, Matches, MaxLength } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { IsBitcoinAddress } from './validators/bitcoin-address.validator'; + +export class ExternalPoolShare { + @IsString() + @MaxLength(64) + worker: string; + + @IsString() + @IsBitcoinAddress() + address: string; + + @IsString() + @MaxLength(128) + userAgent: string; + + @IsString() + @MaxLength(128) + externalPoolName: string; + + @IsString() + @Matches(/^[0-9a-fA-F]+$/, { + message: 'Header must be a valid hex string' + }) + header: string; +} diff --git a/src/models/StratumV1Client.ts b/src/models/StratumV1Client.ts index cb83d15..d3cbc24 100644 --- a/src/models/StratumV1Client.ts +++ b/src/models/StratumV1Client.ts @@ -1,5 +1,4 @@ import { ConfigService } from '@nestjs/config'; -import Big from 'big.js'; import * as bitcoinjs from 'bitcoinjs-lib'; import { plainToInstance } from 'class-transformer'; import { validate, ValidatorOptions } from 'class-validator'; @@ -7,7 +6,6 @@ import * as crypto from 'crypto'; import { Socket } from 'net'; import { firstValueFrom, Subscription } from 'rxjs'; import { clearInterval } from 'timers'; -import { createInterface } from 'readline'; import { AddressSettingsService } from '../ORM/address-settings/address-settings.service'; import { BlocksService } from '../ORM/blocks/blocks.service'; @@ -28,6 +26,8 @@ import { StratumErrorMessage } from './stratum-messages/StratumErrorMessage'; import { SubscriptionMessage } from './stratum-messages/SubscriptionMessage'; import { SuggestDifficulty } from './stratum-messages/SuggestDifficultyMessage'; import { StratumV1ClientStatistics } from './StratumV1ClientStatistics'; +import { ExternalSharesService } from '../services/external-shares.service'; +import { DifficultyUtils } from '../utils/difficulty.utils'; export class StratumV1Client { @@ -63,7 +63,8 @@ export class StratumV1Client { private readonly notificationService: NotificationService, private readonly blocksService: BlocksService, private readonly configService: ConfigService, - private readonly addressSettingsService: AddressSettingsService + private readonly addressSettingsService: AddressSettingsService, + private readonly externalSharesService: ExternalSharesService ) { this.socket.on('data', (data: Buffer) => { @@ -492,7 +493,7 @@ export class StratumV1Client { parseInt(submission.ntime, 16) ); const header = updatedJobBlock.toBuffer(true); - const { submissionDifficulty } = this.calculateDifficulty(header); + const { submissionDifficulty } = DifficultyUtils.calculateDifficulty(header); //console.log(`DIFF: ${submissionDifficulty} of ${this.sessionDifficulty} from ${this.clientAuthorization.worker + '.' + this.extraNonceAndSessionId}`); @@ -549,6 +550,18 @@ export class StratumV1Client { } + const externalShareSubmissionEnabled: boolean = this.configService.get('EXTERNAL_SHARE_SUBMISSION_ENABLED')?.toLowerCase() == 'true'; + const minimumDifficulty: number = parseFloat(this.configService.get('MINIMUM_DIFFICULTY')) || 1000000000000.0; // 1T + if (externalShareSubmissionEnabled && submissionDifficulty >= minimumDifficulty) { + // Submit share to API if enabled + this.externalSharesService.submitShare({ + worker: this.clientAuthorization.worker, + address: this.clientAuthorization.address, + userAgent: this.clientSubscription.userAgent, + header: header.toString('hex'), + externalPoolName: this.configService.get('POOL_IDENTIFIER') || 'Public-Pool' + }); + } } else { const err = new StratumErrorMessage( @@ -596,28 +609,6 @@ export class StratumV1Client { } } - private calculateDifficulty(header: Buffer): { submissionDifficulty: number, submissionHash: string } { - - const hashResult = bitcoinjs.crypto.hash256(header); - - let s64 = this.le256todouble(hashResult); - - const truediffone = Big('26959535291011309493156476344723991336010898738574164086137773096960'); - const difficulty = truediffone.div(s64.toString()); - return { submissionDifficulty: difficulty.toNumber(), submissionHash: hashResult.toString('hex') }; - } - - - private le256todouble(target: Buffer): bigint { - - const number = target.reduceRight((acc, byte) => { - // Shift the number 8 bits to the left and OR with the current byte - return (acc << BigInt(8)) | BigInt(byte); - }, BigInt(0)); - - return number; - } - private async write(message: string): Promise { try { if (!this.socket.destroyed && !this.socket.writableEnded) { diff --git a/src/services/external-shares.service.ts b/src/services/external-shares.service.ts new file mode 100644 index 0000000..45d0004 --- /dev/null +++ b/src/services/external-shares.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { ExternalPoolShare } from '../models/ExternalPoolShare'; + +@Injectable() +export class ExternalSharesService { + private readonly shareApiUrl: string; + private readonly shareApiKey: string; + + constructor( + private readonly configService: ConfigService, + private readonly httpService: HttpService + ) { + this.shareApiUrl = this.configService.get('SHARE_SUBMISSION_URL') || 'https://web.public-pool.io'; + this.shareApiKey = this.configService.get('SHARE_SUBMISSION_API_KEY'); + } + + public submitShare(share: ExternalPoolShare): void { + this.httpService.post(`${this.shareApiUrl}/api/share`, share, { + headers: { + 'x-api-key': this.shareApiKey + } + }).subscribe({ + next: () =>{ + console.log('External share accepted'); + }, + error: (error) => console.error('Failed to submit share to API:', error.message) + }); + } +} diff --git a/src/services/stratum-v1.service.ts b/src/services/stratum-v1.service.ts index ca43df4..3c156e2 100644 --- a/src/services/stratum-v1.service.ts +++ b/src/services/stratum-v1.service.ts @@ -10,6 +10,7 @@ import { ClientService } from '../ORM/client/client.service'; import { BitcoinRpcService } from './bitcoin-rpc.service'; import { NotificationService } from './notification.service'; import { StratumV1JobsService } from './stratum-v1-jobs.service'; +import { ExternalSharesService } from './external-shares.service'; @Injectable() @@ -23,7 +24,8 @@ export class StratumV1Service implements OnModuleInit { private readonly blocksService: BlocksService, private readonly configService: ConfigService, private readonly stratumV1JobsService: StratumV1JobsService, - private readonly addressSettingsService: AddressSettingsService + private readonly addressSettingsService: AddressSettingsService, + private readonly externalSharesService: ExternalSharesService ) { } @@ -54,7 +56,8 @@ export class StratumV1Service implements OnModuleInit { this.notificationService, this.blocksService, this.configService, - this.addressSettingsService + this.addressSettingsService, + this.externalSharesService ); diff --git a/src/utils/difficulty.utils.ts b/src/utils/difficulty.utils.ts new file mode 100644 index 0000000..168f5cd --- /dev/null +++ b/src/utils/difficulty.utils.ts @@ -0,0 +1,23 @@ +import Big from 'big.js'; +import * as bitcoinjs from 'bitcoinjs-lib'; + +export class DifficultyUtils { + static calculateDifficulty(header: Buffer): { submissionDifficulty: number; submissionHash: string } { + const hashResult = bitcoinjs.crypto.hash256(Buffer.isBuffer(header) ? header : Buffer.from(header, 'hex')); + const s64 = DifficultyUtils.le256todouble(hashResult); + const truediffone = Big('26959535291011309493156476344723991336010898738574164086137773096960'); + const difficulty = truediffone.div(s64.toString()); + + return { + submissionDifficulty: difficulty.toNumber(), + submissionHash: hashResult.toString('hex') + }; + } + + private static le256todouble(target: Buffer): bigint { + const number = target.reduceRight((acc, byte) => { + return (acc << BigInt(8)) | BigInt(byte); + }, BigInt(0)); + return number; + } +} From 554a8916a7441262c2d4daaa9e8be7bd10fe0a9e Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Sat, 5 Apr 2025 00:37:45 +0700 Subject: [PATCH 2/3] Fix incorrect default POOL_IDENTIFIER valie in .env.example (#103) --- .env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 21dbe8c..2b238d0 100644 --- a/.env.example +++ b/.env.example @@ -34,5 +34,5 @@ DEV_FEE_ADDRESS= NETWORK=mainnet API_SECURE=false -# Default is "public-pool", you can change it to any string it will be removed if it will make the block or coinbase script too big -POOL_IDENTIFIER="public-pool" +# Default is "Public-Pool", you can change it to any string it will be removed if it will make the block or coinbase script too big +POOL_IDENTIFIER="Public-Pool" From fd775965e5f90f0814302edee8690839af67a603 Mon Sep 17 00:00:00 2001 From: KillerInk <30864229+KillerInk@users.noreply.github.com> Date: Fri, 4 Apr 2025 19:40:02 +0200 Subject: [PATCH 3/3] cherry pick hashrate fix from pqsql branch (#102) --- src/models/StratumV1Client.ts | 2 +- src/models/StratumV1ClientStatistics.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/models/StratumV1Client.ts b/src/models/StratumV1Client.ts index d3cbc24..0cb2207 100644 --- a/src/models/StratumV1Client.ts +++ b/src/models/StratumV1Client.ts @@ -390,7 +390,7 @@ export class StratumV1Client { //50Th/s this.noFee = false; if (this.entity) { - this.hashRate = await this.clientStatisticsService.getHashRateForSession(this.clientAuthorization.address, this.clientAuthorization.worker, this.extraNonceAndSessionId); + this.hashRate = this.statistics.hashRate; this.noFee = this.hashRate != 0 && this.hashRate < 50000000000000; } if (this.noFee || devFeeAddress == null || devFeeAddress.length < 1) { diff --git a/src/models/StratumV1ClientStatistics.ts b/src/models/StratumV1ClientStatistics.ts index c5c5fc9..d00410a 100644 --- a/src/models/StratumV1ClientStatistics.ts +++ b/src/models/StratumV1ClientStatistics.ts @@ -14,6 +14,13 @@ export class StratumV1ClientStatistics { private currentTimeSlot: number = null; private lastSave: number = null; + + public hashRate = 0; + + private previousTimeSlotTime: Date; + private currentTimeSlotTime: Date; + + private previousShares: number = 0; constructor( private readonly clientStatisticsService: ClientStatisticsService @@ -42,6 +49,8 @@ export class StratumV1ClientStatistics { if (this.currentTimeSlot == null) { // First record, insert it + this.previousTimeSlotTime = new Date(); + this.currentTimeSlotTime = new Date(); this.currentTimeSlot = timeSlot; this.shares += targetDifficulty; this.acceptedCount++; @@ -65,6 +74,9 @@ export class StratumV1ClientStatistics { clientName: client.clientName, sessionId: client.sessionId }); + this.previousShares = this.shares; + this.previousTimeSlotTime = this.currentTimeSlotTime; + this.currentTimeSlotTime = new Date(); // Set the new time slot and add incoming shares then insert it this.currentTimeSlot = timeSlot; this.shares = targetDifficulty; @@ -96,6 +108,10 @@ export class StratumV1ClientStatistics { // saving to memory for storing later this.shares += targetDifficulty; this.acceptedCount++; + if(this.shares > 0) { + const time = new Date().getTime() - this.previousTimeSlotTime.getTime(); + this.hashRate = ((this.previousShares + this.shares) * 4294967296) / (time / 1000); + } } }