From 2aad736628c904191fae3b74189c943af95a9c19 Mon Sep 17 00:00:00 2001 From: Ben Wilson Date: Sat, 10 Jun 2023 13:58:27 -0400 Subject: [PATCH] mining is submitting --- src/bitcoin-rpc.service.ts | 16 +--- src/bitcoin-stratum.provider.ts | 53 +++++++++-- src/models/MiningJob.ts | 69 ++++++++++---- src/models/StratumV1Client.ts | 94 ++++++++++--------- .../SuggestDifficultyMessage.ts | 2 +- src/utils/AutoUnsubscribe.ts | 10 ++ 6 files changed, 164 insertions(+), 80 deletions(-) create mode 100644 src/utils/AutoUnsubscribe.ts diff --git a/src/bitcoin-rpc.service.ts b/src/bitcoin-rpc.service.ts index f08d90f..650a486 100644 --- a/src/bitcoin-rpc.service.ts +++ b/src/bitcoin-rpc.service.ts @@ -1,4 +1,4 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { RPCClient } from 'rpc-bitcoin'; @@ -6,7 +6,7 @@ import { IBlockTempalte } from './models/IBlockTempalte'; @Injectable() -export class BitcoinRpcService implements OnModuleInit { +export class BitcoinRpcService { private client: RPCClient; @@ -21,16 +21,10 @@ export class BitcoinRpcService implements OnModuleInit { console.log('Bitcoin RPC connected'); } - async onModuleInit(): Promise { - console.log('onModuleInit'); - // Asynchronous initialization logic - //await this.getBlockTemplate(); - - return; - } - public async getBlockTemplate() { + + public async getBlockTemplate(): Promise { const result: IBlockTempalte = await this.client.getblocktemplate({ template_request: { @@ -40,7 +34,7 @@ export class BitcoinRpcService implements OnModuleInit { } }); - console.log('Block Template: ', JSON.stringify(result)); + return result; } } diff --git a/src/bitcoin-stratum.provider.ts b/src/bitcoin-stratum.provider.ts index ac3e12f..84c72f2 100644 --- a/src/bitcoin-stratum.provider.ts +++ b/src/bitcoin-stratum.provider.ts @@ -1,34 +1,73 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { Server, Socket } from 'net'; +import { BehaviorSubject, take } from 'rxjs'; -import { CoinbaseConstructorService } from './coinbase-constructor.service'; +import { BitcoinRpcService } from './bitcoin-rpc.service'; +import { IBlockTempalte } from './models/IBlockTempalte'; +import { MiningJob } from './models/MiningJob'; import { StratumV1Client } from './models/StratumV1Client'; @Injectable() -export class BitcoinStratumProvider { +export class BitcoinStratumProvider implements OnModuleInit { public clients: StratumV1Client[] = []; private server: Server; - constructor(private coinbaseConstructorService: CoinbaseConstructorService) { + private blockTemplate: IBlockTempalte; + + private interval: NodeJS.Timer; + + private newMiningJobEmitter: BehaviorSubject = new BehaviorSubject(null); + + private latestJob: MiningJob; + + + constructor(private readonly bitcoinRpcService: BitcoinRpcService) { this.server = new Server((socket: Socket) => { console.log('New client connected:', socket.remoteAddress); - const client = new StratumV1Client(socket); - this.clients.push(client); + const client = new StratumV1Client(socket, this.newMiningJobEmitter.asObservable()); - console.log('Number of Clients:', this.clients.length); + client.onInitialized.pipe(take(1)).subscribe(() => { + if (this.latestJob == null) { + return; + } + const job = this.latestJob.response(); + const jobString = JSON.stringify(job); + client.localMiningJobEmitter.next(jobString); + }); + + // this.clients.push(client); + + // console.log('Number of Clients:', this.clients.length); }); + } + async onModuleInit(): Promise { + console.log('onModuleInit'); + this.blockTemplate = await this.bitcoinRpcService.getBlockTemplate(); + clearInterval(this.interval); + const job = new MiningJob(this.blockTemplate).response(); + this.newMiningJobEmitter.next(JSON.stringify(job)); + this.interval = setInterval(() => { + this.latestJob = new MiningJob(this.blockTemplate); + const job = this.latestJob.response(); + const jobString = JSON.stringify(job); + + this.newMiningJobEmitter.next(jobString); + }, 60000); + + return; + } listen(port: number) { diff --git a/src/models/MiningJob.ts b/src/models/MiningJob.ts index e1b5eac..fda63a3 100644 --- a/src/models/MiningJob.ts +++ b/src/models/MiningJob.ts @@ -1,6 +1,8 @@ import * as crypto from 'crypto'; import { eResponseMethod } from './enums/eResponseMethod'; +import { IBlockTempalte } from './IBlockTempalte'; +import { randomUUID } from 'crypto'; export class MiningJob { public id: number; @@ -18,29 +20,38 @@ export class MiningJob { public ntime: string; // 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. - constructor() { + constructor(blockTemplate: IBlockTempalte) { - } + this.job_id = randomUUID(); + this.prevhash = blockTemplate.previousblockhash; - public response() { - - this.job_id = null; - this.prevhash = null; - this.coinb1 = null; - this.coinb2 = null; - this.merkle_branch = null; - this.version = null; - this.nbits = null; + this.version = blockTemplate.version.toString(); + this.nbits = blockTemplate.bits; this.ntime = Math.floor(new Date().getTime() / 1000).toString(); this.clean_jobs = false; + // Construct coinbase transaction + const coinbaseTransaction = blockTemplate.transactions[0]; + const coinbaseHashBin = this.buildCoinbaseHashBin(coinbaseTransaction.data); + this.coinb1 = this.coinbasePrefix(coinbaseTransaction.data, coinbaseHashBin); + this.coinb2 = ''; // Assuming no suffix is required + + // Calculate merkle branch + const merkleBranch = blockTemplate.transactions.slice(1).map((transaction) => transaction.hash); + this.merkle_branch = this.buildMerkleBranch(merkleBranch, coinbaseHashBin); + + } + + + + public response() { return { - id: this.id, - method: this.method, + id: 0, + method: eResponseMethod.MINING_NOTIFY, params: [ - this.job_id, + '123',///this.job_id, this.prevhash, this.coinb1, this.coinb2, @@ -55,7 +66,18 @@ export class MiningJob { } - public buildCoinbaseHashBin(coinbase: string): Buffer { + private coinbasePrefix(coinbase: string, coinbaseHashBin: Buffer): string { + const coinbaseData = Buffer.from(coinbase, 'hex'); + const coinbaseSize = Buffer.alloc(1, coinbaseData.length); + const extraNoncePlaceholder = Buffer.alloc(4); // Assuming 4 bytes for extra nonce + const concatenatedBuffer = Buffer.concat([coinbaseSize, coinbaseData, extraNoncePlaceholder]); + const merkleRoot = this.doubleSHA(Buffer.concat([coinbaseHashBin, concatenatedBuffer])); + + return merkleRoot.toString('hex'); + } + + + private buildCoinbaseHashBin(coinbase: string): Buffer { const sha256 = crypto.createHash('sha256'); const sha256Digest = sha256.update(Buffer.from(coinbase, 'hex')).digest(); @@ -65,15 +87,28 @@ export class MiningJob { return coinbaseHash; } - public buildMerkleRoot(merkleBranch: string[], coinbaseHashBin: Buffer): string { + private buildMerkleBranch(merkleBranch: string[], coinbaseHashBin: Buffer): string[] { + const merkleRoots: string[] = []; let merkleRoot = coinbaseHashBin; + for (const h of merkleBranch) { const concatenatedBuffer = Buffer.concat([merkleRoot, Buffer.from(h, 'hex')]); merkleRoot = this.doubleSHA(concatenatedBuffer); + merkleRoots.push(merkleRoot.toString('hex')); } - return merkleRoot.toString('hex'); + + return merkleRoots.slice(0, 1); } + // private buildMerkleRoot(merkleBranch: string[], coinbaseHashBin: Buffer): string { + // let merkleRoot = coinbaseHashBin; + // for (const h of merkleBranch) { + // const concatenatedBuffer = Buffer.concat([merkleRoot, Buffer.from(h, 'hex')]); + // merkleRoot = this.doubleSHA(concatenatedBuffer); + // } + // return merkleRoot.toString('hex'); + // } + private doubleSHA(data: Buffer): Buffer { const sha256 = crypto.createHash('sha256'); const sha256Digest = sha256.update(data).digest(); diff --git a/src/models/StratumV1Client.ts b/src/models/StratumV1Client.ts index 4a2df98..63f8260 100644 --- a/src/models/StratumV1Client.ts +++ b/src/models/StratumV1Client.ts @@ -1,17 +1,17 @@ import { plainToInstance } from 'class-transformer'; import { validate, ValidatorOptions } from 'class-validator'; import { Socket } from 'net'; -import { interval } from 'rxjs'; +import { BehaviorSubject, combineLatest, concat, merge, Observable, takeUntil } from 'rxjs'; +import { EasyUnsubscribe } from '../utils/AutoUnsubscribe'; import { eRequestMethod } from './enums/eRequestMethod'; -import { eResponseMethod } from './enums/eResponseMethod'; import { AuthorizationMessage } from './stratum-messages/AuthorizationMessage'; import { ConfigurationMessage } from './stratum-messages/ConfigurationMessage'; import { MiningSubmitMessage } from './stratum-messages/MiningSubmitMessage'; 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; @@ -20,17 +20,23 @@ export class StratumV1Client { public initialized = false; - private interval: NodeJS.Timer; + + public onInitialized: BehaviorSubject = new BehaviorSubject(null); + public localMiningJobEmitter: BehaviorSubject = new BehaviorSubject(null); - - constructor(private readonly socket: Socket) { + constructor( + private readonly socket: Socket, + private readonly globalMiningJobEmitter: Observable + ) { + super(); 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) => { @@ -38,6 +44,13 @@ export class StratumV1Client { console.error('Socket error:', error); }); + merge(this.globalMiningJobEmitter, this.localMiningJobEmitter).pipe(takeUntil(this.easyUnsubscribe)).subscribe((job: string) => { + if (!this.initialized) { + return; + } + this.socket.write(job + '\n'); + }) + } @@ -187,7 +200,7 @@ export class StratumV1Client { && this.clientSuggestedDifficulty != null) { this.initialized = true; - this.manualMiningNotify(); + this.onInitialized.next(); } @@ -195,46 +208,39 @@ export class StratumV1Client { } - private manualMiningNotify() { - clearInterval(this.interval); - this.miningNotify(); - this.interval = setInterval(() => { - this.miningNotify(); - }, 60000); - } - 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 - ], + // 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'); - } + // this.socket.write(JSON.stringify(notification) + '\n'); + // } } \ No newline at end of file diff --git a/src/models/stratum-messages/SuggestDifficultyMessage.ts b/src/models/stratum-messages/SuggestDifficultyMessage.ts index d5a6fae..5ed7bfb 100644 --- a/src/models/stratum-messages/SuggestDifficultyMessage.ts +++ b/src/models/stratum-messages/SuggestDifficultyMessage.ts @@ -20,7 +20,7 @@ export class SuggestDifficulty extends StratumBaseMessage { return { id: null, method: eResponseMethod.SET_DIFFICULTY, - params: [1024] + params: [256] } } } \ No newline at end of file diff --git a/src/utils/AutoUnsubscribe.ts b/src/utils/AutoUnsubscribe.ts new file mode 100644 index 0000000..5a03c40 --- /dev/null +++ b/src/utils/AutoUnsubscribe.ts @@ -0,0 +1,10 @@ +import { Subject } from 'rxjs'; + +export class EasyUnsubscribe { + protected easyUnsubscribe = new Subject(); + + public unsubscribeAll(): void { + this.easyUnsubscribe.next(); + this.easyUnsubscribe.complete(); + } +} \ No newline at end of file