mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 17:15:03 -07:00
test coverage
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||||
|
import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
import { MockRecording1 } from '../../test/models/MockRecording1';
|
||||||
|
import { StratumV1JobsService } from '../services/stratum-v1-jobs.service';
|
||||||
|
import { MiningJob } from './MiningJob';
|
||||||
|
|
||||||
|
describe('MiningJob', () => {
|
||||||
|
let jobTemplate;
|
||||||
|
let job: MiningJob;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(new Date(parseInt(MockRecording1.TIME, 16) * 1000));
|
||||||
|
|
||||||
|
const blockTemplate$ = new BehaviorSubject(MockRecording1.BLOCK_TEMPLATE);
|
||||||
|
const bitcoinRpcService = {
|
||||||
|
newBlockTemplate$: blockTemplate$.asObservable(),
|
||||||
|
miningInfo: { blocks: MockRecording1.BLOCK_TEMPLATE.height }
|
||||||
|
};
|
||||||
|
jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||||
|
|
||||||
|
const jobsService = new StratumV1JobsService(bitcoinRpcService as any);
|
||||||
|
jobTemplate = await firstValueFrom(jobsService.newMiningJob$);
|
||||||
|
job = new MiningJob(
|
||||||
|
bitcoinjs.networks.testnet,
|
||||||
|
'1',
|
||||||
|
[{ address: 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4', percent: 100 }],
|
||||||
|
jobTemplate
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should split coinbase around 12 bytes of extranonce space', () => {
|
||||||
|
const notify = JSON.parse(job.response(jobTemplate));
|
||||||
|
const coinbasePart1 = notify.params[2];
|
||||||
|
const coinbasePart2 = notify.params[3];
|
||||||
|
const extraNonce1 = '57a6f098';
|
||||||
|
const extraNonce2 = 'c708000000000000';
|
||||||
|
const coinbase = bitcoinjs.Transaction.fromHex(`${coinbasePart1}${extraNonce1}${extraNonce2}${coinbasePart2}`);
|
||||||
|
|
||||||
|
expect(Buffer.byteLength(extraNonce1 + extraNonce2, 'hex')).toBe(12);
|
||||||
|
expect(coinbase.ins[0].script.toString('hex')).toContain(`${extraNonce1}${extraNonce2}`);
|
||||||
|
expect(coinbase.ins[0].script.toString('hex').endsWith(`${extraNonce1}${extraNonce2}`)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update block nonce, timestamp, version mask, and coinbase script', () => {
|
||||||
|
const extraNonce1 = '57a6f098';
|
||||||
|
const extraNonce2 = 'c708000000000000';
|
||||||
|
const timestamp = parseInt(MockRecording1.TIME, 16);
|
||||||
|
const originalMerkleRoot = Buffer.from(jobTemplate.block.merkleRoot);
|
||||||
|
|
||||||
|
const updatedBlock = job.copyAndUpdateBlock(
|
||||||
|
jobTemplate,
|
||||||
|
parseInt('00002000', 16),
|
||||||
|
parseInt('ed460d91', 16),
|
||||||
|
extraNonce1,
|
||||||
|
extraNonce2,
|
||||||
|
timestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updatedBlock.nonce).toBe(parseInt('ed460d91', 16));
|
||||||
|
expect(updatedBlock.timestamp).toBe(timestamp);
|
||||||
|
expect(updatedBlock.version).toBe(jobTemplate.block.version ^ parseInt('00002000', 16));
|
||||||
|
expect(updatedBlock.transactions[0].ins[0].script.toString('hex').endsWith(`${extraNonce1}${extraNonce2}`)).toBe(true);
|
||||||
|
expect(updatedBlock.merkleRoot.equals(originalMerkleRoot)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should leave block version unchanged without a version mask', () => {
|
||||||
|
const updatedBlock = job.copyAndUpdateBlock(
|
||||||
|
jobTemplate,
|
||||||
|
0,
|
||||||
|
parseInt('ed460d91', 16),
|
||||||
|
'57a6f098',
|
||||||
|
'c708000000000000',
|
||||||
|
parseInt(MockRecording1.TIME, 16)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updatedBlock.version).toBe(jobTemplate.block.version);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@ import { DataSource } from 'typeorm';
|
|||||||
import { MockRecording1 } from '../../test/models/MockRecording1';
|
import { MockRecording1 } from '../../test/models/MockRecording1';
|
||||||
import { AddressSettingsModule } from '../ORM/address-settings/address-settings.module';
|
import { AddressSettingsModule } from '../ORM/address-settings/address-settings.module';
|
||||||
import { AddressSettingsService } from '../ORM/address-settings/address-settings.service';
|
import { AddressSettingsService } from '../ORM/address-settings/address-settings.service';
|
||||||
|
import { BlocksEntity } from '../ORM/blocks/blocks.entity';
|
||||||
import { BlocksService } from '../ORM/blocks/blocks.service';
|
import { BlocksService } from '../ORM/blocks/blocks.service';
|
||||||
import { ClientStatisticsEntity } from '../ORM/client-statistics/client-statistics.entity';
|
import { ClientStatisticsEntity } from '../ORM/client-statistics/client-statistics.entity';
|
||||||
import { ClientStatisticsModule } from '../ORM/client-statistics/client-statistics.module';
|
import { ClientStatisticsModule } from '../ORM/client-statistics/client-statistics.module';
|
||||||
@@ -51,6 +52,8 @@ describe('StratumV1Client', () => {
|
|||||||
|
|
||||||
let socketEmitter: (...args: any[]) => void;
|
let socketEmitter: (...args: any[]) => void;
|
||||||
const emitMessage = (message: string) => socketEmitter(Buffer.from(`${message}\n`));
|
const emitMessage = (message: string) => socketEmitter(Buffer.from(`${message}\n`));
|
||||||
|
let consoleLogSpy: jest.SpyInstance;
|
||||||
|
let consoleErrorSpy: jest.SpyInstance;
|
||||||
|
|
||||||
let newBlockEmitter: BehaviorSubject<IBlockTemplate> = new BehaviorSubject(MockRecording1.BLOCK_TEMPLATE);
|
let newBlockEmitter: BehaviorSubject<IBlockTemplate> = new BehaviorSubject(MockRecording1.BLOCK_TEMPLATE);
|
||||||
|
|
||||||
@@ -65,7 +68,8 @@ describe('StratumV1Client', () => {
|
|||||||
synchronize: true,
|
synchronize: true,
|
||||||
autoLoadEntities: true,
|
autoLoadEntities: true,
|
||||||
cache: true,
|
cache: true,
|
||||||
logging: false
|
logging: false,
|
||||||
|
entities: [ClientEntity, ClientStatisticsEntity, BlocksEntity]
|
||||||
}),
|
}),
|
||||||
ClientModule,
|
ClientModule,
|
||||||
ClientStatisticsModule,
|
ClientStatisticsModule,
|
||||||
@@ -97,6 +101,8 @@ describe('StratumV1Client', () => {
|
|||||||
|
|
||||||
jest.useFakeTimers({ advanceTimers: true })
|
jest.useFakeTimers({ advanceTimers: true })
|
||||||
jest.setSystemTime(new Date(parseInt(MockRecording1.TIME, 16) * 1000));
|
jest.setSystemTime(new Date(parseInt(MockRecording1.TIME, 16) * 1000));
|
||||||
|
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||||
|
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||||
|
|
||||||
clientService = moduleRef.get<ClientService>(ClientService);
|
clientService = moduleRef.get<ClientService>(ClientService);
|
||||||
|
|
||||||
@@ -104,6 +110,7 @@ describe('StratumV1Client', () => {
|
|||||||
|
|
||||||
await dataSource.getRepository(ClientStatisticsEntity).delete({});
|
await dataSource.getRepository(ClientStatisticsEntity).delete({});
|
||||||
await dataSource.getRepository(ClientEntity).delete({});
|
await dataSource.getRepository(ClientEntity).delete({});
|
||||||
|
await dataSource.getRepository(BlocksEntity).delete({});
|
||||||
|
|
||||||
|
|
||||||
clientStatisticsService = moduleRef.get<ClientStatisticsService>(ClientStatisticsService);
|
clientStatisticsService = moduleRef.get<ClientStatisticsService>(ClientStatisticsService);
|
||||||
@@ -132,6 +139,12 @@ describe('StratumV1Client', () => {
|
|||||||
socket.end = jest.fn();
|
socket.end = jest.fn();
|
||||||
|
|
||||||
const addressSettings = moduleRef.get<AddressSettingsService>(AddressSettingsService);
|
const addressSettings = moduleRef.get<AddressSettingsService>(AddressSettingsService);
|
||||||
|
notificationService = {
|
||||||
|
notifySubscribersBlockFound: jest.fn().mockResolvedValue(undefined)
|
||||||
|
} as any;
|
||||||
|
blocksService = {
|
||||||
|
save: jest.fn().mockResolvedValue(undefined)
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
|
||||||
client = new StratumV1Client(
|
client = new StratumV1Client(
|
||||||
@@ -153,6 +166,8 @@ describe('StratumV1Client', () => {
|
|||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
client.destroy();
|
client.destroy();
|
||||||
|
consoleLogSpy.mockRestore();
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -273,6 +288,118 @@ describe('StratumV1Client', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reject duplicate submissions', async () => {
|
||||||
|
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBSCRIBE);
|
||||||
|
emitMessage(`{"id": 4, "method": "mining.suggest_difficulty", "params": [0]}`);
|
||||||
|
emitMessage(MockRecording1.MINING_AUTHORIZE);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBMIT);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
emitMessage(MockRecording1.MINING_SUBMIT);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
expect((client as any).write).lastCalledWith(`{"id":5,"result":null,"error":[22,"Duplicate share",""]}\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject submissions for unknown jobs', async () => {
|
||||||
|
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBSCRIBE);
|
||||||
|
emitMessage(`{"id": 4, "method": "mining.suggest_difficulty", "params": [0]}`);
|
||||||
|
emitMessage(MockRecording1.MINING_AUTHORIZE);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
emitMessage(`{"id": 5, "method": "mining.submit", "params": ["tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4.bitaxe3", "ff", "c708000000000000", "64b3f3ec", "ed460d91", "00002000"]}`);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
expect((client as any).write).lastCalledWith(`{"id":5,"result":null,"error":[21,"Job not found",""]}\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject submissions when the job template has expired', async () => {
|
||||||
|
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBSCRIBE);
|
||||||
|
emitMessage(`{"id": 4, "method": "mining.suggest_difficulty", "params": [0]}`);
|
||||||
|
emitMessage(MockRecording1.MINING_AUTHORIZE);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
stratumV1JobsService.blocks = {};
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBMIT);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
expect((client as any).write).lastCalledWith(`{"id":5,"result":null,"error":[21,"Job Template not found",""]}\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject low difficulty shares', async () => {
|
||||||
|
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBSCRIBE);
|
||||||
|
emitMessage(MockRecording1.MINING_SUGGEST_DIFFICULTY);
|
||||||
|
emitMessage(MockRecording1.MINING_AUTHORIZE);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBMIT);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
expect((client as any).write).lastCalledWith(`{"id":5,"result":null,"error":[23,"Difficulty too low",""]}\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject submissions with short extranonce2 values', async () => {
|
||||||
|
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBSCRIBE);
|
||||||
|
emitMessage(MockRecording1.MINING_AUTHORIZE);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
emitMessage(`{"id": 5, "method": "mining.submit", "params": ["tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4.bitaxe3", "1", "c7080000", "64b3f3ec", "ed460d91", "00002000"]}`);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
expect((client as any).write).lastCalledWith(expect.stringContaining(`"error":[20,"Mining Submit validation error"`));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close socket when a submit arrives before stratum is initialized', async () => {
|
||||||
|
const endSpy = jest.spyOn(socket, 'end');
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBMIT);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
expect(endSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should submit and persist found blocks', 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: 'block-share'
|
||||||
|
});
|
||||||
|
const addressSettings = moduleRef.get<AddressSettingsService>(AddressSettingsService);
|
||||||
|
jest.spyOn(addressSettings, 'resetBestDifficultyAndShares').mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBSCRIBE);
|
||||||
|
emitMessage(`{"id": 4, "method": "mining.suggest_difficulty", "params": [0]}`);
|
||||||
|
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(bitcoinRpcService.SUBMIT_BLOCK).toHaveBeenCalledWith(expect.any(String));
|
||||||
|
expect(blocksService.save).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
height: MockRecording1.BLOCK_TEMPLATE.height,
|
||||||
|
minerAddress: 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4',
|
||||||
|
worker: 'bitaxe3',
|
||||||
|
sessionId: MockRecording1.EXTRA_NONCE,
|
||||||
|
blockData: expect.any(String)
|
||||||
|
}));
|
||||||
|
expect(notificationService.notifySubscribersBlockFound).toHaveBeenCalled();
|
||||||
|
expect(addressSettings.resetBestDifficultyAndShares).toHaveBeenCalled();
|
||||||
|
expect((client as any).write).lastCalledWith(`{"id":5,"error":null,"result":true}\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { StratumV1ClientStatistics } from './StratumV1ClientStatistics';
|
||||||
|
|
||||||
|
describe('StratumV1ClientStatistics', () => {
|
||||||
|
const client = {
|
||||||
|
id: 'client-id',
|
||||||
|
address: 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4',
|
||||||
|
clientName: 'bitaxe3',
|
||||||
|
sessionId: '57a6f098'
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
let clientStatisticsService: {
|
||||||
|
insert: jest.Mock,
|
||||||
|
updateBulkAsync: jest.Mock
|
||||||
|
};
|
||||||
|
let statistics: StratumV1ClientStatistics;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(new Date('2026-05-06T12:00:00Z'));
|
||||||
|
clientStatisticsService = {
|
||||||
|
insert: jest.fn().mockResolvedValue(undefined),
|
||||||
|
updateBulkAsync: jest.fn().mockResolvedValue(undefined)
|
||||||
|
};
|
||||||
|
statistics = new StratumV1ClientStatistics(clientStatisticsService as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should insert the first share bucket', async () => {
|
||||||
|
await statistics.addShares(client, 64);
|
||||||
|
|
||||||
|
expect(clientStatisticsService.insert).toHaveBeenCalledWith({
|
||||||
|
time: new Date('2026-05-06T12:00:00Z').getTime(),
|
||||||
|
clientId: client.id,
|
||||||
|
shares: 64,
|
||||||
|
acceptedCount: 1,
|
||||||
|
address: client.address,
|
||||||
|
clientName: client.clientName,
|
||||||
|
sessionId: client.sessionId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the current share bucket for additional shares', async () => {
|
||||||
|
await statistics.addShares(client, 64);
|
||||||
|
await statistics.addShares(client, 32);
|
||||||
|
|
||||||
|
expect(clientStatisticsService.updateBulkAsync).toHaveBeenCalledWith({
|
||||||
|
time: new Date('2026-05-06T12:00:00Z').getTime(),
|
||||||
|
clientId: client.id,
|
||||||
|
shares: 96,
|
||||||
|
acceptedCount: 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a new share bucket when the time slot changes', async () => {
|
||||||
|
await statistics.addShares(client, 64);
|
||||||
|
jest.setSystemTime(new Date('2026-05-06T12:10:00Z'));
|
||||||
|
|
||||||
|
await statistics.addShares(client, 32);
|
||||||
|
|
||||||
|
expect(clientStatisticsService.updateBulkAsync).toHaveBeenCalledWith({
|
||||||
|
time: new Date('2026-05-06T12:00:00Z').getTime(),
|
||||||
|
clientId: client.id,
|
||||||
|
shares: 64,
|
||||||
|
acceptedCount: 1
|
||||||
|
});
|
||||||
|
expect(clientStatisticsService.insert).toHaveBeenLastCalledWith({
|
||||||
|
time: new Date('2026-05-06T12:10:00Z').getTime(),
|
||||||
|
clientId: client.id,
|
||||||
|
shares: 32,
|
||||||
|
acceptedCount: 1,
|
||||||
|
address: client.address,
|
||||||
|
clientName: client.clientName,
|
||||||
|
sessionId: client.sessionId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not suggest a difficulty change before enough time or shares have passed', () => {
|
||||||
|
expect(statistics.getSuggestedDifficulty(64)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should lower difficulty when a miner has not submitted shares for several minutes', () => {
|
||||||
|
jest.setSystemTime(new Date('2026-05-06T12:06:00Z'));
|
||||||
|
|
||||||
|
expect(statistics.getSuggestedDifficulty(64)).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should increase difficulty for rapid submissions', async () => {
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
jest.setSystemTime(new Date(Date.parse('2026-05-06T12:00:00Z') + (i * 1000)));
|
||||||
|
await statistics.addShares(client, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(statistics.getSuggestedDifficulty(64)).toBe(2048);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should decrease difficulty for slow submissions', async () => {
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
jest.setSystemTime(new Date(Date.parse('2026-05-06T12:00:00Z') + (i * 150000)));
|
||||||
|
await statistics.addShares(client, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(statistics.getSuggestedDifficulty(128)).toBe(16);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
|
||||||
|
import { AuthorizationMessage } from './AuthorizationMessage';
|
||||||
|
|
||||||
|
describe('AuthorizationMessage', () => {
|
||||||
|
it('should parse address, worker, and starting difficulty', () => {
|
||||||
|
const message = plainToInstance(
|
||||||
|
AuthorizationMessage,
|
||||||
|
JSON.parse('{"id":3,"method":"mining.authorize","params":["tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4.worker1","x,d=2048"]}')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(message.address).toBe('tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4');
|
||||||
|
expect(message.worker).toBe('worker1');
|
||||||
|
expect(message.password).toBe('x,d=2048');
|
||||||
|
expect(message.startingDiff).toBe(2048);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default worker name when one is not provided', () => {
|
||||||
|
const message = plainToInstance(
|
||||||
|
AuthorizationMessage,
|
||||||
|
JSON.parse('{"id":3,"method":"mining.authorize","params":["tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4","x"]}')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(message.worker).toBe('worker');
|
||||||
|
expect(message.startingDiff).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build successful authorization responses', () => {
|
||||||
|
const message = plainToInstance(
|
||||||
|
AuthorizationMessage,
|
||||||
|
JSON.parse('{"id":3,"method":"mining.authorize","params":["tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4.worker1","x"]}')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(message.response()).toEqual({
|
||||||
|
id: 3,
|
||||||
|
error: null,
|
||||||
|
result: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { plainToInstance } from 'class-transformer';
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
import { validate } from 'class-validator';
|
||||||
|
|
||||||
import { MiningSubmitMessage } from './MiningSubmitMessage';
|
import { MiningSubmitMessage } from './MiningSubmitMessage';
|
||||||
|
|
||||||
@@ -29,6 +30,34 @@ describe('MiningSubmitMessage', () => {
|
|||||||
expect(message.nonce).toEqual('2402812d');
|
expect(message.nonce).toEqual('2402812d');
|
||||||
expect(message.versionMask).toEqual('00006000');
|
expect(message.versionMask).toEqual('00006000');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should validate 8-byte extranonce2 submissions', async () => {
|
||||||
|
const errors = await validate(message);
|
||||||
|
|
||||||
|
expect(errors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject short extranonce2 submissions', async () => {
|
||||||
|
const shortMessage = plainToInstance(
|
||||||
|
MiningSubmitMessage,
|
||||||
|
JSON.parse(' {"id": 5, "method": "mining.submit", "params": ["tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4.bitaxe3", "1", "99020000", "64b1f10f", "2402812d", "00006000"]}'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const errors = await validate(shortMessage);
|
||||||
|
|
||||||
|
expect(errors.some(error => error.property === 'extraNonce2')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject long extranonce2 submissions', async () => {
|
||||||
|
const longMessage = plainToInstance(
|
||||||
|
MiningSubmitMessage,
|
||||||
|
JSON.parse(' {"id": 5, "method": "mining.submit", "params": ["tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4.bitaxe3", "1", "990200000000000000", "64b1f10f", "2402812d", "00006000"]}'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const errors = await validate(longMessage);
|
||||||
|
|
||||||
|
expect(errors.some(error => error.property === 'extraNonce2')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
|
||||||
|
import { SubscriptionMessage } from './SubscriptionMessage';
|
||||||
|
|
||||||
|
describe('SubscriptionMessage', () => {
|
||||||
|
it('should parse and refine known user agents', () => {
|
||||||
|
const bosminer = plainToInstance(
|
||||||
|
SubscriptionMessage,
|
||||||
|
JSON.parse('{"id":1,"method":"mining.subscribe","params":["bosminer/23.08"]}')
|
||||||
|
);
|
||||||
|
const cpuminer = plainToInstance(
|
||||||
|
SubscriptionMessage,
|
||||||
|
JSON.parse('{"id":1,"method":"mining.subscribe","params":["cpuminer-opt/1.0"]}')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(bosminer.userAgent).toBe('Braiins OS');
|
||||||
|
expect(cpuminer.userAgent).toBe('cpuminer');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default missing user agents to unknown', () => {
|
||||||
|
const message = plainToInstance(
|
||||||
|
SubscriptionMessage,
|
||||||
|
JSON.parse('{"id":1,"method":"mining.subscribe","params":[]}')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(message.userAgent).toBe('unknown');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with extranonce2 size of 8 bytes', () => {
|
||||||
|
const message = plainToInstance(
|
||||||
|
SubscriptionMessage,
|
||||||
|
JSON.parse('{"id":1,"method":"mining.subscribe","params":["bitaxe v2.2"]}')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(message.response('57a6f098')).toEqual({
|
||||||
|
id: 1,
|
||||||
|
error: null,
|
||||||
|
result: [
|
||||||
|
[['mining.notify', '57a6f098']],
|
||||||
|
'57a6f098',
|
||||||
|
8
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { BehaviorSubject, firstValueFrom, skip } from 'rxjs';
|
||||||
|
|
||||||
|
import { MockRecording1 } from '../../test/models/MockRecording1';
|
||||||
|
import { IBlockTemplate } from '../models/bitcoin-rpc/IBlockTemplate';
|
||||||
|
import { StratumV1JobsService } from './stratum-v1-jobs.service';
|
||||||
|
|
||||||
|
describe('StratumV1JobsService', () => {
|
||||||
|
let blockTemplate$: BehaviorSubject<IBlockTemplate>;
|
||||||
|
let bitcoinRpcService: { newBlockTemplate$: any, miningInfo: { blocks: number } };
|
||||||
|
let service: StratumV1JobsService;
|
||||||
|
let consoleLogSpy: jest.SpyInstance;
|
||||||
|
|
||||||
|
const createTemplate = (height = MockRecording1.BLOCK_TEMPLATE.height): IBlockTemplate => ({
|
||||||
|
...MockRecording1.BLOCK_TEMPLATE,
|
||||||
|
height
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(new Date(parseInt(MockRecording1.TIME, 16) * 1000));
|
||||||
|
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||||
|
|
||||||
|
blockTemplate$ = new BehaviorSubject(createTemplate());
|
||||||
|
bitcoinRpcService = {
|
||||||
|
newBlockTemplate$: blockTemplate$.asObservable(),
|
||||||
|
miningInfo: { blocks: MockRecording1.BLOCK_TEMPLATE.height }
|
||||||
|
};
|
||||||
|
service = new StratumV1JobsService(bitcoinRpcService as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
consoleLogSpy.mockRestore();
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create job templates from block templates', async () => {
|
||||||
|
const jobTemplate = await firstValueFrom(service.newMiningJob$);
|
||||||
|
|
||||||
|
expect(jobTemplate.blockData).toEqual(expect.objectContaining({
|
||||||
|
id: '1',
|
||||||
|
height: MockRecording1.BLOCK_TEMPLATE.height,
|
||||||
|
clearJobs: true,
|
||||||
|
coinbasevalue: MockRecording1.BLOCK_TEMPLATE.coinbasevalue
|
||||||
|
}));
|
||||||
|
expect(jobTemplate.merkle_branch.length).toBeGreaterThan(0);
|
||||||
|
expect(jobTemplate.block.transactions[0].ins[0].witness[0]).toHaveLength(32);
|
||||||
|
expect(service.getJobTemplateById('1')).toBe(jobTemplate);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear stale jobs when the block height changes', async () => {
|
||||||
|
await firstValueFrom(service.newMiningJob$);
|
||||||
|
service.addJob({ jobId: 'old-job', creation: Date.now() } as any);
|
||||||
|
|
||||||
|
bitcoinRpcService.miningInfo.blocks = MockRecording1.BLOCK_TEMPLATE.height + 1;
|
||||||
|
const nextTemplate = firstValueFrom(service.newMiningJob$.pipe(skip(1)));
|
||||||
|
blockTemplate$.next(createTemplate(MockRecording1.BLOCK_TEMPLATE.height + 1));
|
||||||
|
const jobTemplate = await nextTemplate;
|
||||||
|
|
||||||
|
expect(jobTemplate.blockData.clearJobs).toBe(true);
|
||||||
|
expect(service.getJobById('old-job')).toBeUndefined();
|
||||||
|
expect(Object.keys(service.blocks)).toEqual([jobTemplate.blockData.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should expire old jobs and templates when the block height is unchanged', async () => {
|
||||||
|
await firstValueFrom(service.newMiningJob$);
|
||||||
|
const oldCreation = Date.now() - (1000 * 60 * 6);
|
||||||
|
service.jobs['old-job'] = { jobId: 'old-job', creation: oldCreation } as any;
|
||||||
|
service.blocks['old-template'] = {
|
||||||
|
blockData: { creation: oldCreation }
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
bitcoinRpcService.miningInfo.blocks = MockRecording1.BLOCK_TEMPLATE.height;
|
||||||
|
const nextTemplate = firstValueFrom(service.newMiningJob$.pipe(skip(1)));
|
||||||
|
blockTemplate$.next(createTemplate());
|
||||||
|
const jobTemplate = await nextTemplate;
|
||||||
|
|
||||||
|
expect(jobTemplate.blockData.clearJobs).toBe(false);
|
||||||
|
expect(service.getJobById('old-job')).toBeUndefined();
|
||||||
|
expect(service.getJobTemplateById('old-template')).toBeUndefined();
|
||||||
|
expect(service.getJobTemplateById(jobTemplate.blockData.id)).toBe(jobTemplate);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should increment job ids when jobs are added', () => {
|
||||||
|
expect(service.getNextId()).toBe('1');
|
||||||
|
|
||||||
|
service.addJob({ jobId: '1', creation: Date.now() } as any);
|
||||||
|
|
||||||
|
expect(service.getNextId()).toBe('2');
|
||||||
|
expect(service.getJobById('1')).toEqual(expect.objectContaining({ jobId: '1' }));
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user