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

IsFrozen() changed to Status() #140

Merged
merged 23 commits into from
May 4, 2021
Merged

IsFrozen() changed to Status() #140

merged 23 commits into from
May 4, 2021

Conversation

colin-axner
Copy link
Contributor

@colin-axner colin-axner commented Apr 26, 2021

Description

closes: #98


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.
  • Wrote unit and integration tests
  • Updated relevant documentation (docs/) or specification (x/<module>/spec/)
  • Added relevant godoc comments.
  • Added a relevant changelog entry to the Unreleased section in CHANGELOG.md
  • Re-reviewed Files changed in the Github PR explorer
  • Review Codecov Report in the comment section below once CI passes

@colin-axner colin-axner mentioned this pull request Apr 27, 2021
13 tasks
@colin-axner
Copy link
Contributor Author

Marking r4r, just need to add gRPC + gRPC test

@colin-axner colin-axner marked this pull request as ready for review April 27, 2021 17:17
@codecov-commenter
Copy link

codecov-commenter commented Apr 27, 2021

Codecov Report

Merging #140 (cca717d) into main (5b9e3c0) will increase coverage by 13.24%.
The diff coverage is 99.05%.

Impacted file tree graph

@@             Coverage Diff             @@
##             main     #140       +/-   ##
===========================================
+ Coverage   65.92%   79.17%   +13.24%     
===========================================
  Files         131      109       -22     
  Lines        8382     6526     -1856     
===========================================
- Hits         5526     5167      -359     
+ Misses       2476     1004     -1472     
+ Partials      380      355       -25     
Impacted Files Coverage Δ
modules/core/keeper/grpc_query.go 0.00% <0.00%> (ø)
...clients/07-tendermint/types/misbehaviour_handle.go 90.69% <ø> (-0.61%) ⬇️
modules/core/02-client/keeper/client.go 98.31% <100.00%> (+0.04%) ⬆️
modules/core/02-client/keeper/grpc_query.go 69.42% <100.00%> (+7.72%) ⬆️
modules/core/02-client/keeper/keeper.go 84.75% <100.00%> (+1.21%) ⬆️
modules/core/03-connection/keeper/verify.go 100.00% <100.00%> (ø)
modules/core/04-channel/keeper/packet.go 93.04% <100.00%> (+0.02%) ⬆️
...light-clients/06-solomachine/types/client_state.go 66.83% <100.00%> (+0.50%) ⬆️
.../light-clients/07-tendermint/types/client_state.go 70.83% <100.00%> (+0.83%) ⬆️
...ght-clients/07-tendermint/types/proposal_handle.go 78.18% <100.00%> (ø)
... and 23 more

Copy link
Contributor

@fedekunze fedekunze left a comment

Choose a reason for hiding this comment

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

changes LGTM, and thanks for the cleanup! I left some minor comments. Also, can you add a changelog entry?

// Expired is a potential status of a client. A client is expired
// if its latest consensus state timestamp added to its trusting period
// is before or equal to the current time.
const Expired = "Expired"
Copy link
Contributor

Choose a reason for hiding this comment

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

nit, this should be defined in exported too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My reasoning is that this is a tendermint specific concept. Is expiry common amongst light clients? cc @AdityaSripal @cwgoes

Copy link
Contributor

Choose a reason for hiding this comment

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

Expiry will be common to light clients other than Tendermint, yes. Anything for proof-of-stake should have this.

Comment on lines 25 to 32
// Active is a status type of a client. An active client is allowed to be used.
Active string = "Active"

// Frozen is a status type of a client. A frozen client is not allowed to be used.
Frozen string = "Frozen"

// Unknown indicates there was an error in determining the status of a client
Unknown string = "Unknown"
Copy link
Contributor

Choose a reason for hiding this comment

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

should these be proto enums?

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? They aren't used in communication. So I think it is probably simplest to leave them as strings. (only use proto when necessary)

modules/light-clients/06-solomachine/types/client_state.go Outdated Show resolved Hide resolved
modules/light-clients/07-tendermint/types/client_state.go Outdated Show resolved Hide resolved
Comment on lines +31 to +36
{"req is nil",
func() {
req = nil
},
false,
},
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 test case panics using the old old testing setup. Another reason to use the new one 🙂

Copy link
Member

@AdityaSripal AdityaSripal left a comment

Choose a reason for hiding this comment

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

Good work!! I think Status should be a go enum, and it would be nice to have a RefreshStatus function with the arguments clientstore, cdc, etc.

And then just a getter function Status that returns the latest status set by RefreshStatus

Comment on lines +58 to +59
consState, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.GetClientConsensusState(suite.chainA.GetContext(), path.EndpointA.ClientID, trustedHeight)
suite.Require().True(found)
Copy link
Member

