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

feat!: use versioned keys for dynamically adding and remove commit stores #3320

Merged
merged 14 commits into from
Apr 17, 2024

Conversation

cmwaters
Copy link
Contributor

As was discovered with the apphash mismatch in #3167, by adding both v1 and v2 stores in the constructor we actually end up with a different app hash to v1.x as now there are a bunch of empy but initialized stores. What's needed is a mechanism that can start with only the v1 stores and then during the upgrade process add or remove the store for v2 as necessary.

The main problem with this is that because we are using a branch of state we can't actually modify the stores until we write the new state to disk. Hence we can no longer perform the migration in EndBlock but in Commit whereby we first write state, then create the new stores, then perform the module migrations (like call InitGenesis) and then write it again finally returning the app hash.

This PR depends on celestiaorg/cosmos-sdk#387 which introduces the upgrade hooks during Commit. This PR itself adds the two hooks for updating the stores and performing the migrations.

@cmwaters cmwaters changed the title feat: use versioned keys for dynamically adding and remove commit stores feat!: use versioned keys for dynamically adding and remove commit stores Apr 15, 2024
@cmwaters cmwaters self-assigned this Apr 15, 2024
@cmwaters cmwaters marked this pull request as ready for review April 16, 2024 08:51
@cmwaters cmwaters requested a review from a team as a code owner April 16, 2024 08:51
@cmwaters cmwaters requested review from rach-id and evan-forbes and removed request for a team April 16, 2024 08:51
Copy link
Contributor

coderabbitai bot commented Apr 16, 2024

Walkthrough

Walkthrough

The recent updates focus on enhancing key management across different versions within the application. This includes restructuring how keys are stored and managed in the App structure, introducing version-based key management, and updating module integration and migration functions to accommodate these changes. Additionally, modifications in module management improve how versions are retrieved and supported, streamlining the integration and upgrade processes.

Changes

File Path Change Summary
app/app.go - Restructured key management with keyVersions.
- Adjusted key declarations and store initialization for version handling.
- Updated migration functions for enhanced versioned key management.
app/module/... - Removed VersionMap.
- Added GetVersionMap and SupportedVersions methods for better version handling in modules.
app/modules.go - Added functionality for module setup and management.
- Introduced ModuleBasics and ModuleEncodingRegisters for basic elements and encoding configurations.
- Enhanced functions for module manager setup, module order definition, and store key handling for app versions.

Recent Review Details

Configuration used: .coderabbit.yaml

Commits Files that changed from the base of the PR and between 877864e and afa9097.
Files selected for processing (3)
  • app/app.go (17 hunks)
  • app/module/module.go (4 hunks)
  • app/modules.go (1 hunks)
Files skipped from review as they are similar to previous changes (2)
  • app/app.go
  • app/module/module.go
Additional comments not posted (7)
app/modules.go (7)

58-86: Introduced ModuleBasics to manage basic, non-dependent module elements like codec registration and genesis verification. This setup is crucial for ensuring that all modules are initialized correctly and can interact without dependencies.


88-90: ModuleEncodingRegisters is set up to keep track of module methods needed for registering interfaces and specific types to the encoding config. This is essential for serialization and deserialization processes across different modules.


93-193: Significant changes in setupModuleManager function to accommodate dynamic module management based on versioning from v1 to v2. This is a critical part of the upgrade process, ensuring that modules are transitioned smoothly between versions.


195-284: Updated setModuleOrder to define the execution order of modules during the blockchain's BeginBlock and EndBlock stages. Correct ordering is vital for maintaining state consistency and ensuring that interdependencies between modules are managed correctly.


286-300: Function allStoreKeys provides a consolidated list of all store keys used across the application. This centralization helps in managing accesses and ensuring that all necessary data stores are accounted for in the system.


302-344: Introduced versionedStoreKeys to map store keys to specific app versions. This function is crucial for supporting multiple versions of the application simultaneously, allowing for a smoother transition during upgrades.


