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

Move port allocation in network vat to PortAllocator #9228

Merged
merged 18 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from 16 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
8 changes: 4 additions & 4 deletions packages/boot/test/bootstrapTests/ibcClientMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import { V as E } from '@agoric/vat-data/vow.js';
/**
* @param {ZCF} zcf
* @param {{
* address: string,
* networkVat: ERef<ReturnType<typeof import('@agoric/vats/src/vat-network').buildRootObject>>;
* portAllocator: ERef<PortAllocator>;
* }} privateArgs
* @param {import("@agoric/vat-data").Baggage} _baggage
*/
export const start = async (zcf, privateArgs, _baggage) => {
const { address, networkVat } = privateArgs;
const myPort = await E(networkVat).bindPort(address);
const { portAllocator } = privateArgs;

const myPort = await E(portAllocator).allocateIBCPort();

const { log } = console;
let connP;
Expand Down
6 changes: 3 additions & 3 deletions packages/boot/test/bootstrapTests/ibcServerMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ const { log } = console;
* @param {ZCF} zcf
* @param {{
* address: string,
* networkVat: ERef<ReturnType<typeof import('@agoric/vats/src/vat-network').buildRootObject>>;
* portAllocator: ERef<PortAllocator>;
* }} privateArgs
* @param {import("@agoric/vat-data").Baggage} _baggage
*/
export const start = async (zcf, privateArgs, _baggage) => {
const { address, networkVat } = privateArgs;
const { portAllocator } = privateArgs;

const boundPort = await E(networkVat).bindPort(address);
const boundPort = await E(portAllocator).allocateIBCPort();

/** @type {Array<[label: string, resolve: (value: any) => void, reject: (reason: any) => void]>} */
const queue = [];
Expand Down
6 changes: 3 additions & 3 deletions packages/boot/test/bootstrapTests/test-net-ibc-upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ const upgradeVats = async (t, EV, vatsToUpgrade) => {
test.serial('upgrade at many points in network API flow', async t => {
const { installation } = t.context;
const { EV } = t.context.runUtils;
const networkVat = await EV.vat('bootstrap').consumeItem('networkVat');
const portAllocator = await EV.vat('bootstrap').consumeItem('portAllocator');
const zoe: ZoeService = await EV.vat('bootstrap').consumeItem('zoe');

const flow = entries({
Expand All @@ -122,7 +122,7 @@ test.serial('upgrade at many points in network API flow', async t => {
installation.ibcServerMock,
{},
{},
{ address: '/ibc-port/', networkVat },
{ portAllocator },
);
t.truthy(started.creatorFacet, `${label} ibcServerMock`);
return [label, { server: started.creatorFacet }];
Expand All @@ -140,7 +140,7 @@ test.serial('upgrade at many points in network API flow', async t => {
installation.ibcClientMock,
{},
{},
{ address: '/ibc-port/', networkVat },
{ portAllocator },
);
t.truthy(started.creatorFacet, `${label} ibcClientMock`);
return [label, { ...opts, client: started.creatorFacet }];
Expand Down
33 changes: 0 additions & 33 deletions packages/boot/test/bootstrapTests/test-vats-restart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,20 +105,6 @@ test.serial('run network vat proposal', async t => {
t.pass(); // reached here without throws
});

test.serial('register network protocol before upgrade', async t => {
const { EV } = t.context.runUtils;
const net = await EV.vat('bootstrap').consumeItem('networkVat');
const h1 = await EV(net).makeLoopbackProtocolHandler();

t.log('register P1');
await EV(net).registerProtocolHandler(['P1'], h1);

t.log('register P1 again? No.');
await t.throwsAsync(EV(net).registerProtocolHandler(['P1'], h1), {
message: /key "P1" already registered/,
});
});

test.serial('make IBC callbacks before upgrade', async t => {
const { EV } = t.context.runUtils;
const vatStore = await EV.vat('bootstrap').consumeItem('vatStore');
Expand Down Expand Up @@ -155,25 +141,6 @@ test.serial('use IBC callbacks after upgrade', async t => {
t.truthy(h.bridgeHandler, 'bridgeHandler');
});

test.serial('networkVat registrations are durable', async t => {
const { EV } = t.context.runUtils;
const net = await EV.vat('bootstrap').consumeItem('networkVat');

const h2 = await EV(net).makeLoopbackProtocolHandler();
t.log('register P1 again? No.');
await t.throwsAsync(EV(net).registerProtocolHandler(['P1'], h2), {
message: /key "P1" already registered/,
});

t.log('IBC protocol handler already registered?');
await t.throwsAsync(
EV(net).registerProtocolHandler(['/ibc-port', '/ibc-hop'], h2),
{
message: /key "\/ibc-port" already registered in collection "prefix"/,
},
);
});

test.serial('read metrics', async t => {
const { EV } = t.context.runUtils;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ export const makeMock = log =>

network: Far('network', {
registerProtocolHandler: noop,
bindPort: () => harden({ addListener: noop }),
}),
},
});
Expand Down
11 changes: 4 additions & 7 deletions packages/network/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,12 @@ E(home.ibcport[0]).connect(remoteEndpoint, connectionHandler)

The other side of `connect()` is a "listening port". These ports are waiting for inbound connections to be established.

To get a listening port, you need a `NetworkInterface` object (such as the one on your `ag-solo` under `home.network`) and ask it to `bindPort()` to an endpoint. You can either provide a specific port name, or allow the API to allocate a random one for you. The endpoint specifies the type of connection that this port will be able to accept (IBC, TCP, etc), and some properties of that connection. `bindPort()` uses a "multiaddress" to encode this information.
To get a listening port, you need a `NetworkInterface` object (such as the one on your `ag-solo` under `home.network`) and ask it for a port, via the `PortAllocator`.

```js
// ask for a random allocation - ends with a slash
E(home.network).bindPort('/ibc-port/')
.then(port => usePort(port));

// or ask for a specific port name
E(home.network).bindPort('/ibc-port/my-cool-port-name')
E(home.network).getPortAllocator()
.then(portAllocator => E(portAllocator).allocateIBCPort())
.then(port => usePort(port));
```

Expand Down Expand Up @@ -147,7 +144,7 @@ Note that if you want to listen on this port again, you can just call `port.addL

### Closing the Port Entirely

Removing a listener doesn't release the port address to make it available for other `bindPort()` requests. You can call:
Removing a listener doesn't release the port address to make it available for other `PortAllocator` requests. You can call:

```js
port.revoke();
Expand Down
72 changes: 72 additions & 0 deletions packages/network/src/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ export function getPrefixes(addr) {
return ret;
}

/**
* Validate IBC port name
* @param {string} specifiedName
*/
function throwIfInvalidPortName(specifiedName) {
// Contains between 2 and 128 characters
// Can contain alphanumeric characters
// Valid symbols: ., ,, _, +, -, #, [, ], <, >
const portNameRegex = new RegExp('^[a-zA-Z0-9.,_+\\-#<>\\[\\]]{2,128}$');
if (!portNameRegex.test(specifiedName)) {
throw new Error(`Invalid IBC port name: ${specifiedName}`);
}
}

/**
* @typedef {object} ConnectionOpts
* @property {Endpoint[]} addrs
Expand Down Expand Up @@ -1426,3 +1440,61 @@ export function prepareLoopbackProtocolHandler(zone, { watch, allVows }) {

return makeLoopbackProtocolHandler;
}

/**
*
* @param {import('@agoric/base-zone').Zone} zone
* @param {ReturnType<import('@agoric/vow').prepareVowTools>} powers
*/
export const preparePortAllocator = (zone, { watch }) =>
zone.exoClass(
'PortAllocator',
M.interface('PortAllocator', {
allocateIBCPort: M.callWhen()
iomekam marked this conversation as resolved.
Show resolved Hide resolved
.optional(M.string())
.returns(Shape.Vow$(Shape.Port)),
allocateICAControllerPort: M.callWhen().returns(Shape.Vow$(Shape.Port)),
allocateLocalPort: M.callWhen()
iomekam marked this conversation as resolved.
Show resolved Hide resolved
.optional(M.string())
.returns(Shape.Vow$(Shape.Port)),
}),
({ protocol }) => ({ protocol, lastICAPortNum: 0n }),
{
allocateIBCPort(specifiedName = '') {
const { state } = this;
let localAddr = `/ibc-port/`;

if (specifiedName) {
throwIfInvalidPortName(specifiedName);

localAddr = `/ibc-port/custom-${specifiedName}`;
}

// Allocate an IBC port with a unique generated name.
return watch(E(state.protocol).bindPort(localAddr));
},
allocateICAControllerPort() {
const { state } = this;
state.lastICAPortNum += 1n;
return watch(
E(state.protocol).bindPort(
`/ibc-port/icacontroller-${state.lastICAPortNum}`,
),
);
},
allocateLocalPort(specifiedName = '') {
const { state } = this;

let localAddr = `/local/`;

if (specifiedName) {
throwIfInvalidPortName(specifiedName);

localAddr = `/local/custom-${specifiedName}`;
}

// Allocate a local port with a unique generated name.
return watch(E(state.protocol).bindPort(localAddr));
},
},
);
2 changes: 2 additions & 0 deletions packages/network/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,5 @@
* ) => PromiseVow<Connection>} outbound
* Create an outbound connection
*/

/** @typedef {ReturnType<ReturnType<typeof import('@agoric/network').preparePortAllocator>>} PortAllocator */
48 changes: 47 additions & 1 deletion packages/network/test/test-network-misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
prepareRouter,
prepareLoopbackProtocolHandler,
prepareNetworkProtocol,
preparePortAllocator,
} from '../src/index.js';

import '../src/types.js';
Expand Down Expand Up @@ -53,7 +54,7 @@ const prepareProtocolHandler = (
},
async generatePortID() {
this.state.nonce += 1;
return `${this.state.nonce}`;
return `port-${this.state.nonce}`;
},
async onBind(port, localAddr) {
t.assert(port, `port is supplied to onBind`);
Expand Down Expand Up @@ -167,6 +168,51 @@ test('handled protocol', async t => {
await when(port.revoke());
});

test('verify port allocation', async t => {
const zone = makeDurableZone(
provideBaggage('network-verify-port-allocation'),
);
const powers = prepareVowTools(zone);
const { when } = powers;
const makeNetworkProtocol = prepareNetworkProtocol(zone, powers);
const makeEchoConnectionHandler = prepareEchoConnectionKit(zone);
const makeProtocolHandler = prepareProtocolHandler(
zone,
t,
makeEchoConnectionHandler,
powers,
);
const protocol = makeNetworkProtocol(makeProtocolHandler());
const makePortAllocator = preparePortAllocator(zone, powers);
const portAllocator = makePortAllocator({ protocol });

const ibcPort = await when(portAllocator.allocateIBCPort());
t.is(ibcPort.getLocalAddress(), '/ibc-port/port-1');

const namedIbcPort = await when(portAllocator.allocateIBCPort('test-1'));
t.is(namedIbcPort.getLocalAddress(), '/ibc-port/custom-test-1');

const icaControllerPort1 = await when(
portAllocator.allocateICAControllerPort(),
);
t.is(icaControllerPort1.getLocalAddress(), '/ibc-port/icacontroller-1');

const icaControllerPort2 = await when(
portAllocator.allocateICAControllerPort(),
);
t.is(icaControllerPort2.getLocalAddress(), '/ibc-port/icacontroller-2');

const localPort = await when(portAllocator.allocateLocalPort());
t.is(localPort.getLocalAddress(), '/local/port-5');

const namedLocalPort = await when(portAllocator.allocateLocalPort('local-1'));
t.is(namedLocalPort.getLocalAddress(), '/local/custom-local-1');

await t.throwsAsync(when(portAllocator.allocateIBCPort('/test-1')), {
message: 'Invalid IBC port name: /test-1',
});
});

test('protocol connection listen', async t => {
const zone = makeDurableZone(provideBaggage('network-protocol-connection'));
const powers = prepareVowTools(zone);
Expand Down
19 changes: 5 additions & 14 deletions packages/orchestration/src/proposals/orchestration-proposal.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// @ts-check
import { V as E } from '@agoric/vat-data/vow.js';
import { Far } from '@endo/far';

/**
* @import { AttenuatedNetwork } from '../types'
* @import { OrchestrationService } from '../service.js'
* @import { OrchestrationVat } from '../vat-orchestration.js'
*/
Expand All @@ -12,7 +10,7 @@ import { Far } from '@endo/far';
* @param {BootstrapPowers & {
* consume: {
* loadCriticalVat: VatLoader<any>;
* networkVat: NetworkVat;
* portAllocator: PortAllocator;
* };
* produce: {
* orchestration: Producer<any>;
Expand All @@ -29,7 +27,7 @@ import { Far } from '@endo/far';
*/
export const setupOrchestrationVat = async (
{
consume: { loadCriticalVat, networkVat },
consume: { loadCriticalVat, portAllocator: portAllocatorP },
produce: {
orchestrationVat,
orchestration,
Expand All @@ -49,17 +47,10 @@ export const setupOrchestrationVat = async (
orchestrationVat.reset();
orchestrationVat.resolve(vats.orchestration);

await networkVat;
/** @type {AttenuatedNetwork} */
const network = Far('Attenuated Network', {
/** @param {string} localAddr */
async bindPort(localAddr) {
return E(networkVat).bindPort(localAddr);
},
});
const portAllocator = await portAllocatorP;

const newOrchestrationKit = await E(vats.orchestration).makeOrchestration({
network,
portAllocator,
});

orchestration.reset();
Expand Down Expand Up @@ -88,7 +79,7 @@ export const getManifestForOrchestration = (_powers, { orchestrationRef }) => ({
[setupOrchestrationVat.name]: {
consume: {
loadCriticalVat: true,
networkVat: true,
portAllocator: 'portAllocator',
},
produce: {
orchestration: 'orchestration',
Expand Down
Loading
Loading