Files
2026-10-19 12:16:49 -04:00

59 lines
3.3 KiB
TypeScript

import { createHash, createPublicKey, verify } from 'node:crypto';
import { argon2id } from 'hash-wasm';
import { z } from 'zod';
import { claimMessage, transferMessage, proofMessage, POW, GENESIS, DIFFICULTY, ownerOf,
type Block, type Challenge, type ChainState, type Transfer } from '../shared/protocol.ts';
const hex = z.string().regex(/^[a-f0-9]{64}$/);
const publicKey = z.string().regex(/^[A-Za-z0-9+/]+={0,2}$/).max(256);
const signature = z.string().regex(/^[A-Za-z0-9+/]+={0,2}$/).max(128);
export const mineSchema = z.object({ owner: hex, publicKey, signature,
nonce: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER), hash: hex,
seed: hex }).strict();
export const transferSchema = z.object({ cardHash: hex, from: hex, to: hex,
sequence: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER), publicKey, signature }).strict();
export function address(key: string): string {
return createHash('sha256').update(Buffer.from(key, 'base64')).digest('hex');
}
export function validSignature(key: string, owner: string, message: string, sig: string): boolean {
try {
if (address(key) !== owner) return false;
const imported = createPublicKey({ key: Buffer.from(key, 'base64'), format: 'der', type: 'spki' });
if (imported.asymmetricKeyType !== 'ec' || imported.asymmetricKeyDetails?.namedCurve !== 'prime256v1') return false;
return verify('sha256', Buffer.from(message), { key: imported, dsaEncoding: 'ieee-p1363' }, Buffer.from(sig, 'base64'));
} catch { return false; }
}
export async function powHash(c: Challenge, owner: string, nonce: number): Promise<string> {
return argon2id({ ...POW, password: proofMessage(c, owner, nonce),
salt: Buffer.from(c.seed, 'hex'), outputType: 'hex' });
}
export async function verifyBlock(block: Block, previousHash: string, height: number): Promise<boolean> {
if (block.previousHash !== previousHash || block.height !== height || block.difficulty !== DIFFICULTY) return false;
if (!/^[a-f0-9]{64}$/.test(block.seed) || !/^[a-f0-9]{64}$/.test(block.hash)) return false;
if (!Number.isSafeInteger(block.nonce) || block.nonce < 0 || !Number.isSafeInteger(block.createdAt)) return false;
if (!Number.isSafeInteger(block.article.pageId) || block.article.pageId <= 0) return false;
if (!block.hash.startsWith('0'.repeat(DIFFICULTY))) return false;
if (!validSignature(block.publicKey, block.owner, claimMessage(block, block.owner, block.nonce, block.hash), block.signature)) return false;
return await powHash(block, block.owner, block.nonce) === block.hash;
}
export function validTransfer(state: ChainState, t: Transfer): boolean {
return transferSchema.safeParse(t).success && t.from !== t.to &&
ownerOf(state, t.cardHash) === t.from &&
t.sequence === state.transfers.filter(x => x.cardHash === t.cardHash).length &&
validSignature(t.publicKey, t.from, transferMessage(t), t.signature);
}
export async function verifyState(state: ChainState): Promise<boolean> {
let previous = GENESIS;
for (const [index, block] of state.blocks.entries()) {
if (!await verifyBlock(block, previous, index + 1)) return false;
previous = block.hash;
}
const checked: ChainState = { blocks: state.blocks, transfers: [] };
for (const t of state.transfers) {
if (!validTransfer(checked, t)) return false;
checked.transfers.push(t);
}
return true;
}