diff --git a/packages/cosmic-swingset/lib/ag-solo/vats/lib-dehydrate.js b/packages/cosmic-swingset/lib/ag-solo/vats/lib-dehydrate.js new file mode 100644 index 00000000000..1d561e1b9dd --- /dev/null +++ b/packages/cosmic-swingset/lib/ag-solo/vats/lib-dehydrate.js @@ -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; + }, + }); +}; diff --git a/packages/cosmic-swingset/package.json b/packages/cosmic-swingset/package.json index 1e71ebed839..ee6ec070875 100644 --- a/packages/cosmic-swingset/package.json +++ b/packages/cosmic-swingset/package.json @@ -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", diff --git a/packages/cosmic-swingset/test/unitTests/test-lib-dehydrate.js b/packages/cosmic-swingset/test/unitTests/test-lib-dehydrate.js new file mode 100644 index 00000000000..c6d8abf08e0 --- /dev/null +++ b/packages/cosmic-swingset/test/unitTests/test-lib-dehydrate.js @@ -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(); + } +});