Skip to content

Commit

Permalink
feat(cosmic-swingset): Add chainStorage interface (#5385)
Browse files Browse the repository at this point in the history
* feat(cosmic-swingset): Add chainStorage interface

Fixes #4558

* refactor(cosmic-swingset): Move makeChainStorageNode into a vat

* chore(cosmic-swingset): Respond to PR feedback

* refactor(cosmic-swingset): Move chainStorage logic into a library

* chore: Improve child key checks and explanations

* test: Add tests for chainStorage

* chore: Allow "-" in chain storage key segments

* chore: Try to appease TypeScript

* chore: Try harder to appease TypeScript

* style: Use a better TypeScript-compatible map initialization

* refactor: Create Go and JS constants for top-level chain storage paths

* refactor: Replace chain storage "key" with "path"

* refactor: Move chain-storage-paths.js to avoid a module cycle

* chore: Appease eslint

* fix: Try adding a delay to fix CI

https://github.com/Agoric/agoric-sdk/runs/6728088019?check_suite_focus=true

* chore: Include relevant lines from failing CI log

* fix(swingset): louder anachrophobio

* ci(vats): correct `chainStorage` bundle in decentral configs

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Michael FIG <mfig@agoric.com>
  • Loading branch information
3 people committed Jun 4, 2022
1 parent c6db041 commit 109ff65
Show file tree
Hide file tree
Showing 13 changed files with 365 additions and 10 deletions.
20 changes: 15 additions & 5 deletions golang/cosmos/x/swingset/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ import (
"github.com/Agoric/agoric-sdk/golang/cosmos/x/swingset/types"
)

// Top-level paths for chain storage should remain synchronized with
// packages/vats/src/chain-storage-paths.js
const (
StoragePathActivityhash = "activityhash"
StoragePathBeansOwing = "beansOwing"
StoragePathEgress = "egress"
StoragePathMailbox = "mailbox"
StoragePathCustom = "published"
)

// Keeper maintains the link to data storage and exposes getter/setter methods for the various parts of the state machine
type Keeper struct {
storeKey sdk.StoreKey
Expand Down Expand Up @@ -140,7 +150,7 @@ func (k Keeper) GetBeansPerUnit(ctx sdk.Context) map[string]sdk.Uint {
}

func getBeansOwingPathForAddress(addr sdk.AccAddress) string {
return "beansOwing." + addr.String()
return StoragePathBeansOwing + "." + addr.String()
}

// GetBeansOwing returns the number of beans that the given address owes to
Expand Down Expand Up @@ -205,7 +215,7 @@ func (k Keeper) GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) s

// GetEgress gets the entire egress struct for a peer
func (k Keeper) GetEgress(ctx sdk.Context, addr sdk.AccAddress) types.Egress {
path := "egress." + addr.String()
path := StoragePathEgress + "." + addr.String()
value := k.GetStorage(ctx, path)
if value == "" {
return types.Egress{}
Expand All @@ -222,7 +232,7 @@ func (k Keeper) GetEgress(ctx sdk.Context, addr sdk.AccAddress) types.Egress {

// SetEgress sets the egress struct for a peer, and ensures its account exists
func (k Keeper) SetEgress(ctx sdk.Context, egress *types.Egress) error {
path := "egress." + egress.Peer.String()
path := StoragePathEgress + "." + egress.Peer.String()

bz, err := json.Marshal(egress)
if err != nil {
Expand Down Expand Up @@ -374,12 +384,12 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger {

// GetMailbox gets the entire mailbox struct for a peer
func (k Keeper) GetMailbox(ctx sdk.Context, peer string) string {
path := "mailbox." + peer
path := StoragePathMailbox + "." + peer
return k.GetStorage(ctx, path)
}

// SetMailbox sets the entire mailbox struct for a peer
func (k Keeper) SetMailbox(ctx sdk.Context, peer string, mailbox string) {
path := "mailbox." + peer
path := StoragePathMailbox + "." + peer
k.SetStorage(ctx, path, mailbox)
}
6 changes: 3 additions & 3 deletions packages/SwingSet/src/kernel/vat-loader/transcript.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import djson from '../../lib/djson.js';

export function requireIdentical(vatID, originalSyscall, newSyscall) {
if (djson.stringify(originalSyscall) !== djson.stringify(newSyscall)) {
console.log(`anachrophobia strikes vat ${vatID}`);
console.log(`expected:`, djson.stringify(originalSyscall));
console.log(`got :`, djson.stringify(newSyscall));
console.error(`anachrophobia strikes vat ${vatID}`);
console.error(`expected:`, djson.stringify(originalSyscall));
console.error(`got :`, djson.stringify(newSyscall));
return new Error(`historical inaccuracy in replay of ${vatID}`);
}
return undefined;
Expand Down
5 changes: 3 additions & 2 deletions packages/cosmic-swingset/src/chain-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { assert, details as X } from '@agoric/assert';
import { makeSlogSenderFromModule } from '@agoric/telemetry';

import * as STORAGE_PATH from '@agoric/vats/src/chain-storage-paths.js';
import stringify from './json-stable-stringify.js';
import { launch } from './launch-chain.js';
import makeBlockManager from './block-manager.js';
Expand Down Expand Up @@ -295,7 +296,7 @@ export default async function main(progname, args, { env, homedir, agcc }) {
// this object is used to store the mailbox state.
const mailboxStorage = makeChainStorage(
msg => chainSend(portNums.storage, msg),
'mailbox.',
`${STORAGE_PATH.MAILBOX}.`,
{
fromChainShape: data => {
const ack = toNumber(data.ack);
Expand All @@ -312,7 +313,7 @@ export default async function main(progname, args, { env, homedir, agcc }) {
function setActivityhash(activityhash) {
const msg = stringify({
method: 'set',
key: 'activityhash',
key: STORAGE_PATH.ACTIVITYHASH,
value: activityhash,
});
chainSend(portNums.storage, msg);
Expand Down
3 changes: 3 additions & 0 deletions packages/vats/decentral-core-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
"board": {
"sourceSpec": "@agoric/vats/src/vat-board.js"
},
"chainStorage": {
"sourceSpec": "@agoric/vats/src/vat-chainStorage.js"
},
"ibc": {
"sourceSpec": "@agoric/vats/src/vat-ibc.js"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/vats/decentral-demo-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"centralSupply": {
"sourceSpec": "@agoric/vats/src/centralSupply.js"
},
"chainStorage": {
"sourceSpec": "@agoric/vats/src/vat-chainStorage.js"
},
"mintHolder": {
"sourceSpec": "@agoric/vats/src/mintHolder.js"
},
Expand Down
1 change: 1 addition & 0 deletions packages/vats/src/bridge-ids.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
export const BANK = 'bank';
export const CORE = 'core';
export const DIBC = 'dibc';
export const STORAGE = 'storage';
export const PROVISION = 'provision';
export const WALLET = 'wallet';
11 changes: 11 additions & 0 deletions packages/vats/src/chain-storage-paths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* These identify top-level paths for SwingSet chain storage
* (and serve as prefixes, with the exception of ACTIVITYHASH).
* To avoid collisions, they should remain synchronized with
* golang/cosmos/x/swingset/keeper/keeper.go
*/
export const ACTIVITYHASH = 'activityhash';
export const BEANSOWING = 'beansOwing';
export const EGRESS = 'egress';
export const MAILBOX = 'mailbox';
export const CUSTOM = 'published';
28 changes: 28 additions & 0 deletions packages/vats/src/core/chain-behaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { importBundle } from '@endo/import-bundle';
import * as Collect from '@agoric/run-protocol/src/collect.js';
import { makeBridgeManager as makeBridgeManagerKit } from '../bridge.js';
import * as BRIDGE_ID from '../bridge-ids.js';
import * as STORAGE_PATH from '../chain-storage-paths.js';

import { callProperties, extractPowers } from './utils.js';

Expand Down Expand Up @@ -282,6 +283,33 @@ export const makeBridgeManager = async ({
};
harden(makeBridgeManager);

/**
* @param {BootstrapPowers & {
* consume: { loadVat: ERef<VatLoader<ChainStorageVat>> }
* }} powers
*/
export const makeChainStorage = async ({
consume: { bridgeManager: bridgeManagerP, loadVat },
produce: { chainStorage: chainStorageP },
}) => {
const bridgeManager = await bridgeManagerP;
if (!bridgeManager) {
console.warn('Cannot support chainStorage without an actual chain.');
chainStorageP.resolve(undefined);
return;
}

const ROOT_PATH = STORAGE_PATH.CUSTOM;

const vat = E(loadVat)('chainStorage');
const rootNodeP = E(vat).makeBridgedChainStorageRoot(
bridgeManager,
BRIDGE_ID.STORAGE,
ROOT_PATH,
);
chainStorageP.resolve(rootNodeP);
};

/**
* no free lunch on chain
*
Expand Down
9 changes: 9 additions & 0 deletions packages/vats/src/core/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ const SHARED_CHAIN_BOOTSTRAP_MANIFEST = harden({
},
home: { produce: { chainTimerService: 'timer' } },
},
makeChainStorage: {
consume: {
bridgeManager: true,
loadVat: true,
},
produce: {
chainStorage: true,
},
},
makeClientBanks: {
consume: {
bankManager: 'bank',
Expand Down
2 changes: 2 additions & 0 deletions packages/vats/src/core/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
* bldIssuerKit: RemoteIssuerKit,
* board: Board,
* bridgeManager: OptionalBridgeManager,
* chainStorage: unknown,
* chainTimerService: TimerService,
* client: ClientManager,
* clientCreator: ClientCreator,
Expand Down Expand Up @@ -235,6 +236,7 @@
* @typedef {{ mint: ERef<Mint>, issuer: ERef<Issuer>, brand: Brand }} RemoteIssuerKit
* @typedef {ReturnType<Awaited<BankVat>['makeBankManager']>} BankManager
* @typedef {ERef<ReturnType<import('../vat-bank.js').buildRootObject>>} BankVat
* @typedef {ERef<ReturnType<import('../vat-chainStorage.js').buildRootObject>>} ChainStorageVat
* @typedef {ERef<ReturnType<import('../vat-provisioning.js').buildRootObject>>} ProvisioningVat
* @typedef {ERef<ReturnType<import('../vat-mints.js').buildRootObject>>} MintsVat
* @typedef {ERef<ReturnType<import('../vat-priceAuthority.js').buildRootObject>>} PriceAuthorityVat
Expand Down
78 changes: 78 additions & 0 deletions packages/vats/src/lib-chainStorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// @ts-check

import { Far } from '@endo/far';

const { details: X } = assert;

// TODO: Formalize segment constraints.
// Must be nonempty and disallow (unescaped) `.`, and for simplicity
// (and future possibility of e.g. escaping) we currently limit to
// ASCII alphanumeric plus underscore.
const pathSegmentPattern = /^[a-zA-Z0-9_-]{1,100}$/;

/**
* Create a root storage node for a given backing function and root path.
*
* @param {(message: any) => any} toStorage a function for sending a storageMessage object to the storage implementation (cf. golang/cosmos/x/swingset/storage.go)
* @param {string} storeName currently limited to "swingset"
* @param {string} rootPath
*/
export function makeChainStorageRoot(toStorage, storeName, rootPath) {
assert.equal(
storeName,
'swingset',
'the only currently-supported store is "swingset"',
);
assert.typeof(rootPath, 'string');

function makeChainStorageNode(path) {
const node = {
getStoreKey() {
// This duplicates the Go code at
// https://github.com/Agoric/agoric-sdk/blob/cb272ae97a042ceefd3af93b1b4601ca49dfe3a7/golang/cosmos/x/swingset/keeper/keeper.go#L295
return { storeName, storeSubkey: `swingset/data:${path}` };
},
getChildNode(name) {
assert.typeof(name, 'string');
assert(
pathSegmentPattern.test(name),
X`Path segment must be a short ASCII identifier: ${name}`,
);
return makeChainStorageNode(`${path}.${name}`);
},
setValue(value) {
assert.typeof(value, 'string');
toStorage({ key: path, method: 'set', value });
},
async delete() {
assert(path !== rootPath);
// A 'set' with no value deletes a key if it has no children, but
// otherwise sets data to the empty string and leaves all nodes intact.
// We want to reject silently incomplete deletes (at least for now).
// This check is unfortunately racy (e.g., a vat could wake up
// and set data for a child before _this_ vat receives an
// already-enqueued response claiming no children), but we can tolerate
// that because transforming a deletion into a set-to-empty is
// effectively indistinguishable from a valid reordering where a fully
// successful 'delete' is followed by a child-key 'set' (for which
// absent parent keys are automatically created with empty-string data).
const childCount = await toStorage({ key: path, method: 'size' });
if (childCount > 0) {
assert.fail(X`Refusing to delete node with children: ${path}`);
}
toStorage({ key: path, method: 'set' });
},
// Possible extensions:
// * getValue()
// * getChildNames() and/or getChildNodes()
// * getName()
// * recursive delete
// * batch operations
// * local buffering (with end-of-block commit)
};
return Far('chainStorageNode', node);
}

const rootNode = makeChainStorageNode(rootPath);
return rootNode;
}
16 changes: 16 additions & 0 deletions packages/vats/src/vat-chainStorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { E, Far } from '@endo/far';
import { makeChainStorageRoot } from './lib-chainStorage.js';

export function buildRootObject(_vatPowers) {
function makeBridgedChainStorageRoot(bridgeManager, bridgeId, rootPath) {
// Note that the uniqueness of rootPath is not validated here,
// and is instead the responsibility of callers.
const toStorage = message => E(bridgeManager).toBridge(bridgeId, message);
const rootNode = makeChainStorageRoot(toStorage, 'swingset', rootPath);
return rootNode;
}

return Far('root', {
makeBridgedChainStorageRoot,
});
}

0 comments on commit 109ff65

Please sign in to comment.