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

ADR-40: update on multi-store refactor and IBC proofs #10191

Merged
Merged
Changes from 6 commits
Commits
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
68 changes: 52 additions & 16 deletions docs/architecture/adr-040-storage-and-smt-state-commitments.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,13 @@ We need to be able to process transactions and roll-back state updates if a tran
We identified use-cases, where modules will need to save an object commitment without storing an object itself. Sometimes clients are receiving complex objects, and they have no way to prove a correctness of that object without knowing the storage layout. For those use cases it would be easier to commit to the object without storing it directly.


### Reduce MultiStore
### Refactor MultiStore

Stargate `/store` implementation (StoreV1) adds an additional layer in the SDK store construction - the `MultiStore` structure. The multistore exists to support the modularity of the Cosmos SDK - each module is using its own instance of IAVL, but in the current implementation, all instances share the same database.
The Stargate `/store` implementation (StoreV1) adds an additional layer in the SDK store construction - the `MultiStore` structure. The multistore exists to support the modularity of the Cosmos SDK - each module is using its own instance of IAVL, but in the current implementation, all instances share the same database.

The latter indicates, however, that the implementation doesn't provide true modularity. Instead it causes problems related to race condition and sync (see: [\#6370](https://github.com/cosmos/cosmos-sdk/issues/6370)).

We propose to reduce the multistore concept from the SDK, and to use a single instance of `SC` and `SS` in a `rootStore` object. To avoid confusion, we should rename the `MultiStore` interface to `RootStore`.
We propose to reduce the multistore concept from the SDK, and to use a single instance of `SC` and `SS` in a `RootStore` object. To avoid confusion, we should rename the `MultiStore` interface to `RootStore`.

Moreover, to improve usability, we should extend the `KVStore` interface with _prefix store_. This will allow module developers to bind a store to a namespace for module sub-components:

Expand All @@ -138,29 +138,65 @@ for each OP in [Get Has, Set, ...]
store.WithPrefix(prefix).OP(key) == store.OP(prefix + key)
```

The `RootStore` will have the following interface; the methods for configuring tracing and listeners are omitted for brevity.

The `RootStore` will have the following interface:
```go
// Used where read-only access to versions is needed.
type BasicRootStore interface {
Store
GetKVStore(StoreKey) KVStore
CacheRootStore() CacheRootStore
}

// Used as the main app state, replacing CommitMultiStore.
type CommitRootStore interface {
BasicRootStore
Committer
Snapshotter

GetVersion(uint64) (BasicRootStore, error)
SetInitialVersion(uint64) error

... // Trace and Listen methods
}

// Replaces CacheMultiStore for branched state.
type CacheRootStore interface {
BasicRootStore
Write()

... // Trace and Listen methods
}

