59 lines
3.3 KiB
TypeScript
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;
|
|
}
|