diff --git a/src/models/StratumV1Client.ts b/src/models/StratumV1Client.ts index 5e55e34..6b13ea7 100644 --- a/src/models/StratumV1Client.ts +++ b/src/models/StratumV1Client.ts @@ -58,6 +58,7 @@ export class StratumV1Client { private buffer: string = ''; private connectionClosed = false; + private lastSentMiningJobTimestamp: number = null; private miningSubmissionHashes = new Set() @@ -467,6 +468,7 @@ export class StratumV1Client { if (!success) { return; } + this.lastSentMiningJobTimestamp = jobTemplate.block.timestamp; //console.log(`Sent new job to ${this.clientAuthorization.worker}.${this.extraNonceAndSessionId}. (clearJobs: ${jobTemplate.blockData.clearJobs}, fee?: ${!this.noFee})`) @@ -659,9 +661,21 @@ export class StratumV1Client { await this.socket.write(data); const jobTemplate = await firstValueFrom(this.stratumV1JobsService.newMiningJob$); - // we need to clear the jobs so that the difficulty set takes effect. Otherwise the different miner implementations can cause issues - jobTemplate.blockData.clearJobs = true; - await this.sendNewMiningJob(jobTemplate); + const nextTimestamp = Math.max( + jobTemplate.block.timestamp, + Math.floor(Date.now() / 1000), + (this.lastSentMiningJobTimestamp ?? 0) + 1 + ); + // We need to clear jobs so the difficulty takes effect, but avoid mutating or + // re-sending the shared cached template with byte-identical work. + const refreshedJobTemplate: IJobTemplate = { + ...jobTemplate, + block: Object.assign(new bitcoinjs.Block(), jobTemplate.block, { + timestamp: nextTimestamp + }), + blockData: { ...jobTemplate.blockData, clearJobs: true } + }; + await this.sendNewMiningJob(refreshedJobTemplate); } } diff --git a/src/services/stratum-v1-jobs.service.spec.ts b/src/services/stratum-v1-jobs.service.spec.ts index 279f235..b0a256e 100644 --- a/src/services/stratum-v1-jobs.service.spec.ts +++ b/src/services/stratum-v1-jobs.service.spec.ts @@ -12,6 +12,7 @@ describe('StratumV1JobsService', () => { const createTemplate = (height = MockRecording1.BLOCK_TEMPLATE.height): IBlockTemplate => ({ ...MockRecording1.BLOCK_TEMPLATE, + transactions: MockRecording1.BLOCK_TEMPLATE.transactions.map(tx => ({ ...tx })), height }); @@ -62,6 +63,34 @@ describe('StratumV1JobsService', () => { expect(service.getJobTemplateById(jobTemplate.blockData.id)).toBe(jobTemplate); }); + it('should skip identical non-clean template refreshes', async () => { + await firstValueFrom(service.newMiningJob$); + + blockTemplate$.next(createTemplate()); + + expect(service.latestJobTemplateId).toBe(2); + expect(Object.keys(service.blocks)).toHaveLength(1); + }); + + it('should emit when transaction identity changes without changing transaction count', async () => { + await firstValueFrom(service.newMiningJob$); + + const reorderedTemplate = createTemplate(); + reorderedTemplate.transactions = [ + reorderedTemplate.transactions[1], + reorderedTemplate.transactions[0], + ...reorderedTemplate.transactions.slice(2) + ]; + + const nextTemplate = firstValueFrom(service.newMiningJob$.pipe(skip(1))); + blockTemplate$.next(reorderedTemplate); + const jobTemplate = await nextTemplate; + + expect(jobTemplate.blockData.clearJobs).toBe(false); + expect(jobTemplate.blockData.id).toBe('2'); + expect(service.getJobTemplateById(jobTemplate.blockData.id)).toBe(jobTemplate); + }); + it('should age old jobs and templates after five minutes', async () => { await firstValueFrom(service.newMiningJob$); const oldCreation = Date.now() - (1000 * 60 * 11); @@ -71,6 +100,7 @@ describe('StratumV1JobsService', () => { } as any; bitcoinRpcService.miningInfo.blocks = MockRecording1.BLOCK_TEMPLATE.height; + jest.setSystemTime(new Date(Date.now() + 1000)); const nextTemplate = firstValueFrom(service.newMiningJob$.pipe(skip(1))); blockTemplate$.next(createTemplate()); const jobTemplate = await nextTemplate; diff --git a/src/services/stratum-v1-jobs.service.ts b/src/services/stratum-v1-jobs.service.ts index 385233a..ca35c82 100644 --- a/src/services/stratum-v1-jobs.service.ts +++ b/src/services/stratum-v1-jobs.service.ts @@ -31,6 +31,7 @@ export class StratumV1JobsService { public blocks: { [id: number]: IJobTemplate } = {}; private lastBlockHeight = 0; + private lastWorkSignature: string; constructor( private readonly bitcoinRpcService: BitcoinRpcService @@ -53,13 +54,29 @@ export class StratumV1JobsService { } const currentTime = Math.floor(new Date().getTime() / 1000); + const timestamp = blockTemplate.mintime > currentTime ? blockTemplate.mintime : currentTime; + const workSignature = [ + blockTemplate.previousblockhash, + blockTemplate.version, + blockTemplate.bits, + timestamp, + blockTemplate.height, + blockTemplate.coinbasevalue, + ...blockTemplate.transactions.map(tx => tx.hash ?? tx.txid ?? tx.data) + ].join('|'); + + if (!clearJobs && workSignature === this.lastWorkSignature) { + return null; + } + this.lastWorkSignature = workSignature; + return { version: blockTemplate.version, bits: parseInt(blockTemplate.bits, 16), prevHash: this.convertToLittleEndian(blockTemplate.previousblockhash), transactions: blockTemplate.transactions.map(t => bitcoinjs.Transaction.fromHex(t.data)), coinbasevalue: blockTemplate.coinbasevalue, - timestamp: blockTemplate.mintime > currentTime ? blockTemplate.mintime : currentTime, + timestamp, networkDifficulty: this.calculateNetworkDifficulty(parseInt(blockTemplate.bits, 16)), clearJobs, height: blockTemplate.height