Skip to content

Commit 7727ee7

Browse files
Merge pull request #22 from did-method-plc/opstore-refactor
Refactor, implement OpStore interface
2 parents dbce2c0 + b83cae6 commit 7727ee7

4 files changed

Lines changed: 358 additions & 195 deletions

File tree

log.go

Lines changed: 11 additions & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -1,174 +1,12 @@
11
package didplc
22

33
import (
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-
17210
type 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
}

operation.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ type Operation interface {
4040
EquivalentRotationKeys() []string
4141
// CID of the previous operation ("" for genesis ops)
4242
PrevCIDStr() string
43+
// converts this operation to an OpEnum
44+
AsOpEnum() *OpEnum
4345
}
4446

4547
type OpService struct {
@@ -267,6 +269,10 @@ func (op *RegularOp) PrevCIDStr() string {
267269
return *op.Prev
268270
}
269271

272+
func (op *RegularOp) AsOpEnum() *OpEnum {
273+
return &OpEnum{Regular: op}
274+
}
275+
270276
func (op *LegacyOp) CID() cid.Cid {
271277
return computeCID(op.SignedCBORBytes())
272278
}
@@ -385,6 +391,10 @@ func (op *LegacyOp) PrevCIDStr() string {
385391
return *op.Prev
386392
}
387393

394+
func (op *LegacyOp) AsOpEnum() *OpEnum {
395+
return &OpEnum{Legacy: op}
396+
}
397+
388398
func (op *TombstoneOp) CID() cid.Cid {
389399
return computeCID(op.SignedCBORBytes())
390400
}
@@ -447,6 +457,10 @@ func (op *TombstoneOp) PrevCIDStr() string {
447457
return op.Prev
448458
}
449459

460+
func (op *TombstoneOp) AsOpEnum() *OpEnum {
461+
return &OpEnum{Tombstone: op}
462+
}
463+
450464
func (o *OpEnum) MarshalJSON() ([]byte, error) {
451465
if o.Regular != nil {
452466
return json.Marshal(o.Regular)
@@ -528,3 +542,4 @@ func (oe *OpEnum) AsOperation() Operation {
528542
return nil
529543
}
530544
}
545+

operation_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,34 +110,34 @@ func TestAuditLogInvalidSigEncoding(t *testing.T) {
110110
assert.ErrorContains(VerifyOpLog(entries), "CRLF")
111111

112112
entries = loadTestLogEntries(t, "testdata/log_invalid_sig_der.json")
113-
assert.EqualError(VerifyOpLog(entries), "crytographic signature invalid") // Note: there is no reliable way to detect DER-encoded signatures syntactically, so a generic invalid signature error is expected
113+
assert.ErrorContains(VerifyOpLog(entries), "crytographic signature invalid") // Note: there is no reliable way to detect DER-encoded signatures syntactically, so a generic invalid signature error is expected
114114

115115
entries = loadTestLogEntries(t, "testdata/log_invalid_sig_p256_high_s.json")
116-
assert.EqualError(VerifyOpLog(entries), "crytographic signature invalid")
116+
assert.ErrorContains(VerifyOpLog(entries), "crytographic signature invalid")
117117

118118
entries = loadTestLogEntries(t, "testdata/log_invalid_sig_k256_high_s.json")
119-
assert.EqualError(VerifyOpLog(entries), "crytographic signature invalid")
119+
assert.ErrorContains(VerifyOpLog(entries), "crytographic signature invalid")
120120

121121
}
122122

123123
func TestAuditLogInvalidNullification(t *testing.T) {
124124
assert := assert.New(t)
125125

126126
entries := loadTestLogEntries(t, "testdata/log_invalid_nullification_reused_key.json")
127-
assert.EqualError(VerifyOpLog(entries), "crytographic signature invalid") // TODO: This is the expected error message for the current impl logic. This could be improved.
127+
assert.ErrorContains(VerifyOpLog(entries), "crytographic signature invalid") // TODO: This is the expected error message for the current impl logic. This could be improved.
128128

129129
entries = loadTestLogEntries(t, "testdata/log_invalid_nullification_too_slow.json")
130130
assert.ErrorContains(VerifyOpLog(entries), "cannot nullify op after 72h")
131131

132132
entries = loadTestLogEntries(t, "testdata/log_invalid_update_nullified.json")
133-
assert.EqualError(VerifyOpLog(entries), "prev CID is nullified")
133+
assert.ErrorContains(VerifyOpLog(entries), "prev CID is nullified")
134134
}
135135

136136
func TestAuditLogInvalidTombstoneUpdate(t *testing.T) {
137137
assert := assert.New(t)
138138

139139
entries := loadTestLogEntries(t, "testdata/log_invalid_update_tombstoned.json")
140-
assert.EqualError(VerifyOpLog(entries), "no keys to verify against") // TODO: This is the expected error message for the current impl logic. This could be improved.
140+
assert.ErrorContains(VerifyOpLog(entries), "no keys to verify against") // TODO: This is the expected error message for the current impl logic. This could be improved.
141141
}
142142

143143
func TestCreatePLC(t *testing.T) {

0 commit comments

Comments
 (0)