#!/usr/bin/env node /** * Dalanga Transparency Chain Verification Tool * * A standalone, zero-dependency Node.js script that verifies the integrity of a * downloaded Dalanga system chain export file. * * Requirements: Node.js 14+ (standard library only). * * Usage: * node verify-chain.js * * Examples: * node verify-chain.js system-chain-export.json * node verify-chain.js /tmp/chain-export.json * * What it verifies: * 1. Consecutive sequence numbers starting from 0 * 2. Hash linking: each block's previous_hash matches the preceding block's hash * 3. Hash recomputation: re-hashes each block's hash_input and compares to stored hash * 4. Genesis block validation: sequence 0 must have previous_hash = null * 5. Payload hash verification: re-hashes payload and compares to stored payload_hash * 6. Draw fairness: for draw.revealed events, re-derives the winner using the * algorithm_version from event_data and confirms the recorded winner matches * * Exit codes: * 0 = Chain is valid (all checks passed) * 1 = Chain is invalid (one or more checks failed) * 2 = Usage error (missing file, invalid JSON, etc.) */ const crypto = require('crypto'); const fs = require('fs'); const process = require('process'); // ANSI color codes const GREEN = '\x1b[32m'; const RED = '\x1b[31m'; const RESET = '\x1b[0m'; const BOLD = '\x1b[1m'; function success(message) { console.log(`${GREEN}\u2713${RESET} ${message}`); } function failure(message) { console.log(`${RED}\u2717${RESET} ${message}`); } function sha256(input) { return crypto.createHash('sha256').update(input, 'utf8').digest('hex'); } function canonicalJsonEncode(data) { if (data === null || typeof data !== 'object') { return JSON.stringify(data); } if (Array.isArray(data)) { return '[' + data.map(item => canonicalJsonEncode(item)).join(',') + ']'; } const sortedKeys = Object.keys(data).sort(); const pairs = sortedKeys.map(key => JSON.stringify(key) + ':' + canonicalJsonEncode(data[key])); return '{' + pairs.join(',') + '}'; } // --- Input Validation ------------------------------------------------------- if (process.argv.length < 3) { console.log('Dalanga Transparency Chain Verification Tool'); console.log(''); console.log(`Usage: node ${process.argv[1]} `); console.log(''); console.log('Verifies the cryptographic integrity of a chain export file.'); console.log('For draw events, re-derives the winner to confirm fairness.'); process.exit(2); } const filePath = process.argv[2]; if (!fs.existsSync(filePath)) { failure(`File not found: ${filePath}`); process.exit(2); } let content; try { content = fs.readFileSync(filePath, 'utf8'); } catch (e) { failure(`Cannot read file: ${e.message}`); process.exit(2); } let data; try { data = JSON.parse(content); } catch (e) { failure(`Invalid JSON: ${e.message}`); process.exit(2); } if (!Array.isArray(data.blocks)) { failure('Invalid export format: missing "blocks" array'); process.exit(2); } // --- Chain Verification ----------------------------------------------------- const blocks = data.blocks; const blockCount = blocks.length; let errors = 0; console.log(`${BOLD}Dalanga Transparency Chain Verifier${RESET}`); if (data.chain) { console.log(`Chain: ${data.chain}`); } if (data.exported_at) { console.log(`Exported: ${data.exported_at}`); } console.log(`Blocks: ${blockCount}`); console.log(''); if (blockCount === 0) { success('Chain is empty (0 blocks)'); process.exit(0); } // Chain integrity checks for (let i = 0; i < blockCount; i++) { const block = blocks[i]; // Check consecutive sequences if (block.sequence !== i) { failure(`Block ${i}: expected sequence ${i}, got ${block.sequence}`); errors++; continue; } // Genesis block validation if (i === 0 && block.previous_hash !== null) { failure('Block 0: genesis block must have previous_hash = null'); errors++; } // Hash linking if (i > 0 && block.previous_hash !== blocks[i - 1].hash) { failure(`Block ${i}: previous_hash does not match preceding block's hash`); errors++; } // Hash recomputation const recomputedHash = sha256(block.hash_input); if (block.hash !== recomputedHash) { failure(`Block ${i}: hash does not match recomputed hash from hash_input`); errors++; } else { const eventType = block.event_type || 'unknown'; success(`Block ${i}: hash valid (${eventType})`); } } // --- Payload Hash Verification ---------------------------------------------- const hasPayloadHashes = blocks.some(b => 'payload_hash' in b); if (hasPayloadHashes) { console.log(''); console.log(`${BOLD}Payload Hash Verification${RESET}`); let payloadChecked = 0; let payloadSkipped = 0; for (const block of blocks) { if (!('payload_hash' in block)) { continue; } if (!('payload' in block)) { payloadSkipped++; continue; } payloadChecked++; const payloadJson = canonicalJsonEncode(block.payload); const recomputedPayloadHash = sha256(payloadJson); if (block.payload_hash !== recomputedPayloadHash) { failure(`Block ${block.sequence}: payload_hash does not match recomputed hash from payload`); errors++; } else { success(`Block ${block.sequence}: payload hash valid (${block.event_type})`); } } if (payloadSkipped > 0) { console.log(`(Skipped ${payloadSkipped} block(s) without payload data \u2014 blocks-only export)`); } console.log(`Payload hashes checked: ${payloadChecked}, skipped: ${payloadSkipped}`); } // --- Draw Event Verification ------------------------------------------------ const committedBlocks = blocks.filter(b => b.event_type === 'draw.committed'); for (const block of blocks) { if (block.event_type !== 'draw.revealed') { continue; } const eventData = block.event_data || {}; const algorithmVersion = eventData.algorithm_version; if (!algorithmVersion) { failure(`Block ${block.sequence}: draw.revealed missing algorithm_version`); errors++; continue; } if (algorithmVersion !== 'sha256-modulo-v1') { failure(`Block ${block.sequence}: unknown algorithm version '${algorithmVersion}'`); errors++; continue; } // Find matching draw.committed block let committed = null; for (const cb of committedBlocks) { const cbData = cb.event_data || {}; if (cbData.eligible_tickets_snapshot) { committed = cb; } } if (!committed) { failure(`Block ${block.sequence}: draw.revealed but no matching draw.committed found`); errors++; continue; } const committedData = committed.event_data; const seed = eventData.seed; const eligibleTickets = committedData.eligible_tickets_snapshot; const totalEligible = eligibleTickets.length; // Re-derive winner using sha256-modulo-v1 const drawHash = sha256(seed); const hexPrefix = drawHash.substring(0, 12); const winnerIndex = parseInt(hexPrefix, 16) % totalEligible; const winnerTicketNumber = eligibleTickets[winnerIndex]; if (winnerIndex !== eventData.winner_index) { failure(`Block ${block.sequence}: draw winner_index mismatch (expected ${winnerIndex}, got ${eventData.winner_index})`); errors++; } else if (winnerTicketNumber !== eventData.winner_ticket_number) { failure(`Block ${block.sequence}: draw winner_ticket_number mismatch (expected ${winnerTicketNumber}, got ${eventData.winner_ticket_number})`); errors++; } else { success(`Block ${block.sequence}: draw winner independently verified (sha256-modulo-v1)`); } // Verify commitment hash const commitmentHash = committedData.commitment_hash; if (commitmentHash) { const recomputedCommitment = sha256(seed + JSON.stringify(eligibleTickets)); if (commitmentHash !== recomputedCommitment) { failure(`Block ${committed.sequence}: commitment hash mismatch`); errors++; } else { success(`Block ${committed.sequence}: commitment hash verified`); } } } // --- Summary ---------------------------------------------------------------- console.log(''); if (errors === 0) { console.log(`${GREEN}Chain valid: ${blockCount} blocks verified, 0 errors${RESET}`); process.exit(0); } console.log(`${RED}Chain INVALID: ${blockCount} blocks verified, ${errors} error(s)${RESET}`); process.exit(1);