···2626 snapshotApplier *snapshotApplier
27272828 lastProcessedProposalHash []byte
2929- lastProcessedProposalExecTxResults []*abcitypes.ExecTxResult
2929+ lastProcessedProposalExecTxResults []*processResult
3030+3131+ aocsByPLC map[string]*authoritativeOperationsCache
3032}
31333234// store and plc must be able to share transaction objects
···5153 d := &DIDPLCApplication{
5254 tree: tree,
5355 snapshotDirectory: snapshotDirectory,
5656+ aocsByPLC: make(map[string]*authoritativeOperationsCache),
5457 }
5558 d.fullyClearTree = func() error {
5659 // 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
···99 abcitypes "github.com/cometbft/cometbft/abci/types"
1010 "github.com/palantir/stacktrace"
1111 "github.com/samber/lo"
1212+ "tangled.org/gbl08ma/didplcbft/store"
1213)
13141415// InitChain implements [types.Application].
···2122func (d *DIDPLCApplication) PrepareProposal(ctx context.Context, req *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) {
2223 defer d.tree.Rollback()
23242424- deps := TransactionProcessorDependencies{
2525- plc: d.plc,
2626- tree: d,
2727- }
2828-2925 st := time.Now()
3026 acceptedTx := make([][]byte, 0, len(req.Txs))
3127 toProcess := req.Txs
3228 for {
3329 toTryNext := [][]byte{}
3430 for _, tx := range toProcess {
3535- result, err := processTx(ctx, deps, tx, req.Time, true)
3131+ result, err := processTx(ctx, d.transactionProcessorDependencies(), tx, req.Time, true)
3632 if err != nil {
3733 return nil, stacktrace.Propagate(err, "")
3834 }
39354040- if result.IsAuthoritativeImportTransaction {
3636+ if result.isAuthoritativeImportTransaction {
4137 // this type of transaction is not meant to appear in the mempool,
4238 // but maybe it's not impossible that a non-compliant node could have gossiped it to us?
4339 // (not sure if CometBFT checks transactions coming from other peers against CheckTx)
···6561 toProcess = toTryNext
6662 }
67636868- totalSize := lo.SumBy(acceptedTx, func(tx []byte) int { return len(tx) })
6969- if totalSize < int(req.MaxTxBytes)-4096 {
7070- // we have space to fit an import transaction
7171- // TODO
6464+ maybeTx, err := d.maybeCreateAuthoritativeImportTx(ctx)
6565+ if err != nil {
6666+ // TODO don't fail absolutely silently always, we should at least check what the error is
6767+ //return nil, stacktrace.Propagate(err, "")
6868+ }
6969+7070+ if err == nil && len(maybeTx) != 0 {
7171+ totalSize := lo.SumBy(acceptedTx, func(tx []byte) int { return len(tx) })
7272+ // 4K safety margin
7373+ if totalSize+len(maybeTx) < int(req.MaxTxBytes)-4096 {
7474+ // we have space to fit the import transaction
7575+7676+ result, err := processTx(ctx, d.transactionProcessorDependencies(), maybeTx, req.Time, true)
7777+ if err != nil {
7878+ return nil, stacktrace.Propagate(err, "")
7979+ }
8080+ if result.Code == 0 {
8181+ acceptedTx = append(acceptedTx, maybeTx)
8282+ }
8383+ }
7284 }
73857486 return &abcitypes.ResponsePrepareProposal{Txs: acceptedTx}, nil
···8294 return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
8395 }
84969797+ if req.Height == 1 {
9898+ tree, err := d.MutableTree()
9999+ if err != nil {
100100+ return nil, stacktrace.Propagate(err, "")
101101+ }
102102+103103+ err = store.Tree.SetAuthoritativePLC(tree, "https://plc.directory")
104104+ if err != nil {
105105+ return nil, stacktrace.Propagate(err, "")
106106+ }
107107+ }
108108+85109 // if we return early, ensure we don't use incomplete results where we haven't voted ACCEPT
86110 d.lastProcessedProposalHash = nil
87111 d.lastProcessedProposalExecTxResults = nil
···93117 }
94118 }()
951199696- deps := TransactionProcessorDependencies{
9797- plc: d.plc,
9898- tree: d,
9999- }
100100-101101- txResults := make([]*abcitypes.ExecTxResult, len(req.Txs))
120120+ txResults := make([]*processResult, len(req.Txs))
102121 for i, tx := range req.Txs {
103103- result, err := processTx(ctx, deps, tx, req.Time, true)
122122+ result, err := processTx(ctx, d.transactionProcessorDependencies(), tx, req.Time, true)
104123 if err != nil {
105124 return nil, stacktrace.Propagate(err, "")
106125 }
···110129 return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
111130 }
112131113113- if result.IsAuthoritativeImportTransaction && i != len(req.Txs)-1 {
132132+ if result.isAuthoritativeImportTransaction && i != len(req.Txs)-1 {
114133 // if an Authoritative Import transaction is present on the block, it must be the last one
115134 return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
116135 }
117136118118- txResults[i] = &abcitypes.ExecTxResult{
119119- Code: result.Code,
120120- Data: result.Data,
121121- Log: result.Log,
122122- Info: result.Info,
123123- GasWanted: result.GasWanted,
124124- GasUsed: result.GasUsed,
125125- Events: result.Events,
126126- Codespace: result.Codespace,
127127- }
137137+ txResults[i] = result
128138 }
129139130140 d.lastProcessedProposalHash = slices.Clone(req.Hash)
···151161 // the block that was decided was the one we processed in ProcessProposal, and ProcessProposal processed successfully
152162 // reuse the uncommitted results
153163 return &abcitypes.ResponseFinalizeBlock{
154154- TxResults: d.lastProcessedProposalExecTxResults,
155155- AppHash: d.tree.WorkingHash(),
164164+ TxResults: lo.Map(d.lastProcessedProposalExecTxResults, func(result *processResult, _ int) *abcitypes.ExecTxResult {
165165+ return result.ToABCI()
166166+ }),
167167+ AppHash: d.tree.WorkingHash(),
156168 }, nil
157169 }
158170 // a block other than the one we processed in ProcessProposal was decided
159171 // discard the current modified state, and process the decided block
160172 d.tree.Rollback()
161173162162- deps := TransactionProcessorDependencies{
163163- plc: d.plc,
164164- tree: d,
165165- }
166166-167167- txResults := make([]*abcitypes.ExecTxResult, len(req.Txs))
174174+ txResults := make([]*processResult, len(req.Txs))
168175 for i, tx := range req.Txs {
169169- result, err := processTx(ctx, deps, tx, req.Time, true)
176176+ var err error
177177+ txResults[i], err = processTx(ctx, d.transactionProcessorDependencies(), tx, req.Time, true)
170178 if err != nil {
171179 return nil, stacktrace.Propagate(err, "")
172180 }
173173- txResults[i] = &abcitypes.ExecTxResult{
174174- Code: result.Code,
175175- Data: result.Data,
176176- Log: result.Log,
177177- Info: result.Info,
178178- GasWanted: result.GasWanted,
179179- GasUsed: result.GasUsed,
180180- Events: result.Events,
181181- Codespace: result.Codespace,
182182- }
183181 }
184182183183+ d.lastProcessedProposalHash = slices.Clone(req.Hash)
184184+ d.lastProcessedProposalExecTxResults = txResults
185185+185186 return &abcitypes.ResponseFinalizeBlock{
186186- TxResults: txResults,
187187- AppHash: d.tree.WorkingHash(),
187187+ TxResults: lo.Map(d.lastProcessedProposalExecTxResults, func(result *processResult, _ int) *abcitypes.ExecTxResult {
188188+ return result.ToABCI()
189189+ }),
190190+ AppHash: d.tree.WorkingHash(),
188191 }, nil
189192}
190193···195198 return nil, stacktrace.Propagate(err, "")
196199 }
197200201201+ for _, r := range d.lastProcessedProposalExecTxResults {
202202+ for _, cb := range r.commitSideEffects {
203203+ cb()
204204+ }
205205+ }
206206+198207 // TODO(later) consider whether we can set some RetainHeight in the response
199208 return &abcitypes.ResponseCommit{}, nil
200209}
210210+211211+func (d *DIDPLCApplication) transactionProcessorDependencies() TransactionProcessorDependencies {
212212+ return TransactionProcessorDependencies{
213213+ plc: d.plc,
214214+ tree: d,
215215+ aocsByPLC: d.aocsByPLC,
216216+ }
217217+}
+154-16
abciapp/import.go
···55 "context"
66 "crypto/sha256"
77 "encoding/binary"
88+ "encoding/hex"
89 "encoding/json"
910 "fmt"
1011 "net/http"
1112 "net/url"
1313+ "sync"
1214 "time"
13151416 "github.com/bluesky-social/indigo/atproto/syntax"
1517 "github.com/did-method-plc/go-didplc"
1618 "github.com/ipfs/go-cid"
1919+ cbornode "github.com/ipfs/go-ipld-cbor"
1720 "github.com/palantir/stacktrace"
1818- "github.com/samber/lo"
2121+ "tangled.org/gbl08ma/didplcbft/plc"
1922 "tangled.org/gbl08ma/didplcbft/store"
2023)
21242222-func fetchExportedBatchFromAuthoritativeSource(ctx context.Context, plcURL string, startAt, maxCount uint64) ([]didplc.LogEntry, uint64, error) {
2525+type authoritativeOperationsCache struct {
2626+ mu sync.Mutex
2727+2828+ plcURL string
2929+ operations map[uint64]logEntryWithSeq
3030+}
3131+3232+type logEntryWithSeq struct {
3333+ didplc.LogEntry
3434+ Seq uint64 `json:"seq"`
3535+}
3636+3737+func newAuthoritativeOperationsCache(plc string) *authoritativeOperationsCache {
3838+ return &authoritativeOperationsCache{
3939+ plcURL: plc,
4040+ operations: make(map[uint64]logEntryWithSeq),
4141+ }
4242+}
4343+4444+func getOrCreateAuthoritativeOperationsCache(aocsByPLC map[string]*authoritativeOperationsCache, plc string) *authoritativeOperationsCache {
4545+ aoc, ok := aocsByPLC[plc]
4646+ if !ok {
4747+ aoc = newAuthoritativeOperationsCache(plc)
4848+ aocsByPLC[plc] = aoc
4949+ }
5050+ return aoc
5151+}
5252+5353+func (a *authoritativeOperationsCache) dropSeqBelowOrEqual(highestCommittedSeq uint64) {
5454+ a.mu.Lock()
5555+ defer a.mu.Unlock()
5656+5757+ for i := range a.operations {
5858+ if a.operations[i].Seq <= highestCommittedSeq {
5959+ delete(a.operations, i)
6060+ }
6161+ }
6262+}
6363+6464+func (a *authoritativeOperationsCache) fetchInMutex(ctx context.Context, after, count uint64) (bool, error) {
6565+ entries, _, err := fetchExportedBatchFromAuthoritativeSource(ctx, a.plcURL, after, count)
6666+ if err != nil {
6767+ return false, stacktrace.Propagate(err, "")
6868+ }
6969+7070+ for _, entry := range entries {
7171+ a.operations[entry.Seq] = entry
7272+ }
7373+ return uint64(len(entries)) < count, nil
7474+}
7575+7676+func (a *authoritativeOperationsCache) get(ctx context.Context, after, count uint64) ([]logEntryWithSeq, error) {
7777+ a.mu.Lock()
7878+ defer a.mu.Unlock()
7979+8080+ result := make([]logEntryWithSeq, 0, count)
8181+ reachedEnd := false
8282+ for i := uint64(0); uint64(len(result)) < count; i++ {
8383+ opSeq := after + i + 1
8484+ op, ok := a.operations[opSeq]
8585+ if !ok {
8686+ if reachedEnd {
8787+ // it's because we are asking about ops that don't exist yet, return
8888+ break
8989+ }
9090+9191+ re, err := a.fetchInMutex(ctx, after+i, count)
9292+ if err != nil {
9393+ return nil, stacktrace.Propagate(err, "")
9494+ }
9595+9696+ reachedEnd = reachedEnd || re
9797+9898+ op, ok = a.operations[opSeq]
9999+ if !ok {
100100+ // still not present even after fetching
101101+ // the authoritative source probably skipped this seq?
102102+ continue
103103+ }
104104+ }
105105+106106+ result = append(result, op)
107107+ }
108108+109109+ return result, nil
110110+}
111111+112112+func fetchExportedBatchFromAuthoritativeSource(ctx context.Context, plcURL string, startAt, maxCount uint64) ([]logEntryWithSeq, uint64, error) {
23113 baseURL, err := url.JoinPath(plcURL, "/export")
24114 if err != nil {
25115 return nil, 0, stacktrace.Propagate(err, "")
···2711728118 client := &http.Client{Timeout: 30 * time.Second}
291193030- entries := make([]didplc.LogEntry, 0, maxCount)
120120+ entries := make([]logEntryWithSeq, 0, maxCount)
31121 for {
32122 req, err := http.NewRequestWithContext(ctx, "GET", baseURL, nil)
33123 if err != nil {
34124 return nil, 0, stacktrace.Propagate(err, "")
35125 }
361263737- req.Header.Set("User-Agent", "go-did-method-plc")
127127+ req.Header.Set("User-Agent", "didplcbft")
3812839129 requestCount := min(1000, maxCount-uint64(len(entries)))
40130···53143 return nil, 0, stacktrace.NewError("non-200 status code")
54144 }
551455656- type logEntryWithSeq struct {
5757- didplc.LogEntry
5858- Seq uint64 `json:"seq"`
5959- }
6060-61146 // Read response body
62147 s := bufio.NewScanner(resp.Body)
63148 numEntriesThisResponse := 0
···66151 if err := json.Unmarshal(s.Bytes(), &entry); err != nil {
67152 return nil, 0, stacktrace.Propagate(err, "")
68153 }
6969- entries = append(entries, entry.LogEntry)
154154+ entries = append(entries, entry)
70155 numEntriesThisResponse++
71156 startAt = entry.Seq
72157 }
···82167 return entries, startAt, nil
83168}
841698585-func computeLogEntriesHash(logEntries []didplc.LogEntry) ([]byte, error) {
170170+func computeLogEntriesHash(logEntries []logEntryWithSeq) ([]byte, error) {
86171 // let's _not_ rely on the specifics of the JSON representation
87172 // (instead let's rely on specifics of our implementation, heh)
88173···125210 return nil, stacktrace.Propagate(err, "")
126211 }
127212128128- // Write Nullified
129129- _, err = hash.Write([]byte{lo.Ternary[byte](entry.Nullified, 1, 0)})
130130- if err != nil {
131131- return nil, stacktrace.Propagate(err, "")
132132- }
213213+ // Nullified can't be part of the hash as it can change on the authoritative source at any moment,
214214+ // we always import operations as if they weren't nullified and recompute the nullification status as needed
133215 }
134216135217 return hash.Sum(nil), nil
136218}
219219+220220+func (d *DIDPLCApplication) maybeCreateAuthoritativeImportTx(ctx context.Context) ([]byte, error) {
221221+ // use WorkingTreeVersion so we take into account any import operation that may have been processed in this block
222222+ roTree, err := d.ImmutableTree(plc.WorkingTreeVersion)
223223+ if err != nil {
224224+ return nil, stacktrace.Propagate(err, "")
225225+ }
226226+227227+ plcURL, err := store.Tree.AuthoritativePLC(roTree)
228228+ if err != nil {
229229+ return nil, stacktrace.Propagate(err, "")
230230+ }
231231+232232+ if plcURL == "" {
233233+ // we're not doing imports
234234+ return nil, nil
235235+ }
236236+237237+ cursor, err := store.Tree.AuthoritativeImportProgress(roTree)
238238+ if err != nil {
239239+ return nil, stacktrace.Propagate(err, "")
240240+ }
241241+242242+ aoc := getOrCreateAuthoritativeOperationsCache(d.aocsByPLC, plcURL)
243243+244244+ entries, err := aoc.get(ctx, cursor, 1000)
245245+ if err != nil {
246246+ return nil, stacktrace.Propagate(err, "")
247247+ }
248248+249249+ if len(entries) == 0 {
250250+ // nothing to import at the moment
251251+ return nil, nil
252252+ }
253253+254254+ hashBytes, err := computeLogEntriesHash(entries)
255255+ if err != nil {
256256+ return nil, stacktrace.Propagate(err, "")
257257+ }
258258+259259+ tx := Transaction[AuthoritativeImportArguments]{
260260+ Action: TransactionActionAuthoritativeImport,
261261+ Arguments: AuthoritativeImportArguments{
262262+ PLCURL: plcURL,
263263+ Hash: hex.EncodeToString(hashBytes),
264264+ Cursor: cursor,
265265+ Count: uint64(len(entries)),
266266+ },
267267+ }
268268+269269+ out, err := cbornode.DumpObject(tx)
270270+ if err != nil {
271271+ return nil, stacktrace.Propagate(err, "")
272272+ }
273273+ return out, nil
274274+}
+2-7
abciapp/mempool.go
···10101111// CheckTx implements [types.Application].
1212func (d *DIDPLCApplication) CheckTx(ctx context.Context, req *abcitypes.RequestCheckTx) (*abcitypes.ResponseCheckTx, error) {
1313- deps := TransactionProcessorDependencies{
1414- plc: d.plc,
1515- tree: d,
1616- }
1717-1818- result, err := processTx(ctx, deps, req.Tx, time.Now(), false)
1313+ result, err := processTx(ctx, d.transactionProcessorDependencies(), req.Tx, time.Now(), false)
1914 if err != nil {
2015 return nil, stacktrace.Propagate(err, "")
2116 }
2222- if result.IsAuthoritativeImportTransaction {
1717+ if result.isAuthoritativeImportTransaction {
2318 // this type of transaction is meant to be included only by validator nodes
2419 return &abcitypes.ResponseCheckTx{
2520 Code: 4002,
+18-3
abciapp/tx.go
···1818type TransactionAction string
19192020type TransactionProcessorDependencies struct {
2121- plc plc.PLC
2222- tree plc.TreeProvider // TODO maybe we should move the TreeProvider definition out of the plc package then?
2121+ plc plc.PLC
2222+ tree plc.TreeProvider // TODO maybe we should move the TreeProvider definition out of the plc package then?
2323+ aocsByPLC map[string]*authoritativeOperationsCache
2324}
24252526type TransactionProcessor func(ctx context.Context, deps TransactionProcessorDependencies, txBytes []byte, atTime time.Time, execute bool) (*processResult, error)
···8283}
83848485type processResult struct {
8585- IsAuthoritativeImportTransaction bool
8686+ isAuthoritativeImportTransaction bool
8787+ commitSideEffects []func()
86888789 Code uint32
8890 Data []byte
···9294 GasUsed int64
9395 Events []abcitypes.Event
9496 Codespace string
9797+}
9898+9999+func (result processResult) ToABCI() *abcitypes.ExecTxResult {
100100+ return &abcitypes.ExecTxResult{
101101+ Code: result.Code,
102102+ Data: result.Data,
103103+ Log: result.Log,
104104+ Info: result.Info,
105105+ GasWanted: result.GasWanted,
106106+ GasUsed: result.GasUsed,
107107+ Events: result.Events,
108108+ Codespace: result.Codespace,
109109+ }
95110}
9611197112func processTx(ctx context.Context, deps TransactionProcessorDependencies, txBytes []byte, atTime time.Time, execute bool) (*processResult, error) {
+29-26
abciapp/tx_import.go
···66 "net/url"
77 "time"
8899- "github.com/did-method-plc/go-didplc"
109 cbornode "github.com/ipfs/go-ipld-cbor"
1110 "github.com/palantir/stacktrace"
1211 "tangled.org/gbl08ma/didplcbft/plc"
···123122 }, nil
124123 }
125124125125+ aoc := getOrCreateAuthoritativeOperationsCache(deps.aocsByPLC, expectedPlcUrl)
126126+126127 expectedCursor, err := store.Tree.AuthoritativeImportProgress(roTree)
127128 if err != nil {
128129 return nil, stacktrace.Propagate(err, "")
···135136 }, nil
136137 }
137138138138- // TODO this shouldn't be happening synchronously! We should always be ahead of the next transaction!
139139- // 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
140140- operations, newCursor, err := fetchExportedBatchFromAuthoritativeSource(ctx, expectedPlcUrl, expectedCursor, tx.Arguments.Count)
139139+ operations, err := aoc.get(ctx, expectedCursor, tx.Arguments.Count)
141140 if err != nil {
142142- // 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
143143- // TODO investigate
144144- return nil, stacktrace.Propagate(err, "")
141141+ return &processResult{
142142+ Code: 4112,
143143+ Info: "Failure to obtain authoritative operations",
144144+ }, nil
145145+ }
146146+147147+ if uint64(len(operations)) < tx.Arguments.Count {
148148+ return &processResult{
149149+ Code: 4113,
150150+ Info: "Unexpected import count",
151151+ }, nil
145152 }
146153147154 expectedHashBytes, err := computeLogEntriesHash(operations)
···151158152159 if hex.EncodeToString(expectedHashBytes) != tx.Arguments.Hash {
153160 return &processResult{
154154- Code: 4112,
161161+ Code: 4114,
155162 Info: "Unexpected import hash",
156163 }, nil
164164+ }
165165+166166+ newCursor := expectedCursor
167167+ if len(operations) > 0 {
168168+ newCursor = operations[len(operations)-1].Seq
157169 }
158170159171 if execute {
···162174 return nil, stacktrace.Propagate(err, "")
163175 }
164176165165- var client didplc.Client
166177 for _, entry := range operations {
167167- err := deps.plc.ImportOperationFromAuthoritativeSource(ctx, entry, func() ([]didplc.LogEntry, error) {
168168- // TODO Oh NOOOOOOO! This is not deterministic
169169- // (fetched at different times, the audit log might grow, therefore we'll fetch and insert more ops, and change the apphash)
170170- // 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)
171171- // 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
172172- // Even then there is a problem: what if the nullified status changes between imports :dizzy_face:
173173- // (can the nullified status change for the ops that are being imported only up until CID? Need to think)
174174- e, err := client.AuditLog(ctx, entry.DID)
175175- return e, stacktrace.Propagate(err, "")
176176- })
178178+ err := deps.plc.ImportOperationFromAuthoritativeSource(ctx, entry.LogEntry)
177179 if err != nil {
178180 return nil, stacktrace.Propagate(err, "")
179181 }
···184186 }
185187 }
186188187187- // TODO finish implementation
188188- // 1. if execute is true: actually import the operations
189189- // 2. if execute is true: update AuthoritativeImportProgress
190190-191189 return &processResult{
192192- IsAuthoritativeImportTransaction: true,
193193- Code: 0,
194194- }, stacktrace.NewError("not implemented")
190190+ isAuthoritativeImportTransaction: true,
191191+ commitSideEffects: []func(){
192192+ func() {
193193+ aoc.dropSeqBelowOrEqual(newCursor)
194194+ },
195195+ },
196196+ Code: 0,
197197+ }, nil
195198}
+2
httpapi/server.go
···305305 sendErrorResponse(w, http.StatusBadRequest, "Invalid count parameter")
306306 return
307307 }
308308+309309+ // TODO limit count to 1000 (for debugging it's more useful without limit)
308310 }
309311310312 afterStr := query.Get("after")
···8080 return nil
8181}
82828383-func (plc *plcImpl) ImportOperationFromAuthoritativeSource(ctx context.Context, newEntry didplc.LogEntry,
8484- authoritativeAuditLogFetcher func() ([]didplc.LogEntry, error)) error {
8383+func (plc *plcImpl) ImportOperationFromAuthoritativeSource(ctx context.Context, newEntry didplc.LogEntry) error {
8584 plc.mu.Lock()
8685 defer plc.mu.Unlock()
8786···9089 return stacktrace.Propagate(err, "failed to obtain mutable tree")
9190 }
92919393- l, _, err := store.Tree.AuditLog(tree, newEntry.DID, false)
9494- if err != nil {
9595- return stacktrace.Propagate(err, "")
9696- }
9797-9892 newCID := newEntry.CID
9993 newPrev := newEntry.Operation.AsOperation().PrevCIDStr()
10094101101- newCreatedAtDT, err := syntax.ParseDatetime(newEntry.CreatedAt)
102102- if err != nil {
103103- return stacktrace.Propagate(err, "")
104104- }
105105- newCreatedAt := newCreatedAtDT.Time()
9595+ mostRecentOpIndex := -1
9696+ indexOfPrev := -1
9797+ var iteratorErr error
9898+ for entryIdx, entry := range store.Tree.AuditLogReverseIterator(tree, newEntry.DID, &iteratorErr) {
9999+ entryCID := entry.CID.String()
100100+ if mostRecentOpIndex == -1 {
101101+ mostRecentOpIndex = entryIdx
102102+103103+ if newPrev == "" && entryCID != newCID {
104104+ // this should never happen unless the authoritative source doesn't compute DIDs from genesis ops the way we do
105105+ return stacktrace.NewError("invalid internal state reached")
106106+ }
107107+ }
108108+109109+ if entryCID == newCID {
110110+ // should we already have an operation with the same CID, this condition should trigger before the next one
111111+ // because this is a reverse iterator
112112+ // looks like we already have the op we're trying to import. just need to update the timestamp
113113+ newCreatedAtDT, err := syntax.ParseDatetime(newEntry.CreatedAt)
114114+ if err != nil {
115115+ return stacktrace.Propagate(err, "")
116116+ }
117117+118118+ return stacktrace.Propagate(
119119+ store.Tree.SetOperationCreatedAt(tree, entry.Seq, newCreatedAtDT.Time()),
120120+ "")
121121+ }
106122107107- mustFullyReplaceHistory := false
108108- for _, entry := range l {
109109- if entry.CreatedAt.After(newCreatedAt) {
110110- // We're trying to import an operation whose timestamp precedes one of the timestamps for operations we already know about
111111- // We'll need to discard all known history and import it anew using the authoritative source data (same as when dealing with sequence forks)
112112- mustFullyReplaceHistory = true
123123+ if entryCID == newPrev {
124124+ indexOfPrev = entryIdx
113125 break
114126 }
127127+ }
128128+ if iteratorErr != nil {
129129+ return stacktrace.Propagate(iteratorErr, "")
130130+ }
115131116116- if entry.CID.String() == newCID && entry.Nullified == newEntry.Nullified {
117117- // If an operation with the same CID already exists -> easy-ish
132132+ nullifiedEntriesStartingIndex := mo.None[int]()
118133119119- // this operation is already present, there is nothing to do
120120- // 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
121121- // (Though, the actually damaging cases of incorrect createdAt are already handled by the prior check)
122122- return nil
134134+ if mostRecentOpIndex < 0 {
135135+ // we have nothing for this DID - this should be a creation op, if not, then we're not importing things in order
136136+ if newPrev != "" {
137137+ return stacktrace.NewError("invalid internal state reached")
123138 }
124124- }
139139+140140+ // there's nothing to do but store the operation, no nullification involved
141141+ newEntry.Nullified = false
125142126126- if len(l) == 0 || (!mustFullyReplaceHistory && l[len(l)-1].CID.String() == newPrev) {
127127- // If DID doesn't exist at all -> easy
128128- // If prev matches CID of latest operation, and resulting timestamp sequence monotonically increases -> easy
129129- err = store.Tree.StoreOperation(tree, newEntry, mo.None[int]())
143143+ err = store.Tree.StoreOperation(tree, newEntry, nullifiedEntriesStartingIndex)
130144 return stacktrace.Propagate(err, "failed to commit operation")
131145 }
132146133133- // if we get here then we're dealing with a DID that has "complicated" history
134134- // 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"
135135- // and which are caused by people purposefully submitting forking ops to the chain vs the authoritative source)
136136- // fetch audit log for DID and replace the entire history with the one from the authoritative source
147147+ if indexOfPrev < 0 {
148148+ // there are entries in the audit log but none of them has a CID matching prev
149149+ // if this isn't a creation op, then this shouldn't happen
150150+ // (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)
151151+ // if this is a creation op, then this case should have been caught above
152152+ return stacktrace.NewError("invalid internal state reached")
153153+ }
137154138138- auditLog, err := authoritativeAuditLogFetcher()
139139- if err != nil {
140140- return stacktrace.Propagate(err, "")
155155+ if indexOfPrev+1 <= mostRecentOpIndex {
156156+ nullifiedEntriesStartingIndex = mo.Some(indexOfPrev + 1)
141157 }
142158143143- err = store.Tree.ReplaceHistory(tree, auditLog)
144144- return stacktrace.Propagate(err, "")
159159+ newEntry.Nullified = false
160160+ err = store.Tree.StoreOperation(tree, newEntry, nullifiedEntriesStartingIndex)
161161+ return stacktrace.Propagate(err, "failed to commit operation")
145162}
146163147164func (plc *plcImpl) Resolve(ctx context.Context, atHeight TreeVersion, did string) (didplc.Doc, error) {
···162179 return didplc.Doc{}, stacktrace.Propagate(ErrDIDNotFound, "")
163180 }
164181165165- // find most recent operation that isn't nullified (during authoritative import, the latest operation might be nullified)
166166- for i := len(l) - 1; i >= 0; i-- {
167167- opEnum := l[i].Operation
168168- if !l[i].Nullified {
169169- if opEnum.Tombstone != nil {
170170- return didplc.Doc{}, stacktrace.Propagate(ErrDIDGone, "")
171171- }
172172- return opEnum.AsOperation().Doc(did)
173173- }
182182+ opEnum := l[len(l)-1].Operation
183183+ if opEnum.Tombstone != nil {
184184+ return didplc.Doc{}, stacktrace.Propagate(ErrDIDGone, "")
174185 }
175175- // in the worst case all operations are somehow nullified and the loop ends with opEnum holding a nullified operation
176176- // that _shouldn't_ be possible (right?) but if it does happen, let's just behave as if the DID was tombstoned
177177- return didplc.Doc{}, stacktrace.Propagate(ErrDIDGone, "")
186186+ return opEnum.AsOperation().Doc(did)
178187}
179188180189func (plc *plcImpl) OperationLog(ctx context.Context, atHeight TreeVersion, did string) ([]didplc.OpEnum, error) {
···229238 return nil, stacktrace.Propagate(ErrDIDNotFound, "")
230239 }
231240232232- // if the latest operations are nullified (happens while authoritative import is in progress), just pretend we don't have them yet,
233233- // since a properly functioning PLC implementation could never have the latest operation for a DID be nullified
234234- dropAfterIdx := len(l) - 1
235235- for ; dropAfterIdx >= 0; dropAfterIdx-- {
236236- if !l[dropAfterIdx].Nullified {
237237- break
238238- }
239239- }
240240- l = l[0 : dropAfterIdx+1]
241241-242241 return lo.Map(l, func(logEntry types.SequencedLogEntry, _ int) didplc.LogEntry {
243242 return logEntry.ToDIDPLCLogEntry()
244243 }), nil
245244}
246245247246func (plc *plcImpl) LastOperation(ctx context.Context, atHeight TreeVersion, did string) (didplc.OpEnum, error) {
248248- // GetLastOp - /:did/log/last - latest op from audit log which isn't nullified
247247+ // GetLastOp - /:did/log/last - latest op from audit log which isn't nullified (the latest op is guaranteed not to be nullified)
249248 // if missing -> returns ErrDIDNotFound
250249 // if tombstone -> returns tombstone op
251250 plc.mu.Lock()
···265264 return didplc.OpEnum{}, stacktrace.Propagate(ErrDIDNotFound, "")
266265 }
267266268268- // find most recent operation that isn't nullified (during authoritative import, the latest operation might be nullified)
269269- for i := len(l) - 1; i >= 0; i-- {
270270- opEnum := l[i].Operation
271271- if !l[i].Nullified {
272272- return opEnum, nil
273273- }
274274- }
275275- // in the worst case all operations are somehow nullified and the loop ends with opEnum holding a nullified operation
276276- // that _shouldn't_ be possible (right?) but if it does happen, let's just behave as if the DID did not exist
277277- return didplc.OpEnum{}, stacktrace.Propagate(ErrDIDNotFound, "")
267267+ return l[len(l)-1].Operation, nil
278268}
279269280270func (plc *plcImpl) Data(ctx context.Context, atHeight TreeVersion, did string) (didplc.RegularOp, error) {
···298288 return didplc.RegularOp{}, stacktrace.Propagate(ErrDIDNotFound, "")
299289 }
300290301301- // find most recent operation that isn't nullified (during authoritative import, the latest operation might be nullified)
302302- for i := len(l) - 1; i >= 0; i-- {
303303- opEnum := l[i].Operation
304304- if !l[i].Nullified {
305305- if opEnum.Tombstone != nil {
306306- return didplc.RegularOp{}, stacktrace.Propagate(ErrDIDGone, "")
307307- }
308308- if opEnum.Regular != nil {
309309- return *opEnum.Regular, nil
310310- }
311311- return *modernizeOp(opEnum.Legacy), nil
312312- }
291291+ opEnum := l[len(l)-1].Operation
292292+ if opEnum.Tombstone != nil {
293293+ return didplc.RegularOp{}, stacktrace.Propagate(ErrDIDGone, "")
294294+ }
295295+ if opEnum.Regular != nil {
296296+ return *opEnum.Regular, nil
313297 }
314314- // in the worst case all operations are somehow nullified and the loop ends with opEnum holding a nullified operation
315315- // that _shouldn't_ be possible (right?) but if it does happen, let's just behave as if the DID was tombstoned
316316- return didplc.RegularOp{}, stacktrace.Propagate(ErrDIDGone, "")
298298+ return *modernizeOp(opEnum.Legacy), nil
317299318300}
319301