diff --git a/src/pow.ts b/src/pow.ts index 3f271a7..64dbb91 100644 --- a/src/pow.ts +++ b/src/pow.ts @@ -6,6 +6,8 @@ import { Kysely } from 'kysely'; const recentChallenges: { [challenge: string]: number } = {}; const CHALLENGE_TIMEOUT = 60 * 1000; +const COMPLEXITY_LOOKBACK = 60 * 1000; // complexity is based on number of successful registrations in this timeframe +const COMPLEXITY_MINIMUM = 5; export class ProofOfWork { #db: Kysely; @@ -30,6 +32,7 @@ export class ProofOfWork { .createTable('authorizedTenants') .ifNotExists() .addColumn('did', 'text', (column) => column.primaryKey()) + .addColumn('timeadded', 'timestamp', (column) => column.notNull()) .execute(); } @@ -55,7 +58,7 @@ export class ProofOfWork { async authorizeTenant(tenant: string): Promise { await this.#db .insertInto('authorizedTenants') - .values({ did: tenant }) + .values({ did: tenant, timeadded: Date.now() }) .executeTakeFirst(); } @@ -64,7 +67,7 @@ export class ProofOfWork { recentChallenges[challenge] = Date.now(); res.json({ challenge: challenge, - complexity: getComplexity(), + complexity: await this.getComplexity(), }); } @@ -79,7 +82,7 @@ export class ProofOfWork { hash.update(body.challenge); hash.update(body.response); - const complexity = getComplexity(); + const complexity = await this.getComplexity(); const digest = hash.digest('hex'); if (!digest.startsWith('0'.repeat(complexity))) { res.status(401).json({ success: false }); @@ -89,7 +92,7 @@ export class ProofOfWork { try { await this.#db .insertInto('authorizedTenants') - .values({ did: body.did }) + .values({ did: body.did, timeadded: Date.now() }) .executeTakeFirst(); } catch (e) { console.log('error inserting did', e); @@ -98,6 +101,25 @@ export class ProofOfWork { } res.json({ success: true }); } + + private async getComplexity(): Promise { + const result = await this.#db + .selectFrom('authorizedTenants') + .where('timeadded', '>', Date.now() - COMPLEXITY_LOOKBACK) + .select((eb) => eb.fn.countAll().as('recent_reg_count')) + .executeTakeFirstOrThrow(); + const recent = result.recent_reg_count as number; + if (recent == 0) { + return COMPLEXITY_MINIMUM; + } + + const complexity = Math.floor(recent / 10); + if (complexity < COMPLEXITY_MINIMUM) { + return COMPLEXITY_MINIMUM; + } + + return complexity; + } } const challengeCharacters = @@ -113,11 +135,9 @@ function generateChallenge(): string { return challenge; } -function getComplexity(): number { - return Object.keys(recentChallenges).length; -} interface AuthorizedTenants { did: string; + timeadded: number; } interface PowDatabase { diff --git a/tests/http-api.spec.ts b/tests/http-api.spec.ts index 8764ca1..9afab0e 100644 --- a/tests/http-api.spec.ts +++ b/tests/http-api.spec.ts @@ -45,10 +45,11 @@ describe('http api', function () { let httpApi: HttpApi; let server: Server; let profile: Profile; + let pow: ProofOfWork; before(async function () { config.powRegistration = true; - const pow = new ProofOfWork(getDialectFromURI(new URL('sqlite://'))); + pow = new ProofOfWork(getDialectFromURI(new URL('sqlite://'))); profile = await createProfile(); httpApi = new HttpApi(dwn, pow); }); @@ -72,7 +73,7 @@ describe('http api', function () { complexity: number; }; expect(body.challenge.length).to.equal(10); - expect(body.complexity).to.equal(1); + expect(body.complexity).to.equal(5); }); it('accepts a correct registration challenge', async function () { @@ -83,7 +84,7 @@ describe('http api', function () { complexity: number; }; expect(body.challenge.length).to.equal(10); - expect(body.complexity).to.equal(2); + expect(body.complexity).to.equal(5); // solve the challenge let response = ''; @@ -102,18 +103,67 @@ describe('http api', function () { }); expect(submitResponse.status).to.equal(200); - }); - - it('increase complexity as more challenges are issued', async function () { - const challengeResponse = await fetch('http://localhost:3000/register'); - expect(challengeResponse.status).to.equal(200); - const body = (await challengeResponse.json()) as { - challenge: string; - complexity: number; - }; - expect(body.challenge.length).to.equal(10); - expect(body.complexity).to.equal(3); - }); + }).timeout(30000); + + it('increase complexity as more challenges are completed', async function () { + for (let i = 1; i <= 60; i++) { + const p = await createProfile(); + if (i < 59) { + pow.authorizeTenant(p.did); + continue; + } + + const challengeResponse = await fetch('http://localhost:3000/register'); + expect(challengeResponse.status).to.equal(200); + const body = (await challengeResponse.json()) as { + challenge: string; + complexity: number; + }; + expect(body.challenge.length).to.equal(10); + + // solve the challenge + let response = ''; + let iterations = 0; + const start = Date.now(); + while (!checkNonce(body.challenge, response, body.complexity)) { + response = generateNonce(5); + iterations++; + if (iterations % 10000000 == 0) { + console.log( + 'complexity:', + body.complexity, + 'iteration count:', + iterations, + 'duration:', + Date.now() - start, + 'ms', + ); + } + } + + console.log( + 'complexity:', + body.complexity, + 'iteration count:', + iterations, + 'duration:', + Date.now() - start, + 'ms', + ); + + const submitResponse = await fetch('http://localhost:3000/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + challenge: body.challenge, + response: response, + did: p.did, + }), + }); + + expect(submitResponse.status).to.equal(200); + } + }).timeout(120000); it('rejects an invalid nonce', async function () { const challengeResponse = await fetch('http://localhost:3000/register');