Skip to content

Commit

Permalink
feat: marshal based on user's petnames (#1092)
Browse files Browse the repository at this point in the history
* feat: add a petname system that hydrates and dehydrates based on petnames
  • Loading branch information
katelynsills committed May 13, 2020
1 parent b1607f9 commit 5e1945c
Show file tree
Hide file tree
Showing 3 changed files with 279 additions and 0 deletions.
87 changes: 87 additions & 0 deletions packages/cosmic-swingset/lib/ag-solo/vats/lib-dehydrate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import harden from '@agoric/harden';
import { makeMarshal } from '@agoric/marshal';
import makeStore from '@agoric/store';
import { assert, details } from '@agoric/assert';

// Marshalling for the UI should only use the user's petnames. We will
// call marshalling for the UI "dehydration" and "hydration" to distinguish it from
// other marshalling.
export const makeDehydrator = (initialUnnamedCount = 0) => {
let unnamedCount = initialUnnamedCount;

const petnameKindToMapping = makeStore();

const searchOrder = [];

const makeMapping = kind => {
assert.typeof(kind, 'string', details`kind ${kind} must be a string`);
const valToPetname = makeStore();
const petnameToVal = makeStore();
const addPetname = (petname, val) => {
assert(
!petnameToVal.has(petname),
details`petname ${petname} is already in use`,
);
assert(!valToPetname.has(val), details`val ${val} already has a petname`);
petnameToVal.init(petname, val);
valToPetname.init(val, petname);
};
const mapping = harden({
valToPetname,
petnameToVal,
addPetname,
kind,
});
petnameKindToMapping.init(kind, mapping);
return mapping;
};

const unnamedMapping = makeMapping('unnamed');

const addToUnnamed = val => {
unnamedCount += 1;
const placeholder = `unnamed-${unnamedCount}`;
const placeholderName = harden({
kind: 'unnamed',
petname: placeholder,
});
unnamedMapping.addPetname(placeholder, val);
return placeholderName;
};

// look through the petname stores in order and create a new
// unnamed record if not found.
const convertValToName = val => {
for (let i = 0; i < searchOrder.length; i += 1) {
const kind = searchOrder[i];
const { valToPetname } = petnameKindToMapping.get(kind);
if (valToPetname.has(val)) {
return harden({
kind,
petname: valToPetname.get(val),
});
}
}
// not found in any map
const placeholderName = addToUnnamed(val);
return placeholderName;
};

const convertNameToVal = ({ kind, petname }) => {
const { petnameToVal } = petnameKindToMapping.get(kind);
return petnameToVal.get(petname);
};
const { serialize: dehydrate, unserialize: hydrate } = makeMarshal(
convertValToName,
convertNameToVal,
);
return harden({
hydrate,
dehydrate,
makeMapping: kind => {
const mapping = makeMapping(kind);
searchOrder.push(kind);
return mapping;
},
});
};
1 change: 1 addition & 0 deletions packages/cosmic-swingset/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@agoric/evaluate": "^2.2.5",
"@agoric/eventual-send": "^0.9.1",
"@agoric/harden": "^0.0.8",
"@agoric/marshal": "^0.2.0",
"@agoric/nat": "2.0.1",
"@agoric/produce-promise": "^0.1.1",
"@agoric/registrar": "^0.1.1",
Expand Down
191 changes: 191 additions & 0 deletions packages/cosmic-swingset/test/unitTests/test-lib-dehydrate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { test } from 'tape-promise/tape';
import harden from '@agoric/harden';

import { makeDehydrator } from '../../lib/ag-solo/vats/lib-dehydrate';

