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: add mockVM for wasm clients testing #4804

Merged
merged 23 commits into from
Oct 4, 2023

Conversation

colin-axner
Copy link
Contributor

@colin-axner colin-axner commented Oct 2, 2023

Description

Initial work on bringing a mock VM. Likely should be merged after:

As this pr should likely handle any extra logic required after those changes

closes: #4798

Commit Message / Changelog Entry

type: commit message

see the guidelines for commit messages. (view raw markdown for examples)


Before we can merge this PR, please make sure that all the following items have been
checked off. If any of the checklist items are not applicable, please leave them but
write a little note why.

  • Targeted PR against correct branch (see CONTRIBUTING.md).
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
  • Code follows the module structure standards and Go style guide.
  • Wrote unit and integration tests.
  • Updated relevant documentation (docs/) or specification (x/<module>/spec/).
  • Added relevant godoc comments.
  • Provide a commit message to be used for the changelog entry in the PR description for review.
  • Re-reviewed Files changed in the Github PR explorer.
  • Review Codecov Report in the comment section below once CI passes.

Comment on lines 12 to 14
// MockWasmEngine implements types.WasmEngine for testing purpose. One or multiple messages can be stubbed.
// Without a stub function a panic is thrown.
type MockWasmEngine struct {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

maybe we can add this link in the code? Will be nice as an easy reference for making changes in the future.

@@ -281,6 +281,7 @@ func NewSimApp(
traceStore io.Writer,
loadLatest bool,
appOpts servertypes.AppOptions,
mockVM wasmtypes.WasmEngine,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

quick hack, didn't want to spend so much time on this

Copy link
Contributor

Choose a reason for hiding this comment

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

We could maybe just add an additional constructor NewSimAppWithVm (also no ideal and would be reworked later) but I'm fine with just adding this as is for now.

Copy link
Contributor Author

@colin-axner colin-axner Oct 4, 2023

Choose a reason for hiding this comment

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

Will leave as is for now, I figure once we get time to reduce the redundant simapps we will solve this issue, but also happy to see a followup with a nicer solution

return m.StoreCodeFn(codeID)
}

func (m *MockWasmEngine) Instantiate(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, initMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Some of these funcs are only expected to perform a single action (initialize the contract or not), so I think a simple override of functionality makes sense for those

return m.InstantiateFn(codeID, env, info, initMsg, store, goapi, querier, gasMeter, gasLimit, deserCost)
}

func (m *MockWasmEngine) Query(codeID wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) ([]byte, uint64, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Other funcs like query are expected to handle a subset of queries, so we may actually want to make the subset actions overrideable, not the entire query fn itself

I could remove the QueryFn field and replace with overrides for each subset action, then the QueryFn would use code magic to determine what the request is and then delegate to the appropriate response handler?

Copy link
Contributor

Choose a reason for hiding this comment

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

could we utilize amap[someKey]QueryFn to handle different message types?

Maybe expose a mockVm.OverrideQueryFn(key, func(){...})

Copy link
Member

Choose a reason for hiding this comment

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

Happy to leave as is for the initial run and see if we can make improvements later 👍

Comment on lines 20 to 23
// CreateClient creates an IBC client on the endpoint. It will update the
// clientID for the endpoint if the message is successfully executed.
// NOTE: a solo machine client will be created with an empty diversifier.
func (endpoint *WasmEndpoint) CreateClient() (err error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

took the quick route. I think the testing pkg will need to define either an interface for the Endpoint type or an interface that the endpoint needs to construct messages (such as a QueryProof interface fn). I think this will be easier to do reason about after we start the testing pkg improvement issues, so for now, I thought it was easier to duplicate the code a bit by implementing the fn's that require wasm client specific logic

Copy link
Contributor Author

Choose a reason for hiding this comment

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

opened #4807 and #4808 for the direction I think makes the most sense. For now, I think we should move forward with continuing to create this override type. Essentially the WasmEndpoint is a mock cometbft chain with a wasm client on it, and the endpoint represents that client and its connection/channel.

Once we implement interfaces for the light client module to plug into the endpoint, we could implement that interface and remove this type

Comment on lines +16 to +17
contractClientState = []byte{1}
contractConsensusState = []byte{2}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

can be changed, just wrote something down for them

clientState.LatestHeight = latestHeight
path.EndpointA.SetClientState(clientState)
suite.mockVM.QueryFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) ([]byte, uint64, error) {
resp := fmt.Sprintf(`{"status":"%s"}`, exported.Expired)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

can also do something like:

        statusResult := new(types.StatusResult)
        statusResult.Status = exported.Active
        resp, err := json.Marshal(&statusResult)
        suite.Require().NoError(err)

I just chose something

Copy link
Contributor

Choose a reason for hiding this comment

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

I actually thing the one liner you have is super readable. I think the other approach you suggested would be preferable for larger types.

That, or pass an fn that acts on the type and handle the marshalling in separately.


func (suite *TypesTestSuite) setupWasmWithMockVM() (ibctesting.TestingApp, map[string]json.RawMessage) {
suite.mockVM = &wasmtesting.MockWasmEngine{}
// TODO: move default functionality required for wasm client testing to the mock VM
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 think it'd be easier to address this in a followup when we better define how the mock VM will be used

Copy link
Contributor

@DimitrisJim DimitrisJim Oct 6, 2023

Choose a reason for hiding this comment

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

aye, I'll be adding some funcs here since we'd need to define ones for storing code and pinning (relevant to issue #4849). I'll open another issue to track this TODO. issue was already opened!

Copy link
Contributor

@crodriguezvega crodriguezvega left a comment

Choose a reason for hiding this comment

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

It's looking good. Thank you, @colin-axner!

I just left one minor comment for now. Looking forward to seeing the progress on this one!

modules/light-clients/08-wasm/testing/mock_engine.go Outdated Show resolved Hide resolved
Comment on lines 20 to 23
// CreateClient creates an IBC client on the endpoint. It will update the
// clientID for the endpoint if the message is successfully executed.
// NOTE: a solo machine client will be created with an empty diversifier.
func (endpoint *WasmEndpoint) CreateClient() (err error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

opened #4807 and #4808 for the direction I think makes the most sense. For now, I think we should move forward with continuing to create this override type. Essentially the WasmEndpoint is a mock cometbft chain with a wasm client on it, and the endpoint represents that client and its connection/channel.

Once we implement interfaces for the light client module to plug into the endpoint, we could implement that interface and remove this type

Comment on lines -47 to -49
if types.WasmVM != nil && !reflect.DeepEqual(types.WasmVM, vm) {
panic(errors.New("global Wasm VM instance should not be set to a different instance"))
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this check doesn't work with mock vm when you have other testing also uses the wasmvm, so I decided to remove it, especially since we are going to manage global assignment internally

Copy link
Contributor

Choose a reason for hiding this comment

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

TBH I like this quite a bit, even though we're still dealing with global variables, removing the scope at which we assign them can never be a bad thing.

Copy link
Contributor

@chatton chatton left a comment

Choose a reason for hiding this comment

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

Overall I really like the direction here. The function override pattern aligns well with the mock module we use in our other tests.

I left a few suggestions, but I think one of the bigger ones would be that maybe we actually can have default functionality baked into the mock vm, and replace it only if an override function is defined.

I'm happy with how it is now though, happy to make changes in a follow up.

Comment on lines -47 to -49
if types.WasmVM != nil && !reflect.DeepEqual(types.WasmVM, vm) {
panic(errors.New("global Wasm VM instance should not be set to a different instance"))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

TBH I like this quite a bit, even though we're still dealing with global variables, removing the scope at which we assign them can never be a bad thing.

Comment on lines 42 to 43
app := simapp.NewSimApp(log.NewNopLogger(), db, nil, true, simtestutil.EmptyAppOptions{}, nil)
return app, simapp.NewDefaultGenesisState(encCdc.Codec)
Copy link
Contributor

Choose a reason for hiding this comment

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

we'll run into some conflicts here with the sync to main. I guess we can hold off on this PR until after that is in.

Comment on lines 12 to 14
// MockWasmEngine implements types.WasmEngine for testing purpose. One or multiple messages can be stubbed.
// Without a stub function a panic is thrown.
type MockWasmEngine struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe we can add this link in the code? Will be nice as an easy reference for making changes in the future.

return m.InstantiateFn(codeID, env, info, initMsg, store, goapi, querier, gasMeter, gasLimit, deserCost)
}

func (m *MockWasmEngine) Query(codeID wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) ([]byte, uint64, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

could we utilize amap[someKey]QueryFn to handle different message types?

Maybe expose a mockVm.OverrideQueryFn(key, func(){...})

Comment on lines +67 to +69
if m.PinFn == nil {
panic("mock engine is not properly initialized")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder maybe some of these fns can have defualts, for Pin I think in all of our tests we'd be happy with a no-op.

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 think so, I decided to hold off for now until we run into the reason they need a default. I attempted to bring in the existing default functionality for initialize and status to the mock engine, but it requires exposing access to a cdc and other fields the suite has, so I decided it was better to handle that later when we have more of the testing pattern defined

modules/light-clients/08-wasm/testing/wasm_endpoint.go Outdated Show resolved Hide resolved
{
"client is frozen",
func() {
suite.mockVM.QueryFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) ([]byte, uint64, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

this is super nice, one thing that might be useful for readability is a function alias, but editors will auto fill this stuff for you so I don't feel super strongly about it.

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 don't feel strongly either, so I'll let it be a followup if we so chose

clientState.LatestHeight = latestHeight
path.EndpointA.SetClientState(clientState)
suite.mockVM.QueryFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) ([]byte, uint64, error) {
resp := fmt.Sprintf(`{"status":"%s"}`, exported.Expired)
Copy link
Contributor

Choose a reason for hiding this comment

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

I actually thing the one liner you have is super readable. I think the other approach you suggested would be preferable for larger types.

That, or pass an fn that acts on the type and handle the marshalling in separately.


// clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), path.EndpointA.ClientID)
// clientState = path.EndpointA.GetClientState().(*types.ClientState)
endpoint := &wasmtesting.WasmEndpoint{Endpoint: ibctesting.NewDefaultEndpoint(suite.chainA)}
Copy link
Contributor

Choose a reason for hiding this comment

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

worth wrapping this in a wasmtesting.NewDefaultEndpoint(suite.chainA)? which would call ibctesting.NewDefaultEndpoint(suite.chainA)

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 can add a wasmtesting.NewWasmEndpoint constructor to perform this

Comment on lines -197 to -200
func (chain *TestChain) SetWasm(wasm bool) *TestChain {
chain.UseWasmClient = wasm
return chain
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🥳

Copy link
Member

@damiannolan damiannolan left a comment

Choose a reason for hiding this comment

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

Super nice! Awesome job, thanks for getting this together. I feel happy enough about moving forward with tests which leverage this!

🚀 🚀 🚀

return m.InstantiateFn(codeID, env, info, initMsg, store, goapi, querier, gasMeter, gasLimit, deserCost)
}

func (m *MockWasmEngine) Query(codeID wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) ([]byte, uint64, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Happy to leave as is for the initial run and see if we can make improvements later 👍

Comment on lines +11 to +13
type WasmEndpoint struct {
*ibctesting.Endpoint
}
Copy link
Member

Choose a reason for hiding this comment

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

Big +

mega nit: should we add a basic godoc?

@colin-axner colin-axner merged commit 7032548 into feat/wasm-clients Oct 4, 2023
58 of 59 checks passed
@colin-axner colin-axner deleted the colin/4798-mock-wasmvm branch October 4, 2023 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants