diff --git a/src/dwn-server.ts b/src/dwn-server.ts index 182d0e4..aba6b8f 100644 --- a/src/dwn-server.ts +++ b/src/dwn-server.ts @@ -62,7 +62,7 @@ export class DwnServer { if (!this.dwn) { registrationManager = await RegistrationManager.create({ sqlDialect: tenantGateDB, termsOfService }); - this.dwn = await Dwn.create(getDWNConfig(this.config, registrationManager.getTenantGate())); + this.dwn = await Dwn.create(getDWNConfig(this.config, registrationManager)); } this.#httpApi = new HttpApi(this.config, this.dwn, registrationManager); diff --git a/src/registration/proof-of-work-manager.ts b/src/registration/proof-of-work-manager.ts index a6e4155..a5ffe8b 100644 --- a/src/registration/proof-of-work-manager.ts +++ b/src/registration/proof-of-work-manager.ts @@ -9,8 +9,8 @@ export class ProofOfWorkManager { private initialMaximumHashValueAsBigInt: bigint; private desiredSolveCountPerMinute: number; - static readonly challengeRefreshFrequencyInMilliseconds = 10 * 60 * 1000; // 10 minutes - static readonly difficultyReevaluationFrequencyInMilliseconds = 10000; + public challengeRefreshFrequencyInSeconds: number; + public difficultyReevaluationFrequencyInSeconds: number; public get currentMaximumAllowedHashValue(): bigint { return this.currentMaximumHashValueAsBigInt; @@ -20,19 +20,40 @@ export class ProofOfWorkManager { return this.proofOfWorkOfLastMinute.size; } - private constructor (desiredSolveCountPerMinute: number, initialMaximumHashValue: string) { + private constructor (input: { + desiredSolveCountPerMinute: number, + initialMaximumHashValue: string, + challengeRefreshFrequencyInSeconds: number, + difficultyReevaluationFrequencyInSeconds: number + }) { + const { desiredSolveCountPerMinute, initialMaximumHashValue } = input; + this.challengeNonces = { currentChallengeNonce: ProofOfWork.generateNonce() }; this.currentMaximumHashValueAsBigInt = BigInt(`0x${initialMaximumHashValue}`); this.initialMaximumHashValueAsBigInt = BigInt(`0x${initialMaximumHashValue}`); this.desiredSolveCountPerMinute = desiredSolveCountPerMinute; + this.challengeRefreshFrequencyInSeconds = input.challengeRefreshFrequencyInSeconds; + this.difficultyReevaluationFrequencyInSeconds = input.difficultyReevaluationFrequencyInSeconds; } public static async create(input: { desiredSolveCountPerMinute: number, initialMaximumHashValue: string, autoStart: boolean, + challengeRefreshFrequencyInSeconds?: number, + difficultyReevaluationFrequencyInSeconds?: number }): Promise { - const proofOfWorkManager = new ProofOfWorkManager(input.desiredSolveCountPerMinute, input.initialMaximumHashValue); + const { desiredSolveCountPerMinute, initialMaximumHashValue } = input; + + const challengeRefreshFrequencyInSeconds = input.challengeRefreshFrequencyInSeconds ?? 10 * 60; // 10 minutes default + const difficultyReevaluationFrequencyInSeconds = input.difficultyReevaluationFrequencyInSeconds ?? 10; // 10 seconds default + + const proofOfWorkManager = new ProofOfWorkManager({ + desiredSolveCountPerMinute, + initialMaximumHashValue, + challengeRefreshFrequencyInSeconds, + difficultyReevaluationFrequencyInSeconds + }); if (input.autoStart) { proofOfWorkManager.start(); @@ -104,12 +125,11 @@ export class ProofOfWorkManager { private periodicallyRefreshChallengeNonce (): void { try { - this.challengeNonces.previousChallengeNonce = this.challengeNonces.currentChallengeNonce; - this.challengeNonces.currentChallengeNonce = ProofOfWork.generateNonce(); + this.refreshChallengeNonce(); } catch (error) { console.error(`Encountered error while refreshing challenge nonce: ${error}`); } finally { - setTimeout(async () => this.periodicallyRefreshChallengeNonce(), ProofOfWorkManager.challengeRefreshFrequencyInMilliseconds); + setTimeout(async () => this.periodicallyRefreshChallengeNonce(), this.challengeRefreshFrequencyInSeconds * 1000); } } @@ -119,7 +139,7 @@ export class ProofOfWorkManager { } catch (error) { console.error(`Encountered error while updating proof of work difficulty: ${error}`); } finally { - setTimeout(async () => this.periodicallyRefreshProofOfWorkDifficulty(), ProofOfWorkManager.difficultyReevaluationFrequencyInMilliseconds); + setTimeout(async () => this.periodicallyRefreshProofOfWorkDifficulty(), this.difficultyReevaluationFrequencyInSeconds * 1000); } } @@ -132,6 +152,11 @@ export class ProofOfWorkManager { } } + private refreshChallengeNonce(): void { + this.challengeNonces.previousChallengeNonce = this.challengeNonces.currentChallengeNonce; + this.challengeNonces.currentChallengeNonce = ProofOfWork.generateNonce(); + } + /** * Refreshes the difficulty by changing the max hash value. * The higher the number, the easier. Scale 1 (hardest) to 2^256 (easiest), represented in HEX. @@ -149,7 +174,7 @@ export class ProofOfWorkManager { // NOTE: bigint arithmetic does NOT work with decimals, so we work with "full numbers" by multiplying by a scale factor. const scaleFactor = 1_000_000; - const difficultyEvaluationsPerMinute = 60000 / ProofOfWorkManager.difficultyReevaluationFrequencyInMilliseconds; // assumed to be >= 1; + const difficultyEvaluationsPerMinute = 60000 / (this.difficultyReevaluationFrequencyInSeconds * 1000); // assumed to be >= 1; // NOTE: easier difficulty is represented by a larger max allowed hash value // and harder difficulty is represented by a smaller max allowed hash value. @@ -173,22 +198,14 @@ export class ProofOfWorkManager { = (this.currentMaximumHashValueAsBigInt - newMaximumHashValueAsBigIntPriorToMultiplierAdjustment) * (BigInt(Math.floor(increaseMultiplier * scaleFactor)) / BigInt(scaleFactor)); - const hashValueDecreaseAmount - = hashValueDecreaseAmountPriorToEvaluationFrequencyAdjustment / BigInt(difficultyEvaluationsPerMinute); - - let newMaximumHashValueAsBigInt = this.currentMaximumHashValueAsBigInt - hashValueDecreaseAmount; + const hashValueDecreaseAmount = hashValueDecreaseAmountPriorToEvaluationFrequencyAdjustment / BigInt(difficultyEvaluationsPerMinute); - if (newMaximumHashValueAsBigInt === BigInt(0)) { - // if newMaximumHashValueAsBigInt is 0, we use 1 instead because 0 cannot multiply another number - newMaximumHashValueAsBigInt = BigInt(1); - } - - this.currentMaximumHashValueAsBigInt = newMaximumHashValueAsBigInt; + this.currentMaximumHashValueAsBigInt -= hashValueDecreaseAmount; } else { // if solve rate is lower than desired, make difficulty easier by making the max allowed hash value larger if (this.currentMaximumHashValueAsBigInt === this.initialMaximumHashValueAsBigInt) { - // if current difficulty is already at initial difficulty, don't make it any easier + // if current difficulty is already at initial difficulty, nothing to do return; } @@ -199,7 +216,13 @@ export class ProofOfWorkManager { = differenceBetweenInitialAndCurrentDifficulty / BigInt(backToInitialDifficultyInMinutes * difficultyEvaluationsPerMinute); } - this.currentMaximumHashValueAsBigInt += this.hashValueIncrementPerEvaluation; + // if newly calculated difficulty is lower than initial difficulty, just use the initial difficulty + const newMaximumAllowedHashValueAsBigInt = this.currentMaximumHashValueAsBigInt + this.hashValueIncrementPerEvaluation; + if (newMaximumAllowedHashValueAsBigInt >= this.initialMaximumHashValueAsBigInt) { + this.currentMaximumHashValueAsBigInt = this.initialMaximumHashValueAsBigInt; + } else { + this.currentMaximumHashValueAsBigInt = newMaximumAllowedHashValueAsBigInt; + } } } } diff --git a/src/registration/proof-of-work.ts b/src/registration/proof-of-work.ts index f7a0464..918ecc1 100644 --- a/src/registration/proof-of-work.ts +++ b/src/registration/proof-of-work.ts @@ -68,18 +68,9 @@ export class ProofOfWork { qualifiedSolutionNonceFound = computedHashAsBigInt <= maximumAllowedHashValueAsBigInt; iterations++; - - // Log every 1M iterations. - if (iterations % 1_000_000 === 0) { - console.log( - `iterations: ${iterations}, time lapsed: ${ - Date.now() - startTime - } ms`, - ); - } } while (!qualifiedSolutionNonceFound); - // Log final/successful attempt. + // Log final/successful iteration. console.log( `iterations: ${iterations}, time lapsed: ${Date.now() - startTime} ms`, ); diff --git a/src/registration/registration-manager.ts b/src/registration/registration-manager.ts index 2b23a8e..0eae158 100644 --- a/src/registration/registration-manager.ts +++ b/src/registration/registration-manager.ts @@ -6,20 +6,14 @@ import type { RegistrationData, RegistrationRequest } from "./registration-types import type { ProofOfWorkChallengeModel } from "./proof-of-work-types.js"; import { DwnServerError, DwnServerErrorCode } from "../dwn-error.js"; import type { TenantGate } from "@tbd54566975/dwn-sdk-js"; -import { RegistrationTenantGate } from "./registration-tenant-gate.js"; -export class RegistrationManager { - private tenantGate: TenantGate; +export class RegistrationManager implements TenantGate { private proofOfWorkManager: ProofOfWorkManager; private registrationStore: RegistrationStore; private termsOfServiceHash?: string; private termsOfService?: string; - public getTenantGate(): TenantGate { - return this.tenantGate; - } - public getTermsOfService(): string { return this.termsOfService; } @@ -28,10 +22,17 @@ export class RegistrationManager { return this.termsOfServiceHash; } + /** + * Updates the terms-of-service. Exposed for testing purposes. + */ + public updateTermsOfService(termsOfService: string): void { + this.termsOfServiceHash = ProofOfWork.hashAsHexString([termsOfService]); + this.termsOfService = termsOfService; + } + private constructor (termsOfService?: string) { if (termsOfService) { - this.termsOfServiceHash = ProofOfWork.hashAsHexString([termsOfService]); - this.termsOfService = termsOfService; + this.updateTermsOfService(termsOfService); } } @@ -52,7 +53,6 @@ export class RegistrationManager { // Initialize RegistrationStore. const registrationStore = await RegistrationStore.create(sqlDialect); registrationManager.registrationStore = registrationStore; - registrationManager.tenantGate = await RegistrationTenantGate.create(registrationStore, registrationManager.getTermsOfServiceHash()); return registrationManager; } @@ -90,4 +90,18 @@ export class RegistrationManager { public async recordTenantRegistration(registrationData: RegistrationData): Promise { await this.registrationStore.insertOrUpdateTenantRegistration(registrationData); } + + public async isActiveTenant(tenant: string): Promise { + const tenantRegistration = await this.registrationStore.getTenantRegistration(tenant); + + if (tenantRegistration === undefined) { + return false + } + + if (tenantRegistration.termsOfServiceHash !== this.termsOfServiceHash) { + return false; + } + + return true; + } } diff --git a/src/registration/registration-tenant-gate.ts b/src/registration/registration-tenant-gate.ts deleted file mode 100644 index df3430e..0000000 --- a/src/registration/registration-tenant-gate.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { TenantGate } from '@tbd54566975/dwn-sdk-js'; -import type { RegistrationStore } from './registration-store.js'; - -export class RegistrationTenantGate implements TenantGate { - private constructor(private registrationStore: RegistrationStore, private termsOfServiceHash: string) { } - - public static async create(registrationStore: RegistrationStore, termsOfServiceHash: string): Promise { - const tenantGate = new RegistrationTenantGate(registrationStore, termsOfServiceHash); - return tenantGate; - } - - public async isActiveTenant(tenant: string): Promise { - const tenantRegistration = await this.registrationStore.getTenantRegistration(tenant); - - if (tenantRegistration === undefined) { - return false - } - - if (tenantRegistration.termsOfServiceHash !== this.termsOfServiceHash) { - return false; - } - - return true; - } -} diff --git a/tests/dwn-server.spec.ts b/tests/dwn-server.spec.ts index b3e5e7a..db3c501 100644 --- a/tests/dwn-server.spec.ts +++ b/tests/dwn-server.spec.ts @@ -7,6 +7,12 @@ import { getTestDwn } from './test-dwn.js'; describe('DwnServer', function () { let dwnServer: DwnServer; before(async function () { + + // Mute all server console logs during tests. + console.log = (): void => {}; + console.error = (): void => {}; + console.info = (): void => {}; + const testDwn = await getTestDwn(); dwnServer = new DwnServer({ dwn: testDwn, config: config }); }); diff --git a/tests/http-api.spec.ts b/tests/http-api.spec.ts index 02b7767..66547aa 100644 --- a/tests/http-api.spec.ts +++ b/tests/http-api.spec.ts @@ -56,13 +56,13 @@ describe('http api', function () { config.registrationProofOfWorkEnabled = true; config.termsOfServiceFilePath = './tests/fixtures/terms-of-service.txt'; - + // RegistrationManager creation const sqlDialect = getDialectFromURI(new URL('sqlite://')); const termsOfService = readFileSync(config.termsOfServiceFilePath).toString(); registrationManager = await RegistrationManager.create({ sqlDialect, termsOfService }); - dwn = await getTestDwn(registrationManager.getTenantGate()); + dwn = await getTestDwn(registrationManager); httpApi = new HttpApi(config, dwn, registrationManager); diff --git a/tests/registration/proof-of-work-manager.spec.ts b/tests/registration/proof-of-work-manager.spec.ts index e4d5954..081c04f 100644 --- a/tests/registration/proof-of-work-manager.spec.ts +++ b/tests/registration/proof-of-work-manager.spec.ts @@ -1,11 +1,12 @@ +import sinon from 'sinon'; + import { expect } from 'chai'; import { useFakeTimers } from 'sinon'; import { v4 as uuidv4 } from 'uuid'; import { ProofOfWorkManager } from '../..//src/registration/proof-of-work-manager.js'; - describe('ProofOfWorkManager', function () { let clock; @@ -23,46 +24,115 @@ describe('ProofOfWorkManager', function () { clock.restore(); }); - describe('complexity', function () { - - it('should become more complex as more successful proof-of-work is submitted', async function () { - const desiredSolveCountPerMinute = 10; - const initialMaximumHashValue = 'FFFFFFFF'; - const proofOfWorkManager = await ProofOfWorkManager.create({ - autoStart: false, - desiredSolveCountPerMinute, - initialMaximumHashValue, - }); + it('should periodically refresh the challenge nonce and proof-of-work difficulty', async function () { + const desiredSolveCountPerMinute = 10; + const initialMaximumHashValue = 'FFFFFFFF'; + const proofOfWorkManager = await ProofOfWorkManager.create({ + autoStart: true, + desiredSolveCountPerMinute, + initialMaximumHashValue, + }); - // Load up desiredSolveRatePerMinute number of proof-of-work entries, so all future new entries will increase the complexity. - for (let i = 0; i < desiredSolveCountPerMinute; i++) { - await proofOfWorkManager.recordProofOfWork(uuidv4()); + // stub that throws half the time + const stub = (): void => { + // Generate a random number between 0 and 1 + const random = Math.random(); + + // If the random number is less than 0.5, throw an error + if (random < 0.5) { + throw new Error('Random error'); } + }; - let lastMaximumAllowedHashValue = BigInt('0x' + initialMaximumHashValue); - for (let i = 0; i < 100; i++) { - // Simulating 1 proof-of-work per second which is ~60/min for 100 seconds. - clock.tick(1000); - await proofOfWorkManager.recordProofOfWork(uuidv4()); - await proofOfWorkManager.refreshMaximumAllowedHashValue(); + const challengeNonceRefreshSpy = sinon.stub(proofOfWorkManager, 'refreshChallengeNonce').callsFake(stub); + const maximumAllowedHashValueRefreshSpy = sinon.stub(proofOfWorkManager, 'refreshMaximumAllowedHashValue').callsFake(stub); - // The maximum allowed hash value should be decreasing as more proof-of-work is submitted. - expect(proofOfWorkManager.currentMaximumAllowedHashValue < lastMaximumAllowedHashValue).to.be.true; - lastMaximumAllowedHashValue = proofOfWorkManager.currentMaximumAllowedHashValue; - } + // Simulated 1 hour has passed, so all proof-of-work entries should be removed. + clock.tick(60 * 60 * 1000); - // Simulated 100 seconds has passed, so all proof-of-work entries should be removed. - clock.tick(100_000); + // 1 hour divided by the challenge refresh frequency + const expectedChallengeNonceRefreshCount = 60 * 60 / proofOfWorkManager.challengeRefreshFrequencyInSeconds; - for (let i = 0; i < 100; i++) { - // Simulating no proof-of-work load for 100 seconds. - clock.tick(1000); - await proofOfWorkManager.refreshMaximumAllowedHashValue(); + // 1 hour divided by the challenge refresh frequency + const expectedDifficultyReevaluationCount = 60 * 60 / proofOfWorkManager.difficultyReevaluationFrequencyInSeconds; - // The maximum allowed hash value should be increasing again. - expect(proofOfWorkManager.currentMaximumAllowedHashValue > lastMaximumAllowedHashValue).to.be.true; - lastMaximumAllowedHashValue = proofOfWorkManager.currentMaximumAllowedHashValue; - } + expect(challengeNonceRefreshSpy.callCount).to.greaterThanOrEqual(expectedChallengeNonceRefreshCount); + expect(maximumAllowedHashValueRefreshSpy.callCount).to.greaterThanOrEqual(expectedDifficultyReevaluationCount); + }); + + it('should increase difficulty if proof-of-work rate goes above desired rate and reduce difficulty as proof-of-work rate falls below desired rate.', async function () { + const desiredSolveCountPerMinute = 10; + const initialMaximumHashValue = 'FFFFFFFF'; + const proofOfWorkManager = await ProofOfWorkManager.create({ + autoStart: true, + desiredSolveCountPerMinute, + initialMaximumHashValue, }); + + // Load up desiredSolveRatePerMinute number of proof-of-work entries, so all future new entries will increase the complexity. + for (let i = 0; i < desiredSolveCountPerMinute; i++) { + await proofOfWorkManager.recordProofOfWork(uuidv4()); + } + + let initialMaximumAllowedHashValue = proofOfWorkManager.currentMaximumAllowedHashValue; + let lastMaximumAllowedHashValue = BigInt('0x' + initialMaximumHashValue); + const lastSolveCountPerMinute = 0; + for (let i = 0; i < 100; i++) { + // Simulating 1 proof-of-work per second which for 100 seconds. + await proofOfWorkManager.recordProofOfWork(uuidv4()); + expect(proofOfWorkManager.currentSolveCountPerMinute).to.be.greaterThanOrEqual(lastSolveCountPerMinute); + clock.tick(1000); + + // The maximum allowed hash value should be monotonically decreasing as more proof-of-work is submitted. + expect(proofOfWorkManager.currentMaximumAllowedHashValue <= lastMaximumAllowedHashValue).to.be.true; + lastMaximumAllowedHashValue = proofOfWorkManager.currentMaximumAllowedHashValue; + } + expect(proofOfWorkManager.currentMaximumAllowedHashValue < initialMaximumAllowedHashValue).to.be.true; + + // Simulated 100 seconds has passed, so all proof-of-work entries should be removed. + clock.tick(100_000); + clock.runToLast(); + + expect(proofOfWorkManager.currentSolveCountPerMinute).to.equal(0); + + initialMaximumAllowedHashValue = proofOfWorkManager.currentMaximumAllowedHashValue; + for (let i = 0; i < 100; i++) { + // Simulating no proof-of-work load for 100 seconds. + clock.tick(1000); + + // The maximum allowed hash value should be monotonically increasing again. + expect(proofOfWorkManager.currentMaximumAllowedHashValue >= lastMaximumAllowedHashValue).to.be.true; + lastMaximumAllowedHashValue = proofOfWorkManager.currentMaximumAllowedHashValue; + } + expect(proofOfWorkManager.currentMaximumAllowedHashValue > initialMaximumAllowedHashValue).to.be.true; + }); + + it('should reduce difficulty back to initial difficulty when proof-of-work rate is lower than desired rate for long enough', async function () { + const desiredSolveCountPerMinute = 10; + const initialMaximumHashValue = 'FFFFFFFF'; + const initialMaximumHashValueAsBigInt = BigInt('0x' + initialMaximumHashValue); + const proofOfWorkManager = await ProofOfWorkManager.create({ + autoStart: true, + desiredSolveCountPerMinute, + initialMaximumHashValue, + }); + + // Load up desiredSolveRatePerMinute number of proof-of-work entries, so all future new entries will increase the complexity. + for (let i = 0; i < desiredSolveCountPerMinute; i++) { + await proofOfWorkManager.recordProofOfWork(uuidv4()); + } + + // Simulating 1 proof-of-work per second which for 100 seconds to increase proof-of-work difficulty. + for (let i = 0; i < 100; i++) { + await proofOfWorkManager.recordProofOfWork(uuidv4()); + clock.tick(1000); + } + expect(proofOfWorkManager.currentMaximumAllowedHashValue < initialMaximumHashValueAsBigInt).to.be.true; + + // Simulated 1 hour has passed. + clock.tick(60 * 60 * 1000); + clock.runToLast(); + + expect(proofOfWorkManager.currentMaximumAllowedHashValue === initialMaximumHashValueAsBigInt).to.be.true; }); }); diff --git a/tests/scenarios/registration.spec.ts b/tests/scenarios/registration.spec.ts index e4bf6b7..fe380b6 100644 --- a/tests/scenarios/registration.spec.ts +++ b/tests/scenarios/registration.spec.ts @@ -63,7 +63,7 @@ describe('Registration scenarios', function () { const termsOfService = readFileSync(config.termsOfServiceFilePath).toString(); registrationManager = await RegistrationManager.create({ sqlDialect, termsOfService }); - dwn = await getTestDwn(registrationManager.getTenantGate()); + dwn = await getTestDwn(registrationManager); httpApi = new HttpApi(config, dwn, registrationManager); @@ -141,7 +141,7 @@ describe('Registration scenarios', function () { expect(registrationResponse.status).to.equal(200); // 6. Alice can now write to the DWN. - const { jsonRpcRequest, dataBytes } = await createRecordsWriteJsonRpcRequest(alice); + const { jsonRpcRequest, dataBytes } = await generateRecordsWriteJsonRpcRequest(alice); const writeResponse = await fetch(dwnMessageEndpoint, { method: 'POST', headers: { @@ -155,7 +155,7 @@ describe('Registration scenarios', function () { // 7. Sanity test that another non-tenant is NOT authorized to write. const nonTenant = await DidKeyResolver.generate(); - const nonTenantJsonRpcRequest = await createRecordsWriteJsonRpcRequest(nonTenant); + const nonTenantJsonRpcRequest = await generateRecordsWriteJsonRpcRequest(nonTenant); const nonTenantJsonRpcResponse = await fetch(dwnMessageEndpoint, { method: 'POST', headers: { @@ -168,9 +168,7 @@ describe('Registration scenarios', function () { expect(nonTenantJsonRpcResponseBody.result.reply.status.code).to.equal(401); }); - it('should reject a registration request that has proof-or-work that does not meet the difficulty requirement.', async function () { - // Scenario: // 0. Assume Alice fetched the terms-of-service and proof-of-work challenge. // 1. Alice computes the proof-of-work response nonce that is insufficient to meet the difficulty requirement. @@ -347,9 +345,53 @@ describe('Registration scenarios', function () { expect(registrationResponse.status).to.equal(400); expect(registrationResponseBody.code).to.equal(DwnServerErrorCode.ProofOfWorkManagerInvalidChallengeNonce); }); + + it('should reject a DWN message for an existing tenant who agreed to an outdated terms-of-service.', async () => { + // Scenario: + // 1. Alice is a registered tenant and is able to write to the DWN. + // 2. DWN server administrator updates the terms-of-service. + // 3. Alice no longer can write to the DWN because she has not agreed to the new terms-of-service. + + + // 1. Alice is a registered tenant and is able to write to the DWN. + // Short-cut to register Alice. + registrationManager.recordTenantRegistration({ + did: alice.did, + termsOfServiceHash: ProofOfWork.hashAsHexString([registrationManager.getTermsOfService()]) + }); + + // Sanity test that Alice can write to the DWN after registration. + const write1 = await generateRecordsWriteJsonRpcRequest(alice); + const write1Response = await fetch(dwnMessageEndpoint, { + method: 'POST', + headers: { + 'dwn-request': JSON.stringify(write1.jsonRpcRequest), + }, + body: new Blob([write1.dataBytes]), + }); + const write1ResponseBody = await write1Response.json() as JsonRpcResponse; + expect(write1Response.status).to.equal(200); + expect(write1ResponseBody.result.reply.status.code).to.equal(202); + + // 2. DWN server administrator updates the terms-of-service. + registrationManager.updateTermsOfService('new terms of service'); + + // 3. Alice no longer can write to the DWN because she has not agreed to the new terms-of-service. + const write2 = await generateRecordsWriteJsonRpcRequest(alice); + const write2Response = await fetch(dwnMessageEndpoint, { + method: 'POST', + headers: { + 'dwn-request': JSON.stringify(write2.jsonRpcRequest), + }, + body: new Blob([write2.dataBytes]), + }); + const write2ResponseBody = await write2Response.json() as JsonRpcResponse; + expect(write2Response.status).to.equal(200); + expect(write2ResponseBody.result.reply.status.code).to.equal(401); + }); }); -async function createRecordsWriteJsonRpcRequest(persona: Persona): Promise<{ jsonRpcRequest: JsonRpcRequest, dataBytes: Uint8Array }> { +async function generateRecordsWriteJsonRpcRequest(persona: Persona): Promise<{ jsonRpcRequest: JsonRpcRequest, dataBytes: Uint8Array }> { const { recordsWrite, dataStream } = await createRecordsWriteMessage(persona); const requestId = uuidv4();