From e00c2593acb92ee97873d65d4ff063b8048f92e5 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 8 Feb 2024 03:28:58 -0600 Subject: [PATCH] docs: draft of durability --- main/guides/zoe/contract-upgrade.md | 158 +++++++++++++++++++++------- 1 file changed, 121 insertions(+), 37 deletions(-) diff --git a/main/guides/zoe/contract-upgrade.md b/main/guides/zoe/contract-upgrade.md index d7d155240..832241b8e 100644 --- a/main/guides/zoe/contract-upgrade.md +++ b/main/guides/zoe/contract-upgrade.md @@ -1,11 +1,11 @@ # Contract Upgrade -Starting a contract results several facets, one of which -includes the right to upgrade the contract. +The result of starting a contract includes the right to upgrade the contract. Governance of the right to upgrade is a complex topic that we cover only briefly here. -A call to [E(zoe).install(...)](/reference/zoe-api/zoe.md#e-zoe-startinstance-installation-issuerkeywordrecord-terms-privateargs) returns -this `adminFacet` as well the more commonly used `publicFacet` and `instance`. +A call to [E(zoe).install(...)](/reference/zoe-api/zoe.md#e-zoe-startinstance-installation-issuerkeywordrecord-terms-privateargs) 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. - When BLD staker governance starts a contract using `swingset.CoreEval`, to date, the `adminFacet` is stored in the bootstrap vat, allowing @@ -15,23 +15,29 @@ this `adminFacet` as well the more commonly used `publicFacet` and `instance`. could, in theory, change the VM itself.) - The `adminFacet` can be shared with a governance contract such as `committee.js`. -Upgrading a contract means re-starting the contract using a different code bundle. -Suppose we start a contract as usual: +Upgrading a contract instance means re-starting the contract using a different code bundle. Suppose we take our `ValueCell` contract ... + +::: details ValueCell contract + +<<< @/snippets/zoe/src/02-state.js#startfn +::: + +... and start it as usual: ```js -const v1BundleID = 'b1-deadbeef...'; -const installation = await E(zoe).installBundleID(v1BundleID); -const facets = await E(zoe).startInstance(installation, ...); +const bundleID = 'b1-1234abcd...'; +const installation = await E(zoe).installBundleID(bundleID); +const { instance, ... facets } = await E(zoe).startInstance(installation, ...); -// ... use facets.publicFacet, facets.instance etc. as usual +// ... use facets.publicFacet, instance etc. as usual ``` -Then suppose we fix a critical bug and make a new bundle. -To upgrade the contract +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 fix -const { incarnationNumber } = await E(zoe).upgradeContract(v2BundleId); +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. @@ -44,44 +50,122 @@ See also `E(adminFacet).restartContract()`. ::: -## Ensuring a Contract is Upgradable +## 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 +1. [Upgradable Declaration](#upgradable-declaration) +2. [Durability](#durability) +3. [Kinds](#kinds) +4. [Crank](#crank) ### Upgradable Declaration The new code bundle declares that it supports upgrade by exporting a `prepare` function in place of `start`. -For example, suppose v1 code of a simple single-increment-counter contract anticipated extension of exported functionality and decided to track it by means of "codeVersion" data in baggage. v2 code could add multi-increment behavior like so: +<<< @/snippets/zoe/src/02b-state-durable.js#export-prepare -<<< @/snippets/zoe/src/counterv2.js +### Durability -For an example contract upgrade, see [test-coveredCall-service-upgrade.js](https://github.com/Agoric/agoric-sdk/blob/master/packages/zoe/test/swingsetTests/upgradeCoveredCall/test-coveredCall-service-upgrade.js). +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: -### Durability +```js +let publicFacet; +if (!baggage.has('publicFacet')) { + // initial incarnation: create the object + publicFacet = makeCell(); + baggage.init('publicFacet', publicFacet); +} else { + // subsequent incarnation: use the object from the initial incarnation + publicFacet = baggage.get('publicFacet'); +} + +return { publicFacet }; +``` + +The `provide` function supports a concise idiom for this find-or-create pattern: -The contract must retain in durable storage anything that must persist between incarnations. All other state will be lost. +::: details import { provide } ... + +<<< @/snippets/zoe/src/02b-state-durable.js#import-provide-zone{2} + +::: + +<<< @/snippets/zoe/src/02b-state-durable.js#provide-cell + +::: details What happens if we don't use baggage? + +When the contract instance is restarted, it gets a fresh [heap](../js-programming/#vats-the-unit-of-synchrony), so [ordinary heap state](./contract-basics.html#state) does not survive upgrade. This implementation does not persist the effect of `E(publicFacet).set(2)` from the first incarnation: + +<<< @/snippets/zoe/src/02-state.js#heap-state{2} + +Also, it creates a fresh `publicFacet` object each time. So messages +from clients that try to use the `publicFacet` from the first incarnation +will fail to reach the `publicFacet` created in later incarnations. + +<<< @/snippets/zoe/src/02-state.js#fresh-export{2} + +::: ### Kinds -The contract defines the kinds that are held in durable storage. Thus the function calls that define the kinds must be run before the objects are deserialized from durable storage. +Use `zone.exoClass()` to define state and methods of kinds of durable objects such as `ValueCell`: -# Crank +::: details import { makeDurableZone } ... -For the first incarnation, `prepare` is allowed to return a promise that takes more than one crank to settle -(e.g., because it depends upon the results of remote calls). -But in later incarnations, `prepare` must settle in one crank. -Therefore such necessary values should be stashed in the baggage by earlier incarnations. -The `provideAll` function in contract support is designed to support this. +<<< @/snippets/zoe/src/02b-state-durable.js#import-provide-zone{1} -The reason is that all vats must be able to finish their upgrade without -contacting other vats. There might be messages queued inbound to the vat being -upgraded, and the kernel safely deliver those messages until the upgrade is -complete. The kernel can't tell which external messages are needed for upgrade, -vs which are new work that need to be delayed until upgrade is finished, so the -rule is that buildRootObject() must be standalone. +::: + +<<< @/snippets/zoe/src/02b-state-durable.js#exo + +Now we have all the parts of an upgradable contract. + +::: details full contract listing + +<<< @/snippets/zoe/src/02b-state-durable.js#contract + +::: + +We can then upgrade it to have another method: + +```js +const CellI = M.interface('ValueCell', { + ... + incr: M.call(M.number()).returns(), +}); + + const makeCell = zone.exoClass('ValueCell', CellI, () => ({ value: 0 }), { + ... + incr(delta) { + this.state.value += delta; + }, + }); +``` + +::: tip 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']`), but you cannot mutate it (`state.zot.push('last')`). +- 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](https://endojs.github.io/endo/functions/_endo_exo.defineExoClass.html) for further detail `zone.exoClass`. +- To define multiple objects that share state, use `zone.exoClassKit`. + - See also [defineExoClassKit](https://endojs.github.io/endo/functions/_endo_exo.defineExoClassKit.html) +- For an extended test / example, see [test-coveredCall-service-upgrade.js](https://github.com/Agoric/agoric-sdk/blob/master/packages/zoe/test/swingsetTests/upgradeCoveredCall/test-coveredCall-service-upgrade.js). + +::: + +## Crank + +Define all exo classes/kits before any incoming method calls from other vats -- in the first "crank". + +::: tip Note + +- For more on crank constraints, see [Virtual and Durable Objects](https://github.com/Agoric/agoric-sdk/blob/master/packages/SwingSet/docs/virtual-objects.md#virtual-and-durable-objects) in [SwingSet docs](https://github.com/Agoric/agoric-sdk/tree/master/packages/SwingSet/docs) + +:::