-
Notifications
You must be signed in to change notification settings - Fork 292
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!: prototype for gatekeeping messages based on their version #3162
Conversation
…nto cal/migration
Co-authored-by: Rootul P <[email protected]>
// the server wrapper wraps the pbgrpc.Server for registering a service but | ||
// includes logic to extract all the sdk.Msg types that the service declares | ||
// in its methods and fires a callback to add them to the configurator. | ||
// This allows us to create a map of which messages are accepted across which | ||
// versions | ||
type serverWrapper struct { | ||
addMessages func(msgs []string) | ||
msgServer pbgrpc.Server | ||
} | ||
|
||
func (s *serverWrapper) RegisterService(sd *grpc.ServiceDesc, v interface{}) { | ||
msgs := make([]string, len(sd.Methods)) | ||
for idx, method := range sd.Methods { | ||
// we execute the handler to extract the message type | ||
_, _ = method.Handler(nil, context.Background(), func(i interface{}) error { | ||
msg, ok := i.(sdk.Msg) | ||
if !ok { | ||
panic(fmt.Errorf("unable to register service method %s/%s: %T does not implement sdk.Msg", sd.ServiceName, method.MethodName, i)) | ||
} | ||
msgs[idx] = sdk.MsgTypeURL(msg) | ||
return nil | ||
}, noopInterceptor) | ||
} | ||
s.addMessages(msgs) | ||
// call the underlying msg server to actually register the grpc server | ||
s.msgServer.RegisterService(sd, v) | ||
} | ||
|
||
func noopInterceptor(_ context.Context, _ interface{}, _ *grpc.UnaryServerInfo, _ grpc.UnaryHandler) (interface{}, error) { | ||
return nil, nil | ||
} |
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 where the fun happens (I basically insert a wrapper to read off the grpc service descriptor before passing it on to the underlying service
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.
Seems clever! How does this handle the case where the new version of a module removes a message type? I infer the message will no longer appear on the module service description methods and so it won't get registered in the map for a particular version. Is that right?
Wait on re-read it looks like a module defines a range of versions it supports so addMessages can't selectively remove a message for a module at a particular version.
Maybe I'm asking about a scenario that isn't possible. But if it is possible (and unlikely) I think we need a plan for it.
// one consensus version of the module to the next. Finally it maps all the messages | ||
// to the app versions that they are accepted in. This then gets used in the antehandler | ||
// to prevent users from submitting messages that can not yet be executed. |
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 had a concern and then realized it's not an issue but wanted to document it. I was originally concerned that we wouldn't be able to submit a crank message to try an upgrade because the v2 crank message would not be acceptable until the app version v2.
But we're using an upgrade height to upgrade from v1 -> v2. The crank message is introduced in app version v2 and will be used to upgrade from v2 -> v3 so it will pass the ante handler b/c all nodes will be on app version v2.
// the server wrapper wraps the pbgrpc.Server for registering a service but | ||
// includes logic to extract all the sdk.Msg types that the service declares | ||
// in its methods and fires a callback to add them to the configurator. | ||
// This allows us to create a map of which messages are accepted across which | ||
// versions | ||
type serverWrapper struct { | ||
addMessages func(msgs []string) | ||
msgServer pbgrpc.Server | ||
} | ||
|
||
func (s *serverWrapper) RegisterService(sd *grpc.ServiceDesc, v interface{}) { | ||
msgs := make([]string, len(sd.Methods)) | ||
for idx, method := range sd.Methods { | ||
// we execute the handler to extract the message type | ||
_, _ = method.Handler(nil, context.Background(), func(i interface{}) error { | ||
msg, ok := i.(sdk.Msg) | ||
if !ok { | ||
panic(fmt.Errorf("unable to register service method %s/%s: %T does not implement sdk.Msg", sd.ServiceName, method.MethodName, i)) | ||
} | ||
msgs[idx] = sdk.MsgTypeURL(msg) | ||
return nil | ||
}, noopInterceptor) | ||
} | ||
s.addMessages(msgs) | ||
// call the underlying msg server to actually register the grpc server | ||
s.msgServer.RegisterService(sd, v) | ||
} | ||
|
||
func noopInterceptor(_ context.Context, _ interface{}, _ *grpc.UnaryServerInfo, _ grpc.UnaryHandler) (interface{}, error) { | ||
return nil, nil | ||
} |
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 that's correct. Say we remove staking.MsgDelegate. It will be registered for v1 and not part of v2 |
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.
overall makes a lot of sense! still chewing on a few questions that I listed below
|
||
func (mgk MsgVersioningGateKeeper) IsAllowed(ctx context.Context, msgName string) (bool, error) { | ||
appVersion := sdk.UnwrapSDKContext(ctx).BlockHeader().Version.App | ||
acceptedMsgs, exists := mgk.acceptedMsgs[appVersion] |
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 this making the assumption that app version has been checked to be supported before hand? I think this is the case, and don't think its an issue, but just want to confirm
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.
Yes that assumption is made but should be caught in the block before if we are going to upgrade to a version that the node doesn't currently support
}, | ||
{ | ||
Module: gov.NewAppModule(appCodec, app.GovKeeper, app.AccountKeeper, app.BankKeeper), | ||
FromVersion: v1, ToVersion: v2, |
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 like the formatting change a lot
unrelated to this PR, does switching the ToVersion
here to 0 allow for any future version to work? if so, can we do that fix the testground issues (app version == to some large number to have large block sizes)?
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.
does switching the ToVersion here to 0 allow for any future version to work?
No everything currently must be explicit (and continuous). I think it's better if we have a different mechanism for testing the large block sizes - we could disconnect them from the appversion by simply having a mapping that is introduced in the app constructor
Blocked on celestiaorg/cosmos-sdk#376 |
WalkthroughWalkthroughThe update enhances the application with module migration tests, message versioning gatekeeper functionality, and significant changes to the Changes
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? TipsChatThere are 3 ways to chat with CodeRabbit:
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)
Additionally, you can add CodeRabbit Configration File (
|
func NewConfigurator(cdc codec.Codec, msgServer, queryServer pbgrpc.Server) VersionedConfigurator { | ||
return VersionedConfigurator{ | ||
cdc: cdc, | ||
msgServer: msgServer, | ||
queryServer: queryServer, | ||
migrations: map[string]map[uint64]module.MigrationHandler{}, | ||
acceptedMessages: map[uint64]map[string]struct{}{}, |
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 introduction of VersionedConfigurator
with added fields and methods for version handling and message registration is a crucial step towards improving the application's capability to manage message versions. It's recommended to ensure comprehensive testing and documentation for these new features to guarantee their effectiveness and usability.
Ensure comprehensive testing and documentation for the new version handling and message registration features introduced by VersionedConfigurator
.
msgVersioningGateKeeper *MsgVersioningGateKeeper, | ||
) sdk.AnteHandler { | ||
return sdk.ChainAnteDecorators( | ||
// Wraps the panic with the string format of the transaction | ||
NewHandlePanicDecorator(), | ||
// Prevents messages that don't belong to the correct app version | ||
// from being executed | ||
msgVersioningGateKeeper, |
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 integration of msgVersioningGateKeeper
into the NewAnteHandler
function is a logical step to enforce message version compatibility at the ante handler level. It's recommended to add tests to ensure the functionality works as expected and that the msgVersioningGateKeeper
is properly integrated into the ante handler chain.
Would you like assistance in creating test cases for the integration of msgVersioningGateKeeper
into the NewAnteHandler
function?
// safely be ported over without any migration | ||
storeKey storetypes.StoreKey | ||
|
||
// in memory copy of the upgrade height if any. This is local per node | ||
// and configured from the config. Used just for V2 | ||
upgradeHeight int64 | ||
|
||
// quorumVersion is the version that has received a quorum of validators | ||
// to signal for it. This variable is relevant just for the scope of the | ||
// lifetime of the block |
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.
📝 NOTE
This review was outside the diff hunks, and no overlapping diff hunk was found. Original lines [1-1]
Removing the upgradeHeight
field from the Keeper
struct and adjusting related methods simplifies the keeper's structure and reflects a more flexible approach to handling upgrades. Ensure that the new upgrade logic is thoroughly tested and documented.
Ensure comprehensive testing and documentation for the new upgrade logic introduced by the changes to the Keeper
struct.
msg: &banktypes.MsgSend{}, | ||
acceptMsg: false, | ||
version: 2, | ||
}, |
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.
[optional] do we want a third test case for an app version that isn't supported (i.e. 3
) to confirm that the ante handler returns ErrNotSupported
return err | ||
} | ||
if toVersion == v2 { | ||
// we need to set the app version in the param store for the first time |
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.
[not blocking] is there a diagram of all the places app version is persisted and plumbed through? I don't have a clear picture of where/why there were so many app version related issues in cosmos-sdk / celestia-app.
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 crux was that since we pass the sdk context everywhere, we should use that to get the app version. The app version should technically be present in the context because it includes the header that has the app version. The problem however was that in most cases when we create the context we only populate parts of the header (the height for example). So I needed to make several changes in PrepareProposal
, ProcessProposal
, InitGenesis
etc. that we actually populated the app version
if module.FromVersion == 0 { | ||
return nil, sdkerrors.ErrInvalidVersion.Wrapf("v0 is not a valid version for module %s", module.Module.Name()) | ||
} |
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.
[question] 0
doesn't seem like a valid ToVersion
either. Should that be a separate check?
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 have the check below that asserts that FromVersion cannot be greater than ToVersion which would catch this scenario
Co-authored-by: Rootul P <[email protected]>
app/module/configurator.go
Outdated
// VersionedConfigurator is a struct used at startup to register all the message and | ||
// query servers for all modules. It allows the module to register any migrations from | ||
// one consensus version of the module to the next. Finally it maps all the messages | ||
// to the app versions that they are accepted in. This then gets used in the antehandler | ||
// to prevent users from submitting messages that can not yet be executed. | ||
type VersionedConfigurator struct { | ||
fromVersion, toVersion uint64 | ||
cdc codec.Codec | ||
msgServer pbgrpc.Server | ||
queryServer pbgrpc.Server | ||
// acceptedMsgs is a map from appVersion -> msgTypeURL -> struct{}. | ||
acceptedMessages map[uint64]map[string]struct{} |
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 introduction of VersionedConfigurator
with added fields for version handling and message registration is a significant enhancement. It's crucial to ensure that these new features are well-documented and tested to guarantee their effectiveness and usability.
Ensure comprehensive testing and documentation for the new version handling and message registration features introduced by VersionedConfigurator
.
Co-authored-by: Rootul P <[email protected]>
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.
Still LGTM
…estiaorg#3162) Ref: celestiaorg#3134 This PR solves the problem of ensuring the messages belonging to modules not part of the current app version aren't executed. It does this in two ways: - Introducing an antehandler decorator to be predominantly used in CheckTx to immediately reject transactions giving users a useful error message (instead of just failing to execute) - Introduces a `CircuitBreaker` implementation to the `MsgServiceRouter` which prevents the execution of messages not belonging to the current app version. We need this because another module may call a message that is not current supported (think a governance proposal) I had several complications with the wiring of this given the structure of the SDK and tried a few different variants - this one I think being the better. It uses the configurator which is reponsible for registering services to read all the methods a modules grpc Server supports and extracting out the message names and mapping them to one or more versions that they are supported for. --------- Co-authored-by: Rootul P <[email protected]>
Ref: #3134
This PR solves the problem of ensuring the messages belonging to modules not part of the current app version aren't executed.
It does this in two ways:
CircuitBreaker
implementation to theMsgServiceRouter
which prevents the execution of messages not belonging to the current app version. We need this because another module may call a message that is not current supported (think a governance proposal)I had several complications with the wiring of this given the structure of the SDK and tried a few different variants - this one I think being the better.
It uses the configurator which is reponsible for registering services to read all the methods a modules grpc Server supports and extracting out the message names and mapping them to one or more versions that they are supported for.
Note: this is a prototype and is not yet complete but is open to feedback. In order to finish this I will need to write tests and more comprehensive documentation.
There is currently an unresolved problem in that prepare proposal's context does not have access to the app version