@agoric/swing-store 0.9.2-dev-2f092c3.0 → 0.9.2-dev-9d4eaad.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.
@@ -16,7 +16,7 @@ The SwingStore export protocol defines two stages (effectively two datasets). Th
16
16
 
17
17
  Each time a SwingStore API is used to modify the state somehow (e.g. adding/changing/deleting a `kvStore` entry, or pushing a new item on to a transcript), the contents of both datasets may change. New first-stage entries can be created, existing ones may be modified or deleted. And the set of second-stage artifacts may change.
18
18
 
19
- These export data/artifact changes can happen when calling into the kernel (e.g. invoking the external API of a device, causing the device code to change its own state or push messages onto the run-queue), or by normal kernel operations as it runs (any time `controller.run()` is executing). When the kernel is idle (after `controller.run()` has completed), the kernel will not make any changes to the SwingStore, and both datasets will be stable.
19
+ These export data/artifact changes can happen when calling into the kernel (e.g. invoking the external API of a device, causing the device code to change its own state or push messages onto the run-queue), or by normal kernel operations as it runs (any time `controller.run()` is executing). When the kernel is idle (after `controller.run()` has completed), and `hostStorage.commit()` is called, the kernel will not make any changes to the SwingStore, and both datasets will be stable.
20
20
 
21
21
  Among other things, the SwingStore records a transcript of deliveries for each vat. The collection of all deliveries to a particular vat since its last heap snapshot was written is called the "current span". For each vat, the first-stage export data will record a single record that remembers the extent and the hash of the current span. This record then refers to a second-stage export artifact that contains the actual transcript contents.
22
22
 
@@ -83,6 +83,8 @@ So, to include SwingStore data in this state-sync snapshot, we need a way to get
83
83
 
84
84
  To support this, SwingStore has an "incremental export" mode. This is activated when the host application supplies an "export callback" option to the SwingStore instance constructor. Instead of retrieving the entire first-stage export data at the end of the block, the host application will be continuously notified about changes to this data as the kernel executes. The host application can then incorporate those entries into an existing hashed Merkle tree (e.g. the cosmos-sdk IAVL tree), whose root hash is included in the consensus block hash. Every time the callback is given `(key, value)`, the host should add a new (or modify some existing) IAVL entry, using an IAVL key within some range dedicated to the SwingStore first-stage export data. When the callback receives `(key, undefined)` or `(key, null)`, it should delete the entry. In this way, the IAVL tree maintains a "shadow copy" of the first-stage export data at all times, making the contents both covered by the consensus hash, and automatically included in the cosmos-sdk IAVL tree where it will become available to the new validator as it begins to reconstruct the SwingStore.
85
85
 
86
+ The export callback must be established from the very beginning, so it includes all changes made during kernel initialization.
87
+
86
88
  All validator nodes use this export callback, even if they never perform the rest of the export process, to ensure that the consensus state includes the entire first-stage dataset. (Note that the first stage data is generally smaller than the full dataset, making this relatively inexpensive).
87
89
 
88
90
  Then, on the few occasions when the application needs to build a full state-sync snapshot, it can ask the SwingStore (after block commit) for the full set of artifacts that match the most recent commit.
@@ -177,18 +179,32 @@ As a result, for each active vat, the first-stage Export Data contains a record
177
179
 
178
180
  The `openSwingStore()` function has an option named `keepTranscripts` (which defaults to `true`), which causes the transcriptStore to retain the old transcript items. A second option named `keepSnapshots` (which defaults to `false`) causes the snapStore to retain the old heap snapshots. Opening the swingStore with a `false` option does not necessarily delete the old items immediately, but they'll probably get deleted the next time the kernel triggers a heap snapshot or transcript-span rollover. Validators who care about minimizing their disk usage will want to set both to `false`. In the future, we will arrange the SwingStore SQLite tables to provide easy `sqlite3` CLI commands that will delete the old data, so validators can also periodically use the CLI command to prune it.
179
181
 
180
- The `getArtifactNames()` API includes an option named `includeHistorical`. If `true`, all available historical artifacts will be included in the export (limited by what the `openSwingStore` options have deleted). If `false`, none will be included. Note that the "export data" is necessarily unaffected: if we *ever* want to validate this optional data, the hashes are mandatory. But the `getArtifactNames()` list will be smaller if you set `includeHistorical = false`. Also, re-exporting from a pruned copy will lack the old data, even if the re-export uses `includeHistorical = true`, because the second SwingStore cannot magically reconstruct the missing data.
182
+ When exporting, the `makeSwingStoreExporter()` function takes an `exportMode=` argument. This serves to limit the set of artifacts that will be provided in the export. The defined values of `exportMode` are:
183
+ * `current`: include only the current transcript span and current snapshot for each vat: just the minimum set necessary for current operations
184
+ * `archival`: include all available transcript spans
185
+ * `debug`: include all available transcript spans *and* all available snapshots. The old snapshots are never necessary for normal operations, nor are they likely to be usefor for extreme upgrade scenarios, but they might be useful for some unusual debugging operation
186
+
187
+ Note that `exportMode` does not affect the Export Data generated by the exporter (if we *ever* want to validate this optional data, the hashes are mandatory). It only affects the names returned by `getArtifactNames()`: the list will be smaller for `current` than for `archival`. Re-exporting from a pruned copy will lack the old data, even if the re-export uses `archival`, because the second SwingStore cannot magically reconstruct the missing data.
181
188
 
