151 lines
5.1 KiB
JavaScript
151 lines
5.1 KiB
JavaScript
|
|
#!/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);
|