From 608405459063303f42f2082107a1dd1cb70c3168 Mon Sep 17 00:00:00 2001 From: Chip Wasson Date: Thu, 4 Apr 2024 17:43:27 -0600 Subject: [PATCH] Add pow metrics --- src/file/file.controller.ts | 4 +- src/kv/kv.controller.ts | 4 +- src/pow/pow.controller.ts | 9 +++- src/pow/pow.module.ts | 30 +++++++++++++- src/pow/pow.service.ts | 23 ++++++++-- views/pow/index.hbs | 83 +++++++++++++++++++++++++++++++++++++ 6 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 views/pow/index.hbs diff --git a/src/file/file.controller.ts b/src/file/file.controller.ts index 47ae6cc..1782d37 100644 --- a/src/file/file.controller.ts +++ b/src/file/file.controller.ts @@ -3,9 +3,11 @@ import { FileService } from './file.service'; import { Post } from '@nestjs/common'; import { UploadedObjectInfo } from 'minio'; import { FileInterceptor } from '@nestjs/platform-express'; -import { ApiConsumes } from '@nestjs/swagger'; +import { ApiConsumes, ApiTags } from '@nestjs/swagger'; +import { InjectMetric } from '@willsoto/nestjs-prometheus'; @Controller('file') +@ApiTags('file') export class FileController { constructor(private readonly fileService: FileService) { } diff --git a/src/kv/kv.controller.ts b/src/kv/kv.controller.ts index 720924e..df3a721 100644 --- a/src/kv/kv.controller.ts +++ b/src/kv/kv.controller.ts @@ -12,7 +12,7 @@ import { } from '@nestjs/common'; import { KvService } from './kv.service'; import { Request } from 'express'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiQuery, ApiTags } from '@nestjs/swagger'; import { FileInterceptor } from '@nestjs/platform-express'; @Controller('kv') @@ -21,6 +21,8 @@ export class KvController { constructor(private readonly kvService: KvService) { } @Get(':namespace/:key/metadata') + @ApiQuery({ name: 'namespace', required: true }) + @ApiQuery({ name: 'key', required: true }) async getMetadata( @Param('namespace') namespace: string, @Param('key') key: string, diff --git a/src/pow/pow.controller.ts b/src/pow/pow.controller.ts index 6025148..5de3656 100644 --- a/src/pow/pow.controller.ts +++ b/src/pow/pow.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Post, Query } from '@nestjs/common'; +import { Body, Controller, Get, Post, Query, Render } from '@nestjs/common'; import { PowService } from './pow.service'; import { ApiBody, ApiQuery, ApiTags } from '@nestjs/swagger'; @@ -9,6 +9,13 @@ export class PowController { private readonly powService: PowService, ) { } + @Get('') + @Render('pow/index') + async index() { + return { + difficulty: this.powService.getDifficulty(), + }; + } @Get('challenge') async generateChallenge() { diff --git a/src/pow/pow.module.ts b/src/pow/pow.module.ts index 6a81dbd..b9c581e 100644 --- a/src/pow/pow.module.ts +++ b/src/pow/pow.module.ts @@ -2,6 +2,7 @@ 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'; @Module({ imports: [ @@ -9,8 +10,35 @@ import { PowController } from './pow.controller'; ttl: 5, max: 10, }), + PrometheusModule.register({ + customMetricPrefix: 'pow', + defaultMetrics: { + enabled: false, + }, + }), + ], + providers: [PowService, + makeGaugeProvider({ + name: 'challenges_generated', + help: 'The total number of POW challenges generated', + }), + makeGaugeProvider({ + name: 'challenges_completed', + help: 'The total number of POW challenges completed', + }), + makeGaugeProvider({ + name: 'successful_verifies', + help: 'The total number of successful POW challenge verifications', + }), + makeGaugeProvider({ + name: 'failed_verifies', + help: 'The total number of failed POW challenge verifications', + }), + makeGaugeProvider({ + name: 'difficulty', + help: 'The current POW difficulty', + }), ], - providers: [PowService], controllers: [PowController] }) export class PowModule { } diff --git a/src/pow/pow.service.ts b/src/pow/pow.service.ts index 2722930..fff8ea9 100644 --- a/src/pow/pow.service.ts +++ b/src/pow/pow.service.ts @@ -1,5 +1,6 @@ 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'; @@ -11,7 +12,14 @@ export class PowService { 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); + } /** * Generate a proof of work challenge, stored to redis for verification within @@ -19,7 +27,8 @@ export class PowService { */ async generateChallenge() { const challenge = this.generateRandom256BitString(); - console.log(challenge) + this.logger.verbose(`Generated challenge: ${challenge}`); + this.challengesGenerated.inc(); await this.cacheManager.set(challenge, true, 60 * 1000); return challenge; } @@ -44,10 +53,17 @@ export class PowService { */ async verifyChallenge(challenge: string, proof: string): Promise { const expected = await this.cacheManager.get(challenge); - return expected ? this.hashAndCheck(proof + challenge) : false; + 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); } @@ -76,6 +92,7 @@ export class PowService { */ setDifficulty(difficulty: number) { this.difficulty = difficulty; + this.powDifficulty.set(this.difficulty); } /** diff --git a/views/pow/index.hbs b/views/pow/index.hbs new file mode 100644 index 0000000..ff198a7 --- /dev/null +++ b/views/pow/index.hbs @@ -0,0 +1,83 @@ + + + Proof Of Work API + + +

PoW API

+

+ This is a simple API that allows you to generate a proof of work + challenges and verify the proof of work. +

+

+ The API has two endpoints: +

    +
  • GET + /pow/challenge + - This endpoint generates a new proof of work challenge.
  • +
  • POST + /pow/challenge + - This endpoint verifies a proof of work challenge.
  • +
+

+

+ The proof of work challenge is to find a proof which, when concatenated + and hashed (via SHA 512) with a challenge string that starts with a + certain number of zeros. The number of zeros is determined by the + difficulty level. The difficulty level is a number between 1 and 10. The + higher the difficulty level, the more zeros the string must start with. +

+

+ The current difficulty is + {{difficulty}}. +

+

+ The example code for performing a proof of work challenge is: +

+          const crypto = require('crypto');
+          const axios = require('axios');
+
+          async function getChallenge() {
+            const response = await axios.get('https://api.us.dev/pow/challenge');
+            return response.data;
+          }
+
+          async function submitSolution(challenge, proof) {
+            const response = await axios.post('https://api.us.dev/pow/challenge', {
+              challenge,
+              proof
+            });
+            return response.data;
+          }
+
+          function hashProof(challenge, proof) {
+            return crypto.createHash('sha512').update(challenge + proof).digest('hex');
+          }
+
+          function verifyChallenge(hash: string, difficulty: number) {
+            return hash.startsWith('0'.repeat(difficulty));
+          } 
+
+          async function proofOfWork() {
+            const challenge = await getChallenge();
+            let proof = 0;
+            let hash = '';
+            do {
+              proof++;
+              hash = hashProof(challenge, proof);
+            } while (!verifyChallenge(hash, 5));
+            return await submitSolution(challenge, proof);
+          }
+
+          proofOfWork().then(console.log).catch(console.error);
+          
+

+

+ If you're a developer you should use the GET /pow/challenge endpoint to + get the challenge, then pass the challenge to your client, then have the + client find a solution to the challenge and then submit their proof as + part of their later action. Your service should then verify the solution + using the POST /pow/challenge endpoint (or your own local implementation) + to verify the solution. +

+ + \ No newline at end of file