-
Notifications
You must be signed in to change notification settings - Fork 206
/
device-vat-admin.js
330 lines (288 loc) · 12.9 KB
/
device-vat-admin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
import { Nat } from '@endo/nat';
import { assert } from '@endo/errors';
import { kunser } from '@agoric/kmarshal';
import { buildSerializationTools } from '../lib/deviceTools.js';
import { insistVatID } from '../../lib/id.js';
/*
The "vatAdmin device" manages code bundles, meters, and the creation /
maintenance of dynamic vats.
Code bundles can be used to define a new dynamic vat
(vatAdminSvc~.createVat), or can be imported/evaluated from within a vat
(importBundle).
Bundles are 'endoZipBase64' archives, which are basically a string-encoded
ZIP file containing a bunch of importable modules and a single "compartment
map". Each bundle has a unique "bundleID" string, which is the SHA512 hash of
the compartment map file, aka the manifest. This manifest contains hashes of
all the component modules, as well as information about how they link
together, so the bundle's behavior is completely specified by the bundleID
(plus the runtime behavior of the module loader, which includes global
endowments, and potentially "holes" in the module map which will be filled in
by the loader with locally-provided module namespace objects). The order of
the modules in a zip file does not affect the bundleID.
Any two bundles are likely to have a lot of modules in common (shared support
libraries, etc). Modules are individual source files, so they tend to be
small (1-50kB), but bundles tend to be several MB in size.
The goals are:
* install bundles "out of band", through controller.validateAndInstallBundle()
* keep large (multi-MB) bundles out of vat messages and transcripts
* reduce communication bandwidth by uploading shared modules only once
* reduce storage costs by storing shared modules only once
The kernel will provide this device with three endowments:
* hasBundle(bundleID) -> boolean
* getBundle(bundleID) -> string
* getNamedBundleID(name) -> bundleID
The root device node offers two methods to callers:
* D(devices.bundle).getBundleCap(bundleID) -> devnode or undefined
* D(devices.bundle).getNamedBundleCap(name) -> devnode or undefined
The device node returned by getBundleCap() is called, unsurprisingly, a
"bundlecap". Most vats interact with bundlecaps, not bundleIDs (although of
course somebody must call `getBundleCap()` first). Holding a bundlecap
guarantees that the bundle contents are available, since `getBundleCap()`
will fail unless the bundle is currently installed. When we implement
refcounting GC for bundles, the bundlecap will maintain a reference and
protect the bundle data from collection.
The bundlecap device node provides two device-invocation methods to callers:
* D(bundleCap).getBundleID() -> string (bundleID)
* D(bundleCap).getBundle() -> string (the bundle contents)
For now, the only way to give a bundle to `importBundle()` is to obtain its
string contents first, but eventually we hope to implement more direct
support and keep the large string out of syscall results and userspace
entirely.
Meters are defined by the wrapper vat, who talks to us with a meterID
instead.
Dynamic vats are created with either a bundlecap (which we convert into a
bundleID before submitting to the kernel), or (temporarily) a full bundle.
*/
/**
* @typedef { import('../../types-external.js').Bundle } Bundle
* @typedef { import('../../types-external.js').BundleCap } BundleCap
* @typedef { import('../../types-external.js').BundleID } BundleID
*/
/**
* if you're into types, this might loosely describe devices.vatAdmin
*
* @typedef { string } MeterID
* @typedef { string } VatID
* @typedef { string } UpgradeID
* @typedef {{
* changeOptions: (vatID: string, options: {}) => void;
* createMeter: (remaining: bigint, threshold: bigint) => MeterID
* createUnlimitedMeter: () => MeterID
* addMeterRemaining: (meterID: MeterID, delta: bigint) => void
* setMeterThreshold: (meterID: MeterID, threshold: bigint) => void
* getMeter: (meterID: MeterID) => { remaining: bigint, threshold: bigint }
* createByBundle: (bundle: Bundle, options: {}) => VatID
* createByBundleID: (bundleID: BundleID, options: {}) => VatID
* getBundleIDByName: (name: string) => string;
* upgradeVat: (vatID: string, bundleID: BundleID, _vatParameters: unknown, upgradeMessage: string) => UpgradeID;
* terminateWithFailure: (vatID: VatID, reason: {}) => void
* getBundleCap: (bundleID: BundleID) => BundleCap
* getNamedBundleCap: (name: string) => BundleCap
* }} VatAdminRootDeviceNode
*/
export function buildDevice(tools, endowments) {
const {
hasBundle,
getBundle,
getNamedBundleID,
meterCreate,
meterAddRemaining,
meterSetThreshold,
meterGet,
} = endowments;
const { syscall } = tools;
const dtools = buildSerializationTools(syscall, 'bundle');
const { unserialize, returnFromInvoke, deviceNodeForSlot } = dtools;
const ROOT = 'd+0';
const bundleIDRE = new RegExp('^b1-[0-9a-f]{128}$');
const nextDeviceNodeIDKey = 'nextDev';
// reminder: you may not perform vatstore writes during buildDevice(),
// because it runs on each kernel reboot, which is not consistent among
// members of a consensus kernel
function allocateDeviceNode() {
let s = syscall.vatstoreGet(nextDeviceNodeIDKey);
if (!s) {
s = '1';
}
const id = BigInt(s);
syscall.vatstoreSet(nextDeviceNodeIDKey, `${id + 1n}`);
return `d+${id}`;
}
function returnCapForBundleID(bundleID) {
assert(bundleID, 'returnCapForBundleID needs bundleID');
const idToBundleKey = `id.${bundleID}`;
let cap;
cap = syscall.vatstoreGet(idToBundleKey);
if (!cap) {
if (!hasBundle(bundleID)) {
return returnFromInvoke(undefined);
}
cap = allocateDeviceNode();
syscall.vatstoreSet(idToBundleKey, cap);
const capToIDKey = `slot.${cap}`;
syscall.vatstoreSet(capToIDKey, bundleID);
}
return returnFromInvoke(deviceNodeForSlot(cap));
}
// invoke() should use unserialize() and returnFromInvoke. Throwing an
// error will cause the calling vat's D() to throw.
const dispatch = {
invoke: (dnid, method, argsCapdata) => {
if (dnid === ROOT) {
// Meters
// D(devices.vatAdmin).createMeter(remaining, threshold) -> meterID
if (method === 'createMeter') {
/** @type {any} */
const args = unserialize(argsCapdata);
const [remaining, threshold] = args;
assert.typeof(remaining, 'bigint', 'createMeter() remaining');
assert.typeof(threshold, 'bigint', 'createMeter() threshold');
const meterID = meterCreate(Nat(remaining), Nat(threshold));
return returnFromInvoke(meterID);
}
// D(devices.vatAdmin).createUnlimitedMeter() -> meterID
if (method === 'createUnlimitedMeter') {
const meterID = meterCreate('unlimited', 0n);
return returnFromInvoke(meterID);
}
// D(devices.vatAdmin).addMeterRemaining(meterID, delta)
if (method === 'addMeterRemaining') {
/** @type {any} */
const args = unserialize(argsCapdata);
const [meterID, delta] = args;
assert.typeof(meterID, 'string', 'addMeterRemaining() meterID');
assert.typeof(delta, 'bigint', 'addMeterRemaining() delta');
meterAddRemaining(meterID, Nat(delta));
return returnFromInvoke(undefined);
}
// D(devices.vatAdmin).setMeterThreshold(meterID, threshold)
if (method === 'setMeterThreshold') {
/** @type {any} */
const args = unserialize(argsCapdata);
const [meterID, threshold] = args;
assert.typeof(meterID, 'string', 'setMeterThreshold() meterID');
assert.typeof(threshold, 'bigint', 'setMeterThreshold() threshold');
meterSetThreshold(meterID, Nat(threshold));
return returnFromInvoke(undefined);
}
// D(devices.vatAdmin).getMeter(meterID) -> { remaining, threshold }
if (method === 'getMeter') {
/** @type {any} */
const args = unserialize(argsCapdata);
const [meterID] = args;
assert.typeof(meterID, 'string', 'getMeter() meterID');
return returnFromInvoke(meterGet(meterID));
}
// Vat Creation
// D(devices.vatAdmin).createByBundle(bundle, options) -> vatID
if (method === 'createByBundle') {
const res = syscall.callKernelHook('createByBundle', argsCapdata);
const vatID = kunser(res);
assert.typeof(vatID, 'string', 'createByBundle gave non-VatID');
return returnFromInvoke(vatID);
}
// D(devices.vatAdmin).createByBundleID(bundleID, options) -> vatID
if (method === 'createByBundleID') {
const res = syscall.callKernelHook('createByID', argsCapdata);
const vatID = kunser(res);
assert.typeof(vatID, 'string', 'createByID gave non-VatID');
return returnFromInvoke(vatID);
}
// D(devices.vatAdmin).upgradeVat(vatID, bundleID, vatParameters, upgradeMessage) -> upgradeID
if (method === 'upgradeVat') {
const args = kunser(argsCapdata);
assert(Array.isArray(args), 'upgradeVat() args array');
assert.equal(args.length, 4, 'upgradeVat() args length');
const [vatID, bundleID, _vatParameters, upgradeMessage] = args;
insistVatID(vatID);
assert.typeof(bundleID, 'string', 'upgradeVat() bundleID');
assert.typeof(
upgradeMessage,
'string',
'upgradeVat() upgradeMessage',
);
const res = syscall.callKernelHook('upgrade', argsCapdata);
const upgradeID = kunser(res);
assert.typeof(upgradeID, 'string', 'upgradeVat gave non-upgradeID');
return returnFromInvoke(upgradeID);
}
// D(devices.vatAdmin).terminateWithFailure(vatID, reason)
if (method === 'terminateWithFailure') {
const args = kunser(argsCapdata);
assert(Array.isArray(args), 'terminateWithFailure() args array');
assert.equal(args.length, 2, 'terminateWithFailure() args length');
const [vatID, _reason] = args;
assert.typeof(vatID, 'string', 'terminateWithFailure() vatID');
syscall.callKernelHook('terminate', argsCapdata);
return returnFromInvoke(undefined);
}
// D(devices.vatAdmin).changeOptions(vatID, options)
if (method === 'changeOptions') {
const args = kunser(argsCapdata);
assert(Array.isArray(args), 'changeOptions() args array');
assert.equal(args.length, 2, 'changeOptions() args length');
const [vatID, _reason] = args;
assert.typeof(vatID, 'string', 'changeOptions() vatID');
syscall.callKernelHook('changeOptions', argsCapdata);
return returnFromInvoke(undefined);
}
// Bundlecaps
// D(devices.bundle).getBundleCap(id) -> bundlecap
if (method === 'getBundleCap') {
const args = unserialize(argsCapdata);
/** @type {any} */
const [bundleID] = args;
assert.typeof(bundleID, 'string', 'getBundleCap() bundleID');
assert(bundleIDRE.test(bundleID), 'getBundleCap() not a bundleID');
return returnCapForBundleID(bundleID);
}
// D(devices.bundle).getNamedBundleCap(name) -> bundlecap
if (method === 'getNamedBundleCap') {
const args = unserialize(argsCapdata);
/** @type {any} */
const [name] = args;
assert.typeof(name, 'string', 'getNamedBundleCap() name');
let bundleID;
try {
// this throws on a bad name, so make a better error
bundleID = getNamedBundleID(name);
} catch (e) {
throw Error(`unregistered bundle name '${name}'`);
}
return returnCapForBundleID(bundleID);
}
// D(devices.bundle).getBundleIDByName(name) -> id
if (method === 'getBundleIDByName') {
const args = unserialize(argsCapdata);
/** @type {any} */
const [name] = args;
assert.typeof(name, 'string', 'getBundleIDByName() name');
let bundleID;
try {
// this throws on a bad name, so make a better error
bundleID = getNamedBundleID(name);
} catch (e) {
throw Error(`unregistered bundle name '${name}'`);
}
return returnFromInvoke(bundleID);
}
throw TypeError(`target[${method}] does not exist`);
}
const capToIDKey = `slot.${dnid}`;
const bundleID = syscall.vatstoreGet(capToIDKey);
if (bundleID) {
// D(bundleCap).getBundleID() -> id
if (method === 'getBundleID') {
return returnFromInvoke(bundleID);
}
// D(bundleCap).getBundle() -> bundle
if (method === 'getBundle') {
return returnFromInvoke(getBundle(bundleID));
}
throw TypeError(`bundlecap[${method}] does not exist`);
}
throw TypeError(`unknown device node ${dnid}, shouldn't happen`);
},
};
return dispatch;
}