ix: Prevent claim overwriting and improve domain removal safety

Fix critical bugs in domain claim management to ensure data integrity:

- Domain addition: Check for existing claims before adding to prevent duplicates
  - Update user's own claim if it exists instead of creating duplicates
  - Skip update if hash and SSL flag unchanged (idempotent)
  - Never touch other users' claims when updating own claim

- Domain removal: Add strict validation to prevent removing other users' claims
  - Only perform full cleanup (atomicDomainCleanup) when user is confirmed resolved claimant
  - Require consensus status 'resolved' AND resolvedClaimant matches localWriter
  - Improved logging to show which removal path is taken
  - Enhanced removeOwnClaimAndVotes with explicit vote key parsing

- Hash preferences cleanup: Fix module path resolution error
  - Correct require path from admin-backend/cache.js (../../core/core)
  - Add lazy require pattern with error handling to prevent crashes
  - Ensure cleanup runs after consensus updates and domain removals
This commit is contained in:
Raven Scott
2025-12-26 16:59:56 -05:00
parent 01acfb766c
commit 0d23e296be
8 changed files with 326 additions and 28 deletions
+83 -2
View File
@@ -1,6 +1,6 @@
const fs = require('fs').promises;
const state = require('../../infrastructure/state');
const { logDebug, logError, logInfo } = require('../../infrastructure/logger');
const { logDebug, logError, logInfo, logWarn } = require('../../infrastructure/logger');
const selectorCacheFile = process.env.SELECTOR_CACHE_FILE || './cache/selector_cache.json';
const localDnsFile = process.env.LOCAL_DNS_FILE || 'cache/local_dns.json';
@@ -196,6 +196,86 @@ async function savePeerHistory() {
}
}
async function cleanupHashPreferences() {
try {
// Lazy require to avoid circular dependencies
let getConsensusState, getLocalClaimHash, getAllEntries, getPersistentPublicKey;
try {
const coreModule = require('../../core/core');
getConsensusState = coreModule.getConsensusState;
getLocalClaimHash = coreModule.getLocalClaimHash;
getAllEntries = coreModule.getAllEntries;
const utilsModule = require('../../infrastructure/utils');
getPersistentPublicKey = utilsModule.getPersistentPublicKey;
} catch (requireErr) {
logError('Admin', `Failed to require core modules for hash preferences cleanup: ${requireErr.message}`);
return;
}
const localWriter = getPersistentPublicKey();
if (!localWriter) {
logDebug('Admin', 'No local writer available for hash preferences cleanup');
return;
}
const allEntries = await getAllEntries(state.dnsPass, false); // Use fresh data, bypass cache
const domainClaimants = new Map();
// Collect all claimants for each domain
for (const entry of allEntries) {
if (entry.key.startsWith('claim:')) {
const parts = entry.key.split(':');
if (parts.length === 3) {
const domain = parts[1];
const claimant = parts[2];
if (!domainClaimants.has(domain)) domainClaimants.set(domain, new Set());
domainClaimants.get(domain).add(claimant);
}
}
}
// Check each hash preference to see if it's still valid
const domainsToRemove = [];
for (const [domain, preference] of state.hashPreferences) {
// Check if user still has a local claim for this domain
if (!domainClaimants.has(domain) || !domainClaimants.get(domain).has(localWriter)) {
logInfo('Admin', `Removing hash preference for ${domain}: user no longer has local claim`);
domainsToRemove.push(domain);
continue;
}
// Check if consensus is still resolved to a different claimant
const consensusState = await getConsensusState(domain);
if (consensusState.status !== 'resolved' || consensusState.resolvedClaimant === localWriter) {
logInfo('Admin', `Removing hash preference for ${domain}: conflict no longer exists (status=${consensusState.status}, resolvedClaimant=${consensusState.resolvedClaimant})`);
domainsToRemove.push(domain);
continue;
}
// Verify local hash still exists
const localHash = await getLocalClaimHash(domain, localWriter);
if (!localHash) {
logInfo('Admin', `Removing hash preference for ${domain}: local claim hash not found`);
domainsToRemove.push(domain);
continue;
}
}
// Remove invalid preferences
if (domainsToRemove.length > 0) {
for (const domain of domainsToRemove) {
state.hashPreferences.delete(domain);
}
await saveSelectorCache();
logInfo('Admin', `Cleaned up ${domainsToRemove.length} invalid hash preference(s) from selector_cache.json`);
} else {
logDebug('Admin', 'All hash preferences are valid');
}
} catch (err) {
logError('Admin', `Failed to cleanup hash preferences: ${err.message}`);
}
}
// Initialize on load
loadSelectorCache();
loadLocalDnsRecords();
@@ -209,6 +289,7 @@ module.exports = {
loadPeerMetrics,
savePeerMetrics,
loadPeerHistory,
savePeerHistory
savePeerHistory,
cleanupHashPreferences
};
+45 -2
View File
@@ -20,7 +20,8 @@ async function handleDomainsRoutes(req, res) {
if (method === 'GET' && urlPath === '/api/resolved-domains') {
try {
const allEntries = await getAllEntries();
// Use fresh data (bypass cache) to ensure latest consensus state
const allEntries = await getAllEntries(state.dnsPass, false);
const domainClaimants = new Map();
for (const entry of allEntries) {
if (entry.key.startsWith('claim:')) {
@@ -101,7 +102,23 @@ async function handleDomainsRoutes(req, res) {
// Extract SSL flag - handle both boolean true and string "true"
const ssl = data.ssl === true || data.ssl === 'true' || data.ssl === 1;
await addDomain(validation.domain, validation.hash, ssl);
// Invalidate cache to ensure fresh consensus check
invalidateEntriesCache();
// Recalculate consensus for the new domain
await doAutoVotes();
// Small delay to allow consensus to settle
await new Promise(resolve => setTimeout(resolve, 200));
// Trigger consensus recalculation request to notify peers
if (state.sendConsensusRequest) {
setTimeout(() => {
state.sendConsensusRequest(validation.domain);
}, 500);
}
let domains = [];
if (await fs.access(domainsFile).then(() => true).catch(() => false)) {
const parsed = JSON.parse(await fs.readFile(domainsFile, 'utf8'));
@@ -123,7 +140,11 @@ async function handleDomainsRoutes(req, res) {
// Note: IP assignment now happens automatically after consensus resolution
// See assignAllIPs() function for automatic IP assignment logic
logDebug('Admin', `Domain ${validation.domain} added - IP will be assigned automatically after consensus resolution`);
// Broadcast multiple update types to ensure frontend refreshes
broadcast({ type: 'update-database' });
broadcast({ type: 'update-local-dns' }); // Also update local DNS tab to show conflicts
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('OK');
} catch (err) {
@@ -171,18 +192,40 @@ async function handleDomainsRoutes(req, res) {
// Check if peer is the resolved claimant
const consensusState = await getConsensusState(domain);
const isResolvedClaimant = consensusState.resolvedClaimant === localWriter;
// Only do full cleanup if:
// 1. Consensus is resolved
// 2. User is the resolved claimant
// 3. Resolved claimant is not null/undefined
const isResolvedClaimant = consensusState.status === 'resolved' &&
consensusState.resolvedClaimant &&
consensusState.resolvedClaimant === localWriter;
if (isResolvedClaimant) {
// Full removal: user is the resolved claimant
logInfo('Admin', `User is resolved claimant for ${domain}, performing full cleanup`);
await atomicDomainCleanup(domain);
} else {
// Partial removal: user has claim but isn't the resolved claimant
// This includes cases where:
// - Consensus is not resolved
// - User is not the resolved claimant (conflict scenario)
// - Resolved claimant is null/undefined
logInfo('Admin', `User is NOT resolved claimant for ${domain} (status=${consensusState.status}, resolvedClaimant=${consensusState.resolvedClaimant}), removing only own claim and votes`);
await removeOwnClaimAndVotes(domain, localWriter);
}
if (state.sendRemovalRequest) {
state.sendRemovalRequest(domain);
}
// Cleanup hash preferences if domain was removed
try {
const { cleanupHashPreferences } = require('../cache');
await cleanupHashPreferences();
} catch (err) {
logWarn('Admin', `Failed to cleanup hash preferences after domain removal: ${err.message}`);
}
trackRequest('/api/remove-domain', true);
broadcast({ type: 'update-database' });
broadcast({ type: 'update-holesail-clients' });