Skip to content

Contract Upgrade

The result of starting a contract includes the right to upgrade the contract instance. A call to E(zoe).startInstance(...) 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

Baggage

baggage is a MapStore that provides a way to preserve the state and behavior of objects between smart contract upgrades in a way that preserves the identity of objects as seen from other vats. In the provided contract, baggage is used to ensure that the state of various components is maintained even after the contract is upgraded.

js
export const start = async (zcf, privateArgs, baggage) => {
  // ...
  const { accountsStorageNode } = await provideAll(baggage, {
    accountsStorageNode: () => E(storageNode).makeChildNode('accounts')
  });
  // ...
};

Exo

An Exo object is an exposed Remotable object with methods (aka a Far object), which is normally defined with an InterfaceGuard as a protective outer layer, providing the first layer of defensiveness.

This @endo/exo package defines the APIs for making Exo objects, and for defining ExoClasses and ExoClassKits for making Exo objects.

js
const publicFacet = zone.exo(
  'StakeAtom',
  M.interface('StakeAtomI', {
    makeAccount: M.callWhen().returns(M.remotable('ChainAccount')),
    makeAccountInvitationMaker: M.callWhen().returns(InvitationShape)
  }),
  {
    async makeAccount() {
      trace('makeAccount');
      const holder = await makeAccountKit();
      return holder;
    },
    makeAccountInvitationMaker() {
      trace('makeCreateAccountInvitation');
      return zcf.makeInvitation(async seat => {
        seat.exit();
        const holder = await makeAccountKit();
        return holder.asContinuingOffer();
      }, 'wantStakingAccount');
    }
  }
);

Zones

Each Zone provides an API that allows the allocation of Exo objects and Stores (object collections) which use the same underlying persistence mechanism. This allows library code to be agnostic to whether its objects are backed purely by the JS heap (ephemeral), pageable out to disk (virtual), or can be revived after a vat upgrade (durable).

See SwingSet vat upgrade documentation for more example use of the zone API.

js
const zone = makeDurableZone(baggage);
// ...
zone.subZone('vows');

Durable Zone

A zone specifically designed for durability, allowing the contract to persist its state across upgrades. This is critical for maintaining the continuity and reliability of the contract’s operations.

js
const zone = makeDurableZone(baggage);

Vow Tools

See Vow; These tools handle promises and asynchronous operations within the contract. prepareVowTools prepares the necessary utilities to manage these asynchronous tasks, ensuring that the contract can handle complex workflows that involve waiting for events or responses from other chains.

js
const vowTools = prepareVowTools(zone.subZone('vows'));
// ...
const makeLocalOrchestrationAccountKit = prepareLocalChainAccountKit(
  zone,
  makeRecorderKit,
  zcf,
  privateArgs.timerService,
  vowTools,
  makeChainHub(privateArgs.agoricNames)
);
// ...
const makeCosmosOrchestrationAccount = prepareCosmosOrchestrationAccount(
  zone,
  makeRecorderKit,
  vowTools,
  zcf
);