Skip to content

Commit

Permalink
Merge branch 'master' into markm-fix-async-flow-endowment-bug-2
Browse files Browse the repository at this point in the history
  • Loading branch information
mhofman committed Oct 1, 2024
2 parents 5d5fc81 + 442f07c commit bd80b3e
Show file tree
Hide file tree
Showing 197 changed files with 3,562 additions and 850 deletions.
8 changes: 6 additions & 2 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,12 @@ module.exports = {
tsconfigRootDir: __dirname,
extraFileExtensions: ['.cjs'],
},
plugins: ['@typescript-eslint', 'prettier'],
extends: ['@agoric', 'plugin:ava/recommended'],
plugins: ['@typescript-eslint', 'prettier', 'require-extensions'],
extends: [
'@agoric',
'plugin:ava/recommended',
'plugin:require-extensions/recommended',
],
// XXX false positive: Unused eslint-disable directive (no problems were reported from 'max-len')
reportUnusedDisableDirectives: true,

Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/mergify-ready.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,12 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Check for fixup commits
id: fixup-commits
run: |
if [[ $(git rev-list "$BASE_SHA".."$HEAD_SHA" --grep="^\(fixup\|amend\|squash\)! " | wc -l) -eq 0 ]]; then
if ! git merge-base --is-ancestor "$BASE_SHA" "$HEAD_SHA"; then
echo "PR is not up to date with target branch, skipping fixup commit check"
elif [[ $(git rev-list "$BASE_SHA".."$HEAD_SHA" --grep="^\(fixup\|amend\|squash\)! " | wc -l) -eq 0 ]]; then
echo "No fixup/amend/squash commits found in commit history"
else
echo "fixup/amend/squash commits found in commit history"
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
SECURITY.md @Agoric/security
* @Agoric/engprodexec
45 changes: 29 additions & 16 deletions docs/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,54 @@ Our use of TypeScript has to accommodate both .js development in agoric-sdk (whi

## Best practices

- `.d.ts` for types modules
### Exported types

- `.ts` for modules defining exported types
- package entrypoint(s) exports explicit types
- use `/** @import ` comments to import types without getting the runtime module

### Ambient types

- `.d.ts` for modules defining ambient types
- import types using [triple-slash reference](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html#-reference-types-)
- for packages upon which other packages expect ambient types:
- `exported.js` supplies ambients
- don't use runtime imports to get types ([issue](https://github.com/Agoric/agoric-sdk/issues/6512))

## .d.ts modules
## .ts modules

We cannot use `.ts` files in any modules that are transitively imported into an Endo bundle. The reason is that the Endo bundler doesn't understand `.ts` syntax and we don't want it to until we have sufficient auditability of the transformation. Moreover we've tried to avoid a build step in order to import a module. (The one exception so far is `@agoric/cosmic-proto` because we codegen the types. Those modules are written in `.ts` syntax and build to `.js` by a build step that creates `dist`, which is the package export.)

### Benefits

- A `.d.ts` module allows defining the type in `.ts` syntax, without any risk that it will be included in runtime code. The `.js` is what actually gets imported.
- Only `.d.ts` files can be used in [triple-slash reference types](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html#-reference-types-)
The trick is to use `.ts` for defining types and then make them available in the packages using a `types-index` module that has both `.js` and `.d.ts` files.

The are some consequences to this approach.

### File pair

You have to create a `.js` and `.d.ts` pair for each module. Usually it's of the form,
**Entrypoint (index.js)**
```js
// eslint-disable-next-line import/export
export * from './src/types-index.js'; // no named exports
```

**types-index.js**
```js
// Empty JS file to correspond with its .d.ts twin
export {};
```

### Lack of type checking
**types-index.d.ts**
```ts
// Export all the types this package provides
export * from './types.js';
export * from './other-types.js';
```

The actual type implementation is then written in `types.ts` and `other-types.ts` files (per the example above).
These files are never runtime imported as they are only linked through a `.d.ts` file.

We have `"skipLibCheck": true"` in the root tsconfig.json because some libraries we depend on have their own type errors. (A massive one is the output of Telescope, used in `@agoric/cosmic-proto`.)

This means that the types you write in `.d.ts` file won't be checked by `tsc`. To gain some confidence, you can temporarily flip that setting in a package's own `tsconfig.json` and pay attention to only the relevant errors.
## d.ts modules

### Alternatives
We take on the complexity above of indirection because `.d.ts` files aren't checked. We have `"skipLibCheck": true"` in the root tsconfig.json because some libraries we depend on have their own type errors. (A massive one is the output of Telescope, used in `@agoric/cosmic-proto`.)

We've experimented with having `.ts` files. It works, and gets around the skipLibCheck problem, but it complicates the build and exports. It also necessitates a build step even in package that don't currently need it.
This means that the types you write in `.d.ts` file won't be checked by `tsc`. To gain some confidence, you can temporarily flip that setting in a package's own `tsconfig.json` and pay attention to only the relevant errors.

## entrypoint

Expand Down
2 changes: 1 addition & 1 deletion golang/cosmos/index.cjs
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/* global module require */
/* eslint-env node */
module.exports = require('bindings')('agcosmosdaemon.node');
2 changes: 1 addition & 1 deletion multichain-testing/scripts/pod-readiness.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { execa } from 'execa';
import { sleep } from '../tools/sleep.ts';
import { sleep } from '../tools/sleep.js';

const checkPodsReadiness = async (): Promise<boolean> => {
const { stdout } = await execa('kubectl', ['get', 'pods']);
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsdoc": "^48.5.2",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-require-extensions": "^0.1.3",
"lerna": "^5.6.2",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.2",
Expand Down
92 changes: 92 additions & 0 deletions packages/SwingSet/docs/async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Asynchronous Considerations in SwingSet

When working with SwingSet, it's crucial to understand how asynchronous operations interact with the system's execution model. A **vat** (a unit of isolated execution in SwingSet) may restart or terminate before an asynchronous operation completes. Although JavaScript's `await` keyword allows you to write asynchronous code that resembles synchronous code, you must be mindful of events that could prevent a promise from settling:

- **Underlying condition never satisfied**: The condition required for the promise to settle may never occur.
- **Vat terminates, restarts, or upgrades before settlement**: The promise will be severed if its decider vat terminates, restarts, or upgrades before it settles.

# Promise Temporality

When programming with promises in SwingSet, it's important to understand the **temporality** of promise settlement—that is, the timing and execution dependencies required for a promise to settle. Promises can be classified into three categories based on their temporality: **Immediate**, **Prompt**, and **Delayed**.

## Immediate Promises

An **Immediate** promise is one that settles within the same Crank as its creation (see [Kernel Cycles](./state.md)). We refer to this as "Immediate" because it occurs before any further message deliveries into the vat, similar to how all promise reaction callbacks happen before any JavaScript event callbacks (such as those queued by `setImmediate`).

### Characteristics of Immediate Promises

- **Settlement Timing**: Settles within the same crank it was created.
- **Dependencies**: Requires no additional input to the "vat"; all information is locally available.
- **Safety**: Not affected by vat restarts or upgrades, which occur in their own crank.

## Prompt Promises

A **Prompt** promise settles without requiring further input from outside SwingSet. All Immediate promises are also Prompt, but not all Prompt promises are Immediate.

The SwingSet kernel processes its run queue items until there is nothing left to run. Most hosts, like cosmic-swingset, do not inject new items on the SwingSet run queue until it's empty, and instead maintain their own queue of input events. In those cases the SwingSet kernel executes similarly to how a vat executes a crank: an external I/O event triggers some execution, and the resulting SwingSet run queue items are drained before returning to the host for the next I/O.

While any of these queue items can technically cause a vat to upgrade, most systems running on SwingSet will trigger a vat upgrade based on some I/O input. As such, Prompt promises are unaffected by vat upgrades, unless the vat getting upgraded somehow got involved in processing the I/O that triggered its own upgrade.

### Characteristics of Prompt Promises

- **Settlement Timing**: Settles before any new I/O is processed.
- **Dependencies**: May involve multiple cranks but does not require external input.
- **Safety**: Safe from being severed by vat upgrades initiated by external I/O.

### Factors That Can Poison Promptness

The following are examples of I/O that can prevent a promise from being Prompt (they "poison" promptness):

- **Waiting on a timer**
- **Waiting on an inter-chain network call**
- **Waiting on a bid being placed**
- **Waiting on a new oracle price**

### Factors That Do Not Affect Promptness

- **Upgrade**: cosmic-swingset ensures quiescence between I/O and vat upgrades are triggered only when processing a new item from the chain's action queue (such as network input).
- **Bridge Calls**: Bridge calls are Prompt. While the caller may subsequently wait for an acknowledgment input (which is not Prompt), the bridge call itself does not affect promptness.

## Delayed Promises

A **Delayed** promise is one that requires additional input or external events to settle. It does not settle within the same crank and depends on factors not available during the current run of SwingSet. Delayed promises are neither Immediate nor Prompt because they involve waiting for external dependencies or future events beyond the current execution cycles.

### Characteristics of Delayed Promises

- **Settlement Timing**: Settles in future cranks after external events or inputs occur.
- **Dependencies**: Requires external input or events outside of SwingSet to settle.
- **Safety**: More susceptible to vat restarts, upgrades, or terminations affecting their settlement.

### Common Scenarios for Delayed Promises

- **Waiting on a Timer**: Introduces a delay until a specified time, relying on external scheduling.
- **Waiting on External Data or Events**: Depends on input from outside the SwingSet environment, such as a new oracle price.
- **User Actions**: Requires actions from external users, like placing a bid.
- **Network Communications**: Involves waiting for responses from external systems or networks, such as inter-chain network calls.

### Contrast with Immediate and Prompt Promises

- **Immediate Promises**:
- **Settlement**: Within the same crank.
- **Dependencies**: No external dependencies; all information is available within the "vat".
- **Example**: A calculation based on existing in-vat data.

- **Prompt Promises**:
- **Settlement**: Before any new I/O occurs, possibly over multiple cranks.
- **Dependencies**: No external input required, but may involve inter-vat asynchronous operations.
- **Example**: Internal message passing between vats that settles before any new external events.

- **Delayed Promises**:
- **Settlement**: After external events or inputs are received, over multiple cranks.
- **Dependencies**: Requires external input, events, or time to pass.
- **Example**: Waiting for a user to place a bid or for an oracle to provide new data.

## Summary

Understanding the temporality of promises in SwingSet helps you write robust asynchronous code that behaves predictably, even in the face of vat restarts, upgrades, or terminations. By being aware of the differences between Immediate, Prompt, and Delayed promises, you can design your smart contracts and applications to be more resilient and efficient.

- **Immediate Promises** are settled within the same crank and are safe from vat restarts or upgrades during that crank.
- **Prompt Promises** settle without external input before any new I/O, making them safe from being severed by vat upgrades initiated by external events.
- **Delayed Promises** depend on external input or events and settle over future cranks, making them more vulnerable to disruptions like vat restarts or terminations.

By classifying your promises appropriately, you can better manage asynchronous operations and ensure the reliability of your SwingSet applications.
Loading

0 comments on commit bd80b3e

Please sign in to comment.