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:
+29
-10
@@ -566,6 +566,14 @@ async function doAutoVotes() {
|
||||
for (const domain of domains) {
|
||||
await autoVoteForDomain(domain, allEntries);
|
||||
}
|
||||
|
||||
// Cleanup hash preferences after consensus is updated
|
||||
try {
|
||||
const { cleanupHashPreferences } = require('../admin/cache');
|
||||
await cleanupHashPreferences();
|
||||
} catch (err) {
|
||||
logWarn('Core', `Failed to cleanup hash preferences after auto-votes: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function voteForDomain(domain, claimant, allEntries = null) {
|
||||
@@ -688,6 +696,7 @@ async function removeDomain(domain) {
|
||||
}
|
||||
|
||||
// Remove only the user's own claim and all votes cast by them
|
||||
// IMPORTANT: This function must NEVER remove other users' claims or votes
|
||||
async function removeOwnClaimAndVotes(domain, localWriter) {
|
||||
const pass = state.dnsPass;
|
||||
if (!pass) {
|
||||
@@ -700,27 +709,37 @@ async function removeOwnClaimAndVotes(domain, localWriter) {
|
||||
}
|
||||
try {
|
||||
const allEntries = await getAllEntries(pass, false); // Don't use cache for removal
|
||||
let removed = 0;
|
||||
let removedClaims = 0;
|
||||
let removedVotes = 0;
|
||||
|
||||
for (const entry of allEntries) {
|
||||
// Remove the user's claim record
|
||||
// Remove ONLY the user's own claim record
|
||||
// Format: claim:domain:claimant
|
||||
if (entry.key === `claim:${domain}:${localWriter}`) {
|
||||
await pass.remove(entry.key);
|
||||
logDebug('Core', `Removed claim ${entry.key}`);
|
||||
removed++;
|
||||
logInfo('Core', `Removed own claim: ${entry.key}`);
|
||||
removedClaims++;
|
||||
}
|
||||
// Remove all votes cast BY the user (votes where localWriter is the voter)
|
||||
else if (entry.key.startsWith(`vote:${domain}:`) && entry.key.endsWith(`:${localWriter}`)) {
|
||||
await pass.remove(entry.key);
|
||||
logDebug('Core', `Removed vote ${entry.key} cast by user`);
|
||||
removed++;
|
||||
// Remove ONLY votes cast BY the user (votes where localWriter is the voter)
|
||||
// Format: vote:domain:claimant:voter
|
||||
// We check that it ends with :localWriter to ensure we only remove votes cast BY this user
|
||||
else if (entry.key.startsWith(`vote:${domain}:`)) {
|
||||
const parts = entry.key.split(':');
|
||||
if (parts.length === 4 && parts[3] === localWriter) {
|
||||
// This is a vote cast BY the user (voter is localWriter)
|
||||
await pass.remove(entry.key);
|
||||
logInfo('Core', `Removed vote cast by user: ${entry.key}`);
|
||||
removedVotes++;
|
||||
}
|
||||
// Explicitly skip votes cast FOR the user's claim (where parts[2] === localWriter but parts[3] !== localWriter)
|
||||
// These are votes cast BY OTHER USERS for the user's claim, and must NOT be removed
|
||||
}
|
||||
}
|
||||
|
||||
invalidateEntriesCache(); // Invalidate cache after removal
|
||||
consensusStateCache.delete(domain); // Remove from consensus cache
|
||||
trackDomainEvent('remove_claim', domain);
|
||||
logInfo('Core', `Removed own claim and ${removed-1} vote(s) cast by user for domain ${domain}`);
|
||||
logInfo('Core', `Removed ${removedClaims} own claim(s) and ${removedVotes} vote(s) cast by user for domain ${domain}`);
|
||||
} catch (err) {
|
||||
logError('Core', `Error removing own claim for domain ${domain}: ${err.message}`);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const state = require('../infrastructure/state');
|
||||
const { logDebug, logInfo } = require('../infrastructure/logger');
|
||||
const { getAllEntries, autoVoteForDomain, invalidateEntriesCache } = require('./core');
|
||||
const { getAllEntries, autoVoteForDomain, invalidateEntriesCache, parseClaimValue } = require('./core');
|
||||
const { trackDomainEvent } = require('../maintenance/metrics');
|
||||
const { getPersistentPublicKey } = require('../infrastructure/utils');
|
||||
// Initialize reserved IPs set if not already present
|
||||
@@ -20,17 +20,48 @@ async function addDomain(domain, hash, ssl = false) {
|
||||
// Ensure ssl is a boolean
|
||||
const sslValue = Boolean(ssl);
|
||||
|
||||
// Check if user already has a claim for this domain
|
||||
const allEntries = await getAllEntries(pass, false); // Use fresh data, bypass cache
|
||||
let existingClaim = null;
|
||||
for (const entry of allEntries) {
|
||||
if (entry.key === claimKey) {
|
||||
existingClaim = entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (existingClaim) {
|
||||
// User already has a claim - update it instead of creating duplicate
|
||||
const parsed = parseClaimValue(existingClaim.value);
|
||||
|
||||
// Check if anything actually changed
|
||||
if (parsed.hash === hash && parsed.ssl === sslValue) {
|
||||
logDebug('Domains', `Claim for ${domain} already exists with same hash and SSL flag, skipping update`);
|
||||
return; // No changes needed
|
||||
}
|
||||
|
||||
logInfo('Domains', `Updating existing claim for ${domain} (hash: ${parsed.hash} -> ${hash}, SSL: ${parsed.ssl} -> ${sslValue})`);
|
||||
|
||||
// Remove old claim and add updated one
|
||||
await pass.remove(claimKey);
|
||||
await pass.add(claimKey, claimValue);
|
||||
|
||||
logInfo('Domains', `Updated claim ${claimKey} with new hash ${hash} and SSL=${sslValue}`);
|
||||
} else {
|
||||
// New claim - add it
|
||||
logDebug('Domains', `Adding new 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}`);
|
||||
}
|
||||
|
||||
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
|
||||
invalidateEntriesCache(); // Invalidate cache after adding/updating
|
||||
trackDomainEvent('add', domain);
|
||||
|
||||
// Immediately auto-vote after adding (if immediate updates enabled)
|
||||
|
||||
Reference in New Issue
Block a user