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

Durable contracts #1255

Merged
merged 6 commits into from
Dec 5, 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
4 changes: 4 additions & 0 deletions main/.vitepress/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ export default defineConfig({
text: 'Complete Contract Walk-Through',
link: '/guides/zoe/contract-walkthru',
},
{
text: 'Durable Contract Details',
link: '/guides/zoe/contract-details',
},
{
text: 'Contract Upgrade',
link: '/guides/zoe/contract-upgrade',
Expand Down
121 changes: 121 additions & 0 deletions main/guides/zoe/contract-details.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Durable Contract Details

Zoe allows you to write smart contracts that manage interactions between cooperative, but possibly untrusting,
parties. Some contracts perform a single simple task like trading one good for another, and then are
done. Others stay around and manage assets and interactions over time. The first kind don't need persistence
or the ability to upgrade, while the second kind need to make use of more sophisticated functionality.

When a contract is intended to continue running and serving many customers over a long time, its objects and
data need to be persistent, its owners or managers may need to be able to adjust parameters or perform other
governance actions, and they may want it to be upgradeable so that the code can adapt over time.

## Durable Objects

The first step toward contracts that can be upgraded is storing all the data that a later incarnation will
need. This means putting relevant state in [Baggage](/guides/zoe/contract-upgrade#baggage), and ensuring that
reachable objects that will be accessible to clients have an identity that can be maintained as the behavior
changes with contract upgrades.

We use zone.exo(), zone.exoClass(), and zone.exoClassKit() to define durable objects.

[Zone](/glossary/#zone) provides an interface for defining objects and classes that supports both ephemeral
objects (allocated on the heap), and durable objects that can persist and that
[SwingSet](/guides/platform/#swingset) will page in or out on demand.

Our persistent objects are designed to encapsulate their state, and can present different facets to different
clients to distinguish different aspects of authority. `zone.exoClass()` defines a kind with a single facet,
while `zone.exoClassKit()` defines a kind with multiple facets. (Here are some relevant [naming
conventions](/guides/ertp/#method-naming-structure).)

```
zone.exoClassKit(tag, guard, init, methodKit, options)
zone.exoClass(tag, guard, init, methods, options)
zone.exo(tag, guard, methods, options)
```

The next several sub-sections explain how the parameters to those functions are used.

### Tag: naming kinds of objects

The `tag` provides the identity of the kind of object, which is associated with the defined behavior. When a
contract (or other vat) is restarted or upgraded, SwingSet requires that all kinds that were previously
defined in a vat be defined again. With an upgrade, the behavior can be changed. We use the term "null
upgrade" to refer to upgrades that don't change the behavior.

### Init: specifying state

The `init` parameter defines a function that is used to define the state associated with each instance of an
object. Notice that `exo()` doesn't take `init` as a parameter; this limits the defined (singleton) object to
referencing values defined in the enclosing scope. Classes can also refer to variables in their defining
scope, but any values inherited from the scope will be accessible to all instances of that type. The `init`
function's parameters become the parameters of the maker function returned by `exoClass()` and
`exoClassKit()`. `init`'s return value is the state that will be preserved by SwingSet for each
object. `exo()` doesn't have an init parameter, so it creates and returns the singleton immediately rather
than returning a maker.

The Exo objects (or just "Exos") defined by these functions are both persistent and virtualizable. SwingSet
knows their complete state, so it can page them out when space is tight, and page them back in when they are
referenced again.

Fields of the object returned by the `init` function become fields of the persistent state. (_These cannot
currently be changed on upgrade, though we're considering relaxing that restriction._) Within methods they can
each be accessed as fields of `this.state`. Our convention is to extract the fields that will be used in a
method on its first line, like `const { a, b } = this.state;` Once that has been done, those variable can be
read or written as normal javascript variables, and the values will persist. (For more details, see [the note
here](/guides/zoe/contract-upgrade.html#kinds)).

### Methods: defining behavior

The methods argument of `exoClass()` is a record of methods, written in [concise method
syntax](https://github.com/Agoric/agoric-sdk/wiki/Arrow-Function-Style#far-classes-do-not-use-arrow-function-style).
`exoClass()` defines a single facet.

The methodKit parameter to `exoClassKit` is a record that can define multiple facets. The keys give the names
of the facets, and each value defines the methods of the corresponding facet. All facets of each object share
access to the entire state defined in `init`, but each facet is a separate capability. Within the methods,
other facets can be reached by name within `this.facets`. The maker returned by `exoClassKit()` builds a new
object each time it is called, and returns all the facets. The caller can decide which of the facets to hold
or to pass to separate recipients.

### Guards: defensive methods

These objects and facets are designed to be durable and shareable across address space boundaries. Since code
in different vats might not be mutually trusting, code needs to be defensive about parameters received from
remote callers. Interface Guards let us express in code what parameters each method expects and can handle
safely. If a caller provides a parameter that doesn't match the template specified in the guard, SwingSet
returns an exception to the caller without notifying the receiver. If the return value doesn't match, the
function returns an exception to the caller.

`exoClass()` takes a guard for a single interface, defined by

```
M.interface('name', {
methodA: M.call(paramsAShape).returns(resultAShape),
methodB: M.callWhen(M.await(paramsBShape)).returns(resultBShape),
}
```

`M.call()` verifies that all parameters match the guard before passing them through to the
method. `M.callWhen(M.await(paramsBGuard))` awaits the resolution of the parameter, and then verifies that the
result matches before invoking the method. When a guard is written this latter way, the method doesn't have to
be `async`. In both cases, the method doesn't have to check for compliance with the guard.

[Shapes can specify](https://endojs.github.io/endo/interfaces/_endo_patterns.PatternMatchers.html) simple
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a patterns cheat-sheet at https://docs.agoric.com/reference/zoe-api/zoe-contract-facet.html#proposal-shapes, but that context is specialized to proposal shapes. hm.

Perhaps using glossary entries for Pattern and Shape would help. Perhaps I should do that in a follow-up PR.

types like `M.string()`, `M.remotable()`, and `M.number()`, as well as complex structures of records and
arrays. The list of parameters can specify required and optional parameters, as well as allowing unspecified
rest parameters.

If you want to make use of the power of this type-checking within methods, you can call `mustMatch(specimen,
pattern)` or `matches(specimen, pattern)` directly. The former throws if the pattern doesn't match, while the
latter returns a boolean.

### Options: finish and stateShape

All the type definers can also take an [options
argument](https://endojs.github.io/endo/types/_endo_exo.FarClassOptions.html) at the end, which is commonly
used for a `finish()` function to complete initialization, or a stateShape, which can enforce invariants on
the state values.

`finish()`, if provided, is called after instances have been created but before they are returned to the
caller. They can be used to send an initial state update, or to complete initialization which couldn't be done
in `init`. `finish` has access to state and facets if needed.
54 changes: 28 additions & 26 deletions main/guides/zoe/contract-upgrade.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Contract Upgrade

The result of starting a contract includes the right to upgrade the contract instance. A call to [E(zoe).startInstance(...)](/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.
The return value when starting a contract includes a capability to upgrade the contract instance. A call to
[E(zoe).startInstance(...)](/reference/zoe-api/zoe.md#e-zoe-startinstance-installation-issuerkeywordrecord-terms-privateargs)
returns a [kit](/guides/ertp/#method-naming-structure) of [facets](/glossary/#facet); 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`](/reference/zoe-api/zoe.html#adminFacet) is defined by Zoe and includes methods to
upgrade the contract.

::: tip Upgrade Governance

Expand Down Expand Up @@ -60,16 +64,26 @@ There are a few requirements for the contract that differ from non-upgradable co

### Upgradable Declaration

The new code bundle declares that it supports upgrade by exporting a `prepare` function in place of `start`.
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._)

<<< @/../snippets/zoe/src/02b-state-durable.js#export-prepare
`meta` is a record with any or all of `upgradability`, `customTermsShape`, and `privateArgsShape`
defined. The latter two are optional
[Patterns](https://endojs.github.io/endo/modules/_endo_patterns.html) restricting respectively
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the patterns link should go by way of the glossary.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel up to writing that glossary entry. I'll leave that to your hypothetical future PR.

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).

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

### 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:
<a id="baggage"></a>

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](/glossary/#vat).

```js
let rooms;
Expand Down Expand Up @@ -113,7 +127,8 @@ When the contract instance is restarted, its [vat](../js-programming/#vats-the-u

### Kinds

Use `zone.exoClass()` to define state and methods of kinds of durable objects such as `Room`:
Use [`zone.exoClass()`](./contract-details.md#durable-objects) to define state and methods of kinds
of durable objects such as `Room`:

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

Expand Down Expand Up @@ -141,8 +156,9 @@ const makeRoom = zone.exoClass('Room', RoomI, id => ({ id, 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>
The interface guard also needs updating. <small>_[The Durable
objects](./contract-details.md#guards-defensive-methods) section has more on interface
guards._</small>

```js
const RoomI = M.interface('Room', {
Expand Down Expand Up @@ -175,20 +191,6 @@ Define all exo classes/kits before any incoming method calls from other vats --

:::

### Baggage

baggage is a MapStore that provides a way to preserve the state and behavior of objects between [smart contract upgrades](/guides/zoe/contract-upgrade) in a way that preserves the identity of objects as seen from other [vats](#vat). In the provided contract, baggage is used to ensure that the state of various components is maintained even after the contract is upgraded.

```js
export const start = async (zcf, privateArgs, baggage) => {
// ...
const { accountsStorageNode } = await provideAll(baggage, {
accountsStorageNode: () => E(storageNode).makeChildNode('accounts')
});
// ...
};
```

### Exo

An Exo object is an exposed Remotable object with methods (aka a [`Far`](/guides/js-programming/far) object) which is
Expand Down
36 changes: 35 additions & 1 deletion main/reference/zoe-api/zoe.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,24 +269,58 @@ It returns a **Promise** for a **StartInstanceResult** object. The object consis
- **instance**: **Instance**
- **creatorInvitation**: **Payment | undefined**

The **adminFacet** has one method:
<a id="adminfacet"></a>

The **adminFacet** has four methods:

- **getVatShutdownPromise()**

- Returns a promise that resolves to reason (the value passed to **fail(reason)**) or
completion (the value passed to **exit(completion)**) when this newly started instance terminates.

- **restartContract(newPrivateArgs?)**

- **newPrivateArgs**: **any** - Optional
- returns VatUpgradeResults (a record with one field: incarnationNumber)

Restarts the contract without changing the contract bundle

- **upgradeContract(contractBundleId, newPrivateArgs)**

- **contractBundleId**: **string**
- **newPrivateArgs**: **any** - Optional

- returns VatUpgradeResults (a record with one field: incarnationNumber)

Upgrades the contract to use source code from a new bundle.

See [Contract Upgrade](/guides/zoe/contract-upgrade) for a description the
process of upgrading contracts.

- **terminateContract(reason)**

- **reason**: **Error**

terminates the contract. `reason` will be provided as the failure reason.

<a id="publicfacet"></a>

A **publicFacet** is an object available via Zoe to anyone knowing
the instance they are associated with. The **publicFacet** is used for general queries
and actions, such as getting a current price or creating public **[Invitations](./zoe-data-types#invitation)**. Since a
facet is defined just as any other object,
the contract developer can add methods to them just like they would any object.

<a id="creatorfacet"></a>

The **creatorFacet** is only available in this return value (i.e. only when starting
a contract instance). The contract designer
should use it to encapsulate things that the contract runner might not want to share,
or might want to control the distribution of. The party who starts the contract
should carefully consider the impact before sharing access to the **creatorFacet**.

<a id="creatorinvitation"></a>

**creatorInvitation** is an **Invitation** that the contract instance creator can use.
It is usually used in contracts where the creator immediately sells
something (auctions, swaps, etc.), so it's helpful for the creator to have
Expand Down
8 changes: 5 additions & 3 deletions snippets/zoe/src/02b-state-durable.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ const RoomMakerI = M.interface('RoomMaker', {
});
// #endregion interface-guard

// #region export-prepare
export const prepare = (_zcf, _privateArgs, baggage) => {
// #endregion export-prepare
// #region export-start
export const meta = { upgradability: 'canUpgrade' };

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