feat(experimental)!: multi-master one ledger (Option B)

Remove the last layer of soft centralization: a lone `--master` on empty
storage could still implicitly genesis a separate Autopass and split the
network on the same TOPIC_SEED. Genesis is now explicit (`--master
--genesis`); secondary masters pair via invite into the same dnsPass as
joiners, with a shared network manifest, split-brain diagnostics, and
writer-aware quorum.

- Genesis vs secondary master boot paths; auto-adopt manifest on upgrade
- Masters without dnsPass accept invites; masters with pass ignore them
- NETWORK_MANIFEST_FILE, P2NS_GENESIS, MASTER_LOAD_DOMAINS, MASTER_INVITE_ONLY
- core.status networkId; admin diagnostics; docs and multi-master tests

BREAKING: operators must run one genesis per network; additional masters
use `node p2ns.js --master` (not `--genesis`) on empty storage.
This commit is contained in:
Raven Scott
2026-05-29 05:34:50 -04:00
parent 9e931b1f4d
commit b1a02e2e84
19 changed files with 920 additions and 55 deletions
@@ -947,12 +947,21 @@ function displayInviteDiagnostics(diagnostics) {
const contentDiv = document.createElement('div');
contentDiv.className = 'space-y-2';
const networkIdShort = diagnostics.networkId
? `${String(diagnostics.networkId).slice(0, 16)}`
: 'n/a';
const statusItems = [
{ label: 'Node', value: diagnostics.nodeType, color: 'text-white' },
{ label: 'Network ID', value: networkIdShort, color: diagnostics.networkId ? 'text-indigo-300' : 'text-gray-400' },
{ label: 'DNS Pass', value: diagnostics.dnsPassInitialized ? 'Ready' : 'Missing', color: diagnostics.dnsPassInitialized ? 'text-green-400' : 'text-red-400' },
{ label: 'Peers', value: String(totalPeers), color: 'text-white' },
{ label: 'Control', value: protocol.control || 'p2ns.core-request-rpc', color: 'text-indigo-300' }
];
if (diagnostics.masterPendingPass) {
statusItems.splice(1, 0, { label: 'Role', value: 'Secondary master (awaiting invite)', color: 'text-yellow-400' });
} else if (diagnostics.isGenesis) {
statusItems.splice(1, 0, { label: 'Role', value: 'Genesis master', color: 'text-green-400' });
}
contentDiv.innerHTML += `
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2 text-sm">
@@ -1010,6 +1019,12 @@ function displayInviteDiagnostics(diagnostics) {
if (inFlight > 0) {
alerts.push(`<div class="text-yellow-300 text-xs">⚙️ ${inFlight} in-flight invite.request handler(s)</div>`);
}
if ((diagnostics.splitBrainWarnings || []).length > 0) {
alerts.push(`<div class="text-red-300 text-xs">🚨 ${diagnostics.splitBrainWarnings.length} split-network warning(s) — peer networkId mismatch</div>`);
}
if (diagnostics.masterPendingPass) {
alerts.push(`<div class="text-yellow-300 text-xs">⏳ Secondary master: waiting for invite to join the network ledger</div>`);
}
if ((diagnostics.consecutiveInviteFailures || 0) > 0) {
const n = diagnostics.consecutiveInviteFailures;
const severity = n >= 3 ? 'text-red-300' : 'text-orange-300';
+12
View File
@@ -274,6 +274,9 @@ function renderCoreStats(core) {
if (protocolEl) {
const parts = [
core.nodeType ? `node=${core.nodeType}` : null,
core.networkId ? `network=${String(core.networkId).slice(0, 12)}` : null,
core.isGenesis ? 'genesis' : null,
core.masterPendingPass ? 'pending-pass' : null,
protocol.control ? `control=${protocol.control}` : null,
protocol.inviteWire ? `wire=${protocol.inviteWire}` : null,
core.schemaVersion ? `schema=v${core.schemaVersion}` : null
@@ -289,6 +292,15 @@ function renderCoreStats(core) {
const peerEntries = core.peers ? Object.entries(core.peers) : [];
let html = '';
if ((core.splitBrainWarnings || []).length > 0) {
html += `
<div class="theme-glass rounded-lg p-3 text-sm border border-red-500/40">
<span class="text-red-400 font-semibold">Split network:</span>
<span class="theme-text-secondary"> ${core.splitBrainWarnings.length} peer(s) report a different networkId</span>
</div>
`;
}
if (masterQueue > 0 || inFlight > 0) {
html += `
<div class="theme-glass rounded-lg p-3 text-sm">