performance and optimization

This commit is contained in:
Ben
2026-06-09 17:13:27 -04:00
parent efbd95e464
commit 1c5a3d9a94
13 changed files with 284 additions and 48 deletions
+1
View File
@@ -20,6 +20,7 @@ lerna-debug.log*
/.nyc_output /.nyc_output
/.baseline-*.json /.baseline-*.json
/.test-artifacts /.test-artifacts
/.profiles
# IDEs and editors # IDEs and editors
/.idea /.idea
+3 -2
View File
@@ -2,6 +2,7 @@ import { AddressType, getAddressInfo } from 'bitcoin-address-validation';
import * as bitcoinjs from 'bitcoinjs-lib'; import * as bitcoinjs from 'bitcoinjs-lib';
import { IJobTemplate } from '../services/stratum-v1-jobs.service'; import { IJobTemplate } from '../services/stratum-v1-jobs.service';
import { hash256 } from '../utils/hash.utils';
import { eResponseMethod } from './enums/eResponseMethod'; import { eResponseMethod } from './enums/eResponseMethod';
import { IMiningNotify } from './stratum-messages/IMiningNotify'; import { IMiningNotify } from './stratum-messages/IMiningNotify';
import { TOTAL_EXTRANONCE_SIZE_BYTES } from './stratum.constants'; import { TOTAL_EXTRANONCE_SIZE_BYTES } from './stratum.constants';
@@ -96,7 +97,7 @@ export class MiningJob {
Buffer.from(`${extraNonce}${extraNonce2}`, 'hex'), Buffer.from(`${extraNonce}${extraNonce2}`, 'hex'),
this.coinbasePart2Buffer, this.coinbasePart2Buffer,
]); ]);
const coinbaseHash = bitcoinjs.crypto.hash256(coinbaseBuffer); const coinbaseHash = hash256(coinbaseBuffer);
const merkleRoot = this.calculateMerkleRootHash(coinbaseHash, this.merkleBranchBuffers); const merkleRoot = this.calculateMerkleRootHash(coinbaseHash, this.merkleBranchBuffers);
let version = jobTemplate.block.version; let version = jobTemplate.block.version;
@@ -154,7 +155,7 @@ export class MiningJob {
for (let i = 0; i < merkleBranches.length; i++) { for (let i = 0; i < merkleBranches.length; i++) {
bothMerkles.set(merkleBranches[i], 32); bothMerkles.set(merkleBranches[i], 32);
newRoot = bitcoinjs.crypto.hash256(bothMerkles); newRoot = hash256(bothMerkles);
bothMerkles.set(newRoot); bothMerkles.set(newRoot);
} }
+26 -2
View File
@@ -9,6 +9,7 @@ import { ClientService } from '../ORM/client/client.service';
import { BitcoinRpcService as MockBitcoinRpcService } from '../services/bitcoin-rpc.service'; import { BitcoinRpcService as MockBitcoinRpcService } from '../services/bitcoin-rpc.service';
import { NotificationService } from '../services/notification.service'; import { NotificationService } from '../services/notification.service';
import { StratumV1JobsService } from '../services/stratum-v1-jobs.service'; import { StratumV1JobsService } from '../services/stratum-v1-jobs.service';
import { DifficultyUtils } from '../utils/difficulty.utils';
import { IBlockTemplate } from './bitcoin-rpc/IBlockTemplate'; import { IBlockTemplate } from './bitcoin-rpc/IBlockTemplate';
import { MiningJob } from './MiningJob'; import { MiningJob } from './MiningJob';
import { StratumV1Client } from './StratumV1Client'; import { StratumV1Client } from './StratumV1Client';
@@ -471,7 +472,8 @@ describe('StratumV1Client', () => {
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true)); jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
jest.spyOn(client as any, 'calculateDifficulty').mockReturnValue({ jest.spyOn(client as any, 'calculateDifficulty').mockReturnValue({
submissionDifficulty: 1024, submissionDifficulty: 1024,
submissionHash: 'share' submissionHash: 'share',
hashBuffer: DifficultyUtils.difficultyToTarget(1024),
}); });
const getSettingsSpy = jest.spyOn(addressSettings, 'getSettings'); const getSettingsSpy = jest.spyOn(addressSettings, 'getSettings');
const updateIfHigherSpy = jest.spyOn(addressSettings as any, 'updateBestDifficultyIfHigher').mockResolvedValue({ affected: 1 }); const updateIfHigherSpy = jest.spyOn(addressSettings as any, 'updateBestDifficultyIfHigher').mockResolvedValue({ affected: 1 });
@@ -495,6 +497,27 @@ describe('StratumV1Client', () => {
expect(getSettingsSpy).not.toHaveBeenCalled(); expect(getSettingsSpy).not.toHaveBeenCalled();
}); });
it('should reject shares by exact target even when reported difficulty is huge', async () => {
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
jest.spyOn(client as any, 'calculateDifficulty').mockReturnValue({
submissionDifficulty: Number.MAX_SAFE_INTEGER,
submissionHash: 'too-easy',
hashBuffer: Buffer.alloc(32, 0xff),
});
emitMessage(MockRecording1.MINING_SUBSCRIBE);
emitMessage(`{"id": 4, "method": "mining.suggest_difficulty", "params": [1024]}`);
emitMessage(MockRecording1.MINING_AUTHORIZE);
await new Promise((r) => setTimeout(r, 100));
emitMessage(MockRecording1.MINING_SUBMIT);
jest.useRealTimers();
await new Promise((r) => setTimeout(r, 1000));
expect((client as any).write).lastCalledWith(`{"id":5,"result":null,"error":[23,"Difficulty too low",""]}\n`);
expect(shareAccountingService.recordAcceptedShare).not.toHaveBeenCalled();
});
it('should reject duplicate submissions', async () => { it('should reject duplicate submissions', async () => {
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true)); jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
@@ -626,7 +649,8 @@ describe('StratumV1Client', () => {
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true)); jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
jest.spyOn(client as any, 'calculateDifficulty').mockReturnValue({ jest.spyOn(client as any, 'calculateDifficulty').mockReturnValue({
submissionDifficulty: Number.MAX_SAFE_INTEGER, submissionDifficulty: Number.MAX_SAFE_INTEGER,
submissionHash: 'block-share' submissionHash: 'block-share',
hashBuffer: Buffer.alloc(32),
}); });
jest.spyOn(addressSettings, 'resetBestDifficultyAndShares').mockResolvedValue(undefined); jest.spyOn(addressSettings, 'resetBestDifficultyAndShares').mockResolvedValue(undefined);
+17 -7
View File
@@ -16,6 +16,8 @@ import { BitcoinRpcService } from '../services/bitcoin-rpc.service';
import { NotificationService } from '../services/notification.service'; import { NotificationService } from '../services/notification.service';
import { RedisMessagingService } from '../services/redis-messaging.service'; import { RedisMessagingService } from '../services/redis-messaging.service';
import { IJobTemplate, StratumV1JobsService } from '../services/stratum-v1-jobs.service'; import { IJobTemplate, StratumV1JobsService } from '../services/stratum-v1-jobs.service';
import { DifficultyUtils } from '../utils/difficulty.utils';
import { hash256 } from '../utils/hash.utils';
import { eRequestMethod } from './enums/eRequestMethod'; import { eRequestMethod } from './enums/eRequestMethod';
import { eResponseMethod } from './enums/eResponseMethod'; import { eResponseMethod } from './enums/eResponseMethod';
import { eStratumErrorCode } from './enums/eStratumErrorCode'; import { eStratumErrorCode } from './enums/eStratumErrorCode';
@@ -51,6 +53,7 @@ export class StratumV1Client {
private stratumInitialized = false; private stratumInitialized = false;
private usedSuggestedDifficulty = false; private usedSuggestedDifficulty = false;
private sessionDifficulty: number = 100000; private sessionDifficulty: number = 100000;
private sessionDifficultyTarget: Buffer = DifficultyUtils.difficultyToTarget(this.sessionDifficulty);
private clientEntity: ClientEntity; private clientEntity: ClientEntity;
private creatingEntity: Promise<void>; private creatingEntity: Promise<void>;
@@ -268,6 +271,7 @@ export class StratumV1Client {
this.clientAuthorization = authorizationMessage; this.clientAuthorization = authorizationMessage;
if (this.clientSuggestedDifficulty == null && this.clientAuthorization.startingDiff != null && this.clientAuthorization.startingDiff > this.sessionDifficulty) { if (this.clientSuggestedDifficulty == null && this.clientAuthorization.startingDiff != null && this.clientAuthorization.startingDiff > this.sessionDifficulty) {
this.sessionDifficulty = this.clientAuthorization.startingDiff; this.sessionDifficulty = this.clientAuthorization.startingDiff;
this.sessionDifficultyTarget = DifficultyUtils.difficultyToTarget(this.sessionDifficulty);
} }
const success = await this.write(JSON.stringify(this.clientAuthorization.response()) + '\n'); const success = await this.write(JSON.stringify(this.clientAuthorization.response()) + '\n');
if (!success) { if (!success) {
@@ -309,6 +313,7 @@ export class StratumV1Client {
this.clientSuggestedDifficulty = suggestDifficultyMessage; this.clientSuggestedDifficulty = suggestDifficultyMessage;
this.sessionDifficulty = this.clampDifficulty(suggestDifficultyMessage.suggestedDifficulty); this.sessionDifficulty = this.clampDifficulty(suggestDifficultyMessage.suggestedDifficulty);
this.sessionDifficultyTarget = DifficultyUtils.difficultyToTarget(this.sessionDifficulty);
const success = await this.write(JSON.stringify(this.clientSuggestedDifficulty.response(this.sessionDifficulty)) + '\n'); const success = await this.write(JSON.stringify(this.clientSuggestedDifficulty.response(this.sessionDifficulty)) + '\n');
if (!success) { if (!success) {
return; return;
@@ -610,19 +615,23 @@ export class StratumV1Client {
submission.extraNonce2, submission.extraNonce2,
timestamp timestamp
); );
const { submissionDifficulty } = this.calculateDifficulty(header); const { submissionDifficulty, hashBuffer } = this.calculateDifficulty(header);
//console.log(`DIFF: ${submissionDifficulty} of ${this.sessionDifficulty} from ${this.clientAuthorization.worker + '.' + this.extraNonceAndSessionId}`); //console.log(`DIFF: ${submissionDifficulty} of ${this.sessionDifficulty} from ${this.clientAuthorization.worker + '.' + this.extraNonceAndSessionId}`);
if (submissionDifficulty >= this.sessionDifficulty) { if (DifficultyUtils.meetsTarget(hashBuffer, this.sessionDifficultyTarget)) {
const success = await this.write(JSON.stringify(submission.response()) + '\n'); const success = await this.write(JSON.stringify(submission.response()) + '\n');
if (!success) { if (!success) {
return false; return false;
} }
let blockSubmissionResult: string = null; let blockSubmissionResult: string = null;
if (submissionDifficulty >= jobTemplate.blockData.networkDifficulty) { const isBlockCandidate = DifficultyUtils.meetsTarget(
hashBuffer,
DifficultyUtils.difficultyToTarget(jobTemplate.blockData.networkDifficulty),
);
if (isBlockCandidate) {
console.log('!!! BLOCK FOUND !!!'); console.log('!!! BLOCK FOUND !!!');
const updatedJobBlock = job.copyAndUpdateBlock( const updatedJobBlock = job.copyAndUpdateBlock(
jobTemplate, jobTemplate,
@@ -668,7 +677,7 @@ export class StratumV1Client {
? (jobTemplate.block.version ^ versionMask).toString(16) ? (jobTemplate.block.version ^ versionMask).toString(16)
: jobTemplate.block.version.toString(16), : jobTemplate.block.version.toString(16),
extraNonce2: submission.extraNonce2, extraNonce2: submission.extraNonce2,
isBlockCandidate: submissionDifficulty >= jobTemplate.blockData.networkDifficulty, isBlockCandidate,
blockSubmissionResult, blockSubmissionResult,
}); });
await this.statistics.addShares(this.clientEntity, this.sessionDifficulty); await this.statistics.addShares(this.clientEntity, this.sessionDifficulty);
@@ -718,6 +727,7 @@ export class StratumV1Client {
if (targetDiff != this.sessionDifficulty) { if (targetDiff != this.sessionDifficulty) {
//console.log(`Adjusting ${this.extraNonceAndSessionId} difficulty from ${this.sessionDifficulty} to ${targetDiff}`); //console.log(`Adjusting ${this.extraNonceAndSessionId} difficulty from ${this.sessionDifficulty} to ${targetDiff}`);
this.sessionDifficulty = targetDiff; this.sessionDifficulty = targetDiff;
this.sessionDifficultyTarget = DifficultyUtils.difficultyToTarget(this.sessionDifficulty);
const data = JSON.stringify({ const data = JSON.stringify({
id: null, id: null,
@@ -748,13 +758,13 @@ export class StratumV1Client {
} }
} }
private calculateDifficulty(header: Buffer): { submissionDifficulty: number, submissionHash: string } { private calculateDifficulty(header: Buffer): { submissionDifficulty: number, submissionHash: string, hashBuffer: Buffer } {
const hashResult = bitcoinjs.crypto.hash256(header); const hashResult = hash256(header);
const target = this.le256todouble(hashResult); const target = this.le256todouble(hashResult);
const submissionDifficulty = target === 0 ? Number.POSITIVE_INFINITY : TRUE_DIFF_ONE / target; const submissionDifficulty = target === 0 ? Number.POSITIVE_INFINITY : TRUE_DIFF_ONE / target;
return { submissionDifficulty, submissionHash: hashResult.toString('hex') }; return { submissionDifficulty, submissionHash: hashResult.toString('hex'), hashBuffer: hashResult };
} }
+98 -3
View File
@@ -1,4 +1,5 @@
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import * as bitcoinjs from 'bitcoinjs-lib';
import { Socket } from 'net'; import { Socket } from 'net';
import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { BehaviorSubject, firstValueFrom } from 'rxjs';
@@ -14,6 +15,7 @@ import {
serializeSubmitSharesExtended, serializeSubmitSharesExtended,
} from './sv2/sv2-extended-messages'; } from './sv2/sv2-extended-messages';
import { deserializeSetNewPrevHash, deserializeSubmitSharesError } from './sv2/sv2-messages'; import { deserializeSetNewPrevHash, deserializeSubmitSharesError } from './sv2/sv2-messages';
import { MiningJob } from './MiningJob';
import { StratumV2Client } from './StratumV2Client'; import { StratumV2Client } from './StratumV2Client';
describe('StratumV2Client extended channels', () => { describe('StratumV2Client extended channels', () => {
@@ -145,9 +147,87 @@ describe('StratumV2Client extended channels', () => {
expect(postAccountingPresenceUpdates.length).toBeGreaterThan(0); expect(postAccountingPresenceUpdates.length).toBeGreaterThan(0);
}); });
it('does not submit a block when only the reported SV2 difficulty is huge', async () => {
const { client, shareAccountingService, bitcoinRpcService, blocksService, jobTemplate } = await createClient();
(client as any).address = 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4';
(client as any).workerName = 'worker';
(client as any).sessionId = 'sv2-session';
(client as any).userAgent = 'test/sv2';
const job = new MiningJob(
bitcoinjs.networks.testnet,
'1',
[{ address: 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4', percent: 100 }],
jobTemplate,
);
await (client as any).handleAcceptedShare(
{
nonce: 123,
ntime: parseInt(MockRecording1.TIME, 16),
version: jobTemplate.block.version,
},
{ extranoncePrefix: Buffer.from(MockRecording1.EXTRA_NONCE, 'hex') },
job,
jobTemplate,
Number.MAX_SAFE_INTEGER,
1024,
Buffer.alloc(32, 0xff),
);
expect(bitcoinRpcService.SUBMIT_BLOCK).not.toHaveBeenCalled();
expect(blocksService.save).not.toHaveBeenCalled();
expect(shareAccountingService.recordAcceptedShare).toHaveBeenCalledWith(expect.objectContaining({
submissionDifficulty: Number.MAX_SAFE_INTEGER,
isBlockCandidate: false,
}));
});
it('submits a block when the SV2 hash target exactly meets network target', async () => {
const { client, bitcoinRpcService, blocksService, notificationService, jobTemplate } = await createClient();
(client as any).address = 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4';
(client as any).workerName = 'worker';
(client as any).sessionId = 'sv2-session';
(client as any).userAgent = 'test/sv2';
const job = new MiningJob(
bitcoinjs.networks.testnet,
'1',
[{ address: 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4', percent: 100 }],
jobTemplate,
);
await (client as any).handleAcceptedShare(
{
nonce: 123,
ntime: parseInt(MockRecording1.TIME, 16),
version: jobTemplate.block.version,
},
{ extranoncePrefix: Buffer.from(MockRecording1.EXTRA_NONCE, 'hex') },
job,
jobTemplate,
1,
1024,
Buffer.alloc(32),
);
expect(bitcoinRpcService.SUBMIT_BLOCK).toHaveBeenCalledWith(expect.any(String));
expect(blocksService.save).toHaveBeenCalledWith(expect.objectContaining({
height: MockRecording1.BLOCK_TEMPLATE.height,
minerAddress: 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4',
worker: 'worker',
sessionId: 'sv2-session',
blockData: expect.any(String),
}));
expect(notificationService.notifySubscribersBlockFound).toHaveBeenCalled();
});
async function createClient(): Promise<{ async function createClient(): Promise<{
client: StratumV2Client; client: StratumV2Client;
sentFrames: any[]; sentFrames: any[];
bitcoinRpcService: { SUBMIT_BLOCK: jest.Mock };
blocksService: { save: jest.Mock };
notificationService: { notifySubscribersBlockFound: jest.Mock };
shareAccountingService: { recordAcceptedShare: jest.Mock }; shareAccountingService: { recordAcceptedShare: jest.Mock };
redisMessagingService: { setClientPresence: jest.Mock; removeClientPresence: jest.Mock }; redisMessagingService: { setClientPresence: jest.Mock; removeClientPresence: jest.Mock };
jobTemplate: any; jobTemplate: any;
@@ -196,6 +276,12 @@ describe('StratumV2Client extended channels', () => {
const shareAccountingService = { const shareAccountingService = {
recordAcceptedShare: jest.fn().mockResolvedValue(undefined), recordAcceptedShare: jest.fn().mockResolvedValue(undefined),
}; };
const notificationService = {
notifySubscribersBlockFound: jest.fn().mockResolvedValue(undefined),
};
const blocksService = {
save: jest.fn().mockResolvedValue(undefined),
};
const redisMessagingService = { const redisMessagingService = {
setClientPresence: jest.fn().mockResolvedValue(undefined), setClientPresence: jest.fn().mockResolvedValue(undefined),
removeClientPresence: jest.fn().mockResolvedValue(undefined), removeClientPresence: jest.fn().mockResolvedValue(undefined),
@@ -225,8 +311,8 @@ describe('StratumV2Client extended channels', () => {
stratumV1JobsService, stratumV1JobsService,
bitcoinRpcService as any, bitcoinRpcService as any,
clientService as any, clientService as any,
{ notifySubscribersBlockFound: jest.fn().mockResolvedValue(undefined) } as any, notificationService as any,
{ save: jest.fn().mockResolvedValue(undefined) } as any, blocksService as any,
{ {
get: jest.fn((key: string) => { get: jest.fn((key: string) => {
switch (key) { switch (key) {
@@ -251,6 +337,15 @@ describe('StratumV2Client extended channels', () => {
return Promise.resolve(); return Promise.resolve();
}); });
return { client, sentFrames, shareAccountingService, redisMessagingService, jobTemplate }; return {
client,
sentFrames,
bitcoinRpcService,
blocksService,
notificationService,
shareAccountingService,
redisMessagingService,
jobTemplate,
};
} }
}); });
+13 -5
View File
@@ -17,6 +17,7 @@ import { StratumV2Service } from '../services/stratum-v2.service';
import { IJobTemplate, StratumV1JobsService } from '../services/stratum-v1-jobs.service'; import { IJobTemplate, StratumV1JobsService } from '../services/stratum-v1-jobs.service';
import { patchCoinbasePrefixVarint } from '../utils/coinbase-prefix.utils'; import { patchCoinbasePrefixVarint } from '../utils/coinbase-prefix.utils';
import { DifficultyUtils } from '../utils/difficulty.utils'; import { DifficultyUtils } from '../utils/difficulty.utils';
import { hash256 } from '../utils/hash.utils';
import { MiningJob } from './MiningJob'; import { MiningJob } from './MiningJob';
import { StratumV1ClientStatistics } from './StratumV1ClientStatistics'; import { StratumV1ClientStatistics } from './StratumV1ClientStatistics';
import { TOTAL_EXTRANONCE_SIZE_BYTES } from './stratum.constants'; import { TOTAL_EXTRANONCE_SIZE_BYTES } from './stratum.constants';
@@ -560,7 +561,7 @@ export class StratumV2Client {
); );
channel.acceptedShareCount++; channel.acceptedShareCount++;
await this.handleAcceptedShare(submission, channel, job, jobTemplate, submissionDifficulty, jobDifficulty); await this.handleAcceptedShare(submission, channel, job, jobTemplate, submissionDifficulty, jobDifficulty, hashBuffer);
} }
private async handleSubmitSharesExtended(payload: Buffer): Promise<void> { private async handleSubmitSharesExtended(payload: Buffer): Promise<void> {
@@ -608,12 +609,12 @@ export class StratumV2Client {
submission.extranonce, submission.extranonce,
extendedJob.coinbaseSuffix, extendedJob.coinbaseSuffix,
]); ]);
let merkleRoot = bitcoinjs.crypto.hash256(coinbaseTxBytes); let merkleRoot = hash256(coinbaseTxBytes);
const merklePair = Buffer.alloc(64); const merklePair = Buffer.alloc(64);
for (const sibling of extendedJob.merklePath) { for (const sibling of extendedJob.merklePath) {
merklePair.set(merkleRoot, 0); merklePair.set(merkleRoot, 0);
merklePair.set(sibling, 32); merklePair.set(sibling, 32);
merkleRoot = bitcoinjs.crypto.hash256(merklePair); merkleRoot = hash256(merklePair);
} }
const header = this.buildHeader( const header = this.buildHeader(
@@ -650,7 +651,10 @@ export class StratumV2Client {
channel.acceptedShareCount++; channel.acceptedShareCount++;
let updatedJobBlock: bitcoinjs.Block = null; let updatedJobBlock: bitcoinjs.Block = null;
if (submissionDifficulty >= extendedJob.jobTemplate.blockData.networkDifficulty) { if (DifficultyUtils.meetsTarget(
hashBuffer,
DifficultyUtils.difficultyToTarget(extendedJob.jobTemplate.blockData.networkDifficulty),
)) {
updatedJobBlock = this.reconstructExtendedBlock(extendedJob, submission, merkleRoot, channel.extranoncePrefix); updatedJobBlock = this.reconstructExtendedBlock(extendedJob, submission, merkleRoot, channel.extranoncePrefix);
} }
await this.recordAcceptedShare(submissionDifficulty, jobDifficulty, extendedJob.jobTemplate, updatedJobBlock, { await this.recordAcceptedShare(submissionDifficulty, jobDifficulty, extendedJob.jobTemplate, updatedJobBlock, {
@@ -669,9 +673,13 @@ export class StratumV2Client {
jobTemplate: IJobTemplate, jobTemplate: IJobTemplate,
submissionDifficulty: number, submissionDifficulty: number,
jobDifficulty: number, jobDifficulty: number,
hashBuffer: Buffer,
): Promise<void> { ): Promise<void> {
let updatedJobBlock: bitcoinjs.Block = null; let updatedJobBlock: bitcoinjs.Block = null;
if (submissionDifficulty >= jobTemplate.blockData.networkDifficulty) { if (DifficultyUtils.meetsTarget(
hashBuffer,
DifficultyUtils.difficultyToTarget(jobTemplate.blockData.networkDifficulty),
)) {
const versionMask = submission.version ^ jobTemplate.block.version; const versionMask = submission.version ^ jobTemplate.block.version;
updatedJobBlock = job.copyAndUpdateBlock( updatedJobBlock = job.copyAndUpdateBlock(
jobTemplate, jobTemplate,
@@ -37,6 +37,10 @@ describe('MiningSubmitMessage', () => {
expect(errors).toEqual([]); expect(errors).toEqual([]);
}); });
it('should hash submissions deterministically', () => {
expect(message.hash()).toBe('t2bFzhZ6mketRxa5nOoKrNxG3RkFIZuOwY1WewFtv9k=');
});
it('should reject short extranonce2 submissions', async () => { it('should reject short extranonce2 submissions', async () => {
const shortMessage = plainToInstance( const shortMessage = plainToInstance(
MiningSubmitMessage, MiningSubmitMessage,
@@ -4,7 +4,7 @@ import { ArrayMaxSize, ArrayMinSize, IsArray, IsString, Length } from 'class-val
import { eRequestMethod } from '../enums/eRequestMethod'; import { eRequestMethod } from '../enums/eRequestMethod';
import { EXTRANONCE2_SIZE_BYTES } from '../stratum.constants'; import { EXTRANONCE2_SIZE_BYTES } from '../stratum.constants';
import { StratumBaseMessage } from './StratumBaseMessage'; import { StratumBaseMessage } from './StratumBaseMessage';
import * as bitcoinjs from 'bitcoinjs-lib'; import { hash256 } from '../../utils/hash.utils';
export class MiningSubmitMessage extends StratumBaseMessage { export class MiningSubmitMessage extends StratumBaseMessage {
@@ -69,7 +69,7 @@ export class MiningSubmitMessage extends StratumBaseMessage {
public hash(): string{ public hash(): string{
const buffer = Buffer.from(this.versionMask + this.nonce + this.extraNonce2 + this.ntime + this.jobId); const buffer = Buffer.from(this.versionMask + this.nonce + this.extraNonce2 + this.ntime + this.jobId);
return bitcoinjs.crypto.hash256(buffer).toString('base64'); return hash256(buffer).toString('base64');
} }
+2 -1
View File
@@ -5,6 +5,7 @@ import * as merkleProof from 'merkle-lib/proof';
import { combineLatest, delay, filter, from, interval, map, Observable, shareReplay, startWith, switchMap, tap } from 'rxjs'; import { combineLatest, delay, filter, from, interval, map, Observable, shareReplay, startWith, switchMap, tap } from 'rxjs';
import { MiningJob } from '../models/MiningJob'; import { MiningJob } from '../models/MiningJob';
import { hash256 } from '../utils/hash.utils';
import { BitcoinRpcService } from './bitcoin-rpc.service'; import { BitcoinRpcService } from './bitcoin-rpc.service';
export interface IJobTemplate { export interface IJobTemplate {
@@ -95,7 +96,7 @@ export class StratumV1JobsService {
const transactionBuffers = transactions.map(tx => tx.getHash(false)); const transactionBuffers = transactions.map(tx => tx.getHash(false));
const merkleTree = merkle(transactionBuffers, bitcoinjs.crypto.hash256); const merkleTree = merkle(transactionBuffers, hash256);
const merkleBranches: Buffer[] = merkleProof(merkleTree, transactionBuffers[0]).filter(h => h != null); const merkleBranches: Buffer[] = merkleProof(merkleTree, transactionBuffers[0]).filter(h => h != null);
block.merkleRoot = merkleBranches.pop(); block.merkleRoot = merkleBranches.pop();
+62
View File
@@ -0,0 +1,62 @@
import { DifficultyUtils } from './difficulty.utils';
describe('DifficultyUtils', () => {
function incrementLe256(target: Buffer): Buffer {
const next = Buffer.from(target);
for (let i = 0; i < next.length; i++) {
next[i]++;
if (next[i] !== 0) {
break;
}
}
return next;
}
it('round-trips common difficulty values through compact targets', () => {
for (const difficulty of [0.001, 1, 128, 512, 1000, 1_000_000, 1_000_000_000, 1e30]) {
const target = DifficultyUtils.difficultyToTarget(difficulty);
const roundTrip = DifficultyUtils.targetToDifficulty(target);
expect(Math.abs(roundTrip - difficulty) / difficulty).toBeLessThan(1e-12);
}
});
it('compares little-endian targets without converting to BigInt', () => {
const target = Buffer.alloc(32);
target[0] = 0x80;
target[30] = 0x01;
const easier = Buffer.from(target);
const harder = Buffer.from(target);
easier[31] += 1;
harder[0] -= 1;
expect(DifficultyUtils.meetsTarget(target, target)).toBe(true);
expect(DifficultyUtils.meetsTarget(harder, target)).toBe(true);
expect(DifficultyUtils.meetsTarget(easier, target)).toBe(false);
});
it('calculates difficulty consistently with the returned hash buffer', () => {
const header = Buffer.alloc(80, 1);
const result = DifficultyUtils.calculateDifficulty(header);
expect(result.submissionDifficulty).toBeCloseTo(
DifficultyUtils.targetToDifficulty(result.hashBuffer),
10,
);
expect(result.submissionHash).toBe(result.hashBuffer.toString('hex'));
});
it('keeps huge-difficulty boundary checks exact with target bytes', () => {
const target = DifficultyUtils.difficultyToTarget(1e30);
const justTooEasy = incrementLe256(target);
expect(DifficultyUtils.targetToDifficulty(target)).toBeGreaterThan(1e29);
expect(DifficultyUtils.meetsTarget(target, target)).toBe(true);
expect(DifficultyUtils.meetsTarget(justTooEasy, target)).toBe(false);
});
it('treats a zero hash target as infinite reported difficulty', () => {
expect(DifficultyUtils.targetToDifficulty(Buffer.alloc(32))).toBe(Number.POSITIVE_INFINITY);
});
});
+35 -26
View File
@@ -1,35 +1,25 @@
import * as bitcoinjs from 'bitcoinjs-lib'; import { hash256 } from './hash.utils';
const TRUE_DIFF_ONE_BIGINT = BigInt( const TRUE_DIFF_ONE_BIGINT = BigInt(
'26959535291011309493156476344723991336010898738574164086137773096960', '26959535291011309493156476344723991336010898738574164086137773096960',
); );
const TRUE_DIFF_ONE_NUMBER = 2.695953529101131e67;
const TWO_TO_256 = 1n << 256n; const TWO_TO_256 = 1n << 256n;
const FRACTION_SCALE = 1_000_000_000_000_000n;
const FRACTION_SCALE_NUM = 1e15;
function bigIntRatioToDifficulty(divisor: bigint): number {
if (divisor === 0n) {
return Number.POSITIVE_INFINITY;
}
const scaled = (TRUE_DIFF_ONE_BIGINT * FRACTION_SCALE) / divisor;
return Number(scaled) / FRACTION_SCALE_NUM;
}
export class DifficultyUtils { export class DifficultyUtils {
public static calculateDifficulty(header: Buffer): { submissionDifficulty: number; submissionHash: string; hashBuffer: Buffer } { public static calculateDifficulty(header: Buffer): { submissionDifficulty: number; submissionHash: string; hashBuffer: Buffer } {
const hashResult = bitcoinjs.crypto.hash256(header); const hashResult = hash256(header);
const target = DifficultyUtils.le256ToBigInt(hashResult); const target = DifficultyUtils.le256ToDouble(hashResult);
return { return {
submissionDifficulty: bigIntRatioToDifficulty(target), submissionDifficulty: target === 0 ? Number.POSITIVE_INFINITY : TRUE_DIFF_ONE_NUMBER / target,
submissionHash: hashResult.toString('hex'), submissionHash: hashResult.toString('hex'),
hashBuffer: hashResult, hashBuffer: hashResult,
}; };
} }
public static meetsTarget(hashBuffer: Buffer, target: Buffer): boolean { public static meetsTarget(hashBuffer: Buffer, target: Buffer): boolean {
return DifficultyUtils.le256ToBigInt(hashBuffer) <= DifficultyUtils.le256ToBigInt(target); return DifficultyUtils.compareLe256(hashBuffer, target) <= 0;
} }
public static difficultyToTarget(difficulty: number): Buffer { public static difficultyToTarget(difficulty: number): Buffer {
@@ -51,8 +41,8 @@ export class DifficultyUtils {
throw new Error('Target must be 32 bytes'); throw new Error('Target must be 32 bytes');
} }
const targetBigInt = DifficultyUtils.le256ToBigInt(target); const targetNumber = DifficultyUtils.le256ToDouble(target);
return bigIntRatioToDifficulty(targetBigInt); return targetNumber === 0 ? Number.POSITIVE_INFINITY : TRUE_DIFF_ONE_NUMBER / targetNumber;
} }
public static hashRateToDifficulty(hashRate: number, sharesPerMinute: number): number { public static hashRateToDifficulty(hashRate: number, sharesPerMinute: number): number {
@@ -65,15 +55,11 @@ export class DifficultyUtils {
return difficulty; return difficulty;
} }
const maxTargetBigInt = DifficultyUtils.le256ToBigInt(maxTarget); if (DifficultyUtils.isZeroTarget(maxTarget)) {
if (maxTargetBigInt === 0n) {
return difficulty; return difficulty;
} }
const computedTargetBigInt = DifficultyUtils.le256ToBigInt( if (DifficultyUtils.compareLe256(DifficultyUtils.difficultyToTarget(difficulty), maxTarget) > 0) {
DifficultyUtils.difficultyToTarget(difficulty),
);
if (computedTargetBigInt > maxTargetBigInt) {
const clamped = DifficultyUtils.targetToDifficulty(maxTarget); const clamped = DifficultyUtils.targetToDifficulty(maxTarget);
return Number.isFinite(clamped) && clamped > 0 ? clamped : difficulty; return Number.isFinite(clamped) && clamped > 0 ? clamped : difficulty;
} }
@@ -111,7 +97,30 @@ export class DifficultyUtils {
return buf; return buf;
} }
private static le256ToBigInt(target: Buffer): bigint { private static le256ToDouble(target: Buffer): number {
return target.reduceRight((acc, byte) => (acc << 8n) | BigInt(byte), 0n); let value = 0;
for (let i = target.length - 1; i >= 0; i--) {
value = value * 256 + target[i];
}
return value;
}
private static compareLe256(left: Buffer, right: Buffer): number {
for (let i = 31; i >= 0; i--) {
const diff = left[i] - right[i];
if (diff !== 0) {
return diff;
}
}
return 0;
}
private static isZeroTarget(target: Buffer): boolean {
for (let i = 0; i < target.length; i++) {
if (target[i] !== 0) {
return false;
}
}
return true;
} }
} }
+15
View File
@@ -0,0 +1,15 @@
import * as crypto from 'crypto';
import { hash256 } from './hash.utils';
describe('hash256', () => {
it('matches native double sha256 for known input', () => {
const data = Buffer.from('public-pool-share-validation', 'utf8');
const expected = crypto
.createHash('sha256')
.update(crypto.createHash('sha256').update(data).digest())
.digest('hex');
expect(hash256(data).toString('hex')).toBe(expected);
});
});
+6
View File
@@ -0,0 +1,6 @@
import * as crypto from 'crypto';
export function hash256(data: Buffer): Buffer {
const first = crypto.createHash('sha256').update(data).digest();
return crypto.createHash('sha256').update(first).digest();
}