reorg
This commit is contained in:
@@ -0,0 +1,711 @@
|
||||
// core.js
|
||||
const state = require('../infrastructure/state');
|
||||
const { logDebug, logError, logInfo, logWarn } = require('../infrastructure/logger');
|
||||
const { trackDomainEvent, trackConsensusEvent } = require('../maintenance/metrics');
|
||||
const { validateConfig } = require('../infrastructure/config');
|
||||
const { secondsToMs, getPersistentPublicKey } = require('../infrastructure/utils');
|
||||
|
||||
// Get consensus configuration
|
||||
let consensusConfig = null;
|
||||
function getConsensusConfig() {
|
||||
if (!consensusConfig) {
|
||||
try {
|
||||
consensusConfig = validateConfig();
|
||||
} catch (err) {
|
||||
// Fallback to defaults if config not available
|
||||
consensusConfig = {
|
||||
CONSENSUS_QUORUM_THRESHOLD: 0.5,
|
||||
CONSENSUS_MIN_VOTES: 2,
|
||||
CONSENSUS_TIE_BREAKER: 'timestamp',
|
||||
CONSENSUS_VOTE_VALIDATION: true,
|
||||
CONSENSUS_IMMEDIATE_UPDATE: true
|
||||
};
|
||||
}
|
||||
}
|
||||
return consensusConfig;
|
||||
}
|
||||
|
||||
// Cache for entries to avoid repeated expensive lookups
|
||||
let entriesCache = null;
|
||||
let entriesCacheTimestamp = 0;
|
||||
const CACHE_TTL_MS = secondsToMs(5); // Cache for 5 seconds
|
||||
|
||||
// Consensus state cache
|
||||
const consensusStateCache = new Map();
|
||||
const CONSENSUS_CACHE_TTL = secondsToMs(10); // 10 seconds
|
||||
|
||||
// Consensus metrics
|
||||
const consensusMetrics = {
|
||||
resolutions: 0,
|
||||
quorumFailures: 0,
|
||||
ties: 0,
|
||||
validationFailures: 0,
|
||||
totalVotes: 0,
|
||||
avgVotesPerDomain: 0,
|
||||
domainResolutions: new Map() // domain -> { resolved: count, failed: count }
|
||||
};
|
||||
|
||||
// Track counted votes and resolutions to prevent double-counting
|
||||
const countedVotes = new Set(); // Set of vote keys: "domain:claimant:voter"
|
||||
const countedResolutions = new Set(); // Set of resolution keys: "domain:claimant"
|
||||
const countedQuorumFailures = new Set(); // Set of domain names that have been counted for quorum failures
|
||||
const countedTies = new Set(); // Set of tie keys: "domain:claimant"
|
||||
|
||||
async function getAllEntries(pass = state.dnsPass, useCache = true) {
|
||||
const now = Date.now();
|
||||
|
||||
// Return cached entries if still valid
|
||||
if (useCache && entriesCache && (now - entriesCacheTimestamp) < CACHE_TTL_MS) {
|
||||
logDebug('Core', 'Returning cached entries');
|
||||
return entriesCache;
|
||||
}
|
||||
|
||||
// Check if pass is null
|
||||
if (!pass) {
|
||||
logDebug('Core', 'dnsPass not initialized, returning empty entries');
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries = [];
|
||||
try {
|
||||
const stream = pass.list();
|
||||
for await (const entry of stream) {
|
||||
entries.push({
|
||||
key: entry.key.toString('utf8'),
|
||||
value: entry.value.toString('utf8')
|
||||
});
|
||||
}
|
||||
// Update cache
|
||||
entriesCache = entries;
|
||||
entriesCacheTimestamp = now;
|
||||
logDebug('Core', `Cached ${entries.length} entries`);
|
||||
} catch (err) {
|
||||
logError('Core', `Error getting entries: ${err.message}`);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
// Function to invalidate entries cache
|
||||
function invalidateEntriesCache() {
|
||||
entriesCache = null;
|
||||
entriesCacheTimestamp = 0;
|
||||
consensusStateCache.clear();
|
||||
logDebug('Core', 'Entries cache invalidated');
|
||||
}
|
||||
|
||||
// Parse claim value to extract hash, clients, timestamp, and ssl flag
|
||||
function parseClaimValue(value) {
|
||||
try {
|
||||
// Try to parse as JSON first (new format with timestamp, clients, and ssl)
|
||||
const parsed = JSON.parse(value);
|
||||
if (parsed.hash && parsed.timestamp) {
|
||||
return {
|
||||
hash: parsed.hash,
|
||||
clients: parsed.clients || [],
|
||||
timestamp: parsed.timestamp,
|
||||
ssl: parsed.ssl === true,
|
||||
legacy: false
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, treat as legacy format (just hash)
|
||||
}
|
||||
// Legacy format: value is just the hash, default ssl to false
|
||||
return { hash: value, clients: [], timestamp: Date.now(), ssl: false, legacy: true };
|
||||
}
|
||||
|
||||
// Get clients from a claim record
|
||||
async function getClaimClients(domain, claimant) {
|
||||
const pass = state.dnsPass;
|
||||
if (!pass) return [];
|
||||
|
||||
try {
|
||||
const claimKey = `claim:${domain}:${claimant}`;
|
||||
const allEntries = await getAllEntries(pass, false);
|
||||
for (const entry of allEntries) {
|
||||
if (entry.key === claimKey) {
|
||||
const parsed = parseClaimValue(entry.value);
|
||||
return parsed.clients || [];
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logError('Core', `Error getting claim clients: ${err.message}`);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Update claim record with clients array
|
||||
async function updateClaimClients(domain, claimant, clients) {
|
||||
const pass = state.dnsPass;
|
||||
if (!pass) {
|
||||
logWarn('Core', 'dnsPass not initialized, cannot update claim clients');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const claimKey = `claim:${domain}:${claimant}`;
|
||||
const allEntries = await getAllEntries(pass, false);
|
||||
|
||||
// Find existing claim
|
||||
let existingClaim = null;
|
||||
for (const entry of allEntries) {
|
||||
if (entry.key === claimKey) {
|
||||
existingClaim = entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!existingClaim) {
|
||||
logWarn('Core', `No claim found for ${domain} by ${claimant}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse existing claim
|
||||
const parsed = parseClaimValue(existingClaim.value);
|
||||
|
||||
// Update with new clients array
|
||||
const updatedValue = JSON.stringify({
|
||||
hash: parsed.hash,
|
||||
clients: clients,
|
||||
timestamp: parsed.timestamp || Date.now()
|
||||
});
|
||||
|
||||
// Remove old claim and add updated one
|
||||
await pass.remove(claimKey);
|
||||
await pass.add(claimKey, updatedValue);
|
||||
|
||||
invalidateEntriesCache();
|
||||
logInfo('Core', `Updated claim clients for ${domain} by ${claimant}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
logError('Core', `Error updating claim clients: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get active peer count
|
||||
function getActivePeerCount() {
|
||||
return (state.connectedPeers?.size || 0) + 1; // +1 for local node
|
||||
}
|
||||
|
||||
// Validate vote references existing claim
|
||||
function validateVote(claimant, claims) {
|
||||
const config = getConsensusConfig();
|
||||
if (!config.CONSENSUS_VOTE_VALIDATION) {
|
||||
return true; // Validation disabled
|
||||
}
|
||||
|
||||
if (!claims.hasOwnProperty(claimant)) {
|
||||
consensusMetrics.validationFailures++;
|
||||
if (trackConsensusEvent) {
|
||||
trackConsensusEvent('validation_failure', { claimant });
|
||||
}
|
||||
logWarn('Core', `Vote validation failed: claimant ${claimant} does not have a claim`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Apply tie-breaking strategy
|
||||
function applyTieBreaker(candidates, claims, claimTimestamps, localWriter) {
|
||||
const config = getConsensusConfig();
|
||||
const strategy = config.CONSENSUS_TIE_BREAKER || 'timestamp';
|
||||
|
||||
logDebug('Core', `Applying tie-breaker strategy: ${strategy} to ${candidates.length} candidates`);
|
||||
|
||||
switch (strategy) {
|
||||
case 'timestamp':
|
||||
// Prefer oldest claim (first-come-first-served)
|
||||
return candidates.sort((a, b) => {
|
||||
const tsA = claimTimestamps[a] || 0;
|
||||
const tsB = claimTimestamps[b] || 0;
|
||||
return tsA - tsB; // Older (smaller timestamp) wins
|
||||
})[0];
|
||||
|
||||
case 'claimant_age':
|
||||
// Prefer claimant with longest history (simplified: prefer local if available)
|
||||
if (candidates.includes(localWriter)) {
|
||||
return localWriter;
|
||||
}
|
||||
// Fallback to lexicographic
|
||||
return candidates.sort((a, b) => a.localeCompare(b))[0];
|
||||
|
||||
case 'lexicographic':
|
||||
default:
|
||||
// Original behavior: prefer local, then lexicographic
|
||||
if (candidates.includes(localWriter)) {
|
||||
return localWriter;
|
||||
}
|
||||
return candidates.sort((a, b) => a.localeCompare(b))[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Get consensus state for a domain
|
||||
async function getConsensusState(domain) {
|
||||
const pass = state.dnsPass;
|
||||
if (!pass) {
|
||||
return {
|
||||
status: 'error',
|
||||
error: 'dnsPass not initialized',
|
||||
hash: null,
|
||||
resolvedClaimant: null,
|
||||
voteCounts: {},
|
||||
activePeers: 0,
|
||||
quorumMet: false,
|
||||
lastResolution: null
|
||||
};
|
||||
}
|
||||
|
||||
// Check cache
|
||||
const cached = consensusStateCache.get(domain);
|
||||
if (cached && (Date.now() - cached.timestamp) < CONSENSUS_CACHE_TTL) {
|
||||
return cached.state;
|
||||
}
|
||||
|
||||
await pass.ready();
|
||||
const localWriter = getPersistentPublicKey();
|
||||
if (!localWriter) {
|
||||
return {
|
||||
status: 'error',
|
||||
error: 'Persistent public key not available',
|
||||
hash: null,
|
||||
resolvedClaimant: null,
|
||||
voteCounts: {},
|
||||
activePeers: 0,
|
||||
quorumMet: false,
|
||||
lastResolution: null
|
||||
};
|
||||
}
|
||||
const allEntries = await getAllEntries(pass, true);
|
||||
|
||||
const claims = {};
|
||||
const claimTimestamps = {};
|
||||
const voteCounts = {};
|
||||
const voters = new Set();
|
||||
|
||||
// Collect claims and their timestamps
|
||||
for (const entry of allEntries) {
|
||||
if (entry.key.startsWith(`claim:${domain}:`)) {
|
||||
const claimant = entry.key.slice(`claim:${domain}:`.length);
|
||||
const parsed = parseClaimValue(entry.value);
|
||||
claims[claimant] = parsed.hash;
|
||||
claimTimestamps[claimant] = parsed.timestamp;
|
||||
voteCounts[claimant] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect votes and validate
|
||||
for (const entry of allEntries) {
|
||||
if (entry.key.startsWith(`vote:${domain}:`)) {
|
||||
const parts = entry.key.split(':');
|
||||
if (parts.length === 4) {
|
||||
const claimant = parts[2];
|
||||
const voter = parts[3];
|
||||
|
||||
// Validate vote
|
||||
if (validateVote(claimant, claims)) {
|
||||
voteCounts[claimant]++;
|
||||
voters.add(voter);
|
||||
// Only count vote once
|
||||
const voteKey = `${domain}:${claimant}:${voter}`;
|
||||
if (!countedVotes.has(voteKey)) {
|
||||
countedVotes.add(voteKey);
|
||||
consensusMetrics.totalVotes++;
|
||||
}
|
||||
} else {
|
||||
logDebug('Core', `Skipping invalid vote: ${entry.key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const activePeers = getActivePeerCount();
|
||||
const config = getConsensusConfig();
|
||||
const minVotes = Math.max(config.CONSENSUS_MIN_VOTES, Math.ceil(activePeers * config.CONSENSUS_QUORUM_THRESHOLD));
|
||||
const totalVotes = Object.values(voteCounts).reduce((sum, count) => sum + count, 0);
|
||||
const quorumMet = totalVotes >= minVotes;
|
||||
|
||||
let status = 'no_claims';
|
||||
let resolvedClaimant = null;
|
||||
let hash = null;
|
||||
|
||||
if (Object.keys(claims).length === 0) {
|
||||
status = 'no_claims';
|
||||
} else if (!quorumMet && totalVotes === 0) {
|
||||
// No votes but have claims - check if single local claim
|
||||
if (Object.keys(claims).length === 1 && claims.hasOwnProperty(localWriter)) {
|
||||
status = 'resolved';
|
||||
resolvedClaimant = localWriter;
|
||||
hash = claims[localWriter];
|
||||
// Count resolution once per domain/claimant combination
|
||||
const resolutionKey = `${domain}:${resolvedClaimant}`;
|
||||
if (!countedResolutions.has(resolutionKey)) {
|
||||
countedResolutions.add(resolutionKey);
|
||||
consensusMetrics.resolutions++;
|
||||
}
|
||||
} else {
|
||||
status = 'insufficient_quorum';
|
||||
// Count quorum failure once per domain
|
||||
if (!countedQuorumFailures.has(domain)) {
|
||||
countedQuorumFailures.add(domain);
|
||||
consensusMetrics.quorumFailures++;
|
||||
}
|
||||
}
|
||||
} else if (!quorumMet) {
|
||||
status = 'insufficient_quorum';
|
||||
// Count quorum failure once per domain
|
||||
if (!countedQuorumFailures.has(domain)) {
|
||||
countedQuorumFailures.add(domain);
|
||||
consensusMetrics.quorumFailures++;
|
||||
}
|
||||
if (trackConsensusEvent) {
|
||||
trackConsensusEvent('quorum_failure', { domain, totalVotes, minVotes, activePeers });
|
||||
}
|
||||
} else {
|
||||
const maxVotes = Math.max(...Object.values(voteCounts));
|
||||
const candidates = Object.keys(voteCounts).filter(claimant => voteCounts[claimant] === maxVotes);
|
||||
|
||||
if (candidates.length === 1) {
|
||||
status = 'resolved';
|
||||
resolvedClaimant = candidates[0];
|
||||
hash = claims[resolvedClaimant];
|
||||
// Count resolution once per domain/claimant combination
|
||||
const resolutionKey = `${domain}:${resolvedClaimant}`;
|
||||
if (!countedResolutions.has(resolutionKey)) {
|
||||
countedResolutions.add(resolutionKey);
|
||||
consensusMetrics.resolutions++;
|
||||
}
|
||||
} else {
|
||||
// Tie - apply tie-breaker
|
||||
status = 'tie';
|
||||
resolvedClaimant = applyTieBreaker(candidates, claims, claimTimestamps, localWriter);
|
||||
hash = claims[resolvedClaimant];
|
||||
// Count tie once per domain/claimant combination
|
||||
const tieKey = `${domain}:${resolvedClaimant}`;
|
||||
if (!countedTies.has(tieKey)) {
|
||||
countedTies.add(tieKey);
|
||||
consensusMetrics.ties++;
|
||||
}
|
||||
if (trackConsensusEvent) {
|
||||
trackConsensusEvent('tie', { domain, candidates: candidates.length });
|
||||
}
|
||||
logInfo('Core', `Tie resolved for ${domain} using ${config.CONSENSUS_TIE_BREAKER} strategy: ${resolvedClaimant}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Update domain resolution metrics
|
||||
if (!consensusMetrics.domainResolutions.has(domain)) {
|
||||
consensusMetrics.domainResolutions.set(domain, { resolved: 0, failed: 0 });
|
||||
}
|
||||
const domainMetrics = consensusMetrics.domainResolutions.get(domain);
|
||||
if (status === 'resolved') {
|
||||
domainMetrics.resolved++;
|
||||
} else if (status !== 'no_claims') {
|
||||
domainMetrics.failed++;
|
||||
}
|
||||
|
||||
// Calculate average votes per domain
|
||||
const domainsWithVotes = Array.from(consensusMetrics.domainResolutions.keys()).length;
|
||||
if (domainsWithVotes > 0) {
|
||||
consensusMetrics.avgVotesPerDomain = consensusMetrics.totalVotes / domainsWithVotes;
|
||||
}
|
||||
|
||||
const consensusState = {
|
||||
status,
|
||||
hash,
|
||||
resolvedClaimant,
|
||||
voteCounts: { ...voteCounts },
|
||||
activePeers,
|
||||
quorumMet,
|
||||
minVotes,
|
||||
totalVotes,
|
||||
lastResolution: Date.now()
|
||||
};
|
||||
|
||||
// Cache the consensus state
|
||||
consensusStateCache.set(domain, {
|
||||
state: consensusState,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return consensusState;
|
||||
}
|
||||
|
||||
async function getHashForDomain(domain) {
|
||||
const consensusState = await getConsensusState(domain);
|
||||
|
||||
if (consensusState.status === 'resolved') {
|
||||
return consensusState.hash;
|
||||
}
|
||||
|
||||
// For backward compatibility, return null if not resolved
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getDomainSSLStatus(domain) {
|
||||
const consensusState = await getConsensusState(domain);
|
||||
|
||||
if (consensusState.status !== 'resolved' || !consensusState.resolvedClaimant) {
|
||||
// Domain not resolved or no claimant, default to false
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the claim for the resolved claimant to extract SSL flag
|
||||
const pass = state.dnsPass;
|
||||
if (!pass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await pass.ready();
|
||||
const allEntries = await getAllEntries(pass, true);
|
||||
const claimKey = `claim:${domain}:${consensusState.resolvedClaimant}`;
|
||||
|
||||
for (const entry of allEntries) {
|
||||
if (entry.key === claimKey) {
|
||||
const parsed = parseClaimValue(entry.value);
|
||||
return parsed.ssl === true;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logError('Core', `Error getting SSL status for domain ${domain}: ${err.message}`);
|
||||
}
|
||||
|
||||
// Default to false if claim not found or error
|
||||
return false;
|
||||
}
|
||||
|
||||
async function doAutoVotes() {
|
||||
const pass = state.dnsPass;
|
||||
if (!pass) return;
|
||||
|
||||
logInfo('Core', 'Performing auto-votes check');
|
||||
const allEntries = await getAllEntries();
|
||||
logDebug('Core', `All keys in auto-votes check: ${allEntries.map(e => e.key).join(', ')}`);
|
||||
const domains = new Set();
|
||||
for (const entry of allEntries) {
|
||||
if (entry.key.startsWith('claim:')) {
|
||||
const parts = entry.key.split(':');
|
||||
if (parts.length === 3) {
|
||||
domains.add(parts[1]);
|
||||
logDebug('Core', `Discovered domain: ${parts[1]} with claim ${entry.key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
logDebug('Core', `Domains found for auto-vote: ${Array.from(domains).join(', ')}`);
|
||||
for (const domain of domains) {
|
||||
await autoVoteForDomain(domain, allEntries);
|
||||
}
|
||||
}
|
||||
|
||||
async function autoVoteForDomain(domain, allEntries) {
|
||||
const pass = state.dnsPass;
|
||||
if (!pass) return;
|
||||
|
||||
logDebug('Core', `Auto-voting for domain: ${domain}`);
|
||||
const config = getConsensusConfig();
|
||||
const claims = {};
|
||||
const claimTimestamps = {};
|
||||
const myVotes = new Map(); // claimant -> voteKey
|
||||
const localWriter = getPersistentPublicKey();
|
||||
if (!localWriter) {
|
||||
logWarn('Core', 'Cannot auto-vote: persistent public key not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect claims and timestamps
|
||||
for (const entry of allEntries) {
|
||||
if (entry.key.startsWith(`claim:${domain}:`)) {
|
||||
const claimant = entry.key.slice(`claim:${domain}:`.length);
|
||||
const parsed = parseClaimValue(entry.value);
|
||||
claims[claimant] = parsed.hash;
|
||||
claimTimestamps[claimant] = parsed.timestamp;
|
||||
logDebug('Core', `Processing claim for ${domain}: claimant=${claimant}, hash=${parsed.hash}, timestamp=${parsed.timestamp}`);
|
||||
}
|
||||
if (entry.key.startsWith(`vote:${domain}:`)) {
|
||||
const parts = entry.key.split(':');
|
||||
if (parts.length === 4 && parts[3] === localWriter) {
|
||||
myVotes.set(parts[2], entry.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const claimsLength = Object.keys(claims).length;
|
||||
logDebug('Core', `Found ${claimsLength} claims for ${domain}`);
|
||||
if (claimsLength === 0) return;
|
||||
|
||||
// Get consensus state to inform voting decision
|
||||
const consensusState = await getConsensusState(domain);
|
||||
|
||||
// Determine intended claimant
|
||||
const localHasClaim = claims.hasOwnProperty(localWriter);
|
||||
let intendedClaimant;
|
||||
|
||||
if (localHasClaim) {
|
||||
intendedClaimant = localWriter;
|
||||
logDebug('Core', `Voting for own claim: ${intendedClaimant}`);
|
||||
} else if (claimsLength === 1) {
|
||||
intendedClaimant = Object.keys(claims)[0];
|
||||
logDebug('Core', `Voting for single claim: ${intendedClaimant}`);
|
||||
} else {
|
||||
// Multiple claims - use tie-breaker strategy
|
||||
const candidates = Object.keys(claims);
|
||||
intendedClaimant = applyTieBreaker(candidates, claims, claimTimestamps, localWriter);
|
||||
logDebug('Core', `Voting for claimant selected by tie-breaker: ${intendedClaimant}`);
|
||||
}
|
||||
|
||||
// Validate intended claimant exists
|
||||
if (!validateVote(intendedClaimant, claims)) {
|
||||
logWarn('Core', `Cannot vote for invalid claimant: ${intendedClaimant}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove votes for other claimants
|
||||
for (const [votedClaimant, voteKey] of myVotes) {
|
||||
if (votedClaimant !== intendedClaimant) {
|
||||
await pass.remove(voteKey);
|
||||
logInfo('Core', `Removed vote for ${domain} claimant ${votedClaimant}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add vote if not already voting for intended claimant
|
||||
if (!myVotes.has(intendedClaimant)) {
|
||||
const voteKey = `vote:${domain}:${intendedClaimant}:${localWriter}`;
|
||||
await pass.add(voteKey, claims[intendedClaimant]);
|
||||
logInfo('Core', `Auto-voted for ${domain} claimant ${intendedClaimant} (reason: ${localHasClaim ? 'own_claim' : claimsLength === 1 ? 'single_claim' : 'tie_breaker'})`);
|
||||
} else {
|
||||
logDebug('Core', `Already voted for ${domain} claimant ${intendedClaimant}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDomain(domain) {
|
||||
const pass = state.dnsPass;
|
||||
if (!pass) {
|
||||
logWarn('Core', 'dnsPass not initialized, cannot remove domain');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const allEntries = await getAllEntries(pass, false); // Don't use cache for removal
|
||||
for (const entry of allEntries) {
|
||||
if (entry.key.startsWith(`claim:${domain}:`) ||
|
||||
entry.key.startsWith(`vote:${domain}:`)) {
|
||||
await pass.remove(entry.key);
|
||||
logDebug('Core', `Removed ${entry.key} for domain removal`);
|
||||
}
|
||||
}
|
||||
invalidateEntriesCache(); // Invalidate cache after removal
|
||||
consensusStateCache.delete(domain); // Remove from consensus cache
|
||||
trackDomainEvent('remove', domain);
|
||||
logInfo('Core', `Completed removal of domain ${domain}`);
|
||||
} catch (err) {
|
||||
logError('Core', `Error removing domain ${domain}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all records (claims and votes) from the network
|
||||
async function removeAllRecords() {
|
||||
const pass = state.dnsPass;
|
||||
if (!pass) {
|
||||
logWarn('Core', 'dnsPass not initialized, cannot remove all records');
|
||||
return { removed: 0, errors: 0 };
|
||||
}
|
||||
try {
|
||||
await pass.ready();
|
||||
const allEntries = await getAllEntries(pass, false); // Don't use cache for removal
|
||||
let removed = 0;
|
||||
let errors = 0;
|
||||
|
||||
// Remove all claims and votes
|
||||
for (const entry of allEntries) {
|
||||
if (entry.key.startsWith('claim:') || entry.key.startsWith('vote:')) {
|
||||
try {
|
||||
await pass.remove(entry.key);
|
||||
removed++;
|
||||
logDebug('Core', `Removed ${entry.key}`);
|
||||
} catch (err) {
|
||||
logError('Core', `Error removing ${entry.key}: ${err.message}`);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate cache after removal
|
||||
invalidateEntriesCache();
|
||||
// Clear all consensus cache
|
||||
consensusStateCache.clear();
|
||||
|
||||
logInfo('Core', `Removed ${removed} records from the network${errors > 0 ? ` (${errors} errors)` : ''}`);
|
||||
return { removed, errors };
|
||||
} catch (err) {
|
||||
logError('Core', `Error removing all records: ${err.message}`);
|
||||
return { removed: 0, errors: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
// Get consensus metrics
|
||||
function getConsensusMetrics() {
|
||||
return {
|
||||
...consensusMetrics,
|
||||
domainResolutions: Array.from(consensusMetrics.domainResolutions.entries()).map(([domain, metrics]) => ({
|
||||
domain,
|
||||
...metrics
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
// Cleanup redundant clients:domain:claimant entries
|
||||
// These entries are redundant since clients are stored in claim records
|
||||
async function cleanupRedundantClientEntries() {
|
||||
const pass = state.dnsPass;
|
||||
if (!pass) {
|
||||
logWarn('Core', 'dnsPass not initialized, cannot cleanup redundant client entries');
|
||||
return { removed: 0, errors: 0 };
|
||||
}
|
||||
|
||||
try {
|
||||
await pass.ready();
|
||||
const allEntries = await getAllEntries(pass, false);
|
||||
let removed = 0;
|
||||
let errors = 0;
|
||||
|
||||
for (const entry of allEntries) {
|
||||
if (entry.key.startsWith('clients:')) {
|
||||
try {
|
||||
await pass.remove(entry.key);
|
||||
removed++;
|
||||
logDebug('Core', `Removed redundant client entry: ${entry.key}`);
|
||||
} catch (err) {
|
||||
logError('Core', `Error removing redundant client entry ${entry.key}: ${err.message}`);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
invalidateEntriesCache();
|
||||
logInfo('Core', `Cleaned up ${removed} redundant client entries${errors > 0 ? ` (${errors} errors)` : ''}`);
|
||||
}
|
||||
|
||||
return { removed, errors };
|
||||
} catch (err) {
|
||||
logError('Core', `Error during cleanup of redundant client entries: ${err.message}`);
|
||||
return { removed: 0, errors: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getHashForDomain,
|
||||
getDomainSSLStatus,
|
||||
doAutoVotes,
|
||||
getAllEntries,
|
||||
autoVoteForDomain,
|
||||
removeDomain,
|
||||
removeAllRecords,
|
||||
invalidateEntriesCache,
|
||||
getConsensusState,
|
||||
getConsensusMetrics,
|
||||
parseClaimValue,
|
||||
getClaimClients,
|
||||
updateClaimClients,
|
||||
cleanupRedundantClientEntries
|
||||
};
|
||||
@@ -0,0 +1,216 @@
|
||||
const state = require('../infrastructure/state');
|
||||
const { removeDomain } = require('./core');
|
||||
const { removeVirtualInterface } = require('../networking/virtual_interfaces');
|
||||
const { freePort } = require('../maintenance/cleanup');
|
||||
const { logDebug, logError, logInfo, logWarn } = require('../infrastructure/logger');
|
||||
const dgram = require('dgram');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const holesailClientsFile = process.env.HOLESAIL_CLIENTS_FILE || './cache/holesail_clients.json';
|
||||
const selectorCacheFile = process.env.SELECTOR_CACHE_FILE || './cache/selector_cache.json';
|
||||
|
||||
/**
|
||||
* Atomically removes a domain and all its associated state
|
||||
* @param {string} domain - Domain to remove
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function atomicDomainCleanup(domain) {
|
||||
const cleanupErrors = [];
|
||||
|
||||
try {
|
||||
// 1. Remove from P2P network
|
||||
try {
|
||||
await removeDomain(domain);
|
||||
logDebug('DomainCleanup', `Removed domain ${domain} from P2P network`);
|
||||
} catch (err) {
|
||||
cleanupErrors.push(`P2P removal: ${err.message}`);
|
||||
logError('DomainCleanup', `Error removing domain from P2P: ${err.message}`);
|
||||
}
|
||||
|
||||
// 2. Clean up Holesail clients
|
||||
const clientIdsToRemove = [];
|
||||
for (const [id, opts] of state.holesailClientOpts) {
|
||||
if (opts.domain === domain) {
|
||||
clientIdsToRemove.push({ id, opts });
|
||||
}
|
||||
}
|
||||
|
||||
for (const { id, opts } of clientIdsToRemove) {
|
||||
try {
|
||||
// Kill child process
|
||||
const child = state.holesailClientChildren.get(id);
|
||||
if (child) {
|
||||
child.kill('SIGTERM');
|
||||
await new Promise(resolve => {
|
||||
child.on('exit', () => resolve());
|
||||
setTimeout(() => {
|
||||
child.kill('SIGKILL');
|
||||
logWarn('DomainCleanup', `Forced SIGKILL for Holesail client child ${id}`);
|
||||
resolve();
|
||||
}, 3000);
|
||||
});
|
||||
state.holesailClientChildren.delete(id);
|
||||
logInfo('DomainCleanup', `Closed Holesail client ${id} for ${domain}:${opts.port}`);
|
||||
}
|
||||
|
||||
// Clean up state
|
||||
state.holesailClientOpts.delete(id);
|
||||
state.holesailClientInfos.delete(id);
|
||||
|
||||
// Close Holesail connections
|
||||
const key = `${domain}:${opts.port}`;
|
||||
const holesail = state.holesails.get(key);
|
||||
if (holesail) {
|
||||
if (holesail instanceof dgram.Socket) {
|
||||
await new Promise(resolve => {
|
||||
holesail.close(() => {
|
||||
logInfo('DomainCleanup', `Closed UDP Holesail connection for ${key}`);
|
||||
resolve();
|
||||
});
|
||||
setTimeout(() => {
|
||||
logWarn('DomainCleanup', `Timeout closing UDP Holesail for ${key}, forcing closure`);
|
||||
holesail.close();
|
||||
resolve();
|
||||
}, 5000);
|
||||
});
|
||||
} else {
|
||||
await holesail.close();
|
||||
logInfo('DomainCleanup', `Closed TCP Holesail connection for ${key}`);
|
||||
}
|
||||
state.holesails.delete(key);
|
||||
if (state.holesailStartTimes) {
|
||||
state.holesailStartTimes.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Close TLS servers
|
||||
const tlsServer = state.tlsServers.get(key);
|
||||
if (tlsServer) {
|
||||
await new Promise(resolve => {
|
||||
tlsServer.close(resolve);
|
||||
setTimeout(() => {
|
||||
logWarn('DomainCleanup', `Timeout closing TLS server for ${key}, forcing closure`);
|
||||
tlsServer.destroy ? tlsServer.destroy() : tlsServer.close();
|
||||
resolve();
|
||||
}, 5000);
|
||||
});
|
||||
state.tlsServers.delete(key);
|
||||
logInfo('DomainCleanup', `Closed TLS server for ${key}`);
|
||||
}
|
||||
|
||||
// Close HTTP servers
|
||||
const httpServer = state.httpServers.get(key);
|
||||
if (httpServer) {
|
||||
await new Promise(resolve => {
|
||||
httpServer.close(resolve);
|
||||
setTimeout(() => {
|
||||
logWarn('DomainCleanup', `Timeout closing HTTP server for ${key}, forcing closure`);
|
||||
httpServer.destroy ? httpServer.destroy() : httpServer.close();
|
||||
resolve();
|
||||
}, 5000);
|
||||
});
|
||||
state.httpServers.delete(key);
|
||||
logInfo('DomainCleanup', `Closed HTTP server for ${key}`);
|
||||
}
|
||||
|
||||
// Free ports
|
||||
const ip = state.domainToIPMap.get(domain);
|
||||
if (ip && opts.port) {
|
||||
try {
|
||||
const freed = await freePort(ip, opts.port);
|
||||
if (!freed) {
|
||||
cleanupErrors.push(`Port ${opts.port} on ${ip} could not be freed`);
|
||||
logError('DomainCleanup', `Failed to ensure port ${opts.port} free on ${ip} for ${key}`);
|
||||
} else {
|
||||
logInfo('DomainCleanup', `Successfully ensured port ${opts.port} free on ${ip} for ${key}`);
|
||||
}
|
||||
} catch (err) {
|
||||
cleanupErrors.push(`Port freeing error: ${err.message}`);
|
||||
logError('DomainCleanup', `Error freeing port ${opts.port} on ${ip}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
cleanupErrors.push(`Holesail client cleanup for ${id}: ${err.message}`);
|
||||
logError('DomainCleanup', `Error cleaning up Holesail client ${id}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Save Holesail clients state
|
||||
try {
|
||||
const clients = Array.from(state.holesailClientOpts.entries()).map(([id, opts]) => ({ id, opts }));
|
||||
await fs.writeFile(holesailClientsFile, JSON.stringify({ clients }, null, 2));
|
||||
logDebug('DomainCleanup', 'Saved holesail_clients.json');
|
||||
} catch (err) {
|
||||
cleanupErrors.push(`Save Holesail clients: ${err.message}`);
|
||||
logError('DomainCleanup', `Error saving Holesail clients: ${err.message}`);
|
||||
}
|
||||
|
||||
// 3. Remove from domains.json file
|
||||
const domainsFile = process.env.DOMAINS_FILE || './cache/domains.json';
|
||||
try {
|
||||
if (await fs.access(domainsFile).then(() => true).catch(() => false)) {
|
||||
let domains = JSON.parse(await fs.readFile(domainsFile, 'utf8'));
|
||||
domains = domains.filter(d => d.domain !== domain);
|
||||
await fs.writeFile(domainsFile, JSON.stringify(domains, null, 2));
|
||||
logDebug('DomainCleanup', `Removed ${domain} from ${domainsFile}`);
|
||||
}
|
||||
} catch (err) {
|
||||
cleanupErrors.push(`File update: ${err.message}`);
|
||||
logError('DomainCleanup', `Error updating domains file: ${err.message}`);
|
||||
}
|
||||
|
||||
// 4. Remove virtual interface
|
||||
if (state.domainToIPMap.has(domain)) {
|
||||
try {
|
||||
const ip = state.domainToIPMap.get(domain);
|
||||
await removeVirtualInterface(ip);
|
||||
state.domainToIPMap.delete(domain);
|
||||
logDebug('DomainCleanup', `Removed IP ${ip} for ${domain}`);
|
||||
} catch (err) {
|
||||
cleanupErrors.push(`Interface removal: ${err.message}`);
|
||||
logError('DomainCleanup', `Error removing interface: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Remove version preferences
|
||||
if (state.versionPreferences.has(domain)) {
|
||||
state.versionPreferences.delete(domain);
|
||||
try {
|
||||
const data = Object.fromEntries(state.versionPreferences);
|
||||
await fs.writeFile(selectorCacheFile, JSON.stringify(data, null, 2));
|
||||
logDebug('DomainCleanup', `Removed version preference for ${domain} and saved selector_cache.json`);
|
||||
} catch (err) {
|
||||
cleanupErrors.push(`Selector cache save: ${err.message}`);
|
||||
logError('DomainCleanup', `Error saving selector cache: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Remove from domains with both
|
||||
if (state.domainsWithBoth) {
|
||||
state.domainsWithBoth.delete(domain);
|
||||
}
|
||||
|
||||
// 7. Remove from public IP cache
|
||||
if (state.publicIpForDomain && state.publicIpForDomain[domain]) {
|
||||
delete state.publicIpForDomain[domain];
|
||||
}
|
||||
|
||||
// 8. Remove from CA cache
|
||||
if (state.caForDomain) {
|
||||
state.caForDomain.delete(domain);
|
||||
}
|
||||
|
||||
if (cleanupErrors.length > 0) {
|
||||
logWarn('DomainCleanup', `Completed cleanup for ${domain} with ${cleanupErrors.length} errors: ${cleanupErrors.join('; ')}`);
|
||||
} else {
|
||||
logInfo('DomainCleanup', `Successfully completed atomic cleanup for ${domain}`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logError('DomainCleanup', `Fatal error during atomic cleanup for ${domain}: ${err.message}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { atomicDomainCleanup };
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
const state = require('../infrastructure/state');
|
||||
const { logDebug, logInfo } = require('../infrastructure/logger');
|
||||
const { getAllEntries, autoVoteForDomain, invalidateEntriesCache } = require('./core');
|
||||
const { trackDomainEvent } = require('../maintenance/metrics');
|
||||
const { getPersistentPublicKey } = require('../infrastructure/utils');
|
||||
// Initialize reserved IPs set if not already present
|
||||
if (!state.reservedIPs) {
|
||||
state.reservedIPs = new Set(['127.0.0.1']);
|
||||
}
|
||||
async function addDomain(domain, hash, ssl = false) {
|
||||
const pass = state.dnsPass;
|
||||
await pass.ready();
|
||||
const claimant = getPersistentPublicKey();
|
||||
if (!claimant) {
|
||||
throw new Error('Cannot add domain: persistent public key not available');
|
||||
}
|
||||
const claimKey = `claim:${domain}:${claimant}`;
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Ensure ssl is a boolean
|
||||
const sslValue = Boolean(ssl);
|
||||
|
||||
// Store claim with timestamp and SSL flag in JSON format for new claims
|
||||
// Format: { hash: "...", timestamp: 1234567890, ssl: true/false }
|
||||
const claimValue = JSON.stringify({ hash, timestamp, ssl: sslValue });
|
||||
|
||||
logDebug('Domains', `Adding domain ${domain} with hash ${hash} and SSL=${sslValue} as claim ${claimKey} at timestamp ${timestamp}`);
|
||||
await pass.add(claimKey, claimValue);
|
||||
|
||||
const verified = await pass.get(claimKey);
|
||||
logDebug('Domains', `Verified add for ${claimKey}: ${verified ? verified.toString('utf8') : 'null'}`);
|
||||
logInfo('Domains', `Domain ${domain} added as claim ${claimKey} with timestamp ${timestamp}`);
|
||||
invalidateEntriesCache(); // Invalidate cache after adding
|
||||
trackDomainEvent('add', domain);
|
||||
|
||||
// Immediately auto-vote after adding (if immediate updates enabled)
|
||||
const { validateConfig } = require('../infrastructure/config');
|
||||
let config;
|
||||
try {
|
||||
config = validateConfig();
|
||||
} catch (e) {
|
||||
config = { CONSENSUS_IMMEDIATE_UPDATE: true };
|
||||
}
|
||||
|
||||
if (config.CONSENSUS_IMMEDIATE_UPDATE !== false) {
|
||||
const allEntries = await getAllEntries();
|
||||
await autoVoteForDomain(domain, allEntries);
|
||||
}
|
||||
}
|
||||
function addInternalDomain(domain, ip = '127.0.0.1') {
|
||||
if (!state.reservedIPs.has(ip)) {
|
||||
state.reservedIPs.add(ip);
|
||||
}
|
||||
state.domainToIPMap[domain] = ip;
|
||||
logInfo('Domains', `Added internal domain ${domain} with IP ${ip}`);
|
||||
}
|
||||
module.exports = { addDomain, addInternalDomain };
|
||||
Reference in New Issue
Block a user