Add contact, upgrade versions, add docs

This commit is contained in:
2024-10-15 17:29:35 -06:00
parent 2fff8e9887
commit d48040bd58
23 changed files with 2299 additions and 2151 deletions

View File

@@ -1,37 +1,66 @@
import { Body, Controller, Get, Post, Query, Render } from '@nestjs/common';
import {
BadRequestException,
Body,
Controller,
Get,
Post,
Query,
Render,
} from '@nestjs/common';
import { PowService } from './pow.service';
import { ApiBody, ApiQuery, ApiTags } from '@nestjs/swagger';
import { ApiBody, ApiProperty, ApiQuery, ApiTags } from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config';
class SolveDto {
@ApiProperty({
description: 'The challenge to solve',
})
challenge: string;
}
@ApiTags('pow')
@Controller('pow')
export class PowController {
constructor(
private readonly powService: PowService,
) { }
constructor(
private readonly powService: PowService,
private readonly configService: ConfigService,
) {}
@Get('')
@Render('pow/index')
async index() {
return {
difficulty: this.powService.getDifficulty(),
};
}
@Get('challenge')
async generateChallenge() {
return this.powService.generateChallenge();
}
@Post('challenge')
@ApiBody({ schema: { properties: { challenge: { type: 'string' }, proof: { type: 'string' } } } })
async verifyChallenge(@Body() body: { challenge: string, proof: string }) {
return this.powService.verifyChallenge(body.challenge, body.proof);
}
@Post('challenge/complete')
@ApiBody({ schema: { properties: { challenge: { type: 'string' } } } })
async markChallengeAsComplete(@Body() body: { challenge: string }) {
return this.powService.markChallengeAsComplete(body.challenge);
@Get('')
@Render('pow/index')
async index() {
return {
difficulty: this.powService.getDifficulty(),
};
}
@Get('challenge')
async generateChallenge() {
return this.powService.generateChallenge();
}
@Post('challenge')
@ApiBody({
schema: {
properties: { challenge: { type: 'string' }, proof: { type: 'string' } },
},
})
async verifyChallenge(@Body() body: { challenge: string; proof: string }) {
return this.powService.verifyChallenge(body.challenge, body.proof);
}
@Post('challenge/complete')
@ApiBody({ schema: { properties: { challenge: { type: 'string' } } } })
async markChallengeAsComplete(@Body() body: { challenge: string }) {
return this.powService.markChallengeAsComplete(body.challenge);
}
@Post('challenge/solve')
async solveChallenge(@Body() body: SolveDto) {
if (this.configService.get<boolean>('isProduction')) {
throw new BadRequestException('This endpoint is disabled in production');
}
return this.powService.performChallenge(body.challenge);
}
}

View File

@@ -1,44 +1,34 @@
import { Module } from '@nestjs/common';
import { PowService } from './pow.service';
import { CacheModule } from '@nestjs/cache-manager';
import { PowController } from './pow.controller';
import { PrometheusModule, makeGaugeProvider } from '@willsoto/nestjs-prometheus';
import { makeGaugeProvider } from '@willsoto/nestjs-prometheus';
@Module({
imports: [
CacheModule.register({
ttl: 5,
max: 10,
}),
PrometheusModule.register({
customMetricPrefix: 'pow',
defaultMetrics: {
enabled: false,
},
}),
],
providers: [PowService,
imports: [],
providers: [
PowService,
makeGaugeProvider({
name: 'challenges_generated',
name: 'pow_challenges_generated',
help: 'The total number of POW challenges generated',
}),
makeGaugeProvider({
name: 'challenges_completed',
name: 'pow_challenges_completed',
help: 'The total number of POW challenges completed',
}),
makeGaugeProvider({
name: 'successful_verifies',
name: 'pow_successful_verifies',
help: 'The total number of successful POW challenge verifications',
}),
makeGaugeProvider({
name: 'failed_verifies',
name: 'pow_failed_verifies',
help: 'The total number of failed POW challenge verifications',
}),
makeGaugeProvider({
name: 'difficulty',
name: 'pow_difficulty',
help: 'The current POW difficulty',
}),
],
controllers: [PowController]
controllers: [PowController],
exports: [PowService],
})
export class PowModule { }
export class PowModule {}

View File

@@ -4,102 +4,99 @@ 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;
private readonly logger = new Logger(PowService.name);
private difficulty = 5;
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache,
@InjectMetric('challenges_generated') private challengesGenerated: any,
@InjectMetric('challenges_completed') private challengesCompleted: any,
@InjectMetric('successful_verifies') private successfulVerifies: any,
@InjectMetric('failed_verifies') private failedVerifies: any,
@InjectMetric('difficulty') private powDifficulty: any,
) {
this.powDifficulty.set(this.difficulty);
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<boolean> {
const expected = await this.cacheManager.get<boolean>(challenge);
const success = expected ? this.hashAndCheck(proof + challenge) : false;
if (success) {
this.successfulVerifies.inc();
} else {
this.failedVerifies.inc();
}
return success;
}
/**
* Generate a proof of work challenge, stored to redis for verification within
* the next 10 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;
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 };
}
generateRandom256BitString() {
const randomString = randomBytes(32).toString('hex');
return randomString;
}
/**
* 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);
}
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<boolean> {
const expected = await this.cacheManager.get<boolean>(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;
}
/**
* Get the current difficulty of the proof of work challenge.
*/
getDifficulty() {
return this.difficulty;
}
}