From 916d5f20e689933bd9bea6b23893d545885373ac Mon Sep 17 00:00:00 2001 From: Ben Wilson Date: Sat, 17 Jun 2023 23:36:14 -0400 Subject: [PATCH] cleaning up --- src/BlockTemplateService.ts | 23 +++++++ src/app.module.ts | 6 +- src/bitcoin-rpc.service.ts | 16 +++-- src/coinbase-constructor.service.ts | 14 ----- src/models/MiningJob.ts | 62 +++++-------------- src/models/StratumV1Client.ts | 57 ++++++++++------- .../stratum-messages/AuthorizationMessage.ts | 12 +++- .../stratum-messages/MiningSubmitMessage.ts | 12 +--- src/stratum-v1-jobs.service.ts | 4 +- src/stratum-v1.service.ts | 39 ++---------- 10 files changed, 101 insertions(+), 144 deletions(-) create mode 100644 src/BlockTemplateService.ts delete mode 100644 src/coinbase-constructor.service.ts diff --git a/src/BlockTemplateService.ts b/src/BlockTemplateService.ts new file mode 100644 index 0000000..13e9868 --- /dev/null +++ b/src/BlockTemplateService.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { from, map, Observable, shareReplay, switchMap, tap } from 'rxjs'; + +import { BitcoinRpcService } from './bitcoin-rpc.service'; +import { IBlockTemplate } from './models/bitcoin-rpc/IBlockTemplate'; +import { IMiningInfo } from './models/bitcoin-rpc/IMiningInfo'; + +@Injectable() +export class BlockTemplateService { + + public currentBlockTemplate: IBlockTemplate; + + public currentBlockTemplate$: Observable<{ miningInfo: IMiningInfo, blockTemplate: IBlockTemplate }>; + + constructor(private readonly bitcoinRpcService: BitcoinRpcService) { + this.currentBlockTemplate$ = this.bitcoinRpcService.newBlock$.pipe( + switchMap((miningInfo) => from(this.bitcoinRpcService.getBlockTemplate()).pipe(map(blockTemplate => { return { miningInfo, blockTemplate } }))), + tap(({ miningInfo, blockTemplate }) => this.currentBlockTemplate = blockTemplate), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + } + +} \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 6e97ea6..f98dbf1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,8 +4,7 @@ import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { BitcoinRpcService } from './bitcoin-rpc.service'; -import { CoinbaseConstructorService } from './coinbase-constructor.service'; -import { StratumV1JobsService } from './stratum-v1-jobs.service'; +import { BlockTemplateService } from './BlockTemplateService'; import { StratumV1Service } from './stratum-v1.service'; @@ -19,8 +18,7 @@ import { StratumV1Service } from './stratum-v1.service'; AppService, StratumV1Service, BitcoinRpcService, - CoinbaseConstructorService, - StratumV1JobsService + BlockTemplateService ], }) export class AppModule { diff --git a/src/bitcoin-rpc.service.ts b/src/bitcoin-rpc.service.ts index 59b489d..eb1de45 100644 --- a/src/bitcoin-rpc.service.ts +++ b/src/bitcoin-rpc.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { RPCClient } from 'rpc-bitcoin'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, filter } from 'rxjs'; import { IBlockTemplate } from './models/bitcoin-rpc/IBlockTemplate'; import { IMiningInfo } from './models/bitcoin-rpc/IMiningInfo'; @@ -12,7 +12,8 @@ export class BitcoinRpcService { private blockHeight = 0; private client: RPCClient; - private newBlock$: BehaviorSubject = new BehaviorSubject(0); + private _newBlock$: BehaviorSubject = new BehaviorSubject(undefined); + public newBlock$ = this._newBlock$.pipe(filter(block => block != null)); constructor(configService: ConfigService) { const url = configService.get('BITCOIN_RPC_URL'); @@ -26,14 +27,14 @@ export class BitcoinRpcService { console.log('Bitcoin RPC connected'); + // Maybe use ZeroMQ ? setInterval(async () => { const miningInfo = await this.getMiningInfo(); if (miningInfo.blocks > this.blockHeight) { - // console.log(miningInfo); - if (this.blockHeight != 0) { - this.newBlock$.next(miningInfo.blocks + 1); - } + + this._newBlock$.next(miningInfo); + this.blockHeight = miningInfo.blocks; } @@ -43,9 +44,6 @@ export class BitcoinRpcService { } - public newBlock(): Observable { - return this.newBlock$.asObservable(); - } public async getBlockTemplate(): Promise { diff --git a/src/coinbase-constructor.service.ts b/src/coinbase-constructor.service.ts deleted file mode 100644 index dead721..0000000 --- a/src/coinbase-constructor.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { BitcoinRpcService } from './bitcoin-rpc.service'; - - -@Injectable() -export class CoinbaseConstructorService { - - constructor(private bitcoinRPCService: BitcoinRpcService) { - - } - - -} \ No newline at end of file diff --git a/src/models/MiningJob.ts b/src/models/MiningJob.ts index 00c5059..eefe5d4 100644 --- a/src/models/MiningJob.ts +++ b/src/models/MiningJob.ts @@ -7,7 +7,7 @@ import { eResponseMethod } from './enums/eResponseMethod'; interface AddressObject { address: string; - percentage: number; + percent: number; } export class MiningJob { public id: number; @@ -25,7 +25,7 @@ export class MiningJob { public version: number; // The hex-encoded block version. public nbits: number; // The hex-encoded network difficulty required for the block. public ntime: number; // Current ntime/ - public clean_jobs: boolean; // When true, server indicates that submitting shares from previous jobs don't have a sense and such shares will be rejected. When this flag is set, miner should also drop all previous jobs too. + //public clean_jobs: boolean; // When true, server indicates that submitting shares from previous jobs don't have a sense and such shares will be rejected. When this flag is set, miner should also drop all previous jobs too. public response: string; @@ -33,9 +33,8 @@ export class MiningJob { public tree: MerkleTree; - constructor(id: string, public blockTemplate: IBlockTemplate, _cleanJobs: boolean) { + constructor(id: string, payoutInformation: AddressObject[], public blockTemplate: IBlockTemplate, public networkDifficulty: number, public clean_jobs: boolean) { - //console.log(blockTemplate); this.job_id = id; this.target = blockTemplate.target; @@ -44,17 +43,14 @@ export class MiningJob { this.version = blockTemplate.version; this.nbits = parseInt(blockTemplate.bits, 16); this.ntime = Math.floor(new Date().getTime() / 1000); - this.clean_jobs = _cleanJobs; const transactionFees = blockTemplate.transactions.reduce((pre, cur, i, arr) => { return pre + cur.fee; }, 0); - const miningReward = this.calculateMiningReward(blockTemplate.height); - //console.log('TRANSACTION FEES', transactionFees); - //console.log('MINING REWARD', miningReward); - const { coinbasePart1, coinbasePart2 } = this.createCoinbaseTransaction([{ address: 'bc1qsa5kr5s74wss7pcpmnrc7e3y5nrgyfcdzpwakk', percentage: 100 }], blockTemplate.height, transactionFees + miningReward); + + const { coinbasePart1, coinbasePart2 } = this.createCoinbaseTransaction(payoutInformation, blockTemplate.height, blockTemplate.coinbasevalue); this.coinb1 = coinbasePart1; this.coinb2 = coinbasePart2; @@ -62,19 +58,12 @@ export class MiningJob { const coinbaseHash = this.sha256(this.coinb1 + this.coinb2).toString('hex'); const coinbaseBuffer = Buffer.from(coinbaseHash, 'hex'); - //transactions.unshift(coinbaseHash); // Calculate merkle branch const transactionBuffers = blockTemplate.transactions.map(tx => Buffer.from(tx.hash, 'hex')); transactionBuffers.unshift(coinbaseBuffer); this.tree = new MerkleTree(transactionBuffers, this.sha256, { isBitcoinTree: true }); - - // // this.merkle_branch = tree.getProof(coinbaseBuffer).map(p => p.data.toString('hex')) - - // this.merkle_branch = tree.getLayers().map(l => l.pop().toString('hex')); - // this.merkleRoot = this.merkle_branch.pop(); - const rootBuffer = this.tree.getRoot(); this.merkleRoot = rootBuffer.toString('hex'); this.merkle_branch = this.tree.getProof(coinbaseBuffer).map(p => p.data.toString('hex')); @@ -84,30 +73,12 @@ export class MiningJob { } - private calculateMiningReward(blockHeight: number): number { - const initialBlockReward = 50 * 1e8; // Initial block reward in satoshis (1 BTC = 100 million satoshis) - const halvingInterval = 210000; // Number of blocks after which the reward halves - - // Calculate the number of times the reward has been halved - const halvingCount = Math.floor(blockHeight / halvingInterval); - - // Calculate the current block reward in satoshis - const currentReward = initialBlockReward / Math.pow(2, halvingCount); - - return currentReward; - } - - - private createCoinbaseTransaction(addresses: AddressObject[], blockHeight: number, reward: number): { coinbasePart1: string, coinbasePart2: string } { - // Generate coinbase script + // Part 1 const blockHeightScript = `03${blockHeight.toString(16).padStart(8, '0')}`; - - - // Create coinbase transaction const outputIndex = 'ffffffff'; - const sequence = 'ffffffff'; - const lockTime = '00000000'; + + const version = '01000000'; const inputCount = '01'; const fakeCoinbaseInput = '0000000000000000000000000000000000000000000000000000000000000000'; @@ -115,15 +86,14 @@ export class MiningJob { const inputScriptBytes = ((blockHeightScript.length + 16) / 2).toString(16).padStart(2, '0'); - const inputTransaction = inputCount + fakeCoinbaseInput + outputIndex + inputScriptBytes + blockHeightScript; + const coinbasePart1 = version + inputCount + fakeCoinbaseInput + outputIndex + inputScriptBytes + blockHeightScript; - //let remainingPayout = reward; - + // Part 2 const outputs = addresses .map((addressObj) => { - const percentage = addressObj.percentage / 100; + const percentage = addressObj.percent / 100; const satoshis = Math.floor(reward * percentage); const satoshiBuff = Buffer.alloc(4); satoshiBuff.writeUInt32LE(satoshis); @@ -135,14 +105,13 @@ export class MiningJob { }) .join(''); - // // Distribute any remaining satoshis to the first address - // const firstAddressSatoshis = (parseInt(outputs.substring(0, 16), 16) + remainingPayout).toString(16).padStart(16, '0'); - // Combine coinbasePart1 and coinbasePart2 - // Create outputs const outputCountHex = addresses.length.toString(16).padStart(2, '0'); - const coinbasePart1 = version + inputTransaction; + + const sequence = 'ffffffff'; + const lockTime = '00000000'; + const coinbasePart2 = sequence + outputCountHex + outputs + lockTime; return { coinbasePart1, coinbasePart2 }; @@ -184,4 +153,5 @@ export class MiningJob { + } \ No newline at end of file diff --git a/src/models/StratumV1Client.ts b/src/models/StratumV1Client.ts index 508d9b3..6018574 100644 --- a/src/models/StratumV1Client.ts +++ b/src/models/StratumV1Client.ts @@ -2,8 +2,11 @@ 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 { BlockTemplateService } from '../BlockTemplateService'; import { StratumV1JobsService } from '../stratum-v1-jobs.service'; +import { EasyUnsubscribe } from '../utils/AutoUnsubscribe'; import { eRequestMethod } from './enums/eRequestMethod'; import { MiningJob } from './MiningJob'; import { AuthorizationMessage } from './stratum-messages/AuthorizationMessage'; @@ -13,7 +16,7 @@ import { SubscriptionMessage } from './stratum-messages/SubscriptionMessage'; import { SuggestDifficulty } from './stratum-messages/SuggestDifficultyMessage'; -export class StratumV1Client { +export class StratumV1Client extends EasyUnsubscribe { private clientSubscription: SubscriptionMessage; private clientConfiguration: ConfigurationMessage; @@ -29,11 +32,15 @@ export class StratumV1Client { public clientDifficulty: number = 512; + public jobRefreshInterval: NodeJS.Timer; + + constructor( public readonly socket: Socket, - private readonly stratumV1JobsService: StratumV1JobsService + private readonly stratumV1JobsService: StratumV1JobsService, + private readonly blockTemplateService: BlockTemplateService ) { - + super(); this.id = this.getRandomHexString(); console.log(`id: ${this.id}`); @@ -181,7 +188,6 @@ export class StratumV1Client { if (errors.length === 0) { this.handleMiningSubmission(miningSubmitMessage); - //this.clientSubmission.next(miningSubmitMessage) socket.write(JSON.stringify(miningSubmitMessage.response()) + '\n'); } else { console.error(errors); @@ -199,35 +205,44 @@ export class StratumV1Client { this.stratumInitialized = true; - this.newBlock(this.stratumV1JobsService.getLatestJob()); - } - } + let lastIntervalCount = undefined; + combineLatest([this.blockTemplateService.currentBlockTemplate$, interval(60000).pipe(startWith(-1))]).subscribe(([{ miningInfo, blockTemplate }, interValCount]) => { + let clearJobs = false; + if (lastIntervalCount === interValCount) { + clearJobs = true; + } + lastIntervalCount = interValCount; + + const job = new MiningJob(this.stratumV1JobsService.getNextId(), [{ address: this.clientAuthorization.address, percent: 100 }], blockTemplate, miningInfo.difficulty, clearJobs); + + this.stratumV1JobsService.addJob(job, clearJobs); + + this.socket.write(job.response + '\n'); + }) + - public newBlock(job: MiningJob) { - this.newJob(job); - clearInterval(this.refreshInterval); - this.refreshInterval = setInterval(async () => { - this.newJob(this.stratumV1JobsService.getLatestJob()); - }, 60000); - } - private newJob(job: MiningJob) { - if (this.stratumInitialized && job != null) { - this.socket.write(job.response + '\n'); } } private handleMiningSubmission(submission: MiningSubmitMessage) { - const networkDifficulty = 0; + const job = this.stratumV1JobsService.getJobById(submission.jobId); - const diff = submission.testNonceValue(this.id, job, submission); + const diff = submission.calculateDifficulty(this.id, job, submission); console.log(`DIFF: ${diff}`); - if (networkDifficulty < diff) { - //this.clientSubmission.next(true); + if (diff >= this.clientDifficulty) { + + if (diff >= job.networkDifficulty) { + console.log('!!! BOCK FOUND !!!'); + } + } else { + console.log(`Difficulty too low`); } + + } } \ No newline at end of file diff --git a/src/models/stratum-messages/AuthorizationMessage.ts b/src/models/stratum-messages/AuthorizationMessage.ts index 63ecd7c..19a1e1d 100644 --- a/src/models/stratum-messages/AuthorizationMessage.ts +++ b/src/models/stratum-messages/AuthorizationMessage.ts @@ -14,9 +14,17 @@ export class AuthorizationMessage extends StratumBaseMessage { @Expose() @IsString() @Transform(({ value, key, obj, type }) => { - return obj.params[0]; + return obj.params[0].split('.')[0]; }) - public username: string; + public address: string; + + @Expose() + @IsString() + @Transform(({ value, key, obj, type }) => { + return obj.params[0].split('.')[1]; + }) + public worker: string; + @Expose() @IsString() diff --git a/src/models/stratum-messages/MiningSubmitMessage.ts b/src/models/stratum-messages/MiningSubmitMessage.ts index 9719d70..c1cc97c 100644 --- a/src/models/stratum-messages/MiningSubmitMessage.ts +++ b/src/models/stratum-messages/MiningSubmitMessage.ts @@ -65,7 +65,7 @@ export class MiningSubmitMessage extends StratumBaseMessage { } - public testNonceValue(clientId: string, job: MiningJob, submission: MiningSubmitMessage): number { + public calculateDifficulty(clientId: string, job: MiningJob, submission: MiningSubmitMessage): number { const nonce = parseInt(submission.nonce, 16); const versionMask = parseInt(submission.versionMask, 16); @@ -76,13 +76,10 @@ export class MiningSubmitMessage extends StratumBaseMessage { const newRoot = this.calculateMerkleRootHash(coinbaseTx, job.merkle_branch) - const truediffone = Big('26959535291011309493156476344723991336010898738574164086137773096960'); const header = Buffer.alloc(80); - // TODO: Use the midstate hash instead of hashing the whole header - let version = job.version; if (versionMask !== undefined && versionMask != 0) { version = (version ^ versionMask); @@ -97,10 +94,6 @@ export class MiningSubmitMessage extends StratumBaseMessage { header.writeBigUint64LE(BigInt(job.nbits), 72); header.writeUInt32LE(nonce, 76); - // for (let i = 0; i < 80; i++) { - // console.log(header[i].toString(10)); - // } - const hashBuffer: Buffer = crypto.createHash('sha256').update(header).digest(); const hashResult: Buffer = crypto.createHash('sha256').update(hashBuffer).digest(); @@ -108,7 +101,7 @@ export class MiningSubmitMessage extends StratumBaseMessage { let s64 = this.le256todouble(hashResult); - return parseInt(truediffone.div(s64.toString()).toString()); + return truediffone.div(s64.toString()).toNumber(); } @@ -132,7 +125,6 @@ export class MiningSubmitMessage extends StratumBaseMessage { }, BigInt(0)); return number; - } private calculateMerkleRootHash(coinbaseTx: string, merkleBranches: string[]): Buffer { diff --git a/src/stratum-v1-jobs.service.ts b/src/stratum-v1-jobs.service.ts index 9068a6d..974c359 100644 --- a/src/stratum-v1-jobs.service.ts +++ b/src/stratum-v1-jobs.service.ts @@ -1,8 +1,6 @@ -import { Injectable } from '@nestjs/common'; - import { MiningJob } from './models/MiningJob'; -@Injectable() + export class StratumV1JobsService { public latestJobId: number = 1; diff --git a/src/stratum-v1.service.ts b/src/stratum-v1.service.ts index cc8025b..a15b946 100644 --- a/src/stratum-v1.service.ts +++ b/src/stratum-v1.service.ts @@ -2,7 +2,7 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { Server, Socket } from 'net'; import { BitcoinRpcService } from './bitcoin-rpc.service'; -import { MiningJob } from './models/MiningJob'; +import { BlockTemplateService } from './BlockTemplateService'; import { StratumV1Client } from './models/StratumV1Client'; import { StratumV1JobsService } from './stratum-v1-jobs.service'; @@ -19,7 +19,7 @@ export class StratumV1Service implements OnModuleInit { constructor( private readonly bitcoinRpcService: BitcoinRpcService, - private readonly stratumV1JobsService: StratumV1JobsService + private readonly blockTemplateService: BlockTemplateService ) { } @@ -28,7 +28,7 @@ export class StratumV1Service implements OnModuleInit { this.startSocketServer(); - this.listenForNewBlocks(); + //this.listenForNewBlocks(); } @@ -37,7 +37,7 @@ export class StratumV1Service implements OnModuleInit { console.log('New client connected:', socket.remoteAddress); - const client = new StratumV1Client(socket, this.stratumV1JobsService); + const client = new StratumV1Client(socket, new StratumV1JobsService(), this.blockTemplateService); this.clients.push(client); @@ -59,37 +59,6 @@ export class StratumV1Service implements OnModuleInit { } - private listenForNewBlocks() { - this.bitcoinRpcService.newBlock().subscribe(async () => { - console.log('NEW BLOCK') - this.resetMiningNotifyInterval(); - const blockTemplate = await this.bitcoinRpcService.getBlockTemplate(); - const job = new MiningJob(this.stratumV1JobsService.getNextId(), blockTemplate, true); - this.stratumV1JobsService.addJob(job, true); - - - this.clients - .filter(client => client.stratumInitialized) - .forEach(client => { - client.newBlock(job); - }); - - }); - } - - - - - private resetMiningNotifyInterval() { - clearInterval(this.miningNotifyInterval); - this.miningNotifyInterval = setInterval(async () => { - - const blockTemplate = await this.bitcoinRpcService.getBlockTemplate(); - const job = new MiningJob(this.stratumV1JobsService.getNextId(), blockTemplate, false); - this.stratumV1JobsService.addJob(job, false); - - }, 60000); - } } \ No newline at end of file