v2
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user