mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 17:15:03 -07:00
Stratum V2 initial commit
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
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 };
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user