Working version Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
2026-11-01 22:07:51 +00:00
parent 1edf2e6200
commit 636f5b9c77
80 changed files with 4065 additions and 549 deletions
+59
View File
@@ -0,0 +1,59 @@
import { DatabaseSync } from 'node:sqlite';
import { mkdtemp, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { performance } from 'node:perf_hooks';
import { CanonicalIndex } from '../server/testnet/canonical-index.ts';
import { CHAIN_ID, GENESIS_HASH, sha256, transactionId } from '../server/testnet/consensus.ts';
import { NETWORK, type TestnetBlock, type NftState, type NftTransfer } from '../shared/testnet.ts';
// Synthetic LOCAL SQL dataset only. Not valid mined blocks; never submitted to Ledger or a peer.
// This measures derived-index costs, not RandomX validation, consensus security or production SLOs.
const text = process.argv[2] ?? '10000';
if (!/^[1-9]\d{0,5}$/.test(text) || Number(text) > 100000) throw new Error('Fixture size must be 1..100000');
const count = Number(text), queries = 1000;
const directory = await mkdtemp(join(tmpdir(), 'wikideck-index-benchmark-'));
const file = join(directory, 'fixture.sqlite'), db = new DatabaseSync(file);
try {
db.exec('PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA foreign_keys=ON; CREATE TABLE blocks(hash TEXT PRIMARY KEY,block_json TEXT NOT NULL)');
const index = new CanonicalIndex(db), chain: TestnetBlock[] = [], state: NftState = Object.create(null);
index.initialize([], state);
const owners = Array.from({ length: 16 }, (_, i) => sha256(`fixture-owner-${i}`));
const insert = db.prepare('INSERT INTO blocks VALUES(?,?)');
const start = performance.now();
db.exec('BEGIN IMMEDIATE');
try {
for (let height = 1; height <= count; height++) {
const hash = sha256(`fixture-block-${height}`), token = sha256(`fixture-token-${height}`), owner = owners[height % owners.length];
const txs: NftTransfer[] = height > 2 ? [{ network: NETWORK, chainId: CHAIN_ID, cardHash: chain[height - 3].tokenId,
from: state[chain[height - 3].tokenId].owner, to: owner, sequence: 0, publicKey: '', signature: '' }] : [];
const block = { height, hash, tokenId: token, previousHash: chain.at(-1)?.hash ?? GENESIS_HASH, owner,
article: { title: `Synthetic index fixture ${height}`, pageId: height, extract: 'x'.repeat(512) }, transactions: txs } as unknown as TestnetBlock;
state[token] = { owner, sequence: 0, mintedHeight: height, pageId: height };
for (const tx of txs) state[tx.cardHash] = { ...state[tx.cardHash], owner: tx.to, sequence: 1 };
insert.run(hash, JSON.stringify(block)); index.connect(block); chain.push(block);
}
db.exec('COMMIT');
} catch (error) { db.exec('ROLLBACK'); throw error; }
const buildMs = performance.now() - start;
const latency: number[] = [];
for (let i = 0; i < queries; i++) {
const block = chain[(i * 7919) % count], before = performance.now();
const nft = index.nft(block.tokenId, count);
if (nft?.sequence !== state[block.tokenId].sequence || nft.owner !== state[block.tokenId].owner) throw new Error('Fixture ownership mismatch');
index.deck(owners[i % owners.length], 0, 24, count);
if (block.transactions.length && !index.transaction(transactionId(block.transactions[0]))) throw new Error('Fixture transaction missing');
latency.push(performance.now() - before);
}
let cursor = 0, batches = 0;
const consumeStart = performance.now();
do { const page = index.events(cursor, 64); cursor = page.cursor; batches++; } while (cursor < index.head());
const eventReadMs = performance.now() - consumeStart;
const verifyStart = performance.now(); index.initialize(chain, state); const verifyMs = performance.now() - verifyStart;
const bytes = (await stat(file)).size + (await stat(file + '-wal')).size;
latency.sort((a, b) => a - b);
console.log(JSON.stringify({ fixture: 'synthetic-derived-index-only', blocks: count, nfts: count,
transactions: Math.max(0, count - 2), queries, buildMs, lookupGroupP50Ms: latency[499], lookupGroupP95Ms: latency[949],
eventBatches: batches, maxEventsPerBatch: 64, eventReadMs, startupVerifyMs: verifyMs, databaseAndWalBytes: bytes,
warning: 'No PoW, HTTP load, real assets, target hardware or production SLO certification.' }, null, 2));
} finally { db.close(); await rm(directory, { recursive: true, force: true }); }
+48
View File
@@ -0,0 +1,48 @@
import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { arch, platform } from 'node:os';
import { RandomX } from '../server/testnet/randomx.ts';
import { MAX_TARGET, sha256 } from '../server/testnet/consensus.ts';
import { expectedHashes, targetForHashrate } from '../server/analysis/calibration.ts';
const args=process.argv.slice(2),known=new Set(['--consent','--fast','--hashes','--network-hps']);
for(let i=0;i<args.length;i++) {
if(!known.has(args[i]))throw new Error('Unknown option');
if(args[i]==='--hashes'||args[i]==='--network-hps')i++;
}
if(!args.includes('--consent'))throw new Error('Explicit --consent required: native RandomX consumes CPU, energy and ~256 MiB light / >2.3 GiB fast. No chain/network access.');
const number=(name:string,fallback:number,min:number,max:number)=>{
const i=args.indexOf(name),text=i<0?String(fallback):args[i+1];
const value=Number(text);
if(!text||!/^\d+$/.test(text)||!Number.isSafeInteger(value)||value<min||value>max)throw new Error(`Invalid ${name}`);
return value;
};
const count=number('--hashes',32,4,256),networkHps=number('--network-hps',1,1,1_000_000_000_000);
const binary=resolve(process.env.RANDOMX_BINARY??`native/build/wikideck-randomx${process.platform==='win32'?'.exe':''}`);
// Fingerprint the exact executable; it must come from the trusted pinned build, never a download URL.
const binarySha256=createHash('sha256').update(await readFile(binary)).digest('hex');
const engine=new RandomX({binary,fast:args.includes('--fast')});
const stop=()=>engine.close();process.once('SIGINT',stop);process.once('SIGTERM',stop);
const deadline=setTimeout(stop,60000);deadline.unref();
try {
const key=sha256('wikideck-offline-benchmark-epoch-a'),otherKey=sha256('wikideck-offline-benchmark-epoch-b');
const blob=Buffer.alloc(40);Buffer.from(sha256('wikideck-offline-benchmark-blob'),'hex').copy(blob);
let start=performance.now();const first=await engine.hash(key,blob);const coldMs=performance.now()-start;
start=performance.now();if(await engine.hash(key,blob)!==first)throw new Error('Repeated reference hash differs');
const repeatMs=performance.now()-start;
const samples:number[]=[];start=performance.now();
for(let i=0;i<count;i++) {
blob.writeBigUInt64LE(BigInt(i+1),32);const t=performance.now();await engine.hash(key,blob);samples.push(performance.now()-t);
}
const warmMs=performance.now()-start,warmHps=count/(warmMs/1000);
start=performance.now();await engine.hash(otherKey,blob);const epochSwitchMs=performance.now()-start;
samples.sort((a,b)=>a-b);
console.log(JSON.stringify({schema:'wikideck-randomx-benchmark-v1',mode:args.includes('--fast')?'fast':'light',
platform:platform(),arch:arch(),node:process.version,binarySha256,hashes:count,coldMs,repeatMs,warmMs,warmHashesPerSecond:warmHps,
p50HashMs:samples[Math.floor(samples.length*.5)],p95HashMs:samples[Math.min(samples.length-1,Math.floor(samples.length*.95))],epochSwitchMs,
currentInitialExpectedHashes:expectedHashes(MAX_TARGET),currentInitialExpectedSecondsAtMeasuredSingleWorker:expectedHashes(MAX_TARGET)/warmHps,
assumedNetworkHashesPerSecond:networkHps,illustrativeTargetForAssumedNetwork:targetForHashrate(networkHps),
note:'Local single-worker IPC-inclusive warm hash benchmark, not a network/miner-throughput estimate. Cold includes initialization; epoch switch is one cache change. Network H/s is an explicit assumption, not extrapolated from this machine.',
consensusChanged:false,productionReady:false},null,2));
} finally {clearTimeout(deadline);engine.close();process.off('SIGINT',stop);process.off('SIGTERM',stop);}
+4
View File
@@ -0,0 +1,4 @@
import { calibrationReport } from '../server/analysis/calibration.ts';
if (process.argv.slice(2).length) throw new Error('No options accepted. This deterministic analysis never selects launch parameters.');
console.log(JSON.stringify(calibrationReport(),null,2));
+20
View File
@@ -0,0 +1,20 @@
import { spawn } from 'node:child_process';
const env = { ...process.env, WIKIDECK_NETWORK: 'testnet', WIKIDECK_GENESIS_TIME: '1792411200',
VITE_WIKIDECK_NETWORK: 'testnet', VITE_WIKIDECK_GENESIS_TIME: '1792411200' };
const children = [
spawn(process.execPath, ['--import', 'tsx', 'server/index.ts'], { env, stdio: 'inherit', shell: false }),
spawn(process.execPath, ['node_modules/vite/bin/vite.js', '--host', '127.0.0.1'], { env, stdio: 'inherit', shell: false }),
];
let stopping = false;
const stop = code => {
if (stopping) return;
stopping = true;
for (const child of children) if (child.exitCode === null) child.kill('SIGTERM');
process.exitCode = code;
};
for (const child of children) {
child.once('error', () => stop(1));
child.once('exit', code => stop(code ?? 1));
}
for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => stop(0));
+10
View File
@@ -0,0 +1,10 @@
import { spawn } from 'node:child_process';
const env = { ...process.env, WIKIDECK_NETWORK: 'testnet', WIKIDECK_GENESIS_TIME: '1792411200' };
const child = spawn(process.execPath, ['--import', 'tsx', 'server/testnet/miner.ts', ...process.argv.slice(2)], {
env, stdio: 'inherit', shell: false,
});
child.once('error', () => { process.exitCode = 1; });
child.once('exit', (code, signal) => {
process.exitCode = code ?? (signal ? 1 : 0);
});
+12
View File
@@ -0,0 +1,12 @@
import { createBackup, verifyBackup, restoreBackup } from '../server/recovery.ts';
const [action,...args]=process.argv.slice(2);
if(!['create','verify','restore'].includes(action))throw new Error('Action must be create, verify or restore');
const allowed=new Set(['--source','--destination','--consent']);
for(let i=0;i<args.length;i++){if(!allowed.has(args[i]))throw new Error('Unknown option');if(args[i]!=='--consent')i++;}
const arg=(key:string)=>{const i=args.indexOf(key);return i<0?undefined:args[i+1];};
const source=arg('--source'),destination=arg('--destination');
if(!source||source.startsWith('--'))throw new Error('Explicit --source required');
if(action!=='verify'&&(!args.includes('--consent')||!destination||destination.startsWith('--')))throw new Error('Explicit --consent and new --destination required; stop the node first');
const result=action==='create'?await createBackup(source,destination!):action==='restore'?await restoreBackup(source,destination!):await verifyBackup(source);
console.log(JSON.stringify(result,null,2));
+11
View File
@@ -0,0 +1,11 @@
import { checkOperations } from '../server/operations-check.ts';
const args=process.argv.slice(2);
if(args.length!==2||args[0]!=='--port'||!/^\d{1,5}$/.test(args[1])||Number(args[1])<1||Number(args[1])>65535)throw new Error('Explicit --port 1..65535 required; loopback diagnostics only.');
const response=await fetch(`http://127.0.0.1:${Number(args[1])}/status`,{redirect:'error',signal:AbortSignal.timeout(5000)});
if(!response.ok||!response.body||!/^application\/json(?:;|$)/i.test(response.headers.get('content-type')??''))throw new Error('Local diagnostics unavailable');
const reader=response.body.getReader(),chunks:Uint8Array[]=[];let bytes=0;
try{for(;;){const part=await reader.read();if(part.done)break;bytes+=part.value.byteLength;if(bytes>65536)throw new Error('Diagnostics too large');chunks.push(part.value);}}
finally{await reader.cancel();reader.releaseLock();}
const joined=new Uint8Array(bytes);let offset=0;for(const chunk of chunks){joined.set(chunk,offset);offset+=chunk.length;}
const report=checkOperations(JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(joined)));
console.log(JSON.stringify(report,null,2));if(!report.healthy)process.exitCode=1;
+33
View File
@@ -0,0 +1,33 @@
export {};
// Opt-in loopback-only, read-only smoke load. Not a production capacity certification.
const args=process.argv.slice(2);
const argument=(key:string,fallback:string)=>{const i=args.indexOf(key);return i<0?fallback:args[i+1];};
if(!args.includes('--consent'))throw new Error('Explicit --consent required; use only an isolated disposable local node.');
const endpoint=new URL(argument('--node','http://127.0.0.1:3001'));
if(!['127.0.0.1','[::1]'].includes(endpoint.hostname)||!['http:','https:'].includes(endpoint.protocol)||
endpoint.username||endpoint.password||endpoint.pathname!=='/'||endpoint.search||endpoint.hash)throw new Error('Only literal loopback HTTP(S) origins are allowed. No remote load targets.');
const concurrency=Number(argument('--concurrency','8')),count=Number(argument('--requests','100'));
if(!Number.isInteger(concurrency)||concurrency<1||concurrency>32||!Number.isInteger(count)||count<1||count>1000)throw new Error('Concurrency 1–32; requests 1–1000.');
let next=0,totalBytes=0;const statuses:Record<string,number>={};const latencies:number[]=[];
const started=performance.now();
await Promise.all(Array.from({length:concurrency},async()=>{
while(next<count){const i=next++,start=performance.now();
try{
const path=i%3===0?'/api/peers':'/api/summary';
const response=await fetch(new URL(path,endpoint),{redirect:'error',signal:AbortSignal.timeout(5000)});
const reader=response.body?.getReader();let bytes=0;
if(reader)try{for(;;){const part=await reader.read();if(part.done)break;bytes+=part.value.byteLength;if(bytes>2*1024*1024)throw new Error('Response too large');}}finally{await reader.cancel();reader.releaseLock();}
totalBytes+=bytes;statuses[response.status]=(statuses[response.status]??0)+1;
}catch{statuses.error=(statuses.error??0)+1;}
latencies.push(performance.now()-start);
}
}));
latencies.sort((a,b)=>a-b);
const health=await fetch(new URL('/healthz',endpoint),{redirect:'error',signal:AbortSignal.timeout(5000)});
console.log(JSON.stringify({scope:'loopback read-only smoke, not production benchmark',requests:count,concurrency,
elapsedMs:Math.round(performance.now()-started),statuses,totalBytes,
p50Ms:Math.round(latencies[Math.floor(latencies.length*.5)]??0),p95Ms:Math.round(latencies[Math.min(latencies.length-1,Math.floor(latencies.length*.95))]??0),
healthStatus:health.status},null,2));
await health.body?.cancel();
if(statuses.error||health.status!==200)process.exitCode=1;
+18
View File
@@ -0,0 +1,18 @@
import { readdirSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { join } from 'node:path';
const testDirectory = fileURLToPath(new URL('../tests/', import.meta.url));
const files = readdirSync(testDirectory)
.filter(name => name.endsWith('.test.ts'))
.sort()
.map(name => join(testDirectory, name));
if (!files.length) throw new Error('No test files found.');
const result = spawnSync(process.execPath, ['--import', 'tsx', '--test', ...files], {
stdio: 'inherit',
env: { ...process.env, WIKIDECK_NETWORK: 'testnet', WIKIDECK_GENESIS_TIME: '1792411200' },
});
if (result.error) throw result.error;
process.exitCode = result.status ?? 1;
+3 -2
View File
@@ -4,7 +4,8 @@ import { generateKeyPairSync, sign } from 'node:crypto';
import { setTimeout as delay } from 'node:timers/promises';
import { RandomX } from '../server/testnet/randomx.ts';
import { address, CHAIN_ID, miningBlob, mintId, sha256 } from '../server/testnet/consensus.ts';
import { blockClaimMessage, nftTransferMessage, NETWORK, testnetOwnerOf, type MiningTemplate, type TestnetSnapshot } from '../shared/testnet.ts';
import { blockClaimMessage, nftTransferMessage, NETWORK, testnetOwnerOf, transactionCardIds,
type MiningTemplate, type TestnetSnapshot } from '../shared/testnet.ts';
if(!process.argv.includes('--consent'))throw new Error('This test mines three blocks with a native CPU worker. Pass --consent explicitly. Disposable NFT rewards are not recoverable.');
const nodes=(process.env.WIKIDECK_SMOKE_NODES??'http://127.0.0.1:3101,http://127.0.0.1:3102,http://127.0.0.1:3103').split(',');
@@ -31,7 +32,7 @@ try {
await api(nodes[0],'/api/transfer',{...tx,publicKey,signature:signed(nftTransferMessage(tx))});
const pending=await api(nodes[0],'/api/state') as TestnetSnapshot;
assert.equal(testnetOwnerOf(pending,first),owner);
assert.ok(pending.pendingTransfers.some(t=>t.cardHash===first));
assert.ok(pending.pendingTransfers.some(t=>transactionCardIds(t).includes(first)));
}
const template=await api(nodes[0],'/api/mining/template',{owner,publicKey}) as MiningTemplate;
let found;
+1
View File
@@ -0,0 +1 @@
// Temporary runtime probe removed after verification.