@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.
package/app/app.go CHANGED
@@ -233,12 +233,13 @@ type GaiaApp struct { // nolint: golint
233
233
  FeeGrantKeeper feegrantkeeper.Keeper
234
234
  AuthzKeeper authzkeeper.Keeper
235
235
 
236
- SwingSetKeeper swingset.Keeper
237
- SwingSetSnapshotter swingset.Snapshotter
238
- VstorageKeeper vstorage.Keeper
239
- VibcKeeper vibc.Keeper
240
- VbankKeeper vbank.Keeper
241
- LienKeeper lien.Keeper
236
+ SwingStoreExportsHandler swingset.SwingStoreExportsHandler
237
+ SwingSetSnapshotter swingset.ExtensionSnapshotter
238
+ SwingSetKeeper swingset.Keeper
239
+ VstorageKeeper vstorage.Keeper
240
+ VibcKeeper vibc.Keeper
241
+ VbankKeeper vbank.Keeper
242
+ LienKeeper lien.Keeper
242
243
 
243
244
  // make scoped keepers public for test purposes
244
245
  ScopedIBCKeeper capabilitykeeper.ScopedKeeper
@@ -457,9 +458,8 @@ func NewAgoricApp(
457
458
  callToController,
458
459
  )
459
460
 
460
- app.SwingSetSnapshotter = swingsetkeeper.NewSwingsetSnapshotter(
461
- bApp,
462
- app.SwingSetKeeper.ExportSwingStore,
461
+ app.SwingStoreExportsHandler = *swingsetkeeper.NewSwingStoreExportsHandler(
462
+ app.Logger(),
463
463
  func(action vm.Jsonable, mustNotBeInited bool) (string, error) {
464
464
  if mustNotBeInited {
465
465
  app.CheckControllerInited(false)
@@ -472,6 +472,11 @@ func NewAgoricApp(
472
472
  return sendToController(true, string(bz))
473
473
  },
474
474
  )
475
+ app.SwingSetSnapshotter = *swingsetkeeper.NewExtensionSnapshotter(
476
+ bApp,
477
+ &app.SwingStoreExportsHandler,
478
+ app.SwingSetKeeper.ExportSwingStore,
479
+ )
475
480
 
476
481
  app.VibcKeeper = vibc.NewKeeper(
477
482
  appCodec, keys[vibc.StoreKey],
@@ -954,10 +959,10 @@ func (app *GaiaApp) InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci
954
959
 
955
960
  // Commit tells the controller that the block is commited
956
961
  func (app *GaiaApp) Commit() abci.ResponseCommit {
957
- err := app.SwingSetSnapshotter.WaitUntilSnapshotStarted()
962
+ err := swingsetkeeper.WaitUntilSwingStoreExportStarted()
958
963
 
959
964
  if err != nil {
960
- app.Logger().Error("swingset snapshot failed to start", "err", err)
965
+ app.Logger().Error("swing-store export failed to start", "err", err)
961
966
  }
962
967
 
963
968
  // Frontrun the BaseApp's Commit method
package/git-revision.txt CHANGED
@@ -1 +1 @@
1
- 5513dea
1
+ 6f05870
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agoric/cosmos",
3
- "version": "0.34.2-dev-5513dea.0+5513dea",
3
+ "version": "0.34.2-dev-6f05870.0+6f05870",
4
4
  "description": "Connect JS to the Cosmos blockchain SDK",
5
5
  "parsers": {
6
6
  "js": "mjs"
@@ -35,5 +35,5 @@
35
35
  "publishConfig": {
36
36
  "access": "public"
37
37
  },
38
- "gitHead": "5513deae3726540e422462d245cffc4bc44894e5"
38
+ "gitHead": "6f05870eef00470eb99022513e2df5ff62e81785"
39
39
  }
@@ -150,8 +150,11 @@ message Egress {
150
150
  ];
151
151
  }
152
152
 
153
- // The payload messages used by swingset state-sync
154
- message ExtensionSnapshotterArtifactPayload {
153
+ // SwingStoreArtifact encodes an artifact of a swing-store export.
154
+ // Artifacts may be stored or transmitted in any order. Most handlers do
155
+ // maintain the artifact order from their original source as an effect of how
156
+ // they handle the artifacts.
157
+ message SwingStoreArtifact {
155
158
  option (gogoproto.equal) = false;
156
159
  string name = 1 [
157
160
  (gogoproto.jsontag) = "name",
@@ -21,10 +21,11 @@ var (
21
21
  )
22
22
 
23
23
  type (
24
- Keeper = keeper.Keeper
25
- Snapshotter = keeper.SwingsetSnapshotter
26
- Egress = types.Egress
27
- MsgDeliverInbound = types.MsgDeliverInbound
28
- MsgProvision = types.MsgProvision
29
- Params = types.Params
24
+ Keeper = keeper.Keeper
25
+ SwingStoreExportsHandler = keeper.SwingStoreExportsHandler
26
+ ExtensionSnapshotter = keeper.ExtensionSnapshotter
27
+ Egress = types.Egress
28
+ MsgDeliverInbound = types.MsgDeliverInbound
29
+ MsgProvision = types.MsgProvision
30
+ Params = types.Params
30
31
  )
@@ -0,0 +1,321 @@
1
+ package keeper
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "io"
9
+ "math"
10
+
11
+ "github.com/Agoric/agoric-sdk/golang/cosmos/x/swingset/types"
12
+ vstoragetypes "github.com/Agoric/agoric-sdk/golang/cosmos/x/vstorage/types"
13
+ "github.com/cosmos/cosmos-sdk/baseapp"
14
+ snapshots "github.com/cosmos/cosmos-sdk/snapshots/types"
15
+ sdk "github.com/cosmos/cosmos-sdk/types"
16
+ "github.com/tendermint/tendermint/libs/log"
17
+ tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
18
+ )
19
+
20
+ // This module implements a Cosmos ExtensionSnapshotter to capture and restore
21
+ // state-sync Swingset state that is not part of the Cosmos DB.
22
+ // See docs/architecture/state-sync.md for a sequence diagram of how this
23
+ // module fits within the state-sync process.
24
+
25
+ var _ snapshots.ExtensionSnapshotter = &ExtensionSnapshotter{}
26
+ var _ SwingStoreExportEventHandler = &ExtensionSnapshotter{}
27
+
28
+ // SnapshotFormat 1 defines all extension payloads to be SwingStoreArtifact proto messages
29
+ const SnapshotFormat = 1
30
+
31
+ // snapshotDetails describes an in-progress state-sync snapshot
32
+ type snapshotDetails struct {
33
+ // blockHeight is the block height of this in-progress snapshot.
34
+ blockHeight uint64
35
+ // logger is the destination for this snapshot's log messages.
36
+ logger log.Logger
37
+ // retrieveExport is the callback provided by the SwingStoreExportsHandler to
38
+ // retrieve the SwingStore's export provider which allows to read the export's
39
+ // artifacts used to populate this state-sync extension's payloads.
40
+ retrieveExport func() error
41
+ // payloadWriter is the callback provided by the state-sync snapshot manager
42
+ // for an extension to write a payload into the under-construction snapshot
43
+ // stream. It may be called multiple times, and often is (currently once per
44
+ // SwingStore export artifact).
45
+ payloadWriter snapshots.ExtensionPayloadWriter
46
+ }
47
+
48
+ // ExtensionSnapshotter is the cosmos state-sync extension snapshotter for the
49
+ // x/swingset module.
50
+ // It handles the SwingSet state that is not part of the Cosmos DB. Currently
51
+ // that state is solely composed of the SwingStore artifacts, as a copy of the
52
+ // SwingStore "export data" is streamed into the cosmos DB during execution.
53
+ // When performing a snapshot, the extension leverages the SwingStoreExportsHandler
54
+ // to retrieve the needed SwingStore artifacts. When restoring a snapshot,
55
+ // the extension combines the artifacts from the state-sync snapshot with the
56
+ // SwingStore "export data" from the already restored cosmos DB, to produce a
57
+ // full SwingStore export that can be imported to create a new JS swing-store DB.
58
+ //
59
+ // Since swing-store is not able to open its DB at historical commit points,
60
+ // the export operation must start before new changes are committed, aka before
61
+ // Swingset is instructed to commit the next block. For that reason the cosmos
62
+ // snapshot operation is currently mediated by the SwingStoreExportsHandler,
63
+ // which helps with the synchronization needed to generate consistent exports,
64
+ // while allowing SwingSet activity to proceed for the next block. This relies
65
+ // on the application calling WaitUntilSwingStoreExportStarted before
66
+ // instructing SwingSet to commit a new block.
67
+ type ExtensionSnapshotter struct {
68
+ isConfigured func() bool
69
+ // takeAppSnapshot is called by OnExportStarted when creating a snapshot
70
+ takeAppSnapshot func(height int64)
71
+ newRestoreContext func(height int64) sdk.Context
72
+ swingStoreExportsHandler *SwingStoreExportsHandler
73
+ getSwingStoreExportDataShadowCopy func(ctx sdk.Context) []*vstoragetypes.DataEntry
74
+ logger log.Logger
75
+ activeSnapshot *snapshotDetails
76
+ }
77
+
78
+ // NewExtensionSnapshotter creates a new swingset ExtensionSnapshotter
79
+ func NewExtensionSnapshotter(
80
+ app *baseapp.BaseApp,
81
+ swingStoreExportsHandler *SwingStoreExportsHandler,
82
+ getSwingStoreExportDataShadowCopy func(ctx sdk.Context) []*vstoragetypes.DataEntry,
83
+ ) *ExtensionSnapshotter {
84
+ return &ExtensionSnapshotter{
85
+ isConfigured: func() bool { return app.SnapshotManager() != nil },
86
+ takeAppSnapshot: app.Snapshot,
87
+ newRestoreContext: func(height int64) sdk.Context {
88
+ return app.NewUncachedContext(false, tmproto.Header{Height: height})
89
+ },
90
+ logger: app.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName), "submodule", "extension snapshotter"),
91
+ swingStoreExportsHandler: swingStoreExportsHandler,
92
+ getSwingStoreExportDataShadowCopy: getSwingStoreExportDataShadowCopy,
93
+ activeSnapshot: nil,
94
+ }
95
+ }
96
+
97
+ // SnapshotName returns the name of the snapshotter, it should be unique in the manager.
98
+ // Implements ExtensionSnapshotter
99
+ func (snapshotter *ExtensionSnapshotter) SnapshotName() string {
100
+ return types.ModuleName
101
+ }
102
+
103
+ // SnapshotFormat returns the extension specific format used to encode the
104
+ // extension payloads when creating a snapshot. It's independent of the format
105
+ // used for the overall state-sync snapshot.
106
+ // Implements ExtensionSnapshotter
107
+ func (snapshotter *ExtensionSnapshotter) SnapshotFormat() uint32 {
108
+ return SnapshotFormat
109
+ }
110
+
111
+ // SupportedFormats returns a list of extension specific payload formats it can
112
+ // restore from.
113
+ // Implements ExtensionSnapshotter
114
+ func (snapshotter *ExtensionSnapshotter) SupportedFormats() []uint32 {
115
+ return []uint32{SnapshotFormat}
116
+ }
117
+
118
+ // InitiateSnapshot initiates a snapshot for the given block height.
119
+ // If a snapshot is already in progress, or if no snapshot manager is
120
+ // configured, this will fail.
121
+ //
122
+ // The snapshot operation is performed in a goroutine.
123
+ // Use WaitUntilSwingStoreExportStarted to synchronize commit boundaries.
124
+ func (snapshotter *ExtensionSnapshotter) InitiateSnapshot(height int64) error {
125
+ if !snapshotter.isConfigured() {
126
+ return fmt.Errorf("snapshot manager not configured")
127
+ }
128
+ if height <= 0 {
129
+ return fmt.Errorf("block height must not be negative or 0")
130
+ }
131
+
132
+ blockHeight := uint64(height)
133
+
134
+ return snapshotter.swingStoreExportsHandler.InitiateExport(blockHeight, snapshotter, SwingStoreExportOptions{
135
+ ExportMode: SwingStoreExportModeCurrent,
136
+ IncludeExportData: false,
137
+ })
138
+ }
139
+
140
+ // OnExportStarted performs the actual cosmos state-sync app snapshot.
141
+ // The cosmos implementation will ultimately call SnapshotExtension, which can
142
+ // retrieve and process the SwingStore artifacts.
143
+ // This method is invoked by the SwingStoreExportsHandler in a goroutine
144
+ // started by InitiateExport, only if no other SwingStore export operation is
145
+ // already in progress.
146
+ //
147
+ // Implements SwingStoreExportEventHandler
148
+ func (snapshotter *ExtensionSnapshotter) OnExportStarted(blockHeight uint64, retrieveExport func() error) error {
149
+ logger := snapshotter.logger.With("height", blockHeight)
150
+
151
+ if blockHeight > math.MaxInt64 {
152
+ return fmt.Errorf("snapshot block height %d is higher than max int64", blockHeight)
153
+ }
154
+ height := int64(blockHeight)
155
+
156
+ // We assume SwingStoreSnapshotter correctly guarded against concurrent snapshots
157
+ snapshotDetails := snapshotDetails{
158
+ blockHeight: blockHeight,
159
+ logger: logger,
160
+ retrieveExport: retrieveExport,
161
+ }
162
+ snapshotter.activeSnapshot = &snapshotDetails
163
+
164
+ snapshotter.takeAppSnapshot(height)
165
+
166
+ snapshotter.activeSnapshot = nil
167
+
168
+ // Unfortunately Cosmos BaseApp.Snapshot() does not report its errors.
169
+ return nil
170
+ }
171
+
172
+ // SnapshotExtension is the method invoked by cosmos to write extension payloads
173
+ // into the underlying protobuf stream of the state-sync snapshot.
174
+ // This method is invoked by the cosmos snapshot manager in a goroutine it
175
+ // started during the call to OnExportStarted. However the snapshot manager
176
+ // fully synchronizes its goroutine with the goroutine started by the
177
+ // SwingStoreSnapshotter, making it safe to invoke callbacks of the
178
+ // SwingStoreSnapshotter. SnapshotExtension actually delegates writing
179
+ // extension payloads to OnExportRetrieved.
180
+ //
181
+ // Implements ExtensionSnapshotter
182
+ func (snapshotter *ExtensionSnapshotter) SnapshotExtension(blockHeight uint64, payloadWriter snapshots.ExtensionPayloadWriter) error {
183
+ logError := func(err error) error {
184
+ // The cosmos layers do a poor job of reporting errors, however
185
+ // SwingStoreExportsHandler arranges to report retrieve errors swallowed by
186
+ // takeAppSnapshot, so we manually report unexpected errors.
187
+ snapshotter.logger.Error("swingset snapshot extension failed", "err", err)
188
+ return err
189
+ }
190
+
191
+ snapshotDetails := snapshotter.activeSnapshot
192
+ if snapshotDetails == nil {
193
+ // shouldn't happen, but return an error if it does
194
+ return logError(errors.New("no active swingset snapshot"))
195
+ }
196
+
197
+ if snapshotDetails.blockHeight != blockHeight {
198
+ return logError(fmt.Errorf("swingset extension snapshot requested for unexpected height %d (expected %d)", blockHeight, snapshotDetails.blockHeight))
199
+ }
200
+
201
+ snapshotDetails.payloadWriter = payloadWriter
202
+
203
+ return snapshotDetails.retrieveExport()
204
+ }
205
+
206
+ // OnExportRetrieved handles the SwingStore export retrieved by the SwingStoreExportsHandler
207
+ // and writes it out to the SnapshotExtension's payloadWriter.
208
+ // This operation is invoked by the SwingStoreExportsHandler in the snapshot
209
+ // manager goroutine synchronized with SwingStoreExportsHandler's own goroutine.
210
+ //
211
+ // Implements SwingStoreExportEventHandler
212
+ func (snapshotter *ExtensionSnapshotter) OnExportRetrieved(provider SwingStoreExportProvider) error {
213
+ snapshotDetails := snapshotter.activeSnapshot
214
+ if snapshotDetails == nil || snapshotDetails.payloadWriter == nil {
215
+ // shouldn't happen, but return an error if it does
216
+ return errors.New("no active swingset snapshot")
217
+ }
218
+
219
+ if snapshotDetails.blockHeight != provider.BlockHeight {
220
+ return fmt.Errorf("SwingStore export received for unexpected block height %d (app snapshot height is %d)", provider.BlockHeight, snapshotDetails.blockHeight)
221
+ }
222
+
223
+ writeArtifactToPayload := func(artifact types.SwingStoreArtifact) error {
224
+ payloadBytes, err := artifact.Marshal()
225
+ if err != nil {
226
+ return err
227
+ }
228
+
229
+ err = snapshotDetails.payloadWriter(payloadBytes)
230
+ if err != nil {
231
+ return err
232
+ }
233
+
234
+ return nil
235
+ }
236
+
237
+ for {
238
+ artifact, err := provider.ReadArtifact()
239
+ if err == io.EOF {
240
+ break
241
+ } else if err != nil {
242
+ return err
243
+ }
244
+
245
+ err = writeArtifactToPayload(artifact)
246
+ if err != nil {
247
+ return err
248
+ }
249
+ }
250
+
251
+ swingStoreExportDataEntries, err := provider.GetExportData()
252
+ if err != nil {
253
+ return err
254
+ }
255
+ if len(swingStoreExportDataEntries) == 0 {
256
+ return nil
257
+ }
258
+
259
+ // For debugging, write out any retrieved export data as a single untrusted artifact
260
+ // which has the same encoding as the internal SwingStore export data representation:
261
+ // a sequence of [key, value] JSON arrays each terminated by a new line.
262
+ exportDataArtifact := types.SwingStoreArtifact{Name: UntrustedExportDataArtifactName}
263
+
264
+ var encodedExportData bytes.Buffer
265
+ encoder := json.NewEncoder(&encodedExportData)
266
+ encoder.SetEscapeHTML(false)
267
+ for _, dataEntry := range swingStoreExportDataEntries {
268
+ entry := []string{dataEntry.Path, dataEntry.Value}
269
+ err := encoder.Encode(entry)
270
+ if err != nil {
271
+ return err
272
+ }
273
+ }
274
+ exportDataArtifact.Data = encodedExportData.Bytes()
275
+
276
+ err = writeArtifactToPayload(exportDataArtifact)
277
+ encodedExportData.Reset()
278
+ if err != nil {
279
+ return err
280
+ }
281
+ return nil
282
+ }
283
+
284
+ // RestoreExtension restores an extension state snapshot,
285
+ // the payload reader returns io.EOF when it reaches the extension boundaries.
286
+ // Implements ExtensionSnapshotter
287
+ func (snapshotter *ExtensionSnapshotter) RestoreExtension(blockHeight uint64, format uint32, payloadReader snapshots.ExtensionPayloadReader) error {
288
+ if format != SnapshotFormat {
289
+ return snapshots.ErrUnknownFormat
290
+ }
291
+
292
+ if blockHeight > math.MaxInt64 {
293
+ return fmt.Errorf("snapshot block height %d is higher than max int64", blockHeight)
294
+ }
295
+ height := int64(blockHeight)
296
+
297
+ // Retrieve the SwingStore "ExportData" from the verified vstorage data.
298
+ // At this point the content of the cosmos DB has been verified against the
299
+ // AppHash, which means the SwingStore data it contains can be used as the
300
+ // trusted root against which to validate the artifacts.
301
+ getExportData := func() ([]*vstoragetypes.DataEntry, error) {
302
+ ctx := snapshotter.newRestoreContext(height)
303
+ exportData := snapshotter.getSwingStoreExportDataShadowCopy(ctx)
304
+ return exportData, nil
305
+ }
306
+
307
+ readArtifact := func() (artifact types.SwingStoreArtifact, err error) {
308
+ payloadBytes, err := payloadReader()
309
+ if err != nil {
310
+ return artifact, err
311
+ }
312
+
313
+ err = artifact.Unmarshal(payloadBytes)
314
+ return artifact, err
315
+ }
316
+
317
+ return snapshotter.swingStoreExportsHandler.RestoreExport(
318
+ SwingStoreExportProvider{BlockHeight: blockHeight, GetExportData: getExportData, ReadArtifact: readArtifact},
319
+ SwingStoreRestoreOptions{IncludeHistorical: false},
320
+ )
321
+ }
@@ -0,0 +1,106 @@
1
+ package keeper
2
+
3
+ import (
4
+ "io"
5
+ "testing"
6
+
7
+ sdk "github.com/cosmos/cosmos-sdk/types"
8
+ "github.com/tendermint/tendermint/libs/log"
9
+ )
10
+
11
+ func newTestExtensionSnapshotter() *ExtensionSnapshotter {
12
+ logger := log.NewNopLogger() // log.NewTMLogger(log.NewSyncWriter( /* os.Stdout*/ io.Discard)).With("module", "sdk/app")
13
+ return &ExtensionSnapshotter{
14
+ isConfigured: func() bool { return true },
15
+ newRestoreContext: func(height int64) sdk.Context { return sdk.Context{} },
16
+ logger: logger,
17
+ swingStoreExportsHandler: newTestSwingStoreExportsHandler(),
18
+ }
19
+ }
20
+
21
+ func TestExtensionSnapshotterInProgress(t *testing.T) {
22
+ extensionSnapshotter := newTestExtensionSnapshotter()
23
+ ch := make(chan struct{})
24
+ extensionSnapshotter.takeAppSnapshot = func(height int64) {
25
+ <-ch
26
+ }
27
+ err := extensionSnapshotter.InitiateSnapshot(123)
28
+ if err != nil {
29
+ t.Fatal(err)
30
+ }
31
+ err = WaitUntilSwingStoreExportStarted()
32
+ if err != nil {
33
+ t.Fatal(err)
34
+ }
35
+
36
+ err = extensionSnapshotter.InitiateSnapshot(456)
37
+ if err == nil {
38
+ t.Error("wanted error for snapshot in progress")
39
+ }
40
+
41
+ err = extensionSnapshotter.RestoreExtension(
42
+ 456, SnapshotFormat,
43
+ func() ([]byte, error) {
44
+ return nil, io.EOF
45
+ })
46
+ if err == nil {
47
+ t.Error("wanted error for snapshot in progress")
48
+ }
49
+
50
+ close(ch)
51
+ err = WaitUntilSwingStoreExportDone()
52
+ if err != nil {
53
+ t.Fatal(err)
54
+ }
55
+
56
+ err = extensionSnapshotter.InitiateSnapshot(456)
57
+ if err != nil {
58
+ t.Fatal(err)
59
+ }
60
+ err = WaitUntilSwingStoreExportDone()
61
+ if err != nil {
62
+ t.Fatal(err)
63
+ }
64
+ }
65
+
66
+ func TestExtensionSnapshotterNotConfigured(t *testing.T) {
67
+ extensionSnapshotter := newTestExtensionSnapshotter()
68
+ extensionSnapshotter.isConfigured = func() bool { return false }
69
+ err := extensionSnapshotter.InitiateSnapshot(123)
70
+ if err == nil {
71
+ t.Error("wanted error for unconfigured snapshot manager")
72
+ }
73
+ }
74
+
75
+ func TestExtensionSnapshotterSecondCommit(t *testing.T) {
76
+ extensionSnapshotter := newTestExtensionSnapshotter()
77
+
78
+ // Use a channel to block the snapshot goroutine after it has started but before it exits.
79
+ ch := make(chan struct{})
80
+ extensionSnapshotter.takeAppSnapshot = func(height int64) {
81
+ <-ch
82
+ }
83
+
84
+ // First run through app.Commit()
85
+ err := WaitUntilSwingStoreExportStarted()
86
+ if err != nil {
87
+ t.Fatal(err)
88
+ }
89
+ err = extensionSnapshotter.InitiateSnapshot(123)
90
+ if err != nil {
91
+ t.Fatal(err)
92
+ }
93
+
94
+ // Second run through app.Commit() - should return right away
95
+ err = WaitUntilSwingStoreExportStarted()
96
+ if err != nil {
97
+ t.Fatal(err)
98
+ }
99
+
100
+ // close the signaling channel to let goroutine exit
101
+ close(ch)
102
+ err = WaitUntilSwingStoreExportDone()
103
+ if err != nil {
104
+ t.Fatal(err)
105
+ }
106
+ }