From b88dfb1e94fa04394bd320860ea2dc799b8a5fad Mon Sep 17 00:00:00 2001 From: Eric Meehan Date: Sun, 1 Nov 2026 16:43:28 -0500 Subject: [PATCH] deploy --- Dockerfile | 4 +-- README.md | 9 +++++ compose.mainnet.yaml | 4 +-- compose.yaml | 4 +-- deploy/kubernetes/README.md | 25 +++++++++++++- docs/mainnet-launch.md | 6 ++-- server/launch.ts | 13 ++++--- server/testnet/peers.ts | 2 +- shared/network.ts | 2 +- shared/wallet-backup.ts | 6 ++-- tests/network.test.ts | 69 ++++++++++++++++++++++++++++++++++--- 11 files changed, 121 insertions(+), 23 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0b38fb7..db9f721 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ COPY package.json package-lock.json ./ RUN npm ci COPY . . ARG WIKIDECK_NETWORK=mainnet -ARG WIKIDECK_GENESIS_TIME=1798761600 +ARG WIKIDECK_GENESIS_TIME=1793491200 ENV VITE_WIKIDECK_NETWORK=$WIKIDECK_NETWORK ENV VITE_WIKIDECK_GENESIS_TIME=$WIKIDECK_GENESIS_TIME RUN npm run build @@ -37,7 +37,7 @@ RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force FROM node:24-alpine AS runtime RUN apk add --no-cache libstdc++ ARG WIKIDECK_NETWORK=mainnet -ARG WIKIDECK_GENESIS_TIME=1798761600 +ARG WIKIDECK_GENESIS_TIME=1793491200 ENV NODE_ENV=production \ HOST=0.0.0.0 \ PORT=3001 \ diff --git a/README.md b/README.md index da570b0..c8649e4 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,15 @@ Mining uses **RandomX, the proof-of-work algorithm also used by Monero**, design ## Default: mainnet candidate (Docker) +The default mainnet genesis is fixed at **November 1, 2026, 00:00 UTC** +(`1793491200`). Rebuilding the current Dockerfile requires no special genesis +argument and no wait until January. Publish the rebuilt image as +`ericomeehan/wikideck:latest` before using the three-node Ansible playbook; it reads +the chain ID automatically and configures the risk acknowledgment and internal +peers. This updates source defaults, not an already published image or an existing +ledger's chain identity. Create/unlock a wallet in the deployed HTTPS UI and run +the native miner to produce cards and confirm transfers/trades. + Install Docker Desktop with Linux containers and Docker Compose v2. From the repository root: ```sh diff --git a/compose.mainnet.yaml b/compose.mainnet.yaml index 1321094..498476f 100755 --- a/compose.mainnet.yaml +++ b/compose.mainnet.yaml @@ -7,12 +7,12 @@ services: target: runtime args: WIKIDECK_NETWORK: mainnet - WIKIDECK_GENESIS_TIME: "${WIKIDECK_GENESIS_TIME:-1798761600}" + WIKIDECK_GENESIS_TIME: "${WIKIDECK_GENESIS_TIME:-1793491200}" ports: - "127.0.0.1:${WIKIDECK_MAINNET_PORT:-3201}:3001" environment: WIKIDECK_NETWORK: mainnet - WIKIDECK_GENESIS_TIME: "${WIKIDECK_GENESIS_TIME:-1798761600}" + WIKIDECK_GENESIS_TIME: "${WIKIDECK_GENESIS_TIME:-1793491200}" PUBLIC_ORIGIN: "${WIKIDECK_MAINNET_ORIGIN:-}" WIKIDECK_PEERS: "${WIKIDECK_MAINNET_PEERS:-}" WIKIDECK_MAINNET_LAUNCH: "${WIKIDECK_MAINNET_LAUNCH:-}" diff --git a/compose.yaml b/compose.yaml index dc320f4..d3cf17b 100644 --- a/compose.yaml +++ b/compose.yaml @@ -5,13 +5,13 @@ services: target: runtime args: WIKIDECK_NETWORK: mainnet - WIKIDECK_GENESIS_TIME: "${WIKIDECK_GENESIS_TIME:-1798761600}" + WIKIDECK_GENESIS_TIME: "${WIKIDECK_GENESIS_TIME:-1793491200}" ports: # Loopback only; terminate reviewed HTTPS at the operator's edge. - "127.0.0.1:${WIKIDECK_MAINNET_PORT:-3201}:3001" environment: WIKIDECK_NETWORK: mainnet - WIKIDECK_GENESIS_TIME: "${WIKIDECK_GENESIS_TIME:-1798761600}" + WIKIDECK_GENESIS_TIME: "${WIKIDECK_GENESIS_TIME:-1793491200}" PUBLIC_ORIGIN: "${WIKIDECK_MAINNET_ORIGIN:-}" WIKIDECK_PEERS: "${WIKIDECK_MAINNET_PEERS:-}" WIKIDECK_MAINNET_LAUNCH: "${WIKIDECK_MAINNET_LAUNCH:-}" diff --git a/deploy/kubernetes/README.md b/deploy/kubernetes/README.md index 59bbfe4..e721500 100755 --- a/deploy/kubernetes/README.md +++ b/deploy/kubernetes/README.md @@ -6,7 +6,7 @@ This directory intentionally contains no apply-ready manifests yet. The code is - Build and publish one immutable image with `WIKIDECK_NETWORK=mainnet` and one chosen `WIKIDECK_GENESIS_TIME`. Give every peer and browser bundle that same chain identity. A runtime-only genesis override is rejected by the image manifest check. - Give each node a stable, unique public HTTPS origin. TLS must terminate at an operator-managed ingress/reverse proxy; the application listener itself is HTTP on port 3001. -- Configure at least two other distinct public HTTPS peer origins per node. The current mainnet peer client rejects private IPs and private DNS answers. Kubernetes-only ClusterIP/headless DNS peers are not accepted; never enable `WIKIDECK_ALLOW_PRIVATE_PEERS=1` to bypass the mainnet launch gate. +- Configure at least two other distinct public HTTPS peer origins per node for public peering. Private IPs/DNS and HTTP are rejected by default. An operator-controlled private candidate cluster may explicitly set `WIKIDECK_ALLOW_PRIVATE_PEERS=1` and use distinct per-node ClusterIP DNS origins. This opt-in does not change the exact chain-ID/risk/genesis gates or the HTTPS browser-origin requirement. - Route each public peer hostname deterministically to exactly one node. A shared load-balanced hostname is not a stable peer identity. Separate per-peer Deployments/Services/Ingress routes are the simplest current shape; each node must exclude its own origin from its peer list. - Give every node an exclusive persistent volume mounted at `/app/data`. Never share a ledger volume between pods. Agree the storage class, capacity, backup/restore procedure, resource limits, failure domains, and upgrade/reorg policy before enabling the launch approval. - Configure the exact manifest `CHAIN_ID` as `WIKIDECK_MAINNET_LAUNCH`, and explicitly set `WIKIDECK_MAINNET_RISK_ACK=operator-accepts-unaudited-risk`. The latter accepts unaudited residual risk; it is not an audit or readiness check. @@ -17,6 +17,29 @@ Use a dedicated namespace and least-privilege service account (or disable token Do not make the public origin or configured peers imply that peers are independent or that the network is secure. This software has no external audit, public bootstrap service, peer discovery, or demonstrated production capacity. +## Private candidate cluster + +Deploy the public node and two internal peers as three separate single-replica +role instances, each with its own Service and PVC. Configure each peer list with +the other two Services, for example `http://wikideck-peer-a.wikideck.svc.cluster.local:80`. +Allow same-namespace validator pods on target TCP 3001 through both ingress and +egress NetworkPolicy; Service port 80 is translated to that target port. Only the +public node needs ingress; all nodes may use the public node's HTTPS browser +origin. Keep API credentials and wallet keys out of validator pods. + +Read `--network-manifest` from the exact published image to obtain the candidate +chain ID without opening storage or starting a listener. Its +`privatePeersSupported: true` capability distinguishes updated images from older +images that reject this topology. Do not override genesis at runtime. The default +image genesis is now fixed at November 1, 2026, 00:00 UTC (`1793491200`), so no +January wait or custom build argument is needed. Rebuild and publish the updated +image; an older published image still contains its previous genesis. Existing +ledgers and wallets must retain their original chain identity. + +Private peer RPC is unauthenticated and, with HTTP, unencrypted. Use a trusted +cluster boundary and scoped policies; add mesh transport protection if needed. +This topology does not demonstrate independent peers/miners or production readiness. + ## Values needed before manifests can be finalized Choose the genesis timestamp and image registry/digest; provide the namespace, ingress class and TLS/certificate mechanism, public DNS name per node, peer topology, storage class/size, resource budget, network-policy egress model, backup destination/retention, and desired availability/recovery behavior. Then calculate and publish the exact manifest and chain approval. Until those values and the peer topology are available, this checklist is the safe preparation artifact rather than a misleading runnable YAML template. diff --git a/docs/mainnet-launch.md b/docs/mainnet-launch.md index 83aea4b..ee88952 100755 --- a/docs/mainnet-launch.md +++ b/docs/mainnet-launch.md @@ -11,7 +11,7 @@ The repository defaults to a **mainnet candidate**, not an audited or launched p | Selection | `WIKIDECK_NETWORK=mainnet` (or unset) | `WIKIDECK_NETWORK=testnet` | | Network ID | `wikideck-randomx-mainnet-1` | `wikideck-randomx-testnet-6` | | Protocol | Version 6 | Existing fixed local testnet version 6 | -| Genesis timestamp | 2027-01-01 00:00 UTC by default; deployment may choose another timestamp before building/freezing | Fixed at 2026-10-19 00:00 UTC | +| Genesis timestamp | Fixed launch default: 2026-11-01 00:00 UTC (`1793491200`); ready to start now | Fixed at 2026-10-19 00:00 UTC | | Ledger directory | mainnet | testnet | | Browser wallet | IndexedDB name derived from exact chain ID | Separate chain identity | | New backup | v2 header authenticates exact chain ID | Same v2 format; legacy v1 is accepted only on the original testnet | @@ -35,9 +35,9 @@ Starting the candidate node or native miner requires all of: 1. `WIKIDECK_MAINNET_LAUNCH` equals the exact candidate chain ID printed by the manifest command. 2. `WIKIDECK_MAINNET_RISK_ACK=operator-accepts-unaudited-risk` records the operator's explicit acceptance that no external audit is planned and residual risks remain. This is not a safety certification; the manifest honestly reports `audited: false`. 3. The candidate genesis timestamp has arrived. If the operator changes the timestamp or consensus manifest before launch, the chain ID also changes and the old approval is invalidated. Freeze and publish the final manifest before launch. -4. An HTTPS `PUBLIC_ORIGIN`, at least two distinct configured HTTPS peer origins, and no private-peer bypass. Configuration count cannot prove the peers are online, independent, or sufficiently distributed; verify that operationally. Existing peer DNS/origin controls still apply. +4. An HTTPS `PUBLIC_ORIGIN` and at least two distinct configured peer origins. Public deployments require public HTTPS peers by default. An operator-controlled private candidate cluster may explicitly set `WIKIDECK_ALLOW_PRIVATE_PEERS=1` to permit private/HTTP peer RPC; this does not relax browser HTTPS, chain approval, risk acknowledgment, or genesis-time gates. Configuration count cannot prove the peers are online, independent, or sufficiently distributed; verify that operationally. Existing public peer DNS/origin controls still apply when the private opt-in is off, and HTTPS certificate verification is never disabled. -For Kubernetes, the current peer implementation requires HTTPS origins that resolve to public addresses; it rejects private cluster DNS/IPs for mainnet. The node serves HTTP internally and needs a separately configured TLS ingress/reverse proxy. Every node needs a stable, unique public HTTPS origin and at least two distinct peer origins. A default ClusterIP/headless Service alone cannot satisfy these rules, and a load-balanced ingress shared by several pods is not a stable per-node peer identity. Do not set the private-peer bypass to work around this; use public per-node HTTPS endpoints or first review a separate authenticated private-peering design that preserves the public independence gate. No cluster-specific manifests are included because ingress class, TLS/certificate management, storage class/size, resource capacity, and stable peer DNS were not provided. +For Kubernetes public peering, give every node a stable, unique public HTTPS endpoint. For an explicitly private candidate, separate single-replica Deployments, Services and exclusive PVCs may use distinct ClusterIP DNS origins with the private opt-in. Each validator lists the other two internal Services, never itself. The browser-facing node needs a TLS ingress/reverse proxy; internal-only validators may share that HTTPS browser origin without their own ingress. NetworkPolicy must restrict peer RPC to the selected validator pods on target TCP port 3001. Private HTTP RPC is unauthenticated and unencrypted unless protected by an independently configured mesh; do not use it across untrusted networks. Three validators in one cluster/storage failure domain are not independent operators or proof of production readiness. See the [Kubernetes mainnet preparation checklist](../deploy/kubernetes/README.md) for the deployment contract and the values needed before manifests can be finalized. Unknown profile names fail closed. A direct import of the HTTP entry point is also guarded. Library-level manifest/consensus/storage inspection remains possible for tests and review; activation gates are accident prevention and operator risk acceptance, not a security boundary against someone who edits the source. diff --git a/server/launch.ts b/server/launch.ts index d95525c..e6e7808 100755 --- a/server/launch.ts +++ b/server/launch.ts @@ -3,7 +3,8 @@ import { CHAIN_ID, GENESIS_HASH, GENESIS_TIME } from './testnet/consensus.ts'; export function launchManifest() { return { profile: PROFILE, network: NETWORK, chainId: CHAIN_ID, genesis: GENESIS_HASH, - genesisTime: GENESIS_TIME, launchStatus: IS_MAINNET ? 'candidate-gated' : 'testnet', audited: false }; + genesisTime: GENESIS_TIME, launchStatus: IS_MAINNET ? 'candidate-gated' : 'testnet', audited: false, + privatePeersSupported: true }; } /** Operator risk acceptance is an accident-prevention gate, NOT evidence of external review. */ export function assertLaunchAllowed(env: NodeJS.ProcessEnv = process.env, now = Date.now()) { @@ -14,11 +15,15 @@ export function assertLaunchAllowed(env: NodeJS.ProcessEnv = process.env, now = if (now < GENESIS_TIME * 1000) throw new Error('Mainnet candidate genesis time has not arrived; this is not an announced launch date'); const origin = new URL(env.PUBLIC_ORIGIN ?? 'http://invalid'); if (origin.protocol !== 'https:' || origin.username || origin.password || origin.pathname !== '/' || origin.search || origin.hash) throw new Error('Mainnet requires an HTTPS PUBLIC_ORIGIN'); + // Explicit operator-controlled cluster opt-in; never inferred from peer input. + // This permits private/HTTP RPC, not weaker browser HTTPS or chain/genesis gates. + const allowPrivate = env.WIKIDECK_ALLOW_PRIVATE_PEERS === '1'; const peers = (env.WIKIDECK_PEERS ?? '').split(',').map(s => s.trim()).filter(Boolean); - if (new Set(peers).size < 2) throw new Error('Mainnet launch requires at least two explicitly configured independent HTTPS peers'); + if (new Set(peers).size < 2) throw new Error('Mainnet launch requires at least two explicitly configured distinct peers'); for (const text of peers) { + if (!/^https?:\/\/[^/?#\\@\s]+\/?$/i.test(text)) throw new Error('Invalid mainnet peer origin'); const peer = new URL(text); - if (peer.protocol !== 'https:' || peer.origin === origin.origin || peer.username || peer.password || peer.pathname !== '/' || peer.search || peer.hash) throw new Error('Invalid mainnet peer origin'); + if ((peer.protocol !== 'https:' && !(allowPrivate && peer.protocol === 'http:')) || peer.origin === origin.origin || peer.username || peer.password || peer.pathname !== '/' || peer.search || peer.hash) throw new Error('Invalid mainnet peer origin'); } - if (new Set(peers.map(p => new URL(p).origin)).size < 2 || env.WIKIDECK_ALLOW_PRIVATE_PEERS === '1') throw new Error('Mainnet requires distinct public peer origins; private peer bypass is disabled'); + if (new Set(peers.map(p => new URL(p).origin)).size < 2) throw new Error('Mainnet requires distinct peer origins'); } diff --git a/server/testnet/peers.ts b/server/testnet/peers.ts index 181e027..6546084 100644 --- a/server/testnet/peers.ts +++ b/server/testnet/peers.ts @@ -31,7 +31,7 @@ const poolSchema = z.object({ transactions: z.array(transactionSchema).max(256) type Peer = { url: string; connected: boolean; error?: string; offset: number; busy: boolean }; /** Outbound-only fixed allowlist. HTTP routes are deliberately left to the caller. - * allowPrivate is an explicit operator permission for local/Docker networks, not peer input. + * allowPrivate is an explicit operator permission for local/Docker/cluster networks, not peer input. */ export class PeerNetwork { private readonly peers: Peer[]; diff --git a/shared/network.ts b/shared/network.ts index 9ded98b..1b6a50e 100755 --- a/shared/network.ts +++ b/shared/network.ts @@ -13,7 +13,7 @@ export const PROFILE = networkProfile(isNodeRuntime export const IS_MAINNET = PROFILE === 'mainnet'; export const NETWORK = IS_MAINNET ? 'wikideck-randomx-mainnet-1' as const : 'wikideck-randomx-testnet-6' as const; const TESTNET_GENESIS_TIME = 1_792_411_200; -const DEFAULT_MAINNET_GENESIS_TIME = 1_798_761_600; // 2027-01-01 UTC candidate; deployment must freeze one value. +const DEFAULT_MAINNET_GENESIS_TIME = 1_793_491_200; // 2026-11-01 00:00 UTC; fixed launch identity, never derived from the current clock. function genesisTime(raw: string | undefined, profile: NetworkProfile): number { const value = raw ?? String(profile === 'mainnet' ? DEFAULT_MAINNET_GENESIS_TIME : TESTNET_GENESIS_TIME); if (!/^(0|[1-9]\d{8,10})$/.test(value)) throw new Error('WIKIDECK_GENESIS_TIME must be a canonical Unix-seconds integer'); diff --git a/shared/wallet-backup.ts b/shared/wallet-backup.ts index a9ddd4a..adc6cd4 100644 --- a/shared/wallet-backup.ts +++ b/shared/wallet-backup.ts @@ -96,10 +96,12 @@ async function validateBackup(value: unknown): Promise { decode(backup.ciphertext, 17, 4096); if (encoder.encode(JSON.stringify(backup)).length > MAX_BYTES) throw new Error('Backup exceeds 32 KB.'); // Snapshot primitives so callers cannot change headers during awaits. - return { format: backup.format, version: backup.version, network: backup.network, - ...(backup.version === 2 ? { chainId: backup.chainId } : {}), + const snapshot: WalletBackupBase = { format: backup.format, network: backup.network, kdf: backup.kdf, iterations: backup.iterations, cipher: backup.cipher, salt: backup.salt, iv: backup.iv, publicKey: backup.publicKey, ciphertext: backup.ciphertext }; + return backup.version === 2 + ? { ...snapshot, version: 2, chainId: backup.chainId } + : { ...snapshot, version: 1 }; } async function validatePair(privateKey: CryptoKey, publicKey: string): Promise { diff --git a/tests/network.test.ts b/tests/network.test.ts index 3ce9d12..0c17bf4 100755 --- a/tests/network.test.ts +++ b/tests/network.test.ts @@ -2,6 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { execFileSync, spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -11,7 +12,7 @@ import { CHAIN_ID, GENESIS_TIME, GENESIS_HASH, MAX_TARGET } from '../server/test function script(code: string, env: Record = {}) { return execFileSync(process.execPath, ['--experimental-strip-types', '--input-type=module', '-e', code], { - env: { ...process.env, WIKIDECK_NETWORK: 'mainnet', WIKIDECK_GENESIS_TIME: '1798761600', ...env }, encoding: 'utf8', timeout: 30000, + env: { ...process.env, WIKIDECK_NETWORK: 'mainnet', WIKIDECK_GENESIS_TIME: '1793491200', ...env }, encoding: 'utf8', timeout: 30000, }).trim(); } test('default profile is mainnet while testnet remains an explicit isolated profile', () => { @@ -38,14 +39,15 @@ test('mainnet candidate has distinct deterministic genesis, signatures and stric }); test('mainnet startup is blocked before resources open, while manifest inspection needs no launch approval', () => { const args = ['--experimental-strip-types', 'server/index.ts']; - const env = { ...process.env, WIKIDECK_NETWORK: 'mainnet', WIKIDECK_GENESIS_TIME: '1798761600', WIKIDECK_MAINNET_LAUNCH: '', WIKIDECK_MAINNET_RISK_ACK: '' }; + const env = { ...process.env, WIKIDECK_NETWORK: 'mainnet', WIKIDECK_GENESIS_TIME: '1793491200', WIKIDECK_MAINNET_LAUNCH: '', WIKIDECK_MAINNET_RISK_ACK: '' }; const result = spawnSync(process.execPath, args, { env, encoding: 'utf8', timeout: 30000 }); assert.notEqual(result.status, 0); assert.match(result.stderr, /launch-gated/); const manifest = JSON.parse(execFileSync(process.execPath, [...args, '--network-manifest'], { env, encoding: 'utf8' })); assert.equal(manifest.profile, 'mainnet'); assert.equal(manifest.audited, false); assert.equal(manifest.launchStatus, 'candidate-gated'); + assert.equal(manifest.privatePeersSupported, true); }); -test('mainnet gates require genesis time, chain-specific approval, HTTPS, and independent peers', () => { +test('mainnet gates require genesis time, chain-specific approval, HTTPS browser origin, and distinct peers', () => { const result = JSON.parse(script(` import { assertLaunchAllowed } from './server/launch.ts'; import { CHAIN_ID, GENESIS_TIME } from './server/testnet/consensus.ts'; @@ -61,7 +63,64 @@ test('mainnet gates require genesis time, chain-specific approval, HTTPS, and in } console.log(JSON.stringify(results)); `)); - assert.deepEqual(result, [false,false,false,false,false,false,false,true]); + assert.deepEqual(result, [false,false,false,false,false,false,true,true]); +}); +test('private mainnet cluster peers require explicit opt-in and preserve every other launch gate', () => { + const result = JSON.parse(script(` + import { assertLaunchAllowed } from './server/launch.ts'; + import { CHAIN_ID, GENESIS_TIME } from './server/testnet/consensus.ts'; + const env={WIKIDECK_MAINNET_LAUNCH:CHAIN_ID,WIKIDECK_MAINNET_RISK_ACK:'operator-accepts-unaudited-risk', + PUBLIC_ORIGIN:'https://wikideck.example',WIKIDECK_ALLOW_PRIVATE_PEERS:'1', + WIKIDECK_PEERS:'http://wikideck-peer-a.wikideck.svc.cluster.local:80,http://wikideck-peer-b.wikideck.svc.cluster.local:80'}; + const cases=[[{},GENESIS_TIME*1000], [{},GENESIS_TIME*1000-1], + [{WIKIDECK_ALLOW_PRIVATE_PEERS:'0'},Infinity], [{WIKIDECK_ALLOW_PRIVATE_PEERS:'true'},Infinity], + [{WIKIDECK_MAINNET_LAUNCH:'wrong'},Infinity], [{WIKIDECK_MAINNET_RISK_ACK:''},Infinity], + [{PUBLIC_ORIGIN:'http://wikideck.example'},Infinity], [{WIKIDECK_PEERS:''},Infinity], + [{WIKIDECK_PEERS:'http://peer-a:80,http://peer-a/'},Infinity], + [{WIKIDECK_PEERS:'http://peer-a,http://user:password@peer-b'},Infinity], + [{WIKIDECK_PEERS:'http://peer-a,http://peer-b/p2p'},Infinity], + [{WIKIDECK_PEERS:'http://peer-a,http://peer-b?x=1'},Infinity], + [{WIKIDECK_PEERS:'http://peer-a,https://wikideck.example'},Infinity], + [{WIKIDECK_PEERS:'http://peer-a,ftp://peer-b'},Infinity]]; + console.log(JSON.stringify(cases.map(([changes,now])=>{ + try {assertLaunchAllowed({...env,...changes},now);return true;}catch{return false;} + }))); + `)); + assert.deepEqual(result, [true, ...Array(13).fill(false)]); +}); +test('default mainnet genesis can launch on November 1 and matches browser, Docker and Compose identities', () => { + const result = JSON.parse(script(` + delete process.env.WIKIDECK_GENESIS_TIME; + delete process.env.WIKIDECK_IMAGE_NETWORK; + delete process.env.WIKIDECK_IMAGE_GENESIS_TIME; + const { assertLaunchAllowed, launchManifest } = await import('./server/launch.ts'); + const { expectedChainIdentity } = await import('./shared/chain-identity.ts'); + const manifest = launchManifest(); + assertLaunchAllowed({WIKIDECK_MAINNET_LAUNCH:manifest.chainId, + WIKIDECK_MAINNET_RISK_ACK:'operator-accepts-unaudited-risk',PUBLIC_ORIGIN:'https://wikideck.eom.dev', + WIKIDECK_ALLOW_PRIVATE_PEERS:'1', + WIKIDECK_PEERS:'http://wikideck-peer-a.wikideck.svc.cluster.local:80,http://wikideck-peer-b.wikideck.svc.cluster.local:80'}, + Date.UTC(2026,10,1)); + console.log(JSON.stringify({manifest,identity:await expectedChainIdentity()})); + `)); + assert.equal(result.manifest.genesisTime, 1_793_491_200); + assert.equal(result.manifest.chainId, result.identity.chainId); + assert.equal(result.manifest.genesis, result.identity.genesis); + const dockerfile = readFileSync(new URL('../Dockerfile', import.meta.url), 'utf8'); + assert.equal(dockerfile.match(/^ARG WIKIDECK_GENESIS_TIME=1793491200\r?$/gm)?.length, 2); + for (const name of ['compose.yaml', 'compose.mainnet.yaml']) { + const compose = readFileSync(new URL('../' + name, import.meta.url), 'utf8'); + assert.equal(compose.match(/WIKIDECK_GENESIS_TIME:-1793491200/g)?.length, 2); + } +}); +test('runtime cannot silently change the image-baked mainnet genesis', () => { + const result = spawnSync(process.execPath, ['--experimental-strip-types', 'server/index.ts', '--network-manifest'], { + env: { ...process.env, WIKIDECK_NETWORK: 'mainnet', WIKIDECK_GENESIS_TIME: '1793491200', + WIKIDECK_IMAGE_NETWORK: 'mainnet', WIKIDECK_IMAGE_GENESIS_TIME: '1798761600' }, + encoding: 'utf8', timeout: 30000, + }); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Runtime genesis time does not match/); }); test('mainnet ledger uses its isolated folder; testnet backup headers are rejected', async () => { const dir = await mkdtemp(join(tmpdir(), 'wikideck-network-')); @@ -91,7 +150,7 @@ test('operator preflight is read-only, fails closed, and does not disclose crede const dataDir = join(dir, 'must-not-be-created'); const secret = 'test-only-sensitive-value-must-not-appear'; const result = spawnSync(process.execPath, ['--experimental-strip-types', 'server/index.ts', '--launch-preflight'], { - env: { ...process.env, WIKIDECK_NETWORK: 'mainnet', WIKIDECK_GENESIS_TIME: '1798761600', WIKIDECK_DATA_DIR: dataDir, + env: { ...process.env, WIKIDECK_NETWORK: 'mainnet', WIKIDECK_GENESIS_TIME: '1793491200', WIKIDECK_DATA_DIR: dataDir, WIKIDECK_MAINNET_LAUNCH: '', WIKIDECK_MAINNET_RISK_ACK: '', PAYMENT_PROVIDER_API_KEY: secret }, encoding: 'utf8', timeout: 30000, });