182
189
  Note that when a vat is terminated, we delete all information about it, including transcript items and snapshots, both current and old. This will remove all the Export Data records, and well as the matching artifacts from `getArtifactNames`.
183
190
 
191
+ When importing, the `importSwingStore()` function takes an options bag, which has property named `includeHistorical`. This property defaults to `false`, which makes the importer ignore any historical artifacts present in the export dataset. To import the historical transcript spans (and snapshots), you must set it to `true`.
192
+
193
+ So, to convey historical transcript spans from one swingstore to another, you must set three options along the way:
194
+
195
+ * the original swingstore must be opened with `{ includeHistorical: true }`, otherwise the old spans will be pruned immediately
196
+ * the export must use `makeSwingStoreExporter(dirpath, 'archival')`, otherwise the export will omit the old spans
197
+ * the import must use `importSwingStore(exporter, dirPath, { includeHistorical: true })`, otherwide teh import will ignore the old spans
198
+
184
199
  ## Implementation Details
185
200
 
186
- SwingStore contains components to accomodate all the various kinds of state that the SwingSet kernel needs to store. This currently consists of three portions:
201
+ SwingStore contains components to accommodate all the various kinds of state that the SwingSet kernel needs to store. This currently consists of four portions:
187
202
 
188
203
  * `kvStore`, a general-purpose string/string key-value table
189
204
  * `transcriptStore`: append-only vat deliveries, broken into "spans", delimited by heap snapshot events
190
205
  * `snapshotStore`: binary blobs containing JS engine heap state, to limit transcript replay depth
206
+ * `bundleStore`: code bundles that can be imported with `@endo/import-bundle`
191
207
 
192
- Currently, the SwingStore treats transcript spans and heap snapshots as export artifacts, with hashes recorded in the export data for validation (and to remember exactly which artifacts are necessary). The `kvStore` is copied one-to-one into the export data (i.e. we keep a full shadow copy in IAVL), because that is the fastest way to ensure the `kvStore` data is fully available and validated.
208
+ Currently, the SwingStore treats transcript spans, heap snapshots, and bundles as export artifacts, with hashes recorded in the export data for validation (and to remember exactly which artifacts are necessary). The `kvStore` is copied one-to-one into the export data (i.e. we keep a full shadow copy in IAVL), because that is the fastest way to ensure the `kvStore` data is fully available and validated.
193
209
 
194
210
  If some day we implement an IAVL-like Merkle tree inside SwingStore, and use it to automatically generate a root hash for the `kvStore` at the end of each block, we will replace this (large) shadow copy with a single `kvStoreRootHash` entry, and add a new export artifact to contain the full contents of the kvStore. This reduce the size of the IAVL tree, as well as the rate of IAVL updates during block execution, at the cost of increased CPU and complexity within SwingStore.
