#!/usr/bin/env node /** * HSD Zone Cache — on-demand query with SOA-based invalidation * * Instead of periodic dumps, this: * 1. Queries HSD tree root hash every N seconds * 2. If tree root changed → regenerate zone from HSD * 3. If unchanged → serve cached zone * 4. CoreDNS reads the cached zone file * * The zone file is ONLY written from HSD RPC data. * No manual edits. The sidechain IS the source of truth. * * Usage: * node zone-cache.js * # Runs as daemon, writes to /opt/noc/coredns/db.lthn.auto */ const fs = require('fs'); const { execFileSync } = require('child_process'); const HSD_URL = process.env.HSD_URL || 'http://127.0.0.1:14037'; const HSD_API_KEY = process.env.HSD_API_KEY || 'testkey'; const ZONE_FILE = process.env.ZONE_FILE || '/opt/noc/coredns/db.lthn.auto'; const CHECK_INTERVAL = parseInt(process.env.CHECK_INTERVAL || '15') * 1000; // Known names to query — in production, scan the UTXO set const KNOWN_NAMES = [ 'lethean', 'snider', 'darbs', 'charon', 'cladius', 'explorer', 'testnet', 'trading', 'gateway', 'network', 'monitor', 'storage', 'support', 'trade', 'builder', 'develop', 'miners', 'relayer', ]; let lastTreeRoot = null; let lastZone = null; async function rpcHSD(method, params = []) { const auth = Buffer.from(`x:${HSD_API_KEY}`).toString('base64'); const resp = await fetch(HSD_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${auth}` }, body: JSON.stringify({ method, params }), }); const data = await resp.json(); if (data.error) throw new Error(data.error.message); return data.result; } async function getTreeRoot() { const info = await rpcHSD('getinfo'); return { height: info.blocks, treeRoot: info.treeroot || 'unknown' }; } async function generateZone(height) { const serial = `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}${String(height).padStart(4, '0')}`; const lines = [ `; .lthn zone — generated from HSD sidechain (height ${height})`, `; Generated: ${new Date().toISOString()}`, `; Source: ${HSD_URL} (READ-ONLY — do not edit this file)`, `; Invalidation: SOA changes when HSD tree root changes`, ``, `lthn. 300 IN SOA ns5.lthn.io. admin.lthn.io. ${serial} 3600 600 86400 300`, `lthn. 300 IN NS ns5.lthn.io.`, `lthn. 300 IN NS ns6.lthn.io.`, ``, `; === Records from HSD sidechain (source of truth) ===`, ]; for (const name of KNOWN_NAMES) { try { const resource = await rpcHSD('getnameresource', [name]); if (resource && resource.records && resource.records.length > 0) { lines.push(`; ${name}.lthn`); for (const record of resource.records) { switch (record.type) { case 'GLUE4': lines.push(`${name}.lthn. 300 IN NS ${record.ns}`); lines.push(`${record.ns.replace(/\.$/, '')}.lthn. 300 IN A ${record.address}`); break; case 'GLUE6': lines.push(`${name}.lthn. 300 IN NS ${record.ns}`); lines.push(`${record.ns.replace(/\.$/, '')}.lthn. 300 IN AAAA ${record.address}`); break; case 'TXT': for (const txt of record.txt || []) lines.push(`${name}.lthn. 300 IN TXT "${txt}"`); break; case 'DS': lines.push(`${name}.lthn. 300 IN DS ${record.keyTag} ${record.algorithm} ${record.digestType} ${record.digest}`); break; case 'NS': lines.push(`${name}.lthn. 300 IN NS ${record.ns}`); break; } } } } catch { /* name not registered — skip */ } } lines.push(``, `; === End (${KNOWN_NAMES.length} names checked) ===`); return lines.join('\n') + '\n'; } async function checkAndUpdate() { try { const { height, treeRoot } = await getTreeRoot(); if (treeRoot === lastTreeRoot) { return; // No change — cache is valid } console.log(`[${new Date().toISOString().slice(11, 19)}] Tree root changed at height ${height} — regenerating zone`); const zone = await generateZone(height); // Atomic write — temp file then rename const tmpFile = `${ZONE_FILE}.tmp`; fs.writeFileSync(tmpFile, zone); // Validate before replacing if (!zone.includes('$ORIGIN lthn.')) { console.error('Zone validation failed — not replacing'); fs.unlinkSync(tmpFile); return; } fs.renameSync(tmpFile, ZONE_FILE); lastTreeRoot = treeRoot; lastZone = zone; const recordCount = (zone.match(/\tIN\s/g) || []).length; console.log(` Zone updated: ${recordCount} records, serial based on height ${height}`); } catch (err) { console.error(` Error: ${err.message} — keeping cached zone`); } } console.log('HSD Zone Cache'); console.log(` HSD: ${HSD_URL}`); console.log(` Zone: ${ZONE_FILE}`); console.log(` Check: every ${CHECK_INTERVAL / 1000}s`); console.log(` Source: HSD sidechain ONLY (read-only output)`); console.log(''); // Initial generation checkAndUpdate(); // Polling loop — checks tree root, only regenerates on change setInterval(checkAndUpdate, CHECK_INTERVAL);