Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
64 changes: 64 additions & 0 deletions packages/kernel-node-runtime/test/e2e/remote-comms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,70 @@ describe.sequential('Remote Communications E2E', () => {
);
});

describe('Revocation', () => {
it(
'blocks remote invocation after revocation and leaves other objects unaffected',
async () => {
await kernel1.initRemoteComms({
relays: testRelays,
...testBackoffOptions,
});
await kernel2.initRemoteComms({
relays: testRelays,
...testBackoffOptions,
});

// step 1: kernel A exports a revocable target and an unrelated bystander
const targetURL = await launchVatAndGetURL(
kernel1,
makeRemoteVatConfig('Target'),
);
const bystanderURL = await launchVatAndGetURL(
kernel1,
makeRemoteVatConfig('Bystander'),
);

// kernel B: a vat that can invoke remote objects via URL
await launchVatAndGetURL(kernel2, makeRemoteVatConfig('Sender'));
const senderRef = getVatRootRef(kernel2, kernelStore2, 'Sender');

// step 2: B can invoke the target before revocation
const beforeResult = await sendRemoteMessage(
kernel2,
senderRef,
targetURL,
'ping',
[],
);
expect(beforeResult).toBe('pong from Target');

// step 3: revoke the target on A through the public API
const targetKRef = getVatRootRef(kernel1, kernelStore1, 'Target');
kernel1.revoke(targetKRef);

// step 4: B's next invocation fails
await expect(
kernel2.queueMessage(senderRef, 'sendRemoteMessage', [
targetURL,
'ping',
[],
]),
).rejects.toThrow('OBJECT_REVOKED');

// step 5: the bystander and B's other references are unaffected
const bystanderResult = await sendRemoteMessage(
kernel2,
senderRef,
bystanderURL,
'ping',
[],
);
expect(bystanderResult).toBe('pong from Bystander');
},
NETWORK_TIMEOUT,
);
});

describe('Distributed Garbage Collection', () => {
it(
'creates remote endpoint with clist entries after cross-kernel message',
Expand Down
67 changes: 67 additions & 0 deletions packages/kernel-test/src/remote-comms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,73 @@ describe('Remote Communications (Integration Tests)', () => {
expect(response).toContain('says hello back to');
});

it('revokes a cross-kernel object held by a remote peer', async () => {
// step 1: kernel A exports a revocable target and an unrelated bystander
const { targetURL, bystanderURL } = (await runTestVats(kernel1, {
bootstrap: 'target',
forceReset: true,
services: ['ocapURLIssuerService'],
vats: {
target: {
bundleSpec: getBundleSpec('revocation-target-vat'),
parameters: { name: 'Target' },
},
},
})) as { targetURL: string; bystanderURL: string };

// Obtain the kernel-level KRef so we can later revoke it via the public API
const targetRootRef = kernelStore1.getRootObject('v1') as KRef;
const targetCapdata = await kernel1.queueMessage(
targetRootRef,
'getTarget',
[],
);
const targetKRef = targetCapdata.slots[0] as KRef;

// kernel B: set up a vat that can invoke objects via ocap URL redemption
const kernelStore2 = makeKernelStore(kernelDatabase2);
await runTestVats(kernel2, {
bootstrap: 'sender',
forceReset: true,
services: ['ocapURLRedemptionService'],
vats: {
sender: {
bundleSpec: getBundleSpec('remote-sender-vat'),
parameters: { name: 'Sender' },
},
},
});
const senderRootRef = kernelStore2.getRootObject('v1') as KRef;

// step 2: B can invoke the target before revocation
const beforeResult = await kernel2.queueMessage(
senderRootRef,
'sendMessage',
[targetURL, 'ping', []],
);
expect(kunser(beforeResult)).toContain('pong from Target target');

// step 3: revoke the target on A through the public API
kernel1.revoke(targetKRef);

// step 4: B's next invocation fails
await expect(
kernel2.queueMessage(senderRootRef, 'sendMessage', [
targetURL,
'ping',
[],
]),
).rejects.toThrow('OBJECT_REVOKED');

// step 5: the bystander object, and B's other references, are unaffected
const bystanderResult = await kernel2.queueMessage(
senderRootRef,
'sendMessage',
[bystanderURL, 'ping', []],
);
expect(kunser(bystanderResult)).toContain('pong from Target bystander');
});

it('remote relationships should survive kernel restart', async () => {
// This test needs file-based databases for persistence across stop/restart.
// Stop the beforeEach kernels and replace them with file-backed ones.
Expand Down
58 changes: 58 additions & 0 deletions packages/kernel-test/src/vats/revocation-target-vat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { E } from '@endo/eventual-send';
import { makeDefaultExo } from '@metamask/kernel-utils/exo';

import type { TestPowers } from '../test-powers.ts';

/**
* Build function for a vat that exports two objects — a revocable target and an
* unrelated bystander — so cross-kernel revocation tests can check both sides.
*
* @param vatPowers - Special powers granted to this vat.
* @param vatPowers.logger - The logger for the vat.
* @param parameters - Initialization parameters from the vat's config object.
* @param parameters.name - The name of the vat.
* @param _baggage - Root of vat's persistent state (not used here).
* @returns The root object for the new vat.
*/
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function buildRootObject(
{ logger }: TestPowers,
parameters: { name?: string } = {},
_baggage: unknown = null,
) {
const name = parameters?.name ?? 'RevocationTarget';
logger.log(`buildRootObject "${name}"`);

const target = makeDefaultExo('target', {
ping() {
return `pong from ${name} target`;
},
});

const bystander = makeDefaultExo('bystander', {
ping() {
return `pong from ${name} bystander`;
},
});

return makeDefaultExo('root', {
async bootstrap(
_vats: unknown,
services: { ocapURLIssuerService?: unknown },
) {
logger.log(`vat ${name} is bootstrap`);
const issuerService = services.ocapURLIssuerService;
const targetURL = await E(issuerService).issue(target);
const bystanderURL = await E(issuerService).issue(bystander);
return harden({ targetURL, bystanderURL });
},

getTarget() {
return target;
},

getBystander() {
return bystander;
},
});
}
Loading