···26 snapshotApplier *snapshotApplier
2728 lastProcessedProposalHash []byte
29- lastProcessedProposalExecTxResults []*abcitypes.ExecTxResult
0030}
3132// store and plc must be able to share transaction objects
···51 d := &DIDPLCApplication{
52 tree: tree,
53 snapshotDirectory: snapshotDirectory,
054 }
55 d.fullyClearTree = func() error {
56 // we assume this is called in a single-threaded context, which should be a safe assumption since we'll only call this during snapshot import
···26 snapshotApplier *snapshotApplier
2728 lastProcessedProposalHash []byte
29+ lastProcessedProposalExecTxResults []*processResult
30+31+ aocsByPLC map[string]*authoritativeOperationsCache
32}
3334// store and plc must be able to share transaction objects
···53 d := &DIDPLCApplication{
54 tree: tree,
55 snapshotDirectory: snapshotDirectory,
56+ aocsByPLC: make(map[string]*authoritativeOperationsCache),
57 }
58 d.fullyClearTree = func() error {
59 // we assume this is called in a single-threaded context, which should be a safe assumption since we'll only call this during snapshot import
+67-50
abciapp/execution.go
···9 abcitypes "github.com/cometbft/cometbft/abci/types"
10 "github.com/palantir/stacktrace"
11 "github.com/samber/lo"
012)
1314// InitChain implements [types.Application].
···21func (d *DIDPLCApplication) PrepareProposal(ctx context.Context, req *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) {
22 defer d.tree.Rollback()
2324- deps := TransactionProcessorDependencies{
25- plc: d.plc,
26- tree: d,
27- }
28-29 st := time.Now()
30 acceptedTx := make([][]byte, 0, len(req.Txs))
31 toProcess := req.Txs
32 for {
33 toTryNext := [][]byte{}
34 for _, tx := range toProcess {
35- result, err := processTx(ctx, deps, tx, req.Time, true)
36 if err != nil {
37 return nil, stacktrace.Propagate(err, "")
38 }
3940- if result.IsAuthoritativeImportTransaction {
41 // this type of transaction is not meant to appear in the mempool,
42 // but maybe it's not impossible that a non-compliant node could have gossiped it to us?
43 // (not sure if CometBFT checks transactions coming from other peers against CheckTx)
···65 toProcess = toTryNext
66 }
6768- totalSize := lo.SumBy(acceptedTx, func(tx []byte) int { return len(tx) })
69- if totalSize < int(req.MaxTxBytes)-4096 {
70- // we have space to fit an import transaction
71- // TODO
000000000000000072 }
7374 return &abcitypes.ResponsePrepareProposal{Txs: acceptedTx}, nil
···82 return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
83 }
8400000000000085 // if we return early, ensure we don't use incomplete results where we haven't voted ACCEPT
86 d.lastProcessedProposalHash = nil
87 d.lastProcessedProposalExecTxResults = nil
···93 }
94 }()
9596- deps := TransactionProcessorDependencies{
97- plc: d.plc,
98- tree: d,
99- }
100-101- txResults := make([]*abcitypes.ExecTxResult, len(req.Txs))
102 for i, tx := range req.Txs {
103- result, err := processTx(ctx, deps, tx, req.Time, true)
104 if err != nil {
105 return nil, stacktrace.Propagate(err, "")
106 }
···110 return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
111 }
112113- if result.IsAuthoritativeImportTransaction && i != len(req.Txs)-1 {
114 // if an Authoritative Import transaction is present on the block, it must be the last one
115 return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
116 }
117118- txResults[i] = &abcitypes.ExecTxResult{
119- Code: result.Code,
120- Data: result.Data,
121- Log: result.Log,
122- Info: result.Info,
123- GasWanted: result.GasWanted,
124- GasUsed: result.GasUsed,
125- Events: result.Events,
126- Codespace: result.Codespace,
127- }
128 }
129130 d.lastProcessedProposalHash = slices.Clone(req.Hash)
···151 // the block that was decided was the one we processed in ProcessProposal, and ProcessProposal processed successfully
152 // reuse the uncommitted results
153 return &abcitypes.ResponseFinalizeBlock{
154- TxResults: d.lastProcessedProposalExecTxResults,
155- AppHash: d.tree.WorkingHash(),
00156 }, nil
157 }
158 // a block other than the one we processed in ProcessProposal was decided
159 // discard the current modified state, and process the decided block
160 d.tree.Rollback()
161162- deps := TransactionProcessorDependencies{
163- plc: d.plc,
164- tree: d,
165- }
166-167- txResults := make([]*abcitypes.ExecTxResult, len(req.Txs))
168 for i, tx := range req.Txs {
169- result, err := processTx(ctx, deps, tx, req.Time, true)
0170 if err != nil {
171 return nil, stacktrace.Propagate(err, "")
172 }
173- txResults[i] = &abcitypes.ExecTxResult{
174- Code: result.Code,
175- Data: result.Data,
176- Log: result.Log,
177- Info: result.Info,
178- GasWanted: result.GasWanted,
179- GasUsed: result.GasUsed,
180- Events: result.Events,
181- Codespace: result.Codespace,
182- }
183 }
184000185 return &abcitypes.ResponseFinalizeBlock{
186- TxResults: txResults,
187- AppHash: d.tree.WorkingHash(),
00188 }, nil
189}
190···195 return nil, stacktrace.Propagate(err, "")
196 }
197000000198 // TODO(later) consider whether we can set some RetainHeight in the response
199 return &abcitypes.ResponseCommit{}, nil
200}
00000000
···9 abcitypes "github.com/cometbft/cometbft/abci/types"
10 "github.com/palantir/stacktrace"
11 "github.com/samber/lo"
12+ "tangled.org/gbl08ma/didplcbft/store"
13)
1415// InitChain implements [types.Application].
···22func (d *DIDPLCApplication) PrepareProposal(ctx context.Context, req *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) {
23 defer d.tree.Rollback()
240000025 st := time.Now()
26 acceptedTx := make([][]byte, 0, len(req.Txs))
27 toProcess := req.Txs
28 for {
29 toTryNext := [][]byte{}
30 for _, tx := range toProcess {
31+ result, err := processTx(ctx, d.transactionProcessorDependencies(), tx, req.Time, true)
32 if err != nil {
33 return nil, stacktrace.Propagate(err, "")
34 }
3536+ if result.isAuthoritativeImportTransaction {
37 // this type of transaction is not meant to appear in the mempool,
38 // but maybe it's not impossible that a non-compliant node could have gossiped it to us?
39 // (not sure if CometBFT checks transactions coming from other peers against CheckTx)
···61 toProcess = toTryNext
62 }
6364+ maybeTx, err := d.maybeCreateAuthoritativeImportTx(ctx)
65+ if err != nil {
66+ // TODO don't fail absolutely silently always, we should at least check what the error is
67+ //return nil, stacktrace.Propagate(err, "")
68+ }
69+70+ if err == nil && len(maybeTx) != 0 {
71+ totalSize := lo.SumBy(acceptedTx, func(tx []byte) int { return len(tx) })
72+ // 4K safety margin
73+ if totalSize+len(maybeTx) < int(req.MaxTxBytes)-4096 {
74+ // we have space to fit the import transaction
75+76+ result, err := processTx(ctx, d.transactionProcessorDependencies(), maybeTx, req.Time, true)
77+ if err != nil {
78+ return nil, stacktrace.Propagate(err, "")
79+ }
80+ if result.Code == 0 {
81+ acceptedTx = append(acceptedTx, maybeTx)
82+ }
83+ }
84 }
8586 return &abcitypes.ResponsePrepareProposal{Txs: acceptedTx}, nil
···94 return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
95 }
9697+ if req.Height == 1 {
98+ tree, err := d.MutableTree()
99+ if err != nil {
100+ return nil, stacktrace.Propagate(err, "")
101+ }
102+103+ err = store.Tree.SetAuthoritativePLC(tree, "https://plc.directory")
104+ if err != nil {
105+ return nil, stacktrace.Propagate(err, "")
106+ }
107+ }
108+109 // if we return early, ensure we don't use incomplete results where we haven't voted ACCEPT
110 d.lastProcessedProposalHash = nil
111 d.lastProcessedProposalExecTxResults = nil
···117 }
118 }()
119120+ txResults := make([]*processResult, len(req.Txs))
00000121 for i, tx := range req.Txs {
122+ result, err := processTx(ctx, d.transactionProcessorDependencies(), tx, req.Time, true)
123 if err != nil {
124 return nil, stacktrace.Propagate(err, "")
125 }
···129 return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
130 }
131132+ if result.isAuthoritativeImportTransaction && i != len(req.Txs)-1 {
133 // if an Authoritative Import transaction is present on the block, it must be the last one
134 return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
135 }
136137+ txResults[i] = result
000000000138 }
139140 d.lastProcessedProposalHash = slices.Clone(req.Hash)
···161 // the block that was decided was the one we processed in ProcessProposal, and ProcessProposal processed successfully
162 // reuse the uncommitted results
163 return &abcitypes.ResponseFinalizeBlock{
164+ TxResults: lo.Map(d.lastProcessedProposalExecTxResults, func(result *processResult, _ int) *abcitypes.ExecTxResult {
165+ return result.ToABCI()
166+ }),
167+ AppHash: d.tree.WorkingHash(),
168 }, nil
169 }
170 // a block other than the one we processed in ProcessProposal was decided
171 // discard the current modified state, and process the decided block
172 d.tree.Rollback()
173174+ txResults := make([]*processResult, len(req.Txs))
00000175 for i, tx := range req.Txs {
176+ var err error
177+ txResults[i], err = processTx(ctx, d.transactionProcessorDependencies(), tx, req.Time, true)
178 if err != nil {
179 return nil, stacktrace.Propagate(err, "")
180 }
0000000000181 }
182183+ d.lastProcessedProposalHash = slices.Clone(req.Hash)
184+ d.lastProcessedProposalExecTxResults = txResults
185+186 return &abcitypes.ResponseFinalizeBlock{
187+ TxResults: lo.Map(d.lastProcessedProposalExecTxResults, func(result *processResult, _ int) *abcitypes.ExecTxResult {
188+ return result.ToABCI()
189+ }),
190+ AppHash: d.tree.WorkingHash(),
191 }, nil
192}
193···198 return nil, stacktrace.Propagate(err, "")
199 }
200201+ for _, r := range d.lastProcessedProposalExecTxResults {
202+ for _, cb := range r.commitSideEffects {
203+ cb()
204+ }
205+ }
206+207 // TODO(later) consider whether we can set some RetainHeight in the response
208 return &abcitypes.ResponseCommit{}, nil
209}
210+211+func (d *DIDPLCApplication) transactionProcessorDependencies() TransactionProcessorDependencies {
212+ return TransactionProcessorDependencies{
213+ plc: d.plc,
214+ tree: d,
215+ aocsByPLC: d.aocsByPLC,
216+ }
217+}
···6 "net/url"
7 "time"
89- "github.com/did-method-plc/go-didplc"
10 cbornode "github.com/ipfs/go-ipld-cbor"
11 "github.com/palantir/stacktrace"
12 "tangled.org/gbl08ma/didplcbft/plc"
···123 }, nil
124 }
12500126 expectedCursor, err := store.Tree.AuthoritativeImportProgress(roTree)
127 if err != nil {
128 return nil, stacktrace.Propagate(err, "")
···135 }, nil
136 }
137138- // TODO this shouldn't be happening synchronously! We should always be ahead of the next transaction!
139- // or at the very least it should only happen once (e.g. when processing the proposal) and then we should cache until it expires or until we actually commit
140- operations, newCursor, err := fetchExportedBatchFromAuthoritativeSource(ctx, expectedPlcUrl, expectedCursor, tx.Arguments.Count)
141 if err != nil {
142- // returning an actual error like this means "consensus failure". Probably not the best way to deal with this, we would rather drop the transaction if not all nodes can fetch the same thing
143- // TODO investigate
144- return nil, stacktrace.Propagate(err, "")
00000000145 }
146147 expectedHashBytes, err := computeLogEntriesHash(operations)
···151152 if hex.EncodeToString(expectedHashBytes) != tx.Arguments.Hash {
153 return &processResult{
154- Code: 4112,
155 Info: "Unexpected import hash",
156 }, nil
00000157 }
158159 if execute {
···162 return nil, stacktrace.Propagate(err, "")
163 }
164165- var client didplc.Client
166 for _, entry := range operations {
167- err := deps.plc.ImportOperationFromAuthoritativeSource(ctx, entry, func() ([]didplc.LogEntry, error) {
168- // TODO Oh NOOOOOOO! This is not deterministic
169- // (fetched at different times, the audit log might grow, therefore we'll fetch and insert more ops, and change the apphash)
170- // we need to either limit how much audit log we return (only doable if how much was fetched for each op was part of the tx, ugh)
171- // or (probably preferred approach) make it so that the ImportOperationFromAuthoritativeSource / ReplaceHistory function only replaces up until the CID that's being imported, and no further ops
172- // Even then there is a problem: what if the nullified status changes between imports :dizzy_face:
173- // (can the nullified status change for the ops that are being imported only up until CID? Need to think)
174- e, err := client.AuditLog(ctx, entry.DID)
175- return e, stacktrace.Propagate(err, "")
176- })
177 if err != nil {
178 return nil, stacktrace.Propagate(err, "")
179 }
···184 }
185 }
186187- // TODO finish implementation
188- // 1. if execute is true: actually import the operations
189- // 2. if execute is true: update AuthoritativeImportProgress
190-191 return &processResult{
192- IsAuthoritativeImportTransaction: true,
193- Code: 0,
194- }, stacktrace.NewError("not implemented")
00000195}
···80 return nil
81}
8283-func (plc *plcImpl) ImportOperationFromAuthoritativeSource(ctx context.Context, newEntry didplc.LogEntry,
84- authoritativeAuditLogFetcher func() ([]didplc.LogEntry, error)) error {
85 plc.mu.Lock()
86 defer plc.mu.Unlock()
87···90 return stacktrace.Propagate(err, "failed to obtain mutable tree")
91 }
9293- l, _, err := store.Tree.AuditLog(tree, newEntry.DID, false)
94- if err != nil {
95- return stacktrace.Propagate(err, "")
96- }
97-98 newCID := newEntry.CID
99 newPrev := newEntry.Operation.AsOperation().PrevCIDStr()
100101- newCreatedAtDT, err := syntax.ParseDatetime(newEntry.CreatedAt)
102- if err != nil {
103- return stacktrace.Propagate(err, "")
104- }
105- newCreatedAt := newCreatedAtDT.Time()
0000000000000000000000106107- mustFullyReplaceHistory := false
108- for _, entry := range l {
109- if entry.CreatedAt.After(newCreatedAt) {
110- // We're trying to import an operation whose timestamp precedes one of the timestamps for operations we already know about
111- // We'll need to discard all known history and import it anew using the authoritative source data (same as when dealing with sequence forks)
112- mustFullyReplaceHistory = true
113 break
114 }
0000115116- if entry.CID.String() == newCID && entry.Nullified == newEntry.Nullified {
117- // If an operation with the same CID already exists -> easy-ish
118119- // this operation is already present, there is nothing to do
120- // TODO re-evaluate whether we want to still update the timestamp on the existing operation, as not doing this will cause the export from our impl to definitely not match the authoritative source
121- // (Though, the actually damaging cases of incorrect createdAt are already handled by the prior check)
122- return nil
123 }
124- }
00125126- if len(l) == 0 || (!mustFullyReplaceHistory && l[len(l)-1].CID.String() == newPrev) {
127- // If DID doesn't exist at all -> easy
128- // If prev matches CID of latest operation, and resulting timestamp sequence monotonically increases -> easy
129- err = store.Tree.StoreOperation(tree, newEntry, mo.None[int]())
130 return stacktrace.Propagate(err, "failed to commit operation")
131 }
132133- // if we get here then we're dealing with a DID that has "complicated" history
134- // to avoid dealing with nullification (which is made complicated here since we don't know which nullified ops are part of the "canonical audit log"
135- // and which are caused by people purposefully submitting forking ops to the chain vs the authoritative source)
136- // fetch audit log for DID and replace the entire history with the one from the authoritative source
000137138- auditLog, err := authoritativeAuditLogFetcher()
139- if err != nil {
140- return stacktrace.Propagate(err, "")
141 }
142143- err = store.Tree.ReplaceHistory(tree, auditLog)
144- return stacktrace.Propagate(err, "")
0145}
146147func (plc *plcImpl) Resolve(ctx context.Context, atHeight TreeVersion, did string) (didplc.Doc, error) {
···162 return didplc.Doc{}, stacktrace.Propagate(ErrDIDNotFound, "")
163 }
164165- // find most recent operation that isn't nullified (during authoritative import, the latest operation might be nullified)
166- for i := len(l) - 1; i >= 0; i-- {
167- opEnum := l[i].Operation
168- if !l[i].Nullified {
169- if opEnum.Tombstone != nil {
170- return didplc.Doc{}, stacktrace.Propagate(ErrDIDGone, "")
171- }
172- return opEnum.AsOperation().Doc(did)
173- }
174 }
175- // in the worst case all operations are somehow nullified and the loop ends with opEnum holding a nullified operation
176- // that _shouldn't_ be possible (right?) but if it does happen, let's just behave as if the DID was tombstoned
177- return didplc.Doc{}, stacktrace.Propagate(ErrDIDGone, "")
178}
179180func (plc *plcImpl) OperationLog(ctx context.Context, atHeight TreeVersion, did string) ([]didplc.OpEnum, error) {
···229 return nil, stacktrace.Propagate(ErrDIDNotFound, "")
230 }
231232- // if the latest operations are nullified (happens while authoritative import is in progress), just pretend we don't have them yet,
233- // since a properly functioning PLC implementation could never have the latest operation for a DID be nullified
234- dropAfterIdx := len(l) - 1
235- for ; dropAfterIdx >= 0; dropAfterIdx-- {
236- if !l[dropAfterIdx].Nullified {
237- break
238- }
239- }
240- l = l[0 : dropAfterIdx+1]
241-242 return lo.Map(l, func(logEntry types.SequencedLogEntry, _ int) didplc.LogEntry {
243 return logEntry.ToDIDPLCLogEntry()
244 }), nil
245}
246247func (plc *plcImpl) LastOperation(ctx context.Context, atHeight TreeVersion, did string) (didplc.OpEnum, error) {
248- // GetLastOp - /:did/log/last - latest op from audit log which isn't nullified
249 // if missing -> returns ErrDIDNotFound
250 // if tombstone -> returns tombstone op
251 plc.mu.Lock()
···265 return didplc.OpEnum{}, stacktrace.Propagate(ErrDIDNotFound, "")
266 }
267268- // find most recent operation that isn't nullified (during authoritative import, the latest operation might be nullified)
269- for i := len(l) - 1; i >= 0; i-- {
270- opEnum := l[i].Operation
271- if !l[i].Nullified {
272- return opEnum, nil
273- }
274- }
275- // in the worst case all operations are somehow nullified and the loop ends with opEnum holding a nullified operation
276- // that _shouldn't_ be possible (right?) but if it does happen, let's just behave as if the DID did not exist
277- return didplc.OpEnum{}, stacktrace.Propagate(ErrDIDNotFound, "")
278}
279280func (plc *plcImpl) Data(ctx context.Context, atHeight TreeVersion, did string) (didplc.RegularOp, error) {
···298 return didplc.RegularOp{}, stacktrace.Propagate(ErrDIDNotFound, "")
299 }
300301- // find most recent operation that isn't nullified (during authoritative import, the latest operation might be nullified)
302- for i := len(l) - 1; i >= 0; i-- {
303- opEnum := l[i].Operation
304- if !l[i].Nullified {
305- if opEnum.Tombstone != nil {
306- return didplc.RegularOp{}, stacktrace.Propagate(ErrDIDGone, "")
307- }
308- if opEnum.Regular != nil {
309- return *opEnum.Regular, nil
310- }
311- return *modernizeOp(opEnum.Legacy), nil
312- }
313 }
314- // in the worst case all operations are somehow nullified and the loop ends with opEnum holding a nullified operation
315- // that _shouldn't_ be possible (right?) but if it does happen, let's just behave as if the DID was tombstoned
316- return didplc.RegularOp{}, stacktrace.Propagate(ErrDIDGone, "")
317318}
319
···80 return nil
81}
8283+func (plc *plcImpl) ImportOperationFromAuthoritativeSource(ctx context.Context, newEntry didplc.LogEntry) error {
084 plc.mu.Lock()
85 defer plc.mu.Unlock()
86···89 return stacktrace.Propagate(err, "failed to obtain mutable tree")
90 }
910000092 newCID := newEntry.CID
93 newPrev := newEntry.Operation.AsOperation().PrevCIDStr()
9495+ mostRecentOpIndex := -1
96+ indexOfPrev := -1
97+ var iteratorErr error
98+ for entryIdx, entry := range store.Tree.AuditLogReverseIterator(tree, newEntry.DID, &iteratorErr) {
99+ entryCID := entry.CID.String()
100+ if mostRecentOpIndex == -1 {
101+ mostRecentOpIndex = entryIdx
102+103+ if newPrev == "" && entryCID != newCID {
104+ // this should never happen unless the authoritative source doesn't compute DIDs from genesis ops the way we do
105+ return stacktrace.NewError("invalid internal state reached")
106+ }
107+ }
108+109+ if entryCID == newCID {
110+ // should we already have an operation with the same CID, this condition should trigger before the next one
111+ // because this is a reverse iterator
112+ // looks like we already have the op we're trying to import. just need to update the timestamp
113+ newCreatedAtDT, err := syntax.ParseDatetime(newEntry.CreatedAt)
114+ if err != nil {
115+ return stacktrace.Propagate(err, "")
116+ }
117+118+ return stacktrace.Propagate(
119+ store.Tree.SetOperationCreatedAt(tree, entry.Seq, newCreatedAtDT.Time()),
120+ "")
121+ }
122123+ if entryCID == newPrev {
124+ indexOfPrev = entryIdx
0000125 break
126 }
127+ }
128+ if iteratorErr != nil {
129+ return stacktrace.Propagate(iteratorErr, "")
130+ }
131132+ nullifiedEntriesStartingIndex := mo.None[int]()
0133134+ if mostRecentOpIndex < 0 {
135+ // we have nothing for this DID - this should be a creation op, if not, then we're not importing things in order
136+ if newPrev != "" {
137+ return stacktrace.NewError("invalid internal state reached")
138 }
139+140+ // there's nothing to do but store the operation, no nullification involved
141+ newEntry.Nullified = false
142143+ err = store.Tree.StoreOperation(tree, newEntry, nullifiedEntriesStartingIndex)
000144 return stacktrace.Propagate(err, "failed to commit operation")
145 }
146147+ if indexOfPrev < 0 {
148+ // there are entries in the audit log but none of them has a CID matching prev
149+ // if this isn't a creation op, then this shouldn't happen
150+ // (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)
151+ // if this is a creation op, then this case should have been caught above
152+ return stacktrace.NewError("invalid internal state reached")
153+ }
154155+ if indexOfPrev+1 <= mostRecentOpIndex {
156+ nullifiedEntriesStartingIndex = mo.Some(indexOfPrev + 1)
0157 }
158159+ newEntry.Nullified = false
160+ err = store.Tree.StoreOperation(tree, newEntry, nullifiedEntriesStartingIndex)
161+ return stacktrace.Propagate(err, "failed to commit operation")
162}
163164func (plc *plcImpl) Resolve(ctx context.Context, atHeight TreeVersion, did string) (didplc.Doc, error) {
···179 return didplc.Doc{}, stacktrace.Propagate(ErrDIDNotFound, "")
180 }
181182+ opEnum := l[len(l)-1].Operation
183+ if opEnum.Tombstone != nil {
184+ return didplc.Doc{}, stacktrace.Propagate(ErrDIDGone, "")
000000185 }
186+ return opEnum.AsOperation().Doc(did)
00187}
188189func (plc *plcImpl) OperationLog(ctx context.Context, atHeight TreeVersion, did string) ([]didplc.OpEnum, error) {
···238 return nil, stacktrace.Propagate(ErrDIDNotFound, "")
239 }
2400000000000241 return lo.Map(l, func(logEntry types.SequencedLogEntry, _ int) didplc.LogEntry {
242 return logEntry.ToDIDPLCLogEntry()
243 }), nil
244}
245246func (plc *plcImpl) LastOperation(ctx context.Context, atHeight TreeVersion, did string) (didplc.OpEnum, error) {
247+ // GetLastOp - /:did/log/last - latest op from audit log which isn't nullified (the latest op is guaranteed not to be nullified)
248 // if missing -> returns ErrDIDNotFound
249 // if tombstone -> returns tombstone op
250 plc.mu.Lock()
···264 return didplc.OpEnum{}, stacktrace.Propagate(ErrDIDNotFound, "")
265 }
266267+ return l[len(l)-1].Operation, nil
000000000268}
269270func (plc *plcImpl) Data(ctx context.Context, atHeight TreeVersion, did string) (didplc.RegularOp, error) {
···288 return didplc.RegularOp{}, stacktrace.Propagate(ErrDIDNotFound, "")
289 }
290291+ opEnum := l[len(l)-1].Operation
292+ if opEnum.Tombstone != nil {
293+ return didplc.RegularOp{}, stacktrace.Propagate(ErrDIDGone, "")
294+ }
295+ if opEnum.Regular != nil {
296+ return *opEnum.Regular, nil
000000297 }
298+ return *modernizeOp(opEnum.Legacy), nil
00299300}
301
···28 AuditLogReverseIterator(tree ReadOnlyTree, did string, err *error) iter.Seq2[int, types.SequencedLogEntry]
29 ExportOperations(tree ReadOnlyTree, after uint64, count int) ([]types.SequencedLogEntry, error) // passing a count of zero means unlimited
30 StoreOperation(tree *iavl.MutableTree, entry didplc.LogEntry, nullifyWithIndexEqualOrGreaterThan mo.Option[int]) error
31- ReplaceHistory(tree *iavl.MutableTree, history []didplc.LogEntry) error
3233 AuthoritativePLC(tree ReadOnlyTree) (string, error)
34 SetAuthoritativePLC(tree *iavl.MutableTree, url string) error
···249 return nil
250}
251252-func (t *TreeStore) ReplaceHistory(tree *iavl.MutableTree, remoteHistory []didplc.LogEntry) error {
253- if len(remoteHistory) == 0 {
254- // for now this isn't needed, if it's needed in the future we'll have to accept a DID as argument on this function
255- return stacktrace.NewError("can't replace with empty history")
256- }
257-258- did := remoteHistory[0].DID
259-260- didBytes, err := DIDToBytes(did)
261- if err != nil {
262- return stacktrace.Propagate(err, "")
263- }
264-265- logKey := marshalDIDLogKey(didBytes)
266-267- localHistory, _, err := t.AuditLog(tree, did, false)
268- if err != nil {
269- return stacktrace.Propagate(err, "")
270- }
271-272- // if the first operations are equal to what we already have, keep them untouched to minimize the turmoil
273- keepLocalBeforeIdx := 0
274- for i, localEntry := range localHistory {
275- if i >= len(remoteHistory) {
276- break
277- }
278- remoteEntry := remoteHistory[i]
279-280- // stop looping once we find a difference
281- // we trust that the authoritative source computes CIDs properly (i.e. that two operations having the same CID are indeed equal)
282- if localEntry.Nullified != remoteEntry.Nullified || localEntry.CID.String() != remoteEntry.CID {
283- break
284- }
285-286- remoteDatetime, err := syntax.ParseDatetime(remoteEntry.CreatedAt)
287- if err != nil {
288- return stacktrace.Propagate(err, "invalid CreatedAt")
289- }
290-291- if !localEntry.CreatedAt.Equal(remoteDatetime.Time()) {
292- break
293- }
294-295- keepLocalBeforeIdx++
296- }
297-298- // all replaced/added operations get new sequence IDs.
299- // Get the highest sequence ID before removing any keys to ensure the sequence IDs actually change
300- seq, err := getNextSeqID(tree)
301- if err != nil {
302- return stacktrace.Propagate(err, "")
303- }
304305- // remove existing conflicting operations for this DID (if any)
306- logOperations, err := tree.Get(logKey)
307 if err != nil {
308 return stacktrace.Propagate(err, "")
309 }
310- logOperationsToDelete := logOperations[8*keepLocalBeforeIdx:]
311- for seqBytes := range slices.Chunk(logOperationsToDelete, 8) {
312- key := sequenceBytesToOperationKey(seqBytes)
313-314- _, _, err = tree.Remove(key)
315- if err != nil {
316- return stacktrace.Propagate(err, "")
317- }
318 }
319320- // add just the operations past the point they weren't kept
321- remoteHistory = remoteHistory[keepLocalBeforeIdx:]
322-323- // keep the operations log up until the point we've kept the history
324- // clone just to make sure we avoid issues since we got this slice from the tree, it is not meant to be modified
325- logOperations = slices.Clone(logOperations[0 : 8*keepLocalBeforeIdx])
326-327- for _, entry := range remoteHistory {
328- opDatetime, err := syntax.ParseDatetime(entry.CreatedAt)
329- if err != nil {
330- return stacktrace.Propagate(err, "invalid CreatedAt")
331- }
332-333- operation := entry.Operation.AsOperation()
334- opKey := marshalOperationKey(seq)
335- seq++
336- opValue := marshalOperationValue(entry.Nullified, didBytes, opDatetime.Time(), operation)
337-338- _, err = tree.Set(opKey, opValue)
339- if err != nil {
340- return stacktrace.Propagate(err, "")
341- }
342-343- // add to log for DID
344- logOperations = append(logOperations, opKey[1:9]...)
345- }
346347- // save updated log for DID
348- _, err = tree.Set(logKey, logOperations)
349- if err != nil {
350- return stacktrace.Propagate(err, "")
351- }
352353- return nil
0354}
355356var minOperationKey = marshalOperationKey(0)