#!/usr/bin/env php * * Examples: * php verify-chain.php system-chain-export.json * php verify-chain.php /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. 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.) */ // ANSI color codes define('GREEN', "\033[32m"); define('RED', "\033[31m"); define('RESET', "\033[0m"); define('BOLD', "\033[1m"); function success(string $message): void { echo GREEN.'✓'.RESET.' '.$message.PHP_EOL; } function failure(string $message): void { echo RED.'✗'.RESET.' '.$message.PHP_EOL; } function canonical_json_encode(mixed $data): string { return json_encode(sort_keys_recursive($data), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } function sort_keys_recursive(mixed $data): mixed { if (! is_array($data)) { return $data; } if (array_is_list($data)) { return array_map('sort_keys_recursive', $data); } ksort($data); return array_map('sort_keys_recursive', $data); } // ─── Input Validation ──────────────────────────────────────────────── if ($argc < 2) { echo 'Dalanga Transparency Chain Verification Tool'.PHP_EOL; echo PHP_EOL; echo 'Usage: php '.basename($argv[0]).' '.PHP_EOL; echo PHP_EOL; echo 'Verifies the cryptographic integrity of a chain export file.'.PHP_EOL; echo 'For draw events, re-derives the winner to confirm fairness.'.PHP_EOL; exit(2); } $path = $argv[1]; if (! file_exists($path)) { failure("File not found: {$path}"); exit(2); } $content = file_get_contents($path); $data = json_decode($content, true); if ($data === null) { failure('Invalid JSON: '.json_last_error_msg()); exit(2); } if (! isset($data['blocks']) || ! is_array($data['blocks'])) { failure('Invalid export format: missing "blocks" array'); exit(2); } // ─── Chain Verification ────────────────────────────────────────────── $blocks = $data['blocks']; $blockCount = count($blocks); $errors = 0; echo BOLD.'Dalanga Transparency Chain Verifier'.RESET.PHP_EOL; if (isset($data['chain'])) { echo 'Chain: '.$data['chain'].PHP_EOL; } if (isset($data['exported_at'])) { echo 'Exported: '.$data['exported_at'].PHP_EOL; } echo 'Blocks: '.$blockCount.PHP_EOL; echo PHP_EOL; if ($blockCount === 0) { success('Chain is empty (0 blocks)'); exit(0); } // Chain integrity checks for ($i = 0; $i < $blockCount; $i++) { $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 $recomputedHash = hash('sha256', $block['hash_input']); if ($block['hash'] !== $recomputedHash) { failure("Block {$i}: hash does not match recomputed hash from hash_input"); $errors++; } else { $eventType = $block['event_type'] ?? 'unknown'; success("Block {$i}: hash valid ({$eventType})"); } } // ─── Payload Hash Verification ────────────────────────────────────── $hasPayloadHashes = false; foreach ($blocks as $block) { if (isset($block['payload_hash'])) { $hasPayloadHashes = true; break; } } if ($hasPayloadHashes) { echo PHP_EOL; echo BOLD.'Payload Hash Verification'.RESET.PHP_EOL; $payloadChecked = 0; $payloadSkipped = 0; foreach ($blocks as $block) { if (! isset($block['payload_hash'])) { continue; } if (! isset($block['payload'])) { $payloadSkipped++; continue; } $payloadChecked++; $recomputedPayloadHash = hash('sha256', canonical_json_encode($block['payload'])); 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) { echo "(Skipped {$payloadSkipped} block(s) without payload data — blocks-only export)".PHP_EOL; } echo "Payload hashes checked: {$payloadChecked}, skipped: {$payloadSkipped}".PHP_EOL; } // ─── Anchor Event Verification ─────────────────────────────────────── $anchorConfiguredCount = 0; $anchoredBlocks = []; foreach ($blocks as $block) { $eventType = $block['event_type'] ?? null; if ($eventType === 'chain.anchor_configured') { $anchorConfiguredCount++; $eventData = $block['event_data'] ?? []; if (! isset($eventData['anchor_service'])) { failure("Block {$block['sequence']}: chain.anchor_configured missing anchor_service"); $errors++; } else { success("Block {$block['sequence']}: anchor configured ({$eventData['anchor_service']})"); } if ($anchorConfiguredCount > 1) { failure("Block {$block['sequence']}: duplicate chain.anchor_configured (expected at most 1)"); $errors++; } } if ($eventType === 'chain.anchored') { $eventData = $block['event_data'] ?? []; if (! isset($eventData['anchor_service'])) { failure("Block {$block['sequence']}: chain.anchored missing anchor_service"); $errors++; } if (! isset($eventData['anchor_reference'])) { failure("Block {$block['sequence']}: chain.anchored missing anchor_reference"); $errors++; } if (! isset($eventData['chain_state_hash'])) { failure("Block {$block['sequence']}: chain.anchored missing chain_state_hash"); $errors++; } if (! isset($eventData['block_count'])) { failure("Block {$block['sequence']}: chain.anchored missing block_count"); $errors++; } if (! isset($eventData['block_sequence'])) { failure("Block {$block['sequence']}: chain.anchored missing block_sequence"); $errors++; } else { // anchor's block_sequence must reference a block that exists before this anchor block if ($eventData['block_sequence'] >= $block['sequence']) { failure("Block {$block['sequence']}: chain.anchored references block_sequence {$eventData['block_sequence']} which is not before this block"); $errors++; } else { success("Block {$block['sequence']}: anchor record valid ({$eventData['anchor_service']} @ seq {$eventData['block_sequence']})"); } } $anchoredBlocks[] = $block; } } if ($anchoredBlocks !== []) { echo PHP_EOL; echo BOLD.'Anchor History'.RESET.PHP_EOL; echo 'Anchor configuration events: '.$anchorConfiguredCount.PHP_EOL; echo 'Anchor records on-chain: '.count($anchoredBlocks).PHP_EOL; foreach ($anchoredBlocks as $ab) { $ed = $ab['event_data'] ?? []; $service = $ed['anchor_service'] ?? '?'; $ref = $ed['anchor_reference'] ?? ''; $seq = $ed['block_sequence'] ?? '?'; echo " [{$service}] seq {$seq} → {$ref}".PHP_EOL; } } // ─── Draw Event Verification ───────────────────────────────────────── $committedBlocks = []; foreach ($blocks as $block) { if (($block['event_type'] ?? null) === 'draw.committed') { $committedBlocks[] = $block; } } foreach ($blocks as $block) { if (($block['event_type'] ?? null) !== 'draw.revealed') { continue; } $eventData = $block['event_data'] ?? []; $algorithmVersion = $eventData['algorithm_version'] ?? null; if ($algorithmVersion === null) { 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 $committed = null; foreach ($committedBlocks as $cb) { $cbData = $cb['event_data'] ?? []; if (isset($cbData['eligible_tickets_snapshot'])) { $committed = $cb; } } if ($committed === null) { failure("Block {$block['sequence']}: draw.revealed but no matching draw.committed found"); $errors++; continue; } $committedData = $committed['event_data']; $seed = $eventData['seed']; $eligibleTickets = $committedData['eligible_tickets_snapshot']; $totalEligible = count($eligibleTickets); // Re-derive winner using sha256-modulo-v1 $winnerIndex = hexdec(substr(hash('sha256', $seed), 0, 12)) % $totalEligible; $winnerTicketNumber = $eligibleTickets[$winnerIndex]; if ($winnerIndex !== $eventData['winner_index']) { failure("Block {$block['sequence']}: draw winner_index mismatch (expected {$winnerIndex}, got {$eventData['winner_index']})"); $errors++; } elseif ($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 $commitmentHash = $committedData['commitment_hash'] ?? null; if ($commitmentHash !== null) { $recomputedCommitment = hash('sha256', $seed.json_encode($eligibleTickets)); if ($commitmentHash !== $recomputedCommitment) { failure("Block {$committed['sequence']}: commitment hash mismatch"); $errors++; } else { success("Block {$committed['sequence']}: commitment hash verified"); } } } // ─── Summary ───────────────────────────────────────────────────────── echo PHP_EOL; if ($errors === 0) { echo GREEN."Chain valid: {$blockCount} blocks verified, 0 errors".RESET.PHP_EOL; exit(0); } echo RED."Chain INVALID: {$blockCount} blocks verified, {$errors} error(s)".RESET.PHP_EOL; exit(1);