From 2cfb7f7f4abdf8f0cba2e36ce9cb89414d2432fe Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 18 Mar 2024 17:44:44 -0700 Subject: [PATCH 1/7] feat(daemon): Add daemon locator strings Adds locator strings of the form `endo://{nodeIdentifier}/?id={formulaNumber}&type={formulaType}` to the daemon, as well as relevant utilities modeled on `formula-identifiers.js`. --- packages/daemon/src/daemon.js | 24 +------- packages/daemon/src/formula-identifier.js | 1 + packages/daemon/src/formula-type.js | 31 ++++++++++ packages/daemon/src/locator.js | 75 +++++++++++++++++++++++ packages/daemon/test/test-formula-type.js | 27 ++++++++ packages/daemon/test/test-locator.js | 72 ++++++++++++++++++++++ 6 files changed, 208 insertions(+), 22 deletions(-) create mode 100644 packages/daemon/src/formula-type.js create mode 100644 packages/daemon/src/locator.js create mode 100644 packages/daemon/test/test-formula-type.js create mode 100644 packages/daemon/test/test-locator.js diff --git a/packages/daemon/src/daemon.js b/packages/daemon/src/daemon.js index b8b5e7761f..af84011a67 100644 --- a/packages/daemon/src/daemon.js +++ b/packages/daemon/src/daemon.js @@ -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. @@ -676,28 +677,7 @@ const makeDaemonCore = async ( } 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 return makeControllerForFormula(id, formulaNumber, formula, context); }; diff --git a/packages/daemon/src/formula-identifier.js b/packages/daemon/src/formula-identifier.js index 516b6ef379..adad28e192 100644 --- a/packages/daemon/src/formula-identifier.js +++ b/packages/daemon/src/formula-identifier.js @@ -2,6 +2,7 @@ const { quote: q } = assert; +export const nodeOrIdPattern = /^[0-9a-f]{128}$/; const idPattern = /^(?[0-9a-f]{128}):(?[0-9a-f]{128})$/; /** diff --git a/packages/daemon/src/formula-type.js b/packages/daemon/src/formula-type.js new file mode 100644 index 0000000000..44dcbe9d17 --- /dev/null +++ b/packages/daemon/src/formula-type.js @@ -0,0 +1,31 @@ +const { quote: q } = assert; + +// Note: Alphabetically sorted +const formulaTypes = new Set([ + 'directory', + 'endo', + 'eval', + 'guest', + 'handle', + 'host', + 'least-authority', + 'lookup', + 'loopback-network', + 'make-bundle', + 'make-unconfined', + 'peer', + 'pet-inspector', + 'pet-store', + 'readable-blob', + 'worker', +]); + +/** @param {string} allegedType */ +export const isValidFormulaType = allegedType => formulaTypes.has(allegedType); + +/** @param {string} allegedType */ +export const assertValidFormulaType = allegedType => { + if (!isValidFormulaType(allegedType)) { + assert.Fail`Unrecognized formula type ${q(allegedType)}`; + } +}; diff --git a/packages/daemon/src/locator.js b/packages/daemon/src/locator.js new file mode 100644 index 0000000000..8371ba6c83 --- /dev/null +++ b/packages/daemon/src/locator.js @@ -0,0 +1,75 @@ +import { parseId, nodeOrIdPattern } from './formula-identifier.js'; +import { assertValidFormulaType, isValidFormulaType } from './formula-type.js'; + +const { quote: q } = assert; + +/** + * The endo locator format: + * ``` + * endo://{nodeIdentifier}/?id={formulaNumber}&type={formulaType} + * ``` + * Note that the `id` query param is just the formula number. + */ + +/** @param {string} allegedLocator */ +export const parseLocator = allegedLocator => { + const errorPrefix = `Invalid locator ${q(allegedLocator)}:`; + + if (!URL.canParse(allegedLocator)) assert.Fail`${errorPrefix} Invalid URL.`; + const url = new URL(allegedLocator); + + if (!allegedLocator.startsWith('endo://')) { + assert.Fail`${errorPrefix} Invalid protocol.`; + } + + const node = url.host; + if (!nodeOrIdPattern.test(node)) { + assert.Fail`${errorPrefix} Invalid node identifier.`; + } + + if ( + url.searchParams.size !== 2 || + !url.searchParams.has('id') || + !url.searchParams.has('type') + ) { + assert.Fail`${errorPrefix} Invalid search params.`; + } + + /** @type {string} */ + const id = url.searchParams.get('id'); + if (!nodeOrIdPattern.test(id)) { + assert.Fail`${errorPrefix} Invalid id.`; + } + + /** @type {string} */ + const formulaType = url.searchParams.get('type'); + if (!isValidFormulaType(formulaType)) { + assert.Fail`${errorPrefix} Invalid type.`; + } + + /** @type {{ formulaType: string, node: string, id: string }} */ + return { formulaType, node, id }; +}; + +/** @param {string} allegedLocator */ +export const assertValidLocator = allegedLocator => { + parseLocator(allegedLocator); +}; + +/** + * @param {string} id - The full formula identifier. + * @param {string} formulaType - The type of the formula with the given id. + */ +export const formatLocator = (id, formulaType) => { + const { number, node } = parseId(id); + const url = new URL(`endo://${node}`); + url.pathname = '/'; + + // The id query param is just the number + url.searchParams.set('id', number); + + assertValidFormulaType(formulaType); + url.searchParams.set('type', formulaType); + + return url.toString(); +}; diff --git a/packages/daemon/test/test-formula-type.js b/packages/daemon/test/test-formula-type.js new file mode 100644 index 0000000000..abd8820221 --- /dev/null +++ b/packages/daemon/test/test-formula-type.js @@ -0,0 +1,27 @@ +import test from '@endo/ses-ava/prepare-endo.js'; + +import { + assertValidFormulaType, + isValidFormulaType, +} from '../src/formula-type.js'; + +test('isValidFormulaType', t => { + [ + ['eval', true], + ['make-unconfined', true], + ['', false], + [null, false], + [undefined, false], + [{}, false], + ].forEach(([value, expected]) => { + t.is(isValidFormulaType(value), expected); + }); +}); + +test('assertValidFormulaType - valid', t => { + t.notThrows(() => assertValidFormulaType('eval')); +}); + +test('assertValidFormulaType - invalid', t => { + t.throws(() => assertValidFormulaType('foobar')); +}); diff --git a/packages/daemon/test/test-locator.js b/packages/daemon/test/test-locator.js new file mode 100644 index 0000000000..aa7b2dfa7f --- /dev/null +++ b/packages/daemon/test/test-locator.js @@ -0,0 +1,72 @@ +import test from '@endo/ses-ava/prepare-endo.js'; + +import { + assertValidLocator, + formatLocator, + parseLocator, +} from '../src/locator.js'; +import { formatId } from '../src/formula-identifier.js'; + +const validNode = + 'd5c98890be3d17ad375517464ec494068267de60bd4b3143ef0214cc895746f2892baca4fec19b6d4dfc1f683b7cf3d2a884dfcae568555dd89665c33dfdc4b3'; +const validId = + '5cf3d8b4d6e03fb51d71fbbb6fa6982edbff673cd193707c902b70a26b7b468017fbcfc5c2895f4379459badbe507a4ef00e1d3638f4a67e8a8c14fd1d85d9aa'; +const validType = 'eval'; + +const makeLocator = (components = {}) => { + const { + protocol = 'endo://', + host = validNode, + param1 = `id=${validId}`, + param2 = `type=${validType}`, + } = components; + return `${protocol}${host}/?${param1}&${param2}`; +}; + +test('assertValidLocator - valid', t => { + t.notThrows(() => assertValidLocator(makeLocator())); + + // Reverse search param order + t.notThrows(() => + assertValidLocator( + makeLocator({ + param1: `type=${validType}`, + param2: `id=${validId}`, + }), + ), + ); +}); + +test('assertValidLocator - invalid', t => { + [ + ['foobar', /Invalid URL.$/u], + ['', /Invalid URL.$/u], + [null, /Invalid URL.$/u], + [undefined, /Invalid URL.$/u], + [{}, /Invalid URL.$/u], + [makeLocator({ protocol: 'foobar://' }), /Invalid protocol.$/u], + [makeLocator({ host: 'foobar' }), /Invalid node identifier.$/u], + [makeLocator({ param1: 'foo=bar' }), /Invalid search params.$/u], + [makeLocator({ param2: 'foo=bar' }), /Invalid search params.$/u], + [`${makeLocator()}&foo=bar`, /Invalid search params.$/u], + [makeLocator({ param1: 'id=foobar' }), /Invalid id.$/u], + [makeLocator({ param2: 'type=foobar' }), /Invalid type.$/u], + ].forEach(([locator, reason]) => { + t.throws(() => assertValidLocator(locator), { message: reason }); + }); +}); + +test('parseLocator', t => { + t.deepEqual(parseLocator(makeLocator()), { + id: validId, + node: validNode, + formulaType: validType, + }); +}); + +test('formatLocator', t => { + t.is( + formatLocator(formatId({ number: validId, node: validNode }), validType), + makeLocator(), + ); +}); From effc083d8c46d30e4d93d9c45aa5e2f007af1cae Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 19 Mar 2024 09:58:04 -0700 Subject: [PATCH 2/7] refactor(daemon): Rationalize DaemonCore types --- packages/daemon/src/daemon.js | 36 ++------- packages/daemon/src/locator.js | 10 +-- packages/daemon/src/types.d.ts | 144 +++++++++++++++++++-------------- 3 files changed, 98 insertions(+), 92 deletions(-) diff --git a/packages/daemon/src/daemon.js b/packages/daemon/src/daemon.js index af84011a67..a3717c527a 100644 --- a/packages/daemon/src/daemon.js +++ b/packages/daemon/src/daemon.js @@ -956,7 +956,7 @@ const makeDaemonCore = async ( }; /** - * @type {import('./types.js').DaemonCoreInternal['formulateHostDependencies']} + * @type {import('./types.js').DaemonCore['formulateHostDependencies']} */ const formulateHostDependencies = async specifiedIdentifiers => { const { specifiedWorkerId, ...remainingSpecifiedIdentifiers } = @@ -977,7 +977,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 = { @@ -1021,7 +1021,7 @@ const makeDaemonCore = async ( ); }; - /** @type {import('./types.js').DaemonCoreInternal['formulateGuestDependencies']} */ + /** @type {import('./types.js').DaemonCore['formulateGuestDependencies']} */ const formulateGuestDependencies = async hostId => harden({ guestFormulaNumber: await randomHex512(), @@ -1032,7 +1032,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 = { @@ -1600,32 +1600,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; }; /** diff --git a/packages/daemon/src/locator.js b/packages/daemon/src/locator.js index 8371ba6c83..ddc1d13de3 100644 --- a/packages/daemon/src/locator.js +++ b/packages/daemon/src/locator.js @@ -15,7 +15,9 @@ const { quote: q } = assert; export const parseLocator = allegedLocator => { const errorPrefix = `Invalid locator ${q(allegedLocator)}:`; - if (!URL.canParse(allegedLocator)) assert.Fail`${errorPrefix} Invalid URL.`; + if (!URL.canParse(allegedLocator)) { + assert.Fail`${errorPrefix} Invalid URL.`; + } const url = new URL(allegedLocator); if (!allegedLocator.startsWith('endo://')) { @@ -35,15 +37,13 @@ export const parseLocator = allegedLocator => { assert.Fail`${errorPrefix} Invalid search params.`; } - /** @type {string} */ const id = url.searchParams.get('id'); - if (!nodeOrIdPattern.test(id)) { + if (id === null || !nodeOrIdPattern.test(id)) { assert.Fail`${errorPrefix} Invalid id.`; } - /** @type {string} */ const formulaType = url.searchParams.get('type'); - if (!isValidFormulaType(formulaType)) { + if (formulaType === null || !isValidFormulaType(formulaType)) { assert.Fail`${errorPrefix} Invalid type.`; } diff --git a/packages/daemon/src/types.d.ts b/packages/daemon/src/types.d.ts index a3f37d0678..b42cc2e34d 100644 --- a/packages/daemon/src/types.d.ts +++ b/packages/daemon/src/types.d.ts @@ -738,7 +738,45 @@ type FormulateNumberedHostParams = { networksDirectoryId: string; }; -export interface DaemonCoreInternal { +export interface DaemonCore { + cancelValue: (id: string, reason: Error) => Promise; + + formulate: ( + formulaNumber: string, + formula: Formula, + ) => Promise<{ + id: string; + value: unknown; + }>; + + formulateBundle: ( + hostId: string, + bundleId: string, + deferredTasks: DeferredTasks, + specifiedWorkerId?: string, + specifiedPowersId?: string, + ) => FormulateResult; + + formulateDirectory: () => FormulateResult; + + formulateEndoBootstrap: ( + specifiedFormulaNumber: string, + ) => FormulateResult; + + formulateEval: ( + hostId: string, + source: string, + codeNames: string[], + endowmentIdsOrPaths: (string | string[])[], + deferredTasks: DeferredTasks, + specifiedWorkerId?: string, + ) => FormulateResult; + + formulateGuest: ( + hostId: string, + deferredTasks: DeferredTasks, + ) => FormulateResult; + /** * Helper for callers of {@link formulateNumberedGuest}. * @param hostId - The formula identifier of the host to formulate a guest for. @@ -747,9 +785,14 @@ export interface DaemonCoreInternal { formulateGuestDependencies: ( hostId: string, ) => Promise>; - formulateNumberedGuest: ( - identifiers: FormulateNumberedGuestParams, - ) => FormulateResult; + + formulateHost: ( + endoId: string, + networksDirectoryId: string, + deferredTasks: DeferredTasks, + specifiedWorkerId?: string | undefined, + ) => FormulateResult; + /** * Helper for callers of {@link formulateNumberedHost}. * @param specifiedIdentifiers - The existing formula identifiers specified to the host formulation. @@ -758,54 +801,29 @@ export interface DaemonCoreInternal { formulateHostDependencies: ( specifiedIdentifiers: FormulateHostDependenciesParams, ) => Promise>; + + formulateLoopbackNetwork: () => FormulateResult; + + formulateNetworksDirectory: () => FormulateResult; + + formulateNumberedGuest: ( + identifiers: FormulateNumberedGuestParams, + ) => FormulateResult; + formulateNumberedHost: ( identifiers: FormulateNumberedHostParams, ) => FormulateResult; -} -export interface DaemonCore { - nodeIdentifier: string; - provide: (id: string) => Promise; - provideController: (id: string) => Controller; - provideControllerAndResolveHandle: (id: string) => Promise; - formulate: ( - formulaNumber: string, - formula: Formula, - ) => Promise<{ - id: string; - value: unknown; - }>; - getIdForRef: (ref: unknown) => string | undefined; - getAllNetworkAddresses: (networksDirectoryId: string) => Promise; - formulateEndoBootstrap: ( - specifiedFormulaNumber: string, - ) => FormulateResult; - formulateWorker: ( - deferredTasks: DeferredTasks, - ) => FormulateResult; - formulateDirectory: () => FormulateResult; - formulateHost: ( - endoId: string, - networksDirectoryId: string, - deferredTasks: DeferredTasks, - specifiedWorkerId?: string | undefined, - ) => FormulateResult; - formulateGuest: ( - hostId: string, - deferredTasks: DeferredTasks, - ) => FormulateResult; + formulatePeer: ( + networksId: string, + addresses: Array, + ) => FormulateResult; + formulateReadableBlob: ( readerRef: ERef>, deferredTasks: DeferredTasks, ) => FormulateResult; - formulateEval: ( - hostId: string, - source: string, - codeNames: string[], - endowmentIdsOrPaths: (string | string[])[], - deferredTasks: DeferredTasks, - specifiedWorkerId?: string, - ) => FormulateResult; + formulateUnconfined: ( hostId: string, specifier: string, @@ -813,22 +831,30 @@ export interface DaemonCore { specifiedWorkerId?: string, specifiedPowersId?: string, ) => FormulateResult; - formulateBundle: ( - hostId: string, - bundleId: string, - deferredTasks: DeferredTasks, - specifiedWorkerId?: string, - specifiedPowersId?: string, - ) => FormulateResult; - formulatePeer: ( - networksId: string, - addresses: Array, - ) => FormulateResult; - formulateNetworksDirectory: () => FormulateResult; - formulateLoopbackNetwork: () => FormulateResult; - cancelValue: (id: string, reason: Error) => Promise; - makeMailbox: MakeMailbox; + + formulateWorker: ( + deferredTasks: DeferredTasks, + ) => FormulateResult; + + getAllNetworkAddresses: (networksDirectoryId: string) => Promise; + + getIdForRef: (ref: unknown) => string | undefined; + makeDirectoryNode: MakeDirectoryNode; + + makeMailbox: MakeMailbox; + + provide: (id: string) => Promise; + + provideController: (id: string) => Controller; + + provideControllerAndResolveHandle: (id: string) => Promise; +} + +export interface DaemonCoreExternal { + formulateEndoBootstrap: DaemonCore['formulateEndoBootstrap']; + nodeIdentifier: string; + provide: DaemonCore['provide']; } export type SerialJobs = { From 4bd2171416969d795438d851b23dd88600612265 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 19 Mar 2024 11:36:06 -0700 Subject: [PATCH 3/7] refactor(daemon): Rationalize formula number validation --- packages/daemon/src/formula-identifier.js | 10 ++++++++-- packages/daemon/src/locator.js | 8 +++++--- packages/daemon/src/pet-store.js | 18 +++++++++--------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/daemon/src/formula-identifier.js b/packages/daemon/src/formula-identifier.js index adad28e192..b570b2ecd6 100644 --- a/packages/daemon/src/formula-identifier.js +++ b/packages/daemon/src/formula-identifier.js @@ -2,16 +2,22 @@ const { quote: q } = assert; -export const nodeOrIdPattern = /^[0-9a-f]{128}$/; +const numberPattern = /^[0-9a-f]{128}$/; const idPattern = /^(?[0-9a-f]{128}):(?[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)}`; diff --git a/packages/daemon/src/locator.js b/packages/daemon/src/locator.js index ddc1d13de3..215e30b6f9 100644 --- a/packages/daemon/src/locator.js +++ b/packages/daemon/src/locator.js @@ -1,4 +1,6 @@ -import { parseId, nodeOrIdPattern } from './formula-identifier.js'; +/// + +import { parseId, isValidNumber } from './formula-identifier.js'; import { assertValidFormulaType, isValidFormulaType } from './formula-type.js'; const { quote: q } = assert; @@ -25,7 +27,7 @@ export const parseLocator = allegedLocator => { } const node = url.host; - if (!nodeOrIdPattern.test(node)) { + if (!isValidNumber(node)) { assert.Fail`${errorPrefix} Invalid node identifier.`; } @@ -38,7 +40,7 @@ export const parseLocator = allegedLocator => { } const id = url.searchParams.get('id'); - if (id === null || !nodeOrIdPattern.test(id)) { + if (id === null || !isValidNumber(id)) { assert.Fail`${errorPrefix} Invalid id.`; } diff --git a/packages/daemon/src/pet-store.js b/packages/daemon/src/pet-store.js index f553dba089..e55a7b2979 100644 --- a/packages/daemon/src/pet-store.js +++ b/packages/daemon/src/pet-store.js @@ -1,12 +1,10 @@ // @ts-check import { makeChangeTopic } from './pubsub.js'; -import { parseId, assertValidId } from './formula-identifier.js'; +import { parseId, assertValidId, isValidNumber } from './formula-identifier.js'; const { quote: q } = assert; -const validNumberPattern = /^[0-9a-f]{128}$/; - /** * @param {import('./types.js').FilePowers} filePowers * @param {import('./types.js').Locator} locator @@ -224,16 +222,18 @@ export const makePetStoreMaker = (filePowers, locator) => { }; /** - * @param {string} id + * @param {string} formulaNumber * @param {(name: string) => void} assertValidName * @returns {Promise} */ - const makeIdentifiedPetStore = (id, assertValidName) => { - if (!validNumberPattern.test(id)) { - throw new Error(`Invalid identifier for pet store ${q(id)}`); + const makeIdentifiedPetStore = (formulaNumber, assertValidName) => { + if (!isValidNumber(formulaNumber)) { + throw new Error( + `Invalid formula number for pet store ${q(formulaNumber)}`, + ); } - const prefix = id.slice(0, 2); - const suffix = id.slice(2); + const prefix = formulaNumber.slice(0, 2); + const suffix = formulaNumber.slice(2); const petNameDirectoryPath = filePowers.joinPath( locator.statePath, 'pet-store', From 6bbce1e2f408d7eca2eb61fa26545ade9fff1422 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 19 Mar 2024 10:15:56 -0700 Subject: [PATCH 4/7] refactor(daemon): Add known peers abstraction --- packages/daemon/src/daemon.js | 63 +++++++++++++++++++------------ packages/daemon/src/locator.js | 2 +- packages/daemon/src/types.d.ts | 10 ++++- packages/daemon/test/test-endo.js | 6 +-- 4 files changed, 52 insertions(+), 29 deletions(-) diff --git a/packages/daemon/src/daemon.js b/packages/daemon/src/daemon.js index a3717c527a..b760ebc659 100644 --- a/packages/daemon/src/daemon.js +++ b/packages/daemon/src/daemon.js @@ -122,7 +122,7 @@ const makeDaemonCore = async ( rootEntropy, cryptoPowers.makeSha512(), ); - const peersId = formatId({ + const ownPeersId = formatId({ number: peersFormulaNumber, node: ownNodeIdentifier, }); @@ -570,15 +570,11 @@ 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; @@ -586,7 +582,7 @@ const makeDaemonCore = async ( 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 { @@ -760,11 +756,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} @@ -773,12 +764,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)}.`, @@ -1328,8 +1316,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. @@ -1352,7 +1340,7 @@ const makeDaemonCore = async ( const formula = { type: 'endo', networks: identifiers.networksDirectoryId, - peers: peersId, + peers: ownPeersId, host: identifiers.defaultHostId, leastAuthority: leastAuthorityId, }; @@ -1434,6 +1422,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. diff --git a/packages/daemon/src/locator.js b/packages/daemon/src/locator.js index 215e30b6f9..81e1c675c8 100644 --- a/packages/daemon/src/locator.js +++ b/packages/daemon/src/locator.js @@ -1,4 +1,4 @@ -/// +// @ts-check import { parseId, isValidNumber } from './formula-identifier.js'; import { assertValidFormulaType, isValidFormulaType } from './formula-type.js'; diff --git a/packages/daemon/src/types.d.ts b/packages/daemon/src/types.d.ts index b42cc2e34d..72d4a1b97e 100644 --- a/packages/daemon/src/types.d.ts +++ b/packages/daemon/src/types.d.ts @@ -391,7 +391,7 @@ export interface NameHub { ): AsyncGenerator; lookup(...petNamePath: string[]): Promise; reverseLookup(value: unknown): Array; - write(petNamePath: string[], id): Promise; + write(petNamePath: string[], id: string): Promise; remove(...petNamePath: string[]): Promise; move(fromPetName: string[], toPetName: string[]): Promise; copy(fromPetName: string[], toPetName: string[]): Promise; @@ -487,6 +487,12 @@ export interface EndoPeer { export type EndoPeerControllerPartial = ControllerPartial; export type EndoPeerController = Controller; +export interface EndoKnownPeers { + has: (nodeIdentifier: string) => boolean; + identify: (nodeIdentifier: string) => string | undefined; + write: (nodeIdentifier: string, peerId: string) => Promise; +} + export interface EndoGateway { provide: (id: string) => Promise; } @@ -849,6 +855,8 @@ export interface DaemonCore { provideController: (id: string) => Controller; provideControllerAndResolveHandle: (id: string) => Promise; + + provideKnownPeers: (peersFormulaId: string) => Promise; } export interface DaemonCoreExternal { diff --git a/packages/daemon/test/test-endo.js b/packages/daemon/test/test-endo.js index 68bd22b118..a00243c34d 100644 --- a/packages/daemon/test/test-endo.js +++ b/packages/daemon/test/test-endo.js @@ -1357,7 +1357,7 @@ test('guest cannot access host methods', async t => { test('read unknown nodeId', async t => { const { promise: cancelled, reject: cancel } = makePromiseKit(); t.teardown(() => cancel(Error('teardown'))); - const locator = makeLocator('tmp', 'read unknown nodeId'); + const locator = makeLocator('tmp', 'read-unknown-nodeid'); await stop(locator).catch(() => {}); await purge(locator); @@ -1390,8 +1390,8 @@ test('read unknown nodeId', async t => { test('read remote value', async t => { const { promise: cancelled, reject: cancel } = makePromiseKit(); t.teardown(() => cancel(Error('teardown'))); - const locatorA = makeLocator('tmp', 'read remote value A'); - const locatorB = makeLocator('tmp', 'read remote value B'); + const locatorA = makeLocator('tmp', 'read-remote-value-a'); + const locatorB = makeLocator('tmp', 'read-remote-value-b'); let hostA; { await stop(locatorA).catch(() => {}); From 89779589db009b3f7919af5759ee659aff933bc1 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 20 Mar 2024 10:42:55 -0700 Subject: [PATCH 5/7] feat(daemon): Add directory `locate()` method Adds `locate()` to the directory, which accepts a pet name and returns a locator string. `locate()` will succeed for any named local value at any point in the daemon's lifecycle. Adds the `remote` formula type to populate the `type` query param of locators, since the "real" type of remote values cannot be known. --- packages/daemon/src/daemon.js | 30 ++++++++++++-- packages/daemon/src/directory.js | 15 +++++++ packages/daemon/src/guest.js | 2 + packages/daemon/src/host.js | 2 + packages/daemon/src/locator.js | 25 ++++++++++-- packages/daemon/src/types.d.ts | 3 ++ packages/daemon/test/test-endo.js | 66 +++++++++++++++++++++++++++++++ 7 files changed, 137 insertions(+), 6 deletions(-) diff --git a/packages/daemon/src/daemon.js b/packages/daemon/src/daemon.js index b760ebc659..de3adb9cc0 100644 --- a/packages/daemon/src/daemon.js +++ b/packages/daemon/src/daemon.js @@ -183,19 +183,37 @@ 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} */ const controllerForId = new Map(); + /** + * Forward look-up, for answering "what is the formula type of this id". + * @type {Map} + */ + const typeForId = new Map(); + + /** @param {string} id */ + const getTypeForId = async id => { + await null; + + const formulaType = typeForId.get(id); + if (formulaType !== undefined) { + return formulaType; + } + + const formula = await persistencePowers.readFormula(parseId(id).number); + 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". @@ -667,14 +685,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}`); assertValidFormulaType(formula.type); // TODO further validation + typeForId.set(id, formula.type); + return makeControllerForFormula(id, formulaNumber, formula, context); }; @@ -707,6 +729,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( @@ -1474,6 +1497,7 @@ const makeDaemonCore = async ( const { makeIdentifiedDirectory, makeDirectoryNode } = makeDirectoryMaker({ provide, getIdForRef, + getTypeForId, formulateDirectory, }); diff --git a/packages/daemon/src/directory.js b/packages/daemon/src/directory.js index f6dd55b025..3a20683ee7 100644 --- a/packages/daemon/src/directory.js +++ b/packages/daemon/src/directory.js @@ -2,6 +2,7 @@ import { E, Far } from '@endo/far'; import { makeIteratorRef } from './reader-ref.js'; +import { formatLocator } from './locator.js'; const { quote: q } = assert; @@ -9,11 +10,13 @@ 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} */ @@ -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) { @@ -192,6 +206,7 @@ export const makeDirectoryMaker = ({ const directory = { has, identify, + locate, list, listIdentifiers, followChanges, diff --git a/packages/daemon/src/guest.js b/packages/daemon/src/guest.js index 3f22c017e3..7f6cd2c7d0 100644 --- a/packages/daemon/src/guest.js +++ b/packages/daemon/src/guest.js @@ -64,6 +64,7 @@ export const makeGuestMaker = ({ const { has, identify, + locate, list, listIdentifiers, followChanges, @@ -93,6 +94,7 @@ export const makeGuestMaker = ({ // Directory has, identify, + locate, list, listIdentifiers, followChanges, diff --git a/packages/daemon/src/host.js b/packages/daemon/src/host.js index e613166bf1..bfb28935f9 100644 --- a/packages/daemon/src/host.js +++ b/packages/daemon/src/host.js @@ -455,6 +455,7 @@ export const makeHostMaker = ({ const { has, identify, + locate, list, listIdentifiers, followChanges, @@ -483,6 +484,7 @@ export const makeHostMaker = ({ // Directory has, identify, + locate, list, listIdentifiers, followChanges, diff --git a/packages/daemon/src/locator.js b/packages/daemon/src/locator.js index 81e1c675c8..91596cb33c 100644 --- a/packages/daemon/src/locator.js +++ b/packages/daemon/src/locator.js @@ -1,7 +1,7 @@ // @ts-check import { parseId, isValidNumber } from './formula-identifier.js'; -import { assertValidFormulaType, isValidFormulaType } from './formula-type.js'; +import { isValidFormulaType } from './formula-type.js'; const { quote: q } = assert; @@ -13,6 +13,25 @@ const { quote: q } = assert; * Note that the `id` query param is just the formula number. */ +/** + * In addition to all valid formula types, the locator `type` query parameter + * also supports `remote` for remote values, since their actual formula type + * cannot be known. + * + * @param {string} allegedType + */ +const isValidLocatorType = allegedType => + isValidFormulaType(allegedType) || allegedType === 'remote'; + +/** + * @param {string} allegedType + */ +const assertValidLocatorType = allegedType => { + if (!isValidLocatorType(allegedType)) { + assert.Fail`Unrecognized locator type ${q(allegedType)}`; + } +}; + /** @param {string} allegedLocator */ export const parseLocator = allegedLocator => { const errorPrefix = `Invalid locator ${q(allegedLocator)}:`; @@ -45,7 +64,7 @@ export const parseLocator = allegedLocator => { } const formulaType = url.searchParams.get('type'); - if (formulaType === null || !isValidFormulaType(formulaType)) { + if (formulaType === null || !isValidLocatorType(formulaType)) { assert.Fail`${errorPrefix} Invalid type.`; } @@ -70,7 +89,7 @@ export const formatLocator = (id, formulaType) => { // The id query param is just the number url.searchParams.set('id', number); - assertValidFormulaType(formulaType); + assertValidLocatorType(formulaType); url.searchParams.set('type', formulaType); return url.toString(); diff --git a/packages/daemon/src/types.d.ts b/packages/daemon/src/types.d.ts index 72d4a1b97e..0b44b5e58d 100644 --- a/packages/daemon/src/types.d.ts +++ b/packages/daemon/src/types.d.ts @@ -384,6 +384,7 @@ export interface PetStore { export interface NameHub { has(...petNamePath: string[]): Promise; identify(...petNamePath: string[]): Promise; + locate(...petNamePath: string[]): Promise; list(...petNamePath: string[]): Promise>; listIdentifiers(...petNamePath: string[]): Promise>; followChanges( @@ -846,6 +847,8 @@ export interface DaemonCore { getIdForRef: (ref: unknown) => string | undefined; + getTypeForId: (id: string) => Promise; + makeDirectoryNode: MakeDirectoryNode; makeMailbox: MakeMailbox; diff --git a/packages/daemon/test/test-endo.js b/packages/daemon/test/test-endo.js index a00243c34d..897a798feb 100644 --- a/packages/daemon/test/test-endo.js +++ b/packages/daemon/test/test-endo.js @@ -24,6 +24,7 @@ import { } from '../index.js'; import { makeCryptoPowers } from '../src/daemon-node-powers.js'; import { formatId } from '../src/formula-identifier.js'; +import { parseLocator } from '../src/locator.js'; const cryptoPowers = makeCryptoPowers(crypto); @@ -1483,3 +1484,68 @@ test('read remote value', async t => { await stop(locatorA); await stop(locatorB); }); + +test('locate local value', async t => { + const { promise: cancelled, reject: cancel } = makePromiseKit(); + t.teardown(() => cancel(Error('teardown'))); + const locator = makeLocator('tmp', 'locate-local-value'); + + await stop(locator).catch(() => {}); + await purge(locator); + await start(locator); + + const { getBootstrap } = await makeEndoClient( + 'client', + locator.sockPath, + cancelled, + ); + const bootstrap = getBootstrap(); + const host = E(bootstrap).host(); + const ten = await E(host).evaluate('MAIN', '10', [], [], 'ten'); + t.is(ten, 10); + + const tenLocator = await E(host).locate('ten'); + const parsedLocator = parseLocator(tenLocator); + t.is(parsedLocator.formulaType, 'eval'); + + await stop(locator); +}); + +test('locate local persisted value', async t => { + const { promise: cancelled, reject: cancel } = makePromiseKit(); + t.teardown(() => cancel(Error('teardown'))); + const locator = makeLocator('tmp', 'locate-local-persisted-value'); + + await stop(locator).catch(() => {}); + await purge(locator); + await start(locator); + + { + const { getBootstrap } = await makeEndoClient( + 'client', + locator.sockPath, + cancelled, + ); + const bootstrap = getBootstrap(); + const host = E(bootstrap).host(); + const ten = await E(host).evaluate('MAIN', '10', [], [], 'ten'); + t.is(ten, 10); + } + + await restart(locator); + + { + const { getBootstrap } = await makeEndoClient( + 'client', + locator.sockPath, + cancelled, + ); + const bootstrap = getBootstrap(); + const host = E(bootstrap).host(); + const tenLocator = await E(host).locate('ten'); + const parsedLocator = parseLocator(tenLocator); + t.is(parsedLocator.formulaType, 'eval'); + } + + await stop(locator); +}); From b08420c0d1b841326cc3f8bc344d65b468385015 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 20 Mar 2024 10:55:31 -0700 Subject: [PATCH 6/7] refactor(daemon): Extract network testing boilerplate to function --- packages/daemon/test/test-endo.js | 116 +++++++++++------------------- 1 file changed, 43 insertions(+), 73 deletions(-) diff --git a/packages/daemon/test/test-endo.js b/packages/daemon/test/test-endo.js index 897a798feb..0fcbb52487 100644 --- a/packages/daemon/test/test-endo.js +++ b/packages/daemon/test/test-endo.js @@ -47,6 +47,47 @@ const makeLocator = (...root) => { }; }; +/** + * @param {ReturnType} locator + * @param {Promise} cancelled + */ +const makeHostWithTestNetwork = async (locator, cancelled) => { + await stop(locator).catch(() => {}); + await purge(locator); + await start(locator); + + const { getBootstrap } = await makeEndoClient( + 'client', + locator.sockPath, + cancelled, + ); + const bootstrap = getBootstrap(); + + const host = E(bootstrap).host(); + // Install test network + const servicePath = path.join(dirname, 'src', 'networks', 'tcp-netstring.js'); + const serviceLocation = url.pathToFileURL(servicePath).href; + const network = E(host).makeUnconfined( + 'MAIN', + serviceLocation, + 'SELF', + 'test-network', + ); + + // set address via request + const iteratorRef = E(host).followMessages(); + const { value: message } = await E(iteratorRef).next(); + const { number } = E.get(message); + await E(host).evaluate('MAIN', '`127.0.0.1:0`', [], [], 'netport'); + await E(host).resolve(await number, 'netport'); + + // move test network to network dir + await network; + await E(host).move(['test-network'], ['NETS', 'tcp']); + + return host; +}; + // The id of the next bundle to make. let bundleId = 0; const textEncoder = new TextEncoder(); @@ -1393,79 +1434,8 @@ test('read remote value', async t => { t.teardown(() => cancel(Error('teardown'))); const locatorA = makeLocator('tmp', 'read-remote-value-a'); const locatorB = makeLocator('tmp', 'read-remote-value-b'); - let hostA; - { - await stop(locatorA).catch(() => {}); - await purge(locatorA); - await start(locatorA); - const { getBootstrap } = await makeEndoClient( - 'client', - locatorA.sockPath, - cancelled, - ); - const bootstrap = getBootstrap(); - hostA = E(bootstrap).host(); - // Install test network - const servicePath = path.join( - dirname, - 'src', - 'networks', - 'tcp-netstring.js', - ); - const serviceLocation = url.pathToFileURL(servicePath).href; - const networkA = E(hostA).makeUnconfined( - 'MAIN', - serviceLocation, - 'SELF', - 'test-network', - ); - // set address via request - const iteratorRef = E(hostA).followMessages(); - const { value: message } = await E(iteratorRef).next(); - const { number } = E.get(message); - await E(hostA).evaluate('MAIN', '`127.0.0.1:0`', [], [], 'netport'); - await E(hostA).resolve(await number, 'netport'); - // move test network to network dir - await networkA; - await E(hostA).move(['test-network'], ['NETS', 'tcp']); - } - - let hostB; - { - await stop(locatorB).catch(() => {}); - await purge(locatorB); - await start(locatorB); - const { getBootstrap } = await makeEndoClient( - 'client', - locatorB.sockPath, - cancelled, - ); - const bootstrap = getBootstrap(); - hostB = E(bootstrap).host(); - // Install test network - const servicePath = path.join( - dirname, - 'src', - 'networks', - 'tcp-netstring.js', - ); - const serviceLocation = url.pathToFileURL(servicePath).href; - const networkB = E(hostB).makeUnconfined( - 'MAIN', - serviceLocation, - 'SELF', - 'test-network', - ); - // set address via requestcd - const iteratorRef = E(hostB).followMessages(); - const { value: message } = await E(iteratorRef).next(); - const { number } = E.get(message); - await E(hostB).evaluate('MAIN', '`127.0.0.1:0`', [], [], 'netport'); - await E(hostB).resolve(await number, 'netport'); - // move test network to network dir - await networkB; - await E(hostB).move(['test-network'], ['NETS', 'tcp']); - } + const hostA = await makeHostWithTestNetwork(locatorA, cancelled); + const hostB = await makeHostWithTestNetwork(locatorB, cancelled); // introduce nodes to each other await E(hostA).addPeerInfo(await E(hostB).getPeerInfo()); From 11198d09b8ff2e3e11aae365b83cc402ce98a811 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 20 Mar 2024 11:04:27 -0700 Subject: [PATCH 7/7] fix(daemon): Enable `locate()` of remote values Fix locating remote values and a test therefore. --- packages/daemon/src/daemon.js | 6 ++++++ packages/daemon/test/test-endo.js | 33 ++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/daemon/src/daemon.js b/packages/daemon/src/daemon.js index de3adb9cc0..44d9e7f953 100644 --- a/packages/daemon/src/daemon.js +++ b/packages/daemon/src/daemon.js @@ -210,7 +210,13 @@ const makeDaemonCore = async ( 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; }; diff --git a/packages/daemon/test/test-endo.js b/packages/daemon/test/test-endo.js index 0fcbb52487..faecb05fb7 100644 --- a/packages/daemon/test/test-endo.js +++ b/packages/daemon/test/test-endo.js @@ -1442,14 +1442,14 @@ test('read remote value', async t => { await E(hostB).addPeerInfo(await E(hostA).getPeerInfo()); // create value to share - await E(hostB).evaluate('MAIN', '`haay wuurl`', [], [], 'salutations'); + await E(hostB).evaluate('MAIN', '"hello, world!"', [], [], 'salutations'); const hostBValueIdentifier = await E(hostB).identify('salutations'); // insert in hostA out of band await E(hostA).write(['greetings'], hostBValueIdentifier); - const hostAValue = await E(hostA).lookup('greetings'); - t.is(hostAValue, 'haay wuurl'); + const hostAValue = await E(hostA).lookup('greetings'); + t.is(hostAValue, 'hello, world!'); await stop(locatorA); await stop(locatorB); @@ -1519,3 +1519,30 @@ test('locate local persisted value', async t => { await stop(locator); }); + +test('locate remote value', async t => { + const { promise: cancelled, reject: cancel } = makePromiseKit(); + t.teardown(() => cancel(Error('teardown'))); + const locatorA = makeLocator('tmp', 'locate-remote-value-a'); + const locatorB = makeLocator('tmp', 'locate-remote-value-b'); + const hostA = await makeHostWithTestNetwork(locatorA, cancelled); + const hostB = await makeHostWithTestNetwork(locatorB, cancelled); + + // introduce nodes to each other + await E(hostA).addPeerInfo(await E(hostB).getPeerInfo()); + await E(hostB).addPeerInfo(await E(hostA).getPeerInfo()); + + // create value to share + await E(hostB).evaluate('MAIN', '"hello, world!"', [], [], 'salutations'); + const hostBValueIdentifier = await E(hostB).identify('salutations'); + + // insert in hostA out of band + await E(hostA).write(['greetings'], hostBValueIdentifier); + + const greetingsLocator = await E(hostA).locate('greetings'); + const parsedGreetingsLocator = parseLocator(greetingsLocator); + t.is(parsedGreetingsLocator.formulaType, 'remote'); + + await stop(locatorA); + await stop(locatorB); +});