V2.1 (#21)
Working version Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
+52
-9
@@ -7,12 +7,16 @@ import { tmpdir } from 'node:os';
|
||||
import { delimiter, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { generateKeyPairSync, sign } from 'node:crypto';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import { setTimeout as delay } from 'node:timers/promises';
|
||||
import { RandomX } from '../server/testnet/randomx.ts';
|
||||
import { address, CHAIN_ID, GENESIS_HASH, MAX_TARGET, miningBlob, mintId, sha256,
|
||||
transferId, workFor } from '../server/testnet/consensus.ts';
|
||||
import { NETWORK, CONSENSUS, blockClaimMessage, nftTransferMessage,
|
||||
type MiningTemplate, type NftTransfer, type TestnetBlock, type TestnetSnapshot } from '../shared/testnet.ts';
|
||||
import { StateClient } from '../shared/rpc-client.ts';
|
||||
import { createBackup,restoreBackup } from '../server/recovery.ts';
|
||||
import { Ledger } from '../server/testnet/ledger.ts';
|
||||
|
||||
const root = fileURLToPath(new URL('../', import.meta.url));
|
||||
const wikipediaMock = resolve(root, 'tests', 'mock-wikipedia.mjs');
|
||||
@@ -80,7 +84,8 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
env: { ...process.env, HOST: '127.0.0.1', PORT: '0', NODE_ENV: 'production',
|
||||
WIKIDECK_DATA_DIR: dataDir, RANDOMX_BINARY: binary,
|
||||
WIKIDECK_PEERS: peer ?? '', WIKIDECK_ALLOW_PRIVATE_PEERS: peer ? '1' : '0',
|
||||
PUBLIC_ORIGIN: 'http://127.0.0.1:3001' },
|
||||
PUBLIC_ORIGIN: 'http://127.0.0.1:3001',
|
||||
WIKIDECK_RPC_WRITES_PER_SECOND: '50' },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
let output = '';
|
||||
@@ -116,11 +121,11 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
async function request(base: string, path: string, expected = 200, body?: unknown,
|
||||
headers: Record<string, string> = {}): Promise<Response> {
|
||||
const response = await fetch(`${base}${path}`, {
|
||||
method: body === undefined ? 'GET' : 'POST', redirect: 'error',
|
||||
signal: AbortSignal.any([t.signal, AbortSignal.timeout(30_000)]),
|
||||
headers: { ...(body === undefined ? {} : { 'Content-Type': 'application/json' }), ...headers },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
method: body === undefined ? 'GET' : 'POST', redirect: 'error',
|
||||
signal: AbortSignal.any([t.signal, AbortSignal.timeout(30_000)]),
|
||||
headers: { ...(body === undefined ? {} : { 'Content-Type': 'application/json' }), ...headers },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
if (response.status !== expected) {
|
||||
assert.fail(`${path}: expected ${expected}, got ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
@@ -171,13 +176,23 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
assert.deepEqual(await json(base, '/api/blocks', 201, block), { accepted: true, reorg: false });
|
||||
assert.deepEqual(await json(base, '/api/blocks', 200, block), { accepted: false, reorg: false });
|
||||
}
|
||||
const persisted = (dir: string) => readFile(join(dir, 'testnet', 'ledger.json'), 'utf8');
|
||||
const persisted = (dir: string) => {
|
||||
const db = new DatabaseSync(join(dir, 'testnet', 'ledger.sqlite'));
|
||||
try {
|
||||
return JSON.stringify({
|
||||
blocks: db.prepare('SELECT hash, previous_hash, block_json FROM blocks ORDER BY sequence').all(),
|
||||
pending: db.prepare('SELECT position, id, transaction_json FROM pending_transfers ORDER BY position').all(),
|
||||
});
|
||||
} finally { db.close(); }
|
||||
};
|
||||
|
||||
try {
|
||||
engine = new RandomX({ binary }); // Always the actual reference RandomX engine, never a test double.
|
||||
const alice = wallet(), bob = wallet();
|
||||
assert.notEqual(alice.owner, bob.owner);
|
||||
let first = await start(firstDir);
|
||||
await assert.rejects(start(firstDir), /Data directory locked/,
|
||||
'a second server process must fail closed rather than run a second writer on the same data directory');
|
||||
const health = await request(first.base, '/healthz');
|
||||
assert.deepEqual(await health.json(), { status: 'ok', network: NETWORK });
|
||||
assert.equal(health.headers.get('cache-control'), 'no-store');
|
||||
@@ -186,6 +201,10 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
await (await request(first.base, '/api/wikipedia/not-a-page', 400)).text();
|
||||
await (await request(first.base, '/api/wikipedia/9999999999', 503)).text();
|
||||
const initial = await snapshot(first.base);
|
||||
const pagedClient=new StateClient(((path: string,init?: RequestInit)=>fetch(new URL(path,first.base),init)) as typeof fetch);
|
||||
assert.deepEqual(await pagedClient.load(),initial);
|
||||
await (await request(first.base,'/api/canonical?tip=bad',400)).text();
|
||||
await (await request(first.base,`/api/canonical?tip=${GENESIS_HASH}&limit=17`,400)).text();
|
||||
assert.equal(initial.network, NETWORK);
|
||||
assert.equal(initial.chainId, CHAIN_ID);
|
||||
assert.equal(initial.genesis, GENESIS_HASH);
|
||||
@@ -199,6 +218,8 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
await (await request(first.base, '/api/mining/template', 400, {})).text();
|
||||
await (await request(first.base, '/api/blocks', 400, {})).text();
|
||||
await (await request(first.base, '/api/transfer', 400, {})).text();
|
||||
await (await request(first.base, '/api/trades', 400, {})).text();
|
||||
await (await request(first.base, `/api/decks/${'x'.repeat(64)}`, 400)).text();
|
||||
await (await request(first.base, '/api/mining/template', 403, {}, { Origin: 'https://evil.example' })).text();
|
||||
await (await request(first.base, '/api/mining/template', 415, {}, { 'Content-Type': 'text/plain' })).text();
|
||||
const malformed = await fetch(`${first.base}/api/blocks`, { method: 'POST',
|
||||
@@ -206,10 +227,14 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
signal: AbortSignal.any([t.signal, AbortSignal.timeout(5000)]) });
|
||||
assert.equal(malformed.status, 400);
|
||||
assert.deepEqual(await malformed.json(), { error: 'Invalid request encoding' });
|
||||
for (const path of ['/api/missing', '/p2p/missing', `/api/nfts/${'0'.repeat(64)}`]) {
|
||||
for (const path of ['/api/missing', '/api/market', '/api/custody', '/api/monero/deposit-config',
|
||||
'/p2p/missing', `/api/nfts/${'0'.repeat(64)}`]) {
|
||||
await (await request(first.base, path, 404)).text();
|
||||
}
|
||||
for (const offset of ['-1', '1.5', '10001', 'nope']) {
|
||||
for (const path of ['/api/market/command', '/api/custody/command', '/api/monero/deposit-address']) {
|
||||
await (await request(first.base, path, 404, {})).text();
|
||||
}
|
||||
for (const offset of ['-1', '1.5', '9007199254740992', 'nope']) {
|
||||
await (await request(first.base, `/p2p/blocks?offset=${offset}`, 400)).text();
|
||||
}
|
||||
|
||||
@@ -231,6 +256,17 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
const block2 = await mine(first.base, alice);
|
||||
assert.equal(block2.height, 2);
|
||||
await submit(first.base, block2);
|
||||
const aliceDeck = await json<{ address: string; tip: string; total: number; nfts: { block: TestnetBlock }[] }>(
|
||||
first.base, `/api/decks/${alice.owner}`);
|
||||
assert.equal(aliceDeck.address, alice.owner);
|
||||
assert.equal(aliceDeck.tip, block2.hash);
|
||||
assert.equal(aliceDeck.total, 2);
|
||||
assert.deepEqual(aliceDeck.nfts.map(item => item.block.tokenId), [block1.tokenId, block2.tokenId]);
|
||||
assert.equal((await json<{ owner: string; sequence: number }>(first.base, `/api/nfts/${block1.tokenId}`)).owner, alice.owner);
|
||||
await request(first.base, '/api/nfts/not-a-token', 400);
|
||||
await request(first.base, `/api/decks/${alice.owner}?tip=${block1.hash}`, 409);
|
||||
assert.equal((await json<{ total: number }>(first.base, `/api/decks/${alice.owner}?tip=${block2.hash}&limit=1`)).total, 2);
|
||||
await (await request(first.base, `/api/decks/${'z'.repeat(64)}`, 400)).text();
|
||||
|
||||
// A truly independent process reconstructs the canonical chain over HTTP and checks real work.
|
||||
const second = await start(secondDir, first.base);
|
||||
@@ -311,8 +347,15 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
|
||||
assert.equal(first.child.exitCode, 0, first.output());
|
||||
assert.equal(existsSync(join(firstDir, 'testnet-node.lock')), false);
|
||||
}
|
||||
const backupDir=join(directory,'stopped-pair-backup'),drillDir=join(directory,'isolated-restore-drill');
|
||||
const backupManifest = await createBackup(firstDir,backupDir);await restoreBackup(backupDir,drillDir);
|
||||
const drillLedger=await Ledger.open(drillDir,engine!); // Revalidate real RandomX proofs from the copied backup.
|
||||
try {
|
||||
assert.deepEqual(drillLedger.snapshot(),{...canonical,peers:0});
|
||||
}finally{drillLedger.close();}
|
||||
first = await start(firstDir); // Ledger.open revalidates every persisted block using native RandomX.
|
||||
const restored = await snapshot(first.base);
|
||||
assert.deepEqual(await pagedClient.load(),restored);
|
||||
assert.deepEqual(restored, { ...canonical, peers: 0 });
|
||||
assert.deepEqual(await json(first.base, `/api/nfts/${block1.tokenId}`), {
|
||||
network: NETWORK, chainId: CHAIN_ID, tokenId: block1.tokenId, owner: bob.owner, sequence: 1,
|
||||
|
||||
Reference in New Issue
Block a user