Choose a reason for hiding this comment

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

Why don't you have this check in future function?

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 added this because the consensus state is required to know the correct timestamp. The future function doesn't need to pass in a timestamp and thus the consensus state isn't needed. Checking for the existence of the consensus state would break a test case below, I'm fairly sure - "consensus state not found"

Comment on lines 26 to 32
Active string = "Active"

// Frozen is a status type of a client. A frozen client is not allowed to be used.
Frozen string = "Frozen"

// Unknown indicates there was an error in determining the status of a client
Unknown string = "Unknown"
Copy link
Member

Choose a reason for hiding this comment

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

These should all be golang enums at the very least. Otherwise there is no obvious relationship between them, and the type of Status is string rather than exported.Status

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 you elaborate on your concern of using strings? My primary concern of using enums is modularity and value registration

Enums are useful when you know the full range of constants (connection/channel state is a good example) since it provides uniqueness. We cannot know all types of status a client may have. The problem is that every light client developer would need to register their own enums in order to avoid registration conflict. Registering status types seems unnecessarily complicated without a concrete benefit. I think the performance impact of using constants should be pretty low (if any impact).

It would also make maintaining backwards compatibility difficult since we would either need to pre-reserve a selected amount of enums (otherwise we could never make a backwards compatible addition)

I tried using a middle ground solution of using a typed constant

type Status string

This changes the function signature to use exported.Status instead of string, but it makes using the gRPC clunky since we cannot use exported.Status as a return type in the gRPC route. Thus we need to cast to/from the exported.Status when returning. Relayers and external third parties would rely on string (unless they decided to cast) whereas the code would rely on its typed string.

I think my preference is continuing to use string. Client types are strings and we use those in identifiers. I don't see the benefit in increasing complexity and potentially creating a divide between what type is used in core IBC vs outside core IBC.

Thoughts?

Copy link
Member

@AdityaSripal AdityaSripal Apr 29, 2021

Choose a reason for hiding this comment

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

Thinking more on this, I guess I'm ok with strings.

My concern is there isn't a very clear global view on what a Status means, beyond just Active or some other value.

At the code level this is really just acting as a boolean.

I would prefer just an enum: Active or Inactive. And either log the specific issue for inactiveness, or allow it as a specific query on the client state.

But in the absence of that, strings are fine

Copy link
Contributor Author

@colin-axner colin-axner Apr 29, 2021

Choose a reason for hiding this comment

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

I agree that "Active" is arbitrary and is likely something developers are capable of missing. Same with "Frozen"

The primary benefit for Active to be used not to be used as a boolean is trivial support for a gRPC route, as stated in the linked issue:

The benefit of using Status() as opposed to IsActive() is trivial support for a Status gRPC route. If the client state interface only supported IsActive then we would likely need to add a Status gRPC route for every light client as opposed to defining it once in 02-client.

What about this as the function signature:

Status(ctx, clientStore, cdc) exported.Status, string

Where exported.Status is an enum of either Active, Frozen, Inactive and string is any additional information such as "Expired"?

Core IBC only uses the string in error messages and for gRPC support

Copy link
Member

Choose a reason for hiding this comment

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

I like this idea! Though I think we just need Active, Inactive no? Frozen is a particular type of inactive

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 like this idea! Though I think we just need Active, Inactive no? Frozen is a particular type of inactive

I believe #141 requires knowing if the client is Frozen? That's the only use case that comes to mind. No strong preference on this decision. Happy to do whichever you think is best

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Going with Active, Frozen, Inactive since misbehaviour/freezing is already a concept in core IBC, it makes sense to have this status type

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 decided to use a typed string. Initially I implemented a typed int32 and the function signature above (hence the force push), but I realized separating the status and info felt unnecessary. The main critique in this thread is separating a string from a status and a typed string accomplishes that.

It also is self descriptive. The value 1 indicates Frozen a lot less than the string "Frozen". Furthermore, backwards compatibility will be easier since the next value added wouldn't have to be 3.

Since we are using a typed string, I kept Active, Frozen, Expired, Unknown as the default supported status types. Core IBC should only ever look for Active and potentially Frozen in exceptional cases.

I tried using proto enums but proto enums will just create a typed int32 and requires the active status to be suffixed with _UNSPECIFIED per proto linting. In the gRPC response, since I couldn't use a typed Status, I went with string as I figured relayers would prefer to compare against a self descriptive string rather than an arbitrary number.

I think there are also less risks for errors, since core IBC defensively uses the status (must be Active to do anything). If I used a typed int32, a typed status return value of 0 would always be interpreted as Active by core IBC. For example

