Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
495
vendor/ruvector/examples/edge-net/tests/manual-credit-test.cjs
vendored
Executable file
495
vendor/ruvector/examples/edge-net/tests/manual-credit-test.cjs
vendored
Executable file
@@ -0,0 +1,495 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Manual Credit Persistence Test Script
|
||||
*
|
||||
* This script manually tests the credit persistence flow:
|
||||
* 1. Connect to relay via WebSocket
|
||||
* 2. Register a node with test public key
|
||||
* 3. Submit a task and complete it
|
||||
* 4. Verify QDAG balance updated
|
||||
* 5. Reconnect and verify balance persisted
|
||||
*
|
||||
* Usage:
|
||||
* node manual-credit-test.cjs [--relay-url <url>]
|
||||
*
|
||||
* Environment:
|
||||
* RELAY_URL - WebSocket URL (default: ws://localhost:8080)
|
||||
*/
|
||||
|
||||
const WebSocket = require('ws');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Configuration
|
||||
const RELAY_URL = process.env.RELAY_URL || 'ws://localhost:8080';
|
||||
const TEST_PUBLIC_KEY = `manual-test-${crypto.randomBytes(8).toString('hex')}`;
|
||||
|
||||
// Colors for terminal output
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
green: '\x1b[32m',
|
||||
red: '\x1b[31m',
|
||||
yellow: '\x1b[33m',
|
||||
cyan: '\x1b[36m',
|
||||
dim: '\x1b[2m',
|
||||
};
|
||||
|
||||
function log(message, color = 'reset') {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`);
|
||||
}
|
||||
|
||||
function logSection(title) {
|
||||
console.log();
|
||||
log(`${'='.repeat(60)}`, 'cyan');
|
||||
log(` ${title}`, 'cyan');
|
||||
log(`${'='.repeat(60)}`, 'cyan');
|
||||
}
|
||||
|
||||
function logStep(step, description) {
|
||||
log(`\n[Step ${step}] ${description}`, 'yellow');
|
||||
}
|
||||
|
||||
function logSuccess(message) {
|
||||
log(` [SUCCESS] ${message}`, 'green');
|
||||
}
|
||||
|
||||
function logError(message) {
|
||||
log(` [ERROR] ${message}`, 'red');
|
||||
}
|
||||
|
||||
function logInfo(message) {
|
||||
log(` ${message}`, 'dim');
|
||||
}
|
||||
|
||||
// Create WebSocket connection
|
||||
function createConnection(nodeId, publicKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(RELAY_URL);
|
||||
const messages = [];
|
||||
|
||||
ws.on('open', () => {
|
||||
logInfo(`Connected to ${RELAY_URL}`);
|
||||
|
||||
// Register with the relay
|
||||
ws.send(JSON.stringify({
|
||||
type: 'register',
|
||||
nodeId: nodeId,
|
||||
publicKey: publicKey,
|
||||
capabilities: ['compute', 'storage'],
|
||||
version: '0.1.0',
|
||||
}));
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
try {
|
||||
const msg = JSON.parse(data.toString());
|
||||
messages.push(msg);
|
||||
|
||||
if (msg.type === 'welcome') {
|
||||
logSuccess(`Registered as ${nodeId}`);
|
||||
logInfo(`Peers: ${msg.peers?.length || 0}`);
|
||||
resolve({ ws, messages, nodeId, publicKey });
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', (err) => {
|
||||
logError(`WebSocket error: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
ws.on('close', (code, reason) => {
|
||||
logInfo(`Connection closed: ${code} ${reason}`);
|
||||
});
|
||||
|
||||
// Timeout
|
||||
setTimeout(() => {
|
||||
reject(new Error('Connection timeout'));
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for specific message type
|
||||
function waitForMessage(node, type, timeout = 5000) {
|
||||
return new Promise((resolve) => {
|
||||
const startIndex = node.messages.length;
|
||||
const startTime = Date.now();
|
||||
|
||||
const check = () => {
|
||||
const newMessages = node.messages.slice(startIndex);
|
||||
const msg = newMessages.find(m => m.type === type);
|
||||
if (msg) {
|
||||
resolve(msg);
|
||||
} else if (Date.now() - startTime > timeout) {
|
||||
resolve(null);
|
||||
} else {
|
||||
setTimeout(check, 50);
|
||||
}
|
||||
};
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
// Get current QDAG balance
|
||||
async function getBalance(node) {
|
||||
node.ws.send(JSON.stringify({
|
||||
type: 'ledger_sync',
|
||||
publicKey: node.publicKey,
|
||||
}));
|
||||
|
||||
const response = await waitForMessage(node, 'ledger_sync_response', 5000);
|
||||
if (response?.ledger) {
|
||||
return {
|
||||
earned: BigInt(response.ledger.earned || '0'),
|
||||
spent: BigInt(response.ledger.spent || '0'),
|
||||
tasksCompleted: response.ledger.tasksCompleted || 0,
|
||||
};
|
||||
}
|
||||
return { earned: 0n, spent: 0n, tasksCompleted: 0 };
|
||||
}
|
||||
|
||||
// Format rUv amount
|
||||
function formatRuv(nanoRuv) {
|
||||
const ruv = Number(nanoRuv) / 1e9;
|
||||
return `${ruv.toFixed(6)} rUv`;
|
||||
}
|
||||
|
||||
// Main test flow
|
||||
async function runTest() {
|
||||
logSection('Edge-Net Credit Persistence Manual Test');
|
||||
|
||||
log(`Relay URL: ${RELAY_URL}`);
|
||||
log(`Test Public Key: ${TEST_PUBLIC_KEY}`);
|
||||
|
||||
let submitter = null;
|
||||
let worker = null;
|
||||
let initialBalance = null;
|
||||
|
||||
try {
|
||||
// Step 1: Connect as submitter
|
||||
logStep(1, 'Connect task submitter');
|
||||
submitter = await createConnection(
|
||||
`submitter-${crypto.randomBytes(4).toString('hex')}`,
|
||||
`submitter-key-${crypto.randomBytes(8).toString('hex')}`
|
||||
);
|
||||
|
||||
// Step 2: Connect as worker with test public key
|
||||
logStep(2, 'Connect worker with test identity');
|
||||
worker = await createConnection(
|
||||
`worker-${crypto.randomBytes(4).toString('hex')}`,
|
||||
TEST_PUBLIC_KEY
|
||||
);
|
||||
|
||||
// Step 3: Get initial QDAG balance
|
||||
logStep(3, 'Get initial QDAG balance');
|
||||
initialBalance = await getBalance(worker);
|
||||
logInfo(`Earned: ${formatRuv(initialBalance.earned)}`);
|
||||
logInfo(`Spent: ${formatRuv(initialBalance.spent)}`);
|
||||
logInfo(`Tasks Completed: ${initialBalance.tasksCompleted}`);
|
||||
logSuccess('Initial balance retrieved');
|
||||
|
||||
// Step 4: Submit a task
|
||||
logStep(4, 'Submit a task');
|
||||
submitter.ws.send(JSON.stringify({
|
||||
type: 'task_submit',
|
||||
task: {
|
||||
type: 'inference',
|
||||
model: 'test-model',
|
||||
data: 'test-data',
|
||||
maxCredits: '5000000000', // 5 rUv
|
||||
},
|
||||
}));
|
||||
|
||||
const acceptance = await waitForMessage(submitter, 'task_accepted', 5000);
|
||||
if (!acceptance) {
|
||||
logError('Task not accepted');
|
||||
throw new Error('Task submission failed');
|
||||
}
|
||||
logSuccess(`Task accepted: ${acceptance.taskId}`);
|
||||
|
||||
// Step 5: Wait for task assignment
|
||||
logStep(5, 'Wait for task assignment');
|
||||
const assignment = await waitForMessage(worker, 'task_assignment', 5000);
|
||||
if (!assignment) {
|
||||
logError('Task not assigned to worker');
|
||||
logInfo('This may happen if there are multiple workers - try again');
|
||||
throw new Error('Task not assigned');
|
||||
}
|
||||
logSuccess(`Task assigned: ${assignment.task.id}`);
|
||||
|
||||
// Step 6: Complete the task
|
||||
logStep(6, 'Complete the task');
|
||||
worker.ws.send(JSON.stringify({
|
||||
type: 'task_complete',
|
||||
taskId: assignment.task.id,
|
||||
result: { completed: true, model: 'test-model' },
|
||||
reward: '5000000000', // 5 rUv
|
||||
}));
|
||||
|
||||
const creditMsg = await waitForMessage(worker, 'credit_earned', 5000);
|
||||
if (!creditMsg) {
|
||||
logError('No credit_earned message received');
|
||||
throw new Error('Credit not earned');
|
||||
}
|
||||
const earnedAmount = BigInt(creditMsg.amount || '0');
|
||||
logSuccess(`Credit earned: ${formatRuv(earnedAmount)}`);
|
||||
|
||||
// Step 7: Verify QDAG balance updated
|
||||
logStep(7, 'Verify QDAG balance updated');
|
||||
const afterBalance = await getBalance(worker);
|
||||
logInfo(`New Earned: ${formatRuv(afterBalance.earned)}`);
|
||||
logInfo(`New Tasks: ${afterBalance.tasksCompleted}`);
|
||||
|
||||
if (afterBalance.earned > initialBalance.earned) {
|
||||
logSuccess('Balance increased correctly!');
|
||||
} else {
|
||||
logError('Balance did not increase');
|
||||
}
|
||||
|
||||
// Step 8: Disconnect worker
|
||||
logStep(8, 'Disconnect worker (simulating refresh)');
|
||||
worker.ws.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
logSuccess('Worker disconnected');
|
||||
|
||||
// Step 9: Reconnect with same identity
|
||||
logStep(9, 'Reconnect with same identity');
|
||||
worker = await createConnection(
|
||||
`worker-reconnect-${crypto.randomBytes(4).toString('hex')}`,
|
||||
TEST_PUBLIC_KEY
|
||||
);
|
||||
logSuccess('Worker reconnected');
|
||||
|
||||
// Step 10: Verify balance persisted
|
||||
logStep(10, 'Verify balance persisted in QDAG');
|
||||
const persistedBalance = await getBalance(worker);
|
||||
logInfo(`Persisted Earned: ${formatRuv(persistedBalance.earned)}`);
|
||||
logInfo(`Persisted Tasks: ${persistedBalance.tasksCompleted}`);
|
||||
|
||||
if (persistedBalance.earned === afterBalance.earned) {
|
||||
logSuccess('Balance persisted correctly across reconnection!');
|
||||
} else {
|
||||
logError(`Balance mismatch: expected ${formatRuv(afterBalance.earned)}, got ${formatRuv(persistedBalance.earned)}`);
|
||||
}
|
||||
|
||||
// Summary
|
||||
logSection('Test Summary');
|
||||
const deltaEarned = persistedBalance.earned - initialBalance.earned;
|
||||
log(`Public Key: ${TEST_PUBLIC_KEY}`);
|
||||
log(`Credits Earned This Session: ${formatRuv(deltaEarned)}`);
|
||||
log(`Total Credits: ${formatRuv(persistedBalance.earned)}`);
|
||||
log(`Total Tasks: ${persistedBalance.tasksCompleted}`);
|
||||
|
||||
if (deltaEarned > 0n && persistedBalance.earned === afterBalance.earned) {
|
||||
logSuccess('\nALL TESTS PASSED!');
|
||||
log('Credit persistence is working correctly.');
|
||||
} else {
|
||||
logError('\nSOME TESTS FAILED');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError(`Test failed: ${error.message}`);
|
||||
console.error(error);
|
||||
} finally {
|
||||
// Cleanup
|
||||
if (submitter?.ws?.readyState === WebSocket.OPEN) {
|
||||
submitter.ws.close();
|
||||
}
|
||||
if (worker?.ws?.readyState === WebSocket.OPEN) {
|
||||
worker.ws.close();
|
||||
}
|
||||
|
||||
// Give connections time to close
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
// Additional test: Self-reporting prevention
|
||||
async function testSelfReportingPrevention() {
|
||||
logSection('Security Test: Self-Reporting Prevention');
|
||||
|
||||
let attacker = null;
|
||||
|
||||
try {
|
||||
// Connect as attacker
|
||||
logStep(1, 'Connect as attacker');
|
||||
attacker = await createConnection(
|
||||
`attacker-${crypto.randomBytes(4).toString('hex')}`,
|
||||
`attacker-key-${crypto.randomBytes(8).toString('hex')}`
|
||||
);
|
||||
|
||||
// Get initial balance
|
||||
logStep(2, 'Get initial balance');
|
||||
const initialBalance = await getBalance(attacker);
|
||||
logInfo(`Initial: ${formatRuv(initialBalance.earned)}`);
|
||||
|
||||
// Attempt to self-report credits
|
||||
logStep(3, 'Attempt to self-report 1000 rUv');
|
||||
attacker.ws.send(JSON.stringify({
|
||||
type: 'ledger_update',
|
||||
publicKey: attacker.publicKey,
|
||||
earned: '1000000000000', // 1000 rUv
|
||||
spent: '0',
|
||||
}));
|
||||
|
||||
const error = await waitForMessage(attacker, 'error', 3000);
|
||||
if (error) {
|
||||
logSuccess(`Attack blocked: ${error.message}`);
|
||||
} else {
|
||||
logError('No error received - attack may have succeeded!');
|
||||
}
|
||||
|
||||
// Verify balance unchanged
|
||||
logStep(4, 'Verify balance unchanged');
|
||||
const finalBalance = await getBalance(attacker);
|
||||
logInfo(`Final: ${formatRuv(finalBalance.earned)}`);
|
||||
|
||||
if (finalBalance.earned === initialBalance.earned) {
|
||||
logSuccess('Self-reporting attack prevented!');
|
||||
} else {
|
||||
logError('SECURITY BREACH: Balance was modified!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError(`Security test failed: ${error.message}`);
|
||||
} finally {
|
||||
if (attacker?.ws?.readyState === WebSocket.OPEN) {
|
||||
attacker.ws.close();
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
// Additional test: Double-completion prevention
|
||||
async function testDoubleCompletionPrevention() {
|
||||
logSection('Security Test: Double-Completion Prevention');
|
||||
|
||||
let submitter = null;
|
||||
let worker = null;
|
||||
|
||||
try {
|
||||
// Connect nodes
|
||||
logStep(1, 'Connect nodes');
|
||||
submitter = await createConnection(
|
||||
`submitter-${crypto.randomBytes(4).toString('hex')}`,
|
||||
`submitter-key-${crypto.randomBytes(8).toString('hex')}`
|
||||
);
|
||||
worker = await createConnection(
|
||||
`worker-${crypto.randomBytes(4).toString('hex')}`,
|
||||
`worker-key-${crypto.randomBytes(8).toString('hex')}`
|
||||
);
|
||||
|
||||
// Submit task
|
||||
logStep(2, 'Submit task');
|
||||
submitter.ws.send(JSON.stringify({
|
||||
type: 'task_submit',
|
||||
task: {
|
||||
type: 'inference',
|
||||
model: 'test-model',
|
||||
maxCredits: '10000000000', // 10 rUv
|
||||
},
|
||||
}));
|
||||
|
||||
const acceptance = await waitForMessage(submitter, 'task_accepted');
|
||||
if (!acceptance) throw new Error('Task not accepted');
|
||||
logSuccess(`Task accepted: ${acceptance.taskId}`);
|
||||
|
||||
// Wait for assignment
|
||||
const assignment = await waitForMessage(worker, 'task_assignment');
|
||||
if (!assignment) throw new Error('Task not assigned');
|
||||
const taskId = assignment.task.id;
|
||||
logSuccess(`Task assigned: ${taskId}`);
|
||||
|
||||
// First completion
|
||||
logStep(3, 'Complete task (first time)');
|
||||
worker.ws.send(JSON.stringify({
|
||||
type: 'task_complete',
|
||||
taskId: taskId,
|
||||
result: { done: true },
|
||||
reward: '10000000000',
|
||||
}));
|
||||
|
||||
const credit1 = await waitForMessage(worker, 'credit_earned', 3000);
|
||||
if (credit1) {
|
||||
logSuccess(`First completion: earned ${formatRuv(credit1.amount)}`);
|
||||
} else {
|
||||
logError('First completion failed');
|
||||
}
|
||||
|
||||
// Second completion (should fail)
|
||||
logStep(4, 'Attempt second completion (should fail)');
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const startIdx = worker.messages.length;
|
||||
|
||||
worker.ws.send(JSON.stringify({
|
||||
type: 'task_complete',
|
||||
taskId: taskId,
|
||||
result: { done: true },
|
||||
reward: '10000000000',
|
||||
}));
|
||||
|
||||
const error = await waitForMessage(worker, 'error', 3000);
|
||||
if (error && error.message.includes('already completed')) {
|
||||
logSuccess(`Double-completion blocked: ${error.message}`);
|
||||
} else {
|
||||
// Check if credit_earned was received (bad)
|
||||
const credit2 = worker.messages.slice(startIdx).find(m => m.type === 'credit_earned');
|
||||
if (credit2) {
|
||||
logError('SECURITY BREACH: Double-completion earned credits!');
|
||||
} else {
|
||||
logInfo('No credit earned (expected behavior)');
|
||||
logSuccess('Double-completion prevented');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError(`Security test failed: ${error.message}`);
|
||||
} finally {
|
||||
if (submitter?.ws?.readyState === WebSocket.OPEN) submitter.ws.close();
|
||||
if (worker?.ws?.readyState === WebSocket.OPEN) worker.ws.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
console.log(`
|
||||
Edge-Net Credit Persistence Manual Test
|
||||
|
||||
Usage:
|
||||
node manual-credit-test.cjs [options]
|
||||
|
||||
Options:
|
||||
--relay-url <url> WebSocket URL (default: ws://localhost:8080)
|
||||
--security-only Only run security tests
|
||||
--help, -h Show this help
|
||||
|
||||
Environment Variables:
|
||||
RELAY_URL Alternative to --relay-url
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Override relay URL from args
|
||||
const urlIdx = args.indexOf('--relay-url');
|
||||
if (urlIdx !== -1 && args[urlIdx + 1]) {
|
||||
process.env.RELAY_URL = args[urlIdx + 1];
|
||||
}
|
||||
|
||||
if (args.includes('--security-only')) {
|
||||
await testSelfReportingPrevention();
|
||||
await testDoubleCompletionPrevention();
|
||||
} else {
|
||||
await runTest();
|
||||
await testSelfReportingPrevention();
|
||||
await testDoubleCompletionPrevention();
|
||||
}
|
||||
|
||||
logSection('All Tests Complete');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user