From b57ca533a75056d5962cf6b39fc8a2b1d2613c77 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 27 Aug 2019 17:28:48 -0700 Subject: [PATCH 01/16] document new objectID/promiseID management scheme --- docs/delivery.md | 1774 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 1522 insertions(+), 252 deletions(-) diff --git a/docs/delivery.md b/docs/delivery.md index fae671e11ff..7a5cf8cf57a 100644 --- a/docs/delivery.md +++ b/docs/delivery.md @@ -1,336 +1,1606 @@ A walkthrough of how messages are passed from one Vat to another. -## Descriptive Conventions - -Numbers are suffixed in this description to indicate which numberspace they -live in, but these suffixes never appear in the implementation. +Each SwingSet machine contains one or more Vats, all talking to a shared +Kernel (and *not* directly to each other). Most Vats are isolated and can +*only* talk to the kernel. Vats correspond to userspace processes in a unix +system, and the SwingSet kernel is very much like the unix kernel which +supports those processes. + +Vats contain some application-specific code (named "Vat Code"), which +corresponds to the unix program written in C or some other language. For +SwingSet, most Vat Code is in the SES subset of Javascript, using orthogonal +persistence, native platform Promises, and making eventual-send calls to +local or remote objects with either the `E()` wrapper `p=E(x).foo(args)` or +(eventually) the infix-bang syntax `p=x!foo(args)`. Other forms of Vat Code +could exist, such as non-orthogonal (database/ORM-based) persistence, or in a +non-SES language (e.g. a WASM box). + +Below the Vat Code, but still inside the Vat, there is a support layer which +translates eventual-sends into kernel syscalls, and manages persistence. This +corresponds to the `libc` layer in a unix process: user code does not invoke +syscalls directly, but instead it calls standard library functions like +`write()` which wrap those syscalls. + +When the vat needs to send a message of some sort, it invokes one of the +kernel's `syscall` functions (the vat receives a `syscall` object with three +methods). To ensure that Vats are isolated from the kernel, and to build a +deterministic transcript, these syscalls are limited to taking pure data as +arguments (i.e. everything could be JSON serialized into a single string, +without loss of correctness). The primary syscall is named `syscall.send()`. + +Each Vat turn is initiated by the kernel invoking a `dispatch` function: the +Vat is defined by a `dispatch` object with two or three methods, which all +close over the vat's internal state. The primary one is `dispatch.deliver()`, +which delivers the messages produced by some other vat when it does +`syscall.send()`. The arguments to `deliver` are also pure data, and are +recorded in a transcript to enable replay-based orthogonal persistence. + +To enable transactional commitments, the syscalls do not return any data. +During the turn, the kernel records the syscall name and arguments in a +queue, but does not execute them (no kernel state is modified during a turn). +Turns might fail because of a fatal error (e.g. addressing a non-existent +object or promise, or resolving a promise that is supposed to be decided by +some other vat), or the turn might be interrupted by an out-of-gas error (in +which case it could be replayed or restarted after a Keeper supplies more +funds). If/when the turn completes successfully, the queued syscalls are +executed and committed as an atomic set. + +All `dispatch` functions can schedule near-term work by using +`Promise.resolve()` to append something to the promise queue. This work will +be completed after the current stack unwinds, but before the turn completes +and the `dispatch` is retired. This allows Vats to use eventual-send +internally, to protect against plan-interference hazards. The kernel waits +for a `setImmediate` on the timer/IO queue before proceeding to the next +message, giving these promise-queue actions a chance to complete. -We use non-overlapping number ranges to avoid confusion. In the actual -implementation, each numberspace is independent (so "1" will be allocated in -each space, with different meanings). Allocations will probably be tightly -packed, beginning at 0 or 1 and always allocating the smallest available -index, to enable constant-time lookup. - -* argSlots index values: 0-9i -* A's imports: negative 10-19a -* A's exports: positive 20-29a -* B's imports: negative 30-39b -* B's exports: positive 40-49b -* C's imports: negative 50-59b -* C's exports: positive 60-69b +``` ++-- Vat A ---+ +- Vat B --+ +- Vat C --+ +| | | | | | +| vat | | vat | | vat | +| code | | code | | code | +| | | | | | +| | | | | | +| -------- | | -------- | | -------- | +| | | | | | +| support | | support | | support | +| layer | | layer | | layer | +| | ^ | | | | | ++-syscall | -+ +-syscall--+ +-syscall--+ ++-|-dispatch-+----+-dispatch-+------+-dispatch-+---------+ +| v | | | | | | | +| clists | | clists | | clists | | +| | | | | | | +| | +| >-v | +| run-queue object-table event-loop | | | +| promise-table ^-< | +| | ++-------------------- Kernel ----------------------------+ +``` +## Vat Object Types + +The `syscall`/`dispatch` API references two kinds of identifiers: `Object` +and `Promise`: + +* `Object`: a callable object (the usual notion of "object" in the ocap + discipline), with methods of various names and private state. These may be + called a "Presence", or a "pass-by-presence" object in some contexts. +* `Promise`: a placeholder for a value which will be determined later + +These Object/Promise identifiers must be handled by the translation layer at +the bottom of each Vat. Upper-layer Vat Code is supposed to be +object-capability -safe, which means it must not be able to forge access to +remote objects by creating new identifiers from nothing. Hence the +upper-layer Vat Code will not generally see these values, as they'll be +wrapped in Presences or platform-native `Promise` objects of some sort, or +stored in a callback function that runs when a platform-native promise is +resolved. + +Both identifiers use integer index values to distinguish between different +instances. When the Vat allocates the identifier, it uses a positive integer. +When the kernel does the allocation, the vat gets a negative integer. This +index always points into a Capability List (the "c-list", described below). + +In some cases, the kernel (or the vat) will allocate an entry in the c-list +when it receives an index that it has not seen before. In other cases, the +index must already be present in the c-list, otherwise it is an error. + +Each Object lives in a specific Vat. The types are named from the perspective +of the Vat: when a Vat references one of its own objects in a syscall, this +is called an Export (exported from the Vat into the Kernel). When a Vat is +told about an object from some other Vat, we call it an Import (imported from +the Kernel into the receiving Vat). + +The basic asymmetry of Vats and Kernels enables a single name to be used for +each type, independent of which direction it is being sent. The "Comms Vats", +described later, do not have this asymmetry (two comms vats talking to each +other are peers), so the names used on that interface *do* depend on the +direction of delivery, and we will use different names over there to avoid +confusion. + +The "resolution authority" for each unresolved Promise either resides in one +specific Vat (known as the "Decider"), or in a message queued within the +kernel (so there is currently no Decider). The Decider Vat, if any, is the +only one which can resolve that Promise. All other Vats can subscribe to hear +about the resolution, but they cannot control it. + +Two places in the API expect or provide a `Promise` identifier in a resolving +capacity: the `result` of a Message arriving in a vat via +`dispatch.deliver()`, and the first argument of a `syscall.resolve()`. These +function signatures take a `Promise` identifier, but the Vat must own the +Decider authority for that Promise. Otherwise the Vat will be terminated. + +Using Rust syntax to capture the types correctly, the Vat-side API has the +following types: -From A's point of view, negative numbers are imports, positive numbers are -exports. We use this polarity so that the deliver(facetID) calls into Vats -can use positive facetID values. +``` +struct ObjectID(i32); +struct PromiseID(i32); +enum CapSlot { + Promise(PromiseID), + Object(ObjectID), +} +struct CapData { + body: Vec, + slots: Vec, +} +struct Message { + method: String, + args: CapData, + result: Option, +} +enum Resolution { + Fulfill(ObjectID), + Forward(PromiseID), + Data(CapData), + Reject(CapData), +} +``` -## Kernel Slot Table +## Kernel Object Types + +For each Vat type, there is a matching kernel type. These are distinct +values, with conversion from one to the other happening at the +syscall/dispatch boundary. Some values are identical (the `body` string is +left untouched), but the object/promise identifiers must be mapped through +the clist tables. The kernel's `ObjectID(5)` may refer to a completely +different object than Vat A's `ObjectID(5)`. Keeping them in different types +helps avoid mistakes. (And note that Vat A's `ObjectID(5)` is probably +unrelated to Vat B's `ObjectID(5)`). + +The kernel maintains two tables to handle the identifiers which appear in Vat +syscalls: one for Objects (Presences), and a second for Promises. Each table +is indexed by a kernel-allocated integer. The rows have types named +`KernelObject` and `KernelPromise`, so the keys are named `KernelObjectID` +and `KernelPromiseID`. These keys are positive integers: all Objects come +from some Vat, and all Promises are managed by the kernel, so from within the +kernel, there is no notion of import-vs-export. + +Each row of the kernel Object table remembers the VatID of the owner of that +object: the one which first exported it into the kernel in the argument of a +`syscall.send` or `syscall.resolve`. Messages sent to this object from other +Vats (via `syscall.send`) must be routed to the owning Vat and delivered with +a `dispatch.deliver`. + +Each row of the kernel Promise table remembers the current promise state and +any related data. There is one unresolved state, and four resolved states +(however we may be able to optimize away some of them, e.g. by rewriting data +in other tables). Each contains some additional state-specific data: + +* `Unresolved`: includes an optional Decider VatID, list of subscribers + (VatIDs), and queue of pending messages +* `Fulfilled`: includes an ObjectID (an Export) to which it was resolved +* `Data`: includes resolution data (body+slots) +* `Rejected`: includes the rejection data (body+slots, maybe an Error object) +* `Forwarded`: includes the `KernelPromiseID` to which it was forwarded + +The kernel also maintains a "run-queue", which is populated with pending +deliveries, each of which references a variety of kernel-side objects. + +Finally, the kernel maintains c-list (Capability List) tables for each Vat, +which map vat-side references into kernel Object/Promise references. For each +vat, there are two bidirectional tables: Objects (Imports and Exports) map to +the kernel Object table, and Promise IDs map to the kernel Promise table. -Each Vat has a table that maps from their slots to other Vat's slots. This -table maps in both directions: +``` +struct VatID(u32); -`kernelSlots[fromVatID] -> ( fromSlotID <-> { toVatID, toSlotID } )` +struct KernelObjectID(u32); +struct KernelPromiseID(u32); -We call the mapping from `fromSlotID` to `{ toVatID, toSlotID }` as -"forward", and the one from `{ toVatId, toSlotID }` to `fromSlotID` as -"backward". +struct KernelObject { + owner: VatID, +} +struct KernelObjectTable { + objects: HashMap, + next_id: u32, +} -`fromSlotID` is always negative (an import for `fromVat`), `toSlotID` is -always positive (an export of `toVat`). Positive numbers are exports, -negative numbers are imports. +// the kernel has types like Message, CapData, and CapSlot, which +// correspond to Vat types with the same names + +enum KernelPromise { + Unresolved { + subscribers: Vec, + decider: Option, + queued_messages: Vec, + } + FulfilledToTarget(KernelObjectID), + FulfilledToData(CapData), + Rejected(CapData), +} -From the kernel's point of view, the `fromSlotID` column matches the sending -Vat ("hi sending vat, I'm the kernel, when you talk about slot -1a, I'm going -to map that to something else"). +struct KernelPromiseTable { + promises: HashMap + next_id: u32, +} -(If the toVatID/toSlotID pair were linked to a single object, that could be -used for refcounting to notify toVat when its export is no longer referenced, -for GC.) +// these two are the same as the Vat's ObjectID/PromiseID +struct VatPromiseID(i32); +struct VatObjectID(i32); -When a vat does `send` syscall, the target is always to one of that vat's -imports, and the targetSlotID must be mapped to a toVatID/toSlotID pair, so -we can use toSlotID as facetID in the subsequent `deliver` invocation. +struct VatCList { + objects: HashMap, + next_object_id: u32, + promises: HashMap, + next_promise_id: u32, +} -When a send cites an export, we must find (or add) an entry in the receiving -Vat's table, allocating an import slotID if necessary. To make sure the same -export is delivered a second time with the same slotID, we must be able to -look up the reverse direction -(`slots[rxVatID].backward[{senderVatID,senderSlotID}]`). +struct VatData { + clists: VatCList, + enablePipelining: bool, +} -When a send cites an import, we must map it to toVatID/toSlotID. If this is -an export of toVatID (arg being sent back home), we just cite the matching -toSlotID in the deliver(). If it is an export of some other vat, we do a -three-party link, which adds an import to toVatID. +struct KernelCLists { + vats: HashMap, +} +``` -## Vat import/export table +`KernelObject` and `KernelPromise` rows are retained as long as they are +referenced by any Vat C-Lists, any `CapData` structures or `Resolution` +targets in the Promise table, or any target/data/result in the run-queue. +When the last reference is removed, the row can be deleted. The ID could also +be recycled, but it seems less confusing to simply retire the number. + + +## Vat Message Types + +We use the term `CapData` to mean a piece of data that can include capability +references. Each reference is known as a `CapSlot`. The data is serialized +into our [augmented form of JSON](https://github.com/agoric/marshal), which +uses special `@qclass` keys to represent things not normally expressible by +JSON (such as `NaN`, `-0`, `Infinity`, `undefined`, BigInts, and `CapSlot` +references). Each appearance of a `CapSlot` causes a Vat reference (`Object` +or `Promise`) to be added to a list named `slots`, and a reference to the new +slot index gets inserted into the JSONified data structure . The serialized +`CapData` thus consists of the JSON-encoded string (named `body`) and the +list of slots (named `slots`). As this `CapData` travels from one Vat, into +the kernel, and off to some other vat, the `body` remains untouched, but the +`slots` are remapped at each vat/kernel boundary. + +A `Message` is the method invocation first given to `syscall.send` for +transmission to some other Vat, then stored in the kernel run-queue, then +finally arriving at the target vat inside a `dispatch.deliver` call. The +Message includes the method name which should be invoked, the `CapData` +arguments to be included, and an optional result identifier (a Promise). The +SwingSet calling model has only positional (not keyword) arguments, hence the +`CapData.body` always deserializes to an array. + +The Message does not include the target, since that changes over time (it +might be sent first to an unresolved Promise, then queued on the decider Vat, +then that Promise might get forwarded to some other promise, etc). The +Message includes the result identifier because it remains tied to the Message +even as it gets queued and forwarded from one place to another. + +## Result Promise Management + +The Vat Message `result` identifier, if present, must refer to a Promise for +which the sending Vat has resolution authority. There are three +possibilities, and `syscall.send` will reject the message (terminating the +Vat) unless the `result` ID falls into one of these categories: + +* A brand new Promise was created just for the result slot. The ID will be a + positive integer that is not already in the c-list. +* The Promise was created by this Vat earlier, and it has never been used as + a result or a resolution. The ID will be a positive integer that is already + present in the c-list, and the Decider will point at this Vat. +* The Promise was received from the kernel earlier as the result slot of an + incoming message. The ID will be a negative integer, and the Decider will + point at this Vat. + +In all cases, the kernel promise table winds up with a Promise for which the +Decider is cleared, as the resolution authority now resides in the queued +message. When the message gets to the front of the queue and is delivered to +some Vat, the Decider is changed to point at that Vat. If that Vat sends the +Promise back into the kernel as the result slot of some further message, the +Decider is cleared again. If the Vat resolves the Promise instead, the state +changes to `Resolved` and the resolution authority has been consumed. + +The only way to shed resolution authority is to either put the promise ID +into the result slot of an outbound message (which transfers the authority to +the run queue, and sets `Decider` to `None`), or to consume it by using the +ID in a `syscall.resolve()`. The only way to acquire resolution authority is +to either create a new Promise (by passing the kernel a new positive +`PromiseID`, inside a `CapData` slot), or to receive it in the result slot of +an inbound message. + +### Pipelining + +Promise Pipelining is the practice of sending one message to the Decider of +the Promise that came back from the transmission of an earlier message. It is +a vital latency-reduction tool for sending multiple messages to a distant +machine. + +In SwingSet, pipelining is most (only?) useful on the Comms Vat. The local +kernel shares a host with the local vats, so the latency is minimal. However +two messages aimed at the same remote machine, through the Comms Vat, would +suffer unnecessary roundtrips unless the second can be delivered earlier. So +each Vat, when it is added, can include an option flag that says "I want to +received pipelined messages early". The default, used by everything except +the Comms Vat, means "I want the kernel to queue those messages, not me". + +```js +const config = await loadBasedir(basedir); +config.vatSources.set('comms', getCommsSourcePath()); +config.vatOptions.set('comms', { enablePipelining: true }); +``` -Each Vat, in its comms library, maintains an internal table which maps two -sets of objects: +(open question: another option would be for `dispatch.deliver()` to return a +special value that means "kernel please queue this for me instead") + +When a message that targets an unresolved promise comes to the front of the +run-queue, the Decider is examined. If the deciding vat has opted in to +pipelined deliveries, `dispatch.deliver()` is invoked and the message is sent +into the deciding Vat, which (in the case of the comms Vat) can transmit the +message to the remote machine where the target will be resolved. + +If a non-comms Vat enables pipelining, it is obligated to queue the pipelined +messages inside the Vat until the target is resolved. When that resolution +happens, the new target might be in some other Vat, in which case the +deciding Vat must re-submit all those messages back into the kernel. Vats may +prefer to avoid deserializing the messages until their resolution is known, +to avoid a wasteful reserialization cycle. + +If the deciding Vat has *not* opted into pipelining, the messages are queued +in the kernel's Promise Table entry instead. They remain there until the +deciding vat uses `syscall.resolve()` to resolve that Promise. At that point, +the behavior depends upon the type of resolution; see the discussion of +`syscall.resolve()` below for details. + +When the initial pair of messages are submitted with `syscall.send()`, the +run-queue will have two pending deliveries: the first is targeting an object +in some Vat, and the second targets a Promise. Until the first message is +delivered, the result Promise has no Decider, so the second message cannot be +delivered. But that's ok, because by the time the second message gets to the +front of the run queue, the first will have been delivered, setting the +Decider of the result Promise to some vat, providing a place to deliver the +second one (or the knowledge that the vat wants the kernel to queue it +instead). + +When we implement Flows or escalators or some other priority mechanism, we +must ensure that we don't try to deliver any message until all its +dependencies (including the target) have been resolved to some particular +vat. A pipelining vat would learn (and probably be able to use) the Meter +attached to the pipelined messages, whereas if these messages are queued in +the kernel, the decider vat would not get access to those Meters. + +### Result Promise Summary + +* Allocating a new Promise in `CapData` creates resolution authority, sending + the Promise in `.result` gives it away, getting a Promise in + `.result` acquires it, and using the Promise in `resolve()` consumes it. +* The kernel will clear the Decider of the Promise while the message sits on + the run-queue. +* If the inbound Message has a `result`, the receiving Vat will be the + Decider for the corresponding Promise. + +When a pipelining-aware Vat resolves a Promise, and then forwards the +previously queued messages which it received before that resolution, it can +return the Message objects unchanged back into the kernel (with +`syscall.send`), keeping the `result` identifiers exactly the same. -* imports have negative numbers and map to/from proxies which support method - invocations, which are sent to the external Vat for execution -* exports have positive numbers and map to/from local objects that can be - invoked +## Descriptive Conventions -Both types of objects might show up in the arguments of a method call. Only -imports can be used as the target of execution (since invoking an exported -object is just a local invocation, and doesn't involve the kernel) (but TODO -serializing a Vat with a queued call to an internal object may require -treating everything as external). +The Objects and Promises are represented in debug logs with a single-letter +prefix (`o` or `p`), a sign, and a number. They also include a Vat ID prefix +(`vNN.`). So when Vat 2 does a `syscall.send` that targets an import (a +kernel-allocated Object identifier, hence negative), and includes an argument +which is a local object (an export, hence positive), and specifies a result +that is a new local Promise, the logs might say `v2.send(target=o-4, +msg={name: foo, slots:[o+3], result=p+5})`. The Promise that results from +this `send` is also labelled `p+5`. -We can use a single table for both, since they use different signs for their -index values. This allows incoming messages to refer to either type with the -same values. +The kernel types use `ko` and `kp`. The run-queue for that message would be +printed as `target=ko6, msg={name: foo, slots:[ko2], result=kp8}`. -`vatSlots[negativeIndex] <-> proxy` +The Comms Vat creates inter-machine messages that refer to Objects and +Promises in per-remote-machine C-List tables that live inside each Comms Vat. +These identifiers use `ro` and `rp` as prefixes. -`vatSlots[positiveIndex] <-> localObject` +The Javascript SwingSet implementation uses these actual strings as keys in +the arguments and the C-list tables. In other languages, they could be +represented by a tagged union whose only real member is an integer. +Each Vat's numberspace is independent (so "1" could nominally be allocated in +each space, with different meanings). To avoid confusion, we start each Vat's +numberspace with a different offset (todo: 1000 times the vat number). -## Initial state -A has a (magically created) reference to B's `bob` object and to C's `carol` -object. No other references. +## Kernel-Side C-Lists -A has a proxy named `bob` which remembers `slot=-10a`, and `carol` with -`slot=-11a` +For each Vat, the Kernel maintains a set of Capability-List structures +(c-lists), which translate between vat-side identifiers and kernel-side +identifiers. Depending upon the operation, this translation might insist that +the value is already present in the table, or it might allocate a new slot +when necessary. -`vatSlotsA`: +C-lists map Vat object/promises to kernel object/promises. After Vat 2 sends +this message into the kernel: -| index | target | -|------:|------------| -| -10a | proxyBob | -| -11a | proxyCarol | +``` +v2.send(target=o-4, msg={name: foo, slots:[o+3], result=p+5}) +``` +.. the Vat-2 clist might contain: -`vatSlotsB`: +| Vat 2 object | Kernel object | Allocator | Decider | +| --- | --- | --- | --- | +| `o-4` | `ko6` | v3 | N/A | +| `o+3` | `ko2` | v2 | N/A | +| `p+5` | `kp8` | N/A | none | -| index | target | -|------:|--------| -| 40b | bob | +## Syscall/Dispatch API +The full API (using Rust syntax to capture the types correctly) is: +``` +trait Syscall { + fn send(target: CapSlot, msg: Message); + fn subscribe(id: PromiseID); + fn resolve(subject: PromiseID, to: Resolution); +} + +trait Dispatch { + fn deliver(target: CapSlot, msg: Message); + fn notify(subject: PromiseID, to: Resolution); +} +``` -`kernelSlots`: +There are a few restrictions on the API: + +* The kernel will never send messages to the wrong place: the `target=` in a + `dispatch.deliver()` will always be owned by the receiving Vat (either an + ObjectID allocated by this Vat, or a PromiseID for which this Vat is the + Decider). +* The `Message.result` in a `syscall.send` must either be a new vat-allocated + (positive) PromiseID, or a previously-allocated PromiseID for which the + calling Vat is the Decider, or a PromiseID that was previously received as + the result of an inbound message. + +Some invocation patterns are legal, but unlikely to be useful: + +* `syscall.send(target=)` can be any `CapSlot`, however it is a bit silly to + reference an Object that lives on the local Vat, or a Promise for which the + local Vat is the Decider. In both cases, the Vat could have delivered the + message directly, instead of taking the time and effort of going through + the kernel's run-queue. On the other hand, this may achieve certain + ordering properties better. + + +## Outbound (Vat->Kernel) translation + +Inside the implementations of all syscall methods (`send` and `resolve`), the +Vat-specific argument slots are first mapped into kernel-space identifiers by +passing them through the Vat's c-list tables. If the Vat identifier is +already present in the table, the corresponding kernel identifier is used. If +it is not already present, the behavior depends upon which method and +argument it appeared in, as well as the category of identifier. + +This section describes the mapping rules for all API slots. If the mapping +signals an error, the sending vat is terminated, as there is no sensible +recovery from mapping errors. + + +### syscall.send() + +The target of a `syscall.send()` specifies where the message should be sent. +`Object` always maps to a `KernelObject`, and `Promise` always maps to a +`KernelPromise`. If the Vat object is not already in the c-list, the +following table describes what the mapping function does: + +| Vat Object | description | action if missing | +| --- | --- | --- | +| `Object (ID > 0)` | Export | allocate KernelObject (loopback) | +| `Object (ID < 0)` | Import | error | +| `Promise (ID > 0)` | local promise | allocate KernelPromise (loopback) | +| `Promise (ID < 0)` | remote promise | error | + +Any `CapSlot` objects inside `Message.args` are translated the same way. +Unlike `target=`, it is extremely common to include `args` that point at +Exports and local Promises. In either case, if a `KernelPromise` is +allocated, its state is marked as `Unresolved`, and its Decider is set to +point at the allocating vat. + +The `Message.result`, if present, must be a `PromiseID`, and will always map +to a `KernelPromise`: + +| Vat Object | action if missing | +| --- | --- | +| `Promise (ID > 0)` | allocate KernelPromise | +| `Promise (ID < 0)` | error | + +In addition, if the `KernelPromise` already exists, its Decider must point at +the Vat which invoked `syscall.send()`, or an error is thrown. The +`KernelPromise` decider field is cleared (set to `None`), because the message +itself now owns the resolution authority, not the sending vat. + +After mapping, the `send` is pushed onto the back of the kernel's run-queue. +Later, when this operation comes to the front, the kernel figures out how it +should be dispatched based upon the target (object or promise) and its +current state: + +| Target | State | action | +| --- | --- | --- | +| Object | n/a | deliver to owning Vat | +| Promise | Unresolved | deliver to Decider Vat, or queue inside promise | +| Promise | Fulfilled | look up fulfilled object, recurse | +| Promise | Forward | look up forwarded promise, recurse | +| Promise | Data | queue `CannotSendToData` rejection to result | +| Promise | Rejected | queue rejection data to result | + +The state of a Promise might change (from Unresolved to some flavor of +Resolved) between the message being placed on the queue and it finally being +delivered. + +As an optimization, when a Promise is fulfilled or forwarded, the kernel +could walk the run-queue and rewrite the targets of those pending deliveries. +This might enable the Promise entries to be removed earlier, and/or remove +some states from the table entirely. + +### syscall.subscribe() + +When a Vat receives a Promise (e.g. in `CapData`), it might use the Promise +as the target for message sends, or it might be interested in when and how it +resolves, or both. To reduce the noise of unwanted notifications, Vats will +not receive a `dispatch.notify()` for a Promise unless they first use +`syscall.subscribe()` to express their interest. + +The `PromiseID` argument to `subscribe()` is translated through the c-list +just like a `CapSlot` in `syscall.send()`. It is not common for this to cause +the allocation of a `KernelPromise`, because Vats don't usually subscribe to +hear about their own Promises, but it is legal. + +### syscall.resolve() + +The subject of a `syscall.resolve` must either be a new Promise ID, or a +pre-existing one for which the calling Vat is the Decider. The +`KernelPromise` must be in the `Unresolved` state. If any of these conditions +are not met, the Vat calling `resolve` will be terminated. It doesn't matter +whether the Promise was allocated by this Vat or a different one. + +| Vat Object | action if missing | +| --- | --- | +| `Promise (ID > 0)` | allocate KernelPromise | +| `Promise (ID < 0)` | error | + +The `resolution` has several forms, and we assign a different name to each. + +* `Fulfill(ObjectID)`: the Promise is "fulfilled" to a callable Object +* `Forward(PromiseID)`: the Promise is now "forwarded": it has not settled to + a specific object, but the original Promise is effectively replaced with + some other Promise +* `Data(CapData)`: the Promise is "fulfilled" to data, rather than a callable + object. It is an error to send messages to data. +* `Reject(CapData)`: the Promise is "rejected" to data which we call the + "error object". Sending a message to a Rejected Promise causes the result + of that message to be Rejected too, known as "rejection contagion". + +. Any `result` + promises in the queued messages should be rejected with the same `CapData` + provided as `resolution`. + +As the `syscall.resolve()` is processed by the kernel, all slots in the +`resolution` should be mapped just like the `Message` slots in +`syscall.send`. If the resolution is `Forward`, the new promise must be +different than the one being resolved, and must not result in a cycle. (TODO +could one Vat force a second one into unknowingly creating a cycle?). + +After mapping, the kernel processing `resolve()` modifies the kernel Promise +table to store the resolution (which clears the Decider pointer, because only +`Unresolved` promises have a Decider), and it queues `notify` deliveries for +all `subscribers`. These `notify` deliveries wait in the same kernel +run-queue as `send` deliveries, and contain a VatID and a kernel PromiseID. +When the `notify` reaches the front of the queue, the vat invoked with a +`dispatch.notify()` call that contains the current state of the promise. + +After queueing any `notify`s, if the Promise table holds any queued messages, +these must be dispatched according to the resolution type: + +* `Fulfill`: Re-queue all Messages to the new target object. The new + `PendingDelivery`s are appended to the back of the run-queue. +* `Forward`: All messages are re-queued to the new target promise. When they + get to the front, they may be delivered to the deciding vat (if it has + opted-in to pipelining) or queued in the new Promise's table entry. +* `Data`: the queued Messages are discarded, however if they have a `result` + promise, a `CannotSendToData` error object is created, and the results are + Rejected with that error object +* `Reject`: the queued Messages are discarded, but a copy of the rejection + data is used to Reject any `result` promises they included + +(todo: think about the ordering properties of the potential approaches) + +TODO: As an important performance optimization, the old now-Resolved Promise +is now removed from the resolving Vat's C-List table. The Vat knows what it +was resolved to, so it should never need to refer to it again in the future. +If the higher-layer vat code still retains the original native Promise and +includes it in an argument, the lower-level translation layer can create a +new promptly-resolved Promise for it. + +(TODO: `resolve()` is a good opportunity to remove the promise from the +resolving vat's c-list, however if we have queued messages, it must live long +enough to forward those messages to the new resolution. It would be nice to +keep the CannotSendToData logic in the kernel, and have the resolving Vat +just re-`send` everything in the queue in all resolution cases. If it +fulfills to an Export, would the extra queue delay violate our ordering +goals?) + +Finally, the kernel returns control to the Vat. + +## Kernel Run-Queue + +The Kernel's run-queue holds two kinds of pending operations: `Send` +(enqueued when a Vat does `syscall.send()`), and `Notify` (enqueued when one +does `syscall.resolve`). -| fromVatID | fromSlotID | toVatID | toSlotID | -|----------:|-----------:|--------:|---------:| -| A | -10a | B | 40b | -| | -11a | C | 50c | +``` +enum KernelSlot { + Object(KernelObjectID), + Promise(KernelPromiseID), +} +enum PendingOperation { + Send { + target: KernelSlot, + message: KernelMessage, + } + Notify { + subscriber: VatID, + subject: KernelPromiseID, + } +} +struct KernelRunQueue { + queue: VecDeque, +} +``` +When a `Send` or `Notify` reaches the front of the run-queue, the kernel must +figure out how to act upon the delivery. The simple cases are a `Send` to an +Object, which always causes a `dispatch.deliver()` to the owning Vat, and +`Notify`, which always performs a `dispatch.notify()` to the given +subscriber. -## First Message: no arguments +If the `Send` is to a Promise, the action depends upon the state of the +promise: + +| State | Action | +| --- | --- | +| Unresolved | queue inside Promise, or deliver() to decider vat | +| Forwarded | process according to target Promise | +| Fulfilled | deliver() to owner of fulfillment object | +| Data | resolve (reject) result to CannotSendToData error | +| Rejected | resolve (reject) result to rejection object | -A invokes `bob.foo()` to B, no arguments, no result promise. +If the Promise is `Unresolved`, the kernel looks at its `Decider` field, and +sees if the named Vat has opted in to pipelining or not. If so, it does a +`dispatch.deliver()` to the named Vat. If not, the message is queued inside +the kernel Promise object. -A does `syscall.send(targetSlot=-10a, method='foo', argsString='[]', slots=[])`. +If it is `Fulfilled` (to an object), the kernel acts as if the `Send` was to +the object itself. As a performance optimization, when a Promise is fulfilled +this way, the kernel could rewrite the run-queue to replace the `target` +fields with the object, and this case would never be encountered. -That uncurries to `baseSend(fromVatID=A, targetSlot=-10a, method='foo', argsString='[]', slots=[])`. +If the Promise has been fulfilled to data (which is not callable), or +explicitly rejected, the original message is discarded, as there is nobody to +accept it. However if the message requested a `result`, that result Promise +must be rejected, just as if decider Vat had called +`syscall.resolve(Reject(error))`. The rejection error is either a +`CannotSendToData` object (for `Data`), or a copy of the Promise's own +rejection object (for `Rejected`). -Kernel translates target slot (`[A, -10a]`) into `[B, 40b]`. No argument -slots to translate. +## Inbound (Kernel->Vat) translation -Kernel queues message `{ vatID=B, slotID=40b, method='foo', argsString='[]', vatSlots=[] }`. +Any slots in the operation must then be translated into identifiers that are +specific to the target Vat, by looking them up in the target Vat's C-List. If +they do not already exist in this C-List, a new (negative) index will be +allocated and added to the C-List. -Message gets to front of queue, kernel dispatches it. +### dispatch.deliver() -Kernel invokes `B.deliver(slotID=40b, method='foo', argsString='[]', argSlots=[])`. +The target argument of `dispatch.deliver()` specifies which Object (or +Promise) should get the message. When this references an Object, the Vat +identifier will always have a positive index, because the kernel will not +deliver a message to the wrong Vat. When it references a Promise, the Vat +will always be the Decider for that promise. -All slot tables remain the same. +Any `CapSlot` objects inside `Message.args` are translated into Vat slots. +The `Message.result`, if present, is always translated into a `PromiseID`, +and the Vat is given resolution authority over that promise. In the kernel +promise table, the "Decider" field is set to point at the Vat just before +`dispatch.deliver()` is called, enabling the Vat to call +`syscall.resolve(result)` during the turn. -## Second Message: multiple arguments, including reference to self +### no dispatch.subscribe() -A has a local pass-by-proxy object `alice`. +Vats are obligated to notify the kernel (via `syscall.resolve()`) about the +resolution of all Promises for which they are the Decider. The kernel promise +tables need to be updated even if no other Vats have subscribed. As a result, +there is no `dispatch.subscribe()` in the API, and the kernel is considered +to be "subscribed by default" to any promises for which the Vat has +resolution authority. -A invokes `bob.foo(alice, bob)`. Sending `bob` to `bob` is not very useful, -but illustrates the stability of table lookups. +### dispatch.notify() -A's `bob` proxy and/or the comms library in A, as it serializes the argument -graph `[alice, bob]`, notices that `alice` is referenced by the graph, but is -not currently being exported (it maintains an internal table `obj <-> -exportID`). It thus adds `20a -> alice` to its internal exports table. The -export ID "20a" is appended to the `argSlots` list (which is currently empty, -since this is the first time the serializer has encountered a pass-by-proxy -argument for this call). The argument is replaced with a reference to the -first entry in the `argSlots` list (i.e. "0i" since `argSlots` is 0-indexed). +The `subject` argument of `dispatch.notify()` specifies which Promise is +being resolved. It is always translated into a `PromiseID`. -The same library notices `bob` is referenced, and is present in the internal -`obj <-> importID` import table (proxies are always already present in this -table, since they were added when the reference was first received). The -matching importID "-10a" is appended to the `argSlots` list, and the index -(the second entry, "1i") is used in the serialization. +`dispatch.notify()` will be sent to all Vats which had subscribed to hear +about the resolution of that Promise. The Decider vat for a Promise will not +generally subscribe themselves, since they are the ones causing the Promise +to become resolved, so they have no need to hear about it from the kernel. -`vatSlots(A)`: +The `Resolution` value of `dispatch.notify()` may contain slots, which are +translated just like `Message.args`. -| index | target | -|------:|------------| -| 20a | alice | -| -10a | proxyBob | -| -11a | proxyCarol | +TODO: After a Vat receives notice of a Promise being resolved, we might +choose to remove that promise from the vat's c-list, and forbid the Vat from +ever mentioning that promise again. -A does: +A Vat might be informed that one Promise has been resolved to another Promise +(`Resolution::Forward`). This new Promise might be local, or imported. -``` -syscall.send(targetSlot=-10a, - method='foo', - argsString='[{@qclass: "ref", index: 0i}, - {@qclass: "ref", index: 1i}, - ]', - argSlots=[20a, -10a]) -``` +## Sample Message Delivery -Kernel translates target slot (`[A, -10a]`) into `[B, 40b]`. It leaves -`argsString` alone (the kernel never tries to parse that string). But it -walks `argSlots` to translate the A-output values into B-input ones. +This section follows a variety of messages are they are sent from Vat-1 to +Vat-2. -The first slot ("20a") is positive, so it is an export, and thus does not -need to appear in `kernelSlots[A].forward`. However we do need something in -`kernelSlots[B].forward` which maps to `[A, 20a]`. It checks -`kernelSlots[B].backward.has([A, 20a])` and sees that it is not yet present, -so it allocates a new import for B ("-30b" is the next available value). Both -directions are added: `kernelSlots[B].forward.set(-30b, [A, 20a])` and -`kernelSlots[B].backward.set([A, 20a], -30b)`. The first value of argSlots is -replaced with "-30b". +### no arguments, resolve to data -The second slot ("-10a") is negative, so it is an import, and must therefore -already be present in `kernelSlots[A].forward]`. It maps to `[B, 40b]`, which -is owned by the same vat as the message is targetting, so `argSlots` can just -use "40b" directly. +The initial conditions are that Vat-1 somehow has a reference to an export of +Vat-2 that we'll name `bob`. -`kernelSlots`: +* Kernel Object Table: + * `ko1` (bob): owner= vat-2 +* Kernel Promise Table: empty +* Kernel run-queue: empty +* Vat-1 C-List: + * `v1.o-1001 <-> ko1` (import of bob) +* Vat-2 C-List: + * `v2.o+2001 <-> ko1` (export of bob) -| fromVatID | fromSlotID | toVatID | toSlotID | -|----------:|-----------:|--------:|---------:| -| A | -10a | B | 40b | -| | -11a | C | 50c | -| B | -30b | A | 20a | +Vat-1 does `p1 = bob!foo()`. This causes the lower layers of Bob's vat to +allocate a new local promise/resolver ID (for the result `p1`), say it +chooses `104`, and then invokes `syscall.send(target=o-1001, msg={method: +"foo", args: "[]", slots=[], result=p+104})`. Vat-1 remembers `p+104` as the +identifier for the result Promise. We assume that Vat-1 uses `p1` later (i.e. +`p1.then(stuff..)`), so it also does a `syscall.subscribe(p+104)`. -The kernel then queues a message: ``` -{ vatID=B, - slotID=40b, - method='foo', - argsString='[{@qclass: "ref", index: 0i}, - {@qclass: "ref", index: 1i}, - ]', - argSlots=[-30b, 40b]) -} ++-- Vat 1 ---+ +- Vat 2 --+ +- Vat 3 --+ +| | | | | | +|p1=bob!foo()| | vat | | vat | +| | | code | | code | +| | | | | | +| | | | | | +| ---------- | | -------- | | -------- | +| | | | | | +| send() | | deliver | | support | +| | | | ^ | | layer | +| | | | | | | | ++----|-------+ +---|------+ +-syscall--+ ++----|-------+---+---|------+------+-dispatch-+-------+ +| v | | | | | | | +| clists | | clists | | clists | | +| | | | ^ | | | | +| | | | +| | | >-v | +| \-> run-queue --/ object-table event-loop | | | +| promise-table ^-< | +| | ++------------------- Kernel --------------------------+ ``` -When this message gets to the front of the queue, the kernel invokes: +The `syscall.send()` is mapped into the kernel through the Vat-1 C-List. The +target is translated to `ko1`, which is looked up in the kernel object table +to learn that the target is in `vat-2`. There are no arguments, so there are +no `VatSlots` to be translated. The `result` (`p+104`) is not present in the +C-List, so a new `KernelPromise` entry is allocated (say `kp24`), and the +Decider is set to None since it is being allocated in the context of a +`send()`'s `.result` field. + +The `Pending Send` is appended to the run-queue. + +* Kernel Promise Table: + * `kp24: state: Unresolved(decider: None, subscribers: [])` +* Kernel run-queue: + * `Send(target: ko1, message: {.. result=kp24})` +* Vat-1 C-List: + * `v1.o-1001 <-> ko1` (import of bob) + * `v1.p+104 <-> kp24` (export of result promise) + +The `syscall.subscribe(p+104)` causes the PromiseID to be looked up in the +kernel promise table, yielding `kp24`. Vat-1 is then added to the +`subscribers` list. + +* Kernel Promise Table: + * `kp24: state: Unresolved(decider: vat-2, subscribers: [vat-1])` + +The run-queue is cycled, and this Send comes to the top. This looks up +`target` in the kernel object table to find the owner (`vat-2`), which +indicates the c-list to use for translating the message. + +The target `ko1` is looked up in the Vat-2 C-List, and maps to `v2.o+2001`. +As a target, it must already be present in that C-List (the kernel will never +send a message to the wrong place). There are no arguments, so no `VatSlots` +to be translated. The `result` (`kp24`) is not present in the C-List, and its +owner (Vat-1) is different than the target (Vat-2), so a new entry is +allocated, and the vat gets `v2.p-2105`. The Decider for `kp24` is set to +`vat-2` because we're about to deliver the message to Vat-2. + +* Kernel Promise Table: + * `kp24: state: Unresolved(decider: vat-2, subscribers: [vat-1])` +* Vat-2 C-List: + * `v2.o+2001 <-> ko1` (export of bob) + * `v2.p-2015 <-> kp24` (import of result) + +and Vat-2 receives a `dispatch.deliver(target=o+2001, msg={method: "foo", +args: "[]", slots=[], result=p-2015})`. + +In the vat code on Vat-2, the `foo()` method returns some basic data "42". +This causes Vat-2 to resolve the promise to data, by calling +`dispatch.resolve(subject=p-2015, resolution=Data(body="42", slots=[]))`. + +The kernel translates the subject (`p-2015`, in its resolution capacity) +through the calling vat's C-List into `kp24`. It confirms in the kernel +promise table that `kp24` is in the `Unresolved` state and that Vat-2 is the +Decider, so the `resolve()` is allowed to proceed. The subscribers +(`[vat-1]`) are stored for later. + +The resolution body has no slots, so translating it is trivial. The +subscribers kernel promise table is then updated to reflect the resolution: + + * `kp24: state: Resolved(data(body="42", slots=[]))` + +The kernel then pushes notifications to all subscribers (just Vat-1) onto the +run-queue: + +* Kernel run-queue: + * `Notify(subscriber: vat-1, subject: kp24)` + +The `dispatch.resolve()` returns, and Vat-2 finishes its turn. + +The run-queue is then cycled again, and the Notify is at the top. The kernel +uses the `subscriber` to pick the Vat-1 C-List for inbound translation, and +the subject (`kp24`) is translated into `p+104`. The promise table is +consulted for `kp24` to determine the resolution, in this case `Data`. The +resolved data (`42`) has no slots, so translation is trivial. The Vat-1 +dispatch function is then invoked as `dispatch.notify(subject: p+104, to: +Data(body="42", slots=[]))`. + +Vat-1 looks up `p+104` in its internal tables to find the resolver function +for the native Promise that it created at the beginning, and invokes it with +`42`, firing whatever `.then()` functions were attached to that Promise. + +### Pipelined Send + +Suppose Vat-1 did `bob!foo()!bar()`, which sends `bar` to the Promise +returned by the initial `bob!foo()`. This is Promise Pipelining, and `bar` is +supposed to be sent into the Vat which owns the result of `bob!foo()` (which +will be the same Vat that owns `bob`, namely Vat-2). Vat-2 has opted into +receiving pipelined messages. + +The two `send` calls will look like: + +* `syscall.send(target=o-1001, msg={method: "foo", .. result=p+104})` +* `syscall.send(target=p+104, msg={method: "bar", .. result=p+105})` + +And after those sends, the kernel state will look like this: + +* Kernel Object Table: + * `ko1` (bob): owner= vat-2 +* Kernel Promise Table: + * `kp24: state: Unresolved(decider: None, subscribers: [vat-1])` + * `kp25: state: Unresolved(decider: None, subscribers: [vat-1])` +* Kernel run-queue: + * `Send(target: ko1, message: {method="foo", .. result=kp24})` + * `Send(target: kp24, message: {method="bar", .. result=kp25})` +* Vat-1 C-List: + * `v1.o-1001 <-> ko1` (import of bob) + * `v1.p+104 <-> kp24` (export of foo() result promise) + * `v1.p+105 <-> kp25` (export of bar() result promise) +* Vat-2 C-List: + * `v2.o+2001 <-> ko1` (export of bob) + +Vat-2 will get the same `dispatch.deliver(target=o+2001, msg={method: "foo", +args: "[]", slots=[], result=p-2015})` as before, and we'll get these changes +to the kernel state: + +* Kernel Promise Table: + * `kp24: state: Unresolved(decider: vat-2, subscribers: [vat-1])` + * `kp25: state: Unresolved(decider: None, subscribers: [vat-1])` +* Kernel run-queue: + * `Send(target: kp24, message: {method="bar", .. result=kp25})` +* Vat-2 C-List: + * `v2.o+2001 <-> ko1` (export of bob) + * `v2.p-2015 <-> kp24` (import of foo() result) + +Then the `bar` message reaches the front of the queue, and the kernel finds +that its target (`kp24`) is in the Unresolved state, and looks up the Decider +(`vat-2`). It sees that `vat-2` accepts pipelined messages, so it delivers +the message to Vat-2, which receives it as `dispatch.deliver(target=p-2015, +msg={method: "bar" .. result=p-2016)`. The kernel state during this call is: + +* Kernel Promise Table: + * `kp24: state: Unresolved(decider: vat-2, subscribers: [vat-1])` + * `kp25: state: Unresolved(decider: vat-2, subscribers: [vat-1])` +* Vat-2 C-List: + * `v2.o+2001 <-> ko1` (export of bob) + * `v2.p-2015 <-> kp24` (import of foo() result) + * `v2.p-2016 <-> kp25` (import of bar() result) + +Vat-2 should store this `bar()` Message in a queue for it's `p-2015` Promise. +If Vat-2 is actually a Comms Vat, it could send `bar()` over the wire to the +remote machine where `bob` lives, right away, instead of waiting for `bob` to +resolve. + +### Forwarding Queued Messages + +Imagine the previous scenario (`bob!foo()!bar()`), but now `bob` resolves the +`foo()` result promise to point at a third object `carol` in Vat-3. The +relevant kernel state looks like: + +* Kernel Object Table: + * `ko1` (bob): owner= vat-2 + * `ko2` (carol): owner= vat-3 +* Kernel Promise Table: + * `kp24: state: Unresolved(decider: vat-2, subscribers: [vat-1])` + * `kp25: state: Unresolved(decider: vat-2, subscribers: [vat-1])` +* Vat-1 C-List: + * `v1.o-1001 <-> ko1` (import of bob) + * `v1.p+104 <-> kp24` (export of foo() result promise) + * `v1.p+105 <-> kp25` (export of bar() result promise) +* Vat-2 C-List: + * `v2.o+2001 <-> ko1` (export of bob) + * `v2.p-2015 <-> kp24` (import of foo() result) + * `v2.p-2016 <-> kp25` (import of bar() result) + * `v2.o-2022 <-> ko2` (import of carol) +* Vat-3 C-List: + * `v3.o+3001 <-> ko2` (export of carol) + +Vat-2 does a `syscall.resolve(subject=p-2015, resolution=Fulfill(o-2022))`. +In the Kernel, the subject is mapped to `kp24`, and we check that it is +Unresolved, and that the Decider matches. The resolution is mapped to `ko2`, +and the promise table is updated. The kernel queues notifications to the only +subscriber (Vat-1): + +* Kernel Promise Table: + * `kp24: state: Resolved(fulfill(ko2))` +* Kernel run-queue: + * `Notify(subscriber: vat-1, subject: kp24)` + +Control returns to Vat-2, which now must send all the messages that were +previously queued for the Promise it just resolved. The same Message +structure that came out of `dispatch.notify` is sent unmodified back into +`syscall.send`, but the target is now the resolution of the promise: +`send(target=ko2, msg={method: "bar", .. result=p-2016})`. + +* Kernel Promise Table: + * `kp24: state: Resolved(fulfill(ko2))` +* Kernel run-queue: + * `Notify(subscriber: vat-1, subject: kp24)` + * `Send(target: ko2, message: {method="bar", .. result=kp25})` + +(TODO: is it necessary/ok/bad that vat-1 sees the Notify before it sees the +queued messages arrive? We could have Vat-2 invoke the syscalls in either +order.) + +Vat-1 gets the notify and must map `carol` into a new C-List entry: + +* Vat-1 C-List: + * `v1.o-1001 <-> ko1` (import of bob) + * `v1.p+104 <-> kp24` (export of foo() result promise) + * `v1.p+105 <-> kp25` (export of bar() result promise) + * `v1.o-1002 <-> ko2` (import of carol) + +Vat-1 gets `dispatch.notify(p+104, to: Fulfill(o-1002))`. + +Then the queued message `bar` is delivered to `carol` in Vat-3, which maps +the result promise into Vat-3's C-List: + +* Vat-3 C-List: + * `v3.o+3001 <-> ko2` (export of carol) + * `v3.p-3031 <-> kp25` (bar() result promise) + +and Vat-3 gets `dispatch.deliver(target=o+3001, message: {method="bar", .. +result=p-3031})`. + +### Pipelined send to a non-Comms vat + +Now let us suppose Vat-2 has *not* elected to accept pipelined messages (i.e. +it is not the Comms Vat). When Vat-1 does `bob!foo()!bar()`, the `bar` should +be queued inside the kernel Promise, rather than being delivered to Vat-2. + +Again, the two `send` calls will look like: + +* `syscall.send(target=o-1001, msg={method: "foo", .. result=p+104})` +* `syscall.send(target=p+104, msg={method: "bar", .. result=p+105})` + +And after those sends, the kernel state will look like this: + +* Kernel Object Table: + * `ko1` (bob): owner= vat-2 +* Kernel Promise Table: + * `kp24: state: Unresolved(decider: None, subscribers: [vat-1])` + * `kp25: state: Unresolved(decider: None, subscribers: [vat-1])` +* Kernel run-queue: + * `Send(target: ko1, message: {method="foo", .. result=kp24})` + * `Send(target: kp24, message: {method="bar", .. result=kp25})` +* Vat-1 C-List: + * `v1.o-1001 <-> ko1` (import of bob) + * `v1.p+104 <-> kp24` (export of foo() result promise) + * `v1.p+105 <-> kp25` (export of bar() result promise) +* Vat-2 C-List: + * `v2.o+2001 <-> ko1` (export of bob) + +Vat-2 will get the same `dispatch.deliver(target=o+2001, msg={method: "foo", +args: "[]", slots=[], result=p-2015})` as before, and we'll get these changes +to the kernel state: + +* Kernel Promise Table: + * `kp24: state: Unresolved(decider: vat-2, subscribers: [vat-1])` + * `kp25: state: Unresolved(decider: None, subscribers: [vat-1])` +* Kernel run-queue: + * `Send(target: kp24, message: {method="bar", .. result=kp25})` +* Vat-2 C-List: + * `v2.o+2001 <-> ko1` (export of bob) + * `v2.p-2015 <-> kp24` (import of foo() result) + +Now, when the `bar` message reaches the front of the queue the kernel finds +that its target (`kp24`) is in the Unresolved state, the kernel sees that +`vat-2` does not accept pipelined messages. So instead of a +`dispatch.deliver`, it queues the message within the Promise: + +* Kernel Promise Table: + * `kp24: state: Unresolved(decider: vat-2, subscribers: [vat-1], queue: [{method="bar", ..}])` + * `kp25: state: Unresolved(decider: None, subscribers: [vat-1])` +* Vat-2 C-List: + * `v2.o+2001 <-> ko1` (export of bob) + * `v2.p-2015 <-> kp24` (import of foo() result) + +Later, if the Vat Code inside Vat-2 resolves the Promise to some new exported +object "quux", the lower-level code will call +`syscall.resolve(subject=p-2015, resolution=Fulfill(o+2022))`, and the kernel +will update the Promise table and re-queue the old messages, as well as +scheduling notification for the subscribers: + +* Kernel Object Table: + * `ko1` (bob): owner= vat-2 + * `ko3` (quux): owner= vat-2 +* Vat-2 C-List: + * `v2.o+2001 <-> ko1` (export of bob) + * `v2.p-2015 <-> kp24` (import of foo() result) + * `v2.o+2022 <-> ko3` (export of quux) +* Kernel Promise Table: + * `kp24: state: Resolved(target(ko3))` + * `kp25: state: Unresolved(decider: None, subscribers: [vat-1])` +* Kernel run-queue: + * `Notify(subscriber: vat-1, subject: kp24)` + * `Send(target: ko3, message: {method="bar", .. result=kp25})` + +When the Send gets to the front of the queue, it will deliver `bar()` into +vat-2, which marks `kp25` as being decided by vat-2. + + +### More Arguments, Three Vats + +Now let's examine how various arguments are managed. Our initial conditions +give Vat-1 access to a previously-exported object `alice` (in Vat-1), an +import from Vat-2 named `bob` as before, an import from Vat-3 named `carol`, +and a Promise received from Vat-2 named `p2`. We're going to send all of +these, plus a local Promise, to `carol`. + +* Kernel Object Table: + * `ko1` (bob): owner= vat-2 + * `ko2` (carol): owner= vat-3 + * `ko3` (alice): owner= vat-1 +* Kernel Promise Table: + * `kp22: state: Unresolved(decider: vat-2, subscribers: [vat-1])` (p2) +* Kernel run-queue: empty +* Vat-1 C-List: + * `v1.o-1001 <-> ko1` (import of bob) + * `v1.o-1002 <-> ko2` (import of carol) + * `v1.o+1044 <-> ko3` (export of alice) + * `v1.p-1052 <-> kp22` (import of p2) +* Vat-2 C-List: + * `v2.o+2001 <-> ko1` (export of bob) +* Vat-3 C-List: + * `v3.o+3001 <-> ko2` (export of carol) + +Vat-1 now does `p3 = make_promise(); p4 = carol!foo(alice, bob, carol, p2, p3)`. + +The `make_promise()` creates a regular Vat-Code promise (a native Javascript +Promise). Nothing special happens until the `foo()` is processed into a +`syscall.send`. During that processing, as the `p3` argument is serialized, +the translation layer in Vat-1 allocates a new local PromiseID for it (say +`p+103`). It allocates `p+104` for the result (p4). The resulting syscall is +`send(target=o-1002, msg={method: "foo", args: "..", slots=[o+1044, o-1001, +o-1002, p-1052, p+103], result=p+104})`. The kernel state now looks like: + +* Kernel Object Table: + * `ko1` (bob): owner= vat-2 + * `ko2` (carol): owner= vat-3 + * `ko3` (alice): owner= vat-1 +* Kernel Promise Table: + * `kp22: state: Unresolved(decider: vat-2, subscribers: [vat-1])` (p2) + * `kp23: state: Unresolved(decider: vat-2, subscribers: [])` (p3) + * `kp24: state: Unresolved(decider: None, subscribers: [vat-1])` (p4) +* Kernel run-queue: + * `Send(target=ko2, msg={method: "foo", args: "..", slots=[ko3, ko1, ko2, kp22, kp23], result=kp24})` +* Vat-1 C-List: + * `v1.o-1001 <-> ko1` (import of bob) + * `v1.o-1002 <-> ko2` (import of carol) + * `v1.o+1044 <-> ko3` (export of alice) + * `v1.p-1052 <-> kp22` (import of p2) + * `v1.p+103 <-> kp23` (export of p3) + * `v1.p+104 <-> kp24` (result p4) +* Vat-2 C-List: + * `v2.o+2001 <-> ko1` (export of bob) + * `v2.p+2002 <-> kp22` (previously exported p2) +* Vat-3 C-List: + * `v3.o+3001 <-> ko2` (export of carol) + +The run-queue is cycled, and `foo` comes to the top. The target is `ko2`, +which is owned by vat-3. The inbound target must already exist in the map, +and gets `v2.o+3001`. The inbound `ko3` is not already present in the Vat-3 +C-List, so we allocate `v3.o-3031`. The same is true for inbound `ko1`, which +gets `v3.o-3032`. The inbound `ko2` is already present (we're sending `carol` +a reference to herself, so `ko2` is coming back home), so it is mapped to +`v3.o+3001`. The inbound `kp22` gets a new import `v3.p-3041`, as does `kp23` +mapping to `v3.p-3042`. Finally the result `kp24` is mapped to `v3.p-3043` +and its Decider is pointed at vat-3. The resulting state, just before +dispatch, is: + +* Kernel Promise Table: + * `kp22: state: Unresolved(decider: vat-2, subscribers: [vat-1])` (p2) + * `kp23: state: Unresolved(decider: vat-2, subscribers: [])` (p3) + * `kp24: state: Unresolved(decider: vat-3, subscribers: [vat-1])` (p4) +* Vat-3 C-List: + * `v3.o+3001 <-> ko2` (export of carol) + * `v3.o-3031 <-> ko3` (import of alice) + * `v3.o-3032 <-> ko1` (import of bob) + * `v3.p-3041 <-> kp22` (import of p2) + * `v3.p-3042 <-> kp23` (import of p3) + * `v3.p-3043 <-> kp24` (result p4) + +Vat-2 then gets a `dispatch.deliver(target=o+3001, msg={method: "foo", args: +"..", slots=[o-3031, o-3032, o+3001, p-3041, p-3042], result=p-3043})`. + +### TODO: more examples + +* `syscall.resolve(to=Forward(p))` +* `syscall.resolve(to=Data())`, showing how queued messages are then rejected +* `syscall.resolve(to=Rejection())`, ditto ``` -B.deliver( - slotID=40b, - method='foo', - argsString='[{@qclass: "ref", index: 0i}, - {@qclass: "ref", index: 1i}, - ]', - argSlots=[-30b, 40b]) +p1 = make_promise(); +x!foo(p1); +function foo(arg) { + return p1; } ``` -B's comms library deserializes argsStrings, replacing "refs" with other -objects. The first one encountered looks up `index=0` in `argSlots` and sees -"-30b", which is negative, so it is an import. This is not yet present in B's -table (`!vatSlotsB.has(-30b)`), so a new proxy is created (which remembers -"-30b" internally), and this proxy is inserted into the slowly-forming -argument graph. - -The second `ref` encountered cites `index=1`, which maps to "40b". This is -positive, so it must be a previously-exported object. This object (i.e. -`bob`) is looked up in `vatSlotsB[40b]` and inserted into the argument graph. - -The comms library finishes creating the object graph: `[proxyAlice, bob]`. It -then invokes `bob.foo([proxyAlice, bob])`. - -`vatSlotsB`: - -| index | target | -|------:|------------| -| 40b | bob | -| -30b | proxyAlice | - - -If A were to invoke `bob.foo(alice, bob)` again, none of the tables would -need updating, and object identity would be retained. A's comms library -notices that `alice` is already present in `vatSlotsA`, so it gets mapped to -"20a" without modifying the table. The kernel sees that `[A, 20a]` is already -present in `kernelSlots[B].backward`, so it can translate that to `-30b` -without modifying the table. B's comms library sees that `-30b` is already -present in `vatSlotsB` so it re-uses `proxyAlice` from the previous delivery. - -If A were to invoke `bob.foo(alice, alice)`, the `argsString` would have -multiple copies of a `{@qclass: "ref", index: 0i}` node, but `argsSlots` -would have just one copy of `-30b`. - - -## Third Message: Three-Party Introduction - -A invokes `bob.bar(carol)`. - -A's comms library serializes the object graph as before, producing a -reference to carol ("-11a") in the arguments: +`foo()` is invoked through an inbound `dispatch.deliver(.., result=p-4)`, and +when it returns a previously-exported promise (aka `p+1`), the support layer +should do `syscall.resolve(p-4, p+1)`. + +These situations are legal/sensible and should be documented (and tested!), +but it may require some creativity to come up with Vat Code that could +produce them: + +* receiving `result=` of a promise you previously exported +* calling `syscall.send()` with a `result=` of a promise you previously exported +* calling `syscall.send()` with a `result=` of a promise you previously + received as the result of a `dispatch.deliver()` + + + +## Comms Protocol + +Normal vats only ever talk to the kernel. The only way to get messages to an +entirely different SwingSet machine is to go through the Comms Vat, which +gets special access to VatTP and can exchange data with other machines. + +The kernel routes messages between multiple vats. The Comms Vat routes +messages between the local kernel and (potentially) multiple external +machines. In this way, the Comms Vat is like the kernel: it must maintain +Object and Promise tables, and a C-List for each remote machine. It does not +need to manage a run-queue (`dispatch()` causes an immediate external +message), nor does it have a kernel-facing C-List (its Object/Promise tables +are indexed by the same `ObjectID`/`PromiseID` Vat types that appear in the +`syscall/dispatch` API. But it does have some routing tables to figure out +where each message needs to go. + +The one wrinkle is that Vat-Vat connections are symmetric, which impacts the +way these types are represented. The Vat-Kernel interface is conveniently +asymmetric, so we can declare that positive index values are allocated by the +Vat, while negative values are allocated by the kernel, and it doesn't matter +which direction the messages are going. The naming scheme is "vat-centric". + +When the messages travel from one Vat to the other, must instead speak in +terms of the sender and the receiver of any particular message. We declare +that messages arriving at a Vat will use positive index values for objects +that are allocated by the receiver, and negative for the ones allocated by +the sender. Both Vats use this same "receiver-centric" convention for the +externally-facing side of their per-machine C-Lists. As a result, the IDs +inside inbound messages can be looked up in the C-List directly, but when +sending outbound messages, all the IDs must have their signs flipped. + +The message names are also different. In the local-machine Vat-Kernel-Vat +flow, the first Vat's outbound message has a different name than the inbound +message (`syscall.send` becomes `dispatch.deliver`, `syscall.resolve` becomes +`dispatch.notify`). In the remote-machine CommsVat-CommsVat flow, there is no +kernel in the middle, so whatever the first Comms Vat sends is exactly what +the second Comms Vat receives. In keeping with the receiver-centric +convention, We use `deliver` for message delivery, and `notify` for promise +resolution. + +We also presume `subscribe` happens without the receiving vat requesting it. +The latencies between separate SwingSet machines are high enough that the +tradeoff between more messages vs more roundtrips seems clearly in favor of +more messages. + +### Comms Tables + +Each comms vat maintains connections to many remote machines, just as each +kernel supports many separate Vats. So comms vats maintain mapping tables +that look very similar to the ones in the kernel. There are two bidirectional +tables for each remote machine (one for objects, the other for promises), +just like the kernel-side C-Lists. There are also routing tables to track +where messages must be sent, similar to the kernel's object and promise +tables. ``` -syscall.send(targetSlot=-10a, - method='bar, - argsString='[{@qclass: "ref", index: 0i}, - {@qclass: "ref", index: 1i}, - ]', - argSlots=[-11a]) + | remote1 | | remote2 | | remote3 | + | ^ | | ^ | | ^ | + | | | | | | | | | + +---|-----+----+----|----+----+---|-----+-------+ + | v | | v | | v | | + | remote1 | | remote2 | | remote3 | | + | tables | | tables | | tables | | + | | + | | + | object-table | + | promise-table ^ | + | Comms Vat | | | + +---------------------------------+-syscall--+--+ ++-----------------------------------+-dispatch-+------+ +| kernel | | | +| v | ``` -The kernel translates the target slot into B's "40b" as before, but when it -translates the `argSlots` it sees `kernelSlots[A].forward(-11a)` maps to `[C, -50c]`, which lives on a Vat (C) that differs from the target of the message -(B). This is not a reference being sent back home, so we must make sure B has -an import of this reference (just as if the value had been negative, which -would have made it an export of A, which is certainly going to become an -import of B). So we check `kernelSlots[B].backward.has([C, 50c])` and come up -empty. This provokes an allocation of a new import for B, and "-31b" is the -next available value. - -`kernelSlots`: - -| fromVatID | fromSlotID | toVatID | toSlotID | -|----------:|-----------:|--------:|---------:| -| A | -10a | B | 40b | -| | -11a | C | 50c | -| B | -30b | A | 20a | -| | -31b | C | 50c | - -The kernel then queues: +The data structures are: ``` -{ vatID=B, - slotID=40b, - method='bar', - argsString='[{@qclass: "ref", index: 0i}]', - argSlots=[-31b]) +struct RemoteID(u32); + +struct VatPromiseID(i32); +struct VatObjectID(i32); +struct RemotePromiseID(i32); +struct RemoteObjectID(i32); + +struct RemoteCList { + objects: HashMap, + next_object_id: u32, + promises: HashMap, + next_promise_id: u32, +} + +# the Comms Vat has exactly one CommsTables instance +struct CommsTables { + remotes: HashMap, + next_object_id: u32, + object_routes: HashMap, + next_promise_id: u32, + promise_routes: HashMap, } + ``` -When this message gets to the front of the queue, the kernel invokes: +The two routing tables (`object_routes` and `promise_routes`) are used to +handle message sends that originate on a local vat and are destined for some +remote vat. The keys of `object_routes` are positive integers (object IDs +allocated by the comms vat and exported into the kernel for use by other +local vats). The keys of `promise_routes` may be positive or negative, but +identify promises for which, from the point of view of the local kernel, the +comms vat holds resolution authority. Other vats on remote machines hold the +real resolution authority, but the local kernel will only learn about +resolution from the local comms vat. + +When the comms vat receives a `dispatch.deliver()` for some target, it is +looked up in the routing tables. If it is not found there, the message must +be meant for a local object (used to manage the comms vat itself, and the +connections it makes to other machines). + +Once the remote machine ID is known, the rest of the message slots (target, +argument slots, and optional result) are mapped through a C-List that is +specific to that machine. The mapping rules are similar to the ones used by +the kernel as it does a `dispatch.deliver()`, but are adjusted to accomodate +three-party handoff (which is disabled until we finish designing it) and the +lack of a single central kernel: + +* The target (object or promise) must already exist in the table. +* When an argument object ID is not already present in the table, we allocate + a new entry if the ID is negative (imported from the kernel), and we throw + a `ThreePartyHandoff` error if it is positive. +* The `result=` promise, if already in the routing table, must have a Decider + that matches the destination machine, else we throw `ThreePartyHandoff`. + New kernel-allocated promises are added to the routing table and the + Decider set to the destination machine. + + +### Comms Example ``` -B.deliver( - slotID=40b, - method='bar', - argsString='[{@qclass: "ref", index: 0i}]', - argSlots=[-31b]) -} ++- Left Vat -+ +- Left --+ +- Right --+ +-Right Vat+ +| | | Comms | | Comms | | | +|p1=bob!foo()| | | | | | | +| | | comms ----------> comms | | | +| | | code | | code | | bob.foo()| +| | | | | | | | +| ---------- | | -------- | | -------- | | -------- | +| | | | | | | | +| send() | | deliver | | send() | | deliver | +| | | | ^ | | | | | ^ | +| | | | | | | | | | | | ++----|-------+ +---|------+ +---|------+ +---|------+ ++----|-------+---+---|------+-+ +-+---|------+----+---|------+-+ +| v | | | | | | | v | | | | | +| clists | | clists | | | | clists | | clists | | +| | | | ^ | | | | | | | | | | +| | | | | | | | +| | | | | | | | +| \-> run-queue --/ | | \-> run-queue --/ | +| | | | +| | | | ++--------- Left Kernel -------+ +----- Right Kernel -----------+ ``` -B's deserialization sees "-31b", which isn't present in `vatSlotsB`, so it -allocates a new proxy: - -`vatSlotsB`: +Initial conditions: + +* Left Kernel Object Table: + * `ko1` (bob): owner= left-comms +* Kernel Promise Table: empty +* Kernel run-queue: empty +* left-vat (id=1) kernel C-List: + * `v1.o-1001 <-> ko1` (import of bob) +* left-comms (id=2) kernel C-List: + * `v2.o+2001 <-> ko1` (export of bob proxy) +* left-comms object routing table: + * `o+2001 -> right` +* left-comms cross-machine C-Lists + * `right:` + * `o+2001 <-> right:ro-3001` +* right-comms cross-machine C-Lists + * `left:` + * `left:ro+3001 <-> o-4001` +* right-comms (id=3) kernel C-List + * `v3.o-4001 <-> ko2` (import of real bob) +* right-vat (id=4) kernel C-List + * `v4.o+5001 <-> ko2` (export of real bob) + +left-vat does `p1 = bob ! foo()`. Left kernel accepts `syscall.send()` and +the run-queue gets `Send(target=ko1, msg={name: foo, result=kp24})`, which +eventually comes to the front and is delivered to left-comms. The left-kernel +tables just before `dispatch.deliver()` is called will look like: + +* Left Kernel Object Table: + * `ko1` (bob): owner= left-comms +* Kernel Promise Table: + * `kp24: state: Unresolved(decider: v2, subscribers: [v1])` +* left-vat (id=1) kernel C-List: + * `v1.o-1001 <-> ko1` (import of bob) + * `v1.p+104 <-> kp24` (export of result promise) +* left-comms (id=2) kernel C-List: + * `v2.o+2001 <-> ko1` (export of bob) + * `v2.p-2015 <-> kp24` (import of result) + +left-comms gets `deliver(target=o+2001)` and looks up the target in the +routing table to see that the destination machine is `right`. It maps +`o+2001` through the `right` c-list table to get `ro-3001`, which it uses in +the wire message. It sees the result promise (`p-2015`) has no mapping in the +routing table, so it adds it (with `Decider: right`), then sees that `p-2015` +is not in the c-list, and adds it too, allocating a new ID (`rp+3202`). +Left-comms uses these identifiers to generate the cross-machine message, +expressed in receiver-centric terms (so the signs are flipped): + +* left-comms object routing table: + * `o+2001 -> right` +* left-comms promise routing table: + * `p-2015 -> right` +* left-comms cross-machine C-Lists + * `right:` + * `o+2001 <-> right:ro-3001` + * `p-2015 <-> right:rp+3202` +* Left Outbox (target=`right`) + * `deliver(target=ro+3001, msg={name: foo, result=rp-3202})` + +An external delivery process copies the cross-machine message from the left +Outbox into the right machine, causing a pending delivery that gets the +message into the right-comms vat, along with the name of the machine that +sent it (`left`). Right-comms looks up the target (`ro+3001`) in the `left` +c-list to find `o-4001`. It sees that the result promise `rp-3202` is not +present in the c-list, so it allocates a new local ID (`p+4002`) and adds it +to the c-list. It does *not* add `p+4002` to the routing table, because the +kernel will hold the resolution authority for this result. + +* right-comms cross-machine C-Lists + * `left:` + * `left:ro+3001 <-> o-4001` + * `left:rp-3202 <-> p+4002` +* right-comms object routing table: empty +* right-comms promise routing table: empty + +Finally it submits +the transformed message to the right kernel: `syscall.send(target=o-4001, +msg={name: foo, result=p+4002})`. + +The right kernel maps the arguments of the `Send` through the right-comms +kernel C-List. The target maps to `ko2`, and the result causes a new Promise +to be allocated (`kp6001`), with a Decider of None. The pending delivery is +pushed onto the back of the run-queue: + +* right-comms (id=3) kernel C-List + * `v3.o-4001 <-> ko2` (import of real bob) + * `v3.p+4002 <-> kp6001` (result promise) +* run-queue: + * `Send(target=ko2, msg={name: foo, result=kp6001})` + +The delivery is dispatched to right-vat, which causes a new C-List entry to +be added for the result promise (`v4.p-5002`), and invokes +`dispatch.deliver(target=o+5001, msg={name: foo, result=p-5002})`: + +* right-vat (id=4) kernel C-List + * `v4.o+5001 <-> ko2` (export of real bob) + * `v4.p-5002 <-> kp6001` (import of result promise) + +#### response + +Now suppose right-vat resolves the result promise to a new local object +(`v4.o+5003`). We trace the `syscall.resolve` back to the left-vat: + +* right-vat: `syscall.resolve(subject=p-5002, Fulfill(o+5003))` +* right kernel right-vat C-List: `v4.o+5003 <-> ko3` +* right run-queue `Notify(target=kp6001, Fulfill(ko3))` +* notification gets to front, right kernel promise table updated + * `kp6001: state = FulfillToTarget(ko3)` + * subscribers each get a `dispatch.notify` +* right-comms: `dispatch.notify(target=p+4002, Fulfill(o-4003))` +* right-comms promise routing table lookup (`p+4002`) says destination machine is `left` +* right-comms allocates `ro+3002` for the object `o-4003` +* outbox message is `notify(target=rp+3202, Fulfill(ro-3002))` +* left-comms gets message from `right`, maps target to `p-2015` +* left-comms maps resolution through right-machine c-list, allocates `o+2002` +* left-comms submits `syscall.resolve(target=p-2015, Fulfill(o+2002))` +* left kernel maps through left-comms c-list, allocates `ko15` for `v2.o+2002` +* left run-queue `Notify(target=kp24, Fulfill(ko15))` +* subscribers each get `dispatch.notify` +* `v1.o-1002` allocated for `ko15` in left-vat c-list +* left-vat gets `dispatch.notify(target=p+104, Fulfill(o-1002))` + + +## comms notes + +The message target has three cases: + +* imported object (owner is some remote machine) +* imported promise (owner+decider are some remote machine) +* "send result"? (promise, owner is us, but decider is remote machine) + +The owner (for objects) or decider (for promises) tells us the target machine +ID, which we must know before we can serialize anything else. + +If the argument is an object, there are three cases: + +* A exported object (owner is us) +* B imported object (owner is target machine) +* C handoff object (owner is neither us nor target) + +If the argument is a promise, there are 3 cases: + +* A exported promise: kernel is decider +* B imported promise: target machine is decider +* C handoff promise: some other machine is decider + +We do not yet attempt to implement three-party handoff, so both handoff cases +currently cause an error. We discover this by comparing the owner/decider of +comms-vat-owned object/promises (i.e. the ID is a positive integer) against +the destination machine. If they differ, the send is abandoned (TODO: how +should we signal this error?). + +Then we check to see if the ObjectID/PromiseID is in the table for the target +machine, and allocate a new connection-local RemoteID if necessary. This +should only wind up allocating if the ID was negative (kernel-owned). + +The optional `result` field, if present, must be a promise. The comms vat is +given resolution authority as it arrives. If the promise is already in +`CommsPromiseTable`, the Decider must be equal to the destination machine, +else the send is abandoned. If it is not already in the table, it is added, +and the Decider set equal to the destination machine. + +The result promise is then mapped through the per-machine table as with +argument promises, and allocated in the same way. + +### Three-Party Handoff + +We're deferring three-party handoff for now, because our main use case +doesn't strictly need it. Any time an object or promise is received from one +machine and then sent to another, the message is abandoned and an error is +reported (TODO: how?). + + +In the future, the simplest way to enable handoffs will be to inject +forwarding objects. Messages and resolutions will take the long path, +traversing through intermediate machines until they reach their destination. + +But the long-term goal is to use a better protocol and shorten the path over +which messages must be delivered. + + +## open questions + +* Dean suggested the distinct `Resolver` type in the syscall API, to avoid + type confusion bugs (passing a Promise when you really meant to refer to + your resolution authority). But I'm finding that makes the spec harder to + explain. I'm inclined to stick to `Object` and `Promise`, and just note in + the API signatures that three `Promise`-accepting slots are special: + passing a Promise into `send()`'s `Message.result` gives away your + resolution authority, getting one in `dispatch()`'s `Message.result` grants + you resolution authority, and putting one in `notify()`'s subject exercises + (and consumes) that authority. +* We must deliver pipelined messages to the comms vat (instead of queueing + them in the kernel) to enable useful pipelining. This design delivers + pipelined messages to *all* vats, even though the comms vat is the only one + where it's useful. We could have a per-Vat flag to enable/disable + kernel-side queueing (but the kernel is a lot simpler without that + queuing). If we stick with deliver-to-everyone, then I'm inclined to have + Vats queue those messages in their original (marshalled) form until we know + whether we're consuming them or forwarding them elsewhere, but Dean + recommended unmarshalling them immediately (to avoid confusion over tables + they might reference which could change between now and delivery), then + remarshalling them later.. it depends on what vat-side tables are involved + and how they might change. +* The message flow would be simpler if these queued messages could be dumped + back into the kernel after a promise is resolved, and let the kernel deal + with it. `syscall.resolve(subject, resolution, queued_messages)`. I'd like + to have `CannotSendToData` errors generated in just one place, for example. + It'd be even simpler if we could use this for resolutions that point at the + same Vat doing the resolution (instead of processing those messages + immediately), but that probably has ordering consequences. +* Dean pointed out an important performance improvement, Promises which are + resolved/rejected to data (or forwarded?) should be removed from the + resolving vat's c-list right away. We'd need an extra message in the future + if that vat ever sends that promise again, but apparenly the vast majority + of the time it never will, so pruning the c-list immediately is a big win. + This might interfere with having the kernel handle dumped queued messages. + Why do this for data+forward but not for fulfill? +* Forwarded promises must not create cycles. Vats should not be able to trick + other Vats into creating a cycle and thus get terminated. Cycles are + cheaper to detect if we can remove forwarded promises from the table right + away. Keeping forwarded promises around makes it easier to use the kernel + to handle the dumped queued messages from the old promise. +* What do we need from the relative ordering of a `dispatch.notify()` and the + queued messages now headed to that resolution? The current design has the + notify first, then the messages, is that ok? -| index | target | -|------:|------------| -| 40b | bob | -| -30b | proxyAlice | -| -31b | proxyCarol | From dd533da47b6c5ed15161935c9cf9f76f11897b21 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 27 Aug 2019 17:35:34 -0700 Subject: [PATCH 02/16] phase 1, only kernel changes "phase 1" means: * change the kernel objectID/promiseIDs formats from the old `{type, id, [vat]}` to o+14, ko4, p-5, etc * add the kernel Object Table to track object owners, instead of recording the owning vat ID inside each c-list entry * replace the kernel/vat c-lists to point at ObjectTable / PromiseTable entries, simplify/robustify implementation * kernel/controller APIs (queueToExport,addImport,addExport) take new refs * update tests to match The following changes are part of #88 but will be implemented in a subsequent phase: * allocate kernel promises in response to vat-supplied p+NN references * remove syscall.createPromise() * allow vats (specifically the comms vat) to opt-in to pipelining The new kernel/vat state vectors are still built out of plain JS objects. After we replace the kernel serialization mechanism (and no longer require fast JSON.stringify() of the whole state) we can switch to Maps. --- src/controller.js | 1 + src/kernel/deviceManager.js | 72 +-- src/kernel/deviceSlots.js | 76 ++-- src/kernel/kernel.js | 178 ++++---- src/kernel/liveSlots.js | 127 +++--- src/kernel/parseKernelSlots.js | 51 +++ src/kernel/state/deviceKeeper.js | 107 +++-- src/kernel/state/kernelKeeper.js | 165 +++++-- src/kernel/state/vatKeeper.js | 267 ++++------- src/kernel/vatManager.js | 310 +++++-------- test/files-devices/bootstrap-1.js | 4 +- test/test-controller.js | 112 ++--- test/test-devices.js | 11 +- test/test-kernel.js | 720 +++++++++++------------------- test/test-liveslots.js | 7 +- test/test-marshal.js | 73 ++- test/util.js | 23 + 17 files changed, 996 insertions(+), 1308 deletions(-) create mode 100644 src/kernel/parseKernelSlots.js create mode 100644 test/util.js diff --git a/src/controller.js b/src/controller.js index 597c93f8baf..9d6ade9ebbe 100644 --- a/src/controller.js +++ b/src/controller.js @@ -212,6 +212,7 @@ export async function buildVatController(config, withSES = true, argv = []) { }, queueToExport(vatID, facetID, method, argsString) { + kernel.addExport(vatID, facetID); kernel.queueToExport(vatID, facetID, method, argsString, []); }, diff --git a/src/kernel/deviceManager.js b/src/kernel/deviceManager.js index 14511b40ac8..9ebd178fc39 100644 --- a/src/kernel/deviceManager.js +++ b/src/kernel/deviceManager.js @@ -1,5 +1,7 @@ import harden from '@agoric/harden'; -import Nat from '@agoric/nat'; +import { insist } from './insist'; +import { insistKernelType } from './parseKernelSlots'; +import { insistVatType, parseVatSlot } from '../vats/parseVatSlots'; export default function makeDeviceManager( deviceName, @@ -11,60 +13,31 @@ export default function makeDeviceManager( ) { const { kdebug, send, log } = syscallManager; - function mapDeviceSlotToKernelSlot(slot) { - // kdebug(`mapOutbound ${JSON.stringify(slot)}`); - if (slot.type === 'deviceExport') { - // one of our exports, so just make the deviceName explicit - Nat(slot.id); - return { type: 'device', deviceName, id: slot.id }; - } - - return deviceKeeper.mapDeviceSlotToKernelSlot(slot); + function mapDeviceSlotToKernelSlot(devSlot) { + insist(`${devSlot}` === devSlot, 'non-string devSlot'); + // kdebug(`mapOutbound ${devSlot}`); + return deviceKeeper.mapDeviceSlotToKernelSlot(devSlot); } // mapInbound: convert from absolute slot to deviceName-relative slot. This // is used when building the arguments for dispatch.invoke. - function mapKernelSlotToDeviceSlot(slot) { - kdebug(`mapInbound for device-${deviceName} of ${JSON.stringify(slot)}`); - - if (slot.type === 'device') { - const { deviceName: fromDeviceName, id } = slot; - Nat(id); - - if (deviceName !== fromDeviceName) { - throw new Error( - `devices cannot accept external device refs: ${JSON.stringify(slot)}`, - ); - } - // this is returning home, so it's one of our own exports - return { type: 'deviceExport', id }; - } - - return deviceKeeper.mapKernelSlotToDeviceSlot(slot); + function mapKernelSlotToDeviceSlot(kernelSlot) { + insist(`${kernelSlot}` === kernelSlot, 'non-string kernelSlot'); + kdebug(`mapInbound for device-${deviceName} of ${kernelSlot}`); + return deviceKeeper.mapKernelSlotToDeviceSlot(kernelSlot); } // syscall handlers: these are wrapped by the 'syscall' object and made // available to userspace function doSendOnly(targetSlot, method, argsString, vatSlots) { - if (targetSlot.type === undefined) { - throw new Error( - `targetSlot isn't really a slot ${JSON.stringify(targetSlot)}`, - ); - } + insist(`${targetSlot}` === targetSlot, 'non-string targetSlot'); + insistVatType('object', targetSlot); const target = mapDeviceSlotToKernelSlot(targetSlot); - if (!target) { - throw Error( - `unable to find target for ${deviceName}/${targetSlot.type}-${targetSlot.id}`, - ); - } - kdebug( - `syscall[${deviceName}].send(vat:${JSON.stringify( - targetSlot, - )}=ker:${JSON.stringify(target)}).${method}`, - ); + insist(target, `unable to find target`); + kdebug(`syscall[${deviceName}].send(${targetSlot}/${target}).${method}`); const slots = vatSlots.map(slot => mapDeviceSlotToKernelSlot(slot)); - kdebug(` ^target is ${JSON.stringify(target)}`); + kdebug(` ^target is ${target}`); const msg = { method, argsString, @@ -102,19 +75,20 @@ export default function makeDeviceManager( // dispatch handlers: these are used by the kernel core function invoke(target, method, data, slots) { - if (target.type !== 'device' || target.deviceName !== deviceName) { - throw new Error(`not for me ${JSON.stringify(target)}`); - } + insistKernelType('device', target); + const t = mapKernelSlotToDeviceSlot(target); + insist(parseVatSlot(t).allocatedByVat, 'not allocated by me'); const inputSlots = slots.map(slot => mapKernelSlotToDeviceSlot(slot)); try { - const results = dispatch.invoke(target.id, method, data, inputSlots); + const results = dispatch.invoke(t, method, data, inputSlots); const resultSlots = results.slots.map(slot => mapDeviceSlotToKernelSlot(slot), ); return { data: results.data, slots: resultSlots }; } catch (e) { console.log( - `device[${deviceName}][${target.id}].${method} invoke failed: ${e}`, + `device[${deviceName}][${t}].${method} invoke failed: ${e}`, + e, ); return { data: `ERROR: ${e}`, slots: [] }; } @@ -122,6 +96,8 @@ export default function makeDeviceManager( const manager = { invoke, + mapDeviceSlotToKernelSlot, + mapKernelSlotToDeviceSlot, }; return manager; } diff --git a/src/kernel/deviceSlots.js b/src/kernel/deviceSlots.js index 0f30eef3875..68f904ef0f8 100644 --- a/src/kernel/deviceSlots.js +++ b/src/kernel/deviceSlots.js @@ -2,6 +2,11 @@ import harden from '@agoric/harden'; import Nat from '@agoric/nat'; import { QCLASS, mustPassByPresence, makeMarshal } from '@agoric/marshal'; import { insist } from './insist'; +import { + insistVatType, + makeVatSlot, + parseVatSlot, +} from '../vats/parseVatSlots'; // 'makeDeviceSlots' is a subset of makeLiveSlots, for device code @@ -25,19 +30,8 @@ function build(syscall, state, makeRoot, forDeviceName) { } const outstandingProxies = new WeakSet(); - - function slotToKey(slot) { - if ( - slot.type === 'deviceExport' || - slot.type === 'import' || - slot.type === 'deviceImport' - ) { - return `${slot.type}-${Nat(slot.id)}`; - } - throw new Error(`unknown slot.type '${slot.type}'`); - } const valToSlot = new WeakMap(); - const slotKeyToVal = new Map(); + const slotToVal = new Map(); let nextExportID = 1; function allocateExportID() { @@ -48,7 +42,7 @@ function build(syscall, state, makeRoot, forDeviceName) { function exportPassByPresence() { const exportID = allocateExportID(); - return harden({ type: 'deviceExport', id: exportID }); + return makeVatSlot('device', true, exportID); } function serializeSlot(val, slots, slotMap) { @@ -71,10 +65,9 @@ function build(syscall, state, makeRoot, forDeviceName) { // lsdebug('must be a new export', JSON.stringify(val)); mustPassByPresence(val); slot = exportPassByPresence(); - - const key = slotToKey(slot); + parseVatSlot(slot); // assertion valToSlot.set(val, slot); - slotKeyToVal.set(key, val); + slotToVal.set(slot, val); } slot = valToSlot.get(val); @@ -90,37 +83,24 @@ function build(syscall, state, makeRoot, forDeviceName) { function unserializeSlot(data, slots) { // lsdebug(`unserializeSlot ${data} ${slots}`); const slot = slots[Nat(data.index)]; - const key = slotToKey(slot); let val; - if (!slotKeyToVal.has(key)) { - if (slot.type === 'import') { + if (!slotToVal.has(slot)) { + const { type, allocatedByVat } = parseVatSlot(slot); + insist(!allocatedByVat, `I don't remember allocating ${slot}`); + if (type === 'object') { // this is a new import value - // lsdebug(`assigning new import ${slot.id}`); - val = makePresence(slot.id); + // lsdebug(`assigning new import ${slot}`); + val = makePresence(slot); // lsdebug(` for presence`, val); - } else if (slot.type === 'deviceExport') { - // huh, the kernel should never reference an export we didn't - // previously send - throw Error(`unrecognized deviceExport '${slot.id}'`); - } else if (slot.type === 'deviceImport') { - // same - throw Error(`unrecognized deviceImport '${slot.id}'`); + } else if (type === 'device') { + throw Error(`devices should not be given other devices '${slot}'`); } else { - throw Error(`unrecognized slot.type '${slot.type}'`); + throw Error(`unrecognized slot type '${type}'`); } - slotKeyToVal.set(key, val); + slotToVal.set(slot, val); valToSlot.set(val, slot); } - return slotKeyToVal.get(key); - } - - // this handles both exports ("targets" which other vats can call) - function getTarget(deviceID) { - const key = slotToKey({ type: 'deviceExport', id: deviceID }); - if (!slotKeyToVal.has(key)) { - throw Error(`no target for facetID ${deviceID}`); - } - return slotKeyToVal.get(key); + return slotToVal.get(slot); } const m = makeMarshal(serializeSlot, unserializeSlot); @@ -156,7 +136,7 @@ function build(syscall, state, makeRoot, forDeviceName) { throw new Error('SO(SO(x)) is invalid'); } const slot = valToSlot.get(x); - if (!slot || slot.type !== 'import') { + if (!slot || parseVatSlot(slot).type !== 'object') { throw new Error(`SO(x) must be called on a Presence, not ${x}`); } const handler = PresenceHandler(slot); @@ -180,15 +160,19 @@ function build(syscall, state, makeRoot, forDeviceName) { // here we finally invoke the device code, and get back the root devnode const rootObject = makeRoot(harden({ SO, getDeviceState, setDeviceState })); - mustPassByPresence(rootObject); - const rootSlot = { type: 'deviceExport', id: 0 }; + + const rootSlot = makeVatSlot('device', true, 0); valToSlot.set(rootObject, rootSlot); - slotKeyToVal.set(slotToKey(rootSlot), rootObject); + slotToVal.set(rootSlot, rootObject); function invoke(deviceID, method, data, slots) { - lsdebug(`ls[${forDeviceName}].dispatch.invoke ${deviceID}.${method}`); - const t = getTarget(deviceID); + insistVatType('device', deviceID); + lsdebug( + `ls[${forDeviceName}].dispatch.invoke ${deviceID}.${method}`, + slots, + ); + const t = slotToVal.get(deviceID); const args = m.unserialize(data, slots); if (!(method in t)) { throw new TypeError( diff --git a/src/kernel/kernel.js b/src/kernel/kernel.js index b4da4f4f779..aa6fd8f572f 100644 --- a/src/kernel/kernel.js +++ b/src/kernel/kernel.js @@ -1,5 +1,4 @@ import harden from '@agoric/harden'; -import Nat from '@agoric/nat'; import { QCLASS, makeMarshal } from '@agoric/marshal'; import { makeLiveSlots } from './liveSlots'; @@ -9,6 +8,9 @@ import makePromise from './makePromise'; import makeVatManager from './vatManager'; import makeDeviceManager from './deviceManager'; import makeKernelKeeper from './state/kernelKeeper'; +import { insistKernelType, parseKernelSlot } from './parseKernelSlots'; +import { insistVatType, makeVatSlot } from '../vats/parseVatSlots'; +import { insist } from './insist'; function abbreviateReviver(_, arg) { if (typeof arg === 'string' && arg.length >= 40) { @@ -72,30 +74,31 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { // deciderVatID can be undefined if the promise is "owned" by the kernel // (pipelining) - // kernel promise is replaced if altered, so we can safely harden - const kernelPromiseID = kernelKeeper.addKernelPromise(deciderVatID); - return kernelPromiseID; + // TODO: not true (but we should make it true): kernel promise is + // replaced if altered, so we can safely harden + return kernelKeeper.addKernelPromise(deciderVatID); } function makeError(s) { // TODO: create a @qclass=error, once we define those // or maybe replicate whatever happens with {}.foo() - // or 3.foo() etc + // or 3.foo() etc: "TypeError: {}.foo is not a function" return s; } function send(target, msg) { - if (target.type === 'export') { + const { type } = parseKernelSlot(target); + if (type === 'object') { kernelKeeper.addToRunQueue({ type: 'deliver', - vatID: target.vatID, + vatID: kernelKeeper.ownerOfKernelObject(target), target, msg, }); - } else if (target.type === 'promise') { - const kp = kernelKeeper.getKernelPromise(target.id); + } else if (type === 'promise') { + const kp = kernelKeeper.getKernelPromise(target); if (kp.state === 'unresolved') { - kernelKeeper.addMessageToPromiseQueue(target.id, msg); + kernelKeeper.addMessageToPromiseQueue(target, msg); } else if (kp.state === 'fulfilledToData') { const s = `data is not callable, has no method ${msg.method}`; // eslint-disable-next-line no-use-before-define @@ -107,29 +110,30 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { // eslint-disable-next-line no-use-before-define reject(msg.kernelPromiseID, kp.rejectData, kp.rejectSlots); } else if (kp.state === 'redirected') { + throw new Error('not implemented yet'); // TODO: shorten as we go - send({ type: 'promise', id: kp.redirectedTo }); + // send(kp.redirectedTo, msg); } else { throw new Error(`unknown kernelPromise state '${kp.state}'`); } } else { - throw Error(`unable to send() to slot.type ${target.slot}`); + throw Error(`unable to send() to slot.type ${JSON.stringify(type)}`); } } - function notifySubscribersAndQueue(id, subscribers, queue, type) { - const pslot = { type: 'promise', id }; + function notifySubscribersAndQueue(kpid, subscribers, queue, type) { + insistKernelType('promise', kpid); for (const subscriberVatID of subscribers) { kernelKeeper.addToRunQueue({ type, vatID: subscriberVatID, - kernelPromiseID: id, + kernelPromiseID: kpid, }); } // re-deliver msg to the now-settled promise, which will forward or // reject depending on the new state of the promise for (const msg of queue) { - send(pslot, msg); + send(kpid, msg); // now that we know where the messages can be sent, we know to whom we // must subscribe to satisfy their resolvers. This wasn't working // correctly, so instead liveSlots just assumes that it must tell the @@ -143,66 +147,61 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { } } - function getUnresolvedPromise(id) { - const p = kernelKeeper.getKernelPromise(id); + function getUnresolvedPromise(kpid) { + const p = kernelKeeper.getKernelPromise(kpid); if (p.state !== 'unresolved') { - throw new Error(`kernelPromise[${id}] is '${p.state}', not 'unresolved'`); + throw new Error( + `kernelPromise[${kpid}] is '${p.state}', not 'unresolved'`, + ); } return p; } - function fulfillToPresence(id, targetSlot) { - if (targetSlot.type !== 'export') { - throw new Error( - `fulfillToPresence() must fulfill to export, not ${targetSlot.type}`, - ); - } - const { subscribers, queue } = getUnresolvedPromise(id); - kernelKeeper.replaceKernelPromise(id, { - state: 'fulfilledToPresence', - fulfillSlot: targetSlot, - }); + function fulfillToPresence(kpid, targetSlot) { + const { type } = parseKernelSlot(targetSlot); + insist( + type === 'object', + `fulfillToPresence() must fulfill to object, not ${type}`, + ); + const { subscribers, queue } = getUnresolvedPromise(kpid); + kernelKeeper.fulfillKernelPromiseToPresence(kpid, targetSlot); notifySubscribersAndQueue( - id, + kpid, subscribers, queue, 'notifyFulfillToPresence', ); - // kernelKeeper.deleteKernelPromiseData(id); + // kernelKeeper.deleteKernelPromiseData(kpid); } - function fulfillToData(id, data, slots) { - kdebug(`fulfillToData[${id}] -> ${data} ${JSON.stringify(slots)}`); - const { subscribers, queue } = getUnresolvedPromise(id); - kernelKeeper.replaceKernelPromise(id, { - state: 'fulfilledToData', - fulfillData: data, - fulfillSlots: slots, - }); - notifySubscribersAndQueue(id, subscribers, queue, 'notifyFulfillToData'); - // kernelKeeper.deleteKernelPromiseData(id); + function fulfillToData(kpid, data, slots) { + kdebug(`fulfillToData[${kpid}] -> ${data} ${JSON.stringify(slots)}`); + insistKernelType('promise', kpid); + const { subscribers, queue } = getUnresolvedPromise(kpid); + + kernelKeeper.fulfillKernelPromiseToData(kpid, data, slots); + notifySubscribersAndQueue(kpid, subscribers, queue, 'notifyFulfillToData'); + // kernelKeeper.deleteKernelPromiseData(kpid); // TODO: we can't delete the promise until all vat references are gone, // and certainly not until the notifyFulfillToData we just queued is // delivered } - function reject(id, val, valSlots) { - const { subscribers, queue } = getUnresolvedPromise(id); - kernelKeeper.replaceKernelPromise(id, { - state: 'rejected', - rejectData: val, - rejectSlots: valSlots, - }); - notifySubscribersAndQueue(id, subscribers, queue, 'notifyReject'); - // kernelKeeper.deleteKernelPromiseData(id); + function reject(kpid, val, valSlots) { + const { subscribers, queue } = getUnresolvedPromise(kpid); + kernelKeeper.rejectKernelPromise(kpid, val, valSlots); + notifySubscribersAndQueue(kpid, subscribers, queue, 'notifyReject'); + // kernelKeeper.deleteKernelPromiseData(kpid); } - function invoke(device, method, data, slots) { - const dev = ephemeral.devices.get(device.deviceName); + function invoke(deviceSlot, method, data, slots) { + insistKernelType('device', deviceSlot); + const deviceName = kernelKeeper.ownerOfKernelDevice(deviceSlot); + const dev = ephemeral.devices.get(deviceName); if (!dev) { - throw new Error(`unknown deviceRef ${JSON.stringify(device)}`); + throw new Error(`unknown deviceRef ${deviceSlot}`); } - return dev.manager.invoke(device, method, data, slots); + return dev.manager.invoke(deviceSlot, method, data, slots); } async function process(f, then, logerr) { @@ -237,53 +236,48 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { }; function addImport(forVatID, what) { + const kernelSlot = `${what}`; + insistKernelType('object', kernelSlot); if (!started) { throw new Error('must do kernel.start() before addImport()'); - // because then we can't get the vatManager + // because otherwise we can't get the vatManager } - const vat = ephemeral.vats.get(forVatID); - return vat.manager.mapKernelSlotToVatSlot(what); + const vat = ephemeral.vats.get(`${forVatID}`); + return vat.manager.mapKernelSlotToVatSlot(kernelSlot); } - function mapQueueSlotToKernelRealm(s) { - if (s.type === 'export') { - return harden({ - type: `${s.type}`, - vatID: `${s.vatID}`, - id: Nat(s.id), - }); - } - if (s.type === 'device') { - return harden({ - type: `${s.type}`, - deviceName: `${s.deviceName}`, - id: Nat(s.id), - }); + function addExport(fromVatID, what) { + const vatSlot = `${what}`; + insistVatType('object', vatSlot); + if (!started) { + throw new Error('must do kernel.start() before addExport()'); + // because otherwise we can't get the vatManager } - throw Error(`unrecognized type '${s.type}'`); + const vat = ephemeral.vats.get(fromVatID); + return vat.manager.mapVatSlotToKernelSlot(vatSlot); } - function queueToExport(vatID, facetID, method, argsString, slots = []) { + function queueToExport(vatID, vatSlot, method, argsString, slots = []) { + vatID = `${vatID}`; + vatSlot = `${vatSlot}`; if (!started) { throw new Error('must do kernel.start() before queueToExport()'); } - // queue a message on the end of the queue, with 'absolute' slots. Use - // 'step' or 'run' to execute it + slots.forEach(s => parseKernelSlot(s)); // typecheck + // queue a message on the end of the queue, with 'absolute' kernelSlots. + // Use 'step' or 'run' to execute it + const kernelSlot = addExport(vatID, vatSlot); kernelKeeper.addToRunQueue( harden({ - vatID: `${vatID}`, + vatID, // TODO remove vatID from run-queue type: 'deliver', - target: { - type: 'export', - vatID: `${vatID}`, - id: Nat(facetID), - }, + target: kernelSlot, msg: { method: `${method}`, argsString: `${argsString}`, // queue() is exposed to the controller's realm, so we must translate // each slot into a kernel-realm object/array - slots: Array.from(slots.map(mapQueueSlotToKernelRealm)), + slots: Array.from(slots.map(s => `${s}`)), kernelResolverID: null, // this will be json stringified }, }), @@ -319,6 +313,7 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { const vatObj0s = {}; kernelKeeper.getAllVatNames().forEach(name => { const targetVatID = name; + const vatManager = ephemeral.vats.get(targetVatID).manager; // we happen to give _bootstrap to itself, because unit tests that // don't have any other vats (bootstrap-only configs) then get a // non-empty object as vatObj0s, since an empty object would be @@ -330,7 +325,9 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { }, }); // marker vatObj0s[targetVatID] = vref; - vrefs.set(vref, { type: 'export', vatID: targetVatID, id: 0 }); + const vatSlot = makeVatSlot('object', true, 0); + const kernelSlot = vatManager.mapVatSlotToKernelSlot(vatSlot); + vrefs.set(vref, kernelSlot); console.log(`adding vref ${targetVatID}`); }); @@ -342,9 +339,12 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { // remove the dummy entry after we add the 'addVat' device const deviceObj0s = { _dummy: 'dummy' }; kernelKeeper.getAllDeviceNames().forEach(deviceName => { + const deviceManager = ephemeral.devices.get(deviceName).manager; const dref = harden({}); deviceObj0s[deviceName] = dref; - drefs.set(dref, { type: 'device', deviceName, id: 0 }); + const devSlot = makeVatSlot('device', true, 0); + const kernelSlot = deviceManager.mapDeviceSlotToKernelSlot(devSlot); + drefs.set(dref, kernelSlot); console.log(`adding dref ${deviceName}`); }); if (Object.getOwnPropertyNames(deviceObj0s) === 0) { @@ -370,8 +370,9 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { } const m = makeMarshal(serializeSlot); const s = m.serialize(harden({ args: [argv, vatObj0s, deviceObj0s] })); - // queueToExport() takes 'neutral' { type: export, vatID, slotID } objects in s.slots - queueToExport(vatID, 0, 'bootstrap', s.argsString, s.slots); + // queueToExport() takes kernel-refs (ko+NN, kd+NN) in s.slots + const boot0 = makeVatSlot('object', true, 0); + queueToExport(vatID, boot0, 'bootstrap', s.argsString, s.slots); } async function start(bootstrapVatID, argvString) { @@ -419,7 +420,6 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { ephemeral.vats.set( vatID, harden({ - id: vatID, manager, }), ); @@ -449,7 +449,6 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { ); // the vat record is not hardened: it holds mutable next-ID values ephemeral.devices.set(name, { - id: name, manager, }); } @@ -513,6 +512,7 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { }, addImport, + addExport, log(str) { ephemeral.log.push(`${str}`); diff --git a/src/kernel/liveSlots.js b/src/kernel/liveSlots.js index 6f352d02b56..5cf06b13aa4 100644 --- a/src/kernel/liveSlots.js +++ b/src/kernel/liveSlots.js @@ -1,6 +1,12 @@ import harden from '@agoric/harden'; import Nat from '@agoric/nat'; import { QCLASS, mustPassByPresence, makeMarshal } from '@agoric/marshal'; +import { insist } from './insist'; +import { + insistVatType, + makeVatSlot, + parseVatSlot, +} from '../vats/parseVatSlots'; // 'makeLiveSlots' is a dispatcher which uses javascript Maps to keep track // of local objects which have been exported. These cannot be persisted @@ -56,19 +62,8 @@ function build(syscall, _state, makeRoot, forVatID) { const outstandingProxies = new WeakSet(); - function slotToKey(slot) { - if ( - slot.type === 'export' || - slot.type === 'import' || - slot.type === 'promise' || - slot.type === 'deviceImport' - ) { - return `${slot.type}-${Nat(slot.id)}`; - } - throw new Error(`unknown slot.type '${slot.type}'`); - } const valToSlot = new WeakMap(); - const slotKeyToVal = new Map(); + const slotToVal = new Map(); const importedPromisesByPromiseID = new Map(); let nextExportID = 1; @@ -85,16 +80,16 @@ function build(syscall, _state, makeRoot, forVatID) { lsdebug(`ls exporting promise ${pr.resolverID}`); // eslint-disable-next-line no-use-before-define p.then(thenResolve(pr.resolverID), thenReject(pr.resolverID)); - return harden({ type: 'promise', id: pr.promiseID }); + return pr.promiseID; } function exportPassByPresence() { const exportID = allocateExportID(); - return harden({ type: 'export', id: exportID }); + return makeVatSlot('object', true, exportID); } - function serializeSlot(val, slots, slotMap) { - // lsdebug(`serializeSlot`, val, Object.isFrozen(val)); + function serializeToSlot(val, slots, slotMap) { + // lsdebug(`serializeToSlot`, val, Object.isFrozen(val)); // This is either a Presence (in presenceToImportID), a // previously-serialized local pass-by-presence object or // previously-serialized local Promise (in valToSlot), a new local @@ -117,9 +112,9 @@ function build(syscall, _state, makeRoot, forVatID) { mustPassByPresence(val); slot = exportPassByPresence(); } - const key = slotToKey(slot); + parseVatSlot(slot); // assertion valToSlot.set(val, slot); - slotKeyToVal.set(key, val); + slotToVal.set(slot, val); } slot = valToSlot.get(val); @@ -132,78 +127,69 @@ function build(syscall, _state, makeRoot, forVatID) { return harden({ [QCLASS]: 'slot', index: slotIndex }); } - function importedPromiseThen(id) { - syscall.subscribe(id); + function importedPromiseThen(vpid) { + insistVatType('promise', vpid); + syscall.subscribe(vpid); } - function importPromise(slot) { - const { id } = slot; - const pr = makeQueued(slot); + function importPromise(vpid) { + insistVatType('promise', vpid); + const pr = makeQueued(vpid); - importedPromisesByPromiseID.set(id, pr); + importedPromisesByPromiseID.set(vpid, pr); const { p } = pr; // ideally we'd wait until .then is called on p before subscribing, but // the current Promise API doesn't give us a way to discover this, so we // must subscribe right away. If we were using Vows or some other // then-able, we could just hook then() to notify us. - lsdebug(`ls[${forVatID}].importPromise.importedPromiseThen ${id}`); - importedPromiseThen(id); + lsdebug(`ls[${forVatID}].importPromise.importedPromiseThen ${vpid}`); + importedPromiseThen(vpid); return p; } function unserializeSlot(data, slots) { // lsdebug(`unserializeSlot ${data} ${slots}`); const slot = slots[Nat(data.index)]; - const key = slotToKey(slot); let val; - if (!slotKeyToVal.has(key)) { - if (slot.type === 'import') { + if (!slotToVal.has(slot)) { + const { type, allocatedByVat } = parseVatSlot(slot); + insist(!allocatedByVat, `I don't remember allocating ${slot}`); + if (type === 'object') { // this is a new import value - // lsdebug(`assigning new import ${slot.id}`); + // lsdebug(`assigning new import ${slot}`); const presence = harden({ toString() { - return `[Presence ${slot.id}]`; + return `[Presence ${slot}]`; }, }); + // prepare a Promise for this Presence, so E(val) can work const pr = makeQueued(slot); pr.res(presence); val = presence; // lsdebug(` for presence`, val); - } else if (slot.type === 'export') { - // huh, the kernel should never reference an export we didn't - // previously send - throw Error(`unrecognized exportID '${slot.id}'`); - } else if (slot.type === 'promise') { + } else if (type === 'promise') { val = importPromise(slot); - } else if (slot.type === 'deviceImport') { - val = makeDeviceNode(slot.id); + } else if (type === 'device') { + val = makeDeviceNode(slot); } else { - throw Error(`unrecognized slot.type '${slot.type}'`); + // todo (temporary): resolver? + throw Error(`unrecognized slot type '${type}'`); } - slotKeyToVal.set(key, val); + slotToVal.set(slot, val); valToSlot.set(val, slot); } - return slotKeyToVal.get(key); - } - - // this handles both exports ("targets" which other vats can call) - function getTarget(facetID) { - const key = slotToKey({ type: 'export', id: facetID }); - if (!slotKeyToVal.has(key)) { - throw Error(`no target for facetID ${facetID}`); - } - return slotKeyToVal.get(key); + return slotToVal.get(slot); } - const m = makeMarshal(serializeSlot, unserializeSlot); + const m = makeMarshal(serializeToSlot, unserializeSlot); function queueMessage(targetSlot, prop, args) { const ser = m.serialize(harden({ args })); lsdebug(`ls.qm send(${JSON.stringify(targetSlot)}, ${prop}`); const promiseID = syscall.send(targetSlot, prop, ser.argsString, ser.slots); + insistVatType('promise', promiseID); lsdebug(` ls.qm got promiseID ${promiseID}`); - const slot = { type: 'promise', id: promiseID }; - const done = makeQueued(slot); + const done = makeQueued(promiseID); // prepare for notifyFulfillToData/etc importedPromisesByPromiseID.set(promiseID, done); @@ -215,9 +201,8 @@ function build(syscall, _state, makeRoot, forVatID) { // prepare the serializer to recognize it, if it's used as an argument or // return value - const key = slotToKey(slot); - valToSlot.set(done.p, slot); - slotKeyToVal.set(key, done.p); + valToSlot.set(done.p, promiseID); + slotToVal.set(promiseID, done.p); return done.p; } @@ -277,7 +262,7 @@ function build(syscall, _state, makeRoot, forVatID) { const slot = valToSlot.get(x); - if (slot && slot.type === 'deviceImport') { + if (slot && parseVatSlot(slot).type === 'device') { throw new Error(`E() does not accept device nodes`); } @@ -320,7 +305,7 @@ function build(syscall, _state, makeRoot, forVatID) { throw new Error('D(D(x)) is invalid'); } const slot = valToSlot.get(x); - if (!slot || slot.type !== 'deviceImport') { + if (!slot || parseVatSlot(slot).type !== 'device') { throw new Error('D() must be given a device node'); } const handler = DeviceHandler(slot); @@ -329,11 +314,14 @@ function build(syscall, _state, makeRoot, forVatID) { return pr; } - function deliver(facetid, method, argsbytes, caps, { id: resolverID }) { + function deliver(target, method, argsbytes, caps, resolverID) { lsdebug( - `ls[${forVatID}].dispatch.deliver ${facetid}.${method} -> ${resolverID}`, + `ls[${forVatID}].dispatch.deliver ${target}.${method} -> ${resolverID}`, ); - const t = getTarget(facetid); + const t = slotToVal.get(target); + if (!t) { + throw Error(`no target ${target}`); + } const args = m.unserialize(argsbytes, caps); const p = Promise.resolve().then(_ => { if (!(method in t)) { @@ -354,6 +342,7 @@ function build(syscall, _state, makeRoot, forVatID) { }); if (resolverID !== undefined && resolverID !== null) { lsdebug(` ls.deliver attaching then ->${resolverID}`); + insistVatType('resolver', resolverID); // temporary // eslint-disable-next-line no-use-before-define p.then(thenResolve(resolverID), thenReject(resolverID)); } @@ -361,6 +350,7 @@ function build(syscall, _state, makeRoot, forVatID) { } function thenResolve(resolverID) { + insistVatType('resolver', resolverID); // temporary return res => { harden(res); lsdebug(`ls.thenResolve fired`, res); @@ -369,6 +359,7 @@ function build(syscall, _state, makeRoot, forVatID) { // could build a simpler way of doing this. const ser = m.serialize(res); lsdebug(` ser ${ser.argsString} ${JSON.stringify(ser.slots)}`); + // find out what resolution category we're using const unser = JSON.parse(ser.argsString); if ( Object(unser) === unser && @@ -376,8 +367,11 @@ function build(syscall, _state, makeRoot, forVatID) { unser[QCLASS] === 'slot' ) { const slot = ser.slots[unser.index]; - if (slot.type === 'import' || slot.type === 'export') { + const { type } = parseVatSlot(slot); + if (type === 'object') { syscall.fulfillToPresence(resolverID, slot); + } else { + throw new Error(`thenResolve to non-object slot ${slot}`); } } else { // if it resolves to data, .thens fire but kernel-queued messages are @@ -408,6 +402,7 @@ function build(syscall, _state, makeRoot, forVatID) { function notifyFulfillToData(promiseID, data, slots) { lsdebug(`ls.dispatch.notifyFulfillToData(${promiseID}, ${data}, ${slots})`); + insistVatType('promise', promiseID); if (!importedPromisesByPromiseID.has(promiseID)) { throw new Error(`unknown promiseID '${promiseID}'`); } @@ -417,6 +412,7 @@ function build(syscall, _state, makeRoot, forVatID) { function notifyFulfillToPresence(promiseID, slot) { lsdebug(`ls.dispatch.notifyFulfillToPresence(${promiseID}, ${slot})`); + insistVatType('promise', promiseID); if (!importedPromisesByPromiseID.has(promiseID)) { throw new Error(`unknown promiseID '${promiseID}'`); } @@ -426,6 +422,7 @@ function build(syscall, _state, makeRoot, forVatID) { function notifyReject(promiseID, data, slots) { lsdebug(`ls.dispatch.notifyReject(${promiseID}, ${data}, ${slots})`); + insistVatType('promise', promiseID); if (!importedPromisesByPromiseID.has(promiseID)) { throw new Error(`unknown promiseID '${promiseID}'`); } @@ -433,11 +430,13 @@ function build(syscall, _state, makeRoot, forVatID) { importedPromisesByPromiseID.get(promiseID).rej(val); } + // here we finally invoke the vat code, and get back the root object const rootObject = makeRoot(E, D); mustPassByPresence(rootObject); - const rootSlot = { type: 'export', id: 0 }; + + const rootSlot = makeVatSlot('object', true, 0); valToSlot.set(rootObject, rootSlot); - slotKeyToVal.set(slotToKey(rootSlot), rootObject); + slotToVal.set(rootSlot, rootObject); return { m, diff --git a/src/kernel/parseKernelSlots.js b/src/kernel/parseKernelSlots.js new file mode 100644 index 00000000000..199e6018bae --- /dev/null +++ b/src/kernel/parseKernelSlots.js @@ -0,0 +1,51 @@ +import Nat from '@agoric/nat'; +import { insist } from './insist'; + +// Object/promise references (in the kernel) contain a two-tuple of (type, +// index). All object references point to entries in the kernel Object +// Table, which records the vat that owns the actual object. In that vat, +// the object reference will be expressed as 'o+NN', and the NN was +// allocated by that vat when they first exported the reference into the +// kernel. In all other vats, if/when they are given a reference to this +// object, they will receive 'o-NN', with the NN allocated by the kernel +// clist for the recipient vat. + +export function parseKernelSlot(s) { + insist(s === `${s}`); + let type; + let idSuffix; + if (s.startsWith('ko')) { + type = 'object'; + idSuffix = s.slice(2); + } else if (s.startsWith('kd')) { + type = 'device'; + idSuffix = s.slice(2); + } else if (s.startsWith('kp')) { + type = 'promise'; + idSuffix = s.slice(2); + } else { + throw new Error(`invalid kernelSlot ${s}`); + } + const id = Nat(Number(idSuffix)); + return { type, id }; +} + +export function makeKernelSlot(type, id) { + if (type === 'object') { + return `ko${Nat(id)}`; + } + if (type === 'device') { + return `kd${Nat(id)}`; + } + if (type === 'promise') { + return `kp${Nat(id)}`; + } + throw new Error(`unknown type ${type}`); +} + +export function insistKernelType(type, kernelSlot) { + insist( + type === parseKernelSlot(kernelSlot).type, + `kernelSlot ${kernelSlot} is not of type ${type}`, + ); +} diff --git a/src/kernel/state/deviceKeeper.js b/src/kernel/state/deviceKeeper.js index ecc1d3ad982..206fe1d9774 100644 --- a/src/kernel/state/deviceKeeper.js +++ b/src/kernel/state/deviceKeeper.js @@ -1,66 +1,73 @@ import harden from '@agoric/harden'; -import Nat from '@agoric/nat'; +import { insist } from '../insist'; +import { parseKernelSlot } from '../parseKernelSlots'; +import { makeVatSlot, parseVatSlot } from '../../vats/parseVatSlots'; // makeVatKeeper is a pure function: all state is kept in the argument object -function makeDeviceKeeper(state) { +function makeDeviceKeeper(state, deviceName, addKernelObject, addKernelDevice) { function createStartingDeviceState() { - state.imports = { - outbound: {}, - inbound: {}, - }; - state.nextImportID = 10; + state.kernelSlotToDevSlot = {}; // kdNN -> d+NN, koNN -> o-NN + state.devSlotToKernelSlot = {}; // d+NN -> kdNN, o-NN -> koNN + state.nextObjectID = 10; // for imports, the NN in o-NN } - function allocateImportIndex() { - const i = state.nextImportID; - state.nextImportID = i + 1; - return i; - } - - function mapDeviceSlotToKernelSlot(slot) { - // kdebug(`mapOutbound ${JSON.stringify(slot)}`); + function mapDeviceSlotToKernelSlot(devSlot) { + insist(`${devSlot}` === devSlot, 'non-string devSlot'); + // kdebug(`mapOutbound ${devSlot}`); + const existing = state.devSlotToKernelSlot[devSlot]; + if (existing === undefined) { + const { type, allocatedByVat } = parseVatSlot(devSlot); - // export already handled - - if (slot.type === 'import') { - // an import from somewhere else, so look in the sending Vat's table to - // translate into absolute form - Nat(slot.id); - return state.imports.outbound[`${slot.id}`]; + if (allocatedByVat) { + let kernelSlot; + if (type === 'object') { + kernelSlot = addKernelObject(deviceName); + } else if (type === 'promise') { + throw new Error(`devices do not accept Promises`); + } else if (type === 'device') { + kernelSlot = addKernelDevice(deviceName); + } else { + throw new Error(`unknown type ${type}`); + } + state.devSlotToKernelSlot[devSlot] = kernelSlot; + state.kernelSlotToDevSlot[kernelSlot] = devSlot; + } else { + // the vat didn't allocate it, and the kernel didn't allocate it + // (else it would have been in the c-list), so it must be bogus + throw new Error(`unknown devSlot ${devSlot}`); + } } - throw Error(`unknown slot.type '${slot.type}'`); + return state.devSlotToKernelSlot[devSlot]; } // mapInbound: convert from absolute slot to deviceName-relative slot. This // is used when building the arguments for dispatch.invoke. - function mapKernelSlotToDeviceSlot(slot) { - // device already handled + function mapKernelSlotToDeviceSlot(kernelSlot) { + insist(`${kernelSlot}` === kernelSlot, 'non-string kernelSlot'); + const existing = state.kernelSlotToDevSlot[kernelSlot]; + if (existing === undefined) { + const { type } = parseKernelSlot(kernelSlot); - if (slot.type === 'export') { - const { vatID: fromVatID, id } = slot; - Nat(id); - - const { imports } = state; - const { inbound } = imports; - const { outbound } = imports; - const key = `${slot.type}.${fromVatID}.${id}`; // ugh javascript - if (!Object.getOwnPropertyDescriptor(inbound, key)) { - // must add both directions - const newSlotID = Nat(allocateImportIndex()); - // kdebug(` adding ${newSlotID}`); - inbound[key] = newSlotID; - outbound[`${newSlotID}`] = harden({ - type: 'export', - vatID: fromVatID, - id, - }); // TODO just 'slot'? + let devSlot; + if (type === 'object') { + const id = state.nextObjectID; + state.nextObjectID += 1; + devSlot = makeVatSlot(type, false, id); + } else if (type === 'device') { + throw new Error('devices cannot import other device nodes'); + } else if (type === 'promise') { + throw new Error('devices cannot import Promises'); + } else { + throw new Error(`unknown type ${type}`); } - return { type: 'import', id: inbound[key] }; + + state.devSlotToKernelSlot[devSlot] = kernelSlot; + state.kernelSlotToDevSlot[kernelSlot] = devSlot; } - throw Error(`unknown type '${slot.type}'`); + return state.kernelSlotToDevSlot[kernelSlot]; } function getDeviceState() { @@ -71,12 +78,22 @@ function makeDeviceKeeper(state) { state.deviceState = value; } + function dumpState() { + const res = []; + Object.getOwnPropertyNames(state.kernelSlotToDevSlot).forEach(ks => { + const ds = state.kernelSlotToDevSlot[ks]; + res.push([ks, deviceName, ds]); + }); + return harden(res); + } + return harden({ createStartingDeviceState, mapDeviceSlotToKernelSlot, mapKernelSlotToDeviceSlot, getDeviceState, setDeviceState, + dumpState, }); } diff --git a/src/kernel/state/kernelKeeper.js b/src/kernel/state/kernelKeeper.js index 9665593b1aa..db9ce576a06 100644 --- a/src/kernel/state/kernelKeeper.js +++ b/src/kernel/state/kernelKeeper.js @@ -1,7 +1,7 @@ import harden from '@agoric/harden'; -import Nat from '@agoric/nat'; import makeVatKeeper from './vatKeeper'; import makeDeviceKeeper from './deviceKeeper'; +import { insistKernelType, makeKernelSlot } from '../parseKernelSlots'; // This holds all the kernel state, including that of each Vat and Device, in // a single JSON-serializable object. At any moment (well, really only @@ -24,69 +24,122 @@ function makeKernelKeeper(initialState) { state.vats = {}; state.devices = {}; state.runQueue = []; - state.kernelPromises = {}; + state.kernelObjects = {}; // kernelObjects[koNN] = { owner: vatID } + state.nextObjectIndex = 20; + state.kernelDevices = {}; // kernelDevices[kdNN] = { owner: vatID } + state.nextDeviceIndex = 30; + state.kernelPromises = {}; // kernelPromises[kpNN] = {..} state.nextPromiseIndex = 40; } + function addKernelObject(ownerVatID) { + const id = state.nextObjectIndex; + state.nextObjectIndex = id + 1; + const s = makeKernelSlot('object', id); + state.kernelObjects[s] = harden({ + owner: ownerVatID, + }); + return s; + } + + function ownerOfKernelObject(kernelSlot) { + insistKernelType('object', kernelSlot); + return state.kernelObjects[kernelSlot].owner; + } + + function addKernelDevice(deviceName) { + const id = state.nextDeviceIndex; + state.nextDeviceIndex = id + 1; + const s = makeKernelSlot('device', id); + state.kernelDevices[s] = harden({ + owner: deviceName, + }); + return s; + } + + function ownerOfKernelDevice(kernelSlot) { + insistKernelType('device', kernelSlot); + return state.kernelDevices[kernelSlot].owner; + } + function addKernelPromise(deciderVatID) { - function allocateNextPromiseIndex() { - const id = state.nextPromiseIndex; - state.nextPromiseIndex = id + 1; - return id; - } - const kernelPromiseID = allocateNextPromiseIndex(); + const kernelPromiseID = state.nextPromiseIndex; + state.nextPromiseIndex = kernelPromiseID + 1; + const s = makeKernelSlot('promise', kernelPromiseID); - const kernelPromiseObj = { + // we leave this unfrozen, because the queue and subscribers are mutable + state.kernelPromises[s] = { state: 'unresolved', decider: deciderVatID, queue: [], subscribers: [], }; - state.kernelPromises[kernelPromiseID] = kernelPromiseObj; - return kernelPromiseID; + return s; } - function getKernelPromise(kernelPromiseID) { - const p = state.kernelPromises[Nat(kernelPromiseID)]; + function getKernelPromise(kernelSlot) { + insistKernelType('promise', kernelSlot); + const p = state.kernelPromises[kernelSlot]; if (p === undefined) { - throw new Error(`unknown kernelPromise id '${kernelPromiseID}'`); + throw new Error(`unknown kernelPromise '${kernelSlot}'`); } return p; } - function hasKernelPromise(kernelPromiseID) { - return !!Object.getOwnPropertyDescriptor( - state.kernelPromises, - Nat(kernelPromiseID), - ); + function hasKernelPromise(kernelSlot) { + insistKernelType('promise', kernelSlot); + return !!Object.getOwnPropertyDescriptor(state.kernelPromises, kernelSlot); + } + + function fulfillKernelPromiseToPresence(kernelSlot, targetSlot) { + insistKernelType('promise', kernelSlot); + state.kernelPromises[kernelSlot] = harden({ + state: 'fulfilledToPresence', + fulfillSlot: targetSlot, + }); } - function replaceKernelPromise(kernelPromiseID, p) { - state.kernelPromises[Nat(kernelPromiseID)] = p; + function fulfillKernelPromiseToData(kernelSlot, data, slots) { + insistKernelType('promise', kernelSlot); + state.kernelPromises[kernelSlot] = harden({ + state: 'fulfilledToData', + fulfillData: data, + fulfillSlots: slots, + }); } - function deleteKernelPromiseData(kernelPromiseID) { - delete state.kernelPromises[Nat(kernelPromiseID)]; + function rejectKernelPromise(kernelSlot, val, valSlots) { + insistKernelType('promise', kernelSlot); + state.kernelPromises[kernelSlot] = harden({ + state: 'rejected', + rejectData: val, + rejectSlots: valSlots, + }); } - function addMessageToPromiseQueue(kernelPromiseID, msg) { - const p = state.kernelPromises[Nat(kernelPromiseID)]; + function deleteKernelPromiseData(kernelSlot) { + insistKernelType('promise', kernelSlot); + delete state.kernelPromises[kernelSlot]; + } + + function addMessageToPromiseQueue(kernelSlot, msg) { + insistKernelType('promise', kernelSlot); + const p = state.kernelPromises[kernelSlot]; if (p === undefined) { - throw new Error(`unknown kernelPromise id '${kernelPromiseID}'`); + throw new Error(`unknown kernelPromise '${kernelSlot}'`); } if (p.state !== 'unresolved') { - throw new Error( - `kernelPromise[${kernelPromiseID}] is '${p.state}', not 'unresolved'`, - ); + throw new Error(`${kernelSlot} is '${p.state}', not 'unresolved'`); } p.queue.push(msg); } - function addSubscriberToPromise(kernelPromiseID, vatID) { - const p = state.kernelPromises[Nat(kernelPromiseID)]; + function addSubscriberToPromise(kernelSlot, vatID) { + insistKernelType('promise', kernelSlot); + const p = state.kernelPromises[kernelSlot]; if (p === undefined) { - throw new Error(`unknown kernelPromise id '${kernelPromiseID}'`); + throw new Error(`unknown kernelPromise '${kernelSlot}'`); } const subscribersSet = new Set(p.subscribers); subscribersSet.add(vatID); @@ -115,7 +168,7 @@ function makeKernelKeeper(initialState) { if (vatState === undefined) { throw new Error(`unknown vatID id '${vatID}'`); } - return makeVatKeeper(vatState); + return makeVatKeeper(vatState, vatID, addKernelObject, addKernelPromise); } function createVat(vatID) { @@ -125,7 +178,12 @@ function makeKernelKeeper(initialState) { } const vatState = {}; state.vats[vatID] = vatState; - const vk = makeVatKeeper(vatState, vatID); + const vk = makeVatKeeper( + vatState, + vatID, + addKernelObject, + addKernelPromise, + ); vk.createStartingVatState(); return vk; } @@ -140,7 +198,12 @@ function makeKernelKeeper(initialState) { if (deviceState === undefined) { throw new Error(`unknown deviceID id '${deviceID}'`); } - return makeDeviceKeeper(deviceState); + return makeDeviceKeeper( + deviceState, + deviceID, + addKernelObject, + addKernelDevice, + ); } function createDevice(deviceID) { @@ -150,7 +213,12 @@ function makeKernelKeeper(initialState) { } const deviceState = {}; state.devices[deviceID] = deviceState; - const dk = makeDeviceKeeper(deviceState); + const dk = makeDeviceKeeper( + deviceState, + deviceID, + addKernelObject, + addKernelDevice, + ); dk.createStartingDeviceState(); return dk; } @@ -173,9 +241,7 @@ function makeKernelKeeper(initialState) { const vatTables = []; const kernelTable = []; - const { vats } = state; - - for (const vatID of Object.getOwnPropertyNames(vats)) { + for (const vatID of getAllVatNames()) { const vk = getVat(vatID); // TODO: find some way to expose the liveSlots internal tables, the @@ -185,7 +251,12 @@ function makeKernelKeeper(initialState) { state: { transcript: vk.getTranscript() }, }; vatTables.push(vatTable); - vk.dumpState(vatID).forEach(e => kernelTable.push(e)); + vk.dumpState().forEach(e => kernelTable.push(e)); + } + + for (const deviceName of getAllDeviceNames()) { + const dk = getDevice(deviceName); + dk.dumpState().forEach(e => kernelTable.push(e)); } function compareNumbers(a, b) { @@ -216,15 +287,16 @@ function makeKernelKeeper(initialState) { const promises = []; const { kernelPromises } = state; - Object.getOwnPropertyNames(kernelPromises).forEach(id => { - const p = kernelPromises[id]; - const kp = { id: Number(id) }; + Object.getOwnPropertyNames(kernelPromises).forEach(s => { + const kp = { id: s }; + const p = kernelPromises[s]; Object.defineProperties(kp, Object.getOwnPropertyDescriptors(p)); if (p.subscribers) { kp.subscribers = Array.from(p.subscribers); } promises.push(kp); }); + promises.sort((a, b) => compareNumbers(a.id, b.id)); const runQueue = Array.from(state.runQueue); @@ -241,10 +313,15 @@ function makeKernelKeeper(initialState) { setInitialized, createStartingKernelState, + ownerOfKernelObject, + ownerOfKernelDevice, + addKernelPromise, getKernelPromise, hasKernelPromise, - replaceKernelPromise, + fulfillKernelPromiseToPresence, + fulfillKernelPromiseToData, + rejectKernelPromise, deleteKernelPromiseData, addMessageToPromiseQueue, addSubscriberToPromise, diff --git a/src/kernel/state/vatKeeper.js b/src/kernel/state/vatKeeper.js index f5ebb3c74ff..7ed6b122d4e 100644 --- a/src/kernel/state/vatKeeper.js +++ b/src/kernel/state/vatKeeper.js @@ -1,188 +1,110 @@ import harden from '@agoric/harden'; -import Nat from '@agoric/nat'; import { insist } from '../insist'; +import { insistKernelType, parseKernelSlot } from '../parseKernelSlots'; +import { makeVatSlot, parseVatSlot } from '../../vats/parseVatSlots'; // makeVatKeeper is a pure function: all state is kept in the argument object -export default function makeVatKeeper(state) { +export default function makeVatKeeper( + state, + vatID, + addKernelObject, + addKernelPromise, +) { function createStartingVatState() { // kernelSlotToVatSlot is an object with four properties: // exports, devices, promises, resolvers. // vatSlotToKernelSlot has imports, deviceImports, promises, // resolvers - state.kernelSlotToVatSlot = { - exports: {}, - devices: {}, - promises: {}, - resolvers: {}, - }; - state.vatSlotToKernelSlot = { - imports: {}, - deviceImports: {}, - promises: {}, - resolvers: {}, - }; - - state.nextIDs = { - import: 10, - promise: 20, - resolver: 30, - deviceImport: 40, - }; + state.kernelSlotToVatSlot = {}; // kpNN -> p+NN, etc + state.vatSlotToKernelSlot = {}; // p+NN -> kpNN, etc - state.transcript = []; - } - - const allowedVatSlotTypes = [ - 'export', - 'import', - 'deviceImport', - 'promise', - 'resolver', - ]; + state.nextObjectID = 50; + state.nextPromiseID = 60; + state.nextDeviceID = 70; - const allowedKernelSlotTypes = ['export', 'device', 'promise', 'resolver']; - - function insistVatSlot(slot) { - const properties = Object.getOwnPropertyNames(slot); - insist( - properties.length === 2, - `wrong number of properties for a vatSlot ${JSON.stringify(slot)}`, - ); - Nat(slot.id); - insist( - allowedVatSlotTypes.includes(slot.type), - `unknown slot.type in '${JSON.stringify(slot)}'`, - ); + state.transcript = []; } - function insistKernelSlot(slot) { - const properties = Object.getOwnPropertyNames(slot); - insist( - properties.length === 3 || - (properties.length === 2 && - (slot.type === 'promise' || slot.type === 'resolver')), - `wrong number of properties for a kernelSlot ${JSON.stringify(slot)}`, - ); // a kernel slot has a vatID property, unless it is a promise or a resolver - Nat(slot.id); + function insistVatSlotType(type, slot) { insist( - allowedKernelSlotTypes.includes(slot.type), - `unknown slot.type in '${JSON.stringify(slot)}'`, + type === parseVatSlot(slot).type, + `vatSlot ${slot} is not of type ${type}`, ); } - function getKernelSlotTypedMapAndKey(kernelSlot) { - insistKernelSlot(kernelSlot); - - const { type, id } = kernelSlot; - - const tables = state.kernelSlotToVatSlot; - - switch (type) { - case 'promise': { - return { - table: tables.promises, - key: id, - }; - } - case 'resolver': { - return { - table: tables.resolvers, - key: id, - }; - } - case 'export': { - return { - table: tables.exports, - key: `${kernelSlot.vatID}-${id}`, - }; - } - case 'device': { - return { - table: tables.devices, - key: `${kernelSlot.deviceName}-${id}`, - }; + function mapVatSlotToKernelSlot(vatSlot) { + insist(`${vatSlot}` === vatSlot, 'non-string vatSlot'); + const existing = state.vatSlotToKernelSlot[vatSlot]; + if (existing === undefined) { + const { type, allocatedByVat } = parseVatSlot(vatSlot); + + if (allocatedByVat) { + let kernelSlot; + if (type === 'object') { + kernelSlot = addKernelObject(vatID); + } else if (type === 'device') { + throw new Error(`normal vats aren't allowed to export device nodes`); + } else if (type === 'promise') { + kernelSlot = addKernelPromise(vatID); + } else { + throw new Error(`unknown type ${type}`); + } + state.vatSlotToKernelSlot[vatSlot] = kernelSlot; + state.kernelSlotToVatSlot[kernelSlot] = vatSlot; + } else { + // the vat didn't allocate it, and the kernel didn't allocate it + // (else it would have been in the c-list), so it must be bogus + throw new Error(`unknown vatSlot ${vatSlot}`); } - default: - throw new Error(`unexpected kernelSlot type ${kernelSlot.type}`); } - } - - function getVatSlotTypedMapAndKey(vatSlot) { - insistVatSlot(vatSlot); - const { type, id } = vatSlot; - const tables = state.vatSlotToKernelSlot; - let table; - // imports, deviceImports, promises, resolvers + return state.vatSlotToKernelSlot[vatSlot]; + } - switch (type) { - case 'import': { - table = tables.imports; - break; - } - case 'deviceImport': { - table = tables.deviceImports; - break; - } - case 'promise': { - table = tables.promises; - break; - } - case 'resolver': { - table = tables.resolvers; - break; + function mapKernelSlotToVatSlot(kernelSlot) { + insist(`${kernelSlot}` === kernelSlot, 'non-string kernelSlot'); + const existing = state.kernelSlotToVatSlot[kernelSlot]; + if (existing === undefined) { + const { type } = parseKernelSlot(kernelSlot); + + let vatSlot; + if (type === 'object') { + const id = state.nextObjectID; + state.nextObjectID += 1; + vatSlot = makeVatSlot(type, false, id); + } else if (type === 'device') { + const id = state.nextDeviceID; + state.nextDeviceID += 1; + vatSlot = makeVatSlot(type, false, id); + } else if (type === 'promise') { + const id = state.nextPromiseID; + state.nextPromiseID += 1; + vatSlot = makeVatSlot(type, false, id); + } else { + throw new Error(`unknown type ${type}`); } - default: - throw new Error(`unexpected vatSlot type ${vatSlot.type}`); - } - return { - table, - key: `${type}-${id}`, - }; - } - function getVatSlotTypeFromKernelSlot(kernelSlot) { - switch (kernelSlot.type) { - case 'export': - return 'import'; - case 'device': - return 'deviceImport'; - case 'promise': - return 'promise'; - case 'resolver': - return 'resolver'; - default: - throw new Error('unrecognized kernelSlot type'); + state.vatSlotToKernelSlot[vatSlot] = kernelSlot; + state.kernelSlotToVatSlot[kernelSlot] = vatSlot; } - } - function mapVatSlotToKernelSlot(vatSlot) { - const { table, key } = getVatSlotTypedMapAndKey(vatSlot); - const kernelSlot = table[key]; - if (kernelSlot === undefined) { - throw new Error(`unknown ${vatSlot.type} slot '${vatSlot.id}'`); - } - return kernelSlot; + return state.kernelSlotToVatSlot[kernelSlot]; } - function mapKernelSlotToVatSlot(kernelSlot) { - const { table, key } = getKernelSlotTypedMapAndKey(kernelSlot); - if (!Object.getOwnPropertyDescriptor(table, `${key}`)) { - // must add both directions - const vatSlotType = getVatSlotTypeFromKernelSlot(kernelSlot); - const { nextIDs } = state; - const newVatSlotID = nextIDs[vatSlotType]; - nextIDs[vatSlotType] = newVatSlotID + 1; - const vatSlot = { type: vatSlotType, id: newVatSlotID }; - table[`${key}`] = vatSlot; - const { table: vatSlotTable, key: vatSlotKey } = getVatSlotTypedMapAndKey( - vatSlot, - ); - vatSlotTable[vatSlotKey] = kernelSlot; + // temporary + function mapKernelPromiseToVatResolver(kernelSlot) { + insistKernelType('promise', kernelSlot); + const vp = mapKernelSlotToVatSlot(kernelSlot); + const parsed = parseVatSlot(vp); + const vr = makeVatSlot('resolver', parsed.allocatedByVat, parsed.id); + const existing = state.vatSlotToKernelSlot[vr]; + if (existing === undefined) { + // the vat resolver and the vat promise both map to the kernel promise + state.vatSlotToKernelSlot[vr] = kernelSlot; } - return table[`${key}`]; + return vr; } function getTranscript() { @@ -194,33 +116,12 @@ export default function makeVatKeeper(state) { } // pretty print for logging and testing - function dumpState(vatID) { + function dumpState() { const res = []; - - function printSlots(vatSlot) { - const { table, key } = getVatSlotTypedMapAndKey(vatSlot); - const kernelSlot = table[key]; - if (vatSlot.type === 'promise' || vatSlot.type === 'resolver') { - res.push([vatID, vatSlot.type, vatSlot.id, kernelSlot.id]); - } else { - res.push([ - vatID, - vatSlot.type, - vatSlot.id, - kernelSlot.type, - kernelSlot.vatID, - kernelSlot.id, - ]); - } - } - - // 'exports', 'devices', 'promises', 'resolvers' - - for (const n of ['exports', 'devices', 'promises', 'resolvers']) { - const t = state.kernelSlotToVatSlot[n]; - Object.getOwnPropertyNames(t).forEach(name => printSlots(t[name])); - } - + Object.getOwnPropertyNames(state.kernelSlotToVatSlot).forEach(ks => { + const vs = state.kernelSlotToVatSlot[ks]; + res.push([ks, vatID, vs]); + }); return harden(res); } @@ -228,10 +129,10 @@ export default function makeVatKeeper(state) { createStartingVatState, mapVatSlotToKernelSlot, mapKernelSlotToVatSlot, + mapKernelPromiseToVatResolver, getTranscript, dumpState, addToTranscript, - insistKernelSlot, - insistVatSlot, + insistVatSlotType, }); } diff --git a/src/kernel/vatManager.js b/src/kernel/vatManager.js index 322c6be0852..5a1c307ddae 100644 --- a/src/kernel/vatManager.js +++ b/src/kernel/vatManager.js @@ -1,6 +1,8 @@ import harden from '@agoric/harden'; -import Nat from '@agoric/nat'; import djson from './djson'; +import { insist } from './insist'; +import { insistKernelType, parseKernelSlot } from './parseKernelSlots'; +import { insistVatType, parseVatSlot } from '../vats/parseVatSlots'; export default function makeVatManager( vatID, @@ -41,11 +43,10 @@ export default function makeVatManager( // "exports" are callable objects inside the Vat which it has made // available to the kernel (so that other vats can invoke it). The exports // table is managed by userspace code inside the vat. The kernel tables map - // one vat's import IDs to a pair of (exporting vat, vat's export-id) in - // `vats[vatid].imports.outbound[importID]`. To make sure we use the same - // importID each time, we also need to keep a reverse table: - // `vats[vatid].imports.inbound` maps the (exporting-vat, export-id) back - // to the importID. + // one vat's import IDs (o-NN) to a kernel object ID (koNN) in the + // vatKeeper's state.vatSlotToKernelSlot table. To make sure we use the + // same importID each time, we also need to keep a reverse table: + // kernelSlotToVatSlot maps them back. // Comms vats will have their own internal tables to track references // shared with other machines. These will have mapInbound/mapOutbound too. @@ -57,88 +58,38 @@ export default function makeVatManager( // point to other machines, usually the sending machine). The machine // imports will be presented to the kernel as exports of the comms vat. - // 'id' is always a non-negative integer (a Nat) - // 'slot' is a type(string)+id, plus maybe a vatid (string) - // * 'relative slot' lacks a vatid because it is implicit - // used in syscall.send and dispatch.deliver - // * 'absolute slot' includes an explicit vatid - // used in kernel's runQueue - // 'slots' is an array of 'slot', in send/deliver - // 'slotIndex' is an index into 'slots', used in serialized JSON - - // key = `${type}.${toVatID}.${slotID}` - // tables = { imports: { inbound[key] = importID, - // outbound[importID] = slot, }, - // nextImportID, - // promises: { inbound[kernelPromiseID] = id, - // outbound[id] = kernelPromiseID, }, - // nextPromiseID, - // resolvers: { inbound[kernelPromiseID] = id, - // outbound[id] = kernelPromiseID, }, - // nextResolverID, - // } - - // we define three types of slot identifiers: inbound, neutral, outbound - // * outbound is what syscall.send(slots=) contains, it is always scoped to - // the sending vat, and the values are {type, id}. 'type' is either - // 'import' ("my import") or 'export'. Message targets are always imports. - // * middle is stored in runQueue, and contains {type: export, vatID, slotID} - // * inbound is passed into deliver(slots=), is always scoped to the - // receiving/target vat, and the values are {type, id} where 'type' is - // either 'export' (for arguments coming back home) or 'import' for - // arguments from other vats - // - // * To convert outbound->middle, we look up imports in kernelSlots, and - // just append the sending vatID to exports - // - // * To convert middle->inbound, we set attach type='export' when the vatID - // matches that of the receiving vat, and we look up the others in - // kernelSlots (adding one if necessary) to deliver type='import' - - // mapOutbound: convert fromVatID-relative slot to absolute slot. fromVatID - // just did syscall.send and referenced 'slot' (in an argument, or as the - // target of a send), what are they talking about? - - // vatSlot to kernelSlot - // mapOutbound - function mapVatSlotToKernelSlot(vatSlot) { - vatKeeper.insistVatSlot(vatSlot); - const { type, id } = vatSlot; + // The vat sees "vat slots" (object references) as the arguments of + // syscall/dispatch functions. These take on the following forms (where + // "NN" is an integer): - if (type === 'export') { - // one of our exports, so just make the vatID explicit - return { type: 'export', vatID, id }; - } + // o+NN : an object ref allocated by this Vat, hence an export + // o-NN : an object ref allocated by the kernel, an imported object + // p-NN : a promise ref allocated by the kernel + // p+NN : (todo) a promise ref allocated by this vat + // d-NN : a device ref allocated by the kernel, imported + + // Within the kernel, we use "kernel slots", with the following forms: + // koNN : an object reference + // kpNN : a promise reference + // kdNN : a device reference + + // The vatManager is responsible for translating vat slots into kernel + // slots on the outbound (syscall) path, and kernel slots back into vat + // slots on the inbound (dispatch) path. + + // mapOutbound: e.g. arguments of syscall.send() + function mapVatSlotToKernelSlot(vatSlot) { + insist(`${vatSlot}` === vatSlot, 'non-string vatSlot'); return vatKeeper.mapVatSlotToKernelSlot(vatSlot); } - // kernelSlot to VatSlot - // mapInbound: convert from absolute slot to forVatID-relative slot. This - // is used when building the arguments for dispatch.deliver. - + // mapInbound: e.g. arguments of dispatch.deliver() function mapKernelSlotToVatSlot(kernelSlot) { + insist(`${kernelSlot}` === kernelSlot, 'non-string kernelSlot'); kdebug( `mapKernelSlotToVatSlot for ${vatID} of ${JSON.stringify(kernelSlot)}`, ); - - if (Object.getPrototypeOf(kernelSlot) !== Object.getPrototypeOf({})) { - throw new Error( - `hey, mapInbound given wrong-realm slot ${JSON.stringify(kernelSlot)}`, - ); - } - - vatKeeper.insistKernelSlot(kernelSlot); - - if (kernelSlot.type === 'export') { - const { vatID: fromVatID, id } = kernelSlot; - - if (vatID === fromVatID) { - // this is returning home, so it's one of the target's exports - return { type: 'export', id }; - } - } - return vatKeeper.mapKernelSlotToVatSlot(kernelSlot); } @@ -164,41 +115,20 @@ export default function makeVatManager( // available to userspace function doSend(targetSlot, method, argsString, vatSlots) { - if (targetSlot.type === undefined) { - throw new Error( - `targetSlot isn't really a slot ${JSON.stringify(targetSlot)}`, - ); - } - - if (targetSlot.type === 'export') { - // Disable send-to-self for now. It might be useful in the future, but - // I doubt it, and we can prevent some confusion by flagging a specific - // error here. See issue #43 for details. - throw new Error(`send() is calling itself, see issue #43`); - } - + insist(`${targetSlot}` === targetSlot, 'non-string targetSlot'); + // TODO: disable send-to-self for now, qv issue #43 const target = mapVatSlotToKernelSlot(targetSlot); - - if (!target) { - throw Error( - `unable to find target for ${vatID}/${targetSlot.type}-${targetSlot.id}`, - ); - } - kdebug( - `syscall[${vatID}].send(vat:${JSON.stringify( - targetSlot, - )}=ker:${JSON.stringify(target)}).${method}`, - ); + kdebug(`syscall[${vatID}].send(vat:${targetSlot}=ker:${target}).${method}`); const slots = vatSlots.map(slot => mapVatSlotToKernelSlot(slot)); // who will decide the answer? If the message is being queued for a // promise, then the kernel will decide (when the answer gets // resolved). If it is going to a specific export, the exporting vat // gets to decide. let decider; - if (target.type === 'export') { - decider = target.vatID; + if (parseKernelSlot(target).type === 'object') { + decider = kernelKeeper.ownerOfKernelObject(target); } - kdebug(` ^target is ${JSON.stringify(target)}`); + kdebug(` ^target is ${target}`); const kernelPromiseID = createPromiseWithDecider(decider); const msg = { method, @@ -207,34 +137,21 @@ export default function makeVatManager( kernelResolverID: kernelPromiseID, }; send(target, msg); - const p = mapKernelSlotToVatSlot({ - type: 'promise', - id: kernelPromiseID, - }); - return p.id; // relative to caller + return mapKernelSlotToVatSlot(kernelPromiseID); } function doCreatePromise() { const kernelPromiseID = createPromiseWithDecider(vatID); - const p = mapKernelSlotToVatSlot({ - type: 'promise', - id: kernelPromiseID, - }); - const r = mapKernelSlotToVatSlot({ - type: 'resolver', - id: kernelPromiseID, - }); + const p = mapKernelSlotToVatSlot(kernelPromiseID); kdebug( - `syscall[${vatID}].createPromise -> (vat:p${p.id}/r${r.id}=ker:${kernelPromiseID})`, + `syscall[${vatID}].createPromise -> (vat:${p}=ker:${kernelPromiseID})`, ); - return harden({ promiseID: p.id, resolverID: r.id }); + const r = vatKeeper.mapKernelPromiseToVatResolver(kernelPromiseID); // temporary + return harden({ promiseID: p, resolverID: r }); } function doSubscribe(promiseID) { - const { id } = mapVatSlotToKernelSlot({ - type: 'promise', - id: promiseID, - }); + const id = mapVatSlotToKernelSlot(promiseID); kdebug(`syscall[${vatID}].subscribe(vat:${promiseID}=ker:${id})`); if (!kernelKeeper.hasKernelPromise(id)) { throw new Error(`unknown kernelPromise id '${id}'`); @@ -242,6 +159,11 @@ export default function makeVatManager( const p = kernelKeeper.getKernelPromise(id); /* + // Originally the kernel didn't subscribe to a vat's promise until some + // other vat had subscribed to the kernel's promise. That proved too hard + // to manage, so now the kernel always subscribes to every vat promise. + // This code was to handle the original behavior but has bitrotted + // because it is unused. kdebug(` decider is ${p.decider} in ${JSON.stringify(p)}`); if (p.subscribers.size === 0 && p.decider !== undefined) { runQueue.push({ @@ -311,73 +233,56 @@ export default function makeVatManager( } */ function doFulfillToData(resolverID, fulfillData, vatSlots) { - Nat(resolverID); - const { id } = mapVatSlotToKernelSlot({ - type: 'resolver', - id: resolverID, - }); - if (!kernelKeeper.hasKernelPromise(id)) { - throw new Error(`unknown kernelPromise id '${id}'`); - } + insistVatType('resolver', resolverID); + const kp = mapVatSlotToKernelSlot(resolverID); + insistKernelType('promise', kp); + insist(kernelKeeper.hasKernelPromise(kp), `unknown kernelPromise '${kp}'`); + const slots = vatSlots.map(slot => mapVatSlotToKernelSlot(slot)); kdebug( - `syscall[${vatID}].fulfillData(vatid=${resolverID}/kid=${id}) = ${fulfillData} v=${JSON.stringify( + `syscall[${vatID}].fulfillData(${resolverID}/${kp}) = ${fulfillData} ${JSON.stringify( vatSlots, - )}/k=${JSON.stringify(slots)}`, + )}/${JSON.stringify(slots)}`, ); - fulfillToData(id, fulfillData, slots); + fulfillToData(kp, fulfillData, slots); } function doFulfillToPresence(resolverID, slot) { - Nat(resolverID); - const { id } = mapVatSlotToKernelSlot({ - type: 'resolver', - id: resolverID, - }); - if (!kernelKeeper.hasKernelPromise(id)) { - throw new Error(`unknown kernelPromise id '${id}'`); - } + insistVatType('resolver', resolverID); + const kp = mapVatSlotToKernelSlot(resolverID); + insistKernelType('promise', kp); + insist(kernelKeeper.hasKernelPromise(kp), `unknown kernelPromise '${kp}'`); const targetSlot = mapVatSlotToKernelSlot(slot); kdebug( - `syscall[${vatID}].fulfillToPresence(vatid=${resolverID}/kid=${id}) = vat:${JSON.stringify( - targetSlot, - )}=ker:${id})`, + `syscall[${vatID}].fulfillToPresence(${resolverID}/${kp}) = ${slot}/${targetSlot})`, ); - fulfillToPresence(id, targetSlot); + fulfillToPresence(kp, targetSlot); } function doReject(resolverID, rejectData, vatSlots) { - Nat(resolverID); - const { id } = mapVatSlotToKernelSlot({ - type: 'resolver', - id: resolverID, - }); - if (!kernelKeeper.hasKernelPromise(id)) { - throw new Error(`unknown kernelPromise id '${id}'`); - } + insistVatType('resolver', resolverID); + const kp = mapVatSlotToKernelSlot(resolverID); + insistKernelType('promise', kp); + insist(kernelKeeper.hasKernelPromise(kp), `unknown kernelPromise '${kp}'`); + const slots = vatSlots.map(slot => mapVatSlotToKernelSlot(slot)); kdebug( - `syscall[${vatID}].reject(vatid=${resolverID}/kid=${id}) = ${rejectData} v=${JSON.stringify( + `syscall[${vatID}].reject(${resolverID}/${kp}) = ${rejectData} ${JSON.stringify( vatSlots, - )}/k=${JSON.stringify(slots)}`, + )}/${JSON.stringify(slots)}`, ); - reject(id, rejectData, slots); + reject(kp, rejectData, slots); } function doCallNow(target, method, argsString, argsSlots) { const dev = mapVatSlotToKernelSlot(target); - if (dev.type !== 'device') { - throw new Error( - `doCallNow must target a device, not ${JSON.stringify(target)}`, - ); + const { type } = parseKernelSlot(dev); + if (type !== 'device') { + throw new Error(`doCallNow must target a device, not ${dev}`); } const slots = argsSlots.map(slot => mapVatSlotToKernelSlot(slot)); - kdebug( - `syscall[${vatID}].callNow(vat:device:${target.id}=ker:${JSON.stringify( - dev, - )}).${method}`, - ); + kdebug(`syscall[${vatID}].callNow(${target}/${dev}).${method}`); const ret = invoke(dev, method, argsString, slots); const retSlots = ret.slots.map(slot => mapKernelSlotToVatSlot(slot)); return harden({ data: ret.data, slots: retSlots }); @@ -458,9 +363,11 @@ export default function makeVatManager( // dispatch handlers: these are used by the kernel core function doProcess(d, errmsg) { + const dispatchOp = d[0]; + const dispatchArgs = d.slice(1); transcriptStartDispatch(d); return process( - () => dispatch[d[0]](...d.slice(1)), + () => dispatch[dispatchOp](...dispatchArgs), () => transcriptFinishDispatch(), err => console.log(`doProcess: ${errmsg}: ${err}`, err), ); @@ -471,33 +378,25 @@ export default function makeVatManager( const { type } = message; if (type === 'deliver') { const { target, msg } = message; - if (target.type !== 'export') { - throw new Error( - `processOneMessage got 'deliver' for non-export ${JSON.stringify( - target, - )}`, - ); - } - if (target.vatID !== vatID) { - throw new Error( - `vatManager[${vatID}] given 'deliver' for ${target.vatID}`, - ); - } + // temporary, until we allow delivery of pipelined messages to vats + // which have opted-in + insistKernelType('object', target); + const targetSlot = mapKernelSlotToVatSlot(target); + insist(parseVatSlot(targetSlot).allocatedByVat, `deliver() to wrong vat`); const inputSlots = msg.slots.map(slot => mapKernelSlotToVatSlot(slot)); const resolverID = msg.kernelResolverID && - mapKernelSlotToVatSlot({ type: 'resolver', id: msg.kernelResolverID }) - .id; + vatKeeper.mapKernelPromiseToVatResolver(msg.kernelResolverID); return doProcess( [ 'deliver', - target.id, + targetSlot, msg.method, msg.argsString, inputSlots, - harden({ type: 'resolver', id: resolverID }), + resolverID, ], - `vat[${vatID}][${target.id}].${msg.method} dispatch failed`, + `vat[${vatID}][${targetSlot}].${msg.method} dispatch failed`, ); } @@ -515,43 +414,34 @@ export default function makeVatManager( if (type === 'notifyFulfillToData') { const { kernelPromiseID } = message; - const p = kernelKeeper.getKernelPromise(kernelPromiseID); - const relativeID = mapKernelSlotToVatSlot({ - type: 'promise', - id: kernelPromiseID, - }).id; - const slots = p.fulfillSlots.map(slot => mapKernelSlotToVatSlot(slot)); + const kp = kernelKeeper.getKernelPromise(kernelPromiseID); + const vpid = mapKernelSlotToVatSlot(kernelPromiseID); + const slots = kp.fulfillSlots.map(slot => mapKernelSlotToVatSlot(slot)); return doProcess( - ['notifyFulfillToData', relativeID, p.fulfillData, slots], - `vat[${vatID}].promise[${relativeID}] fulfillToData failed`, + ['notifyFulfillToData', vpid, kp.fulfillData, slots], + `vat[${vatID}].promise[${vpid}] fulfillToData failed`, ); } if (type === 'notifyFulfillToPresence') { const { kernelPromiseID } = message; - const p = kernelKeeper.getKernelPromise(kernelPromiseID); - const relativeID = mapKernelSlotToVatSlot({ - type: 'promise', - id: kernelPromiseID, - }).id; - const slot = mapKernelSlotToVatSlot(p.fulfillSlot); + const kp = kernelKeeper.getKernelPromise(kernelPromiseID); + const vpid = mapKernelSlotToVatSlot(kernelPromiseID); + const slot = mapKernelSlotToVatSlot(kp.fulfillSlot); return doProcess( - ['notifyFulfillToPresence', relativeID, slot], - `vat[${vatID}].promise[${relativeID}] fulfillToPresence failed`, + ['notifyFulfillToPresence', vpid, slot], + `vat[${vatID}].promise[${vpid}] fulfillToPresence failed`, ); } if (type === 'notifyReject') { const { kernelPromiseID } = message; - const p = kernelKeeper.getKernelPromise(kernelPromiseID); - const relativeID = mapKernelSlotToVatSlot({ - type: 'promise', - id: kernelPromiseID, - }).id; - const slots = p.rejectSlots.map(slot => mapKernelSlotToVatSlot(slot)); + const kp = kernelKeeper.getKernelPromise(kernelPromiseID); + const vpid = mapKernelSlotToVatSlot(kernelPromiseID); + const slots = kp.rejectSlots.map(slot => mapKernelSlotToVatSlot(slot)); return doProcess( - ['notifyReject', relativeID, p.rejectData, slots], - `vat[${vatID}].promise[${relativeID}] reject failed`, + ['notifyReject', vpid, kp.rejectData, slots], + `vat[${vatID}].promise[${vpid}] reject failed`, ); } diff --git a/test/files-devices/bootstrap-1.js b/test/files-devices/bootstrap-1.js index 1bf6122c041..dd1567b0860 100644 --- a/test/files-devices/bootstrap-1.js +++ b/test/files-devices/bootstrap-1.js @@ -9,8 +9,8 @@ export default function setup(syscall, state, helpers, _devices) { const { args } = JSON.parse(argsbytes); const deviceIndex = args[2].d1.index; deviceRef = caps[deviceIndex]; - if (deviceRef.type !== 'deviceImport') { - throw new Error(`bad deviceRef type ${deviceRef.type}`); + if (deviceRef !== 'd-70') { + throw new Error(`bad deviceRef ${deviceRef}`); } } else if (method === 'step1') { console.log('in step1'); diff --git a/test/test-controller.js b/test/test-controller.js index 928bcbcf84e..71a7bf06ac9 100644 --- a/test/test-controller.js +++ b/test/test-controller.js @@ -1,6 +1,7 @@ import path from 'path'; import { test } from 'tape-promise/tape'; import { buildVatController, loadBasedir } from '../src/index'; +import { checkKT } from './util'; test('load empty', async t => { const config = { @@ -22,7 +23,7 @@ async function simpleCall(t, withSES) { t.deepEqual(data.vatTables, [{ vatID: 'vat1', state: { transcript: [] } }]); t.deepEqual(data.kernelTable, []); - controller.queueToExport('vat1', 1, 'foo', 'args'); + controller.queueToExport('vat1', 'o+1', 'foo', 'args'); t.deepEqual(controller.dump().runQueue, [ { msg: { @@ -31,14 +32,14 @@ async function simpleCall(t, withSES) { method: 'foo', slots: [], }, - target: { id: 1, type: 'export', vatID: 'vat1' }, + target: 'ko20', type: 'deliver', vatID: 'vat1', }, ]); await controller.run(); t.deepEqual(JSON.parse(controller.dump().log[0]), { - facetID: 1, + facetID: 'o+1', method: 'foo', argsString: 'args', slots: [], @@ -101,7 +102,17 @@ async function bootstrapExport(t, withSES) { const c = await buildVatController(config, withSES); // console.log(c.dump()); // console.log('SLOTS: ', c.dump().runQueue[0].slots); - t.deepEqual(c.dump().kernelTable, []); + + // the expected kernel object indices + const boot0 = 'ko20'; + const left0 = 'ko21'; + const right0 = 'ko22'; + const kt = [ + [boot0, '_bootstrap', 'o+0'], + [left0, 'left', 'o+0'], + [right0, 'right', 'o+0'], + ]; + checkKT(t, c, kt); t.deepEqual(c.dump().runQueue, [ { @@ -110,13 +121,9 @@ async function bootstrapExport(t, withSES) { '{"args":[[],{"_bootstrap":{"@qclass":"slot","index":0},"left":{"@qclass":"slot","index":1},"right":{"@qclass":"slot","index":2}},{"_dummy":"dummy"}]}', kernelResolverID: null, method: 'bootstrap', - slots: [ - { id: 0, type: 'export', vatID: '_bootstrap' }, - { id: 0, type: 'export', vatID: 'left' }, - { id: 0, type: 'export', vatID: 'right' }, - ], + slots: [boot0, left0, right0], }, - target: { id: 0, type: 'export', vatID: '_bootstrap' }, + target: boot0, type: 'deliver', vatID: '_bootstrap', }, @@ -129,36 +136,34 @@ async function bootstrapExport(t, withSES) { ]); // console.log('--- c.step() running bootstrap.obj0.bootstrap'); await c.step(); + // kernel promise for result of the foo() that bootstrap sends to vat-left + const fooP = 'kp40'; t.deepEqual(c.dump().log, [ 'left.setup called', 'right.setup called', 'bootstrap called', 'bootstrap.obj0.bootstrap()', ]); - t.deepEqual(c.dump().kernelTable, [ - ['_bootstrap', 'import', 10, 'export', 'left', 0], - ['_bootstrap', 'import', 11, 'export', 'right', 0], - ['_bootstrap', 'promise', 20, 40], - ]); + kt.push([left0, '_bootstrap', 'o-50']); + kt.push([right0, '_bootstrap', 'o-51']); + kt.push([fooP, '_bootstrap', 'p-60']); + checkKT(t, c, kt); t.deepEqual(c.dump().runQueue, [ { vatID: 'left', type: 'deliver', - target: { - type: 'export', - vatID: 'left', - id: 0, - }, + target: left0, msg: { method: 'foo', argsString: '{"args":[1,{"@qclass":"slot","index":0}]}', - slots: [{ type: 'export', vatID: 'right', id: 0 }], - kernelResolverID: 40, + slots: [right0], + kernelResolverID: fooP, }, }, ]); await c.step(); + const barP = 'kp41'; t.deepEqual(c.dump().log, [ 'left.setup called', 'right.setup called', @@ -166,31 +171,24 @@ async function bootstrapExport(t, withSES) { 'bootstrap.obj0.bootstrap()', 'left.foo 1', ]); - t.deepEqual(c.dump().kernelTable, [ - ['_bootstrap', 'import', 10, 'export', 'left', 0], - ['_bootstrap', 'import', 11, 'export', 'right', 0], - ['_bootstrap', 'promise', 20, 40], - ['left', 'import', 10, 'export', 'right', 0], - ['left', 'promise', 20, 41], - ['left', 'resolver', 30, 40], - ]); + kt.push([right0, 'left', 'o-50']); + kt.push([fooP, 'left', 'p-60']); + kt.push([barP, 'left', 'p-61']); + checkKT(t, c, kt); + t.deepEqual(c.dump().runQueue, [ { vatID: 'right', type: 'deliver', - target: { - type: 'export', - vatID: 'right', - id: 0, - }, + target: right0, msg: { method: 'bar', argsString: '{"args":[2,{"@qclass":"slot","index":0}]}', - slots: [{ type: 'export', vatID: 'right', id: 0 }], - kernelResolverID: 41, + slots: [right0], + kernelResolverID: barP, }, }, - { type: 'notifyFulfillToData', vatID: '_bootstrap', kernelPromiseID: 40 }, + { type: 'notifyFulfillToData', vatID: '_bootstrap', kernelPromiseID: fooP }, ]); await c.step(); @@ -204,18 +202,12 @@ async function bootstrapExport(t, withSES) { 'right.obj0.bar 2 true', ]); - t.deepEqual(c.dump().kernelTable, [ - ['_bootstrap', 'import', 10, 'export', 'left', 0], - ['_bootstrap', 'import', 11, 'export', 'right', 0], - ['_bootstrap', 'promise', 20, 40], - ['left', 'import', 10, 'export', 'right', 0], - ['left', 'promise', 20, 41], - ['left', 'resolver', 30, 40], - ['right', 'resolver', 30, 41], - ]); + kt.push([barP, 'right', 'p-60']); + checkKT(t, c, kt); + t.deepEqual(c.dump().runQueue, [ - { type: 'notifyFulfillToData', vatID: '_bootstrap', kernelPromiseID: 40 }, - { type: 'notifyFulfillToData', vatID: 'left', kernelPromiseID: 41 }, + { type: 'notifyFulfillToData', vatID: '_bootstrap', kernelPromiseID: fooP }, + { type: 'notifyFulfillToData', vatID: 'left', kernelPromiseID: barP }, ]); await c.step(); @@ -228,18 +220,10 @@ async function bootstrapExport(t, withSES) { 'left.foo 1', 'right.obj0.bar 2 true', ]); + checkKT(t, c, kt); - t.deepEqual(c.dump().kernelTable, [ - ['_bootstrap', 'import', 10, 'export', 'left', 0], - ['_bootstrap', 'import', 11, 'export', 'right', 0], - ['_bootstrap', 'promise', 20, 40], - ['left', 'import', 10, 'export', 'right', 0], - ['left', 'promise', 20, 41], - ['left', 'resolver', 30, 40], - ['right', 'resolver', 30, 41], - ]); t.deepEqual(c.dump().runQueue, [ - { type: 'notifyFulfillToData', vatID: 'left', kernelPromiseID: 41 }, + { type: 'notifyFulfillToData', vatID: 'left', kernelPromiseID: barP }, ]); await c.step(); @@ -253,15 +237,7 @@ async function bootstrapExport(t, withSES) { 'right.obj0.bar 2 true', ]); - t.deepEqual(c.dump().kernelTable, [ - ['_bootstrap', 'import', 10, 'export', 'left', 0], - ['_bootstrap', 'import', 11, 'export', 'right', 0], - ['_bootstrap', 'promise', 20, 40], - ['left', 'import', 10, 'export', 'right', 0], - ['left', 'promise', 20, 41], - ['left', 'resolver', 30, 40], - ['right', 'resolver', 30, 41], - ]); + checkKT(t, c, kt); t.deepEqual(c.dump().runQueue, []); t.end(); diff --git a/test/test-devices.js b/test/test-devices.js index 2b764b67f10..a7acd02550a 100644 --- a/test/test-devices.js +++ b/test/test-devices.js @@ -24,10 +24,7 @@ async function test0(t, withSES) { }, ], }); - t.deepEqual(JSON.parse(c.dump().log[1]), [ - { type: 'export', id: 0 }, - { type: 'deviceImport', id: 40 }, - ]); + t.deepEqual(JSON.parse(c.dump().log[1]), ['o+0', 'd-70']); t.end(); } @@ -56,12 +53,12 @@ async function test1(t, withSES) { }; const c = await buildVatController(config, withSES); await c.step(); - c.queueToExport('_bootstrap', 0, 'step1', '{"args":[]}'); + c.queueToExport('_bootstrap', 'o+0', 'step1', '{"args":[]}'); await c.step(); console.log(c.dump().log); t.deepEqual(c.dump().log, [ 'callNow', - 'invoke 0 set', + 'invoke d+0 set', '{"data":"{}","slots":[]}', ]); t.deepEqual(sharedArray, ['pushed']); @@ -188,7 +185,7 @@ async function testState(t, withSES) { await c1.run(); t.deepEqual(c1.dump().log, ['undefined', 'w+r', 'called', 'got {"s":"new"}']); t.deepEqual(JSON.parse(c1.getState()).devices.d3.deviceState, { s: 'new' }); - t.deepEqual(JSON.parse(c1.getState()).devices.d3.nextImportID, 10); + t.deepEqual(JSON.parse(c1.getState()).devices.d3.nextObjectID, 10); t.end(); } diff --git a/test/test-kernel.js b/test/test-kernel.js index eded8b89ee4..c7be378d150 100644 --- a/test/test-kernel.js +++ b/test/test-kernel.js @@ -2,6 +2,21 @@ /* global setImmediate */ import { test } from 'tape-promise/tape'; import buildKernel from '../src/kernel/index'; +import { checkKT } from './util'; + +function checkPromises(t, kernel, expected) { + // extract the kernel promise table and assert that the contents match the + // expected list. This sorts on the promise ID, then does a t.deepEqual + function comparePromiseIDs(a, b) { + return a.id - b.id; + } + + const got = Array.from(kernel.dump().promises); + got.sort(comparePromiseIDs); + expected = Array.from(expected); + expected.sort(comparePromiseIDs); + t.deepEqual(got, expected); +} test('build kernel', async t => { const kernel = buildKernel({ setImmediate }); @@ -30,16 +45,12 @@ test('simple call', async t => { t.deepEqual(data.log, []); t.deepEqual(log, []); - kernel.queueToExport('vat1', 1, 'foo', 'args'); + kernel.queueToExport('vat1', 'o+1', 'foo', 'args'); t.deepEqual(kernel.dump().runQueue, [ { vatID: 'vat1', type: 'deliver', - target: { - type: 'export', - vatID: 'vat1', - id: 1, - }, + target: 'ko20', msg: { method: 'foo', argsString: 'args', @@ -50,12 +61,12 @@ test('simple call', async t => { ]); t.deepEqual(log, []); await kernel.run(); - t.deepEqual(log, [[1, 'foo', 'args', []]]); + t.deepEqual(log, [['o+1', 'foo', 'args', []]]); data = kernel.dump(); t.equal(data.log.length, 1); t.deepEqual(JSON.parse(data.log[0]), { - facetID: 1, + facetID: 'o+1', method: 'foo', argsString: 'args', slots: [], @@ -74,39 +85,40 @@ test('map inbound', async t => { return { deliver }; } kernel.addGenesisVat('vat1', setup1); + kernel.addGenesisVat('vat2', setup1); await kernel.start(); const data = kernel.dump(); - t.deepEqual(data.vatTables, [{ vatID: 'vat1', state: { transcript: [] } }]); + t.deepEqual(data.vatTables, [ + { vatID: 'vat1', state: { transcript: [] } }, + { vatID: 'vat2', state: { transcript: [] } }, + ]); t.deepEqual(data.kernelTable, []); t.deepEqual(log, []); - kernel.queueToExport('vat1', 1, 'foo', 'args', [ - { type: 'export', vatID: 'vat1', id: 5 }, - { type: 'export', vatID: 'vat2', id: 6 }, - ]); + const koFor5 = kernel.addExport('vat1', 'o+5'); + const koFor6 = kernel.addExport('vat2', 'o+6'); + kernel.queueToExport('vat1', 'o+1', 'foo', 'args', [koFor5, koFor6]); t.deepEqual(kernel.dump().runQueue, [ { msg: { argsString: 'args', kernelResolverID: null, method: 'foo', - slots: [ - { id: 5, type: 'export', vatID: 'vat1' }, - { id: 6, type: 'export', vatID: 'vat2' }, - ], + slots: [koFor5, koFor6], }, - target: { id: 1, type: 'export', vatID: 'vat1' }, + target: 'ko22', type: 'deliver', vatID: 'vat1', }, ]); t.deepEqual(log, []); await kernel.run(); - t.deepEqual(log, [ - [1, 'foo', 'args', [{ type: 'export', id: 5 }, { type: 'import', id: 10 }]], - ]); + t.deepEqual(log, [['o+1', 'foo', 'args', ['o+5', 'o-50']]]); t.deepEqual(kernel.dump().kernelTable, [ - ['vat1', 'import', 10, 'export', 'vat2', 6], + [koFor5, 'vat1', 'o+5'], + [koFor6, 'vat1', 'o-50'], + [koFor6, 'vat2', 'o+6'], + ['ko22', 'vat1', 'o+1'], ]); t.end(); @@ -122,40 +134,15 @@ test('addImport', async t => { kernel.addGenesisVat('vat2', setup); await kernel.start(); - const slot = kernel.addImport('vat1', { - type: 'export', - vatID: 'vat2', - id: 5, - }); - t.deepEqual(slot, { type: 'import', id: 10 }); // first import + const slot = kernel.addImport('vat1', kernel.addExport('vat2', 'o+5')); + t.deepEqual(slot, 'o-50'); // first import t.deepEqual(kernel.dump().kernelTable, [ - ['vat1', 'import', 10, 'export', 'vat2', 5], + ['ko20', 'vat1', 'o-50'], + ['ko20', 'vat2', 'o+5'], ]); t.end(); }); -test('outbound call to my own export should fail', async t => { - const kernel = buildKernel({ setImmediate }); - const log = []; - let s; - function setup1(syscall) { - s = syscall; - function deliver(facetID, method, argsString, slots) { - log.push([facetID, method, argsString, slots]); - } - return { deliver }; - } - kernel.addGenesisVat('vat1', setup1); - await kernel.start(); - - t.throws( - () => s.send({ type: 'export', id: 5 }, 'methodname', 'body', []), - /send\(\) is calling itself/, - ); - - t.end(); -}); - test('outbound call', async t => { const kernel = buildKernel({ setImmediate }); const log = []; @@ -165,12 +152,7 @@ test('outbound call', async t => { function deliver(facetID, method, argsString, slots) { // console.log(`d1/${facetID} called`); log.push(['d1', facetID, method, argsString, slots]); - const pid = syscall.send( - { type: 'import', id: v1tovat25.id }, - 'bar', - 'bargs', - [{ type: 'import', id: v1tovat25.id }, { type: 'export', id: 7 }], - ); + const pid = syscall.send(v1tovat25, 'bar', 'bargs', [v1tovat25, 'o+7']); log.push(['d1', 'promiseid', pid]); } return { deliver }; @@ -187,24 +169,26 @@ test('outbound call', async t => { kernel.addGenesisVat('vat2', setup2); await kernel.start(); - v1tovat25 = kernel.addImport('vat1', { - type: 'export', - vatID: 'vat2', - id: 5, - }); - t.deepEqual(v1tovat25, { type: 'import', id: 10 }); // first allocation + const t1 = kernel.addExport('vat1', 'o+1'); + const vat2Obj5 = kernel.addExport('vat2', 'o+5'); + v1tovat25 = kernel.addImport('vat1', vat2Obj5); + t.deepEqual(v1tovat25, 'o-50'); // first allocation const data = kernel.dump(); t.deepEqual(data.vatTables, [ { vatID: 'vat1', state: { transcript: [] } }, { vatID: 'vat2', state: { transcript: [] } }, ]); - t.deepEqual(data.kernelTable, [ - ['vat1', 'import', v1tovat25.id, 'export', 'vat2', 5], - ]); + + const kt = [ + [t1, 'vat1', 'o+1'], + ['ko21', 'vat1', v1tovat25], + ['ko21', 'vat2', 'o+5'], + ]; + checkKT(t, kernel, kt); t.deepEqual(log, []); - kernel.queueToExport('vat1', 1, 'foo', 'args'); + kernel.queueToExport('vat1', 'o+1', 'foo', 'args'); t.deepEqual(log, []); t.deepEqual(kernel.dump().runQueue, [ { @@ -214,7 +198,7 @@ test('outbound call', async t => { method: 'foo', slots: [], }, - target: { id: 1, type: 'export', vatID: 'vat1' }, + target: t1, type: 'deliver', vatID: 'vat1', }, @@ -222,60 +206,45 @@ test('outbound call', async t => { await kernel.step(); - t.deepEqual(log.shift(), ['d1', 1, 'foo', 'args', []]); - t.deepEqual(log.shift(), ['d1', 'promiseid', 20]); + t.deepEqual(log.shift(), ['d1', 'o+1', 'foo', 'args', []]); + t.deepEqual(log.shift(), ['d1', 'promiseid', 'p-60']); t.deepEqual(log, []); t.deepEqual(kernel.dump().runQueue, [ { vatID: 'vat2', type: 'deliver', - target: { - type: 'export', - vatID: 'vat2', - id: 5, - }, + target: vat2Obj5, msg: { method: 'bar', argsString: 'bargs', - slots: [ - { type: 'export', vatID: 'vat2', id: 5 }, - { type: 'export', vatID: 'vat1', id: 7 }, - ], - kernelResolverID: 40, + slots: [vat2Obj5, 'ko22'], + kernelResolverID: 'kp40', }, }, ]); t.deepEqual(kernel.dump().promises, [ { - id: 40, + id: 'kp40', state: 'unresolved', decider: 'vat2', subscribers: [], queue: [], }, ]); - t.deepEqual(kernel.dump().kernelTable, [ - ['vat1', 'import', v1tovat25.id, 'export', 'vat2', 5], - ['vat1', 'promise', 20, 40], - ]); + kt.push(['ko22', 'vat1', 'o+7']); + kt.push(['kp40', 'vat1', 'p-60']); + checkKT(t, kernel, kt); await kernel.step(); - t.deepEqual(log, [ - [ - 'd2', - 5, - 'bar', - 'bargs', - [{ type: 'export', id: 5 }, { type: 'import', id: 10 }], - ], - ]); - t.deepEqual(kernel.dump().kernelTable, [ - ['vat1', 'import', v1tovat25.id, 'export', 'vat2', 5], - ['vat1', 'promise', 20, 40], - ['vat2', 'import', 10, 'export', 'vat1', 7], - ['vat2', 'resolver', 30, 40], - ]); + t.deepEqual(log, [['d2', 'o+5', 'bar', 'bargs', ['o+5', 'o-50']]]); + // our temporary handling of resolverIDs causes vat2's clist to have a + // funny mapping. (kp40->p-60, p-60->kp40, r-60->kp40). The dump() function + // assumes a strictly bijective mapping and doesn't include the surjective + // r-60->kp40 edge. So instead of ['kp40', 'vat2', 'r-60'], we see: + kt.push(['kp40', 'vat2', 'p-60']); + kt.push(['ko22', 'vat2', 'o-50']); + checkKT(t, kernel, kt); t.end(); }); @@ -286,25 +255,20 @@ test('three-party', async t => { let bobForA; let carolForA; + let aliceSyscall; function setupA(syscall) { + aliceSyscall = syscall; function deliver(facetID, method, argsString, slots) { console.log(`vatA/${facetID} called`); log.push(['vatA', facetID, method, argsString, slots]); - const pid = syscall.send( - { type: 'import', id: bobForA.id }, - 'intro', - 'bargs', - [{ type: 'import', id: carolForA.id }], - ); + const pid = syscall.send(bobForA, 'intro', 'bargs', [carolForA]); log.push(['vatA', 'promiseID', pid]); } return { deliver }; } kernel.addGenesisVat('vatA', setupA); - let bobSyscall; - function setupB(syscall) { - bobSyscall = syscall; + function setupB(_syscall) { function deliver(facetID, method, argsString, slots) { console.log(`vatB/${facetID} called`); log.push(['vatB', facetID, method, argsString, slots]); @@ -323,21 +287,18 @@ test('three-party', async t => { await kernel.start(); - bobForA = kernel.addImport('vatA', { - type: 'export', - vatID: 'vatB', - id: 5, - }); - carolForA = kernel.addImport('vatA', { - type: 'export', - vatID: 'vatC', - id: 6, - }); + const alice = kernel.addExport('vatA', 'o+4'); + const bob = kernel.addExport('vatB', 'o+5'); + const carol = kernel.addExport('vatC', 'o+6'); + + bobForA = kernel.addImport('vatA', bob); + carolForA = kernel.addImport('vatA', carol); - // get bob's promise index to be different than alice's, to check that the - // promiseID coming back from send() is scoped to the sender, not the - // recipient - const bp = bobSyscall.createPromise(); + // this extra allocation causes alice's send() promise index to be + // different than bob's, so we can check that the promiseID coming back + // from send() is scoped to the sender, not the recipient + const ap = aliceSyscall.createPromise(); + t.equal(ap.promiseID, 'p-60'); const data = kernel.dump(); t.deepEqual(data.vatTables, [ @@ -345,75 +306,61 @@ test('three-party', async t => { { vatID: 'vatB', state: { transcript: [] } }, { vatID: 'vatC', state: { transcript: [] } }, ]); - t.deepEqual(data.kernelTable, [ - ['vatA', 'import', bobForA.id, 'export', 'vatB', 5], - ['vatA', 'import', carolForA.id, 'export', 'vatC', 6], - ['vatB', 'promise', bp.promiseID, 40], - ['vatB', 'resolver', 30, 40], - ]); + const kt = [ + [alice, 'vatA', 'o+4'], + [bob, 'vatA', bobForA], + [bob, 'vatB', 'o+5'], + [carol, 'vatA', carolForA], + [carol, 'vatC', 'o+6'], + ['kp40', 'vatA', ap.promiseID], + ]; + checkKT(t, kernel, kt); t.deepEqual(log, []); - kernel.queueToExport('vatA', 1, 'foo', 'args'); + kernel.queueToExport('vatA', 'o+4', 'foo', 'args'); await kernel.step(); - t.deepEqual(log.shift(), ['vatA', 1, 'foo', 'args', []]); - t.deepEqual(log.shift(), ['vatA', 'promiseID', 20]); + t.deepEqual(log.shift(), ['vatA', 'o+4', 'foo', 'args', []]); + t.deepEqual(log.shift(), ['vatA', 'promiseID', 'p-61']); t.deepEqual(log, []); t.deepEqual(kernel.dump().runQueue, [ { vatID: 'vatB', type: 'deliver', - target: { - type: 'export', - vatID: 'vatB', - id: 5, - }, + target: bob, msg: { method: 'intro', argsString: 'bargs', - slots: [{ type: 'export', vatID: 'vatC', id: 6 }], - kernelResolverID: 41, + slots: [carol], + kernelResolverID: 'kp41', }, }, ]); t.deepEqual(kernel.dump().promises, [ { - id: 40, + id: 'kp40', state: 'unresolved', - decider: 'vatB', + decider: 'vatA', subscribers: [], queue: [], }, { - id: 41, + id: 'kp41', state: 'unresolved', decider: 'vatB', subscribers: [], queue: [], }, ]); - t.deepEqual(kernel.dump().kernelTable, [ - ['vatA', 'import', bobForA.id, 'export', 'vatB', 5], - ['vatA', 'import', carolForA.id, 'export', 'vatC', 6], - ['vatA', 'promise', 20, 41], - ['vatB', 'promise', bp.promiseID, 40], - ['vatB', 'resolver', 30, 40], - ]); + kt.push(['kp41', 'vatA', 'p-61']); + checkKT(t, kernel, kt); await kernel.step(); - t.deepEqual(log, [ - ['vatB', 5, 'intro', 'bargs', [{ type: 'import', id: 10 }]], - ]); - t.deepEqual(kernel.dump().kernelTable, [ - ['vatA', 'import', bobForA.id, 'export', 'vatB', 5], - ['vatA', 'import', carolForA.id, 'export', 'vatC', 6], - ['vatA', 'promise', 20, 41], - ['vatB', 'import', 10, 'export', 'vatC', 6], - ['vatB', 'promise', bp.promiseID, 40], - ['vatB', 'resolver', 30, 40], - ['vatB', 'resolver', 31, 41], - ]); + t.deepEqual(log, [['vatB', 'o+5', 'intro', 'bargs', ['o-50']]]); + kt.push([carol, 'vatB', 'o-50']); + kt.push(['kp41', 'vatB', 'p-60']); // dump() omits resolver + checkKT(t, kernel, kt); t.end(); }); @@ -431,10 +378,10 @@ test('createPromise', async t => { t.deepEqual(kernel.dump().promises, []); const pr = syscall.createPromise(); - t.deepEqual(pr, { promiseID: 20, resolverID: 30 }); + t.deepEqual(pr, { promiseID: 'p-60', resolverID: 'r-60' }); t.deepEqual(kernel.dump().promises, [ { - id: 40, + id: 'kp40', state: 'unresolved', decider: 'vat1', subscribers: [], @@ -443,8 +390,7 @@ test('createPromise', async t => { ]); t.deepEqual(kernel.dump().kernelTable, [ - ['vat1', 'promise', 20, 40], - ['vat1', 'resolver', 30, 40], + ['kp40', 'vat1', 'p-60'], // dump() omits resolver ]); t.end(); }); @@ -475,265 +421,131 @@ test('transfer promise', async t => { await kernel.start(); - const B = kernel.addImport('vatA', { type: 'export', vatID: 'vatB', id: 5 }); - const A = kernel.addImport('vatB', { type: 'export', vatID: 'vatA', id: 6 }); + const alice = kernel.addExport('vatA', 'o+6'); + const bob = kernel.addExport('vatB', 'o+5'); + + const B = kernel.addImport('vatA', bob); + const A = kernel.addImport('vatB', alice); const pr1 = syscallA.createPromise(); - t.deepEqual(pr1, { promiseID: 20, resolverID: 30 }); + t.deepEqual(pr1, { promiseID: 'p-60', resolverID: 'r-60' }); // we send pr2 const pr2 = syscallA.createPromise(); - t.deepEqual(pr2, { promiseID: 21, resolverID: 31 }); - - t.deepEqual(kernel.dump().kernelTable, [ - ['vatA', 'import', 10, 'export', 'vatB', 5], - ['vatA', 'promise', 20, 40], // pr1 - ['vatA', 'promise', 21, 41], // pr2 - ['vatA', 'resolver', 30, 40], // pr1 - ['vatA', 'resolver', 31, 41], // pr2 - ['vatB', 'import', 10, 'export', 'vatA', 6], - ]); - t.deepEqual(kernel.dump().promises, [ - { - id: 40, + t.deepEqual(pr2, { promiseID: 'p-61', resolverID: 'r-61' }); + + const kt = [ + ['ko20', 'vatA', 'o+6'], + ['ko20', 'vatB', 'o-50'], + ['ko21', 'vatA', 'o-50'], + ['ko21', 'vatB', 'o+5'], + ['kp40', 'vatA', 'p-60'], // pr1 + ['kp41', 'vatA', 'p-61'], // pr2 + ]; + checkKT(t, kernel, kt); + const kp = [ + { + id: 'kp40', state: 'unresolved', decider: 'vatA', subscribers: [], queue: [], }, { - id: 41, + id: 'kp41', state: 'unresolved', decider: 'vatA', subscribers: [], queue: [], }, - ]); + ]; + checkPromises(t, kernel, kp); // sending a promise should arrive as a promise - syscallA.send({ type: 'import', id: B.id }, 'foo1', 'args', [ - { type: 'promise', id: pr2.promiseID }, - ]); + syscallA.send(B, 'foo1', 'args', [pr2.promiseID]); t.deepEqual(kernel.dump().runQueue, [ { vatID: 'vatB', type: 'deliver', - target: { - type: 'export', - vatID: 'vatB', - id: 5, - }, + target: bob, msg: { method: 'foo1', argsString: 'args', - slots: [{ type: 'promise', id: 41 }], - kernelResolverID: 42, + slots: ['kp41'], + kernelResolverID: 'kp42', }, }, ]); - t.deepEqual(kernel.dump().kernelTable, [ - ['vatA', 'import', 10, 'export', 'vatB', 5], - ['vatA', 'promise', 20, 40], // pr1 - ['vatA', 'promise', 21, 41], // pr2 - ['vatA', 'promise', 22, 42], // answer for foo1() - ['vatA', 'resolver', 30, 40], // pr1 - ['vatA', 'resolver', 31, 41], // pr2 - ['vatB', 'import', 10, 'export', 'vatA', 6], - ]); - t.deepEqual(kernel.dump().promises, [ - { - id: 40, - state: 'unresolved', - decider: 'vatA', - subscribers: [], - queue: [], - }, - { - id: 41, - state: 'unresolved', - decider: 'vatA', - subscribers: [], - queue: [], - }, - { - id: 42, - state: 'unresolved', - decider: 'vatB', - subscribers: [], - queue: [], - }, - ]); + kt.push(['kp42', 'vatA', 'p-62']); // promise for answer of foo1() + checkKT(t, kernel, kt); + kp.push({ + id: 'kp42', + state: 'unresolved', + decider: 'vatB', + subscribers: [], + queue: [], + }); + checkPromises(t, kernel, kp); await kernel.run(); - t.deepEqual(logB.shift(), [5, 'foo1', 'args', [{ type: 'promise', id: 20 }]]); + t.deepEqual(logB.shift(), ['o+5', 'foo1', 'args', ['p-60']]); t.deepEqual(logB, []); - t.deepEqual(kernel.dump().kernelTable, [ - ['vatA', 'import', 10, 'export', 'vatB', 5], - ['vatA', 'promise', 20, 40], // pr1 - ['vatA', 'promise', 21, 41], // pr2 - ['vatA', 'promise', 22, 42], // promise for answer of foo1() - ['vatA', 'resolver', 30, 40], // pr1 - ['vatA', 'resolver', 31, 41], // pr2 - ['vatB', 'import', 10, 'export', 'vatA', 6], - ['vatB', 'promise', 20, 41], - ['vatB', 'resolver', 30, 42], // resolver for answer of foo1() - ]); + kt.push(['kp41', 'vatB', 'p-60']); // pr2 for B + kt.push(['kp42', 'vatB', 'p-61']); // resolver for answer of foo1() + checkKT(t, kernel, kt); // sending it a second time should arrive as the same thing - syscallA.send({ type: 'import', id: B.id }, 'foo2', 'args', [ - { type: 'promise', id: pr2.promiseID }, - ]); + syscallA.send(B, 'foo2', 'args', [pr2.promiseID]); await kernel.run(); - t.deepEqual(logB.shift(), [5, 'foo2', 'args', [{ type: 'promise', id: 20 }]]); + t.deepEqual(logB.shift(), ['o+5', 'foo2', 'args', ['p-60']]); t.deepEqual(logB, []); - t.deepEqual(kernel.dump().kernelTable, [ - ['vatA', 'import', 10, 'export', 'vatB', 5], - ['vatA', 'promise', 20, 40], // pr1 - ['vatA', 'promise', 21, 41], // pr2 - ['vatA', 'promise', 22, 42], // promise for answer of foo1() - ['vatA', 'promise', 23, 43], // promise for answer of foo2() - ['vatA', 'resolver', 30, 40], // pr1 - ['vatA', 'resolver', 31, 41], // pr2 - ['vatB', 'import', 10, 'export', 'vatA', 6], - ['vatB', 'promise', 20, 41], - ['vatB', 'resolver', 30, 42], // resolver for answer of foo1() - ['vatB', 'resolver', 31, 43], // resolver for answer of foo2() - ]); - - t.deepEqual(kernel.dump().promises, [ - { - id: 40, - state: 'unresolved', - decider: 'vatA', - subscribers: [], - queue: [], - }, - { - id: 41, - state: 'unresolved', - decider: 'vatA', - subscribers: [], - queue: [], - }, - { - id: 42, - state: 'unresolved', - decider: 'vatB', - subscribers: [], - queue: [], - }, - { - id: 43, - state: 'unresolved', - decider: 'vatB', - subscribers: [], - queue: [], - }, - ]); + kt.push(['kp43', 'vatA', 'p-63']); // promise for answer of foo2() + kt.push(['kp43', 'vatB', 'p-62']); // resolver for answer of foo2() + checkKT(t, kernel, kt); + + kp.push({ + id: 'kp43', + state: 'unresolved', + decider: 'vatB', + subscribers: [], + queue: [], + }); + checkPromises(t, kernel, kp); // sending it back should arrive with the sender's index - syscallB.send({ type: 'import', id: A.id }, 'foo3', 'args', [ - { type: 'promise', id: 20 }, - ]); + syscallB.send(A, 'foo3', 'args', ['p-60']); await kernel.run(); - t.deepEqual(logA.shift(), [ - 6, - 'foo3', - 'args', - [{ type: 'promise', id: pr2.promiseID }], - ]); + t.deepEqual(logA.shift(), ['o+6', 'foo3', 'args', [pr2.promiseID]]); t.deepEqual(logA, []); - t.deepEqual(kernel.dump().kernelTable, [ - ['vatA', 'import', 10, 'export', 'vatB', 5], - ['vatA', 'promise', 20, 40], // pr1 - ['vatA', 'promise', 21, 41], // pr2 - ['vatA', 'promise', 22, 42], // promise for answer of foo1() - ['vatA', 'promise', 23, 43], // promise for answer of foo2() - ['vatA', 'resolver', 30, 40], // pr1 - ['vatA', 'resolver', 31, 41], // pr2 - ['vatA', 'resolver', 32, 44], // resolver for answer of foo3() - ['vatB', 'import', 10, 'export', 'vatA', 6], - ['vatB', 'promise', 20, 41], - ['vatB', 'promise', 21, 44], // promise for answer of foo3() - ['vatB', 'resolver', 30, 42], // resolver for answer of foo1() - ['vatB', 'resolver', 31, 43], // resolver for answer of foo2() - ]); + kt.push(['kp44', 'vatA', 'p-64']); // resolver for answer of foo3() + kt.push(['kp44', 'vatB', 'p-63']); // promise for answer of foo3() + checkKT(t, kernel, kt); // sending it back a second time should arrive as the same thing - syscallB.send({ type: 'import', id: A.id }, 'foo4', 'args', [ - { type: 'promise', id: 20 }, - ]); + syscallB.send(A, 'foo4', 'args', ['p-60']); await kernel.run(); - t.deepEqual(logA.shift(), [ - 6, - 'foo4', - 'args', - [{ type: 'promise', id: pr2.promiseID }], - ]); + t.deepEqual(logA.shift(), ['o+6', 'foo4', 'args', [pr2.promiseID]]); t.deepEqual(logA, []); - t.deepEqual(kernel.dump().promises, [ - { - id: 40, - state: 'unresolved', - decider: 'vatA', - subscribers: [], - queue: [], - }, - { - id: 41, - state: 'unresolved', - decider: 'vatA', - subscribers: [], - queue: [], - }, - { - id: 42, - state: 'unresolved', - decider: 'vatB', - subscribers: [], - queue: [], - }, - { - id: 43, - state: 'unresolved', - decider: 'vatB', - subscribers: [], - queue: [], - }, - { - id: 44, - state: 'unresolved', - decider: 'vatA', - subscribers: [], - queue: [], - }, - { - id: 45, - state: 'unresolved', - decider: 'vatA', - subscribers: [], - queue: [], - }, - ]); + kp.push({ + id: 'kp44', + state: 'unresolved', + decider: 'vatA', + subscribers: [], + queue: [], + }); + kp.push({ + id: 'kp45', + state: 'unresolved', + decider: 'vatA', + subscribers: [], + queue: [], + }); + checkPromises(t, kernel, kp); - t.deepEqual(kernel.dump().kernelTable, [ - ['vatA', 'import', 10, 'export', 'vatB', 5], - ['vatA', 'promise', 20, 40], // pr1 - ['vatA', 'promise', 21, 41], // pr2 - ['vatA', 'promise', 22, 42], // promise for answer of foo1() - ['vatA', 'promise', 23, 43], // promise for answer of foo2() - ['vatA', 'resolver', 30, 40], // pr1 - ['vatA', 'resolver', 31, 41], // pr2 - ['vatA', 'resolver', 32, 44], // resolver for answer of foo3() - ['vatA', 'resolver', 33, 45], // resolver for answer of foo4() - ['vatB', 'import', 10, 'export', 'vatA', 6], - ['vatB', 'promise', 20, 41], - ['vatB', 'promise', 21, 44], // promise for answer of foo3() - ['vatB', 'promise', 22, 45], // promise for answer of foo4() - ['vatB', 'resolver', 30, 42], // resolver for answer of foo1() - ['vatB', 'resolver', 31, 43], // resolver for answer of foo2() - ]); + kt.push(['kp45', 'vatA', 'p-65']); // resolver for answer of foo4() + kt.push(['kp45', 'vatB', 'p-64']); // promise for answer of foo4() + checkKT(t, kernel, kt); t.end(); }); @@ -753,16 +565,13 @@ test('subscribe to promise', async t => { await kernel.start(); const pr = syscall.createPromise(); - t.deepEqual(pr, { promiseID: 20, resolverID: 30 }); - t.deepEqual(kernel.dump().kernelTable, [ - ['vat1', 'promise', 20, 40], - ['vat1', 'resolver', 30, 40], - ]); + t.deepEqual(pr, { promiseID: 'p-60', resolverID: 'r-60' }); + t.deepEqual(kernel.dump().kernelTable, [['kp40', 'vat1', 'p-60']]); syscall.subscribe(pr.promiseID); t.deepEqual(kernel.dump().promises, [ { - id: 40, + id: 'kp40', state: 'unresolved', decider: 'vat1', subscribers: ['vat1'], @@ -856,19 +665,14 @@ test('promise resolveToData', async t => { kernel.addGenesisVat('vatA', setup); await kernel.start(); + const alice = 'o+6'; const pr = syscall.createPromise(); - t.deepEqual(kernel.dump().kernelTable, [ - ['vatA', 'promise', 20, 40], - ['vatA', 'resolver', 30, 40], - ]); - - const ex1 = { type: 'export', vatID: 'vatB', id: 6 }; - const A = kernel.addImport('vatA', ex1); + t.deepEqual(kernel.dump().kernelTable, [['kp40', 'vatA', 'p-60']]); syscall.subscribe(pr.promiseID); t.deepEqual(kernel.dump().promises, [ { - id: 40, + id: 'kp40', state: 'unresolved', decider: 'vatA', subscribers: ['vatA'], @@ -876,27 +680,27 @@ test('promise resolveToData', async t => { }, ]); - syscall.fulfillToData(pr.resolverID, 'args', [A]); - // A gets mapped to a kernelPromiseID first, and a notifyFulfillToData - // message is queued + syscall.fulfillToData(pr.resolverID, 'args', [alice]); + // the resolverID gets mapped to a kernelPromiseID first, and a + // notifyFulfillToData message is queued t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().runQueue, [ { type: 'notifyFulfillToData', vatID: 'vatA', - kernelPromiseID: 40, + kernelPromiseID: 'kp40', }, ]); await kernel.step(); - // the kernelPromiseID gets mapped back to A - t.deepEqual(log.shift(), ['notify', pr.promiseID, 'args', [A]]); + // the kernelPromiseID gets mapped back to the vat PromiseID + t.deepEqual(log.shift(), ['notify', pr.promiseID, 'args', [alice]]); t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().promises, [ { - id: 40, + id: 'kp40', fulfillData: 'args', - fulfillSlots: [{ id: 6, type: 'export', vatID: 'vatB' }], + fulfillSlots: ['ko20'], state: 'fulfilledToData', }, ]); @@ -905,6 +709,11 @@ test('promise resolveToData', async t => { t.end(); }); +function emptySetup(_syscall) { + function deliver() {} + return { deliver }; +} + test('promise resolveToPresence', async t => { const kernel = buildKernel({ setImmediate }); let syscall; @@ -920,21 +729,23 @@ test('promise resolveToPresence', async t => { return { deliver, notifyFulfillToPresence }; } kernel.addGenesisVat('vatA', setup); + kernel.addGenesisVat('vatB', emptySetup); await kernel.start(); + const bob = kernel.addExport('vatB', 'o+6'); + const bobForAlice = kernel.addImport('vatA', bob); const pr = syscall.createPromise(); - t.deepEqual(kernel.dump().kernelTable, [ - ['vatA', 'promise', 20, 40], - ['vatA', 'resolver', 30, 40], - ]); - - const ex1 = { type: 'export', vatID: 'vatB', id: 6 }; - const A = kernel.addImport('vatA', ex1); + const kt = [ + ['ko20', 'vatB', 'o+6'], + ['ko20', 'vatA', 'o-50'], + ['kp40', 'vatA', 'p-60'], + ]; + checkKT(t, kernel, kt); syscall.subscribe(pr.promiseID); t.deepEqual(kernel.dump().promises, [ { - id: 40, + id: 'kp40', state: 'unresolved', decider: 'vatA', subscribers: ['vatA'], @@ -942,27 +753,24 @@ test('promise resolveToPresence', async t => { }, ]); - syscall.fulfillToPresence(pr.resolverID, A); - // A gets mapped to a kernelPromiseID first, and a notifyFulfillToPresence - // message is queued + syscall.fulfillToPresence(pr.resolverID, bobForAlice); t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().runQueue, [ { type: 'notifyFulfillToPresence', vatID: 'vatA', - kernelPromiseID: 40, + kernelPromiseID: 'kp40', }, ]); await kernel.step(); - // the kernelPromiseID gets mapped back to A - t.deepEqual(log.shift(), ['notify', pr.promiseID, A]); + t.deepEqual(log.shift(), ['notify', pr.promiseID, bobForAlice]); t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().promises, [ { - id: 40, - fulfillSlot: { id: 6, type: 'export', vatID: 'vatB' }, + id: 'kp40', + fulfillSlot: bob, state: 'fulfilledToPresence', }, ]); @@ -985,21 +793,24 @@ test('promise reject', async t => { return { deliver, notifyReject }; } kernel.addGenesisVat('vatA', setup); + kernel.addGenesisVat('vatB', emptySetup); await kernel.start(); - const pr = syscall.createPromise(); - t.deepEqual(kernel.dump().kernelTable, [ - ['vatA', 'promise', 20, 40], - ['vatA', 'resolver', 30, 40], - ]); + const bob = kernel.addExport('vatB', 'o+6'); + const bobForAlice = kernel.addImport('vatA', bob); - const ex1 = { type: 'export', vatID: 'vatB', id: 6 }; - const A = kernel.addImport('vatA', ex1); + const pr = syscall.createPromise(); + const kt = [ + ['ko20', 'vatB', 'o+6'], + ['ko20', 'vatA', 'o-50'], + ['kp40', 'vatA', 'p-60'], + ]; + checkKT(t, kernel, kt); syscall.subscribe(pr.promiseID); t.deepEqual(kernel.dump().promises, [ { - id: 40, + id: 'kp40', state: 'unresolved', decider: 'vatA', subscribers: ['vatA'], @@ -1010,27 +821,24 @@ test('promise reject', async t => { // the resolver-holder calls reject right away, because now we // automatically subscribe - syscall.reject(pr.resolverID, 'args', [A]); - // A gets mapped to a kernelPromiseID first, and a notifyReject message is - // queued + syscall.reject(pr.resolverID, 'args', [bobForAlice]); t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().runQueue, [ { type: 'notifyReject', vatID: 'vatA', - kernelPromiseID: 40, + kernelPromiseID: 'kp40', }, ]); await kernel.step(); - // the kernelPromiseID gets mapped back to A - t.deepEqual(log.shift(), ['notify', pr.promiseID, 'args', [A]]); + t.deepEqual(log.shift(), ['notify', pr.promiseID, 'args', [bobForAlice]]); t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().promises, [ { - id: 40, + id: 'kp40', rejectData: 'args', - rejectSlots: [{ id: 6, type: 'export', vatID: 'vatB' }], + rejectSlots: [bob], state: 'rejected', }, ]); @@ -1040,57 +848,51 @@ test('promise reject', async t => { }); test('transcript', async t => { + const aliceForAlice = 'o+1'; const kernel = buildKernel({ setImmediate }); const log = []; function setup(syscall, _state) { function deliver(facetID, _method, _argsString, slots) { - if (facetID === 1) { - syscall.send(slots[1], 'foo', 'fooarg', []); + if (facetID === aliceForAlice) { + const p = syscall.send(slots[1], 'foo', 'fooarg', []); + log.push(p); } } return { deliver }; } kernel.addGenesisVat('vatA', setup); + kernel.addGenesisVat('vatB', emptySetup); await kernel.start(); - kernel.queueToExport('vatA', 1, 'store', 'args string', [ - { type: 'export', vatID: 'vatA', id: 1 }, - { type: 'export', vatID: 'vatB', id: 2 }, + const alice = kernel.addExport('vatA', aliceForAlice); + const bob = kernel.addExport('vatB', 'o+2'); + const bobForAlice = kernel.addImport('vatA', bob); + + kernel.queueToExport('vatA', aliceForAlice, 'store', 'args string', [ + alice, + bob, ]); await kernel.step(); + const p1 = log.shift(); + t.equal(p1, 'p-60'); - // the transcript records vat-specific import/export slots, so figure out - // what vatA will receive - const slot1 = kernel.dump().kernelTable[0]; - // vatA's import-X is mapped to vatB:export-2 - t.equal(slot1[0], 'vatA'); - t.equal(slot1[1], 'import'); - const X = slot1[2]; - t.equal(slot1[3], 'export'); - t.equal(slot1[4], 'vatB'); - t.equal(slot1[5], 2); - - const slot2 = kernel.dump().kernelTable[1]; - t.equal(slot2[0], 'vatA'); - t.equal(slot2[1], 'promise'); - const Y = slot2[2]; + // the transcript records vat-specific import/export slots - t.deepEqual(log, []); const tr = kernel.dump().vatTables[0].state.transcript; t.equal(tr.length, 1); t.deepEqual(tr[0], { d: [ 'deliver', - 1, + aliceForAlice, 'store', 'args string', - [{ id: 1, type: 'export' }, { id: 10, type: 'import' }], - { id: null, type: 'resolver' }, + [aliceForAlice, bobForAlice], + null, ], syscalls: [ { - d: ['send', { type: 'import', id: X }, 'foo', 'fooarg', []], - response: Y, + d: ['send', bobForAlice, 'foo', 'fooarg', []], + response: p1, }, ], }); diff --git a/test/test-liveslots.js b/test/test-liveslots.js index 9e274e4bddc..07fc3f571fb 100644 --- a/test/test-liveslots.js +++ b/test/test-liveslots.js @@ -37,14 +37,11 @@ test('liveslots pipelines to syscall.send', async t => { await kernel.start(); // no bootstrap t.deepEqual(kernel.dump().runQueue, []); - const root = kernel.addImport( - 'b', - harden({ type: 'export', vatID: 'a', id: 0 }), - ); + const root = kernel.addImport('b', kernel.addExport('a', 'o+0')); // root!one(x) // sendOnly const arg0 = JSON.stringify({ args: [{ '@qclass': 'slot', index: 0 }] }); - syscall.send(root, 'one', arg0, [harden({ type: 'export', id: 5 })]); + syscall.send(root, 'one', arg0, ['o+5']); // calling one() should cause three syscall.send() calls to be made: one // for x!pipe1(), a second pipelined to the result promise of it, and a diff --git a/test/test-marshal.js b/test/test-marshal.js index 2fb05388758..d5e8eaa0cea 100644 --- a/test/test-marshal.js +++ b/test/test-marshal.js @@ -7,6 +7,17 @@ import { makeMarshal, mustPassByPresence } from '@agoric/marshal'; import { makeMarshaller } from '../src/kernel/liveSlots'; import makePromise from '../src/kernel/makePromise'; +import { buildVatController } from '../src/index'; + +async function prep() { + const config = { + vatSources: new Map(), + bootstrapIndexJS: undefined, + }; + const controller = await buildVatController(config, false); + await controller.run(); +} + test('serialize static data', t => { const m = makeMarshal(); const ser = val => m.serialize(val); @@ -168,42 +179,35 @@ test('serialize exports', t => { }); t.deepEqual(ser(o1), { argsString: '{"@qclass":"slot","index":0}', - slots: [{ type: 'export', id: 1 }], + slots: ['o+1'], }); // m now remembers that o1 is exported as 1 t.deepEqual(ser(harden([o1, o1])), { argsString: '[{"@qclass":"slot","index":0},{"@qclass":"ibid","index":1}]', - slots: [{ type: 'export', id: 1 }], + slots: ['o+1'], }); t.deepEqual(ser(harden([o2, o1])), { argsString: '[{"@qclass":"slot","index":0},{"@qclass":"slot","index":1}]', - slots: [{ type: 'export', id: 2 }, { type: 'export', id: 1 }], + slots: ['o+2', 'o+1'], }); t.end(); }); -test('deserialize imports', t => { +test('deserialize imports', async t => { + await prep(); const { m } = makeMarshaller(); - const a = m.unserialize('{"@qclass":"slot","index":0}', [ - { type: 'import', id: 1 }, - ]); + const a = m.unserialize('{"@qclass":"slot","index":0}', ['o-1']); // a should be a proxy/presence. For now these are obvious. - t.equal(a.toString(), '[Presence 1]'); + t.equal(a.toString(), '[Presence o-1]'); t.ok(Object.isFrozen(a)); // m now remembers the proxy - const b = m.unserialize('{"@qclass":"slot","index":0}', [ - { type: 'import', id: 1 }, - ]); + const b = m.unserialize('{"@qclass":"slot","index":0}', ['o-1']); t.is(a, b); // the slotid is what matters, not the index - const c = m.unserialize('{"@qclass":"slot","index":2}', [ - 'x', - 'x', - { type: 'import', id: 1 }, - ]); + const c = m.unserialize('{"@qclass":"slot","index":2}', ['x', 'x', 'o-1']); t.is(a, c); t.end(); @@ -213,22 +217,19 @@ test('deserialize exports', t => { const { m } = makeMarshaller(); const o1 = harden({}); m.serialize(o1); // allocates slot=1 - const a = m.unserialize('{"@qclass":"slot","index":0}', [ - { type: 'export', id: 1 }, - ]); + const a = m.unserialize('{"@qclass":"slot","index":0}', ['o+1']); t.is(a, o1); t.end(); }); -test('serialize imports', t => { +test('serialize imports', async t => { + await prep(); const { m } = makeMarshaller(); - const a = m.unserialize('{"@qclass":"slot","index":0}', [ - { type: 'import', id: 1 }, - ]); + const a = m.unserialize('{"@qclass":"slot","index":0}', ['o-1']); t.deepEqual(m.serialize(a), { argsString: '{"@qclass":"slot","index":0}', - slots: [{ type: 'import', id: 1 }], + slots: ['o-1'], }); t.end(); @@ -239,8 +240,8 @@ test('serialize promise', async t => { const syscall = { createPromise() { return { - promiseID: 1, - resolverID: 2, + promiseID: 'p-1', + resolverID: 'r-1', }; }, fulfillToData(resolverID, data, slots) { @@ -252,19 +253,16 @@ test('serialize promise', async t => { const { p, res } = makePromise(); t.deepEqual(m.serialize(p), { argsString: '{"@qclass":"slot","index":0}', - slots: [{ type: 'promise', id: 1 }], + slots: ['p-1'], }); // serializer should remember the promise t.deepEqual(m.serialize(harden(['other stuff', p])), { argsString: '["other stuff",{"@qclass":"slot","index":0}]', - slots: [{ type: 'promise', id: 1 }], + slots: ['p-1'], }); // inbound should recognize it and return the promise - t.deepEqual( - m.unserialize('{"@qclass":"slot","index":0}', [{ type: 'promise', id: 1 }]), - p, - ); + t.deepEqual(m.unserialize('{"@qclass":"slot","index":0}', ['p-1']), p); res(5); t.deepEqual(log, []); @@ -272,12 +270,13 @@ test('serialize promise', async t => { const { p: pauseP, res: pauseRes } = makePromise(); setImmediate(() => pauseRes()); await pauseP; - t.deepEqual(log, [{ resolverID: 2, data: '5', slots: [] }]); + t.deepEqual(log, [{ resolverID: 'r-1', data: '5', slots: [] }]); t.end(); }); -test('unserialize promise', t => { +test('unserialize promise', async t => { + await prep(); const log = []; const syscall = { subscribe(promiseID) { @@ -286,10 +285,8 @@ test('unserialize promise', t => { }; const { m } = makeMarshaller(syscall); - const p = m.unserialize('{"@qclass":"slot","index":0}', [ - { type: 'promise', id: 1 }, - ]); - t.deepEqual(log, ['subscribe-1']); + const p = m.unserialize('{"@qclass":"slot","index":0}', ['p-1']); + t.deepEqual(log, ['subscribe-p-1']); t.ok(p instanceof Promise); t.end(); diff --git a/test/util.js b/test/util.js new file mode 100644 index 00000000000..0649766ee8d --- /dev/null +++ b/test/util.js @@ -0,0 +1,23 @@ +export function checkKT(t, kernel, expected) { + // extract the "kernel table" (a summary of all Vat clists) and assert that + // the contents match the expected list. This does a sort of the two lists + // before a t.deepEqual, which makes it easier to incrementally add + // expected mappings. + + function compareArraysOfStrings(a, b) { + a = a.join(' '); + b = b.join(' '); + if (a > b) { + return 1; + } + if (a < b) { + return -1; + } + return 0; + } + const got = Array.from(kernel.dump().kernelTable); + got.sort(compareArraysOfStrings); + expected = Array.from(expected); + expected.sort(compareArraysOfStrings); + t.deepEqual(got, expected); +} From 102bd83bba9668cc52dbdb62fa397953cb2727b0 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 27 Aug 2019 17:51:47 -0700 Subject: [PATCH 03/16] phase 1, comms/vattp changes * rewrite comms layer: use new vat/kernel ref format * use new inter-machine message format (object+promise, not resolver) * but still use old kernel API (syscall.createPromise) * generate more concise wire messages, with less JSON overhead * change transmitter/receiver registration (separate object per remote) * expose comms vat builder source path, like we expose the vattp builder * update tests to match, removing some unnecessary turns in basedir-commsvat Pipelining will be added in a later phase. --- demo/encouragementBotComms/bootstrap.js | 20 +- demo/encouragementBotComms/vat-botcomms.js | 4 +- demo/encouragementBotComms/vat-botvattp.js | 2 +- demo/encouragementBotComms/vat-usercomms.js | 4 +- demo/encouragementBotComms/vat-uservattp.js | 2 +- demo/encouragementBotCommsBang/bootstrap.js | 14 +- .../encouragementBotCommsBang/vat-botcomms.js | 4 +- .../encouragementBotCommsBang/vat-botvattp.js | 2 +- .../vat-usercomms.js | 4 +- .../vat-uservattp.js | 2 +- src/index.js | 6 +- src/kernel/commsSlots/commsController.js | 124 ------- src/kernel/commsSlots/commsSlots.js | 224 ------------- .../commsSlots/inbound/inboundHandler.js | 150 --------- src/kernel/commsSlots/inbound/mapInbound.js | 91 ------ src/kernel/commsSlots/index.js | 3 - src/kernel/commsSlots/outbound/mapOutbound.js | 128 -------- src/kernel/commsSlots/state/CLists.js | 139 -------- src/kernel/commsSlots/state/allocateID.js | 14 - src/kernel/commsSlots/state/channels.js | 25 -- src/kernel/commsSlots/state/index.js | 37 --- src/kernel/commsSlots/state/machineState.js | 14 - .../commsSlots/state/promiseResolverPairs.js | 20 -- src/kernel/kernel.js | 2 - src/vat-tp/vattp.js | 64 ---- src/vats/comms/clist.js | 233 ++++++++++++++ src/vats/comms/controller.js | 77 +++++ src/vats/comms/dispatch.js | 121 +++++++ src/vats/comms/inbound.js | 90 ++++++ src/vats/comms/index.js | 3 + src/vats/comms/outbound.js | 109 +++++++ src/vats/comms/parseRemoteSlot.js | 76 +++++ src/vats/comms/remote.js | 62 ++++ src/vats/comms/state.js | 51 +++ src/vats/parseVatSlots.js | 71 ++++ src/{ => vats}/vat-tp/bootstrap.js | 7 +- src/vats/vat-tp/vattp.js | 78 +++++ test/basedir-commsvat/bootstrap.js | 28 +- test/basedir-commsvat/vat-left.js | 39 ++- test/basedir-commsvat/vat-leftcomms.js | 4 +- test/basedir-commsvat/vat-leftvattp.js | 2 +- test/basedir-commsvat/vat-right.js | 51 ++- test/basedir-commsvat/vat-rightcomms.js | 4 +- test/basedir-commsvat/vat-rightvattp.js | 2 +- test/commsSlots-integration/run-test.js | 73 ----- test/commsSlots-integration/test-commsvats.js | 129 -------- test/commsSlots-integration/test-logs.js | 149 --------- .../commsController/test-addEgress.js | 60 ---- .../commsController/test-addIngress.js | 268 --------------- test/commsSlots/commsController/test-init.js | 78 ----- .../commsSlots/makeCommsSlots/test-deliver.js | 246 -------------- .../test-notifyFulfillToData.js | 56 ---- .../test-notifyFulfillToPresence.js | 60 ---- test/commsSlots/state/test-makeCLists.js | 62 ---- test/commsSlots/state/test-makeChannels.js | 11 - .../state/test-makeGetNextImportID.js | 11 - .../commsSlots/state/test-makeMachineState.js | 16 - test/commsSlots/test-inboundHandler.js | 304 ------------------ test/files-vattp/bootstrap-test-vattp.js | 14 +- test/test-comms-integration.js | 272 ++++++++++++++++ test/test-comms.js | 155 +++++++++ test/test-demos-comms.js | 38 +++ test/test-demos.js | 28 -- test/test-vattp.js | 32 +- 64 files changed, 1612 insertions(+), 2657 deletions(-) delete mode 100644 src/kernel/commsSlots/commsController.js delete mode 100644 src/kernel/commsSlots/commsSlots.js delete mode 100644 src/kernel/commsSlots/inbound/inboundHandler.js delete mode 100644 src/kernel/commsSlots/inbound/mapInbound.js delete mode 100644 src/kernel/commsSlots/index.js delete mode 100644 src/kernel/commsSlots/outbound/mapOutbound.js delete mode 100644 src/kernel/commsSlots/state/CLists.js delete mode 100644 src/kernel/commsSlots/state/allocateID.js delete mode 100644 src/kernel/commsSlots/state/channels.js delete mode 100644 src/kernel/commsSlots/state/index.js delete mode 100644 src/kernel/commsSlots/state/machineState.js delete mode 100644 src/kernel/commsSlots/state/promiseResolverPairs.js delete mode 100644 src/vat-tp/vattp.js create mode 100644 src/vats/comms/clist.js create mode 100644 src/vats/comms/controller.js create mode 100644 src/vats/comms/dispatch.js create mode 100644 src/vats/comms/inbound.js create mode 100644 src/vats/comms/index.js create mode 100644 src/vats/comms/outbound.js create mode 100644 src/vats/comms/parseRemoteSlot.js create mode 100644 src/vats/comms/remote.js create mode 100644 src/vats/comms/state.js create mode 100644 src/vats/parseVatSlots.js rename src/{ => vats}/vat-tp/bootstrap.js (50%) create mode 100644 src/vats/vat-tp/vattp.js delete mode 100644 test/commsSlots-integration/run-test.js delete mode 100644 test/commsSlots-integration/test-commsvats.js delete mode 100644 test/commsSlots-integration/test-logs.js delete mode 100644 test/commsSlots/commsController/test-addEgress.js delete mode 100644 test/commsSlots/commsController/test-addIngress.js delete mode 100644 test/commsSlots/commsController/test-init.js delete mode 100644 test/commsSlots/makeCommsSlots/test-deliver.js delete mode 100644 test/commsSlots/makeCommsSlots/test-notifyFulfillToData.js delete mode 100644 test/commsSlots/makeCommsSlots/test-notifyFulfillToPresence.js delete mode 100644 test/commsSlots/state/test-makeCLists.js delete mode 100644 test/commsSlots/state/test-makeChannels.js delete mode 100644 test/commsSlots/state/test-makeGetNextImportID.js delete mode 100644 test/commsSlots/state/test-makeMachineState.js delete mode 100644 test/commsSlots/test-inboundHandler.js create mode 100644 test/test-comms-integration.js create mode 100644 test/test-comms.js create mode 100644 test/test-demos-comms.js diff --git a/demo/encouragementBotComms/bootstrap.js b/demo/encouragementBotComms/bootstrap.js index 5c6f3166991..5d8c64cbd90 100644 --- a/demo/encouragementBotComms/bootstrap.js +++ b/demo/encouragementBotComms/bootstrap.js @@ -23,12 +23,28 @@ export default function setup(syscall, state, helpers) { D(devices.loopbox).registerInboundHandler(USER, vats.uservattp); const usersender = D(devices.loopbox).makeSender(USER); await E(vats.uservattp).registerMailboxDevice(usersender); - await E(vats.usercomms).init(vats.uservattp); + const { + transmitter: txToBotForUser, + setReceiver: setRxFromBotForUser, + } = await E(vats.uservattp).addRemote(BOT); + const rxFromBotForUser = await E(vats.usercomms).addRemote( + BOT, + txToBotForUser, + ); + E(setRxFromBotForUser).setReceiver(rxFromBotForUser); D(devices.loopbox).registerInboundHandler(BOT, vats.botvattp); const botsender = D(devices.loopbox).makeSender(BOT); await E(vats.botvattp).registerMailboxDevice(botsender); - await E(vats.botcomms).init(vats.botvattp); + const { + transmitter: txToUserForBot, + setReceiver: setRxFromUserForBot, + } = await E(vats.botvattp).addRemote(USER); + const rxFromUserForBot = await E(vats.botcomms).addRemote( + USER, + txToUserForBot, + ); + E(setRxFromUserForBot).setReceiver(rxFromUserForBot); await E(vats.botcomms).addEgress( USER, diff --git a/demo/encouragementBotComms/vat-botcomms.js b/demo/encouragementBotComms/vat-botcomms.js index ef4a86aad38..ee58e3d31eb 100644 --- a/demo/encouragementBotComms/vat-botcomms.js +++ b/demo/encouragementBotComms/vat-botcomms.js @@ -1,3 +1,5 @@ +import buildCommsDispatch from '../../src/vats/comms'; + export default function setup(syscall, state, helpers) { - return helpers.makeCommsSlots(syscall, state, helpers); + return buildCommsDispatch(syscall, state, helpers); } diff --git a/demo/encouragementBotComms/vat-botvattp.js b/demo/encouragementBotComms/vat-botvattp.js index b215edb7ee0..11419b6c79b 100644 --- a/demo/encouragementBotComms/vat-botvattp.js +++ b/demo/encouragementBotComms/vat-botvattp.js @@ -1,3 +1,3 @@ -import setup from '../../src/vat-tp/vattp'; +import setup from '../../src/vats/vat-tp/vattp'; export default setup; diff --git a/demo/encouragementBotComms/vat-usercomms.js b/demo/encouragementBotComms/vat-usercomms.js index ef4a86aad38..ee58e3d31eb 100644 --- a/demo/encouragementBotComms/vat-usercomms.js +++ b/demo/encouragementBotComms/vat-usercomms.js @@ -1,3 +1,5 @@ +import buildCommsDispatch from '../../src/vats/comms'; + export default function setup(syscall, state, helpers) { - return helpers.makeCommsSlots(syscall, state, helpers); + return buildCommsDispatch(syscall, state, helpers); } diff --git a/demo/encouragementBotComms/vat-uservattp.js b/demo/encouragementBotComms/vat-uservattp.js index b215edb7ee0..11419b6c79b 100644 --- a/demo/encouragementBotComms/vat-uservattp.js +++ b/demo/encouragementBotComms/vat-uservattp.js @@ -1,3 +1,3 @@ -import setup from '../../src/vat-tp/vattp'; +import setup from '../../src/vats/vat-tp/vattp'; export default setup; diff --git a/demo/encouragementBotCommsBang/bootstrap.js b/demo/encouragementBotCommsBang/bootstrap.js index 6a423a3ee6c..76cded19930 100644 --- a/demo/encouragementBotCommsBang/bootstrap.js +++ b/demo/encouragementBotCommsBang/bootstrap.js @@ -23,12 +23,22 @@ export default function setup(syscall, state, helpers) { D(devices.loopbox).registerInboundHandler(USER, vats.uservattp); const usersender = D(devices.loopbox).makeSender(USER); await vats.uservattp!registerMailboxDevice(usersender); - await vats.usercomms!init(vats.uservattp); + const { + transmitter: txToBotForUser, + setReceiver: setRxFromBotForUser, + } = await vats.uservattp!addRemote(BOT); + const rxFromBotForUser = await vats.usercomms!addRemote(BOT, txToBotForUser); + setRxFromBotForUser!setReceiver(rxFromBotForUser); D(devices.loopbox).registerInboundHandler(BOT, vats.botvattp); const botsender = D(devices.loopbox).makeSender(BOT); await vats.botvattp!registerMailboxDevice(botsender); - await vats.botcomms!init(vats.botvattp); + const { + transmitter: txToUserForBot, + setReceiver: setRxFromUserForBot, + } = await vats.botvattp!addRemote(USER); + const rxFromUserForBot = await vats.botcomms!addRemote(USER, txToUserForBot); + setRxFromUserForBot!setReceiver(rxFromUserForBot); await vats.botcomms!addEgress( USER, diff --git a/demo/encouragementBotCommsBang/vat-botcomms.js b/demo/encouragementBotCommsBang/vat-botcomms.js index ef4a86aad38..ee58e3d31eb 100644 --- a/demo/encouragementBotCommsBang/vat-botcomms.js +++ b/demo/encouragementBotCommsBang/vat-botcomms.js @@ -1,3 +1,5 @@ +import buildCommsDispatch from '../../src/vats/comms'; + export default function setup(syscall, state, helpers) { - return helpers.makeCommsSlots(syscall, state, helpers); + return buildCommsDispatch(syscall, state, helpers); } diff --git a/demo/encouragementBotCommsBang/vat-botvattp.js b/demo/encouragementBotCommsBang/vat-botvattp.js index b215edb7ee0..11419b6c79b 100644 --- a/demo/encouragementBotCommsBang/vat-botvattp.js +++ b/demo/encouragementBotCommsBang/vat-botvattp.js @@ -1,3 +1,3 @@ -import setup from '../../src/vat-tp/vattp'; +import setup from '../../src/vats/vat-tp/vattp'; export default setup; diff --git a/demo/encouragementBotCommsBang/vat-usercomms.js b/demo/encouragementBotCommsBang/vat-usercomms.js index ef4a86aad38..ee58e3d31eb 100644 --- a/demo/encouragementBotCommsBang/vat-usercomms.js +++ b/demo/encouragementBotCommsBang/vat-usercomms.js @@ -1,3 +1,5 @@ +import buildCommsDispatch from '../../src/vats/comms'; + export default function setup(syscall, state, helpers) { - return helpers.makeCommsSlots(syscall, state, helpers); + return buildCommsDispatch(syscall, state, helpers); } diff --git a/demo/encouragementBotCommsBang/vat-uservattp.js b/demo/encouragementBotCommsBang/vat-uservattp.js index b215edb7ee0..11419b6c79b 100644 --- a/demo/encouragementBotCommsBang/vat-uservattp.js +++ b/demo/encouragementBotCommsBang/vat-uservattp.js @@ -1,3 +1,3 @@ -import setup from '../../src/vat-tp/vattp'; +import setup from '../../src/vats/vat-tp/vattp'; export default setup; diff --git a/src/index.js b/src/index.js index 80222072b77..2c63a1dacee 100644 --- a/src/index.js +++ b/src/index.js @@ -4,5 +4,9 @@ import { buildMailboxStateMap, buildMailbox } from './devices/mailbox'; export { loadBasedir, buildVatController, buildMailboxStateMap, buildMailbox }; export function getVatTPSourcePath() { - return require.resolve('./vat-tp/vattp'); + return require.resolve('./vats/vat-tp/vattp'); +} + +export function getCommsSourcePath() { + return require.resolve('./vats/comms'); } diff --git a/src/kernel/commsSlots/commsController.js b/src/kernel/commsSlots/commsController.js deleted file mode 100644 index ffa297ed0e9..00000000000 --- a/src/kernel/commsSlots/commsController.js +++ /dev/null @@ -1,124 +0,0 @@ -const UNDEFINED = JSON.stringify({ '@qclass': 'undefined' }); - -export default function handleCommsController( - state, - syscall, - method, - argsbytes, - slots, - resolverID, - helpers, - inboundHandlerFacetID, -) { - function init([vattp]) { - if (state.machineState.getVatTP()) { - throw new Error('commsVat has already been initialized'); - } - - // remember the vatTP helper so we can send messages later - state.machineState.setVatTP(vattp); - - // Create a handler object, and register it with vatTP. After - // registration, each time a message arrives for our machine, vatTP will - // invoke the handler like: handler.inbound(senderName, message) - const handlerExport = { type: 'export', id: inboundHandlerFacetID }; - const regArgs = JSON.stringify({ - args: [{ '@qclass': 'slot', index: 0 }], - }); - syscall.send(vattp, 'registerCommsHandler', regArgs, [handlerExport]); - - syscall.fulfillToData(resolverID, UNDEFINED, []); - } - - // an egress is something on my machine that I make available to - // another machine - function addEgress([sender, index, valslot]) { - helpers.log( - `addEgress called with sender ${sender}, index ${index}, valslot ${valslot}`, - ); - if ( - Object(valslot) !== valslot || - !('@qclass' in valslot) || - valslot['@qclass'] !== 'slot' - ) { - throw new Error(`value must be a slot, not ${JSON.stringify(valslot)}`); - } - const kernelToMeSlot = slots[valslot.index]; - const meToYouSlot = { - type: 'your-ingress', - id: index, - }; - - const youToMeSlot = state.clists.changePerspective(meToYouSlot); - - state.clists.add(sender, kernelToMeSlot, youToMeSlot, meToYouSlot); - syscall.fulfillToData(resolverID, UNDEFINED, []); - } - - // an ingress is something that lives on another machine - // this function should only be used for bootstrapping as a shortcut - // in addIngress, we know the common index that we want to - // use to communicate about something on the right machine, - // but the leftcomms needs to export it to the kernel - function addIngress([otherMachineName, index]) { - helpers.log( - `addIngress called with machineName ${otherMachineName}, index ${index}`, - ); - - const youToMeSlot = { - type: 'your-ingress', - id: index, - }; - - // if we have already imported this, return the same id - let kernelToMeSlot = state.clists.mapIncomingWireMessageToKernelSlot( - otherMachineName, - youToMeSlot, - ); - - if (kernelToMeSlot === undefined) { - const id = state.ids.allocateID(); - - kernelToMeSlot = { - type: 'export', // this is an ingress that we are exporting to the kernel - id, - }; - - const meToYouSlot = state.clists.changePerspective(youToMeSlot); - - state.clists.add( - otherMachineName, - kernelToMeSlot, - youToMeSlot, - meToYouSlot, - ); - } - - // notify the kernel that this call has resolved - syscall.fulfillToPresence(resolverID, kernelToMeSlot); - } - - // This is a degenerate form of deserialization, just enough to handle the - // handful of methods implemented by the commsController. 'argsbytes' can - // normally have arbitrary {'@qclass':'slot', index} objects, which point - // into the 'slots' array. The only method that expects one is init(), and - // it always expects it in args[2], so we manually translate it here. - const { args } = JSON.parse(argsbytes); - - // translate args that are slots to the slot rather than qclass - - switch (method) { - case 'init': - if (args[0]['@qclass'] !== 'slot' || args[0].index !== 0) { - throw new Error(`unexpected args for init(): ${argsbytes}`); - } - args[0] = slots[args[0].index]; - return init(args); - case 'addEgress': - return addEgress(args); - case 'addIngress': - return addIngress(args); - default: - throw new Error(`method ${method} is not available`); - } -} diff --git a/src/kernel/commsSlots/commsSlots.js b/src/kernel/commsSlots/commsSlots.js deleted file mode 100644 index 92b9fd897c8..00000000000 --- a/src/kernel/commsSlots/commsSlots.js +++ /dev/null @@ -1,224 +0,0 @@ -import harden from '@agoric/harden'; - -// state -import makeState from './state/index'; - -// methods that can be called on the inital obj, 0 -import handleCommsController from './commsController'; -import makeMapOutbound from './outbound/mapOutbound'; - -// access to the outside world -import makeInboundHandler from './inbound/inboundHandler'; - -export default function makeCommsSlots(syscall, _state, helpers) { - const enableCSDebug = false; - const { vatID } = helpers; - function csdebug(...args) { - if (enableCSDebug) { - console.log(...args); - } - } - - // setup - const state = makeState(vatID); - const { mapOutbound, mapOutboundTarget, mapResultSlot } = makeMapOutbound( - syscall, - state, - ); - const { inboundHandler } = makeInboundHandler(state, syscall); - const inboundHandlerFacetID = state.ids.allocateID(); - - function sendToVatTP(toMachineName, data) { - const vt = state.machineState.getVatTP(); - if (!vt) { - throw new Error('sendToVatTP() called before init() did setVatTP()'); - } - const args = { args: [toMachineName, data] }; - // TODO: this should be sendOnly, once vatManager provides that - syscall.send(vt, 'send', JSON.stringify(args), []); - } - - const dispatch = harden({ - // eslint-disable-next-line consistent-return - deliver(facetid, method, argsStr, kernelToMeSlots, resolver) { - const kernelToMeSlotTarget = { type: 'export', id: facetid }; - csdebug( - `cs[${vatID}].dispatch.deliver ${facetid}.${method} -> ${resolver && - resolver.id}`, - ); - - // CASE 1: we are hitting the initial object (0) - if (facetid === 0) { - const result = handleCommsController( - state, - syscall, - method, - argsStr, - kernelToMeSlots, - resolver.id, - helpers, - inboundHandlerFacetID, - ); - return result; - } - - // CASE 2: messages are coming from a device node that we have - // registered an object with. The device node will use sendOnly(), so - // we won't get a resolverID. - - // TODO: build out this case, should look like liveSlots - // TODO: figure out how to move commsSlots into liveSlots - if (facetid === inboundHandlerFacetID) { - return inboundHandler(method, argsStr, kernelToMeSlots); - // TODO: resolve resolverID, at least until we change vattp.js to use - // sendOnly() for commsHandler.inbound instead of send(), at which - // point resolverID should always be empty - } - - // TODO: move the rest of this method into outbound/ somewhere - - // CASE 3: messages from other vats to send to other machines - - // since we are sending to a target that is an object on another - // machine, it is an ingress to us - // we must have it in our tables at this point, if we don't it's an error. - // Otherwise we wouldn't know where to send the outgoing - // message. - - // we may have multiple machines to send the message to - const outboundWireMessageList = mapOutboundTarget(kernelToMeSlotTarget); - - function mapAndSend(outboundWireMessage) { - const { - otherMachineName, - meToYouSlot: meToYouTargetSlot, - } = outboundWireMessage; - - // TODO: throw an exception if the otherMachineName that we get - // from slots is different than otherMachineName that we get - // from the target slot. That would be the three-party handoff - // case which we are not supporting yet - - // TODO: don't parse the args we get from the kernel, we should wrap - // these in a struture that adds methodName/event/target/etc and send - // that to the other side - const { args } = JSON.parse(argsStr); - - // map the slots - // we want to ensure that we are not revealing anything about - // our internal data - thus we need to transform everything - // related to slots - - // if something isn't present in the slots (note, not the target), we need to allocate an id for it - // and store it - - const meToYouSlots = kernelToMeSlots.map(slot => - mapOutbound(otherMachineName, slot), - ); - - // TODO: resolverID might be empty if the local vat did - // syscall.sendOnly, in which case we should leave resultSlot empty too - const resultSlot = mapResultSlot(otherMachineName, resolver); - - const message = JSON.stringify({ - event: 'send', - target: meToYouTargetSlot, - methodName: method, - args, // TODO just include argsStr directly - slots: meToYouSlots, - resultSlot, - }); - - return sendToVatTP(otherMachineName, message); - } - outboundWireMessageList.map(mapAndSend); - }, - - // TODO: change promiseID to a slot instead of wrapping it - notifyFulfillToData(promiseID, dataStr, kernelToMeSlots) { - csdebug( - `cs.dispatch.notifyFulfillToData(${promiseID}, ${dataStr}, ${kernelToMeSlots})`, - ); - - const outgoingWireMessageList = mapOutboundTarget({ - type: 'promise', - id: promiseID, // cast to kernelToMeSlot - }); - - function mapAndSend(outboundWireMessage) { - const { otherMachineName, meToYouSlot } = outboundWireMessage; - const meToYouSlots = kernelToMeSlots.map(slot => - mapOutbound(otherMachineName, slot), - ); - - // we need to map the slots and pass those on - const dataMsg = JSON.stringify({ - event: 'fulfillToData', - promise: meToYouSlot, - args: dataStr, - slots: meToYouSlots, // TODO: these should be dependent on the machine we are sending to - }); - - // TODO: figure out whether there is a one-to-one correspondance - // between our exports to the kernel and objects - - sendToVatTP(otherMachineName, dataMsg); - } - - outgoingWireMessageList.map(mapAndSend); - }, - - // TODO: use a slot with type promise instead of a promiseID - notifyFulfillToPresence(promiseID, slot) { - csdebug(`cs.dispatch.notifyFulfillToPresence(${promiseID}, ${slot})`); - - const outgoingWireMessageList = mapOutboundTarget({ - type: 'promise', - id: promiseID, - }); - - function mapAndSend(outgoingWireMessage) { - const { otherMachineName, meToYouSlot } = outgoingWireMessage; - const dataMsg = JSON.stringify({ - event: 'fulfillToPresence', - promise: meToYouSlot, - target: mapOutbound(otherMachineName, slot), - }); - - sendToVatTP(otherMachineName, dataMsg); - } - - outgoingWireMessageList.map(mapAndSend); - }, - - // TODO: use promise slot rather than promiseID - notifyReject(promiseID, data, slots) { - csdebug(`cs.dispatch.notifyReject(${promiseID}, ${data}, ${slots})`); - - const outgoingWireMessageList = mapOutboundTarget({ - type: 'promise', - id: promiseID, - }); - - function mapAndSend(outgoingWireMessage) { - const { otherMachineName, meToYouSlot } = outgoingWireMessage; - const msg = JSON.stringify({ - event: 'reject', - promise: meToYouSlot, - args: data, - slots: slots.map(slot => mapOutbound(otherMachineName, slot)), - }); - - sendToVatTP(otherMachineName, msg); - } - outgoingWireMessageList.map(mapAndSend); - }, - - // for testing purposes only - getState() { - return state; - }, - }); - - return dispatch; -} diff --git a/src/kernel/commsSlots/inbound/inboundHandler.js b/src/kernel/commsSlots/inbound/inboundHandler.js deleted file mode 100644 index 089d0922ee1..00000000000 --- a/src/kernel/commsSlots/inbound/inboundHandler.js +++ /dev/null @@ -1,150 +0,0 @@ -import makeMapInbound from './mapInbound'; - -function parseJSON(data) { - try { - const d = JSON.parse(data); - return d; - } catch (e) { - console.log(`unparseable data: ${data}`); - throw e; - } -} - -export default function makeInboundHandler(state, syscall) { - const enableSIDebug = true; - function sidebug(...args) { - if (enableSIDebug) { - console.log(...args); - } - } - - return { - /** - * aka 'inbound' from SwingSet-Cosmos - * @param {string} senderID public key? - * @param {string} dataStr JSON, such as: - * { - * index: 0, - * methodName: 'getIssuer', - * args: [], - * resultIndex: 1, - * } - * - */ - inboundHandler(method, argsStr, deviceToMeSlots) { - if (method !== 'inbound') { - throw new Error(`inboundHandler got method '${method}', not 'inbound'`); - } - if (deviceToMeSlots.length !== 0) { - throw new Error( - `inboundHandler got unexpected slots, ${JSON.stringify( - deviceToMeSlots, - )}`, - ); - } - const [senderID, dataStr] = JSON.parse(argsStr).args; - sidebug(`sendIn ${senderID} => ${dataStr}`); - - const { - mapInboundTarget, - mapInboundSlot, - mapInboundResolver, - } = makeMapInbound(syscall, state, senderID); - const data = parseJSON(dataStr); - - // everything that comes in to us as a target or a slot needs to - // get mapped to a kernel slot. If we don't already have a kernel slot for - // something, it is either an error or we should allocate it. - - // there are four potential events which map onto the syscalls - // * send - // * fulfillToData - // * fulfillToPresence - // * reject - - // the syscall interfaces are: - // * syscall.send(kernelToMeTarget, methodName, argsString, kernelToMeSlots) -> - // resultPromiseID - // * syscall.fulfillToData(resolverID, resultString, - // kernelToMeSlots) - // * syscall.fulfillToPresence(resolverID, kernelToMeSlot) - // * syscall.reject(resolverID, resultString, kernelToMeSlots) - - switch (data.event) { - case 'send': { - // build the arguments to syscall.send() - const kernelToMeTarget = mapInboundTarget(data.target); - const { methodName } = data; - - const kernelToMeSlots = data.slots.map(mapInboundSlot); - - // put the target.methodName(args, slots) call on the runQueue to - // be delivered - const promiseID = syscall.send( - kernelToMeTarget, - methodName, - JSON.stringify({ args: data.args }), - kernelToMeSlots, - ); - - // if there is a resultIndex passed in, the inbound sender wants - // to know about the result, so we need to store this in clist for - // the sender to have future access to - - // in the sendOnly case, no resultSlot should be passed - - if (data.resultSlot) { - const kernelToMeSlot = { - type: 'promise', - id: promiseID, - }; - - // We don't need to create a promise because the result of - // syscall.send() is a promiseID already. - - const youToMeSlot = data.resultSlot; - const meToYouSlot = state.clists.changePerspective(youToMeSlot); - state.clists.add( - senderID, - kernelToMeSlot, - youToMeSlot, - meToYouSlot, - ); - syscall.subscribe(promiseID); - } - break; - } - case 'fulfillToData': { - const resolverKernelToMeSlot = mapInboundResolver(data.promise); - const kernelToMeSlots = data.slots.map(mapInboundSlot); - - syscall.fulfillToData( - resolverKernelToMeSlot.id, - data.args, - kernelToMeSlots, - ); - return; - } - case 'fulfillToPresence': { - const resolverKernelToMeSlot = mapInboundResolver(data.promise); - const kernelToMeTarget = mapInboundSlot(data.target); - - syscall.fulfillToPresence( - resolverKernelToMeSlot.id, - kernelToMeTarget, - ); - return; - } - case 'reject': { - const resolverKernelToMeSlot = mapInboundResolver(data.promise); - const kernelToMeSlots = data.slots.map(mapInboundSlot); - - syscall.reject(resolverKernelToMeSlot.id, data.args, kernelToMeSlots); - return; - } - default: - throw new Error(`unknown event ${data.event}`); - } - }, - }; -} diff --git a/src/kernel/commsSlots/inbound/mapInbound.js b/src/kernel/commsSlots/inbound/mapInbound.js deleted file mode 100644 index 4b8d10c2dd1..00000000000 --- a/src/kernel/commsSlots/inbound/mapInbound.js +++ /dev/null @@ -1,91 +0,0 @@ -function makeMapInbound(syscall, state, senderID) { - function mapInboundTarget(youToMeSlot) { - const kernelToMeSlot = state.clists.mapIncomingWireMessageToKernelSlot( - senderID, - youToMeSlot, - ); - if (kernelToMeSlot === undefined) { - throw new Error( - `unrecognized inbound egress target ${JSON.stringify(youToMeSlot)}`, - ); - } - return kernelToMeSlot; - } - - function mapInboundSlot(youToMeSlot) { - let kernelToMeSlot = state.clists.mapIncomingWireMessageToKernelSlot( - senderID, - youToMeSlot, - ); - - if (kernelToMeSlot === undefined) { - // we are telling the kernel about something that exists on - // another machine, that it doesn't know about yet - - switch (youToMeSlot.type) { - // an object that lives on the other machine - case 'your-ingress': { - const exportID = state.ids.allocateID(); - kernelToMeSlot = { type: 'export', id: exportID }; - break; - } - // the right to resolve, the decider right is with the other machine - case 'your-promise': { - const pr = syscall.createPromise(); - - // we need to keep references to both the promise and - // resolver - // we use the promise when we talk to the kernel in terms of - // slots - // we use the resolver when we tell the kernel to resolve - // the promise (reject, fulfill, ...) - - kernelToMeSlot = { type: 'promise', id: pr.promiseID }; - state.promiseResolverPairs.add( - { type: 'promise', id: pr.promiseID }, - { type: 'resolver', id: pr.resolverID }, - ); - - // do not subscribe to the promise since all settlement - // messages should be coming in from other machines - break; - } - - default: - throw new Error(`youToMeSlot.type ${youToMeSlot.type} is unexpected`); - } - - state.clists.add( - senderID, - kernelToMeSlot, - youToMeSlot, - state.clists.changePerspective(youToMeSlot), - ); - } - return state.clists.mapIncomingWireMessageToKernelSlot( - senderID, - youToMeSlot, - ); - } - - function mapInboundResolver(youToMeSlot) { - // sometimes the resolver is stored (the resultSlot case), sometimes the promise is stored - const kernelToMeSlot = state.clists.mapIncomingWireMessageToKernelSlot( - senderID, - youToMeSlot, - ); - - if (kernelToMeSlot.type === 'resolver') { - return kernelToMeSlot; - } - return state.promiseResolverPairs.getResolver(kernelToMeSlot); - } - - return { - mapInboundTarget, - mapInboundSlot, - mapInboundResolver, - }; -} - -export default makeMapInbound; diff --git a/src/kernel/commsSlots/index.js b/src/kernel/commsSlots/index.js deleted file mode 100644 index 33016714cbd..00000000000 --- a/src/kernel/commsSlots/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import makeCommsSlots from './commsSlots'; - -export { makeCommsSlots }; diff --git a/src/kernel/commsSlots/outbound/mapOutbound.js b/src/kernel/commsSlots/outbound/mapOutbound.js deleted file mode 100644 index 2ad5ac3c80e..00000000000 --- a/src/kernel/commsSlots/outbound/mapOutbound.js +++ /dev/null @@ -1,128 +0,0 @@ -function makeMapOutbound(syscall, state) { - function mapOutbound(otherMachineName, kernelToMeSlot) { - const outgoingWireMessageObj = state.clists.mapKernelSlotToOutgoingWireMessage( - kernelToMeSlot, - otherMachineName, - ); // otherMachineName, direction, meToYouSlot - if (outgoingWireMessageObj === undefined) { - // this is something that we have on this machine, and we want - // to tell another machine about it. - // OR we have an error - - let meToYouSlot; - - switch (kernelToMeSlot.type) { - case 'export': { - throw new Error( - 'we do not expect an export (something we have given to the kernel) if outgoingWireMessageObj is undefined', - ); - } - case 'import': { - const type = 'your-ingress'; - const id = state.ids.allocateID(); - meToYouSlot = { - type, - id, - }; - break; - } - case 'promise': { - // kernel gives a promise, that means that we want to - // subscribe to it and notify the other side when it - // settles. - - // we will need to create a new ID to tell the other side about. - - // the promise in an argument is always sent as a promise, - // even if it resolves to a presence - - const type = 'your-promise'; - const id = state.ids.allocateID(); - meToYouSlot = { - type, - id, - }; - - // when we send this as a slot, we also need to subscribe - // such that we can pass on all of the notifications of the - // promise settlement - syscall.subscribe(kernelToMeSlot.id); - - break; - } - default: { - throw new Error('unexpected kernelToMeSlot.type'); - } - } - - const youToMeSlot = state.clists.changePerspective(meToYouSlot); - state.clists.add( - otherMachineName, - kernelToMeSlot, - youToMeSlot, - meToYouSlot, - ); - } - const outgoingWireMessage = state.clists.mapKernelSlotToOutgoingWireMessage( - kernelToMeSlot, - otherMachineName, - ); - return outgoingWireMessage.meToYouSlot; - } - - function mapOutboundTarget(kernelToMeSlot) { - const outgoingWireMessageList = state.clists.mapKernelSlotToOutgoingWireMessageList( - kernelToMeSlot, - ); // otherMachineName, direction, meToYouSlot - // we will need to know what machine to send it to, just from the - // kernel slot - - // we also do not allocate a new id, if we can't find it, it's an - // error. - if (!outgoingWireMessageList) { - throw new Error( - `targetSlot ${JSON.stringify(kernelToMeSlot)} is not recognized`, - ); - } - return outgoingWireMessageList; - } - - function mapResultSlot(otherMachineName, kernelToMeSlot) { - // kernel gives us a resolver - // this is the resultIndex/resultSlot case - - // that means that the kernel is asking the commsVat to send - // a message on, and has asked to be notified when it resolves. - - // when we talk about this over the wire, this will be - // 'your-resolver' in meToYou language, and 'your-promise' in - // youToMe language - - const meToYouSlot = { - type: 'your-resolver', - id: state.ids.allocateID(), - }; - - const youToMeSlot = state.clists.changePerspective(meToYouSlot); - state.clists.add( - otherMachineName, - kernelToMeSlot, - youToMeSlot, - meToYouSlot, - ); - - const outgoingWireMessage = state.clists.mapKernelSlotToOutgoingWireMessage( - kernelToMeSlot, - otherMachineName, - ); - return outgoingWireMessage.meToYouSlot; - } - - return { - mapOutbound, - mapOutboundTarget, - mapResultSlot, - }; -} - -export default makeMapOutbound; diff --git a/src/kernel/commsSlots/state/CLists.js b/src/kernel/commsSlots/state/CLists.js deleted file mode 100644 index b8053ccd3ad..00000000000 --- a/src/kernel/commsSlots/state/CLists.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * MakeCLists Module - * This module is instantiated per CommsVat and stores data about - * mappings between external machines and slots. - * - * a clist maps a local machine kernel slot to what will be sent over the wire - * - * @module makeCLists - */ - -export function makeCLists() { - const state = new Map(); - - function checkIfAlreadyExists(incomingWireMessageKey, kernelToMeKey) { - const slot = state.get(incomingWireMessageKey); - const outgoing = state.get(kernelToMeKey); - if (slot && outgoing) { - throw new Error(`${kernelToMeKey} already exists in clist`); - } - } - - const changePerspectiveMap = new Map(); - // youToMe: your-egress, meToYou: your-ingress - // youToMe: your-ingress, meToYou: your-egress - // youToMe: your-promise, meToYou: your-resolver - // youToMe: your-resolver, meToYou: your-promise - changePerspectiveMap.set('your-egress', 'your-ingress'); - changePerspectiveMap.set('your-ingress', 'your-egress'); - changePerspectiveMap.set('your-promise', 'your-resolver'); - changePerspectiveMap.set('your-resolver', 'your-promise'); - - function changePerspective(slot) { - const otherPerspective = changePerspectiveMap.get(slot.type); - if (otherPerspective === undefined) { - throw new Error(`slot type ${slot.type} is not an allowed type`); - } - return { - type: otherPerspective, - id: slot.id, - }; - } - - function createIncomingWireMessageKey(otherMachineName, youToMeSlot) { - // TODO: we need an encoding scheme that is both stable and - // collision-free under adversarial attack. Any encoding scheme that can - // be decoded unambiguously is sufficiently collision-free. This first - // used JSON.stringify, which is collision-free, but its stability - // depends upon the order in which the object keys were added. "djson" (a - // library that provides deterministic JSON encoding) would be stable, - // but importing it into a SES environment sounds like an adventure. For - // now, we use a cheap concatenation that is stable but not - // collision-free. However, 'otherMachineName' will generally be a public - // key, which won't have hyphens, so the attacker is not likely to be - // able to force a collision with other machines, which is the only kind - // of collision that could get them unintended access. See #45 for more. - return `incoming-${otherMachineName}-${youToMeSlot.type}-${youToMeSlot.id}`; - } - - function createOutgoingWireMessageObj(otherMachineName, meToYouSlot) { - return { - otherMachineName, - meToYouSlot, // could be a your-ingress, your-egress - }; - } - - function createKernelToMeKey(kernelToMeSlot) { - return `kernel-${kernelToMeSlot.type}-${kernelToMeSlot.id}`; - } - - // takes youToMeSlot, returns kernelToMeSlot - function mapIncomingWireMessageToKernelSlot(otherMachineName, youToMeSlot) { - const key = createIncomingWireMessageKey(otherMachineName, youToMeSlot); - // if (!state.has(key)) { - // console.log(`unable to find key ${key}`); - // } - return state.get(key); - } - - // takes kernelToMeSlot, returns meToYouSlot and machineName - // we don't know the otherMachineName - function mapKernelSlotToOutgoingWireMessageList(kernelToMeSlot) { - const machineNameToOutgoingWireMessageMap = state.get( - createKernelToMeKey(kernelToMeSlot), - ); - return Array.from(machineNameToOutgoingWireMessageMap.values()); - } - - function mapKernelSlotToOutgoingWireMessage( - kernelToMeSlot, - otherMachineName, - ) { - const machineNameToOutgoingMessageMap = state.get( - createKernelToMeKey(kernelToMeSlot), - ); - if (machineNameToOutgoingMessageMap === undefined) { - return undefined; - } - return machineNameToOutgoingMessageMap.get(otherMachineName); // the meToYouSlot for that otherMachineName - } - - // kernelToMeSlot can have type: import, export or promise - // youToMe and meToYou slots can have type: your-ingress or - // your-egress - - // we will use this in the following ways: - // 1) to send out information about something that we know as a - // kernelToMeSlot - we will need to allocate an id if it doesn't - // already exist and then get the 'meToYouSlot' to send over the - // wire - // 2) to translate something that we get over the wire (youToMeSlot) - // into a kernelToMeSlot. - function add(otherMachineName, kernelToMeSlot, youToMeSlot, meToYouSlot) { - const incomingWireMessageKey = createIncomingWireMessageKey( - otherMachineName, - youToMeSlot, - ); - const outgoingWireMessageObj = createOutgoingWireMessageObj( - otherMachineName, - meToYouSlot, - ); - const kernelToMeKey = createKernelToMeKey(kernelToMeSlot); - checkIfAlreadyExists(incomingWireMessageKey, kernelToMeKey); - const machineNameToOutgoingMessage = state.get(kernelToMeKey) || new Map(); - machineNameToOutgoingMessage.set(otherMachineName, outgoingWireMessageObj); - state.set(kernelToMeKey, machineNameToOutgoingMessage); - state.set(incomingWireMessageKey, kernelToMeSlot); - } - - return { - mapIncomingWireMessageToKernelSlot, - mapKernelSlotToOutgoingWireMessageList, - mapKernelSlotToOutgoingWireMessage, - changePerspective, - add, - dump() { - return state; - }, - }; -} diff --git a/src/kernel/commsSlots/state/allocateID.js b/src/kernel/commsSlots/state/allocateID.js deleted file mode 100644 index 3de2bb48172..00000000000 --- a/src/kernel/commsSlots/state/allocateID.js +++ /dev/null @@ -1,14 +0,0 @@ -export function makeAllocateID() { - let nextID = 1; - - return { - allocateID() { - const id = nextID; - nextID += 1; - return id; - }, - dump() { - return JSON.stringify(nextID); - }, - }; -} diff --git a/src/kernel/commsSlots/state/channels.js b/src/kernel/commsSlots/state/channels.js deleted file mode 100644 index 631d86708e7..00000000000 --- a/src/kernel/commsSlots/state/channels.js +++ /dev/null @@ -1,25 +0,0 @@ -// TODO: check signature on this -// in the future, data structure would contain name and predicate - -// for now, this just tracks the one channel we use for all messages -export function makeChannels() { - let channelDev; - - return { - /** - * @param {slotref} device - */ - setChannelDevice(device) { - channelDev = device; - }, - /** - * @returns {slotref} device - */ - getChannelDevice() { - return channelDev; - }, - dump() { - return channelDev; - }, - }; -} diff --git a/src/kernel/commsSlots/state/index.js b/src/kernel/commsSlots/state/index.js deleted file mode 100644 index 2e5523def92..00000000000 --- a/src/kernel/commsSlots/state/index.js +++ /dev/null @@ -1,37 +0,0 @@ -import { makeCLists } from './CLists'; -import { makeChannels } from './channels'; -import { makeAllocateID } from './allocateID'; -import { makeMachineState } from './machineState'; -import { makePromiseResolverPairs } from './promiseResolverPairs'; - -function makeState(name) { - const vatName = name; - const machineState = makeMachineState(); - const clists = makeCLists(); - const channels = makeChannels(); - const ids = makeAllocateID(); - const promiseResolverPairs = makePromiseResolverPairs(); - - function dumpState() { - console.log('STATE', { - machineState: machineState.dump(), - clists: clists.dump(), - channels: channels.dump(), - nextID: ids.dump(), - vatName, - promiseResolverPairs: promiseResolverPairs.dump(), - }); - } - - return { - clists, - channels, - ids, - machineState, - dumpState, - vatName, - promiseResolverPairs, - }; -} - -export default makeState; diff --git a/src/kernel/commsSlots/state/machineState.js b/src/kernel/commsSlots/state/machineState.js deleted file mode 100644 index e1800c1dea4..00000000000 --- a/src/kernel/commsSlots/state/machineState.js +++ /dev/null @@ -1,14 +0,0 @@ -export function makeMachineState() { - let vattp; - return { - getVatTP() { - return vattp; - }, - setVatTP(v) { - vattp = v; - }, - dump() { - return JSON.stringify({}); - }, - }; -} diff --git a/src/kernel/commsSlots/state/promiseResolverPairs.js b/src/kernel/commsSlots/state/promiseResolverPairs.js deleted file mode 100644 index 1e26b4858ad..00000000000 --- a/src/kernel/commsSlots/state/promiseResolverPairs.js +++ /dev/null @@ -1,20 +0,0 @@ -export function makePromiseResolverPairs() { - const resolverToPromise = new Map(); - const promiseToResolver = new Map(); - - return { - add(promise, resolver) { - promiseToResolver.set(JSON.stringify(promise), resolver); - resolverToPromise.set(JSON.stringify(resolver), promise); - }, - getResolver(promise) { - return promiseToResolver.get(JSON.stringify(promise)); - }, - getPromise(resolver) { - return resolverToPromise.get(JSON.stringify(resolver)); - }, - dump() { - return promiseToResolver; - }, - }; -} diff --git a/src/kernel/kernel.js b/src/kernel/kernel.js index aa6fd8f572f..0fd9c219d65 100644 --- a/src/kernel/kernel.js +++ b/src/kernel/kernel.js @@ -3,7 +3,6 @@ import { QCLASS, makeMarshal } from '@agoric/marshal'; import { makeLiveSlots } from './liveSlots'; import { makeDeviceSlots } from './deviceSlots'; -import { makeCommsSlots } from './commsSlots/index'; import makePromise from './makePromise'; import makeVatManager from './vatManager'; import makeDeviceManager from './deviceManager'; @@ -394,7 +393,6 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { const helpers = harden({ vatID, makeLiveSlots, - makeCommsSlots, log(...args) { const rendered = args.map(arg => typeof arg === 'string' diff --git a/src/vat-tp/vattp.js b/src/vat-tp/vattp.js deleted file mode 100644 index 360ef236354..00000000000 --- a/src/vat-tp/vattp.js +++ /dev/null @@ -1,64 +0,0 @@ -import harden from '@agoric/harden'; - -function build(E, D) { - let mailbox; // mailbox device - // inbound - let commsHandler; - // outbound - const peers = new Map(); - // { outbound: { highestRemoved, highestAdded }, inbound: { highestDelivered } } - - function getPeer(peer) { - if (!peers.has(peer)) { - peers.set(peer, { - outbound: { highestRemoved: 0, highestAdded: 0 }, - inbound: { highestDelivered: 0 }, - }); - } - return peers.get(peer); - } - - const handler = harden({ - registerMailboxDevice(mailboxDevnode) { - mailbox = mailboxDevnode; - }, - registerCommsHandler(h) { - commsHandler = h; - }, - deliverInboundMessages(peer, newMessages) { - const p = getPeer(peer).inbound; - newMessages.forEach(m => { - const [num, body] = m; - if (num > p.highestDelivered) { - // TODO: SO() / sendOnly() - E(commsHandler).inbound(peer, body); - p.highestDelivered = num; - D(mailbox).ackInbound(peer, num); - } - }); - }, - - deliverInboundAck(peer, ack) { - const p = getPeer(peer).outbound; - let num = p.highestRemoved + 1; - while (num <= p.highestAdded && num <= ack) { - D(mailbox).remove(peer, num); - p.highestRemoved = num; - num += 1; - } - }, - - send(peer, msg) { - const p = getPeer(peer).outbound; - const num = p.highestAdded + 1; - D(mailbox).add(peer, num, msg); - p.highestAdded = num; - }, - }); - - return handler; -} - -export default function setup(syscall, state, helpers) { - return helpers.makeLiveSlots(syscall, state, build, helpers.vatID); -} diff --git a/src/vats/comms/clist.js b/src/vats/comms/clist.js new file mode 100644 index 00000000000..1d100d426fb --- /dev/null +++ b/src/vats/comms/clist.js @@ -0,0 +1,233 @@ +import Nat from '@agoric/nat'; +import { makeVatSlot, parseVatSlot, insistVatType } from '../parseVatSlots'; +import { + flipRemoteSlot, + makeRemoteSlot, + parseRemoteSlot, +} from './parseRemoteSlot'; +import { getRemote } from './remote'; +import { insist } from '../../kernel/insist'; + +export function getOutbound(state, remoteID, target) { + const remote = getRemote(state, remoteID); + + if (!remote.toRemote.has(target)) { + throw new Error( + `target ${target} must already be in ${remote.remoteID} (${remote.name})`, + ); + } + return remote.toRemote.get(target); +} + +export function mapOutbound(state, remoteID, s, syscall) { + // We're sending a slot to a remote system. If we've ever sent it before, + // or if they're the ones who sent it to us in the first place, it will be + // in the outbound table already. + const remote = getRemote(state, remoteID); + const existing = remote.toRemote.get(s); + + if (existing === undefined) { + const { type, allocatedByVat } = parseVatSlot(s); + if (type === 'object') { + if (allocatedByVat) { + // we (the comms vat) allocated this local object earlier, because + // we sent it into the kernel to some other local vat, because it + // represents some object on a remote vat that we're proxying. But + // if it wasn't in the outbound table for *this* remote, then it + // must have arrived from some *different* remote, which makes this + // a three-party handoff, and we haven't implemented that yet. + if (!state.objectTable.has(s)) { + // Or it's a local object (like one of the receivers) that isn't + // supposed to be sent off-device + throw new Error(`sending non-remote object ${s} to remote machine`); + } + const owner = state.objectTable.get(s); + throw new Error( + `unimplemented three-party handoff (object ${s}) from ${owner} to ${remote.id})`, + ); + } else { + // allocated by kernel: a local vat is exposing an object to the + // remote machine. Allocate a clist entry for it. + const index = remote.nextObjectIndex; + remote.nextObjectIndex += 1; + // The recipient will receive ro-NN + remote.toRemote.set(s, makeRemoteSlot('object', false, index)); + // but when they send it back, they'll send ro+NN + remote.fromRemote.set(makeRemoteSlot('object', true, index), s); + } + } else if (type === 'promise') { + if (allocatedByVat) { + throw new Error(`unable to handle vat-allocated promise ${s} yet`); + } else { + if (state.promiseTable.has(s)) { + // We already know about this promise, either because it arrived + // from some other remote, or because we sent it to some other + // remote. That limits what we can do with it. We can only send the + // promise if we allocated the index and we are the decider. + const p = state.promiseTable.get(s); + insist( + !p.owner, + `promise ${s} owner is ${p.owner}, not me, so I cannot send to ${remoteID}`, + ); + insist( + !p.decider, + `promise ${s} decider is ${p.decider}, not me, so I cannot send to ${remoteID}`, + ); + // also we can't handle more than a single subscriber yet + insist( + !p.subscriber, + `promise ${s} already has subscriber ${p.subscriber}`, + ); + // TODO: we currently only handle sending unresolved promises, but + // obviously we must fix that, by arranging to send a resolution + // message just after we send the reference that creates it, or + // perhaps tolerating sending the messages in the opposite order. + insist( + !p.resolved, + `promise ${s} is already resolved and I can't deal with that yet`, + ); + } + const index = remote.nextPromiseIndex; + remote.nextPromiseIndex += 1; + // the recipient receives rp-NN + const rs = makeRemoteSlot('promise', false, index); + remote.toRemote.set(s, rs); + remote.fromRemote.set(flipRemoteSlot(rs), s); + if (!state.promiseTable.has(s)) { + state.promiseTable.set(s, { + owner: null, // kernel-allocated, so ID is this-machine-allocated + resolved: false, + decider: null, // we decide + subscriber: remoteID, + }); + syscall.subscribe(s); + } + } + } else if (type === 'resolver') { + // TODO: is this clause currently unused? + insist(!allocatedByVat, `resolver ${s} must be kernel-allocated for now`); + const index = remote.nextResolverIndex; + remote.nextResolverIndex += 1; + // recipient gets rr-NN + remote.toRemote.set(s, makeRemoteSlot('resolver', false, index)); + // recipient sends rr+NN + remote.fromRemote.set(makeRemoteSlot('resolver', true, index), s); + } else { + throw new Error(`unknown type ${type}`); + } + } + return remote.toRemote.get(s); +} + +export function mapInbound(state, remoteID, s, syscall) { + // We're receiving a slot from a remote system. If they've sent it to us + // previously, or if we're the ones who sent it to them earlier, it will be + // in the inbound table already. + const remote = getRemote(state, remoteID); + const existing = remote.fromRemote.get(s); + + if (existing === undefined) { + const { type, allocatedByRecipient } = parseRemoteSlot(s); + if (type === 'object') { + if (allocatedByRecipient) { + throw new Error(`I don't remember giving ${s} to ${remoteID}`); + } + // this must be a new import. Allocate a new vat object for it, which + // will be the local machine's proxy for use by all other local vats + const localSlot = makeVatSlot(type, true, state.nextObjectIndex); + state.nextObjectIndex += 1; + + // they sent us ro-NN + remote.fromRemote.set(s, localSlot); + // when we send it back, we'll send ro+NN + remote.toRemote.set(localSlot, flipRemoteSlot(s)); + // and remember how to route this later + state.objectTable.set(localSlot, remoteID); + } else if (type === 'promise') { + if (allocatedByRecipient) { + throw new Error(`promises not implemented yet`); + } else { + const { promiseID, resolverID } = syscall.createPromise(); + remote.fromRemote.set(s, promiseID); + remote.toRemote.set(promiseID, s); + state.promiseTable.set(promiseID, { + owner: remoteID, + resolved: false, + decider: remoteID, + subscriber: null, + resolverID, // temporary + }); + console.log( + `inbound promise ${s} mapped to ${promiseID}/${resolverID}`, + ); + } + } else if (type === 'resolver') { + throw new Error(`resolvers not implemented yet`); + } else { + throw new Error(`unknown type ${type}`); + } + } + return remote.fromRemote.get(s); +} + +export function getInbound(state, remoteID, target) { + const remote = getRemote(state, remoteID); + if (!remote.fromRemote.has(target)) { + throw new Error( + `target ${target} must already be in ${remote.remoteID} (${remote.name})`, + ); + } + return remote.fromRemote.get(target); +} + +export function addEgress(state, remoteID, remoteRefID, localRef) { + // Make 'localRef' available to remoteID as 'remoteRefID'. This is kind of + // like mapOutbound, but it uses a caller-provided remoteRef instead of + // allocating a new one. This is used to bootstrap initial connectivity + // between machines. + const remote = getRemote(state, remoteID); + Nat(remoteRefID); + insistVatType('object', localRef); + const { allocatedByVat } = parseVatSlot(localRef); + insist(!allocatedByVat, `localRef should be kernel-allocated`); + insist(!remote.toRemote.has(localRef), `already present ${localRef}`); + + const inboundRemoteRef = makeRemoteSlot('object', true, remoteRefID); + const outboundRemoteRef = flipRemoteSlot(inboundRemoteRef); + insist( + !remote.fromRemote.has(inboundRemoteRef), + `already present ${inboundRemoteRef}`, + ); + insist( + !remote.toRemote.has(outboundRemoteRef), + `already present ${outboundRemoteRef}`, + ); + remote.fromRemote.set(inboundRemoteRef, localRef); + remote.toRemote.set(localRef, outboundRemoteRef); + if (remote.nextObjectIndex <= remoteRefID) { + remote.nextObjectIndex = remoteRefID + 1; + } +} + +// to let machine2 access 'o-5' on machine1, pick an unused index (12), then: +// * machine1 does addEgress(state, 'machine2', 12, 'o-5') +// * machine2 does addIngress(state, 'machine1', 12, 'o+8') +// Messages sent to the object: +// * machine2 toRemote[o+8] = ro+12 +// * machine1 fromRemote[ro+12] = o-5 +// Messages sent from m1 to m2 that reference the object: +// * machine1 toRemote[o-5] = ro-12 +// * machine2 fromRemote[ro-12] = o+8 + +export function addIngress(state, remoteID, remoteRefID) { + // Return a localRef that maps to 'remoteRef' at remoteRefID. Just a + // wrapper around mapInbound. + const inboundRemoteRef = makeRemoteSlot('object', false, remoteRefID); + const localRef = mapInbound( + state, + remoteID, + inboundRemoteRef, + 'fake syscall unused', + ); + return localRef; +} diff --git a/src/vats/comms/controller.js b/src/vats/comms/controller.js new file mode 100644 index 00000000000..bec6c010fad --- /dev/null +++ b/src/vats/comms/controller.js @@ -0,0 +1,77 @@ +import Nat from '@agoric/nat'; +import { addRemote } from './remote'; +import { addEgress, addIngress } from './clist'; +import { insist } from '../../kernel/insist'; + +const UNDEFINED = JSON.stringify({ '@qclass': 'undefined' }); + +// deliverToController() is used for local vats which want to talk to us as a +// vat, rather than as a conduit to talk to remote vats. The bootstrap +// function can use this to invoid our addRemote() and connect us with a +// transport layer (the 'vattp' vat). This is a little awkward, because we +// need the demarshalling and promise-resolution tooling that liveSlots.js +// usually provides, but we avoid liveSlots here because the dominant use +// case (deliverFromRemote and deliverToRemote) don't need it. So we have to +// reconstruct a little of it manually. + +export function deliverToController( + state, + method, + data, + slots, + resolverID, + syscall, +) { + function doAddRemote(args) { + const name = args[0]; + insist(name === `${name}`, `bad addRemote name ${name}`); + if (args[1]['@qclass'] !== 'slot' || args[1].index !== 0) { + throw new Error(`unexpected args for addRemote(): ${data}`); + } + const transmitterID = slots[args[1].index]; + const { receiverID } = addRemote(state, name, transmitterID); + syscall.fulfillToPresence(resolverID, receiverID); + } + + function doAddEgress(args) { + const remoteName = args[0]; + insist(state.names.has(remoteName), `unknown remote name ${remoteName}`); + const remoteID = state.names.get(remoteName); + const remoteRefID = Nat(args[1]); + if (args[2]['@qclass'] !== 'slot' || args[2].index !== 0) { + throw new Error(`unexpected args for addEgress(): ${data}`); + } + const localRef = slots[args[2].index]; + addEgress(state, remoteID, remoteRefID, localRef); + syscall.fulfillToData(resolverID, UNDEFINED, []); + } + + function doAddIngress(args) { + const remoteName = args[0]; + insist(state.names.has(remoteName), `unknown remote name ${remoteName}`); + const remoteID = state.names.get(remoteName); + const remoteRefID = Nat(args[1]); + const localRef = addIngress(state, remoteID, remoteRefID); + syscall.fulfillToPresence(resolverID, localRef); + } + + // This is a degenerate form of deserialization, just enough to handle the + // handful of methods implemented by the commsController. 'argsbytes' can + // normally have arbitrary {'@qclass':'slot', index} objects, which point + // into the 'slots' array. The only method that expects one is init(), and + // it always expects it in args[2], so we manually translate it here. + const { args } = JSON.parse(data); + + // translate args that are slots to the slot rather than qclass + + switch (method) { + case 'addRemote': + return doAddRemote(args); + case 'addEgress': + return doAddEgress(args); + case 'addIngress': + return doAddIngress(args); + default: + throw new Error(`method ${method} is not available`); + } +} diff --git a/src/vats/comms/dispatch.js b/src/vats/comms/dispatch.js new file mode 100644 index 00000000000..0eaeb779c92 --- /dev/null +++ b/src/vats/comms/dispatch.js @@ -0,0 +1,121 @@ +import harden from '@agoric/harden'; +import { makeVatSlot } from '../parseVatSlots'; +import { getRemote } from './remote'; +import { makeState } from './state'; +import { deliverToRemote, resolvePromiseToRemote } from './outbound'; +import { deliverFromRemote } from './inbound'; +import { deliverToController } from './controller'; +import { insist } from '../../kernel/insist'; + +function transmit(syscall, state, remoteID, msg) { + const remote = getRemote(state, remoteID); + // the vat-tp "integrity layer" is a regular vat, so it expects an argument + // encoded as JSON + const body = JSON.stringify({ args: [msg] }); + syscall.send(remote.transmitterID, 'transmit', body, []); // todo: sendOnly +} + +export const debugState = new WeakMap(); + +export function buildCommsDispatch(syscall, _state, _helpers) { + // TODO: state.activate(), put this data on state.stuff instead of closing + // over a local object + const state = makeState(); + + // our root object (o+0) is the Comms Controller + const controller = makeVatSlot('object', true, 0); + + function deliver(target, method, argsbytes, caps, resolverID) { + if (target === controller) { + return deliverToController( + state, + method, + argsbytes, + caps, + resolverID, + syscall, + ); + } + if (state.objectTable.has(target)) { + insist( + method.indexOf(':') === -1 && method.indexOf(';') === -1, + `illegal method name ${method}`, + ); + const [remoteID, body] = deliverToRemote( + syscall, + state, + target, + method, + argsbytes, + caps, + resolverID, + ); + return transmit(syscall, state, remoteID, body); + } + if (state.remoteReceivers.has(target)) { + insist(method === 'receive', `unexpected method ${method}`); + // the vat-tp integrity layer is a regular vat, so when they send the + // received message to us, it will be embedded in a JSON array + const message = JSON.parse(argsbytes).args[0]; + return deliverFromRemote( + syscall, + state, + state.remoteReceivers.get(target), + message, + ); + } + // TODO: if (target in PromiseTable) : pipelining + throw new Error(`unknown target ${target}`); + } + + function notifyFulfillToData(promiseID, data, slots) { + // if (promiseID in localPromises) { + // resolveLocal(promiseID, { type: 'data', data, slots }); + // } + // console.log(`notifyFulfillToData ${promiseID}`); + const [remoteID, body] = resolvePromiseToRemote(syscall, state, promiseID, { + type: 'data', + data, + slots, + }); + if (remoteID) { + return transmit(syscall, state, remoteID, body); + } + throw new Error(`unknown promise ${promiseID}`); + } + + function notifyFulfillToPresence(promiseID, slot) { + // console.log(`notifyFulfillToPresence ${promiseID}`); + const [remoteID, body] = resolvePromiseToRemote(syscall, state, promiseID, { + type: 'object', + slot, + }); + if (remoteID) { + return transmit(syscall, state, remoteID, body); + } + throw new Error(`unknown promise ${promiseID}`); + } + + function notifyReject(promiseID, data, slots) { + // console.log(`notifyReject ${promiseID}`); + const [remoteID, body] = resolvePromiseToRemote(syscall, state, promiseID, { + type: 'reject', + data, + slots, + }); + if (remoteID) { + return transmit(syscall, state, remoteID, body); + } + throw new Error(`unknown promise ${promiseID}`); + } + + const dispatch = harden({ + deliver, + notifyFulfillToData, + notifyFulfillToPresence, + notifyReject, + }); + debugState.set(dispatch, state); + + return dispatch; +} diff --git a/src/vats/comms/inbound.js b/src/vats/comms/inbound.js new file mode 100644 index 00000000000..b0a7f7173ac --- /dev/null +++ b/src/vats/comms/inbound.js @@ -0,0 +1,90 @@ +import { getRemote } from './remote'; +import { + flipRemoteSlot, + insistRemoteType, + parseRemoteSlot, +} from './parseRemoteSlot'; +import { mapInbound, getInbound } from './clist'; +import { insist } from '../../kernel/insist'; + +export function deliverFromRemote(syscall, state, remoteID, message) { + const remote = getRemote(state, remoteID); + const command = message.split(':', 1)[0]; + if (command === 'deliver') { + // deliver:$target:$method:[$result][:$slots..];body + const sci = message.indexOf(';'); + insist(sci !== -1, `missing semicolon in deliver ${message}`); + const slots = message + .slice(0, sci) + .split(':') + .slice(1); + // slots: [$target, $method, $result, $slots..] + const target = getInbound(state, remoteID, slots[0]); + const method = slots[1]; + const result = slots[2]; // 'rp-NN' or empty string + const msgSlots = slots + .slice(3) + .map(s => mapInbound(state, remoteID, s, syscall)); + const body = message.slice(sci + 1); + // todo: sendOnly if (!result.length) + const p = syscall.send(target, method, body, msgSlots); + syscall.subscribe(p); + // console.log(`-- deliverFromRemote, result=${result} local p=${p}`); + // for now p is p-NN, later we will allocate+provide p+NN instead + if (result.length) { + insistRemoteType('promise', result); + insist(!parseRemoteSlot(result).allocatedByRecipient, result); + state.promiseTable.set(p, { + owner: remoteID, + resolved: false, + decider: null, + subscriber: remoteID, + }); + remote.fromRemote.set(result, p); + remote.toRemote.set(p, flipRemoteSlot(result)); + } + // dumpState(state); + } else if (command === 'resolve') { + // console.log(`-- deliverFromRemote.resolve, ${message}, pre-state is:`); + // dumpState(state); + const sci = message.indexOf(';'); + insist(sci !== -1, `missing semicolon in resolve ${message}`); + // message is created by resolvePromiseToRemote, so one of: + // `resolve:object:${target}:${resolutionRef};` + // `resolve:data:${target}${rmss};${resolution.data}` + // `resolve:reject:${target}${rmss};${resolution.data}` + + const pieces = message.slice(0, sci).split(':'); + // pieces[0] is 'resolve' + const type = pieces[1]; + const remoteTarget = pieces[2]; + const remoteSlots = pieces.slice(3); // length=1 for resolve:object + insistRemoteType('promise', remoteTarget); // slots[0] is 'rp+NN`. + const target = getInbound(state, remoteID, remoteTarget); + // rp+NN maps to target=p-+NN and we look at the promiseTable to make + // sure it's in the right state. + const p = state.promiseTable.get(target); + insist(p, `${target} is not in the promiseTable`); + insist(!p.resolved, `${target} is already resolved`); + insist( + p.decider === remoteID, + `${p.decider} is the decider of ${target}, not ${remoteID}`, + ); + const { resolverID } = p; + const slots = remoteSlots.map(s => mapInbound(state, remoteID, s, syscall)); + const body = message.slice(sci + 1); + if (type === 'object') { + syscall.fulfillToPresence(resolverID, slots[0]); + } else if (type === 'data') { + syscall.fulfillToData(resolverID, body, slots); + } else if (type === 'reject') { + syscall.reject(resolverID, body, slots); + } else { + throw new Error(`unknown resolution type ${type} in ${message}`); + } + } else { + throw new Error( + `unrecognized command ${command} in received message ${message}`, + ); + } +} diff --git a/src/vats/comms/index.js b/src/vats/comms/index.js new file mode 100644 index 00000000000..eb19b6da7d8 --- /dev/null +++ b/src/vats/comms/index.js @@ -0,0 +1,3 @@ +import { buildCommsDispatch } from './dispatch'; + +export default buildCommsDispatch; diff --git a/src/vats/comms/outbound.js b/src/vats/comms/outbound.js new file mode 100644 index 00000000000..bc4033e76a1 --- /dev/null +++ b/src/vats/comms/outbound.js @@ -0,0 +1,109 @@ +import { makeVatSlot, insistVatType } from '../parseVatSlots'; +import { + flipRemoteSlot, + insistRemoteType, + makeRemoteSlot, +} from './parseRemoteSlot'; +import { getOutbound, mapOutbound } from './clist'; +import { allocatePromiseIndex } from './state'; +import { getRemote, insistRemoteID } from './remote'; +import { insist } from '../../kernel/insist'; + +export function deliverToRemote( + syscall, + state, + target, + method, + data, + slots, + resolverID, +) { + // this object lives on 'remoteID', so we send messages at them + const remoteID = state.objectTable.get(target); + insist(remoteID !== undefined, `oops ${target}`); + const remote = getRemote(state, remoteID); + + const remoteTargetSlot = getOutbound(state, remoteID, target); + const remoteMessageSlots = slots.map(s => + mapOutbound(state, remoteID, s, syscall), + ); + let rmss = remoteMessageSlots.join(':'); + if (rmss) { + rmss = `:${rmss}`; + } + let remoteResultSlot = ''; + if (resolverID) { + insistVatType('resolver', resolverID); + // outbound: resolverID=r-NN -> rp-NN + // inbound: rp+NN -> r-NN + const pIndex = allocatePromiseIndex(state); + const p = makeVatSlot('promise', true, pIndex); // p+NN + state.promiseTable.set(p, { + owner: null, + resolved: false, + decider: remoteID, + subscriber: null, + resolverID, // r-NN, temporary + }); + remoteResultSlot = makeRemoteSlot('promise', false, pIndex); // rp-NN + remote.toRemote.set(p, remoteResultSlot); // p+NN -> rp-NN + remote.fromRemote.set(flipRemoteSlot(remoteResultSlot), p); // rp+NN -> p+NN + } + + // now render the transmission. todo: 'method' lives in the transmission + // for now, but will be moved to 'data' + const msg = `deliver:${remoteTargetSlot}:${method}:${remoteResultSlot}${rmss};${data}`; + // console.log(`deliverToRemote(target=${target}/${remoteTargetSlot}, result=${resolverID}/${remoteResultSlot}) leaving state as:`); + // dumpState(state); + return [remoteID, msg]; +} + +export function resolvePromiseToRemote(syscall, state, promiseID, resolution) { + // console.log(`resolvePromiseToRemote ${promiseID}`, resolution); + insistVatType('promise', promiseID); + const p = state.promiseTable.get(promiseID); + if (!p || !p.subscriber) { + return [undefined, undefined]; // todo: local promise? + } + insist(!p.resolved, `${promiseID} is already resolved`); + insist(!p.decider, `${p.decider} is the decider for ${promiseID}, not me`); + const remoteID = p.subscriber; + insistRemoteID(remoteID); + // for now, promiseID = p-NN, later will be p+NN + const target = getOutbound(state, remoteID, promiseID); + // target should be rp+NN + insistRemoteType('promise', target); + // insist(parseRemoteSlot(target).allocatedByRecipient, target); // rp+NN for them + function mapSlots() { + const { slots } = resolution; + const rms = slots.map(s => mapOutbound(state, remoteID, s, syscall)); + let rmss = rms.join(':'); + if (rmss) { + rmss = `:${rmss}`; + } + return rmss; + } + + let msg; + if (resolution.type === 'object') { + const resolutionRef = mapOutbound( + state, + remoteID, + resolution.slot, + syscall, + ); + msg = `resolve:object:${target}:${resolutionRef};`; + } else if (resolution.type === 'data') { + const rmss = mapSlots(); + msg = `resolve:data:${target}${rmss};${resolution.data}`; + } else if (resolution.type === 'reject') { + const rmss = mapSlots(); + msg = `resolve:reject:${target}${rmss};${resolution.data}`; + } else { + throw new Error(`unknown resolution type ${resolution.type}`); + } + p.resolved = true; + p.decider = undefined; + p.subscriber = undefined; + return [remoteID, msg]; +} diff --git a/src/vats/comms/parseRemoteSlot.js b/src/vats/comms/parseRemoteSlot.js new file mode 100644 index 00000000000..0038d28ee58 --- /dev/null +++ b/src/vats/comms/parseRemoteSlot.js @@ -0,0 +1,76 @@ +import Nat from '@agoric/nat'; +import { insist } from '../../kernel/insist'; + +// Object/promise references (in remote messages) contain a three-tuple of +// (type, allocator flag, index). The allocator flag inside an inbound +// message is "+" when the index was allocated by the recipient of that +// message, and "-" when allocated by the sender of the message. + +export function parseRemoteSlot(s) { + insist(s === `${s}`); + let type; + let allocatedByRecipient; + const typechars = s.slice(0, 2); + const allocchar = s[2]; + const indexSuffix = s.slice(3); + + if (typechars === 'ro') { + type = 'object'; + } else if (typechars === 'rp') { + type = 'promise'; + } else if (typechars === 'rr') { + type = 'resolver'; + } else { + throw new Error(`invalid remoteSlot ${s}`); + } + + if (allocchar === '+') { + allocatedByRecipient = true; + } else if (allocchar === '-') { + allocatedByRecipient = false; + } else { + throw new Error(`invalid remoteSlot ${s}`); + } + + const id = Nat(Number(indexSuffix)); + return { type, allocatedByRecipient, id }; +} + +export function makeRemoteSlot(type, allocatedByRecipient, id) { + let indexSuffix; + if (allocatedByRecipient) { + indexSuffix = `+${Nat(id)}`; + } else { + indexSuffix = `-${Nat(id)}`; + } + + if (type === 'object') { + return `ro${indexSuffix}`; + } + if (type === 'promise') { + return `rp${indexSuffix}`; + } + if (type === 'resolver') { + return `rr${indexSuffix}`; + } + throw new Error(`unknown type ${type}`); +} + +export function insistRemoteType(type, remoteSlot) { + insist( + type === parseRemoteSlot(remoteSlot).type, + `remoteSlot ${remoteSlot} is not of type ${type}`, + ); +} + +// The clist for each remote-machine has two sides: fromRemote (used to +// parse inbound messages arriving from a remote machine) and toRemote (used +// to create outbound messages). The keys of the fromRemote table will have +// the opposite allocator flag as the corresponding value of the toRemote +// table. The only time we must reverse the polarity of the flag is when we +// add a new entry to the clist. + +export function flipRemoteSlot(remoteSlot) { + const { type, allocatedByRecipient, id } = parseRemoteSlot(remoteSlot); + return makeRemoteSlot(type, !allocatedByRecipient, id); +} diff --git a/src/vats/comms/remote.js b/src/vats/comms/remote.js new file mode 100644 index 00000000000..9432e0f3ded --- /dev/null +++ b/src/vats/comms/remote.js @@ -0,0 +1,62 @@ +import Nat from '@agoric/nat'; +import { makeVatSlot, insistVatType } from '../parseVatSlots'; +import { insist } from '../../kernel/insist'; + +function makeRemoteID(index) { + return `remote${Nat(index)}`; +} + +export function insistRemoteID(remoteID) { + insist(remoteID.startsWith('remote'), `not a remoteID: ${remoteID}`); +} + +export function getRemote(state, remoteID) { + insistRemoteID(remoteID); + const remote = state.remotes.get(remoteID); + insist(remote !== undefined, `oops ${remoteID}`); + return remote; +} + +export function addRemote(state, name, transmitterID) { + insistVatType('object', transmitterID); + insist(!state.names.has(name), `remote name ${name} already in use`); + + const remoteID = makeRemoteID(state.nextRemoteIndex); + state.nextRemoteIndex += 1; + + // The keys of the fromRemote table will have the opposite allocator flag + // as the corresponding value of the toRemote table. The only time we must + // reverse the polarity of the flag is when we add a new entry to the + // clist. + + // fromRemote has: + // ro-NN -> o+NN (imported/importing from remote machine) + // ro+NN -> o-NN (previously exported to remote machine) + const fromRemote = new Map(); // {ro/rp/rr+-NN} -> o+-NN/p+-NN/r+-NN + + // toRemote has: + // o+NN -> ro+NN (previously imported from remote machine) + // o-NN -> ro-NN (exported/exporting to remote machine) + const toRemote = new Map(); // o/p/r+-NN -> ro/rp/rr+-NN + + state.remotes.set(remoteID, { + remoteID, + name, + fromRemote, + toRemote, + nextObjectIndex: 20, + nextResolverIndex: 30, + nextPromiseIndex: 40, + transmitterID, + }); + state.names.set(name, remoteID); + + // inbound messages will be directed at this exported object + const receiverID = makeVatSlot('object', true, state.nextObjectIndex); + state.nextObjectIndex += 1; + // remoteReceivers are our vat objects to which the transport layer will + // send incoming messages. Each remote machine is assigned a separate + // receiver object. + state.remoteReceivers.set(receiverID, remoteID); + return { remoteID, receiverID }; +} diff --git a/src/vats/comms/state.js b/src/vats/comms/state.js new file mode 100644 index 00000000000..561441d5e1a --- /dev/null +++ b/src/vats/comms/state.js @@ -0,0 +1,51 @@ +export function makeState() { + const state = { + nextRemoteIndex: 1, + remotes: new Map(), // remoteNN -> { remoteID, name, fromRemote/toRemote, etc } + names: new Map(), // name -> remoteNN + + nextObjectIndex: 10, + remoteReceivers: new Map(), // o+NN -> remoteNN + objectTable: new Map(), // o+NN -> owning remote + + // hopefully we can avoid the need for local promises + // localPromises: new Map(), // p+NN/p-NN -> local purpose + promiseTable: new Map(), // p+NN/p-NN -> { owner, resolved, decider, subscriber } + nextPromiseIndex: 20, + }; + + return state; // mutable +} + +export function dumpState(state) { + console.log(`Object Table:`); + for (const id of state.objectTable.keys()) { + console.log(`${id} : owner=${state.objectTable.get(id)}`); + } + console.log(); + + console.log(`Promise Table:`); + for (const id of state.promiseTable.keys()) { + const p = state.promiseTable.get(id); + console.log( + `${id} : owner=${p.owner}, resolved=${p.resolved}, decider=${p.decider}, sub=${p.subscriber}`, + ); + } + console.log(); + + for (const remoteID of state.remotes.keys()) { + const r = state.remotes.get(remoteID); + console.log(`${remoteID} '${r.name}':`); + for (const inbound of r.fromRemote.keys()) { + const id = r.fromRemote.get(inbound); + const outbound = r.toRemote.get(id); + console.log(` ${inbound} -> ${id} -> ${outbound}`); + } + } +} + +export function allocatePromiseIndex(state) { + const index = state.nextPromiseIndex; + state.nextPromiseIndex += 1; + return index; +} diff --git a/src/vats/parseVatSlots.js b/src/vats/parseVatSlots.js new file mode 100644 index 00000000000..6024028273c --- /dev/null +++ b/src/vats/parseVatSlots.js @@ -0,0 +1,71 @@ +import Nat from '@agoric/nat'; +import { insist } from '../kernel/insist'; + +// Object/promise references (in vats) contain a three-tuple of (type, +// allocator flag, index). The 'ownership' flag is expressed as a sign: "-" +// means the index was allocated by the kernel, and thus the actual object +// lives in some other vat (and any messages sent to it must go into the +// kernel). "+" means the index was allocated by this vat, hence the object +// lives in this vat, and we can expect to receive messages for it. + +export function parseVatSlot(s) { + insist(s === `${s}`); + let type; + let allocatedByVat; + const typechar = s[0]; + const allocchar = s[1]; + const idSuffix = s.slice(2); + + if (typechar === 'o') { + type = 'object'; + } else if (typechar === 'd') { + type = 'device'; + } else if (typechar === 'p') { + type = 'promise'; + } else if (typechar === 'r') { + type = 'resolver'; + } else { + throw new Error(`invalid vatSlot ${s}`); + } + + if (allocchar === '+') { + allocatedByVat = true; + } else if (allocchar === '-') { + allocatedByVat = false; + } else { + throw new Error(`invalid vatSlot ${s}`); + } + + const id = Nat(Number(idSuffix)); + return { type, allocatedByVat, id }; +} + +export function makeVatSlot(type, allocatedByVat, id) { + let idSuffix; + if (allocatedByVat) { + idSuffix = `+${Nat(id)}`; + } else { + idSuffix = `-${Nat(id)}`; + } + + if (type === 'object') { + return `o${idSuffix}`; + } + if (type === 'device') { + return `d${idSuffix}`; + } + if (type === 'promise') { + return `p${idSuffix}`; + } + if (type === 'resolver') { + return `r${idSuffix}`; + } + throw new Error(`unknown type ${type}`); +} + +export function insistVatType(type, vatSlot) { + insist( + type === parseVatSlot(vatSlot).type, + `vatSlot ${vatSlot} is not of type ${type}`, + ); +} diff --git a/src/vat-tp/bootstrap.js b/src/vats/vat-tp/bootstrap.js similarity index 50% rename from src/vat-tp/bootstrap.js rename to src/vats/vat-tp/bootstrap.js index ef0f296ed46..246a116b1a4 100644 --- a/src/vat-tp/bootstrap.js +++ b/src/vats/vat-tp/bootstrap.js @@ -2,8 +2,11 @@ // bootstrap vat to glue together the three comms pieces (mailbox device, // VatTP vat, comms vat). -export function doVatTPBootstrap(D, E, vats, devices) { +export async function doVatTPBootstrap(D, E, vats, devices) { D(devices.mailbox).registerInboundHandler(vats.vattp); E(vats.vattp).registerMailboxDevice(devices.mailbox); - E(vats.comms).init(vats.vattp); + const name = 'remote1'; + const { transmitter, setReceiver } = await E(vats.vattp).addRemote(name); + const receiver = await E(vats.comms).addRemote(name, transmitter); + await E(setReceiver).setReceiver(receiver); } diff --git a/src/vats/vat-tp/vattp.js b/src/vats/vat-tp/vattp.js new file mode 100644 index 00000000000..bb33837e9a2 --- /dev/null +++ b/src/vats/vat-tp/vattp.js @@ -0,0 +1,78 @@ +import harden from '@agoric/harden'; +import { insist } from '../../kernel/insist'; + +function build(E, D) { + let mailbox; // mailbox device + const remotes = new Map(); + // { outbound: { highestRemoved, highestAdded }, + // inbound: { highestDelivered, receiver } } + + function getRemote(name) { + if (!remotes.has(name)) { + remotes.set(name, { + outbound: { highestRemoved: 0, highestAdded: 0 }, + inbound: { highestDelivered: 0, receiver: null }, + }); + } + return remotes.get(name); + } + + const handler = harden({ + registerMailboxDevice(mailboxDevnode) { + mailbox = mailboxDevnode; + }, + + addRemote(name) { + insist(!remotes.has(name), `already have remote ${name}`); + const r = getRemote(name); + const transmitter = harden({ + transmit(msg) { + const o = r.outbound; + const num = o.highestAdded + 1; + // console.log(`transmit to ${name}[${num}]: ${msg}`); + D(mailbox).add(name, num, msg); + o.highestAdded = num; + }, + }); + const setReceiver = harden({ + setReceiver(newReceiver) { + if (r.inbound.receiver) { + throw new Error(`setReceiver is call-once`); + } + r.inbound.receiver = newReceiver; + }, + }); + return harden({ transmitter, setReceiver }); + }, + + deliverInboundMessages(name, newMessages) { + const i = getRemote(name).inbound; + newMessages.forEach(m => { + const [num, body] = m; + if (num > i.highestDelivered) { + // TODO: SO() / sendOnly() + // console.log(`receive from ${name}[${num}]: ${body}`); + E(i.receiver).receive(body); + i.highestDelivered = num; + D(mailbox).ackInbound(name, num); + } + }); + }, + + deliverInboundAck(name, ack) { + const o = getRemote(name).outbound; + let num = o.highestRemoved + 1; + while (num <= o.highestAdded && num <= ack) { + D(mailbox).remove(name, num); + o.highestRemoved = num; + num += 1; + } + }, + }); + + return handler; +} + +export default function setup(syscall, state, helpers) { + return helpers.makeLiveSlots(syscall, state, build, helpers.vatID); +} diff --git a/test/basedir-commsvat/bootstrap.js b/test/basedir-commsvat/bootstrap.js index e6ee09cc472..f3e7be8534a 100644 --- a/test/basedir-commsvat/bootstrap.js +++ b/test/basedir-commsvat/bootstrap.js @@ -19,21 +19,39 @@ export default function setup(syscall, state, helpers) { // setup const LEFT = 'left'; const RIGHT = 'right'; - const INDEX_FOR_RIGHT_INITIAL_OBJ = 0; + const RIGHT_OBJECT_INDEX = 12; D(devices.loopbox).registerInboundHandler(LEFT, vats.leftvattp); const leftsender = D(devices.loopbox).makeSender(LEFT); await E(vats.leftvattp).registerMailboxDevice(leftsender); - await E(vats.leftcomms).init(vats.leftvattp); + + const { + transmitter: txToRightForLeft, + setReceiver: setRxFromRightForLeft, + } = await E(vats.leftvattp).addRemote(RIGHT); + const rxFromRightForLeft = await E(vats.leftcomms).addRemote( + RIGHT, + txToRightForLeft, + ); + await E(setRxFromRightForLeft).setReceiver(rxFromRightForLeft); D(devices.loopbox).registerInboundHandler(RIGHT, vats.rightvattp); const rightsender = D(devices.loopbox).makeSender(RIGHT); await E(vats.rightvattp).registerMailboxDevice(rightsender); - await E(vats.rightcomms).init(vats.rightvattp); + + const { + transmitter: txToLeftForRight, + setReceiver: setRxFromLeftForRight, + } = await E(vats.rightvattp).addRemote(LEFT); + const rxFromLeftForRight = await E(vats.rightcomms).addRemote( + LEFT, + txToLeftForRight, + ); + await E(setRxFromLeftForRight).setReceiver(rxFromLeftForRight); await E(vats.rightcomms).addEgress( LEFT, - INDEX_FOR_RIGHT_INITIAL_OBJ, + RIGHT_OBJECT_INDEX, vats.right, ); @@ -42,7 +60,7 @@ export default function setup(syscall, state, helpers) { // but the leftcomms needs to export it to the kernel const rootRightPresence = await E(vats.leftcomms).addIngress( RIGHT, - INDEX_FOR_RIGHT_INITIAL_OBJ, + RIGHT_OBJECT_INDEX, ); // run tests diff --git a/test/basedir-commsvat/vat-left.js b/test/basedir-commsvat/vat-left.js index 24233ce6b57..60e44f240b3 100644 --- a/test/basedir-commsvat/vat-left.js +++ b/test/basedir-commsvat/vat-left.js @@ -47,8 +47,7 @@ export default function setup(syscall, state, helpers) { case 'left does: E(right.0).method(left.1) => returnData': { const rightRootPresence = args[0]; - const leftRootPresence = args[1]; - const leftNewObjPresence = await E(leftRootPresence).createNewObj(); + const leftNewObjPresence = createNewObj(); E(rightRootPresence) .methodWithPresence(leftNewObjPresence) .then(r => log(`=> left vat receives the returnedData: ${r}`)); @@ -57,8 +56,7 @@ export default function setup(syscall, state, helpers) { case 'left does: E(right.0).method(left.1) => returnData twice': { const rightRootPresence = args[0]; - const leftRootPresence = args[1]; - const leftNewObjPresence = await E(leftRootPresence).createNewObj(); + const leftNewObjPresence = createNewObj(); // first time E(rightRootPresence) @@ -110,6 +108,7 @@ export default function setup(syscall, state, helpers) { const rightRootPresence = args[0]; const result = E(rightRootPresence).methodReturnsPromise(); log(`=> left vat receives the returnedPromise: ${result}`); + E(rightRootPresence).resolveToFoo(); result.then(r => log(`=> returnedPromise.then: ${r}`)); break; } @@ -120,6 +119,7 @@ export default function setup(syscall, state, helpers) { rightRootPresence, ).methodReturnsPromiseForRightPresence(); log(`=> left vat receives the returnedPromise: ${result}`); + E(rightRootPresence).resolveToNewObj(); result.then(async r => { log(`=> returnedPromise.then: ${r}`); // call method on presence to confirm expected presence @@ -134,8 +134,9 @@ export default function setup(syscall, state, helpers) { const leftPresence = createNewObj(); const result = E( rightRootPresence, - ).methodReturnsPromiseForLeftPresence(leftPresence); + ).methodReturnsPromiseForLeftPresence(); log(`=> left vat receives the returnedPromise: ${result}`); + E(rightRootPresence).resolveToLeftPresence(leftPresence); result.then(async r => { log(`=> returnedPromise.then: ${r}`); // call method on presence to confirm expected presence @@ -147,8 +148,10 @@ export default function setup(syscall, state, helpers) { case 'left does: E(right.0).method() => right.promise => reject': { const rightRootPresence = args[0]; + const p = E(rightRootPresence).methodReturnsPromiseReject(); + E(rightRootPresence).rejectThatPromise(); try { - await E(rightRootPresence).methodReturnsPromiseReject(); + await p; } catch (err) { log( `=> left vat receives the rejected promise with error ${err}`, @@ -168,12 +171,23 @@ export default function setup(syscall, state, helpers) { break; } - case 'left does: E(right.0).method(right.promise) => returnData': { + case 'left does: E(right.0).method(right.promise) => returnData 1': { const rightRootPresence = args[0]; const rpromise = E(rightRootPresence).methodReturnsPromise(); - E(rightRootPresence) - .methodWithPromise(rpromise) - .then(r => log(`=> left vat receives the returnedData: ${r}`)); + const p = E(rightRootPresence).methodWithPromise(rpromise); + E(rightRootPresence).resolveToFoo(); + p.then(r => log(`=> left vat receives the returnedData: ${r}`)); + break; + } + + case 'left does: E(right.0).method(right.promise) => returnData 2': { + // test resolving the promise before sending it + // TODO: I'm not convinced this is working yet. + const rightRootPresence = args[0]; + const rpromise = E(rightRootPresence).methodReturnsPromise(); + E(rightRootPresence).resolveToFoo(); + const p = E(rightRootPresence).methodWithPromise(rpromise); + p.then(r => log(`=> left vat receives the returnedData: ${r}`)); break; } @@ -182,6 +196,7 @@ export default function setup(syscall, state, helpers) { const rPromisePresence = E( rightRootPresence, ).methodReturnsPromiseForRightPresence(); + E(rightRootPresence).resolveToNewObj(); E(rightRootPresence) .methodOnPromiseForPresence(rPromisePresence) .then(r => log(`=> left vat receives the returnedData: ${r}`)); @@ -193,7 +208,8 @@ export default function setup(syscall, state, helpers) { const leftPresence = createNewObj(); const lPromisePresence = E( rightRootPresence, - ).methodReturnsPromiseForLeftPresence(leftPresence); + ).methodReturnsPromiseForLeftPresence(); + E(rightRootPresence).resolveToLeftPresence(leftPresence); E(rightRootPresence) .methodOnPromiseForPresence(lPromisePresence) .then(r => log(`=> left vat receives the returnedData: ${r}`)); @@ -207,7 +223,6 @@ export default function setup(syscall, state, helpers) { return harden({ startTest, - createNewObj, }); }, helpers.vatID, diff --git a/test/basedir-commsvat/vat-leftcomms.js b/test/basedir-commsvat/vat-leftcomms.js index ef4a86aad38..ee58e3d31eb 100644 --- a/test/basedir-commsvat/vat-leftcomms.js +++ b/test/basedir-commsvat/vat-leftcomms.js @@ -1,3 +1,5 @@ +import buildCommsDispatch from '../../src/vats/comms'; + export default function setup(syscall, state, helpers) { - return helpers.makeCommsSlots(syscall, state, helpers); + return buildCommsDispatch(syscall, state, helpers); } diff --git a/test/basedir-commsvat/vat-leftvattp.js b/test/basedir-commsvat/vat-leftvattp.js index b215edb7ee0..11419b6c79b 100644 --- a/test/basedir-commsvat/vat-leftvattp.js +++ b/test/basedir-commsvat/vat-leftvattp.js @@ -1,3 +1,3 @@ -import setup from '../../src/vat-tp/vattp'; +import setup from '../../src/vats/vat-tp/vattp'; export default setup; diff --git a/test/basedir-commsvat/vat-right.js b/test/basedir-commsvat/vat-right.js index d81a0c97ccf..aec659537a0 100644 --- a/test/basedir-commsvat/vat-right.js +++ b/test/basedir-commsvat/vat-right.js @@ -1,7 +1,9 @@ import harden from '@agoric/harden'; +import makePromise from '../../src/kernel/makePromise'; export default function setup(syscall, state, helpers) { function log(what) { + console.log(what); helpers.log(what); } @@ -17,6 +19,25 @@ export default function setup(syscall, state, helpers) { }; } + let stashedPromise; + let stashedResolver; + let stashedRejector; + function createNewPromise() { + const p0 = makePromise(); + stashedPromise = p0.p; + stashedResolver = p0.res; + stashedRejector = p0.rej; + return stashedPromise; + } + + function resolve(what) { + stashedResolver(what); + } + + function reject(what) { + stashedRejector(what); + } + return helpers.makeLiveSlots( syscall, state, @@ -53,25 +74,29 @@ export default function setup(syscall, state, helpers) { }, methodReturnsPromise() { log(`=> right.methodReturnsPromise was invoked`); - return new Promise((resolve, _reject) => { - resolve('foo'); - }); + return createNewPromise(); + }, + resolveToFoo() { + resolve('foo'); }, methodReturnsPromiseForRightPresence() { - return new Promise((resolve, _reject) => { - resolve(this.createNewObj()); - }); + return createNewPromise(); + }, + resolveToNewObj() { + resolve(this.createNewObj()); }, - methodReturnsPromiseForLeftPresence(leftPresence) { - return new Promise((resolve, _reject) => { - resolve(leftPresence); - }); + methodReturnsPromiseForLeftPresence() { + return createNewPromise(); + }, + resolveToLeftPresence(leftPresence) { + resolve(leftPresence); }, methodReturnsPromiseReject() { log(`=> right.methodReturnsPromiseReject was invoked`); - return new Promise((_resolve, reject) => { - reject(new Error('this was rejected')); - }); + return createNewPromise(); + }, + rejectThatPromise() { + reject(new Error('this was rejected')); }, async methodWithPromise(promise) { const promiseResult = await promise; diff --git a/test/basedir-commsvat/vat-rightcomms.js b/test/basedir-commsvat/vat-rightcomms.js index ef4a86aad38..ee58e3d31eb 100644 --- a/test/basedir-commsvat/vat-rightcomms.js +++ b/test/basedir-commsvat/vat-rightcomms.js @@ -1,3 +1,5 @@ +import buildCommsDispatch from '../../src/vats/comms'; + export default function setup(syscall, state, helpers) { - return helpers.makeCommsSlots(syscall, state, helpers); + return buildCommsDispatch(syscall, state, helpers); } diff --git a/test/basedir-commsvat/vat-rightvattp.js b/test/basedir-commsvat/vat-rightvattp.js index b215edb7ee0..11419b6c79b 100644 --- a/test/basedir-commsvat/vat-rightvattp.js +++ b/test/basedir-commsvat/vat-rightvattp.js @@ -1,3 +1,3 @@ -import setup from '../../src/vat-tp/vattp'; +import setup from '../../src/vats/vat-tp/vattp'; export default setup; diff --git a/test/commsSlots-integration/run-test.js b/test/commsSlots-integration/run-test.js deleted file mode 100644 index fb43dfb99c3..00000000000 --- a/test/commsSlots-integration/run-test.js +++ /dev/null @@ -1,73 +0,0 @@ -import path from 'path'; -import { test } from 'tape-promise/tape'; -import testLogs from './test-logs'; -import { buildVatController, loadBasedir } from '../../src/index'; - -export async function runVats(t, withSES, argv) { - const config = await loadBasedir( - // TODO: move basedir-commsvat to ./ , since it isn't used anywhere else - path.resolve(__dirname, '../basedir-commsvat'), - ); - - const ldSrcPath = require.resolve('../../src/devices/loopbox-src'); - config.devices = [['loopbox', ldSrcPath, {}]]; - const c = await buildVatController(config, withSES, argv); - return c; -} - -export function runTest(testStr) { - test(testStr, async t => { - const c = await runVats(t, false, [testStr]); - await c.run(); - const { log } = c.dump(); - t.deepEqual(log, testLogs[testStr]); - t.end(); - }); -} - -export function runTestOnly(testStr) { - test.only(testStr, async t => { - const c = await runVats(t, false, [testStr]); - await c.run(); - const dump = c.dump(); - t.deepEqual(dump.log, testLogs[testStr]); - t.end(); - }); -} - -export function runTestSkip(testStr) { - test.skip(testStr, async t => { - const c = await runVats(t, false, [testStr]); - await c.run(); - const dump = c.dump(); - t.deepEqual(dump.log, testLogs[testStr]); - t.end(); - }); -} - -export function stepTestOnly(testStr) { - test.only(testStr, async t => { - const c = await runVats(t, false, [testStr]); - await c.step(); - await c.step(); - await c.step(); - await c.step(); - await c.step(); - await c.step(); - await c.step(); - await c.step(); - await c.step(); - await c.step(); - await c.step(); - await c.step(); - await c.step(); - await c.step(); - await c.step(); - await c.step(); - await c.step(); - await c.step(); - const dump = c.dump(); - t.deepEqual(dump.log, testLogs[testStr]); - t.end(); - }); -} diff --git a/test/commsSlots-integration/test-commsvats.js b/test/commsSlots-integration/test-commsvats.js deleted file mode 100644 index 96d116f6abe..00000000000 --- a/test/commsSlots-integration/test-commsvats.js +++ /dev/null @@ -1,129 +0,0 @@ -// eslint-disable-next-line no-unused-vars -import { runTest, runTestOnly, stepTestOnly, runTestSkip } from './run-test'; - -/* TABLE OF CONTENTS OF TESTS */ -// left does: E(right.0).method() => returnData -// left does: E(right.0).method(dataArg1) => returnData -// left does: E(right.0).method(right.0) => returnData -// left does: E(right.0).method(left.1) => returnData -// left does: E(right.0).method(left.1) => returnData twice -// left does: E(right.1).method() => returnData -// left does: E(right.0).method() => right.presence -// left does: E(right.0).method() => left.presence -// left does: E(right.0).method() => right.promise => data -// left does: E(right.0).method() => right.promise => right.presence -// left does: E(right.0).method() => right.promise => left.presence -// left does: E(right.0).method() => right.promise => reject -// left does: E(right.0).method(left.promise) => returnData -// left does: E(right.0).method(right.promise) => returnData -// left does: E(right.0).method(right.promise => right.presence) => returnData -// left does: E(right.0).method(right.promise => left.presence) => returnData - -/* TEST: left does: E(right.0).method() => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object and returns data. - */ -runTest('left does: E(right.0).method() => returnData'); - -/* TEST: left does: E(right.0).method(dataArg1) => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object with an argument and returns data connected to that argument. - */ -runTest('left does: E(right.0).method(dataArg1) => returnData'); - -/* TEST: left does: E(right.0).method(right.0) => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object with the right vat's root object as an argument and does a - * method call on the argument. It returns the result of the method - * call on the argument, i.e. right.0.method() => 'called method' - */ -runTest('left does: E(right.0).method(right.0) => returnData'); - -/* TEST: left does: E(right.0).method(left.1) => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's - * object with a new left object as an argument and returns data. - */ -runTest('left does: E(right.0).method(left.1) => returnData'); - -/* TEST: left does: E(right.0).method(left.1) => returnData twice - * DESCRIPTION: The left vat invokes a method on the right vat's - * object with a new left object as an argument and returns data. It - * repeats this a second time. No new egresses/ingresses should be - * allocated the second time. Also, both left.1 args should have the - * same identity. - */ -runTest('left does: E(right.0).method(left.1) => returnData twice'); - -/* TEST: left does: E(right.1).method() => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's - * object (a new object, not the root object) and returns data. - */ -runTest('left does: E(right.1).method() => returnData'); - -/* TEST: left does: E(right.0).method() => right.presence - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object and is given presence that represents a new object on the right - * side - */ -runTest('left does: E(right.0).method() => right.presence'); - -/* TEST: left does: E(right.0).method() => left.presence - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object and is given presence that represents a new object on the left - * side - */ -runTest('left does: E(right.0).method() => left.presence'); - -/* TEST: left does: E(right.0).method() => right.promise => data - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object and is given a promise that resolves to data. - */ -runTest('left does: E(right.0).method() => right.promise => data'); - -/* TEST: left does: E(right.0).method() => right.promise => right.presence - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object and is given a promise that resolves to a presence that - * represents an object on the right side. - */ -runTest('left does: E(right.0).method() => right.promise => right.presence'); - -/* TEST: left does: E(right.0).method() => right.promise => left.presence - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object and is given a promise that resolves to a presence that - * represents an object on the left side. - */ -runTest('left does: E(right.0).method() => right.promise => left.presence'); - -/* TEST: left does: E(right.0).method() => right.promise => reject - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object and is given a promise that is rejected. - */ -runTest('left does: E(right.0).method() => right.promise => reject'); - -/* TEST: left does: E(right.0).method(left.promise) => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object with a promise that the left machine knows about - */ -runTest('left does: E(right.0).method(left.promise) => returnData'); - -/* TEST: left does: E(right.0).method(right.promise) => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object with a promise that the right machine knows about - */ -runTest('left does: E(right.0).method(right.promise) => returnData'); - -/* TEST: left does: E(right.0).method(right.promise => right.presence) => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object with a promise that resolves to a right.presence - */ -runTest( - 'left does: E(right.0).method(right.promise => right.presence) => returnData', -); - -/* TEST: left does: E(right.0).method(right.promise => left.presence) => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object with a promise that resolves to a left.presence - */ -runTest( - 'left does: E(right.0).method(right.promise => left.presence) => returnData', -); diff --git a/test/commsSlots-integration/test-logs.js b/test/commsSlots-integration/test-logs.js deleted file mode 100644 index 62b834fff03..00000000000 --- a/test/commsSlots-integration/test-logs.js +++ /dev/null @@ -1,149 +0,0 @@ -const testLogs = { - 'left does: E(right.0).method() => returnData': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - '=> right.method was invoked', - '=> left vat receives the returnedData: called method', - ], - 'left does: E(right.0).method(dataArg1) => returnData': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - '=> right.methodWithArgs got the arg: hello', - '=> left vat receives the returnedData: hello was received', - ], - 'left does: E(right.0).method(right.0) => returnData': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - '=> right.methodWithPresence got the ref [object Object]', - '=> right.method was invoked', - '=> left vat receives the returnedData: called method', - ], - 'left does: E(right.0).method(left.1) => returnData': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - '=> right.methodWithPresence got the ref [Presence 10]', - '=> left.1.method was invoked', - '=> left vat receives the returnedData: called method', - ], - - 'left does: E(right.0).method(left.1) => returnData twice': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - '=> right.methodWithPresence got the ref [Presence 10]', - 'ref equal each time: true', - '=> right.methodWithPresence got the ref [Presence 10]', - '=> left.1.method was invoked', - '=> left.1.method was invoked', - '=> left vat receives the returnedData: called method', - '=> left vat receives the returnedData: called method', - ], - - 'left does: E(right.1).method() => returnData': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - '=> right.1.method was invoked', - '=> left vat receives the returnedData: called method', - ], - 'left does: E(right.0).method() => right.presence': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - '=> right.1.method was invoked', - '=> left vat receives the returnedData: called method', - ], - 'left does: E(right.0).method() => left.presence': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - '=> left.1.method was invoked', - '=> left vat receives the returnedData: called method', - ], - 'left does: E(right.0).method() => right.promise => data': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - '=> left vat receives the returnedPromise: [object Promise]', - '=> right.methodReturnsPromise was invoked', - '=> returnedPromise.then: foo', - ], - 'left does: E(right.0).method() => right.promise => right.presence': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - '=> left vat receives the returnedPromise: [object Promise]', - '=> returnedPromise.then: [Presence 11]', - '=> right.1.method was invoked', - '=> presence methodCallResult: called method', - ], - - 'left does: E(right.0).method() => right.promise => left.presence': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - '=> left vat receives the returnedPromise: [object Promise]', - '=> returnedPromise.then: [object Object]', - '=> left.1.method was invoked', - '=> presence methodCallResult: called method', - ], - 'left does: E(right.0).method() => right.promise => reject': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - '=> right.methodReturnsPromiseReject was invoked', - '=> left vat receives the rejected promise with error Error: this was rejected', - ], - 'left does: E(right.0).method(left.promise) => returnData': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - 'promise resolves to foo', - '=> left vat receives the returnedData: foo', - ], - 'left does: E(right.0).method(right.promise) => returnData': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - '=> right.methodReturnsPromise was invoked', - 'promise resolves to foo', - '=> left vat receives the returnedData: foo', - ], - 'left does: E(right.0).method(right.promise => right.presence) => returnData': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - '=> right.1.method was invoked', - '=> left vat receives the returnedData: called method', - ], - - 'left does: E(right.0).method(right.promise => left.presence) => returnData': [ - '=> setup called', - '=> bootstrap() called', - 'addEgress called with sender left, index 0, valslot [object Object]', - 'addIngress called with machineName right, index 0', - '=> left.1.method was invoked', - '=> left vat receives the returnedData: called method', - ], -}; - -export default testLogs; diff --git a/test/commsSlots/commsController/test-addEgress.js b/test/commsSlots/commsController/test-addEgress.js deleted file mode 100644 index c126947c880..00000000000 --- a/test/commsSlots/commsController/test-addEgress.js +++ /dev/null @@ -1,60 +0,0 @@ -import { test } from 'tape-promise/tape'; -import handleCommsController from '../../../src/kernel/commsSlots/commsController'; -import makeState from '../../../src/kernel/commsSlots/state'; - -test('handleCommsController addEgress', t => { - let fulfillToDataArgs; - - const mockSyscall = { - fulfillToData(...args) { - fulfillToDataArgs = args; - }, - }; - - const state = makeState(); - - const resolverID = 2; - const sender = 'user'; - const index = 4; - const valslot = { '@qclass': 'slot', index: 0 }; - const caps = [{ type: 'import', id: 10 }]; - const helpers = { - log: console.log, - }; - - const result = handleCommsController( - state, - mockSyscall, - 'addEgress', - JSON.stringify({ - args: [sender, index, valslot], - }), - caps, - resolverID, - helpers, - ); - - t.equal(result, undefined); - - const UNDEFINED = JSON.stringify({ '@qclass': 'undefined' }); - - // ensure calls to syscall are correct - t.deepEqual(fulfillToDataArgs, [resolverID, UNDEFINED, []]); - - // ensure state updated correctly - const youToMeSlot = { - type: 'your-egress', - id: index, - }; - const meToYouSlot = state.clists.changePerspective(youToMeSlot); - const kernelToMeSlot = state.clists.mapIncomingWireMessageToKernelSlot( - sender, - youToMeSlot, - ); - const { - meToYouSlot: actualMeToYouSlot, - } = state.clists.mapKernelSlotToOutgoingWireMessage(caps[0], sender); - t.deepEqual(kernelToMeSlot, caps[0]); // actual, expected - t.deepEqual(actualMeToYouSlot, meToYouSlot); - t.end(); -}); diff --git a/test/commsSlots/commsController/test-addIngress.js b/test/commsSlots/commsController/test-addIngress.js deleted file mode 100644 index 67132c9fd66..00000000000 --- a/test/commsSlots/commsController/test-addIngress.js +++ /dev/null @@ -1,268 +0,0 @@ -import { test } from 'tape-promise/tape'; -import handleCommsController from '../../../src/kernel/commsSlots/commsController'; -import makeState from '../../../src/kernel/commsSlots/state'; - -const helpers = { - log: console.log, -}; - -test('handleCommsController addIngress', t => { - let fulfillToPresenceArgs; - - const mockSyscall = { - fulfillToPresence(...args) { - fulfillToPresenceArgs = args; - }, - }; - - const state = makeState(); - - const resolverID = 2; - const sender = 'bot'; - const index = 8; - - const result = handleCommsController( - state, - mockSyscall, - 'addIngress', - JSON.stringify({ - args: [sender, index], - }), - [{ type: 'your-ingress', id: index }], - resolverID, - helpers, - ); - - t.equal(result, undefined); - - // ensure calls to syscall are correct - t.deepEqual(fulfillToPresenceArgs, [ - resolverID, - { - type: 'export', - id: 1, // first import is 1 - }, - ]); - - // ensure state updated correctly - const expectedYouToMeSlot = { - type: 'your-ingress', - id: index, - }; - const expectedMeToYouSlot = state.clists.changePerspective( - expectedYouToMeSlot, - ); - const kernelToMeSlot = state.clists.mapIncomingWireMessageToKernelSlot( - sender, - expectedYouToMeSlot, - ); - const { - meToYouSlot: actualMeToYouSlot, - } = state.clists.mapKernelSlotToOutgoingWireMessage( - { - type: 'export', - id: 1, - }, - sender, - ); - t.deepEqual(kernelToMeSlot, { type: 'export', id: 1 }); // actual, expected - t.deepEqual(actualMeToYouSlot, expectedMeToYouSlot); - t.end(); -}); - -test('handleCommsController addIngress twice', t => { - let fulfillToPresenceArgs; - - const mockSyscall = { - fulfillToPresence(...args) { - fulfillToPresenceArgs = args; - }, - }; - - const state = makeState(); - - const resolverID = 2; - const sender = 'bot'; - const sender2 = 'user'; - const index = 8; - const index2 = 9; - - const result = handleCommsController( - state, - mockSyscall, - 'addIngress', - JSON.stringify({ - args: [sender, index], - }), - [{ type: 'your-ingress', id: index }], - resolverID, - helpers, - ); - - t.equal(result, undefined); - - // ensure calls to syscall are correct - t.deepEqual(fulfillToPresenceArgs, [ - resolverID, - { - type: 'export', - id: 1, // first import is 1 - }, - ]); - - // ensure state updated correctly - const actualKernelToMeSlot = state.clists.mapIncomingWireMessageToKernelSlot( - sender, - { type: 'your-ingress', id: index }, - ); - const { - meToYouSlot: actualMeToYouSlot, - } = state.clists.mapKernelSlotToOutgoingWireMessage( - { - type: 'export', - id: 1, - }, - sender, - ); - t.deepEqual(actualKernelToMeSlot, { type: 'export', id: 1 }); // actual, expected - t.deepEqual(actualMeToYouSlot, { type: 'your-egress', id: index }); - - const result2 = handleCommsController( - state, - mockSyscall, - 'addIngress', - JSON.stringify({ - args: [sender2, index2], - }), - [{ type: 'your-ingress', id: index2 }], - resolverID, - helpers, - ); - - t.equal(result2, undefined); - - // ensure calls to syscall are correct - t.deepEqual(fulfillToPresenceArgs, [ - resolverID, - { - type: 'export', - id: 2, - }, - ]); - - // ensure state updated correctly - const actualKernelToMeSlot2 = state.clists.mapIncomingWireMessageToKernelSlot( - sender2, - { type: 'your-ingress', id: index2 }, - ); - const { - meToYouSlot: actualMeToYouSlot2, - } = state.clists.mapKernelSlotToOutgoingWireMessage( - { - type: 'export', - id: 2, - }, - sender2, - ); - t.deepEqual(actualKernelToMeSlot2, { type: 'export', id: 2 }); // actual, expected - t.deepEqual(actualMeToYouSlot2, { type: 'your-egress', id: index2 }); - t.end(); -}); - -test('handleCommsController addIngress same again', t => { - let fulfillToPresenceArgs; - - const mockSyscall = { - fulfillToPresence(...args) { - fulfillToPresenceArgs = args; - }, - }; - - const state = makeState(); - - const resolverID = 2; - const sender = 'bot'; - const index = 5; - - const result = handleCommsController( - state, - mockSyscall, - 'addIngress', - JSON.stringify({ - args: [sender, index], - }), - [{ type: 'your-ingress', id: index }], - resolverID, - helpers, - ); - - t.equal(result, undefined); - - // ensure calls to syscall are correct - t.deepEqual(fulfillToPresenceArgs, [ - resolverID, - { - type: 'export', - id: 1, // first import is 1 - }, - ]); - - // ensure state updated correctly - const actualKernelToMeSlot = state.clists.mapIncomingWireMessageToKernelSlot( - sender, - { type: 'your-ingress', id: index }, - ); - const { - meToYouSlot: actualMeToYouSlot, - } = state.clists.mapKernelSlotToOutgoingWireMessage( - { - type: 'export', - id: 1, - }, - sender, - ); - t.deepEqual(actualKernelToMeSlot, { type: 'export', id: 1 }); // actual, expected - t.deepEqual(actualMeToYouSlot, { type: 'your-egress', id: index }); - - const result2 = handleCommsController( - state, - mockSyscall, - 'addIngress', - JSON.stringify({ - args: [sender, index], - }), - [{ type: 'your-ingress', id: index }], - resolverID, - helpers, - ); - - t.equal(result2, undefined); - - // ensure calls to syscall are correct - t.deepEqual(fulfillToPresenceArgs, [ - resolverID, - { - type: 'export', - id: 1, // first import is 1 - }, - ]); - - // ensure state updated correctly - const actualKernelToMeSlot2 = state.clists.mapIncomingWireMessageToKernelSlot( - sender, - { type: 'your-ingress', id: index }, - ); - const { - meToYouSlot: actualMeToYouSlot2, - } = state.clists.mapKernelSlotToOutgoingWireMessage( - { - type: 'export', - id: 1, - }, - sender, - ); - t.deepEqual(actualKernelToMeSlot2, { type: 'export', id: 1 }); // actual, expected - t.deepEqual(actualMeToYouSlot2, { type: 'your-egress', id: index }); - - t.end(); -}); diff --git a/test/commsSlots/commsController/test-init.js b/test/commsSlots/commsController/test-init.js deleted file mode 100644 index 495898d61b5..00000000000 --- a/test/commsSlots/commsController/test-init.js +++ /dev/null @@ -1,78 +0,0 @@ -import { test } from 'tape-promise/tape'; -import handleCommsController from '../../../src/kernel/commsSlots/commsController'; -import makeState from '../../../src/kernel/commsSlots/state'; - -const helpers = { - log: console.log, -}; - -const UNDEFINED = JSON.stringify({ '@qclass': 'undefined' }); - -test('handleCommsController init', t => { - const calls = []; - - const mockSyscall = { - fulfillToData(...args) { - calls.push(['fulfillToData', args]); - }, - send(...args) { - calls.push(['send', args]); - }, - }; - - const state = makeState(); - t.equal(state.machineState.getVatTP(), undefined); - - const vatTP = { type: 'import', id: 3 }; - const slot0 = { '@qclass': 'slot', index: 0 }; - const inboundHandlerFacetID = 4; - const resolverID = 2; - - const result = handleCommsController( - state, - mockSyscall, - 'init', - JSON.stringify({ - args: [slot0], - }), - [vatTP], - resolverID, - helpers, - inboundHandlerFacetID, - ); - - t.equal(result, undefined); - - // ensure calls to syscall are correct - t.deepEqual(calls.shift(), [ - 'send', - [ - vatTP, - 'registerCommsHandler', - JSON.stringify({ args: [slot0] }), - [{ type: 'export', id: inboundHandlerFacetID }], - ], - ]); - - t.deepEqual(calls.shift(), ['fulfillToData', [resolverID, UNDEFINED, []]]); - - // ensure state updated correctly - t.equal(state.machineState.getVatTP(), vatTP); - - t.throws(() => { - return handleCommsController( - state, - mockSyscall, - 'init', - JSON.stringify({ - args: [slot0], - }), - [vatTP], - resolverID, - helpers, - inboundHandlerFacetID, - ); - }); - - t.end(); -}); diff --git a/test/commsSlots/makeCommsSlots/test-deliver.js b/test/commsSlots/makeCommsSlots/test-deliver.js deleted file mode 100644 index 312e7232558..00000000000 --- a/test/commsSlots/makeCommsSlots/test-deliver.js +++ /dev/null @@ -1,246 +0,0 @@ -import { test } from 'tape-promise/tape'; -import { makeCommsSlots } from '../../../src/kernel/commsSlots'; -import makePromise from '../../../src/kernel/makePromise'; - -const helpers = { - log: console.log, - vatID: 'botcomms', -}; - -const UNDEFINED = JSON.stringify({ '@qclass': 'undefined' }); - -test('makeCommsSlots deliver to commsController (facetid 0)', t => { - const calls = []; - - const mockSyscall = { - fulfillToData(...args) { - calls.push(['fulfillToData', args]); - }, - send(...args) { - calls.push(['send', args]); - }, - }; - - const vatTP = { type: 'import', id: 4 }; - const commsSlots = makeCommsSlots(mockSyscall, {}, helpers); - commsSlots.deliver( - 0, - 'init', - '{"args":[{"@qclass":"slot","index":0}]}', - [vatTP], - { type: 'resolver', id: 30 }, - ); - - t.deepEqual(calls[0], [ - 'send', - [ - vatTP, - 'registerCommsHandler', - '{"args":[{"@qclass":"slot","index":0}]}', - [{ type: 'export', id: 1 }], - ], - ]); - calls.shift(); - - t.deepEqual(calls[0], ['fulfillToData', [30, UNDEFINED, []]]); - calls.shift(); - - t.end(); -}); - -test('makeCommsSlots deliver to egress', t => { - const calls = []; - - const mockSyscall = { - send(...args) { - calls.push(['send', args]); - if (args[1] === 'bar') { - return 66; // TODO: issue #34 this ought to be {type: 'promise', id: 66} - } - return undefined; - }, - fulfillToData(...args) { - calls.push(['fulfillToData', args]); - }, - callNow(...args) { - calls.push(['callNow', args]); - }, - subscribe(...args) { - calls.push(['subscribe', args]); - }, - }; - - const commsSlots = makeCommsSlots(mockSyscall, {}, helpers); - const state = commsSlots.getState(); - - const vatTP = { type: 'import', id: 42 }; - // we need to get the vatTP object registered, but we don't otherwise test - // the consequences of init(), because we tested that earlier in this file - commsSlots.deliver( - 0, - 'init', - '{"args":[{"@qclass":"slot","index":0}]}', - [vatTP], - { type: 'resolver', id: 30 }, - ); - t.equal(calls[0][0], 'send'); - calls.shift(); - t.deepEqual(calls[0], ['fulfillToData', [30, UNDEFINED, []]]); - calls.shift(); - - // inboundHandlerFacetID is probably 1, since it's the first thing - // allocated, but we aren't invoking deliver(init) (or capturing the - // callNow(registerInboundHandler) args) to learn it more directly. Still make a - // half-hearted effort to compare it against the state object, to improve - // the debugging experience for someone in the future - const inboundHandlerFacetID = JSON.parse(state.ids.dump()) - 1; - t.equal(inboundHandlerFacetID, 1); - - // setup with an addEgress - commsSlots.deliver( - 0, - 'addEgress', - '{"args":["bot", 70, {"@qclass":"slot","index":0}]}', - [{ type: 'import', id: 55 }], - { type: 'resolver', id: 31 }, - ); - t.deepEqual(calls, [['fulfillToData', [31, UNDEFINED, []]]]); - calls.shift(); - - const machineArgs = { - event: 'send', - target: { type: 'your-egress', id: 70 }, - methodName: 'bar', - // args: JSON.stringify([{foo: 1}]), - args: [{ foo: 1 }], - slots: [], - resultSlot: { type: 'your-resolver', id: 71 }, - }; - const a2 = JSON.stringify(machineArgs); - const msgArgs = ['bot', a2]; - const inboundArgs = { args: msgArgs }; - commsSlots.deliver( - inboundHandlerFacetID, - 'inbound', - JSON.stringify(inboundArgs), - [], - undefined, - ); - // that ought to cause a syscall.send to import:55 - t.deepEqual(calls[0], [ - 'send', - [ - { type: 'import', id: 55 }, - 'bar', - JSON.stringify({ args: [{ foo: 1 }] }), - [], - ], - ]); - calls.shift(); - t.deepEqual(calls[0], ['subscribe', [66]]); - calls.shift(); - - t.deepEqual( - state.clists.mapKernelSlotToOutgoingWireMessage( - { - type: 'promise', - id: 66, - }, - 'bot', - ), - { otherMachineName: 'bot', meToYouSlot: { type: 'your-promise', id: 71 } }, - ); - t.end(); -}); - -test('makeCommsSlots deliver facetid is unexpected', t => { - const calls = []; - - const mockSyscall = { - fulfillToData(...args) { - calls.push(['fulfillToData', args]); - }, - }; - - const commsSlots = makeCommsSlots(mockSyscall, {}, helpers); - - t.throws(() => { - commsSlots.deliver(99, 'init', '{"args":["bot","botSigningKey"]}', [], { - type: 'resolver', - id: 30, - }); - }, "{[Error: unknown facetid] message: 'unknown facetid' }"); - t.equal(calls.length, 0); - // TODO: init() really ought to notifyReject() upon error, not leave the - // caller hanging - t.end(); -}); - -test('makeCommsSlots deliver to ingress', t => { - const calls = []; - - const mockSyscall = { - fulfillToPresence(...args) { - calls.push(['fulfillToPresence', args]); - }, - fulfillToData(...args) { - calls.push(['fulfillToData', args]); - }, - createPromise() { - return makePromise(); - }, - send(...args) { - calls.push(['send', args]); - }, - callNow(...args) { - calls.push(['callNow', args]); - }, - }; - - const commsSlots = makeCommsSlots(mockSyscall, {}, helpers); - - const vatTP = { type: 'import', id: 42 }; - // we need to get the vatTP object registered, but we don't otherwise test - // the consequences of init(), because we tested that earlier in this file - commsSlots.deliver( - 0, - 'init', - '{"args":[{"@qclass":"slot","index":0}]}', - [vatTP], - { type: 'resolver', id: 30 }, - ); - t.equal(calls[0][0], 'send'); - calls.shift(); - t.deepEqual(calls[0], ['fulfillToData', [30, UNDEFINED, []]]); - calls.shift(); - - // setup with an addIngress - commsSlots.deliver( - 0, - 'addIngress', - '{"args":["bot", {"@qclass":"slot","index":0}]}', - [{ type: 'your-ingress', id: 0 }], - { type: 'resolver', id: 31 }, - ); - t.deepEqual(calls[0], ['fulfillToPresence', [31, { type: 'export', id: 2 }]]); - calls.shift(); - - commsSlots.deliver(2, 'encourageMe', '{"args":["me"]}', [], { - type: 'resolver', - id: 32, - }); - t.equal(calls[0][0], 'send'); - const args = calls[0][1]; - calls.shift(); - t.equal(args.length, 4); - t.deepEqual(args[0], vatTP); - t.equal(args[1], 'send'); - t.deepEqual(JSON.parse(args[2]), { - args: [ - 'bot', - '{"event":"send","target":{"type":"your-egress","id":{"@qclass":"slot","index":0}},"methodName":"encourageMe","args":["me"],"slots":[],"resultSlot":{"type":"your-resolver","id":3}}', - ], - }); - t.deepEqual(args[3], []); - t.end(); -}); diff --git a/test/commsSlots/makeCommsSlots/test-notifyFulfillToData.js b/test/commsSlots/makeCommsSlots/test-notifyFulfillToData.js deleted file mode 100644 index 117a24d7453..00000000000 --- a/test/commsSlots/makeCommsSlots/test-notifyFulfillToData.js +++ /dev/null @@ -1,56 +0,0 @@ -import { test } from 'tape-promise/tape'; -import { makeCommsSlots } from '../../../src/kernel/commsSlots'; - -const helpers = { - log: console.log, - vatID: 'botcomms', -}; - -test('makeCommsSlots fulfillToData', t => { - const calls = []; - const mockSyscall = { - send(...args) { - calls.push(['send', args]); - }, - }; - - const commsSlots = makeCommsSlots(mockSyscall, {}, helpers); - const vatTP = { type: 'import', id: 4 }; - const promiseID = 22; - - // setup - const state = commsSlots.getState(); - state.machineState.setVatTP(vatTP); - const kernelToMeSlot = { - type: 'promise', - id: promiseID, - }; - const youToMeSlot = { - type: 'your-egress', - id: 1, - }; - const meToYouSlot = state.clists.changePerspective(youToMeSlot); - - state.clists.add('machine1', kernelToMeSlot, youToMeSlot, meToYouSlot); - - commsSlots.notifyFulfillToData(22, 'hello', []); - t.equal(calls.length, 1); - t.equal(calls[0][0], 'send'); - t.deepEqual(calls[0][1], [ - vatTP, - 'send', - JSON.stringify({ - args: [ - 'machine1', - JSON.stringify({ - event: 'fulfillToData', - promise: meToYouSlot, - args: 'hello', - slots: [], - }), - ], - }), - [], - ]); - t.end(); -}); diff --git a/test/commsSlots/makeCommsSlots/test-notifyFulfillToPresence.js b/test/commsSlots/makeCommsSlots/test-notifyFulfillToPresence.js deleted file mode 100644 index a113dacc2c7..00000000000 --- a/test/commsSlots/makeCommsSlots/test-notifyFulfillToPresence.js +++ /dev/null @@ -1,60 +0,0 @@ -import { test } from 'tape-promise/tape'; -import { makeCommsSlots } from '../../../src/kernel/commsSlots'; - -const helpers = { - log: console.log, -}; - -test('makeCommsSlots fulfillToPresence', t => { - const calls = []; - const mockSyscall = { - send(...args) { - calls.push(['send', args]); - }, - }; - - const commsSlots = makeCommsSlots(mockSyscall, {}, helpers); - const vatTP = { type: 'import', id: 4 }; - const state = commsSlots.getState(); - state.machineState.setVatTP(vatTP); - - state.clists.add( - 'abc', - { type: 'promise', id: 20 }, - { type: 'your-egress', id: 9 }, - { type: 'your-ingress', id: 9 }, - ); - - const kernelToMeSlot = { - type: 'import', - id: 11, - }; - const youToMeSlot = { - type: 'your-ingress', - id: 3, - }; - const meToYouSlot = state.clists.changePerspective(youToMeSlot); - - // is this what we need to add? - state.clists.add('abc', kernelToMeSlot, youToMeSlot, meToYouSlot); - - commsSlots.notifyFulfillToPresence(20, kernelToMeSlot); - t.equal(calls.length, 1); - t.equal(calls[0][0], 'send'); - t.deepEqual(calls[0][1], [ - vatTP, - 'send', - JSON.stringify({ - args: [ - 'abc', - JSON.stringify({ - event: 'fulfillToPresence', - promise: { type: 'your-ingress', id: 9 }, - target: { type: 'your-egress', id: 3 }, - }), - ], - }), - [], - ]); - t.end(); -}); diff --git a/test/commsSlots/state/test-makeCLists.js b/test/commsSlots/state/test-makeCLists.js deleted file mode 100644 index 0fd9c815b52..00000000000 --- a/test/commsSlots/state/test-makeCLists.js +++ /dev/null @@ -1,62 +0,0 @@ -import { test } from 'tape-promise/tape'; -import { makeCLists } from '../../../src/kernel/commsSlots/state/CLists'; - -test('Clists add and get', t => { - const clists = makeCLists(); - const kernelToMeSlot = { type: 'export', id: 1 }; - const youToMeSlot = { type: 'your-ingress', id: 102 }; - const meToYouSlot = clists.changePerspective(youToMeSlot); - clists.add('machine0', kernelToMeSlot, youToMeSlot, meToYouSlot); - const actualKernelToMeSlot = clists.mapIncomingWireMessageToKernelSlot( - 'machine0', - youToMeSlot, - ); - t.deepEqual(actualKernelToMeSlot, kernelToMeSlot); - const { - meToYouSlot: actualMeToYouSlot, - } = clists.mapKernelSlotToOutgoingWireMessage(kernelToMeSlot, 'machine0'); - t.equal(actualMeToYouSlot, meToYouSlot); - t.end(); -}); - -test('Add same object from multiple machines', t => { - const clists = makeCLists(); - const kernelToMeSlot = { type: 'export', id: 1 }; - const youToMeSlot0 = { type: 'your-ingress', id: 102 }; - const meToYouSlot0 = clists.changePerspective(youToMeSlot0); - - const youToMeSlot3 = { type: 'your-ingress', id: 593 }; - const meToYouSlot3 = clists.changePerspective(youToMeSlot3); - - clists.add('machine0', kernelToMeSlot, youToMeSlot0, meToYouSlot0); - clists.add('machine3', kernelToMeSlot, youToMeSlot3, meToYouSlot3); - const actualKernelToMeSlot0 = clists.mapIncomingWireMessageToKernelSlot( - 'machine0', - youToMeSlot0, - ); - const actualKernelToMeSlot3 = clists.mapIncomingWireMessageToKernelSlot( - 'machine3', - youToMeSlot3, - ); - t.deepEqual(actualKernelToMeSlot0, actualKernelToMeSlot3); - - const outgoingWireMessage0 = clists.mapKernelSlotToOutgoingWireMessage( - kernelToMeSlot, - 'machine0', - ); - const outgoingWireMessage3 = clists.mapKernelSlotToOutgoingWireMessage( - kernelToMeSlot, - 'machine3', - ); - t.deepEqual(outgoingWireMessage0.meToYouSlot, meToYouSlot0); - t.notDeepEqual( - outgoingWireMessage3.meToYouSlot, - outgoingWireMessage0.meToYouSlot, - ); - - const messageList = clists.mapKernelSlotToOutgoingWireMessageList( - kernelToMeSlot, - ); - t.deepEqual(messageList, [outgoingWireMessage0, outgoingWireMessage3]); - t.end(); -}); diff --git a/test/commsSlots/state/test-makeChannels.js b/test/commsSlots/state/test-makeChannels.js deleted file mode 100644 index feadab1d82f..00000000000 --- a/test/commsSlots/state/test-makeChannels.js +++ /dev/null @@ -1,11 +0,0 @@ -import { test } from 'tape-promise/tape'; -import { makeChannels } from '../../../src/kernel/commsSlots/state/channels'; - -test('channels set and get', t => { - const channels = makeChannels(); - const dev1 = { type: 'device', index: 1 }; - channels.setChannelDevice(dev1); - const channelDev = channels.getChannelDevice(); - t.deepEqual(channelDev, dev1); - t.end(); -}); diff --git a/test/commsSlots/state/test-makeGetNextImportID.js b/test/commsSlots/state/test-makeGetNextImportID.js deleted file mode 100644 index 63d8fab2c36..00000000000 --- a/test/commsSlots/state/test-makeGetNextImportID.js +++ /dev/null @@ -1,11 +0,0 @@ -import { test } from 'tape-promise/tape'; -import { makeAllocateID } from '../../../src/kernel/commsSlots/state/allocateID'; - -test('allocateID', t => { - const id = makeAllocateID(); - const id1 = id.allocateID(); - const id2 = id.allocateID(); - t.equal(id1, 1); - t.equal(id2, 2); - t.end(); -}); diff --git a/test/commsSlots/state/test-makeMachineState.js b/test/commsSlots/state/test-makeMachineState.js deleted file mode 100644 index bbd5317704e..00000000000 --- a/test/commsSlots/state/test-makeMachineState.js +++ /dev/null @@ -1,16 +0,0 @@ -import { test } from 'tape-promise/tape'; -import { makeMachineState } from '../../../src/kernel/commsSlots/state/machineState'; - -test('machineState set and get', t => { - const machineState = makeMachineState(); - t.equal(machineState.getVatTP(), undefined); - machineState.setVatTP('fake-vattp'); - t.equal(machineState.getVatTP(), 'fake-vattp'); - - // test that it creates a new instance - const otherMachineState = makeMachineState(); - t.equal(otherMachineState.getVatTP(), undefined); - otherMachineState.setVatTP('other-fake-vattp'); - t.equal(otherMachineState.getVatTP(), 'other-fake-vattp'); - t.end(); -}); diff --git a/test/commsSlots/test-inboundHandler.js b/test/commsSlots/test-inboundHandler.js deleted file mode 100644 index 72b2d30cee7..00000000000 --- a/test/commsSlots/test-inboundHandler.js +++ /dev/null @@ -1,304 +0,0 @@ -// import { test } from 'tape-promise/tape'; -// import makeInboundHandler from '../../src/kernel/commsSlots/inbound/inboundHandler'; -// import makeState from '../../src/kernel/commsSlots/state'; - -// // send [ { type: 'your-egress', id: 10 }, 'getIssuer', '{"args":[]}', [] ] -// // subscribe 20 -// test('SendIn Cosmos-SwingSet 0', t => { -// let sentArgs; -// let subscribeArgs; - -// const senderID = 'abc'; -// const promiseID = 72; - -// const mockSyscall = { -// send(...args) { -// sentArgs = args; -// return promiseID; -// }, -// subscribe(...args) { -// subscribeArgs = args; -// }, -// }; - -// const state = makeState(); - -// state.clists.add( -// senderID, -// 'egress', -// { type: 'export', id: 0 }, -// { type: 'your-egress', id: 10 }, -// { type: 'your-ingress', id: 10 }, -// ); - -// const { inboundHandler } = makeInboundHandler(state, mockSyscall); -// inboundHandler( -// senderID, -// JSON.stringify({ -// target: { type: 'your-egress', id: 10 }, -// methodName: 'getIssuer', -// args: [], -// slots: [], -// resultIndex: 1, -// }), -// ); -// // ensure calls to syscall are correct -// t.deepEqual(sentArgs, [ -// { type: 'your-egress', id: 10 }, -// 'getIssuer', -// '{"args":[]}', -// [], -// ]); -// t.deepEqual(subscribeArgs, [promiseID]); - -// // ensure state updated correctly -// const storedPromise = state.clists.mapIncomingWireMessageToKernelSlot( -// senderID, -// 'egress', -// { type: 'egress', id: 1 }, -// ); -// t.deepEqual(storedPromise, { -// type: 'promise', -// id: promiseID, -// }); -// const storedSubscribers = state.subscribers.get(promiseID); -// t.deepEqual(storedSubscribers, [senderID]); -// t.end(); -// }); - -// // send [ {type: "promise", id: 20}, 'makeEmptyPurse', {"args":["purse2"]}, [] ] -// // subscribe 21 -// test('SendIn Cosmos-SwingSet 1', t => { -// let sentArgs; -// let subscribeArgs; - -// const senderID = 'abc'; -// const promiseID = 72; - -// const mockSyscall = { -// send(...args) { -// sentArgs = args; -// return promiseID; -// }, -// subscribe(...args) { -// subscribeArgs = args; -// }, -// }; - -// const state = makeState(); - -// state.clists.add(senderID, 'egress', 0, { type: 'your-egress', id: 10 }); -// state.clists.add(senderID, 'egress', 1, { type: 'promise', id: 20 }); -// const { inboundHandler } = makeInboundHandler(state, mockSyscall); -// inboundHandler( -// senderID, -// JSON.stringify({ -// index: 1, -// methodName: 'makeEmptyPurse', -// args: ['purse2'], -// slots: [], -// resultIndex: 2, -// }), -// ); - -// // ensure calls to syscall are correct -// t.deepEqual(sentArgs, [ -// { type: 'promise', id: 20 }, -// 'makeEmptyPurse', -// '{"args":["purse2"]}', -// [], -// ]); -// t.deepEqual(subscribeArgs, [promiseID]); - -// // ensure state updated correctly -// const storedPromise = state.clists.mapIncomingWireMessageToKernelSlot( -// senderID, -// 'egress', -// { type: 'ingress', id: 2 }, -// ); -// t.deepEqual(storedPromise, { -// type: 'promise', -// id: promiseID, -// }); -// const storedSubscribers = state.subscribers.get(promiseID); -// t.deepEqual(storedSubscribers, [senderID]); -// t.end(); -// }); - -// // send [ {type: "promise", id: 21}, 'deposit', {"args":[20,{"@qclass":"slot","id":0}]}, [{type: "your-egress", id: 10}] ] -// // subscribe 22 -// test('SendIn Cosmos-SwingSet 2', t => { -// let sentArgs; -// let subscribeArgs; - -// const senderID = 'abc'; -// const promiseID = 72; - -// const mockSyscall = { -// send(...args) { -// sentArgs = args; -// return promiseID; -// }, -// subscribe(...args) { -// subscribeArgs = args; -// }, -// }; - -// const state = makeState(); - -// state.clists.add(senderID, 'egress', 0, { type: 'your-egress', id: 10 }); -// state.clists.add(senderID, 'egress', 1, { type: 'promise', id: 20 }); -// state.clists.add(senderID, 'egress', 2, { type: 'promise', id: 21 }); -// const { inboundHandler } = makeInboundHandler(state, mockSyscall); -// inboundHandler( -// senderID, -// JSON.stringify({ -// index: 2, -// methodName: 'deposit', -// args: [20, { '@qclass': 'slot', index: 0 }], -// slots: [{ type: 'your-egress', id: 0 }], -// resultIndex: 3, -// }), -// ); - -// // ensure calls to syscall correct -// t.deepEqual(sentArgs, [ -// { type: 'promise', id: 21 }, -// 'deposit', -// '{"args":[20,{"@qclass":"slot","index":0}]}', -// [{ type: 'your-egress', id: 10 }], -// ]); -// t.deepEqual(subscribeArgs, [promiseID]); - -// // ensure state changes correct -// const storedPromise = state.clists.mapIncomingWireMessageToKernelSlot( -// senderID, -// 'egress', -// { type: 'egress', id: 3 }, -// ); -// t.deepEqual(storedPromise, { -// type: 'promise', -// id: promiseID, -// }); -// const storedSubscribers = state.subscribers.get(promiseID); -// t.deepEqual(storedSubscribers, [senderID]); -// t.end(); -// }); - -// // send [ {type: "promise", id: 21}, getBalance {"args":[]}, [] ] -// // subscribe 23 -// test('SendIn Cosmos-SwingSet 3', t => { -// let sentArgs; -// let subscribeArgs; - -// const senderID = 'abc'; -// const promiseID = 72; - -// const mockSyscall = { -// send(...args) { -// sentArgs = args; -// return promiseID; -// }, -// subscribe(...args) { -// subscribeArgs = args; -// }, -// }; - -// const state = makeState(); - -// state.clists.add(senderID, 'egress', 0, { type: 'your-egress', id: 10 }); -// state.clists.add(senderID, 'egress', 1, { type: 'promise', id: 20 }); -// state.clists.add(senderID, 'egress', 2, { type: 'promise', id: 21 }); -// const { inboundHandler } = makeInboundHandler(state, mockSyscall); -// inboundHandler( -// senderID, -// JSON.stringify({ -// index: 2, -// methodName: 'getBalance', -// args: [], -// slots: [], -// resultIndex: 4, -// }), -// ); -// // ensure syscall calls correct -// t.deepEqual(sentArgs, [ -// { type: 'promise', id: 21 }, -// 'getBalance', -// '{"args":[]}', -// [], -// ]); -// t.deepEqual(subscribeArgs, [promiseID]); - -// // ensure state changes correct -// const storedPromise = state.clists.mapIncomingWireMessageToKernelSlot( -// senderID, -// 'egress', -// { type: 'egress', id: 4 }, -// ); -// t.deepEqual(storedPromise, { -// type: 'promise', -// id: promiseID, -// }); -// const storedSubscribers = state.subscribers.get(promiseID); -// t.deepEqual(storedSubscribers, [senderID]); -// t.end(); -// }); - -// // send [ {type: "your-egress", id: 10}, getBalance {"args":[]}', [] ] -// // subscribe 24 -// test('SendIn Cosmos-SwingSet 4', t => { -// let sentArgs; -// let subscribeArgs; - -// const senderID = 'abc'; -// const promiseID = 72; - -// const mockSyscall = { -// send(...args) { -// sentArgs = args; -// return promiseID; -// }, -// subscribe(...args) { -// subscribeArgs = args; -// }, -// }; - -// const state = makeState(); - -// state.clists.add(senderID, 'egress', 0, { type: 'your-egress', id: 10 }); -// state.clists.add(senderID, 'egress', 1, { type: 'promise', id: 20 }); -// state.clists.add(senderID, 'egress', 2, { type: 'promise', id: 21 }); -// const { inboundHandler } = makeInboundHandler(state, mockSyscall); -// inboundHandler( -// senderID, -// JSON.stringify({ -// index: 0, -// methodName: 'getBalance', -// args: [], -// slots: [], -// resultIndex: 5, -// }), -// ); -// // ensure syscall calls correct -// t.deepEqual(sentArgs, [ -// { type: 'your-egress', id: 10 }, -// 'getBalance', -// '{"args":[]}', -// [], -// ]); -// t.deepEqual(subscribeArgs, [promiseID]); - -// // ensure state changes correct -// const storedPromise = state.clists.mapIncomingWireMessageToKernelSlot( -// senderID, -// 'egress', -// { type: 'egress', id: 5 }, -// ); -// t.deepEqual(storedPromise, { -// type: 'promise', -// id: promiseID, -// }); -// const storedSubscribers = state.subscribers.get(promiseID); -// t.deepEqual(storedSubscribers, [senderID]); -// t.end(); -// }); diff --git a/test/files-vattp/bootstrap-test-vattp.js b/test/files-vattp/bootstrap-test-vattp.js index 3fe77e60505..facdba15034 100644 --- a/test/files-vattp/bootstrap-test-vattp.js +++ b/test/files-vattp/bootstrap-test-vattp.js @@ -1,9 +1,9 @@ const harden = require('@agoric/harden'); function build(E, D, log) { - const commsHandler = harden({ - inbound(peer, body) { - log(`ch.inbound ${peer} ${body}`); + const receiver = harden({ + receive(body) { + log(`ch.receive ${body}`); }, }); @@ -11,13 +11,15 @@ function build(E, D, log) { async bootstrap(argv, vats, devices) { D(devices.mailbox).registerInboundHandler(vats.vattp); await E(vats.vattp).registerMailboxDevice(devices.mailbox); - await E(vats.vattp).registerCommsHandler(commsHandler); - // await E(vats.comms).init(vats.vattp); + const name = 'remote1'; + const { transmitter, setReceiver } = await E(vats.vattp).addRemote(name); + // const receiver = await E(vats.comms).addRemote(name, transmitter); + await E(setReceiver).setReceiver(receiver); if (argv[0] === '1') { log('not sending anything'); } else if (argv[0] === '2') { - E(vats.vattp).send('peer1', 'out1'); + E(transmitter).transmit('out1'); } else { throw new Error(`unknown argv mode '${argv[0]}'`); } diff --git a/test/test-comms-integration.js b/test/test-comms-integration.js new file mode 100644 index 00000000000..6c39ef94405 --- /dev/null +++ b/test/test-comms-integration.js @@ -0,0 +1,272 @@ +import path from 'path'; +import { test } from 'tape-promise/tape'; +import { buildVatController, loadBasedir } from '../src/index'; + +export async function runVats(t, withSES, argv) { + const config = await loadBasedir( + path.resolve(__dirname, './basedir-commsvat'), + ); + + const ldSrcPath = require.resolve('../src/devices/loopbox-src'); + config.devices = [['loopbox', ldSrcPath, {}]]; + const c = await buildVatController(config, withSES, argv); + return c; +} + +// use e.g. runTest(test.only, name) to run only one test + +const setupLogs = ['=> setup called', '=> bootstrap() called']; + +export function runTest(testType, withSES, testStr, expectedLogs) { + const expected = setupLogs.concat(expectedLogs); + testType(testStr, async t => { + const c = await runVats(t, withSES, [testStr]); + await c.run(); + const { log } = c.dump(); + t.deepEqual(log, expected); + t.end(); + }); +} + +/* TABLE OF CONTENTS OF TESTS */ +// left does: E(right.0).method() => returnData +// left does: E(right.0).method(dataArg1) => returnData +// left does: E(right.0).method(right.0) => returnData +// left does: E(right.0).method(left.1) => returnData +// left does: E(right.0).method(left.1) => returnData twice +// left does: E(right.1).method() => returnData +// left does: E(right.0).method() => right.presence +// left does: E(right.0).method() => left.presence +// left does: E(right.0).method() => right.promise => data +// left does: E(right.0).method() => right.promise => right.presence +// left does: E(right.0).method() => right.promise => left.presence +// left does: E(right.0).method() => right.promise => reject +// left does: E(right.0).method(left.promise) => returnData +// left does: E(right.0).method(right.promise) => returnData +// left does: E(right.0).method(right.promise => right.presence) => returnData +// left does: E(right.0).method(right.promise => left.presence) => returnData + +/* TEST: left does: E(right.0).method() => returnData + * DESCRIPTION: The left vat invokes a method on the right vat's root + * object and returns data. + */ +runTest(test, false, 'left does: E(right.0).method() => returnData', [ + '=> right.method was invoked', + '=> left vat receives the returnedData: called method', +]); + +/* TEST: left does: E(right.0).method(dataArg1) => returnData + * DESCRIPTION: The left vat invokes a method on the right vat's root + * object with an argument and returns data connected to that argument. + */ +runTest(test, false, 'left does: E(right.0).method(dataArg1) => returnData', [ + '=> right.methodWithArgs got the arg: hello', + '=> left vat receives the returnedData: hello was received', +]); + +/* TEST: left does: E(right.0).method(right.0) => returnData + * DESCRIPTION: The left vat invokes a method on the right vat's root + * object with the right vat's root object as an argument and does a + * method call on the argument. It returns the result of the method + * call on the argument, i.e. right.0.method() => 'called method' + */ +runTest(test, false, 'left does: E(right.0).method(right.0) => returnData', [ + '=> right.methodWithPresence got the ref [object Object]', + '=> right.method was invoked', + '=> left vat receives the returnedData: called method', +]); + +/* TEST: left does: E(right.0).method(left.1) => returnData + * DESCRIPTION: The left vat invokes a method on the right vat's + * object with a new left object as an argument and returns data. + */ +runTest(test, false, 'left does: E(right.0).method(left.1) => returnData', [ + '=> right.methodWithPresence got the ref [Presence o-50]', + '=> left.1.method was invoked', + '=> left vat receives the returnedData: called method', +]); + +/* TEST: left does: E(right.0).method(left.1) => returnData twice + * DESCRIPTION: The left vat invokes a method on the right vat's + * object with a new left object as an argument and returns data. It + * repeats this a second time. No new egresses/ingresses should be + * allocated the second time. Also, both left.1 args should have the + * same identity. + */ +runTest( + test, + false, + 'left does: E(right.0).method(left.1) => returnData twice', + [ + '=> right.methodWithPresence got the ref [Presence o-50]', + 'ref equal each time: true', + '=> right.methodWithPresence got the ref [Presence o-50]', + '=> left.1.method was invoked', + '=> left.1.method was invoked', + '=> left vat receives the returnedData: called method', + '=> left vat receives the returnedData: called method', + ], +); + +/* TEST: left does: E(right.1).method() => returnData + * DESCRIPTION: The left vat invokes a method on the right vat's + * object (a new object, not the root object) and returns data. + */ +runTest(test, false, 'left does: E(right.1).method() => returnData', [ + '=> right.1.method was invoked', + '=> left vat receives the returnedData: called method', +]); + +/* TEST: left does: E(right.0).method() => right.presence + * DESCRIPTION: The left vat invokes a method on the right vat's root + * object and is given presence that represents a new object on the right + * side + */ +runTest(test, false, 'left does: E(right.0).method() => right.presence', [ + '=> right.1.method was invoked', + '=> left vat receives the returnedData: called method', +]); + +/* TEST: left does: E(right.0).method() => left.presence + * DESCRIPTION: The left vat invokes a method on the right vat's root + * object and is given presence that represents a new object on the left + * side + */ +runTest(test, false, 'left does: E(right.0).method() => left.presence', [ + '=> left.1.method was invoked', + '=> left vat receives the returnedData: called method', +]); + +/* TEST: left does: E(right.0).method() => right.promise => data + * DESCRIPTION: The left vat invokes a method on the right vat's root + * object and is given a promise that is later resolved to data. + */ +runTest( + test, + false, + 'left does: E(right.0).method() => right.promise => data', + [ + '=> left vat receives the returnedPromise: [object Promise]', + '=> right.methodReturnsPromise was invoked', + '=> returnedPromise.then: foo', + ], +); + +/* TEST: left does: E(right.0).method() => right.promise => right.presence + * DESCRIPTION: The left vat invokes a method on the right vat's root + * object and is given a promise that resolves to a presence that + * represents an object on the right side. + */ +runTest( + test, + false, + 'left does: E(right.0).method() => right.promise => right.presence', + [ + '=> left vat receives the returnedPromise: [object Promise]', + '=> returnedPromise.then: [Presence o-51]', + '=> right.1.method was invoked', + '=> presence methodCallResult: called method', + ], +); + +/* TEST: left does: E(right.0).method() => right.promise => left.presence + * DESCRIPTION: The left vat invokes a method on the right vat's root + * object and is given a promise that resolves to a presence that + * represents an object on the left side. + */ +runTest( + test, + false, + 'left does: E(right.0).method() => right.promise => left.presence', + [ + '=> left vat receives the returnedPromise: [object Promise]', + '=> returnedPromise.then: [object Object]', + '=> left.1.method was invoked', + '=> presence methodCallResult: called method', + ], +); + +/* TEST: left does: E(right.0).method() => right.promise => reject + * DESCRIPTION: The left vat invokes a method on the right vat's root + * object and is given a promise that is rejected. + */ +runTest( + test, + false, + 'left does: E(right.0).method() => right.promise => reject', + [ + '=> right.methodReturnsPromiseReject was invoked', + '=> left vat receives the rejected promise with error Error: this was rejected', + ], +); + +/* TEST: left does: E(right.0).method(left.promise) => returnData + * DESCRIPTION: The left vat invokes a method on the right vat's root + * object with a promise that the left machine knows about + */ +runTest( + test, + false, + 'left does: E(right.0).method(left.promise) => returnData', + ['promise resolves to foo', '=> left vat receives the returnedData: foo'], +); + +/* TEST: left does: E(right.0).method(right.promise) => returnData + * DESCRIPTION: The left vat invokes a method on the right vat's root + * object with a promise that the right machine knows about. The promise + should be unresolved at the time the right machine sends it. + */ +runTest( + test, + false, + 'left does: E(right.0).method(right.promise) => returnData 1', + [ + '=> right.methodReturnsPromise was invoked', + 'promise resolves to foo', + '=> left vat receives the returnedData: foo', + ], +); + +/* TEST: left does: E(right.0).method(right.promise) => returnData + * DESCRIPTION: The left vat invokes a method on the right vat's root + * object with a promise that the right machine knows about. The promise + should be resolved by the time the right machine sends it. + */ +runTest( + test, + false, + 'left does: E(right.0).method(right.promise) => returnData 2', + [ + '=> right.methodReturnsPromise was invoked', + 'promise resolves to foo', + '=> left vat receives the returnedData: foo', + ], +); + +/* TEST: left does: E(right.0).method(right.promise => right.presence) => returnData + * DESCRIPTION: The left vat invokes a method on the right vat's root + * object with a promise that resolves to a right.presence + */ +runTest( + test, + false, + 'left does: E(right.0).method(right.promise => right.presence) => returnData', + [ + '=> right.1.method was invoked', + '=> left vat receives the returnedData: called method', + ], +); + +/* TEST: left does: E(right.0).method(right.promise => left.presence) => returnData + * DESCRIPTION: The left vat invokes a method on the right vat's root + * object with a promise that resolves to a left.presence + */ +runTest( + test, + false, + 'left does: E(right.0).method(right.promise => left.presence) => returnData', + [ + '=> left.1.method was invoked', + '=> left vat receives the returnedData: called method', + ], +); diff --git a/test/test-comms.js b/test/test-comms.js new file mode 100644 index 00000000000..b301c9f5f47 --- /dev/null +++ b/test/test-comms.js @@ -0,0 +1,155 @@ +import harden from '@agoric/harden'; +import { test } from 'tape-promise/tape'; +import buildCommsDispatch from '../src/vats/comms'; +import { flipRemoteSlot } from '../src/vats/comms/parseRemoteSlot'; +import { makeState } from '../src/vats/comms/state'; +import { addRemote } from '../src/vats/comms/remote'; +import { + getInbound, + getOutbound, + mapInbound, + mapOutbound, +} from '../src/vats/comms/clist'; +import { debugState } from '../src/vats/comms/dispatch'; + +test('mapOutbound', t => { + const s = makeState(); + const { remoteID } = addRemote(s, 'remote1', 'o-1'); + t.equal(mapOutbound(s, remoteID, 'o-4'), 'ro-20'); + t.equal(mapOutbound(s, remoteID, 'o-4'), 'ro-20'); + t.equal(mapOutbound(s, remoteID, 'o-5'), 'ro-21'); + t.throws( + () => mapOutbound(s, remoteID, 'o+5'), + /sending non-remote object o\+5 to remote machine/, + ); + t.end(); +}); + +function mockSyscall() { + const sends = []; + const syscall = harden({ + send(targetSlot, method, argsString, vatSlots) { + sends.push([targetSlot, method, argsString, vatSlots]); + return 'r-1'; + }, + subscribe(_targetSlot) {}, + }); + return { syscall, sends }; +} + +function encodeArgs(body) { + return JSON.stringify({ args: [body] }); +} + +test('transmit', t => { + // look at machine A, on which some local vat is sending messages to a + // remote 'bob' on machine B + const { syscall, sends } = mockSyscall(); + const d = buildCommsDispatch(syscall, 'fakestate', 'fakehelpers'); + const state = debugState.get(d); + // add the remote, and an object to send at + const transmitterID = 'o-1'; + const alice = 'o-10'; + const { remoteID } = addRemote(state, 'remote1', transmitterID); + const bob = mapInbound(state, remoteID, 'ro-23'); + + // now tell the comms vat to send a message to a remote machine, the + // equivalent of bob!foo() + d.deliver(bob, 'foo', 'argsbytes', [], null); + t.deepEquals(sends.shift(), [ + transmitterID, + 'transmit', + encodeArgs('deliver:ro+23:foo:;argsbytes'), + [], + ]); + + // bob!bar(alice, bob) + d.deliver(bob, 'bar', 'argsbytes', [alice, bob], null); + t.deepEquals(sends.shift(), [ + transmitterID, + 'transmit', + encodeArgs('deliver:ro+23:bar::ro-20:ro+23;argsbytes'), + [], + ]); + // the outbound ro-20 should match an inbound ro+20, both represent 'alice' + t.equal(getInbound(state, remoteID, 'ro+20'), alice); + // do it again, should use same values + d.deliver(bob, 'bar', 'argsbytes', [alice, bob], null); + t.deepEquals(sends.shift(), [ + transmitterID, + 'transmit', + encodeArgs('deliver:ro+23:bar::ro-20:ro+23;argsbytes'), + [], + ]); + + // bob!cat(alice, bob, ayana) + const ayana = 'o-11'; + d.deliver(bob, 'cat', 'argsbytes', [alice, bob, ayana], null); + t.deepEquals(sends.shift(), [ + transmitterID, + 'transmit', + encodeArgs('deliver:ro+23:cat::ro-20:ro+23:ro-21;argsbytes'), + [], + ]); + + t.end(); +}); + +test('receive', t => { + // look at machine B, which is receiving remote messages aimed at a local + // vat's object 'bob' + const { syscall, sends } = mockSyscall(); + const d = buildCommsDispatch(syscall, 'fakestate', 'fakehelpers'); + const state = debugState.get(d); + // add the remote, and an object to send at + const transmitterID = 'o-1'; + const bob = 'o-10'; + const { remoteID, receiverID } = addRemote(state, 'remote1', transmitterID); + const remoteBob = flipRemoteSlot(mapOutbound(state, remoteID, bob)); + t.equal(remoteBob, 'ro+20'); + + // now pretend the transport layer received a message from remote1, as if + // the remote machine had performed bob!foo() + d.deliver( + receiverID, + 'receive', + encodeArgs(`deliver:${remoteBob}:foo:;argsbytes`), + [], + null, + ); + t.deepEquals(sends.shift(), [bob, 'foo', 'argsbytes', []]); + + // bob!bar(alice, bob) + d.deliver( + receiverID, + 'receive', + encodeArgs(`deliver:${remoteBob}:bar::ro-20:${remoteBob};argsbytes`), + [], + null, + ); + t.deepEquals(sends.shift(), [bob, 'bar', 'argsbytes', ['o+11', bob]]); + // if we were to send o+11, the other side should get ro+20, which is alice + t.equal(getOutbound(state, remoteID, 'o+11'), 'ro+20'); + + // bob!bar(alice, bob) + d.deliver( + receiverID, + 'receive', + encodeArgs(`deliver:${remoteBob}:bar::ro-20:${remoteBob};argsbytes`), + [], + null, + ); + t.deepEquals(sends.shift(), [bob, 'bar', 'argsbytes', ['o+11', bob]]); + + // bob!cat(alice, bob, ayana) + d.deliver( + receiverID, + 'receive', + encodeArgs(`deliver:${remoteBob}:cat::ro-20:${remoteBob}:ro-21;argsbytes`), + [], + null, + ); + t.deepEquals(sends.shift(), [bob, 'cat', 'argsbytes', ['o+11', bob, 'o+12']]); + + t.end(); +}); diff --git a/test/test-demos-comms.js b/test/test-demos-comms.js new file mode 100644 index 00000000000..aba796c3250 --- /dev/null +++ b/test/test-demos-comms.js @@ -0,0 +1,38 @@ +import { test } from 'tape-promise/tape'; +import { loadBasedir, buildVatController } from '../src/index'; + +async function main(withSES, basedir, argv) { + const config = await loadBasedir(basedir); + const ldSrcPath = require.resolve('../src/devices/loopbox-src'); + config.devices = [['loopbox', ldSrcPath, {}]]; + + const controller = await buildVatController(config, withSES, argv); + await controller.run(); + return controller.dump(); +} + +const encouragementBotCommsGolden = [ + '=> setup called', + '=> user.talkToBot is called with bot', + "=> the promise given by the call to user.talkToBot resolved to 'Thanks for the setup. I sure hope I get some encouragement...'", + '=> encouragementBot.encourageMe got the name: user', + '=> user receives the encouragement: user, you are awesome, keep it up!', +]; + +test('run encouragementBotComms Demo with SES', async t => { + const dump = await main(true, 'demo/encouragementBotComms', []); + t.deepEquals(dump.log, encouragementBotCommsGolden); + t.end(); +}); + +test('run encouragementBotComms Demo without SES', async t => { + const dump = await main(false, 'demo/encouragementBotComms'); + t.deepEquals(dump.log, encouragementBotCommsGolden); + t.end(); +}); + +test('run encouragementBotCommsBang Demo with SES', async t => { + const dump = await main(true, 'demo/encouragementBotCommsBang', []); + t.deepEquals(dump.log, encouragementBotCommsGolden); + t.end(); +}); diff --git a/test/test-demos.js b/test/test-demos.js index 8ea228f629e..9e281d8d184 100644 --- a/test/test-demos.js +++ b/test/test-demos.js @@ -30,31 +30,3 @@ test('run encouragementBot Demo without SES', async t => { t.deepEquals(dump.log, encouragementBotGolden); t.end(); }); - -const encouragementBotCommsGolden = [ - '=> setup called', - 'addEgress called with sender user, index 0, valslot [object Object]', - 'addIngress called with machineName bot, index 0', - '=> user.talkToBot is called with bot', - "=> the promise given by the call to user.talkToBot resolved to 'Thanks for the setup. I sure hope I get some encouragement...'", - '=> encouragementBot.encourageMe got the name: user', - '=> user receives the encouragement: user, you are awesome, keep it up!', -]; - -test('run encouragementBotComms Demo with SES', async t => { - const dump = await main(true, 'demo/encouragementBotComms', []); - t.deepEquals(dump.log, encouragementBotCommsGolden); - t.end(); -}); - -test('run encouragementBotComms Demo without SES', async t => { - const dump = await main(false, 'demo/encouragementBotComms'); - t.deepEquals(dump.log, encouragementBotCommsGolden); - t.end(); -}); - -test('run encouragementBotCommsBang Demo with SES', async t => { - const dump = await main(true, 'demo/encouragementBotCommsBang', []); - t.deepEquals(dump.log, encouragementBotCommsGolden); - t.end(); -}); diff --git a/test/test-vattp.js b/test/test-vattp.js index 36ab0767500..98349e1174d 100644 --- a/test/test-vattp.js +++ b/test/test-vattp.js @@ -16,18 +16,18 @@ async function testVatTP(t, withSES) { await c.run(); t.deepEqual(s.exportToData(), {}); - t.equal(mb.deliverInbound('peer1', [[1, 'msg1'], [2, 'msg2']], 0), true); + t.equal(mb.deliverInbound('remote1', [[1, 'msg1'], [2, 'msg2']], 0), true); await c.run(); t.deepEqual(c.dump().log, [ 'not sending anything', - 'ch.inbound peer1 msg1', - 'ch.inbound peer1 msg2', + 'ch.receive msg1', + 'ch.receive msg2', ]); - t.deepEqual(s.exportToData(), { peer1: { outbox: [], inboundAck: 2 } }); + t.deepEqual(s.exportToData(), { remote1: { outbox: [], inboundAck: 2 } }); - t.equal(mb.deliverInbound('peer1', [[1, 'msg1'], [2, 'msg2']], 0), false); + t.equal(mb.deliverInbound('remote1', [[1, 'msg1'], [2, 'msg2']], 0), false); await c.run(); - t.deepEqual(s.exportToData(), { peer1: { outbox: [], inboundAck: 2 } }); + t.deepEqual(s.exportToData(), { remote1: { outbox: [], inboundAck: 2 } }); t.end(); } @@ -53,25 +53,25 @@ async function testVatTP2(t, withSES) { const c = await buildVatController(config, withSES, ['2']); await c.run(); t.deepEqual(s.exportToData(), { - peer1: { outbox: [[1, 'out1']], inboundAck: 0 }, + remote1: { outbox: [[1, 'out1']], inboundAck: 0 }, }); - t.equal(mb.deliverInbound('peer1', [], 1), true); + t.equal(mb.deliverInbound('remote1', [], 1), true); await c.run(); t.deepEqual(c.dump().log, []); - t.deepEqual(s.exportToData(), { peer1: { outbox: [], inboundAck: 0 } }); + t.deepEqual(s.exportToData(), { remote1: { outbox: [], inboundAck: 0 } }); - t.equal(mb.deliverInbound('peer1', [[1, 'msg1']], 1), true); + t.equal(mb.deliverInbound('remote1', [[1, 'msg1']], 1), true); await c.run(); - t.deepEqual(c.dump().log, ['ch.inbound peer1 msg1']); - t.deepEqual(s.exportToData(), { peer1: { outbox: [], inboundAck: 1 } }); + t.deepEqual(c.dump().log, ['ch.receive msg1']); + t.deepEqual(s.exportToData(), { remote1: { outbox: [], inboundAck: 1 } }); - t.equal(mb.deliverInbound('peer1', [[1, 'msg1']], 1), false); + t.equal(mb.deliverInbound('remote1', [[1, 'msg1']], 1), false); - t.equal(mb.deliverInbound('peer1', [[1, 'msg1'], [2, 'msg2']], 1), true); + t.equal(mb.deliverInbound('remote1', [[1, 'msg1'], [2, 'msg2']], 1), true); await c.run(); - t.deepEqual(c.dump().log, ['ch.inbound peer1 msg1', 'ch.inbound peer1 msg2']); - t.deepEqual(s.exportToData(), { peer1: { outbox: [], inboundAck: 2 } }); + t.deepEqual(c.dump().log, ['ch.receive msg1', 'ch.receive msg2']); + t.deepEqual(s.exportToData(), { remote1: { outbox: [], inboundAck: 2 } }); t.end(); } From 5409955ed56fdbd2fde4c631031278ca71a634e5 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 28 Aug 2019 16:43:27 -0700 Subject: [PATCH 04/16] phase 2: send() now takes a result promise, instead of returning one * removed dead code (doRedirect, dispatch.subscribe) * remove the notion of "resolverID" * remove resolverIDs from vat clists * run-queue messages have result, not resolverID * createPromise() returns just a promiseID, not promiseID+resolverID * update liveSlots to make result with createPromise() (temporary) We still use syscall.createPromise() instead of allocating a vat-local p+NN index and giving that to the kernel. --- src/kernel/deviceManager.js | 2 +- src/kernel/kernel.js | 2 +- src/kernel/liveSlots.js | 45 +++----- src/kernel/state/vatKeeper.js | 22 +--- src/kernel/vatManager.js | 149 ++++++++++++------------ src/vats/comms/clist.js | 57 ++++++++- src/vats/comms/controller.js | 8 +- src/vats/comms/dispatch.js | 16 ++- src/vats/comms/inbound.js | 31 ++--- src/vats/comms/outbound.js | 39 ++----- test/files-devices/bootstrap-0.js | 2 +- test/files-devices/bootstrap-1.js | 2 +- test/test-comms-integration.js | 7 ++ test/test-controller.js | 8 +- test/test-kernel.js | 184 ++++++++++++++++-------------- test/test-liveslots.js | 94 ++++++++++++++- test/test-marshal.js | 11 +- 17 files changed, 399 insertions(+), 280 deletions(-) diff --git a/src/kernel/deviceManager.js b/src/kernel/deviceManager.js index 9ebd178fc39..473d7908eac 100644 --- a/src/kernel/deviceManager.js +++ b/src/kernel/deviceManager.js @@ -42,7 +42,7 @@ export default function makeDeviceManager( method, argsString, slots, - kernelResolverID: undefined, + result: null, }; send(target, msg); } diff --git a/src/kernel/kernel.js b/src/kernel/kernel.js index 0fd9c219d65..80b505c1125 100644 --- a/src/kernel/kernel.js +++ b/src/kernel/kernel.js @@ -277,7 +277,7 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { // queue() is exposed to the controller's realm, so we must translate // each slot into a kernel-realm object/array slots: Array.from(slots.map(s => `${s}`)), - kernelResolverID: null, // this will be json stringified + result: null, // this will be json stringified }, }), ); diff --git a/src/kernel/liveSlots.js b/src/kernel/liveSlots.js index 5cf06b13aa4..b4e0d63666a 100644 --- a/src/kernel/liveSlots.js +++ b/src/kernel/liveSlots.js @@ -74,13 +74,13 @@ function build(syscall, _state, makeRoot, forVatID) { } function exportPromise(p) { - const pr = syscall.createPromise(); + const pid = syscall.createPromise(); // we ignore the kernel promise, but we use the resolver to notify the // kernel when our local promise changes state - lsdebug(`ls exporting promise ${pr.resolverID}`); + lsdebug(`ls exporting promise ${pid}`); // eslint-disable-next-line no-use-before-define - p.then(thenResolve(pr.resolverID), thenReject(pr.resolverID)); - return pr.promiseID; + p.then(thenResolve(pid), thenReject(pid)); + return pid; } function exportPassByPresence() { @@ -186,7 +186,8 @@ function build(syscall, _state, makeRoot, forVatID) { function queueMessage(targetSlot, prop, args) { const ser = m.serialize(harden({ args })); lsdebug(`ls.qm send(${JSON.stringify(targetSlot)}, ${prop}`); - const promiseID = syscall.send(targetSlot, prop, ser.argsString, ser.slots); + const promiseID = syscall.createPromise(); // temporary + syscall.send(targetSlot, prop, ser.argsString, ser.slots, promiseID); insistVatType('promise', promiseID); lsdebug(` ls.qm got promiseID ${promiseID}`); const done = makeQueued(promiseID); @@ -314,9 +315,9 @@ function build(syscall, _state, makeRoot, forVatID) { return pr; } - function deliver(target, method, argsbytes, caps, resolverID) { + function deliver(target, method, argsbytes, caps, result) { lsdebug( - `ls[${forVatID}].dispatch.deliver ${target}.${method} -> ${resolverID}`, + `ls[${forVatID}].dispatch.deliver ${target}.${method} -> ${result}`, ); const t = slotToVal.get(target); if (!t) { @@ -340,17 +341,17 @@ function build(syscall, _state, makeRoot, forVatID) { } return t[method](...args.args); }); - if (resolverID !== undefined && resolverID !== null) { - lsdebug(` ls.deliver attaching then ->${resolverID}`); - insistVatType('resolver', resolverID); // temporary + if (result) { + lsdebug(` ls.deliver attaching then ->${result}`); + insistVatType('promise', result); // eslint-disable-next-line no-use-before-define - p.then(thenResolve(resolverID), thenReject(resolverID)); + p.then(thenResolve(result), thenReject(result)); } return p; } - function thenResolve(resolverID) { - insistVatType('resolver', resolverID); // temporary + function thenResolve(promiseID) { + insistVatType('promise', promiseID); return res => { harden(res); lsdebug(`ls.thenResolve fired`, res); @@ -369,37 +370,27 @@ function build(syscall, _state, makeRoot, forVatID) { const slot = ser.slots[unser.index]; const { type } = parseVatSlot(slot); if (type === 'object') { - syscall.fulfillToPresence(resolverID, slot); + syscall.fulfillToPresence(promiseID, slot); } else { throw new Error(`thenResolve to non-object slot ${slot}`); } } else { // if it resolves to data, .thens fire but kernel-queued messages are // rejected, because you can't send messages to data - syscall.fulfillToData(resolverID, ser.argsString, ser.slots); + syscall.fulfillToData(promiseID, ser.argsString, ser.slots); } }; } - function thenReject(resolverID) { + function thenReject(promiseID) { return rej => { harden(rej); lsdebug(`ls thenReject fired`, rej); const ser = m.serialize(rej); - syscall.reject(resolverID, ser.argsString, ser.slots); + syscall.reject(promiseID, ser.argsString, ser.slots); }; } - /* - function subscribe(resolverID) { - lsdebug(`ls.dispatch.subscribe(${resolverID})`); - if (!exportedPromisesByResolverID.has(resolverID)) { - throw new Error(`unknown resolverID '${resolverID}'`); - } - const p = exportedPromisesByResolverID.get(resolverID); - p.then(thenResolve(resolverID), thenReject(resolverID)); - } */ - function notifyFulfillToData(promiseID, data, slots) { lsdebug(`ls.dispatch.notifyFulfillToData(${promiseID}, ${data}, ${slots})`); insistVatType('promise', promiseID); diff --git a/src/kernel/state/vatKeeper.js b/src/kernel/state/vatKeeper.js index 7ed6b122d4e..82c2ce121e7 100644 --- a/src/kernel/state/vatKeeper.js +++ b/src/kernel/state/vatKeeper.js @@ -1,6 +1,6 @@ import harden from '@agoric/harden'; import { insist } from '../insist'; -import { insistKernelType, parseKernelSlot } from '../parseKernelSlots'; +import { parseKernelSlot } from '../parseKernelSlots'; import { makeVatSlot, parseVatSlot } from '../../vats/parseVatSlots'; // makeVatKeeper is a pure function: all state is kept in the argument object @@ -12,11 +12,6 @@ export default function makeVatKeeper( addKernelPromise, ) { function createStartingVatState() { - // kernelSlotToVatSlot is an object with four properties: - // exports, devices, promises, resolvers. - // vatSlotToKernelSlot has imports, deviceImports, promises, - // resolvers - state.kernelSlotToVatSlot = {}; // kpNN -> p+NN, etc state.vatSlotToKernelSlot = {}; // p+NN -> kpNN, etc @@ -93,20 +88,6 @@ export default function makeVatKeeper( return state.kernelSlotToVatSlot[kernelSlot]; } - // temporary - function mapKernelPromiseToVatResolver(kernelSlot) { - insistKernelType('promise', kernelSlot); - const vp = mapKernelSlotToVatSlot(kernelSlot); - const parsed = parseVatSlot(vp); - const vr = makeVatSlot('resolver', parsed.allocatedByVat, parsed.id); - const existing = state.vatSlotToKernelSlot[vr]; - if (existing === undefined) { - // the vat resolver and the vat promise both map to the kernel promise - state.vatSlotToKernelSlot[vr] = kernelSlot; - } - return vr; - } - function getTranscript() { return Array.from(state.transcript); } @@ -129,7 +110,6 @@ export default function makeVatKeeper( createStartingVatState, mapVatSlotToKernelSlot, mapKernelSlotToVatSlot, - mapKernelPromiseToVatResolver, getTranscript, dumpState, addToTranscript, diff --git a/src/kernel/vatManager.js b/src/kernel/vatManager.js index 5a1c307ddae..136ea224194 100644 --- a/src/kernel/vatManager.js +++ b/src/kernel/vatManager.js @@ -114,30 +114,40 @@ export default function makeVatManager( // syscall handlers: these are wrapped by the 'syscall' object and made // available to userspace - function doSend(targetSlot, method, argsString, vatSlots) { + function doSend(targetSlot, method, argsString, vatSlots, resultSlot) { insist(`${targetSlot}` === targetSlot, 'non-string targetSlot'); // TODO: disable send-to-self for now, qv issue #43 const target = mapVatSlotToKernelSlot(targetSlot); kdebug(`syscall[${vatID}].send(vat:${targetSlot}=ker:${target}).${method}`); const slots = vatSlots.map(slot => mapVatSlotToKernelSlot(slot)); - // who will decide the answer? If the message is being queued for a - // promise, then the kernel will decide (when the answer gets - // resolved). If it is going to a specific export, the exporting vat - // gets to decide. - let decider; - if (parseKernelSlot(target).type === 'object') { - decider = kernelKeeper.ownerOfKernelObject(target); + let result; + if (resultSlot) { + insistVatType('promise', resultSlot); + result = mapVatSlotToKernelSlot(resultSlot); + insistKernelType('promise', result); + // The promise must be unresolved, and this Vat must be the decider. + // The most common case is that 'resultSlot' is a new exported promise + // (p+NN). But it might be a previously-imported promise (p-NN) that + // they got in a deliver() call, which gave them resolution authority. + const p = kernelKeeper.getKernelPromise(result); + insist( + p.state === 'unresolved', + `send() result ${result} is already resolved`, + ); + insist( + p.decider === vatID, + `send() result ${result} is decided by ${p.decider} not ${vatID}`, + ); + p.decider = undefined; // resolution authority now held by run-queue } - kdebug(` ^target is ${target}`); - const kernelPromiseID = createPromiseWithDecider(decider); + const msg = { method, argsString, slots, - kernelResolverID: kernelPromiseID, + result, }; send(target, msg); - return mapKernelSlotToVatSlot(kernelPromiseID); } function doCreatePromise() { @@ -146,8 +156,7 @@ export default function makeVatManager( kdebug( `syscall[${vatID}].createPromise -> (vat:${p}=ker:${kernelPromiseID})`, ); - const r = vatKeeper.mapKernelPromiseToVatResolver(kernelPromiseID); // temporary - return harden({ promiseID: p, resolverID: r }); + return p; } function doSubscribe(promiseID) { @@ -199,76 +208,61 @@ export default function makeVatManager( } } - /* - function doRedirect(resolverID, targetPromiseID) { - const { id } = mapVatSlotToKernelSlot({ type: 'resolver', id: resolverID }); - if (!kernelKeeper.hasKernelPromise(id)) { - throw new Error(`unknown kernelPromise id '${id}'`); - } - const p = kernelKeeper.getKernelPromise(id); - if (p.state !== 'unresolved') { - throw new Error(`kernelPromise[${id}] is '${p.state}', not 'unresolved'`); - } - - let { id: targetID } = mapVatSlotToKernelSlot({ type: 'promise', id: targetPromiseID }); - if (!kernelKeeper.hasKernelPromise(targetID)) { - throw new Error(`unknown kernelPromise id '${targetID}'`); - } - - targetID = chaseRedirections(targetID); - const target = kernelKeeper.getKernelPromise(targetID); - - for (let s of p.subscribers) { - // TODO: we need to remap their subscriptions, somehow - } - - p.state = 'redirected'; - delete p.decider; - const subscribers = p.subscribers; - delete p.subscribers; - p.redirectedTo = targetID; - if (p.state !== 'unresolved') { - throw new Error(`kernelPromise[${id}] is '${p.state}', not 'unresolved'`); - } - } */ - - function doFulfillToData(resolverID, fulfillData, vatSlots) { - insistVatType('resolver', resolverID); - const kp = mapVatSlotToKernelSlot(resolverID); + function doFulfillToData(promiseID, fulfillData, vatSlots) { + insistVatType('promise', promiseID); + const kp = mapVatSlotToKernelSlot(promiseID); insistKernelType('promise', kp); insist(kernelKeeper.hasKernelPromise(kp), `unknown kernelPromise '${kp}'`); + const p = kernelKeeper.getKernelPromise(kp); + insist(p.state === 'unresolved', `${kp} was already resolved`); + insist( + p.decider === vatID, + `${kp} is decided by ${p.decider}, not ${vatID}`, + ); const slots = vatSlots.map(slot => mapVatSlotToKernelSlot(slot)); kdebug( - `syscall[${vatID}].fulfillData(${resolverID}/${kp}) = ${fulfillData} ${JSON.stringify( + `syscall[${vatID}].fulfillData(${promiseID}/${kp}) = ${fulfillData} ${JSON.stringify( vatSlots, )}/${JSON.stringify(slots)}`, ); fulfillToData(kp, fulfillData, slots); } - function doFulfillToPresence(resolverID, slot) { - insistVatType('resolver', resolverID); - const kp = mapVatSlotToKernelSlot(resolverID); + function doFulfillToPresence(promiseID, slot) { + insistVatType('promise', promiseID); + const kp = mapVatSlotToKernelSlot(promiseID); insistKernelType('promise', kp); insist(kernelKeeper.hasKernelPromise(kp), `unknown kernelPromise '${kp}'`); + const p = kernelKeeper.getKernelPromise(kp); + insist(p.state === 'unresolved', `${kp} was already resolved`); + insist( + p.decider === vatID, + `${kp} is decided by ${p.decider}, not ${vatID}`, + ); const targetSlot = mapVatSlotToKernelSlot(slot); kdebug( - `syscall[${vatID}].fulfillToPresence(${resolverID}/${kp}) = ${slot}/${targetSlot})`, + `syscall[${vatID}].fulfillToPresence(${promiseID}/${kp}) = ${slot}/${targetSlot})`, ); fulfillToPresence(kp, targetSlot); } - function doReject(resolverID, rejectData, vatSlots) { - insistVatType('resolver', resolverID); - const kp = mapVatSlotToKernelSlot(resolverID); + function doReject(promiseID, rejectData, vatSlots) { + insistVatType('promise', promiseID); + const kp = mapVatSlotToKernelSlot(promiseID); insistKernelType('promise', kp); insist(kernelKeeper.hasKernelPromise(kp), `unknown kernelPromise '${kp}'`); + const p = kernelKeeper.getKernelPromise(kp); + insist(p.state === 'unresolved', `${kp} was already resolved`); + insist( + p.decider === vatID, + `${kp} is decided by ${p.decider}, not ${vatID}`, + ); const slots = vatSlots.map(slot => mapVatSlotToKernelSlot(slot)); kdebug( - `syscall[${vatID}].reject(${resolverID}/${kp}) = ${rejectData} ${JSON.stringify( + `syscall[${vatID}].reject(${promiseID}/${kp}) = ${rejectData} ${JSON.stringify( vatSlots, )}/${JSON.stringify(slots)}`, ); @@ -302,6 +296,7 @@ export default function makeVatManager( const syscall = harden({ send(...args) { const promiseID = inReplay ? replay('send', ...args) : doSend(...args); + // todo: remove return value transcriptAddSyscall(['send', ...args], promiseID); return promiseID; }, @@ -377,6 +372,7 @@ export default function makeVatManager( kdebug(`process ${JSON.stringify(message)}`); const { type } = message; if (type === 'deliver') { + // console.log(` process ${vatID} ${message.msg.method}() -> result=${message.msg.result}`); const { target, msg } = message; // temporary, until we allow delivery of pipelined messages to vats // which have opted-in @@ -384,9 +380,22 @@ export default function makeVatManager( const targetSlot = mapKernelSlotToVatSlot(target); insist(parseVatSlot(targetSlot).allocatedByVat, `deliver() to wrong vat`); const inputSlots = msg.slots.map(slot => mapKernelSlotToVatSlot(slot)); - const resolverID = - msg.kernelResolverID && - vatKeeper.mapKernelPromiseToVatResolver(msg.kernelResolverID); + let resultSlot; + if (msg.result) { + insistKernelType('promise', msg.result); + const p = kernelKeeper.getKernelPromise(msg.result); + insist( + p.state === 'unresolved', + `result ${msg.result} already resolved`, + ); + insist( + !p.decider, + `result ${msg.result} already has decider ${p.decider}`, + ); + resultSlot = vatKeeper.mapKernelSlotToVatSlot(msg.result); + insistVatType('promise', resultSlot); + p.decider = vatID; + } return doProcess( [ 'deliver', @@ -394,29 +403,18 @@ export default function makeVatManager( msg.method, msg.argsString, inputSlots, - resolverID, + resultSlot, ], `vat[${vatID}][${targetSlot}].${msg.method} dispatch failed`, ); } - /* - if (type === 'subscribe') { - const { kernelPromiseID, vatID } = message; - const relativeID = mapKernelSlotToVatSlot({ - type: 'resolver', - id: kernelPromiseID, - }).id; - return doProcess(['subscribe', relativeID], - `vat[${vatID}].promise[${relativeID}] subscribe failed`); - } - */ - if (type === 'notifyFulfillToData') { const { kernelPromiseID } = message; const kp = kernelKeeper.getKernelPromise(kernelPromiseID); const vpid = mapKernelSlotToVatSlot(kernelPromiseID); const slots = kp.fulfillSlots.map(slot => mapKernelSlotToVatSlot(slot)); + // console.log(` ${vatID} ${message.type} ${message.kernelPromiseID}`, kp.fulfillData, kp.fulfillSlots); return doProcess( ['notifyFulfillToData', vpid, kp.fulfillData, slots], `vat[${vatID}].promise[${vpid}] fulfillToData failed`, @@ -428,6 +426,7 @@ export default function makeVatManager( const kp = kernelKeeper.getKernelPromise(kernelPromiseID); const vpid = mapKernelSlotToVatSlot(kernelPromiseID); const slot = mapKernelSlotToVatSlot(kp.fulfillSlot); + // console.log(` ${vatID} ${message.type} ${message.kernelPromiseID}`, kp.fulfillSlot); return doProcess( ['notifyFulfillToPresence', vpid, slot], `vat[${vatID}].promise[${vpid}] fulfillToPresence failed`, diff --git a/src/vats/comms/clist.js b/src/vats/comms/clist.js index 1d100d426fb..d88526c7458 100644 --- a/src/vats/comms/clist.js +++ b/src/vats/comms/clist.js @@ -119,6 +119,55 @@ export function mapOutbound(state, remoteID, s, syscall) { return remote.toRemote.get(s); } +export function mapOutboundResult(state, remoteID, s) { + // We're sending a slot to a remote system for use as a result. We must do + // some additional who-is-the-decider checks. + const remote = getRemote(state, remoteID); + insistVatType('promise', s); + + if (!state.promiseTable.has(s)) { + state.promiseTable.set(s, { + owner: null, // todo: what is this for anyways? + resolved: false, + decider: null, // for a brief moment, we're the decider + subscriber: null, + }); + } + const p = state.promiseTable.get(s); + // we (the local machine) must have resolution authority, which happens for + // new promises, and for old ones that arrived as the 'result' of inbound + // messages from remote machines (transferring authority to us). + insist(!p.decider, `result ${s} has decider ${p.decider}, not us`); + insist(!p.resolved, `result ${s} is already resolved`); + // if we received this promise from remote1, we can send it back to them, + // but we can't send it to any other remote. + insist( + !p.subscriber || p.subscriber === remoteID, + `result ${s} has subscriber ${p.subscriber}, not none or ${remoteID}`, + ); + // todo: if we previously held resolution authority for this promise, then + // transferred it to some local vat, we'll have subscribed to the kernel to + // hear about it. If we then get the authority back again, we no longer + // want to hear about its resolution (since we're the ones doing the + // resolving), but the kernel still thinks of us as subscribing, so we'll + // get a bogus dispatch.notifyFulfill*. Currently we throw an error, which + // is currently ignored but might prompt a vat shutdown in the future. + + p.decider = remoteID; // transfer authority to recipient + + const existing = remote.toRemote.get(s); + if (!existing) { + const index = remote.nextPromiseIndex; + remote.nextPromiseIndex += 1; + // the recipient receives rp-NN + const rs = makeRemoteSlot('promise', false, index); + remote.toRemote.set(s, rs); + remote.fromRemote.set(flipRemoteSlot(rs), s); + } + + return remote.toRemote.get(s); +} + export function mapInbound(state, remoteID, s, syscall) { // We're receiving a slot from a remote system. If they've sent it to us // previously, or if we're the ones who sent it to them earlier, it will be @@ -147,7 +196,8 @@ export function mapInbound(state, remoteID, s, syscall) { if (allocatedByRecipient) { throw new Error(`promises not implemented yet`); } else { - const { promiseID, resolverID } = syscall.createPromise(); + // todo: temporary, replace with locally-allocated p+NN index + const promiseID = syscall.createPromise(); remote.fromRemote.set(s, promiseID); remote.toRemote.set(promiseID, s); state.promiseTable.set(promiseID, { @@ -155,11 +205,8 @@ export function mapInbound(state, remoteID, s, syscall) { resolved: false, decider: remoteID, subscriber: null, - resolverID, // temporary }); - console.log( - `inbound promise ${s} mapped to ${promiseID}/${resolverID}`, - ); + console.log(`inbound promise ${s} mapped to ${promiseID}`); } } else if (type === 'resolver') { throw new Error(`resolvers not implemented yet`); diff --git a/src/vats/comms/controller.js b/src/vats/comms/controller.js index bec6c010fad..7272bb848bb 100644 --- a/src/vats/comms/controller.js +++ b/src/vats/comms/controller.js @@ -19,7 +19,7 @@ export function deliverToController( method, data, slots, - resolverID, + result, syscall, ) { function doAddRemote(args) { @@ -30,7 +30,7 @@ export function deliverToController( } const transmitterID = slots[args[1].index]; const { receiverID } = addRemote(state, name, transmitterID); - syscall.fulfillToPresence(resolverID, receiverID); + syscall.fulfillToPresence(result, receiverID); } function doAddEgress(args) { @@ -43,7 +43,7 @@ export function deliverToController( } const localRef = slots[args[2].index]; addEgress(state, remoteID, remoteRefID, localRef); - syscall.fulfillToData(resolverID, UNDEFINED, []); + syscall.fulfillToData(result, UNDEFINED, []); } function doAddIngress(args) { @@ -52,7 +52,7 @@ export function deliverToController( const remoteID = state.names.get(remoteName); const remoteRefID = Nat(args[1]); const localRef = addIngress(state, remoteID, remoteRefID); - syscall.fulfillToPresence(resolverID, localRef); + syscall.fulfillToPresence(result, localRef); } // This is a degenerate form of deserialization, just enough to handle the diff --git a/src/vats/comms/dispatch.js b/src/vats/comms/dispatch.js index 0eaeb779c92..966e0e0d86e 100644 --- a/src/vats/comms/dispatch.js +++ b/src/vats/comms/dispatch.js @@ -25,17 +25,19 @@ export function buildCommsDispatch(syscall, _state, _helpers) { // our root object (o+0) is the Comms Controller const controller = makeVatSlot('object', true, 0); - function deliver(target, method, argsbytes, caps, resolverID) { + function deliver(target, method, argsbytes, caps, result) { if (target === controller) { return deliverToController( state, method, argsbytes, caps, - resolverID, + result, syscall, ); } + // console.log(`comms.deliver ${target} r=${result}`); + // dumpState(state); if (state.objectTable.has(target)) { insist( method.indexOf(':') === -1 && method.indexOf(';') === -1, @@ -48,7 +50,7 @@ export function buildCommsDispatch(syscall, _state, _helpers) { method, argsbytes, caps, - resolverID, + result, ); return transmit(syscall, state, remoteID, body); } @@ -73,6 +75,7 @@ export function buildCommsDispatch(syscall, _state, _helpers) { // resolveLocal(promiseID, { type: 'data', data, slots }); // } // console.log(`notifyFulfillToData ${promiseID}`); + // dumpState(state); const [remoteID, body] = resolvePromiseToRemote(syscall, state, promiseID, { type: 'data', data, @@ -81,6 +84,13 @@ export function buildCommsDispatch(syscall, _state, _helpers) { if (remoteID) { return transmit(syscall, state, remoteID, body); } + // todo: if we previously held resolution authority for this promise, then + // transferred it to some local vat, we'll have subscribed to the kernel to + // hear about it. If we then get the authority back again, we no longer + // want to hear about its resolution (since we're the ones doing the + // resolving), but the kernel still thinks of us as subscribing, so we'll + // get a bogus dispatch.notifyFulfill*. Currently we throw an error, which + // is currently ignored but might prompt a vat shutdown in the future. throw new Error(`unknown promise ${promiseID}`); } diff --git a/src/vats/comms/inbound.js b/src/vats/comms/inbound.js index b0a7f7173ac..c8388bce696 100644 --- a/src/vats/comms/inbound.js +++ b/src/vats/comms/inbound.js @@ -26,22 +26,28 @@ export function deliverFromRemote(syscall, state, remoteID, message) { .slice(3) .map(s => mapInbound(state, remoteID, s, syscall)); const body = message.slice(sci + 1); - // todo: sendOnly if (!result.length) - const p = syscall.send(target, method, body, msgSlots); - syscall.subscribe(p); - // console.log(`-- deliverFromRemote, result=${result} local p=${p}`); - // for now p is p-NN, later we will allocate+provide p+NN instead + let r; if (result.length) { + // todo: replace with mapInboundResult insistRemoteType('promise', result); - insist(!parseRemoteSlot(result).allocatedByRecipient, result); - state.promiseTable.set(p, { + insist(!parseRemoteSlot(result).allocatedByRecipient, result); // temp? + // for now p is p-NN, later we will allocate+provide p+NN instead + r = syscall.createPromise(); // temporary + state.promiseTable.set(r, { owner: remoteID, resolved: false, decider: null, subscriber: remoteID, }); - remote.fromRemote.set(result, p); - remote.toRemote.set(p, flipRemoteSlot(result)); + remote.fromRemote.set(result, r); + remote.toRemote.set(r, flipRemoteSlot(result)); + } + // else it's a sendOnly() + syscall.send(target, method, body, msgSlots, r); + if (r) { + // todo: can/should we subscribe to this early, before syscall.send()? + // probably not. + syscall.subscribe(r); } // dumpState(state); } else if (command === 'resolve') { @@ -70,15 +76,14 @@ export function deliverFromRemote(syscall, state, remoteID, message) { p.decider === remoteID, `${p.decider} is the decider of ${target}, not ${remoteID}`, ); - const { resolverID } = p; const slots = remoteSlots.map(s => mapInbound(state, remoteID, s, syscall)); const body = message.slice(sci + 1); if (type === 'object') { - syscall.fulfillToPresence(resolverID, slots[0]); + syscall.fulfillToPresence(target, slots[0]); } else if (type === 'data') { - syscall.fulfillToData(resolverID, body, slots); + syscall.fulfillToData(target, body, slots); } else if (type === 'reject') { - syscall.reject(resolverID, body, slots); + syscall.reject(target, body, slots); } else { throw new Error(`unknown resolution type ${type} in ${message}`); } diff --git a/src/vats/comms/outbound.js b/src/vats/comms/outbound.js index bc4033e76a1..f9a40fb81c6 100644 --- a/src/vats/comms/outbound.js +++ b/src/vats/comms/outbound.js @@ -1,12 +1,7 @@ -import { makeVatSlot, insistVatType } from '../parseVatSlots'; -import { - flipRemoteSlot, - insistRemoteType, - makeRemoteSlot, -} from './parseRemoteSlot'; -import { getOutbound, mapOutbound } from './clist'; -import { allocatePromiseIndex } from './state'; -import { getRemote, insistRemoteID } from './remote'; +import { insistVatType } from '../parseVatSlots'; +import { insistRemoteType } from './parseRemoteSlot'; +import { getOutbound, mapOutbound, mapOutboundResult } from './clist'; +import { insistRemoteID } from './remote'; import { insist } from '../../kernel/insist'; export function deliverToRemote( @@ -16,12 +11,11 @@ export function deliverToRemote( method, data, slots, - resolverID, + result, ) { // this object lives on 'remoteID', so we send messages at them const remoteID = state.objectTable.get(target); insist(remoteID !== undefined, `oops ${target}`); - const remote = getRemote(state, remoteID); const remoteTargetSlot = getOutbound(state, remoteID, target); const remoteMessageSlots = slots.map(s => @@ -32,28 +26,17 @@ export function deliverToRemote( rmss = `:${rmss}`; } let remoteResultSlot = ''; - if (resolverID) { - insistVatType('resolver', resolverID); - // outbound: resolverID=r-NN -> rp-NN - // inbound: rp+NN -> r-NN - const pIndex = allocatePromiseIndex(state); - const p = makeVatSlot('promise', true, pIndex); // p+NN - state.promiseTable.set(p, { - owner: null, - resolved: false, - decider: remoteID, - subscriber: null, - resolverID, // r-NN, temporary - }); - remoteResultSlot = makeRemoteSlot('promise', false, pIndex); // rp-NN - remote.toRemote.set(p, remoteResultSlot); // p+NN -> rp-NN - remote.fromRemote.set(flipRemoteSlot(remoteResultSlot), p); // rp+NN -> p+NN + if (result) { + insistVatType('promise', result); + // outbound: promiseID=p-NN -> rp-NN + // inbound: rp+NN -> p-NN + remoteResultSlot = mapOutboundResult(state, remoteID, result); } // now render the transmission. todo: 'method' lives in the transmission // for now, but will be moved to 'data' const msg = `deliver:${remoteTargetSlot}:${method}:${remoteResultSlot}${rmss};${data}`; - // console.log(`deliverToRemote(target=${target}/${remoteTargetSlot}, result=${resolverID}/${remoteResultSlot}) leaving state as:`); + // console.log(`deliverToRemote(target=${target}/${remoteTargetSlot}, result=${result}/${remoteResultSlot}) leaving state as:`); // dumpState(state); return [remoteID, msg]; } diff --git a/test/files-devices/bootstrap-0.js b/test/files-devices/bootstrap-0.js index b18d54802ae..087ccc0fa6c 100644 --- a/test/files-devices/bootstrap-0.js +++ b/test/files-devices/bootstrap-0.js @@ -3,7 +3,7 @@ const harden = require('@agoric/harden'); export default function setup(syscall, state, helpers, _devices) { const { log } = helpers; const dispatch = harden({ - deliver(facetid, method, argsbytes, caps, _resolverID) { + deliver(facetid, method, argsbytes, caps, _result) { log(argsbytes); log(JSON.stringify(caps)); }, diff --git a/test/files-devices/bootstrap-1.js b/test/files-devices/bootstrap-1.js index dd1567b0860..6e9b684a9c0 100644 --- a/test/files-devices/bootstrap-1.js +++ b/test/files-devices/bootstrap-1.js @@ -4,7 +4,7 @@ export default function setup(syscall, state, helpers, _devices) { const { log } = helpers; let deviceRef; const dispatch = harden({ - deliver(facetid, method, argsbytes, caps, _resolverID) { + deliver(facetid, method, argsbytes, caps, _result) { if (method === 'bootstrap') { const { args } = JSON.parse(argsbytes); const deviceIndex = args[2].d1.index; diff --git a/test/test-comms-integration.js b/test/test-comms-integration.js index 6c39ef94405..84527b4a123 100644 --- a/test/test-comms-integration.js +++ b/test/test-comms-integration.js @@ -21,6 +21,13 @@ export function runTest(testType, withSES, testStr, expectedLogs) { const expected = setupLogs.concat(expectedLogs); testType(testStr, async t => { const c = await runVats(t, withSES, [testStr]); + /* + while (c.dump().runQueue.length) { + console.log('-'); + console.log(`--- turn starts`); + await c.step(); + //console.log(c.dump().kernelTable); + } */ await c.run(); const { log } = c.dump(); t.deepEqual(log, expected); diff --git a/test/test-controller.js b/test/test-controller.js index 71a7bf06ac9..35ccd283847 100644 --- a/test/test-controller.js +++ b/test/test-controller.js @@ -28,7 +28,7 @@ async function simpleCall(t, withSES) { { msg: { argsString: 'args', - kernelResolverID: null, + result: null, method: 'foo', slots: [], }, @@ -119,7 +119,7 @@ async function bootstrapExport(t, withSES) { msg: { argsString: '{"args":[[],{"_bootstrap":{"@qclass":"slot","index":0},"left":{"@qclass":"slot","index":1},"right":{"@qclass":"slot","index":2}},{"_dummy":"dummy"}]}', - kernelResolverID: null, + result: null, method: 'bootstrap', slots: [boot0, left0, right0], }, @@ -157,7 +157,7 @@ async function bootstrapExport(t, withSES) { method: 'foo', argsString: '{"args":[1,{"@qclass":"slot","index":0}]}', slots: [right0], - kernelResolverID: fooP, + result: fooP, }, }, ]); @@ -185,7 +185,7 @@ async function bootstrapExport(t, withSES) { method: 'bar', argsString: '{"args":[2,{"@qclass":"slot","index":0}]}', slots: [right0], - kernelResolverID: barP, + result: barP, }, }, { type: 'notifyFulfillToData', vatID: '_bootstrap', kernelPromiseID: fooP }, diff --git a/test/test-kernel.js b/test/test-kernel.js index c7be378d150..f0b0d92c5f8 100644 --- a/test/test-kernel.js +++ b/test/test-kernel.js @@ -55,7 +55,7 @@ test('simple call', async t => { method: 'foo', argsString: 'args', slots: [], - kernelResolverID: null, + result: null, }, }, ]); @@ -102,7 +102,7 @@ test('map inbound', async t => { { msg: { argsString: 'args', - kernelResolverID: null, + result: null, method: 'foo', slots: [koFor5, koFor6], }, @@ -147,12 +147,14 @@ test('outbound call', async t => { const kernel = buildKernel({ setImmediate }); const log = []; let v1tovat25; + const p7 = 'p+7'; function setup1(syscall) { function deliver(facetID, method, argsString, slots) { // console.log(`d1/${facetID} called`); log.push(['d1', facetID, method, argsString, slots]); - const pid = syscall.send(v1tovat25, 'bar', 'bargs', [v1tovat25, 'o+7']); + const pid = syscall.createPromise(); + syscall.send(v1tovat25, 'bar', 'bargs', [v1tovat25, 'o+7', p7], pid); log.push(['d1', 'promiseid', pid]); } return { deliver }; @@ -163,6 +165,7 @@ test('outbound call', async t => { function deliver(facetID, method, argsString, slots) { // console.log(`d2/${facetID} called`); log.push(['d2', facetID, method, argsString, slots]); + log.push(['d2 promises', kernel.dump().promises]); } return { deliver }; } @@ -194,7 +197,7 @@ test('outbound call', async t => { { msg: { argsString: 'args', - kernelResolverID: null, + result: null, method: 'foo', slots: [], }, @@ -218,8 +221,8 @@ test('outbound call', async t => { msg: { method: 'bar', argsString: 'bargs', - slots: [vat2Obj5, 'ko22'], - kernelResolverID: 'kp40', + slots: [vat2Obj5, 'ko22', 'kp41'], + result: 'kp40', }, }, ]); @@ -227,24 +230,72 @@ test('outbound call', async t => { { id: 'kp40', state: 'unresolved', - decider: 'vat2', + decider: undefined, + subscribers: [], + queue: [], + }, + { + id: 'kp41', + state: 'unresolved', + decider: 'vat1', subscribers: [], queue: [], }, ]); + kt.push(['ko22', 'vat1', 'o+7']); kt.push(['kp40', 'vat1', 'p-60']); + kt.push(['kp41', 'vat1', p7]); checkKT(t, kernel, kt); await kernel.step(); - t.deepEqual(log, [['d2', 'o+5', 'bar', 'bargs', ['o+5', 'o-50']]]); - // our temporary handling of resolverIDs causes vat2's clist to have a - // funny mapping. (kp40->p-60, p-60->kp40, r-60->kp40). The dump() function - // assumes a strictly bijective mapping and doesn't include the surjective - // r-60->kp40 edge. So instead of ['kp40', 'vat2', 'r-60'], we see: - kt.push(['kp40', 'vat2', 'p-60']); + // this checks that the decider was set to vat2 while bar() was delivered + t.deepEqual(log, [ + ['d2', 'o+5', 'bar', 'bargs', ['o+5', 'o-50', 'p-60']], + [ + 'd2 promises', + [ + { + id: 'kp40', + state: 'unresolved', + decider: 'vat2', + subscribers: [], + queue: [], + }, + { + id: 'kp41', + state: 'unresolved', + decider: 'vat1', + subscribers: [], + queue: [], + }, + ], + ], + ]); kt.push(['ko22', 'vat2', 'o-50']); + kt.push(['kp41', 'vat2', 'p-60']); + kt.push(['kp40', 'vat2', 'p-61']); checkKT(t, kernel, kt); + t.deepEqual(kernel.dump().promises, [ + { + id: 'kp40', + state: 'unresolved', + decider: 'vat2', + subscribers: [], + queue: [], + }, + { + id: 'kp41', + state: 'unresolved', + decider: 'vat1', + // Sending a promise from vat1 to vat2 doesn't cause vat2 to be + // subscribed unless they want it. Liveslots will always subscribe, + // because we don't have enough hooks into Promises to detect a + // .then(), but non-liveslots vats don't have to. + subscribers: [], + queue: [], + }, + ]); t.end(); }); @@ -261,7 +312,8 @@ test('three-party', async t => { function deliver(facetID, method, argsString, slots) { console.log(`vatA/${facetID} called`); log.push(['vatA', facetID, method, argsString, slots]); - const pid = syscall.send(bobForA, 'intro', 'bargs', [carolForA]); + const pid = syscall.createPromise(); + syscall.send(bobForA, 'intro', 'bargs', [carolForA], pid); log.push(['vatA', 'promiseID', pid]); } return { deliver }; @@ -298,7 +350,7 @@ test('three-party', async t => { // different than bob's, so we can check that the promiseID coming back // from send() is scoped to the sender, not the recipient const ap = aliceSyscall.createPromise(); - t.equal(ap.promiseID, 'p-60'); + t.equal(ap, 'p-60'); const data = kernel.dump(); t.deepEqual(data.vatTables, [ @@ -312,7 +364,7 @@ test('three-party', async t => { [bob, 'vatB', 'o+5'], [carol, 'vatA', carolForA], [carol, 'vatC', 'o+6'], - ['kp40', 'vatA', ap.promiseID], + ['kp40', 'vatA', ap], ]; checkKT(t, kernel, kt); t.deepEqual(log, []); @@ -333,7 +385,7 @@ test('three-party', async t => { method: 'intro', argsString: 'bargs', slots: [carol], - kernelResolverID: 'kp41', + result: 'kp41', }, }, ]); @@ -348,7 +400,7 @@ test('three-party', async t => { { id: 'kp41', state: 'unresolved', - decider: 'vatB', + decider: undefined, subscribers: [], queue: [], }, @@ -378,7 +430,7 @@ test('createPromise', async t => { t.deepEqual(kernel.dump().promises, []); const pr = syscall.createPromise(); - t.deepEqual(pr, { promiseID: 'p-60', resolverID: 'r-60' }); + t.deepEqual(pr, 'p-60'); t.deepEqual(kernel.dump().promises, [ { id: 'kp40', @@ -428,10 +480,10 @@ test('transfer promise', async t => { const A = kernel.addImport('vatB', alice); const pr1 = syscallA.createPromise(); - t.deepEqual(pr1, { promiseID: 'p-60', resolverID: 'r-60' }); + t.deepEqual(pr1, 'p-60'); // we send pr2 const pr2 = syscallA.createPromise(); - t.deepEqual(pr2, { promiseID: 'p-61', resolverID: 'r-61' }); + t.deepEqual(pr2, 'p-61'); const kt = [ ['ko20', 'vatA', 'o+6'], @@ -461,7 +513,7 @@ test('transfer promise', async t => { checkPromises(t, kernel, kp); // sending a promise should arrive as a promise - syscallA.send(B, 'foo1', 'args', [pr2.promiseID]); + syscallA.send(B, 'foo1', 'args', [pr2]); t.deepEqual(kernel.dump().runQueue, [ { vatID: 'vatB', @@ -471,80 +523,41 @@ test('transfer promise', async t => { method: 'foo1', argsString: 'args', slots: ['kp41'], - kernelResolverID: 'kp42', + result: undefined, }, }, ]); - kt.push(['kp42', 'vatA', 'p-62']); // promise for answer of foo1() checkKT(t, kernel, kt); - kp.push({ - id: 'kp42', - state: 'unresolved', - decider: 'vatB', - subscribers: [], - queue: [], - }); checkPromises(t, kernel, kp); await kernel.run(); t.deepEqual(logB.shift(), ['o+5', 'foo1', 'args', ['p-60']]); t.deepEqual(logB, []); kt.push(['kp41', 'vatB', 'p-60']); // pr2 for B - kt.push(['kp42', 'vatB', 'p-61']); // resolver for answer of foo1() checkKT(t, kernel, kt); // sending it a second time should arrive as the same thing - syscallA.send(B, 'foo2', 'args', [pr2.promiseID]); + syscallA.send(B, 'foo2', 'args', [pr2]); await kernel.run(); t.deepEqual(logB.shift(), ['o+5', 'foo2', 'args', ['p-60']]); - t.deepEqual(logB, []); - kt.push(['kp43', 'vatA', 'p-63']); // promise for answer of foo2() - kt.push(['kp43', 'vatB', 'p-62']); // resolver for answer of foo2() checkKT(t, kernel, kt); - - kp.push({ - id: 'kp43', - state: 'unresolved', - decider: 'vatB', - subscribers: [], - queue: [], - }); checkPromises(t, kernel, kp); // sending it back should arrive with the sender's index syscallB.send(A, 'foo3', 'args', ['p-60']); await kernel.run(); - t.deepEqual(logA.shift(), ['o+6', 'foo3', 'args', [pr2.promiseID]]); + t.deepEqual(logA.shift(), ['o+6', 'foo3', 'args', [pr2]]); t.deepEqual(logA, []); - kt.push(['kp44', 'vatA', 'p-64']); // resolver for answer of foo3() - kt.push(['kp44', 'vatB', 'p-63']); // promise for answer of foo3() checkKT(t, kernel, kt); + checkPromises(t, kernel, kp); // sending it back a second time should arrive as the same thing syscallB.send(A, 'foo4', 'args', ['p-60']); await kernel.run(); - t.deepEqual(logA.shift(), ['o+6', 'foo4', 'args', [pr2.promiseID]]); + t.deepEqual(logA.shift(), ['o+6', 'foo4', 'args', [pr2]]); t.deepEqual(logA, []); - - kp.push({ - id: 'kp44', - state: 'unresolved', - decider: 'vatA', - subscribers: [], - queue: [], - }); - kp.push({ - id: 'kp45', - state: 'unresolved', - decider: 'vatA', - subscribers: [], - queue: [], - }); checkPromises(t, kernel, kp); - - kt.push(['kp45', 'vatA', 'p-65']); // resolver for answer of foo4() - kt.push(['kp45', 'vatB', 'p-64']); // promise for answer of foo4() checkKT(t, kernel, kt); t.end(); @@ -565,10 +578,10 @@ test('subscribe to promise', async t => { await kernel.start(); const pr = syscall.createPromise(); - t.deepEqual(pr, { promiseID: 'p-60', resolverID: 'r-60' }); + t.deepEqual(pr, 'p-60'); t.deepEqual(kernel.dump().kernelTable, [['kp40', 'vat1', 'p-60']]); - syscall.subscribe(pr.promiseID); + syscall.subscribe(pr); t.deepEqual(kernel.dump().promises, [ { id: 'kp40', @@ -608,7 +621,7 @@ test.skip('promise redirection', async t => { ['vat1', 'resolver', 31, 41], ]); - syscall.subscribe(pr1.promiseID); + syscall.subscribe(pr1); t.deepEqual(kernel.dump().promises, [ { id: 40, @@ -626,7 +639,7 @@ test.skip('promise redirection', async t => { }, ]); - syscall.redirect(pr1.resolverID, pr2.promiseID); + syscall.redirect(pr1, pr2); t.deepEqual(log, []); // vat is not notified t.deepEqual(kernel.dump().promises, [ { @@ -669,7 +682,7 @@ test('promise resolveToData', async t => { const pr = syscall.createPromise(); t.deepEqual(kernel.dump().kernelTable, [['kp40', 'vatA', 'p-60']]); - syscall.subscribe(pr.promiseID); + syscall.subscribe(pr); t.deepEqual(kernel.dump().promises, [ { id: 'kp40', @@ -680,9 +693,8 @@ test('promise resolveToData', async t => { }, ]); - syscall.fulfillToData(pr.resolverID, 'args', [alice]); - // the resolverID gets mapped to a kernelPromiseID first, and a - // notifyFulfillToData message is queued + syscall.fulfillToData(pr, 'args', [alice]); + // this causes a notifyFulfillToData message to be queued t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().runQueue, [ { @@ -694,7 +706,7 @@ test('promise resolveToData', async t => { await kernel.step(); // the kernelPromiseID gets mapped back to the vat PromiseID - t.deepEqual(log.shift(), ['notify', pr.promiseID, 'args', [alice]]); + t.deepEqual(log.shift(), ['notify', pr, 'args', [alice]]); t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().promises, [ { @@ -742,7 +754,7 @@ test('promise resolveToPresence', async t => { ]; checkKT(t, kernel, kt); - syscall.subscribe(pr.promiseID); + syscall.subscribe(pr); t.deepEqual(kernel.dump().promises, [ { id: 'kp40', @@ -753,7 +765,7 @@ test('promise resolveToPresence', async t => { }, ]); - syscall.fulfillToPresence(pr.resolverID, bobForAlice); + syscall.fulfillToPresence(pr, bobForAlice); t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().runQueue, [ { @@ -765,7 +777,7 @@ test('promise resolveToPresence', async t => { await kernel.step(); - t.deepEqual(log.shift(), ['notify', pr.promiseID, bobForAlice]); + t.deepEqual(log.shift(), ['notify', pr, bobForAlice]); t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().promises, [ { @@ -807,7 +819,7 @@ test('promise reject', async t => { ]; checkKT(t, kernel, kt); - syscall.subscribe(pr.promiseID); + syscall.subscribe(pr); t.deepEqual(kernel.dump().promises, [ { id: 'kp40', @@ -821,7 +833,7 @@ test('promise reject', async t => { // the resolver-holder calls reject right away, because now we // automatically subscribe - syscall.reject(pr.resolverID, 'args', [bobForAlice]); + syscall.reject(pr, 'args', [bobForAlice]); t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().runQueue, [ { @@ -832,7 +844,7 @@ test('promise reject', async t => { ]); await kernel.step(); - t.deepEqual(log.shift(), ['notify', pr.promiseID, 'args', [bobForAlice]]); + t.deepEqual(log.shift(), ['notify', pr, 'args', [bobForAlice]]); t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().promises, [ { @@ -854,7 +866,8 @@ test('transcript', async t => { function setup(syscall, _state) { function deliver(facetID, _method, _argsString, slots) { if (facetID === aliceForAlice) { - const p = syscall.send(slots[1], 'foo', 'fooarg', []); + const p = syscall.createPromise(); + syscall.send(slots[1], 'foo', 'fooarg', [], p); log.push(p); } } @@ -887,12 +900,13 @@ test('transcript', async t => { 'store', 'args string', [aliceForAlice, bobForAlice], - null, + undefined, ], syscalls: [ + { d: ['createPromise'], response: p1 }, { - d: ['send', bobForAlice, 'foo', 'fooarg', []], - response: p1, + d: ['send', bobForAlice, 'foo', 'fooarg', [], p1], + response: undefined, }, ], }); diff --git a/test/test-liveslots.js b/test/test-liveslots.js index 07fc3f571fb..7446111a0d3 100644 --- a/test/test-liveslots.js +++ b/test/test-liveslots.js @@ -7,6 +7,92 @@ import evaluateExpr from '@agoric/evaluate'; // to get Promise.makeHandled import buildKernel from '../src/kernel/index'; import { makeLiveSlots } from '../src/kernel/liveSlots'; +test('calls', async t => { + const kernel = buildKernel({ setImmediate }); + const log = []; + let syscall; + + function setupBootstrap(syscallBootstrap, _state, _helpers) { + syscall = syscallBootstrap; + function deliver(facetID, method, argsString, slots, result) { + log.push(['deliver', facetID, method, argsString, slots, result]); + } + return { deliver }; + } + kernel.addGenesisVat('bootstrap', setupBootstrap); + + function setup(syscallVat, state, helpers) { + function build(_E, _D) { + return harden({ + one() { + log.push('one'); + }, + two(p) { + log.push(`two ${Promise.resolve(p) === p}`); + p.then(res => log.push(['res', res]), rej => log.push(['rej', rej])); + }, + }); + } + return makeLiveSlots(syscallVat, state, build, helpers.vatID); + } + kernel.addGenesisVat('vat', setup); + + await kernel.start('bootstrap', `[]`); + // cycle past the bootstrap() call + await kernel.step(); + log.shift(); + t.deepEqual(kernel.dump().runQueue, []); + + const root = kernel.addImport('bootstrap', kernel.addExport('vat', 'o+0')); + + // root!one() // sendOnly + syscall.send(root, 'one', JSON.stringify({ args: [] }), [], undefined); + + await kernel.step(); + t.deepEqual(log.shift(), 'one'); + t.deepEqual(kernel.dump().runQueue, []); + // console.log(kernel.dump().runQueue); + + // pr = makePromise() + // root!two(pr.promise) + // pr.resolve('result') + syscall.send( + root, + 'two', + JSON.stringify({ args: [{ '@qclass': 'slot', index: 0 }] }), + ['p+1'], + undefined, + ); + await kernel.step(); + t.deepEqual(log.shift(), 'two true'); + + syscall.fulfillToData('p+1', JSON.stringify('result'), []); + await kernel.step(); + t.deepEqual(log.shift(), ['res', 'result']); + + // pr = makePromise() + // root!two(pr.promise) + // pr.reject('rejection') + + syscall.send( + root, + 'two', + JSON.stringify({ args: [{ '@qclass': 'slot', index: 0 }] }), + ['p+2'], + undefined, + ); + await kernel.step(); + t.deepEqual(log.shift(), 'two true'); + + syscall.reject('p+2', JSON.stringify('rejection'), []); + await kernel.step(); + t.deepEqual(log.shift(), ['rej', 'rejection']); + + // TODO: more calls, more slot types + + t.end(); +}); + test('liveslots pipelines to syscall.send', async t => { const kernel = buildKernel({ setImmediate }); const log = []; @@ -50,13 +136,13 @@ test('liveslots pipelines to syscall.send', async t => { // kernel promise queue for the result of the first, and likewise the // third. await kernel.step(); - const resolverID = kernel.dump().runQueue[0].msg.kernelResolverID; + const { result } = kernel.dump().runQueue[0].msg; const state = JSON.parse(kernel.getState()); - const kp = state.kernelPromises[resolverID]; + const kp = state.kernelPromises[result]; t.equal(kp.queue.length, 1); t.equal(kp.queue[0].method, 'pipe2'); - const resolverID2 = kp.queue[0].kernelResolverID; - const kp2 = state.kernelPromises[resolverID2]; + const result2 = kp.queue[0].result; + const kp2 = state.kernelPromises[result2]; t.equal(kp2.queue.length, 1); t.equal(kp2.queue[0].method, 'pipe3'); diff --git a/test/test-marshal.js b/test/test-marshal.js index d5e8eaa0cea..05c275f7a40 100644 --- a/test/test-marshal.js +++ b/test/test-marshal.js @@ -239,13 +239,10 @@ test('serialize promise', async t => { const log = []; const syscall = { createPromise() { - return { - promiseID: 'p-1', - resolverID: 'r-1', - }; + return 'p-1'; }, - fulfillToData(resolverID, data, slots) { - log.push({ resolverID, data, slots }); + fulfillToData(result, data, slots) { + log.push({ result, data, slots }); }, }; @@ -270,7 +267,7 @@ test('serialize promise', async t => { const { p: pauseP, res: pauseRes } = makePromise(); setImmediate(() => pauseRes()); await pauseP; - t.deepEqual(log, [{ resolverID: 'r-1', data: '5', slots: [] }]); + t.deepEqual(log, [{ result: 'p-1', data: '5', slots: [] }]); t.end(); }); From 5cac54eb105929cf91ca542f0028ea3609401022 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 29 Aug 2019 14:52:25 -0700 Subject: [PATCH 05/16] stop using syscall.createPromise() in liveSlots and comms Use vat-allocated p+NN refs instead, for which the kernel will allocate KernelPromiseTable entries when it first sees them. --- src/kernel/liveSlots.js | 31 +++++++++++++++++-------------- src/vats/comms/clist.js | 12 +++++------- src/vats/comms/inbound.js | 7 ++++--- src/vats/comms/state.js | 6 ++++-- test/test-controller.js | 4 ++-- test/test-marshal.js | 8 ++++---- 6 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/kernel/liveSlots.js b/src/kernel/liveSlots.js index b4e0d63666a..2a9cc5e2c8b 100644 --- a/src/kernel/liveSlots.js +++ b/src/kernel/liveSlots.js @@ -66,6 +66,7 @@ function build(syscall, _state, makeRoot, forVatID) { const slotToVal = new Map(); const importedPromisesByPromiseID = new Map(); let nextExportID = 1; + let nextPromiseID = 5; function allocateExportID() { const exportID = nextExportID; @@ -73,10 +74,14 @@ function build(syscall, _state, makeRoot, forVatID) { return exportID; } + function allocatePromiseID() { + const promiseID = nextPromiseID; + nextPromiseID += 1; + return makeVatSlot('promise', true, promiseID); + } + function exportPromise(p) { - const pid = syscall.createPromise(); - // we ignore the kernel promise, but we use the resolver to notify the - // kernel when our local promise changes state + const pid = allocatePromiseID(); lsdebug(`ls exporting promise ${pid}`); // eslint-disable-next-line no-use-before-define p.then(thenResolve(pid), thenReject(pid)); @@ -185,25 +190,23 @@ function build(syscall, _state, makeRoot, forVatID) { function queueMessage(targetSlot, prop, args) { const ser = m.serialize(harden({ args })); - lsdebug(`ls.qm send(${JSON.stringify(targetSlot)}, ${prop}`); - const promiseID = syscall.createPromise(); // temporary - syscall.send(targetSlot, prop, ser.argsString, ser.slots, promiseID); - insistVatType('promise', promiseID); - lsdebug(` ls.qm got promiseID ${promiseID}`); - const done = makeQueued(promiseID); + const pid = allocatePromiseID(); + const done = makeQueued(pid); + lsdebug(`ls.qm send(${JSON.stringify(targetSlot)}, ${prop}) -> ${pid}`); + syscall.send(targetSlot, prop, ser.argsString, ser.slots, pid); // prepare for notifyFulfillToData/etc - importedPromisesByPromiseID.set(promiseID, done); + importedPromisesByPromiseID.set(pid, done); // ideally we'd wait until someone .thens done.p, but with native // Promises we have no way of spotting that, so subscribe immediately - lsdebug(`ls[${forVatID}].queueMessage.importedPromiseThen ${promiseID}`); - importedPromiseThen(promiseID); + lsdebug(`ls[${forVatID}].queueMessage.importedPromiseThen ${pid}`); + importedPromiseThen(pid); // prepare the serializer to recognize it, if it's used as an argument or // return value - valToSlot.set(done.p, promiseID); - slotToVal.set(promiseID, done.p); + valToSlot.set(done.p, pid); + slotToVal.set(pid, done.p); return done.p; } diff --git a/src/vats/comms/clist.js b/src/vats/comms/clist.js index d88526c7458..093232999a3 100644 --- a/src/vats/comms/clist.js +++ b/src/vats/comms/clist.js @@ -6,6 +6,7 @@ import { parseRemoteSlot, } from './parseRemoteSlot'; import { getRemote } from './remote'; +import { allocatePromise } from './state'; import { insist } from '../../kernel/insist'; export function getOutbound(state, remoteID, target) { @@ -168,7 +169,7 @@ export function mapOutboundResult(state, remoteID, s) { return remote.toRemote.get(s); } -export function mapInbound(state, remoteID, s, syscall) { +export function mapInbound(state, remoteID, s, _syscall) { // We're receiving a slot from a remote system. If they've sent it to us // previously, or if we're the ones who sent it to them earlier, it will be // in the inbound table already. @@ -196,20 +197,17 @@ export function mapInbound(state, remoteID, s, syscall) { if (allocatedByRecipient) { throw new Error(`promises not implemented yet`); } else { - // todo: temporary, replace with locally-allocated p+NN index - const promiseID = syscall.createPromise(); - remote.fromRemote.set(s, promiseID); - remote.toRemote.set(promiseID, s); + const promiseID = allocatePromise(state); state.promiseTable.set(promiseID, { owner: remoteID, resolved: false, decider: remoteID, subscriber: null, }); + remote.fromRemote.set(s, promiseID); + remote.toRemote.set(promiseID, s); console.log(`inbound promise ${s} mapped to ${promiseID}`); } - } else if (type === 'resolver') { - throw new Error(`resolvers not implemented yet`); } else { throw new Error(`unknown type ${type}`); } diff --git a/src/vats/comms/inbound.js b/src/vats/comms/inbound.js index c8388bce696..f33657e30d7 100644 --- a/src/vats/comms/inbound.js +++ b/src/vats/comms/inbound.js @@ -5,6 +5,7 @@ import { parseRemoteSlot, } from './parseRemoteSlot'; import { mapInbound, getInbound } from './clist'; +import { allocatePromise } from './state'; import { insist } from '../../kernel/insist'; export function deliverFromRemote(syscall, state, remoteID, message) { @@ -28,11 +29,11 @@ export function deliverFromRemote(syscall, state, remoteID, message) { const body = message.slice(sci + 1); let r; if (result.length) { - // todo: replace with mapInboundResult + // todo: replace with mapInboundResult, allow pre-existing promises if + // the decider is right insistRemoteType('promise', result); insist(!parseRemoteSlot(result).allocatedByRecipient, result); // temp? - // for now p is p-NN, later we will allocate+provide p+NN instead - r = syscall.createPromise(); // temporary + r = allocatePromise(state); state.promiseTable.set(r, { owner: remoteID, resolved: false, diff --git a/src/vats/comms/state.js b/src/vats/comms/state.js index 561441d5e1a..ea8f7d5d1be 100644 --- a/src/vats/comms/state.js +++ b/src/vats/comms/state.js @@ -1,3 +1,5 @@ +import { makeVatSlot } from '../parseVatSlots'; + export function makeState() { const state = { nextRemoteIndex: 1, @@ -44,8 +46,8 @@ export function dumpState(state) { } } -export function allocatePromiseIndex(state) { +export function allocatePromise(state) { const index = state.nextPromiseIndex; state.nextPromiseIndex += 1; - return index; + return makeVatSlot('promise', true, index); } diff --git a/test/test-controller.js b/test/test-controller.js index 35ccd283847..98b24bc72b1 100644 --- a/test/test-controller.js +++ b/test/test-controller.js @@ -146,7 +146,7 @@ async function bootstrapExport(t, withSES) { ]); kt.push([left0, '_bootstrap', 'o-50']); kt.push([right0, '_bootstrap', 'o-51']); - kt.push([fooP, '_bootstrap', 'p-60']); + kt.push([fooP, '_bootstrap', 'p+5']); checkKT(t, c, kt); t.deepEqual(c.dump().runQueue, [ { @@ -173,7 +173,7 @@ async function bootstrapExport(t, withSES) { ]); kt.push([right0, 'left', 'o-50']); kt.push([fooP, 'left', 'p-60']); - kt.push([barP, 'left', 'p-61']); + kt.push([barP, 'left', 'p+5']); checkKT(t, c, kt); t.deepEqual(c.dump().runQueue, [ diff --git a/test/test-marshal.js b/test/test-marshal.js index 05c275f7a40..4c2028f1f1d 100644 --- a/test/test-marshal.js +++ b/test/test-marshal.js @@ -250,16 +250,16 @@ test('serialize promise', async t => { const { p, res } = makePromise(); t.deepEqual(m.serialize(p), { argsString: '{"@qclass":"slot","index":0}', - slots: ['p-1'], + slots: ['p+5'], }); // serializer should remember the promise t.deepEqual(m.serialize(harden(['other stuff', p])), { argsString: '["other stuff",{"@qclass":"slot","index":0}]', - slots: ['p-1'], + slots: ['p+5'], }); // inbound should recognize it and return the promise - t.deepEqual(m.unserialize('{"@qclass":"slot","index":0}', ['p-1']), p); + t.deepEqual(m.unserialize('{"@qclass":"slot","index":0}', ['p+5']), p); res(5); t.deepEqual(log, []); @@ -267,7 +267,7 @@ test('serialize promise', async t => { const { p: pauseP, res: pauseRes } = makePromise(); setImmediate(() => pauseRes()); await pauseP; - t.deepEqual(log, [{ result: 'p-1', data: '5', slots: [] }]); + t.deepEqual(log, [{ result: 'p+5', data: '5', slots: [] }]); t.end(); }); From 62b09f0ff027101c0cd36acb7d8f5656640cd58a Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 29 Aug 2019 22:25:19 -0700 Subject: [PATCH 06/16] remove syscall.createPromise() from tests --- src/kernel/kernel.js | 4 +- test/test-kernel.js | 414 ++++++++++++++++++------------------------- test/test-marshal.js | 3 - 3 files changed, 172 insertions(+), 249 deletions(-) diff --git a/src/kernel/kernel.js b/src/kernel/kernel.js index 80b505c1125..e7b2dd3784d 100644 --- a/src/kernel/kernel.js +++ b/src/kernel/kernel.js @@ -8,7 +8,7 @@ import makeVatManager from './vatManager'; import makeDeviceManager from './deviceManager'; import makeKernelKeeper from './state/kernelKeeper'; import { insistKernelType, parseKernelSlot } from './parseKernelSlots'; -import { insistVatType, makeVatSlot } from '../vats/parseVatSlots'; +import { makeVatSlot } from '../vats/parseVatSlots'; import { insist } from './insist'; function abbreviateReviver(_, arg) { @@ -236,7 +236,6 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { function addImport(forVatID, what) { const kernelSlot = `${what}`; - insistKernelType('object', kernelSlot); if (!started) { throw new Error('must do kernel.start() before addImport()'); // because otherwise we can't get the vatManager @@ -247,7 +246,6 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { function addExport(fromVatID, what) { const vatSlot = `${what}`; - insistVatType('object', vatSlot); if (!started) { throw new Error('must do kernel.start() before addExport()'); // because otherwise we can't get the vatManager diff --git a/test/test-kernel.js b/test/test-kernel.js index f0b0d92c5f8..2002054cd2b 100644 --- a/test/test-kernel.js +++ b/test/test-kernel.js @@ -2,6 +2,7 @@ /* global setImmediate */ import { test } from 'tape-promise/tape'; import buildKernel from '../src/kernel/index'; +import { makeVatSlot } from '../src/vats/parseVatSlots'; import { checkKT } from './util'; function checkPromises(t, kernel, expected) { @@ -18,6 +19,11 @@ function checkPromises(t, kernel, expected) { t.deepEqual(got, expected); } +function emptySetup(_syscall) { + function deliver() {} + return { deliver }; +} + test('build kernel', async t => { const kernel = buildKernel({ setImmediate }); await kernel.start(); // empty queue @@ -150,12 +156,17 @@ test('outbound call', async t => { const p7 = 'p+7'; function setup1(syscall) { + let nextPromiseIndex = 5; + function allocatePromise() { + const index = nextPromiseIndex; + nextPromiseIndex += 1; + return makeVatSlot('promise', true, index); + } function deliver(facetID, method, argsString, slots) { // console.log(`d1/${facetID} called`); log.push(['d1', facetID, method, argsString, slots]); - const pid = syscall.createPromise(); + const pid = allocatePromise(); syscall.send(v1tovat25, 'bar', 'bargs', [v1tovat25, 'o+7', p7], pid); - log.push(['d1', 'promiseid', pid]); } return { deliver }; } @@ -210,7 +221,6 @@ test('outbound call', async t => { await kernel.step(); t.deepEqual(log.shift(), ['d1', 'o+1', 'foo', 'args', []]); - t.deepEqual(log.shift(), ['d1', 'promiseid', 'p-60']); t.deepEqual(log, []); t.deepEqual(kernel.dump().runQueue, [ @@ -221,8 +231,8 @@ test('outbound call', async t => { msg: { method: 'bar', argsString: 'bargs', - slots: [vat2Obj5, 'ko22', 'kp41'], - result: 'kp40', + slots: [vat2Obj5, 'ko22', 'kp40'], + result: 'kp41', }, }, ]); @@ -230,27 +240,28 @@ test('outbound call', async t => { { id: 'kp40', state: 'unresolved', - decider: undefined, + decider: 'vat1', subscribers: [], queue: [], }, { id: 'kp41', state: 'unresolved', - decider: 'vat1', + decider: undefined, subscribers: [], queue: [], }, ]); kt.push(['ko22', 'vat1', 'o+7']); - kt.push(['kp40', 'vat1', 'p-60']); - kt.push(['kp41', 'vat1', p7]); + kt.push(['kp40', 'vat1', p7]); + kt.push(['kp41', 'vat1', 'p+5']); checkKT(t, kernel, kt); await kernel.step(); // this checks that the decider was set to vat2 while bar() was delivered t.deepEqual(log, [ + // todo: check result ['d2', 'o+5', 'bar', 'bargs', ['o+5', 'o-50', 'p-60']], [ 'd2 promises', @@ -258,40 +269,42 @@ test('outbound call', async t => { { id: 'kp40', state: 'unresolved', - decider: 'vat2', + decider: 'vat1', subscribers: [], queue: [], }, { id: 'kp41', state: 'unresolved', - decider: 'vat1', + decider: 'vat2', subscribers: [], queue: [], }, ], ], ]); + kt.push(['ko22', 'vat2', 'o-50']); - kt.push(['kp41', 'vat2', 'p-60']); - kt.push(['kp40', 'vat2', 'p-61']); + kt.push(['kp41', 'vat2', 'p-61']); + kt.push(['kp40', 'vat2', 'p-60']); checkKT(t, kernel, kt); + t.deepEqual(kernel.dump().promises, [ { id: 'kp40', state: 'unresolved', - decider: 'vat2', + decider: 'vat1', + // Sending a promise from vat1 to vat2 doesn't cause vat2 to be + // subscribed unless they want it. Liveslots will always subscribe, + // because we don't have enough hooks into Promises to detect a + // .then(), but non-liveslots vats don't have to. subscribers: [], queue: [], }, { id: 'kp41', state: 'unresolved', - decider: 'vat1', - // Sending a promise from vat1 to vat2 doesn't cause vat2 to be - // subscribed unless they want it. Liveslots will always subscribe, - // because we don't have enough hooks into Promises to detect a - // .then(), but non-liveslots vats don't have to. + decider: 'vat2', subscribers: [], queue: [], }, @@ -306,13 +319,17 @@ test('three-party', async t => { let bobForA; let carolForA; - let aliceSyscall; function setupA(syscall) { - aliceSyscall = syscall; + let nextPromiseIndex = 5; + function allocatePromise() { + const index = nextPromiseIndex; + nextPromiseIndex += 1; + return makeVatSlot('promise', true, index); + } function deliver(facetID, method, argsString, slots) { console.log(`vatA/${facetID} called`); log.push(['vatA', facetID, method, argsString, slots]); - const pid = syscall.createPromise(); + const pid = allocatePromise(); syscall.send(bobForA, 'intro', 'bargs', [carolForA], pid); log.push(['vatA', 'promiseID', pid]); } @@ -346,11 +363,9 @@ test('three-party', async t => { bobForA = kernel.addImport('vatA', bob); carolForA = kernel.addImport('vatA', carol); - // this extra allocation causes alice's send() promise index to be - // different than bob's, so we can check that the promiseID coming back - // from send() is scoped to the sender, not the recipient - const ap = aliceSyscall.createPromise(); - t.equal(ap, 'p-60'); + // do an extra allocation to make sure we aren't confusing the indices + const extraP = 'p+99'; + const ap = kernel.addExport('vatA', extraP); const data = kernel.dump(); t.deepEqual(data.vatTables, [ @@ -364,7 +379,7 @@ test('three-party', async t => { [bob, 'vatB', 'o+5'], [carol, 'vatA', carolForA], [carol, 'vatC', 'o+6'], - ['kp40', 'vatA', ap], + [ap, 'vatA', extraP], ]; checkKT(t, kernel, kt); t.deepEqual(log, []); @@ -373,7 +388,7 @@ test('three-party', async t => { await kernel.step(); t.deepEqual(log.shift(), ['vatA', 'o+4', 'foo', 'args', []]); - t.deepEqual(log.shift(), ['vatA', 'promiseID', 'p-61']); + t.deepEqual(log.shift(), ['vatA', 'promiseID', 'p+5']); t.deepEqual(log, []); t.deepEqual(kernel.dump().runQueue, [ @@ -391,7 +406,7 @@ test('three-party', async t => { ]); t.deepEqual(kernel.dump().promises, [ { - id: 'kp40', + id: ap, state: 'unresolved', decider: 'vatA', subscribers: [], @@ -405,48 +420,18 @@ test('three-party', async t => { queue: [], }, ]); - kt.push(['kp41', 'vatA', 'p-61']); + kt.push(['kp41', 'vatA', 'p+5']); checkKT(t, kernel, kt); await kernel.step(); t.deepEqual(log, [['vatB', 'o+5', 'intro', 'bargs', ['o-50']]]); kt.push([carol, 'vatB', 'o-50']); - kt.push(['kp41', 'vatB', 'p-60']); // dump() omits resolver + kt.push(['kp41', 'vatB', 'p-60']); checkKT(t, kernel, kt); t.end(); }); -test('createPromise', async t => { - const kernel = buildKernel({ setImmediate }); - let syscall; - function setup(s) { - syscall = s; - function deliver(_facetID, _method, _argsString, _slots) {} - return { deliver }; - } - kernel.addGenesisVat('vat1', setup); - await kernel.start(); - - t.deepEqual(kernel.dump().promises, []); - const pr = syscall.createPromise(); - t.deepEqual(pr, 'p-60'); - t.deepEqual(kernel.dump().promises, [ - { - id: 'kp40', - state: 'unresolved', - decider: 'vat1', - subscribers: [], - queue: [], - }, - ]); - - t.deepEqual(kernel.dump().kernelTable, [ - ['kp40', 'vat1', 'p-60'], // dump() omits resolver - ]); - t.end(); -}); - test('transfer promise', async t => { const kernel = buildKernel({ setImmediate }); let syscallA; @@ -479,41 +464,21 @@ test('transfer promise', async t => { const B = kernel.addImport('vatA', bob); const A = kernel.addImport('vatB', alice); - const pr1 = syscallA.createPromise(); - t.deepEqual(pr1, 'p-60'); - // we send pr2 - const pr2 = syscallA.createPromise(); - t.deepEqual(pr2, 'p-61'); + // we send pr1 + const pr1 = 'p+6'; const kt = [ ['ko20', 'vatA', 'o+6'], ['ko20', 'vatB', 'o-50'], ['ko21', 'vatA', 'o-50'], ['ko21', 'vatB', 'o+5'], - ['kp40', 'vatA', 'p-60'], // pr1 - ['kp41', 'vatA', 'p-61'], // pr2 ]; checkKT(t, kernel, kt); - const kp = [ - { - id: 'kp40', - state: 'unresolved', - decider: 'vatA', - subscribers: [], - queue: [], - }, - { - id: 'kp41', - state: 'unresolved', - decider: 'vatA', - subscribers: [], - queue: [], - }, - ]; + const kp = []; checkPromises(t, kernel, kp); // sending a promise should arrive as a promise - syscallA.send(B, 'foo1', 'args', [pr2]); + syscallA.send(B, 'foo1', 'args', [pr1]); t.deepEqual(kernel.dump().runQueue, [ { vatID: 'vatB', @@ -522,22 +487,29 @@ test('transfer promise', async t => { msg: { method: 'foo1', argsString: 'args', - slots: ['kp41'], + slots: ['kp40'], result: undefined, }, }, ]); + kt.push(['kp40', 'vatA', pr1]); checkKT(t, kernel, kt); + kp.push({ + id: 'kp40', + state: 'unresolved', + decider: 'vatA', + subscribers: [], + queue: [], + }); checkPromises(t, kernel, kp); - await kernel.run(); t.deepEqual(logB.shift(), ['o+5', 'foo1', 'args', ['p-60']]); t.deepEqual(logB, []); - kt.push(['kp41', 'vatB', 'p-60']); // pr2 for B + kt.push(['kp40', 'vatB', 'p-60']); // pr1 for B checkKT(t, kernel, kt); // sending it a second time should arrive as the same thing - syscallA.send(B, 'foo2', 'args', [pr2]); + syscallA.send(B, 'foo2', 'args', [pr1]); await kernel.run(); t.deepEqual(logB.shift(), ['o+5', 'foo2', 'args', ['p-60']]); t.deepEqual(logB, []); @@ -547,7 +519,7 @@ test('transfer promise', async t => { // sending it back should arrive with the sender's index syscallB.send(A, 'foo3', 'args', ['p-60']); await kernel.run(); - t.deepEqual(logA.shift(), ['o+6', 'foo3', 'args', [pr2]]); + t.deepEqual(logA.shift(), ['o+6', 'foo3', 'args', [pr1]]); t.deepEqual(logA, []); checkKT(t, kernel, kt); checkPromises(t, kernel, kp); @@ -555,7 +527,7 @@ test('transfer promise', async t => { // sending it back a second time should arrive as the same thing syscallB.send(A, 'foo4', 'args', ['p-60']); await kernel.run(); - t.deepEqual(logA.shift(), ['o+6', 'foo4', 'args', [pr2]]); + t.deepEqual(logA.shift(), ['o+6', 'foo4', 'args', [pr1]]); t.deepEqual(logA, []); checkPromises(t, kernel, kp); checkKT(t, kernel, kt); @@ -575,18 +547,24 @@ test('subscribe to promise', async t => { return { deliver }; } kernel.addGenesisVat('vat1', setup); + kernel.addGenesisVat('vat2', emptySetup); + await kernel.start(); - const pr = syscall.createPromise(); + const kp = kernel.addExport('vat2', 'p+5'); + const pr = kernel.addImport('vat1', kp); t.deepEqual(pr, 'p-60'); - t.deepEqual(kernel.dump().kernelTable, [['kp40', 'vat1', 'p-60']]); + t.deepEqual(kernel.dump().kernelTable, [ + [kp, 'vat1', pr], + [kp, 'vat2', 'p+5'], + ]); syscall.subscribe(pr); t.deepEqual(kernel.dump().promises, [ { - id: 'kp40', + id: kp, state: 'unresolved', - decider: 'vat1', + decider: 'vat2', subscribers: ['vat1'], queue: [], }, @@ -597,120 +575,68 @@ test('subscribe to promise', async t => { t.end(); }); -// promise redirection is not yet implemented -test.skip('promise redirection', async t => { - const kernel = buildKernel({ setImmediate }); - let syscall; - const log = []; - function setup(s) { - syscall = s; - function deliver(facetID, method, argsString, slots) { - log.push([facetID, method, argsString, slots]); - } - return { deliver }; - } - kernel.addGenesisVat('vat1', setup); - await kernel.start(); - - const pr1 = syscall.createPromise(); - const pr2 = syscall.createPromise(); - t.deepEqual(kernel.dump().kernelTable, [ - ['vat1', 'promise', 20, 40], - ['vat1', 'promise', 21, 41], - ['vat1', 'resolver', 30, 40], - ['vat1', 'resolver', 31, 41], - ]); - - syscall.subscribe(pr1); - t.deepEqual(kernel.dump().promises, [ - { - id: 40, - state: 'unresolved', - decider: 'vat1', - subscribers: ['vat1'], - queue: [], - }, - { - id: 41, - state: 'unresolved', - decider: 'vat1', - subscribers: [], - queue: [], - }, - ]); - - syscall.redirect(pr1, pr2); - t.deepEqual(log, []); // vat is not notified - t.deepEqual(kernel.dump().promises, [ - { - id: 40, - state: 'redirected', - redirectedTo: 41, - subscribers: ['vat1'], - queue: [], - }, - { - id: 41, - state: 'unresolved', - decider: 'vat1', - subscribers: [], - queue: [], - }, - ]); - - t.end(); -}); - test('promise resolveToData', async t => { const kernel = buildKernel({ setImmediate }); - let syscall; const log = []; - function setup(s) { - syscall = s; - function deliver(facetID, method, argsString, slots) { - log.push(['deliver', facetID, method, argsString, slots]); - } + + let syscallA; + function setupA(s) { + syscallA = s; + function deliver() {} function notifyFulfillToData(promiseID, fulfillData, slots) { log.push(['notify', promiseID, fulfillData, slots]); } return { deliver, notifyFulfillToData }; } - kernel.addGenesisVat('vatA', setup); + kernel.addGenesisVat('vatA', setupA); + + let syscallB; + function setupB(s) { + syscallB = s; + function deliver() {} + return { deliver }; + } + kernel.addGenesisVat('vatB', setupB); await kernel.start(); - const alice = 'o+6'; - const pr = syscall.createPromise(); - t.deepEqual(kernel.dump().kernelTable, [['kp40', 'vatA', 'p-60']]); + const aliceForA = 'o+6'; + const pForB = 'p+5'; + const pForKernel = kernel.addExport('vatB', pForB); + const pForA = kernel.addImport('vatA', pForKernel); + t.deepEqual(kernel.dump().kernelTable, [ + [pForKernel, 'vatA', pForA], + [pForKernel, 'vatB', pForB], + ]); - syscall.subscribe(pr); + syscallA.subscribe(pForA); t.deepEqual(kernel.dump().promises, [ { - id: 'kp40', + id: pForKernel, state: 'unresolved', - decider: 'vatA', + decider: 'vatB', subscribers: ['vatA'], queue: [], }, ]); - syscall.fulfillToData(pr, 'args', [alice]); + syscallB.fulfillToData(pForB, 'args', [aliceForA]); // this causes a notifyFulfillToData message to be queued t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().runQueue, [ { type: 'notifyFulfillToData', vatID: 'vatA', - kernelPromiseID: 'kp40', + kernelPromiseID: pForKernel, }, ]); await kernel.step(); // the kernelPromiseID gets mapped back to the vat PromiseID - t.deepEqual(log.shift(), ['notify', pr, 'args', [alice]]); + t.deepEqual(log.shift(), ['notify', pForA, 'args', ['o-50']]); t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().promises, [ { - id: 'kp40', + id: pForKernel, fulfillData: 'args', fulfillSlots: ['ko20'], state: 'fulfilledToData', @@ -721,68 +647,73 @@ test('promise resolveToData', async t => { t.end(); }); -function emptySetup(_syscall) { - function deliver() {} - return { deliver }; -} - test('promise resolveToPresence', async t => { const kernel = buildKernel({ setImmediate }); - let syscall; const log = []; - function setup(s) { - syscall = s; - function deliver(facetID, method, argsString, slots) { - log.push(['deliver', facetID, method, argsString, slots]); - } + + let syscallA; + function setupA(s) { + syscallA = s; + function deliver() {} function notifyFulfillToPresence(promiseID, slot) { log.push(['notify', promiseID, slot]); } return { deliver, notifyFulfillToPresence }; } - kernel.addGenesisVat('vatA', setup); - kernel.addGenesisVat('vatB', emptySetup); + kernel.addGenesisVat('vatA', setupA); + + let syscallB; + function setupB(s) { + syscallB = s; + function deliver() {} + return { deliver }; + } + kernel.addGenesisVat('vatB', setupB); await kernel.start(); - const bob = kernel.addExport('vatB', 'o+6'); - const bobForAlice = kernel.addImport('vatA', bob); - const pr = syscall.createPromise(); + const bobForB = 'o+6'; + const bobForKernel = kernel.addExport('vatB', 'o+6'); + const bobForA = kernel.addImport('vatA', bobForKernel); + + const pForB = 'p+5'; + const pForKernel = kernel.addExport('vatB', 'p+5'); + const pForA = kernel.addImport('vatA', pForKernel); const kt = [ - ['ko20', 'vatB', 'o+6'], - ['ko20', 'vatA', 'o-50'], - ['kp40', 'vatA', 'p-60'], + [bobForKernel, 'vatB', bobForB], + [bobForKernel, 'vatA', bobForA], + [pForKernel, 'vatA', pForA], + [pForKernel, 'vatB', pForB], ]; checkKT(t, kernel, kt); - syscall.subscribe(pr); + syscallA.subscribe(pForA); t.deepEqual(kernel.dump().promises, [ { - id: 'kp40', + id: pForKernel, state: 'unresolved', - decider: 'vatA', + decider: 'vatB', subscribers: ['vatA'], queue: [], }, ]); - syscall.fulfillToPresence(pr, bobForAlice); + syscallB.fulfillToPresence(pForB, bobForB); t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().runQueue, [ { type: 'notifyFulfillToPresence', vatID: 'vatA', - kernelPromiseID: 'kp40', + kernelPromiseID: pForKernel, }, ]); await kernel.step(); - - t.deepEqual(log.shift(), ['notify', pr, bobForAlice]); + t.deepEqual(log.shift(), ['notify', pForA, bobForA]); t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().promises, [ { - id: 'kp40', - fulfillSlot: bob, + id: pForKernel, + fulfillSlot: bobForKernel, state: 'fulfilledToPresence', }, ]); @@ -792,65 +723,68 @@ test('promise resolveToPresence', async t => { test('promise reject', async t => { const kernel = buildKernel({ setImmediate }); - let syscall; const log = []; - function setup(s) { - syscall = s; - function deliver(facetID, method, argsString, slots) { - log.push(['deliver', facetID, method, argsString, slots]); - } + + let syscallA; + function setupA(s) { + syscallA = s; + function deliver() {} function notifyReject(promiseID, rejectData, slots) { log.push(['notify', promiseID, rejectData, slots]); } return { deliver, notifyReject }; } - kernel.addGenesisVat('vatA', setup); - kernel.addGenesisVat('vatB', emptySetup); - await kernel.start(); + kernel.addGenesisVat('vatA', setupA); - const bob = kernel.addExport('vatB', 'o+6'); - const bobForAlice = kernel.addImport('vatA', bob); + let syscallB; + function setupB(s) { + syscallB = s; + function deliver() {} + return { deliver }; + } + kernel.addGenesisVat('vatB', setupB); + await kernel.start(); - const pr = syscall.createPromise(); - const kt = [ - ['ko20', 'vatB', 'o+6'], - ['ko20', 'vatA', 'o-50'], - ['kp40', 'vatA', 'p-60'], - ]; - checkKT(t, kernel, kt); + const aliceForA = 'o+6'; + const pForB = 'p+5'; + const pForKernel = kernel.addExport('vatB', pForB); + const pForA = kernel.addImport('vatA', pForKernel); + t.deepEqual(kernel.dump().kernelTable, [ + [pForKernel, 'vatA', pForA], + [pForKernel, 'vatB', pForB], + ]); - syscall.subscribe(pr); + syscallA.subscribe(pForA); t.deepEqual(kernel.dump().promises, [ { - id: 'kp40', + id: pForKernel, state: 'unresolved', - decider: 'vatA', + decider: 'vatB', subscribers: ['vatA'], queue: [], }, ]); - // the resolver-holder calls reject right away, because now we - // automatically subscribe - - syscall.reject(pr, 'args', [bobForAlice]); + syscallB.reject(pForB, 'args', [aliceForA]); + // this causes a notifyFulfillToData message to be queued t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().runQueue, [ { type: 'notifyReject', vatID: 'vatA', - kernelPromiseID: 'kp40', + kernelPromiseID: pForKernel, }, ]); - await kernel.step(); - t.deepEqual(log.shift(), ['notify', pr, 'args', [bobForAlice]]); + await kernel.step(); + // the kernelPromiseID gets mapped back to the vat PromiseID + t.deepEqual(log.shift(), ['notify', pForA, 'args', ['o-50']]); t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().promises, [ { - id: 'kp40', + id: pForKernel, rejectData: 'args', - rejectSlots: [bob], + rejectSlots: ['ko20'], state: 'rejected', }, ]); @@ -862,13 +796,10 @@ test('promise reject', async t => { test('transcript', async t => { const aliceForAlice = 'o+1'; const kernel = buildKernel({ setImmediate }); - const log = []; function setup(syscall, _state) { function deliver(facetID, _method, _argsString, slots) { if (facetID === aliceForAlice) { - const p = syscall.createPromise(); - syscall.send(slots[1], 'foo', 'fooarg', [], p); - log.push(p); + syscall.send(slots[1], 'foo', 'fooarg', [], 'p+5'); } } return { deliver }; @@ -886,8 +817,6 @@ test('transcript', async t => { bob, ]); await kernel.step(); - const p1 = log.shift(); - t.equal(p1, 'p-60'); // the transcript records vat-specific import/export slots @@ -903,9 +832,8 @@ test('transcript', async t => { undefined, ], syscalls: [ - { d: ['createPromise'], response: p1 }, { - d: ['send', bobForAlice, 'foo', 'fooarg', [], p1], + d: ['send', bobForAlice, 'foo', 'fooarg', [], 'p+5'], response: undefined, }, ], diff --git a/test/test-marshal.js b/test/test-marshal.js index 4c2028f1f1d..6ba9bbf0ced 100644 --- a/test/test-marshal.js +++ b/test/test-marshal.js @@ -238,9 +238,6 @@ test('serialize imports', async t => { test('serialize promise', async t => { const log = []; const syscall = { - createPromise() { - return 'p-1'; - }, fulfillToData(result, data, slots) { log.push({ result, data, slots }); }, From dac17829530ddd3e8f448deb072affe2885b170f Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 29 Aug 2019 22:27:01 -0700 Subject: [PATCH 07/16] remove syscall.createPromise() entirely --- src/kernel/kernel.js | 10 ---------- src/kernel/vatManager.js | 17 ----------------- 2 files changed, 27 deletions(-) diff --git a/src/kernel/kernel.js b/src/kernel/kernel.js index e7b2dd3784d..c8086512018 100644 --- a/src/kernel/kernel.js +++ b/src/kernel/kernel.js @@ -69,15 +69,6 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { } */ - function createPromiseWithDecider(deciderVatID) { - // deciderVatID can be undefined if the promise is "owned" by the kernel - // (pipelining) - - // TODO: not true (but we should make it true): kernel promise is - // replaced if altered, so we can safely harden - return kernelKeeper.addKernelPromise(deciderVatID); - } - function makeError(s) { // TODO: create a @qclass=error, once we define those // or maybe replicate whatever happens with {}.foo() @@ -224,7 +215,6 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { const syscallManager = { kdebug, - createPromiseWithDecider, send, fulfillToData, fulfillToPresence, diff --git a/src/kernel/vatManager.js b/src/kernel/vatManager.js index 136ea224194..643f81b45b4 100644 --- a/src/kernel/vatManager.js +++ b/src/kernel/vatManager.js @@ -13,7 +13,6 @@ export default function makeVatManager( ) { const { kdebug, - createPromiseWithDecider, send, fulfillToData, fulfillToPresence, @@ -150,15 +149,6 @@ export default function makeVatManager( send(target, msg); } - function doCreatePromise() { - const kernelPromiseID = createPromiseWithDecider(vatID); - const p = mapKernelSlotToVatSlot(kernelPromiseID); - kdebug( - `syscall[${vatID}].createPromise -> (vat:${p}=ker:${kernelPromiseID})`, - ); - return p; - } - function doSubscribe(promiseID) { const id = mapVatSlotToKernelSlot(promiseID); kdebug(`syscall[${vatID}].subscribe(vat:${promiseID}=ker:${id})`); @@ -300,13 +290,6 @@ export default function makeVatManager( transcriptAddSyscall(['send', ...args], promiseID); return promiseID; }, - createPromise(...args) { - const pr = inReplay - ? replay('createPromise', ...args) - : doCreatePromise(...args); - transcriptAddSyscall(['createPromise', ...args], pr); - return pr; - }, subscribe(...args) { transcriptAddSyscall(['subscribe', ...args]); return inReplay ? replay('subscribe', ...args) : doSubscribe(...args); From 295fb08e0100bd9de2d137dfb65867b4ccd51509 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 31 Aug 2019 00:46:25 -0700 Subject: [PATCH 08/16] change run-queue handling The run-queue now contains only 'send' and 'notify' entries. The 'send' entries specify a target (object or promise) but we no longer figure out the vatID ahead of time: we wait until the entry comes to the front of the queue before finding the vat to which it should be delivered (or the promise on which it should be queued). The 'notify' entries do not specify the flavor of resolution, they just name a vatID and the promiseID they should be told about. We pull the resolution type from the promise when the notify comes to the front of the queue. Subscribing to a promise after it's been resolved queues a new notify entry. This prepares the way for pipelining. Each time we deliver a message to a vat, we set the `.decider` of any result promise to point at that vat too. Then, if a 'send' gets to the front of the queue and targets such a result (an unresolved promise that has a decider), we check to see if the deciding vat supports pipelining or not (this is currently hardcoded to 'false'). If yes, we deliver the message to that vat (which will also set *it's* result's decider). If no, we queue the message within the target promise. To enable pipelining, we need to remove the hardcoded 'false', add an option to kernel.addGenesisVat() to enable pipelining, and write some new tests. We should also decide whether liveSlots should support this mode (which requires vat-side queue handling code), or whether to only enable it for the comms vat. --- src/kernel/kernel.js | 323 +++++++++++++++++-------------- src/kernel/state/kernelKeeper.js | 6 +- src/kernel/vatManager.js | 232 +++++++--------------- test/test-controller.js | 20 +- test/test-kernel.js | 142 ++++++++++++-- test/test-liveslots.js | 22 +-- 6 files changed, 394 insertions(+), 351 deletions(-) diff --git a/src/kernel/kernel.js b/src/kernel/kernel.js index c8086512018..3d9c56a4d0d 100644 --- a/src/kernel/kernel.js +++ b/src/kernel/kernel.js @@ -55,20 +55,6 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { // in the kernel table, promises and resolvers are both indexed by the same // value. kernelPromises[promiseID] = { decider, subscribers } - /* - function chaseRedirections(promiseID) { - let targetID = Nat(promiseID); - while (true) { - const p = kernelKeeper.getKernelPromise(targetID); - if (p.state === 'redirected') { - targetID = Nat(p.redirectedTo); - continue; - } - return targetID; - } - } - */ - function makeError(s) { // TODO: create a @qclass=error, once we define those // or maybe replicate whatever happens with {}.foo() @@ -76,112 +62,58 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { return s; } - function send(target, msg) { - const { type } = parseKernelSlot(target); - if (type === 'object') { - kernelKeeper.addToRunQueue({ - type: 'deliver', - vatID: kernelKeeper.ownerOfKernelObject(target), - target, - msg, - }); - } else if (type === 'promise') { - const kp = kernelKeeper.getKernelPromise(target); - if (kp.state === 'unresolved') { - kernelKeeper.addMessageToPromiseQueue(target, msg); - } else if (kp.state === 'fulfilledToData') { - const s = `data is not callable, has no method ${msg.method}`; - // eslint-disable-next-line no-use-before-define - reject(msg.kernelPromiseID, makeError(s), []); - } else if (kp.state === 'fulfilledToPresence') { - send(kp.fulfillSlot, msg); - } else if (kp.state === 'rejected') { - // TODO would it be simpler to redirect msg.kernelPromiseID to kp? - // eslint-disable-next-line no-use-before-define - reject(msg.kernelPromiseID, kp.rejectData, kp.rejectSlots); - } else if (kp.state === 'redirected') { - throw new Error('not implemented yet'); - // TODO: shorten as we go - // send(kp.redirectedTo, msg); - } else { - throw new Error(`unknown kernelPromise state '${kp.state}'`); - } - } else { - throw Error(`unable to send() to slot.type ${JSON.stringify(type)}`); - } - } - - function notifySubscribersAndQueue(kpid, subscribers, queue, type) { + function notifySubscribersAndQueue(kpid, subscribers, queue) { insistKernelType('promise', kpid); - for (const subscriberVatID of subscribers) { - kernelKeeper.addToRunQueue({ - type, - vatID: subscriberVatID, - kernelPromiseID: kpid, - }); + for (const vatID of subscribers) { + kernelKeeper.addToRunQueue( + harden({ + type: 'notify', + vatID, + kpid, + }), + ); } // re-deliver msg to the now-settled promise, which will forward or // reject depending on the new state of the promise for (const msg of queue) { - send(kpid, msg); - // now that we know where the messages can be sent, we know to whom we - // must subscribe to satisfy their resolvers. This wasn't working - // correctly, so instead liveSlots just assumes that it must tell the - // kernel about the resolution for resolver it hears about - /* - runQueue.push({ - type: 'subscribe', - vatID: XXX, - kernelPromiseID: msg.kernelResolverID, - }); */ - } - } - - function getUnresolvedPromise(kpid) { - const p = kernelKeeper.getKernelPromise(kpid); - if (p.state !== 'unresolved') { - throw new Error( - `kernelPromise[${kpid}] is '${p.state}', not 'unresolved'`, + // todo: this is slightly lazy, sending the message back to the same + // promise that just got resolved. When this message makes it to the + // front of the run-queue, we'll look up the resolution. Instead, we + // could maybe look up the resolution *now* and set the correct target + // early. Doing that might make it easier to remove the Promise Table + // entry earlier. + kernelKeeper.addToRunQueue( + harden({ + type: 'send', + target: kpid, + msg, + }), ); } - return p; } - function fulfillToPresence(kpid, targetSlot) { - const { type } = parseKernelSlot(targetSlot); - insist( - type === 'object', - `fulfillToPresence() must fulfill to object, not ${type}`, - ); - const { subscribers, queue } = getUnresolvedPromise(kpid); - kernelKeeper.fulfillKernelPromiseToPresence(kpid, targetSlot); - notifySubscribersAndQueue( - kpid, - subscribers, - queue, - 'notifyFulfillToPresence', - ); - // kernelKeeper.deleteKernelPromiseData(kpid); - } + async function process(f, then, logerr) { + // the delivery might cause some number of (native) Promises to be + // created and resolved, so we use the IO queue to detect when the + // Promise queue is empty. The IO queue (setImmediate and setTimeout) is + // lower-priority than the Promise queue on browsers and Node 11, but on + // Node 10 it is higher. So this trick requires Node 11. + // https://jsblog.insiderattack.net/new-changes-to-timers-and-microtasks-from-node-v11-0-0-and-above-68d112743eb3 - function fulfillToData(kpid, data, slots) { - kdebug(`fulfillToData[${kpid}] -> ${data} ${JSON.stringify(slots)}`); - insistKernelType('promise', kpid); - const { subscribers, queue } = getUnresolvedPromise(kpid); + const { p: queueEmptyP, res } = makePromise(); + setImmediate(() => res()); - kernelKeeper.fulfillKernelPromiseToData(kpid, data, slots); - notifySubscribersAndQueue(kpid, subscribers, queue, 'notifyFulfillToData'); - // kernelKeeper.deleteKernelPromiseData(kpid); - // TODO: we can't delete the promise until all vat references are gone, - // and certainly not until the notifyFulfillToData we just queued is - // delivered + // protect f() with promise/then + Promise.resolve() + .then(f) + .then(undefined, logerr); + await queueEmptyP; + then(); } - function reject(kpid, val, valSlots) { - const { subscribers, queue } = getUnresolvedPromise(kpid); - kernelKeeper.rejectKernelPromise(kpid, val, valSlots); - notifySubscribersAndQueue(kpid, subscribers, queue, 'notifyReject'); - // kernelKeeper.deleteKernelPromiseData(kpid); + function send(target, msg) { + const m = harden({ type: 'send', target, msg }); + kernelKeeper.addToRunQueue(m); } function invoke(deviceSlot, method, data, slots) { @@ -194,34 +126,69 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { return dev.manager.invoke(deviceSlot, method, data, slots); } - async function process(f, then, logerr) { - // the delivery might cause some number of (native) Promises to be - // created and resolved, so we use the IO queue to detect when the - // Promise queue is empty. The IO queue (setImmediate and setTimeout) is - // lower-priority than the Promise queue on browsers and Node 11, but on - // Node 10 it is higher. So this trick requires Node 11. - // https://jsblog.insiderattack.net/new-changes-to-timers-and-microtasks-from-node-v11-0-0-and-above-68d112743eb3 + function subscribe(vatID, kpid) { + const p = kernelKeeper.getKernelPromise(kpid); + if (p.state === 'unresolved') { + kernelKeeper.addSubscriberToPromise(kpid, vatID); + } else { + // otherwise it's already resolved, you probably want to know how + const m = harden({ type: 'notify', vatID, kpid }); + kernelKeeper.addToRunQueue(m); + } + } - const { p: queueEmptyP, res } = makePromise(); - setImmediate(() => res()); + function getResolveablePromise(kpid, resolvingVatID) { + insistKernelType('promise', kpid); + const p = kernelKeeper.getKernelPromise(kpid); + insist(p.state === 'unresolved', `${kpid} was already resolved`); + insist( + p.decider === resolvingVatID, + `${kpid} is decided by ${p.decider}, not ${resolvingVatID}`, + ); + return p; + } - // protect f() with promise/then - Promise.resolve() - .then(f) - .then(undefined, logerr); - await queueEmptyP; - then(); + function fulfillToPresence(vatID, kpid, targetSlot) { + const p = getResolveablePromise(kpid, vatID); + const { type } = parseKernelSlot(targetSlot); + insist( + type === 'object', + `fulfillToPresence() must fulfill to object, not ${type}`, + ); + const { subscribers, queue } = p; + kernelKeeper.fulfillKernelPromiseToPresence(kpid, targetSlot); + notifySubscribersAndQueue(kpid, subscribers, queue); + // todo: some day it'd be nice to delete the promise table entry now. To + // do that correctly, we must make sure no vats still hold pointers to + // it, which means vats must drop their refs when they get notified about + // the resolution ("you knew it was resolved, you shouldn't be sending + // any more messages to it, send them to the resolution instead"), and we + // must wait for those notifications to be delivered. + } + + function fulfillToData(vatID, kpid, data, slots) { + const p = getResolveablePromise(kpid, vatID); + const { subscribers, queue } = p; + kernelKeeper.fulfillKernelPromiseToData(kpid, data, slots); + notifySubscribersAndQueue(kpid, subscribers, queue); + } + + function reject(vatID, kpid, data, slots) { + const p = getResolveablePromise(kpid, vatID); + const { subscribers, queue } = p; + kernelKeeper.rejectKernelPromise(kpid, data, slots); + notifySubscribersAndQueue(kpid, subscribers, queue); } const syscallManager = { kdebug, + process, send, - fulfillToData, + invoke, + subscribe, fulfillToPresence, + fulfillToData, reject, - process, - invoke, - kernelKeeper, }; function addImport(forVatID, what) { @@ -256,8 +223,7 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { const kernelSlot = addExport(vatID, vatSlot); kernelKeeper.addToRunQueue( harden({ - vatID, // TODO remove vatID from run-queue - type: 'deliver', + type: 'send', target: kernelSlot, msg: { method: `${method}`, @@ -271,26 +237,102 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { ); } - function processQueueMessage(message) { - kdebug(`processQ ${JSON.stringify(message)}`); - const vat = ephemeral.vats.get(message.vatID); - if (vat === undefined) { - throw new Error( - `unknown vatID in target ${JSON.stringify( - message, - )}, have ${JSON.stringify(kernelKeeper.getAllVatNames())}`, - ); + async function deliverToVat(vatID, target, msg) { + const vat = ephemeral.vats.get(vatID); + insist(vat, `unknown vatID ${vatID}`); + try { + await vat.manager.deliverOneMessage(target, msg); + } catch (e) { + // log so we get a stack trace + console.log(`error in kernel.deliver: ${e} ${e.message}`, e); + throw e; } - const { manager } = vat; + } + + function getKernelResolveablePromise(kpid) { + insistKernelType('promise', kpid); + const p = kernelKeeper.getKernelPromise(kpid); + insist(p.state === 'unresolved', `${kpid} was already resolved`); + insist(!p.decider, `${kpid} is decided by ${p.decider}, not kernel`); + return p; + } + + function deliverToError(kpid, errorData, errorSlots) { + // todo: see if this can be merged with reject() + const p = getKernelResolveablePromise(kpid); + const { subscribers, queue } = p; + kernelKeeper.rejectKernelPromise(kpid, errorData, errorSlots); + notifySubscribersAndQueue(kpid, subscribers, queue); + } + + async function deliverToTarget(target, msg) { + const { type } = parseKernelSlot(target); + if (type === 'object') { + const vatID = kernelKeeper.ownerOfKernelObject(target); + await deliverToVat(vatID, target, msg); + } else if (type === 'promise') { + const kp = kernelKeeper.getKernelPromise(target); + if (kp.state === 'fulfilledToPresence') { + await deliverToTarget(kp.fulfillSlot, msg); + } else if (kp.state === 'redirected') { + // await deliverToTarget(kp.redirectTarget, msg); // probably correct + throw new Error('not implemented yet'); + } else if (kp.state === 'fulfilledToData') { + if (msg.result) { + const s = `data is not callable, has no method ${msg.method}`; + await deliverToError(msg.result, makeError(s), []); + } + // todo: maybe log error? + } else if (kp.state === 'rejected') { + // TODO would it be simpler to redirect msg.kpid to kp? + if (msg.result) { + await deliverToError(msg.result, kp.rejectData, kp.rejectSlots); + } + } else if (kp.state === 'unresolved') { + if (!kp.decider) { + kernelKeeper.addMessageToPromiseQueue(target, msg); + } else { + // const vat = ephemeral.vats.get(kp.decider); + const vatDoesPipelining = false; // todo: check the vat + if (vatDoesPipelining) { + await deliverToVat(kp.decider, target, msg); // pipeline + } else { + kernelKeeper.addMessageToPromiseQueue(target, msg); + } + } + } else { + throw new Error(`unknown kernelPromise state '${kp.state}'`); + } + } else { + throw Error(`unable to send() to slot.type ${type}`); + } + } + + async function processNotify(message) { + const { vatID, kpid } = message; + const vat = ephemeral.vats.get(vatID); + insist(vat, `unknown vatID ${vatID}`); + const p = kernelKeeper.getKernelPromise(kpid); try { - return manager.processOneMessage(message); + await vat.manager.deliverOneNotification(kpid, p); } catch (e) { // log so we get a stack trace - console.log(`error in processOneMessage: ${e} ${e.message}`, e); + console.log(`error in kernel.processNotify: ${e} ${e.message}`, e); throw e; } } + async function processQueueMessage(message) { + kdebug(`processQ ${JSON.stringify(message)}`); + if (message.type === 'send') { + await deliverToTarget(message.target, message.msg); + } else if (message.type === 'notify') { + await processNotify(message); + } else { + throw Error(`unable to process message.type ${message.type}`); + } + } + function callBootstrap(vatID, argvString) { // we invoke obj[0].bootstrap with an object that contains 'vats' and // 'argv'. @@ -401,6 +443,7 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { syscallManager, setup, helpers, + kernelKeeper, vatKeeper, ); ephemeral.vats.set( diff --git a/src/kernel/state/kernelKeeper.js b/src/kernel/state/kernelKeeper.js index db9ce576a06..6446f5c3589 100644 --- a/src/kernel/state/kernelKeeper.js +++ b/src/kernel/state/kernelKeeper.js @@ -63,9 +63,9 @@ function makeKernelKeeper(initialState) { } function addKernelPromise(deciderVatID) { - const kernelPromiseID = state.nextPromiseIndex; - state.nextPromiseIndex = kernelPromiseID + 1; - const s = makeKernelSlot('promise', kernelPromiseID); + const kpid = state.nextPromiseIndex; + state.nextPromiseIndex = kpid + 1; + const s = makeKernelSlot('promise', kpid); // we leave this unfrozen, because the queue and subscribers are mutable state.kernelPromises[s] = { diff --git a/src/kernel/vatManager.js b/src/kernel/vatManager.js index 643f81b45b4..8cd5473c2fc 100644 --- a/src/kernel/vatManager.js +++ b/src/kernel/vatManager.js @@ -9,18 +9,10 @@ export default function makeVatManager( syscallManager, setup, helpers, + kernelKeeper, vatKeeper, ) { - const { - kdebug, - send, - fulfillToData, - fulfillToPresence, - reject, - process, - invoke, - kernelKeeper, - } = syscallManager; + const { kdebug, process } = syscallManager; // We use vat-centric terminology here, so "inbound" means "into a vat", // generally from the kernel. We also have "comms vats" which use special @@ -146,7 +138,7 @@ export default function makeVatManager( slots, result, }; - send(target, msg); + syscallManager.send(target, msg); } function doSubscribe(promiseID) { @@ -155,108 +147,42 @@ export default function makeVatManager( if (!kernelKeeper.hasKernelPromise(id)) { throw new Error(`unknown kernelPromise id '${id}'`); } - const p = kernelKeeper.getKernelPromise(id); - - /* - // Originally the kernel didn't subscribe to a vat's promise until some - // other vat had subscribed to the kernel's promise. That proved too hard - // to manage, so now the kernel always subscribes to every vat promise. - // This code was to handle the original behavior but has bitrotted - // because it is unused. - kdebug(` decider is ${p.decider} in ${JSON.stringify(p)}`); - if (p.subscribers.size === 0 && p.decider !== undefined) { - runQueue.push({ - type: 'subscribe', - vatID: p.decider, - kernelPromiseID: id, - }); - } */ - - if (p.state === 'unresolved') { - kernelKeeper.addSubscriberToPromise(id, vatID); - // otherwise it's already resolved, you probably want to know how - } else if (p.state === 'fulfilledToPresence') { - kernelKeeper.addToRunQueue({ - type: 'notifyFulfillToPresence', - vatID, - kernelPromiseID: id, - }); - } else if (p.state === 'fulfilledToData') { - kernelKeeper.addToRunQueue({ - type: 'notifyFulfillToData', - vatID, - kernelPromiseID: id, - }); - } else if (p.state === 'rejected') { - kernelKeeper.addToRunQueue({ - type: 'notifyReject', - vatID, - kernelPromiseID: id, - }); - } else { - throw new Error(`unknown p.state '${p.state}'`); - } + syscallManager.subscribe(vatID, id); } - function doFulfillToData(promiseID, fulfillData, vatSlots) { + function doFulfillToPresence(promiseID, slot) { insistVatType('promise', promiseID); - const kp = mapVatSlotToKernelSlot(promiseID); - insistKernelType('promise', kp); - insist(kernelKeeper.hasKernelPromise(kp), `unknown kernelPromise '${kp}'`); - const p = kernelKeeper.getKernelPromise(kp); - insist(p.state === 'unresolved', `${kp} was already resolved`); - insist( - p.decider === vatID, - `${kp} is decided by ${p.decider}, not ${vatID}`, - ); - - const slots = vatSlots.map(slot => mapVatSlotToKernelSlot(slot)); + const kpid = mapVatSlotToKernelSlot(promiseID); + const targetSlot = mapVatSlotToKernelSlot(slot); kdebug( - `syscall[${vatID}].fulfillData(${promiseID}/${kp}) = ${fulfillData} ${JSON.stringify( - vatSlots, - )}/${JSON.stringify(slots)}`, + `syscall[${vatID}].fulfillToPresence(${promiseID / kpid}) = ${slot / + targetSlot})`, ); - fulfillToData(kp, fulfillData, slots); + syscallManager.fulfillToPresence(vatID, kpid, targetSlot); } - function doFulfillToPresence(promiseID, slot) { + function doFulfillToData(promiseID, fulfillData, vatSlots) { insistVatType('promise', promiseID); - const kp = mapVatSlotToKernelSlot(promiseID); - insistKernelType('promise', kp); - insist(kernelKeeper.hasKernelPromise(kp), `unknown kernelPromise '${kp}'`); - const p = kernelKeeper.getKernelPromise(kp); - insist(p.state === 'unresolved', `${kp} was already resolved`); - insist( - p.decider === vatID, - `${kp} is decided by ${p.decider}, not ${vatID}`, - ); - - const targetSlot = mapVatSlotToKernelSlot(slot); + const kpid = mapVatSlotToKernelSlot(promiseID); + const slots = vatSlots.map(slot => mapVatSlotToKernelSlot(slot)); kdebug( - `syscall[${vatID}].fulfillToPresence(${promiseID}/${kp}) = ${slot}/${targetSlot})`, + `syscall[${vatID}].fulfillData(${promiseID}/${kpid}) = ${fulfillData} ${JSON.stringify( + vatSlots, + )}/${JSON.stringify(slots)}`, ); - fulfillToPresence(kp, targetSlot); + syscallManager.fulfillToData(vatID, kpid, fulfillData, slots); } function doReject(promiseID, rejectData, vatSlots) { insistVatType('promise', promiseID); - const kp = mapVatSlotToKernelSlot(promiseID); - insistKernelType('promise', kp); - insist(kernelKeeper.hasKernelPromise(kp), `unknown kernelPromise '${kp}'`); - const p = kernelKeeper.getKernelPromise(kp); - insist(p.state === 'unresolved', `${kp} was already resolved`); - insist( - p.decider === vatID, - `${kp} is decided by ${p.decider}, not ${vatID}`, - ); - + const kpid = mapVatSlotToKernelSlot(promiseID); const slots = vatSlots.map(slot => mapVatSlotToKernelSlot(slot)); kdebug( - `syscall[${vatID}].reject(${promiseID}/${kp}) = ${rejectData} ${JSON.stringify( + `syscall[${vatID}].reject(${promiseID}/${kpid}) = ${rejectData} ${JSON.stringify( vatSlots, )}/${JSON.stringify(slots)}`, ); - reject(kp, rejectData, slots); + syscallManager.reject(vatID, kpid, rejectData, slots); } function doCallNow(target, method, argsString, argsSlots) { @@ -267,7 +193,7 @@ export default function makeVatManager( } const slots = argsSlots.map(slot => mapVatSlotToKernelSlot(slot)); kdebug(`syscall[${vatID}].callNow(${target}/${dev}).${method}`); - const ret = invoke(dev, method, argsString, slots); + const ret = syscallManager.invoke(dev, method, argsString, slots); const retSlots = ret.slots.map(slot => mapKernelSlotToVatSlot(slot)); return harden({ data: ret.data, slots: retSlots }); } @@ -351,83 +277,70 @@ export default function makeVatManager( ); } - function processOneMessage(message) { - kdebug(`process ${JSON.stringify(message)}`); - const { type } = message; - if (type === 'deliver') { - // console.log(` process ${vatID} ${message.msg.method}() -> result=${message.msg.result}`); - const { target, msg } = message; - // temporary, until we allow delivery of pipelined messages to vats - // which have opted-in - insistKernelType('object', target); - const targetSlot = mapKernelSlotToVatSlot(target); - insist(parseVatSlot(targetSlot).allocatedByVat, `deliver() to wrong vat`); - const inputSlots = msg.slots.map(slot => mapKernelSlotToVatSlot(slot)); - let resultSlot; - if (msg.result) { - insistKernelType('promise', msg.result); - const p = kernelKeeper.getKernelPromise(msg.result); - insist( - p.state === 'unresolved', - `result ${msg.result} already resolved`, - ); - insist( - !p.decider, - `result ${msg.result} already has decider ${p.decider}`, - ); - resultSlot = vatKeeper.mapKernelSlotToVatSlot(msg.result); - insistVatType('promise', resultSlot); - p.decider = vatID; - } - return doProcess( - [ - 'deliver', - targetSlot, - msg.method, - msg.argsString, - inputSlots, - resultSlot, - ], - `vat[${vatID}][${targetSlot}].${msg.method} dispatch failed`, - ); + async function deliverOneMessage(target, msg) { + const { type } = parseKernelSlot(target); + const vatDoesPipelining = false; // todo: allow vats to opt-in + if (!vatDoesPipelining) { + insist(type === 'object', `cannot deliver to ${type}`); // temporary } - if (type === 'notifyFulfillToData') { - const { kernelPromiseID } = message; - const kp = kernelKeeper.getKernelPromise(kernelPromiseID); - const vpid = mapKernelSlotToVatSlot(kernelPromiseID); - const slots = kp.fulfillSlots.map(slot => mapKernelSlotToVatSlot(slot)); - // console.log(` ${vatID} ${message.type} ${message.kernelPromiseID}`, kp.fulfillData, kp.fulfillSlots); - return doProcess( - ['notifyFulfillToData', vpid, kp.fulfillData, slots], - `vat[${vatID}].promise[${vpid}] fulfillToData failed`, + const targetSlot = mapKernelSlotToVatSlot(target); + insist(parseVatSlot(targetSlot).allocatedByVat, `deliver() to wrong vat`); + const inputSlots = msg.slots.map(slot => mapKernelSlotToVatSlot(slot)); + let resultSlot; + if (msg.result) { + insistKernelType('promise', msg.result); + const p = kernelKeeper.getKernelPromise(msg.result); + insist(p.state === 'unresolved', `result ${msg.result} already resolved`); + insist( + !p.decider, + `result ${msg.result} already has decider ${p.decider}`, ); + resultSlot = vatKeeper.mapKernelSlotToVatSlot(msg.result); + insistVatType('promise', resultSlot); + p.decider = vatID; // todo: depends on mutable keeper promises } + await doProcess( + [ + 'deliver', + targetSlot, + msg.method, + msg.argsString, + inputSlots, + resultSlot, + ], + `vat[${vatID}][${targetSlot}].${msg.method} dispatch failed`, + ); + } - if (type === 'notifyFulfillToPresence') { - const { kernelPromiseID } = message; - const kp = kernelKeeper.getKernelPromise(kernelPromiseID); - const vpid = mapKernelSlotToVatSlot(kernelPromiseID); + async function deliverOneNotification(kpid, kp) { + insist(kp.state !== 'unresolved', `spurious notification ${kpid}`); + if (kp.state === 'fulfilledToPresence') { + const vpid = mapKernelSlotToVatSlot(kpid); const slot = mapKernelSlotToVatSlot(kp.fulfillSlot); - // console.log(` ${vatID} ${message.type} ${message.kernelPromiseID}`, kp.fulfillSlot); - return doProcess( + await doProcess( ['notifyFulfillToPresence', vpid, slot], `vat[${vatID}].promise[${vpid}] fulfillToPresence failed`, ); - } - - if (type === 'notifyReject') { - const { kernelPromiseID } = message; - const kp = kernelKeeper.getKernelPromise(kernelPromiseID); - const vpid = mapKernelSlotToVatSlot(kernelPromiseID); + } else if (kp.state === 'redirected') { + throw new Error('not implemented yet'); + } else if (kp.state === 'fulfilledToData') { + const vpid = mapKernelSlotToVatSlot(kpid); + const slots = kp.fulfillSlots.map(slot => mapKernelSlotToVatSlot(slot)); + await doProcess( + ['notifyFulfillToData', vpid, kp.fulfillData, slots], + `vat[${vatID}].promise[${vpid}] fulfillToData failed`, + ); + } else if (kp.state === 'rejected') { + const vpid = mapKernelSlotToVatSlot(kpid); const slots = kp.rejectSlots.map(slot => mapKernelSlotToVatSlot(slot)); - return doProcess( + await doProcess( ['notifyReject', vpid, kp.rejectData, slots], `vat[${vatID}].promise[${vpid}] reject failed`, ); + } else { + throw new Error(`unknown kernelPromise state '${kp.state}'`); } - - throw new Error(`unknown message type '${type}'`); } async function replayTranscript() { @@ -449,7 +362,8 @@ export default function makeVatManager( const manager = { mapKernelSlotToVatSlot, mapVatSlotToKernelSlot, - processOneMessage, + deliverOneMessage, + deliverOneNotification, replayTranscript, }; return manager; diff --git a/test/test-controller.js b/test/test-controller.js index 98b24bc72b1..fd9e21cc2d9 100644 --- a/test/test-controller.js +++ b/test/test-controller.js @@ -33,8 +33,7 @@ async function simpleCall(t, withSES) { slots: [], }, target: 'ko20', - type: 'deliver', - vatID: 'vat1', + type: 'send', }, ]); await controller.run(); @@ -124,8 +123,7 @@ async function bootstrapExport(t, withSES) { slots: [boot0, left0, right0], }, target: boot0, - type: 'deliver', - vatID: '_bootstrap', + type: 'send', }, ]); @@ -150,8 +148,7 @@ async function bootstrapExport(t, withSES) { checkKT(t, c, kt); t.deepEqual(c.dump().runQueue, [ { - vatID: 'left', - type: 'deliver', + type: 'send', target: left0, msg: { method: 'foo', @@ -178,8 +175,7 @@ async function bootstrapExport(t, withSES) { t.deepEqual(c.dump().runQueue, [ { - vatID: 'right', - type: 'deliver', + type: 'send', target: right0, msg: { method: 'bar', @@ -188,7 +184,7 @@ async function bootstrapExport(t, withSES) { result: barP, }, }, - { type: 'notifyFulfillToData', vatID: '_bootstrap', kernelPromiseID: fooP }, + { type: 'notify', vatID: '_bootstrap', kpid: fooP }, ]); await c.step(); @@ -206,8 +202,8 @@ async function bootstrapExport(t, withSES) { checkKT(t, c, kt); t.deepEqual(c.dump().runQueue, [ - { type: 'notifyFulfillToData', vatID: '_bootstrap', kernelPromiseID: fooP }, - { type: 'notifyFulfillToData', vatID: 'left', kernelPromiseID: barP }, + { type: 'notify', vatID: '_bootstrap', kpid: fooP }, + { type: 'notify', vatID: 'left', kpid: barP }, ]); await c.step(); @@ -223,7 +219,7 @@ async function bootstrapExport(t, withSES) { checkKT(t, c, kt); t.deepEqual(c.dump().runQueue, [ - { type: 'notifyFulfillToData', vatID: 'left', kernelPromiseID: barP }, + { type: 'notify', vatID: 'left', kpid: barP }, ]); await c.step(); diff --git a/test/test-kernel.js b/test/test-kernel.js index 2002054cd2b..b74a909e14f 100644 --- a/test/test-kernel.js +++ b/test/test-kernel.js @@ -54,8 +54,7 @@ test('simple call', async t => { kernel.queueToExport('vat1', 'o+1', 'foo', 'args'); t.deepEqual(kernel.dump().runQueue, [ { - vatID: 'vat1', - type: 'deliver', + type: 'send', target: 'ko20', msg: { method: 'foo', @@ -106,15 +105,14 @@ test('map inbound', async t => { kernel.queueToExport('vat1', 'o+1', 'foo', 'args', [koFor5, koFor6]); t.deepEqual(kernel.dump().runQueue, [ { + type: 'send', + target: 'ko22', msg: { argsString: 'args', result: null, method: 'foo', slots: [koFor5, koFor6], }, - target: 'ko22', - type: 'deliver', - vatID: 'vat1', }, ]); t.deepEqual(log, []); @@ -202,31 +200,31 @@ test('outbound call', async t => { checkKT(t, kernel, kt); t.deepEqual(log, []); + // o1!foo(args) kernel.queueToExport('vat1', 'o+1', 'foo', 'args'); t.deepEqual(log, []); t.deepEqual(kernel.dump().runQueue, [ { + type: 'send', + target: t1, msg: { argsString: 'args', result: null, method: 'foo', slots: [], }, - target: t1, - type: 'deliver', - vatID: 'vat1', }, ]); await kernel.step(); + // that queues pid=o2!bar(o2, o7, p7) t.deepEqual(log.shift(), ['d1', 'o+1', 'foo', 'args', []]); t.deepEqual(log, []); t.deepEqual(kernel.dump().runQueue, [ { - vatID: 'vat2', - type: 'deliver', + type: 'send', target: vat2Obj5, msg: { method: 'bar', @@ -259,7 +257,6 @@ test('outbound call', async t => { checkKT(t, kernel, kt); await kernel.step(); - // this checks that the decider was set to vat2 while bar() was delivered t.deepEqual(log, [ // todo: check result ['d2', 'o+5', 'bar', 'bargs', ['o+5', 'o-50', 'p-60']], @@ -393,8 +390,7 @@ test('three-party', async t => { t.deepEqual(kernel.dump().runQueue, [ { - vatID: 'vatB', - type: 'deliver', + type: 'send', target: bob, msg: { method: 'intro', @@ -481,8 +477,7 @@ test('transfer promise', async t => { syscallA.send(B, 'foo1', 'args', [pr1]); t.deepEqual(kernel.dump().runQueue, [ { - vatID: 'vatB', - type: 'deliver', + type: 'send', target: bob, msg: { method: 'foo1', @@ -624,9 +619,9 @@ test('promise resolveToData', async t => { t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().runQueue, [ { - type: 'notifyFulfillToData', + type: 'notify', vatID: 'vatA', - kernelPromiseID: pForKernel, + kpid: pForKernel, }, ]); @@ -701,9 +696,9 @@ test('promise resolveToPresence', async t => { t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().runQueue, [ { - type: 'notifyFulfillToPresence', + type: 'notify', vatID: 'vatA', - kernelPromiseID: pForKernel, + kpid: pForKernel, }, ]); @@ -770,9 +765,9 @@ test('promise reject', async t => { t.deepEqual(log, []); // no other dispatch calls t.deepEqual(kernel.dump().runQueue, [ { - type: 'notifyReject', + type: 'notify', vatID: 'vatA', - kernelPromiseID: pForKernel, + kpid: pForKernel, }, ]); @@ -841,3 +836,108 @@ test('transcript', async t => { t.end(); }); + +// todo: p1=x!foo(); p2=p1!bar(); p3=p2!urgh(); no pipelining. p1 will have a +// decider but p2 gets queued in p1 (not pipelined to vat-with-x) so p2 won't +// have a decider. Make sure p3 gets queued in p2 rather than exploding. + +test('non-pipelined promise queueing', async t => { + const kernel = buildKernel({ setImmediate }); + + let syscall; + function setupA(s) { + syscall = s; + function deliver() {} + return { deliver }; + } + kernel.addGenesisVat('vatA', setupA); + + function setupB(_s) { + function deliver() {} + return { deliver }; + } + kernel.addGenesisVat('vatB', setupB); + await kernel.start(); + + const bobForB = 'o+6'; + const bobForKernel = kernel.addExport('vatB', bobForB); + const bobForA = kernel.addImport('vatA', bobForKernel); + + const p1ForA = 'p+1'; + syscall.send(bobForA, 'foo', 'fooargs', [], p1ForA); + const p1ForKernel = kernel.addExport('vatA', p1ForA); + + const p2ForA = 'p+2'; + syscall.send(p1ForA, 'bar', 'barargs', [], p2ForA); + const p2ForKernel = kernel.addExport('vatA', p2ForA); + + const p3ForA = 'p+3'; + syscall.send(p2ForA, 'urgh', 'urghargs', [], p3ForA); + const p3ForKernel = kernel.addExport('vatA', p3ForA); + + t.deepEqual(kernel.dump().promises, [ + { + id: p1ForKernel, + state: 'unresolved', + decider: undefined, + subscribers: [], + queue: [], + }, + { + id: p2ForKernel, + state: 'unresolved', + decider: undefined, + subscribers: [], + queue: [], + }, + { + id: p3ForKernel, + state: 'unresolved', + decider: undefined, + subscribers: [], + queue: [], + }, + ]); + + await kernel.run(); + + t.deepEqual(kernel.dump().promises, [ + { + id: p1ForKernel, + state: 'unresolved', + decider: 'vatB', + subscribers: [], + queue: [ + { + method: 'bar', + argsString: 'barargs', + slots: [], + result: p2ForKernel, + }, + ], + }, + { + id: p2ForKernel, + state: 'unresolved', + decider: undefined, + subscribers: [], + queue: [ + { + method: 'urgh', + argsString: 'urghargs', + slots: [], + result: p3ForKernel, + }, + ], + }, + { + id: p3ForKernel, + state: 'unresolved', + decider: undefined, + subscribers: [], + queue: [], + }, + ]); + + t.end(); +}); diff --git a/test/test-liveslots.js b/test/test-liveslots.js index 7446111a0d3..8fa9ba5ece9 100644 --- a/test/test-liveslots.js +++ b/test/test-liveslots.js @@ -127,28 +127,18 @@ test('liveslots pipelines to syscall.send', async t => { // root!one(x) // sendOnly const arg0 = JSON.stringify({ args: [{ '@qclass': 'slot', index: 0 }] }); - syscall.send(root, 'one', arg0, ['o+5']); + syscall.send(root, 'one', arg0, ['o+5'], undefined); + + await kernel.step(); + // console.log(kernel.dump().runQueue); // calling one() should cause three syscall.send() calls to be made: one // for x!pipe1(), a second pipelined to the result promise of it, and a - // third pipelined to the result of the second. With the current design, - // the kernel ought to put the first onto the runQueue, and second onto the - // kernel promise queue for the result of the first, and likewise the - // third. - await kernel.step(); - const { result } = kernel.dump().runQueue[0].msg; - const state = JSON.parse(kernel.getState()); - const kp = state.kernelPromises[result]; - t.equal(kp.queue.length, 1); - t.equal(kp.queue[0].method, 'pipe2'); - const result2 = kp.queue[0].result; - const kp2 = state.kernelPromises[result2]; - t.equal(kp2.queue.length, 1); - t.equal(kp2.queue[0].method, 'pipe3'); + // third pipelined to the result of the second. // in the new design, three sends() mean three items on the runqueue, and // they'll be appended to kernel promise queues after they get to the front - // t.deepEqual(kernel.dump().runQueue.length, 3); + t.deepEqual(kernel.dump().runQueue.length, 3); t.end(); }); From 34d40792ef16bfb9c3796bb1f7911f5e85777617 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 31 Aug 2019 11:24:01 -0700 Subject: [PATCH 09/16] add vat option to enable pipelining Add the vat with: ``` kernel.addVat(name, setup, { enablePipelining: true }); ``` and the vat will receive `dispatch.deliver(promiseID, ..)` for messages sent to results of previously-delivered messages. The default is for pipelining to be disabled, so these messages will be queued in the kernel's Promise Table (inside the result promise) until the result gets resolved by the target vat. The comms vat is the only likely candidate for enabling pipelining. We might not take full advantage of pipelining until we also forward promises (`syscall.resolve(p1, ForwardTo(p2))`) aggressively, to give the kernel an opportunity to bypass the local vats as much as possible. --- src/kernel/kernel.js | 27 +++++++--- src/kernel/vatManager.js | 13 +++-- test/test-kernel.js | 112 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 135 insertions(+), 17 deletions(-) diff --git a/src/kernel/kernel.js b/src/kernel/kernel.js index 3d9c56a4d0d..481bcfca0a3 100644 --- a/src/kernel/kernel.js +++ b/src/kernel/kernel.js @@ -27,7 +27,7 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { let started = false; // this holds externally-added vats, which are present at startup, but not // vats that are added later from within the kernel - const genesisVats = new Map(); + const genesisVats = new Map(); // vatID -> { setup, options } // we name this 'genesisDevices' for parallelism, but actually all devices // must be present at genesis const genesisDevices = new Map(); @@ -292,10 +292,9 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { if (!kp.decider) { kernelKeeper.addMessageToPromiseQueue(target, msg); } else { - // const vat = ephemeral.vats.get(kp.decider); - const vatDoesPipelining = false; // todo: check the vat - if (vatDoesPipelining) { - await deliverToVat(kp.decider, target, msg); // pipeline + const vat = ephemeral.vats.get(kp.decider); + if (vat.enablePipelining) { + await deliverToVat(kp.decider, target, msg); } else { kernelKeeper.addMessageToPromiseQueue(target, msg); } @@ -419,7 +418,7 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { // instantiate all vats and devices for (const vatID of genesisVats.keys()) { - const setup = genesisVats.get(vatID); + const { setup, options } = genesisVats.get(vatID); const helpers = harden({ vatID, makeLiveSlots, @@ -450,6 +449,7 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { vatID, harden({ manager, + enablePipelining: Boolean(options.enablePipelining), }), ); } @@ -507,7 +507,7 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { } const kernel = harden({ - addGenesisVat(vatID0, setup) { + addGenesisVat(vatID0, setup, options = {}) { const vatID = `${vatID0}`; harden(setup); // 'setup' must be an in-realm function. This test guards against @@ -516,13 +516,24 @@ export default function buildKernel(kernelEndowments, initialState = '{}') { if (!(setup instanceof Function)) { throw Error('setup is not an in-realm function'); } + // for now, we guard against 'options' by treating it as JSON-able data + options = JSON.parse(JSON.stringify(options)); + // todo: consider having vats indicate 'enablePipelining' during + // setup(), rather than using options= during kernel.addVat() + const knownOptions = new Set(['enablePipelining']); + for (const k of Object.getOwnPropertyNames(options)) { + if (!knownOptions.has(k)) { + throw new Error(`unknown option ${k}`); + } + } + if (started) { throw new Error(`addGenesisVat() cannot be called after kernel.start`); } if (genesisVats.has(vatID)) { throw new Error(`vatID ${vatID} already added`); } - genesisVats.set(vatID, setup); + genesisVats.set(vatID, { setup, options }); }, addGenesisDevice(deviceName, setup, endowments) { diff --git a/src/kernel/vatManager.js b/src/kernel/vatManager.js index 8cd5473c2fc..ac4cf53b0bb 100644 --- a/src/kernel/vatManager.js +++ b/src/kernel/vatManager.js @@ -278,14 +278,13 @@ export default function makeVatManager( } async function deliverOneMessage(target, msg) { - const { type } = parseKernelSlot(target); - const vatDoesPipelining = false; // todo: allow vats to opt-in - if (!vatDoesPipelining) { - insist(type === 'object', `cannot deliver to ${type}`); // temporary - } - const targetSlot = mapKernelSlotToVatSlot(target); - insist(parseVatSlot(targetSlot).allocatedByVat, `deliver() to wrong vat`); + if (targetSlot.type === 'object') { + insist(parseVatSlot(targetSlot).allocatedByVat, `deliver() to wrong vat`); + } else if (targetSlot.type === 'promise') { + const p = kernelKeeper.getKernelPromise(target); + insist(p.decider === vatID, `wrong decider`); + } const inputSlots = msg.slots.map(slot => mapKernelSlotToVatSlot(slot)); let resultSlot; if (msg.result) { diff --git a/test/test-kernel.js b/test/test-kernel.js index b74a909e14f..22afae53c3f 100644 --- a/test/test-kernel.js +++ b/test/test-kernel.js @@ -837,12 +837,13 @@ test('transcript', async t => { t.end(); }); -// todo: p1=x!foo(); p2=p1!bar(); p3=p2!urgh(); no pipelining. p1 will have a +// p1=x!foo(); p2=p1!bar(); p3=p2!urgh(); no pipelining. p1 will have a // decider but p2 gets queued in p1 (not pipelined to vat-with-x) so p2 won't // have a decider. Make sure p3 gets queued in p2 rather than exploding. test('non-pipelined promise queueing', async t => { const kernel = buildKernel({ setImmediate }); + const log = []; let syscall; function setupA(s) { @@ -853,7 +854,9 @@ test('non-pipelined promise queueing', async t => { kernel.addGenesisVat('vatA', setupA); function setupB(_s) { - function deliver() {} + function deliver(target, method, argsString, slots, result) { + log.push([target, method, argsString, slots, result]); + } return { deliver }; } kernel.addGenesisVat('vatB', setupB); @@ -901,6 +904,10 @@ test('non-pipelined promise queueing', async t => { await kernel.run(); + const p1ForB = kernel.addImport('vatB', p1ForKernel); + t.deepEqual(log.shift(), [bobForB, 'foo', 'fooargs', [], p1ForB]); + t.deepEqual(log, []); + t.deepEqual(kernel.dump().promises, [ { id: p1ForKernel, @@ -941,3 +948,104 @@ test('non-pipelined promise queueing', async t => { t.end(); }); + +// p1=x!foo(); p2=p1!bar(); p3=p2!urgh(); with pipelining. All three should +// get delivered to vat-with-x. + +test('pipelined promise queueing', async t => { + const kernel = buildKernel({ setImmediate }); + const log = []; + + let syscall; + function setupA(s) { + syscall = s; + function deliver() {} + return { deliver }; + } + kernel.addGenesisVat('vatA', setupA); + + function setupB(_s) { + function deliver(target, method, argsString, slots, result) { + log.push([target, method, argsString, slots, result]); + } + return { deliver }; + } + kernel.addGenesisVat('vatB', setupB, { enablePipelining: true }); + await kernel.start(); + + const bobForB = 'o+6'; + const bobForKernel = kernel.addExport('vatB', bobForB); + const bobForA = kernel.addImport('vatA', bobForKernel); + + const p1ForA = 'p+1'; + syscall.send(bobForA, 'foo', 'fooargs', [], p1ForA); + const p1ForKernel = kernel.addExport('vatA', p1ForA); + + const p2ForA = 'p+2'; + syscall.send(p1ForA, 'bar', 'barargs', [], p2ForA); + const p2ForKernel = kernel.addExport('vatA', p2ForA); + + const p3ForA = 'p+3'; + syscall.send(p2ForA, 'urgh', 'urghargs', [], p3ForA); + const p3ForKernel = kernel.addExport('vatA', p3ForA); + + t.deepEqual(kernel.dump().promises, [ + { + id: p1ForKernel, + state: 'unresolved', + decider: undefined, + subscribers: [], + queue: [], + }, + { + id: p2ForKernel, + state: 'unresolved', + decider: undefined, + subscribers: [], + queue: [], + }, + { + id: p3ForKernel, + state: 'unresolved', + decider: undefined, + subscribers: [], + queue: [], + }, + ]); + + await kernel.run(); + + const p1ForB = kernel.addImport('vatB', p1ForKernel); + const p2ForB = kernel.addImport('vatB', p2ForKernel); + const p3ForB = kernel.addImport('vatB', p3ForKernel); + t.deepEqual(log.shift(), [bobForB, 'foo', 'fooargs', [], p1ForB]); + t.deepEqual(log.shift(), [p1ForB, 'bar', 'barargs', [], p2ForB]); + t.deepEqual(log.shift(), [p2ForB, 'urgh', 'urghargs', [], p3ForB]); + t.deepEqual(log, []); + + t.deepEqual(kernel.dump().promises, [ + { + id: p1ForKernel, + state: 'unresolved', + decider: 'vatB', + subscribers: [], + queue: [], + }, + { + id: p2ForKernel, + state: 'unresolved', + decider: 'vatB', + subscribers: [], + queue: [], + }, + { + id: p3ForKernel, + state: 'unresolved', + decider: 'vatB', + subscribers: [], + queue: [], + }, + ]); + + t.end(); +}); From 0a68c11acbc595e6c333b523ba478efcc0544b5b Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 31 Aug 2019 22:42:20 -0700 Subject: [PATCH 10/16] add controller option to enable pipelining Enable pipelining for the comms vats in test-comms-integration.js --- src/controller.js | 10 ++++++---- test/basedir-commsvat/vat-leftcomms.js | 5 ----- test/basedir-commsvat/vat-rightcomms.js | 5 ----- test/test-comms-integration.js | 11 +++++++++-- test/test-controller.js | 5 ++++- test/test-devices.js | 8 ++++++++ test/test-vattp.js | 2 ++ 7 files changed, 29 insertions(+), 17 deletions(-) delete mode 100644 test/basedir-commsvat/vat-leftcomms.js delete mode 100644 test/basedir-commsvat/vat-rightcomms.js diff --git a/src/controller.js b/src/controller.js index 9d6ade9ebbe..a0f361b0139 100644 --- a/src/controller.js +++ b/src/controller.js @@ -18,6 +18,7 @@ const evaluateOptions = makeDefaultEvaluateOptions(); export function loadBasedir(basedir) { console.log(`= loading config from basedir ${basedir}`); const vatSources = new Map(); + const vatOptions = new Map(); const subs = fs.readdirSync(basedir, { withFileTypes: true }); subs.forEach(dirent => { if (dirent.name.endsWith('~')) { @@ -41,7 +42,7 @@ export function loadBasedir(basedir) { } catch (e) { bootstrapIndexJS = undefined; } - return { vatSources, bootstrapIndexJS }; + return { vatSources, vatOptions, bootstrapIndexJS }; } function getKernelSource() { @@ -132,7 +133,7 @@ export async function buildVatController(config, withSES = true, argv = []) { : buildNonSESKernel(initialState); // console.log('kernel', kernel); - async function addGenesisVat(vatID, sourceIndex, _options = {}) { + async function addGenesisVat(vatID, sourceIndex, options = {}) { console.log(`= adding vat '${vatID}' from ${sourceIndex}`); if (!(sourceIndex[0] === '.' || path.isAbsolute(sourceIndex))) { throw Error( @@ -162,7 +163,7 @@ export async function buildVatController(config, withSES = true, argv = []) { // eslint-disable-next-line global-require,import/no-dynamic-require setup = require(`${sourceIndex}`).default; } - kernel.addGenesisVat(vatID, setup); + kernel.addGenesisVat(vatID, setup, options); } async function addGenesisDevice(name, sourceIndex, endowments) { @@ -230,8 +231,9 @@ export async function buildVatController(config, withSES = true, argv = []) { if (config.vatSources) { for (const vatID of config.vatSources.keys()) { + const options = config.vatOptions.get(vatID); // eslint-disable-next-line no-await-in-loop - await addGenesisVat(vatID, config.vatSources.get(vatID)); + await addGenesisVat(vatID, config.vatSources.get(vatID), options); } } diff --git a/test/basedir-commsvat/vat-leftcomms.js b/test/basedir-commsvat/vat-leftcomms.js deleted file mode 100644 index ee58e3d31eb..00000000000 --- a/test/basedir-commsvat/vat-leftcomms.js +++ /dev/null @@ -1,5 +0,0 @@ -import buildCommsDispatch from '../../src/vats/comms'; - -export default function setup(syscall, state, helpers) { - return buildCommsDispatch(syscall, state, helpers); -} diff --git a/test/basedir-commsvat/vat-rightcomms.js b/test/basedir-commsvat/vat-rightcomms.js deleted file mode 100644 index ee58e3d31eb..00000000000 --- a/test/basedir-commsvat/vat-rightcomms.js +++ /dev/null @@ -1,5 +0,0 @@ -import buildCommsDispatch from '../../src/vats/comms'; - -export default function setup(syscall, state, helpers) { - return buildCommsDispatch(syscall, state, helpers); -} diff --git a/test/test-comms-integration.js b/test/test-comms-integration.js index 84527b4a123..98bcdd27e39 100644 --- a/test/test-comms-integration.js +++ b/test/test-comms-integration.js @@ -1,12 +1,19 @@ import path from 'path'; import { test } from 'tape-promise/tape'; -import { buildVatController, loadBasedir } from '../src/index'; +import { + buildVatController, + getCommsSourcePath, + loadBasedir, +} from '../src/index'; export async function runVats(t, withSES, argv) { const config = await loadBasedir( path.resolve(__dirname, './basedir-commsvat'), ); - + config.vatSources.set('leftcomms', getCommsSourcePath()); + config.vatOptions.set('leftcomms', { enablePipelining: !true }); + config.vatSources.set('rightcomms', getCommsSourcePath()); + config.vatOptions.set('rightcomms', { enablePipelining: !true }); const ldSrcPath = require.resolve('../src/devices/loopbox-src'); config.devices = [['loopbox', ldSrcPath, {}]]; const c = await buildVatController(config, withSES, argv); diff --git a/test/test-controller.js b/test/test-controller.js index fd9e21cc2d9..6c716df23ab 100644 --- a/test/test-controller.js +++ b/test/test-controller.js @@ -6,6 +6,7 @@ import { checkKT } from './util'; test('load empty', async t => { const config = { vatSources: new Map(), + vatOptions: new Map(), bootstrapIndexJS: undefined, }; const controller = await buildVatController(config); @@ -17,6 +18,7 @@ test('load empty', async t => { async function simpleCall(t, withSES) { const config = { vatSources: new Map([['vat1', require.resolve('./vat-controller-1')]]), + vatOptions: new Map(), }; const controller = await buildVatController(config, withSES); const data = controller.dump(); @@ -67,8 +69,9 @@ test('reject module-like sourceIndex', async t => { // be treated as something to load from node_modules/ (i.e. something // installed from npm), so we want to reject that. vatSources.set('vat1', 'vatsource'); + const vatOptions = new Map(); t.rejects( - async () => buildVatController({ vatSources }, false), + async () => buildVatController({ vatSources, vatOptions }, false), /sourceIndex must be relative/, ); t.end(); diff --git a/test/test-devices.js b/test/test-devices.js index a7acd02550a..749d38ddfeb 100644 --- a/test/test-devices.js +++ b/test/test-devices.js @@ -6,6 +6,7 @@ import buildCommand from '../src/devices/command'; async function test0(t, withSES) { const config = { vatSources: new Map(), + vatOptions: new Map(), devices: [['d0', require.resolve('./files-devices/device-0'), {}]], bootstrapIndexJS: require.resolve('./files-devices/bootstrap-0'), }; @@ -40,6 +41,7 @@ async function test1(t, withSES) { const sharedArray = []; const config = { vatSources: new Map(), + vatOptions: new Map(), devices: [ [ 'd1', @@ -76,6 +78,7 @@ test('d1 without SES', async t => { async function test2(t, mode, withSES) { const config = { vatSources: new Map(), + vatOptions: new Map(), devices: [['d2', require.resolve('./files-devices/device-2'), {}]], bootstrapIndexJS: require.resolve('./files-devices/bootstrap-2'), }; @@ -174,6 +177,7 @@ test('d2.5 without SES', async t => { async function testState(t, withSES) { const config = { vatSources: new Map(), + vatOptions: new Map(), devices: [['d3', require.resolve('./files-devices/device-3'), {}]], bootstrapIndexJS: require.resolve('./files-devices/bootstrap-3'), initialState: JSON.stringify({}), @@ -203,6 +207,7 @@ async function testMailboxOutbound(t, withSES) { const mb = buildMailbox(s); const config = { vatSources: new Map(), + vatOptions: new Map(), devices: [['mailbox', mb.srcPath, mb.endowments]], bootstrapIndexJS: require.resolve('./files-devices/bootstrap-2'), }; @@ -244,6 +249,7 @@ async function testMailboxInbound(t, withSES) { const mb = buildMailbox(s); const config = { vatSources: new Map(), + vatOptions: new Map(), devices: [['mailbox', mb.srcPath, mb.endowments]], bootstrapIndexJS: require.resolve('./files-devices/bootstrap-2'), }; @@ -327,6 +333,7 @@ async function testCommandBroadcast(t, withSES) { const cm = buildCommand(); const config = { vatSources: new Map(), + vatOptions: new Map(), devices: [['command', cm.srcPath, cm.endowments]], bootstrapIndexJS: require.resolve('./files-devices/bootstrap-2'), }; @@ -353,6 +360,7 @@ async function testCommandDeliver(t, withSES) { const cm = buildCommand(); const config = { vatSources: new Map(), + vatOptions: new Map(), devices: [['command', cm.srcPath, cm.endowments]], bootstrapIndexJS: require.resolve('./files-devices/bootstrap-2'), }; diff --git a/test/test-vattp.js b/test/test-vattp.js index 98349e1174d..e0800c7d0fd 100644 --- a/test/test-vattp.js +++ b/test/test-vattp.js @@ -7,6 +7,7 @@ async function testVatTP(t, withSES) { const mb = buildMailbox(s); const config = { vatSources: new Map(), + vatOptions: new Map(), devices: [['mailbox', mb.srcPath, mb.endowments]], bootstrapIndexJS: require.resolve('./files-vattp/bootstrap-test-vattp'), }; @@ -45,6 +46,7 @@ async function testVatTP2(t, withSES) { const mb = buildMailbox(s); const config = { vatSources: new Map(), + vatOptions: new Map(), devices: [['mailbox', mb.srcPath, mb.endowments]], bootstrapIndexJS: require.resolve('./files-vattp/bootstrap-test-vattp'), }; From 55cac11add45a8678658f1cc3c8d647dfda8a5fd Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 7 Sep 2019 15:11:01 -0700 Subject: [PATCH 11/16] implement pipelining in comms outbound side --- src/vats/comms/clist.js | 90 +++++++++++++++--------------- src/vats/comms/dispatch.js | 64 +++++++++------------- src/vats/comms/inbound.js | 79 +++++++++++++-------------- src/vats/comms/outbound.js | 74 ++++++++++++++++++------- src/vats/comms/parseRemoteSlot.js | 2 +- src/vats/comms/state.js | 91 ++++++++++++++++++++++++++++++- 6 files changed, 253 insertions(+), 147 deletions(-) diff --git a/src/vats/comms/clist.js b/src/vats/comms/clist.js index 093232999a3..9b8a0511921 100644 --- a/src/vats/comms/clist.js +++ b/src/vats/comms/clist.js @@ -2,11 +2,21 @@ import Nat from '@agoric/nat'; import { makeVatSlot, parseVatSlot, insistVatType } from '../parseVatSlots'; import { flipRemoteSlot, + insistRemoteType, makeRemoteSlot, parseRemoteSlot, } from './parseRemoteSlot'; import { getRemote } from './remote'; -import { allocatePromise } from './state'; +import { + allocateUnresolvedPromise, + insistPromiseIsUnresolved, + insistPromiseDeciderIs, + insistPromiseDeciderIsMe, + insistPromiseSubscriberIsNotDifferent, + trackUnresolvedPromise, + setPromiseDecider, + setPromiseSubscriber, +} from './state'; import { insist } from '../../kernel/insist'; export function getOutbound(state, remoteID, target) { @@ -52,14 +62,17 @@ export function mapOutbound(state, remoteID, s, syscall) { const index = remote.nextObjectIndex; remote.nextObjectIndex += 1; // The recipient will receive ro-NN - remote.toRemote.set(s, makeRemoteSlot('object', false, index)); + const rs = makeRemoteSlot('object', false, index); + remote.toRemote.set(s, rs); // but when they send it back, they'll send ro+NN - remote.fromRemote.set(makeRemoteSlot('object', true, index), s); + remote.fromRemote.set(flipRemoteSlot(rs), s); } } else if (type === 'promise') { if (allocatedByVat) { + // this is probably a three-party handoff, not sure throw new Error(`unable to handle vat-allocated promise ${s} yet`); } else { + // todo: this might be overly restrictive if (state.promiseTable.has(s)) { // We already know about this promise, either because it arrived // from some other remote, or because we sent it to some other @@ -70,10 +83,7 @@ export function mapOutbound(state, remoteID, s, syscall) { !p.owner, `promise ${s} owner is ${p.owner}, not me, so I cannot send to ${remoteID}`, ); - insist( - !p.decider, - `promise ${s} decider is ${p.decider}, not me, so I cannot send to ${remoteID}`, - ); + insistPromiseDeciderIsMe(state, s); // also we can't handle more than a single subscriber yet insist( !p.subscriber, @@ -95,24 +105,11 @@ export function mapOutbound(state, remoteID, s, syscall) { remote.toRemote.set(s, rs); remote.fromRemote.set(flipRemoteSlot(rs), s); if (!state.promiseTable.has(s)) { - state.promiseTable.set(s, { - owner: null, // kernel-allocated, so ID is this-machine-allocated - resolved: false, - decider: null, // we decide - subscriber: remoteID, - }); + trackUnresolvedPromise(state, null, s); + setPromiseSubscriber(state, s, remoteID); syscall.subscribe(s); } } - } else if (type === 'resolver') { - // TODO: is this clause currently unused? - insist(!allocatedByVat, `resolver ${s} must be kernel-allocated for now`); - const index = remote.nextResolverIndex; - remote.nextResolverIndex += 1; - // recipient gets rr-NN - remote.toRemote.set(s, makeRemoteSlot('resolver', false, index)); - // recipient sends rr+NN - remote.fromRemote.set(makeRemoteSlot('resolver', true, index), s); } else { throw new Error(`unknown type ${type}`); } @@ -127,25 +124,19 @@ export function mapOutboundResult(state, remoteID, s) { insistVatType('promise', s); if (!state.promiseTable.has(s)) { - state.promiseTable.set(s, { - owner: null, // todo: what is this for anyways? - resolved: false, - decider: null, // for a brief moment, we're the decider - subscriber: null, - }); + // 'null' because, for a brief moment, we're the decider + trackUnresolvedPromise(state, null, s); } - const p = state.promiseTable.get(s); + // we (the local machine) must have resolution authority, which happens for // new promises, and for old ones that arrived as the 'result' of inbound // messages from remote machines (transferring authority to us). - insist(!p.decider, `result ${s} has decider ${p.decider}, not us`); - insist(!p.resolved, `result ${s} is already resolved`); + insistPromiseDeciderIsMe(state, s); + insistPromiseIsUnresolved(state, s); // if we received this promise from remote1, we can send it back to them, // but we can't send it to any other remote. - insist( - !p.subscriber || p.subscriber === remoteID, - `result ${s} has subscriber ${p.subscriber}, not none or ${remoteID}`, - ); + insistPromiseSubscriberIsNotDifferent(state, s, remoteID); + // todo: if we previously held resolution authority for this promise, then // transferred it to some local vat, we'll have subscribed to the kernel to // hear about it. If we then get the authority back again, we no longer @@ -154,7 +145,8 @@ export function mapOutboundResult(state, remoteID, s) { // get a bogus dispatch.notifyFulfill*. Currently we throw an error, which // is currently ignored but might prompt a vat shutdown in the future. - p.decider = remoteID; // transfer authority to recipient + // transfer resolution authority to recipient + setPromiseDecider(state, s, remoteID); const existing = remote.toRemote.get(s); if (!existing) { @@ -195,15 +187,9 @@ export function mapInbound(state, remoteID, s, _syscall) { state.objectTable.set(localSlot, remoteID); } else if (type === 'promise') { if (allocatedByRecipient) { - throw new Error(`promises not implemented yet`); + throw new Error(`I don't remember giving ${s} to ${remoteID}`); } else { - const promiseID = allocatePromise(state); - state.promiseTable.set(promiseID, { - owner: remoteID, - resolved: false, - decider: remoteID, - subscriber: null, - }); + const promiseID = allocateUnresolvedPromise(state, remoteID); remote.fromRemote.set(s, promiseID); remote.toRemote.set(promiseID, s); console.log(`inbound promise ${s} mapped to ${promiseID}`); @@ -225,6 +211,22 @@ export function getInbound(state, remoteID, target) { return remote.fromRemote.get(target); } +export function mapInboundResult(syscall, state, remoteID, result) { + insistRemoteType('promise', result); + insist(!parseRemoteSlot(result).allocatedByRecipient, result); // temp? + const r = mapInbound(state, remoteID, result, syscall); + insistVatType('promise', r); + insistPromiseIsUnresolved(state, r); + insistPromiseDeciderIs(state, r, remoteID); + insistPromiseSubscriberIsNotDifferent(state, r, remoteID); + setPromiseDecider(state, r, null); // (local kernel / other local vat) now decides + setPromiseSubscriber(state, r, remoteID); // auto-subscribe the sender + const remote = getRemote(state, remoteID); + remote.fromRemote.set(result, r); + remote.toRemote.set(r, flipRemoteSlot(result)); + return r; +} + export function addEgress(state, remoteID, remoteRefID, localRef) { // Make 'localRef' available to remoteID as 'remoteRefID'. This is kind of // like mapOutbound, but it uses a caller-provided remoteRef instead of diff --git a/src/vats/comms/dispatch.js b/src/vats/comms/dispatch.js index 966e0e0d86e..fc021a661c8 100644 --- a/src/vats/comms/dispatch.js +++ b/src/vats/comms/dispatch.js @@ -12,7 +12,7 @@ function transmit(syscall, state, remoteID, msg) { // the vat-tp "integrity layer" is a regular vat, so it expects an argument // encoded as JSON const body = JSON.stringify({ args: [msg] }); - syscall.send(remote.transmitterID, 'transmit', body, []); // todo: sendOnly + syscall.send(remote.transmitterID, 'transmit', body, []); // sendOnly } export const debugState = new WeakMap(); @@ -38,12 +38,12 @@ export function buildCommsDispatch(syscall, _state, _helpers) { } // console.log(`comms.deliver ${target} r=${result}`); // dumpState(state); - if (state.objectTable.has(target)) { + if (state.objectTable.has(target) || state.promiseTable.has(target)) { insist( method.indexOf(':') === -1 && method.indexOf(';') === -1, `illegal method name ${method}`, ); - const [remoteID, body] = deliverToRemote( + return deliverToRemote( syscall, state, target, @@ -51,8 +51,8 @@ export function buildCommsDispatch(syscall, _state, _helpers) { argsbytes, caps, result, + transmit, ); - return transmit(syscall, state, remoteID, body); } if (state.remoteReceivers.has(target)) { insist(method === 'receive', `unexpected method ${method}`); @@ -66,24 +66,23 @@ export function buildCommsDispatch(syscall, _state, _helpers) { message, ); } - // TODO: if (target in PromiseTable) : pipelining + + // TODO: if promise target not in PromiseTable: resolve result to error + // this will happen if someone pipelines to our controller/receiver throw new Error(`unknown target ${target}`); } - function notifyFulfillToData(promiseID, data, slots) { + function notifyFulfillToData(promiseID, body, slots) { + console.log(`comms.notifyFulfillToData(${promiseID})`); + // dumpState(state); + + // I *think* we should never get here for local promises, since the + // controller only does sendOnly. But if we change that, we need to catch + // locally-generated promises and deal with them. // if (promiseID in localPromises) { - // resolveLocal(promiseID, { type: 'data', data, slots }); + // resolveLocal(promiseID, { type: 'data', body, slots }); // } - // console.log(`notifyFulfillToData ${promiseID}`); - // dumpState(state); - const [remoteID, body] = resolvePromiseToRemote(syscall, state, promiseID, { - type: 'data', - data, - slots, - }); - if (remoteID) { - return transmit(syscall, state, remoteID, body); - } + // todo: if we previously held resolution authority for this promise, then // transferred it to some local vat, we'll have subscribed to the kernel to // hear about it. If we then get the authority back again, we no longer @@ -91,32 +90,21 @@ export function buildCommsDispatch(syscall, _state, _helpers) { // resolving), but the kernel still thinks of us as subscribing, so we'll // get a bogus dispatch.notifyFulfill*. Currently we throw an error, which // is currently ignored but might prompt a vat shutdown in the future. - throw new Error(`unknown promise ${promiseID}`); + + const resolution = harden({ type: 'data', body, slots }); + resolvePromiseToRemote(syscall, state, promiseID, resolution, transmit); } function notifyFulfillToPresence(promiseID, slot) { - // console.log(`notifyFulfillToPresence ${promiseID}`); - const [remoteID, body] = resolvePromiseToRemote(syscall, state, promiseID, { - type: 'object', - slot, - }); - if (remoteID) { - return transmit(syscall, state, remoteID, body); - } - throw new Error(`unknown promise ${promiseID}`); + console.log(`comms.notifyFulfillToPresence(${promiseID}) = ${slot}`); + const resolution = harden({ type: 'object', slot }); + resolvePromiseToRemote(syscall, state, promiseID, resolution, transmit); } - function notifyReject(promiseID, data, slots) { - // console.log(`notifyReject ${promiseID}`); - const [remoteID, body] = resolvePromiseToRemote(syscall, state, promiseID, { - type: 'reject', - data, - slots, - }); - if (remoteID) { - return transmit(syscall, state, remoteID, body); - } - throw new Error(`unknown promise ${promiseID}`); + function notifyReject(promiseID, body, slots) { + console.log(`comms.notifyReject(${promiseID})`); + const resolution = harden({ type: 'reject', body, slots }); + resolvePromiseToRemote(syscall, state, promiseID, resolution, transmit); } const dispatch = harden({ diff --git a/src/vats/comms/inbound.js b/src/vats/comms/inbound.js index f33657e30d7..468f4a2ba94 100644 --- a/src/vats/comms/inbound.js +++ b/src/vats/comms/inbound.js @@ -1,16 +1,16 @@ -import { getRemote } from './remote'; +import harden from '@agoric/harden'; +import { insistRemoteType } from './parseRemoteSlot'; +import { getInbound, mapInbound, mapInboundResult } from './clist'; import { - flipRemoteSlot, - insistRemoteType, - parseRemoteSlot, -} from './parseRemoteSlot'; -import { mapInbound, getInbound } from './clist'; -import { allocatePromise } from './state'; + insistPromiseIsUnresolved, + insistPromiseDeciderIs, + markPromiseAsResolved, +} from './state'; import { insist } from '../../kernel/insist'; export function deliverFromRemote(syscall, state, remoteID, message) { - const remote = getRemote(state, remoteID); const command = message.split(':', 1)[0]; + if (command === 'deliver') { // deliver:$target:$method:[$result][:$slots..];body const sci = message.indexOf(';'); @@ -27,39 +27,29 @@ export function deliverFromRemote(syscall, state, remoteID, message) { .slice(3) .map(s => mapInbound(state, remoteID, s, syscall)); const body = message.slice(sci + 1); - let r; + let r; // send() if result promise is provided, else sendOnly() if (result.length) { - // todo: replace with mapInboundResult, allow pre-existing promises if - // the decider is right - insistRemoteType('promise', result); - insist(!parseRemoteSlot(result).allocatedByRecipient, result); // temp? - r = allocatePromise(state); - state.promiseTable.set(r, { - owner: remoteID, - resolved: false, - decider: null, - subscriber: remoteID, - }); - remote.fromRemote.set(result, r); - remote.toRemote.set(r, flipRemoteSlot(result)); + r = mapInboundResult(syscall, state, remoteID, result); } - // else it's a sendOnly() syscall.send(target, method, body, msgSlots, r); if (r) { - // todo: can/should we subscribe to this early, before syscall.send()? - // probably not. + // syscall.subscribe() happens after the send(), so it doesn't have to + // deal with allocating promises too syscall.subscribe(r); } // dumpState(state); - } else if (command === 'resolve') { + return; + } + + if (command === 'resolve') { // console.log(`-- deliverFromRemote.resolve, ${message}, pre-state is:`); // dumpState(state); const sci = message.indexOf(';'); insist(sci !== -1, `missing semicolon in resolve ${message}`); // message is created by resolvePromiseToRemote, so one of: // `resolve:object:${target}:${resolutionRef};` - // `resolve:data:${target}${rmss};${resolution.data}` - // `resolve:reject:${target}${rmss};${resolution.data}` + // `resolve:data:${target}${rmss};${resolution.body}` + // `resolve:reject:${target}${rmss};${resolution.body}` const pieces = message.slice(0, sci).split(':'); // pieces[0] is 'resolve' @@ -68,29 +58,34 @@ export function deliverFromRemote(syscall, state, remoteID, message) { const remoteSlots = pieces.slice(3); // length=1 for resolve:object insistRemoteType('promise', remoteTarget); // slots[0] is 'rp+NN`. const target = getInbound(state, remoteID, remoteTarget); - // rp+NN maps to target=p-+NN and we look at the promiseTable to make - // sure it's in the right state. - const p = state.promiseTable.get(target); - insist(p, `${target} is not in the promiseTable`); - insist(!p.resolved, `${target} is already resolved`); - insist( - p.decider === remoteID, - `${p.decider} is the decider of ${target}, not ${remoteID}`, - ); const slots = remoteSlots.map(s => mapInbound(state, remoteID, s, syscall)); const body = message.slice(sci + 1); + + // rp+NN maps to target=p-+NN and we look at the promiseTable to make + // sure it's in the right state. + insistPromiseIsUnresolved(state, target); + insistPromiseDeciderIs(state, target, remoteID); + if (type === 'object') { - syscall.fulfillToPresence(target, slots[0]); + const slot = slots[0]; + const resolution = harden({ type: 'object', slot }); + markPromiseAsResolved(state, target, resolution); + syscall.fulfillToPresence(target, slot); } else if (type === 'data') { + const resolution = harden({ type: 'data', body, slots }); + markPromiseAsResolved(state, target, resolution); syscall.fulfillToData(target, body, slots); } else if (type === 'reject') { + const resolution = harden({ type: 'reject', body, slots }); + markPromiseAsResolved(state, target, resolution); syscall.reject(target, body, slots); } else { throw new Error(`unknown resolution type ${type} in ${message}`); } - } else { - throw new Error( - `unrecognized command ${command} in received message ${message}`, - ); + return; } + + throw new Error( + `unrecognized command ${command} in received message ${message}`, + ); } diff --git a/src/vats/comms/outbound.js b/src/vats/comms/outbound.js index f9a40fb81c6..df91b8a4b4b 100644 --- a/src/vats/comms/outbound.js +++ b/src/vats/comms/outbound.js @@ -1,9 +1,41 @@ import { insistVatType } from '../parseVatSlots'; import { insistRemoteType } from './parseRemoteSlot'; import { getOutbound, mapOutbound, mapOutboundResult } from './clist'; +import { + getPromiseSubscriber, + insistPromiseDeciderIsMe, + insistPromiseIsUnresolved, + markPromiseAsResolved, +} from './state'; import { insistRemoteID } from './remote'; import { insist } from '../../kernel/insist'; +function getRemoteFor(state, target) { + if (state.objectTable.has(target)) { + return state.objectTable.get(target); + } + if (state.promiseTable.has(target)) { + const p = state.promiseTable.get(target); + if (p.state === 'unresolved') { + return p.decider; + } + if (p.state === 'fulfilledToPresence') { + return getRemoteFor(state, p.fulfillSlot); + } + if (p.state === 'fulfilledToData') { + throw new Error(`todo: error for fulfilledToData`); + } else if (p.state === 'rejected') { + throw new Error(`todo: error for rejected`); + } else if (p.stated === 'forwarded') { + // todo + return getRemoteFor(state, p.forwardedSlot); + } else { + throw new Error(`unknown p.state ${p.state}`); + } + } + throw new Error(`unknown target type ${target}`); +} + export function deliverToRemote( syscall, state, @@ -12,10 +44,11 @@ export function deliverToRemote( data, slots, result, + transmit, ) { // this object lives on 'remoteID', so we send messages at them - const remoteID = state.objectTable.get(target); - insist(remoteID !== undefined, `oops ${target}`); + const remoteID = getRemoteFor(state, target); + insist(remoteID, `oops ${target}`); const remoteTargetSlot = getOutbound(state, remoteID, target); const remoteMessageSlots = slots.map(s => @@ -38,20 +71,27 @@ export function deliverToRemote( const msg = `deliver:${remoteTargetSlot}:${method}:${remoteResultSlot}${rmss};${data}`; // console.log(`deliverToRemote(target=${target}/${remoteTargetSlot}, result=${result}/${remoteResultSlot}) leaving state as:`); // dumpState(state); - return [remoteID, msg]; + transmit(syscall, state, remoteID, msg); } -export function resolvePromiseToRemote(syscall, state, promiseID, resolution) { +export function resolvePromiseToRemote( + syscall, + state, + promiseID, + resolution, + transmit, +) { // console.log(`resolvePromiseToRemote ${promiseID}`, resolution); insistVatType('promise', promiseID); - const p = state.promiseTable.get(promiseID); - if (!p || !p.subscriber) { - return [undefined, undefined]; // todo: local promise? - } - insist(!p.resolved, `${promiseID} is already resolved`); - insist(!p.decider, `${p.decider} is the decider for ${promiseID}, not me`); - const remoteID = p.subscriber; + insistPromiseIsUnresolved(state, promiseID); + insistPromiseDeciderIsMe(state, promiseID); + const remoteID = getPromiseSubscriber(state, promiseID); insistRemoteID(remoteID); + + // mark it as resolved in the promise table, so later messages to it will + // be handled properly + markPromiseAsResolved(state, promiseID, resolution); + // for now, promiseID = p-NN, later will be p+NN const target = getOutbound(state, remoteID, promiseID); // target should be rp+NN @@ -77,16 +117,12 @@ export function resolvePromiseToRemote(syscall, state, promiseID, resolution) { ); msg = `resolve:object:${target}:${resolutionRef};`; } else if (resolution.type === 'data') { - const rmss = mapSlots(); - msg = `resolve:data:${target}${rmss};${resolution.data}`; + msg = `resolve:data:${target}${mapSlots()};${resolution.body}`; } else if (resolution.type === 'reject') { - const rmss = mapSlots(); - msg = `resolve:reject:${target}${rmss};${resolution.data}`; + msg = `resolve:reject:${target}${mapSlots()};${resolution.body}`; } else { throw new Error(`unknown resolution type ${resolution.type}`); } - p.resolved = true; - p.decider = undefined; - p.subscriber = undefined; - return [remoteID, msg]; + + transmit(syscall, state, remoteID, msg); } diff --git a/src/vats/comms/parseRemoteSlot.js b/src/vats/comms/parseRemoteSlot.js index 0038d28ee58..f576a666698 100644 --- a/src/vats/comms/parseRemoteSlot.js +++ b/src/vats/comms/parseRemoteSlot.js @@ -7,7 +7,7 @@ import { insist } from '../../kernel/insist'; // message, and "-" when allocated by the sender of the message. export function parseRemoteSlot(s) { - insist(s === `${s}`); + insist(s === `${s}`, `${s} is not a string`); let type; let allocatedByRecipient; const typechars = s.slice(0, 2); diff --git a/src/vats/comms/state.js b/src/vats/comms/state.js index ea8f7d5d1be..6483a132d4d 100644 --- a/src/vats/comms/state.js +++ b/src/vats/comms/state.js @@ -1,3 +1,4 @@ +import { insist } from '../../kernel/insist'; import { makeVatSlot } from '../parseVatSlots'; export function makeState() { @@ -12,7 +13,11 @@ export function makeState() { // hopefully we can avoid the need for local promises // localPromises: new Map(), // p+NN/p-NN -> local purpose - promiseTable: new Map(), // p+NN/p-NN -> { owner, resolved, decider, subscriber } + promiseTable: new Map(), // p+NN/p-NN -> { state, owner, decider, subscriber } + // and maybe resolution, one of: + // * {type: 'object', slot} + // * {type: 'data', body, slots} + // * {type: 'reject', body, slots} nextPromiseIndex: 20, }; @@ -46,8 +51,88 @@ export function dumpState(state) { } } -export function allocatePromise(state) { +export function trackUnresolvedPromise(state, remoteID, pid) { + insist(!state.promiseTable.has(pid), `${pid} already present`); + state.promiseTable.set(pid, { + owner: remoteID, + state: 'unresolved', + decider: remoteID, + subscriber: null, + }); +} + +export function allocateUnresolvedPromise(state, remoteID) { const index = state.nextPromiseIndex; state.nextPromiseIndex += 1; - return makeVatSlot('promise', true, index); + const pid = makeVatSlot('promise', true, index); + trackUnresolvedPromise(state, remoteID, pid); + return pid; +} + +export function setPromiseDecider(state, promiseID, decider) { + insist(state.promiseTable.has(promiseID), `unknown ${promiseID}`); + state.promiseTable.get(promiseID).decider = decider; +} + +export function setPromiseSubscriber(state, promiseID, subscriber) { + insist(state.promiseTable.has(promiseID), `unknown ${promiseID}`); + state.promiseTable.get(promiseID).subscriber = subscriber; +} + +export function insistPromiseIsUnresolved(state, promiseID) { + insist(state.promiseTable.has(promiseID), `unknown ${promiseID}`); + const pstate = state.promiseTable.get(promiseID).state; + insist( + pstate === 'unresolved', + `${promiseID} has state ${pstate}, not 'unresolved'`, + ); +} + +export function insistPromiseDeciderIs(state, promiseID, remoteID) { + insist(state.promiseTable.has(promiseID), `unknown ${promiseID}`); + const { decider } = state.promiseTable.get(promiseID); + insist( + decider === remoteID, + `${promiseID} is decided by ${decider}, not ${remoteID}`, + ); +} + +export function insistPromiseDeciderIsMe(state, promiseID) { + insist(state.promiseTable.has(promiseID), `unknown ${promiseID}`); + const { decider } = state.promiseTable.get(promiseID); + insist(!decider, `${decider} is the decider for ${promiseID}, not me`); +} + +export function insistPromiseSubscriberIsNotDifferent( + state, + promiseID, + remoteID, +) { + insist(state.promiseTable.has(promiseID), `unknown ${promiseID}`); + const { subscriber } = state.promiseTable.get(promiseID); + if (subscriber) { + insist( + subscriber === remoteID, + `${promiseID} subscriber is ${subscriber}, not ${remoteID}`, + ); + } +} + +export function getPromiseSubscriber(state, promiseID) { + insist(state.promiseTable.has(promiseID), `unknown ${promiseID}`); + const { subscriber } = state.promiseTable.get(promiseID); + insist(subscriber, `${promiseID} has no subscriber`); + return subscriber; +} + +export function markPromiseAsResolved(state, promiseID, resolution) { + insist(state.promiseTable.has(promiseID), `unknown ${promiseID}`); + const p = state.promiseTable.get(promiseID); + insist( + p.state === 'unresolved', + `${promiseID} is already resolved (${p.state})`, + ); + p.resolution = resolution; + p.decider = undefined; + p.subscriber = undefined; } From 73399b07701cade1b2f2c809c88f371297fdad9e Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 4 Sep 2019 16:40:15 -0700 Subject: [PATCH 12/16] tests: new framework to exercise various message patterns including pipelining. This will absorb the comms-integration tests, and will exercise the same patterns on both local-only and comms-connected vats. I'm trying to make the test format as concise as possible, so we feel no hesitation to add lots of them. Some of the non-comms patterns are showing pipeline-esque behavior anyways, because of the relative ordering of promise resolution: if the initial promise in a pipeline chain is resolved before the second message reaches the front of the queue, the second message will be delivered right away instead of being queued inside the promise, which results in the same delivery order as real pipelining would provide. To demonstrate non-pipelining in the local (non-comms) case, I had to add a new pattern (a72) which explicitly delays that first resolution until all the pipelined messages have made it through the kernel queue. That pattern shows pipelining when run through comms vats, but not when run locally. --- .../bootstrap-comms.js | 61 +++ .../bootstrap-local.js | 18 + test/basedir-message-patterns/vat-a.js | 33 ++ test/basedir-message-patterns/vat-b.js | 26 + test/message-patterns.js | 517 ++++++++++++++++++ test/test-message-patterns.js | 111 ++++ 6 files changed, 766 insertions(+) create mode 100644 test/basedir-message-patterns/bootstrap-comms.js create mode 100644 test/basedir-message-patterns/bootstrap-local.js create mode 100644 test/basedir-message-patterns/vat-a.js create mode 100644 test/basedir-message-patterns/vat-b.js create mode 100644 test/message-patterns.js create mode 100644 test/test-message-patterns.js diff --git a/test/basedir-message-patterns/bootstrap-comms.js b/test/basedir-message-patterns/bootstrap-comms.js new file mode 100644 index 00000000000..8ce97a8c5d8 --- /dev/null +++ b/test/basedir-message-patterns/bootstrap-comms.js @@ -0,0 +1,61 @@ +import harden from '@agoric/harden'; + +function build(E, D) { + const root = harden({ + async bootstrap(argv, vats, devices) { + // setup + const LEFT = 'left'; // for vat A + const RIGHT = 'right'; // for vat B + + D(devices.loopbox).registerInboundHandler(LEFT, vats.leftvattp); + const leftsender = D(devices.loopbox).makeSender(LEFT); + await E(vats.leftvattp).registerMailboxDevice(leftsender); + + const { + transmitter: txToRightForLeft, + setReceiver: setRxFromRightForLeft, + } = await E(vats.leftvattp).addRemote(RIGHT); + const rxFromRightForLeft = await E(vats.leftcomms).addRemote( + RIGHT, + txToRightForLeft, + ); + await E(setRxFromRightForLeft).setReceiver(rxFromRightForLeft); + + D(devices.loopbox).registerInboundHandler(RIGHT, vats.rightvattp); + const rightsender = D(devices.loopbox).makeSender(RIGHT); + await E(vats.rightvattp).registerMailboxDevice(rightsender); + + const { + transmitter: txToLeftForRight, + setReceiver: setRxFromLeftForRight, + } = await E(vats.rightvattp).addRemote(LEFT); + const rxFromLeftForRight = await E(vats.rightcomms).addRemote( + LEFT, + txToLeftForRight, + ); + await E(setRxFromLeftForRight).setReceiver(rxFromLeftForRight); + + // get B set up + const { bob, bert } = await E(vats.b).init(); + + const BOB_INDEX = 12; + const BERT_INDEX = 13; + + await E(vats.rightcomms).addEgress(LEFT, BOB_INDEX, bob); + const aBob = await E(vats.leftcomms).addIngress(RIGHT, BOB_INDEX); + + await E(vats.rightcomms).addEgress(LEFT, BERT_INDEX, bert); + const aBert = await E(vats.leftcomms).addIngress(RIGHT, BERT_INDEX); + + // eslint-disable-next-line no-unused-vars + const a = await E(vats.a).init(aBob, aBert); + const which = argv[0]; + await E(vats.a).run(which); + }, + }); + return root; +} + +export default function setup(syscall, state, helpers) { + return helpers.makeLiveSlots(syscall, state, build, helpers.vatID); +} diff --git a/test/basedir-message-patterns/bootstrap-local.js b/test/basedir-message-patterns/bootstrap-local.js new file mode 100644 index 00000000000..b76dbed131a --- /dev/null +++ b/test/basedir-message-patterns/bootstrap-local.js @@ -0,0 +1,18 @@ +import harden from '@agoric/harden'; + +function build(E) { + const root = harden({ + async bootstrap(argv, vats, _devices) { + const which = argv[0]; + const b = await E(vats.b).init(); + // eslint-disable-next-line no-unused-vars + const a = await E(vats.a).init(b.bob, b.bert); + await E(vats.a).run(which); + }, + }); + return root; +} + +export default function setup(syscall, state, helpers) { + return helpers.makeLiveSlots(syscall, state, E => build(E), helpers.vatID); +} diff --git a/test/basedir-message-patterns/vat-a.js b/test/basedir-message-patterns/vat-a.js new file mode 100644 index 00000000000..04a834ea24c --- /dev/null +++ b/test/basedir-message-patterns/vat-a.js @@ -0,0 +1,33 @@ +import harden from '@agoric/harden'; +import { buildPatterns } from '../message-patterns'; + +function build(E, log) { + const amy = harden({ toString: () => 'obj-amy' }); + let alice; + + const root = harden({ + init(bob, bert) { + const { setA, setB, objA } = buildPatterns(E, log); + alice = objA; + const a = harden({ alice, amy }); + setA(a); + setB(harden({ bob, bert })); + return a; + }, + + async run(which) { + console.log(`running alice[${which}]`); + await alice[which](); + }, + }); + return root; +} + +export default function setup(syscall, state, helpers) { + return helpers.makeLiveSlots( + syscall, + state, + E => build(E, helpers.log), + helpers.vatID, + ); +} diff --git a/test/basedir-message-patterns/vat-b.js b/test/basedir-message-patterns/vat-b.js new file mode 100644 index 00000000000..dc036338895 --- /dev/null +++ b/test/basedir-message-patterns/vat-b.js @@ -0,0 +1,26 @@ +import harden from '@agoric/harden'; +import { buildPatterns } from '../message-patterns'; + +function build(E, log) { + const bert = harden({ toString: () => 'obj-bert' }); + const bill = harden({ toString: () => 'obj-bill' }); + + const root = harden({ + init() { + const { setB, objB } = buildPatterns(E, log); + const b = harden({ bob: objB, bert, bill }); + setB(b); + return harden({ bob: objB, bert }); + }, + }); + return root; +} + +export default function setup(syscall, state, helpers) { + return helpers.makeLiveSlots( + syscall, + state, + E => build(E, helpers.log), + helpers.vatID, + ); +} diff --git a/test/message-patterns.js b/test/message-patterns.js new file mode 100644 index 00000000000..019ed149b12 --- /dev/null +++ b/test/message-patterns.js @@ -0,0 +1,517 @@ +/* eslint no-lone-blocks: "off" */ +/* eslint dot-notation: "off" */ +// I turned off dot-notation so eslint won't rewrite the grep-preserving +// test.stuff patterns. + +import harden from '@agoric/harden'; +import makePromise from '../src/kernel/makePromise'; + +// Exercise a set of increasingly complex object-capability message patterns, +// for testing. + +// This file can be used two ways: +// 1: included by a vat: 'objA' and 'objB' implement the root object on the +// two sides, and 'setA'/'setB' can be used during bootstrap to establish +// the initial conditions. The caller (a vat setup function) must provide +// 'E' and 'log'. +// 2: included by the test code: 'patterns' and 'expected' tell the test +// harness which tests to run and what log messages to expect. + +// Each test is identified by the name of a property on objA (like 'a10'). +// Each test begins by invoking that method, which may invoke one or more +// methods on objB (with a related name like 'b10' or 'b10_2'). The 'a10' +// property on the 'out' object is a list of log() calls we expect to see if +// the test passes. + +// Sometimes the expected output depends upon whether pipelining is enabled +// (which will only occur when using the comms layer, not in the +// direct-to-kernel test). 'outPipelined' holds these alternate expectations. + +// 'patterns' is used to track which tests should be skipped (or which should +// be the only test run at all). Each defined pattern must call test(name) to +// add it to the list. In addition, if you want to skip something, call +// 'test. skipLocal(name)' (without the space) and/or 'test. +// skipComms(name)'. To mark a test as the only one to run, call `test. +// onlyLocal(name)' or 'test. onlyComms(name)' (again without the space). (We +// insert a space in this description so a simple 'grep' can still accurately +// show the presence of skipped/only tests). + +// Initial Conditions: vat A (which hosts objects 'alice' and 'amy'), and vat +// B (hosting objects 'bob' and 'bert' and 'bill'). Initially alice has +// access to amy/bob/bert but not bill. Bob has access to bert and bill. + +// The setup step requires more functionality than the initial tests, but +// that's ok, they're still useful to investigate the messages +// created/delivered by specific patterns + +// All messages should be sent twice, to check that the recipient gets the +// same object reference in both messages + +export function buildPatterns(E, log) { + let a; + let b; + + function setA(newA) { + a = newA; + } + function setB(newB) { + b = newB; + } + + const patterns = new Map(); + const objA = { toString: () => 'obj-alice' }; + const objB = { toString: () => 'obj-bob' }; + const out = {}; + const outPipelined = {}; + + // avoid dot-notation to preserve the utility of 'grep test(.)only' + const test = name => patterns.set(name, { local: 'test', comms: 'test' }); + test['onlyLocal'] = n => patterns.set(n, { local: 'only', comms: 'test' }); + test['onlyComms'] = n => patterns.set(n, { local: 'test', comms: 'only' }); + test['skipLocal'] = n => patterns.set(n, { local: 'skip', comms: 'test' }); + test['skipComms'] = n => patterns.set(n, { local: 'test', comms: 'skip' }); + test['skipBoth'] = n => patterns.set(n, { local: 'skip', comms: 'skip' }); + + // bob!x() + test('a10'); + { + objA.a10 = async () => { + await E(b.bob).b10(); + log('a10 done'); + }; + objB.b10 = async () => { + log('b10 called'); + }; + } + out.a10 = ['b10 called', 'a10 done']; + + // bob!x(data) + { + objA.a11 = async () => { + await E(b.bob).b11('data'); + log('a11 done'); + }; + objB.b11 = async data => { + log(`b11 got ${data}`); + }; + } + out.a11 = ['b11 got data', 'a11 done']; + test('a11'); + + // bob!x({key:'value'}) + { + objA.a12 = async () => { + await E(b.bob).b12({ key: 'value' }); + log('a12 done'); + }; + objB.b12 = async data => { + log(`b12 got ${data.key}`); + }; + } + out.a12 = ['b12 got value', 'a12 done']; + test('a12'); + + // bob!x(amy) // new reference + { + objA.a20 = async () => { + await E(b.bob).b20_1(a.amy); + await E(b.bob).b20_2(a.amy); + log('a20 done'); + }; + let b20amy; + objB.b20_1 = async data => { + b20amy = data; + log(`b20 got ${data}`); + }; + objB.b20_2 = async data => { + log(`match: ${b20amy === data}`); + }; + } + out.a20 = ['b20 got [Presence o-50]', 'match: true', 'a20 done']; + test('a20'); + + // bob!x({key: amy}) + { + objA.a21 = async () => { + await E(b.bob).b21_1({ key1: a.amy }); + await E(b.bob).b21_2({ key2: a.amy }); + log('a21 done'); + }; + let b21amy; + objB.b21_1 = async data => { + b21amy = data.key1; + log(`b21 got ${data.key1}`); + }; + objB.b21_2 = async data => { + log(`match: ${b21amy === data.key2}`); + }; + } + out.a21 = ['b21 got [Presence o-50]', 'match: true', 'a21 done']; + test('a21'); + + // bob!x(bob) + { + objA.a30 = async () => { + await E(b.bob).b30_1(b.bob); + await E(b.bob).b30_2(b.bob); + log('a30 done'); + }; + objB.b30_1 = async data => { + log(`b30 got ${data}`); + log(`match: ${data === objB}`); + }; + objB.b30_2 = async data => { + log(`match: ${data === objB}`); + }; + } + out.a30 = ['b30 got obj-bob', 'match: true', 'match: true', 'a30 done']; + test('a30'); + + // bob!x(bert) // old reference + { + objA.a31 = async () => { + await E(b.bob).b31_1(b.bert); + await E(b.bob).b31_2(b.bert); + log('a31 done'); + }; + objB.b31_1 = async data => { + log(`b31 got ${data}`); + log(`match: ${data === b.bert}`); + }; + objB.b31_2 = async data => { + log(`match: ${data === b.bert}`); + }; + } + out.a31 = ['b31 got obj-bert', 'match: true', 'match: true', 'a31 done']; + test('a31'); + + // bob!x() -> bob // bob returns himself + { + objA.a40 = async () => { + const ret = await E(b.bob).b40(); + const ret2 = await E(b.bob).b40(); + log(`a40 done, match ${ret === b.bob} ${ret === ret2}`); + }; + objB.b40 = async () => { + return b.bob; + }; + } + out.a40 = ['a40 done, match true true']; + test('a40'); + + // bob!x() -> bert // old reference + { + objA.a41 = async () => { + const ret = await E(b.bob).b41(); + const ret2 = await E(b.bob).b41(); + log(`a41 done, match ${ret === b.bert} ${ret === ret2}`); + }; + objB.b41 = async () => { + return b.bert; + }; + } + out.a41 = ['a41 done, match true true']; + test('a41'); + + // bob!x() -> bill // new reference + { + objA.a42 = async () => { + const ret = await E(b.bob).b42(); + const ret2 = await E(b.bob).b42(); + log(`a42 done, ${ret} match ${ret === ret2}`); + }; + objB.b42 = async () => { + return b.bill; + }; + } + out.a42 = ['a42 done, [Presence o-52] match true']; + test('a42'); + + // bob!x() -> P(data) + { + // bob returns a (wrapped) promise, then resolves it to data. We wrap it + // so the resolution is delivered as a separate event. + objA.a50 = async () => { + const ret = await E(b.bob).b50(); + const data = await ret.p; + log(`a50 done, got ${data}`); + }; + const p1 = makePromise(); + objB.b50 = async () => { + p1.res('data'); + return harden({ p: p1.p }); + }; + } + out.a50 = ['a50 done, got data']; + test('a50'); + + // bob!x() -> P(bert) // old reference + { + // bob returns a (wrapped) promise, then resolves it to a presence + objA.a51 = async () => { + const ret = await E(b.bob).b51(); + const bert = await ret.p; + const bert2 = await E(b.bob).b51_2(); + log(`a51 done, got ${bert}, match ${bert === bert2} ${bert === b.bert}`); + }; + const p1 = makePromise(); + objB.b51 = async () => { + p1.res(b.bert); + return harden({ p: p1.p }); + }; + objB.b51_2 = async () => { + return b.bert; + }; + } + out.a51 = ['a51 done, got [Presence o-51], match true true']; + test('a51'); + + // bob!x() -> P(bill) // new reference + { + // bob returns a (wrapped) promise, then resolves it to a new presence + objA.a52 = async () => { + const ret = await E(b.bob).b52(); + const bill = await ret.p; + const bill2 = await E(b.bob).b52_2(); + log(`a52 done, got ${bill}, match ${bill === bill2}`); + }; + const p1 = makePromise(); + objB.b52 = async () => { + p1.res(b.bill); + return harden({ p: p1.p }); + }; + objB.b52_2 = async () => { + return b.bill; + }; + } + out.a52 = ['a52 done, got [Presence o-52], match true']; + test('a52'); + + // bob!x(amy) -> P(amy) // new to bob + { + objA.a53 = async () => { + const ret = await E(b.bob).b53(a.amy); + const amy2 = await ret.p; + log(`a53 done, match ${amy2 === a.amy}`); + }; + const p1 = makePromise(); + objB.b53 = async amy => { + p1.res(amy); + return harden({ p: p1.p }); + }; + } + out.a53 = ['a53 done, match true']; + test('a53'); + + // bob!x(P(amy)) -> amy // resolve after sending + { + objA.a60 = async () => { + const p1 = makePromise(); + const p2 = E(b.bob).b60({ p: p1.p }); + p1.res(a.amy); + const amy2 = await p2; + log(`a60 done, match ${amy2 === a.amy}`); + }; + objB.b60 = async Pamy => { + const amy = await Pamy.p; + return amy; + }; + } + out.a60 = ['a60 done, match true']; + test('a60'); + + // bob!x(P(amy)) -> amy // resolve before sending + { + objA.a61 = async () => { + const p1 = Promise.resolve(a.amy); + const p2 = E(b.bob).b61({ p: p1 }); + const amy2 = await p2; + log(`a61 done, match ${amy2 === a.amy}`); + }; + objB.b61 = async Pamy => { + const amy = await Pamy.p; + return amy; + }; + } + out.a61 = ['a61 done, match true']; + test('a61'); + + // bob!x() -> P(bill) // resolve after receipt + { + objA.a62 = async () => { + const p2 = await E(b.bob).b62_1(); + E(b.bob).b62_2(); + const bill = await p2.p; + log(`a62 done, got ${bill}`); + }; + const p1 = makePromise(); + objB.b62_1 = async () => { + return { p: p1.p }; + }; + objB.b62_2 = async () => { + p1.res(b.bill); + }; + } + out.a62 = ['a62 done, got [Presence o-52]']; + test('a62'); + + // bob!x(amy) -> P(amy) // resolve after receipt + { + objA.a63 = async () => { + const p2 = await E(b.bob).b63_1(a.amy); + E(b.bob).b63_2(); + const amy2 = await p2.p; + log(`a63 done, match ${amy2 === a.amy}`); + }; + const p1 = makePromise(); + let amyOnBob; + objB.b63_1 = async amy2 => { + amyOnBob = amy2; + return { p: p1.p }; + }; + objB.b63_2 = async () => { + p1.res(amyOnBob); + }; + } + out.a63 = ['a63 done, match true']; + test('a63'); + + // bob!pipe1()!pipe2()!pipe3() // pipelining + { + objA.a70 = async () => { + const p1 = E(b.bob).b70_pipe1(); + const p2 = E(p1).pipe2(); + const p3 = E(p2).pipe3(); + p1.then(_ => log('p1.then')); + p2.then(_ => log('p2.then')); + p3.then(_ => log('p3.then')); + }; + objB.b70_pipe1 = async () => { + log(`pipe1`); + const pipe2 = harden({ + pipe2() { + log(`pipe2`); + const pipe3 = harden({ + pipe3() { + log(`pipe3`); + }, + }); + return pipe3; + }, + }); + return pipe2; + }; + } + out.a70 = ['pipe1', 'pipe2', 'pipe3', 'p1.then', 'p2.then', 'p3.then']; + outPipelined.a70 = [ + 'pipe1', + 'pipe2', + 'pipe3', + 'p1.then', + 'p2.then', + 'p3.then', + ]; + test('a70'); + + // px!pipe1()!pipe2()!pipe3(); px.resolve() + { + objA.a71 = async () => { + // todo: all the pipelined calls (pipe123) get sent to the kernel a + // turn after the two call-to-presence calls (getpx/resolvex), which is + // a bit weird. Feels like p.post gets one extra stall. + const px = E(b.bob).b71_getpx(); + const p1 = E(px).pipe1(); + const p2 = E(p1).pipe2(); + const p3 = E(p2).pipe3(); + p1.then(_ => log('p1.then')); + p2.then(_ => log('p2.then')); + p3.then(_ => log('p3.then')); + E(b.bob).b71_resolvex(); + }; + const p1 = makePromise(); + objB.b71_getpx = async () => p1.p; + objB.b71_resolvex = async () => { + const x = harden({ + pipe1() { + log(`pipe1`); + const pipe2 = harden({ + pipe2() { + log(`pipe2`); + const pipe3 = harden({ + pipe3() { + log(`pipe3`); + }, + }); + return pipe3; + }, + }); + return pipe2; + }, + }); + p1.res(x); + }; + } + out.a71 = ['pipe1', 'pipe2', 'pipe3', 'p1.then', 'p2.then', 'p3.then']; + test('a71'); + + // px!pipe1()!pipe2()!pipe3(); px.resolve() but better + { + objA.a72 = async () => { + const px = E(b.bob).b72_getpx(); + const p1 = E(px).pipe1(); + const p2 = E(p1).pipe2(); + const p3 = E(p2).pipe3(); + p1.then(_ => log('p1.then')); + p2.then(_ => log('p2.then')); + p3.then(_ => log('p3.then')); + // make sure px isn't available for delivery until pipe123 are queued + // in the kernel + E(b.bob) + .b72_wait() + .then(() => E(b.bob).b72_resolvex()); + }; + const p1 = makePromise(); + objB.b72_wait = async () => 0; + objB.b72_getpx = async () => p1.p; + objB.b72_resolvex = async () => { + const x = harden({ + pipe1() { + log(`pipe1`); + const pipe2 = harden({ + pipe2() { + log(`pipe2`); + const pipe3 = harden({ + pipe3() { + log(`pipe3`); + }, + }); + return pipe3; + }, + }); + return pipe2; + }, + }); + p1.res(x); + }; + } + out.a72 = ['pipe1', 'p1.then', 'pipe2', 'p2.then', 'pipe3', 'p3.then']; + outPipelined.a72 = [ + 'pipe1', + 'pipe2', + 'pipe3', + 'p1.then', + 'p2.then', + 'p3.then', + ]; + test('a72'); + + return harden({ + setA, + setB, + patterns, + objA, + objB, + expected: out, + expected_pipelined: outPipelined, + }); +} diff --git a/test/test-message-patterns.js b/test/test-message-patterns.js new file mode 100644 index 00000000000..2060ce9c1c2 --- /dev/null +++ b/test/test-message-patterns.js @@ -0,0 +1,111 @@ +/* eslint no-await-in-loop: "off" */ +/* eslint dot-notation: "off" */ +/* eslint object-shorthand: "off" */ + +import path from 'path'; +import { test } from 'tape-promise/tape'; +import { + buildVatController, + getCommsSourcePath, + getVatTPSourcePath, + loadBasedir, +} from '../src/index'; +import { buildPatterns } from './message-patterns'; + +// This exercises all the patterns in 'message-patterns.js' twice (once with +// vatA/vatB connected directly through the kernel, and a second time with +// comms vats in the path). To enable/disable specific tests, edit the +// entries in that file. + +// use test['only'] so 'grep test(.)only' won't have false matches +const modes = { + test: test, + only: test['only'], + skip: test['skip'], +}; + +// eslint-disable-next-line no-unused-vars +async function runWithTrace(c) { + let count = 0; + while (c.dump().runQueue.length) { + console.log('-'); + console.log(`--- turn starts`, count); + count += 1; + await c.step(); + // console.log(c.dump().kernelTable); + for (const q of c.dump().runQueue) { + if (q.type === 'send') { + console.log( + ` send ${q.msg.result} = ${q.target}!${ + q.msg.method + }(${q.msg.slots.join(',')} ${q.msg.argsString})`, + ); + } else if (q.type === 'notify') { + console.log(` notify ${q.vatID}: ${q.kpid}`); + } + } + } +} + +export async function runVatsLocally(t, withSES, name) { + console.log(`------ testing pattern (local) -- ${name}`); + const bdir = path.resolve(__dirname, 'basedir-message-patterns'); + const config = await loadBasedir(bdir); + config.bootstrapIndexJS = path.join(bdir, 'bootstrap-local.js'); + const c = await buildVatController(config, withSES, [name]); + // await runWithTrace(c); + await c.run(); + return c.dump().log; +} + +function testLocalPatterns() { + const withSES = false; + const bp = buildPatterns(); + for (const name of Array.from(bp.patterns.keys()).sort()) { + const mode = bp.patterns.get(name).local; + modes[mode](`test pattern ${name} locally`, async t => { + const logs = await runVatsLocally(t, withSES, name); + t.deepEqual(logs, bp.expected[name]); + t.end(); + }); + } +} +testLocalPatterns(); + +export async function runVatsInComms(t, withSES, enablePipelining, name) { + console.log(`------ testing pattern (comms) -- ${name}`); + const bdir = path.resolve(__dirname, 'basedir-message-patterns'); + const config = await loadBasedir(bdir); + config.bootstrapIndexJS = path.join(bdir, 'bootstrap-comms.js'); + config.vatSources.set('leftcomms', getCommsSourcePath()); + config.vatOptions.set('leftcomms', { enablePipelining }); + config.vatSources.set('rightcomms', getCommsSourcePath()); + config.vatOptions.set('rightcomms', { enablePipelining }); + config.vatSources.set('leftvattp', getVatTPSourcePath()); + config.vatSources.set('rightvattp', getVatTPSourcePath()); + const ldSrcPath = require.resolve('../src/devices/loopbox-src'); + config.devices = [['loopbox', ldSrcPath, {}]]; + const c = await buildVatController(config, withSES, [name]); + // await runWithTrace(c); + await c.run(); + return c.dump().log; +} + +function testCommsPatterns() { + const withSES = false; + const enablePipelining = true; + const bp = buildPatterns(); + for (const name of Array.from(bp.patterns.keys()).sort()) { + const mode = bp.patterns.get(name).comms; + modes[mode](`test pattern ${name} locally`, async t => { + const logs = await runVatsInComms(t, withSES, enablePipelining, name); + let expected = bp.expected[name]; + if (enablePipelining && name in bp.expected_pipelined) { + expected = bp.expected_pipelined[name]; + } + t.deepEqual(logs, expected); + t.end(); + }); + } +} +testCommsPatterns(); From d39ed5bb500500a6c7bef2533ef054804719e4b4 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 7 Sep 2019 15:10:54 -0700 Subject: [PATCH 13/16] delete test-comms-integration, replaced by test-message-patterns --- test/basedir-commsvat/bootstrap.js | 78 ------- test/basedir-commsvat/vat-left.js | 230 ------------------- test/basedir-commsvat/vat-leftvattp.js | 3 - test/basedir-commsvat/vat-right.js | 113 ---------- test/basedir-commsvat/vat-rightvattp.js | 3 - test/test-comms-integration.js | 286 ------------------------ 6 files changed, 713 deletions(-) delete mode 100644 test/basedir-commsvat/bootstrap.js delete mode 100644 test/basedir-commsvat/vat-left.js delete mode 100644 test/basedir-commsvat/vat-leftvattp.js delete mode 100644 test/basedir-commsvat/vat-right.js delete mode 100644 test/basedir-commsvat/vat-rightvattp.js delete mode 100644 test/test-comms-integration.js diff --git a/test/basedir-commsvat/bootstrap.js b/test/basedir-commsvat/bootstrap.js deleted file mode 100644 index f3e7be8534a..00000000000 --- a/test/basedir-commsvat/bootstrap.js +++ /dev/null @@ -1,78 +0,0 @@ -import harden from '@agoric/harden'; - -console.log(`=> loading bootstrap.js`); - -export default function setup(syscall, state, helpers) { - function log(what) { - helpers.log(what); - } - log(`=> setup called`); - - return helpers.makeLiveSlots( - syscall, - state, - (E, D) => - harden({ - async bootstrap(argv, vats, devices) { - log('=> bootstrap() called'); - - // setup - const LEFT = 'left'; - const RIGHT = 'right'; - const RIGHT_OBJECT_INDEX = 12; - - D(devices.loopbox).registerInboundHandler(LEFT, vats.leftvattp); - const leftsender = D(devices.loopbox).makeSender(LEFT); - await E(vats.leftvattp).registerMailboxDevice(leftsender); - - const { - transmitter: txToRightForLeft, - setReceiver: setRxFromRightForLeft, - } = await E(vats.leftvattp).addRemote(RIGHT); - const rxFromRightForLeft = await E(vats.leftcomms).addRemote( - RIGHT, - txToRightForLeft, - ); - await E(setRxFromRightForLeft).setReceiver(rxFromRightForLeft); - - D(devices.loopbox).registerInboundHandler(RIGHT, vats.rightvattp); - const rightsender = D(devices.loopbox).makeSender(RIGHT); - await E(vats.rightvattp).registerMailboxDevice(rightsender); - - const { - transmitter: txToLeftForRight, - setReceiver: setRxFromLeftForRight, - } = await E(vats.rightvattp).addRemote(LEFT); - const rxFromLeftForRight = await E(vats.rightcomms).addRemote( - LEFT, - txToLeftForRight, - ); - await E(setRxFromLeftForRight).setReceiver(rxFromLeftForRight); - - await E(vats.rightcomms).addEgress( - LEFT, - RIGHT_OBJECT_INDEX, - vats.right, - ); - - // in addIngress, we know the common index that we want to - // use to communicate about something on the right machine, - // but the leftcomms needs to export it to the kernel - const rootRightPresence = await E(vats.leftcomms).addIngress( - RIGHT, - RIGHT_OBJECT_INDEX, - ); - - // run tests - const test = argv[0]; - const rootLeftPresence = vats.left; - - await E(vats.left).startTest(test, [ - rootRightPresence, - rootLeftPresence, - ]); - }, - }), - helpers.vatID, - ); -} diff --git a/test/basedir-commsvat/vat-left.js b/test/basedir-commsvat/vat-left.js deleted file mode 100644 index 60e44f240b3..00000000000 --- a/test/basedir-commsvat/vat-left.js +++ /dev/null @@ -1,230 +0,0 @@ -import harden from '@agoric/harden'; - -export default function setup(syscall, state, helpers) { - function log(what) { - helpers.log(what); - console.log(what); - } - - function createNewObj() { - return { - method() { - log(`=> left.1.method was invoked`); - return 'called method'; - }, - }; - } - - return helpers.makeLiveSlots( - syscall, - state, - E => { - async function startTest(test, args) { - switch (test) { - case 'left does: E(right.0).method() => returnData': { - const rightRootPresence = args[0]; - E(rightRootPresence) - .method() - .then(r => log(`=> left vat receives the returnedData: ${r}`)); - break; - } - - case 'left does: E(right.0).method(dataArg1) => returnData': { - const rightRootPresence = args[0]; - E(rightRootPresence) - .methodWithArgs('hello') - .then(r => log(`=> left vat receives the returnedData: ${r}`)); - break; - } - - case 'left does: E(right.0).method(right.0) => returnData': { - const rightRootPresence = args[0]; - E(rightRootPresence) - .methodWithPresence(rightRootPresence) - .then(r => log(`=> left vat receives the returnedData: ${r}`)); - break; - } - - case 'left does: E(right.0).method(left.1) => returnData': { - const rightRootPresence = args[0]; - const leftNewObjPresence = createNewObj(); - E(rightRootPresence) - .methodWithPresence(leftNewObjPresence) - .then(r => log(`=> left vat receives the returnedData: ${r}`)); - break; - } - - case 'left does: E(right.0).method(left.1) => returnData twice': { - const rightRootPresence = args[0]; - const leftNewObjPresence = createNewObj(); - - // first time - E(rightRootPresence) - .methodWithPresenceTwice(leftNewObjPresence) - .then(r => log(`=> left vat receives the returnedData: ${r}`)); - - // second time - E(rightRootPresence) - .methodWithPresenceTwice(leftNewObjPresence) - .then(r => log(`=> left vat receives the returnedData: ${r}`)); - - // check logs to ensure the same ids are used - break; - } - - case 'left does: E(right.1).method() => returnData': { - const rightRootPresence = args[0]; - const rightNewObjPresence = await E( - rightRootPresence, - ).createNewObj(); - E(rightNewObjPresence) - .method() - .then(r => log(`=> left vat receives the returnedData: ${r}`)); - break; - } - - case 'left does: E(right.0).method() => right.presence': { - const rightRootPresence = args[0]; - const presence = E(rightRootPresence).methodReturnsRightPresence(); - E(presence) - .method() - .then(r => log(`=> left vat receives the returnedData: ${r}`)); - break; - } - - case 'left does: E(right.0).method() => left.presence': { - const rightRootPresence = args[0]; - const newLeftObj = createNewObj(); - const presence = E(rightRootPresence).methodReturnsLeftPresence( - newLeftObj, - ); - E(presence) - .method() - .then(r => log(`=> left vat receives the returnedData: ${r}`)); - break; - } - - case 'left does: E(right.0).method() => right.promise => data': { - const rightRootPresence = args[0]; - const result = E(rightRootPresence).methodReturnsPromise(); - log(`=> left vat receives the returnedPromise: ${result}`); - E(rightRootPresence).resolveToFoo(); - result.then(r => log(`=> returnedPromise.then: ${r}`)); - break; - } - - case 'left does: E(right.0).method() => right.promise => right.presence': { - const rightRootPresence = args[0]; - const result = E( - rightRootPresence, - ).methodReturnsPromiseForRightPresence(); - log(`=> left vat receives the returnedPromise: ${result}`); - E(rightRootPresence).resolveToNewObj(); - result.then(async r => { - log(`=> returnedPromise.then: ${r}`); - // call method on presence to confirm expected presence - const methodCallResult = await E(r).method(); - log(`=> presence methodCallResult: ${methodCallResult}`); - }); - break; - } - - case 'left does: E(right.0).method() => right.promise => left.presence': { - const rightRootPresence = args[0]; - const leftPresence = createNewObj(); - const result = E( - rightRootPresence, - ).methodReturnsPromiseForLeftPresence(); - log(`=> left vat receives the returnedPromise: ${result}`); - E(rightRootPresence).resolveToLeftPresence(leftPresence); - result.then(async r => { - log(`=> returnedPromise.then: ${r}`); - // call method on presence to confirm expected presence - const methodCallResult = await E(r).method(); - log(`=> presence methodCallResult: ${methodCallResult}`); - }); - break; - } - - case 'left does: E(right.0).method() => right.promise => reject': { - const rightRootPresence = args[0]; - const p = E(rightRootPresence).methodReturnsPromiseReject(); - E(rightRootPresence).rejectThatPromise(); - try { - await p; - } catch (err) { - log( - `=> left vat receives the rejected promise with error ${err}`, - ); - } - break; - } - - case 'left does: E(right.0).method(left.promise) => returnData': { - const rightRootPresence = args[0]; - const lpromise = new Promise((resolve, _reject) => { - resolve('foo'); - }); - E(rightRootPresence) - .methodWithPromise(lpromise) - .then(r => log(`=> left vat receives the returnedData: ${r}`)); - break; - } - - case 'left does: E(right.0).method(right.promise) => returnData 1': { - const rightRootPresence = args[0]; - const rpromise = E(rightRootPresence).methodReturnsPromise(); - const p = E(rightRootPresence).methodWithPromise(rpromise); - E(rightRootPresence).resolveToFoo(); - p.then(r => log(`=> left vat receives the returnedData: ${r}`)); - break; - } - - case 'left does: E(right.0).method(right.promise) => returnData 2': { - // test resolving the promise before sending it - // TODO: I'm not convinced this is working yet. - const rightRootPresence = args[0]; - const rpromise = E(rightRootPresence).methodReturnsPromise(); - E(rightRootPresence).resolveToFoo(); - const p = E(rightRootPresence).methodWithPromise(rpromise); - p.then(r => log(`=> left vat receives the returnedData: ${r}`)); - break; - } - - case 'left does: E(right.0).method(right.promise => right.presence) => returnData': { - const rightRootPresence = args[0]; - const rPromisePresence = E( - rightRootPresence, - ).methodReturnsPromiseForRightPresence(); - E(rightRootPresence).resolveToNewObj(); - E(rightRootPresence) - .methodOnPromiseForPresence(rPromisePresence) - .then(r => log(`=> left vat receives the returnedData: ${r}`)); - break; - } - - case 'left does: E(right.0).method(right.promise => left.presence) => returnData': { - const rightRootPresence = args[0]; - const leftPresence = createNewObj(); - const lPromisePresence = E( - rightRootPresence, - ).methodReturnsPromiseForLeftPresence(); - E(rightRootPresence).resolveToLeftPresence(leftPresence); - E(rightRootPresence) - .methodOnPromiseForPresence(lPromisePresence) - .then(r => log(`=> left vat receives the returnedData: ${r}`)); - break; - } - - default: - throw new Error(`test ${test} not recognized`); - } - } - - return harden({ - startTest, - }); - }, - helpers.vatID, - ); -} diff --git a/test/basedir-commsvat/vat-leftvattp.js b/test/basedir-commsvat/vat-leftvattp.js deleted file mode 100644 index 11419b6c79b..00000000000 --- a/test/basedir-commsvat/vat-leftvattp.js +++ /dev/null @@ -1,3 +0,0 @@ -import setup from '../../src/vats/vat-tp/vattp'; - -export default setup; diff --git a/test/basedir-commsvat/vat-right.js b/test/basedir-commsvat/vat-right.js deleted file mode 100644 index aec659537a0..00000000000 --- a/test/basedir-commsvat/vat-right.js +++ /dev/null @@ -1,113 +0,0 @@ -import harden from '@agoric/harden'; -import makePromise from '../../src/kernel/makePromise'; - -export default function setup(syscall, state, helpers) { - function log(what) { - console.log(what); - helpers.log(what); - } - - let ingressRef; - let hasBeenCalled = false; - - function createNewObj() { - return { - method() { - log(`=> right.1.method was invoked`); - return 'called method'; - }, - }; - } - - let stashedPromise; - let stashedResolver; - let stashedRejector; - function createNewPromise() { - const p0 = makePromise(); - stashedPromise = p0.p; - stashedResolver = p0.res; - stashedRejector = p0.rej; - return stashedPromise; - } - - function resolve(what) { - stashedResolver(what); - } - - function reject(what) { - stashedRejector(what); - } - - return helpers.makeLiveSlots( - syscall, - state, - E => - harden({ - method() { - log(`=> right.method was invoked`); - return 'called method'; - }, - methodWithArgs(arg) { - log(`=> right.methodWithArgs got the arg: ${arg}`); - return `${arg} was received`; - }, - methodWithPresence(ref) { - log(`=> right.methodWithPresence got the ref ${ref}`); - // invoke method on ref object - return E(ref).method(); - }, - methodWithPresenceTwice(ref) { - if (hasBeenCalled) { - log(`ref equal each time: ${ref === ingressRef}`); - } - log(`=> right.methodWithPresence got the ref ${ref}`); - hasBeenCalled = true; - ingressRef = ref; - // invoke method on ref object - return E(ref).method(); - }, - methodReturnsRightPresence() { - return this.createNewObj(); - }, - methodReturnsLeftPresence(leftPresence) { - return leftPresence; - }, - methodReturnsPromise() { - log(`=> right.methodReturnsPromise was invoked`); - return createNewPromise(); - }, - resolveToFoo() { - resolve('foo'); - }, - methodReturnsPromiseForRightPresence() { - return createNewPromise(); - }, - resolveToNewObj() { - resolve(this.createNewObj()); - }, - methodReturnsPromiseForLeftPresence() { - return createNewPromise(); - }, - resolveToLeftPresence(leftPresence) { - resolve(leftPresence); - }, - methodReturnsPromiseReject() { - log(`=> right.methodReturnsPromiseReject was invoked`); - return createNewPromise(); - }, - rejectThatPromise() { - reject(new Error('this was rejected')); - }, - async methodWithPromise(promise) { - const promiseResult = await promise; - log(`promise resolves to ${promiseResult}`); - return promiseResult; - }, - methodOnPromiseForPresence(promiseForPresence) { - return E(promiseForPresence).method(); - }, - createNewObj, - }), - helpers.vatID, - ); -} diff --git a/test/basedir-commsvat/vat-rightvattp.js b/test/basedir-commsvat/vat-rightvattp.js deleted file mode 100644 index 11419b6c79b..00000000000 --- a/test/basedir-commsvat/vat-rightvattp.js +++ /dev/null @@ -1,3 +0,0 @@ -import setup from '../../src/vats/vat-tp/vattp'; - -export default setup; diff --git a/test/test-comms-integration.js b/test/test-comms-integration.js deleted file mode 100644 index 98bcdd27e39..00000000000 --- a/test/test-comms-integration.js +++ /dev/null @@ -1,286 +0,0 @@ -import path from 'path'; -import { test } from 'tape-promise/tape'; -import { - buildVatController, - getCommsSourcePath, - loadBasedir, -} from '../src/index'; - -export async function runVats(t, withSES, argv) { - const config = await loadBasedir( - path.resolve(__dirname, './basedir-commsvat'), - ); - config.vatSources.set('leftcomms', getCommsSourcePath()); - config.vatOptions.set('leftcomms', { enablePipelining: !true }); - config.vatSources.set('rightcomms', getCommsSourcePath()); - config.vatOptions.set('rightcomms', { enablePipelining: !true }); - const ldSrcPath = require.resolve('../src/devices/loopbox-src'); - config.devices = [['loopbox', ldSrcPath, {}]]; - const c = await buildVatController(config, withSES, argv); - return c; -} - -// use e.g. runTest(test.only, name) to run only one test - -const setupLogs = ['=> setup called', '=> bootstrap() called']; - -export function runTest(testType, withSES, testStr, expectedLogs) { - const expected = setupLogs.concat(expectedLogs); - testType(testStr, async t => { - const c = await runVats(t, withSES, [testStr]); - /* - while (c.dump().runQueue.length) { - console.log('-'); - console.log(`--- turn starts`); - await c.step(); - //console.log(c.dump().kernelTable); - } */ - await c.run(); - const { log } = c.dump(); - t.deepEqual(log, expected); - t.end(); - }); -} - -/* TABLE OF CONTENTS OF TESTS */ -// left does: E(right.0).method() => returnData -// left does: E(right.0).method(dataArg1) => returnData -// left does: E(right.0).method(right.0) => returnData -// left does: E(right.0).method(left.1) => returnData -// left does: E(right.0).method(left.1) => returnData twice -// left does: E(right.1).method() => returnData -// left does: E(right.0).method() => right.presence -// left does: E(right.0).method() => left.presence -// left does: E(right.0).method() => right.promise => data -// left does: E(right.0).method() => right.promise => right.presence -// left does: E(right.0).method() => right.promise => left.presence -// left does: E(right.0).method() => right.promise => reject -// left does: E(right.0).method(left.promise) => returnData -// left does: E(right.0).method(right.promise) => returnData -// left does: E(right.0).method(right.promise => right.presence) => returnData -// left does: E(right.0).method(right.promise => left.presence) => returnData - -/* TEST: left does: E(right.0).method() => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object and returns data. - */ -runTest(test, false, 'left does: E(right.0).method() => returnData', [ - '=> right.method was invoked', - '=> left vat receives the returnedData: called method', -]); - -/* TEST: left does: E(right.0).method(dataArg1) => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object with an argument and returns data connected to that argument. - */ -runTest(test, false, 'left does: E(right.0).method(dataArg1) => returnData', [ - '=> right.methodWithArgs got the arg: hello', - '=> left vat receives the returnedData: hello was received', -]); - -/* TEST: left does: E(right.0).method(right.0) => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object with the right vat's root object as an argument and does a - * method call on the argument. It returns the result of the method - * call on the argument, i.e. right.0.method() => 'called method' - */ -runTest(test, false, 'left does: E(right.0).method(right.0) => returnData', [ - '=> right.methodWithPresence got the ref [object Object]', - '=> right.method was invoked', - '=> left vat receives the returnedData: called method', -]); - -/* TEST: left does: E(right.0).method(left.1) => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's - * object with a new left object as an argument and returns data. - */ -runTest(test, false, 'left does: E(right.0).method(left.1) => returnData', [ - '=> right.methodWithPresence got the ref [Presence o-50]', - '=> left.1.method was invoked', - '=> left vat receives the returnedData: called method', -]); - -/* TEST: left does: E(right.0).method(left.1) => returnData twice - * DESCRIPTION: The left vat invokes a method on the right vat's - * object with a new left object as an argument and returns data. It - * repeats this a second time. No new egresses/ingresses should be - * allocated the second time. Also, both left.1 args should have the - * same identity. - */ -runTest( - test, - false, - 'left does: E(right.0).method(left.1) => returnData twice', - [ - '=> right.methodWithPresence got the ref [Presence o-50]', - 'ref equal each time: true', - '=> right.methodWithPresence got the ref [Presence o-50]', - '=> left.1.method was invoked', - '=> left.1.method was invoked', - '=> left vat receives the returnedData: called method', - '=> left vat receives the returnedData: called method', - ], -); - -/* TEST: left does: E(right.1).method() => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's - * object (a new object, not the root object) and returns data. - */ -runTest(test, false, 'left does: E(right.1).method() => returnData', [ - '=> right.1.method was invoked', - '=> left vat receives the returnedData: called method', -]); - -/* TEST: left does: E(right.0).method() => right.presence - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object and is given presence that represents a new object on the right - * side - */ -runTest(test, false, 'left does: E(right.0).method() => right.presence', [ - '=> right.1.method was invoked', - '=> left vat receives the returnedData: called method', -]); - -/* TEST: left does: E(right.0).method() => left.presence - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object and is given presence that represents a new object on the left - * side - */ -runTest(test, false, 'left does: E(right.0).method() => left.presence', [ - '=> left.1.method was invoked', - '=> left vat receives the returnedData: called method', -]); - -/* TEST: left does: E(right.0).method() => right.promise => data - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object and is given a promise that is later resolved to data. - */ -runTest( - test, - false, - 'left does: E(right.0).method() => right.promise => data', - [ - '=> left vat receives the returnedPromise: [object Promise]', - '=> right.methodReturnsPromise was invoked', - '=> returnedPromise.then: foo', - ], -); - -/* TEST: left does: E(right.0).method() => right.promise => right.presence - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object and is given a promise that resolves to a presence that - * represents an object on the right side. - */ -runTest( - test, - false, - 'left does: E(right.0).method() => right.promise => right.presence', - [ - '=> left vat receives the returnedPromise: [object Promise]', - '=> returnedPromise.then: [Presence o-51]', - '=> right.1.method was invoked', - '=> presence methodCallResult: called method', - ], -); - -/* TEST: left does: E(right.0).method() => right.promise => left.presence - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object and is given a promise that resolves to a presence that - * represents an object on the left side. - */ -runTest( - test, - false, - 'left does: E(right.0).method() => right.promise => left.presence', - [ - '=> left vat receives the returnedPromise: [object Promise]', - '=> returnedPromise.then: [object Object]', - '=> left.1.method was invoked', - '=> presence methodCallResult: called method', - ], -); - -/* TEST: left does: E(right.0).method() => right.promise => reject - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object and is given a promise that is rejected. - */ -runTest( - test, - false, - 'left does: E(right.0).method() => right.promise => reject', - [ - '=> right.methodReturnsPromiseReject was invoked', - '=> left vat receives the rejected promise with error Error: this was rejected', - ], -); - -/* TEST: left does: E(right.0).method(left.promise) => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object with a promise that the left machine knows about - */ -runTest( - test, - false, - 'left does: E(right.0).method(left.promise) => returnData', - ['promise resolves to foo', '=> left vat receives the returnedData: foo'], -); - -/* TEST: left does: E(right.0).method(right.promise) => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object with a promise that the right machine knows about. The promise - should be unresolved at the time the right machine sends it. - */ -runTest( - test, - false, - 'left does: E(right.0).method(right.promise) => returnData 1', - [ - '=> right.methodReturnsPromise was invoked', - 'promise resolves to foo', - '=> left vat receives the returnedData: foo', - ], -); - -/* TEST: left does: E(right.0).method(right.promise) => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object with a promise that the right machine knows about. The promise - should be resolved by the time the right machine sends it. - */ -runTest( - test, - false, - 'left does: E(right.0).method(right.promise) => returnData 2', - [ - '=> right.methodReturnsPromise was invoked', - 'promise resolves to foo', - '=> left vat receives the returnedData: foo', - ], -); - -/* TEST: left does: E(right.0).method(right.promise => right.presence) => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object with a promise that resolves to a right.presence - */ -runTest( - test, - false, - 'left does: E(right.0).method(right.promise => right.presence) => returnData', - [ - '=> right.1.method was invoked', - '=> left vat receives the returnedData: called method', - ], -); - -/* TEST: left does: E(right.0).method(right.promise => left.presence) => returnData - * DESCRIPTION: The left vat invokes a method on the right vat's root - * object with a promise that resolves to a left.presence - */ -runTest( - test, - false, - 'left does: E(right.0).method(right.promise => left.presence) => returnData', - [ - '=> left.1.method was invoked', - '=> left vat receives the returnedData: called method', - ], -); From 53b3fba6c547454f6e1b0426bcd7679abd12bd82 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 8 Sep 2019 18:10:50 -0700 Subject: [PATCH 14/16] move insist.js up a level, out of src/kernel/ into src/ --- src/{kernel => }/insist.js | 0 src/kernel/deviceManager.js | 2 +- src/kernel/deviceSlots.js | 2 +- src/kernel/kernel.js | 2 +- src/kernel/liveSlots.js | 2 +- src/kernel/parseKernelSlots.js | 2 +- src/kernel/state/deviceKeeper.js | 2 +- src/kernel/state/vatKeeper.js | 2 +- src/kernel/vatManager.js | 2 +- src/vats/comms/clist.js | 2 +- src/vats/comms/controller.js | 2 +- src/vats/comms/dispatch.js | 2 +- src/vats/comms/inbound.js | 2 +- src/vats/comms/outbound.js | 2 +- src/vats/comms/parseRemoteSlot.js | 2 +- src/vats/comms/remote.js | 2 +- src/vats/comms/state.js | 2 +- src/vats/parseVatSlots.js | 2 +- src/vats/vat-tp/vattp.js | 2 +- 19 files changed, 18 insertions(+), 18 deletions(-) rename src/{kernel => }/insist.js (100%) diff --git a/src/kernel/insist.js b/src/insist.js similarity index 100% rename from src/kernel/insist.js rename to src/insist.js diff --git a/src/kernel/deviceManager.js b/src/kernel/deviceManager.js index 473d7908eac..d93d38e49b3 100644 --- a/src/kernel/deviceManager.js +++ b/src/kernel/deviceManager.js @@ -1,5 +1,5 @@ import harden from '@agoric/harden'; -import { insist } from './insist'; +import { insist } from '../insist'; import { insistKernelType } from './parseKernelSlots'; import { insistVatType, parseVatSlot } from '../vats/parseVatSlots'; diff --git a/src/kernel/deviceSlots.js b/src/kernel/deviceSlots.js index 68f904ef0f8..9dc0d1328af 100644 --- a/src/kernel/deviceSlots.js +++ b/src/kernel/deviceSlots.js @@ -1,7 +1,7 @@ import harden from '@agoric/harden'; import Nat from '@agoric/nat'; import { QCLASS, mustPassByPresence, makeMarshal } from '@agoric/marshal'; -import { insist } from './insist'; +import { insist } from '../insist'; import { insistVatType, makeVatSlot, diff --git a/src/kernel/kernel.js b/src/kernel/kernel.js index 481bcfca0a3..e34db61f56e 100644 --- a/src/kernel/kernel.js +++ b/src/kernel/kernel.js @@ -9,7 +9,7 @@ import makeDeviceManager from './deviceManager'; import makeKernelKeeper from './state/kernelKeeper'; import { insistKernelType, parseKernelSlot } from './parseKernelSlots'; import { makeVatSlot } from '../vats/parseVatSlots'; -import { insist } from './insist'; +import { insist } from '../insist'; function abbreviateReviver(_, arg) { if (typeof arg === 'string' && arg.length >= 40) { diff --git a/src/kernel/liveSlots.js b/src/kernel/liveSlots.js index 2a9cc5e2c8b..8dfad7cd7e1 100644 --- a/src/kernel/liveSlots.js +++ b/src/kernel/liveSlots.js @@ -1,7 +1,7 @@ import harden from '@agoric/harden'; import Nat from '@agoric/nat'; import { QCLASS, mustPassByPresence, makeMarshal } from '@agoric/marshal'; -import { insist } from './insist'; +import { insist } from '../insist'; import { insistVatType, makeVatSlot, diff --git a/src/kernel/parseKernelSlots.js b/src/kernel/parseKernelSlots.js index 199e6018bae..af0f684135d 100644 --- a/src/kernel/parseKernelSlots.js +++ b/src/kernel/parseKernelSlots.js @@ -1,5 +1,5 @@ import Nat from '@agoric/nat'; -import { insist } from './insist'; +import { insist } from '../insist'; // Object/promise references (in the kernel) contain a two-tuple of (type, // index). All object references point to entries in the kernel Object diff --git a/src/kernel/state/deviceKeeper.js b/src/kernel/state/deviceKeeper.js index 206fe1d9774..afab7f70dfb 100644 --- a/src/kernel/state/deviceKeeper.js +++ b/src/kernel/state/deviceKeeper.js @@ -1,5 +1,5 @@ import harden from '@agoric/harden'; -import { insist } from '../insist'; +import { insist } from '../../insist'; import { parseKernelSlot } from '../parseKernelSlots'; import { makeVatSlot, parseVatSlot } from '../../vats/parseVatSlots'; diff --git a/src/kernel/state/vatKeeper.js b/src/kernel/state/vatKeeper.js index 82c2ce121e7..5d211d7e1ef 100644 --- a/src/kernel/state/vatKeeper.js +++ b/src/kernel/state/vatKeeper.js @@ -1,5 +1,5 @@ import harden from '@agoric/harden'; -import { insist } from '../insist'; +import { insist } from '../../insist'; import { parseKernelSlot } from '../parseKernelSlots'; import { makeVatSlot, parseVatSlot } from '../../vats/parseVatSlots'; diff --git a/src/kernel/vatManager.js b/src/kernel/vatManager.js index ac4cf53b0bb..babb89e051b 100644 --- a/src/kernel/vatManager.js +++ b/src/kernel/vatManager.js @@ -1,6 +1,6 @@ import harden from '@agoric/harden'; import djson from './djson'; -import { insist } from './insist'; +import { insist } from '../insist'; import { insistKernelType, parseKernelSlot } from './parseKernelSlots'; import { insistVatType, parseVatSlot } from '../vats/parseVatSlots'; diff --git a/src/vats/comms/clist.js b/src/vats/comms/clist.js index 9b8a0511921..54949fdb361 100644 --- a/src/vats/comms/clist.js +++ b/src/vats/comms/clist.js @@ -17,7 +17,7 @@ import { setPromiseDecider, setPromiseSubscriber, } from './state'; -import { insist } from '../../kernel/insist'; +import { insist } from '../../insist'; export function getOutbound(state, remoteID, target) { const remote = getRemote(state, remoteID); diff --git a/src/vats/comms/controller.js b/src/vats/comms/controller.js index 7272bb848bb..5b5a5e7e1bb 100644 --- a/src/vats/comms/controller.js +++ b/src/vats/comms/controller.js @@ -1,7 +1,7 @@ import Nat from '@agoric/nat'; import { addRemote } from './remote'; import { addEgress, addIngress } from './clist'; -import { insist } from '../../kernel/insist'; +import { insist } from '../../insist'; const UNDEFINED = JSON.stringify({ '@qclass': 'undefined' }); diff --git a/src/vats/comms/dispatch.js b/src/vats/comms/dispatch.js index fc021a661c8..0c44e3b1375 100644 --- a/src/vats/comms/dispatch.js +++ b/src/vats/comms/dispatch.js @@ -5,7 +5,7 @@ import { makeState } from './state'; import { deliverToRemote, resolvePromiseToRemote } from './outbound'; import { deliverFromRemote } from './inbound'; import { deliverToController } from './controller'; -import { insist } from '../../kernel/insist'; +import { insist } from '../../insist'; function transmit(syscall, state, remoteID, msg) { const remote = getRemote(state, remoteID); diff --git a/src/vats/comms/inbound.js b/src/vats/comms/inbound.js index 468f4a2ba94..9c3d2f6a1d0 100644 --- a/src/vats/comms/inbound.js +++ b/src/vats/comms/inbound.js @@ -6,7 +6,7 @@ import { insistPromiseDeciderIs, markPromiseAsResolved, } from './state'; -import { insist } from '../../kernel/insist'; +import { insist } from '../../insist'; export function deliverFromRemote(syscall, state, remoteID, message) { const command = message.split(':', 1)[0]; diff --git a/src/vats/comms/outbound.js b/src/vats/comms/outbound.js index df91b8a4b4b..66ad202ec89 100644 --- a/src/vats/comms/outbound.js +++ b/src/vats/comms/outbound.js @@ -8,7 +8,7 @@ import { markPromiseAsResolved, } from './state'; import { insistRemoteID } from './remote'; -import { insist } from '../../kernel/insist'; +import { insist } from '../../insist'; function getRemoteFor(state, target) { if (state.objectTable.has(target)) { diff --git a/src/vats/comms/parseRemoteSlot.js b/src/vats/comms/parseRemoteSlot.js index f576a666698..07e30222479 100644 --- a/src/vats/comms/parseRemoteSlot.js +++ b/src/vats/comms/parseRemoteSlot.js @@ -1,5 +1,5 @@ import Nat from '@agoric/nat'; -import { insist } from '../../kernel/insist'; +import { insist } from '../../insist'; // Object/promise references (in remote messages) contain a three-tuple of // (type, allocator flag, index). The allocator flag inside an inbound diff --git a/src/vats/comms/remote.js b/src/vats/comms/remote.js index 9432e0f3ded..f9e5b2c1915 100644 --- a/src/vats/comms/remote.js +++ b/src/vats/comms/remote.js @@ -1,6 +1,6 @@ import Nat from '@agoric/nat'; import { makeVatSlot, insistVatType } from '../parseVatSlots'; -import { insist } from '../../kernel/insist'; +import { insist } from '../../insist'; function makeRemoteID(index) { return `remote${Nat(index)}`; diff --git a/src/vats/comms/state.js b/src/vats/comms/state.js index 6483a132d4d..09da1e3abe0 100644 --- a/src/vats/comms/state.js +++ b/src/vats/comms/state.js @@ -1,4 +1,4 @@ -import { insist } from '../../kernel/insist'; +import { insist } from '../../insist'; import { makeVatSlot } from '../parseVatSlots'; export function makeState() { diff --git a/src/vats/parseVatSlots.js b/src/vats/parseVatSlots.js index 6024028273c..446eb70dc38 100644 --- a/src/vats/parseVatSlots.js +++ b/src/vats/parseVatSlots.js @@ -1,5 +1,5 @@ import Nat from '@agoric/nat'; -import { insist } from '../kernel/insist'; +import { insist } from '../insist'; // Object/promise references (in vats) contain a three-tuple of (type, // allocator flag, index). The 'ownership' flag is expressed as a sign: "-" diff --git a/src/vats/vat-tp/vattp.js b/src/vats/vat-tp/vattp.js index bb33837e9a2..433371f0c82 100644 --- a/src/vats/vat-tp/vattp.js +++ b/src/vats/vat-tp/vattp.js @@ -1,5 +1,5 @@ import harden from '@agoric/harden'; -import { insist } from '../../kernel/insist'; +import { insist } from '../../insist'; function build(E, D) { let mailbox; // mailbox device From fa5c054039ce9c11ffa780e4ae1dea5628b3586c Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 8 Sep 2019 18:14:03 -0700 Subject: [PATCH 15/16] move makePromise.js out of src/kernel/ up into src/ --- src/devices/command.js | 2 +- src/kernel/kernel.js | 2 +- src/{kernel => }/makePromise.js | 0 test/basedir-promises-2/bootstrap.js | 2 +- test/basedir-promises/bootstrap.js | 2 +- test/message-patterns.js | 2 +- test/test-marshal.js | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename src/{kernel => }/makePromise.js (100%) diff --git a/src/devices/command.js b/src/devices/command.js index 588f8a2af08..6fcbc228f31 100644 --- a/src/devices/command.js +++ b/src/devices/command.js @@ -1,5 +1,5 @@ import Nat from '@agoric/nat'; -import makePromise from '../kernel/makePromise'; +import makePromise from '../makePromise'; export default function buildCommand() { let inboundCallback; diff --git a/src/kernel/kernel.js b/src/kernel/kernel.js index e34db61f56e..1a35200b720 100644 --- a/src/kernel/kernel.js +++ b/src/kernel/kernel.js @@ -3,7 +3,7 @@ import { QCLASS, makeMarshal } from '@agoric/marshal'; import { makeLiveSlots } from './liveSlots'; import { makeDeviceSlots } from './deviceSlots'; -import makePromise from './makePromise'; +import makePromise from '../makePromise'; import makeVatManager from './vatManager'; import makeDeviceManager from './deviceManager'; import makeKernelKeeper from './state/kernelKeeper'; diff --git a/src/kernel/makePromise.js b/src/makePromise.js similarity index 100% rename from src/kernel/makePromise.js rename to src/makePromise.js diff --git a/test/basedir-promises-2/bootstrap.js b/test/basedir-promises-2/bootstrap.js index 0893b3bc685..c951d5241d5 100644 --- a/test/basedir-promises-2/bootstrap.js +++ b/test/basedir-promises-2/bootstrap.js @@ -1,5 +1,5 @@ import harden from '@agoric/harden'; -import makePromise from '../../src/kernel/makePromise'; +import makePromise from '../../src/makePromise'; console.log(`loading bootstrap`); diff --git a/test/basedir-promises/bootstrap.js b/test/basedir-promises/bootstrap.js index 5ad887e1cdf..da7d7e5bf86 100644 --- a/test/basedir-promises/bootstrap.js +++ b/test/basedir-promises/bootstrap.js @@ -1,5 +1,5 @@ import harden from '@agoric/harden'; -import makePromise from '../../src/kernel/makePromise'; +import makePromise from '../../src/makePromise'; console.log(`loading bootstrap`); diff --git a/test/message-patterns.js b/test/message-patterns.js index 019ed149b12..dd4b0f9108a 100644 --- a/test/message-patterns.js +++ b/test/message-patterns.js @@ -4,7 +4,7 @@ // test.stuff patterns. import harden from '@agoric/harden'; -import makePromise from '../src/kernel/makePromise'; +import makePromise from '../src/makePromise'; // Exercise a set of increasingly complex object-capability message patterns, // for testing. diff --git a/test/test-marshal.js b/test/test-marshal.js index 6ba9bbf0ced..baa4deee8db 100644 --- a/test/test-marshal.js +++ b/test/test-marshal.js @@ -5,7 +5,7 @@ import harden from '@agoric/harden'; import { makeMarshal, mustPassByPresence } from '@agoric/marshal'; import { makeMarshaller } from '../src/kernel/liveSlots'; -import makePromise from '../src/kernel/makePromise'; +import makePromise from '../src/makePromise'; import { buildVatController } from '../src/index'; From 2e24d8d49e79690d2cb9fae7d4e7bb6247a7000d Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 8 Sep 2019 18:19:09 -0700 Subject: [PATCH 16/16] move parseVatSlots.js out of src/vats/ up to src/ --- src/kernel/deviceManager.js | 2 +- src/kernel/deviceSlots.js | 6 +----- src/kernel/kernel.js | 2 +- src/kernel/liveSlots.js | 6 +----- src/kernel/state/deviceKeeper.js | 2 +- src/kernel/state/vatKeeper.js | 2 +- src/kernel/vatManager.js | 2 +- src/{vats => }/parseVatSlots.js | 2 +- src/vats/comms/clist.js | 2 +- src/vats/comms/dispatch.js | 2 +- src/vats/comms/outbound.js | 2 +- src/vats/comms/remote.js | 2 +- src/vats/comms/state.js | 2 +- test/test-kernel.js | 2 +- 14 files changed, 14 insertions(+), 22 deletions(-) rename src/{vats => }/parseVatSlots.js (98%) diff --git a/src/kernel/deviceManager.js b/src/kernel/deviceManager.js index d93d38e49b3..16bdff34adb 100644 --- a/src/kernel/deviceManager.js +++ b/src/kernel/deviceManager.js @@ -1,7 +1,7 @@ import harden from '@agoric/harden'; import { insist } from '../insist'; import { insistKernelType } from './parseKernelSlots'; -import { insistVatType, parseVatSlot } from '../vats/parseVatSlots'; +import { insistVatType, parseVatSlot } from '../parseVatSlots'; export default function makeDeviceManager( deviceName, diff --git a/src/kernel/deviceSlots.js b/src/kernel/deviceSlots.js index 9dc0d1328af..96ad49f40d9 100644 --- a/src/kernel/deviceSlots.js +++ b/src/kernel/deviceSlots.js @@ -2,11 +2,7 @@ import harden from '@agoric/harden'; import Nat from '@agoric/nat'; import { QCLASS, mustPassByPresence, makeMarshal } from '@agoric/marshal'; import { insist } from '../insist'; -import { - insistVatType, - makeVatSlot, - parseVatSlot, -} from '../vats/parseVatSlots'; +import { insistVatType, makeVatSlot, parseVatSlot } from '../parseVatSlots'; // 'makeDeviceSlots' is a subset of makeLiveSlots, for device code diff --git a/src/kernel/kernel.js b/src/kernel/kernel.js index 1a35200b720..7e0c23b5b7d 100644 --- a/src/kernel/kernel.js +++ b/src/kernel/kernel.js @@ -8,7 +8,7 @@ import makeVatManager from './vatManager'; import makeDeviceManager from './deviceManager'; import makeKernelKeeper from './state/kernelKeeper'; import { insistKernelType, parseKernelSlot } from './parseKernelSlots'; -import { makeVatSlot } from '../vats/parseVatSlots'; +import { makeVatSlot } from '../parseVatSlots'; import { insist } from '../insist'; function abbreviateReviver(_, arg) { diff --git a/src/kernel/liveSlots.js b/src/kernel/liveSlots.js index 8dfad7cd7e1..32a2cb39aa2 100644 --- a/src/kernel/liveSlots.js +++ b/src/kernel/liveSlots.js @@ -2,11 +2,7 @@ import harden from '@agoric/harden'; import Nat from '@agoric/nat'; import { QCLASS, mustPassByPresence, makeMarshal } from '@agoric/marshal'; import { insist } from '../insist'; -import { - insistVatType, - makeVatSlot, - parseVatSlot, -} from '../vats/parseVatSlots'; +import { insistVatType, makeVatSlot, parseVatSlot } from '../parseVatSlots'; // 'makeLiveSlots' is a dispatcher which uses javascript Maps to keep track // of local objects which have been exported. These cannot be persisted diff --git a/src/kernel/state/deviceKeeper.js b/src/kernel/state/deviceKeeper.js index afab7f70dfb..d23423e3b0d 100644 --- a/src/kernel/state/deviceKeeper.js +++ b/src/kernel/state/deviceKeeper.js @@ -1,7 +1,7 @@ import harden from '@agoric/harden'; import { insist } from '../../insist'; import { parseKernelSlot } from '../parseKernelSlots'; -import { makeVatSlot, parseVatSlot } from '../../vats/parseVatSlots'; +import { makeVatSlot, parseVatSlot } from '../../parseVatSlots'; // makeVatKeeper is a pure function: all state is kept in the argument object diff --git a/src/kernel/state/vatKeeper.js b/src/kernel/state/vatKeeper.js index 5d211d7e1ef..78b1c306087 100644 --- a/src/kernel/state/vatKeeper.js +++ b/src/kernel/state/vatKeeper.js @@ -1,7 +1,7 @@ import harden from '@agoric/harden'; import { insist } from '../../insist'; import { parseKernelSlot } from '../parseKernelSlots'; -import { makeVatSlot, parseVatSlot } from '../../vats/parseVatSlots'; +import { makeVatSlot, parseVatSlot } from '../../parseVatSlots'; // makeVatKeeper is a pure function: all state is kept in the argument object diff --git a/src/kernel/vatManager.js b/src/kernel/vatManager.js index babb89e051b..fa547907dd0 100644 --- a/src/kernel/vatManager.js +++ b/src/kernel/vatManager.js @@ -2,7 +2,7 @@ import harden from '@agoric/harden'; import djson from './djson'; import { insist } from '../insist'; import { insistKernelType, parseKernelSlot } from './parseKernelSlots'; -import { insistVatType, parseVatSlot } from '../vats/parseVatSlots'; +import { insistVatType, parseVatSlot } from '../parseVatSlots'; export default function makeVatManager( vatID, diff --git a/src/vats/parseVatSlots.js b/src/parseVatSlots.js similarity index 98% rename from src/vats/parseVatSlots.js rename to src/parseVatSlots.js index 446eb70dc38..314d2e474db 100644 --- a/src/vats/parseVatSlots.js +++ b/src/parseVatSlots.js @@ -1,5 +1,5 @@ import Nat from '@agoric/nat'; -import { insist } from '../insist'; +import { insist } from './insist'; // Object/promise references (in vats) contain a three-tuple of (type, // allocator flag, index). The 'ownership' flag is expressed as a sign: "-" diff --git a/src/vats/comms/clist.js b/src/vats/comms/clist.js index 54949fdb361..93bd0f5e7b5 100644 --- a/src/vats/comms/clist.js +++ b/src/vats/comms/clist.js @@ -1,5 +1,5 @@ import Nat from '@agoric/nat'; -import { makeVatSlot, parseVatSlot, insistVatType } from '../parseVatSlots'; +import { makeVatSlot, parseVatSlot, insistVatType } from '../../parseVatSlots'; import { flipRemoteSlot, insistRemoteType, diff --git a/src/vats/comms/dispatch.js b/src/vats/comms/dispatch.js index 0c44e3b1375..eacf614bfb9 100644 --- a/src/vats/comms/dispatch.js +++ b/src/vats/comms/dispatch.js @@ -1,5 +1,5 @@ import harden from '@agoric/harden'; -import { makeVatSlot } from '../parseVatSlots'; +import { makeVatSlot } from '../../parseVatSlots'; import { getRemote } from './remote'; import { makeState } from './state'; import { deliverToRemote, resolvePromiseToRemote } from './outbound'; diff --git a/src/vats/comms/outbound.js b/src/vats/comms/outbound.js index 66ad202ec89..2fed53ae76b 100644 --- a/src/vats/comms/outbound.js +++ b/src/vats/comms/outbound.js @@ -1,4 +1,4 @@ -import { insistVatType } from '../parseVatSlots'; +import { insistVatType } from '../../parseVatSlots'; import { insistRemoteType } from './parseRemoteSlot'; import { getOutbound, mapOutbound, mapOutboundResult } from './clist'; import { diff --git a/src/vats/comms/remote.js b/src/vats/comms/remote.js index f9e5b2c1915..560a4522c81 100644 --- a/src/vats/comms/remote.js +++ b/src/vats/comms/remote.js @@ -1,5 +1,5 @@ import Nat from '@agoric/nat'; -import { makeVatSlot, insistVatType } from '../parseVatSlots'; +import { makeVatSlot, insistVatType } from '../../parseVatSlots'; import { insist } from '../../insist'; function makeRemoteID(index) { diff --git a/src/vats/comms/state.js b/src/vats/comms/state.js index 09da1e3abe0..3cefcf41797 100644 --- a/src/vats/comms/state.js +++ b/src/vats/comms/state.js @@ -1,5 +1,5 @@ import { insist } from '../../insist'; -import { makeVatSlot } from '../parseVatSlots'; +import { makeVatSlot } from '../../parseVatSlots'; export function makeState() { const state = { diff --git a/test/test-kernel.js b/test/test-kernel.js index 22afae53c3f..06910d1e0f7 100644 --- a/test/test-kernel.js +++ b/test/test-kernel.js @@ -2,7 +2,7 @@ /* global setImmediate */ import { test } from 'tape-promise/tape'; import buildKernel from '../src/kernel/index'; -import { makeVatSlot } from '../src/vats/parseVatSlots'; +import { makeVatSlot } from '../src/parseVatSlots'; import { checkKT } from './util'; function checkPromises(t, kernel, expected) {