#!/usr/bin/env node /** * Lethean Alias-HNS Resolver * * Bridges the main chain alias system to the HNS sidechain. * For each alias with v=lthn1;hns=name.lthn comment: * 1. Queries HSD for the name's resource records * 2. Verifies the TXT record contains the matching lthn-address * 3. Returns the verified binding (alias ↔ HNS name ↔ DNS records) * * Usage: * node resolver.js [--daemon http://127.0.0.1:46941] [--hsd http://127.0.0.1:14037] */ const DAEMON_URL = process.env.DAEMON_URL || 'http://127.0.0.1:46941'; const HSD_URL = process.env.HSD_URL || 'http://127.0.0.1:14037'; const HSD_API_KEY = process.env.HSD_API_KEY || 'testkey'; async function rpcDaemon(method, params = {}) { const resp = await fetch(`${DAEMON_URL}/json_rpc`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: '0', method, params }) }); const data = await resp.json(); if (data.error) throw new Error(`Daemon ${method}: ${data.error.message}`); return data.result; } 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(`HSD ${method}: ${data.error.message}`); return data.result; } async function hsdGetNameResource(name) { try { return await rpcHSD('getnameresource', [name]); } catch { return null; } } function parseComment(comment) { if (!comment || !comment.includes('v=lthn1')) return null; const fields = {}; comment.split(';').forEach(pair => { const [key, ...rest] = pair.trim().split('='); if (key && rest.length) fields[key.trim()] = rest.join('=').trim(); }); if (fields.v !== 'lthn1') return null; return { hns: fields.hns || null, // explicit override, usually derived from alias name type: fields.type || 'user', capabilities: fields.cap ? fields.cap.split(',') : [], tls: fields.tls || null, }; } /** * Resolve and verify a single alias → HNS binding */ async function resolveAlias(alias) { // 1. Get alias details from main chain const result = await rpcDaemon('get_alias_details', { alias: alias.alias || alias }); if (result.status !== 'OK') return { alias: alias.alias || alias, verified: false, reason: 'alias not found' }; const details = result.alias_details; const parsed = parseComment(details.comment); if (!parsed) return { alias: alias.alias || alias, verified: false, reason: 'no v=lthn1 comment' }; // 2. HNS name: explicit from hns= field, or derived from alias name // @charon → charon.lthn (implicit), hns=custom.lthn (explicit override) const aliasName = alias.alias || alias; const hnsFullName = parsed.hns || `${aliasName}.lthn`; const hnsName = hnsFullName.replace(/\.lthn$/, ''); // 3. Query HSD for the name's resource records const resource = await hsdGetNameResource(hnsName); if (!resource || !resource.records) return { alias: alias.alias || alias, hns: parsed.hns, verified: false, reason: 'HNS name not found or no records' }; // 4. Find TXT record with lthn-address let hnsAddress = null; let dnsRecords = []; for (const record of resource.records) { dnsRecords.push(record); if (record.type === 'TXT' && record.txt) { for (const txt of record.txt) { const match = txt.match(/lthn-address=([a-zA-Z0-9]+)/); if (match) hnsAddress = match[1]; } } } // 5. Verify the chain address matches the HNS TXT record const verified = hnsAddress === details.address; return { alias: alias.alias || alias, address: details.address, hns: parsed.hns, hnsName, type: parsed.type, capabilities: parsed.capabilities, tls: parsed.tls, dnsRecords, hnsAddress, verified, reason: verified ? 'chain address matches HNS TXT record' : `mismatch: chain=${details.address?.slice(0,16)} hns=${hnsAddress?.slice(0,16) || 'none'}`, }; } /** * Resolve all aliases and verify HNS bindings */ async function resolveAll() { const chainInfo = await rpcDaemon('getinfo'); console.log(`Chain: height=${chainInfo.height}, aliases=${chainInfo.alias_count}`); let hsdInfo; try { hsdInfo = await rpcHSD('getinfo'); console.log(`HSD: height=${hsdInfo.blocks}, network=${hsdInfo.network}`); } catch (e) { console.log(`HSD: not reachable (${e.message})`); } if (chainInfo.alias_count === 0) { console.log('\nNo aliases registered yet (requires HF4)'); console.log('When aliases exist, this resolver will:'); console.log(' 1. Read all aliases from chain'); console.log(' 2. Parse v=lthn1 comments for HNS bindings'); console.log(' 3. Query HSD for each name\'s DNS records'); console.log(' 4. Verify chain address matches HNS TXT lthn-address'); console.log(' 5. Output verified bindings as JSON'); return; } const allAliases = await rpcDaemon('get_all_alias_details', {}); const aliases = allAliases.aliases || []; const results = []; for (const alias of aliases) { const parsed = parseComment(alias.comment); if (parsed) { // v=lthn1 present — HNS name derived from alias if not explicit const resolved = await resolveAlias(alias); results.push(resolved); } } console.log(`\nResolved ${results.length} aliases with HNS bindings:`); for (const r of results) { const status = r.verified ? '✓ VERIFIED' : '✗ UNVERIFIED'; console.log(` ${r.alias}.lthn → ${r.hns} [${status}] ${r.reason}`); if (r.dnsRecords) { for (const rec of r.dnsRecords) { console.log(` ${rec.type}: ${JSON.stringify(rec).slice(0, 80)}`); } } } // Write registry const fs = await import('fs'); fs.writeFileSync('/tmp/lthn-resolved-registry.json', JSON.stringify({ timestamp: new Date().toISOString(), chain: { height: chainInfo.height, aliases: chainInfo.alias_count }, hsd: hsdInfo ? { height: hsdInfo.blocks, network: hsdInfo.network } : null, bindings: results, }, null, 2)); console.log('\nRegistry written to /tmp/lthn-resolved-registry.json'); } resolveAll().catch(console.error);