Fork of github.com/did-method-plc/did-method-plc
1import { CID } from 'multiformats/cid'
2import { check, cidForCbor, HOUR } from '@atproto/common'
3import * as t from './types'
4import {
5 assureValidCreationOp,
6 assureValidSig,
7 normalizeOp,
8} from './operations'
9import {
10 ImproperOperationError,
11 LateRecoveryError,
12 MisorderedOperationError,
13} from './error'
14
15export const assureValidNextOp = async (
16 did: string,
17 ops: t.IndexedOperation[],
18 proposed: t.CompatibleOpOrTombstone,
19): Promise<{ nullified: CID[]; prev: CID | null }> => {
20 // special case if account creation
21 if (ops.length === 0) {
22 await assureValidCreationOp(did, proposed)
23 return { nullified: [], prev: null }
24 }
25
26 const proposedPrev = proposed.prev ? CID.parse(proposed.prev) : undefined
27 if (!proposedPrev) {
28 throw new MisorderedOperationError()
29 }
30
31 const indexOfPrev = ops.findIndex((op) => proposedPrev.equals(op.cid))
32 if (indexOfPrev < 0) {
33 throw new MisorderedOperationError()
34 }
35
36 // if we are forking history, these are the ops still in the proposed canonical history
37 const opsInHistory = ops.slice(0, indexOfPrev + 1)
38 const nullified = ops.slice(indexOfPrev + 1)
39 const lastOp = opsInHistory.at(-1)
40 if (!lastOp) {
41 throw new MisorderedOperationError()
42 }
43 if (check.is(lastOp.operation, t.def.tombstone)) {
44 throw new MisorderedOperationError()
45 }
46 const lastOpNormalized = normalizeOp(lastOp.operation)
47 const firstNullified = nullified[0]
48
49 // if this does not involve nullification
50 if (!firstNullified) {
51 await assureValidSig(lastOpNormalized.rotationKeys, proposed)
52 return { nullified: [], prev: proposedPrev }
53 }
54
55 const disputedSigner = await assureValidSig(
56 lastOpNormalized.rotationKeys,
57 firstNullified.operation,
58 )
59
60 const indexOfSigner = lastOpNormalized.rotationKeys.indexOf(disputedSigner)
61 const morePowerfulKeys = lastOpNormalized.rotationKeys.slice(0, indexOfSigner)
62
63 await assureValidSig(morePowerfulKeys, proposed)
64
65 // recovery key gets a 72hr window to do historical re-wrties
66 if (nullified.length > 0) {
67 const RECOVERY_WINDOW = 72 * HOUR
68 const timeLapsed = Date.now() - firstNullified.createdAt.getTime()
69 if (timeLapsed > RECOVERY_WINDOW) {
70 throw new LateRecoveryError(timeLapsed)
71 }
72 }
73
74 return {
75 nullified: nullified.map((op) => op.cid),
76 prev: proposedPrev,
77 }
78}
79
80export const validateOperationLog = async (
81 did: string,
82 ops: t.CompatibleOpOrTombstone[],
83): Promise<t.DocumentData | null> => {
84 // make sure they're all validly formatted operations
85 const [first, ...rest] = ops
86 if (!check.is(first, t.def.compatibleOp)) {
87 throw new ImproperOperationError('incorrect structure', first)
88 }
89 for (const op of rest) {
90 if (!check.is(op, t.def.opOrTombstone)) {
91 throw new ImproperOperationError('incorrect structure', op)
92 }
93 }
94
95 // ensure the first op is a valid & signed create operation
96 let doc = await assureValidCreationOp(did, first)
97 let prev = await cidForCbor(first)
98
99 for (let i = 0; i < rest.length; i++) {
100 const op = rest[i]
101 if (!op.prev || !CID.parse(op.prev).equals(prev)) {
102 throw new MisorderedOperationError()
103 }
104 await assureValidSig(doc.rotationKeys, op)
105 const data = opToData(did, op)
106 // if tombstone & last op, return null. else throw
107 if (data === null) {
108 if (i === rest.length - 1) {
109 return null
110 } else {
111 throw new MisorderedOperationError()
112 }
113 }
114 doc = data
115 prev = await cidForCbor(op)
116 }
117
118 return doc
119}
120
121export const opToData = (
122 did: string,
123 op: t.CompatibleOpOrTombstone,
124): t.DocumentData | null => {
125 if (check.is(op, t.def.tombstone)) {
126 return null
127 }
128 const { verificationMethods, rotationKeys, alsoKnownAs, services } =
129 normalizeOp(op)
130 return { did, verificationMethods, rotationKeys, alsoKnownAs, services }
131}
132
133export const getLastOpWithCid = async (
134 ops: t.CompatibleOpOrTombstone[],
135): Promise<{ op: t.CompatibleOpOrTombstone; cid: CID }> => {
136 const op = ops.at(-1)
137 if (!op) {
138 throw new Error('log is empty')
139 }
140 const cid = await cidForCbor(op)
141 return { op, cid }
142}