// Example of constructor parameters for the concrete type.
type RootStoreConfig struct {
Upgrades *StoreUpgrades
InitialVersion uint64

ReservePrefix(StoreKey, StoreType)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

TODO: validate if we really need all these interfaces. It seams to me that the app only needs the CommitRootStore.

We let's add this TODO into the code and merge it - we can do that check in the "main" PR.

```
TODO
```
<!-- TODO: Review whether these types can be further reduced or simplified -->
<!-- TODO: RootStorePersistentCache type -->

In contrast to `MultiStore`, `RootStore` doesn't allow to mount dynamically sub stores. However it can inherit a multistore functionality in it's concrete implementation (which will be needed for IBC support).
In contrast to `MultiStore`, `RootStore` doesn't allow to dynamically mount sub-stores or provide an arbitrary backing DB.

#### Merkle Proofs and IBC
#### Compatibility support

To ease the transition to this new interface for users, we can create a shim which wraps a `CommitMultiStore` but provides a `CommitRootStore` interface, and expose functions to safely create and access the underlying `CommitMultiStore`.

Currently IBC (v1.0) module merkle proof for a `(key, value)` consists of two elements `[storeKey, proof]`. Verification using 2 passes:
1. Checks that a `proof` is a valid merkle proof `(key, value)` using ICS-23 spec.
2. Then it checks that the `storeKey` and `root(proof)` hashes to the AppHash (App state commitment).
The new `RootStore` and supporting types can be implemented in a `store/v2` package to avoid breaking existing code.

#### Merkle Proofs and IBC

Breaking this behavior would severely impact the Cosmos ecosystem which already widely adopts the IBC module. Unfortunately, the straightforward implementation is breaking because all keys in `SC` are hashed. So we can't simply make the last step. Even if we set the `storeKey` to an empty string it will rehash.
Currently, an IBC (v1.0) Merkle proof path consists of two elements (`["<store-key>", "<record-key>"]`), with each key corresponding to a separate proof. These are each verified according to individual [ICS-23 specs](https://github.com/cosmos/ibc-go/blob/f7051429e1cf833a6f65d51e6c3df1609290a549/modules/core/23-commitment/types/merkle.go#L17), and the result hash of each step is used as the committed value of the next step, until a root commitment hash is obtained.
roysc marked this conversation as resolved.
Show resolved Hide resolved
The root hash of the proof for `"<record-key>"` is hashed with the `"<store-key>"` to validate against the App Hash.

For workaround we need to:
+ keep the double hashing and multistore concept for IBC.
+ the `RootStore` will have only two stores: the general one, and another for IBC. `RootStore` should not expose mounting stores in a "runtime" (it's only possible to do it through constructor).
+ The App Hash is a hash of both stores in the `RootStore`.
This is not compatible with the `RootStore`, which stores all records in a single Merkle tree structure, and won't produce separate proofs for the store- and record-key. Ideally, the store-key component of the proof could just be omitted, and updated to use a "no-op" spec, so only the record-key is used. However, because the IBC verification code hardcodes the `"ibc"` prefix and applies it to the SDK proof as a separate element of the proof path, this isn't possible without a breaking change. Breaking this behavior would severely impact the Cosmos ecosystem which already widely adopts the IBC module, and updating it across the chains is a time consuming effort.

As a workaround, the `RootStore` will have to maintain two logically separate SMT-based stores: one for IBC state and one for everything else. A simple Merkle map containing these two store keys will act as a `MultiStore`-like index, and the final App hash will be the root hash of this index.

This workaround can be used until the IBC connection code is fully upgraded and supports single-element commitment proofs.
Copy link
Collaborator

Choose a reason for hiding this comment

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

how about keeping the paragraph: "the RootStore will have only two stores....".
Maybe we can be also more specific:

  • IBC store key is 01 (2 bits)
  • "general" store key is 1 (1 bit).
    Not sure though if we can only add as much as 1-2 bits though.

Copy link
Contributor Author

@roysc roysc Sep 20, 2021

Choose a reason for hiding this comment

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

Ah, I think I may have misunderstood Aditya's comment - I had thought ibc/clients, ibc/connections were the prefixes of separate stores. Now, if I've got it right, ibc is the only store prefix for IBC data, and the rest are just parts of the key paths. I'll update it.

I'm not sure I understand the prefixes you're proposing though. If you mean how the data is partitioned in the DB, that doesn't seem to belong at this level of design, also I don't think it's feasible to have prefixes smaller than a byte - it would shift the alignment of all the following data.

For IBC proofs, I believe the IBC data must be under the ibc store key, and the general data can just use the empty string.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, my understanding is that there is one store for IBC: the ibc/. Then they have a 3 groups of objects: clients, connections, channels.
So my proposal is that in the root store we have 2 stores:

  • IBC with 01 prefix (2 bits or 1 byte). AFAIU, IBC supports changing the prefix without doing x/ibc update -- it requires a migration only.
  • "general" store for everything else.

@AdityaSripal , @colin-axner - could you validate this?

Copy link
Member

Choose a reason for hiding this comment

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

IBC does not support changing the prefix unfortunately because the prefix is in the connection which is non-upgradable.

Can the separate IBC store retain the ibc/ prefix?

Copy link
Collaborator

Choose a reason for hiding this comment

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

yes, if we can't change it then we will keep it.


### Optimization: compress module keys

Expand Down