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

227 lines
8.0 KiB
TypeScript

import { BufferReader } from './sv2-binary-codec';
import {
Sv2TdpCoinbaseOutputConstraints,
serializeTdpCoinbaseOutputConstraints,
deserializeTdpCoinbaseOutputConstraints,
Sv2TdpNewTemplate,
serializeTdpNewTemplate,
deserializeTdpNewTemplate,
Sv2TdpSetNewPrevHash,
serializeTdpSetNewPrevHash,
deserializeTdpSetNewPrevHash,
Sv2TdpRequestTransactionData,
serializeTdpRequestTransactionData,
deserializeTdpRequestTransactionData,
Sv2TdpRequestTransactionDataSuccess,
serializeTdpRequestTransactionDataSuccess,
deserializeTdpRequestTransactionDataSuccess,
Sv2TdpRequestTransactionDataError,
serializeTdpRequestTransactionDataError,
deserializeTdpRequestTransactionDataError,
Sv2TdpSubmitSolution,
serializeTdpSubmitSolution,
deserializeTdpSubmitSolution,
} from './sv2-tdp-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 TDP Messages', () => {
it('CoinbaseOutputConstraints round-trips', () => {
const msg: Sv2TdpCoinbaseOutputConstraints = {
coinbaseOutputMaxAdditionalSize: 128,
coinbaseOutputMaxAdditionalSigops: 400,
};
const result = roundTrip(msg, serializeTdpCoinbaseOutputConstraints, deserializeTdpCoinbaseOutputConstraints);
expect(result).toEqual(msg);
});
it('NewTemplate round-trips with merkle path', () => {
const hash1 = Buffer.alloc(32, 0xaa);
const hash2 = Buffer.alloc(32, 0xbb);
const msg: Sv2TdpNewTemplate = {
templateId: 42n,
futureTemplate: false,
version: 0x20000000,
coinbaseTxVersion: 2,
coinbasePrefix: Buffer.from('deadbeef', 'hex'),
coinbaseTxInputSequence: 0xffffffff,
coinbaseTxValueRemaining: 625000000n,
coinbaseTxOutputsCount: 1,
coinbaseTxOutputs: Buffer.from('cafebabe', 'hex'),
coinbaseTxLocktime: 0,
merklePath: [hash1, hash2],
};
const result = roundTrip(msg, serializeTdpNewTemplate, deserializeTdpNewTemplate);
expect(result.templateId).toBe(msg.templateId);
expect(result.futureTemplate).toBe(msg.futureTemplate);
expect(result.version).toBe(msg.version);
expect(result.coinbasePrefix).toEqual(msg.coinbasePrefix);
expect(result.coinbaseTxVersion).toBe(msg.coinbaseTxVersion);
expect(result.coinbaseTxInputSequence).toBe(msg.coinbaseTxInputSequence);
expect(result.coinbaseTxValueRemaining).toBe(msg.coinbaseTxValueRemaining);
expect(result.coinbaseTxOutputsCount).toBe(msg.coinbaseTxOutputsCount);
expect(result.coinbaseTxOutputs).toEqual(msg.coinbaseTxOutputs);
expect(result.coinbaseTxLocktime).toBe(msg.coinbaseTxLocktime);
expect(result.merklePath).toHaveLength(2);
expect(result.merklePath[0]).toEqual(hash1);
expect(result.merklePath[1]).toEqual(hash2);
});
it('NewTemplate round-trips with empty merkle path', () => {
const msg: Sv2TdpNewTemplate = {
templateId: 1n,
futureTemplate: true,
version: 0x20000000,
coinbaseTxVersion: 2,
coinbasePrefix: Buffer.alloc(0),
coinbaseTxInputSequence: 0xffffffff,
coinbaseTxValueRemaining: 0n,
coinbaseTxOutputsCount: 0,
coinbaseTxOutputs: Buffer.alloc(0),
coinbaseTxLocktime: 0,
merklePath: [],
};
const result = roundTrip(msg, serializeTdpNewTemplate, deserializeTdpNewTemplate);
expect(result.merklePath).toHaveLength(0);
expect(result.futureTemplate).toBe(true);
});
it('NewTemplate round-trips with large coinbase data', () => {
const largeCoinbase = Buffer.alloc(100, 0xcc); // B0_255 max 255 bytes
const largeOutputs = Buffer.alloc(2000, 0xdd);
const msg: Sv2TdpNewTemplate = {
templateId: 999n,
futureTemplate: false,
version: 0x20400000,
coinbaseTxVersion: 2,
coinbasePrefix: largeCoinbase,
coinbaseTxInputSequence: 0xfffffffe,
coinbaseTxValueRemaining: 312500000n,
coinbaseTxOutputsCount: 3,
coinbaseTxOutputs: largeOutputs,
coinbaseTxLocktime: 500000,
merklePath: Array.from({ length: 12 }, (_, i) => Buffer.alloc(32, i)),
};
const result = roundTrip(msg, serializeTdpNewTemplate, deserializeTdpNewTemplate);
expect(result.coinbasePrefix).toEqual(largeCoinbase); // B0_255, 100 bytes
expect(result.coinbaseTxOutputs).toEqual(largeOutputs);
expect(result.merklePath).toHaveLength(12);
});
it('SetNewPrevHash round-trips', () => {
const prevHash = Buffer.alloc(32, 0xab);
const target = Buffer.alloc(32, 0x00);
target[0] = 0xff;
const msg: Sv2TdpSetNewPrevHash = {
templateId: 42n,
prevHash,
headerTimestamp: 1700000000,
nBits: 0x1d00ffff,
target,
};
const result = roundTrip(msg, serializeTdpSetNewPrevHash, deserializeTdpSetNewPrevHash);
expect(result).toEqual(msg);
});
it('RequestTransactionData round-trips', () => {
const msg: Sv2TdpRequestTransactionData = {
templateId: 100n,
};
const result = roundTrip(msg, serializeTdpRequestTransactionData, deserializeTdpRequestTransactionData);
expect(result).toEqual(msg);
});
it('RequestTransactionData.Success round-trips with transactions', () => {
const tx1 = Buffer.from('0100000001', 'hex');
const tx2 = Buffer.from('0200000002abcd', 'hex');
const msg: Sv2TdpRequestTransactionDataSuccess = {
templateId: 100n,
excessData: Buffer.from('extra', 'utf8'),
transactionList: [tx1, tx2],
};
const result = roundTrip(
msg,
serializeTdpRequestTransactionDataSuccess,
deserializeTdpRequestTransactionDataSuccess,
);
expect(result.templateId).toBe(msg.templateId);
expect(result.excessData).toEqual(msg.excessData);
expect(result.transactionList).toHaveLength(2);
expect(result.transactionList[0]).toEqual(tx1);
expect(result.transactionList[1]).toEqual(tx2);
});
it('RequestTransactionData.Success round-trips with empty transaction list', () => {
const msg: Sv2TdpRequestTransactionDataSuccess = {
templateId: 1n,
excessData: Buffer.alloc(0),
transactionList: [],
};
const result = roundTrip(
msg,
serializeTdpRequestTransactionDataSuccess,
deserializeTdpRequestTransactionDataSuccess,
);
expect(result.transactionList).toHaveLength(0);
});
it('RequestTransactionData.Error round-trips', () => {
const msg: Sv2TdpRequestTransactionDataError = {
templateId: 42n,
errorCode: 'template-not-found',
};
const result = roundTrip(
msg,
serializeTdpRequestTransactionDataError,
deserializeTdpRequestTransactionDataError,
);
expect(result).toEqual(msg);
});
it('SubmitSolution round-trips', () => {
const coinbaseTx = Buffer.alloc(250, 0xef);
const msg: Sv2TdpSubmitSolution = {
templateId: 42n,
version: 0x20000000,
headerTimestamp: 1700000000,
headerNonce: 0xdeadbeef,
coinbaseTx,
};
const result = roundTrip(msg, serializeTdpSubmitSolution, deserializeTdpSubmitSolution);
expect(result.templateId).toBe(msg.templateId);
expect(result.version).toBe(msg.version);
expect(result.headerTimestamp).toBe(msg.headerTimestamp);
expect(result.headerNonce).toBe(msg.headerNonce);
expect(result.coinbaseTx).toEqual(coinbaseTx);
});
it('serialized NewTemplate has correct byte layout', () => {
const msg: Sv2TdpNewTemplate = {
templateId: 1n,
futureTemplate: false,
version: 0x20000000,
coinbaseTxVersion: 2,
coinbasePrefix: Buffer.from([0xab]),
coinbaseTxInputSequence: 0xffffffff,
coinbaseTxValueRemaining: 100n,
coinbaseTxOutputsCount: 0,
coinbaseTxOutputs: Buffer.alloc(0),
coinbaseTxLocktime: 0,
merklePath: [],
};
const buf = serializeTdpNewTemplate(msg);
// U64(8) + BOOL(1) + U32(4) + U32(4) + B0_255(1+1) + U32(4) + U64(8) + U32(4) + B0_64K(2+0) + U32(4) + SEQ0_255(1+0)
// = 8 + 1 + 4 + 4 + 2 + 4 + 8 + 4 + 2 + 4 + 1 = 42
expect(buf.length).toBe(42);
expect(buf.readBigUInt64LE(0)).toBe(1n);
expect(buf[8]).toBe(0); // futureTemplate = false
});
});