var inactive exported.Status = 0
return inactive

With a typed string, returning a typed status of "Active" is self descriptive. You wouldn't use that return value to indicate the client is inactive.

This last part is a minor nit, but a typed int32 just felt like unnecessary indirection.

testing/endpoint.go Show resolved Hide resolved
@colin-axner
Copy link
Contributor Author

colin-axner commented Apr 29, 2021

it would be nice to have a RefreshStatus function with the arguments clientstore, cdc, etc.

And then just a getter function Status that returns the latest status set by RefreshStatus

I don't understand the usefulness of this. If you don't call RefreshStatus you run the risk of querying a stale status. Clients would then need to keep internal logic as to what height (and block time, or other client specific logic) they last refreshed their status. It seems simplest to always return the current status

In the tendermint case, you can almost never rely on a non-refreshed status as time is always incrementing (except for during the execution of a block)

I'm going to leave the API as is. I think allowing stale status' runs a higher risk of bugs as it would be problematic if we relied on a non-refreshed status. If the block execution always relies on refreshing the status, then we gain no benefit from separating the logic and increasing the overhead on light client implementations.

@AdityaSripal
Copy link
Member

My concern is if I want to make a Status check, I need to have ctx, clientStore, cdc. If i don't have them (let's say im in a stateless helper function), then I need to add those arguments to my helper function. Or i need to make the status check earlier even if it doesn't make the most sense from a code-organization POV.

My proposal was that you call RefreshStatus once at the start of each client handler. And then all subsequent logic can just use Status and get the latest status. I think that's a bit cleaner, but I don't have a strong preference so I leave it to your decision

@colin-axner
Copy link
Contributor Author

I agree Status is a hefty function. Here's my current reasoning.

The primary motivation for this change was based on 2 issues

  • developer demand for a standard way of determining the client status
  • preventing verification/packet sends on clients which are not active

There are a few constraints:

  • requiring clientstore, cdc, and ctx to determine status
  • cannot add fields to ClientState without breaking IBC
  • gRPC callers need to set context block time

There's no way around the first constraint, thus the question is whether we should cache the status

Trying to cache the status in the client state causes us to deal with backwards compatibility. I believe it isn't possible to do a backwards compatible fix without introducing client versions. Client versions are useful regardless, but that fix is out of scope of this issue. Furthermore, a modification to the client state proto file should really require an ICS spec change

gRPC callers need to set the current time in the context. This might be difficult for non-go implementations. I'm not sure if it is possible for relayers like ibc-rs to set sdk context specific fields. There's no way around this, unless we set the context time to time.Now in the gRPC call. I'm not sure of the ramifications of this.

Lets say we wanted to cache the status. We know that because the context is passed in, we can probably assume each status is tied to a block height (of the current chain, note this block height is not the same as the consensus state block height as they represent different chains). Thus we could create a mapping from block height -> status. This doesn't solve the gRPC constraint since queries at a height which didn't have a status set would need to depend on the context block time which may or may not be set. If a status isn't set for a block height, it is not safe to assume it has the same status as the next/prev block height which has a status set.

Using the client store mapping, a helper function would still need access to the client store and ctx (and potentially cdc)

Based on the above reasoning, without any specific use case in mind for caching the status, I'd prefer to do the simplest solution possible. Otherwise we increase the amount of things all light clients would need to do or break IBC. I think the heftiness of Status may be revisited so I think it is best to build a first step solution that is compatible with a future extension.

I like the naming of Status as it is given the function signature since it returns the current status based on the context.

I'm not really sure how you could cache the status within the client state since the status is dependent upon the block height it was updated against. Therefore a client status would need to keep its current status and the block height it was updated against, otherwise it is impossible to verify if the status is stale

My proposal was that you call RefreshStatus once at the start of each client handler. And then all subsequent logic can just use Status and get the latest status

My main concern is there is no clear contract here. The Status function will likely be accessible without requiring RefreshStatus. You'd need to add a check to ensure the status isn't stale or increase the risk for bugs within core IBC or by external developers

Copy link
Member

@AdityaSripal AdityaSripal left a comment

Choose a reason for hiding this comment

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

Looks good, clean fix!

@colin-axner colin-axner enabled auto-merge (squash) May 4, 2021 09:18
@colin-axner colin-axner merged commit 2c880a2 into main May 4, 2021
@colin-axner colin-axner deleted the colin/98-client-status branch May 4, 2021 09:24
faddat referenced this pull request in notional-labs/ibc-go Feb 23, 2022
Add integration tests with updated wasm contracts
faddat referenced this pull request in notional-labs/ibc-go Mar 1, 2022
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.

Modify 'IsFrozen' to be a 'Status' function
5 participants