@node-dlc/messaging
Version:
DLC Messaging Protocol
375 lines (316 loc) • 13.4 kB
text/typescript
/* eslint-disable no-console */
import { expect } from 'chai';
import { execSync } from 'child_process';
import * as fs from 'fs';
import JSONbig from 'json-bigint';
import * as path from 'path';
import { DlcAccept, DlcOffer, DlcSign } from '../../lib';
// Configure json-bigint to handle large integers as BigInt but preserve floating point numbers
const JSONBigInt = JSONbig({
storeAsString: false, // Store as BigInt, not string
useNativeBigInt: true, // Use native BigInt support
alwaysParseAsBig: false, // Don't convert all numbers to BigInt - preserve smaller numbers and floats
// This will automatically use BigInt for integers that exceed MAX_SAFE_INTEGER
// and keep regular numbers for smaller integers and floats
});
interface RustDlcCliResult {
status: 'success' | 'error';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any;
messageType?: string;
message: string;
}
// Helper function to call rust-dlc CLI
function callRustCli(command: string, input?: string): RustDlcCliResult {
const rustCliPath = path.join(__dirname, '../../../../rust-dlc-cli');
const cliCmd = `cd ${rustCliPath} && cargo run --bin dlc-compat -- ${command}`;
try {
// Use shorter timeout in CI environments to avoid test timeouts
const isCI = process.env.CI === 'true';
const cliTimeout = isCI ? 60000 : 300000; // 1 minute in CI, 5 minutes locally
const result = execSync(cliCmd, {
input: input || '',
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: cliTimeout,
});
return JSON.parse(result.trim());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
// If the command failed, try to parse stderr or stdout as JSON
try {
const errorOutput = error.stdout || error.stderr || '';
if (errorOutput.trim().startsWith('{')) {
return JSON.parse(errorOutput.trim());
}
// If not JSON, create error response
return {
status: 'error',
message: `CLI execution failed: ${error.message}. Output: ${errorOutput}`,
};
} catch {
return {
status: 'error',
message: `CLI execution failed: ${error.message}`,
};
}
}
}
// Load test vectors at module level so they're available for test generation
const testVectorsDir = path.join(__dirname, '../../test_vectors/dlcspecs');
const testVectorFiles: string[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const allTestData: { [filename: string]: any } = {};
// Load test data synchronously at module level
if (fs.existsSync(testVectorsDir)) {
const files = fs
.readdirSync(testVectorsDir)
.filter((f) => f.endsWith('.json'))
.sort(); // Sort for consistent test order
files.forEach((filename) => {
// if (filename !== 'enum_3_of_3_test.json') return;
const filePath = path.join(testVectorsDir, filename);
try {
const fileContent = fs.readFileSync(filePath, 'utf8');
allTestData[filename] = JSONBigInt.parse(fileContent);
testVectorFiles.push(filename);
} catch (error) {
console.warn(
`Failed to load test vector ${filename}:`,
(error as Error).message,
);
}
});
}
describe('True DLC Serialization Compatibility Tests', () => {
describe('Comprehensive Test Vector Coverage', () => {
it('should discover and load all available test vector files', () => {
expect(testVectorFiles.length).to.be.greaterThan(
0,
'Should find test vector files',
);
expect(Object.keys(allTestData).length).to.be.greaterThan(
0,
'Should load test data',
);
console.log(`\nTest Vector Discovery:`);
console.log(` Found ${testVectorFiles.length} test vector files:`);
testVectorFiles.forEach((file, index) => {
const hasOfferMessage = allTestData[file]?.offer_message
? '[OFFER]'
: '[NO_OFFER]';
const hasAcceptMessage = allTestData[file]?.accept_message
? '[ACCEPT]'
: '[NO_ACCEPT]';
const hasSignMessage = allTestData[file]?.sign_message
? '[SIGN]'
: '[NO_SIGN]';
console.log(
` ${
index + 1
}. ${file} (${hasOfferMessage} ${hasAcceptMessage} ${hasSignMessage})`,
);
});
});
});
describe('Round-trip Serialization Compatibility', () => {
// Test representative samples against current Rust-DLC CLI for compatibility
const rustCompatTestVectors = [
'enum_3_of_3_test.json',
'single_oracle_numerical_test.json',
];
rustCompatTestVectors.forEach((filename) => {
const testData = allTestData[filename];
if (!testData) return;
if (testData.offer_message) {
it(`should have Node.js DlcOffer compatible with Rust-DLC CLI for ${filename}`, function () {
// Use longer timeout in CI to account for slower build times
const isCI = process.env.CI === 'true';
this.timeout(isCI ? 90000 : 15000); // 90s in CI, 15s locally
const nodeOffer = DlcOffer.fromJSON(testData.offer_message.message);
const nodeJson = nodeOffer.toJSON();
const nodeHex = nodeOffer.serialize().toString('hex');
const rustSerialization = callRustCli(
'serialize -t offer',
JSONBigInt.stringify(nodeJson),
);
expect(rustSerialization.status).to.equal(
'success',
`Rust CLI serialization failed: ${rustSerialization.message}`,
);
const rustHex = rustSerialization.data;
expect(nodeHex).to.equal(
rustHex,
`Node.js vs Rust-DLC CLI serialization mismatch for ${filename}`,
);
});
}
if (testData.accept_message) {
it(`should have Node.js DlcAccept compatible with Rust-DLC CLI for ${filename}`, function () {
// Use longer timeout in CI to account for slower build times
const isCI = process.env.CI === 'true';
this.timeout(isCI ? 90000 : 15000); // 90s in CI, 15s locally
const nodeAccept = DlcAccept.fromJSON(
testData.accept_message.message,
);
const nodeJson = nodeAccept.toJSON();
const nodeHex = nodeAccept.serialize().toString('hex');
const rustSerialization = callRustCli(
'serialize -t accept',
JSONBigInt.stringify(nodeJson),
);
expect(rustSerialization.status).to.equal(
'success',
`Rust CLI serialization failed: ${rustSerialization.message}`,
);
const rustHex = rustSerialization.data;
expect(nodeHex).to.equal(
rustHex,
`Node.js vs Rust-DLC CLI serialization mismatch for ${filename}`,
);
});
}
if (testData.sign_message) {
it(`should have Node.js DlcSign compatible with Rust-DLC CLI for ${filename}`, function () {
// Use longer timeout in CI to account for slower build times
const isCI = process.env.CI === 'true';
this.timeout(isCI ? 90000 : 15000); // 90s in CI, 15s locally
const nodeSign = DlcSign.fromJSON(testData.sign_message.message);
const nodeJson = nodeSign.toJSON();
const nodeHex = nodeSign.serialize().toString('hex');
const rustSerialization = callRustCli(
'serialize -t sign',
JSONBigInt.stringify(nodeJson),
);
expect(rustSerialization.status).to.equal(
'success',
`Rust CLI serialization failed: ${rustSerialization.message}`,
);
const rustHex = rustSerialization.data;
expect(nodeHex).to.equal(
rustHex,
`Node.js vs Rust-DLC CLI serialization mismatch for ${filename}`,
);
});
}
});
// Generate individual test cases for DlcOffer serialization
testVectorFiles.forEach((filename) => {
const testData = allTestData[filename];
if (!testData?.offer_message) return;
// Skip DlcInput test vectors as they were programmatically generated without serialized hex
if (filename === 'enum_3_of_5_with_dlc_input_test.json') {
it.skip(
`should correctly serialize DlcOffer for ${filename} (SKIP: Programmatically generated test vector)`,
);
return;
}
it(`should correctly serialize DlcOffer for ${filename}`, () => {
const expectedHex = testData.offer_message.serialized;
const messageJson = testData.offer_message.message;
const offer = DlcOffer.fromJSON(messageJson);
const actualHex = offer.serialize().toString('hex');
expect(actualHex).to.equal(
expectedHex,
`DlcOffer serialization mismatch for ${filename}`,
);
});
});
// Generate individual test cases for DlcAccept serialization
testVectorFiles.forEach((filename) => {
const testData = allTestData[filename];
if (!testData?.accept_message) return;
// Skip DlcInput test vectors as they were programmatically generated without serialized hex
if (filename === 'enum_3_of_5_with_dlc_input_test.json') {
it.skip(
`should correctly serialize DlcAccept for ${filename} (SKIP: Programmatically generated test vector)`,
);
return;
}
it(`should correctly serialize DlcAccept for ${filename}`, () => {
const expectedHex = testData.accept_message.serialized;
const messageJson = testData.accept_message.message;
const accept = DlcAccept.fromJSON(messageJson);
const actualHex = accept.serialize().toString('hex');
expect(actualHex).to.equal(
expectedHex,
`DlcAccept serialization mismatch for ${filename}`,
);
});
});
// Generate individual test cases for DlcSign serialization
testVectorFiles.forEach((filename) => {
const testData = allTestData[filename];
if (!testData?.sign_message) return;
it(`should correctly serialize DlcSign for ${filename}`, () => {
const expectedHex = testData.sign_message.serialized;
const messageJson = testData.sign_message.message;
const sign = DlcSign.fromJSON(messageJson);
const actualHex = sign.serialize().toString('hex');
expect(actualHex).to.equal(
expectedHex,
`DlcSign serialization mismatch for ${filename}`,
);
});
});
// Generate individual test cases for DlcOffer deserialization
testVectorFiles.forEach((filename) => {
const testData = allTestData[filename];
if (!testData?.offer_message) return;
// Skip DlcInput test vectors as they were programmatically generated without serialized hex
if (filename === 'enum_3_of_5_with_dlc_input_test.json') {
it.skip(
`should correctly deserialize DlcOffer for ${filename} (SKIP: Programmatically generated test vector)`,
);
return;
}
it(`should correctly deserialize DlcOffer for ${filename}`, () => {
const serializedHex = testData.offer_message.serialized;
const inputBuffer = Buffer.from(serializedHex, 'hex');
const offer = DlcOffer.deserialize(inputBuffer);
const reserializedHex = offer.serialize().toString('hex');
expect(reserializedHex).to.equal(
serializedHex,
`DlcOffer deserialization round-trip failed for ${filename}`,
);
});
});
// Generate individual test cases for DlcAccept deserialization
testVectorFiles.forEach((filename) => {
const testData = allTestData[filename];
if (!testData?.accept_message) return;
// Skip DlcInput test vectors as they were programmatically generated without serialized hex
if (filename === 'enum_3_of_5_with_dlc_input_test.json') {
it.skip(
`should correctly deserialize DlcAccept for ${filename} (SKIP: Programmatically generated test vector)`,
);
return;
}
it(`should correctly deserialize DlcAccept for ${filename}`, () => {
const serializedHex = testData.accept_message.serialized;
const inputBuffer = Buffer.from(serializedHex, 'hex');
const accept = DlcAccept.deserialize(inputBuffer);
const reserializedHex = accept.serialize().toString('hex');
expect(reserializedHex).to.equal(
serializedHex,
`DlcAccept deserialization round-trip failed for ${filename}`,
);
});
});
// Generate individual test cases for DlcSign deserialization
testVectorFiles.forEach((filename) => {
const testData = allTestData[filename];
if (!testData?.sign_message) return;
it(`should correctly deserialize DlcSign for ${filename}`, () => {
const serializedHex = testData.sign_message.serialized;
const inputBuffer = Buffer.from(serializedHex, 'hex');
const sign = DlcSign.deserialize(inputBuffer);
const reserializedHex = sign.serialize().toString('hex');
expect(reserializedHex).to.equal(
serializedHex,
`DlcSign deserialization round-trip failed for ${filename}`,
);
});
});
});
});