@agoric/cosmos 0.34.2-dev-5513dea.0 → 0.34.2-dev-6f05870.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.
@@ -1,528 +0,0 @@
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
- "github.com/cosmos/cosmos-sdk/baseapp"
16
- snapshots "github.com/cosmos/cosmos-sdk/snapshots/types"
17
- sdk "github.com/cosmos/cosmos-sdk/types"
18
- "github.com/tendermint/tendermint/libs/log"
19
- tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
20
- )
21
-
22
- var _ snapshots.ExtensionSnapshotter = &SwingsetSnapshotter{}
23
-
24
- // SnapshotFormat 1 is a proto message containing an artifact name, and the binary artifact data
25
- const SnapshotFormat = 1
26
-
27
- // The manifest filename must be synchronized with the JS export/import tooling
28
- const ExportManifestFilename = "export-manifest.json"
29
- const ExportDataFilename = "export-data.jsonl"
30
- const UntrustedExportDataArtifactName = "UNTRUSTED-EXPORT-DATA"
31
- const UntrustedExportDataFilename = "untrusted-export-data.jsonl"
32
- const ExportedFilesMode = 0644
33
-
34
- var disallowedArtifactNameChar = regexp.MustCompile(`[^-_.a-zA-Z0-9]`)
35
-
36
- // sanitizeArtifactName searches a string for all characters
37
- // other than ASCII alphanumerics, hyphens, underscores, and dots,
38
- // and replaces each of them with a hyphen.
39
- func sanitizeArtifactName(name string) string {
40
- return disallowedArtifactNameChar.ReplaceAllString(name, "-")
41
- }
42
-
43
- type activeSnapshot struct {
44
- // Whether the operation in progress is a restore
45
- isRestore bool
46
- // The block height of the snapshot in progress
47
- height int64
48
- // The logger for this snapshot
49
- logger log.Logger
50
- // Use to synchronize the commit boundary
51
- startedResult chan error
52
- // Internal flag indicating whether the cosmos driven snapshot process completed
53
- // Only read or written by the snapshot worker goroutine.
54
- retrieved bool
55
- // Closed when this snapshot is complete
56
- done chan struct{}
57
- }
58
-
59
- type exportManifest struct {
60
- BlockHeight uint64 `json:"blockHeight,omitempty"`
61
- // The filename of the export data
62
- Data string `json:"data,omitempty"`
63
- // The list of artifact names and their corresponding filenames
64
- Artifacts [][2]string `json:"artifacts"`
65
- }
66
-
67
- type SwingsetSnapshotter struct {
68
- isConfigured func() bool
69
- takeSnapshot func(height int64)
70
- newRestoreContext func(height int64) sdk.Context
71
- logger log.Logger
72
- getSwingStoreExportData func(ctx sdk.Context) []*vstoragetypes.DataEntry
73
- blockingSend func(action vm.Jsonable, mustNotBeInited bool) (string, error)
74
- // Only modified by the main goroutine.
75
- activeSnapshot *activeSnapshot
76
- }
77
-
78
- type snapshotAction struct {
79
- Type string `json:"type"` // COSMOS_SNAPSHOT
80
- BlockHeight int64 `json:"blockHeight"`
81
- Request string `json:"request"` // "initiate", "discard", "retrieve", or "restore"
82
- Args []json.RawMessage `json:"args,omitempty"`
83
- }
84
-
85
- // NewSwingsetSnapshotter creates a SwingsetSnapshotter which exclusively
86
- // manages communication with the JS side for Swingset snapshots, ensuring
87
- // insensitivity to sub-block timing, and enforcing concurrency requirements.
88
- // The caller of this submodule must arrange block level commit synchronization,
89
- // to ensure the results are deterministic.
90
- //
91
- // Some `blockingSend` calls performed by this submodule are non-deterministic.
92
- // This submodule will send messages to JS from goroutines at unpredictable
93
- // times, but this is safe because when handling the messages, the JS side
94
- // does not perform operations affecting consensus and ignores state changes
95
- // since committing the previous block.
96
- // Some other `blockingSend` calls however do change the JS swing-store and
97
- // must happen before the Swingset controller on the JS side was inited.
98
- func NewSwingsetSnapshotter(
99
- app *baseapp.BaseApp,
100
- getSwingStoreExportData func(ctx sdk.Context) []*vstoragetypes.DataEntry,
101
- blockingSend func(action vm.Jsonable, mustNotBeInited bool) (string, error),
102
- ) SwingsetSnapshotter {
103
- return SwingsetSnapshotter{
104
- isConfigured: func() bool { return app.SnapshotManager() != nil },
105
- takeSnapshot: app.Snapshot,
106
- newRestoreContext: func(height int64) sdk.Context {
107
- return app.NewUncachedContext(false, tmproto.Header{Height: height})
108
- },
109
- logger: app.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName), "submodule", "snapshotter"),
110
- getSwingStoreExportData: getSwingStoreExportData,
111
- blockingSend: blockingSend,
112
- activeSnapshot: nil,
113
- }
114
- }
115
-
116
- // checkNotActive returns an error if there is an active snapshot.
117
- func (snapshotter *SwingsetSnapshotter) checkNotActive() error {
118
- active := snapshotter.activeSnapshot
119
- if active != nil {
120
- select {
121
- case <-active.done:
122
- snapshotter.activeSnapshot = nil
123
- default:
124
- if active.isRestore {
125
- return fmt.Errorf("snapshot restore already in progress for height %d", active.height)
126
- } else {
127
- return fmt.Errorf("snapshot already in progress for height %d", active.height)
128
- }
129
- }
130
- }
131
- return nil
132
- }
133
-
134
- // InitiateSnapshot synchronously initiates a snapshot for the given height.
135
- // If a snapshot is already in progress, or if no snapshot manager is configured,
136
- // this will fail.
137
- // The snapshot operation is performed in a goroutine, and synchronized with the
138
- // main thread through the `WaitUntilSnapshotStarted` method.
139
- func (snapshotter *SwingsetSnapshotter) InitiateSnapshot(height int64) error {
140
- err := snapshotter.checkNotActive()
141
- if err != nil {
142
- return err
143
- }
144
-
145
- if !snapshotter.isConfigured() {
146
- return fmt.Errorf("snapshot manager not configured")
147
- }
148
-
149
- logger := snapshotter.logger.With("height", height)
150
-
151
- // Indicate that a snapshot has been initiated by setting `activeSnapshot`.
152
- // This structure is used to synchronize with the goroutine spawned below.
153
- // It's nilled-out before exiting (and is the only code that does so).
154
- active := &activeSnapshot{
155
- height: height,
156
- logger: logger,
157
- startedResult: make(chan error, 1),
158
- retrieved: false,
159
- done: make(chan struct{}),
160
- }
161
- snapshotter.activeSnapshot = active
162
-
163
- go func() {
164
- defer close(active.done)
165
-
166
- action := &snapshotAction{
167
- Type: "COSMOS_SNAPSHOT",
168
- BlockHeight: height,
169
- Request: "initiate",
170
- }
171
-
172
- // blockingSend for COSMOS_SNAPSHOT action is safe to call from a goroutine
173
- _, err := snapshotter.blockingSend(action, false)
174
-
175
- if err != nil {
176
- // First indicate a snapshot is no longer in progress if the call to
177
- // `WaitUntilSnapshotStarted` has't happened yet.
178
- // Then signal the current snapshot operation if a call to
179
- // `WaitUntilSnapshotStarted` was already waiting.
180
- active.startedResult <- err
181
- close(active.startedResult)
182
- logger.Error("failed to initiate swingset snapshot", "err", err)
183
- return
184
- }
185
-
186
- // Signal that the snapshot operation has started in the goroutine. Calls to
187
- // `WaitUntilSnapshotStarted` will no longer block.
188
- close(active.startedResult)
189
-
190
- // In production this should indirectly call SnapshotExtension().
191
- snapshotter.takeSnapshot(height)
192
-
193
- // Check whether the cosmos Snapshot() method successfully handled our extension
194
- if active.retrieved {
195
- return
196
- }
197
-
198
- logger.Error("failed to make swingset snapshot")
199
- action = &snapshotAction{
200
- Type: "COSMOS_SNAPSHOT",
201
- BlockHeight: height,
202
- Request: "discard",
203
- }
204
- _, err = snapshotter.blockingSend(action, false)
205
-
206
- if err != nil {
207
- logger.Error("failed to discard swingset snapshot", "err", err)
208
- }
209
- }()
210
-
211
- return nil
212
- }
213
-
214
- // WaitUntilSnapshotStarted synchronizes with a snapshot in progress, if any.
215
- // The JS SwingStore export must have started before a new block is committed.
216
- // The app must call this method before sending a commit action to SwingSet.
217
- //
218
- // Waits for a just initiated snapshot to have started in its goroutine.
219
- // If no snapshot is in progress (`InitiateSnapshot` hasn't been called or
220
- // already completed), or if we previously checked if the snapshot had started,
221
- // returns immediately.
222
- func (snapshotter *SwingsetSnapshotter) WaitUntilSnapshotStarted() error {
223
- activeSnapshot := snapshotter.activeSnapshot
224
- if activeSnapshot == nil {
225
- return nil
226
- }
227
- // Block until the active snapshot has started, saving the result.
228
- // The snapshot goroutine only produces a value in case of an error,
229
- // and closes the channel once the snapshot has started or failed.
230
- // Only the first call after a snapshot was initiated will report an error.
231
- startErr := <-activeSnapshot.startedResult
232
-
233
- // Check if the active snapshot is done, and if so, nil it out so future
234
- // calls are faster.
235
- select {
236
- case <-activeSnapshot.done:
237
- snapshotter.activeSnapshot = nil
238
- default:
239
- // don't wait for it to finish
240
- }
241
-
242
- return startErr
243
- }
244
-
245
- // SnapshotName returns the name of snapshotter, it should be unique in the manager.
246
- // Implements ExtensionSnapshotter
247
- func (snapshotter *SwingsetSnapshotter) SnapshotName() string {
248
- return types.ModuleName
249
- }
250
-
251
- // SnapshotFormat returns the default format the extension snapshotter uses to encode the
252
- // payloads when taking a snapshot.
253
- // It's defined within the extension, different from the global format for the whole state-sync snapshot.
254
- // Implements ExtensionSnapshotter
255
- func (snapshotter *SwingsetSnapshotter) SnapshotFormat() uint32 {
256
- return SnapshotFormat
257
- }
258
-
259
- // SupportedFormats returns a list of formats it can restore from.
260
- // Implements ExtensionSnapshotter
261
- func (snapshotter *SwingsetSnapshotter) SupportedFormats() []uint32 {
262
- return []uint32{SnapshotFormat}
263
- }
264
-
265
- // SnapshotExtension writes extension payloads into the underlying protobuf stream.
266
- // This operation is invoked by the snapshot manager in the goroutine started by
267
- // `InitiateSnapshot`.
268
- // Implements ExtensionSnapshotter
269
- func (snapshotter *SwingsetSnapshotter) SnapshotExtension(height uint64, payloadWriter snapshots.ExtensionPayloadWriter) (err error) {
270
- defer func() {
271
- // Since the cosmos layers do a poor job of reporting errors, do our own reporting
272
- // `err` will be set correctly regardless if it was explicitly assigned or
273
- // a value was provided to a `return` statement.
274
- // See https://go.dev/blog/defer-panic-and-recover for details
275
- if err != nil {
276
- var logger log.Logger
277
- if snapshotter.activeSnapshot != nil {
278
- logger = snapshotter.activeSnapshot.logger
279
- } else {
280
- logger = snapshotter.logger
281
- }
282
-
283
- logger.Error("swingset snapshot extension failed", "err", err)
284
- }
285
- }()
286
-
287
- activeSnapshot := snapshotter.activeSnapshot
288
- if activeSnapshot == nil {
289
- // shouldn't happen, but return an error if it does
290
- return errors.New("no active swingset snapshot")
291
- }
292
-
293
- if activeSnapshot.height != int64(height) {
294
- return fmt.Errorf("swingset snapshot requested for unexpected height %d (expected %d)", height, activeSnapshot.height)
295
- }
296
-
297
- action := &snapshotAction{
298
- Type: "COSMOS_SNAPSHOT",
299
- BlockHeight: activeSnapshot.height,
300
- Request: "retrieve",
301
- }
302
- out, err := snapshotter.blockingSend(action, false)
303
-
304
- if err != nil {
305
- return err
306
- }
307
-
308
- var exportDir string
309
- err = json.Unmarshal([]byte(out), &exportDir)
310
- if err != nil {
311
- return err
312
- }
313
-
314
- defer os.RemoveAll(exportDir)
315
-
316
- rawManifest, err := os.ReadFile(filepath.Join(exportDir, ExportManifestFilename))
317
- if err != nil {
318
- return err
319
- }
320
-
321
- var manifest exportManifest
322
- err = json.Unmarshal(rawManifest, &manifest)
323
- if err != nil {
324
- return err
325
- }
326
-
327
- if manifest.BlockHeight != height {
328
- return fmt.Errorf("snapshot manifest blockHeight (%d) doesn't match (%d)", manifest.BlockHeight, height)
329
- }
330
-
331
- writeFileToPayload := func(fileName string, artifactName string) error {
332
- payload := types.ExtensionSnapshotterArtifactPayload{Name: artifactName}
333
-
334
- payload.Data, err = os.ReadFile(filepath.Join(exportDir, fileName))
335
- if err != nil {
336
- return err
337
- }
338
-
339
- payloadBytes, err := payload.Marshal()
340
- if err != nil {
341
- return err
342
- }
343
-
344
- err = payloadWriter(payloadBytes)
345
- if err != nil {
346
- return err
347
- }
348
-
349
- return nil
350
- }
351
-
352
- if manifest.Data != "" {
353
- err = writeFileToPayload(manifest.Data, UntrustedExportDataArtifactName)
354
- if err != nil {
355
- return err
356
- }
357
- }
358
-
359
- for _, artifactInfo := range manifest.Artifacts {
360
- artifactName := artifactInfo[0]
361
- fileName := artifactInfo[1]
362
- if artifactName == UntrustedExportDataArtifactName {
363
- return fmt.Errorf("unexpected artifact name %s", artifactName)
364
- }
365
- err = writeFileToPayload(fileName, artifactName)
366
- if err != nil {
367
- return err
368
- }
369
- }
370
-
371
- activeSnapshot.retrieved = true
372
- activeSnapshot.logger.Info("retrieved snapshot", "exportDir", exportDir)
373
-
374
- return nil
375
- }
376
-
377
- // RestoreExtension restores an extension state snapshot,
378
- // the payload reader returns `io.EOF` when it reaches the extension boundaries.
379
- // Implements ExtensionSnapshotter
380
- func (snapshotter *SwingsetSnapshotter) RestoreExtension(height uint64, format uint32, payloadReader snapshots.ExtensionPayloadReader) error {
381
- if format != SnapshotFormat {
382
- return snapshots.ErrUnknownFormat
383
- }
384
-
385
- err := snapshotter.checkNotActive()
386
- if err != nil {
387
- return err
388
- }
389
-
390
- // We technically don't need to create an active snapshot here since both
391
- // `InitiateSnapshot` and `RestoreExtension` should only be called from the
392
- // main thread, but it doesn't cost much to add in case things go wrong.
393
- active := &activeSnapshot{
394
- isRestore: true,
395
- height: int64(height),
396
- logger: snapshotter.logger,
397
- // goroutine synchronization is unnecessary since anything checking should
398
- // be called from the same thread.
399
- // Effectively `WaitUntilSnapshotStarted` would block infinitely and
400
- // and `InitiateSnapshot` will error when calling `checkNotActive`.
401
- startedResult: nil,
402
- done: nil,
403
- }
404
- snapshotter.activeSnapshot = active
405
- defer func() {
406
- snapshotter.activeSnapshot = nil
407
- }()
408
-
409
- ctx := snapshotter.newRestoreContext(int64(height))
410
-
411
- exportDir, err := os.MkdirTemp("", fmt.Sprintf("agd-state-sync-restore-%d-*", height))
412
- if err != nil {
413
- return err
414
- }
415
- defer os.RemoveAll(exportDir)
416
-
417
- manifest := exportManifest{
418
- BlockHeight: height,
419
- Data: ExportDataFilename,
420
- }
421
-
422
- exportDataFile, err := os.OpenFile(filepath.Join(exportDir, ExportDataFilename), os.O_CREATE|os.O_WRONLY, ExportedFilesMode)
423
- if err != nil {
424
- return err
425
- }
426
- defer exportDataFile.Close()
427
-
428
- // Retrieve the SwingStore "ExportData" from the verified vstorage data.
429
- // At this point the content of the cosmos DB has been verified against the
430
- // AppHash, which means the SwingStore data it contains can be used as the
431
- // trusted root against which to validate the artifacts.
432
- swingStoreEntries := snapshotter.getSwingStoreExportData(ctx)
433
-
434
- if len(swingStoreEntries) > 0 {
435
- encoder := json.NewEncoder(exportDataFile)
436
- encoder.SetEscapeHTML(false)
437
- for _, dataEntry := range swingStoreEntries {
438
- entry := []string{dataEntry.Path, dataEntry.Value}
439
- err := encoder.Encode(entry)
440
- if err != nil {
441
- return err
442
- }
443
- }
444
- }
445
-
446
- writeExportFile := func(filename string, data []byte) error {
447
- return os.WriteFile(filepath.Join(exportDir, filename), data, ExportedFilesMode)
448
- }
449
-
450
- for {
451
- payloadBytes, err := payloadReader()
452
- if err == io.EOF {
453
- break
454
- } else if err != nil {
455
- return err
456
- }
457
-
458
- payload := types.ExtensionSnapshotterArtifactPayload{}
459
- if err = payload.Unmarshal(payloadBytes); err != nil {
460
- return err
461
- }
462
-
463
- switch {
464
- case payload.Name != UntrustedExportDataArtifactName:
465
- // Artifact verifiable on import from the export data
466
- // Since we cannot trust the state-sync payload at this point, we generate
467
- // a safe and unique filename from the artifact name we received, by
468
- // substituting any non letters-digits-hyphen-underscore-dot by a hyphen,
469
- // and prefixing with an incremented id.
470
- // The filename is not used for any purpose in the snapshotting logic.
471
- filename := sanitizeArtifactName(payload.Name)
472
- filename = fmt.Sprintf("%d-%s", len(manifest.Artifacts), filename)
473
- manifest.Artifacts = append(manifest.Artifacts, [2]string{payload.Name, filename})
474
- err = writeExportFile(filename, payload.Data)
475
-
476
- case len(swingStoreEntries) > 0:
477
- // Pseudo artifact containing untrusted export data which may have been
478
- // saved separately for debugging purposes (not referenced from the manifest)
479
- err = writeExportFile(UntrustedExportDataFilename, payload.Data)
480
-
481
- default:
482
- // There is no trusted export data
483
- err = errors.New("cannot restore from untrusted export data")
484
- // snapshotter.logger.Info("using untrusted export data for swingstore restore")
485
- // _, err = exportDataFile.Write(payload.Data)
486
- }
487
-
488
- if err != nil {
489
- return err
490
- }
491
- }
492
-
493
- err = exportDataFile.Sync()
494
- if err != nil {
495
- return err
496
- }
497
- exportDataFile.Close()
498
-
499
- manifestBytes, err := json.MarshalIndent(manifest, "", " ")
500
- if err != nil {
501
- return err
502
- }
503
- err = writeExportFile(ExportManifestFilename, manifestBytes)
504
- if err != nil {
505
- return err
506
- }
507
-
508
- encodedExportDir, err := json.Marshal(exportDir)
509
- if err != nil {
510
- return err
511
- }
512
-
513
- action := &snapshotAction{
514
- Type: "COSMOS_SNAPSHOT",
515
- BlockHeight: int64(height),
516
- Request: "restore",
517
- Args: []json.RawMessage{encodedExportDir},
518
- }
519
-
520
- _, err = snapshotter.blockingSend(action, true)
521
- if err != nil {
522
- return err
523
- }
524
-
525
- snapshotter.logger.Info("restored snapshot", "exportDir", exportDir, "height", height)
526
-
527
- return nil
528
- }