diff --git a/golang/cosmos/x/vstorage/keeper/keeper.go b/golang/cosmos/x/vstorage/keeper/keeper.go index d9858aa2dded..b9e7e3c546b4 100644 --- a/golang/cosmos/x/vstorage/keeper/keeper.go +++ b/golang/cosmos/x/vstorage/keeper/keeper.go @@ -2,6 +2,8 @@ package keeper import ( "bytes" + "encoding/json" + "strconv" "strings" sdk "github.com/cosmos/cosmos-sdk/types" @@ -11,6 +13,19 @@ import ( "github.com/Agoric/agoric-sdk/golang/cosmos/x/vstorage/types" ) +// StreamCell is an envelope representing a sequence of values written at a path in a single block. +// It is persisted to storage as a { "height": "", "values": ["...", ...] } JSON text +// that off-chain consumers rely upon. +type StreamCell struct { + Height string `json:"height"` + // XXX Should Values be []string or []interface{}? + // The latter would remove a layer of JSON encoding (e.g., `[{…}]` rather than `["{…}"]`, + // but would add a requirement exclusive to AppendStorageValueAndNotify that its input be JSON. + // On the other hand, we could always extend this format in the future to include an indication + // that values are subject to a different encoding, e.g. `"valueEncoding":"base64"`. + Values []string `json:"values"` +} + // Keeper maintains the link to data storage and exposes getter/setter methods // for the various parts of the state machine type Keeper struct { @@ -127,6 +142,31 @@ func (k Keeper) SetStorageAndNotify(ctx sdk.Context, path, value string) { ) } +func (k Keeper) AppendStorageValueAndNotify(ctx sdk.Context, path, value string) error { + height := strconv.FormatInt(ctx.BlockHeight(), 10) + + // Preserve correctly-formatted data within the current block, + // otherwise initialize a blank cell. + currentData := k.GetData(ctx, path) + var cell StreamCell + _ = json.Unmarshal([]byte(currentData), &cell) + if cell.Height != height { + cell.Height = height + cell.Values = make([]string, 0, 1) + } + + // Append the new value. + cell.Values = append(cell.Values, value) + + // Perform the write. + bz, err := json.Marshal(cell) + if err != nil { + return err + } + k.SetStorageAndNotify(ctx, path, string(bz)) + return nil +} + func componentsToPath(components []string) string { return strings.Join(components, types.PathSeparator) } diff --git a/golang/cosmos/x/vstorage/vstorage.go b/golang/cosmos/x/vstorage/vstorage.go index 84ea1fdb0e2b..afdfd6998fe0 100644 --- a/golang/cosmos/x/vstorage/vstorage.go +++ b/golang/cosmos/x/vstorage/vstorage.go @@ -61,7 +61,15 @@ func (sh vstorageHandler) Receive(cctx *vm.ControllerContext, str string) (ret s keeper.SetStorageAndNotify(cctx.Context, msg.Path, msg.Value) return "true", nil + case "append": + err = keeper.AppendStorageValueAndNotify(cctx.Context, msg.Path, msg.Value) + if err != nil { + return "", err + } + return "true", nil + case "get": + // Note that "get" does not (currently) unwrap a StreamCell. value := keeper.GetData(cctx.Context, msg.Path) if value == "" { return "null", nil diff --git a/packages/vats/src/lib-chainStorage.js b/packages/vats/src/lib-chainStorage.js index 4b3bc92f983d..12720d2d5281 100644 --- a/packages/vats/src/lib-chainStorage.js +++ b/packages/vats/src/lib-chainStorage.js @@ -38,11 +38,14 @@ harden(sanitizePathSegment); * @param {(message: StorageMessage) => any} handleStorageMessage a function for sending a storageMessage object to the storage implementation (cf. golang/cosmos/x/vstorage/vstorage.go) * @param {string} storeName currently limited to "swingset" * @param {string} rootPath + * @param {object} [rootOptions] + * @param {boolean} [rootOptions.sequence] employ a wrapping structure that preserves each value set within a single block, and default child nodes to do the same */ export function makeChainStorageRoot( handleStorageMessage, storeName, rootPath, + rootOptions = {}, ) { assert.equal( storeName, @@ -51,22 +54,28 @@ export function makeChainStorageRoot( ); assert.typeof(rootPath, 'string'); - function makeChainStorageNode(path) { + function makeChainStorageNode(path, options = {}) { + const { sequence = false } = options; const node = { getStoreKey() { return handleStorageMessage({ key: path, method: 'getStoreKey' }); }, - getChildNode(name) { + getChildNode(name, childNodeOptions = {}) { assert.typeof(name, 'string'); assert( pathSegmentPattern.test(name), X`Path segment names must consist of 1 to 100 characters limited to ASCII alphanumerics, underscores, and/or dashes: ${name}`, ); - return makeChainStorageNode(`${path}.${name}`); + const mergedOptions = { sequence, ...childNodeOptions }; + return makeChainStorageNode(`${path}.${name}`, mergedOptions); }, setValue(value) { assert.typeof(value, 'string'); - handleStorageMessage({ key: path, method: 'set', value }); + handleStorageMessage({ + key: path, + method: sequence ? 'append' : 'set', + value, + }); }, clearValue() { handleStorageMessage({ key: path, method: 'set' }); @@ -82,7 +91,7 @@ export function makeChainStorageRoot( return Far('chainStorageNode', node); } - const rootNode = makeChainStorageNode(rootPath); + const rootNode = makeChainStorageNode(rootPath, rootOptions); return rootNode; } /** @typedef {ReturnType} ChainStorageNode */ diff --git a/packages/vats/src/vat-chainStorage.js b/packages/vats/src/vat-chainStorage.js index 9e020cf0e716..9020eda9606d 100644 --- a/packages/vats/src/vat-chainStorage.js +++ b/packages/vats/src/vat-chainStorage.js @@ -7,12 +7,23 @@ export function buildRootObject(_vatPowers) { * @param {ERef} bridgeManager * @param {string} bridgeId * @param {string} rootPath must be unique (caller responsibility to ensure) + * @param {object} [options] */ - function makeBridgedChainStorageRoot(bridgeManager, bridgeId, rootPath) { + function makeBridgedChainStorageRoot( + bridgeManager, + bridgeId, + rootPath, + options, + ) { // 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); + const rootNode = makeChainStorageRoot( + toStorage, + 'swingset', + rootPath, + options, + ); return rootNode; }