mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2026-08-24 04:36:39 +00:00
160 lines
5.9 KiB
JavaScript
160 lines
5.9 KiB
JavaScript
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const Database = require('better-sqlite3');
|
|
const nacl = require('tweetnacl');
|
|
const { ReticulumChatWorkerPool } = require('../build/src/reticulum-chat-worker-pool.js');
|
|
|
|
function digestHash(events) {
|
|
const ids = [...events]
|
|
.sort((a, b) => a.timestamp - b.timestamp || a.eventId.localeCompare(b.eventId))
|
|
.map((event) => event.eventId);
|
|
return crypto.createHash('sha256').update(JSON.stringify(ids), 'utf8').digest('hex');
|
|
}
|
|
|
|
async function main() {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rchat-worker-'));
|
|
const dbPath = path.join(tempDir, 'chat.db');
|
|
const db = new Database(dbPath);
|
|
const pool = new ReticulumChatWorkerPool('digest-integration', 1, 8);
|
|
const createdAt = Date.now();
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE reticulum_chat_channels (
|
|
group_id INTEGER NOT NULL,
|
|
channel_id TEXT NOT NULL,
|
|
read_mode TEXT,
|
|
PRIMARY KEY (group_id, channel_id)
|
|
);
|
|
CREATE TABLE reticulum_chat_events (
|
|
event_id TEXT PRIMARY KEY,
|
|
group_id INTEGER NOT NULL,
|
|
channel_id TEXT NOT NULL,
|
|
timestamp INTEGER NOT NULL,
|
|
feed_timestamp INTEGER NOT NULL,
|
|
event_type TEXT NOT NULL,
|
|
expires_at INTEGER
|
|
);
|
|
`);
|
|
const insertChannel = db.prepare(
|
|
'INSERT INTO reticulum_chat_channels (group_id, channel_id, read_mode) VALUES (?, ?, ?)'
|
|
);
|
|
insertChannel.run(716, 'testing', 'members');
|
|
insertChannel.run(716, 'admin-private', 'admins');
|
|
const insertEvent = db.prepare(`
|
|
INSERT INTO reticulum_chat_events
|
|
(event_id, group_id, channel_id, timestamp, feed_timestamp, event_type, expires_at)
|
|
VALUES (?, 716, ?, ?, ?, ?, NULL)
|
|
`);
|
|
insertEvent.run('public-message', 'testing', createdAt - 300, createdAt - 300, 'message');
|
|
insertEvent.run('private-message', 'admin-private', createdAt - 200, createdAt - 200, 'message');
|
|
insertEvent.run(
|
|
'private-metadata',
|
|
'admin-private',
|
|
createdAt - 100,
|
|
createdAt - 100,
|
|
'channel_update'
|
|
);
|
|
|
|
const result = await pool.run({
|
|
kind: 'build_group_digest_state',
|
|
dbPath,
|
|
groupId: 716,
|
|
createdAt,
|
|
});
|
|
if (!result || !result.ok || result.kind !== 'build_group_digest_state') {
|
|
throw new Error(`Worker failed: ${result && !result.ok ? result.error : 'no result'}`);
|
|
}
|
|
const channelIds = result.state.channelHeads.map((head) => head.channelId);
|
|
for (const expected of ['admin-private', 'general', 'qortal-land', 'testing']) {
|
|
if (!channelIds.includes(expected)) throw new Error(`Missing channel head: ${expected}`);
|
|
}
|
|
const expectedDigest = digestHash([
|
|
{ eventId: 'public-message', timestamp: createdAt - 300 },
|
|
{ eventId: 'private-metadata', timestamp: createdAt - 100 },
|
|
]);
|
|
if (result.state.snapshot.digestHash !== expectedDigest) {
|
|
throw new Error('Digest included private message content or omitted public metadata');
|
|
}
|
|
if (result.state.snapshot.latest?.eventId !== 'private-metadata') {
|
|
throw new Error('Latest digest cursor is incorrect');
|
|
}
|
|
const privateHead = result.state.channelHeads.find(
|
|
(head) => head.channelId === 'admin-private'
|
|
);
|
|
if (privateHead?.latest?.eventId !== 'private-metadata') {
|
|
throw new Error('Admin-private channel head is incorrect');
|
|
}
|
|
const landKeyPair = nacl.sign.keyPair();
|
|
const landStateBytes = Buffer.from('qortal-land-state-worker-test', 'utf8');
|
|
const landStateSignature = nacl.sign.detached(landStateBytes, landKeyPair.secretKey);
|
|
const validLandState = await pool.run({
|
|
kind: 'verify_land_state_signature',
|
|
signedBytes: landStateBytes,
|
|
signature: landStateSignature,
|
|
publicKey: landKeyPair.publicKey,
|
|
});
|
|
if (
|
|
!validLandState ||
|
|
!validLandState.ok ||
|
|
validLandState.kind !== 'verify_land_state_signature' ||
|
|
!validLandState.valid
|
|
) {
|
|
throw new Error('Worker rejected a valid QortalLand state signature');
|
|
}
|
|
const invalidLandState = await pool.run({
|
|
kind: 'verify_land_state_signature',
|
|
signedBytes: Buffer.from('tampered-qortal-land-state', 'utf8'),
|
|
signature: landStateSignature,
|
|
publicKey: landKeyPair.publicKey,
|
|
});
|
|
if (
|
|
!invalidLandState ||
|
|
!invalidLandState.ok ||
|
|
invalidLandState.kind !== 'verify_land_state_signature' ||
|
|
invalidLandState.valid
|
|
) {
|
|
throw new Error('Worker accepted an invalid QortalLand state signature');
|
|
}
|
|
const malformedLandStateSignature = await pool.run({
|
|
kind: 'verify_land_state_signature',
|
|
signedBytes: landStateBytes,
|
|
signature: landStateSignature.subarray(0, landStateSignature.length - 1),
|
|
publicKey: landKeyPair.publicKey,
|
|
});
|
|
if (
|
|
!malformedLandStateSignature ||
|
|
!malformedLandStateSignature.ok ||
|
|
malformedLandStateSignature.kind !== 'verify_land_state_signature' ||
|
|
malformedLandStateSignature.valid
|
|
) {
|
|
throw new Error('Worker accepted a malformed QortalLand state signature');
|
|
}
|
|
const malformedLandStatePublicKey = await pool.run({
|
|
kind: 'verify_land_state_signature',
|
|
signedBytes: landStateBytes,
|
|
signature: landStateSignature,
|
|
publicKey: landKeyPair.publicKey.subarray(0, landKeyPair.publicKey.length - 1),
|
|
});
|
|
if (
|
|
!malformedLandStatePublicKey ||
|
|
!malformedLandStatePublicKey.ok ||
|
|
malformedLandStatePublicKey.kind !== 'verify_land_state_signature' ||
|
|
malformedLandStatePublicKey.valid
|
|
) {
|
|
throw new Error('Worker accepted a malformed QortalLand public key');
|
|
}
|
|
console.log('reticulum chat worker integration: passed');
|
|
} finally {
|
|
pool.stop();
|
|
db.close();
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|