Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Preserve smart wallets through bulldozer upgrade #7599

Merged
merged 23 commits into from
May 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6b2f8f4
refactor(vstorage): Export empty non-terminal entries without a value
gibson042 May 4, 2023
3147039
style: Clean up smartWallet and provisionPool files
gibson042 May 4, 2023
160bf6c
fix: Preserve smart wallets through bulldozer upgrade
gibson042 May 4, 2023
1e6f1b6
chore: add PSM exportStorageSubtrees to bootstrap swingset config
dckc May 1, 2023
5a1e322
fix(vats): Be mindful of unsettled promises
gibson042 May 4, 2023
2454d3f
fix(vats): Extract revivable wallet addresses from the correct chain …
gibson042 May 4, 2023
2751e76
fix(internal): Make makeFakeStorageKit value auto-wrapping depend upo…
gibson042 May 5, 2023
6a69aab
feat(internal): makeFakeStorageKit supports "get" and "entries"
gibson042 May 5, 2023
a708b92
chore: Fix lint issues
gibson042 May 7, 2023
f8abb60
chore(vats): Improve typing
gibson042 May 8, 2023
69ec2e7
fix: Improve the smart wallet revival handshake
gibson042 May 8, 2023
4b7afc4
chore(vats): harden in the right place
gibson042 May 8, 2023
339292d
test: Update makeSwingsetTestKit etc. to support providing storage
gibson042 May 8, 2023
e3a38ad
test(vats): Add coverage for smart wallet survival through bulldozer …
gibson042 May 8, 2023
45249fa
test(vats): Update for ackWallet
gibson042 May 8, 2023
d6c70a2
chore: Fix bootstrap swingset config exportStorageSubtrees
gibson042 May 8, 2023
de7a92f
fix(vats): Store and read vstorage StreamCell values correctly
gibson042 May 10, 2023
433c1f1
fix: Process remotables in vstorage data migrated through bootstrap
gibson042 May 10, 2023
d6bb01d
test(vats): Use better variable names and assertion messages
gibson042 May 10, 2023
94081be
fix(vats): Revert breaking change to contract facets
gibson042 May 10, 2023
196616c
test: Update makeMockChainStorageRoot for proper StreamCell handling
gibson042 May 10, 2023
21809ba
fix(vats): Preserve receiver for unmarshalFromVstorage fromCapData ca…
gibson042 May 10, 2023
60c504f
chore(vats): Add logging for addRevivableAddresses
gibson042 May 10, 2023
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
6 changes: 5 additions & 1 deletion golang/cosmos/x/vstorage/vstorage.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,11 @@ func (sh vstorageHandler) Receive(cctx *vm.ControllerContext, str string) (ret s
entries := make([][]interface{}, len(children.Children))
for i, child := range children.Children {
entry := keeper.GetEntry(cctx.Context, fmt.Sprintf("%s.%s", path, child))
entries[i] = []interface{}{child, entry.Value()}
if !entry.HasData() {
gibson042 marked this conversation as resolved.
Show resolved Hide resolved
entries[i] = []interface{}{child}
} else {
entries[i] = []interface{}{child, entry.Value()}
}
}
bytes, err := json.Marshal(entries)
if err != nil {
Expand Down
8 changes: 4 additions & 4 deletions golang/cosmos/x/vstorage/vstorage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func TestGetAndHas(t *testing.T) {
t.Errorf("%s: got unexpected error %v", desc.label, err)
}

// Verify that has returns false iff get returns null.
// Verify that `has` returns false iff `get` returns null.
noData := desc.want == `null`
if (noData && has != `false`) || (!noData && has != `true`) {
t.Errorf("%s: got has %v; want %v", desc.label, has, !noData)
Expand Down Expand Up @@ -274,13 +274,13 @@ func TestEntries(t *testing.T) {
want string
}
cases := []testCase{
{path: "key1", want: `[["child1",null]]`},
{path: "key1", want: `[["child1"]]`},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change? does null act too much like an empty string at some FFI boundary?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was requested in #7489 (comment) , and I went with null at the time but reconsidered when I got to this PR because it was unnecessary overhead.

{path: "key1.child1",
// Empty non-terminals are included, empty leaves are not.
want: `[["empty-non-terminal",null],["grandchild1","value1grandchild"]]`},
want: `[["empty-non-terminal"],["grandchild1","value1grandchild"]]`},
{path: "key1.child1.grandchild1", want: `[]`},
{path: "key1.child1.empty-non-terminal", want: `[["leaf",""]]`},
{path: "key2", want: `[["child2",null]]`},
{path: "key2", want: `[["child2"]]`},
{path: "key2.child2",
want: `[["grandchild2","value2grandchild"],["grandchild2a","value2grandchilda"]]`},
{path: "nosuchkey", want: `[]`},
Expand Down
2 changes: 1 addition & 1 deletion packages/SwingSet/src/types-external.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export {};
* @property {string[]} [exportStorageSubtrees] chain storage paths identifying roots of subtrees
* for which data should be exported into bootstrap vat parameter `chainStorageEntries`
* (e.g., `exportStorageSubtrees: ['c.o']` might result in vatParameters including
* `chainStorageEntries: [ ['c.o', null], ['c.o.i', null], ['c.o.i.n', '42'], ['c.o.w', '"moo"'] ]`).
* `chainStorageEntries: [ ['c.o', '"top"'], ['c.o.i'], ['c.o.i.n', '42'], ['c.o.w', '"moo"'] ]`).
* @property {boolean} [includeDevDependencies] indicates that
* `devDependencies` of the surrounding `package.json` should be accessible to
* bundles.
Expand Down
1 change: 1 addition & 0 deletions packages/casting/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"author": "Agoric",
"license": "Apache-2.0",
"dependencies": {
"@agoric/internal": "^0.2.1",
"@agoric/notifier": "^0.5.1",
"@agoric/spawner": "^0.6.3",
"@agoric/store": "^0.8.3",
Expand Down
17 changes: 2 additions & 15 deletions packages/casting/src/follower-cosmjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { E, Far } from '@endo/far';
import * as tendermint34 from '@cosmjs/tendermint-rpc';
import * as stargateStar from '@cosmjs/stargate';

import { isStreamCell } from '@agoric/internal/src/lib-chainStorage.js';

import { MAKE_DEFAULT_DECODER, MAKE_DEFAULT_UNSERIALIZER } from './defaults.js';
import { makeCastingSpec } from './casting-spec.js';
import { makeLeader as defaultMakeLeader } from './leader-netconfig.js';
Expand All @@ -27,21 +29,6 @@ const textDecoder = new TextDecoder();
* to abstract away Tendermint versions.
*/

/**
* This is an imperfect heuristic to navigate the migration from value cells to
* stream cells.
* At time of writing, no legacy cells have the same shape as a stream cell,
* and we do not intend to create any more legacy value cells.
*
* @param {any} cell
*/
const isStreamCell = cell =>
cell &&
typeof cell === 'object' &&
Array.isArray(cell.values) &&
typeof cell.blockHeight === 'string' &&
/^0$|^[1-9][0-9]*$/.test(cell.blockHeight);

/**
* @param {Uint8Array} a
* @param {Uint8Array} b
Expand Down
11 changes: 7 additions & 4 deletions packages/cosmic-swingset/src/launch-chain.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export async function buildSwingset(
bootVat.parameters = { ...bootVat.parameters, coreProposalCode: code };
}

// Extract data from chain storage.
// Extract data from chain storage as [path, value?] pairs.
if (exportStorageSubtrees) {
// Disallow exporting internal details like bundle contents and the action queue.
const exportRoot = STORAGE_PATH.CUSTOM;
Expand All @@ -137,19 +137,22 @@ export async function buildSwingset(

const callChainStorage = (method, path) =>
bridgeOutbound(BRIDGE_ID.STORAGE, { method, args: [path] });
const makeExportEntry = (path, value) =>
value == null ? [path] : [path, value];

const chainStorageEntries = [];
// Preserve the ordering of each subtree via depth-first traversal.
let pendingEntries = exportStorageSubtrees.map(path => {
const value = callChainStorage('get', path);
return [path, value];
return makeExportEntry(path, value);
});
while (pendingEntries.length > 0) {
const entry = /** @type {[string, string]} */ (pendingEntries.shift());
const entry = /** @type {[string, string?]} */ (pendingEntries.shift());
chainStorageEntries.push(entry);
const [path, _value] = entry;
const childEntryData = callChainStorage('entries', path);
const childEntries = childEntryData.map(([pathSegment, value]) => {
return [`${path}.${pathSegment}`, value];
return makeExportEntry(`${path}.${pathSegment}`, value);
});
pendingEntries = [...childEntries, ...pendingEntries];
}
Expand Down
35 changes: 27 additions & 8 deletions packages/inter-protocol/src/proposals/startPSM.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js';
import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js';
import { E } from '@endo/far';
import { Stable } from '@agoric/vats/src/tokens.js';
import { makeHistoryReviver } from '@agoric/vats/tools/board-utils.js';
import {
makeHistoryReviver,
makeBoardRemote,
slotToBoardRemote,
} from '@agoric/vats/tools/board-utils.js';
import { deeplyFulfilledObject } from '@agoric/internal';
import { makeScalarBigMapStore } from '@agoric/vat-data';

Expand Down Expand Up @@ -39,20 +43,30 @@ const stablePsmKey = `published.psm.${Stable.symbol}`;
const findOldPSMState = (chainStorageEntries, keyword, brands) => {
// In this reviver, object references are revived as boardIDs
// from the pre-bulldozer board.
const toSlotReviver = makeHistoryReviver(chainStorageEntries);
const toSlotReviver = makeHistoryReviver(
chainStorageEntries,
slotToBoardRemote,
);
if (!toSlotReviver.has(`${stablePsmKey}.${keyword}.metrics`)) {
return {};
}
const metricsWithOldBoardIDs = toSlotReviver.getItem(
`${stablePsmKey}.${keyword}.metrics`,
);
const oldIDtoNewBrand = makeMap([
[metricsWithOldBoardIDs.feePoolBalance.brand, brands.minted],
[metricsWithOldBoardIDs.anchorPoolBalance.brand, brands.anchor],
[metricsWithOldBoardIDs.feePoolBalance.brand.getBoardId(), brands.minted],
[
metricsWithOldBoardIDs.anchorPoolBalance.brand.getBoardId(),
brands.anchor,
],
]);
// revive brands; other object references map to undefined
const brandReviver = makeHistoryReviver(chainStorageEntries, s =>
oldIDtoNewBrand.get(s),
// revive brands; other object references map to dummy remotables
const brandReviver = makeHistoryReviver(
chainStorageEntries,
(slotID, iface) => {
const newBrand = oldIDtoNewBrand.get(slotID);
return newBrand || makeBoardRemote({ boardId: slotID, iface });
},
);
return {
metrics: brandReviver.getItem(`${stablePsmKey}.${keyword}.metrics`),
Expand Down Expand Up @@ -340,7 +354,10 @@ export const makeAnchorAsset = async (

testFirstAnchorKit.resolve(kit);

const toSlotReviver = makeHistoryReviver(chainStorageEntries);
const toSlotReviver = makeHistoryReviver(
chainStorageEntries,
slotToBoardRemote,
);
const metricsKey = `${stablePsmKey}.${keyword}.metrics`;
if (toSlotReviver.has(metricsKey)) {
const metrics = toSlotReviver.getItem(metricsKey);
Expand All @@ -351,6 +368,8 @@ export const makeAnchorAsset = async (
// eslint-disable-next-line @jessie.js/no-nested-await
const anchorPaymentMap = await anchorBalancePayments;

// TODO: validate that `metrics.anchorPoolBalance.value` is
// pass-by-copy PureData (e.g., contains no remotables).
Comment on lines +371 to +372
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use Nat or M.nat(). PSM brands have to be fungible.

// eslint-disable-next-line @jessie.js/no-nested-await
const pmt = await E(mint).mintPayment(
AmountMath.make(brand, metrics.anchorPoolBalance.value),
Expand Down
71 changes: 71 additions & 0 deletions packages/internal/src/lib-chainStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ const { Fail } = assert;
* @property {string} [noDataValue]
*/

/**
* @template [T=unknown]
* @typedef StreamCell
* @property {string} blockHeight decimal representation of a natural number
* @property {T[]} values
*/

/**
* This represents a node in an IAVL tree.
*
Expand All @@ -44,6 +51,70 @@ const ChainStorageNodeI = M.interface('StorageNode', {
.returns(M.remotable('StorageNode')),
});

/**
* This is an imperfect heuristic to navigate the migration from value cells to
* stream cells.
* At time of writing, no legacy cells have the same shape as a stream cell,
* and we do not intend to create any more legacy value cells.
*
* @param {any} cell
* @returns {cell is StreamCell}
*/
export const isStreamCell = cell =>
cell &&
typeof cell === 'object' &&
Array.isArray(cell.values) &&
typeof cell.blockHeight === 'string' &&
/^0$|^[1-9][0-9]*$/.test(cell.blockHeight);
harden(isStreamCell);

// TODO: Consolidate with `insistCapData` functions from swingset-liveslots,
// swingset-xsnap-supervisor, etc.
/**
* @param {unknown} data
* @returns {asserts data is import('@endo/marshal').CapData<string>}
*/
export const assertCapData = data => {
assert.typeof(data, 'object');
assert(data);
assert.typeof(data.body, 'string');
assert(Array.isArray(data.slots));
// XXX check that the .slots array elements are actually strings
};
harden(assertCapData);

/**
* Read and unmarshal a value from a map representation of vstorage data
*
* @param {Map<string, string>} data
* @param {string} key
* @param {ReturnType<typeof import('@endo/marshal').makeMarshal>['fromCapData']} fromCapData
* @param {number} [index=-1] index of the desired value in a deserialized stream cell
*/
export const unmarshalFromVstorage = (data, key, fromCapData, index = -1) => {
const serialized = data.get(key) || Fail`no data for ${key}`;
assert.typeof(serialized, 'string');

const streamCell = JSON.parse(serialized);
if (!isStreamCell(streamCell)) {
throw Fail`not a StreamCell: ${streamCell}`;
}

const { values } = streamCell;
values.length > 0 || Fail`no StreamCell values: ${streamCell}`;

const marshalled = values.at(index);
assert.typeof(marshalled, 'string');

/** @type {import("@endo/marshal").CapData<string>} */
const capData = harden(JSON.parse(marshalled));
assertCapData(capData);

const unmarshalled = fromCapData(capData);
return unmarshalled;
};
harden(unmarshalFromVstorage);

/**
* @typedef {object} StoredFacet
* @property {() => Promise<string>} getPath the chain storage path at which the node was constructed
Expand Down
Loading