From 57b5b1ed538106046e73729af4450a0e491919af Mon Sep 17 00:00:00 2001 From: Ben Wilson Date: Sun, 16 Jul 2023 16:36:12 -0400 Subject: [PATCH] refactor project to use one singleton for blocks (memory optimization) --- .vscode/settings.json | 1 + src/app.module.ts | 2 - src/models/MiningJob.ts | 97 +++++----------- src/models/StratumV1Client.spec.ts | 16 +-- src/models/StratumV1Client.ts | 69 ++++-------- src/services/block-template.service.ts | 20 ---- src/services/stratum-v1-jobs.service.ts | 143 +++++++++++++++++++++++- src/services/stratum-v1.service.ts | 5 +- 8 files changed, 205 insertions(+), 148 deletions(-) delete mode 100644 src/services/block-template.service.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index c59f322..efe6615 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ "coinb", + "coinbasevalue", "Fastify", "getblocktemplate", "getmininginfo", diff --git a/src/app.module.ts b/src/app.module.ts index 7cc8c5d..8983c75 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,7 +14,6 @@ import { ClientStatisticsModule } from './ORM/client-statistics/client-statistic import { ClientModule } from './ORM/client/client.module'; import { TelegramSubscriptionsModule } from './ORM/telegram-subscriptions/telegram-subscriptions.module'; import { BitcoinRpcService } from './services/bitcoin-rpc.service'; -import { BlockTemplateService } from './services/block-template.service'; import { CleanupService } from './services/cleanup.service'; import { DiscordService } from './services/discord.service'; import { NotificationService } from './services/notification.service'; @@ -56,7 +55,6 @@ const ORMModules = [ StratumV1Service, TelegramService, BitcoinRpcService, - BlockTemplateService, NotificationService, BitcoinAddressValidator ], diff --git a/src/models/MiningJob.ts b/src/models/MiningJob.ts index ffd2842..c8a73eb 100644 --- a/src/models/MiningJob.ts +++ b/src/models/MiningJob.ts @@ -1,51 +1,41 @@ import { AddressType, getAddressInfo } from 'bitcoin-address-validation'; import * as bitcoinjs from 'bitcoinjs-lib'; -import * as merkle from 'merkle-lib'; -import * as merkleProof from 'merkle-lib/proof'; -import { IBlockTemplate } from './bitcoin-rpc/IBlockTemplate'; +import { IJobTemplate } from '../services/stratum-v1-jobs.service'; import { eResponseMethod } from './enums/eResponseMethod'; + interface AddressObject { address: string; percent: number; } export class MiningJob { + private coinbaseTransaction: bitcoinjs.Transaction; private coinbasePart1: string; private coinbasePart2: string; - private merkle_branch: string[]; // List of hashes, will be used for calculation of merkle root. This is not a list of all transactions, it only contains prepared hashes of steps of merkle tree algorithm. + // private merkle_branch: string[]; // List of hashes, will be used for calculation of merkle root. This is not a list of all transactions, it only contains prepared hashes of steps of merkle tree algorithm. public jobId: string; // ID of the job. Use this ID while submitting share generated from this job. - public block: bitcoinjs.Block = new bitcoinjs.Block(); + public jobTemplateId: string; + //public block: bitcoinjs.Block = new bitcoinjs.Block(); public networkDifficulty: number; + + + constructor( private network: bitcoinjs.networks.Network, id: string, payoutInformation: AddressObject[], - public blockTemplate: IBlockTemplate, - public clean_jobs: boolean) { - + jobTemplate: IJobTemplate + ) { this.jobId = id; - this.block.prevHash = this.convertToLittleEndian(blockTemplate.previousblockhash); + this.jobTemplateId = jobTemplate.blockData.id, - this.block.version = blockTemplate.version; - this.block.bits = parseInt(blockTemplate.bits, 16); - this.networkDifficulty = this.calculateNetworkDifficulty(this.block.bits); - this.block.timestamp = Math.floor(new Date().getTime() / 1000); - - this.block.transactions = blockTemplate.transactions.map(t => bitcoinjs.Transaction.fromHex(t.data)); - - const coinbaseTransaction = this.createCoinbaseTransaction(payoutInformation, this.blockTemplate.coinbasevalue); - this.block.transactions.unshift(coinbaseTransaction); - - this.block.witnessCommit = bitcoinjs.Block.calculateMerkleRoot(this.block.transactions, true); - - // https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki - const littleEndianBlockHeight = this.convertToLittleEndian(this.blockTemplate.height.toString(16).padStart(6, '0')) + this.coinbaseTransaction = this.createCoinbaseTransaction(payoutInformation, jobTemplate.blockData.coinbasevalue); //The commitment is recorded in a scriptPubKey of the coinbase transaction. It must be at least 38 bytes, with the first 6-byte of 0x6a24aa21a9ed, that is: // 1-byte - OP_RETURN (0x6a) @@ -56,15 +46,16 @@ export class MiningJob { // 39th byte onwards: Optional data with no consensus meaning const extra = Buffer.from('\\public-pool\\'); - coinbaseTransaction.ins[0].script = Buffer.concat([Buffer.from([littleEndianBlockHeight.byteLength]), littleEndianBlockHeight, extra, Buffer.alloc(8, 0)]); + // https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki + this.coinbaseTransaction.ins[0].script = Buffer.concat([Buffer.from([jobTemplate.blockData.littleEndianBlockHeight.byteLength]), jobTemplate.blockData.littleEndianBlockHeight, extra, Buffer.alloc(8, 0)]); - coinbaseTransaction.addOutput(bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.concat([segwitMagicBits, this.block.witnessCommit])]), 0); + this.coinbaseTransaction.addOutput(bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.concat([segwitMagicBits, jobTemplate.block.witnessCommit])]), 0); // get the non-witness coinbase tx //@ts-ignore - const serializedCoinbaseTx = coinbaseTransaction.__toBuffer().toString('hex'); + const serializedCoinbaseTx = this.coinbaseTransaction.__toBuffer().toString('hex'); - const inputScript = coinbaseTransaction.ins[0].script.toString('hex'); + const inputScript = this.coinbaseTransaction.ins[0].script.toString('hex'); const partOneIndex = serializedCoinbaseTx.indexOf(inputScript) + inputScript.length; @@ -72,23 +63,13 @@ export class MiningJob { this.coinbasePart2 = serializedCoinbaseTx.slice(partOneIndex); - // Calculate merkle branch - const transactionBuffers = this.block.transactions.map(tx => tx.getHash(false)); - - const merkleTree = merkle(transactionBuffers, bitcoinjs.crypto.hash256); - const merkleBranches: Buffer[] = merkleProof(merkleTree, transactionBuffers[0]).filter(h => h != null); - this.block.merkleRoot = merkleBranches.pop(); - - // remove the first (coinbase) and last (root) element from the branch - this.merkle_branch = merkleBranches.slice(1, merkleBranches.length).map(b => b.toString('hex')) - - this.block.transactions[0] = coinbaseTransaction; - } - public copyAndUpdateBlock(versionMask: number, nonce: number, extraNonce: string, extraNonce2: string, timestamp: number): bitcoinjs.Block { + public copyAndUpdateBlock(jobTemplate: IJobTemplate, versionMask: number, nonce: number, extraNonce: string, extraNonce2: string, timestamp: number): bitcoinjs.Block { - const testBlock = bitcoinjs.Block.fromBuffer(this.block.toBuffer()); + const testBlock = bitcoinjs.Block.fromBuffer(jobTemplate.block.toBuffer()); + + testBlock.transactions[0] = this.coinbaseTransaction; testBlock.nonce = nonce; @@ -103,7 +84,7 @@ export class MiningJob { testBlock.transactions[0].ins[0].script = Buffer.from(`${nonceScript.substring(0, nonceScript.length - 16)}${extraNonce}${extraNonce2}`, 'hex'); //recompute the root since we updated the coinbase script with the nonces - testBlock.merkleRoot = this.calculateMerkleRootHash(testBlock.transactions[0].getHash(false), this.merkle_branch); + testBlock.merkleRoot = this.calculateMerkleRootHash(testBlock.transactions[0].getHash(false), jobTemplate.merkle_branch); testBlock.timestamp = timestamp; @@ -111,6 +92,7 @@ export class MiningJob { return testBlock; } + private calculateMerkleRootHash(newRoot: Buffer, merkleBranches: string[]): Buffer { const bothMerkles = Buffer.alloc(64); @@ -181,21 +163,21 @@ export class MiningJob { } } - public response(): string { + public response(jobTemplate: IJobTemplate): string { const job = { id: null, method: eResponseMethod.MINING_NOTIFY, params: [ this.jobId, - this.swapEndianWords(this.block.prevHash).toString('hex'), + this.swapEndianWords(jobTemplate.block.prevHash).toString('hex'), this.coinbasePart1, this.coinbasePart2, - this.merkle_branch, - this.block.version.toString(16), - this.block.bits.toString(16), - this.block.timestamp.toString(16), - this.clean_jobs + jobTemplate.merkle_branch, + jobTemplate.block.version.toString(16), + jobTemplate.block.bits.toString(16), + jobTemplate.block.timestamp.toString(16), + jobTemplate.blockData.clearJobs ] }; @@ -203,12 +185,6 @@ export class MiningJob { } - private convertToLittleEndian(hash: string): Buffer { - const bytes = Buffer.from(hash, 'hex'); - Array.prototype.reverse.call(bytes); - return bytes; - } - private swapEndianWords(buffer: Buffer): Buffer { const swappedBuffer = Buffer.alloc(buffer.length); @@ -223,15 +199,4 @@ export class MiningJob { } - private calculateNetworkDifficulty(nBits: number) { - const mantissa: number = nBits & 0x007fffff; // Extract the mantissa from nBits - const exponent: number = (nBits >> 24) & 0xff; // Extract the exponent from nBits - - const target: number = mantissa * Math.pow(256, (exponent - 3)); // Calculate the target value - - const difficulty: number = (Math.pow(2, 208) * 65535) / target; // Calculate the difficulty - - return difficulty; - } - } \ No newline at end of file diff --git a/src/models/StratumV1Client.spec.ts b/src/models/StratumV1Client.spec.ts index 831f046..5ca1309 100644 --- a/src/models/StratumV1Client.spec.ts +++ b/src/models/StratumV1Client.spec.ts @@ -14,7 +14,6 @@ import { ClientEntity } from '../ORM/client/client.entity'; import { ClientModule } from '../ORM/client/client.module'; import { ClientService } from '../ORM/client/client.service'; import { BitcoinRpcService as MockBitcoinRpcService } from '../services/bitcoin-rpc.service'; -import { BlockTemplateService } from '../services/block-template.service'; import { NotificationService } from '../services/notification.service'; import { StratumV1JobsService } from '../services/stratum-v1-jobs.service'; import { IMiningInfo } from './bitcoin-rpc/IMiningInfo'; @@ -37,9 +36,9 @@ describe('StratumV1Client', () => { let promiseSocket: PromiseSocket; - let stratumV1JobsService: StratumV1JobsService = new StratumV1JobsService(); + let stratumV1JobsService: StratumV1JobsService; let bitcoinRpcService: MockBitcoinRpcService; - let blockTemplateService: BlockTemplateService; + let clientService: ClientService; let clientStatisticsService: ClientStatisticsService; let notificationService: NotificationService; @@ -92,7 +91,7 @@ describe('StratumV1Client', () => { beforeEach(async () => { - + console.log('NEW TEST') clientService = moduleRef.get(ClientService); @@ -107,12 +106,11 @@ describe('StratumV1Client', () => { configService = moduleRef.get(ConfigService); bitcoinRpcService = new MockBitcoinRpcService(null); - jest.spyOn(bitcoinRpcService, 'getBlockTemplate').mockReturnValue(Promise.resolve(MockRecording1.BLOCK_TEMPLATE)); bitcoinRpcService.newBlock$ = newBlockEmitter.asObservable(); - blockTemplateService = new BlockTemplateService(bitcoinRpcService); + stratumV1JobsService = new StratumV1JobsService(bitcoinRpcService); promiseSocket = new PromiseSocket(); jest.spyOn(promiseSocket.socket, 'on').mockImplementation((event: string, fn: (data: Buffer) => void) => { @@ -125,7 +123,6 @@ describe('StratumV1Client', () => { client = new StratumV1Client( promiseSocket, stratumV1JobsService, - blockTemplateService, bitcoinRpcService, clientService, clientStatisticsService, @@ -245,7 +242,7 @@ describe('StratumV1Client', () => { - expect(promiseSocket.write).lastCalledWith(`{"id":null,"method":"mining.notify","params":["3","171592f223740e92d223f6e68bff25279af7ac4f2246451e0000000200000000","02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1903c943255c7075626c69632d706f6f6c5c","ffffffff037a90000000000000160014e6f22ca44dc800e9d049621a3b9a42c509f1c4bc3b0f250000000000160014e6f22ca44dc800e9d049621a3b9a42c509f1c4bc0000000000000000266a24aa21a9edbd3d1d916aa0b57326a2d88ebe1b68a1d7c48585f26d8335fe6a94b62755f64c00000000",["175335649d5e8746982969ec88f52e85ac9917106fba5468e699c8879ab974a1","d5644ab3e708c54cd68dc5aedc92b8d3037449687f92ec41ed6e37673d969d4a","5c9ec187517edc0698556cca5ce27e54c96acb014770599ed9df4d4937fbf2b0"],"20000000","192495f8","${MockRecording1.TIME}",false]}\n`); + expect(promiseSocket.write).lastCalledWith(`{"id":null,"method":"mining.notify","params":["1","171592f223740e92d223f6e68bff25279af7ac4f2246451e0000000200000000","02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1903c943255c7075626c69632d706f6f6c5c","ffffffff037a90000000000000160014e6f22ca44dc800e9d049621a3b9a42c509f1c4bc3b0f250000000000160014e6f22ca44dc800e9d049621a3b9a42c509f1c4bc0000000000000000266a24aa21a9edbd3d1d916aa0b57326a2d88ebe1b68a1d7c48585f26d8335fe6a94b62755f64c00000000",["175335649d5e8746982969ec88f52e85ac9917106fba5468e699c8879ab974a1","d5644ab3e708c54cd68dc5aedc92b8d3037449687f92ec41ed6e37673d969d4a","5c9ec187517edc0698556cca5ce27e54c96acb014770599ed9df4d4937fbf2b0"],"20000000","192495f8","${MockRecording1.TIME}",false]}\n`); socketEmitter(Buffer.from(MockRecording1.MINING_SUBMIT)); @@ -253,8 +250,11 @@ describe('StratumV1Client', () => { jest.useRealTimers(); await new Promise((r) => setTimeout(r, 100)); + expect(promiseSocket.write).lastCalledWith(`{\"id\":5,\"error\":null,\"result\":true}\n`); + }); + }); \ No newline at end of file diff --git a/src/models/StratumV1Client.ts b/src/models/StratumV1Client.ts index 4fe55ac..548cdb6 100644 --- a/src/models/StratumV1Client.ts +++ b/src/models/StratumV1Client.ts @@ -6,18 +6,16 @@ import { validate, ValidatorOptions } from 'class-validator'; import * as crypto from 'crypto'; import { Socket } from 'net'; import PromiseSocket from 'promise-socket'; -import { combineLatest, firstValueFrom, interval, startWith, takeUntil } from 'rxjs'; +import { firstValueFrom, takeUntil } from 'rxjs'; import { BlocksService } from '../ORM/blocks/blocks.service'; import { ClientStatisticsService } from '../ORM/client-statistics/client-statistics.service'; import { ClientEntity } from '../ORM/client/client.entity'; import { ClientService } from '../ORM/client/client.service'; import { BitcoinRpcService } from '../services/bitcoin-rpc.service'; -import { BlockTemplateService } from '../services/block-template.service'; import { NotificationService } from '../services/notification.service'; -import { StratumV1JobsService } from '../services/stratum-v1-jobs.service'; +import { IJobTemplate, StratumV1JobsService } from '../services/stratum-v1-jobs.service'; import { EasyUnsubscribe } from '../utils/EasyUnsubscribe'; -import { IBlockTemplate } from './bitcoin-rpc/IBlockTemplate'; import { eRequestMethod } from './enums/eRequestMethod'; import { eResponseMethod } from './enums/eResponseMethod'; import { eStratumErrorCode } from './enums/eStratumErrorCode'; @@ -49,7 +47,6 @@ export class StratumV1Client extends EasyUnsubscribe { constructor( public readonly promiseSocket: PromiseSocket, private readonly stratumV1JobsService: StratumV1JobsService, - private readonly blockTemplateService: BlockTemplateService, private readonly bitcoinRpcService: BitcoinRpcService, private readonly clientService: ClientService, private readonly clientStatisticsService: ClientStatisticsService, @@ -274,47 +271,24 @@ export class StratumV1Client extends EasyUnsubscribe { startTime: new Date(), }); - let lastIntervalCount = undefined; - let skipNext = false; - combineLatest([this.blockTemplateService.currentBlockTemplate$, interval(60000).pipe(startWith(-1))]) - .pipe( - takeUntil(this.easyUnsubscribe) - ) - .subscribe(async ([{ blockTemplate }, interValCount]) => { + this.stratumV1JobsService.newMiningJob$.pipe( + takeUntil(this.easyUnsubscribe) + ).subscribe(async (jobTemplate) => { + await this.sendNewMiningJob(jobTemplate); - - - let clearJobs = false; - if (lastIntervalCount === interValCount) { - clearJobs = true; - skipNext = true; - console.log('new block') - } - - if (skipNext == true && clearJobs == false) { - skipNext = false; - return; - } - - lastIntervalCount = interValCount; - - await this.sendNewMiningJob(blockTemplate, clearJobs); - - await this.checkDifficulty(); - - - }); + await this.checkDifficulty(); + }) } } - private async sendNewMiningJob(blockTemplate: IBlockTemplate, clearJobs: boolean) { + private async sendNewMiningJob(jobTemplate: IJobTemplate) { const hashRate = await this.clientStatisticsService.getHashRateForSession(this.clientAuthorization.address, this.clientAuthorization.worker, this.extraNonceAndSessionId); let payoutInformation; - //10Th/s - const noFee = hashRate < 10000000000000; + //50Th/s + const noFee = hashRate < 50000000000000; if (noFee) { payoutInformation = [ { address: this.clientAuthorization.address, percent: 100 } @@ -332,21 +306,20 @@ export class StratumV1Client extends EasyUnsubscribe { this.configService.get('NETWORK') === 'mainnet' ? bitcoinjs.networks.bitcoin : bitcoinjs.networks.testnet, this.stratumV1JobsService.getNextId(), payoutInformation, - blockTemplate, - clearJobs + jobTemplate ); - this.stratumV1JobsService.addJob(job, clearJobs); + this.stratumV1JobsService.addJob(job, jobTemplate.blockData.clearJobs); try { - await this.promiseSocket.write(job.response()); + await this.promiseSocket.write(job.response(jobTemplate)); } catch (e) { await this.promiseSocket.end(); } - console.log(`Sent new job to ${this.clientAuthorization.worker}.${this.extraNonceAndSessionId}. (clearJobs: ${clearJobs}, fee?: ${!noFee})`) + console.log(`Sent new job to ${this.clientAuthorization.worker}.${this.extraNonceAndSessionId}. (clearJobs: ${jobTemplate.blockData.clearJobs}, fee?: ${!noFee})`) } @@ -354,6 +327,7 @@ export class StratumV1Client extends EasyUnsubscribe { private async handleMiningSubmission(submission: MiningSubmitMessage) { const job = this.stratumV1JobsService.getJobById(submission.jobId); + const jobTemplate = this.stratumV1JobsService.getJobTemplateById(job.jobTemplateId); // a miner may submit a job that doesn't exist anymore if it was removed by a new block notification if (job == null) { const err = new StratumErrorMessage( @@ -365,6 +339,7 @@ export class StratumV1Client extends EasyUnsubscribe { return false; } const updatedJobBlock = job.copyAndUpdateBlock( + jobTemplate, parseInt(submission.versionMask, 16), parseInt(submission.nonce, 16), this.extraNonceAndSessionId, @@ -375,7 +350,7 @@ export class StratumV1Client extends EasyUnsubscribe { const { submissionDifficulty, submissionHash } = this.calculateDifficulty(header); console.log(`DIFF: ${submissionDifficulty} of ${this.sessionDifficulty} from ${this.clientAuthorization.worker + '.' + this.extraNonceAndSessionId}`); - console.log(`Header: ${header.toString('hex')}`); + //console.log(`Header: ${header.toString('hex')}`); if (submissionDifficulty >= this.sessionDifficulty) { @@ -384,13 +359,13 @@ export class StratumV1Client extends EasyUnsubscribe { const blockHex = updatedJobBlock.toHex(false); const result = await this.bitcoinRpcService.SUBMIT_BLOCK(blockHex); await this.blocksService.save({ - height: job.blockTemplate.height, + height: jobTemplate.height, minerAddress: this.clientAuthorization.address, worker: this.clientAuthorization.worker, sessionId: this.extraNonceAndSessionId, blockData: blockHex }); - await this.notificationService.notifySubscribersBlockFound(this.clientAuthorization.address, job.blockTemplate.height, updatedJobBlock, result); + await this.notificationService.notifySubscribersBlockFound(this.clientAuthorization.address, jobTemplate.height, updatedJobBlock, result); } try { await this.statistics.addSubmission(this.entity, submissionHash, this.sessionDifficulty); @@ -444,8 +419,8 @@ export class StratumV1Client extends EasyUnsubscribe { await this.promiseSocket.write(data); // we need to clear the jobs so that the difficulty set takes effect. Otherwise the different miner implementations can cause issues - const { blockTemplate } = await firstValueFrom(this.blockTemplateService.currentBlockTemplate$); - await this.sendNewMiningJob(blockTemplate, true); + const jobTemplate = await firstValueFrom(this.stratumV1JobsService.newMiningJob$); + await this.sendNewMiningJob(jobTemplate); } } diff --git a/src/services/block-template.service.ts b/src/services/block-template.service.ts deleted file mode 100644 index 0272f11..0000000 --- a/src/services/block-template.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { from, map, Observable, shareReplay, switchMap } from 'rxjs'; - -import { IBlockTemplate } from '../models/bitcoin-rpc/IBlockTemplate'; -import { BitcoinRpcService } from './bitcoin-rpc.service'; - -@Injectable() -export class BlockTemplateService { - - - public currentBlockTemplate$: Observable<{ 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 } }))), - shareReplay({ refCount: true, bufferSize: 1 }) - ); - } - -} \ No newline at end of file diff --git a/src/services/stratum-v1-jobs.service.ts b/src/services/stratum-v1-jobs.service.ts index e59d09d..a43abe2 100644 --- a/src/services/stratum-v1-jobs.service.ts +++ b/src/services/stratum-v1-jobs.service.ts @@ -1,12 +1,153 @@ -import { MiningJob } from '../models/MiningJob'; +import * as bitcoinjs from 'bitcoinjs-lib'; +import * as merkle from 'merkle-lib'; +import * as merkleProof from 'merkle-lib/proof'; +import { combineLatest, filter, from, interval, map, Observable, shareReplay, startWith, switchMap, tap } from 'rxjs'; +import { IBlockTemplate } from '../models/bitcoin-rpc/IBlockTemplate'; +import { MiningJob } from '../models/MiningJob'; +import { BitcoinRpcService } from './bitcoin-rpc.service'; + +export interface IJobTemplate { + + block: bitcoinjs.Block; + merkle_branch: string[]; + blockData: { + id: string, + littleEndianBlockHeight: Buffer; + coinbasevalue: number; + networkDifficulty: number; + clearJobs: boolean; + }; +} export class StratumV1JobsService { + private lastIntervalCount: number; + private skipNext: boolean = false; + public newMiningJob$: Observable; + public latestJobId: number = 1; public jobs: MiningJob[] = []; + public blocks: { [id: number]: IJobTemplate } = {}; + + private currentBlockTemplate$: Observable<{ blockTemplate: IBlockTemplate }>; + + constructor( + private readonly bitcoinRpcService: BitcoinRpcService + ) { + this.currentBlockTemplate$ = this.bitcoinRpcService.newBlock$.pipe( + switchMap((miningInfo) => { + return from(this.bitcoinRpcService.getBlockTemplate()) + .pipe(map(blockTemplate => { return { miningInfo, blockTemplate } })) + } + ), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + this.newMiningJob$ = combineLatest([this.currentBlockTemplate$, interval(60000).pipe(startWith(-1))]).pipe( + map(([{ blockTemplate }, interValCount]) => { + + let clearJobs = false; + if (this.lastIntervalCount === interValCount) { + clearJobs = true; + this.skipNext = true; + console.log('new block') + } + + if (this.skipNext == true && clearJobs == false) { + this.skipNext = false; + return null; + } + + this.lastIntervalCount = interValCount; + + return { + version: blockTemplate.version, + bits: parseInt(blockTemplate.bits, 16), + prevHash: this.convertToLittleEndian(blockTemplate.previousblockhash), + transactions: blockTemplate.transactions.map(t => bitcoinjs.Transaction.fromHex(t.data)), + littleEndianBlockHeight: this.convertToLittleEndian(blockTemplate.height.toString(16).padStart(6, '0')), + coinbasevalue: blockTemplate.coinbasevalue, + timestamp: Math.floor(new Date().getTime() / 1000), + networkDifficulty: this.calculateNetworkDifficulty(parseInt(blockTemplate.bits, 16)), + clearJobs + }; + }), + filter(next => next != null), + map(({ version, bits, prevHash, transactions, timestamp, littleEndianBlockHeight, coinbasevalue, networkDifficulty, clearJobs }) => { + const block = new bitcoinjs.Block(); + + //create an empty coinbase tx + const tempCoinbaseTx = new bitcoinjs.Transaction(); + tempCoinbaseTx.version = 2; + tempCoinbaseTx.addInput(Buffer.alloc(32, 0), 0xffffffff, 0xffffffff); + tempCoinbaseTx.ins[0].witness = [Buffer.alloc(32, 0)]; + transactions.unshift(tempCoinbaseTx); + + + const transactionBuffers = transactions.map(tx => tx.getHash(false)); + + const merkleTree = merkle(transactionBuffers, bitcoinjs.crypto.hash256); + const merkleBranches: Buffer[] = merkleProof(merkleTree, transactionBuffers[0]).filter(h => h != null); + block.merkleRoot = merkleBranches.pop(); + + // remove the first (coinbase) and last (root) element from the branch + const merkle_branch = merkleBranches.slice(1, merkleBranches.length).map(b => b.toString('hex')) + + + + block.prevHash = prevHash; + block.version = version; + block.bits = bits; + block.timestamp = timestamp; + + block.transactions = transactions; + block.witnessCommit = bitcoinjs.Block.calculateMerkleRoot(transactions, true); + + return { + block, + merkle_branch, + blockData: { + id: this.getNextId(), + littleEndianBlockHeight, + coinbasevalue, + networkDifficulty, + clearJobs + } + } + }), + tap((data) => { + if (data.blockData.clearJobs) { + this.blocks = {}; + } + this.blocks[data.blockData.id] = data; + }), + shareReplay({ refCount: true, bufferSize: 1 }) + ) + } + + private calculateNetworkDifficulty(nBits: number) { + const mantissa: number = nBits & 0x007fffff; // Extract the mantissa from nBits + const exponent: number = (nBits >> 24) & 0xff; // Extract the exponent from nBits + + const target: number = mantissa * Math.pow(256, (exponent - 3)); // Calculate the target value + + const difficulty: number = (Math.pow(2, 208) * 65535) / target; // Calculate the difficulty + + return difficulty; + } + + private convertToLittleEndian(hash: string): Buffer { + const bytes = Buffer.from(hash, 'hex'); + Array.prototype.reverse.call(bytes); + return bytes; + } + + public getJobTemplateById(jobTemplateId: string) { + return this.blocks[jobTemplateId]; + } public addJob(job: MiningJob, clearJobs: boolean) { if (clearJobs) { diff --git a/src/services/stratum-v1.service.ts b/src/services/stratum-v1.service.ts index 06791d7..b091eac 100644 --- a/src/services/stratum-v1.service.ts +++ b/src/services/stratum-v1.service.ts @@ -8,7 +8,6 @@ import { BlocksService } from '../ORM/blocks/blocks.service'; import { ClientStatisticsService } from '../ORM/client-statistics/client-statistics.service'; import { ClientService } from '../ORM/client/client.service'; import { BitcoinRpcService } from './bitcoin-rpc.service'; -import { BlockTemplateService } from './block-template.service'; import { NotificationService } from './notification.service'; import { StratumV1JobsService } from './stratum-v1-jobs.service'; @@ -18,7 +17,6 @@ export class StratumV1Service implements OnModuleInit { constructor( private readonly bitcoinRpcService: BitcoinRpcService, - private readonly blockTemplateService: BlockTemplateService, private readonly clientService: ClientService, private readonly clientStatisticsService: ClientStatisticsService, private readonly notificationService: NotificationService, @@ -43,8 +41,7 @@ export class StratumV1Service implements OnModuleInit { const client = new StratumV1Client( promiseSocket, - new StratumV1JobsService(), - this.blockTemplateService, + new StratumV1JobsService(this.bitcoinRpcService), this.bitcoinRpcService, this.clientService, this.clientStatisticsService,