V2.1 (#21)
Working version Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
Executable
+59
@@ -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 }); }
|
||||
Executable
+48
@@ -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);}
|
||||
Executable
+4
@@ -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));
|
||||
Executable
+20
@@ -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));
|
||||
Executable
+10
@@ -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);
|
||||
});
|
||||
Executable
+12
@@ -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));
|
||||
Executable
+11
@@ -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;
|
||||
Executable
+33
@@ -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;
|
||||
Executable
+18
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
// Temporary runtime probe removed after verification.
|
||||
Reference in New Issue
Block a user