Files
wikideck/tests/chain.test.ts
T
2026-10-19 12:16:49 -04:00

87 lines
5.2 KiB
TypeScript

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);
}
});