import test from 'node:test'; import assert from 'node:assert/strict'; import { generateKeyPairSync, sign } from 'node:crypto'; import { mkdtemp, readFile, writeFile, rm, rename, mkdir, stat, truncate } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'; import dns from 'node:dns/promises'; import { DatabaseSync } from 'node:sqlite'; import { setTimeout as delay } from 'node:timers/promises'; import { Ledger } from '../server/testnet/ledger.ts'; import { PeerNetwork } from '../server/testnet/peers.ts'; import { CHAIN_ID, GENESIS_HASH, GENESIS_TIME, MAX_TARGET, address, makeTemplate, miningBlob, mintId, sha256, stateRoot, transferId, transition, workFor } from '../server/testnet/consensus.ts'; import { NETWORK, CONSENSUS, blockClaimMessage, nftTransferMessage, testnetOwnerOf, transactionCardIds, nftTradeMessage, type MiningTemplate, type NftState, type NftStateLayer, type NftStateView, type NftTrade, type NftTransfer, type TestnetBlock } from '../shared/testnet.ts'; import { deriveStats } from '../shared/enrichment.ts'; import type { PowEngine } from '../server/testnet/randomx.ts'; // Deliberately test-only: production has no fake PoW selector or fallback. class FakePow implements PowEngine { calls = 0; async hash(key: string, blob: Uint8Array): Promise { this.calls++; return sha256(Buffer.concat([Buffer.from(key, 'hex'), Buffer.from(blob)])); } close(): void {} } function identity() { const keys = generateKeyPairSync('ec', { namedCurve: 'prime256v1' }); const publicKey = keys.publicKey.export({ format: 'der', type: 'spki' }).toString('base64'); return { publicKey, owner: address(publicKey), sign: (message: string) => sign('sha256', Buffer.from(message), { key: keys.privateKey, dsaEncoding: 'ieee-p1363' }).toString('base64') }; } const alice = identity(), bob = identity(), carol = identity(); type Identity = ReturnType; const pow = new FakePow(); async function mine(template: MiningTemplate, miner: Identity = alice): Promise { const header = structuredClone(template.header); assert.ok(BigInt(`0x${header.target}`) <= BigInt(`0x${MAX_TARGET}`)); for (header.nonce = 0; header.nonce < 1_000_000; header.nonce++) { const blob = miningBlob(header); const powHash = await pow.hash(header.key, blob); if (BigInt(`0x${powHash}`) > BigInt(`0x${header.target}`)) continue; const hash = sha256(blob); return { ...header, hash, tokenId: mintId(header), powHash, signature: miner.sign(blockClaimMessage(hash)), transactions: structuredClone(template.transactions) }; } throw new Error('Test nonce search exhausted'); } type Branch = { blocks: TestnetBlock[]; state: NftStateView }; function branch(): Branch { return { blocks: [], state: {} }; } function articleFor(_ancestors: TestnetBlock[]) { const pageId = 50_000; return { pageId, title: `Test article ${pageId}`, extract: `A deterministic test article snapshot for Wikipedia page ${pageId}.`, image: 'https://upload.wikimedia.org/wikipedia/commons/test-image.jpg', url: `https://en.wikipedia.org/?curid=${pageId}`, revisionId: 1, source: 'live' as const, category: 'Culture' }; } function statsSnapshot() { const pageBytes = 2048, languageCount = 3; return { capturedAt: '2026-10-19T00:00:00.000Z', revisionId: 1, pageBytes, languageCount, languagesTruncated: false, pageviews: { status: 'unavailable' as const, months: [], latestMonth: '2026-09', total: null, dailyAverage: null, trendPercent: null }, stats: deriveStats(pageBytes, languageCount, null) }; } async function extend(chain: Branch, miner: Identity = alice, transactions: NftTransfer[] = [], spacing = 30): Promise { const now = GENESIS_TIME + (chain.blocks.length + 1) * spacing; const block = await mine(makeTemplate(chain.blocks, chain.state, miner.owner, miner.publicKey, transactions, articleFor(chain.blocks), statsSnapshot(), now), miner); chain.state = transition(chain.state, block, transactions); chain.blocks.push(block); return block; } function transfer(cardHash: string, from: Identity = alice, to: Identity = bob, sequence = 0): NftTransfer { const tx = { network: NETWORK, chainId: CHAIN_ID, cardHash, from: from.owner, to: to.owner, sequence, publicKey: from.publicKey }; return { ...tx, signature: from.sign(nftTransferMessage(tx)) }; } function trade(offered: TestnetBlock[], requested: TestnetBlock[], proposer: Identity = alice, counterparty: Identity = bob): NftTrade { const terms = { network: NETWORK, chainId: CHAIN_ID, proposer: proposer.owner, counterparty: counterparty.owner, offered: offered.map(block => ({ cardHash: block.tokenId, sequence: 0 })), requested: requested.map(block => ({ cardHash: block.tokenId, sequence: 0 })) }; const message = nftTradeMessage(terms); return { type: 'trade', ...terms, proposerPublicKey: proposer.publicKey, proposerSignature: proposer.sign(message), counterpartyPublicKey: counterparty.publicKey, counterpartySignature: counterparty.sign(message) }; } async function fixture(t: { after: (fn: () => Promise) => void }) { const dir = await mkdtemp(join(tmpdir(), 'wikideck-testnet-')); const engine = new FakePow(); const ledger = await Ledger.open(dir, engine); t.after(async () => { ledger.close(); engine.close(); await rm(dir, { recursive: true, force: true }); }); return { dir, ledger, engine, file: join(dir, 'testnet', 'ledger.sqlite') }; } test('testnet genesis is empty, manifest-bound, isolated and defensively copied', async t => { const { dir, ledger, file } = await fixture(t); const initial = ledger.snapshot(); assert.equal(initial.network, NETWORK); assert.equal(initial.chainId, CHAIN_ID); assert.equal(initial.genesis, GENESIS_HASH); assert.equal(initial.tip, GENESIS_HASH); assert.equal(initial.cumulativeWork, '0'); assert.equal(initial.algorithm, 'RandomX'); assert.equal(initial.maturity, 2); assert.equal(initial.peers, 0); assert.deepEqual(initial.blocks, []); assert.deepEqual(initial.transfers, []); assert.deepEqual(initial.pendingTransfers, []); assert.equal(stateRoot({}), sha256(`${NETWORK}:empty`)); await assert.rejects(readFile(join(dir, 'chain.json')), /ENOENT/); assert.equal((await stat(file)).isFile(), true); const database = new DatabaseSync(file); assert.equal((database.prepare('SELECT COUNT(*) AS count FROM blocks').get() as { count: number }).count, 0); database.close(); const candidate = await mine(ledger.template(alice.owner, alice.publicKey, articleFor([]), statsSnapshot())); assert.deepEqual(await ledger.submit(candidate), { accepted: true, reorg: false }); const persisted = new DatabaseSync(file); assert.equal((persisted.prepare('SELECT COUNT(*) AS count FROM blocks').get() as { count: number }).count, 1); persisted.close(); candidate.article.title = 'caller mutation'; const copy = ledger.block(candidate.hash)!; copy.article.title = 'getter mutation'; assert.notEqual(ledger.block(candidate.hash)!.article.title, copy.article.title); assert.equal(ledger.size, 1); const summary=ledger.summary(); assert.equal(summary.height,1);assert.equal(summary.tip,candidate.hash); const page=ledger.canonicalPage(summary.tip,0,1); assert.equal(page.blocks.length,1);assert.equal(page.next,null); page.blocks[0].article.title='mutated page'; assert.notEqual(ledger.canonicalPage(summary.tip,0).blocks[0].article.title,'mutated page'); assert.throws(()=>ledger.canonicalPage(GENESIS_HASH,0),/tip changed/); assert.throws(()=>ledger.canonicalPage(summary.tip,0,17),/Invalid/); assert.throws(()=>ledger.legacySnapshot(0),/Full snapshot disabled/); assert.equal(ledger.legacySnapshot(1).blocks.length,1); assert.throws(() => ledger.inventory(-1, 32)); assert.throws(() => ledger.inventory(0, 0)); }); test('timestamp attack boundaries reject at median and beyond future limit before proof work',async t=>{ const {ledger,engine}=await fixture(t); const now=GENESIS_TIME+CONSENSUS.targetSeconds*20; t.mock.method(Date,'now',()=>now*1000); const candidate=await mine(ledger.template(alice.owner,alice.publicKey,articleFor([]),statsSnapshot())); const calls=engine.calls; await assert.rejects(ledger.submit({...candidate,createdAt:GENESIS_TIME}),/timestamp/); await assert.rejects(ledger.submit({...candidate,createdAt:now+CONSENSUS.maxFutureSeconds+1}),/timestamp/); assert.equal(engine.calls,calls);assert.equal(ledger.size,0); // Full validation reaches commitment checks for the exact allowed future edge, not timestamp rejection. await assert.rejects(ledger.submit({...candidate,createdAt:now+CONSENSUS.maxFutureSeconds}),/block ID/); assert.equal(engine.calls,calls); }); test('DAG retains equal-work branches, serializes duplicates and selects strictly greater cumulative work', async t => { const { ledger, dir } = await fixture(t); t.mock.method(Date, 'now', () => (GENESIS_TIME + CONSENSUS.targetSeconds * 20) * 1000); const slow = branch(), fast = branch(); for (let i = 0; i < 12; i++) await ledger.submit(await extend(slow, alice, [], CONSENSUS.targetSeconds)); for (let i = 0; i < 10; i++) { const result = await ledger.submit(await extend(fast, bob, [], 1)); assert.equal(result.reorg, false); assert.equal(ledger.snapshot().tip, slow.blocks.at(-1)!.hash); } const hard = await extend(fast, bob, [], 1); assert.ok(workFor(hard.target) > workFor(slow.blocks.at(-1)!.target)); assert.equal(hard.height, 11); // A shorter, harder branch wins over height 12. const results = await Promise.all([ledger.submit(hard), ledger.submit(hard), ledger.submit(hard)]); assert.deepEqual(results, [{ accepted: true, reorg: true }, { accepted: false, reorg: false }, { accepted: false, reorg: false }]); assert.equal(ledger.snapshot().tip, hard.hash); assert.deepEqual(ledger.snapshot().blocks, fast.blocks); assert.equal(ledger.size, 23); assert.equal(ledger.snapshot().cumulativeWork, fast.blocks.reduce((n, b) => n + workFor(b.target), 0n).toString()); const stored = ledger.allBlocks; const seen = new Set([GENESIS_HASH]); for (const b of stored) { assert.ok(seen.has(b.previousHash)); seen.add(b.hash); } assert.deepEqual(ledger.inventory(10, 3), stored.slice(10, 13)); const restart = await Ledger.open(dir, new FakePow()); t.after(async () => { restart.close(); }); assert.deepEqual(restart.snapshot(), ledger.snapshot()); assert.deepEqual(restart.allBlocks, stored); }); test('equal-work ties preserve the current tip across restart and only extension triggers reorg', async t => { const { dir, ledger } = await fixture(t); const a = branch(), b = branch(); const a1 = await extend(a), b1 = await extend(b, bob); await ledger.submit(a1); assert.deepEqual(await ledger.submit(b1), { accepted: true, reorg: false }); assert.equal(ledger.snapshot().tip, a1.hash); const restart = await Ledger.open(dir, new FakePow()); t.after(async () => { restart.close(); }); assert.equal(restart.snapshot().tip, a1.hash); const b2 = await extend(b, bob); assert.deepEqual(await restart.submit(b2), { accepted: true, reorg: true }); assert.equal(restart.snapshot().tip, b2.hash); }); test('NFT maturity, ordered mempool, block commitments and replay protection', async t => { const { ledger, dir } = await fixture(t); const chain = branch(); const minted = await extend(chain); await ledger.submit(minted); const tx = transfer(minted.tokenId); await assert.rejects(ledger.submitTransfer(tx), /immature/); await assert.rejects(async () => ledger.submit(await extend({ blocks: [...chain.blocks], state: structuredClone(chain.state) }, alice, [tx])), /immature/); await ledger.submit(await extend(chain)); assert.equal(await ledger.submitTransfer(tx), transferId(tx)); const tx2 = transfer(minted.tokenId, bob, carol, 1); await Promise.all([ledger.submitTransfer(tx2), ledger.submitTransfer(tx)]); assert.deepEqual(ledger.snapshot().pendingTransfers, [tx, tx2]); assert.equal(testnetOwnerOf(ledger.snapshot(), minted.tokenId), alice.owner); // Unconfirmed is not ownership. const restarted = await Ledger.open(dir, new FakePow()); t.after(async () => { restarted.close(); }); assert.deepEqual(restarted.snapshot().pendingTransfers, [tx, tx2]); const block = await mine(restarted.template(alice.owner, alice.publicKey, articleFor(restarted.snapshot().blocks), statsSnapshot())); assert.deepEqual(block.transactions, [tx, tx2]); await restarted.submit(block); assert.equal(testnetOwnerOf(restarted.snapshot(), minted.tokenId), carol.owner); assert.deepEqual(restarted.snapshot().transfers, [tx, tx2]); assert.deepEqual(restarted.snapshot().pendingTransfers, []); await assert.rejects(restarted.submitTransfer(tx), /owner\/sequence/); await assert.rejects(restarted.submitTransfer(transfer(minted.tokenId, carol, alice, 0)), /owner\/sequence/); }); test('signed multi-card trade is validated, mempooled, mined, and applied atomically', async t => { const { ledger } = await fixture(t); const chain = branch(); const aliceFirst = await extend(chain, alice); await ledger.submit(aliceFirst); const bobFirst = await extend(chain, bob); await ledger.submit(bobFirst); const aliceSecond = await extend(chain, alice); await ledger.submit(aliceSecond); const bobSecond = await extend(chain, bob); await ledger.submit(bobSecond); await ledger.submit(await extend(chain, alice)); // All four NFTs are mature at the next inclusion height. const offered = [aliceFirst, aliceSecond], requested = [bobFirst, bobSecond]; const invalid = trade(offered, requested); const badTerms = { ...invalid, requested: invalid.requested.map((leg, index) => index === 1 ? { ...leg, sequence: 1 } : leg) }; const badMessage = nftTradeMessage(badTerms); const badSequence = { ...badTerms, proposerSignature: alice.sign(badMessage), counterpartySignature: bob.sign(badMessage) }; const before = ledger.snapshot(); await assert.rejects(ledger.submitTransaction(badSequence), /missing, stale, or immature/); const badSignature = { ...invalid, proposerSignature: bob.sign(nftTradeMessage(invalid)) }; await assert.rejects(ledger.submitTransaction(badSignature), /signature/i); assert.deepEqual(ledger.snapshot().pendingTransfers, []); for (const block of offered) assert.equal(testnetOwnerOf(before, block.tokenId), alice.owner); for (const block of requested) assert.equal(testnetOwnerOf(before, block.tokenId), bob.owner); const bundle = trade(offered, requested); await ledger.submitTransaction(bundle); assert.deepEqual(ledger.snapshot().pendingTransfers, [bundle]); const template = ledger.template(alice.owner, alice.publicKey, articleFor(ledger.snapshot().blocks), statsSnapshot()); assert.deepEqual(template.transactions, [bundle]); await ledger.submit(await mine(template)); const after = ledger.snapshot(); assert.equal(after.pendingTransfers.length, 0); for (const block of offered) assert.equal(testnetOwnerOf(after, block.tokenId), bob.owner); for (const block of requested) assert.equal(testnetOwnerOf(after, block.tokenId), alice.owner); assert.deepEqual(after.transfers.at(-1), bundle); for (const block of after.blocks) { const indexed = ledger.nft(block.tokenId)!; assert.equal(indexed.owner, testnetOwnerOf(after, block.tokenId)); assert.equal(indexed.sequence, after.transfers.filter(tx => transactionCardIds(tx).includes(block.tokenId)).length); } const deck = ledger.deck(alice.owner, 0, 2); assert.equal(deck.total, after.blocks.filter(block => testnetOwnerOf(after, block.tokenId) === alice.owner).length); assert.equal(deck.nfts.length, 2); assert.equal(deck.next, 2); assert.deepEqual(ledger.deck(alice.owner, deck.next!, 2).nfts.map(item => item.block.tokenId).sort(), ledger.deck(alice.owner).nfts.slice(2).map(item => item.block.tokenId).sort()); assert.deepEqual(ledger.deck(alice.owner).nfts.map(item => item.block.tokenId).sort(), after.blocks.filter(block => testnetOwnerOf(after, block.tokenId) === alice.owner) .map(block => block.tokenId).sort()); }); test('reorg rolls back NFT ownership and requeues only still-valid disconnected transfers in order', async t => { const { ledger, dir } = await fixture(t); const original = branch(); const minted = await extend(original); await ledger.submit(minted); await ledger.submit(await extend(original)); const fork: Branch = structuredClone(original); const tx = transfer(minted.tokenId); const tx2 = transfer(minted.tokenId, bob, carol, 1); const committed = await extend(original, alice, [tx, tx2]); await ledger.submit(committed); const orphanTx = transfer(committed.tokenId); await ledger.submit(await extend(original)); await ledger.submitTransfer(orphanTx); for (let i = 0; i < 3; i++) await ledger.submit(await extend(fork, bob)); const snapshot = ledger.snapshot(); assert.equal(snapshot.tip, fork.blocks.at(-1)!.hash); assert.equal(testnetOwnerOf(snapshot, minted.tokenId), alice.owner); assert.equal(testnetOwnerOf(snapshot, committed.tokenId), undefined); assert.deepEqual(snapshot.transfers, []); assert.deepEqual(snapshot.pendingTransfers, [tx, tx2]); // Orphan issuance is discarded. assert.equal(ledger.nft(minted.tokenId)!.owner, alice.owner); assert.equal(ledger.nft(minted.tokenId)!.sequence, 0); assert.equal(ledger.nft(committed.tokenId), undefined); assert.equal(ledger.canonicalTransaction(transferId(tx)), undefined); assert.ok(ledger.block(committed.hash)); const restored = await Ledger.open(dir, new FakePow()); t.after(async () => { restored.close(); }); assert.deepEqual(restored.snapshot(), snapshot); assert.equal(restored.canonicalHead(), ledger.canonicalHead()); assert.deepEqual(restored.canonicalEvents(0), ledger.canonicalEvents(0)); const next = await mine(ledger.template(bob.owner, bob.publicKey, articleFor(ledger.snapshot().blocks), statsSnapshot()), bob); await ledger.submit(next); assert.equal(testnetOwnerOf(ledger.snapshot(), minted.tokenId), carol.owner); assert.equal(ledger.nft(minted.tokenId)!.owner, carol.owner); assert.equal(ledger.nft(minted.tokenId)!.sequence, 2); }); test('canonical event pages record reorg order durably but exclude side branches, duplicates and mempool', async t => { const { ledger, file, dir } = await fixture(t), original = branch(); const first = await extend(original); await ledger.submit(first); const second = await extend(original); await ledger.submit(second); const fork: Branch = structuredClone(original); const tx = transfer(first.tokenId); await ledger.submitTransfer(tx); const beforePending = ledger.canonicalHead(); assert.equal(beforePending, 2); const third = await extend(original, alice, [tx]); await ledger.submit(third); assert.equal(ledger.canonicalTransaction(transferId(tx))?.blockHash, third.hash); const competing = await extend(fork, bob); await ledger.submit(competing); assert.equal(ledger.canonicalHead(), 3); // Equal-work branch does not change canonical state. const winner = await extend(fork, bob); await ledger.submit(winner); assert.equal(ledger.canonicalHead(), 6); assert.equal(ledger.canonicalTransaction(transferId(tx)), undefined); const events = ledger.canonicalEvents(0).events; assert.deepEqual(events.map(e => [e.type, e.hash]), [ ['connect', first.hash], ['connect', second.hash], ['connect', third.hash], ['disconnect', third.hash], ['connect', competing.hash], ['connect', winner.hash], ]); assert.deepEqual(events[3].transactionIds, [transferId(tx)]); const page = ledger.canonicalEvents(2, 1); assert.equal(page.cursor, 3); assert.equal(page.caughtUp, false); assert.equal(page.head, 6); page.events[0].transactionIds.length = 0; assert.equal(ledger.canonicalEvents(2, 1).events[0].transactionIds.length, 1); for (const [after, limit] of [[-1, 1], [7, 1], [0, 65], [0, 0], [0.5, 1]]) { assert.throws(() => ledger.canonicalEvents(after, limit), /cursor/); } await ledger.submit(winner); assert.equal(ledger.canonicalHead(), 6); ledger.close(); const reopened = await Ledger.open(dir, new FakePow()); try { assert.equal(reopened.canonicalHead(), 6); assert.deepEqual(reopened.canonicalEvents(0).events, events); const db = new DatabaseSync(file); try { assert.throws(() => db.exec("UPDATE canonical_events SET type='connect'"), /Immutable/); } finally { db.close(); } } finally { reopened.close(); } }); test('block, mempool, canonical indexes and events roll back together on storage failure', async t => { const { ledger, file } = await fixture(t), chain = branch(); const candidate = await extend(chain), db = new DatabaseSync(file); try { db.exec("CREATE TRIGGER fail_event BEFORE INSERT ON canonical_events BEGIN SELECT RAISE(ABORT,'injected storage failure'); END"); await assert.rejects(ledger.submit(candidate), /injected storage failure/); assert.equal(ledger.size, 0); assert.equal(ledger.canonicalHead(), 0); assert.equal(ledger.nft(candidate.tokenId), undefined); assert.equal((db.prepare('SELECT COUNT(*) AS n FROM blocks').get() as { n: number }).n, 0); db.exec('DROP TRIGGER fail_event'); await ledger.submit(candidate); assert.equal(ledger.nft(candidate.tokenId)!.owner, alice.owner); assert.equal(ledger.canonicalHead(), 1); } finally { db.close(); } }); test('validated existing ledgers backfill indexes once and refuse corrupt ownership or event history', async t => { const { ledger, file, dir } = await fixture(t), chain = branch(); await ledger.submit(await extend(chain)); await ledger.submit(await extend(chain)); const expected = ledger.snapshot(); ledger.close(); const db = new DatabaseSync(file); try { // Simulate the prior schema: preserve original blocks/metadata/pending exactly, remove only new derived tables. db.exec('DROP TABLE canonical_undo; DROP TABLE canonical_transactions; DROP TABLE canonical_nfts; DROP TABLE canonical_blocks; DROP TABLE canonical_events; DROP TABLE canonical_meta;'); } finally { db.close(); } const migrated = await Ledger.open(dir, new FakePow()); assert.deepEqual(migrated.snapshot(), expected); assert.equal(migrated.canonicalHead(), 2); migrated.close(); const corrupt = new DatabaseSync(file); try { corrupt.prepare('UPDATE canonical_nfts SET owner=? WHERE token=?').run(bob.owner, chain.blocks[0].tokenId); } finally { corrupt.close(); } await assert.rejects(Ledger.open(dir, new FakePow()), /ownership index mismatch/); const repair = new DatabaseSync(file); try { repair.prepare('UPDATE canonical_nfts SET owner=? WHERE token=?').run(alice.owner, chain.blocks[0].tokenId); repair.exec('DROP TRIGGER canonical_events_no_update'); repair.exec("UPDATE canonical_events SET transactions_json='[\"wrong\"]' WHERE sequence=1"); } finally { repair.close(); } await assert.rejects(Ledger.open(dir, new FakePow()), /event integrity mismatch/); }); test('reorg removes mempool conflicts and does not requeue already committed transfers', async t => { const { ledger } = await fixture(t); const a = branch(); const minted = await extend(a); await ledger.submit(minted); await ledger.submit(await extend(a)); const b: Branch = structuredClone(a); const tx = transfer(minted.tokenId); await ledger.submit(await extend(a, alice, [tx])); await ledger.submitTransfer(transfer(minted.tokenId, bob, alice, 1)); await ledger.submit(await extend(b, bob, [tx, transfer(minted.tokenId, bob, carol, 1)])); await ledger.submit(await extend(b, bob)); assert.deepEqual(ledger.snapshot().pendingTransfers, []); assert.equal(testnetOwnerOf(ledger.snapshot(), minted.tokenId), carol.owner); }); test('invalid parent, manifest, proof, article/stat snapshots, and commitments leave ledger unchanged', async t => { const { ledger } = await fixture(t); const valid = await extend(branch()); assert.notEqual(mintId(valid), mintId({ ...valid, statsSnapshot: { ...valid.statsSnapshot, capturedAt: '2026-10-19T00:00:01.000Z' } })); for (const changed of [ { previousHash: 'aa'.repeat(32) }, { chainId: 'bb'.repeat(32) }, { network: 'another-network' }, { article: { ...valid.article, extract: 'tampered snapshot' } }, { article: { ...valid.article, image: 'https://evil.example/image.jpg' } }, { article: { ...valid.article, image: undefined } }, { statsSnapshot: { ...valid.statsSnapshot, capturedAt: '2026-10-19T00:00:01.000Z' } }, { statsSnapshot: { ...valid.statsSnapshot, stats: { ...valid.statsSnapshot.stats, depth: 0 } } }, { stateRoot: '00'.repeat(32) }, { transactionsRoot: '00'.repeat(32) }, { tokenId: '00'.repeat(32) }, { powHash: '00'.repeat(32) }, { nonce: valid.nonce + 1 }, { signature: bob.sign(blockClaimMessage(valid.hash)) } ]) { await assert.rejects(ledger.submit({ ...valid, ...changed })); assert.equal(ledger.size, 0); } await assert.rejects(ledger.submitTransfer({ ...transfer('aa'.repeat(32)), chainId: 'bb'.repeat(32) })); await ledger.submit(valid); // Queue recovers from failed validations. const repeated = await mine(ledger.template(alice.owner, alice.publicKey, valid.article, valid.statsSnapshot)); assert.equal(repeated.article.pageId, valid.article.pageId); assert.notEqual(repeated.tokenId, valid.tokenId); await ledger.submit(repeated); assert.equal(ledger.size, 2); }); test('block state is stored as a one-entry delta that shares its parent state layer', async () => { const parent: NftState = Object.assign(Object.create(null) as NftState, { ['a'.repeat(64)]: { owner: alice.owner, sequence: 0, mintedHeight: 1, pageId: 50_000 }, }); const chain: Branch = { blocks: [], state: parent }; await extend(chain); const layer = chain.state as NftStateLayer; assert.equal(layer.kind, 'layer'); assert.equal(layer.parent, parent); assert.equal(Object.keys(layer.changes).length, 1); }); test('SQLite startup rechecks every proof and fails closed on database corruption or manifest mismatch', async t => { const { dir, ledger, file } = await fixture(t); const chain = branch(); await ledger.submit(await extend(chain)); await ledger.submit(await extend(chain)); const checking = new FakePow(); const verified = await Ledger.open(dir, checking); t.after(async () => { verified.close(); }); assert.equal(checking.calls, 2); const originalDb = new DatabaseSync(file); const row = originalDb.prepare('SELECT hash, block_json FROM blocks ORDER BY sequence LIMIT 1').get() as { hash: string; block_json: string }; const originalBlock = JSON.parse(row.block_json) as TestnetBlock; originalDb.close(); verified.close(); ledger.close(); const malformed = new DatabaseSync(file); malformed.prepare('UPDATE blocks SET block_json = ? WHERE hash = ?').run('{truncated', row.hash); malformed.close(); await assert.rejects(Ledger.open(dir, new FakePow())); const restoreMalformed = new DatabaseSync(file); restoreMalformed.prepare('UPDATE blocks SET block_json = ? WHERE hash = ?').run(row.block_json, row.hash); restoreMalformed.close(); const tampered = new DatabaseSync(file); tampered.prepare('UPDATE blocks SET block_json = ? WHERE hash = ?') .run(JSON.stringify({ ...originalBlock, stateRoot: '00'.repeat(32) }), row.hash); tampered.close(); await assert.rejects(Ledger.open(dir, new FakePow()), /state commitment/); const restoreTampered = new DatabaseSync(file); restoreTampered.prepare('UPDATE blocks SET block_json = ? WHERE hash = ?').run(row.block_json, row.hash); restoreTampered.prepare("UPDATE metadata SET value = 'wrong' WHERE key = 'network'").run(); restoreTampered.close(); await assert.rejects(Ledger.open(dir, new FakePow()), /mismatch/); const restoreManifest = new DatabaseSync(file); restoreManifest.prepare('UPDATE metadata SET value = ? WHERE key = ?').run(NETWORK, 'network'); restoreManifest.close(); await assert.rejects(Ledger.open(dir, { hash: async () => 'ff'.repeat(32), close() {} }), /RandomX proof/); }); test('failed SQLite transaction never commits memory, and later mutations recover', async t => { const { ledger, file } = await fixture(t); const database = new DatabaseSync(file); database.exec("CREATE TRIGGER reject_block BEFORE INSERT ON blocks BEGIN SELECT RAISE(ABORT, 'test persistence failure'); END;"); database.close(); const valid = await extend(branch()); await assert.rejects(ledger.submit(valid)); assert.equal(ledger.size, 0); assert.equal(ledger.snapshot().tip, GENESIS_HASH); const removeTrigger = new DatabaseSync(file); removeTrigger.exec('DROP TRIGGER reject_block'); removeTrigger.close(); await ledger.submit(valid); assert.equal(ledger.size, 1); }); test('legacy JSON ledger imports once into SQLite without altering its source file', async t => { const dir = await mkdtemp(join(tmpdir(), 'wikideck-legacy-ledger-')); const block = await extend(branch()); const original = { version: 1, network: NETWORK, chainId: CHAIN_ID, blocks: [block], pendingTransfers: [] }; const legacyFile = join(dir, 'testnet', 'ledger.json'); await mkdir(join(dir, 'testnet'), { recursive: true }); await writeFile(legacyFile, JSON.stringify(original)); const ledger = await Ledger.open(dir, new FakePow()); t.after(async () => { ledger.close(); await rm(dir, { recursive: true, force: true }); }); assert.deepEqual(ledger.snapshot().blocks, [block]); assert.deepEqual(JSON.parse(await readFile(legacyFile, 'utf8')), original); const database = new DatabaseSync(join(dir, 'testnet', 'ledger.sqlite')); assert.equal((database.prepare("SELECT value FROM metadata WHERE key = 'legacy_json_migrated'").get() as { value: string }).value, '1'); database.close(); }); test('mempool is ordered, capped at 256, and templates include at most 64 without new issuance', async t => { const { ledger, dir } = await fixture(t); const chain = branch(); const minted = await extend(chain); await ledger.submit(minted); await ledger.submit(await extend(chain)); const transactions = Array.from({ length: 256 }, (_, sequence) => transfer(minted.tokenId, sequence % 2 === 0 ? alice : bob, sequence % 2 === 0 ? bob : alice, sequence)); await Promise.all(transactions.map(tx => ledger.submitTransfer(tx))); assert.deepEqual(ledger.snapshot().pendingTransfers, transactions); await assert.rejects(ledger.submitTransfer(transfer(minted.tokenId, alice, bob, 256)), /256/); const template = ledger.template(alice.owner, alice.publicKey, articleFor(ledger.snapshot().blocks), statsSnapshot()); assert.deepEqual(template.transactions, transactions.slice(0, 64)); await assert.rejects(ledger.submitTransfer(transfer(mintId(template.header))), /256/); const block = await mine(template); await ledger.submit(block); assert.deepEqual(ledger.snapshot().pendingTransfers, transactions.slice(64)); await assert.rejects(ledger.submitTransfer(transfer(block.tokenId)), /immature/); const restarted = await Ledger.open(dir, new FakePow()); t.after(async () => { restarted.close(); }); assert.deepEqual(restarted.snapshot(), ledger.snapshot()); }); async function server(t: { after: (fn: () => Promise) => void }, handler: (req: IncomingMessage, res: ServerResponse) => void) { const instance = createServer(handler); await new Promise(resolve => instance.listen(0, '127.0.0.1', resolve)); t.after(() => new Promise((resolve, reject) => { instance.close(error => error ? reject(error) : resolve()); instance.closeAllConnections(); })); const addr = instance.address(); assert.ok(addr && typeof addr !== 'string'); return `http://127.0.0.1:${addr.port}`; } function json(res: ServerResponse, body: unknown) { res.setHeader('content-type', 'application/json'); res.end(JSON.stringify(body)); } async function until(check: () => boolean) { const deadline = Date.now() + 5000; while (!check()) { if (Date.now() > deadline) assert.fail('Peer operation timed out'); await delay(10); } } test('peer configuration rejects unsafe origins; explicit private permission enables local sync', async t => { const { ledger } = await fixture(t); for (const url of ['http://example.com', 'https://localhost', 'https://foo.localhost', 'https://127.1', 'https://10.1.2.3', 'https://169.254.169.254', 'https://[::1]', 'https://[::ffff:127.0.0.1]', 'https://[fc00::1]', 'https://[fe80::1]', 'https://[2002:7f00:1::]', 'https://example.com/path', 'https://user:pass@example.com', 'https://example.com/?q=1', 'https://example.com/#fragment', 'https://example.com/?', 'https://example.com/#', 'https://example.com/path/..', 'https://@example.com', 'ftp://example.com']) { assert.throws(() => new PeerNetwork({ ledger, urls: [url], allowPrivate: false }), url); } const chain = branch(); const b = await extend(chain); const paths: string[] = []; const url = await server(t, (req, res) => { paths.push(req.url!); if (req.url === '/p2p/status') json(res, { network: NETWORK, chainId: CHAIN_ID, size: 1 }); else if (req.url === '/p2p/blocks?offset=0') json(res, { blocks: [b], next: null }); else if (req.url === '/p2p/mempool') json(res, { transactions: [] }); else json(res, { accepted: true }); }); const network = new PeerNetwork({ ledger, urls: [url], allowPrivate: true }); t.after(async () => network.close()); network.start(); network.start(); await until(() => network.status()[0].connected); assert.equal(ledger.size, 1); assert.equal(ledger.snapshot().peers, 1); await network.broadcastBlock(b); assert.ok(paths.includes('/p2p/block')); await assert.rejects(network.broadcastTransfer(transfer(b.tokenId)), /unaccepted/); network.close(); assert.equal(ledger.snapshot().peers, 0); }); test('unknown SQLite files are rejected without adding or replacing tables', async t => { const dir = await mkdtemp(join(tmpdir(), 'wikideck-foreign-sqlite-')); const file = join(dir, 'testnet', 'ledger.sqlite'); await mkdir(join(dir, 'testnet'), { recursive: true }); const foreign = new DatabaseSync(file); foreign.exec('CREATE TABLE operator_data(value TEXT NOT NULL); INSERT INTO operator_data VALUES(\'keep\');'); foreign.close(); t.after(async () => { await rm(dir, { recursive: true, force: true }); }); await assert.rejects(Ledger.open(dir, new FakePow()), /metadata missing/); const verify = new DatabaseSync(file); assert.deepEqual((verify.prepare("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name").all() as { name: string }[]) .map(row => row.name), ['operator_data']); assert.equal((verify.prepare('SELECT value FROM operator_data').get() as { value: string }).value, 'keep'); verify.close(); }); test('peers verify DNS, manifests, pagination, payloads and consensus; redirects are not followed', async t => { const { ledger } = await fixture(t); const chain = branch(); const b = await extend(chain); let redirected = false; for (const mode of ['manifest', 'pagination', 'proof', 'oversized', 'redirect'] as const) { const url = await server(t, (req, res) => { if (req.url === '/redirect-target') { redirected = true; json(res, {}); return; } if (mode === 'redirect') { res.writeHead(302, { location: '/redirect-target' }); res.end(); return; } if (mode === 'oversized') { res.setHeader('content-type', 'application/json'); res.end(' '.repeat(2 * 1024 * 1024 + 1)); return; } if (req.url === '/p2p/status') json(res, { network: NETWORK, chainId: mode === 'manifest' ? 'bad' : CHAIN_ID, size: 1 }); else json(res, mode === 'pagination' ? { blocks: [b], next: 0 } : { blocks: [{ ...b, powHash: '00'.repeat(32) }], next: null }); }); const network = new PeerNetwork({ ledger, urls: [url], allowPrivate: true }); network.start(); await until(() => Boolean(network.status()[0].error)); assert.equal(network.status()[0].connected, false); assert.equal(ledger.size, 0); network.close(); } assert.equal(redirected, false); // Deterministic DNS rebinding defense test: no external resolver/network dependency. const lookupMock = t.mock.method(dns, 'lookup', async () => [ { address: '93.184.216.34', family: 4 }, { address: '127.0.0.1', family: 4 }, ]); const dnsNetwork = new PeerNetwork({ ledger, urls: ['https://peer.example'], allowPrivate: false }); t.after(async () => dnsNetwork.close()); dnsNetwork.start(); await until(() => Boolean(dnsNetwork.status()[0].error)); assert.equal(dnsNetwork.status()[0].connected, false); assert.match(dnsNetwork.status()[0].error!, /non-public/); lookupMock.mock.restore(); }); test('peer sync pages at most 32 blocks per pass, preserves cursor and skips known blocks', async t => { const { ledger, engine } = await fixture(t); const chain = branch(); for (let i = 0; i < 33; i++) await extend(chain, alice, [], 30); await ledger.submit(chain.blocks[0]); const offsets: number[] = []; const url = await server(t, (req, res) => { if (req.url === '/p2p/status') json(res, { network: NETWORK, chainId: CHAIN_ID, size: chain.blocks.length }); else if (req.url === '/p2p/mempool') json(res, { transactions: [] }); else { const offset = Number(new URL(req.url!, 'http://localhost').searchParams.get('offset')); offsets.push(offset); const blocks = chain.blocks.slice(offset, offset + 32); json(res, { blocks, next: offset + blocks.length < chain.blocks.length ? offset + blocks.length : null }); } }); const network = new PeerNetwork({ ledger, urls: [url], allowPrivate: true, intervalMs: 1000 }); t.after(async () => network.close()); network.start(); await until(() => network.status()[0].connected); assert.equal(ledger.size, 32); assert.deepEqual(offsets, [0]); await until(() => ledger.size === 33); assert.deepEqual(offsets, [0, 32]); assert.equal(engine.calls, 33); // The already-known first block was not revalidated. assert.deepEqual(ledger.snapshot().blocks, chain.blocks); assert.equal(CONSENSUS.maxTransactions, 64); });