mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 09:05:06 -07:00
207 lines
8.9 KiB
TypeScript
207 lines
8.9 KiB
TypeScript
import { ConfigService } from '@nestjs/config';
|
|
import { Socket } from 'net';
|
|
import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
|
|
|
import { MockRecording1 } from '../../test/models/MockRecording1';
|
|
import { ClientEntity } from '../ORM/client/client.entity';
|
|
import { StratumV1JobsService } from '../services/stratum-v1-jobs.service';
|
|
import { BufferReader } from './sv2/sv2-binary-codec';
|
|
import { SV2_CHANNEL_MSG_FLAG, Sv2MsgType } from './sv2/sv2-constants';
|
|
import {
|
|
deserializeOpenExtendedMiningChannelSuccess,
|
|
deserializeNewExtendedMiningJob,
|
|
serializeOpenExtendedMiningChannel,
|
|
serializeSubmitSharesExtended,
|
|
} from './sv2/sv2-extended-messages';
|
|
import { deserializeSetNewPrevHash, deserializeSubmitSharesError } from './sv2/sv2-messages';
|
|
import { StratumV2Client } from './StratumV2Client';
|
|
|
|
describe('StratumV2Client extended channels', () => {
|
|
let consoleLogSpy: jest.SpyInstance;
|
|
let consoleWarnSpy: jest.SpyInstance;
|
|
let consoleErrorSpy: jest.SpyInstance;
|
|
|
|
beforeEach(() => {
|
|
jest.useFakeTimers();
|
|
jest.setSystemTime(new Date(parseInt(MockRecording1.TIME, 16) * 1000));
|
|
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
});
|
|
|
|
afterEach(() => {
|
|
consoleLogSpy.mockRestore();
|
|
consoleWarnSpy.mockRestore();
|
|
consoleErrorSpy.mockRestore();
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it('opens an extended channel and sends initial extended work', async () => {
|
|
const { client, sentFrames } = await createClient();
|
|
const maxTarget = Buffer.alloc(32, 0xff);
|
|
|
|
await (client as any).handleOpenExtendedMiningChannel(serializeOpenExtendedMiningChannel({
|
|
requestId: 7,
|
|
userIdentity: 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4.worker',
|
|
nominalHashRate: 0,
|
|
maxTarget,
|
|
minExtranonceSize: 8,
|
|
}));
|
|
|
|
const successFrame = sentFrames.find(frame => frame.msgType === Sv2MsgType.OPEN_EXTENDED_MINING_CHANNEL_SUCCESS);
|
|
const jobFrames = sentFrames.filter(frame => frame.msgType === Sv2MsgType.NEW_EXTENDED_MINING_JOB);
|
|
const jobFrame = jobFrames[jobFrames.length - 1];
|
|
const prevHashFrames = sentFrames.filter(frame => frame.msgType === Sv2MsgType.SET_NEW_PREV_HASH);
|
|
const prevHashFrame = prevHashFrames[prevHashFrames.length - 1];
|
|
|
|
expect(successFrame?.extensionType).toBe(0);
|
|
const success = deserializeOpenExtendedMiningChannelSuccess(new BufferReader(successFrame.payload));
|
|
expect(success.requestId).toBe(7);
|
|
expect(success.channelId).toBe(1);
|
|
expect(success.extranonceSize).toBe(8);
|
|
expect(success.extranoncePrefix).toEqual(Buffer.from('00000001', 'hex'));
|
|
|
|
expect(jobFrame?.extensionType).toBe(SV2_CHANNEL_MSG_FLAG);
|
|
const job = deserializeNewExtendedMiningJob(new BufferReader(jobFrame.payload));
|
|
expect(job.channelId).toBe(1);
|
|
expect(job.minNtime).toBeNull();
|
|
expect(job.coinbasePrefix.length).toBeGreaterThan(0);
|
|
expect(job.coinbaseSuffix.length).toBeGreaterThan(0);
|
|
expect(job.merklePath.length).toBeGreaterThan(0);
|
|
|
|
expect(prevHashFrame?.extensionType).toBe(SV2_CHANNEL_MSG_FLAG);
|
|
const prevHash = deserializeSetNewPrevHash(new BufferReader(prevHashFrame.payload));
|
|
expect(prevHash.channelId).toBe(1);
|
|
expect(prevHash.jobId).toBe(job.jobId);
|
|
});
|
|
|
|
it('rejects extended shares with the wrong negotiated extranonce size', async () => {
|
|
const { client, sentFrames } = await createClient();
|
|
await (client as any).handleOpenExtendedMiningChannel(serializeOpenExtendedMiningChannel({
|
|
requestId: 1,
|
|
userIdentity: 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4.worker',
|
|
nominalHashRate: 0,
|
|
maxTarget: Buffer.alloc(32, 0xff),
|
|
minExtranonceSize: 8,
|
|
}));
|
|
const jobFrames = sentFrames.filter(frame => frame.msgType === Sv2MsgType.NEW_EXTENDED_MINING_JOB);
|
|
const jobFrame = jobFrames[jobFrames.length - 1];
|
|
const job = deserializeNewExtendedMiningJob(new BufferReader(jobFrame.payload));
|
|
sentFrames.length = 0;
|
|
|
|
await (client as any).handleSubmitSharesExtended(serializeSubmitSharesExtended({
|
|
channelId: 1,
|
|
sequenceNumber: 42,
|
|
jobId: job.jobId,
|
|
nonce: 0,
|
|
ntime: parseInt(MockRecording1.TIME, 16),
|
|
version: job.version,
|
|
extranonce: Buffer.alloc(7),
|
|
}));
|
|
|
|
const errorFrame = sentFrames.find(frame => frame.msgType === Sv2MsgType.SUBMIT_SHARES_ERROR);
|
|
expect(errorFrame?.extensionType).toBe(SV2_CHANNEL_MSG_FLAG);
|
|
const error = deserializeSubmitSharesError(new BufferReader(errorFrame.payload));
|
|
expect(error.channelId).toBe(1);
|
|
expect(error.sequenceNumber).toBe(42);
|
|
expect(error.errorCode).toBe('invalid-extranonce-size');
|
|
});
|
|
|
|
async function createClient(): Promise<{ client: StratumV2Client; sentFrames: any[] }> {
|
|
const blockTemplate$ = new BehaviorSubject(MockRecording1.BLOCK_TEMPLATE);
|
|
const bitcoinRpcService = {
|
|
newBlockTemplate$: blockTemplate$.asObservable(),
|
|
miningInfo: { blocks: MockRecording1.BLOCK_TEMPLATE.height },
|
|
SUBMIT_BLOCK: jest.fn().mockResolvedValue(null),
|
|
};
|
|
const stratumV1JobsService = new StratumV1JobsService(bitcoinRpcService as any);
|
|
await firstValueFrom(stratumV1JobsService.newMiningJob$);
|
|
|
|
const mockSocket: any = {
|
|
destroyed: false,
|
|
writableEnded: false,
|
|
on: jest.fn().mockReturnThis(),
|
|
write: jest.fn((data, callback) => {
|
|
callback?.();
|
|
return true;
|
|
}),
|
|
destroy: jest.fn(function destroy() {
|
|
mockSocket.destroyed = true;
|
|
return mockSocket;
|
|
}),
|
|
};
|
|
const socket = mockSocket as Socket;
|
|
|
|
const clientEntity = {
|
|
id: 1,
|
|
sessionId: 'sv2-session',
|
|
address: 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4',
|
|
clientName: 'worker',
|
|
userAgent: 'test/sv2',
|
|
bestDifficulty: 0,
|
|
} as unknown as ClientEntity;
|
|
|
|
let nextChannelId = 1;
|
|
const sentFrames: any[] = [];
|
|
const client = new StratumV2Client(
|
|
socket,
|
|
Buffer.alloc(0),
|
|
{
|
|
getNoiseConfig: () => ({
|
|
staticKeypair: {
|
|
privateKey: Buffer.alloc(32, 1),
|
|
publicKey: Buffer.alloc(64, 2),
|
|
},
|
|
certificateMessage: {
|
|
version: 0,
|
|
validFrom: 0,
|
|
notValidAfter: 1,
|
|
signature: Buffer.alloc(64),
|
|
},
|
|
}),
|
|
getNextChannelId: () => nextChannelId++,
|
|
generateExtranoncePrefix: () => Buffer.from('00000002', 'hex'),
|
|
allocateExtendedExtranoncePrefix: jest.fn(() => Buffer.from('00000001', 'hex')),
|
|
releaseExtendedExtranoncePrefix: jest.fn(),
|
|
getExtendedMinerExtranonceSize: () => 8,
|
|
} as any,
|
|
stratumV1JobsService,
|
|
bitcoinRpcService as any,
|
|
{
|
|
insert: jest.fn().mockResolvedValue(clientEntity),
|
|
delete: jest.fn().mockResolvedValue(undefined),
|
|
heartbeatBulkAsync: jest.fn(),
|
|
updateBestDifficultyIfHigher: jest.fn().mockResolvedValue(undefined),
|
|
} as any,
|
|
{
|
|
insert: jest.fn().mockResolvedValue(undefined),
|
|
updateBulkAsync: jest.fn(),
|
|
} as any,
|
|
{ notifySubscribersBlockFound: jest.fn().mockResolvedValue(undefined) } as any,
|
|
{ save: jest.fn().mockResolvedValue(undefined) } as any,
|
|
{
|
|
get: jest.fn((key: string) => {
|
|
switch (key) {
|
|
case 'NETWORK':
|
|
return 'testnet';
|
|
case 'SV2_START_DIFFICULTY':
|
|
return '1';
|
|
default:
|
|
return null;
|
|
}
|
|
}),
|
|
} as unknown as ConfigService,
|
|
{
|
|
resetBestDifficultyAndShares: jest.fn().mockResolvedValue(undefined),
|
|
updateBestDifficultyIfHigher: jest.fn().mockResolvedValue(undefined),
|
|
} as any,
|
|
);
|
|
(client as any).sendFrame = jest.fn((msgType: number, payload: Buffer, extensionType = 0) => {
|
|
sentFrames.push({ msgType, payload, extensionType });
|
|
return Promise.resolve();
|
|
});
|
|
|
|
return { client, sentFrames };
|
|
}
|
|
});
|