347-373: assertAllKeysArePresent performs sanity checks to ensure that all necessary keys are present for each supported app version. This is an essential safety measure to prevent runtime errors due to missing configuration.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

Share
Tips

Chat

There are 3 ways to chat with CodeRabbit:

Note: Auto-reply has been disabled for this repository by the repository owner. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.

  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai generate interesting stats about this repository and render them as a table.
    • @coderabbitai show all the console.log statements in this repository.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (invoked as PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger a review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai help to get help.

Additionally, you can add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.

CodeRabbit Configration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Comment on lines +723 to +725
func (app *App) migrateModules(ctx sdk.Context, fromVersion, toVersion uint64) error {
return app.mm.RunMigrations(ctx, app.configurator, fromVersion, toVersion)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Ensure error handling in module migrations.

The migrateModules function is responsible for running module-specific migrations between versions. It's important to ensure robust error handling within this function to manage any issues that might arise during the migration process. Consider implementing comprehensive logging and error reporting mechanisms to aid in troubleshooting migration issues.

evan-forbes
evan-forbes previously approved these changes Apr 16, 2024
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Comment on lines 247 to 253
bApp.SetVersion(version.Version)
bApp.SetInterfaceRegistry(interfaceRegistry)

keys := sdk.NewKVStoreKeys(
authtypes.StoreKey, authzkeeper.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey,
minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey,
govtypes.StoreKey, paramstypes.StoreKey, upgradetypes.StoreKey, feegrant.StoreKey,
evidencetypes.StoreKey, capabilitytypes.StoreKey,
blobstreamtypes.StoreKey,
ibctransfertypes.StoreKey,
ibchost.StoreKey,
packetforwardtypes.StoreKey,
icahosttypes.StoreKey,
signaltypes.StoreKey,
)
keys := sdk.NewKVStoreKeys(allStoreKeys()...)
tkeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey)
memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey)

Copy link
Contributor

Choose a reason for hiding this comment

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

📝 NOTE
This review was outside the diff hunks and was mapped to the diff hunk with the greatest overlap. Original lines [250-260]

Ensure correct initialization of key maps in the New function.

The initialization of keys, tkeys, and memKeys using sdk.NewKVStoreKeys, sdk.NewTransientStoreKeys, and sdk.NewMemoryStoreKeys respectively, is critical. However, it's important to ensure that these functions are correctly populating the maps based on the current application version. This might require additional validation or error handling to ensure that the keys are correctly set up for the specified version.

app/app.go Outdated
panic(err)
}
app.SetAppVersion(ctx, newVersion)
app.SignalKeeper.ResetTally(ctx)
Copy link
Contributor

Choose a reason for hiding this comment

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

Correct formatting issue.

-      app.SignalKeeper.ResetTally(ctx)
+		app.SignalKeeper.ResetTally(ctx)

The indentation here does not follow Go's conventional tab-based indentation. Correcting this will improve the readability and maintain the standard coding style.


Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
app.SignalKeeper.ResetTally(ctx)
app.SignalKeeper.ResetTally(ctx)

@rootulp
Copy link
Collaborator

rootulp commented Apr 16, 2024

I think this needs a go work sync and/or updating the cosmos-sdk version in all go.mods because when I pull it locally, I'm observing:

$ make build
--> Updating go.mod
go: downloading github.com/celestiaorg/cosmos-sdk v1.21.0-sdk-v0.46.16
--> Updating go.mod in ./test/testground
go: conflicting replacements for github.com/cosmos/cosmos-sdk:
	github.com/celestiaorg/[email protected]
	github.com/celestiaorg/[email protected]
use "go work edit -replace github.com/cosmos/cosmos-sdk=[override]" to resolve
make: *** [build] Error 1

rootulp
rootulp previously approved these changes Apr 16, 2024
Copy link
Collaborator

