Agoric / agoric-sdk

monorepo for the Agoric Javascript smart contract platform
Apache License 2.0
327 stars 207 forks source link

make vat-vattp upgradable #5667

Closed warner closed 2 years ago

warner commented 2 years ago

What is the Problem Being Solved?

vat-vattp needs to be upgradable, as part of #5666 .

vattp has two halves. I'm more familiar with the comms routing half than the networking half (and we may need @michaelfig 's help on that side).

The comms routing half is responsible for dispatching messages to/from named remotes. On the input side, the mailbox device sends all inbound messages to a single object. Each message includes both the string "remote ID" of the sender (e.g. an agoric1234 public-key -based address), the list of pending messages, and the highest ACK sequence number that the sender has seen (of our outgoing messages). It looks the remoteID up in a table to find the vat-comms receiver object, filters out duplicate messages (by seqnum), then sends the new ones in separate syscall.sends to the comms receiver.

On the output side, each remote has a distinct transmitter object within vat-vattp. Comms sends individual messages to the transmitter, vat-vattp looks up the remoteID for than transmitter, then vat-vattp invokes the mailbox device to push the message onto the named remote's outbox.

vat-vattp also has some wiring APIs: one to teach it about the mailbox device (and vice versa), and another named addRemote which causes it to create a new transmitter, and to be connected to the vat-comms receiver. There's a short-lived receiverSetter object used to finish this handoff.

I don't know what the networking half does, but it appears to share the remoteID-keyed table.

Description of the Design

I'm thinking that the remoteID-keyed table should be replaced by a durable Kind, with one instance per transmitter. The transmitter objects will have a seqnum or two in their state properties, as well as a remoteID.

The wiring API will need to become a singleton durable Kind. The receiverSetter can probably be ephemeral, if we feel safe in assuming that an addRemote won't span an upgrade.

Security Considerations

All mailbox-based messages are routed through vat-vattp, and it has full authority to route them to whatever transmitter/receiver it wishes, so it has considerable power to subvert (or entirely replace) arbitrary user communication. So the authority to upgrade it must be closely held. (this concern is not specific to vat-vattp, it also applies to comms, and to a lesser extent to vat-vat-admin).

Test Plan

At least careful audit, but ideally (if #5666 also provide a trigger mechanism) we should have unit tests of the actual upgrade process (using a null upgrade).

warner commented 2 years ago

@gibson042 I think I'm going to have you work on at least part of this task.. even if only the mapping from remote ID to transmitter/receiver objects were durable, that'd help us in the case of a future upgrade. We'll talk tomorrow about the details.

warner commented 2 years ago

@gibson042 so https://github.com/Agoric/agoric-sdk/blob/master/packages/SwingSet/src/vats/vattp/vat-vattp.js is the code that needs updating. Here's a copy of the vat-timer work I've started today, that might be useful as a template (although it almost certainly doesn't work yet):

// @ts-check

import { Nat } from '@agoric/nat';
import { assert, details as X } from '@agoric/assert';
import { Far, passStyleOf } from '@endo/marshal';
import { makeNotifierFromAsyncIterable } from '@agoric/notifier';
import { makePromiseKit } from '@endo/promise-kit';
import { makeTimedIterable } from './timed-iteration.js';
import { provideKindHandle, defineDurableKind } from '@agoric/vat-data';

export function buildRootObject(vatPowers, _vatParameters, baggage) {
  const { D } = vatPowers;
  const repeaters = new Map();
  const serviceHandle = provideKindHandle(baggage, 'timerService');

  /**
   * @typedef {bigint} Time
   * @typedef {unknown} Handler
   * @typedef {unknown} CancelToken
   * @typedef { { handler: Handler, cancel: CancelToken } } HandlerEntry
   * @typedef { { handlers: HandlerEntry[] } } WakeupEntry
   * @typedef {MapStore<Time, WakeupEntry>} WakeupTable
   *
   * @typedef {WeakMapStore<CancelToken, { times: Time[] }>} CancelTable
   *
   */

  // we rely upon the sortability of keys that are BigInts, and our
  // Stores performing efficient iteration
  if (!baggage.has('wakeups')) {
    baggage.init('wakeups', makeScalarBigMapStore('wakeups', { durable: true }));
  }
  /** @type {WakeupTable} */
  const wakeups = baggage.get('wakeups');

  // map cancel handles to the times that hold their events
  if (!baggage.has('cancels')) {
    baggage.init('cancels', makeScalarBigWeakMapStore('cancels', { durable: true }));
  }
  /** @type {CancelTable} */
  const cancels = baggage.get('removals');

  /**
   * return list of [ { time, handlers }] for time <= upto
   *
   * @param {Time} upto
   * @typedef { time: Time, handlers: Handler[] } ActionRecord
   * @returns { ActionRecord[] }
   */
  function removeEventsUpTo(upto) {
    const events = [];
    for (const [time, entry] of wakeups.entries()) {
      if (time <= upto) {
        events.push({ time, handlers: entry.handlers });
        events.delete(time);
      } else {
        break;
      }
    }
    return events;
  }

  /**
   * @param {Time} time
   * @param {Handler} handler
   * @param {CancelToken} cancel[]
   */
  function addEvent(time, handler, cancel = undefined) {
    const handlerEntry = harden({ handler, cancel });
    if (!wakeups.has(time)) {
      wakeups.init(time, harden({ handlers: [] }));
    }
    const entry = wakeups.get(time);
    const handlers = entry.handlers.concat([handlerEntry]);
    wakeups.set(time, harden({...entry, handlers}));

    if (cancel) {
      if (cancels.has(cancel)) {
        const cancelEntry = cancels.get(cancel);
        const { times: oldTimes } = cancelEntry;
        if (oldTimes.indexOf(time) !== -1) {
          const times = oldTimes.concat(time);
          const newEntry = { ...cancelEntry, times };
          cancels.set(cancel, harden(newEntry));
        }
      } else {
        const newEntry = { times: [ time ] };
        cancels.init(cancel, harden(newEntry));
      }
    }
  }

  /**
   * @param {CancelToken} cancel
   */
  function removeEvent(cancel) {
    assert(cancel !== undefined); // that would be super confusing
    const cancelEntry = cancels.get(cancel); // might throw
    cancels.delete(cancel);
    for (const time of cancels.times) {
      if (wakeups.has(time)) {
        /** @typedef { WakeupEntry } */
        const oldEntry = wakeups.get(time);
        const { handlers: oldHandlers } = oldEntry;
        /** @typedef { HandlerEntry[] } */
        const newHandlers = [];
        for (const handlerEntry of newHandlers) {
          if (handlerEntry.

  }