@@ -0,0 +1,52 @@
1
+ # SwingStore Data Model
2
+
3
+ The "SwingStore" provides a database to hold SwingSet kernel state, with an API crafted to help both the kernel and the host application mutate, commit, export, and import this state.
4
+
5
+ The state is broken up into several pieces, or "stores":
6
+
7
+ * `bundleStore`: a string-keyed Bundle-value table, holding source bundles which can be evaluated by `importBundle` to create vats, or new Compartments within a vat
8
+ * `transcriptStore`: records a linear sequence of deliveries and syscalls (with results), collectively known as "transcript entries", for each vat
9
+ * `snapStore`: records one or more XS heap snapshots for each vat, to rebuild a worker more efficiently than replaying all transcript entries from the beginning
10
+ * `kvStore`: a string-keyed string-valued table, which holds everything else. Currently, this holds each vat's c-list and vatstore data, as well as the kernel-wide object and promise tables, and run-queues.
11
+
12
+ ## Incarnations, Spans, Snapshots
13
+
14
+ The kernel tracks the state of one or more vats. Each vat's execution is split into "incarnations", which are separated by a "vat upgrade" (a call to `E(vatAdminFacet).upgrade(newBundleCap, options)`, see https://github.com/Agoric/agoric-sdk/blob/master/packages/SwingSet/docs/vat-upgrade.md for details). Each incarnation gets a new worker, which erases the heap state and only retains durable vatstore data across the upgrade. Every active vat has a "current incarnation", and zero or more "historic incarnations". Only the current incarnation is instantiated.
15
+
16
+ Within each incarnation, execution is broken into one or more "spans", with a "current span" and zero or more "historic spans". This breaks up the transcript into corresponding spans.
17
+
18
+ Each historic span ends with a `save-snapshot` entry which records the creation and saving of an XS heap snapshot. The initial span starts with a `start-worker` entry, while all non-initial spans start with a `load-snapshot` entry. The final span of historic incarnations each end with a `shutdown-worker` entry.
19
+
20
+ Each `save-snapshot` entry adds a new snapshot to the `snapStore`, so each vat has zero or more snapshots, of which the last one is called the "current" or "in-use" snapshot, and the earlier ones are called "historical snapshots".
21
+
22
+ (note: the `deliveryNum` counter is scoped to the vat and does not reset at incarnation or span boundaries)
23
+
24
+ ## Artifacts
25
+
26
+ The import/export process (using `makeSwingStoreExporter` and `importSwingStore`) defines some number of "artifacts" to contain much of the SwingStore data. Each bundle is a separate artifact, as is each heap snapshot. Each transcript span is a separate artifact (an aggregate of the individual transcript entries comprising that span).
27
+
28
+ During export, the `getArtifactNames()` method provides a list of all available artifacts, while `getArtifact(name)` is used to retrieve the actual data. The import function processes each artifact separately.
29
+
30
+ ## Populated vs Pruned
31
+
32
+ For normal operation, the kernel does not require historical incarnations, spans, or snapshots. It only needs the ability to reconstruct a worker for the current incarnation of each vat, which means loading the current snapshot (if any), and replaying the contents of the current transcript span.
33
+
34
+ For this reason, the swingstore must always contain the current transcript span, and the current snapshot (if any), for every vat.
35
+
36
+ However, to save space, historical spans/snapshots might be pruned, by deleting their contents from the database (but retaining the metadata, which includes a hash of the contents for later validation). Historical snapshots are pruned by default (unless `openSwingStore()` is given an options bag with `keepSnapshots: true`). Historical spans are not currently pruned (the `keepTranscripts` option defaults to `true`), but that may change.
37
+
38
+ In addition, `importSwingStore()` can be used to create a SwingStore from data exported out of some other SwingStore. The export-then-import process might result in a pruned DB in one of three ways:
39
+
40
+ * the import-time options might instruct the import process to ignore some of the available data
41
+ * the export-time options might have done the same
42
+ * the original DB was itself already pruned, so the data was not available in the first place
43
+
44
+ In the future, a separate SwingStore API will exist to allow previously-pruned artifacts to be repopulated. Every artifact has a metadata record which *is* included in the export (in the `exportData` section, but separate from the kvStore shadow table entries, see [data-export.md](./data-export.md)), regardless of pruning modes, to ensure that this API can check the integrity of these repopulated artifacts. This reduces the reliance set and trust burden of the repopulation process (we can safely use untrusted artifact providers).
45
+
46
+ When a snapshot is pruned, the `snapshots` SQL table row is modified, replacing its `compressedSnapshot` BLOB with a NULL. The other columns are left alone, especially the `hash` column, which retains the integrity-checking metadata to support a future repopulation.
47
+
48
+ When a transcript span is pruned, the `transcriptSpans` row is left alone, but the collection of `transcriptItems` rows are deleted. Any span for which all the `transcriptItems` rows are present is said to be "populated", while any span that is missing one or more `transcriptItems` rows is said to be "pruned". (There is no good reason for a span to be only partially pruned, but until we compress historical spans into a single row, in some new table, there remains the possibility of partial pruning).
49
+
50
+ During import, we create the metadata first (as the export-data is parsed), then later, we fill in the details as the artifacts are read.
51
+
52
+ Bundles are never pruned, however during import, the `bundles` table will temporarily contain rows whose `bundle` BLOB is NULL.
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@agoric/swing-store",
3
- "version": "0.9.2-dev-2f092c3.0+2f092c3",
3
+ "version": "0.9.2-dev-9d4eaad.0+9d4eaad",
4
4
  "description": "Persistent storage for SwingSet",
5
5
  "type": "module",
6
- "main": "src/swingStore.js",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
7
10
  "repository": "https://github.com/Agoric/agoric-sdk",
8
11
  "author": "Agoric",
9
12
  "license": "Apache-2.0",
@@ -18,8 +21,8 @@
18
21
  "lint:eslint": "eslint ."
19
22
  },
