import test from 'node:test'; import assert from 'node:assert/strict'; import { generateKeyPairSync, sign } from 'node:crypto'; import { address, powHash, validSignature, validTransfer, verifyBlock, verifyState, mineSchema, transferSchema } from '../server/chain.ts'; import { GENESIS, DIFFICULTY, proofMessage, claimMessage, transferMessage, ownerOf, rarity, stats, type Block, type Challenge, type ChainState } from '../shared/protocol.ts'; import { STARTER_ARTICLES } from '../shared/catalog.ts'; 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 challenge: Challenge = { height: 1, previousHash: GENESIS, seed: 'ab'.repeat(32), article: STARTER_ARTICLES[0], difficulty: DIFFICULTY, createdAt: 1792411200000 }; const alice = identity(); const bob = identity(); let block: Block; test('real Argon2id proof is deterministic, owner-bound and independently verified', async () => { let nonce = 0; let hash = await powHash(challenge, alice.owner, nonce); while (!hash.startsWith('0'.repeat(DIFFICULTY))) hash = await powHash(challenge, alice.owner, ++nonce); block = { ...challenge, nonce, hash, owner: alice.owner, publicKey: alice.publicKey, signature: alice.sign(claimMessage(challenge, alice.owner, nonce, hash)) }; assert.equal(await powHash(challenge, alice.owner, nonce), hash); assert.equal(await verifyBlock(block, GENESIS, 1), true); assert.equal(await verifyState({ blocks: [block], transfers: [] }), true); assert.equal(await verifyBlock({ ...block, nonce: nonce + 1 }, GENESIS, 1), false); assert.equal(await verifyBlock({ ...block, owner: bob.owner }, GENESIS, 1), false); assert.equal(await verifyBlock({ ...block, article: { ...block.article, pageId: 1 } }, GENESIS, 1), false); assert.equal(await verifyBlock({ ...block, article: { ...block.article, extract: 'Altered' } }, GENESIS, 1), false); assert.equal(await verifyBlock({ ...block, previousHash: 'ff'.repeat(32) }, GENESIS, 1), false); assert.equal(await verifyBlock({ ...block, difficulty: 0 }, GENESIS, 1), false); assert.equal(await verifyBlock({ ...block, nonce: -1 }, GENESIS, 1), false); }); test('canonical proof commits all article content and mining fields', () => { const original = proofMessage(challenge, alice.owner, 1); for (const field of ['title', 'extract', 'url', 'image', 'category', 'source'] as const) { const changed = { ...challenge, article: { ...challenge.article, [field]: 'changed' } } as Challenge; assert.notEqual(proofMessage(changed, alice.owner, 1), original); } assert.notEqual(proofMessage({ ...challenge, createdAt: 1 }, alice.owner, 1), original); assert.notEqual(proofMessage(challenge, bob.owner, 1), original); }); test('P-256 signatures reject tampering, incorrect owner and malformed keys', () => { const sig = alice.sign('original'); assert.equal(validSignature(alice.publicKey, alice.owner, 'original', sig), true); assert.equal(validSignature(alice.publicKey, alice.owner, 'tampered', sig), false); assert.equal(validSignature(alice.publicKey, bob.owner, 'original', sig), false); assert.equal(validSignature('bad key', alice.owner, 'original', sig), false); }); test('transfers verify ownership, signatures, sequence and prevent replay', async () => { const state: ChainState = { blocks: [block], transfers: [] }; const body = { cardHash: block.hash, from: alice.owner, to: bob.owner, sequence: 0 }; const transfer = { ...body, publicKey: alice.publicKey, signature: alice.sign(transferMessage(body)) }; assert.equal(validTransfer(state, transfer), true); assert.equal(validTransfer(state, { ...transfer, to: alice.owner }), false); assert.equal(validTransfer(state, { ...transfer, sequence: 2 }), false); assert.equal(validTransfer(state, { ...transfer, publicKey: bob.publicKey }), false); const next = { ...state, transfers: [transfer] }; assert.equal(ownerOf(next, block.hash), bob.owner); assert.equal(validTransfer(next, transfer), false); assert.equal(await verifyState(next), true); assert.equal(await verifyState({ ...next, transfers: [transfer, transfer] }), false); }); test('untrusted request schemas reject malformed and oversized inputs', () => { assert.equal(mineSchema.safeParse({}).success, false); assert.equal(mineSchema.safeParse({ ...block, nonce: -1 }).success, false); assert.equal(transferSchema.safeParse({ cardHash: 'x'.repeat(10000) }).success, false); assert.equal(transferSchema.safeParse({ from: 'a'.repeat(64), to: 'b'.repeat(64), sequence: Infinity }).success, false); }); test('rarity and game stats are stable and bounded', () => { assert.equal(rarity('0'.repeat(64)), 'Legendary'); assert.equal(rarity(`${'0'.repeat(60)}0008`), 'Epic'); for (let n = 0; n < 100; n++) { const hash = n.toString(16).padStart(64, '0'); for (const stat of Object.values(stats(hash))) assert.ok(stat >= 30 && stat <= 99); } });