Skip to content

Commit

Permalink
docs: draft of durability
Browse files Browse the repository at this point in the history
  • Loading branch information
dckc committed Feb 8, 2024
1 parent bbd8169 commit e00c259
Showing 1 changed file with 121 additions and 37 deletions.
158 changes: 121 additions & 37 deletions main/guides/zoe/contract-upgrade.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand All @@ -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)

:::

0 comments on commit e00c259

Please sign in to comment.