20
23
  "dependencies": {
21
- "@agoric/assert": "0.6.1-dev-2f092c3.0+2f092c3",
22
- "@agoric/internal": "0.3.3-dev-2f092c3.0+2f092c3",
24
+ "@agoric/assert": "0.6.1-dev-9d4eaad.0+9d4eaad",
25
+ "@agoric/internal": "0.3.3-dev-9d4eaad.0+9d4eaad",
23
26
  "@endo/base64": "^0.2.32",
24
27
  "@endo/bundle-source": "^2.5.2",
25
28
  "@endo/check-bundle": "^0.2.19",
@@ -42,5 +45,5 @@
42
45
  ],
43
46
  "timeout": "2m"
44
47
  },
45
- "gitHead": "2f092c3a20ac275c3402645d3d984a3657cf20d5"
48
+ "gitHead": "9d4eaadaa3caa031a0410f747ff06a4855ece40a"
46
49
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @param {import('./internal.js').SwingStoreInternal} internal
3
+ * @param {'operational'} level
4
+ * @returns {void}
5
+ */
6
+ export function assertComplete(internal, level) {
7
+ assert.equal(level, 'operational'); // only option for now
8
+ // every bundle must be populated
9
+ internal.bundleStore.assertComplete(level);
10
+
11
+ // every 'isCurrent' transcript span must have all items
12
+ // TODO: every vat with any data must have a isCurrent transcript
13
+ // span
14
+ internal.transcriptStore.assertComplete(level);
15
+
16
+ // every 'inUse' snapshot must be populated
17
+ internal.snapStore.assertComplete(level);
18
+
19
+ // TODO: every isCurrent span that starts with load-snapshot has a
20
+ // matching snapshot (counter-argument: swing-store should not know
21
+ // those details about transcript entries)
22
+ }
@@ -7,7 +7,6 @@ import { checkBundle } from '@endo/check-bundle/lite.js';
7
7
  import { Nat } from '@endo/nat';
8
8
  import { Fail, q } from '@agoric/assert';
9
9
  import { createSHA256 } from './hasher.js';
10
- import { buffer } from './util.js';
11
10
 
12
11
  /**
13
12
  * @typedef { { moduleFormat: 'getExport', source: string, sourceMap?: string } } GetExportBundle
@@ -16,7 +15,7 @@ import { buffer } from './util.js';
16
15
  * @typedef { EndoZipBase64Bundle | GetExportBundle | NestedEvaluateBundle } Bundle
17
16
  */
18
17
  /**
19
- * @typedef { import('./swingStore').SwingStoreExporter } SwingStoreExporter
18
+ * @typedef { import('./exporter').SwingStoreExporter } SwingStoreExporter
20
19
  *
21
20
  * @typedef {{
22
21
  * addBundle: (bundleID: string, bundle: Bundle) => void;
@@ -27,7 +26,10 @@ import { buffer } from './util.js';
27
26
  *
28
27
  * @typedef {{
29
28
  * exportBundle: (name: string) => AsyncIterableIterator<Uint8Array>,
30
- * importBundle: (artifactName: string, exporter: SwingStoreExporter, bundleID: string) => void,
29
+ * repairBundleRecord: (key: string, value: string) => void,
30
+ * importBundleRecord: (key: string, value: string) => void,
31
+ * importBundle: (name: string, dataProvider: () => Promise<Buffer>) => Promise<void>,
32
+ * assertComplete: (level: 'operational') => void,
31
33
  * getExportRecords: () => IterableIterator<readonly [key: string, value: string]>,
32
34
  * getArtifactNames: () => AsyncIterableIterator<string>,
33
35
  * getBundleIDs: () => IterableIterator<string>,
@@ -39,6 +41,18 @@ import { buffer } from './util.js';
39
41
  *
40
42
  */
41
43
 
