-
Notifications
You must be signed in to change notification settings - Fork 206
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
feat(cosmic-swingset): Add chainStorage interface #5385
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are still questions to address, either here or in a followup.
const rootNode = makeChainStorageNode(ROOT_KEY); | ||
chainStorageP.resolve(rootNode); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR shares the root node directly, but it would also be possible to host it from a vat with access to bridgeManager
or a derived capability like toStorage
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We try not to host objects directly from the bootstrap vat. Could you add a packages/vats/src/vat-publisher.js
or somesuch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seconded
// TODO: Formalize root key. | ||
// Must not be any of {activityhash,beansOwing,egress,mailbox}, | ||
// and must be reserved in sites that use those keys (both Go and JS). | ||
const ROOT_KEY = 'published'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid the risk of future collision across our own codebase, I've opted to give vat-exposed chain storage its own single top-level key. Any dissenting opinions or suggestions for a different name than "published" would be worth hearing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A single top-level keys sounds excellent to me.
Does that make the RPC query path something like /swingset/published/$SUFFIX
?
I'm wondering if we should use the same name for the IAVL/RPC-query -side prefix (or post-/swingset/
path component), the vat which holds the publishing nodes, and the overall name of the feature. When someone comes grepping, it'd be nice if they could get from the RPC query path (or client-side code that's reading from it) to the agoric-sdk directories that serve up that data.
As a general feature name, "chain storage" suggests read-write storage to me (held in the chain's data structures), whereas swingset never reads from this data.
"publish" or "publish node" is a term we've used a lot. I don't want to cause a lot of churn, but I'm wondering it this should be makeChainPublisher
instead of makeChainStorage
. And then ROOT_KEY = 'publish'
, and the vat this lives in could be called vat-chain-publisher.js
.
Names are, of course, the most difficult thing in programming.
// 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}$/; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A key is a dot-separated list of segments (e.g., "published.foo.bar"). It seems necessary to disallow (unescaped) "." in a segment, and this PR imposes further restrictions:
- Each character of a segment must be an ASCII letter, number, or underscore.
- A segment may not be empty.
- A segment may not be longer than 100 characters (where the differences between character/code point/code unit/etc. can be ignored because of the first restriction).
It does not currently impose a maximum length on the full key, but could easily do so.
This leaves the door open for future extension, in particular to something like backslash escape sequences and/or a larger subset of Unicode (with the requisite care for control characters/homoglyphs/right-to-left subsequences/etc.).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm slightly in favor of using a slash as separator, rather than dot. What do other cosmos-sdk RPC query paths use? We should probably match their style.
I'm personally a fan of hyphen over underscore, for human-readable things (which don't need to follow e.g. JS identifier syntax), so I'd lobby to allow hyphens too. I'm quite happy with the other limits, but we should probably impose some sort of overall limit to prevent abuse/attack via multi-megabyte keys.
// Possible extensions: | ||
// * getValue() | ||
// * getChildNames() and/or getChildNodes() | ||
// * getName() | ||
// * recursive delete | ||
// * batch operations | ||
// * local buffering (with end-of-block commit) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Omitted for now, but de facto documented for posterity. Note that the interface for vats is currently write-only—this storage is being treated as precious, and we're intentionally discouraging database-like use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I guess that feeds into the naming question above, if we think there's a decent chance that we'd extend this API to include reads as well, then chain storage
is a good name for the feature. If it seems more likely that it will remain strictly write-only, then publish
or something other than storage
seems better.
bridgeManager: true, | ||
}, | ||
produce: { | ||
chainStorage: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This produces chainStorage, but there is not currently anything to consume it. The biggest open question is "how should a derived capability be made available to vats—what makes new child nodes, and where do they get sent?" Neither @warner nor I could find an existing analog, but perhaps the board comes closest?
Some possible answers:
- Zoe has access to the root node, and issues a child node of a common parent to contract instances that are somehow marked as privileged.
- Something in the bootstrap code has access to the root node, and issues descendant nodes as directed by manifest data (either as part of startInstance or after the fact).
- Other post-chain-initialization code has access to the root node, and issues descandant nodes as remoteables to already-running contract instances.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Summarising what I understood from the most recent conversation:
- We start by bootstrap having access to the root node, as you have now, on which we will publish (via this publication mechanism) a name-to-storage-path mapping.
- Governance "core eval" code will issue descendants to certain contracts via Zoe private arguments and update the name-to-storage path mapping.
- We will use explicit ocap introduction patterns to obtain descendants and descendants of descendants outside bootstrap.
- There's no pressing need for automatic allocation of descendants. Indeed, that's probably an antipattern until we're certain of performance characteristics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good!
I'd like to see this separated into its own vat instead of adding a lot more to chain-behaviors.js
. I realise I may be hypocritical here, but there are isolation benefits to having its own vat, too.
async delete() { | ||
assert(key !== ROOT_KEY); | ||
const childKeys = await toStorage({ key, method: 'keys' }); | ||
if (childKeys.length > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is racy. Is there a specific reason to refuse to delete nodes with children? If so, we should fix this in Go as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch! #5381 describes the root problem and anticipates a future fix in Go. In the meantime, I'll add a comment here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think #5394 is the right place to do this check, let's remove it from here to avoid having two different kinds of errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approach looks good, but let's settle on a feature name (since that has a shallow but broad effect on the API naming) and move it into a separate vat. And let's settle the dot-vs-slash path separator question.
// TODO: Formalize root key. | ||
// Must not be any of {activityhash,beansOwing,egress,mailbox}, | ||
// and must be reserved in sites that use those keys (both Go and JS). | ||
const ROOT_KEY = 'published'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A single top-level keys sounds excellent to me.
Does that make the RPC query path something like /swingset/published/$SUFFIX
?
I'm wondering if we should use the same name for the IAVL/RPC-query -side prefix (or post-/swingset/
path component), the vat which holds the publishing nodes, and the overall name of the feature. When someone comes grepping, it'd be nice if they could get from the RPC query path (or client-side code that's reading from it) to the agoric-sdk directories that serve up that data.
As a general feature name, "chain storage" suggests read-write storage to me (held in the chain's data structures), whereas swingset never reads from this data.
"publish" or "publish node" is a term we've used a lot. I don't want to cause a lot of churn, but I'm wondering it this should be makeChainPublisher
instead of makeChainStorage
. And then ROOT_KEY = 'publish'
, and the vat this lives in could be called vat-chain-publisher.js
.
Names are, of course, the most difficult thing in programming.
// 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}$/; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm slightly in favor of using a slash as separator, rather than dot. What do other cosmos-sdk RPC query paths use? We should probably match their style.
I'm personally a fan of hyphen over underscore, for human-readable things (which don't need to follow e.g. JS identifier syntax), so I'd lobby to allow hyphens too. I'm quite happy with the other limits, but we should probably impose some sort of overall limit to prevent abuse/attack via multi-megabyte keys.
async delete() { | ||
assert(key !== ROOT_KEY); | ||
const childKeys = await toStorage({ key, method: 'keys' }); | ||
if (childKeys.length > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think #5394 is the right place to do this check, let's remove it from here to avoid having two different kinds of errors
// Possible extensions: | ||
// * getValue() | ||
// * getChildNames() and/or getChildNodes() | ||
// * getName() | ||
// * recursive delete | ||
// * batch operations | ||
// * local buffering (with end-of-block commit) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I guess that feeds into the naming question above, if we think there's a decent chance that we'd extend this API to include reads as well, then chain storage
is a good name for the feature. If it seems more likely that it will remain strictly write-only, then publish
or something other than storage
seems better.
const rootNode = makeChainStorageNode(ROOT_KEY); | ||
chainStorageP.resolve(rootNode); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seconded
c171175
to
8cfd6f5
Compare
Chain storage is now moved into its own vat wrapping a unit-tested library. I have a slight preference for "chain storage" over "chain publisher", because adding read support in the future seems likely (and I have already confirmed read-after-write consistency locally). I have also added support for @warner Regarding the paths, I don't know about other cosmos-sdk, but swingset has an existing precedent for separating segments with Regarding size limits, I have omitted several:
Any one of them leaves the door open for abuse, as does even limiting them all but ignoring interactions (e.g., imagine being "constrained" to 10k descendant keys with up to 1 MB of value data apiece). So this PR effectively takes the position that a chain storage node is a powerful capability to be closely guarded and issued only to trustworthy entities (quite possibly with additional limitations), with the key segment length constraint present basically because it was trivial to add alongside character validity checks. Which (if any) of the others do you think should be added? |
I agree with you. The chain storage is fundamentally a store of the current state, and the events are just gossip to help inform clients when that state is updated. It doesn't look like Cosmos events are going to be in consensus anytime soon (and indeed, future versions of Tendermint are making them more performant but less reliable), but the IAVL tree to which we publish has good support for light client proofs, which is a feature I really think we need to lean into (even if there are a few options as to where on the trusting vs. interactive spectrum the client* is wrt. its RPC nodes).
|
]); | ||
/** @type { Map<string, any> } */ | ||
const nonStrings = new Map(); | ||
nonStrings.set('number', 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why the change from new Map(...contents...)
to using .set()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Appeasing TypeScript's complaint about the former: https://github.com/Agoric/agoric-sdk/runs/6563310401?check_suite_focus=true#step:4:260
error TS2769: No overload matches this call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sigh.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, but new Map(Object.entries(…))
works: 4afe517
agoric-sdk/packages/vats/test/test-lib-chainStorage.js
Lines 35 to 50 in 4afe517
const nonStrings = new Map( | |
Object.entries({ | |
number: 1, | |
bigint: 1n, | |
boolean: true, | |
null: null, | |
undefined, | |
symbol: Symbol('foo'), | |
array: ['foo'], | |
object: { | |
toString() { | |
return 'foo'; | |
}, | |
}, | |
}), | |
); |
}, | ||
}); | ||
const nonStrings = new Map( | ||
Object.entries({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cute. That's a keeper.
4afe517
to
3229474
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per discussion with @michaelfig
* @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} rootKey | ||
*/ | ||
export function makeChainStorageRoot(toStorage, rootKey) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename all use of "key" with "path".
getKey() { | ||
return key; | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getKey() { | |
return key; | |
}, | |
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: "swingset", storeSubkey: `swingset/data:${path}` } ; | |
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is that colon a third separator character, visible in the RPC queries? or is it only used internally?
@@ -16,6 +16,7 @@ import { | |||
import { importBundle } from '@endo/import-bundle'; | |||
|
|||
import * as Collect from '@agoric/run-protocol/src/collect.js'; | |||
import * as STORAGE_PATH from '@agoric/cosmic-swingset/src/chain-storage-paths.js'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm, if packages/cosmic-swingset has a dependencies
on packages/vats , chain-storage-paths.js
might need to move into packages/vats
to avoid a circular dependency
Ok, groovy.
Thanks.
I'm ok with whatever works, but it seems like a wart (and a security footgun) to have two different kinds of separators in the same string. I guess I don't really understand how the RPC query parser/router interacts with the IAVL tree. Maybe the IAVL "tree" is really a flat list of string keys, and the only structure to it comes from the semi-arbitrary partitioning of leaves into the various branches of the Merkle tree. If that's the case, IAVL doesn't care what separators you use, so the only semantics are imposed by the RPC query parser. I guess I imagined that the Go side would route
Yeah, I'm good with that for MN-1. Of course we must be more defensive shortly afterwards. It feels like these usage/capacity limits overlap a lot with metering fees (in a general strategy of "you can use as much as you're willing to pay for"), so we should probably find a way to unify this with e.g. paying for virtual-collection DB storage, or paying for RAM footprint over time. This might drive some discussion around one vat charging a different vat for service: the new+separate change-storage handler vat might get full control over the IAVL store (and not be charged anything by the kernel), but it might demand payment from the clients who are holding a Presence for one of its storage nodes. That said, short-term fixed limits to protect against abuse is good policy, even in MN-1. Anything which doesn't require a lot of design work. So yeah, let's land this as-is, but make a separate issue to consider imposing additional (easy) limits for MN-1. |
}, | ||
async delete() { | ||
assert(key !== rootKey); | ||
// A 'set' with no value deletes a key if it has no children, but |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fully agree with retaining an empty parent to avoid orphans, but it occurred to me this leaves us no good way to delete the parent later. I don't really want to spend a lot of time on deletion, it seems uncommon for now, but eventually maybe each time we manage to delete a node, we should check the parent and delete it if already empty and childless, and recurse upwards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The x/swingset
implementation already does that.
|
||
export function buildRootObject(_vatPowers) { | ||
function makeBridgedChainStorageRoot(bridgeManager, bridgeId, rootKey) { | ||
// XXX: Should we validate uniqueness of rootKey, or is that an external concern? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my hunch is that it can be an external concern, although I suppose the real question is how much damage/confusion would be caused by duplicate nodes that use the same rootKey
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, @michaelfig convinced me to be ok with the path names.
Approved, conditional on fixing the CI failure (which probably relates to the lint warnings that github is showing, and might require moving the file full of constants into a different package)
8e88f2f
to
69d8d4f
Compare
bridgeManager: bridgeManagerP, | ||
loadVat, | ||
// provisioning is here to attempt delaying execution for avoiding failures like | ||
// https://github.com/Agoric/agoric-sdk/runs/6728088019?check_suite_focus=true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
copy 1 or 2 of the most relevant lines, please? ci log URLs have a limited lifetime.
// https://github.com/Agoric/agoric-sdk/runs/6728088019?check_suite_focus=true | |
// Error#1: historical inaccuracy in replay of v2 [vatAdmin] | |
// https://github.com/Agoric/agoric-sdk/runs/6728088019?check_suite_focus=true |
c2df386
to
e718f25
Compare
Apparently the
Unfortunately the loadgen job doesn't currently have debug output enabled so this didn't show up in the stdout capture. I'm re-running it with it enabled. |
Here is a probably relevant snippet from the log output:
|
Thank you so much! That's what I was missing. |
62f24b8
to
f182c61
Compare
f182c61
to
f22b8f8
Compare
Looks like it's green now! I'll let you re-enable automerge (which I had disabled because the error needed addressing) |
Thanks! |
closes: #4558
Description
Adds a
chainStorage
interface that allows writing to the cosmos KVStore.Security Considerations
It is important to prevent vats from writing to each others' subtrees, and also to prevent excessive writes to this rather expensive storage.
Documentation Considerations
TBD
Testing Considerations
n/a