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 laterswingset.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 thecommittee.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:
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:
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:
Upgradable Declaration
The new code bundle declares that it supports upgrade by exporting a prepare
function in place of start
.
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:
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:
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 } ...
import { makeDurableZone } from '@agoric/zone/durable.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:
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
:
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:
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
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:
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.
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 likestate.zot.push('last')
, and if jot is part of state (state.jot = { x: 1 };
), then you can't dostate.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
.- See also defineExoClassKit
- 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
- For more on crank constraints, see Virtual and Durable Objects in SwingSet docs