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