test('makeDehydrator', async t => {
try {
const { hydrate, dehydrate, makeMapping } = makeDehydrator();

const instanceHandleMapping = makeMapping('instanceHandle');
const brandMapping = makeMapping('brand');

const handle1 = harden({});
const handle2 = harden({});
const handle3 = harden({});
instanceHandleMapping.addPetname('simpleExchange', handle1);
instanceHandleMapping.addPetname('atomicSwap', handle2);
instanceHandleMapping.addPetname('automaticRefund', handle3);
t.throws(
() => instanceHandleMapping.addPetname('simpleExchange2', handle1),
`cannot add a second petname for the same value`,
);
t.throws(
() => instanceHandleMapping.addPetname('simpleExchange', harden({})),
`cannot add another value for the same petname`,
);

const makeMockBrand = () =>
harden({
isMyIssuer: _allegedIssuer => {},
getAllegedName: () => {},
});

const brand1 = makeMockBrand();
const brand2 = makeMockBrand();
const brand3 = makeMockBrand();
brandMapping.addPetname('moola', brand1);
brandMapping.addPetname('simolean', brand2);
brandMapping.addPetname('zoeInvite', brand3);

t.deepEquals(
dehydrate(harden({ handle: handle1 })),
{
body: '{"handle":{"@qclass":"slot","index":0}}',
slots: [{ kind: 'instanceHandle', petname: 'simpleExchange' }],
},
`serialize val with petname`,
);
t.deepEquals(
hydrate(
harden({
body: '{"handle":{"@qclass":"slot","index":0}}',
slots: [{ kind: 'instanceHandle', petname: 'simpleExchange' }],
}),
),
harden({ handle: handle1 }),
`deserialize val with petname`,
);
t.deepEquals(
dehydrate(harden({ brand: brand1, extent: 40 })),
harden({
body: '{"brand":{"@qclass":"slot","index":0},"extent":40}',
slots: [{ kind: 'brand', petname: 'moola' }],
}),
`serialize brand with petname`,
);
t.deepEquals(
hydrate(
harden({
body: '{"brand":{"@qclass":"slot","index":0},"extent":40}',
slots: [{ kind: 'brand', petname: 'moola' }],
}),
),
harden({ brand: brand1, extent: 40 }),
`deserialize brand with petname`,
);
const proposal = harden({
want: {
Asset1: { brand: brand1, extent: 60 },
Asset2: { brand: brand3, extent: { instanceHandle: handle3 } },
},
give: {
Price: { brand: brand2, extent: 3 },
},
exit: {
afterDeadline: {
timer: {},
deadline: 55,
},
},
});
t.deepEquals(
dehydrate(proposal),
{
body:
'{"want":{"Asset1":{"brand":{"@qclass":"slot","index":0},"extent":60},"Asset2":{"brand":{"@qclass":"slot","index":1},"extent":{"instanceHandle":{"@qclass":"slot","index":2}}}},"give":{"Price":{"brand":{"@qclass":"slot","index":3},"extent":3}},"exit":{"afterDeadline":{"timer":{"@qclass":"slot","index":4},"deadline":55}}}',
slots: [
{ kind: 'brand', petname: 'moola' },
{ kind: 'brand', petname: 'zoeInvite' },
{ kind: 'instanceHandle', petname: 'automaticRefund' },
{ kind: 'brand', petname: 'simolean' },
{ kind: 'unnamed', petname: 'unnamed-1' },
],
},
`dehydrated proposal`,
);
t.deepEquals(
hydrate(
harden({
body:
'{"want":{"Asset1":{"brand":{"@qclass":"slot","index":0},"extent":60},"Asset2":{"brand":{"@qclass":"slot","index":1},"extent":{"instanceHandle":{"@qclass":"slot","index":2}}}},"give":{"Price":{"brand":{"@qclass":"slot","index":3},"extent":3}},"exit":{"afterDeadline":{"timer":{"@qclass":"slot","index":4},"deadline":55}}}',
slots: [
{ kind: 'brand', petname: 'moola' },
{ kind: 'brand', petname: 'zoeInvite' },
{ kind: 'instanceHandle', petname: 'automaticRefund' },
{ kind: 'brand', petname: 'simolean' },
{ kind: 'unnamed', petname: 'unnamed-1' },
],
}),
),
proposal,
`hydrated proposal`,
);
const handle4 = harden({});
t.deepEquals(
dehydrate(harden({ handle: handle4 })),
{
body: '{"handle":{"@qclass":"slot","index":0}}',
slots: [{ kind: 'unnamed', petname: 'unnamed-2' }],
},
`serialize val with no petname`,
);
t.deepEquals(
hydrate(
harden({
body: '{"handle":{"@qclass":"slot","index":0}}',
slots: [{ kind: 'unnamed', petname: 'unnamed-2' }],
}),
),
{ handle: handle4 },
`deserialize same val with no petname`,
);
// Name a previously unnamed handle
instanceHandleMapping.addPetname('autoswap', handle4);
t.deepEquals(
dehydrate(harden({ handle: handle4 })),
{
body: '{"handle":{"@qclass":"slot","index":0}}',
slots: [{ kind: 'instanceHandle', petname: 'autoswap' }],
},
`serialize val with new petname`,
);
t.deepEquals(
hydrate(
harden({
body: '{"handle":{"@qclass":"slot","index":0}}',
slots: [{ kind: 'instanceHandle', petname: 'autoswap' }],
}),
),
{ handle: handle4 },
`deserialize same val with new petname`,
);

// Test spoofing
t.notDeepEqual(
hydrate(
harden({
body: '{"handle":{"kind":"instanceHandle","petname":"autoswap"}}',
slots: [],
}),
),
{ handle: handle4 },
`deserialize with no slots does not produce the real object`,
);
t.deepEquals(
hydrate(
harden({
body: '{"handle":{"kind":"instanceHandle","petname":"autoswap"}}',
slots: [],
}),
),
{ handle: { kind: 'instanceHandle', petname: 'autoswap' } },
`deserialize with no slots does not produce the real object`,
);
} catch (e) {
t.isNot(e, e, 'unexpected exception');
} finally {
t.end();
}
});

0 comments on commit 5e1945c

Please sign in to comment.