import test from 'node:test'; import assert from 'node:assert/strict'; import { spawn, type ChildProcess } from 'node:child_process'; import { access, mkdtemp, readFile, rm, unlink } from 'node:fs/promises'; import { constants, existsSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { delimiter, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { generateKeyPairSync, randomUUID, 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 { type MarketAction, type MarketInfo } from '../shared/market.ts'; import { marketMessageV3, type MarketActionV3, type MarketActivityV3, type MarketListingV3, type MarketOrderV3 } from '../shared/market-v3.ts'; import { StateClient } from '../shared/rpc-client.ts'; import { createBackup,restoreBackup } from '../server/recovery.ts'; import { Ledger } from '../server/testnet/ledger.ts'; import { Market } from '../server/market/store.ts'; import { CUSTODY_POLICY, custodyMessage, type CustodyCommand, type CustodyPage, type CustodyActivity } from '../shared/custody.ts'; import { CustodyMarket } from '../server/market/custody.ts'; const root = fileURLToPath(new URL('../', import.meta.url)); const wikipediaMock = resolve(root, 'tests', 'mock-wikipedia.mjs'); const binary = resolve(process.env.RANDOMX_BINARY ?? join(root, 'native', 'build', `wikideck-randomx${process.platform === 'win32' ? '.exe' : ''}`)); function wallet() { // Ephemeral, independent P-256 keys; no credentials or private keys go to the nodes. const keys = generateKeyPairSync('ec', { namedCurve: 'prime256v1' }); const publicKey = keys.publicKey.export({ type: 'spki', format: 'der' }).toString('base64'); return { publicKey, owner: address(publicKey), signed: (message: string) => sign('sha256', Buffer.from(message), { key: keys.privateKey, dsaEncoding: 'ieee-p1363' }).toString('base64') }; } function tamperSignature(signature: string): string { const bytes = Buffer.from(signature, 'base64'); bytes[0] ^= 1; return bytes.toString('base64'); } async function bounded(promise: Promise, milliseconds: number, label: string): Promise { let timer: ReturnType | undefined; try { return await Promise.race([promise, new Promise((_resolve, reject) => { timer = setTimeout(() => reject(new Error(`${label} timed out`)), milliseconds); })]); } finally { clearTimeout(timer); } } test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and durable restart', { timeout: 180_000 }, async t => { try { await access(binary, constants.X_OK); } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; t.skip(`Native RandomX binary absent: ${binary}. Build the reference engine as documented in docs/randomx-native.md, or set RANDOMX_BINARY. No fake PoW fallback.`); return; } const directory = await mkdtemp(join(tmpdir(), 'wikideck-randomx-http-')); const firstDir = join(directory, 'node-1'); const secondDir = join(directory, 'node-2'); const originalPath = process.env.PATH; // The local MinGW build depends on these DLLs. Inherit the caller's PATH elsewhere. const mingw = `${process.env.SystemDrive ?? 'C:'}/Strawberry/c/bin`; if (process.platform === 'win32' && existsSync(mingw)) { process.env.PATH = `${mingw}${delimiter}${originalPath ?? ''}`; } type RunningNode = { child: ChildProcess; closed: Promise; output: () => string; base: string; }; const nodes: RunningNode[] = []; let engine: RandomX | undefined; const abort = () => { engine?.close(); for (const { child } of nodes) { if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL'); } }; t.signal.addEventListener('abort', abort, { once: true }); async function start(dataDir: string, peer?: string, marketRuntimeEnabled = true): Promise { t.signal.throwIfAborted(); const child = spawn(process.execPath, ['--import', wikipediaMock, '--experimental-strip-types', 'server/index.ts'], { cwd: root, windowsHide: true, shell: false, 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', WIKIDECK_PAYMENT_MODE: 'mock', WIKIDECK_MARKET_RUNTIME_ENABLED: marketRuntimeEnabled ? '1' : '0', WIKIDECK_MARKET_FEE_BPS: '250', WIKIDECK_MARKET_CONFIRMATIONS: '2', WIKIDECK_CUSTODY_MODE: peer ? 'off' : 'sandbox', // This integration burst now includes signed custody reads/replay probes; admission limits have dedicated tests. WIKIDECK_RPC_WRITES_PER_SECOND: '50' }, stdio: ['ignore', 'pipe', 'pipe'], }); let output = ''; const node: RunningNode = { child, base: '', output: () => output, closed: new Promise(resolve => child.once('close', () => resolve())) }; nodes.push(node); // Register before startup can fail, so finally owns every child. const ready = new Promise((resolve, reject) => { const log = (chunk: Buffer) => { output = (output + chunk.toString()).slice(-16_384); const match = /Wikideck RandomX testnet: (http:\/\/127\.0\.0\.1:\d+)/.exec(output); if (match) resolve(match[1]); }; child.stdout!.on('data', log); child.stderr!.on('data', chunk => { output = (output + chunk.toString()).slice(-16_384); }); child.once('error', reject); child.once('exit', (code, signal) => reject(new Error(`Node exited (${signal ?? code}): ${output}`))); }); try { node.base = await bounded(ready, 30_000, 'Node startup'); } catch (error) { throw new Error(`${String(error)}\n${output}`); } return node; } async function stop(node: RunningNode): Promise { if (node.child.exitCode === null && node.child.signalCode === null) node.child.kill('SIGTERM'); try { await bounded(node.closed, 5000, 'Node shutdown'); } catch { node.child.kill('SIGKILL'); await bounded(node.closed, 5000, 'Forced node shutdown'); } assert.ok(node.child.exitCode !== null || node.child.signalCode !== null, node.output()); } async function request(base: string, path: string, expected = 200, body?: unknown, headers: Record = {}): Promise { const until = Date.now() + 20_000; let response: Response; do { 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), }); if (path !== '/api/market/command' || response.status !== 503 || Date.now() >= until || !(await response.clone().text()).includes('reconciliation is catching up')) break; const retryAfter = Number(response.headers.get('Retry-After')); await delay(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 25, undefined, { signal: t.signal }); } while (Date.now() < until); if (response.status !== expected) { assert.fail(`${path}: expected ${expected}, got ${response.status}: ${await response.text()}`); } return response; } async function json(base: string, path: string, expected = 200, body?: unknown): Promise { return (await request(base, path, expected, body)).json() as Promise; } const snapshot = (base: string) => json(base, '/api/chain'); async function marketCommand(base: string, info: MarketInfo, who: ReturnType, action: MarketAction) { const atomicFromCents = (cents: number) => ((BigInt(cents) * 1_000_000_000_000n + 14999n) / 15000n).toString(); let current: MarketActionV3; if (action.type === 'list') current = { type: 'list', cardHash: action.cardHash, sequence: action.sequence, kind: action.kind, priceAtomicXmr: atomicFromCents(action.priceCents), endsAt: action.endsAt, feeBps: action.feeBps }; else if (action.type === 'buy') current = { type: 'buy', listingId: action.listingId, priceAtomicXmr: atomicFromCents(action.priceCents), feeBps: action.feeBps }; else if (action.type === 'bid') current = { type: 'bid', listingId: action.listingId, priceAtomicXmr: atomicFromCents(action.priceCents) }; else current = action; const terms = { version: 3 as const, network: NETWORK, chainId: CHAIN_ID, marketId: info.marketId, actor: who.owner, publicKey: who.publicKey, nonce: randomUUID(), expiresAt: Date.now() + 60000, action: current }; return json(base, '/api/market/command', 200, { ...terms, signature: who.signed(marketMessageV3(terms)) }); } async function waitFor(label: string, read: () => Promise, predicate: (value: T) => boolean): Promise { const until = Date.now() + 25_000; let value: T; do { value = await read(); if (predicate(value)) return value; await delay(500, undefined, { signal: t.signal }); } while (Date.now() < until); assert.fail(`${label} did not converge: ${JSON.stringify(value)}`); } async function mine(base: string, miner: ReturnType): Promise { const template = await json(base, '/api/mining/template', 200, { owner: miner.owner, publicKey: miner.publicKey }); const header = { ...template.header }; assert.equal(header.network, NETWORK); assert.equal(header.chainId, CHAIN_ID); assert.equal(header.target, MAX_TARGET); // Initial target is approximately one success per 16 hashes. assert.equal(template.nonceOffset, 32); assert.equal(template.blob, miningBlob(header).toString('hex')); const until = Date.now() + 45_000; for (let batch = 0; batch < 16 && Date.now() < until; batch++) { t.signal.throwIfAborted(); const result = await bounded(engine!.mine(header.key, miningBlob(header), header.target, template.nonceOffset, 64), Math.min(45_000, until - Date.now()), 'Native mining batch'); if (result) { header.nonce = result.nonce; const blob = miningBlob(header); assert.equal(await engine!.hash(header.key, blob), result.hash); assert.ok(BigInt(`0x${result.hash}`) <= BigInt(`0x${header.target}`)); const hash = sha256(blob); return { ...header, hash, tokenId: mintId(header), powHash: result.hash, signature: miner.signed(blockClaimMessage(hash)), transactions: template.transactions }; } header.nonce += 64; } assert.fail('Native RandomX mining exhausted its bounded search (1024 nonces / 45s)'); } async function submit(base: string, block: TestnetBlock) { 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) => { 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); const marketDisabledNode = await start(firstDir, undefined, false); await json(marketDisabledNode.base, '/api/summary'); await request(marketDisabledNode.base, '/api/market', 404); await stop(marketDisabledNode); let first = await start(firstDir); await assert.rejects(start(firstDir), /Data directory locked/, 'a second server process must fail closed rather than run another marketplace worker on the same data directory'); const custody = await json(first.base, '/api/custody'); assert.equal(custody.info.mode, 'sandbox'); assert.equal(custody.info.liveDeposits, false); const custodyTerms: Omit = { version: 1, mode: 'sandbox', policy: CUSTODY_POLICY, network: NETWORK, chainId: CHAIN_ID, custodianId: custody.info.custodianId, actor: bob.owner, publicKey: bob.publicKey, nonce: randomUUID(), expiresAt: Date.now() + 60000, action: { type: 'mock-credit' } }; const custodyCredit = { ...custodyTerms, signature: bob.signed(custodyMessage(custodyTerms)) }; const credited = await json(first.base, '/api/custody/command', 200, custodyCredit); assert.equal(credited.availableAtomicXmr, '1000000000000'); assert.deepEqual(await json(first.base, '/api/custody/command', 200, custodyCredit), credited); await request(first.base, '/api/custody/command', 400, { ...custodyCredit, actor: alice.owner }); await request(first.base, '/api/custody/command', 400, { ...custodyCredit, mode: 'live' }); assert.match((await request(first.base, '/api/custody/command', 200, custodyCredit)).headers.get('cache-control') ?? '', /no-store/); 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'); assert.equal(health.headers.has('ratelimit'), false); await (await request(first.base, '/api/wikipedia/0', 400)).text(); 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); assert.equal(initial.tip, GENESIS_HASH); assert.equal(initial.cumulativeWork, '0'); assert.equal(initial.algorithm, 'RandomX'); assert.equal(initial.maturity, CONSENSUS.coinbaseMaturity); assert.deepEqual(initial.blocks, []); assert.deepEqual(await json(first.base, '/p2p/status'), { network: NETWORK, chainId: CHAIN_ID, size: 0, tip: GENESIS_HASH }); 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', headers: { 'Content-Type': 'application/json' }, body: '{', 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)}`]) { 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(); } const block1 = await mine(first.base, alice); assert.equal(block1.height, 1); assert.equal(block1.article.source, 'live'); assert.ok(block1.article.image); assert.ok(['https://upload.wikimedia.org', 'https://thumb.wikimedia.org'].includes(new URL(block1.article.image).origin)); assert.equal(block1.statsSnapshot.pageviews.status, 'available'); assert.equal(block1.statsSnapshot.pageviews.total, 600); assert.equal(block1.statsSnapshot.revisionId, block1.article.pageId + 1); await submit(first.base, block1); const unsigned = { network: NETWORK, chainId: CHAIN_ID, cardHash: block1.tokenId, from: alice.owner, to: bob.owner, sequence: 0 }; const transfer: NftTransfer = { ...unsigned, publicKey: alice.publicKey, signature: alice.signed(nftTransferMessage(unsigned)) }; const immature = await json<{ error: string }>(first.base, '/api/transfer', 400, transfer); assert.match(immature.error, /immature/i); 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); const market = await json<{ info: MarketInfo; listings: MarketListingV3[] }>(first.base, '/api/market'); assert.equal(market.info.paymentMode, 'mock'); assert.deepEqual(market.listings, []); await (await request(first.base, '/api/market/command', 400, {})).text(); await (await request(first.base, '/api/market/command', 403, {}, { Origin: 'https://evil.example' })).text(); const sale = await marketCommand(first.base, market.info, alice, { type: 'list', cardHash: block1.tokenId, sequence: 0, kind: 'fixed', priceCents: 1500, endsAt: null, feeBps: 250 }); assert.equal((await json<{ listings: MarketListingV3[] }>(first.base, '/api/market')).listings[0].id, sale.id); const order = await marketCommand(first.base, market.info, bob, { type: 'buy', listingId: sale.id, priceCents: 1500, feeBps: 250 }); assert.equal(order.invoice.address, null); assert.equal(order.invoice.amountXmr, '0.100000000000'); await marketCommand(first.base, market.info, bob, { type: 'mock-pay', orderId: order.id }); 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); assert.deepEqual(await json(second.base, '/api/custody'), { enabled: false }); await request(second.base, '/api/custody/command', 503, custodyCredit); const synced = await waitFor('Second node canonical sync', () => snapshot(second.base), state => state.tip === block2.hash && state.peers === 1); assert.deepEqual(synced.blocks, [block1, block2]); assert.equal(synced.cumulativeWork, (2n * workFor(MAX_TARGET)).toString()); const peerStatus = await json<{ peers: { url: string; connected: boolean }[] }>(second.base, '/api/peers'); assert.deepEqual(peerStatus.peers, [{ url: first.base, connected: true }]); const badTransfer = await json<{ error: string }>(second.base, '/api/transfer', 400, { ...transfer, signature: tamperSignature(transfer.signature) }); assert.match(badTransfer.error, /signature/i); await marketCommand(first.base, market.info, alice, { type: 'deliver', orderId: order.id, transfer }); assert.equal((await marketCommand(first.base, market.info, bob, { type: 'activity' })).orders[0].status, 'delivery-pending'); const pending = { id: transferId(transfer), status: 'pending', message: 'Ownership changes only when this transaction is mined.' }; assert.deepEqual(await json(second.base, '/api/transfer', 202, transfer), pending); assert.deepEqual(await json(second.base, '/api/transfer', 202, transfer), pending); // Pending duplicates are idempotent. await waitFor('Transaction gossip to first node', () => snapshot(first.base), state => state.pendingTransfers.length === 1); for (const node of [first, second]) { const state = await snapshot(node.base); assert.deepEqual(state.pendingTransfers, [transfer]); assert.deepEqual(state.transfers, []); const metadata = await json>(node.base, `/api/nfts/${block1.tokenId}`); assert.equal(metadata.owner, alice.owner); // Mempool never changes canonical ownership. assert.equal(metadata.sequence, 0); assert.equal(metadata.confirmations, 2); } const block3 = await mine(second.base, bob); assert.equal(block3.height, 3); assert.deepEqual(block3.transactions, [transfer]); // Reject validly encoded signature and PoW tampering before accepting this same block ID. const beforeInvalid = await persisted(secondDir); const badSignature = await json<{ error: string }>(second.base, '/api/blocks', 400, { ...block3, signature: tamperSignature(block3.signature) }); assert.match(badSignature.error, /miner signature/i); const badProof = await json<{ error: string }>(second.base, '/api/blocks', 400, { ...block3, powHash: block3.powHash === '0'.repeat(64) ? '0'.repeat(63) + '1' : '0'.repeat(64) }); assert.match(badProof.error, /RandomX proof/i); assert.equal(await persisted(secondDir), beforeInvalid, 'Invalid blocks must not touch durable state'); assert.equal((await snapshot(second.base)).tip, block2.hash); await submit(second.base, block3); const gossiped = await waitFor('Block gossip to first node', () => snapshot(first.base), state => state.tip === block3.hash); const canonical = await snapshot(second.base); assert.deepEqual(gossiped.blocks, canonical.blocks); assert.deepEqual(canonical.blocks, [block1, block2, block3]); assert.deepEqual(canonical.transfers, [transfer]); assert.deepEqual(canonical.pendingTransfers, []); const settling = await waitFor('Worker market delivery reconciliation', () => marketCommand(first.base, market.info, bob, { type: 'activity' }), activity => activity.orders[0]?.deliveryConfirmations === 1); assert.equal(settling.orders[0].deliveryConfirmations, 1); assert.equal(settling.orders[0].payout, 'not-started'); assert.equal((await json<{ listings: MarketListingV3[] }>(second.base, '/api/market')).listings.length, 0); assert.equal(canonical.cumulativeWork, (3n * workFor(MAX_TARGET)).toString()); for (const node of [first, second]) { const replay = await json<{ error: string }>(node.base, '/api/transfer', 400, transfer); assert.match(replay.error, /owner\/sequence/i); assert.deepEqual(await json(node.base, `/api/nfts/${block1.tokenId}`), { network: NETWORK, chainId: CHAIN_ID, tokenId: block1.tokenId, owner: bob.owner, sequence: 1, blockHash: block1.hash, confirmations: 3, article: block1.article, statsSnapshot: block1.statsSnapshot, stateRoot: block3.stateRoot, }); const inventory = await json<{ blocks: TestnetBlock[]; next: null }>(node.base, '/p2p/blocks?offset=0'); assert.deepEqual(inventory, { blocks: canonical.blocks, next: null }); assert.deepEqual(await json(node.base, '/p2p/blocks?offset=3'), { blocks: [], next: null }); } await stop(second); // Stop its outbound sync before restarting its peer. await stop(first); if (process.platform === 'win32') { // Windows kill(SIGTERM) bypasses the JS handler. Explicit crash-recovery simulation: // only our isolated lock, only after stop() confirmed process exit/stdio closure. const lock = join(firstDir, 'testnet-node.lock'); try { assert.equal(await readFile(lock, 'utf8'), String(first.child.pid)); await unlink(lock); } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } } else { 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); assert.ok(backupManifest.files.some(f=>f.name==='custody-sandbox.sqlite')); const drillCustody = await CustodyMarket.open(drillDir, { feeBps: 250, card: () => undefined }); try { assert.equal(drillCustody.id, custody.info.custodianId); assert.equal(drillCustody.audit().assetsAtomicXmr, '1000000000000'); } finally { drillCustody.close(); } const drillLedger=await Ledger.open(drillDir,engine!); // Revalidate real RandomX proofs from the copied backup. try { assert.deepEqual(drillLedger.snapshot(),{...canonical,peers:0}); const drillMarket=await Market.open(drillDir,drillLedger); try {assert.equal(drillMarket.id,market.info.marketId);}finally{drillMarket.close();} const db=new DatabaseSync(join(drillDir,'testnet','market.sqlite'),{readOnly:true}); try { const row=db.prepare('SELECT data FROM orders WHERE id=?').get(order.id) as {data:string}; assert.equal(JSON.parse(row.data).buyer,bob.owner); assert.ok((db.prepare('SELECT COUNT(*) AS n FROM receipts').get() as {n:number}).n>0); }finally{db.close();} }finally{drillLedger.close();} first = await start(firstDir); // Ledger.open revalidates every persisted block using native RandomX. const restartedCustody = await json(first.base, '/api/custody'); assert.equal(restartedCustody.info.custodianId, custody.info.custodianId); const balanceTerms: Omit = { ...custodyTerms, nonce: randomUUID(), expiresAt: Date.now() + 60000, action: { type: 'activity', offset: 0 } }; const restoredBalance = await json(first.base, '/api/custody/command', 200, { ...balanceTerms, signature: bob.signed(custodyMessage(balanceTerms)) }); assert.equal(restoredBalance.availableAtomicXmr, '1000000000000'); const restored = await snapshot(first.base); assert.deepEqual(await pagedClient.load(),restored); const restoredMarket = await json<{ info: MarketInfo }>(first.base, '/api/market'); assert.equal(restoredMarket.info.marketId, market.info.marketId); assert.equal((await marketCommand(first.base, market.info, bob, { type: 'activity' })).orders[0].id, order.id); 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, blockHash: block1.hash, confirmations: 3, article: block1.article, statsSnapshot: block1.statsSnapshot, stateRoot: block3.stateRoot, }); assert.deepEqual(await json(first.base, '/api/blocks', 200, block3), { accepted: false, reorg: false }); await (await request(first.base, '/api/transfer', 400, transfer)).text(); t.diagnostic('Real native RandomX: 3 mined blocks, 2 independent HTTP nodes, transfer gossip/confirmation, rejection and restart checks passed.'); } finally { engine?.close(); const stopped = await Promise.allSettled(nodes.map(stop)); t.signal.removeEventListener('abort', abort); if (originalPath === undefined) delete process.env.PATH; else process.env.PATH = originalPath; // Do not remove a directory if a child could still be writing to it. if (stopped.every(result => result.status === 'fulfilled')) { await rm(directory, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } else { throw new AggregateError(stopped.filter(result => result.status === 'rejected') .map(result => result.reason), `Child cleanup failed; isolated data retained at ${directory}`); } } });