Files
public-pool/src/models/sv2/sv2-jdp-messages.spec.ts
T
2026-06-05 13:14:09 -04:00

313 lines
11 KiB
TypeScript

import { BufferReader } from './sv2-binary-codec';
import {
Sv2AllocateMiningJobToken,
serializeAllocateMiningJobToken,
deserializeAllocateMiningJobToken,
Sv2AllocateMiningJobTokenSuccess,
serializeAllocateMiningJobTokenSuccess,
deserializeAllocateMiningJobTokenSuccess,
Sv2DeclareMiningJob,
serializeDeclareMiningJob,
deserializeDeclareMiningJob,
Sv2DeclareMiningJobSuccess,
serializeDeclareMiningJobSuccess,
deserializeDeclareMiningJobSuccess,
Sv2DeclareMiningJobError,
serializeDeclareMiningJobError,
deserializeDeclareMiningJobError,
Sv2ProvideMissingTransactions,
serializeProvideMissingTransactions,
deserializeProvideMissingTransactions,
Sv2ProvideMissingTransactionsSuccess,
serializeProvideMissingTransactionsSuccess,
deserializeProvideMissingTransactionsSuccess,
Sv2SetCustomMiningJob,
serializeSetCustomMiningJob,
deserializeSetCustomMiningJob,
Sv2SetCustomMiningJobSuccess,
serializeSetCustomMiningJobSuccess,
deserializeSetCustomMiningJobSuccess,
Sv2SetCustomMiningJobError,
serializeSetCustomMiningJobError,
deserializeSetCustomMiningJobError,
Sv2PushSolution,
serializePushSolution,
deserializePushSolution,
} from './sv2-jdp-messages';
function roundTrip<T>(
msg: T,
serialize: (m: T) => Buffer,
deserialize: (r: BufferReader) => T,
): T {
const buf = serialize(msg);
return deserialize(new BufferReader(buf));
}
describe('SV2 JDP Messages', () => {
it('AllocateMiningJobToken round-trips', () => {
const msg: Sv2AllocateMiningJobToken = {
userIdentifier: 'miner-001',
requestId: 1,
};
const result = roundTrip(msg, serializeAllocateMiningJobToken, deserializeAllocateMiningJobToken);
expect(result.userIdentifier).toBe(msg.userIdentifier);
expect(result.requestId).toBe(msg.requestId);
});
it('AllocateMiningJobTokenSuccess round-trips', () => {
const msg: Sv2AllocateMiningJobTokenSuccess = {
requestId: 1,
miningJobToken: Buffer.from('token123', 'utf8'),
coinbaseOutputs: Buffer.from('cafebabe', 'hex'),
};
const result = roundTrip(msg, serializeAllocateMiningJobTokenSuccess, deserializeAllocateMiningJobTokenSuccess);
expect(result.requestId).toBe(msg.requestId);
expect(result.miningJobToken).toEqual(msg.miningJobToken);
expect(result.coinbaseOutputs).toEqual(msg.coinbaseOutputs);
});
it('AllocateMiningJobTokenSuccess with empty coinbaseOutputs', () => {
const msg: Sv2AllocateMiningJobTokenSuccess = {
requestId: 42,
miningJobToken: Buffer.from('tok'),
coinbaseOutputs: Buffer.alloc(0),
};
const result = roundTrip(msg, serializeAllocateMiningJobTokenSuccess, deserializeAllocateMiningJobTokenSuccess);
expect(result.coinbaseOutputs.length).toBe(0);
});
it('DeclareMiningJob round-trips', () => {
const wtxid1 = Buffer.alloc(32, 0x11);
const wtxid2 = Buffer.alloc(32, 0x22);
const msg: Sv2DeclareMiningJob = {
requestId: 42,
miningJobToken: Buffer.from('mytoken'),
version: 0x20000000,
coinbaseTxPrefix: Buffer.from('deadbeef', 'hex'),
coinbaseTxSuffix: Buffer.from('cafebabe', 'hex'),
wtxidList: [wtxid1, wtxid2],
excessData: Buffer.from('extra'),
};
const result = roundTrip(msg, serializeDeclareMiningJob, deserializeDeclareMiningJob);
expect(result.requestId).toBe(msg.requestId);
expect(result.miningJobToken).toEqual(msg.miningJobToken);
expect(result.version).toBe(msg.version);
expect(result.coinbaseTxPrefix).toEqual(msg.coinbaseTxPrefix);
expect(result.coinbaseTxSuffix).toEqual(msg.coinbaseTxSuffix);
expect(result.wtxidList).toHaveLength(2);
expect(result.wtxidList[0]).toEqual(wtxid1);
expect(result.wtxidList[1]).toEqual(wtxid2);
expect(result.excessData).toEqual(msg.excessData);
});
it('DeclareMiningJob with empty tx list', () => {
const msg: Sv2DeclareMiningJob = {
requestId: 1,
miningJobToken: Buffer.alloc(0),
version: 0x20000000,
coinbaseTxPrefix: Buffer.alloc(0),
coinbaseTxSuffix: Buffer.alloc(0),
wtxidList: [],
excessData: Buffer.alloc(0),
};
const result = roundTrip(msg, serializeDeclareMiningJob, deserializeDeclareMiningJob);
expect(result.wtxidList).toHaveLength(0);
});
it('DeclareMiningJobSuccess round-trips', () => {
const msg: Sv2DeclareMiningJobSuccess = {
requestId: 42,
newMiningJobToken: Buffer.from('newtoken'),
};
const result = roundTrip(msg, serializeDeclareMiningJobSuccess, deserializeDeclareMiningJobSuccess);
expect(result).toEqual(msg);
});
it('DeclareMiningJobError round-trips', () => {
const msg: Sv2DeclareMiningJobError = {
requestId: 42,
errorCode: 'invalid-mining-job-token',
errorDetails: Buffer.from('Token expired'),
};
const result = roundTrip(msg, serializeDeclareMiningJobError, deserializeDeclareMiningJobError);
expect(result.requestId).toBe(msg.requestId);
expect(result.errorCode).toBe(msg.errorCode);
expect(result.errorDetails).toEqual(msg.errorDetails);
});
it('ProvideMissingTransactions round-trips', () => {
const msg: Sv2ProvideMissingTransactions = {
requestId: 7,
unknownTxPositionList: [0, 3, 7, 15],
};
const result = roundTrip(msg, serializeProvideMissingTransactions, deserializeProvideMissingTransactions);
expect(result.requestId).toBe(msg.requestId);
expect(result.unknownTxPositionList).toEqual([0, 3, 7, 15]);
});
it('ProvideMissingTransactionsSuccess round-trips (B0_16M inner type)', () => {
const tx1 = Buffer.from('0100000001', 'hex');
const tx2 = Buffer.from('0200000002abcdef', 'hex');
const msg: Sv2ProvideMissingTransactionsSuccess = {
requestId: 7,
transactionList: [tx1, tx2],
};
const result = roundTrip(msg, serializeProvideMissingTransactionsSuccess, deserializeProvideMissingTransactionsSuccess);
expect(result.requestId).toBe(msg.requestId);
expect(result.transactionList).toHaveLength(2);
expect(result.transactionList[0]).toEqual(tx1);
expect(result.transactionList[1]).toEqual(tx2);
});
it('ProvideMissingTransactionsSuccess with large transaction (>64KB)', () => {
const largeTx = Buffer.alloc(100000, 0xab); // 100KB > 64KB limit of B0_64K
const msg: Sv2ProvideMissingTransactionsSuccess = {
requestId: 1,
transactionList: [largeTx],
};
const result = roundTrip(msg, serializeProvideMissingTransactionsSuccess, deserializeProvideMissingTransactionsSuccess);
expect(result.transactionList[0].length).toBe(100000);
expect(result.transactionList[0]).toEqual(largeTx);
});
it('SetCustomMiningJob round-trips', () => {
const hash1 = Buffer.alloc(32, 0xaa);
const msg: Sv2SetCustomMiningJob = {
channelId: 1,
requestId: 100,
token: Buffer.from('mytoken'),
version: 0x20000000,
prevHash: Buffer.alloc(32, 0xbb),
minNtime: 1700000000,
nBits: 0x1d00ffff,
coinbaseTxVersion: 2,
coinbasePrefix: Buffer.from('deadbeef', 'hex'),
coinbaseTxInputNSequence: 0xffffffff,
coinbaseTxOutputs: Buffer.from('outputs'),
coinbaseTxLocktime: 0,
merklePath: [hash1],
};
const result = roundTrip(msg, serializeSetCustomMiningJob, deserializeSetCustomMiningJob);
expect(result.channelId).toBe(msg.channelId);
expect(result.requestId).toBe(msg.requestId);
expect(result.token).toEqual(msg.token);
expect(result.version).toBe(msg.version);
expect(result.prevHash).toEqual(msg.prevHash);
expect(result.minNtime).toBe(msg.minNtime);
expect(result.nBits).toBe(msg.nBits);
expect(result.coinbaseTxVersion).toBe(msg.coinbaseTxVersion);
expect(result.coinbasePrefix).toEqual(msg.coinbasePrefix);
expect(result.coinbaseTxInputNSequence).toBe(msg.coinbaseTxInputNSequence);
expect(result.coinbaseTxOutputs).toEqual(msg.coinbaseTxOutputs);
expect(result.coinbaseTxLocktime).toBe(msg.coinbaseTxLocktime);
expect(result.merklePath).toHaveLength(1);
expect(result.merklePath[0]).toEqual(hash1);
});
it('SetCustomMiningJobSuccess round-trips', () => {
const msg: Sv2SetCustomMiningJobSuccess = {
channelId: 1,
requestId: 100,
jobId: 42,
};
const result = roundTrip(msg, serializeSetCustomMiningJobSuccess, deserializeSetCustomMiningJobSuccess);
expect(result).toEqual(msg);
});
it('SetCustomMiningJobError round-trips', () => {
const msg: Sv2SetCustomMiningJobError = {
channelId: 1,
requestId: 100,
errorCode: 'invalid-mining-job-token',
};
const result = roundTrip(msg, serializeSetCustomMiningJobError, deserializeSetCustomMiningJobError);
expect(result).toEqual(msg);
});
it('serialized DeclareMiningJob has correct structure', () => {
const msg: Sv2DeclareMiningJob = {
requestId: 1,
miningJobToken: Buffer.from([0xab]),
version: 2,
coinbaseTxPrefix: Buffer.alloc(0),
coinbaseTxSuffix: Buffer.alloc(0),
wtxidList: [],
excessData: Buffer.alloc(0),
};
const buf = serializeDeclareMiningJob(msg);
// U32(4) + B0_255(1+1) + U32(4) + B0_64K(2+0) + B0_64K(2+0) + SEQ0_64K(2+0) + B0_64K(2+0)
// = 4 + 2 + 4 + 2 + 2 + 2 + 2 = 18
expect(buf.length).toBe(18);
});
it('PushSolution round-trips', () => {
const msg: Sv2PushSolution = {
extranonce: Buffer.from('aabbccdd', 'hex'),
prevHash: Buffer.alloc(32, 0x11),
nonce: 0xdeadbeef,
ntime: 1700000000,
nBits: 0x1d00ffff,
version: 0x20000000,
};
const result = roundTrip(msg, serializePushSolution, deserializePushSolution);
expect(result.extranonce).toEqual(msg.extranonce);
expect(result.prevHash).toEqual(msg.prevHash);
expect(result.nonce).toBe(msg.nonce);
expect(result.ntime).toBe(msg.ntime);
expect(result.nBits).toBe(msg.nBits);
expect(result.version).toBe(msg.version);
});
it('PushSolution with empty extranonce', () => {
const msg: Sv2PushSolution = {
extranonce: Buffer.alloc(0),
prevHash: Buffer.alloc(32, 0xff),
nonce: 0,
ntime: 0,
nBits: 0,
version: 0,
};
const result = roundTrip(msg, serializePushSolution, deserializePushSolution);
expect(result.extranonce.length).toBe(0);
expect(result.prevHash).toEqual(msg.prevHash);
});
it('serialized PushSolution has correct size', () => {
const msg: Sv2PushSolution = {
extranonce: Buffer.alloc(8),
prevHash: Buffer.alloc(32),
nonce: 0,
ntime: 0,
nBits: 0,
version: 0,
};
const buf = serializePushSolution(msg);
// B0_32(1+8) + U256(32) + U32(4) + U32(4) + U32(4) + U32(4) = 57
expect(buf.length).toBe(57);
});
it('serialized SetCustomMiningJob has correct fixed overhead', () => {
const msg: Sv2SetCustomMiningJob = {
channelId: 1,
requestId: 2,
token: Buffer.alloc(0),
version: 0x20000000,
prevHash: Buffer.alloc(32),
minNtime: 0,
nBits: 0,
coinbaseTxVersion: 2,
coinbasePrefix: Buffer.alloc(0),
coinbaseTxInputNSequence: 0xffffffff,
coinbaseTxOutputs: Buffer.alloc(0),
coinbaseTxLocktime: 0,
merklePath: [],
};
const buf = serializeSetCustomMiningJob(msg);
// U32(4) + U32(4) + B0_255(1+0) + U32(4) + U256(32) + U32(4) + U32(4) + U32(4) +
// B0_255(1+0) + U32(4) + B0_64K(2+0) + U32(4) + SEQ0_255(1+0)
// = 4+4+1+4+32+4+4+4+1+4+2+4+1 = 69
expect(buf.length).toBe(69);
});
});