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,312 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user