Skip to content

Contract Upgrade

The result of starting a contract includes the right to upgrade the contract. A call to E(zoe).install(...) returns a record of several objects that represent different levels of access. The publicFacet and creatorFacet are defined by the contract. The adminFacet is defined by Zoe and includes methods to upgrade the contract.

Upgrade Governance

Governance of the right to upgrade is a complex topic that we cover only briefly here.

  • When BLD staker governance makes a decision to start a contract using swingset.CoreEval, to date, the adminFacet is stored in the bootstrap vat, allowing the BLD stakers to upgrade such a contract in a later swingset.CoreEval.
  • The adminFacet reference can be discarded, so that noone can upgrade the contract from within the JavaScript VM. (BLD staker governace could, in theory, change the VM itself.)
  • The adminFacet can be managed using the @agoric/governance framework; for example, using the committee.js contract.

Upgrading a Contract

Upgrading a contract instance means re-starting the contract using a different code bundle. Suppose we start a contract as usual, using the bundle ID of a bundle we already sent to the chain:

js
const bundleID = 'b1-1234abcd...';
const installation = await E(zoe).installBundleID(bundleID);
const { instance, ... facets } = await E(zoe).startInstance(installation, ...);

// ... use facets.publicFacet, instance etc. as usual

If we have the adminFacet and the bundle ID of a new version, we can use the upgradeContract method to upgrade the contract instance:

js
const v2BundleId = 'b1-feed1234...`; // hash of bundle with new feature
const { incarnationNumber } = await E(facets.adminFacet).upgradeContract(v2BundleId);

The incarnationNumber is 1 after the 1st upgrade, 2 after the 2nd, and so on.

re-using the same bundle

Note that a "null upgrade" that re-uses the original bundle is valid, and a legitimate approach to deleting accumulated heap state.

See also E(adminFacet).restartContract().

Upgradable Contracts

There are a few requirements for the contract that differ from non-upgradable contracts:

  1. Upgradable Declaration
  2. Durability
  3. Kinds
  4. Crank

Upgradable Declaration

The new code bundle declares that it supports upgrade by exporting a prepare function in place of start.

js
export const prepare = (_zcf, _privateArgs, baggage) => {

Durability

The 3rd argument, baggage, of the prepare function is a MapStore that provides a way to preserve state and behavior of objects between incarnations in a way that preserves identity of objects as seen from other vats:

js
let rooms;
if (!baggage.has('rooms')) {
  // initial incarnation: create the object
  rooms = makeScalarBigMapStore('rooms', { durable: true });
  baggage.init('rooms', rooms);
} else {
  // subsequent incarnation: use the object from the initial incarnation
  rooms = baggage.get('rooms');
}

The provide function supports a concise idiom for this find-or-create pattern:

js
import { provide } from '@agoric/vat-data';

const rooms = provide(baggage, 'rooms', () =>
  makeScalarBigMapStore('rooms', { durable: true }),
);

The zone API is a convenient way to manage durability. Its store methods integrate the provide pattern:

import { makeDurableZone } ...
js
import { makeDurableZone } from '@agoric/zone/durable.js';
js
const zone = makeDurableZone(baggage);
const rooms = zone.mapStore('rooms');
What happens if we don't use baggage?

When the contract instance is restarted, its vat gets a fresh heap, so ordinary heap state does not survive upgrade. This implementation does not persist the rooms nor their counts between incarnations:

js
export const start = () => {
  const rooms = new Map();

  const getRoomCount = () => rooms.size;
  const makeRoom = id => {
    let count = 0;
    const room = Far('Room', {
      getId: () => id,
      incr: () => (count += 1),
      decr: () => (count -= 1),
    });
    rooms.set(id, room);
    return room;
  };

Kinds

Use zone.exoClass() to define state and methods of kinds of durable objects such as Room:

js
const makeRoom = zone.exoClass('Room', RoomI, id => ({ id, count: 0 }), {
  getId() {
    return this.state.id;
  },
  incr() {
    this.state.count += 1;
    return this.state.count;
  },
  decr() {
    this.state.count -= 1;
    return this.state.count;
  },
});

Defining publicFacet as a singleton exo allows clients to continue to use it after an upgrade:

js
const publicFacet = zone.exo('RoomMaker', RoomMakerI, {
  makeRoom() {
    const room = makeRoom();
    const id = rooms.size;
    rooms.init(id, room);
    return room;
  },
});

return { publicFacet };

Now we have all the parts of an upgradable contract.

full contract listing
js
import { M } from '@endo/patterns';
import { makeDurableZone } from '@agoric/zone/durable.js';

const RoomI = M.interface('Room', {
  getId: M.call().returns(M.number()),
  incr: M.call().returns(M.number()),
  decr: M.call().returns(M.number()),
});

const RoomMakerI = M.interface('RoomMaker', {
  makeRoom: M.call().returns(M.remotable()),
});

export const prepare = (_zcf, _privateArgs, baggage) => {
  const zone = makeDurableZone(baggage);
  const rooms = zone.mapStore('rooms');

  const makeRoom = zone.exoClass('Room', RoomI, id => ({ id, count: 0 }), {
    getId() {
      return this.state.id;
    },
    incr() {
      this.state.count += 1;
      return this.state.count;
    },
    decr() {
      this.state.count -= 1;
      return this.state.count;
    },
  });

  const publicFacet = zone.exo('RoomMaker', RoomMakerI, {
    makeRoom() {
      const room = makeRoom();
      const id = rooms.size;
      rooms.init(id, room);
      return room;
    },
  });

  return { publicFacet };
};

We can then upgrade it to have another method:

js
  const makeRoom = zone.exoClass('Room', RoomI, (id) => ({ id, value: 0 }), {
    ...
    clear(delta) {
      this.state.value = 0;
    },
  });

The interface guard also needs updating. See @endo/patterns for more on interface guards.

js
const RoomI = M.interface('Room', {
  ...
  clear: M.call().returns(),
});

Notes

  • Once the state is defined by the init function (3rd arg), properties cannot be added or removed.
  • Values of state properties must be serializable.
  • Values of state properties are hardened on assignment.
  • You can replace the value of a state property (e.g. state.zot = [...state.zot, 'last']), and you can update stores (state.players.set(1, player1)), but you cannot do things like state.zot.push('last'), and if jot is part of state (state.jot = { x: 1 };), then you can't do state.jot.x = 2;
  • The tag (1st arg) is used to form a key in baggage, so take care to avoid collisions. zone.subZone() may be used to partition namespaces.
  • See also defineExoClass for further detail zone.exoClass.
  • To define multiple objects that share state, use zone.exoClassKit.
  • For an extended test / example, see test-coveredCall-service-upgrade.js.

Crank

Define all exo classes/kits before any incoming method calls from other vats -- in the first "crank".

Note