@rootulp rootulp left a comment

Choose a reason for hiding this comment

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

awesome work!

app/app.go Outdated Show resolved Hide resolved
app/app.go Show resolved Hide resolved
@@ -231,7 +232,6 @@ func New(
logger log.Logger,
db dbm.DB,
traceStore io.Writer,
loadLatest bool,
Copy link
Collaborator

Choose a reason for hiding this comment

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

[question][not blocking] out of curiosity, what was this param previously used for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I couldn't find any use of it in the code i.e. this was always true.

My theory is that you might want to start at an earlier height if you wanted to query the state at that height

Copy link
Collaborator

Choose a reason for hiding this comment

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

sounds good, agreed we should remove it then

app/app.go Outdated
Comment on lines 697 to 722
func (app *App) migrateCommitStore(fromVersion, toVersion uint64) (baseapp.StoreMigrations, error) {
oldStoreKeys := app.keyVersions[fromVersion]
newStoreKeys := app.keyVersions[toVersion]
newMap := make(map[string]bool)
output := baseapp.StoreMigrations{
Added: make(map[string]*storetypes.KVStoreKey),
Deleted: make(map[string]*storetypes.KVStoreKey),
}
if toVersion == v2 {
// we need to set the app version in the param store for the first time
app.SetInitialAppVersionInConsensusParams(ctx, toVersion)
for _, storeKey := range newStoreKeys {
newMap[storeKey] = false
}
app.SetAppVersion(ctx, toVersion)
app.SignalKeeper.ResetTally(ctx)
return nil
for _, storeKey := range oldStoreKeys {
if _, ok := newMap[storeKey]; !ok {
output.Deleted[storeKey] = app.keys[storeKey]
} else {
// this module exists in both the old and new modules
newMap[storeKey] = true
}
}
for storeKey, existsInOldModule := range newMap {
if !existsInOldModule {
output.Added[storeKey] = app.keys[storeKey]
}
}
return output, nil
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

[optional] Here's a refactored implementation that I thought was easier to read. Defer to you if you want to include it:

func (app *App) MigrateCommitStore(oldStoreKeys []string, newStoreKeys []string) (baseapp.StoreMigrations, error) {
	result := baseapp.StoreMigrations{
		Added:   make(map[string]*storetypes.KVStoreKey),
		Deleted: make(map[string]*storetypes.KVStoreKey),
	}
	for _, oldKey := range oldStoreKeys {
		if !slices.Contains(newStoreKeys, oldKey) {
			result.Deleted[oldKey] = app.keys[oldKey]
		}
	}
	for _, newKey := range newStoreKeys {
		if !slices.Contains(oldStoreKeys, newKey) {
			result.Added[newKey] = app.keys[newKey]
		}
	}
	return result, nil
}

with unit tests

func Test_migrateCommitStore(t *testing.T) {
	logger := log.NewNopLogger()
	db := tmdb.NewMemDB()
	traceStore := &NoopWriter{}
	invCheckPeriod := uint(1)
	encodingConfig := encoding.MakeConfig(app.ModuleEncodingRegisters...)
	upgradeHeight := int64(0)
	appOptions := NoopAppOptions{}
	testApp := app.New(logger, db, traceStore, invCheckPeriod, encodingConfig, upgradeHeight, appOptions)

	type testCase struct {
		oldStoreKeys []string
		newStoreKeys []string
		want         baseapp.StoreMigrations
	}
	testCases := []testCase{
		{
			oldStoreKeys: []string{"key1", "key2"},
			newStoreKeys: []string{"key1", "key2"},
			want: baseapp.StoreMigrations{
				Added:   map[string]*storetypes.KVStoreKey{},
				Deleted: map[string]*storetypes.KVStoreKey{},
			},
		},
		{
			oldStoreKeys: []string{"key1"},
			newStoreKeys: []string{"key1", "key2"},
			want: baseapp.StoreMigrations{
				Added:   map[string]*storetypes.KVStoreKey{"key2": nil},
				Deleted: map[string]*storetypes.KVStoreKey{},
			},
		},
		{
			oldStoreKeys: []string{"key1", "key2"},
			newStoreKeys: []string{"key1"},
			want: baseapp.StoreMigrations{
				Added:   map[string]*storetypes.KVStoreKey{},
				Deleted: map[string]*storetypes.KVStoreKey{"key2": nil},
			},
		},
	}
	for _, tc := range testCases {
		got, err := testApp.MigrateCommitStore(tc.oldStoreKeys, tc.newStoreKeys)
		require.NoError(t, err)
		assert.Equal(t, tc.want, got)
	}
}

Note: you'll have to convert the function signature back to using fromVersion / toVersion

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks I definitely need to add the unit tests. It also reads easier and I don't think this is something that needs to be optimized so I will adopt your suggestion

app/app.go Show resolved Hide resolved
Comment on lines +804 to +805
// we need to know the app version to know what stores to mount
// thus the paramstore must always be a store that is mounted
Copy link
Collaborator

Choose a reason for hiding this comment

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

[not blocking][question] why is app version stored in the param store and consensus params? Is it stored anywhere else?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No app version is only part of the consensus params in the param store. We load it in memory upon startup for easy reference.

I think the SDK ended up splitting out the param module which will likely be an annoying change to adopt

app/app.go Outdated
Comment on lines 995 to 997
// versionedStoreKeys returns the store keys for each app version
// ... I wish there was an easier way than this (like using the modules which are already versioned)
func versionedStoreKeys() map[uint64][]string {
Copy link
Collaborator

Choose a reason for hiding this comment

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

[question] does this mean that to disable blobstream in v2, we have to do:

		{
			Module:      blobstream.NewAppModule(appCodec, app.BlobstreamKeeper),
			FromVersion: v1, ToVersion: v1,
		},

and

		2: {
			authtypes.StoreKey, authzkeeper.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey,
			minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey,
			govtypes.StoreKey, upgradetypes.StoreKey, feegrant.StoreKey,
			evidencetypes.StoreKey, capabilitytypes.StoreKey,
-			blobstreamtypes.StoreKey, ibctransfertypes.StoreKey, ibchost.StoreKey,
+			ibctransfertypes.StoreKey, ibchost.StoreKey,
			packetforwardtypes.StoreKey, icahosttypes.StoreKey, signaltypes.StoreKey,
		},

Copy link
Collaborator

Choose a reason for hiding this comment

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

[nit] it may be easier to read subsequent diffs if all the store keys were on new lines:

		1: {
			authtypes.StoreKey,
			authzkeeper.StoreKey,
			banktypes.StoreKey,
			blobstreamtypes.StoreKey,
			blobtypes.StoreKey,
			capabilitytypes.StoreKey,
			distrtypes.StoreKey,
			evidencetypes.StoreKey,
			feegrant.StoreKey,
			govtypes.StoreKey,
			ibchost.StoreKey,
			ibctransfertypes.StoreKey,
			minttypes.StoreKey,
			slashingtypes.StoreKey,
			stakingtypes.StoreKey,
			upgradetypes.StoreKey,
		},
		2: {
			authtypes.StoreKey,
			authzkeeper.StoreKey,
			banktypes.StoreKey,
			blobstreamtypes.StoreKey,
			capabilitytypes.StoreKey,
			distrtypes.StoreKey,
			evidencetypes.StoreKey,
			feegrant.StoreKey,
			govtypes.StoreKey,
			ibchost.StoreKey,
			ibctransfertypes.StoreKey,
			icahosttypes.StoreKey, // added in v2
			minttypes.StoreKey,
			packetforwardtypes.StoreKey, // added in v2
			signaltypes.StoreKey, // added in v2
			slashingtypes.StoreKey,
			stakingtypes.StoreKey,
			upgradetypes.StoreKey,
		},

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, I think that should be everything.

I wonder if we can even remove the directory and import "github.com/celestiaorg/celestia-app/x/blobstream"

I can list it out new lines

Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if we can even remove the directory and import "github.com/celestiaorg/celestia-app/x/blobstream"

Cool idea! I just checked and it looks like we may have to use the old name still b/c the last published release of celestia-app (v1.8.0) still calls it qgb: https://pkg.go.dev/github.com/celestiaorg/[email protected]/x/qgb

Co-authored-by: Rootul P <[email protected]>
@celestia-bot celestia-bot requested a review from a team April 17, 2024 08:42
@cmwaters
Copy link
Contributor Author

I've added a few sanity checks upon initialisation to make it more difficult to miss a component when adding modules

@cmwaters cmwaters requested a review from rootulp April 17, 2024 09:40
Copy link
Member

@evan-forbes evan-forbes left a comment

Choose a reason for hiding this comment

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

👍

}

// versionedStoreKeys returns the store keys for each app version
// ... I wish there was an easier way than this (like using the modules which are already versioned)
Copy link
Member

Choose a reason for hiding this comment

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

[optional]

maybe remove this comment or rephrase. "While this mapping is redundant, unfortunately this has to be done atm because..."

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I'll remove it in a follow up PR

Copy link
Collaborator

@rootulp rootulp left a comment

Choose a reason for hiding this comment

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

Still LGTM.

@@ -231,7 +232,6 @@ func New(
logger log.Logger,
db dbm.DB,
traceStore io.Writer,
loadLatest bool,
Copy link
Collaborator

Choose a reason for hiding this comment

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

sounds good, agreed we should remove it then

app/app.go Outdated
Comment on lines 995 to 997
// versionedStoreKeys returns the store keys for each app version
// ... I wish there was an easier way than this (like using the modules which are already versioned)
func versionedStoreKeys() map[uint64][]string {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if we can even remove the directory and import "github.com/celestiaorg/celestia-app/x/blobstream"

Cool idea! I just checked and it looks like we may have to use the old name still b/c the last published release of celestia-app (v1.8.0) still calls it qgb: https://pkg.go.dev/github.com/celestiaorg/[email protected]/x/qgb

@cmwaters cmwaters merged commit 9db2f8b into main Apr 17, 2024
33 checks passed
@cmwaters cmwaters deleted the cal/versioned-keys branch April 17, 2024 17:09
rootulp added a commit that referenced this pull request May 14, 2024
Closes #3392
Opens #3472

Fixes a few bugs:
1. Previously all modules had `ExportGenesis` invoked on them even if
they weren't supported by the current app version. Now we only call
`ExportGenesis` for the modules that are supported by the current app
version
2. The export command wasn't updated to account for the changes in
#3320 which force us to
mount stores after `app.New()` based on the current app version
3. The minfee module couldn't be exported b/c it didn't register a key
table in `ExportGenesis`

## Testing

I could export an app on app version 1 and 2. See
[output](https://gist.github.com/rootulp/dfea2b5b40f7366b03706fc39321ceee)
0xchainlover pushed a commit to celestia-org/celestia-app that referenced this pull request Aug 1, 2024
Closes celestiaorg/celestia-app#3392
Opens celestiaorg/celestia-app#3472

Fixes a few bugs:
1. Previously all modules had `ExportGenesis` invoked on them even if
they weren't supported by the current app version. Now we only call
`ExportGenesis` for the modules that are supported by the current app
version
2. The export command wasn't updated to account for the changes in
celestiaorg/celestia-app#3320 which force us to
mount stores after `app.New()` based on the current app version
3. The minfee module couldn't be exported b/c it didn't register a key
table in `ExportGenesis`

## Testing

I could export an app on app version 1 and 2. See
[output](https://gist.github.com/rootulp/dfea2b5b40f7366b03706fc39321ceee)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants