Contract Upgrade
The return value when starting a contract includes a capability to upgrade the contract instance. A call to E(zoe).startInstance(...) returns a kit of facets; that is a record of several objects that represent different ways to access the contract instance. 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 including a meta
record in addition to start
. (We used to indicate upgradability by using prepare
instead of start
, but that approach is deprecated.)
meta
is a record with any or all of upgradability
, customTermsShape
, and privateArgsShape
defined. The latter two are optional Patterns restricting respectively acceptable terms
, and privateArgs
. upgradability
can be none
(the contract is not upgradable), canUpgrade
(this code can perform an upgrade), or canBeUpgraded
(the contract stores kinds durably such that the next version can upgrade).
export const meta = { upgradability: 'canUpgrade' };
export const start = (_zcf, _privateArgs, baggage) => {
Durability
The 3rd argument, baggage
, of the start
function is a MapStore
that is saved by the kernel across restarts of the contract. It provides a way to preserve state and behavior of objects between incarnations in a way that also maintains the 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 meta = { upgradability: 'canUpgrade' };
export const start = (_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. The Durable objects section has 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
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.
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.
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.
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.
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
);