Fork of github.com/did-method-plc/did-method-plc
1import { P256Keypair } from '@atproto/crypto'
2import * as plc from '@did-plc/lib'
3import { CloseFn, runTestServer } from './_util'
4import { check } from '@atproto/common'
5import { Database } from '../src'
6import { didForCreateOp, PlcClientError } from '@did-plc/lib'
7
8describe('PLC server', () => {
9 let handle1 = 'at://alice.example.com'
10 let handle2 = 'at://bob.example.com'
11 let atpPds = 'https://example.com'
12
13 let close: CloseFn
14 let db: Database
15 let client: plc.Client
16
17 let signingKey: P256Keypair
18 let rotationKey1: P256Keypair
19 let rotationKey2: P256Keypair
20 let rotationKey3: P256Keypair
21
22 let did1: string
23 let did2: string
24
25 beforeAll(async () => {
26 const server = await runTestServer({
27 dbSchema: 'server',
28 })
29
30 db = server.db
31 close = server.close
32 client = new plc.Client(server.url)
33 signingKey = await P256Keypair.create()
34 rotationKey1 = await P256Keypair.create()
35 rotationKey2 = await P256Keypair.create()
36 rotationKey3 = await P256Keypair.create()
37 })
38
39 afterAll(async () => {
40 if (close) {
41 await close()
42 }
43 })
44
45 const verifyDoc = (doc: plc.DocumentData | null) => {
46 if (!doc) {
47 throw new Error('expected doc')
48 }
49 expect(doc.did).toEqual(did1)
50 expect(doc.verificationMethods).toEqual({ atproto: signingKey.did() })
51 expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()])
52 expect(doc.alsoKnownAs).toEqual([handle1])
53 expect(doc.services).toEqual({
54 atproto_pds: {
55 type: 'AtprotoPersonalDataServer',
56 endpoint: atpPds,
57 },
58 })
59 }
60
61 it('registers a did', async () => {
62 did1 = await client.createDid({
63 signingKey: signingKey.did(),
64 rotationKeys: [rotationKey1.did(), rotationKey2.did()],
65 handle: handle1,
66 pds: atpPds,
67 signer: rotationKey1,
68 })
69
70 did2 = await client.createDid({
71 signingKey: signingKey.did(),
72 rotationKeys: [rotationKey3.did()],
73 handle: handle2,
74 pds: atpPds,
75 signer: rotationKey3,
76 })
77 })
78
79 it('retrieves did doc data', async () => {
80 const doc = await client.getDocumentData(did1)
81 verifyDoc(doc)
82 })
83
84 it('can perform some updates', async () => {
85 const newRotationKey = await P256Keypair.create()
86 signingKey = await P256Keypair.create()
87 handle1 = 'at://ali.example2.com'
88 atpPds = 'https://example2.com'
89
90 await client.updateAtprotoKey(did1, rotationKey1, signingKey.did())
91 await client.updateRotationKeys(did1, rotationKey1, [
92 newRotationKey.did(),
93 rotationKey2.did(),
94 ])
95 rotationKey1 = newRotationKey
96
97 await client.updateHandle(did1, rotationKey1, handle1)
98 await client.updatePds(did1, rotationKey1, atpPds)
99
100 const doc = await client.getDocumentData(did1)
101 verifyDoc(doc)
102 })
103
104 it('does not allow *rotation* key types that we do not yet support', async () => {
105 // an ed25519 key, which we don't yet support
106 const newRotationKey =
107 'did:key:z6MkjwbBXZnFqL8su24wGL2Fdjti6GSLv9SWdYGswfazUPm9'
108
109 const promise = client.updateRotationKeys(did2, rotationKey3, [
110 rotationKey2.did(),
111 newRotationKey,
112 ])
113 await expect(promise).rejects.toThrow(PlcClientError)
114 })
115
116 it('allows *verificationMethod* key types that we do not explicitly support', async () => {
117 // an ed25519 key, which we don't explicitly support
118 const newSigningKey =
119 'did:key:z6MkjwbBXZnFqL8su24wGL2Fdjti6GSLv9SWdYGswfazUPm9'
120
121 // Note: atproto itself does not currently support ed25519 keys, but PLC
122 // does not have opinions about atproto (or other services!)
123 await client.updateAtprotoKey(did2, rotationKey3, newSigningKey)
124
125 // a BLS12-381 key
126 const exoticSigningKeyFromTheFuture =
127 'did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY'
128 await client.updateAtprotoKey(
129 did2,
130 rotationKey3,
131 exoticSigningKeyFromTheFuture,
132 )
133
134 // check that we can still read back the rendered did document
135 const doc = await client.getDocument(did2)
136 expect(doc.verificationMethod).toEqual([
137 {
138 id: did2 + '#atproto',
139 type: 'Multikey',
140 controller: did2,
141 publicKeyMultibase: exoticSigningKeyFromTheFuture.slice(8),
142 },
143 ])
144 })
145
146 it('does not allow syntactically invalid verificationMethod keys', async () => {
147 const promise1 = client.updateAtprotoKey(
148 did2,
149 rotationKey3,
150 'did:key:BJV2WY5DJMJQXGZJANFZSAYLXMVZW63LFEEQFY3ZP', // not b58 (b32!)
151 )
152 await expect(promise1).rejects.toThrow(PlcClientError)
153 const promise2 = client.updateAtprotoKey(
154 did2,
155 rotationKey3,
156 'did:banana', // a malformed did:key
157 )
158 await expect(promise2).rejects.toThrow(PlcClientError)
159 const promise3 = client.updateAtprotoKey(
160 did2,
161 rotationKey3,
162 'blah', // an even more malformed did:key
163 )
164 await expect(promise3).rejects.toThrow(PlcClientError)
165 })
166
167 it('does not allow unreasonably long verificationMethod keys', async () => {
168 const promise = client.updateAtprotoKey(
169 did2,
170 rotationKey3,
171 'did:key:z41vu8qtWtp8XRJ9Te5QhkyzU9ByBbiw7bZHKXDjZ8iYorixqZQmEZpxgVSteYirYWMBjqQuEbMYTDsCzXXCAanCSH2xG2cwpbCWGZ2coY2PnhbrDVo7QghsAHpm2X5zsRRwDLyUcm9MTNQAZuRs2B22ygQw3UwkKLA7PZ9ZQ9wMHppmkoaBapmUGaxRNjp1Mt4zxrm9RbEx8FiK3ANBL1fsjggNqvkKpbj6MjntRScPQnJCes9Vt1cFe3iwNP7Ya9RfbaKsVi1eothvSBcbWoouHActGeakHgqFLj1JpbkP7PL3hGGSWLQbXxzmdrfzBCYAtiUxGRvpf3JiaNA2WYbJTh58bzx',
172 )
173 await expect(promise).rejects.toThrow(PlcClientError)
174 })
175
176 it('retrieves the operation log', async () => {
177 const doc = await client.getDocumentData(did1)
178 const ops = await client.getOperationLog(did1)
179 const computedDoc = await plc.validateOperationLog(did1, ops)
180 expect(computedDoc).toEqual(doc)
181 })
182
183 it('rejects on bad updates', async () => {
184 const newKey = await P256Keypair.create()
185 const operation = client.updateAtprotoKey(did1, newKey, newKey.did())
186 await expect(operation).rejects.toThrow()
187 })
188
189 it('allows for recovery through a forked history', async () => {
190 const attackerKey = await P256Keypair.create()
191 await client.updateRotationKeys(did1, rotationKey2, [attackerKey.did()])
192
193 const newKey = await P256Keypair.create()
194 const ops = await client.getOperationLog(did1)
195 const forkPoint = ops.at(-2)
196 if (!check.is(forkPoint, plc.def.operation)) {
197 throw new Error('Could not find fork point')
198 }
199 const op = await plc.updateRotationKeysOp(forkPoint, rotationKey1, [
200 rotationKey1.did(),
201 newKey.did(),
202 ])
203 await client.sendOperation(did1, op)
204
205 rotationKey2 = newKey
206
207 const doc = await client.getDocumentData(did1)
208 verifyDoc(doc)
209 })
210
211 it('retrieves the auditable operation log', async () => {
212 const log = await client.getOperationLog(did1)
213 const auditable = await client.getAuditableLog(did1)
214 // has one nullifed op
215 expect(auditable.length).toBe(log.length + 1)
216 expect(auditable.filter((op) => op.nullified).length).toBe(1)
217 expect(auditable.at(-2)?.nullified).toBe(true)
218 expect(
219 auditable.every((op) => check.is(op, plc.def.exportedOp)),
220 ).toBeTruthy()
221 })
222
223 it('retrieves the did doc', async () => {
224 const data = await client.getDocumentData(did1)
225 const doc = await client.getDocument(did1)
226 expect(doc).toEqual(plc.formatDidDoc(data))
227 })
228
229 it('handles concurrent requests to many docs', async () => {
230 const COUNT = 20
231 const keys: P256Keypair[] = []
232 for (let i = 0; i < COUNT; i++) {
233 keys.push(await P256Keypair.create())
234 }
235 await Promise.all(
236 keys.map(async (key, index) => {
237 await client.createDid({
238 signingKey: key.did(),
239 rotationKeys: [key.did()],
240 handle: `user${index}`,
241 pds: `example.com`,
242 signer: key,
243 })
244 }),
245 )
246 })
247
248 it('resolves races into a coherent history with no forks', async () => {
249 const COUNT = 20
250 const keys: P256Keypair[] = []
251 for (let i = 0; i < COUNT; i++) {
252 keys.push(await P256Keypair.create())
253 }
254 // const prev = await client.getPrev(did)
255
256 let successes = 0
257 let failures = 0
258 await Promise.all(
259 keys.map(async (key) => {
260 try {
261 await client.updateAtprotoKey(did1, rotationKey1, key.did())
262 successes++
263 } catch (err) {
264 failures++
265 }
266 }),
267 )
268 expect(successes).toBe(1)
269 expect(failures).toBe(19)
270
271 const ops = await client.getOperationLog(did1)
272 await plc.validateOperationLog(did1, ops)
273 })
274
275 it('tombstones the did', async () => {
276 await client.tombstone(did1, rotationKey1)
277
278 const promise = client.getDocument(did1)
279 await expect(promise).rejects.toThrow(PlcClientError)
280 const promise2 = client.getDocumentData(did1)
281 await expect(promise2).rejects.toThrow(PlcClientError)
282 })
283
284 it('exports the data set', async () => {
285 const data = await client.export()
286 expect(data.every((row) => check.is(row, plc.def.exportedOp))).toBeTruthy()
287 expect(data.length).toBe(32)
288 for (let i = 1; i < data.length; i++) {
289 expect(data[i].createdAt >= data[i - 1].createdAt).toBeTruthy()
290 }
291 })
292
293 it('disallows create v1s', async () => {
294 const createV1 = await plc.deprecatedSignCreate(
295 {
296 type: 'create',
297 signingKey: signingKey.did(),
298 recoveryKey: rotationKey1.did(),
299 handle: handle1,
300 service: atpPds,
301 prev: null,
302 },
303 signingKey,
304 )
305 const did = await didForCreateOp(createV1)
306 const attempt = client.sendOperation(did, createV1 as any)
307 await expect(attempt).rejects.toThrow()
308 })
309
310 it('healthcheck succeeds when database is available.', async () => {
311 const res = await client.health()
312 expect(res).toEqual({ version: '0.0.0' })
313 })
314
315 it('healthcheck fails when database is unavailable.', async () => {
316 await db.db.destroy()
317 let error: PlcClientError
318 try {
319 await client.health()
320 throw new Error('Healthcheck should have failed')
321 } catch (err) {
322 if (err instanceof PlcClientError) {
323 error = err
324 } else {
325 throw err
326 }
327 }
328 expect(error.status).toEqual(503)
329 expect(error.data).toEqual({
330 version: '0.0.0',
331 error: 'Service Unavailable',
332 })
333 })
334})