Skip to content

Commit

Permalink
btcstaking: API for querying voting power at current height (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastianElvis authored Aug 7, 2023
1 parent 6ae786b commit 19d0fb7
Show file tree
Hide file tree
Showing 6 changed files with 683 additions and 66 deletions.
23 changes: 23 additions & 0 deletions proto/babylon/btcstaking/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ service Query {
option (google.api.http).get = "/babylon/btcstaking/v1/btc_validators/{val_btc_pk_hex}/power/{height}";
}

// BTCValidatorCurrentPower queries the voting power of a BTC validator at the current height
rpc BTCValidatorCurrentPower(QueryBTCValidatorCurrentPowerRequest) returns (QueryBTCValidatorCurrentPowerResponse) {
option (google.api.http).get = "/babylon/btcstaking/v1/btc_validators/{val_btc_pk_hex}/power";
}

// ActivatedHeight queries the height when BTC staking protocol is activated, i.e., the first height when
// there exists 1 BTC validator with voting power
rpc ActivatedHeight(QueryActivatedHeightRequest) returns (QueryActivatedHeightResponse) {
Expand Down Expand Up @@ -122,6 +127,24 @@ message QueryBTCValidatorPowerAtHeightResponse {
uint64 voting_power = 1;
}

// QueryBTCValidatorCurrentPowerRequest is the request type for the
// Query/BTCValidatorCurrentPower RPC method.
message QueryBTCValidatorCurrentPowerRequest {
// val_btc_pk_hex is the hex str of Bitcoin secp256k1 PK of the BTC validator that
// this BTC delegation delegates to
// the PK follows encoding in BIP-340 spec
string val_btc_pk_hex = 1;
}

// QueryBTCValidatorCurrentPowerResponse is the response type for the
// Query/BTCValidatorCurrentPower RPC method.
message QueryBTCValidatorCurrentPowerResponse {
// height is the current height
uint64 height = 1;
// voting_power is the voting power of the BTC validator
uint64 voting_power = 2;
}

// QueryActiveBTCValidatorsAtHeightRequest is the request type for the
// Query/ActiveBTCValidatorsAtHeight RPC method.
message QueryActiveBTCValidatorsAtHeightRequest {
Expand Down
30 changes: 30 additions & 0 deletions x/btcstaking/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,36 @@ func (k Keeper) BTCValidatorPowerAtHeight(ctx context.Context, req *types.QueryB
return &types.QueryBTCValidatorPowerAtHeightResponse{VotingPower: power}, nil
}

// BTCValidatorCurrentPower returns the voting power of the specified validator
// at the current height
func (k Keeper) BTCValidatorCurrentPower(ctx context.Context, req *types.QueryBTCValidatorCurrentPowerRequest) (*types.QueryBTCValidatorCurrentPowerResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}

valBTCPK, err := bbn.NewBIP340PubKeyFromHex(req.ValBtcPkHex)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to unmarshal validator BTC PK hex: %v", err)
}

sdkCtx := sdk.UnwrapSDKContext(ctx)
power := uint64(0)
curHeight := uint64(sdkCtx.BlockHeight())

// if voting power table is recorded at the current height, use this voting power
if k.HasVotingPowerTable(sdkCtx, curHeight) {
power = k.GetVotingPower(sdkCtx, valBTCPK.MustMarshal(), curHeight)
} else {
// NOTE: it's possible that the voting power is not recorded at the current height,
// e.g., `EndBlock` is not reached yet
// in this case, we use the last height
curHeight -= 1
power = k.GetVotingPower(sdkCtx, valBTCPK.MustMarshal(), curHeight)
}

return &types.QueryBTCValidatorCurrentPowerResponse{Height: curHeight, VotingPower: power}, nil
}

// ActiveBTCValidatorsAtHeight returns the active BTC validators at the provided height
func (k Keeper) ActiveBTCValidatorsAtHeight(ctx context.Context, req *types.QueryActiveBTCValidatorsAtHeightRequest) (*types.QueryActiveBTCValidatorsAtHeightResponse, error) {
if req == nil {
Expand Down
45 changes: 45 additions & 0 deletions x/btcstaking/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,51 @@ func FuzzBTCValidatorVotingPowerAtHeight(f *testing.F) {
})
}

func FuzzBTCValidatorCurrentVotingPower(f *testing.F) {
datagen.AddRandomSeedsToFuzzer(f, 10)
f.Fuzz(func(t *testing.T, seed int64) {
r := rand.New(rand.NewSource(seed))

// Setup keeper and context
keeper, ctx := testkeeper.BTCStakingKeeper(t, nil, nil)

// random BTC validator
btcVal, err := datagen.GenRandomBTCValidator(r)
require.NoError(t, err)
// add this BTC validator
keeper.SetBTCValidator(ctx, btcVal)
// set random voting power at random height
randomHeight := datagen.RandomInt(r, 100) + 1
ctx = ctx.WithBlockHeight(int64(randomHeight))
randomPower := datagen.RandomInt(r, 100) + 1
keeper.SetVotingPower(ctx, btcVal.BtcPk.MustMarshal(), randomHeight, randomPower)

// assert voting power at current height
req := &types.QueryBTCValidatorCurrentPowerRequest{
ValBtcPkHex: btcVal.BtcPk.MarshalHex(),
}
resp, err := keeper.BTCValidatorCurrentPower(ctx, req)
require.NoError(t, err)
require.Equal(t, randomHeight, resp.Height)
require.Equal(t, randomPower, resp.VotingPower)

// if height increments but voting power hasn't recorded yet, then
// we need to return the height and voting power at the last height
ctx = ctx.WithBlockHeight(int64(randomHeight + 1))
resp, err = keeper.BTCValidatorCurrentPower(ctx, req)
require.NoError(t, err)
require.Equal(t, randomHeight, resp.Height)
require.Equal(t, randomPower, resp.VotingPower)

// but no more
ctx = ctx.WithBlockHeight(int64(randomHeight + 2))
resp, err = keeper.BTCValidatorCurrentPower(ctx, req)
require.NoError(t, err)
require.Equal(t, randomHeight+1, resp.Height)
require.Equal(t, uint64(0), resp.VotingPower)
})
}

func FuzzActiveBTCValidatorsAtHeight(f *testing.F) {
datagen.AddRandomSeedsToFuzzer(f, 10)
f.Fuzz(func(t *testing.T, seed int64) {
Expand Down
8 changes: 8 additions & 0 deletions x/btcstaking/keeper/voting_power_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ func (k Keeper) GetVotingPower(ctx sdk.Context, valBTCPK []byte, height uint64)
return sdk.BigEndianToUint64(powerBytes)
}

// HasVotingPowerTable checks if the voting power table exists at a given height
func (k Keeper) HasVotingPowerTable(ctx sdk.Context, height uint64) bool {
store := k.votingPowerStore(ctx, height)
iter := store.Iterator(nil, nil)
defer iter.Close()
return iter.Valid()
}

// GetVotingPowerTable gets the voting power table, i.e., validator set at a given height
func (k Keeper) GetVotingPowerTable(ctx sdk.Context, height uint64) map[string]uint64 {
store := k.votingPowerStore(ctx, height)
Expand Down
Loading

0 comments on commit 19d0fb7

Please sign in to comment.