Skip to content

Commit 8baa679

Browse files
authored
Added DWN Registrar utility class (#765)
1 parent 750aa1c commit 8baa679

File tree

4 files changed

+152
-0
lines changed

4 files changed

+152
-0
lines changed

.changeset/heavy-papayas-reflect.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@web5/agent": patch
3+
---
4+
5+
Added DWN Registrar utility class

packages/agent/.c8rc.json

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
],
1717
"reporter": [
1818
"cobertura",
19+
"html",
1920
"text"
2021
]
2122
}

packages/agent/src/dwn-registrar.ts

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { Sha256, utils } from '@web5/crypto';
2+
import { concatenateUrl } from './utils.js';
3+
import { Convert } from '@web5/common';
4+
5+
/**
6+
* A client for registering tenants with a DWN.
7+
*/
8+
export class DwnRegistrar {
9+
/**
10+
* Registers a new tenant with the given DWN.
11+
* NOTE: Assumes the user has already accepted the terms of service.
12+
* NOTE: Currently the DWN Server from `dwn-server` does not require user signature.
13+
* TODO: bring in types from `dwn-server`.
14+
*/
15+
public static async registerTenant(dwnEndpoint: string, did: string): Promise<void> {
16+
17+
const registrationEndpoint = concatenateUrl(dwnEndpoint, 'registration');
18+
const termsOfUseEndpoint = concatenateUrl(registrationEndpoint, 'terms-of-service');
19+
const proofOfWorkEndpoint = concatenateUrl(registrationEndpoint, 'proof-of-work');
20+
21+
// fetch the terms-of-service
22+
const termsOfServiceGetResponse = await fetch(termsOfUseEndpoint, {
23+
method: 'GET',
24+
});
25+
26+
if (termsOfServiceGetResponse.status !== 200) {
27+
const statusCode = termsOfServiceGetResponse.status;
28+
const statusText = termsOfServiceGetResponse.statusText;
29+
const errorText = await termsOfServiceGetResponse.text();
30+
throw new Error(`Failed fetching terms-of-service: ${statusCode} ${statusText}: ${errorText}`);
31+
}
32+
const termsOfServiceFetched = await termsOfServiceGetResponse.text();
33+
34+
// fetch the proof-of-work challenge
35+
const proofOfWorkChallengeGetResponse = await fetch(proofOfWorkEndpoint, {
36+
method: 'GET',
37+
});
38+
const { challengeNonce, maximumAllowedHashValue} = await proofOfWorkChallengeGetResponse.json();
39+
40+
// create registration data based on the hash of the terms-of-service and the DID
41+
const registrationData = {
42+
did,
43+
termsOfServiceHash: await DwnRegistrar.hashAsHexString(termsOfServiceFetched),
44+
};
45+
46+
// compute the proof-of-work response nonce based on the the proof-of-work challenge and the registration data.
47+
const responseNonce = await DwnRegistrar.findQualifiedResponseNonce({
48+
challengeNonce,
49+
maximumAllowedHashValue,
50+
requestData: JSON.stringify(registrationData),
51+
});
52+
53+
// send the registration request to the server
54+
const registrationRequest = {
55+
registrationData,
56+
proofOfWork: {
57+
challengeNonce,
58+
responseNonce,
59+
},
60+
};
61+
62+
const registrationResponse = await fetch(registrationEndpoint, {
63+
method : 'POST',
64+
headers : { 'Content-Type': 'application/json' },
65+
body : JSON.stringify(registrationRequest),
66+
});
67+
68+
if (registrationResponse.status !== 200) {
69+
const statusCode = registrationResponse.status;
70+
const statusText = registrationResponse.statusText;
71+
const errorText = await registrationResponse.text();
72+
throw new Error(`Registration failed: ${statusCode} ${statusText}: ${errorText}`);
73+
}
74+
}
75+
76+
/**
77+
* Computes the SHA-256 hash of the given array of strings.
78+
*/
79+
public static async hashAsHexString(input: string): Promise<string> {
80+
const hashAsBytes = await Sha256.digest({ data: Convert.string(input).toUint8Array()});
81+
const hashAsHex = Convert.uint8Array(hashAsBytes).toHex();
82+
return hashAsHex;
83+
}
84+
85+
/**
86+
* Finds a response nonce that qualifies the difficulty requirement for the given proof-of-work challenge and request data.
87+
*/
88+
public static async findQualifiedResponseNonce(input: {
89+
maximumAllowedHashValue: string;
90+
challengeNonce: string;
91+
requestData: string;
92+
}): Promise<string> {
93+
const startTime = Date.now();
94+
95+
const { maximumAllowedHashValue, challengeNonce, requestData } = input;
96+
const maximumAllowedHashValueAsBigInt = BigInt(`0x${maximumAllowedHashValue}`);
97+
98+
let iterations = 1;
99+
let responseNonce;
100+
let qualifiedSolutionNonceFound = false;
101+
do {
102+
responseNonce = await this.generateNonce();
103+
const computedHash = await DwnRegistrar.hashAsHexString(challengeNonce + responseNonce + requestData);
104+
const computedHashAsBigInt = BigInt(`0x${computedHash}`);
105+
106+
qualifiedSolutionNonceFound = computedHashAsBigInt <= maximumAllowedHashValueAsBigInt;
107+
108+
iterations++;
109+
} while (!qualifiedSolutionNonceFound);
110+
111+
// Log final/successful iteration.
112+
console.log(
113+
`iterations: ${iterations}, time lapsed: ${Date.now() - startTime} ms`,
114+
);
115+
116+
return responseNonce;
117+
}
118+
119+
/**
120+
* Generates 32 random bytes expressed as a HEX string.
121+
*/
122+
public static async generateNonce(): Promise<string> {
123+
const randomBytes = utils.randomBytes(32);
124+
const hexString = await Convert.uint8Array(randomBytes).toHex().toUpperCase();
125+
return hexString;
126+
}
127+
}

packages/agent/src/utils.ts

+19
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,23 @@ export async function getPaginationCursor(message: RecordsWriteMessage, dateSort
8484

8585
export function webReadableToIsomorphicNodeReadable(webReadable: ReadableStream<any>) {
8686
return new ReadableWebToNodeStream(webReadable);
87+
}
88+
89+
90+
/**
91+
* Concatenates a base URL and a path, ensuring that there is exactly one slash between them.
92+
* TODO: Move this function to a more common shared utility library across pacakges.
93+
*/
94+
export function concatenateUrl(baseUrl: string, path: string): string {
95+
// Remove trailing slash from baseUrl if it exists
96+
if (baseUrl.endsWith('/')) {
97+
baseUrl = baseUrl.slice(0, -1);
98+
}
99+
100+
// Remove leading slash from path if it exists
101+
if (path.startsWith('/')) {
102+
path = path.slice(1);
103+
}
104+
105+
return `${baseUrl}/${path}`;
87106
}

0 commit comments

Comments
 (0)