mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 09:05:06 -07:00
performance and optimization
This commit is contained in:
@@ -20,6 +20,7 @@ lerna-debug.log*
|
||||
/.nyc_output
|
||||
/.baseline-*.json
|
||||
/.test-artifacts
|
||||
/.profiles
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AddressType, getAddressInfo } from 'bitcoin-address-validation';
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
|
||||
import { IJobTemplate } from '../services/stratum-v1-jobs.service';
|
||||
import { hash256 } from '../utils/hash.utils';
|
||||
import { eResponseMethod } from './enums/eResponseMethod';
|
||||
import { IMiningNotify } from './stratum-messages/IMiningNotify';
|
||||
import { TOTAL_EXTRANONCE_SIZE_BYTES } from './stratum.constants';
|
||||
@@ -96,7 +97,7 @@ export class MiningJob {
|
||||
Buffer.from(`${extraNonce}${extraNonce2}`, 'hex'),
|
||||
this.coinbasePart2Buffer,
|
||||
]);
|
||||
const coinbaseHash = bitcoinjs.crypto.hash256(coinbaseBuffer);
|
||||
const coinbaseHash = hash256(coinbaseBuffer);
|
||||
const merkleRoot = this.calculateMerkleRootHash(coinbaseHash, this.merkleBranchBuffers);
|
||||
|
||||
let version = jobTemplate.block.version;
|
||||
@@ -154,7 +155,7 @@ export class MiningJob {
|
||||
|
||||
for (let i = 0; i < merkleBranches.length; i++) {
|
||||
bothMerkles.set(merkleBranches[i], 32);
|
||||
newRoot = bitcoinjs.crypto.hash256(bothMerkles);
|
||||
newRoot = hash256(bothMerkles);
|
||||
bothMerkles.set(newRoot);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ClientService } from '../ORM/client/client.service';
|
||||
import { BitcoinRpcService as MockBitcoinRpcService } from '../services/bitcoin-rpc.service';
|
||||
import { NotificationService } from '../services/notification.service';
|
||||
import { StratumV1JobsService } from '../services/stratum-v1-jobs.service';
|
||||
import { DifficultyUtils } from '../utils/difficulty.utils';
|
||||
import { IBlockTemplate } from './bitcoin-rpc/IBlockTemplate';
|
||||
import { MiningJob } from './MiningJob';
|
||||
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, 'calculateDifficulty').mockReturnValue({
|
||||
submissionDifficulty: 1024,
|
||||
submissionHash: 'share'
|
||||
submissionHash: 'share',
|
||||
hashBuffer: DifficultyUtils.difficultyToTarget(1024),
|
||||
});
|
||||
const getSettingsSpy = jest.spyOn(addressSettings, 'getSettings');
|
||||
const updateIfHigherSpy = jest.spyOn(addressSettings as any, 'updateBestDifficultyIfHigher').mockResolvedValue({ affected: 1 });
|
||||
@@ -495,6 +497,27 @@ describe('StratumV1Client', () => {
|
||||
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 () => {
|
||||
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, 'calculateDifficulty').mockReturnValue({
|
||||
submissionDifficulty: Number.MAX_SAFE_INTEGER,
|
||||
submissionHash: 'block-share'
|
||||
submissionHash: 'block-share',
|
||||
hashBuffer: Buffer.alloc(32),
|
||||
});
|
||||
jest.spyOn(addressSettings, 'resetBestDifficultyAndShares').mockResolvedValue(undefined);
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import { BitcoinRpcService } from '../services/bitcoin-rpc.service';
|
||||
import { NotificationService } from '../services/notification.service';
|
||||
import { RedisMessagingService } from '../services/redis-messaging.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 { eResponseMethod } from './enums/eResponseMethod';
|
||||
import { eStratumErrorCode } from './enums/eStratumErrorCode';
|
||||
@@ -51,6 +53,7 @@ export class StratumV1Client {
|
||||
private stratumInitialized = false;
|
||||
private usedSuggestedDifficulty = false;
|
||||
private sessionDifficulty: number = 100000;
|
||||
private sessionDifficultyTarget: Buffer = DifficultyUtils.difficultyToTarget(this.sessionDifficulty);
|
||||
|
||||
private clientEntity: ClientEntity;
|
||||
private creatingEntity: Promise<void>;
|
||||
@@ -268,6 +271,7 @@ export class StratumV1Client {
|
||||
this.clientAuthorization = authorizationMessage;
|
||||
if (this.clientSuggestedDifficulty == null && this.clientAuthorization.startingDiff != null && this.clientAuthorization.startingDiff > this.sessionDifficulty) {
|
||||
this.sessionDifficulty = this.clientAuthorization.startingDiff;
|
||||
this.sessionDifficultyTarget = DifficultyUtils.difficultyToTarget(this.sessionDifficulty);
|
||||
}
|
||||
const success = await this.write(JSON.stringify(this.clientAuthorization.response()) + '\n');
|
||||
if (!success) {
|
||||
@@ -309,6 +313,7 @@ export class StratumV1Client {
|
||||
|
||||
this.clientSuggestedDifficulty = suggestDifficultyMessage;
|
||||
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');
|
||||
if (!success) {
|
||||
return;
|
||||
@@ -610,19 +615,23 @@ export class StratumV1Client {
|
||||
submission.extraNonce2,
|
||||
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}`);
|
||||
|
||||
|
||||
if (submissionDifficulty >= this.sessionDifficulty) {
|
||||
if (DifficultyUtils.meetsTarget(hashBuffer, this.sessionDifficultyTarget)) {
|
||||
const success = await this.write(JSON.stringify(submission.response()) + '\n');
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 !!!');
|
||||
const updatedJobBlock = job.copyAndUpdateBlock(
|
||||
jobTemplate,
|
||||
@@ -668,7 +677,7 @@ export class StratumV1Client {
|
||||
? (jobTemplate.block.version ^ versionMask).toString(16)
|
||||
: jobTemplate.block.version.toString(16),
|
||||
extraNonce2: submission.extraNonce2,
|
||||
isBlockCandidate: submissionDifficulty >= jobTemplate.blockData.networkDifficulty,
|
||||
isBlockCandidate,
|
||||
blockSubmissionResult,
|
||||
});
|
||||
await this.statistics.addShares(this.clientEntity, this.sessionDifficulty);
|
||||
@@ -718,6 +727,7 @@ export class StratumV1Client {
|
||||
if (targetDiff != this.sessionDifficulty) {
|
||||
//console.log(`Adjusting ${this.extraNonceAndSessionId} difficulty from ${this.sessionDifficulty} to ${targetDiff}`);
|
||||
this.sessionDifficulty = targetDiff;
|
||||
this.sessionDifficultyTarget = DifficultyUtils.difficultyToTarget(this.sessionDifficulty);
|
||||
|
||||
const data = JSON.stringify({
|
||||
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 submissionDifficulty = target === 0 ? Number.POSITIVE_INFINITY : TRUE_DIFF_ONE / target;
|
||||
return { submissionDifficulty, submissionHash: hashResult.toString('hex') };
|
||||
return { submissionDifficulty, submissionHash: hashResult.toString('hex'), hashBuffer: hashResult };
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import { Socket } from 'net';
|
||||
import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
||||
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
serializeSubmitSharesExtended,
|
||||
} from './sv2/sv2-extended-messages';
|
||||
import { deserializeSetNewPrevHash, deserializeSubmitSharesError } from './sv2/sv2-messages';
|
||||
import { MiningJob } from './MiningJob';
|
||||
import { StratumV2Client } from './StratumV2Client';
|
||||
|
||||
describe('StratumV2Client extended channels', () => {
|
||||
@@ -145,9 +147,87 @@ describe('StratumV2Client extended channels', () => {
|
||||
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<{
|
||||
client: StratumV2Client;
|
||||
sentFrames: any[];
|
||||
bitcoinRpcService: { SUBMIT_BLOCK: jest.Mock };
|
||||
blocksService: { save: jest.Mock };
|
||||
notificationService: { notifySubscribersBlockFound: jest.Mock };
|
||||
shareAccountingService: { recordAcceptedShare: jest.Mock };
|
||||
redisMessagingService: { setClientPresence: jest.Mock; removeClientPresence: jest.Mock };
|
||||
jobTemplate: any;
|
||||
@@ -196,6 +276,12 @@ describe('StratumV2Client extended channels', () => {
|
||||
const shareAccountingService = {
|
||||
recordAcceptedShare: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const notificationService = {
|
||||
notifySubscribersBlockFound: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const blocksService = {
|
||||
save: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const redisMessagingService = {
|
||||
setClientPresence: jest.fn().mockResolvedValue(undefined),
|
||||
removeClientPresence: jest.fn().mockResolvedValue(undefined),
|
||||
@@ -225,8 +311,8 @@ describe('StratumV2Client extended channels', () => {
|
||||
stratumV1JobsService,
|
||||
bitcoinRpcService as any,
|
||||
clientService as any,
|
||||
{ notifySubscribersBlockFound: jest.fn().mockResolvedValue(undefined) } as any,
|
||||
{ save: jest.fn().mockResolvedValue(undefined) } as any,
|
||||
notificationService as any,
|
||||
blocksService as any,
|
||||
{
|
||||
get: jest.fn((key: string) => {
|
||||
switch (key) {
|
||||
@@ -251,6 +337,15 @@ describe('StratumV2Client extended channels', () => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
return { client, sentFrames, shareAccountingService, redisMessagingService, jobTemplate };
|
||||
return {
|
||||
client,
|
||||
sentFrames,
|
||||
bitcoinRpcService,
|
||||
blocksService,
|
||||
notificationService,
|
||||
shareAccountingService,
|
||||
redisMessagingService,
|
||||
jobTemplate,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import { StratumV2Service } from '../services/stratum-v2.service';
|
||||
import { IJobTemplate, StratumV1JobsService } from '../services/stratum-v1-jobs.service';
|
||||
import { patchCoinbasePrefixVarint } from '../utils/coinbase-prefix.utils';
|
||||
import { DifficultyUtils } from '../utils/difficulty.utils';
|
||||
import { hash256 } from '../utils/hash.utils';
|
||||
import { MiningJob } from './MiningJob';
|
||||
import { StratumV1ClientStatistics } from './StratumV1ClientStatistics';
|
||||
import { TOTAL_EXTRANONCE_SIZE_BYTES } from './stratum.constants';
|
||||
@@ -560,7 +561,7 @@ export class StratumV2Client {
|
||||
);
|
||||
|
||||
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> {
|
||||
@@ -608,12 +609,12 @@ export class StratumV2Client {
|
||||
submission.extranonce,
|
||||
extendedJob.coinbaseSuffix,
|
||||
]);
|
||||
let merkleRoot = bitcoinjs.crypto.hash256(coinbaseTxBytes);
|
||||
let merkleRoot = hash256(coinbaseTxBytes);
|
||||
const merklePair = Buffer.alloc(64);
|
||||
for (const sibling of extendedJob.merklePath) {
|
||||
merklePair.set(merkleRoot, 0);
|
||||
merklePair.set(sibling, 32);
|
||||
merkleRoot = bitcoinjs.crypto.hash256(merklePair);
|
||||
merkleRoot = hash256(merklePair);
|
||||
}
|
||||
|
||||
const header = this.buildHeader(
|
||||
@@ -650,7 +651,10 @@ export class StratumV2Client {
|
||||
|
||||
channel.acceptedShareCount++;
|
||||
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);
|
||||
}
|
||||
await this.recordAcceptedShare(submissionDifficulty, jobDifficulty, extendedJob.jobTemplate, updatedJobBlock, {
|
||||
@@ -669,9 +673,13 @@ export class StratumV2Client {
|
||||
jobTemplate: IJobTemplate,
|
||||
submissionDifficulty: number,
|
||||
jobDifficulty: number,
|
||||
hashBuffer: Buffer,
|
||||
): Promise<void> {
|
||||
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;
|
||||
updatedJobBlock = job.copyAndUpdateBlock(
|
||||
jobTemplate,
|
||||
|
||||
@@ -37,6 +37,10 @@ describe('MiningSubmitMessage', () => {
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should hash submissions deterministically', () => {
|
||||
expect(message.hash()).toBe('t2bFzhZ6mketRxa5nOoKrNxG3RkFIZuOwY1WewFtv9k=');
|
||||
});
|
||||
|
||||
it('should reject short extranonce2 submissions', async () => {
|
||||
const shortMessage = plainToInstance(
|
||||
MiningSubmitMessage,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ArrayMaxSize, ArrayMinSize, IsArray, IsString, Length } from 'class-val
|
||||
import { eRequestMethod } from '../enums/eRequestMethod';
|
||||
import { EXTRANONCE2_SIZE_BYTES } from '../stratum.constants';
|
||||
import { StratumBaseMessage } from './StratumBaseMessage';
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import { hash256 } from '../../utils/hash.utils';
|
||||
|
||||
|
||||
export class MiningSubmitMessage extends StratumBaseMessage {
|
||||
@@ -69,7 +69,7 @@ export class MiningSubmitMessage extends StratumBaseMessage {
|
||||
|
||||
public hash(): string{
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 { MiningJob } from '../models/MiningJob';
|
||||
import { hash256 } from '../utils/hash.utils';
|
||||
import { BitcoinRpcService } from './bitcoin-rpc.service';
|
||||
|
||||
export interface IJobTemplate {
|
||||
@@ -95,7 +96,7 @@ export class StratumV1JobsService {
|
||||
|
||||
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);
|
||||
block.merkleRoot = merkleBranches.pop();
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -1,35 +1,25 @@
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import { hash256 } from './hash.utils';
|
||||
|
||||
const TRUE_DIFF_ONE_BIGINT = BigInt(
|
||||
'26959535291011309493156476344723991336010898738574164086137773096960',
|
||||
);
|
||||
const TRUE_DIFF_ONE_NUMBER = 2.695953529101131e67;
|
||||
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 {
|
||||
public static calculateDifficulty(header: Buffer): { submissionDifficulty: number; submissionHash: string; hashBuffer: Buffer } {
|
||||
const hashResult = bitcoinjs.crypto.hash256(header);
|
||||
const target = DifficultyUtils.le256ToBigInt(hashResult);
|
||||
const hashResult = hash256(header);
|
||||
const target = DifficultyUtils.le256ToDouble(hashResult);
|
||||
|
||||
return {
|
||||
submissionDifficulty: bigIntRatioToDifficulty(target),
|
||||
submissionDifficulty: target === 0 ? Number.POSITIVE_INFINITY : TRUE_DIFF_ONE_NUMBER / target,
|
||||
submissionHash: hashResult.toString('hex'),
|
||||
hashBuffer: hashResult,
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -51,8 +41,8 @@ export class DifficultyUtils {
|
||||
throw new Error('Target must be 32 bytes');
|
||||
}
|
||||
|
||||
const targetBigInt = DifficultyUtils.le256ToBigInt(target);
|
||||
return bigIntRatioToDifficulty(targetBigInt);
|
||||
const targetNumber = DifficultyUtils.le256ToDouble(target);
|
||||
return targetNumber === 0 ? Number.POSITIVE_INFINITY : TRUE_DIFF_ONE_NUMBER / targetNumber;
|
||||
}
|
||||
|
||||
public static hashRateToDifficulty(hashRate: number, sharesPerMinute: number): number {
|
||||
@@ -65,15 +55,11 @@ export class DifficultyUtils {
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
const maxTargetBigInt = DifficultyUtils.le256ToBigInt(maxTarget);
|
||||
if (maxTargetBigInt === 0n) {
|
||||
if (DifficultyUtils.isZeroTarget(maxTarget)) {
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
const computedTargetBigInt = DifficultyUtils.le256ToBigInt(
|
||||
DifficultyUtils.difficultyToTarget(difficulty),
|
||||
);
|
||||
if (computedTargetBigInt > maxTargetBigInt) {
|
||||
if (DifficultyUtils.compareLe256(DifficultyUtils.difficultyToTarget(difficulty), maxTarget) > 0) {
|
||||
const clamped = DifficultyUtils.targetToDifficulty(maxTarget);
|
||||
return Number.isFinite(clamped) && clamped > 0 ? clamped : difficulty;
|
||||
}
|
||||
@@ -111,7 +97,30 @@ export class DifficultyUtils {
|
||||
return buf;
|
||||
}
|
||||
|
||||
private static le256ToBigInt(target: Buffer): bigint {
|
||||
return target.reduceRight((acc, byte) => (acc << 8n) | BigInt(byte), 0n);
|
||||
private static le256ToDouble(target: Buffer): number {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user