import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { InjectMetric } from '@willsoto/nestjs-prometheus'; import { Cache } from 'cache-manager'; import { randomBytes, createHash } from 'crypto'; @Injectable() export class PowService { private readonly logger = new Logger(PowService.name); private difficulty = 5; constructor( @Inject(CACHE_MANAGER) private cacheManager: Cache, @InjectMetric('pow_challenges_generated') private challengesGenerated: any, @InjectMetric('pow_challenges_completed') private challengesCompleted: any, @InjectMetric('pow_successful_verifies') private successfulVerifies: any, @InjectMetric('pow_failed_verifies') private failedVerifies: any, @InjectMetric('pow_difficulty') private powDifficulty: any, ) { this.powDifficulty.set(this.difficulty); } /** * Generate a proof of work challenge, stored to redis for verification within * the next 60 seconds. */ async generateChallenge() { const challenge = this.generateRandom256BitString(); this.logger.verbose(`Generated challenge: ${challenge}`); this.challengesGenerated.inc(); await this.cacheManager.set(challenge, true, 60 * 1000); return challenge; } generateRandom256BitString() { const randomString = randomBytes(32).toString('hex'); return randomString; } hashAndCheck(string: string) { return this.hashPassesDifficulty(this.hashString(string), this.difficulty); } hashPassesDifficulty(hash: string, difficulty: number) { return hash.startsWith('0'.repeat(difficulty)); } /** * Verify that the proof of work submitted has a leading number of * zeroes equal to the challenge length and the challenge exists. */ async verifyChallenge(challenge: string, proof: string): Promise { const expected = await this.cacheManager.get(challenge); const success = expected ? this.hashAndCheck(proof + challenge) : false; if (success) { this.successfulVerifies.inc(); } else { this.failedVerifies.inc(); } return success; } async markChallengeAsComplete(challenge: string) { this.challengesCompleted.inc(); await this.cacheManager.del(challenge); } /** * Perform a proof of work challenge to find a proof that hashes to a value */ async performChallenge(challenge: string) { let proof = this.generateRandom256BitString(); let hash = this.hashString(proof + challenge); while (!this.hashPassesDifficulty(hash, this.difficulty)) { proof = this.generateRandom256BitString(); hash = this.hashString(proof + challenge); } return { proof, hash }; } /** * sha512 the provided string and return the result. */ hashString(input: string) { return createHash('sha512').update(input).digest('hex'); } /** * Set the difficulty of the proof of work challenge. */ setDifficulty(difficulty: number) { this.difficulty = difficulty; this.powDifficulty.set(this.difficulty); } /** * Get the current difficulty of the proof of work challenge. */ getDifficulty() { return this.difficulty; } }