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

zoneconcierge: API for querying headers in a given epoch #261

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,955 changes: 2,674 additions & 1,281 deletions client/docs/swagger-ui/swagger.yaml

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions proto/babylon/zoneconcierge/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,18 @@ service Query {
rpc ChainInfo(QueryChainInfoRequest) returns (QueryChainInfoResponse) {
option (google.api.http).get = "/babylon/zoneconcierge/v1/chain_info/{chain_id}";
}
// EpochChainInfo queries the latest info of a chain in a given epoch of Babylon's view
rpc EpochChainInfo(QueryEpochChainInfoRequest) returns (QueryEpochChainInfoResponse) {
option (google.api.http).get = "/babylon/zoneconcierge/v1/epochs/{epoch_num}/chain_info/{chain_id}";
}
// ListHeaders queries the headers of a chain in Babylon's view, with pagination support
rpc ListHeaders(QueryListHeadersRequest) returns (QueryListHeadersResponse) {
option (google.api.http).get = "/babylon/zoneconcierge/v1/headers/{chain_id}";
}
// ListEpochHeaders queries the headers of a chain timestamped in a given epoch of Babylon, with pagination support
rpc ListEpochHeaders(QueryListEpochHeadersRequest) returns (QueryListEpochHeadersResponse) {
option (google.api.http).get = "/babylon/zoneconcierge/v1/epochs/{epoch_num}/headers/{chain_id}";
Copy link
Member

Choose a reason for hiding this comment

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

nit: I suggest the link be /headers/{chain_id}/epoch/{epoch_num} as the epoch is a more specific part of a headers query.

}
// FinalizedChainInfo queries the BTC-finalised info of a chain, with proofs
rpc FinalizedChainInfo(QueryFinalizedChainInfoRequest) returns (QueryFinalizedChainInfoResponse) {
option (google.api.http).get = "/babylon/zoneconcierge/v1/finalized_chain_info/{chain_id}";
Expand Down Expand Up @@ -68,6 +76,18 @@ message QueryChainInfoResponse {
babylon.zoneconcierge.v1.ChainInfo chain_info = 1;
}

// QueryEpochChainInfoRequest is request type for the Query/EpochChainInfo RPC method.
message QueryEpochChainInfoRequest {
uint64 epoch_num = 1;
string chain_id = 2;
}

// QueryEpochChainInfoResponse is response type for the Query/EpochChainInfo RPC method.
message QueryEpochChainInfoResponse {
// chain_info is the info of the CZ
babylon.zoneconcierge.v1.ChainInfo chain_info = 1;
}

// QueryListHeadersRequest is request type for the Query/ListHeaders RPC method.
message QueryListHeadersRequest {
string chain_id = 1;
Expand All @@ -83,6 +103,18 @@ message QueryListHeadersResponse {
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

// QueryListEpochHeadersRequest is request type for the Query/ListEpochHeaders RPC method.
message QueryListEpochHeadersRequest {
uint64 epoch_num = 1;
string chain_id = 2;
}

// QueryListEpochHeadersResponse is response type for the Query/ListEpochHeaders RPC method.
message QueryListEpochHeadersResponse {
// headers is the list of headers
repeated babylon.zoneconcierge.v1.IndexedHeader headers = 1;
Copy link
Member

Choose a reason for hiding this comment

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

I suggest we have pagination here.

Copy link
Member Author

Choose a reason for hiding this comment

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

I was also considering pagination for this API. The problem is that this neither is straightforward nor gives us performance improvements due to the KVStore structure.

We are querying a segment in the KVStore and the keys in this KVStore is not consecutive. The non-consecutiveness requires us to find the start header and the end header of this epoch, which is already O(epoch_interval) in the worst case.

For the sake of efficiency, maybe we can consider a slightly different API, which only returns a list of heights whose corresponding headers are timestamped in this given epoch. With the list of heights the explorer can query its interested indexed headers from ZoneConcierge individually.

I have marked a TODO here. Once we have a decision I will make corresponding changes in a subsequent API.

Copy link
Member

Choose a reason for hiding this comment

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

Yep, I also encountered a similar issue with the MainChain query of the btclightclient module (ref). I think it is possible although it makes the code more complex so we should decide on the tradeoff.

}

// QueryFinalizedChainInfoRequest is request type for the Query/FinalizedChainInfo RPC method.
message QueryFinalizedChainInfoRequest {
// chain_id is the ID of the CZ
Expand Down
3 changes: 2 additions & 1 deletion x/zoneconcierge/keeper/canonical_chain_indexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ func FuzzCanonicalChainIndexer(f *testing.F) {
f.Fuzz(func(t *testing.T, seed int64) {
rand.Seed(seed)

_, babylonChain, czChain, zcKeeper := SetupTest(t)
_, babylonChain, czChain, babylonApp := SetupTest(t)
zcKeeper := babylonApp.ZoneConciergeKeeper

ctx := babylonChain.GetContext()
hooks := zcKeeper.Hooks()
Expand Down
41 changes: 41 additions & 0 deletions x/zoneconcierge/keeper/epoch_chain_info_indexer.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keeper

import (
bbn "github.com/babylonchain/babylon/types"
"github.com/babylonchain/babylon/x/zoneconcierge/types"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
Expand All @@ -19,6 +20,46 @@ func (k Keeper) GetEpochChainInfo(ctx sdk.Context, chainID string, epochNumber u
return &chainInfo, nil
}

// GetEpochHeaders gets the headers timestamped in a given epoch, in the ascending order
func (k Keeper) GetEpochHeaders(ctx sdk.Context, chainID string, epochNumber uint64) ([]*types.IndexedHeader, error) {
headers := []*types.IndexedHeader{}

// find the last timestamped header of this chain in the epoch
epochChainInfo, err := k.GetEpochChainInfo(ctx, chainID, epochNumber)
if err != nil {
return nil, err
}
// it's possible that this epoch's snapshot is not updated for many epochs
// this implies that this epoch does not timestamp any header for this chain at all
// and the query should return empty
if epochChainInfo.LatestHeader.BabylonEpoch < epochNumber {
return nil, types.ErrEpochHeadersNotFound
Copy link
Member

Choose a reason for hiding this comment

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

Above you mention that the query should return empty, yet here you return an error. I'm happy with returning an empty set.

Copy link
Member Author

Choose a reason for hiding this comment

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

I feel an error gives a more clear message to the function invoker. The comment in L34 is indeed inconsistent though so I deleted it.

}
// now we have the last header in this epoch
headers = append(headers, epochChainInfo.LatestHeader)

// append all previous headers until reaching the previous epoch
canonicalChainStore := k.canonicalChainStore(ctx, chainID)
lastHeaderKey := sdk.Uint64ToBigEndian(epochChainInfo.LatestHeader.Height)
// NOTE: even in ReverseIterator, start and end should still be specified in ascending order
canonicalChainIter := canonicalChainStore.ReverseIterator(nil, lastHeaderKey)
defer canonicalChainIter.Close()
for ; canonicalChainIter.Valid(); canonicalChainIter.Next() {
var prevHeader types.IndexedHeader
k.cdc.MustUnmarshal(canonicalChainIter.Value(), &prevHeader)
if prevHeader.BabylonEpoch < epochNumber {
// we have reached the previous epoch, break the loop
break
}
headers = append(headers, &prevHeader)
}

// reverse the list so that it remains ascending order
bbn.Reverse(headers)

return headers, nil
}

// recordEpochChainInfo records the chain info for a given epoch number of given chain ID
// where the latest chain info is retrieved from the chain info indexer
func (k Keeper) recordEpochChainInfo(ctx sdk.Context, chainID string, epochNumber uint64) {
Expand Down
40 changes: 39 additions & 1 deletion x/zoneconcierge/keeper/epoch_chain_info_indexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ func FuzzEpochChainInfoIndexer(f *testing.F) {
f.Fuzz(func(t *testing.T, seed int64) {
rand.Seed(seed)

_, babylonChain, czChain, zcKeeper := SetupTest(t)
_, babylonChain, czChain, babylonApp := SetupTest(t)
zcKeeper := babylonApp.ZoneConciergeKeeper

ctx := babylonChain.GetContext()
hooks := zcKeeper.Hooks()
Expand All @@ -35,3 +36,40 @@ func FuzzEpochChainInfoIndexer(f *testing.F) {
require.Equal(t, numForkHeaders, uint64(len(chainInfo.LatestForks.Headers)))
})
}

func FuzzGetEpochHeaders(f *testing.F) {
datagen.AddRandomSeedsToFuzzer(f, 10)

f.Fuzz(func(t *testing.T, seed int64) {
rand.Seed(seed)

_, babylonChain, czChain, babylonApp := SetupTest(t)
zcKeeper := babylonApp.ZoneConciergeKeeper
epochingKeeper := babylonApp.EpochingKeeper

ctx := babylonChain.GetContext()
hooks := zcKeeper.Hooks()

// enter a random epoch
epochNum := datagen.RandomInt(10) + 1
for i := uint64(0); i < epochNum; i++ {
epochingKeeper.IncEpoch(ctx)
}

// invoke the hook a random number of times to simulate a random number of blocks
numHeaders := datagen.RandomInt(100) + 1
numForkHeaders := datagen.RandomInt(10) + 1
expectedHeaders, _ := SimulateHeadersAndForksViaHook(ctx, hooks, czChain.ChainID, numHeaders, numForkHeaders)

// end this epoch so that the chain info is recorded
hooks.AfterEpochEnds(ctx, epochNum)

// check if the headers are same as expected
headers, err := zcKeeper.GetEpochHeaders(ctx, czChain.ChainID, epochNum)
require.NoError(t, err)
require.Equal(t, len(expectedHeaders), len(headers))
for i := 0; i < len(expectedHeaders); i++ {
require.Equal(t, expectedHeaders[i].Header.LastCommitHash, headers[i].Hash)
}
Copy link
Member

Choose a reason for hiding this comment

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

As with my comment on #260 (ref) not entirely sure whether we check all cases here.

})
}
3 changes: 2 additions & 1 deletion x/zoneconcierge/keeper/fork_indexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ func FuzzForkIndexer(f *testing.F) {
f.Fuzz(func(t *testing.T, seed int64) {
rand.Seed(seed)

_, babylonChain, czChain, zcKeeper := SetupTest(t)
_, babylonChain, czChain, babylonApp := SetupTest(t)
zcKeeper := babylonApp.ZoneConciergeKeeper

ctx := babylonChain.GetContext()
hooks := zcKeeper.Hooks()
Expand Down
46 changes: 45 additions & 1 deletion x/zoneconcierge/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,27 @@ func (k Keeper) ChainInfo(c context.Context, req *types.QueryChainInfoRequest) (
return resp, nil
}

// EpochChainInfo returns the info of a chain with given ID in a given epoch
func (k Keeper) EpochChainInfo(c context.Context, req *types.QueryEpochChainInfoRequest) (*types.QueryEpochChainInfoResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}

if len(req.ChainId) == 0 {
return nil, status.Error(codes.InvalidArgument, "chain ID cannot be empty")
}

ctx := sdk.UnwrapSDKContext(c)

// find the chain info of the given epoch
chainInfo, err := k.GetEpochChainInfo(ctx, req.ChainId, req.EpochNum)
if err != nil {
return nil, err
}
resp := &types.QueryEpochChainInfoResponse{ChainInfo: chainInfo}
return resp, nil
}

// ListHeaders returns all headers of a chain with given ID, with pagination support
func (k Keeper) ListHeaders(c context.Context, req *types.QueryListHeadersRequest) (*types.QueryListHeadersResponse, error) {
if req == nil {
Expand Down Expand Up @@ -76,6 +97,29 @@ func (k Keeper) ListHeaders(c context.Context, req *types.QueryListHeadersReques
return resp, nil
}

// ListEpochHeaders returns all headers of a chain with given ID, with pagination support
func (k Keeper) ListEpochHeaders(c context.Context, req *types.QueryListEpochHeadersRequest) (*types.QueryListEpochHeadersResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}

if len(req.ChainId) == 0 {
return nil, status.Error(codes.InvalidArgument, "chain ID cannot be empty")
}

ctx := sdk.UnwrapSDKContext(c)

headers, err := k.GetEpochHeaders(ctx, req.ChainId, req.EpochNum)
if err != nil {
return nil, err
}

resp := &types.QueryListEpochHeadersResponse{
Headers: headers,
}
return resp, nil
}

func (k Keeper) FinalizedChainInfo(c context.Context, req *types.QueryFinalizedChainInfoRequest) (*types.QueryFinalizedChainInfoResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
Expand All @@ -101,7 +145,7 @@ func (k Keeper) FinalizedChainInfo(c context.Context, req *types.QueryFinalizedC

// It's possible that the chain info's epoch is way before the last finalised epoch
// e.g., when there is no relayer for many epochs
// NOTE: if an epoch is finalisedm then all of its previous epochs are also finalised
// NOTE: if an epoch is finalised then all of its previous epochs are also finalised
if chainInfo.LatestHeader.BabylonEpoch < finalizedEpoch {
finalizedEpoch = chainInfo.LatestHeader.BabylonEpoch
}
Expand Down
80 changes: 77 additions & 3 deletions x/zoneconcierge/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ func FuzzChainList(f *testing.F) {
f.Fuzz(func(t *testing.T, seed int64) {
rand.Seed(seed)

_, babylonChain, _, zcKeeper := SetupTest(t)
_, babylonChain, _, babylonApp := SetupTest(t)
zcKeeper := babylonApp.ZoneConciergeKeeper

ctx := babylonChain.GetContext()
hooks := zcKeeper.Hooks()
Expand Down Expand Up @@ -66,7 +67,8 @@ func FuzzChainInfo(f *testing.F) {
f.Fuzz(func(t *testing.T, seed int64) {
rand.Seed(seed)

_, babylonChain, czChain, zcKeeper := SetupTest(t)
_, babylonChain, czChain, babylonApp := SetupTest(t)
zcKeeper := babylonApp.ZoneConciergeKeeper

ctx := babylonChain.GetContext()
hooks := zcKeeper.Hooks()
Expand All @@ -85,13 +87,44 @@ func FuzzChainInfo(f *testing.F) {
})
}

func FuzzEpochChainInfo(f *testing.F) {
datagen.AddRandomSeedsToFuzzer(f, 10)

f.Fuzz(func(t *testing.T, seed int64) {
rand.Seed(seed)

_, babylonChain, czChain, babylonApp := SetupTest(t)
zcKeeper := babylonApp.ZoneConciergeKeeper

ctx := babylonChain.GetContext()
hooks := zcKeeper.Hooks()

// invoke the hook a random number of times to simulate a random number of blocks
numHeaders := datagen.RandomInt(100) + 1
numForkHeaders := datagen.RandomInt(10) + 1
SimulateHeadersAndForksViaHook(ctx, hooks, czChain.ChainID, numHeaders, numForkHeaders)

// simulate the scenario that a random epoch has ended
epochNum := datagen.RandomInt(10)
hooks.AfterEpochEnds(ctx, epochNum)

// check if the chain info of is recorded or not
resp, err := zcKeeper.EpochChainInfo(ctx, &zctypes.QueryEpochChainInfoRequest{EpochNum: epochNum, ChainId: czChain.ChainID})
require.NoError(t, err)
chainInfo := resp.ChainInfo
require.Equal(t, numHeaders-1, chainInfo.LatestHeader.Height)
require.Equal(t, numForkHeaders, uint64(len(chainInfo.LatestForks.Headers)))
})
}

func FuzzListHeaders(f *testing.F) {
datagen.AddRandomSeedsToFuzzer(f, 10)

f.Fuzz(func(t *testing.T, seed int64) {
rand.Seed(seed)

_, babylonChain, czChain, zcKeeper := SetupTest(t)
_, babylonChain, czChain, babylonApp := SetupTest(t)
zcKeeper := babylonApp.ZoneConciergeKeeper

ctx := babylonChain.GetContext()
hooks := zcKeeper.Hooks()
Expand All @@ -118,6 +151,47 @@ func FuzzListHeaders(f *testing.F) {
})
}

func FuzzListEpochHeaders(f *testing.F) {
datagen.AddRandomSeedsToFuzzer(f, 10)

f.Fuzz(func(t *testing.T, seed int64) {
rand.Seed(seed)

_, babylonChain, czChain, babylonApp := SetupTest(t)
zcKeeper := babylonApp.ZoneConciergeKeeper
epochingKeeper := babylonApp.EpochingKeeper

ctx := babylonChain.GetContext()
hooks := zcKeeper.Hooks()

// enter a random epoch
epochNum := datagen.RandomInt(10) + 1
for i := uint64(0); i < epochNum; i++ {
epochingKeeper.IncEpoch(ctx)
}

// invoke the hook a random number of times to simulate a random number of blocks
numHeaders := datagen.RandomInt(100) + 1
numForkHeaders := datagen.RandomInt(10) + 1
expectedHeaders, _ := SimulateHeadersAndForksViaHook(ctx, hooks, czChain.ChainID, numHeaders, numForkHeaders)

// end this epoch so that the chain info is recorded
hooks.AfterEpochEnds(ctx, epochNum)

req := &zctypes.QueryListEpochHeadersRequest{
ChainId: czChain.ChainID,
EpochNum: epochNum,
}
resp, err := zcKeeper.ListEpochHeaders(ctx, req)
require.NoError(t, err)

require.Equal(t, len(expectedHeaders), len(resp.Headers))
for i := 0; i < len(expectedHeaders); i++ {
require.Equal(t, expectedHeaders[i].Header.LastCommitHash, resp.Headers[i].Hash)
}
})
}

func FuzzFinalizedChainInfo(f *testing.F) {
datagen.AddRandomSeedsToFuzzer(f, 10)

Expand Down
8 changes: 4 additions & 4 deletions x/zoneconcierge/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import (
)

// SetupTest creates a coordinator with 2 test chains, and a ZoneConcierge keeper.
func SetupTest(t *testing.T) (*ibctesting.Coordinator, *ibctesting.TestChain, *ibctesting.TestChain, zckeeper.Keeper) {
var zcKeeper zckeeper.Keeper
func SetupTest(t *testing.T) (*ibctesting.Coordinator, *ibctesting.TestChain, *ibctesting.TestChain, *app.BabylonApp) {
var bbnApp *app.BabylonApp
coordinator := ibctesting.NewCoordinator(t, 2)
// replace the first test chain with a Babylon chain
ibctesting.DefaultTestingAppInit = func() (ibctesting.TestingApp, map[string]json.RawMessage) {
babylonApp := app.Setup(t, false)
zcKeeper = babylonApp.ZoneConciergeKeeper
bbnApp = babylonApp
encCdc := app.MakeTestEncodingConfig()
genesis := app.NewDefaultGenesisState(encCdc.Marshaler)
return babylonApp, genesis
Expand All @@ -30,7 +30,7 @@ func SetupTest(t *testing.T) (*ibctesting.Coordinator, *ibctesting.TestChain, *i
babylonChain := coordinator.GetChain(ibctesting.GetChainID(1))
czChain := coordinator.GetChain(ibctesting.GetChainID(2))

return coordinator, babylonChain, czChain, zcKeeper
return coordinator, babylonChain, czChain, bbnApp
}

// SimulateHeadersViaHook generates a non-zero number of canonical headers via the hook
Expand Down
Loading