diff --git a/package.json b/package.json index 6ab498d..aab6ca1 100644 --- a/package.json +++ b/package.json @@ -73,4 +73,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} +} \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 09745d1..6e97ea6 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,8 +4,9 @@ import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { BitcoinRpcService } from './bitcoin-rpc.service'; -import { BitcoinStratumProvider } from './bitcoin-stratum.provider'; import { CoinbaseConstructorService } from './coinbase-constructor.service'; +import { StratumV1JobsService } from './stratum-v1-jobs.service'; +import { StratumV1Service } from './stratum-v1.service'; @@ -16,9 +17,10 @@ import { CoinbaseConstructorService } from './coinbase-constructor.service'; controllers: [AppController], providers: [ AppService, - BitcoinStratumProvider, + StratumV1Service, BitcoinRpcService, - CoinbaseConstructorService + CoinbaseConstructorService, + StratumV1JobsService ], }) export class AppModule { diff --git a/src/bitcoin-rpc.service.ts b/src/bitcoin-rpc.service.ts index a1dcc25..59b489d 100644 --- a/src/bitcoin-rpc.service.ts +++ b/src/bitcoin-rpc.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { RPCClient } from 'rpc-bitcoin'; +import { BehaviorSubject, Observable } from 'rxjs'; import { IBlockTemplate } from './models/bitcoin-rpc/IBlockTemplate'; -import { BehaviorSubject, Observable } from 'rxjs'; import { IMiningInfo } from './models/bitcoin-rpc/IMiningInfo'; @@ -30,7 +30,7 @@ export class BitcoinRpcService { setInterval(async () => { const miningInfo = await this.getMiningInfo(); if (miningInfo.blocks > this.blockHeight) { - console.log(miningInfo); + // console.log(miningInfo); if (this.blockHeight != 0) { this.newBlock$.next(miningInfo.blocks + 1); } diff --git a/src/bitcoin-stratum.provider.ts b/src/bitcoin-stratum.provider.ts deleted file mode 100644 index 8fd2581..0000000 --- a/src/bitcoin-stratum.provider.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { Server, Socket } from 'net'; -import { BehaviorSubject, skip, take } from 'rxjs'; - -import { BitcoinRpcService } from './bitcoin-rpc.service'; -import { IBlockTemplate } from './models/bitcoin-rpc/IBlockTemplate'; -import { MiningJob } from './models/MiningJob'; -import { StratumV1Client } from './models/StratumV1Client'; - -@Injectable() -export class BitcoinStratumProvider implements OnModuleInit { - - public clients: StratumV1Client[] = []; - - private server: Server; - - private blockTemplate: IBlockTemplate; - - private interval: NodeJS.Timer; - - private newMiningJobEmitter: BehaviorSubject = new BehaviorSubject(null); - - private latestJob: MiningJob; - - - constructor(private readonly bitcoinRpcService: BitcoinRpcService) { - } - - - async onModuleInit(): Promise { - console.log('onModuleInit'); - - this.server = new Server((socket: Socket) => { - console.log('New client connected:', socket.remoteAddress); - - const client = new StratumV1Client(socket, this.newMiningJobEmitter.asObservable()); - - client.onInitialized.pipe(skip(1), take(1)).subscribe(() => { - console.log('Client Ready') - if (this.latestJob == null) { - return; - } - - this.latestJob.constructResponse(); - client.localMiningJobEmitter.next(this.latestJob); - }); - - // this.clients.push(client); - - // console.log('Number of Clients:', this.clients.length); - - - }); - - this.bitcoinRpcService.newBlock().subscribe(async () => { - console.log('NEW BLOCK') - - this.blockTemplate = await this.bitcoinRpcService.getBlockTemplate(); - this.latestJob = new MiningJob(this.blockTemplate) - - this.latestJob.constructResponse(); - this.newMiningJobEmitter.next(this.latestJob); - clearInterval(this.interval); - this.interval = setInterval(async () => { - this.blockTemplate = await this.bitcoinRpcService.getBlockTemplate(); - - this.latestJob = new MiningJob(this.blockTemplate) - this.latestJob.constructResponse(); - this.newMiningJobEmitter.next(this.latestJob); - }, 60000); - }) - - this.server.listen(3333, () => { - console.log(`Bitcoin Stratum server is listening on port ${3333}`); - }); - - } - - -} \ No newline at end of file diff --git a/src/models/MiningJob.ts b/src/models/MiningJob.ts index b08a822..625b487 100644 --- a/src/models/MiningJob.ts +++ b/src/models/MiningJob.ts @@ -16,7 +16,7 @@ export class MiningJob { public target: string; public merkleRoot: string; - public job_id: number; // ID of the job. Use this ID while submitting share generated from this job. + public job_id: string; // ID of the job. Use this ID while submitting share generated from this job. public prevhash: string; // The hex-encoded previous block hash. public coinb1: string; // The hex-encoded prefix of the coinbase transaction (to precede extra nonce 2). public coinb2: string; //The hex-encoded suffix of the coinbase transaction (to follow extra nonce 2). @@ -32,26 +32,26 @@ export class MiningJob { public tree: MerkleTree; - constructor(blockTemplate: IBlockTemplate) { + constructor(id: string, blockTemplate: IBlockTemplate, _cleanJobs: boolean) { //console.log(blockTemplate); - this.job_id = 1; + this.job_id = id; this.target = blockTemplate.target; this.prevhash = this.convertToLittleEndian(blockTemplate.previousblockhash); this.version = blockTemplate.version; this.nbits = parseInt(blockTemplate.bits, 16); this.ntime = Math.floor(new Date().getTime() / 1000); - this.clean_jobs = false; + 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); + //console.log('TRANSACTION FEES', transactionFees); + //console.log('MINING REWARD', miningReward); const { coinbasePart1, coinbasePart2 } = this.createCoinbaseTransaction([{ address: '', percentage: 100 }], blockTemplate.height, transactionFees + miningReward); @@ -79,10 +79,7 @@ export class MiningJob { this.merkle_branch = this.tree.getProof(coinbaseBuffer).map(p => p.data.toString('hex')); - // let test = this.tree.getRoot(); - // for (let i = 0; i < test.length; i++) { - // console.log(test[i]) - // } + this.constructResponse(); } @@ -99,10 +96,6 @@ export class MiningJob { return currentReward; } - private bufferToHex(buffer: Buffer): string { - return buffer.toString('hex'); - } - private createCoinbaseTransaction(addresses: AddressObject[], blockHeight: number, reward: number): { coinbasePart1: string, coinbasePart2: string } { @@ -149,13 +142,13 @@ export class MiningJob { } - public constructResponse() { + private constructResponse() { const job = { id: 0, method: eResponseMethod.MINING_NOTIFY, params: [ - this.job_id.toString(16), + this.job_id, this.prevhash, this.coinb1, this.coinb2, diff --git a/src/models/StratumV1Client.ts b/src/models/StratumV1Client.ts index 38d1570..4c3fc2f 100644 --- a/src/models/StratumV1Client.ts +++ b/src/models/StratumV1Client.ts @@ -1,9 +1,9 @@ import { plainToInstance } from 'class-transformer'; import { validate, ValidatorOptions } from 'class-validator'; +import * as crypto from 'crypto'; import { Socket } from 'net'; -import { BehaviorSubject, merge, Observable, takeUntil } from 'rxjs'; -import { EasyUnsubscribe } from '../utils/AutoUnsubscribe'; +import { StratumV1JobsService } from '../stratum-v1-jobs.service'; import { eRequestMethod } from './enums/eRequestMethod'; import { MiningJob } from './MiningJob'; import { AuthorizationMessage } from './stratum-messages/AuthorizationMessage'; @@ -12,53 +12,40 @@ import { MiningSubmitMessage } from './stratum-messages/MiningSubmitMessage'; import { SubscriptionMessage } from './stratum-messages/SubscriptionMessage'; import { SuggestDifficulty } from './stratum-messages/SuggestDifficultyMessage'; -export class StratumV1Client extends EasyUnsubscribe { + +export class StratumV1Client { private clientSubscription: SubscriptionMessage; private clientConfiguration: ConfigurationMessage; private clientAuthorization: AuthorizationMessage; private clientSuggestedDifficulty: SuggestDifficulty; - public initialized = false; + // public clientSubmission: BehaviorSubject = new BehaviorSubject(null); + public id: string; + public stratumInitialized = false; - public onInitialized: BehaviorSubject = new BehaviorSubject(null); - public localMiningJobEmitter: BehaviorSubject = new BehaviorSubject(null); - public blockFoundEmitter: BehaviorSubject = new BehaviorSubject(null); - - - private currentJob: MiningJob; + public refreshInterval: NodeJS.Timer; constructor( - private readonly socket: Socket, - private readonly globalMiningJobEmitter: Observable + public readonly socket: Socket, + private readonly stratumV1JobsService: StratumV1JobsService ) { - super(); + + this.id = this.getRandomHexString(); + + console.log(`id: ${this.id}`); this.socket.on('data', this.handleData.bind(this, this.socket)); - this.socket.on('end', () => { - // Handle socket disconnection - console.log('Client disconnected:', socket.remoteAddress); - this.unsubscribeAll(); - }); - - this.socket.on('error', (error: Error) => { - // Handle socket error - console.error('Socket error:', error); - }); - - merge(this.globalMiningJobEmitter, this.localMiningJobEmitter).pipe(takeUntil(this.easyUnsubscribe)).subscribe((job: MiningJob) => { - this.currentJob = job; - if (!this.initialized) { - return; - } - this.socket.write(job.response + '\n'); - }) - - } + private getRandomHexString() { + const randomBytes = crypto.randomBytes(4); // 4 bytes = 32 bits + const randomNumber = randomBytes.readUInt32BE(0); // Convert bytes to a 32-bit unsigned integer + const hexString = randomNumber.toString(16).padStart(8, '0'); // Convert to hex and pad with zeros + return hexString; + } private async handleData(socket: Socket, data: Buffer) { const message = data.toString(); @@ -98,7 +85,7 @@ export class StratumV1Client extends EasyUnsubscribe { if (errors.length === 0) { this.clientSubscription = subscriptionMessage; - socket.write(JSON.stringify(this.clientSubscription.response()) + '\n'); + socket.write(JSON.stringify(this.clientSubscription.response(this.id)) + '\n'); } else { console.error(errors); } @@ -189,9 +176,8 @@ export class StratumV1Client extends EasyUnsubscribe { const errors = await validate(miningSubmitMessage, validatorOptions); if (errors.length === 0) { - //this.clientSuggestedDifficulty = miningSubmitMessage; - miningSubmitMessage.parse(); this.handleMiningSubmission(miningSubmitMessage); + //this.clientSubmission.next(miningSubmitMessage) socket.write(JSON.stringify(miningSubmitMessage.response()) + '\n'); } else { console.error(errors); @@ -200,63 +186,45 @@ export class StratumV1Client extends EasyUnsubscribe { } } - if (!this.initialized) { - if (this.clientSubscription != null - && this.clientConfiguration != null - && this.clientAuthorization != null - && this.clientSuggestedDifficulty != null) { - this.initialized = true; - this.onInitialized.next(); + if (this.clientSubscription != null + && this.clientConfiguration != null + && this.clientAuthorization != null + && this.clientSuggestedDifficulty != null + && this.stratumInitialized == false) { - } + this.stratumInitialized = true; + + this.newBlock(this.stratumV1JobsService.getLatestJob()); } + } + 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) { + submission.parse(); const networkDifficulty = 0; - const diff = submission.testNonceValue(this.currentJob, submission); - console.log('DIFF'); - console.log(diff); + const job = this.stratumV1JobsService.getJobById(submission.jobId); + const diff = submission.testNonceValue(this.id, job, submission); + console.log(`DIFF: ${diff}`); if (networkDifficulty < diff) { - this.blockFoundEmitter.next(true); + //this.clientSubmission.next(true); } } - // private miningNotify() { - // const notification = { - // id: null, - // method: eResponseMethod.MINING_NOTIFY, - // params: [ - // '64756fab0000442e', - // '39dbb5b4e173e1f9ac6f6ad92e9dde300effce6b0003ea860000000000000000', - // '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff35033e1c0c00048fef83640447483e060c', - // '0a636b706f6f6c112f736f6c6f2e636b706f6f6c2e6f72672fffffffff03e1c7872600000000160014876961d21eaba10f0701dcc78f6624a4c682270d384dc900000000001976a914f4cbe6c6bb3a8535c963169c22963d3a20e7686988ac0000000000000000266a24aa21a9ed751521cd94e7780a9a13ac8bb1da5410d30acdd2f6348876835623b04b2dc83b00000000', - // [ - // 'c0c9d351b9e094dd85bc64c8909dece6226269ebfe173bb74ba2f89c51df7066', - // '6c56f47cbfaef5688bb338bc56c4189530f12cdb98f8cc46b6a12053f1e69fdd', - // '697cdaa8d15691f7b30dfe7f6c33957f04cfc8126fed16d2fe38c96adaa59c41', - // 'cbe8aa6e0343884a40b850df8fd3c2ffcc026acec392ce93f9e28619eb0d3dac', - // 'e023091cf0fc02684c77730a34791181a44be92f966ba579aa4cf6e98d754548', - // '6b8fea64efe363e02ff42a4257d76a77381006eade804c8a8c9b96c9c98b1d9e', - // '1c936bc5320cdbbe7348cdd4bf272529822e9d34dfa8e0ee0041eae635891cc3', - // '8fed2682a3c95863c6b9440b1a47abdd3d4230181f61edf0daa2e5f0befbcf65', - // '0837c4d162e1086ec553ea90af4be4f9747958e556598cc38cb08149e58227b1', - // '0b5287e647c7cb6f2fcdf13d5ef3bf091d1137773d7695405d0b2768f442ee78', - // '71eaff9247b5556fa88bca6da2e055d5db8aef2969a2d5c68f8e7efd7d39a283' - // ], - // '20000000', - // '17057e69', - // '6483ef8f', - // true - // ], - - // }; - - // this.socket.write(JSON.stringify(notification) + '\n'); - // } - } \ No newline at end of file diff --git a/src/models/stratum-messages/MiningSubmitMessage.ts b/src/models/stratum-messages/MiningSubmitMessage.ts index deae37d..709a4e8 100644 --- a/src/models/stratum-messages/MiningSubmitMessage.ts +++ b/src/models/stratum-messages/MiningSubmitMessage.ts @@ -43,11 +43,11 @@ export class MiningSubmitMessage extends StratumBaseMessage { } - public testNonceValue(job: MiningJob, submission: MiningSubmitMessage): number { + public testNonceValue(clientId: string, job: MiningJob, submission: MiningSubmitMessage): number { const nonce = parseInt(submission.nonce, 16); const versionMask = parseInt(submission.versionMask, 16); - const extraNonce = 'ccc5d664'; + const extraNonce = clientId; const extraNonce2 = submission.extraNonce2; const coinbaseTx = `${job.coinb1}${extraNonce}${extraNonce2}${job.coinb2}`; diff --git a/src/models/stratum-messages/SubscriptionMessage.ts b/src/models/stratum-messages/SubscriptionMessage.ts index 4715be1..8974d00 100644 --- a/src/models/stratum-messages/SubscriptionMessage.ts +++ b/src/models/stratum-messages/SubscriptionMessage.ts @@ -16,7 +16,7 @@ export class SubscriptionMessage extends StratumBaseMessage { console.log('constructor SubscriptionMessage'); } - public response() { + public response(clientId: string) { return { id: null, error: null, @@ -24,7 +24,7 @@ export class SubscriptionMessage extends StratumBaseMessage { [ ['mining.notify', '64d8c004'] ], //subscription details - 'ccc5d664', //Extranonce1 - Hex-encoded, per-connection unique string which will be used for coinbase serialization later. Keep it safe! + clientId, //Extranonce1 - Hex-encoded, per-connection unique string which will be used for coinbase serialization later. Keep it safe! 8 //Extranonce2_size - Represents expected length of extranonce2 which will be generated by the miner. ] } diff --git a/src/stratum-v1-jobs.service.ts b/src/stratum-v1-jobs.service.ts new file mode 100644 index 0000000..9068a6d --- /dev/null +++ b/src/stratum-v1-jobs.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; + +import { MiningJob } from './models/MiningJob'; + +@Injectable() +export class StratumV1JobsService { + + public latestJobId: number = 1; + + public jobs: MiningJob[] = []; + + + public addJob(job: MiningJob, clearJobs: boolean) { + if (clearJobs) { + this.jobs = []; + } + this.jobs.push(job); + this.latestJobId++; + } + + public getLatestJob() { + return this.jobs[this.jobs.length - 1]; + } + + public getJobById(jobId: string) { + return this.jobs.find(job => job.job_id == jobId); + } + + public getNextId() { + return this.latestJobId.toString(16); + } + + +} \ No newline at end of file diff --git a/src/stratum-v1.service.ts b/src/stratum-v1.service.ts new file mode 100644 index 0000000..cc8025b --- /dev/null +++ b/src/stratum-v1.service.ts @@ -0,0 +1,95 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { Server, Socket } from 'net'; + +import { BitcoinRpcService } from './bitcoin-rpc.service'; +import { MiningJob } from './models/MiningJob'; +import { StratumV1Client } from './models/StratumV1Client'; +import { StratumV1JobsService } from './stratum-v1-jobs.service'; + + +@Injectable() +export class StratumV1Service implements OnModuleInit { + + private miningNotifyInterval: NodeJS.Timer; + + + public clients: StratumV1Client[] = []; + + + + constructor( + private readonly bitcoinRpcService: BitcoinRpcService, + private readonly stratumV1JobsService: StratumV1JobsService + ) { + } + + + async onModuleInit(): Promise { + + this.startSocketServer(); + + this.listenForNewBlocks(); + + } + + private startSocketServer() { + new Server((socket: Socket) => { + + console.log('New client connected:', socket.remoteAddress); + + const client = new StratumV1Client(socket, this.stratumV1JobsService); + this.clients.push(client); + + + socket.on('end', () => { + // Handle socket disconnection + console.log('Client disconnected:', socket.remoteAddress); + this.clients = this.clients.filter(c => c.id == client.id); + }); + + socket.on('error', (error: Error) => { + // Handle socket error + console.error('Socket error:', error); + this.clients = this.clients.filter(c => c.id == client.id); + }); + + }).listen(3333, () => { + console.log(`Bitcoin Stratum server is listening on port ${3333}`); + }); + + } + + 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