Edge-Net Test Suite
Comprehensive security and persistence tests for the Edge-Net WebSocket relay server.
Test Coverage
Credit Persistence Tests
- Local IndexedDB Persistence - Credits earned locally persist across sessions
- QDAG Sync - Credits sync to QDAG (Firestore) as source of truth
- Refresh Recovery - After browser refresh, QDAG balance is loaded
- Pending Credits - Pending credits shown correctly before sync
- Double-Sync Prevention - Prevents duplicate credit awards
- Rate Limiting - Prevents abuse via message throttling
- Cross-Device Sync - Same publicKey = same credits everywhere
Attack Vectors Tested
- Task Completion Spoofing - Prevents nodes from completing tasks not assigned to them
- Replay Attacks - Prevents completing the same task multiple times
- Credit Self-Reporting - Prevents clients from claiming their own credits
- Public Key Spoofing - Tests identity verification and credit attribution
- Rate Limiting - Validates per-node message throttling
- Message Size Limits - Tests protection against oversized payloads
- Connection Limits - Validates per-IP connection restrictions
- Task Expiration - Tests cleanup of unfinished tasks
Setup
Prerequisites
- Node.js 20+
- Running Edge-Net relay server (or will start one for tests)
Installation
cd /workspaces/ruvector/examples/edge-net/tests
npm install
Environment Variables
# Optional: Override relay URL (default: ws://localhost:8080)
export RELAY_URL=ws://localhost:8080
Running Tests
Run all security tests
npm test
Run specific test suite
# Security tests
npm run test:security
# Credit persistence tests
npm run test:credits
# All tests (in sequence)
npm run test:all
Manual credit test (interactive)
npm run test:credits-manual
# With custom relay URL
RELAY_URL=wss://edge-net-relay.example.com npm run test:credits-manual
Watch mode (development)
npm run test:watch
Run with coverage
npm test -- --coverage
Test Structure
tests/
├── relay-security.test.ts # Security test suite (attack vectors)
├── credit-persistence.test.ts # Credit persistence test suite
├── manual-credit-test.cjs # Manual interactive credit test
├── jest.config.js # Jest configuration
├── package.json # Test dependencies
├── tsconfig.json # TypeScript configuration
└── README.md # This file
Credit Persistence Flow
Architecture Overview
[User Completes Task]
|
v
[Relay Verifies Assignment] -----> [REJECT if not assigned]
|
v
[QDAG Credits Account (Firestore)] <-- SOURCE OF TRUTH
|
v
[Relay Sends credit_earned + balance]
|
v
[Dashboard Updates Local State]
|
v
[IndexedDB Saves as Backup]
Reconnection Flow
[Browser Refresh / App Reopen]
|
v
[Load from IndexedDB (backup)]
|
v
[Connect to Relay with publicKey]
|
v
[Request ledger_sync from QDAG]
|
v
[QDAG Returns Authoritative Balance]
|
v
[Update Local State to Match QDAG]
Key Design Principles
- QDAG is Source of Truth - Credits stored in Firestore, keyed by publicKey
- Same Key = Same Credits - publicKey links all devices/CLI
- Server Awards Credits - Only relay can credit accounts
- Client Cannot Self-Report - ledger_update is rejected
- Double-Completion Prevented - completedTasks set tracks finished work
- Task Assignment Verified - assignedTasks map tracks who works on what
Test Scenarios
1. Task Completion Spoofing
// Scenario: Attacker tries to complete victim's task
const victim = await createTestNode();
const attacker = await createTestNode();
// Task assigned to victim
// Attacker tries to complete it
attacker.ws.send({ type: 'task_complete', taskId: victimTask });
// Expected: Error "Task not assigned to you"
2. Replay Attacks
// Scenario: Node tries to complete same task twice
worker.ws.send({ type: 'task_complete', taskId }); // First time: succeeds
worker.ws.send({ type: 'task_complete', taskId }); // Second time: fails
// Expected: Error "Task already completed"
3. Credit Self-Reporting
// Scenario: Client tries to report their own credits
attacker.ws.send({
type: 'ledger_update',
ledger: { earned: 999999999, spent: 0 }
});
// Expected: Error "Credit self-reporting disabled"
4. Public Key Spoofing
// Scenario: Attacker uses victim's public key
const attacker = await createTestNode('attacker', 'victim-public-key');
// Expected: Cannot steal existing credits (assigned at task time)
// Note: Current implementation allows read-only access to balances
Expected Results
All tests should pass:
✓ Task Completion Spoofing
✓ should reject task completion from node not assigned
✓ should only allow assigned worker to complete task
✓ Replay Attacks
✓ should reject duplicate task completion
✓ Credit Self-Reporting
✓ should reject ledger_update messages
✓ should only credit via verified completions
✓ Public Key Spoofing
✓ should not allow stealing credits via key spoofing
✓ should maintain separate ledgers
✓ Rate Limiting
✓ should enforce rate limits per node
✓ Message Size Limits
✓ should reject oversized messages
✓ Connection Limits
✓ should limit connections per IP
✓ Combined Attack Scenario
✓ should defend against combined attacks
Known Issues / Limitations
Current Implementation Gaps
-
No cryptographic signature verification - Public key ownership not proven
- Impact: Medium (cannot verify identity)
- Status: Documented in security audit
- Recommendation: Implement Ed25519 signature validation
-
Unbounded completed tasks set - Memory leak over time
- Impact: Low (development only)
- Status: Needs cleanup implementation
- Recommendation: Periodic cleanup of old tasks
-
Read-only public key spoofing - Can check another user's balance
- Impact: Low (privacy leak only)
- Status: By design (multi-device support)
- Recommendation: Rate-limit balance queries
Security Audit
See comprehensive security audit report:
- Report:
/workspaces/ruvector/examples/edge-net/docs/SECURITY_AUDIT_REPORT.md - Date: 2026-01-03
- Rating: ✅ GOOD (with recommendations)
Debugging Failed Tests
Relay server not running
# Start relay server in separate terminal
cd /workspaces/ruvector/examples/edge-net/relay
npm start
Connection timeout errors
# Check if relay is accessible
curl http://localhost:8080/health
# Expected: {"status":"healthy","nodes":0,"uptime":...}
Firestore errors
# Tests use in-memory ledger cache
# Firestore failures won't break tests, but will log errors
# Set up Firestore credentials for full integration testing
export GOOGLE_CLOUD_PROJECT=your-project-id
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
Contributing
When adding new security tests:
- Add test to appropriate
describe()block - Use helper functions (
createTestNode,waitForMessage, etc.) - Always clean up connections in
finallyblocks - Document the attack vector being tested
- Update this README with new test scenarios