package plc import ( "context" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/did-method-plc/go-didplc" "github.com/gbl08ma/stacktrace" "github.com/samber/lo" "github.com/samber/mo" "tangled.org/gbl08ma.com/didplcbft/store" "tangled.org/gbl08ma.com/didplcbft/transaction" "tangled.org/gbl08ma.com/didplcbft/types" ) type plcImpl struct { validator OperationValidator } var _ PLC = (*plcImpl)(nil) func NewPLC() *plcImpl { p := &plcImpl{} p.validator = NewV0OperationValidator() return p } func (plc *plcImpl) ValidateOperation(ctx context.Context, readTx transaction.Read, did string, opBytes []byte) error { timestamp := syntax.Datetime(readTx.Timestamp().Format(types.ActualAtprotoDatetimeLayout)) // TODO set last parameter to true only while importing old ops _, err := plc.validator.Validate(ctx, readTx, timestamp, did, opBytes, true) if err != nil { return stacktrace.Propagate(err, "operation failed validation") } return nil } func (plc *plcImpl) ExecuteOperation(ctx context.Context, tx transaction.Write, did string, opBytes []byte) error { timestamp := syntax.Datetime(tx.Timestamp().Format(types.ActualAtprotoDatetimeLayout)) // TODO set last parameter to true only while importing old ops effects, err := plc.validator.Validate(ctx, tx.Downgrade(), timestamp, did, opBytes, true) if err != nil { return stacktrace.Propagate(err, "operation failed validation") } err = store.Consensus.StoreOperation(ctx, tx, effects.NewLogEntry, effects.NullifiedEntriesStartingSeq) if err != nil { return stacktrace.Propagate(err, "failed to commit operation") } return nil } func (plc *plcImpl) ImportOperationFromAuthoritativeSource(ctx context.Context, tx transaction.Write, newEntry didplc.LogEntry) error { newCID := newEntry.CID newPrev := newEntry.Operation.AsOperation().PrevCIDStr() hasExistingOps := false var seqOfPrev mo.Option[uint64] nullifiedEntriesStartingSeq := mo.None[uint64]() var iteratorErr error for entry := range store.Consensus.AuditLogReverseIterator(ctx, tx.Downgrade(), newEntry.DID, &iteratorErr) { entryCID := entry.CID.String() if !hasExistingOps { hasExistingOps = true if newPrev == "" && entryCID != newCID { // this should never happen unless the authoritative source doesn't compute DIDs from genesis ops the way we do return stacktrace.NewError("invalid internal state reached") } } if entryCID == newCID { // should we already have an operation with the same CID, this condition should trigger before the next one // because this is a reverse iterator // looks like we already have the op we're trying to import. just need to update the timestamp newCreatedAtDT, err := syntax.ParseDatetime(newEntry.CreatedAt) if err != nil { return stacktrace.Propagate(err) } return stacktrace.Propagate( store.Consensus.SetOperationCreatedAt(tx, entry.Seq, newCreatedAtDT.Time()), "") } if entryCID == newPrev { seqOfPrev = mo.Some(entry.Seq) break } else { // we only get here if there's an operation between the new latest and prev // this will keep decreasing until we find prev, at which point it'll have the lowest seq for this DID before that of prev nullifiedEntriesStartingSeq = mo.Some(entry.Seq) } } if iteratorErr != nil { return stacktrace.Propagate(iteratorErr) } if !hasExistingOps { // we have nothing for this DID - this should be a creation op, if not, then we're not importing things in order if newPrev != "" { return stacktrace.NewError("invalid internal state reached") } // there's nothing to do but store the operation, no nullification involved newEntry.Nullified = false err := store.Consensus.StoreOperation(ctx, tx, newEntry, nullifiedEntriesStartingSeq) return stacktrace.Propagate(err, "failed to commit operation") } if !seqOfPrev.IsPresent() { // there are entries in the audit log but none of them has a CID matching prev // if this isn't a creation op, then this shouldn't happen // (even when history forks between us and the authoritative source, at least the initial op should be the same, otherwise the DIDs wouldn't match) // if this is a creation op, then this case should have been caught above return stacktrace.NewError("invalid internal state reached, %+v", newEntry) } newEntry.Nullified = false err := store.Consensus.StoreOperation(ctx, tx, newEntry, nullifiedEntriesStartingSeq) return stacktrace.Propagate(err, "failed to commit operation") } func (plc *plcImpl) Resolve(ctx context.Context, tx transaction.Read, did string) (didplc.Doc, error) { var iteratorErr error for entry := range store.Consensus.AuditLogReverseIterator(ctx, tx, did, &iteratorErr) { if entry.Operation.Tombstone != nil { return didplc.Doc{}, stacktrace.Propagate(ErrDIDGone) } return entry.Operation.AsOperation().Doc(did) } if iteratorErr != nil { return didplc.Doc{}, stacktrace.Propagate(iteratorErr) } return didplc.Doc{}, stacktrace.Propagate(ErrDIDNotFound) } func (plc *plcImpl) OperationLog(ctx context.Context, tx transaction.Read, did string) ([]didplc.OpEnum, error) { // GetPlcOpLog - /:did/log - same data as audit log but excludes nullified. just the inner operations // if missing -> returns ErrDIDNotFound // if tombstone -> returns log as normal l, _, err := store.Consensus.AuditLog(ctx, tx, did, false) if err != nil { return nil, stacktrace.Propagate(err) } if len(l) == 0 { return nil, stacktrace.Propagate(ErrDIDNotFound) } l = lo.Filter(l, func(logEntry types.SequencedLogEntry, _ int) bool { return !logEntry.Nullified }) return lo.Map(l, func(logEntry types.SequencedLogEntry, _ int) didplc.OpEnum { return logEntry.Operation }), nil } func (plc *plcImpl) AuditLog(ctx context.Context, tx transaction.Read, did string) ([]didplc.LogEntry, error) { // GetPlcAuditLog - /:did/log/audit - full audit log, with nullified // if missing -> returns ErrDIDNotFound // if tombstone -> returns log as normal l, _, err := store.Consensus.AuditLog(ctx, tx, did, false) if err != nil { return nil, stacktrace.Propagate(err) } if len(l) == 0 { return nil, stacktrace.Propagate(ErrDIDNotFound) } return lo.Map(l, func(logEntry types.SequencedLogEntry, _ int) didplc.LogEntry { return logEntry.ToDIDPLCLogEntry() }), nil } func (plc *plcImpl) LastOperation(ctx context.Context, tx transaction.Read, did string) (didplc.OpEnum, error) { // GetLastOp - /:did/log/last - latest op from audit log which isn't nullified (the latest op is guaranteed not to be nullified) // if missing -> returns ErrDIDNotFound // if tombstone -> returns tombstone op var iteratorErr error for entry := range store.Consensus.AuditLogReverseIterator(ctx, tx, did, &iteratorErr) { return entry.Operation, nil } if iteratorErr != nil { return didplc.OpEnum{}, stacktrace.Propagate(iteratorErr) } return didplc.OpEnum{}, stacktrace.Propagate(ErrDIDNotFound) } func (plc *plcImpl) Data(ctx context.Context, tx transaction.Read, did string) (didplc.RegularOp, error) { // GetPlcData - /:did/data - similar to GetLastOp but applies a transformation on the op which normalizes it into a modern op // if missing -> returns ErrDIDNotFound // if tombstone -> returns ErrDIDGone var iteratorErr error for entry := range store.Consensus.AuditLogReverseIterator(ctx, tx, did, &iteratorErr) { opEnum := entry.Operation if opEnum.Tombstone != nil { return didplc.RegularOp{}, stacktrace.Propagate(ErrDIDGone) } if opEnum.Regular != nil { return *opEnum.Regular, nil } return *modernizeOp(opEnum.Legacy), nil } if iteratorErr != nil { return didplc.RegularOp{}, stacktrace.Propagate(iteratorErr) } return didplc.RegularOp{}, stacktrace.Propagate(ErrDIDNotFound) } func (plc *plcImpl) Export(ctx context.Context, tx transaction.Read, after uint64, count int) ([]types.SequencedLogEntry, error) { entries, err := store.Consensus.ExportOperations(ctx, tx, after, count) return entries, stacktrace.Propagate(err) }