Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

contract upgrade #968

Merged
merged 2 commits into from
Feb 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion main/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,11 @@ module.exports = {
title: 'Zoe',
path: '/guides/zoe/',
collapsible: false,
children: ['/guides/zoe/contract-basics.html', '/guides/zoe/'],
children: [
'/guides/zoe/contract-basics.html',
'/guides/zoe/',
'/guides/zoe/contract-upgrade.html',
],
},
{
title: 'Agoric CLI',
Expand Down
6 changes: 3 additions & 3 deletions main/guides/zoe/contract-basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ A test that the `greet` method works as expected looks like:

## State

Contracts can use ordinary variables for state.
Contracts can use ordinary variables and data structures for state.

<<< @/snippets/zoe/src/02-state.js#startfn

Using `set` changes the results of the following call to `get`:
Using `makeRoom` changes the results of the following call to `getRoomCount`:

<<< @/snippets/zoe/contracts/test-zoe-hello.js#test-state

Expand All @@ -58,7 +58,7 @@ Ordinary heap state persists between contract invocations.

We'll discuss more explicit state management for
large numbers of objects (_virtual objects_) and
objects that last across upgrades (_durable objects_) later.
objects that last across upgrades ([durable objects](./contract-upgrade.md#durability)) later.

:::

Expand Down
176 changes: 176 additions & 0 deletions main/guides/zoe/contract-upgrade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Contract Upgrade

The result of starting a contract includes the right to upgrade the contract. 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.

::: tip Upgrade Governance

Governance of the right to upgrade is a complex topic that we cover only briefly here.

- When [BLD staker governance](https://community.agoric.com/t/about-the-governance-category/15) makes a decision to start a contract using [swingset.CoreEval](../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](https://github.com/Agoric/agoric-sdk/tree/master/packages/governance#readme) 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](./#bundling-a-contract). 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.

::: details 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](#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`.

<<< @/snippets/zoe/src/02b-state-durable.js#export-prepare

### 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 });
erights marked this conversation as resolved.
Show resolved Hide resolved
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:

::: details import { makeDurableZone } ...

<<< @/snippets/zoe/src/02b-state-durable.js#import-zone

:::

<<< @/snippets/zoe/src/02b-state-durable.js#zone1

::: details What happens if we don't use baggage?

When the contract instance is restarted, its [vat](../js-programming/#vats-the-unit-of-synchrony) gets a fresh heap, so [ordinary heap state](./contract-basics.md#state) does not survive upgrade. This implementation does not persist the rooms nor their counts between incarnations:

<<< @/snippets/zoe/src/02-state.js#heap-state{2}

:::

### Kinds

Use `zone.exoClass()` to define state and methods of kinds of durable objects such as `Room`:

<<< @/snippets/zoe/src/02b-state-durable.js#exoclass

Defining `publicFacet` as a singleton `exo` allows clients to
continue to use it after an upgrade:

<<< @/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 makeRoom = zone.exoClass('Room', RoomI, (id) => ({ id, value: 0 }), {
...
clear(delta) {
this.state.value = 0;
},
});
```

The interface guard also needs updating.
<small>_See [@endo/patterns](https://endojs.github.io/endo/modules/_endo_patterns.html) for more on interface guards._</small>

```js
const RoomI = M.interface('Room', {
...
clear: M.call().returns(),
});
```

::: 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']`), 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](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)
dckc marked this conversation as resolved.
Show resolved Hide resolved

:::
dckc marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@
},
"homepage": "https://github.com/Agoric/documentation#readme",
"dependencies": {
"@agoric/zoe": "community-dev",
"@agoric/ertp": "community-dev",
"@agoric/zoe": "community-dev",
"@agoric/zone": "0.2.3-u13.0",
"@endo/far": "^0.2.19",
"@endo/marshal": "^0.8.6",
"@endo/patterns": "^0.2.3",
"@endo/pass-style": "^0.1.6",
"@endo/patterns": "^0.2.3",
"typescript": "^4.0.3"
},
"devDependencies": {
Expand Down Expand Up @@ -140,6 +141,7 @@
],
"prettier": {
"trailingComma": "all",
"arrowParens": "avoid",
"singleQuote": true
}
}
7 changes: 4 additions & 3 deletions snippets/zoe/contracts/test-zoe-hello.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ test('contract greets by name', async t => {
// #region test-state
test('state', async t => {
const { publicFacet } = state.start();
t.is(await E(publicFacet).get(), 'Hello, World!');
await E(publicFacet).set(2);
t.is(await E(publicFacet).get(), 2);
const actual = await E(publicFacet).getRoomCount();
t.is(actual, 0);
await E(publicFacet).makeRoom(2);
t.is(await E(publicFacet).getRoomCount(), 1);
});
// #endregion test-state

Expand Down
20 changes: 16 additions & 4 deletions snippets/zoe/src/02-state.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { Far } from '@endo/far';

// #region startfn
// #region heap-state
export const start = () => {
let value = 'Hello, World!';
const get = () => value;
const set = v => (value = v);
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;
};
// #endregion heap-state

return {
publicFacet: Far('ValueCell', { get, set }),
publicFacet: Far('RoomMaker', { getRoomCount, makeRoom }),
};
};
// #endregion startfn
57 changes: 57 additions & 0 deletions snippets/zoe/src/02b-state-durable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// @ts-check
// #region contract
import { M } from '@endo/patterns';
// #region import-zone
import { makeDurableZone } from '@agoric/zone/durable.js';
// #endregion import-zone

// #region interface-guard
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()),
});
// #endregion interface-guard

// #region export-prepare
export const prepare = (_zcf, _privateArgs, baggage) => {
// #endregion export-prepare
// #region zone1
const zone = makeDurableZone(baggage);
const rooms = zone.mapStore('rooms');
// #endregion zone1

// #region exoclass
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;
},
});
// #endregion exoclass

// #region exo
const publicFacet = zone.exo('RoomMaker', RoomMakerI, {
makeRoom() {
const room = makeRoom();
const id = rooms.size;
rooms.init(id, room);
return room;
},
});

return { publicFacet };
// #endregion exo
};
// #endregion contract
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@
"@endo/patterns" "0.2.2"
"@endo/promise-kit" "0.2.56"

"@agoric/zone@^0.2.3-u13.0":
"@agoric/zone@0.2.3-u13.0", "@agoric/zone@^0.2.3-u13.0":
version "0.2.3-u13.0"
resolved "https://registry.yarnpkg.com/@agoric/zone/-/zone-0.2.3-u13.0.tgz#218e6372bfd44122ca0a0524649f1b3acbd40c52"
integrity sha512-NfH7fCrSI7wQ8wun8fhRBXEQdqkmjf4OdPXLwProYoxBIEJ4eML/CiGBDpE6DBeGDyS0YfJExcp7HV9nFsYi7g==
Expand Down
Loading