@agoric/cosmos 0.34.2-dev-5513dea.0 → 0.34.2-dev-3679b4c.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,818 @@
1
+ package keeper
2
+
3
+ import (
4
+ "encoding/json"
5
+ "errors"
6
+ "fmt"
7
+ "io"
8
+ "os"
9
+ "path/filepath"
10
+ "regexp"
11
+
12
+ "github.com/Agoric/agoric-sdk/golang/cosmos/vm"
13
+ "github.com/Agoric/agoric-sdk/golang/cosmos/x/swingset/types"
14
+ vstoragetypes "github.com/Agoric/agoric-sdk/golang/cosmos/x/vstorage/types"
15
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
16
+ "github.com/tendermint/tendermint/libs/log"
17
+ )
18
+
19
+ // This module abstracts the generation and handling of swing-store exports,
20
+ // including the communication with the JS side to generate and restore them.
21
+ //
22
+ // Its interface derives from the following requirements:
23
+ // - Multiple golang components may perform swing-store export or import
24
+ // operations, but the JS side does not support concurrent operations as
25
+ // there are no legitimate use cases.
26
+ // - Some components cannot block the main execution while performing an export
27
+ // operation. In particular, cosmos's state-sync snapshot process cannot
28
+ // block the logic handling tendermint events.
29
+ // - The JS swing-store cannot access historical states. To generate
30
+ // deterministic exports, the export operations that cannot block must be able
31
+ // to synchronize with commit points that will change the JS swing-store.
32
+ // - The JS swing-store export logic does however support mutation of the
33
+ // JS swing-store state after an export operation has started. Such mutations
34
+ // do not affect the export that is produced, and can span multiple blocks.
35
+ // - This implies the commit synchronization is only necessary until the JS
36
+ // side of the export operation has started.
37
+ // - Some components, in particular state-sync, may need to perform other work
38
+ // alongside generating a swing-store export. This work similarly cannot block
39
+ // the main execution, but must allow for the swing-store synchronization
40
+ // that enables generating deterministic export. For state-sync, this work
41
+ // happens before the generated swing-store export can be consumed.
42
+ //
43
+ // The general approach taken is to implement a SwingStoreExportsHandler that
44
+ // implements the communication with the JS side, enforces that no concurrent
45
+ // operations take place, defers the consumption of the export to a provided
46
+ // SwingStoreExportEventHandler, and provides some synchronization methods to
47
+ // let the application enforce mutation boundaries.
48
+ //
49
+ // There should be a single SwingStoreExportsHandler instance, and all its method
50
+ // calls should be performed from the same goroutine (no mutex enforcement).
51
+ //
52
+ // The process of generating a SwingStore export proceeds as follow:
53
+ // - The component invokes swingStoreExportsHandler.InitiateExport with an
54
+ // eventHandler for the export.
55
+ // - InitiateExport verifies no other export operation is in progress and
56
+ // starts a goroutine to perform the export operation. It requests the JS
57
+ // side to start generating an export of the swing-store, and calls the
58
+ // eventHandler's OnExportStarted method with a function param allowing it to
59
+ // retrieve the export.
60
+ // - The cosmos app will call WaitUntilSwingStoreExportStarted before
61
+ // instructing the JS controller to commit its work, satisfying the
62
+ // deterministic exports requirement.
63
+ // - OnExportStarted must call the retrieve function before returning, however
64
+ // it may perform other work before. For cosmos state-sync snapshots,
65
+ // OnExportStarted will call app.Snapshot which will invoke the swingset
66
+ // module's ExtensionSnapshotter that will retrieve and process the
67
+ // swing-store export.
68
+ // - When the retrieve function is called, it blocks until the JS export is
69
+ // ready, then creates a SwingStoreExportProvider that abstract accessing
70
+ // the content of the export. The eventHandler's OnExportRetrieved is called
71
+ // with the export provider.
72
+ // - OnExportRetrieved reads the export using the provider.
73
+ //
74
+ // Restoring a swing-store export does not have similar non-blocking requirements.
75
+ // The component simply invokes swingStoreExportHandler.RestoreExport with a
76
+ // SwingStoreExportProvider representing the swing-store export to
77
+ // be restored, and RestoreExport will consume it and block until the JS side
78
+ // has completed the restore before returning.
79
+
80
+ // exportManifest represents the content of the JS swing-store export manifest.
81
+ // The export is exchanged between Cosmos and JS using the file system, and only
82
+ // the directory containing the export is exchanged with a blockingSend. The
83
+ // manifest is a JSON file with the agreed upon file name of
84
+ // "export-manifest.json" in the export directory. It contains the file names
85
+ // for the "export data" (described in the godoc for exportDataFilename), and
86
+ // for the opaque artifacts of the export.
87
+ type exportManifest struct {
88
+ // BlockHeight is the block height of the manifest.
89
+ BlockHeight uint64 `json:"blockHeight,omitempty"`
90
+ // Data is the filename of the export data.
91
+ Data string `json:"data,omitempty"`
92
+ // Artifacts is the list of [artifact name, file name] pairs.
93
+ Artifacts [][2]string `json:"artifacts"`
94
+ }
95
+
96
+ // ExportManifestFilename is the manifest filename which must be synchronized with the JS export/import tooling
97
+ // See packages/cosmic-swingset/src/export-kernel-db.js and packages/cosmic-swingset/src/import-kernel-db.js
98
+ const ExportManifestFilename = "export-manifest.json"
99
+
100
+ // For restore operations, the swing-store "export data" is exchanged with the
101
+ // JS side as a file which encodes "export data" entries as a sequence of
102
+ // [key, value] JSON arrays each terminated by a new line.
103
+ // NB: this is not technically jsonlines since the entries are new line
104
+ // terminated instead of being new line separated, however the parsers in both
105
+ // JS and golang handle such extra whitespace.
106
+ const exportDataFilename = "export-data.jsonl"
107
+
108
+ // UntrustedExportDataArtifactName is a special artifact name that the provider
109
+ // and consumer of an export can use to indicate the presence of a synthetic
110
+ // artifact containing untrusted "export data". This artifact must not end up in
111
+ // the list of artifacts imported by the JS import tooling (which would fail).
112
+ const UntrustedExportDataArtifactName = "UNTRUSTED-EXPORT-DATA"
113
+ const untrustedExportDataFilename = "untrusted-export-data.jsonl"
114
+
115
+ const exportedFilesMode = 0644
116
+
117
+ // swingStoreExportActionType is the action type used for all swing-store
118
+ // export blockingSend, and synchronized with the JS side in
119
+ // packages/internal/src/action-types.js
120
+ const swingStoreExportActionType = "SWING_STORE_EXPORT"
121
+
122
+ // initiateRequest is the request type for initiating an export
123
+ const initiateRequest = "initiate"
124
+
125
+ type swingStoreInitiateExportAction struct {
126
+ Type string `json:"type"` // "SWING_STORE_EXPORT"
127
+ Request string `json:"request"` // "initiate"
128
+ BlockHeight uint64 `json:"blockHeight,omitempty"` // empty if no blockHeight requested (latest)
129
+ Args [1]SwingStoreExportOptions `json:"args"`
130
+ }
131
+
132
+ // retrieveRequest is the request type for retrieving an initiated export
133
+ const retrieveRequest = "retrieve"
134
+
135
+ type swingStoreRetrieveExportAction struct {
136
+ Type string `json:"type"` // "SWING_STORE_EXPORT"
137
+ Request string `json:"request"` // "retrieve"
138
+ }
139
+ type swingStoreRetrieveResult = string
140
+
141
+ // discardRequest is the request type for discarding an initiated but an export
142
+ // that was not retrieved
143
+ const discardRequest = "discard"
144
+
145
+ type swingStoreDiscardExportAction struct {
146
+ Type string `json:"type"` // "SWING_STORE_EXPORT"
147
+ Request string `json:"request"` // "discard"
148
+ }
149
+
150
+ // restoreRequest is the request type for restoring an export
151
+ const restoreRequest = "restore"
152
+
153
+ type swingStoreRestoreExportAction struct {
154
+ Type string `json:"type"` // "SWING_STORE_EXPORT"
155
+ Request string `json:"request"` // "restore"
156
+ BlockHeight uint64 `json:"blockHeight,omitempty"` // empty if deferring blockHeight to the manifest
157
+ Args [1]swingStoreImportOptions `json:"args"`
158
+ }
159
+
160
+ // SwingStoreExportModeCurrent represents the minimal set of artifacts needed
161
+ // to operate a node.
162
+ const SwingStoreExportModeCurrent = "current"
163
+
164
+ // SwingStoreExportModeArchival represents the set of all artifacts needed to
165
+ // not lose any historical state.
166
+ const SwingStoreExportModeArchival = "archival"
167
+
168
+ // SwingStoreExportModeDebug represents the maximal set of artifacts available
169
+ // in the JS swing-store, including any kept around for debugging purposed only
170
+ // (like previous XS heap snapshots)
171
+ const SwingStoreExportModeDebug = "debug"
172
+
173
+ // SwingStoreExportOptions are configurable options provided to the JS swing-store export
174
+ type SwingStoreExportOptions struct {
175
+ // The export mode can be "current", "archival" or "debug" (SwingStoreExportMode* const)
176
+ // See packages/cosmic-swingset/src/export-kernel-db.js initiateSwingStoreExport and
177
+ // packages/swing-store/src/swingStore.js makeSwingStoreExporter
178
+ ExportMode string `json:"exportMode,omitempty"`
179
+ // A flag indicating whether "export data" should be part of the swing-store export
180
+ // If false, the resulting SwingStoreExportProvider's GetExportData will
181
+ // return an empty list of "export data" entries.
182
+ IncludeExportData bool `json:"includeExportData,omitempty"`
183
+ }
184
+
185
+ // SwingStoreRestoreOptions are configurable options provided to the JS swing-store import
186
+ type SwingStoreRestoreOptions struct {
187
+ // A flag indicating whether the swing-store import should attempt to load
188
+ // all historical artifacts available from the export provider
189
+ IncludeHistorical bool `json:"includeHistorical,omitempty"`
190
+ }
191
+
192
+ type swingStoreImportOptions struct {
193
+ // ExportDir is the directory created by RestoreExport that JS swing-store
194
+ // should import from.
195
+ ExportDir string `json:"exportDir"`
196
+ // IncludeHistorical is a copy of SwingStoreRestoreOptions.IncludeHistorical
197
+ IncludeHistorical bool `json:"includeHistorical,omitempty"`
198
+ }
199
+
200
+ var disallowedArtifactNameChar = regexp.MustCompile(`[^-_.a-zA-Z0-9]`)
201
+
202
+ // sanitizeArtifactName searches a string for all characters
203
+ // other than ASCII alphanumerics, hyphens, underscores, and dots,
204
+ // and replaces each of them with a hyphen.
205
+ func sanitizeArtifactName(name string) string {
206
+ return disallowedArtifactNameChar.ReplaceAllString(name, "-")
207
+ }
208
+
209
+ type operationDetails struct {
210
+ // isRestore indicates whether the operation in progress is a restore.
211
+ // It is assigned at creation and never mutated.
212
+ isRestore bool
213
+ // blockHeight is the block height of this in-progress operation.
214
+ // It is assigned at creation and never mutated.
215
+ blockHeight uint64
216
+ // logger is the destination for this operation's log messages.
217
+ // It is assigned at creation and never mutated.
218
+ logger log.Logger
219
+ // exportStartedResult is used to synchronize the commit boundary by the
220
+ // component performing the export operation to ensure export determinism.
221
+ // unused for restore operations
222
+ // It is assigned at creation and never mutated. The started goroutine
223
+ // writes into the channel and closes it. The main goroutine reads from the
224
+ // channel.
225
+ exportStartedResult chan error
226
+ // exportRetrieved is an internal flag indicating whether the JS generated
227
+ // export was retrieved. It can be false regardless of the component's
228
+ // eventHandler reporting an error or not. It is only indicative of whether
229
+ // the component called retrieveExport, and used to control whether to send
230
+ // a discard request if the JS side stayed responsible for the generated but
231
+ // un-retrieved export.
232
+ // It is only read or written by the export operation's goroutine.
233
+ exportRetrieved bool
234
+ // exportDone is a channel that is closed when the active export operation
235
+ // is complete.
236
+ // It is assigned at creation and never mutated. The started goroutine
237
+ // writes into the channel and closes it. The main goroutine reads from the
238
+ // channel.
239
+ exportDone chan error
240
+ }
241
+
242
+ // activeOperation is a global variable reflecting a swing-store import or
243
+ // export in progress on the JS side.
244
+ // This variable is only assigned to through calls of the public methods of
245
+ // SwingStoreExportsHandler, which rely on the exportDone channel getting
246
+ // closed to nil this variable.
247
+ // Only the calls to InitiateExport and RestoreExport set this to a non-nil
248
+ // value. The goroutine in which these calls occur is referred to as the
249
+ // "main goroutine". That goroutine may be different over time, but it's the
250
+ // caller's responsibility to ensure those goroutines do not overlap calls to
251
+ // the SwingStoreExportsHandler public methods.
252
+ // See also the details of each field for the conditions under which they are
253
+ // accessed.
254
+ var activeOperation *operationDetails
255
+
256
+ // WaitUntilSwingStoreExportStarted synchronizes with an export operation in
257
+ // progress, if any.
258
+ // The JS swing-store export must have started before a new block is committed
259
+ // to ensure the content of the export is the one expected. The app must call
260
+ // this method before sending a commit action to the JS controller.
261
+ //
262
+ // Waits for a just initiated export operation to have started in its goroutine.
263
+ // If no operation is in progress (InitiateExport hasn't been called or
264
+ // already completed), or if we previously checked if the operation had started,
265
+ // returns immediately.
266
+ //
267
+ // Must be called by the main goroutine
268
+ func WaitUntilSwingStoreExportStarted() error {
269
+ operationDetails := activeOperation
270
+ if operationDetails == nil {
271
+ return nil
272
+ }
273
+ // Block until the active operation has started, saving the result.
274
+ // The operation's goroutine only produces a value in case of an error,
275
+ // and closes the channel once the export has started or failed.
276
+ // Only the first call after an export was initiated will report an error.
277
+ startErr := <-operationDetails.exportStartedResult
278
+
279
+ // Check if the active export operation is done, and if so, nil it out so
280
+ // future calls are faster.
281
+ select {
282
+ case <-operationDetails.exportDone:
283
+ // If there was a start error, the channel is already closed at this point.
284
+ activeOperation = nil
285
+ default:
286
+ // don't wait for it to finish
287
+ // If there is no start error, the operation may take an arbitrary amount
288
+ // of time to terminate, likely spanning multiple blocks. However this
289
+ // function will only ever observe the expected activeOperation since the
290
+ // internal checkNotActive() called immediately on InitiateSnapshot will
291
+ // nil-out activeOperation if a stale value was sill sitting around.
292
+ }
293
+
294
+ return startErr
295
+ }
296
+
297
+ // WaitUntilSwingStoreExportDone synchronizes with the completion of an export
298
+ // operation in progress, if any.
299
+ // Only a single swing-store operation may execute at a time. Calling
300
+ // InitiateExport or RestoreExport will fail if a swing-store operation is
301
+ // already in progress. Furthermore, a component may need to know once an
302
+ // export it initiated has completed. Once this method call returns, the
303
+ // goroutine is guaranteed to have terminated, and the SwingStoreExportEventHandler
304
+ // provided to InitiateExport to no longer be in use.
305
+ //
306
+ // Reports any error that may have occurred from InitiateExport.
307
+ // If no export operation is in progress (InitiateExport hasn't been called or
308
+ // already completed), or if we previously checked if an export had completed,
309
+ // returns immediately.
310
+ //
311
+ // Must be called by the main goroutine
312
+ func WaitUntilSwingStoreExportDone() error {
313
+ operationDetails := activeOperation
314
+ if operationDetails == nil {
315
+ return nil
316
+ }
317
+ // Block until the active export has completed.
318
+ // The export operation's goroutine only produces a value in case of an error,
319
+ // and closes the channel once the export has completed or failed.
320
+ // Only the first call after an export was initiated will report an error.
321
+ exportErr := <-operationDetails.exportDone
322
+ activeOperation = nil
323
+
324
+ return exportErr
325
+ }
326
+
327
+ // checkNotActive returns an error if there is an active operation.
328
+ //
329
+ // Always internally called by the main goroutine
330
+ func checkNotActive() error {
331
+ operationDetails := activeOperation
332
+ if operationDetails != nil {
333
+ select {
334
+ case <-operationDetails.exportDone:
335
+ // nil-out any stale operation
336
+ activeOperation = nil
337
+ default:
338
+ if operationDetails.isRestore {
339
+ return fmt.Errorf("restore operation already in progress for height %d", operationDetails.blockHeight)
340
+ } else {
341
+ return fmt.Errorf("export operation already in progress for height %d", operationDetails.blockHeight)
342
+ }
343
+ }
344
+ }
345
+ return nil
346
+ }
347
+
348
+ // SwingStoreExportProvider gives access to a SwingStore "export data" and the
349
+ // related artifacts.
350
+ // A JS swing-store export is composed of optional "export data" (a set of
351
+ // key/value pairs), and opaque artifacts (a name and data as bytes) that
352
+ // complement the "export data".
353
+ // The abstraction is similar to the JS side swing-store export abstraction,
354
+ // but without the ability to list artifacts or random access them.
355
+ //
356
+ // A swing-store export for creating a state-sync snapshot will not contain any
357
+ // "export data" since this information is reflected every block into the
358
+ // verified cosmos DB.
359
+ // On state-sync snapshot restore, the swingset ExtensionSnapshotter will
360
+ // synthesize a provider for this module with "export data" sourced from the
361
+ // restored cosmos DB, and artifacts from the extension's payloads. When
362
+ // importing, the JS swing-store will verify that the artifacts match hashes
363
+ // contained in the trusted "export data".
364
+ type SwingStoreExportProvider struct {
365
+ // BlockHeight is the block height of the SwingStore export.
366
+ BlockHeight uint64
367
+ // GetExportData is a function to return the "export data" of the SwingStore export, if any.
368
+ GetExportData func() ([]*vstoragetypes.DataEntry, error)
369
+ // ReadArtifact is a function to return the next unread artifact in the SwingStore export.
370
+ // It errors with io.EOF upon reaching the end of the artifact list.
371
+ ReadArtifact func() (types.SwingStoreArtifact, error)
372
+ }
373
+
374
+ // SwingStoreExportEventHandler is used to handle events that occur while generating
375
+ // a swing-store export. It is provided to SwingStoreExportsHandler.InitiateExport.
376
+ type SwingStoreExportEventHandler interface {
377
+ // OnExportStarted is called by InitiateExport in a goroutine after the
378
+ // swing-store export has successfully started.
379
+ // This is where the component performing the export must initiate its own
380
+ // off main goroutine work, which results in retrieving and processing the
381
+ // swing-store export.
382
+ //
383
+ // Must call the retrieveExport function before returning, which will in turn
384
+ // synchronously invoke OnExportRetrieved once the swing-store export is ready.
385
+ OnExportStarted(blockHeight uint64, retrieveExport func() error) error
386
+ // OnExportRetrieved is called when the swing-store export has been retrieved,
387
+ // during the retrieveExport invocation.
388
+ // The provider is not a return value to retrieveExport in order to
389
+ // report errors in components that are unable to propagate errors back to the
390
+ // OnExportStarted result, like cosmos state-sync ExtensionSnapshotter.
391
+ // The implementation must synchronously consume the provider, which becomes
392
+ // invalid after the method returns.
393
+ OnExportRetrieved(provider SwingStoreExportProvider) error
394
+ }
395
+
396
+ // SwingStoreExportsHandler exclusively manages the communication with the JS side
397
+ // related to swing-store exports, ensuring insensitivity to sub-block timing,
398
+ // and enforcing concurrency requirements.
399
+ // The caller of this submodule must arrange block level commit synchronization,
400
+ // to ensure the results are deterministic.
401
+ //
402
+ // Some blockingSend calls performed by this submodule are non-deterministic.
403
+ // This submodule will send messages to JS from goroutines at unpredictable
404
+ // times, but this is safe because when handling the messages, the JS side
405
+ // does not perform operations affecting consensus and ignores state changes
406
+ // since committing the previous block.
407
+ // Some other blockingSend calls however do change the JS swing-store and
408
+ // must happen before the Swingset controller on the JS side was inited, in
409
+ // which case the mustNotBeInited parameter will be set to true.
410
+ type SwingStoreExportsHandler struct {
411
+ logger log.Logger
412
+ blockingSend func(action vm.Jsonable, mustNotBeInited bool) (string, error)
413
+ }
414
+
415
+ // NewSwingStoreExportsHandler creates a SwingStoreExportsHandler
416
+ func NewSwingStoreExportsHandler(logger log.Logger, blockingSend func(action vm.Jsonable, mustNotBeInited bool) (string, error)) *SwingStoreExportsHandler {
417
+ return &SwingStoreExportsHandler{
418
+ logger: logger.With("module", fmt.Sprintf("x/%s", types.ModuleName), "submodule", "SwingStoreExportsHandler"),
419
+ blockingSend: blockingSend,
420
+ }
421
+ }
422
+
423
+ // InitiateExport synchronously verifies that there is not already an export or
424
+ // import operation in progress and initiates a new export in a goroutine,
425
+ // via a dedicated SWING_STORE_EXPORT blockingSend action independent of other
426
+ // block related blockingSends, calling the given eventHandler when a related
427
+ // blockingSend completes. If the eventHandler doesn't retrieve the export,
428
+ // then it sends another blockingSend action to discard it.
429
+ //
430
+ // eventHandler is invoked solely from the spawned goroutine.
431
+ // The "started" and "done" events can be used for synchronization with an
432
+ // active operation taking place in the goroutine, by calling respectively the
433
+ // WaitUntilSwingStoreExportStarted and WaitUntilSwingStoreExportDone methods
434
+ // from the goroutine that initiated the export.
435
+ //
436
+ // Must be called by the main goroutine
437
+ func (exportsHandler SwingStoreExportsHandler) InitiateExport(blockHeight uint64, eventHandler SwingStoreExportEventHandler, exportOptions SwingStoreExportOptions) error {
438
+ err := checkNotActive()
439
+ if err != nil {
440
+ return err
441
+ }
442
+
443
+ var logger log.Logger
444
+ if blockHeight != 0 {
445
+ logger = exportsHandler.logger.With("height", blockHeight)
446
+ } else {
447
+ logger = exportsHandler.logger.With("height", "latest")
448
+ }
449
+
450
+ // Indicate that an export operation has been initiated by setting the global
451
+ // activeOperation var.
452
+ // This structure is used to synchronize with the goroutine spawned below.
453
+ operationDetails := &operationDetails{
454
+ blockHeight: blockHeight,
455
+ logger: logger,
456
+ exportStartedResult: make(chan error, 1),
457
+ exportRetrieved: false,
458
+ exportDone: make(chan error, 1),
459
+ }
460
+ activeOperation = operationDetails
461
+
462
+ go func() {
463
+ var err error
464
+ var startedErr error
465
+ defer func() {
466
+ if err == nil {
467
+ err = startedErr
468
+ }
469
+ if err != nil {
470
+ operationDetails.exportDone <- err
471
+ }
472
+ // First, indicate an export is no longer in progress. This ensures that
473
+ // for an operation with a start error, a call to WaitUntilSwingStoreExportStarted
474
+ // waiting on exportStartedResult will always find the operation has
475
+ // completed, and clear the active operation instead of racing if the
476
+ // channel close order was reversed.
477
+ close(operationDetails.exportDone)
478
+ // Then signal the current export operation that it failed to start,
479
+ // which will be reported to a waiting WaitUntilSwingStoreExportStarted,
480
+ // or the next call otherwise.
481
+ if startedErr != nil {
482
+ operationDetails.exportStartedResult <- startedErr
483
+ close(operationDetails.exportStartedResult)
484
+ }
485
+ }()
486
+
487
+ initiateAction := &swingStoreInitiateExportAction{
488
+ Type: swingStoreExportActionType,
489
+ BlockHeight: blockHeight,
490
+ Request: initiateRequest,
491
+ Args: [1]SwingStoreExportOptions{exportOptions},
492
+ }
493
+
494
+ // blockingSend for SWING_STORE_EXPORT action is safe to call from a goroutine
495
+ _, startedErr = exportsHandler.blockingSend(initiateAction, false)
496
+
497
+ if startedErr != nil {
498
+ logger.Error("failed to initiate swing-store export", "err", startedErr)
499
+ // The deferred function will communicate the error and close channels
500
+ // in the appropriate order.
501
+ return
502
+ }
503
+
504
+ // Signal that the export operation has started successfully in the goroutine.
505
+ // Calls to WaitUntilSwingStoreExportStarted will no longer block.
506
+ close(operationDetails.exportStartedResult)
507
+
508
+ // The user provided OnExportStarted function should call retrieveExport()
509
+ var retrieveErr error
510
+ err = eventHandler.OnExportStarted(blockHeight, func() error {
511
+ activeOperationDetails := activeOperation
512
+ if activeOperationDetails != operationDetails || operationDetails.exportRetrieved {
513
+ // shouldn't happen, but return an error if it does
514
+ return errors.New("export operation no longer active")
515
+ }
516
+
517
+ retrieveErr = exportsHandler.retrieveExport(eventHandler.OnExportRetrieved)
518
+
519
+ return retrieveErr
520
+ })
521
+
522
+ // Restore any retrieve error swallowed by OnExportStarted
523
+ if err == nil {
524
+ err = retrieveErr
525
+ }
526
+ if err != nil {
527
+ logger.Error("failed to process swing-store export", "err", err)
528
+ }
529
+
530
+ // Check whether the JS generated export was retrieved by eventHandler
531
+ if operationDetails.exportRetrieved {
532
+ return
533
+ }
534
+
535
+ // Discarding the export so invalidate retrieveExport
536
+ operationDetails.exportRetrieved = true
537
+
538
+ discardAction := &swingStoreDiscardExportAction{
539
+ Type: swingStoreExportActionType,
540
+ Request: discardRequest,
541
+ }
542
+ _, discardErr := exportsHandler.blockingSend(discardAction, false)
543
+
544
+ if discardErr != nil {
545
+ logger.Error("failed to discard swing-store export", "err", err)
546
+ }
547
+
548
+ if err == nil {
549
+ err = discardErr
550
+ } else if discardErr != nil {
551
+ // Safe to wrap error and use detailed error info since this error
552
+ // will not go back into swingset layers
553
+ err = sdkerrors.Wrapf(err, "failed to discard swing-store export after failing to process export: %+v", discardErr)
554
+ }
555
+ }()
556
+
557
+ return nil
558
+ }
559
+
560
+ // retrieveExport retrieves an initiated export then invokes onExportRetrieved
561
+ // with the retrieved export.
562
+ //
563
+ // It performs a SWING_STORE_EXPORT blockingSend which on success returns a
564
+ // string of the directory containing the JS swing-store export. It then reads
565
+ // the export manifest generated by the JS side, and synthesizes a
566
+ // SwingStoreExportProvider for the onExportRetrieved callback to access the
567
+ // retrieved swing-store export.
568
+ // The export manifest format is described by the exportManifest struct.
569
+ //
570
+ // After calling onExportRetrieved, the export directory and its contents are
571
+ // deleted.
572
+ //
573
+ // This will block until the export is ready. Internally invoked by the
574
+ // InitiateExport logic in the export operation's goroutine.
575
+ func (exportsHandler SwingStoreExportsHandler) retrieveExport(onExportRetrieved func(provider SwingStoreExportProvider) error) (err error) {
576
+ operationDetails := activeOperation
577
+ if operationDetails == nil {
578
+ // shouldn't happen, but return an error if it does
579
+ return errors.New("no active swing-store export operation")
580
+ }
581
+
582
+ blockHeight := operationDetails.blockHeight
583
+
584
+ action := &swingStoreRetrieveExportAction{
585
+ Type: swingStoreExportActionType,
586
+ Request: retrieveRequest,
587
+ }
588
+ out, err := exportsHandler.blockingSend(action, false)
589
+
590
+ if err != nil {
591
+ return err
592
+ }
593
+ operationDetails.exportRetrieved = true
594
+
595
+ var exportDir swingStoreRetrieveResult
596
+ err = json.Unmarshal([]byte(out), &exportDir)
597
+ if err != nil {
598
+ return err
599
+ }
600
+
601
+ defer os.RemoveAll(exportDir)
602
+
603
+ rawManifest, err := os.ReadFile(filepath.Join(exportDir, ExportManifestFilename))
604
+ if err != nil {
605
+ return err
606
+ }
607
+
608
+ var manifest exportManifest
609
+ err = json.Unmarshal(rawManifest, &manifest)
610
+ if err != nil {
611
+ return err
612
+ }
613
+
614
+ if blockHeight != 0 && manifest.BlockHeight != blockHeight {
615
+ return fmt.Errorf("export manifest blockHeight (%d) doesn't match (%d)", manifest.BlockHeight, blockHeight)
616
+ }
617
+
618
+ getExportData := func() ([]*vstoragetypes.DataEntry, error) {
619
+ entries := []*vstoragetypes.DataEntry{}
620
+ if manifest.Data == "" {
621
+ return entries, nil
622
+ }
623
+
624
+ dataFile, err := os.Open(filepath.Join(exportDir, manifest.Data))
625
+ if err != nil {
626
+ return nil, err
627
+ }
628
+ defer dataFile.Close()
629
+
630
+ decoder := json.NewDecoder(dataFile)
631
+ for {
632
+ var jsonEntry []string
633
+ err = decoder.Decode(&jsonEntry)
634
+ if err == io.EOF {
635
+ break
636
+ } else if err != nil {
637
+ return nil, err
638
+ }
639
+
640
+ if len(jsonEntry) != 2 {
641
+ return nil, fmt.Errorf("invalid export data entry (length %d)", len(jsonEntry))
642
+ }
643
+ entry := vstoragetypes.DataEntry{Path: jsonEntry[0], Value: jsonEntry[1]}
644
+ entries = append(entries, &entry)
645
+ }
646
+
647
+ return entries, nil
648
+ }
649
+
650
+ nextArtifact := 0
651
+
652
+ readArtifact := func() (artifact types.SwingStoreArtifact, err error) {
653
+ if nextArtifact == len(manifest.Artifacts) {
654
+ return artifact, io.EOF
655
+ } else if nextArtifact > len(manifest.Artifacts) {
656
+ return artifact, fmt.Errorf("exceeded expected artifact count: %d > %d", nextArtifact, len(manifest.Artifacts))
657
+ }
658
+
659
+ artifactEntry := manifest.Artifacts[nextArtifact]
660
+ nextArtifact++
661
+
662
+ artifactName := artifactEntry[0]
663
+ fileName := artifactEntry[1]
664
+ if artifactName == UntrustedExportDataArtifactName {
665
+ return artifact, fmt.Errorf("unexpected export artifact name %s", artifactName)
666
+ }
667
+ artifact.Name = artifactName
668
+ artifact.Data, err = os.ReadFile(filepath.Join(exportDir, fileName))
669
+
670
+ return artifact, err
671
+ }
672
+
673
+ err = onExportRetrieved(SwingStoreExportProvider{BlockHeight: manifest.BlockHeight, GetExportData: getExportData, ReadArtifact: readArtifact})
674
+ if err != nil {
675
+ return err
676
+ }
677
+
678
+ // if nextArtifact != len(manifest.Artifacts) {
679
+ // return errors.New("not all export artifacts were retrieved")
680
+ // }
681
+
682
+ operationDetails.logger.Info("retrieved swing-store export", "exportDir", exportDir)
683
+
684
+ return nil
685
+ }
686
+
687
+ // RestoreExport restores the JS swing-store using previously exported data and artifacts.
688
+ //
689
+ // Must be called by the main goroutine
690
+ func (exportsHandler SwingStoreExportsHandler) RestoreExport(provider SwingStoreExportProvider, restoreOptions SwingStoreRestoreOptions) error {
691
+ err := checkNotActive()
692
+ if err != nil {
693
+ return err
694
+ }
695
+
696
+ blockHeight := provider.BlockHeight
697
+
698
+ // We technically don't need to create an active operation here since both
699
+ // InitiateExport and RestoreExport should only be called from the main
700
+ // goroutine, but it doesn't cost much to add in case things go wrong.
701
+ operationDetails := &operationDetails{
702
+ isRestore: true,
703
+ blockHeight: blockHeight,
704
+ logger: exportsHandler.logger,
705
+ // goroutine synchronization is unnecessary since anything checking should
706
+ // be called from the same goroutine.
707
+ // Effectively WaitUntilSwingStoreExportStarted would block infinitely and
708
+ // exportsHandler.InitiateExport will error when calling checkNotActive.
709
+ exportStartedResult: nil,
710
+ exportDone: nil,
711
+ }
712
+ activeOperation = operationDetails
713
+ defer func() {
714
+ activeOperation = nil
715
+ }()
716
+
717
+ exportDir, err := os.MkdirTemp("", fmt.Sprintf("agd-swing-store-restore-%d-*", blockHeight))
718
+ if err != nil {
719
+ return err
720
+ }
721
+ defer os.RemoveAll(exportDir)
722
+
723
+ manifest := exportManifest{
724
+ BlockHeight: blockHeight,
725
+ }
726
+
727
+ exportDataEntries, err := provider.GetExportData()
728
+ if err != nil {
729
+ return err
730
+ }
731
+
732
+ if len(exportDataEntries) > 0 {
733
+ manifest.Data = exportDataFilename
734
+ exportDataFile, err := os.OpenFile(filepath.Join(exportDir, exportDataFilename), os.O_CREATE|os.O_WRONLY, exportedFilesMode)
735
+ if err != nil {
736
+ return err
737
+ }
738
+ defer exportDataFile.Close()
739
+
740
+ encoder := json.NewEncoder(exportDataFile)
741
+ encoder.SetEscapeHTML(false)
742
+ for _, dataEntry := range exportDataEntries {
743
+ entry := []string{dataEntry.Path, dataEntry.Value}
744
+ err := encoder.Encode(entry)
745
+ if err != nil {
746
+ return err
747
+ }
748
+ }
749
+
750
+ err = exportDataFile.Sync()
751
+ if err != nil {
752
+ return err
753
+ }
754
+ }
755
+
756
+ writeExportFile := func(filename string, data []byte) error {
757
+ return os.WriteFile(filepath.Join(exportDir, filename), data, exportedFilesMode)
758
+ }
759
+
760
+ for {
761
+ artifact, err := provider.ReadArtifact()
762
+ if err == io.EOF {
763
+ break
764
+ } else if err != nil {
765
+ return err
766
+ }
767
+
768
+ if artifact.Name != UntrustedExportDataArtifactName {
769
+ // An artifact is only verifiable by the JS swing-store import using the
770
+ // information contained in the "export data".
771
+ // Since we cannot trust the source of the artifact at this point,
772
+ // including that the artifact's name is genuine, we generate a safe and
773
+ // unique filename from the artifact's name we received, by substituting
774
+ // any non letters-digits-hyphen-underscore-dot by a hyphen, and
775
+ // prefixing with an incremented id.
776
+ // The filename is not used for any purpose in the import logic.
777
+ filename := sanitizeArtifactName(artifact.Name)
778
+ filename = fmt.Sprintf("%d-%s", len(manifest.Artifacts), filename)
779
+ manifest.Artifacts = append(manifest.Artifacts, [2]string{artifact.Name, filename})
780
+ err = writeExportFile(filename, artifact.Data)
781
+ } else {
782
+ // Pseudo artifact containing untrusted export data which may have been
783
+ // saved separately for debugging purposes (not referenced from the manifest)
784
+ err = writeExportFile(untrustedExportDataFilename, artifact.Data)
785
+ }
786
+ if err != nil {
787
+ return err
788
+ }
789
+ }
790
+
791
+ manifestBytes, err := json.MarshalIndent(manifest, "", " ")
792
+ if err != nil {
793
+ return err
794
+ }
795
+ err = writeExportFile(ExportManifestFilename, manifestBytes)
796
+ if err != nil {
797
+ return err
798
+ }
799
+
800
+ action := &swingStoreRestoreExportAction{
801
+ Type: swingStoreExportActionType,
802
+ BlockHeight: blockHeight,
803
+ Request: restoreRequest,
804
+ Args: [1]swingStoreImportOptions{{
805
+ ExportDir: exportDir,
806
+ IncludeHistorical: restoreOptions.IncludeHistorical,
807
+ }},
808
+ }
809
+
810
+ _, err = exportsHandler.blockingSend(action, true)
811
+ if err != nil {
812
+ return err
813
+ }
814
+
815
+ exportsHandler.logger.Info("restored swing-store export", "exportDir", exportDir, "height", blockHeight)
816
+
817
+ return nil
818
+ }