87 lines
5.2 KiB
TypeScript
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);
|
|
}
|
|
});
|