Skip to content

Commit

Permalink
9449 resumable chainHub (#9544)
Browse files Browse the repository at this point in the history
refs: #9449 

## Description

Make ChainHub exo resumable, returning vows and never promises.

Reviewers, this should confirm to the criteria in 9449,
- no async functions
- no callWhen guards
- methods that don't return immediately return Vows
- methods that return vows don't throw

This also makes the orchestration facade able to receive vows but still return promises.

### Security Considerations
none

### Scaling Considerations
none

### Documentation Considerations
none

### Testing Considerations
Existing coverage

### Upgrade Considerations
Not yet deployed. Vows improve upgrade robustness
  • Loading branch information
mergify[bot] authored Jun 20, 2024
2 parents f9bd118 + 5e0643b commit 944adea
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 118 deletions.
47 changes: 43 additions & 4 deletions packages/orchestration/src/chain-info.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { registerChain } from './exos/chain-hub.js';

// Refresh with scripts/refresh-chain-info.ts
import fetchedChainInfo from './fetched-chain-info.js';
import { E } from '@endo/far';
import { mustMatch } from '@endo/patterns';
import { connectionKey } from './exos/chain-hub.js';
import fetchedChainInfo from './fetched-chain-info.js'; // Refresh with scripts/refresh-chain-info.ts
import { CosmosChainInfoShape } from './typeGuards.js';

/** @import {CosmosChainInfo, EthChainInfo} from './types.js'; */

Expand Down Expand Up @@ -64,6 +65,44 @@ const knownChains = /** @satisfies {Record<string, ChainInfo>} */ (

/** @typedef {typeof knownChains} KnownChains */

/**
* @param {ERef<import('@agoric/vats').NameHubKit['nameAdmin']>} agoricNamesAdmin
* @param {string} name
* @param {CosmosChainInfo} chainInfo
* @param {(...messages: string[]) => void} log
*/
export const registerChain = async (
agoricNamesAdmin,
name,
chainInfo,
log = () => {},
) => {
const { nameAdmin } = await E(agoricNamesAdmin).provideChild('chain');
const { nameAdmin: connAdmin } =
await E(agoricNamesAdmin).provideChild('chainConnection');

mustMatch(chainInfo, CosmosChainInfoShape);
const { connections = {}, ...vertex } = chainInfo;

const promises = [
E(nameAdmin)
.update(name, vertex)
.then(() => log(`registered agoricNames chain.${name}`)),
];

// FIXME updates redundantly, twice per edge
for (const [counterChainId, connInfo] of Object.entries(connections)) {
const key = connectionKey(chainInfo.chainId, counterChainId);
promises.push(
E(connAdmin)
.update(key, connInfo)
.then(() => log(`registering agoricNames chainConnection.${key}`)),
);
}
// Bundle to pipeline IO
await Promise.all(promises);
};

/**
* @param {ERef<import('@agoric/vats').NameHubKit['nameAdmin']>} agoricNamesAdmin
* @param {(...messages: string[]) => void} log
Expand Down
6 changes: 4 additions & 2 deletions packages/orchestration/src/examples/sendAnywhere.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { withdrawFromSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js'
import { InvitationShape } from '@agoric/zoe/src/typeGuards.js';
import { E } from '@endo/far';
import { M, mustMatch } from '@endo/patterns';

import { V } from '@agoric/vow/vat.js';
import { AmountShape } from '@agoric/ertp';
import { CosmosChainInfoShape } from '../typeGuards.js';
import { provideOrchestration } from '../utils/start-helper.js';
Expand Down Expand Up @@ -134,7 +134,9 @@ export const start = async (zcf, privateArgs, baggage) => {
*/
async addChain(chainInfo, connectionInfo) {
const chainKey = `${chainInfo.chainId}-${(nonce += 1n)}`;
const agoricChainInfo = await chainHub.getChainInfo('agoric');
// when() because chainHub methods return vows. If this were inside
// orchestrate() the membrane would wrap/unwrap automatically.
const agoricChainInfo = await V.when(chainHub.getChainInfo('agoric'));
chainHub.registerChain(chainKey, chainInfo);
chainHub.registerConnection(
agoricChainInfo.chainId,
Expand Down
148 changes: 60 additions & 88 deletions packages/orchestration/src/exos/chain-hub.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { E } from '@endo/far';
import { M, mustMatch } from '@endo/patterns';
import { VowShape } from '@agoric/vow';
import { allVows, watch } from '@agoric/vow/vat.js';
import { makeHeapZone } from '@agoric/zone';
import { E } from '@endo/far';
import { M } from '@endo/patterns';
import { CosmosChainInfoShape, IBCConnectionInfoShape } from '../typeGuards.js';

const { Fail } = assert;

/**
* @import {NameHub} from '@agoric/vats';
* @import {Vow} from '@agoric/vow';
* @import {CosmosChainInfo, IBCConnectionInfo} from '../cosmos-api.js';
* @import {ChainInfo, KnownChains} from '../chain-info.js';
* @import {Remote} from '@agoric/internal';
Expand Down Expand Up @@ -67,15 +70,14 @@ const ChainIdArgShape = M.or(

const ChainHubI = M.interface('ChainHub', {
registerChain: M.call(M.string(), CosmosChainInfoShape).returns(),
getChainInfo: M.callWhen(M.string()).returns(CosmosChainInfoShape),
registerConnection: M.callWhen(
getChainInfo: M.call(M.string()).returns(VowShape),
registerConnection: M.call(
M.string(),
M.string(),
IBCConnectionInfoShape,
).returns(),
getConnectionInfo: M.callWhen(ChainIdArgShape, ChainIdArgShape).returns(
IBCConnectionInfoShape,
),
getConnectionInfo: M.call(ChainIdArgShape, ChainIdArgShape).returns(VowShape),
getChainsAndConnection: M.call(M.string(), M.string()).returns(VowShape),
});

/**
Expand Down Expand Up @@ -120,22 +122,25 @@ export const makeChainHub = (agoricNames, zone = makeHeapZone()) => {
/**
* @template {string} K
* @param {K} chainName
* @returns {Promise<ActualChainInfo<K>>}
* @returns {Vow<ActualChainInfo<K>>}
*/
async getChainInfo(chainName) {
getChainInfo(chainName) {
// Either from registerChain or memoized remote lookup()
if (chainInfos.has(chainName)) {
// @ts-expect-error cast
return chainInfos.get(chainName);
return /** @type {Vow<ActualChainInfo<K>>} */ (
watch(chainInfos.get(chainName))
);
}

const chainInfo = await E(agoricNames)
.lookup(CHAIN_KEY, chainName)
.catch(_cause => {
return watch(E(agoricNames).lookup(CHAIN_KEY, chainName), {
onFulfilled: chainInfo => {
chainInfos.init(chainName, chainInfo);
return chainInfo;
},
onRejected: _cause => {
throw assert.error(`chain not found:${chainName}`);
});
chainInfos.init(chainName, chainInfo);
return chainInfo;
},
});
},
/**
* @param {string} chainId1
Expand All @@ -150,88 +155,55 @@ export const makeChainHub = (agoricNames, zone = makeHeapZone()) => {
/**
* @param {string | { chainId: string }} chain1
* @param {string | { chainId: string }} chain2
* @returns {Promise<IBCConnectionInfo>}
* @returns {Vow<IBCConnectionInfo>}
*/
async getConnectionInfo(chain1, chain2) {
getConnectionInfo(chain1, chain2) {
const chainId1 = typeof chain1 === 'string' ? chain1 : chain1.chainId;
const chainId2 = typeof chain2 === 'string' ? chain2 : chain2.chainId;
const key = connectionKey(chainId1, chainId2);
if (connectionInfos.has(key)) {
return connectionInfos.get(key);
return watch(connectionInfos.get(key));
}

const connectionInfo = await E(agoricNames)
.lookup(CONNECTIONS_KEY, key)
.catch(_cause => {
return watch(E(agoricNames).lookup(CONNECTIONS_KEY, key), {
onFulfilled: connectionInfo => {
connectionInfos.init(key, connectionInfo);
return connectionInfo;
},
onRejected: _cause => {
throw assert.error(`connection not found: ${chainId1}<->${chainId2}`);
});
connectionInfos.init(key, connectionInfo);
return connectionInfo;
},
});
},

/**
* @template {string} C1
* @template {string} C2
* @param {C1} chainName1
* @param {C2} chainName2
* @returns {Vow<
* [ActualChainInfo<C1>, ActualChainInfo<C2>, IBCConnectionInfo]
* >}
*/
getChainsAndConnection(chainName1, chainName2) {
return watch(
allVows([
chainHub.getChainInfo(chainName1),
chainHub.getChainInfo(chainName2),
]),
{
onFulfilled: ([chain1, chain2]) => {
return watch(chainHub.getConnectionInfo(chain2, chain1), {
onFulfilled: connectionInfo => {
return [chain1, chain2, connectionInfo];
},
});
},
},
);
},
});

return chainHub;
};
/** @typedef {ReturnType<typeof makeChainHub>} ChainHub */

/**
* @param {ERef<import('@agoric/vats').NameHubKit['nameAdmin']>} agoricNamesAdmin
* @param {string} name
* @param {CosmosChainInfo} chainInfo
* @param {(...messages: string[]) => void} log
*/
export const registerChain = async (
agoricNamesAdmin,
name,
chainInfo,
log = () => {},
) => {
const { nameAdmin } = await E(agoricNamesAdmin).provideChild('chain');
const { nameAdmin: connAdmin } =
await E(agoricNamesAdmin).provideChild('chainConnection');

mustMatch(chainInfo, CosmosChainInfoShape);
const { connections = {}, ...vertex } = chainInfo;

const promises = [
E(nameAdmin)
.update(name, vertex)
.then(() => log(`registered agoricNames chain.${name}`)),
];

// FIXME updates redundantly, twice per edge
for await (const [counterChainId, connInfo] of Object.entries(connections)) {
const key = connectionKey(chainInfo.chainId, counterChainId);
promises.push(
E(connAdmin)
.update(key, connInfo)
.then(() => log(`registering agoricNames chainConnection.${key}`)),
);
}
// Bundle to pipeline IO
await Promise.all(promises);
};

/**
* @template {string} C1
* @template {string} C2
* @param {ChainHub} chainHub
* @param {C1} chainName1
* @param {C2} chainName2
* @returns {Promise<
* [ActualChainInfo<C1>, ActualChainInfo<C2>, IBCConnectionInfo]
* >}
*/
export const getChainsAndConnection = async (
chainHub,
chainName1,
chainName2,
) => {
const [chain1, chain2] = await Promise.all([
chainHub.getChainInfo(chainName1),
chainHub.getChainInfo(chainName2),
]);
const connectionInfo = await chainHub.getConnectionInfo(chain2, chain1);

return [chain1, chain2, connectionInfo];
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { typedJson } from '@agoric/cosmic-proto/vatsafe';
import { AmountShape, PaymentShape } from '@agoric/ertp';
import { makeTracer } from '@agoric/internal';
import { M } from '@agoric/vat-data';
import { VowShape } from '@agoric/vow';
import { V } from '@agoric/vow/vat.js';
import { TopicsRecordShape } from '@agoric/zoe/src/contractSupport/index.js';
import { InvitationShape } from '@agoric/zoe/src/typeGuards.js';
Expand Down Expand Up @@ -89,7 +90,7 @@ export const prepareLocalOrchestrationAccountKit = (
getChainInfoWatcher: M.interface('getChainInfoWatcher', {
onFulfilled: M.call(M.record()) // agoric chain info
.optional({ destination: ChainAddressShape }) // empty context
.returns(M.promise()), // transfer channel
.returns(VowShape), // transfer channel
}),
getTimeoutTimestampWatcher: M.interface('getTimeoutTimestampWatcher', {
onFulfilled: M.call(M.bigint())
Expand Down
13 changes: 7 additions & 6 deletions packages/orchestration/src/exos/orchestrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import { makeTracer } from '@agoric/internal';
import { E } from '@endo/far';
import { M } from '@endo/patterns';
import {
ChainInfoShape,
LocalChainAccountShape,
DenomShape,
BrandInfoShape,
ChainInfoShape,
DenomAmountShape,
DenomShape,
LocalChainAccountShape,
} from '../typeGuards.js';
import { getChainsAndConnection } from './chain-hub.js';

/**
* @import {Zone} from '@agoric/base-zone';
Expand Down Expand Up @@ -78,7 +77,7 @@ export const prepareOrchestratorKit = (
makeRemoteChainFacadeWatcher: M.interface(
'makeRemoteChainFacadeWatcher',
{
onFulfilled: M.call(M.arrayOf(M.record()))
onFulfilled: M.call(M.any())
.optional(M.arrayOf(M.undefined()))
.returns(M.any()), // FIXME narrow
},
Expand Down Expand Up @@ -115,16 +114,18 @@ export const prepareOrchestratorKit = (
/** @type {Orchestrator['getChain']} */
getChain(name) {
if (name === 'agoric') {
// XXX when() until membrane
return when(
watch(
chainHub.getChainInfo('agoric'),
this.facets.makeLocalChainFacadeWatcher,
),
);
}
// XXX when() until membrane
return when(
watch(
getChainsAndConnection(chainHub, 'agoric', name),
chainHub.getChainsAndConnection('agoric', name),
this.facets.makeRemoteChainFacadeWatcher,
),
);
Expand Down
11 changes: 8 additions & 3 deletions packages/orchestration/src/facade.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,23 @@ export const makeOrchestrationFacade = ({

return {
/**
* @template Return
* @template Context
* @template {any[]} Args
* @param {string} durableName - the orchestration flow identity in the zone
* (to resume across upgrades)
* @param {Context} ctx - values to pass through the async flow membrane
* @param {(orc: Orchestrator, ctx2: Context, ...args: Args) => object} fn
* @returns {(...args: Args) => Promise<unknown>}
* @param {(
* orc: Orchestrator,
* ctx2: Context,
* ...args: Args
* ) => Promise<Return>} fn
* @returns {(...args: Args) => Promise<Return>}
*/
orchestrate(durableName, ctx, fn) {
const orc = makeOrchestrator();

return async (...args) => fn(orc, ctx, ...args);
return async (...args) => vowTools.when(fn(orc, ctx, ...args));
},
};
};
Expand Down
9 changes: 4 additions & 5 deletions packages/orchestration/src/proposals/start-stakeAtom.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { makeTracer } from '@agoric/internal';
import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js';
import { E } from '@endo/far';
import { getChainsAndConnection, makeChainHub } from '../exos/chain-hub.js';
import { V } from '@agoric/vow/vat.js';
import { makeChainHub } from '../exos/chain-hub.js';

/**
* @import {IBCConnectionID} from '@agoric/vats';
Expand Down Expand Up @@ -46,10 +47,8 @@ export const startStakeAtom = async ({

const chainHub = makeChainHub(await agoricNames);

const [_, cosmoshub, connectionInfo] = await getChainsAndConnection(
chainHub,
'agoric',
'cosmoshub',
const [_, cosmoshub, connectionInfo] = await V.when(
chainHub.getChainsAndConnection('agoric', 'cosmoshub'),
);

/** @type {StartUpgradableOpts<StakeIcaSF>} */
Expand Down
Loading

0 comments on commit 944adea

Please sign in to comment.