Skip to content

Commit

Permalink
feat(daemon): Add locate() facility (merge #2160)
Browse files Browse the repository at this point in the history
Adds locator strings and a `locate()` method to the daemon's directory abstraction, and thereby hosts and guests. A locator string is a URL of the following form:
```text
endo://{nodeIdentifier}/?id={formulaNumber}&type={formulaType}
```

Formula identifiers are of the form `{formulaNumber}:{nodeIdentifier}`. Therefore, we can say that a locator is the URL form of the formula identifier, annotated with the `formulaType`. If a value is remote, i.e. belonging to a different peer, we set the `type` query param to `remote` to indicate this. `remote` is not a valid formula type in the general case—i.e. there are no formula objects with that type—but perhaps it should be.

Note that the `id` query param is just the `formulaNumber`, not the formula identifier. We may address this internal discrepancy in naming by using locators and formula numbers instead of the `{formulaNumber}:{nodeIdentifier}` construction.
  • Loading branch information
rekmarks authored Mar 21, 2024
2 parents 9a5027f + 11198d0 commit 796e4bb
Show file tree
Hide file tree
Showing 12 changed files with 568 additions and 211 deletions.
159 changes: 82 additions & 77 deletions packages/daemon/src/daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { parseId, formatId } from './formula-identifier.js';
import { makeSerialJobs } from './serial-jobs.js';
import { makeWeakMultimap } from './weak-multimap.js';
import { makeLoopbackNetwork } from './networks/loopback.js';
import { assertValidFormulaType } from './formula-type.js';

const delay = async (ms, cancelled) => {
// Do not attempt to set up a timer if already cancelled.
Expand Down Expand Up @@ -121,7 +122,7 @@ const makeDaemonCore = async (
rootEntropy,
cryptoPowers.makeSha512(),
);
const peersId = formatId({
const ownPeersId = formatId({
number: peersFormulaNumber,
node: ownNodeIdentifier,
});
Expand Down Expand Up @@ -182,19 +183,43 @@ const makeDaemonCore = async (

/**
* The two functions "formulate" and "provide" share a responsibility for
* maintaining the memoization tables "controllerForId" and
* maintaining the memoization tables "controllerForId", "typeForId", and
* "idForRef".
* "formulate" is used for creating and persisting new formulas, whereas
* "provide" is used for "reincarnating" the values of stored formulas.
*/

/**
* Reverse look-up, for answering "what is my name for this near or far
* reference", and not for "what is my name for this promise".
* Forward look-up, for answering "what is the value of this id".
* @type {Map<string, import('./types.js').Controller>}
*/
const controllerForId = new Map();

/**
* Forward look-up, for answering "what is the formula type of this id".
* @type {Map<string, string>}
*/
const typeForId = new Map();

/** @param {string} id */
const getTypeForId = async id => {
await null;

const formulaType = typeForId.get(id);
if (formulaType !== undefined) {
return formulaType;
}

if (parseId(id).node !== ownNodeIdentifier) {
typeForId.set(id, 'remote');
return 'remote';
}

const formula = await persistencePowers.readFormula(parseId(id).number);
typeForId.set(id, formula.type);
return formula.type;
};

/**
* Reverse look-up, for answering "what is my name for this near or far
* reference", and not for "what is my name for this promise".
Expand Down Expand Up @@ -569,23 +594,19 @@ const makeDaemonCore = async (
);
},
addPeerInfo: async peerInfo => {
const peerPetstore =
/** @type {import('./types.js').PetStore} */
// Behold, recursion:
// eslint-disable-next-line no-use-before-define
(await provide(formula.peers));
const { node, addresses } = peerInfo;
// eslint-disable-next-line no-use-before-define
const nodeName = petStoreNameForNodeIdentifier(node);
if (peerPetstore.has(nodeName)) {
const knownPeers = await provideKnownPeers(formula.peers);
const { node: nodeIdentifier, addresses } = peerInfo;
// eslint-disable-next-line no-use-before-define
if (knownPeers.has(nodeIdentifier)) {
// We already have this peer.
// TODO: merge connection info
return;
}
const { id: peerId } =
// eslint-disable-next-line no-use-before-define
await formulatePeer(formula.networks, addresses);
await peerPetstore.write(nodeName, peerId);
await knownPeers.write(nodeIdentifier, peerId);
},
});
return {
Expand Down Expand Up @@ -670,35 +691,18 @@ const makeDaemonCore = async (
if (isRemote) {
// eslint-disable-next-line no-use-before-define
const peerIdentifier = await getPeerIdForNodeIdentifier(formulaNode);
typeForId.set(id, 'remote');
// Behold, forward reference:
// eslint-disable-next-line no-use-before-define
return provideRemoteValue(peerIdentifier, id);
}

const formula = await persistencePowers.readFormula(formulaNumber);
console.log(`Making ${formula.type} ${formulaNumber}`);
if (
![
'endo',
'worker',
'eval',
'readable-blob',
'make-unconfined',
'make-bundle',
'host',
'guest',
'least-authority',
'loopback-network',
'peer',
'handle',
'pet-inspector',
'pet-store',
'lookup',
'directory',
].includes(formula.type)
) {
assert.Fail`Invalid formula identifier, unrecognized type ${q(id)}`;
}
assertValidFormulaType(formula.type);
// TODO further validation
typeForId.set(id, formula.type);

return makeControllerForFormula(id, formulaNumber, formula, context);
};

Expand Down Expand Up @@ -731,6 +735,7 @@ const makeDaemonCore = async (
internal: E.get(partial).internal,
});
controllerForId.set(id, controller);
typeForId.set(id, formula.type);

// The controller _must_ be constructed in the synchronous prelude of this function.
const controllerValue = makeControllerForFormula(
Expand Down Expand Up @@ -780,11 +785,6 @@ const makeDaemonCore = async (
return controller;
};

// TODO: sorry, forcing nodeId into a petstore name
const petStoreNameForNodeIdentifier = nodeIdentifier => {
return `p${nodeIdentifier.slice(0, 126)}`;
};

/**
* @param {string} nodeIdentifier
* @returns {Promise<string>}
Expand All @@ -793,12 +793,9 @@ const makeDaemonCore = async (
if (nodeIdentifier === ownNodeIdentifier) {
throw new Error(`Cannot get peer formula identifier for self`);
}
const peerStore = /** @type {import('./types.js').PetStore} */ (
// eslint-disable-next-line no-use-before-define
await provide(peersId)
);
const nodeName = petStoreNameForNodeIdentifier(nodeIdentifier);
const peerId = peerStore.identifyLocal(nodeName);
// eslint-disable-next-line no-use-before-define
const knownPeers = await provideKnownPeers(ownPeersId);
const peerId = knownPeers.identify(nodeIdentifier);
if (peerId === undefined) {
throw new Error(
`No peer found for node identifier ${q(nodeIdentifier)}.`,
Expand Down Expand Up @@ -976,7 +973,7 @@ const makeDaemonCore = async (
};

/**
* @type {import('./types.js').DaemonCoreInternal['formulateHostDependencies']}
* @type {import('./types.js').DaemonCore['formulateHostDependencies']}
*/
const formulateHostDependencies = async specifiedIdentifiers => {
const { specifiedWorkerId, ...remainingSpecifiedIdentifiers } =
Expand All @@ -997,7 +994,7 @@ const makeDaemonCore = async (
});
};

/** @type {import('./types.js').DaemonCoreInternal['formulateNumberedHost']} */
/** @type {import('./types.js').DaemonCore['formulateNumberedHost']} */
const formulateNumberedHost = identifiers => {
/** @type {import('./types.js').HostFormula} */
const formula = {
Expand Down Expand Up @@ -1041,7 +1038,7 @@ const makeDaemonCore = async (
);
};

/** @type {import('./types.js').DaemonCoreInternal['formulateGuestDependencies']} */
/** @type {import('./types.js').DaemonCore['formulateGuestDependencies']} */
const formulateGuestDependencies = async hostId =>
harden({
guestFormulaNumber: await randomHex512(),
Expand All @@ -1052,7 +1049,7 @@ const makeDaemonCore = async (
workerId: (await formulateNumberedWorker(await randomHex512())).id,
});

/** @type {import('./types.js').DaemonCoreInternal['formulateNumberedGuest']} */
/** @type {import('./types.js').DaemonCore['formulateNumberedGuest']} */
const formulateNumberedGuest = identifiers => {
/** @type {import('./types.js').GuestFormula} */
const formula = {
Expand Down Expand Up @@ -1348,8 +1345,8 @@ const makeDaemonCore = async (
const { id: newPeersId } = await formulateNumberedPetStore(
peersFormulaNumber,
);
if (newPeersId !== peersId) {
assert.Fail`Peers PetStore formula identifier did not match expected value, expected ${peersId}, got ${newPeersId}`;
if (newPeersId !== ownPeersId) {
assert.Fail`Peers PetStore formula identifier did not match expected value, expected ${ownPeersId}, got ${newPeersId}`;
}

// Ensure the default host is formulated and persisted.
Expand All @@ -1372,7 +1369,7 @@ const makeDaemonCore = async (
const formula = {
type: 'endo',
networks: identifiers.networksDirectoryId,
peers: peersId,
peers: ownPeersId,
host: identifiers.defaultHostId,
leastAuthority: leastAuthorityId,
};
Expand Down Expand Up @@ -1454,6 +1451,33 @@ const makeDaemonCore = async (
throw new Error('Cannot connect to peer: no supported addresses');
};

/**
* The "known peers store" is like a pet store, but maps node identifiers to
* full peer ids.
*
* @type {import('./types.js').DaemonCore['provideKnownPeers']}
*/
const provideKnownPeers = async peersFormulaId => {
// "Known peers" is just a pet store with an adapter over it.
const petStore = /** @type {import('./types.js').PetStore} */ (
await provide(peersFormulaId)
);

// Pet stores do not accept full ids as names.
/** @param {string} nodeIdentifier */
const getNameFor = nodeIdentifier => {
return `p${nodeIdentifier.slice(0, 126)}`;
};

return harden({
has: nodeIdentifier => petStore.has(getNameFor(nodeIdentifier)),
identify: nodeIdentifier =>
petStore.identifyLocal(getNameFor(nodeIdentifier)),
write: (nodeIdentifier, peerId) =>
petStore.write(getNameFor(nodeIdentifier), peerId),
});
};

/**
* This is used to provide a value for a formula identifier that is known to
* originate from the specified peer.
Expand All @@ -1479,6 +1503,7 @@ const makeDaemonCore = async (
const { makeIdentifiedDirectory, makeDirectoryNode } = makeDirectoryMaker({
provide,
getIdForRef,
getTypeForId,
formulateDirectory,
});

Expand Down Expand Up @@ -1620,32 +1645,12 @@ const makeDaemonCore = async (
return info;
};

/** @type {import('./types.js').DaemonCore} */
const daemonCore = {
nodeIdentifier: ownNodeIdentifier,
provideController,
provideControllerAndResolveHandle,
provide,
formulate,
getIdForRef,
getAllNetworkAddresses,
cancelValue,
makeMailbox,
makeDirectoryNode,
/** @type {import('./types.js').DaemonCoreExternal} */
return {
formulateEndoBootstrap,
formulateNetworksDirectory,
formulateLoopbackNetwork,
formulateDirectory,
formulateWorker,
formulateHost,
formulateGuest,
formulatePeer,
formulateEval,
formulateUnconfined,
formulateReadableBlob,
formulateBundle,
provide,
nodeIdentifier: ownNodeIdentifier,
};
return daemonCore;
};

/**
Expand Down
15 changes: 15 additions & 0 deletions packages/daemon/src/directory.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@

import { E, Far } from '@endo/far';
import { makeIteratorRef } from './reader-ref.js';
import { formatLocator } from './locator.js';

const { quote: q } = assert;

/**
* @param {object} args
* @param {import('./types.js').DaemonCore['provide']} args.provide
* @param {import('./types.js').DaemonCore['getIdForRef']} args.getIdForRef
* @param {import('./types.js').DaemonCore['getTypeForId']} args.getTypeForId
* @param {import('./types.js').DaemonCore['formulateDirectory']} args.formulateDirectory
*/
export const makeDirectoryMaker = ({
provide,
getIdForRef,
getTypeForId,
formulateDirectory,
}) => {
/** @type {import('./types.js').MakeDirectoryNode} */
Expand Down Expand Up @@ -84,6 +87,17 @@ export const makeDirectoryMaker = ({
return hub.identify(name);
};

/** @type {import('./types.js').EndoDirectory['locate']} */
const locate = async (...petNamePath) => {
const id = await identify(...petNamePath);
if (id === undefined) {
return undefined;
}

const formulaType = await getTypeForId(id);
return formatLocator(id, formulaType);
};

/** @type {import('./types.js').EndoDirectory['list']} */
const list = async (...petNamePath) => {
if (petNamePath.length === 0) {
Expand Down Expand Up @@ -192,6 +206,7 @@ export const makeDirectoryMaker = ({
const directory = {
has,
identify,
locate,
list,
listIdentifiers,
followChanges,
Expand Down
9 changes: 8 additions & 1 deletion packages/daemon/src/formula-identifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@

const { quote: q } = assert;

const numberPattern = /^[0-9a-f]{128}$/;
const idPattern = /^(?<number>[0-9a-f]{128}):(?<node>[0-9a-f]{128})$/;

/**
* @param {string} allegedNumber - The formula number or node identifier to test.
*/
export const isValidNumber = allegedNumber =>
typeof allegedNumber === 'string' && numberPattern.test(allegedNumber);

/**
* @param {string} id
* @param {string} [petName]
* @returns {void}
*/
export const assertValidId = (id, petName) => {
if (!idPattern.test(id)) {
if (typeof id !== 'string' || !idPattern.test(id)) {
let message = `Invalid formula identifier ${q(id)}`;
if (petName !== undefined) {
message += ` for pet name ${q(petName)}`;
Expand Down
Loading

0 comments on commit 796e4bb

Please sign in to comment.