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

epoching API: add epoch msg range queries and add timestamp to validator lifecycle #119

Merged
merged 10 commits into from
Sep 9, 2022

Conversation

SebastianElvis
Copy link
Member

@SebastianElvis SebastianElvis commented Sep 7, 2022

Fixes BM-120

This PR

  • implements the API /babylon/epoching/v1/latest_epoch_msgs/{num_latest_epochs} that given the number num_latest_epochs of epochs, returns a map of epoch msgs keyed by the epoch number
  • fixes a bug on validator lifecycle API and adds timestamp to validator lifecycle

Also marked @toliujiayi as reviewer since the bug was reported by Jack (Thanks!)

Fuzz tests on the new APIs will be implemented by subsequent PRs in the next sprint.

Example execution

image

Copy link
Member

@vitsalis vitsalis left a comment

Choose a reason for hiding this comment

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

Few minor things:

}

// QueryLatestEpochMsgsResponse is response type for the Query/LatestEpochMsgs RPC method
message QueryLatestEpochMsgsResponse {
Copy link
Member

Choose a reason for hiding this comment

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

Should we add pagination to this? Even though the query is supposed to be used for the "latest" epochs, someone could provide a large number as an argument.

func (k Keeper) LatestEpochMsgs(c context.Context, req *types.QueryLatestEpochMsgsRequest) (*types.QueryLatestEpochMsgsResponse, error) {
ctx := sdk.UnwrapSDKContext(c)

// TODO: enforce limit of the number of epochs in each query
Copy link
Member

Choose a reason for hiding this comment

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

Why do we want to have a limit here? Seems a little bit arbitrary.

Copy link
Contributor

Choose a reason for hiding this comment

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

Usually gRPC listing endpoints have the option to limit the pagination size to protect themselves against resource exhaustion: https://cloud.google.com/apis/design/design_patterns#list_pagination

Copy link
Member Author

Choose a reason for hiding this comment

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

Great, I can always learn something new from your code reviews 👍

Yes I believe this API has to do pagination. There are two approaches to do the pagination:

  • Paginating the epoch number. Each page contains epoch msgs of a limited number of epochs
  • Paginating epoch messages. Each page contains a limited number of epoch messages, ordered by their corresponding epoch numbers

I'm inclined to do the former since we always want the complete set of msgs in an epoch. WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

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

In addition, Cosmos SDK implements its own pagination functionalities, i.e., cosmos.base.query.v1beta1.PageRequest and cosmos.base.query.v1beta1.PageResponse, and does not make use of the List feature of HTTP (as shown in the screenshot below). Perhaps for our implementation let's stick to Cosmos SDK's approach for consistency?

image

Copy link
Contributor

Choose a reason for hiding this comment

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

I totally agree with using the Cosmos pagination machinery! I only included the link to quote the gRPC authorities on why adding pagination is a good idea, but it's a pain in the ass to do it their way, Cosmos is much more convenient than doing it from scratch.

msgMap := make(map[uint64]*types.QueuedMessageList)

for i := beginEpoch; i <= endEpoch; i++ {
msgs := k.GetEpochMsgs(ctx, i)
Copy link
Member

Choose a reason for hiding this comment

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

This iteration means that we are doing req.NumLatestEpochs accesses to storage. Since we are accessing concurrent epochs, I think a more performant way to be to iterate the epoch storage in reverse and stop as soon as you get to an epoch that has a lower height than beginEpoch.

Copy link
Contributor

@aakoshh aakoshh left a comment

Choose a reason for hiding this comment

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

Just commenting on the URL formats. Maybe it would be more flexible to add a begin_epoch and end_epoch parameter, or end_epoch and epoch_count to the query, so you can add pagination to the whole thing if you want to.

@@ -26,6 +26,11 @@ service Query {
option (google.api.http).get = "/babylon/epoching/v1/epoch_msgs/{epoch_num}";
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
option (google.api.http).get = "/babylon/epoching/v1/epoch_msgs/{epoch_num}";
option (google.api.http).get = "/babylon/epoching/v1/epochs/{epoch_num=*}/messages";

These are supposed to be RESTful looking URLs.

See:

Comment on lines 29 to 32
// LatestEpochMsgs queries the messages within a given number of most recent epochs
rpc LatestEpochMsgs(QueryLatestEpochMsgsRequest) returns (QueryLatestEpochMsgsResponse) {
option (google.api.http).get = "/babylon/epoching/v1/latest_epoch_msgs/{num_latest_epochs}";
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// LatestEpochMsgs queries the messages within a given number of most recent epochs
rpc LatestEpochMsgs(QueryLatestEpochMsgsRequest) returns (QueryLatestEpochMsgsResponse) {
option (google.api.http).get = "/babylon/epoching/v1/latest_epoch_msgs/{num_latest_epochs}";
}
// LatestEpochMsgs queries the messages within a given number of most recent epochs
rpc LatestEpochMsgs(QueryLatestEpochMsgsRequest) returns (QueryLatestEpochMsgsResponse) {
option (google.api.http).get = "/babylon/epoching/v1/epochs:latest/messages";
}

And the num_latest_epochs (I would name it epoch_count would appear in the query string like ?epoch_count=10. Again, the RESTful URL design; the count is not part of the resource name, it does not identify it like epochs/10 would the 10th epoch.

This is a custom method; we can have epochs/10/messages for listing messages in a specific epoch, and we could also use epochs/-/messages?account_key=0x123 to list stuff from a single account across all epochs, but it feels like having an ID in the path for this method would make no sense with an epoch_count parameter, so it's custom.

// epoch_msg_map is the map where
// - key is the epoch number
// - value is the list of queued messages in this epoch
map<uint64, babylon.epoching.v1.QueuedMessageList> epoch_msg_map = 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Maps are rather poor in protobuf. They keys can't be custom types, and even though no matter how hard I tried I could not convince @gitferry that losing the information of a CheckpointStatus in favor of string is a bad tradeoff, I still think we should stay away from them. A list of key-value pairs (a nested type under QueryLatestEpochMsgsResponse, or maybe just adding an epoch number to QueuedMessageList should do it).

func (k Keeper) LatestEpochMsgs(c context.Context, req *types.QueryLatestEpochMsgsRequest) (*types.QueryLatestEpochMsgsResponse, error) {
ctx := sdk.UnwrapSDKContext(c)

// TODO: enforce limit of the number of epochs in each query
Copy link
Contributor

Choose a reason for hiding this comment

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

Usually gRPC listing endpoints have the option to limit the pagination size to protect themselves against resource exhaustion: https://cloud.google.com/apis/design/design_patterns#list_pagination

@SebastianElvis
Copy link
Member Author

Thanks for the comments Vitalis and Akosh! I learned a lot about API design via this PR 😎

Now this PR has fixed the comments. Specifically, this PR

  • makes QueryLatestEpochMsgs to use epoching/v1/epochs:latest/messages URL, have two parameters end_epoch and epoch_count, return a list of msg queues, and support pagination (via Cosmos SDK's PageRequest and PageResponse).
  • changes QueryEpochMsgs API's URL to babylon/epoching/v1/epochs/{epoch_num=*}/messages

An example invocation of QueryLatestEpochMsgs is attached below. Feel free to review again.

image


// iterate over queueLenStore since we only need to iterate over the epoch number
queueLenStore := k.msgQueueLengthStore(ctx)
pageRes, err := query.FilteredPaginate(queueLenStore, req.Pagination, func(key []byte, _ []byte, accumulate bool) (bool, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Note the closure/anonymous function passing here ☝️

Copy link
Contributor

@aakoshh aakoshh left a comment

Choose a reason for hiding this comment

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

Awesome, thank your for looking into pagination!

@SebastianElvis SebastianElvis merged commit ea000fa into main Sep 9, 2022
@SebastianElvis SebastianElvis deleted the epoching-api-range-query branch September 9, 2022 00:38
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