EXPERIMENTAL: replace KV-scan consensus with Autobase sidecar engine

Cut over domain resolution to an Autobase apply-based sidecar fed by
claim/vote dual-writes from dnsPass. Remove the legacy full-KV scan
getConsensusState path and wire all reads through consensus-view.

- Add consensus-resolver, consensus-events, consensus-apply,
  consensus-autobase, and consensus-view modules
- Dual-append consensus events on claim/vote dnsPassAdd/Remove
- Bootstrap sidecar from existing KV entries; persist
  consensusAutobaseKey in network manifest
- Expose sidecar health via GET /api/consensus/status and metrics
- Add consensus-resolver and consensus-apply unit tests
- Expand RFC 0001 to Implemented; update CONSENSUS.md

Rollback: deploy prior release; KV data unchanged, sidecar rebuilds on
next startup.
This commit is contained in:
Raven Scott
2026-05-30 23:49:48 -04:00
parent 3a8daefc30
commit d230fb3360
19 changed files with 1867 additions and 890 deletions
@@ -10,6 +10,25 @@ async function handleConsensusRoutes(req, res) {
const method = req.method;
const url = new URL(req.url, `https://${req.headers.host}`);
// GET /api/consensus/status - Sidecar health
if (method === 'GET' && urlPath === '/api/consensus/status') {
try {
trackRequest('/api/consensus/status', true);
const { getConsensusStatus } = require('../../../core/consensus-autobase');
const status = getConsensusStatus();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(status));
return true;
} catch (err) {
logError('Consensus', `Failed to get consensus status: ${err.message}`);
trackRequest('/api/consensus/status', false);
const errorResponse = createErrorResponse(err, 500);
res.writeHead(errorResponse.statusCode, errorResponse.headers);
res.end(errorResponse.body);
return true;
}
}
// GET /api/consensus/metrics - Get consensus metrics
// Check this BEFORE the domain route to avoid matching "metrics" as a domain
if (method === 'GET' && urlPath === '/api/consensus/metrics') {
+102
View File
@@ -0,0 +1,102 @@
/**
* Autobase apply handler and in-memory consensus view state.
*/
const { decodeEvent, EVENT_TYPES } = require('./consensus-events');
function createEmptyViewState() {
return {
domains: new Map(),
eventCount: 0,
lastApplyAt: 0
};
}
function getOrCreateDomain(view, domain) {
if (!view.domains.has(domain)) {
view.domains.set(domain, {
claims: new Map(),
voterVotes: new Map()
});
}
return view.domains.get(domain);
}
function applyEventToView(view, event) {
switch (event.type) {
case EVENT_TYPES.CLAIM_UPSERT: {
const domainState = getOrCreateDomain(view, event.domain);
domainState.claims.set(event.claimant, {
hash: event.hash,
timestamp: event.timestamp,
ssl: event.ssl === true,
clients: event.clients || []
});
break;
}
case EVENT_TYPES.CLAIM_REMOVE: {
const domainState = view.domains.get(event.domain);
if (!domainState) break;
domainState.claims.delete(event.claimant);
for (const [voter, claimant] of domainState.voterVotes) {
if (claimant === event.claimant) {
domainState.voterVotes.delete(voter);
}
}
if (domainState.claims.size === 0 && domainState.voterVotes.size === 0) {
view.domains.delete(event.domain);
}
break;
}
case EVENT_TYPES.VOTE_UPSERT: {
const domainState = getOrCreateDomain(view, event.domain);
domainState.voterVotes.set(event.voter, event.claimant);
break;
}
case EVENT_TYPES.VOTE_REMOVE: {
const domainState = view.domains.get(event.domain);
if (!domainState) break;
domainState.voterVotes.delete(event.voter);
if (domainState.claims.size === 0 && domainState.voterVotes.size === 0) {
view.domains.delete(event.domain);
}
break;
}
default:
break;
}
}
function openConsensusView() {
return createEmptyViewState();
}
async function applyConsensusNodes(nodes, view, host) {
for (const node of nodes) {
let event;
try {
event = decodeEvent(node.value);
} catch (err) {
continue;
}
if (event.type === 'add_writer' && event.key && host && typeof host.addWriter === 'function') {
await host.addWriter(Buffer.from(event.key, 'hex'), { indexer: true });
view.eventCount++;
view.lastApplyAt = Date.now();
continue;
}
applyEventToView(view, event);
view.eventCount++;
view.lastApplyAt = Date.now();
}
}
module.exports = {
createEmptyViewState,
getOrCreateDomain,
applyEventToView,
openConsensusView,
applyConsensusNodes
};
+271
View File
@@ -0,0 +1,271 @@
/**
* Consensus Autobase sidecar lifecycle.
*/
const Autobase = require('autobase');
const state = require('../infrastructure/state');
const { logDebug, logInfo, logWarn, logError } = require('../infrastructure/logger');
const { openConsensusView, applyConsensusNodes } = require('./consensus-apply');
const { encodeEvent, entriesToBootstrapEvents } = require('./consensus-events');
const { listAllEntries, waitForAutobaseIdle } = require('./dns-pass-queue');
let consensusBase = null;
let bootstrapComplete = false;
let initPromise = null;
function getConsensusBase() {
return consensusBase;
}
function isBootstrapComplete() {
return bootstrapComplete;
}
function createConsensusAutobase(store, bootstrapKey) {
const opts = {
open() {
return openConsensusView();
},
async apply(nodes, view, host) {
await applyConsensusNodes(nodes, view, host);
}
};
if (bootstrapKey) {
return new Autobase(store, bootstrapKey, opts);
}
return new Autobase(store, opts);
}
async function appendConsensusEvent(event) {
if (!consensusBase || consensusBase.closed) {
throw new Error('consensus autobase not open');
}
await waitForAutobaseIdle({ base: consensusBase });
await consensusBase.append(encodeEvent(event));
await consensusBase.update();
}
async function appendConsensusEvents(events) {
for (const event of events) {
await appendConsensusEvent(event);
}
}
async function ensureLocalWriter() {
if (!consensusBase || consensusBase.closed) return;
await consensusBase.ready();
await consensusBase.update();
if (consensusBase.writable) return;
const localKey = consensusBase.local?.key;
if (!localKey) return;
try {
await appendConsensusEvent({ type: 'add_writer', key: localKey.toString('hex') });
await consensusBase.update();
} catch (err) {
logWarn('ConsensusAutobase', `Could not request writer access: ${err.message}`);
}
}
async function bootstrapFromDnsPass(pass, options = {}) {
const { allowLocalBootstrap = true, syncWaitMs = 3000 } = options;
if (!consensusBase || bootstrapComplete) return;
await consensusBase.ready();
await consensusBase.update();
if (!allowLocalBootstrap && syncWaitMs > 0) {
const deadline = Date.now() + syncWaitMs;
while (Date.now() < deadline) {
await consensusBase.update();
if (consensusBase.view && consensusBase.view.eventCount > 0) break;
await sleep(200);
}
}
const view = consensusBase.view;
if (view && view.eventCount > 0) {
bootstrapComplete = true;
logInfo('ConsensusAutobase', `Sidecar already has ${view.eventCount} events, skipping bootstrap`);
return;
}
if (!allowLocalBootstrap) {
logInfo('ConsensusAutobase', 'Joiner sidecar empty after sync — waiting for replication before local bootstrap');
bootstrapComplete = true;
return;
}
if (!pass) {
logWarn('ConsensusAutobase', 'dnsPass not available for bootstrap');
bootstrapComplete = true;
return;
}
const entries = await listAllEntries(pass);
const events = entriesToBootstrapEvents(entries);
if (events.length === 0) {
bootstrapComplete = true;
logInfo('ConsensusAutobase', 'No claim/vote entries to bootstrap');
return;
}
logInfo('ConsensusAutobase', `Bootstrapping sidecar with ${events.length} events from dnsPass`);
await appendConsensusEvents(events);
bootstrapComplete = true;
logInfo('ConsensusAutobase', 'Bootstrap complete');
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* @param {object} options
* @param {import('corestore')} options.store
* @param {string|null} options.bootstrapKey
* @param {import('autopass')} options.dnsPass
* @param {boolean} options.isGenesis
*/
async function initializeConsensusAutobase({ store, bootstrapKey, dnsPass, isGenesis }) {
if (!store || typeof store.get !== 'function') {
logWarn('ConsensusAutobase', 'Valid corestore required — skipping consensus sidecar init');
return null;
}
if (initPromise) return initPromise;
initPromise = (async () => {
if (consensusBase && !consensusBase.closed) {
return consensusBase;
}
try {
consensusBase = createConsensusAutobase(store, bootstrapKey || null);
state.consensusAutobase = consensusBase;
await consensusBase.ready();
await consensusBase.update();
if (!bootstrapKey && isGenesis) {
const keyHex = consensusBase.key.toString('hex');
logInfo('ConsensusAutobase', `Genesis consensus autobase created: ${keyHex.slice(0, 16)}...`);
}
await ensureLocalWriter();
await bootstrapFromDnsPass(dnsPass, {
allowLocalBootstrap: !bootstrapKey || isGenesis,
syncWaitMs: bootstrapKey ? 3000 : 0
});
consensusBase.on('update', () => {
try {
const { invalidateConsensusCache } = require('./consensus-view');
invalidateConsensusCache();
} catch (err) {
logDebug('ConsensusAutobase', `Cache invalidation on update: ${err.message}`);
}
});
return consensusBase;
} catch (err) {
initPromise = null;
if (consensusBase && !consensusBase.closed) {
try {
await consensusBase.close();
} catch (closeErr) {
logDebug('ConsensusAutobase', `Close after failed init: ${closeErr.message}`);
}
}
consensusBase = null;
state.consensusAutobase = null;
throw err;
}
})();
try {
return await initPromise;
} catch (err) {
initPromise = null;
throw err;
}
}
function replicateConsensus(connection) {
if (!consensusBase || consensusBase.closed || !connection) return;
try {
consensusBase.replicate(connection);
} catch (err) {
logDebug('ConsensusAutobase', `Replicate failed: ${err.message}`);
}
}
async function closeConsensusAutobase() {
bootstrapComplete = false;
initPromise = null;
if (!consensusBase) return;
try {
if (!consensusBase.closed) {
await consensusBase.close();
}
} catch (err) {
logError('ConsensusAutobase', `Error closing consensus autobase: ${err.message}`);
} finally {
consensusBase = null;
state.consensusAutobase = null;
}
}
function getConsensusStatus() {
const base = consensusBase;
const view = base && !base.closed ? base.view : null;
return {
open: !!(base && !base.closed),
ready: !!(base && base.opened !== false && !base.closed),
writable: !!(base && base.writable),
bootstrapComplete,
eventCount: view ? view.eventCount : 0,
domainCount: view ? view.domains.size : 0,
lastApplyAt: view ? view.lastApplyAt : null,
indexedLength: base ? base.indexedLength : 0,
length: base ? base.length : 0,
key: base && base.key ? base.key.toString('hex') : null
};
}
async function initConsensusForNetwork({ store, dnsPass, networkManifest, manifestPath }) {
const base = await initializeConsensusAutobase({
store,
bootstrapKey: networkManifest?.consensusAutobaseKey || null,
dnsPass,
isGenesis: !networkManifest?.consensusAutobaseKey
});
if (!base) return null;
const keyHex = base.key ? base.key.toString('hex') : null;
if (keyHex && !networkManifest?.consensusAutobaseKey && manifestPath) {
const networkManifestModule = require('../infrastructure/network-manifest');
await networkManifestModule.updateManifestConsensusKey(manifestPath, keyHex);
if (state.networkManifest) {
state.networkManifest.consensusAutobaseKey = keyHex;
}
}
return base;
}
module.exports = {
getConsensusBase,
isBootstrapComplete,
initializeConsensusAutobase,
initConsensusForNetwork,
appendConsensusEvent,
appendConsensusEvents,
replicateConsensus,
closeConsensusAutobase,
getConsensusStatus,
bootstrapFromDnsPass,
ensureLocalWriter
};
+106
View File
@@ -0,0 +1,106 @@
/**
* Consensus event encoding and KV key translation.
*/
const { parseClaimValue } = require('./consensus-resolver');
const EVENT_TYPES = {
CLAIM_UPSERT: 'claim_upsert',
CLAIM_REMOVE: 'claim_remove',
VOTE_UPSERT: 'vote_upsert',
VOTE_REMOVE: 'vote_remove'
};
function encodeEvent(event) {
return Buffer.from(JSON.stringify(event), 'utf8');
}
function decodeEvent(buffer) {
const raw = Buffer.isBuffer(buffer) ? buffer.toString('utf8') : String(buffer);
return JSON.parse(raw);
}
function isConsensusKvKey(key) {
return key.startsWith('claim:') || key.startsWith('vote:');
}
function kvMutationToEvent(key, value, isRemove) {
if (key.startsWith('claim:')) {
const parts = key.split(':');
if (parts.length !== 3) return null;
const domain = parts[1];
const claimant = parts[2];
if (isRemove) {
return { type: EVENT_TYPES.CLAIM_REMOVE, domain, claimant };
}
const parsed = parseClaimValue(value);
return {
type: EVENT_TYPES.CLAIM_UPSERT,
domain,
claimant,
hash: parsed.hash,
timestamp: parsed.timestamp,
ssl: parsed.ssl,
clients: parsed.clients || []
};
}
if (key.startsWith('vote:')) {
const parts = key.split(':');
if (parts.length !== 4) return null;
const domain = parts[1];
const claimant = parts[2];
const voter = parts[3];
if (isRemove) {
return { type: EVENT_TYPES.VOTE_REMOVE, domain, claimant, voter };
}
return { type: EVENT_TYPES.VOTE_UPSERT, domain, claimant, voter };
}
return null;
}
/**
* Deterministic bootstrap ordering for dnsPass KV entries.
* @param {Array<{ key: string, value: string }>} entries
* @returns {Array<object>}
*/
function entriesToBootstrapEvents(entries) {
const events = [];
for (const entry of entries) {
if (!isConsensusKvKey(entry.key)) continue;
const event = kvMutationToEvent(entry.key, entry.value, false);
if (event) events.push(event);
}
events.sort((a, b) => {
const typeOrder = (t) => {
if (t.startsWith('claim_')) return 0;
return 1;
};
const ta = typeOrder(a.type);
const tb = typeOrder(b.type);
if (ta !== tb) return ta - tb;
if (a.domain !== b.domain) return a.domain.localeCompare(b.domain);
const tsA = a.timestamp ?? 0;
const tsB = b.timestamp ?? 0;
if (tsA !== tsB) return tsA - tsB;
const ca = a.claimant || '';
const cb = b.claimant || '';
if (ca !== cb) return ca.localeCompare(cb);
const va = a.voter || '';
const vb = b.voter || '';
return va.localeCompare(vb);
});
return events;
}
module.exports = {
EVENT_TYPES,
encodeEvent,
decodeEvent,
isConsensusKvKey,
kvMutationToEvent,
entriesToBootstrapEvents
};
+181
View File
@@ -0,0 +1,181 @@
/**
* Pure consensus resolution logic for domain ownership.
* Used by the Autobase apply view at read time.
*/
const LEGACY_CLAIM_TIMESTAMP = 0;
function parseClaimValue(value) {
try {
const parsed = JSON.parse(value);
if (parsed.hash && parsed.timestamp !== undefined) {
return {
hash: parsed.hash,
clients: parsed.clients || [],
timestamp: parsed.timestamp,
ssl: parsed.ssl === true,
legacy: false
};
}
if (parsed.hash) {
return {
hash: parsed.hash,
clients: parsed.clients || [],
timestamp: parsed.timestamp || LEGACY_CLAIM_TIMESTAMP,
ssl: parsed.ssl === true,
legacy: false
};
}
} catch (e) {
// legacy hash-only string
}
return {
hash: value,
clients: [],
timestamp: LEGACY_CLAIM_TIMESTAMP,
ssl: false,
legacy: true
};
}
function validateVote(claimant, claims, voteValidationEnabled) {
if (!voteValidationEnabled) return true;
return Object.prototype.hasOwnProperty.call(claims, claimant);
}
function applyTieBreaker(candidates, claimTimestamps, localWriter, strategy) {
switch (strategy) {
case 'timestamp':
return candidates.sort((a, b) => {
const tsA = claimTimestamps[a] || 0;
const tsB = claimTimestamps[b] || 0;
return tsA - tsB;
})[0];
case 'claimant_age':
if (candidates.includes(localWriter)) return localWriter;
return candidates.sort((a, b) => a.localeCompare(b))[0];
case 'lexicographic':
default:
if (candidates.includes(localWriter)) return localWriter;
return candidates.sort((a, b) => a.localeCompare(b))[0];
}
}
/**
* @param {object} params
* @param {string} params.domain
* @param {Record<string, { hash: string, timestamp: number, ssl?: boolean, clients?: array }>} params.claims
* @param {Record<string, string>} params.voterVotes - voter -> claimant
* @param {object} params.config
* @param {string|null} params.localWriter
* @param {number} params.activePeers
*/
function resolveDomainConsensus({
domain,
claims,
voterVotes,
config,
localWriter,
activePeers
}) {
const claimTimestamps = {};
const claimHashes = {};
const voteCounts = {};
for (const [claimant, claim] of Object.entries(claims || {})) {
claimHashes[claimant] = claim.hash;
claimTimestamps[claimant] = claim.timestamp;
voteCounts[claimant] = 0;
}
const voters = new Set();
const voteValidation = config.CONSENSUS_VOTE_VALIDATION !== false;
for (const [voter, claimant] of Object.entries(voterVotes || {})) {
if (!validateVote(claimant, claimHashes, voteValidation)) continue;
voteCounts[claimant] = (voteCounts[claimant] || 0) + 1;
voters.add(voter);
}
const minVotes = Math.max(
config.CONSENSUS_MIN_VOTES || 2,
Math.ceil(activePeers * (config.CONSENSUS_QUORUM_THRESHOLD ?? 0.5))
);
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;
const claimants = Object.keys(claimHashes);
if (claimants.length === 0) {
status = 'no_claims';
} else if (!quorumMet) {
const isSingleLocalClaim =
claimants.length === 1 && localWriter && Object.prototype.hasOwnProperty.call(claimHashes, localWriter);
const onlyWeVoted = totalVotes === 0 || (totalVotes === 1 && voters.has(localWriter));
if (isSingleLocalClaim && (totalVotes === 0 || onlyWeVoted)) {
status = 'resolved';
resolvedClaimant = localWriter;
hash = claimHashes[localWriter];
} else {
status = 'insufficient_quorum';
}
} else {
const maxVotes = Math.max(...Object.values(voteCounts));
const candidates = Object.keys(voteCounts).filter((c) => voteCounts[c] === maxVotes);
if (candidates.length === 1) {
status = 'resolved';
resolvedClaimant = candidates[0];
hash = claimHashes[resolvedClaimant];
} else {
status = 'tie';
const strategy = config.CONSENSUS_TIE_BREAKER || 'timestamp';
resolvedClaimant = applyTieBreaker(candidates, claimTimestamps, localWriter, strategy);
hash = claimHashes[resolvedClaimant];
}
}
return {
status,
hash,
resolvedClaimant,
voteCounts: { ...voteCounts },
activePeers,
quorumMet,
minVotes,
totalVotes,
lastResolution: Date.now()
};
}
function buildDomainSnapshotFromView(viewState, domain) {
const domainData = viewState.domains.get(domain);
if (!domainData) {
return { claims: {}, voterVotes: {} };
}
const claims = {};
for (const [claimant, claim] of domainData.claims) {
claims[claimant] = { ...claim };
}
const voterVotes = {};
for (const [voter, claimant] of domainData.voterVotes) {
voterVotes[voter] = claimant;
}
return { claims, voterVotes };
}
module.exports = {
LEGACY_CLAIM_TIMESTAMP,
parseClaimValue,
validateVote,
applyTieBreaker,
resolveDomainConsensus,
buildDomainSnapshotFromView
};
+230
View File
@@ -0,0 +1,230 @@
/**
* Consensus read API backed by the Autobase sidecar view.
*/
const state = require('../infrastructure/state');
const { validateConfig } = require('../infrastructure/config');
const { getPersistentPublicKey } = require('../infrastructure/utils');
const { logDebug, logInfo, logWarn } = require('../infrastructure/logger');
const { trackConsensusEvent } = require('../maintenance/metrics');
const {
resolveDomainConsensus,
buildDomainSnapshotFromView
} = require('./consensus-resolver');
const { getConsensusBase, getConsensusStatus, isBootstrapComplete } = require('./consensus-autobase');
function getActivePeerCount() {
const writers = state.networkWriterPeers;
if (writers && writers.size > 0) {
return writers.size + 1;
}
return (state.connectedPeers?.size || 0) + 1;
}
const consensusStateCache = new Map();
const CONSENSUS_CACHE_TTL = 10000;
const consensusMetrics = {
resolutions: 0,
quorumFailures: 0,
ties: 0,
validationFailures: 0,
totalVotes: 0,
avgVotesPerDomain: 0,
domainResolutions: new Map()
};
const countedVotes = new Set();
const countedResolutions = new Set();
const countedQuorumFailures = new Set();
const countedTies = new Set();
let consensusConfig = null;
function getConsensusConfig() {
if (!consensusConfig) {
try {
consensusConfig = validateConfig();
} catch (err) {
consensusConfig = {
CONSENSUS_QUORUM_THRESHOLD: 0.5,
CONSENSUS_MIN_VOTES: 2,
CONSENSUS_TIE_BREAKER: 'timestamp',
CONSENSUS_VOTE_VALIDATION: true,
CONSENSUS_IMMEDIATE_UPDATE: true
};
}
}
return consensusConfig;
}
function invalidateConsensusCache(domain) {
if (domain) {
consensusStateCache.delete(domain);
} else {
consensusStateCache.clear();
}
}
function recordResolutionMetrics(domain, status, resolvedClaimant, config) {
if (status === 'resolved' && resolvedClaimant) {
const resolutionKey = `${domain}:${resolvedClaimant}`;
if (!countedResolutions.has(resolutionKey)) {
countedResolutions.add(resolutionKey);
consensusMetrics.resolutions++;
}
} else if (status === 'insufficient_quorum') {
if (!countedQuorumFailures.has(domain)) {
countedQuorumFailures.add(domain);
consensusMetrics.quorumFailures++;
}
} else if (status === 'tie' && resolvedClaimant) {
const tieKey = `${domain}:${resolvedClaimant}`;
if (!countedTies.has(tieKey)) {
countedTies.add(tieKey);
consensusMetrics.ties++;
}
if (trackConsensusEvent) {
trackConsensusEvent('tie', { domain });
}
logInfo('ConsensusView', `Tie resolved for ${domain} using ${config.CONSENSUS_TIE_BREAKER}: ${resolvedClaimant}`);
}
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++;
}
}
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
};
}
const cached = consensusStateCache.get(domain);
if (cached && Date.now() - cached.timestamp < CONSENSUS_CACHE_TTL) {
return cached.state;
}
const base = getConsensusBase();
if (!base || base.closed) {
return {
status: 'error',
error: 'consensus sidecar not initialized',
hash: null,
resolvedClaimant: null,
voteCounts: {},
activePeers: 0,
quorumMet: false,
lastResolution: null
};
}
await base.ready();
await base.update();
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 view = base.view;
if (!view) {
return {
status: 'error',
error: 'consensus view not available',
hash: null,
resolvedClaimant: null,
voteCounts: {},
activePeers: 0,
quorumMet: false,
lastResolution: null
};
}
const snapshot = buildDomainSnapshotFromView(view, domain);
const config = getConsensusConfig();
const activePeers = getActivePeerCount();
const consensusState = resolveDomainConsensus({
domain,
claims: snapshot.claims,
voterVotes: snapshot.voterVotes,
config,
localWriter,
activePeers
});
recordResolutionMetrics(domain, consensusState.status, consensusState.resolvedClaimant, config);
for (const [claimant, count] of Object.entries(consensusState.voteCounts)) {
if (count <= 0) continue;
for (const voter of Object.keys(snapshot.voterVotes)) {
if (snapshot.voterVotes[voter] !== claimant) continue;
const voteKey = `${domain}:${claimant}:${voter}`;
if (!countedVotes.has(voteKey)) {
countedVotes.add(voteKey);
consensusMetrics.totalVotes++;
}
}
}
const domainsWithVotes = consensusMetrics.domainResolutions.size;
if (domainsWithVotes > 0) {
consensusMetrics.avgVotesPerDomain = consensusMetrics.totalVotes / domainsWithVotes;
}
consensusStateCache.set(domain, { state: consensusState, timestamp: Date.now() });
return consensusState;
}
function getConsensusMetrics() {
const sidecar = getConsensusStatus();
return {
...consensusMetrics,
sidecar,
bootstrapComplete: isBootstrapComplete(),
domainResolutions: Array.from(consensusMetrics.domainResolutions.entries()).map(([d, metrics]) => ({
domain: d,
...metrics
}))
};
}
function getAllDomainNamesFromView() {
const base = getConsensusBase();
if (!base || !base.view) return [];
return Array.from(base.view.domains.keys());
}
module.exports = {
getConsensusState,
getConsensusMetrics,
getConsensusConfig,
invalidateConsensusCache,
getAllDomainNamesFromView
};
+23
View File
@@ -106,6 +106,29 @@ function createCoreSwarmHandlers(ctx) {
}
setupListeners();
try {
const { initConsensusForNetwork } = require('./consensus-autobase');
const networkManifest = require('../infrastructure/network-manifest');
const manifestPath = networkManifest.getManifestPath(process.env.NETWORK_MANIFEST_FILE);
let manifest = state.networkManifest;
if (!manifest) {
manifest = await networkManifest.readManifest(manifestPath);
if (manifest) state.networkManifest = manifest;
}
const base = await initConsensusForNetwork({
store,
dnsPass: newPass,
networkManifest: manifest,
manifestPath
});
if (base) {
logInfo('Swarm', 'Consensus sidecar initialized after pairing');
}
} catch (err) {
logError('Swarm', `Failed to initialize consensus sidecar: ${err.message}`);
}
if (isMaster) {
state.masterPendingPass = false;
logInfo('Swarm', 'Secondary master paired — dnsPass ready, master invite policy active');
+427 -770
View File
File diff suppressed because it is too large Load Diff
+37 -8
View File
@@ -156,14 +156,6 @@ async function runSerialized(pass, fn) {
});
}
async function dnsPassAdd(pass, key, value, file) {
return runSerialized(pass, () => pass.add(key, value, file));
}
async function dnsPassRemove(pass, key) {
return runSerialized(pass, () => pass.remove(key));
}
async function dnsPassGet(pass, key) {
return runSerialized(pass, () => pass.get(key));
}
@@ -182,10 +174,47 @@ async function listAllEntries(pass) {
});
}
async function appendConsensusEventForKvMutation(key, value, isRemove) {
try {
const { kvMutationToEvent, isConsensusKvKey } = require('./consensus-events');
const { appendConsensusEvent } = require('./consensus-autobase');
if (!isConsensusKvKey(key)) return;
const event = kvMutationToEvent(key, value, isRemove);
if (!event) return;
await appendConsensusEvent(event);
} catch (err) {
logWarn('DnsPassQueue', `Consensus event append failed for ${key}: ${err.message}`);
}
}
async function dnsPassAdd(pass, key, value, file) {
return runSerialized(pass, async () => {
const result = await pass.add(key, value, file);
await appendConsensusEventForKvMutation(key, value, false);
return result;
});
}
async function dnsPassRemove(pass, key) {
return runSerialized(pass, async () => {
let existingValue = null;
try {
existingValue = await pass.get(key);
} catch (err) {
// ignore lookup errors before remove
}
const result = await pass.remove(key);
const valueStr = existingValue != null ? existingValue.toString('utf8') : '';
await appendConsensusEventForKvMutation(key, valueStr, true);
return result;
});
}
module.exports = {
enqueueDnsPass,
whenDnsPassIdle,
ensureDnsPassOpen,
waitForAutobaseIdle,
syncDnsPassView,
isInviteCreationActive,
isAtomicDnsPassError,
+17 -2
View File
@@ -93,16 +93,30 @@ async function writeManifest(manifestPath, fields) {
}
async function adoptManifestFromPass(pass, topicSeed, manifestPath, options = {}) {
const { genesisPublicKey, isGenesisRun = false } = options;
const { genesisPublicKey, isGenesisRun = false, consensusAutobaseKey = null } = options;
const identity = extractNetworkIdentity(pass, genesisPublicKey);
const doc = await writeManifest(manifestPath, {
...identity,
topicSeed: topicSeed || process.env.TOPIC_SEED || 'p2ns-dns',
isGenesis: isGenesisRun
isGenesis: isGenesisRun,
...(consensusAutobaseKey ? { consensusAutobaseKey } : {})
});
return doc;
}
async function updateManifestConsensusKey(manifestPath, consensusAutobaseKey) {
const filePath = getManifestPath(manifestPath);
const existing = await readManifest(manifestPath);
if (!existing) return null;
const doc = {
...existing,
consensusAutobaseKey
};
await fs.writeFile(filePath, `${JSON.stringify(doc, null, 2)}\n`, 'utf8');
logInfo('NetworkManifest', `Updated manifest with consensusAutobaseKey: ${consensusAutobaseKey.slice(0, 16)}...`);
return doc;
}
function recordPeerNetworkStatus(peerId, status) {
if (!peerId || !status) return;
const state = require('./state');
@@ -163,6 +177,7 @@ module.exports = {
readManifest,
writeManifest,
adoptManifestFromPass,
updateManifestConsensusKey,
recordPeerNetworkStatus,
checkSplitBrain,
getLocalNetworkSummary
+1
View File
@@ -42,6 +42,7 @@ const currentSubnetIndex = 0; // Start with first subnet for round-robin
module.exports = {
dnsPass: null,
consensusAutobase: null,
corestore: null,
hypercoreStats: null,
/** Incremented while dns-pass-queue runs a serialized Autopass op (defer update handlers) */