Repair swarm bootstrap seed for new and existing nodes
Hop-State: A_06FP90SDWE5709BB1G6J4B8 Hop-Proposal: R_06FP90RS27BNBV5H5N053A0 Hop-Task: T_06FP905Q50D1ZFPC56N3DZG Hop-Attempt: AT_06FP905Q50E6P72ZMVT4038
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
DELETE FROM `swarm_seeds`
|
||||||
|
WHERE `domain` = 'node.synapsis.social'
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM `swarm_seeds` AS `canonical_seed`
|
||||||
|
WHERE `canonical_seed`.`domain` = 'synapsis.social'
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE `swarm_seeds`
|
||||||
|
SET
|
||||||
|
`domain` = 'synapsis.social',
|
||||||
|
`consecutive_failures` = 0
|
||||||
|
WHERE `domain` = 'node.synapsis.social';
|
||||||
@@ -8,7 +8,11 @@ import { db, nodes, users, posts } from '@/db';
|
|||||||
import { eq, sql } from 'drizzle-orm';
|
import { eq, sql } from 'drizzle-orm';
|
||||||
import type { SwarmAnnouncement, SwarmNodeInfo, SwarmCapability } from './types';
|
import type { SwarmAnnouncement, SwarmNodeInfo, SwarmCapability } from './types';
|
||||||
import { upsertSwarmNode, getSeedNodes, markNodeSuccess, markNodeFailure } from './registry';
|
import { upsertSwarmNode, getSeedNodes, markNodeSuccess, markNodeFailure } from './registry';
|
||||||
import { getPublicSwarmDomain, isPublicSwarmDomain } from './node-domain';
|
import {
|
||||||
|
getCanonicalSwarmSeedDomain,
|
||||||
|
getPublicSwarmDomain,
|
||||||
|
isPublicSwarmDomain,
|
||||||
|
} from './node-domain';
|
||||||
|
|
||||||
const PUBLIC_SWARM_DOMAIN_ERROR = 'Public swarm participation requires a real ICANN domain';
|
const PUBLIC_SWARM_DOMAIN_ERROR = 'Public swarm participation requires a real ICANN domain';
|
||||||
|
|
||||||
@@ -84,7 +88,7 @@ export async function announceToNode(targetDomain: string): Promise<{ success: b
|
|||||||
return { success: false, error: PUBLIC_SWARM_DOMAIN_ERROR };
|
return { success: false, error: PUBLIC_SWARM_DOMAIN_ERROR };
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicTargetDomain = getPublicSwarmDomain(targetDomain);
|
const publicTargetDomain = getCanonicalSwarmSeedDomain(targetDomain);
|
||||||
if (!publicTargetDomain) {
|
if (!publicTargetDomain) {
|
||||||
return { success: false, error: `Invalid public swarm domain: ${targetDomain}` };
|
return { success: false, error: `Invalid public swarm domain: ${targetDomain}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* The Synapsis swarm is a decentralized network of nodes that discover
|
* The Synapsis swarm is a decentralized network of nodes that discover
|
||||||
* and communicate with each other using a hybrid approach:
|
* and communicate with each other using a hybrid approach:
|
||||||
*
|
*
|
||||||
* 1. Seed nodes (like node.synapsis.social) provide initial bootstrap
|
* 1. Seed nodes (like synapsis.social) provide initial bootstrap
|
||||||
* 2. Gossip protocol spreads node/handle information epidemically
|
* 2. Gossip protocol spreads node/handle information epidemically
|
||||||
* 3. Any node can discover the full network without central authority
|
* 3. Any node can discover the full network without central authority
|
||||||
* 4. Direct node-to-node interactions (likes, follows, etc.) for instant delivery
|
* 4. Direct node-to-node interactions (likes, follows, etc.) for instant delivery
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { getPublicSwarmDomain, isPublicSwarmDomain } from './node-domain';
|
import {
|
||||||
|
getCanonicalSwarmSeedDomain,
|
||||||
|
getPublicSwarmDomain,
|
||||||
|
isPublicSwarmDomain,
|
||||||
|
} from './node-domain';
|
||||||
|
|
||||||
describe('public swarm domains', () => {
|
describe('public swarm domains', () => {
|
||||||
it.each([
|
it.each([
|
||||||
@@ -31,3 +35,17 @@ describe('public swarm domains', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('swarm seed domains', () => {
|
||||||
|
it('maps the retired bootstrap hostname to the official node identity', () => {
|
||||||
|
expect(getCanonicalSwarmSeedDomain('node.synapsis.social')).toBe('synapsis.social');
|
||||||
|
expect(getCanonicalSwarmSeedDomain('https://NODE.SYNAPSIS.SOCIAL/')).toBe('synapsis.social');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves other valid seed domains unchanged', () => {
|
||||||
|
expect(getCanonicalSwarmSeedDomain('batorbros.bond')).toBe('batorbros.bond');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects non-public seed domains', () => {
|
||||||
|
expect(getCanonicalSwarmSeedDomain('localhost:43821')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { parse } from 'tldts';
|
import { parse } from 'tldts';
|
||||||
|
|
||||||
|
const LEGACY_SWARM_SEED_DOMAINS: Readonly<Record<string, string>> = {
|
||||||
|
'node.synapsis.social': 'synapsis.social',
|
||||||
|
};
|
||||||
|
|
||||||
export function normalizeNodeDomain(domain: string): string {
|
export function normalizeNodeDomain(domain: string): string {
|
||||||
return domain
|
return domain
|
||||||
.trim()
|
.trim()
|
||||||
@@ -52,3 +56,14 @@ export function isPublicSwarmDomain(value: string | null | undefined): boolean {
|
|||||||
return getPublicSwarmDomain(value) !== null;
|
return getPublicSwarmDomain(value) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve retired bootstrap hostnames to the node identity they represent.
|
||||||
|
* This keeps upgraded nodes working even before their persisted seed rows are
|
||||||
|
* rewritten by the accompanying data migration.
|
||||||
|
*/
|
||||||
|
export function getCanonicalSwarmSeedDomain(value: string | null | undefined): string | null {
|
||||||
|
const domain = getPublicSwarmDomain(value);
|
||||||
|
if (!domain) return null;
|
||||||
|
|
||||||
|
return LEGACY_SWARM_SEED_DOMAINS[domain] ?? domain;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import { db, swarmNodes, swarmSeeds, swarmSyncLog } from '@/db';
|
|||||||
import { eq, desc, and, gt, lt, sql } from 'drizzle-orm';
|
import { eq, desc, and, gt, lt, sql } from 'drizzle-orm';
|
||||||
import type { SwarmNodeInfo, SwarmCapability, SwarmSyncResult } from './types';
|
import type { SwarmNodeInfo, SwarmCapability, SwarmSyncResult } from './types';
|
||||||
import { SWARM_CONFIG, DEFAULT_SEED_NODES } from './types';
|
import { SWARM_CONFIG, DEFAULT_SEED_NODES } from './types';
|
||||||
import { getPublicSwarmDomain, isPublicSwarmDomain } from './node-domain';
|
import {
|
||||||
|
getCanonicalSwarmSeedDomain,
|
||||||
|
getPublicSwarmDomain,
|
||||||
|
isPublicSwarmDomain,
|
||||||
|
} from './node-domain';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or create a swarm node entry
|
* Get or create a swarm node entry
|
||||||
@@ -273,7 +277,11 @@ export async function getSeedNodes(): Promise<string[]> {
|
|||||||
orderBy: (swarmSeeds) => [swarmSeeds.priority],
|
orderBy: (swarmSeeds) => [swarmSeeds.priority],
|
||||||
});
|
});
|
||||||
|
|
||||||
const publicSeeds = seeds.map(s => s.domain).filter(isPublicSwarmDomain);
|
const publicSeeds = Array.from(new Set(
|
||||||
|
seeds
|
||||||
|
.map((seed) => getCanonicalSwarmSeedDomain(seed.domain))
|
||||||
|
.filter((domain): domain is string => domain !== null)
|
||||||
|
));
|
||||||
|
|
||||||
return publicSeeds.length > 0
|
return publicSeeds.length > 0
|
||||||
? publicSeeds
|
? publicSeeds
|
||||||
@@ -286,7 +294,7 @@ export async function getSeedNodes(): Promise<string[]> {
|
|||||||
export async function addSeedNode(domain: string, priority = 100): Promise<void> {
|
export async function addSeedNode(domain: string, priority = 100): Promise<void> {
|
||||||
if (!db) return;
|
if (!db) return;
|
||||||
|
|
||||||
const normalizedDomain = getPublicSwarmDomain(domain);
|
const normalizedDomain = getCanonicalSwarmSeedDomain(domain);
|
||||||
if (!normalizedDomain) {
|
if (!normalizedDomain) {
|
||||||
throw new Error(`Seed nodes must use a public ICANN domain: ${domain}`);
|
throw new Error(`Seed nodes must use a public ICANN domain: ${domain}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export interface SwarmStats {
|
|||||||
|
|
||||||
// Default seed nodes for bootstrapping
|
// Default seed nodes for bootstrapping
|
||||||
export const DEFAULT_SEED_NODES = [
|
export const DEFAULT_SEED_NODES = [
|
||||||
'node.synapsis.social',
|
'synapsis.social',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Swarm configuration
|
// Swarm configuration
|
||||||
|
|||||||
Reference in New Issue
Block a user