This commit is contained in:
Raven Scott
2025-12-17 22:32:07 -05:00
parent ad4784d05d
commit eb5675adb2
6 changed files with 106 additions and 61 deletions
@@ -94,6 +94,11 @@ async function handleConsensusRoutes(req, res) {
const allEntries = await getAllEntries(); const allEntries = await getAllEntries();
await autoVoteForDomain(domain, allEntries); await autoVoteForDomain(domain, allEntries);
// Notify other peers to recalculate consensus for this domain
if (state.sendConsensusRequest) {
state.sendConsensusRequest(domain);
}
// Broadcast update // Broadcast update
broadcast({ type: 'update-database' }); broadcast({ type: 'update-database' });
broadcast({ type: 'update-stats' }); broadcast({ type: 'update-stats' });
+73 -61
View File
@@ -331,9 +331,12 @@ async function getConsensusState(domain) {
if (Object.keys(claims).length === 0) { if (Object.keys(claims).length === 0) {
status = 'no_claims'; status = 'no_claims';
} else if (!quorumMet && totalVotes === 0) { } else if (!quorumMet) {
// No votes but have claims - check if single local claim // IMPROVED: Allow resolution for a single local claim even if we've already auto-voted
if (Object.keys(claims).length === 1 && claims.hasOwnProperty(localWriter)) { const isSingleLocalClaim = Object.keys(claims).length === 1 && claims.hasOwnProperty(localWriter);
const onlyWeVoted = totalVotes === 0 || (totalVotes === 1 && voters.has(localWriter));
if (isSingleLocalClaim && (totalVotes === 0 || onlyWeVoted)) {
status = 'resolved'; status = 'resolved';
resolvedClaimant = localWriter; resolvedClaimant = localWriter;
hash = claims[localWriter]; hash = claims[localWriter];
@@ -350,17 +353,10 @@ async function getConsensusState(domain) {
countedQuorumFailures.add(domain); countedQuorumFailures.add(domain);
consensusMetrics.quorumFailures++; consensusMetrics.quorumFailures++;
} }
} if (trackConsensusEvent && totalVotes > 0) {
} 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 }); trackConsensusEvent('quorum_failure', { domain, totalVotes, minVotes, activePeers });
} }
}
} else { } else {
const maxVotes = Math.max(...Object.values(voteCounts)); const maxVotes = Math.max(...Object.values(voteCounts));
const candidates = Object.keys(voteCounts).filter(claimant => voteCounts[claimant] === maxVotes); const candidates = Object.keys(voteCounts).filter(claimant => voteCounts[claimant] === maxVotes);
@@ -498,29 +494,29 @@ async function doAutoVotes() {
} }
} }
async function autoVoteForDomain(domain, allEntries) { async function voteForDomain(domain, claimant, allEntries = null) {
const pass = state.dnsPass; const pass = state.dnsPass;
if (!pass) return; if (!pass) return false;
logDebug('Core', `Auto-voting for domain: ${domain}`);
const config = getConsensusConfig();
const claims = {};
const claimTimestamps = {};
const myVotes = new Map(); // claimant -> voteKey
const localWriter = getPersistentPublicKey(); const localWriter = getPersistentPublicKey();
if (!localWriter) { if (!localWriter) {
logWarn('Core', 'Cannot auto-vote: persistent public key not available'); logWarn('Core', 'Cannot vote: persistent public key not available');
return; return false;
} }
// Collect claims and timestamps if (!allEntries) {
allEntries = await getAllEntries(pass, true);
}
const claims = {};
const myVotes = new Map(); // claimant -> voteKey
// Collect claims and our current votes
for (const entry of allEntries) { for (const entry of allEntries) {
if (entry.key.startsWith(`claim:${domain}:`)) { if (entry.key.startsWith(`claim:${domain}:`)) {
const claimant = entry.key.slice(`claim:${domain}:`.length); const entryClaimant = entry.key.slice(`claim:${domain}:`.length);
const parsed = parseClaimValue(entry.value); const parsed = parseClaimValue(entry.value);
claims[claimant] = parsed.hash; claims[entryClaimant] = 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}:`)) { if (entry.key.startsWith(`vote:${domain}:`)) {
const parts = entry.key.split(':'); const parts = entry.key.split(':');
@@ -530,52 +526,67 @@ async function autoVoteForDomain(domain, allEntries) {
} }
} }
const claimsLength = Object.keys(claims).length; if (!claims.hasOwnProperty(claimant)) {
logDebug('Core', `Found ${claimsLength} claims for ${domain}`); logWarn('Core', `Cannot vote for claimant ${claimant}: no claim found for domain ${domain}`);
if (claimsLength === 0) return; return false;
// 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 // Validate vote
if (!validateVote(intendedClaimant, claims)) { if (!validateVote(claimant, claims)) {
logWarn('Core', `Cannot vote for invalid claimant: ${intendedClaimant}`); return false;
return;
} }
// Remove votes for other claimants // Remove existing votes for other claimants
for (const [votedClaimant, voteKey] of myVotes) { for (const [votedClaimant, voteKey] of myVotes) {
if (votedClaimant !== intendedClaimant) { if (votedClaimant !== claimant) {
await pass.remove(voteKey); await pass.remove(voteKey);
logInfo('Core', `Removed vote for ${domain} claimant ${votedClaimant}`); logInfo('Core', `Removed vote for ${domain} claimant ${votedClaimant}`);
} }
} }
// Add vote if not already voting for intended claimant // Add new vote if not already voting for this claimant
if (!myVotes.has(intendedClaimant)) { if (!myVotes.has(claimant)) {
const voteKey = `vote:${domain}:${intendedClaimant}:${localWriter}`; const voteKey = `vote:${domain}:${claimant}:${localWriter}`;
await pass.add(voteKey, claims[intendedClaimant]); await pass.add(voteKey, claims[claimant]);
logInfo('Core', `Auto-voted for ${domain} claimant ${intendedClaimant} (reason: ${localHasClaim ? 'own_claim' : claimsLength === 1 ? 'single_claim' : 'tie_breaker'})`); logInfo('Core', `Voted for ${domain} claimant ${claimant}`);
} else { invalidateEntriesCache();
logDebug('Core', `Already voted for ${domain} claimant ${intendedClaimant}`);
} }
return true;
}
async function autoVoteForDomain(domain, allEntries) {
const localWriter = getPersistentPublicKey();
if (!localWriter) return;
const claims = {};
const claimTimestamps = {};
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;
}
}
const claimsLength = Object.keys(claims).length;
if (claimsLength === 0) return;
const localHasClaim = claims.hasOwnProperty(localWriter);
let intendedClaimant;
if (localHasClaim) {
intendedClaimant = localWriter;
} else if (claimsLength === 1) {
intendedClaimant = Object.keys(claims)[0];
} else {
const candidates = Object.keys(claims);
intendedClaimant = applyTieBreaker(candidates, claims, claimTimestamps, localWriter);
}
await voteForDomain(domain, intendedClaimant, allEntries);
} }
async function removeDomain(domain) { async function removeDomain(domain) {
@@ -699,6 +710,7 @@ module.exports = {
doAutoVotes, doAutoVotes,
getAllEntries, getAllEntries,
autoVoteForDomain, autoVoteForDomain,
voteForDomain,
removeDomain, removeDomain,
removeAllRecords, removeAllRecords,
invalidateEntriesCache, invalidateEntriesCache,
+5
View File
@@ -45,6 +45,11 @@ async function addDomain(domain, hash, ssl = false) {
if (config.CONSENSUS_IMMEDIATE_UPDATE !== false) { if (config.CONSENSUS_IMMEDIATE_UPDATE !== false) {
const allEntries = await getAllEntries(); const allEntries = await getAllEntries();
await autoVoteForDomain(domain, allEntries); await autoVoteForDomain(domain, allEntries);
// Notify other peers to recalculate consensus for this domain
if (state.sendConsensusRequest) {
state.sendConsensusRequest(domain);
}
} }
} }
function addInternalDomain(domain, ip = '127.0.0.1') { function addInternalDomain(domain, ip = '127.0.0.1') {
+1
View File
@@ -70,6 +70,7 @@ module.exports = {
localDnsRecords: [], localDnsRecords: [],
isMaster: process.argv.includes('--master'), isMaster: process.argv.includes('--master'),
sendRemovalRequest: null, sendRemovalRequest: null,
sendConsensusRequest: null,
domainsWithBoth: new Set(), domainsWithBoth: new Set(),
publicIpForDomain: {}, publicIpForDomain: {},
caForDomain: new Map(), caForDomain: new Map(),
+9 -3
View File
@@ -21,6 +21,7 @@ const {
removeDomain, removeDomain,
doAutoVotes, doAutoVotes,
autoVoteForDomain, autoVoteForDomain,
voteForDomain,
invalidateEntriesCache invalidateEntriesCache
} = require('../core/core'); } = require('../core/core');
const { atomicDomainCleanup } = require('../core/domain_cleanup'); const { atomicDomainCleanup } = require('../core/domain_cleanup');
@@ -685,9 +686,14 @@ const sdk = {
if (!state.dnsPass) { if (!state.dnsPass) {
throw new Error('DNS service not initialized'); throw new Error('DNS service not initialized');
} }
const allEntries = await getAllEntries();
await autoVoteForDomain(domain, allEntries); const success = await voteForDomain(domain, claimant);
// Check if vote was successful if (!success) return false;
// Invalidate cache to ensure subsequent state check is fresh
invalidateEntriesCache();
// Check if vote resulted in expected resolution
const consensusState = await getConsensusState(domain); const consensusState = await getConsensusState(domain);
return consensusState.resolvedClaimant === claimant; return consensusState.resolvedClaimant === claimant;
}, },
+16
View File
@@ -804,6 +804,14 @@ async function main() {
const domain = message.slice(14); const domain = message.slice(14);
logDebug('Swarm', `Processing removal request for domain: ${domain}`); logDebug('Swarm', `Processing removal request for domain: ${domain}`);
await removeDomain(domain); await removeDomain(domain);
} else if (message.startsWith('recalculate_consensus:')) {
const domain = message.slice(22);
logDebug('Swarm', `Processing consensus recalculation request for domain: ${domain}`);
// Invalidate cache first to ensure we get the latest replicated data
const { invalidateEntriesCache, getAllEntries, autoVoteForDomain } = require('./includes/core/core');
invalidateEntriesCache();
const allEntries = await getAllEntries();
await autoVoteForDomain(domain, allEntries);
} else { } else {
logWarn('Swarm', `Unknown message from ${peerId}: ${message}`); logWarn('Swarm', `Unknown message from ${peerId}: ${message}`);
} }
@@ -2031,6 +2039,14 @@ async function main() {
} }
state.sendRemovalRequest = sendRemovalRequest; state.sendRemovalRequest = sendRemovalRequest;
// Function to send consensus recalculation request to all peers
function sendConsensusRequest(domain) {
// Use channel-manager broadcast to send to all peers
const count = channelManager.broadcastToPeers(CORE_DOMAIN, 'request', `recalculate_consensus:${domain}`);
logDebug('Main', `Sent consensus recalculation request for ${domain} to ${count} peers`);
}
state.sendConsensusRequest = sendConsensusRequest;
// Function to auto-subscribe to services on bootup // Function to auto-subscribe to services on bootup
async function autoSubscribeToServices() { async function autoSubscribeToServices() {
const disableAutoSubscription = process.env.DISABLE_AUTO_SUBSCRIPTION === 'true'; const disableAutoSubscription = process.env.DISABLE_AUTO_SUBSCRIPTION === 'true';