44
+ function bundleIDFromName(name) {
45
+ typeof name === 'string' || Fail`artifact name must be a string`;
46
+ const [tag, ...pieces] = name.split('.');
47
+ if (tag !== 'bundle' || pieces.length !== 1) {
48
+ Fail`expected artifact name of the form 'bundle.{bundleID}', saw ${q(
49
+ name,
50
+ )}`;
51
+ }
52
+ const bundleID = pieces[0];
53
+ return bundleID;
54
+ }
55
+
42
56
  /**
43
57
  * @param {*} db
44
58
  * @param {() => void} ensureTxn
@@ -54,6 +68,9 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
54
68
  )
55
69
  `);
56
70
 
71
+ // A populated record contains both bundleID and bundle, while a
72
+ // pruned record has a bundle of NULL.
73
+
57
74
  function bundleArtifactName(bundleID) {
58
75
  return `bundle.${bundleID}`;
59
76
  }
@@ -62,20 +79,36 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
62
79
  return `b${Nat(version)}-${hash}`;
63
80
  }
64
81
 
65
- const sqlAddBundle = db.prepare(`
66
- INSERT OR REPLACE INTO bundles
67
- (bundleID, bundle)
68
- VALUES (?, ?)
82
+ // the PRIMARY KEY constraint requires the bundleID not already
83
+ // exist
84
+ const sqlAddBundleRecord = db.prepare(`
85
+ INSERT INTO bundles (bundleID, bundle) VALUES (?, NULL)
69
86
  `);
70
87
 
71
- /**
72
- * Store a bundle. Here the bundle itself is presumed valid.
73
- *
74
- * @param {string} bundleID
75
- * @param {Bundle} bundle
76
- */
77
- function addBundle(bundleID, bundle) {
88
+ // this sees both populated and pruned (not-yet-populated) records
89
+ const sqlHasBundleRecord = db.prepare(`
90
+ SELECT count(*)
91
+ FROM bundles
92
+ WHERE bundleID = ?
93
+ `);
94
+ sqlHasBundleRecord.pluck();
95
+
96
+ const sqlPopulateBundleRecord = db.prepare(`
97
+ UPDATE bundles SET bundle = $serialized WHERE bundleID = $bundleID
98
+ `);
99
+
100
+ function addBundleRecord(bundleID) {
101
+ ensureTxn();
102
+ sqlAddBundleRecord.run(bundleID);
103
+ }
104
+
105
+ function populateBundle(bundleID, serialized) {
78
106
  ensureTxn();
107
+ sqlHasBundleRecord.get(bundleID) || Fail`missing ${bundleID}`;
108
+ sqlPopulateBundleRecord.run({ bundleID, serialized });
109
+ }
110
+
111
+ function serializeBundle(bundleID, bundle) {
79
112
  const { moduleFormat } = bundle;
80
113
  let serialized;
81
114
  if (bundleID.startsWith('b0-')) {
@@ -98,19 +131,55 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
98
131
  } else {
99
132
  throw Fail`unsupported BundleID ${bundleID}`;
100
133
  }
101
- sqlAddBundle.run(bundleID, serialized);
134
+ return serialized;
135
+ }
136
+
137
+ /**
138
+ * Store a complete bundle in a single operation, used by runtime
139
+ * (i.e. not an import). We rely upon the caller to provide a
140
+ * correct bundle (e.g. no unexpected properties), but we still
141
+ * check the ID against the contents.
142
+ *
143
+ * @param {string} bundleID
144
+ * @param {Bundle} bundle
145
+ */
146
+ function addBundle(bundleID, bundle) {
147
+ const serialized = serializeBundle(bundleID, bundle);
148
+ addBundleRecord(bundleID);
149
+ populateBundle(bundleID, serialized);
102
150
  noteExport(bundleArtifactName(bundleID), bundleID);
103
151
  }
104
152
 
105
- const sqlHasBundle = db.prepare(`
153
+ const sqlGetPrunedBundles = db.prepare(`
154
+ SELECT bundleID
155
+ FROM bundles
156
+ WHERE bundle IS NULL
157
+ ORDER BY bundleID
158
+ `);
159
+ sqlGetPrunedBundles.pluck();
160
+
161
+ function getPrunedBundles() {
162
+ return sqlGetPrunedBundles.all();
163
+ }
164
+
165
+ function assertComplete(level) {
166
+ assert.equal(level, 'operational'); // for now
167
+ const pruned = getPrunedBundles();
168
+ if (pruned.length) {
169
+ throw Fail`missing bundles for: ${pruned.join(',')}`;
170
+ }
171
+ }
172
+
173
+ const sqlHasPopulatedBundle = db.prepare(`
106
174
  SELECT count(*)
107
175
  FROM bundles
108
176
  WHERE bundleID = ?
177
+ AND bundle IS NOT NULL
109
178
  `);
110
- sqlHasBundle.pluck(true);
179
+ sqlHasPopulatedBundle.pluck(true);
111
180
 
112
181
  function hasBundle(bundleID) {
113
- const count = sqlHasBundle.get(bundleID);
182
+ const count = sqlHasPopulatedBundle.get(bundleID);
114
183
  return count !== 0;
115
184
  }
116
185
 
@@ -119,15 +188,15 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
119
188
  FROM bundles
120
189
  WHERE bundleID = ?
121
190
  `);
122
- sqlGetBundle.pluck(true);
123
191
 
124
192
  /**
125
193
  * @param {string} bundleID
126
194
  * @returns {Bundle}
127
195
  */
128
196
  function getBundle(bundleID) {
129
- const rawBundle = sqlGetBundle.get(bundleID);
130
- rawBundle || Fail`bundle ${q(bundleID)} not found`;
197
+ const row =
198
+ sqlGetBundle.get(bundleID) || Fail`bundle ${q(bundleID)} not found`;
199
+ const rawBundle = row.bundle || Fail`bundle ${q(bundleID)} pruned`;
131
200
  if (bundleID.startsWith('b0-')) {
132
201
  return harden(JSON.parse(rawBundle));
133
202
  } else if (bundleID.startsWith('b1-')) {
@@ -153,6 +222,31 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
153
222
  }
154
223
  }
155
224
 
225
+ // take an export-data record (id/hash but not bundle contents) and
226
+ // insert something in the DB
227
+ function importBundleRecord(key, value) {
228
+ const bundleID = bundleIDFromName(key);
229
+ assert.equal(bundleID, value);
230
+ addBundleRecord(bundleID);
231
+ }
232
+
233
+ function repairBundleRecord(key, value) {
234
+ // Bundle records have no metadata, and all bundles must be
235
+ // present (there's no notion of "historical bundle"). So there's
236
+ // no "repair", and if the repair process supplies a bundle record
237
+ // that isn't already present, we throw an error. The repair
238
+ // process doesn't get artifacts, so adding a new record here
239
+ // would fail the subsequent completeness check anyways.
240
+
241
+ const bundleID = bundleIDFromName(key);
242
+ assert.equal(bundleID, value);
243
+ if (sqlHasBundleRecord.get(bundleID)) {
244
+ // record is present, there's no metadata to mismatch, so ignore quietly
245
+ return;
246
+ }
247
+ throw Fail`unexpected new bundle record for ${bundleID} during repair`;
248
+ }
249
+
156
250
  /**
157
251
  * Read a bundle and return it as a stream of data suitable for export to
158
252
  * another store.
@@ -166,14 +260,10 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
166
260
  * @returns {AsyncIterableIterator<Uint8Array>}
167
261
  */
168
262
  async function* exportBundle(name) {
169
- typeof name === 'string' || Fail`artifact name must be a string`;
170
- const parts = name.split('.');
171
- const [type, bundleID] = parts;
172
- // prettier-ignore
173
- (parts.length === 2 && type === 'bundle') ||
174
- Fail`expected artifact name of the form 'bundle.{bundleID}', saw ${q(name)}`;
175
- const rawBundle = sqlGetBundle.get(bundleID);
176
- rawBundle || Fail`bundle ${q(name)} not available`;
263
+ const bundleID = bundleIDFromName(name);
264
+ const row =
265
+ sqlGetBundle.get(bundleID) || Fail`bundle ${q(bundleID)} not found`;
266
+ const rawBundle = row.bundle || Fail`bundle ${q(bundleID)} pruned`;
177
267
  yield* Readable.from(Buffer.from(rawBundle));
178
268
  }
179
269
 
@@ -209,23 +299,17 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
209
299
  }
