11package didplc
22
33import (
4- "errors "
4+ "context "
55 "fmt"
6- "sync"
7- "time"
86
97 "github.com/bluesky-social/indigo/atproto/syntax"
108)
119
12- type opStatus struct {
13- DID string
14- CreatedAt time.Time // fields below this line may be mutated
15- Nullified bool
16- LastChild string // CID
17- AllowedKeys []string // the set of public did:keys currently allowed to update from this op
18- }
19-
20- // Note: logValidationContext is designed such that it could later be turned into an interface,
21- // optionally backed by a db rather than in-memory
22- // Note: ops are globally unique by CID, so opStatus map can be shared across all DIDs
23- type logValidationContext struct {
24- head map [string ]string // DID -> CID, tracks most recent valid op for a particular DID
25- opStatus map [string ]* opStatus // CID -> OpStatus
26- lock sync.RWMutex
27- }
28-
29- var errLogValidationUnrecoverableInternalError = errors .New ("logValidationContext internal state has become inconsistent. This is very bad and should be impossible" )
30-
31- func newLogValidationContext () * logValidationContext {
32- return & logValidationContext {
33- head : make (map [string ]string ),
34- opStatus : make (map [string ]* opStatus ),
35- }
36- }
37-
38- // Retrieve the information required to validate a signature for a particular operation, where `cidStr`
39- // corresponds to the `prev` field of the operation you're trying to validate.
40- // If you're validating a genesis op (i.e. prev==nil), pass cidStr==""
41- //
42- // The returned string is the current "head" CID of the passed DID.
43- // Any subsequent calls to CommitValidOperation must pass the corresponding head, opStatus values.
44- //
45- // This method may also be used to inspect the nullification status and/or createdAt timestamp for a particular op (by did+cid)
46- func (lvc * logValidationContext ) GetValidationContext (did string , cidStr string ) (string , * opStatus , error ) {
47- lvc .lock .RLock ()
48- defer lvc .lock .RUnlock ()
49-
50- head , exists := lvc .head [did ]
51- if ! exists {
52- if cidStr != "" {
53- return "" , nil , fmt .Errorf ("DID not found" )
54- }
55- return "" , nil , nil // Not an error condition! just means DID is not created yet
56- }
57- status := lvc .opStatus [cidStr ]
58- if status == nil {
59- return "" , nil , fmt .Errorf ("CID not found" )
60- }
61- if status .DID != did {
62- return "" , nil , fmt .Errorf ("op belongs to a different DID" )
63- }
64-
65- // make a deep copy of the status struct so that concurrent mutations are safe
66- statusCopy := * status
67- statusCopy .AllowedKeys = make ([]string , len (status .AllowedKeys ))
68- copy (statusCopy .AllowedKeys , status .AllowedKeys )
69-
70- return head , & statusCopy , nil
71- }
72-
73- // `head` and `prevStatus` MUST be values that were returned from a previous call to GetValidationContext, with the same `did`.
74- // The caller is responsible for syntax validation and signature verification of the Operation.
75- // CommitValidOperation will ensure that:
76- // 1. If this is the first operation for a particular DID, it must be a genesis operation
77- // 2. Else, it must not be a genesis operation.
78- // 3. The passed `createdAt` timestamp is greater than that of the current `head` op
79- // 4. If the operation nullifies a previous operation, the nullified op is less than (or exactly equal to) 72h old
80- // 5. This DID has not been updated since the corresponding GetValidationContext call
81- //
82- // Additionally, the lvc head+opStatus maps are updated to reflect the changes (including updating nullification status if applicable).
83- //
84- // Although it should be unreachable, errLogValidationUnrecoverableInternalError
85- // may be returned if the logValidationContext internal state has become inconsistent.
86- // This could happen due to an implementation bug, or if an invalid prevStatus is passed
87- // (one not produced by an earlier call to GetValidationContext).
88- func (lvc * logValidationContext ) CommitValidOperation (did string , head string , prevStatus * opStatus , op Operation , createdAt time.Time , keyIndex int ) error {
89- thisCid := op .CID ().String () // CID() involves expensive-ish serialisation/hashing, best to keep out of the critical section
90-
91- lvc .lock .Lock ()
92- defer lvc .lock .Unlock ()
93-
94- if head != lvc .head [did ] {
95- return fmt .Errorf ("head CID mismatch" )
96- }
97- if head == "" {
98- if ! op .IsGenesis () {
99- return fmt .Errorf ("expected genesis op" )
100- }
101- } else {
102- if op .IsGenesis () {
103- return fmt .Errorf ("unexpected genesis op" )
104- }
105- if prevStatus == nil {
106- return fmt .Errorf ("invalid prevStatus" )
107- }
108- if prevStatus .Nullified {
109- return fmt .Errorf ("prev CID is nullified" )
110- }
111- if prevStatus .LastChild == "" { // regular update (not a nullification)
112- // note: prevStatus == c.opStatus[head]
113- if createdAt .Sub (prevStatus .CreatedAt ) <= 0 {
114- return fmt .Errorf ("invalid operation timestamp order" )
115- }
116- } else { // this is a nullification. prevStatus.LastChild is the CID of the op being nullified
117- // note: prevStatus != c.opStatus[head]
118- headStatus := lvc .opStatus [head ]
119- if headStatus == nil {
120- return errLogValidationUnrecoverableInternalError
121- }
122- if createdAt .Sub (headStatus .CreatedAt ) <= 0 {
123- return fmt .Errorf ("invalid operation timestamp order" )
124- }
125- lastChildStatus := lvc .opStatus [prevStatus .LastChild ]
126- if lastChildStatus == nil {
127- return errLogValidationUnrecoverableInternalError
128- }
129- if createdAt .Sub (lastChildStatus .CreatedAt ) > 72 * time .Hour {
130- return fmt .Errorf ("cannot nullify op after 72h (%s - %s = %s)" , createdAt , prevStatus .CreatedAt , createdAt .Sub (prevStatus .CreatedAt ))
131- }
132- err := lvc .markNullifiedOp (did , prevStatus .LastChild ) // recursive
133- if err != nil {
134- return err // should never happen, if it does we're in a broken state
135- }
136- }
137- prevStatus .AllowedKeys = prevStatus .AllowedKeys [:keyIndex ]
138- prevStatus .LastChild = thisCid
139- lvc .opStatus [op .PrevCIDStr ()] = prevStatus // prevStatus was a copy so we need to write it back
140- }
141- lvc .head [did ] = thisCid
142- lvc .opStatus [thisCid ] = & opStatus {
143- DID : did ,
144- CreatedAt : createdAt ,
145- Nullified : false ,
146- LastChild : "" ,
147- AllowedKeys : op .EquivalentRotationKeys (),
148- }
149- return nil
150- }
151-
152- // Recurses if more than one op needs to be nullified (if the nullified op has descendents)
153- // Note: lvc.lock is expected to be held by caller
154- func (lvc * logValidationContext ) markNullifiedOp (did string , cidStr string ) error {
155- if cidStr == "" {
156- return nil
157- }
158- op := lvc .opStatus [cidStr ]
159- if op == nil { // this *should* be unreachable
160- return errLogValidationUnrecoverableInternalError
161- }
162- if op .DID != did { // likewise
163- return errLogValidationUnrecoverableInternalError
164- }
165- if op .Nullified {
166- return nil
167- }
168- op .Nullified = true
169- return lvc .markNullifiedOp (did , op .LastChild )
170- }
171-
17210type LogEntry struct {
17311 DID string `json:"did"`
17412 Operation OpEnum `json:"operation"`
@@ -213,53 +51,37 @@ func VerifyOpLog(entries []LogEntry) error {
21351 }
21452
21553 did := entries [0 ].DID
216- lvc := newLogValidationContext ()
54+ mos := NewMemOpStore ()
55+ ctx := context .Background ()
21756
21857 for _ , oe := range entries {
21958 if oe .DID != did {
22059 return fmt .Errorf ("inconsistent DID" )
22160 }
22261 // NOTE: we do not call oe.Validate() here because we'd end up verifying
22362 // genesis op signatures twice.
224- // We check for CID consistency here, and will verify signatures (for all op types) later.
63+ // All validation is performed inside VerifyOperation()
22564 op := oe .Operation .AsOperation ()
22665 if op == nil {
22766 return fmt .Errorf ("invalid operation type" )
22867 }
229- if op .CID ().String () != oe .CID {
230- return fmt .Errorf ("inconsistent CID" )
231- }
23268
23369 datetime , err := syntax .ParseDatetime (oe .CreatedAt )
23470 if err != nil {
23571 return err
23672 }
23773 timestamp := datetime .Time ()
23874
239- head , prevStatus , err := lvc . GetValidationContext ( did , op . PrevCIDStr () )
75+ po , err := VerifyOperation ( ctx , mos , did , op , timestamp )
24076 if err != nil {
24177 return err
24278 }
243-
244- var allowedKeys * []string
245- if op .IsGenesis () {
246- calcDid , err := op .DID ()
247- if err != nil {
248- return err
249- }
250- if calcDid != did {
251- return fmt .Errorf ("genesis DID does not match" )
252- }
253- rotationKeys := op .EquivalentRotationKeys ()
254- allowedKeys = & rotationKeys
255- } else { // not-genesis
256- allowedKeys = & prevStatus .AllowedKeys
257- }
258- keyIdx , err := VerifySignatureAny (op , * allowedKeys )
259- if err != nil {
260- return err
79+ // extra CID check (since oe.CID is not checked inside VerifyOperation)
80+ if po .OpCid != oe .CID {
81+ return fmt .Errorf ("inconsistent CID" )
26182 }
262- err = lvc .CommitValidOperation (did , head , prevStatus , op , timestamp , keyIdx )
83+
84+ err = mos .CommitOperations (ctx , []* PreparedOperation {po })
26385 if err != nil {
26486 return err
26587 }
@@ -273,7 +95,7 @@ func VerifyOpLog(entries []LogEntry) error {
27395 return fmt .Errorf ("genesis op cannot be nullified" )
27496 }
27597 }
276- _ , status , err := lvc . GetValidationContext ( did , oe .CID )
98+ status , err := mos . GetEntry ( ctx , did , oe .CID )
27799 if err != nil {
278100 return err
279101 }
0 commit comments