@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.
- package/app/app.go +16 -11
- package/git-revision.txt +1 -1
- package/package.json +2 -2
- package/proto/agoric/swingset/swingset.proto +5 -2
- package/x/swingset/alias.go +7 -6
- package/x/swingset/keeper/extension_snapshotter.go +321 -0
- package/x/swingset/keeper/extension_snapshotter_test.go +106 -0
- package/x/swingset/keeper/swing_store_exports_handler.go +818 -0
- package/x/swingset/keeper/swing_store_exports_handler_test.go +247 -0
- package/x/swingset/types/swingset.pb.go +82 -80
- package/x/swingset/keeper/snapshotter.go +0 -528
- package/x/swingset/keeper/snapshotter_test.go +0 -195
|
@@ -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
|
+
}
|