210
300
 
211
301
  /**
212
- * @param {string} name Artifact name of the bundle
213
- * @param {SwingStoreExporter} exporter Whence to get the bits
214
- * @param {string} bundleID Bundle ID of the bundle
302
+ * Call addBundleRecord() first, then this importBundle() will
303
+ * populate the record.
304
+ *
305
+ * @param {string} name Artifact name, `bundle.${bundleID}`
306
+ * @param {() => Promise<Buffer>} dataProvider Function to get bundle bytes
215
307
  * @returns {Promise<void>}
216
308
  */
217
- async function importBundle(name, exporter, bundleID) {
309
+ async function importBundle(name, dataProvider) {
218
310
  await 0; // no synchronous prefix
219
- const parts = name.split('.');
220
- const [type, bundleIDkey] = parts;
221
- // prettier-ignore
222
- parts.length === 2 && type === 'bundle' ||
223
- Fail`expected artifact name of the form 'bundle.{bundleID}', saw '${q(name)}'`;
224
- bundleIDkey === bundleID ||
225
- Fail`bundle artifact name ${name} doesn't match bundleID ${bundleID}`;
226
- const artifactChunks = exporter.getArtifact(name);
227
- const inStream = Readable.from(artifactChunks);
228
- const data = await buffer(inStream);
311
+ const bundleID = bundleIDFromName(name);
312
+ const data = await dataProvider();
229
313
  if (bundleID.startsWith('b0-')) {
230
314
  // we dissect and reassemble the bundle, to exclude unexpected properties
231
315
  const { moduleFormat, source, sourceMap } = JSON.parse(data.toString());
@@ -234,7 +318,7 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
234
318
  const serialized = JSON.stringify(bundle);
235
319
  bundleID === bundleIdFromHash(0, createSHA256(serialized).finish()) ||
236
320
  Fail`bundleID ${q(bundleID)} does not match bundle artifact`;
237
- addBundle(bundleID, bundle);
321
+ populateBundle(bundleID, serialized);
238
322
  } else if (bundleID.startsWith('b1-')) {
239
323
  /** @type {EndoZipBase64Bundle} */
240
324
  const bundle = harden({
@@ -244,7 +328,7 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
244
328
  });
245
329
  // Assert that the bundle contents match the ID and hash
246
330
  await checkBundle(bundle, computeSha512, bundleID);
247
- addBundle(bundleID, bundle);
331
+ populateBundle(bundleID, serializeBundle(bundleID, bundle));
248
332
  } else {
249
333
  Fail`unsupported BundleID ${q(bundleID)}`;
250
334
  }
@@ -264,7 +348,7 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
264
348
  const dump = {};
265
349
  for (const row of sql.iterate()) {
266
350
  const { bundleID, bundle } = row;
267
- dump[bundleID] = encodeBase64(bundle);
351
+ dump[bundleID] = encodeBase64(Buffer.from(bundle, 'utf-8'));
268
352
  }
269
353
  return dump;
270
354
  }
@@ -281,15 +365,20 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
281
365
  }
282
366
 
283
367
  return harden({
368
+ importBundleRecord,
369
+ importBundle,
370
+ assertComplete,
371
+
284
372
  addBundle,
285
373
  hasBundle,
286
374
  getBundle,
287
375
  deleteBundle,
376
+
288
377
  getExportRecords,
289
378
  getArtifactNames,
290
379
  exportBundle,
291
- importBundle,
292
380
  getBundleIDs,
381
+ repairBundleRecord,
293
382
 
294
383
  dumpBundles,
295
384
  });
@@ -0,0 +1,175 @@
1
+ import sqlite3 from 'better-sqlite3';
2
+
3
+ import { Fail, q } from '@agoric/assert';
4
+
5
+ import { dbFileInDirectory } from './util.js';
6
+ import { getKeyType } from './kvStore.js';
7
+ import { makeBundleStore } from './bundleStore.js';
8
+ import { makeSnapStore } from './snapStore.js';
9
+ import { makeSnapStoreIO } from './snapStoreIO.js';
10
+ import { makeTranscriptStore } from './transcriptStore.js';
11
+
12
+ /**
13
+ * @template T
14
+ * @typedef { Iterable<T> | AsyncIterable<T> } AnyIterable<T>
15
+ */
16
+ /**
17
+ * @template T
18
+ * @typedef { IterableIterator<T> | AsyncIterableIterator<T> } AnyIterableIterator<T>
19
+ */
20
+
21
+ /**
22
+ *
23
+ * @typedef {readonly [
24
+ * key: string,
25
+ * value?: string | null | undefined,
26
+ * ]} KVPair
27
+ *
28
+ * @typedef {object} SwingStoreExporter
29
+ *
30
+ * Allows export of data from a swingStore as a fixed view onto the content as
31
+ * of the most recent commit point at the time the exporter was created. The
32
+ * exporter may be used while another SwingStore instance is active for the same
33
+ * DB, possibly in another thread or process. It guarantees that regardless of
34
+ * the concurrent activity of other swingStore instances, the data representing
35
+ * the commit point will stay consistent and available.
36
+ *
37
+ * @property {() => AnyIterableIterator<KVPair>} getExportData
38
+ *
39
+ * Get a full copy of the first-stage export data (key-value pairs) from the
40
+ * swingStore. This represents both the contents of the KVStore (excluding host
41
+ * and local prefixes), as well as any data needed to validate all artifacts,
42
+ * both current and historical. As such it represents the root of trust for the
43
+ * application.
44
+ *
45
+ * Content of validation data (with supporting entries for indexing):
46
+ * - kv.${key} = ${value} // ordinary kvStore data entry
47
+ * - snapshot.${vatID}.${snapPos} = ${{ vatID, snapPos, hash }};
48
+ * - snapshot.${vatID}.current = `snapshot.${vatID}.${snapPos}`
49
+ * - transcript.${vatID}.${startPos} = ${{ vatID, startPos, endPos, hash }}
50
+ * - transcript.${vatID}.current = ${{ vatID, startPos, endPos, hash }}
51
+ *
52
+ * @property {() => AnyIterableIterator<string>} getArtifactNames
53
+ *
54
+ * Get a list of name of artifacts available from the swingStore. A name
55
+ * returned by this method guarantees that a call to `getArtifact` on the same
56
+ * exporter instance will succeed. The `exportMode` option to
57
+ * `makeSwingStoreExporter` controls the filtering of the artifact names
58
+ * yielded.
59
+ *
60
+ * Artifact names:
61
+ * - transcript.${vatID}.${startPos}.${endPos}
62
+ * - snapshot.${vatID}.${snapPos}
63
+ * - bundle.${bundleID}
64
+ *
65
+ * @property {(name: string) => AnyIterableIterator<Uint8Array>} getArtifact
66
+ *
67
+ * Retrieve an artifact by name as a sequence of binary chunks. May throw if
68
+ * the artifact is not available, which can occur if the artifact is historical
69
+ * and wasn't preserved.
70
+ *
71
+ * @property {() => Promise<void>} close
72
+ *
73
+ * Dispose of all resources held by this exporter. Any further operation on this
74
+ * exporter or its outstanding iterators will fail.
75
+ */
76
+
77
+ /**
78
+ * @typedef {'current' | 'archival' | 'debug'} ExportMode
79
+ */
80
+
81
+ /**
82
+ * @param {string} dirPath
83
+ * @param { ExportMode } exportMode
84
+ * @returns {SwingStoreExporter}
85
+ */
86
+ export function makeSwingStoreExporter(dirPath, exportMode = 'current') {
87
+ typeof dirPath === 'string' || Fail`dirPath must be a string`;
88
+ exportMode === 'current' ||
89
+ exportMode === 'archival' ||
90
+ exportMode === 'debug' ||
91
+ Fail`invalid exportMode ${q(exportMode)}`;
92
+ const exportHistoricalSnapshots = exportMode === 'debug';
93
+ const exportHistoricalTranscripts = exportMode !== 'current';
94
+ const filePath = dbFileInDirectory(dirPath);
95
+ const db = sqlite3(filePath);
96
+
97
+ // Execute the data export in a (read) transaction, to ensure that we are
98
+ // capturing the state of the database at a single point in time. Our close()
99
+ // will ROLLBACK the txn just in case some bug tried to change the DB.
100
+ const sqlBeginTransaction = db.prepare('BEGIN TRANSACTION');
101
+ sqlBeginTransaction.run();
102
+
103
+ // ensureTxn can be a dummy, we just started one
104
+ const ensureTxn = () => {};
105
+ const snapStore = makeSnapStore(db, ensureTxn, makeSnapStoreIO());
106
+ const bundleStore = makeBundleStore(db, ensureTxn);
107
+ const transcriptStore = makeTranscriptStore(db, ensureTxn, () => {});
108
+
109
+ const sqlGetAllKVData = db.prepare(`
110
+ SELECT key, value
111
+ FROM kvStore
112
+ ORDER BY key
113
+ `);
114
+
115
+ /**
116
+ * @returns {AsyncIterableIterator<KVPair>}
117
+ * @yields {KVPair}
118
+ */
119
+ async function* getExportData() {
120
+ for (const { key, value } of sqlGetAllKVData.iterate()) {
121
+ if (getKeyType(key) === 'consensus') {
122
+ yield [`kv.${key}`, value];
123
+ }
124
+ }
125
+ yield* snapStore.getExportRecords(true);
126
+ yield* transcriptStore.getExportRecords(true);
127
+ yield* bundleStore.getExportRecords();
128
+ }
129
+
130
+ /**
131
+ * @returns {AsyncIterableIterator<string>}
132
+ * @yields {string}
133
+ */
134
+ async function* getArtifactNames() {
135
+ yield* snapStore.getArtifactNames(exportHistoricalSnapshots);
136
+ yield* transcriptStore.getArtifactNames(exportHistoricalTranscripts);
137
+ yield* bundleStore.getArtifactNames();
138
+ }
139
+
140
+ /**
141
+ * @param {string} name
142
+ * @returns {AsyncIterableIterator<Uint8Array>}
143
+ */
144
+ function getArtifact(name) {
145
+ typeof name === 'string' || Fail`artifact name must be a string`;
146
+ const [type] = name.split('.', 1);
147
+
148
+ if (type === 'snapshot') {
149
+ return snapStore.exportSnapshot(name);
150
+ } else if (type === 'transcript') {
151
+ return transcriptStore.exportSpan(name);
152
+ } else if (type === 'bundle') {
153
+ return bundleStore.exportBundle(name);
154
+ } else {
155
+ throw Fail`invalid type in artifact name ${q(name)}`;
156
+ }
157
+ }
158
+
159
+ const sqlAbort = db.prepare('ROLLBACK');
160
+
161
+ async function close() {
162
+ // After all the data has been extracted, always abort the export
163
+ // transaction to ensure that the export was read-only (i.e., that no bugs
164
+ // inadvertantly modified the database).
165
+ sqlAbort.run();
166
+ db.close();
167
+ }
168
+
169
+ return harden({
170
+ getExportData,
171
+ getArtifactNames,
172
+ getArtifact,
173
+ close,
174
+ });
175
+ }