Skip to content

Commit

Permalink
Merge pull request #1227 from lavanet/CNS-869-iprpc-fund-tx
Browse files Browse the repository at this point in the history
CNS-869: IPRPC part 3 - IPRPC fund TX
  • Loading branch information
Yaroms authored Mar 6, 2024
2 parents 07f4dff + b246353 commit 227d6b8
Show file tree
Hide file tree
Showing 15 changed files with 805 additions and 24 deletions.
14 changes: 14 additions & 0 deletions proto/lavanet/lava/rewards/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ option go_package = "github.com/lavanet/lava/x/rewards/types";
// Msg defines the Msg service.
service Msg {
rpc SetIprpcData(MsgSetIprpcData) returns (MsgSetIprpcDataResponse);
rpc FundIprpc(MsgFundIprpc) returns (MsgFundIprpcResponse);
// this line is used by starport scaffolding # proto/tx/rpc
}

Expand All @@ -23,4 +24,17 @@ message MsgSetIprpcData {
message MsgSetIprpcDataResponse {
}

message MsgFundIprpc {
string creator = 1;
uint64 duration = 2; // vesting duration in months
repeated cosmos.base.v1beta1.Coin amounts = 3 [
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins",
(gogoproto.nullable) = false
]; // tokens to be distributed as reward
string spec = 4; // spec on which the providers get incentive
}

message MsgFundIprpcResponse {
}

// this line is used by starport scaffolding # proto/tx/message
4 changes: 4 additions & 0 deletions scripts/init_chain_commands.sh
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ sleep_until_next_epoch
HEALTH_FILE="config/health_examples/health_template.yml"
create_health_config $HEALTH_FILE $(lavad keys show user1 -a) $(lavad keys show servicer2 -a) $(lavad keys show servicer3 -a)

lavad tx gov submit-legacy-proposal set-iprpc-data 1000000000ulava --min-cost 100ulava --add-subscriptions $(lavad keys show -a user1) --from alice -y
wait_count_blocks 1
lavad tx gov vote $(latest_vote) yes -y --from alice --gas-adjustment "1.5" --gas "auto" --gas-prices $GASPRICE

if [[ "$1" != "--skip-providers" ]]; then
. ${__dir}/setup_providers.sh
echo "letting providers start and running health check"
Expand Down
12 changes: 12 additions & 0 deletions utils/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,15 @@ func NextMonth(date time.Time) time.Time {
time.UTC,
)
}

func IsMiddleOfMonthPassed(date time.Time) bool {
// Get the total number of days in the current month
_, month, year := date.Date()
daysInMonth := time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day()

// Calculate the middle day of the month
middleDay := daysInMonth / 2

// Check if the day of the given date is greater than the middle day
return date.Day() > middleDay
}
1 change: 1 addition & 0 deletions x/rewards/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func GetTxCmd() *cobra.Command {
RunE: client.ValidateCmd,
}

cmd.AddCommand(CmdFundIprpc())
// this line is used by starport scaffolding # 1

return cmd
Expand Down
60 changes: 60 additions & 0 deletions x/rewards/client/cli/tx_fund_iprpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package cli

import (
"strconv"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/lavanet/lava/x/rewards/types"
"github.com/spf13/cobra"
)

var _ = strconv.Itoa(0)

func CmdFundIprpc() *cobra.Command {
cmd := &cobra.Command{
Use: "fund-iprpc [spec] [duration] [coins] --from <creator>",
Short: `fund the IPRPC pool to a specific spec with ulava or IBC wrapped tokens. The tokens will be vested for <duration> months.
Note that the amount of coins you put is the monthly quota (it's not for the total period of time).
Also, the tokens must include duration*min_iprpc_cost of ulava tokens (min_iprpc_cost is shown with the show-iprpc-data command)`,
Example: `lavad tx rewards fund-iprpc ETH1 4 100000ulava,50000ibctoken --from alice
This command will transfer 4*100000ulava and 4*50000ibctoken to the IPRPC pool to be distributed for 4 months`,
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) (err error) {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

spec := args[0]
durationStr := args[1]
duration, err := strconv.ParseUint(durationStr, 10, 64)
if err != nil {
return err
}

fundStr := args[2]
fund, err := sdk.ParseCoinsNormalized(fundStr)
if err != nil {
return err
}

msg := types.NewMsgFundIprpc(
clientCtx.GetFromAddress().String(),
spec,
duration,
fund,
)
if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)

return cmd
}
5 changes: 5 additions & 0 deletions x/rewards/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ import (
func NewHandler(k keeper.Keeper) sdk.Handler {
// this line is used by starport scaffolding # handler/msgServer

msgServer := keeper.NewMsgServerImpl(k)

return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
_ = ctx.WithEventManager(sdk.NewEventManager())

switch msg := msg.(type) {
case *types.MsgFundIprpc:
res, err := msgServer.FundIprpc(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
// this line is used by starport scaffolding # 1
default:
errMsg := fmt.Sprintf("unrecognized %s message type: %T", types.ModuleName, msg)
Expand Down
56 changes: 56 additions & 0 deletions x/rewards/keeper/iprpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,64 @@ package keeper

import (
"fmt"
"strconv"

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/lavanet/lava/utils"
"github.com/lavanet/lava/x/rewards/types"
)

func (k Keeper) FundIprpc(ctx sdk.Context, creator string, duration uint64, fund sdk.Coins, spec string) error {
// verify spec exists and active
foundAndActive, _, _ := k.specKeeper.IsSpecFoundAndActive(ctx, spec)
if !foundAndActive {
return utils.LavaFormatWarning("spec not found or disabled", types.ErrFundIprpc)
}

// check fund consists of minimum amount of ulava (min_iprpc_cost)
minIprpcFundCost := k.GetMinIprpcCost(ctx)
if fund.AmountOf(k.stakingKeeper.BondDenom(ctx)).LT(minIprpcFundCost.Amount) {
return utils.LavaFormatWarning("insufficient ulava tokens in fund. should be at least min iprpc cost * duration", types.ErrFundIprpc,
utils.LogAttr("min_iprpc_cost", k.GetMinIprpcCost(ctx).String()),
utils.LogAttr("duration", strconv.FormatUint(duration, 10)),
utils.LogAttr("fund_ulava_amount", fund.AmountOf(k.stakingKeeper.BondDenom(ctx))),
)
}

// check creator has enough balance
addr, err := sdk.AccAddressFromBech32(creator)
if err != nil {
return utils.LavaFormatWarning("invalid creator address", types.ErrFundIprpc)
}

// send the minimum cost to the validators allocation pool (and subtract them from the fund)
minIprpcFundCostCoins := sdk.NewCoins(minIprpcFundCost).MulInt(sdk.NewIntFromUint64(duration))
err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, addr, string(types.ValidatorsRewardsAllocationPoolName), minIprpcFundCostCoins)
if err != nil {
return utils.LavaFormatError(types.ErrFundIprpc.Error()+"for funding validator allocation pool", err,
utils.LogAttr("creator", creator),
utils.LogAttr("min_iprpc_fund_cost", minIprpcFundCost.String()),
)
}
fund = fund.Sub(minIprpcFundCost)
allFunds := fund.MulInt(math.NewIntFromUint64(duration))

// send the funds to the iprpc pool
err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, addr, string(types.IprpcPoolName), allFunds)
if err != nil {
return utils.LavaFormatError(types.ErrFundIprpc.Error()+"for funding iprpc pool", err,
utils.LogAttr("creator", creator),
utils.LogAttr("fund", fund.String()),
)
}

// add spec funds to next month IPRPC reward object
k.addSpecFunds(ctx, spec, fund, duration)

return nil
}

// handleNoIprpcRewardToProviders handles the situation in which there are no providers to send IPRPC rewards to
// so the IPRPC rewards transfer to the next month
func (k Keeper) handleNoIprpcRewardToProviders(ctx sdk.Context, iprpcFunds []types.Specfund) {
Expand Down Expand Up @@ -95,6 +147,10 @@ func (k Keeper) distributeIprpcRewards(ctx sdk.Context, iprpcReward types.IprpcR
if err != nil {
continue
}
if specCu.TotalCu == 0 {
// spec was not serviced by any provider, continue
continue
}
// calculate provider IPRPC reward
providerIprpcReward := specFund.Fund.MulInt(sdk.NewIntFromUint64(providerCU.CU)).QuoInt(sdk.NewIntFromUint64(specCu.TotalCu))

Expand Down
32 changes: 32 additions & 0 deletions x/rewards/keeper/msg_server_fund_iprpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package keeper

import (
"context"
"strconv"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/lavanet/lava/utils"
"github.com/lavanet/lava/x/rewards/types"
)

func (k msgServer) FundIprpc(goCtx context.Context, msg *types.MsgFundIprpc) (*types.MsgFundIprpcResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

err := msg.ValidateBasic()
if err != nil {
return &types.MsgFundIprpcResponse{}, err
}

err = k.Keeper.FundIprpc(ctx, msg.Creator, msg.Duration, msg.Amounts, msg.Spec)
if err == nil {
logger := k.Keeper.Logger(ctx)
details := map[string]string{
"spec": msg.Spec,
"duration": strconv.FormatUint(msg.Duration, 10),
"amounts": msg.Amounts.String(),
}
utils.LogLavaEvent(ctx, logger, types.FundIprpcEventName, details, "Funded IPRPC pool successfully")
}

return &types.MsgFundIprpcResponse{}, err
}
2 changes: 2 additions & 0 deletions x/rewards/types/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import (

func RegisterCodec(cdc *codec.LegacyAmino) {
cdc.RegisterConcrete(&MsgSetIprpcData{}, "rewards/MsgSetIprpcData", nil)
cdc.RegisterConcrete(&MsgFundIprpc{}, "rewards/MsgFundIprpc", nil)
// this line is used by starport scaffolding # 2
}

func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
// this line is used by starport scaffolding # 3
registry.RegisterImplementations((*sdk.Msg)(nil),
&MsgSetIprpcData{},
&MsgFundIprpc{},
)
msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)
}
Expand Down
2 changes: 1 addition & 1 deletion x/rewards/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ import (

// x/rewards module sentinel errors
var (
ErrSample = sdkerrors.Register(ModuleName, 1100, "sample error")
ErrFundIprpc = sdkerrors.Register(ModuleName, 1, "fund iprpc TX failed")
)
2 changes: 2 additions & 0 deletions x/rewards/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ type BankKeeper interface {
GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin
SendCoinsFromModuleToModule(ctx sdk.Context, senderPool, recipientPool string, amt sdk.Coins) error
GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
// Methods imported from bank should be defined here
}

type SpecKeeper interface {
GetAllChainIDs(ctx sdk.Context) (chainIDs []string)
GetSpec(ctx sdk.Context, index string) (val spectypes.Spec, found bool)
IsSpecFoundAndActive(ctx sdk.Context, chainID string) (foundAndActive, found bool, providersType spectypes.Spec_ProvidersTypes)
}

type TimerStoreKeeper interface {
Expand Down
63 changes: 63 additions & 0 deletions x/rewards/types/message_fund_iprpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package types

import (
fmt "fmt"

sdkerrors "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
legacyerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

const TypeMsgFundIprpc = "fund_iprpc"

var _ sdk.Msg = &MsgFundIprpc{}

func NewMsgFundIprpc(creator string, spec string, duration uint64, amounts sdk.Coins) *MsgFundIprpc {
return &MsgFundIprpc{
Creator: creator,
Spec: spec,
Duration: duration,
Amounts: amounts,
}
}

func (msg *MsgFundIprpc) Route() string {
return RouterKey
}

func (msg *MsgFundIprpc) Type() string {
return TypeMsgFundIprpc
}

func (msg *MsgFundIprpc) GetSigners() []sdk.AccAddress {
creator, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
panic(err)
}
return []sdk.AccAddress{creator}
}

func (msg *MsgFundIprpc) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}

func (msg *MsgFundIprpc) ValidateBasic() error {
_, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
return sdkerrors.Wrapf(legacyerrors.ErrInvalidAddress, "invalid creator address (%s)", err)
}

unique := map[string]struct{}{}
for _, amount := range msg.Amounts {
if !amount.IsValid() {
return sdkerrors.Wrap(fmt.Errorf("invalid amount; invalid denom or negative amount. coin: %s", amount.String()), "")
}
if _, ok := unique[amount.Denom]; ok {
return sdkerrors.Wrap(fmt.Errorf("invalid coins, duplicated denom: %s", amount.Denom), "")
}
unique[amount.Denom] = struct{}{}
}

return nil
}
49 changes: 49 additions & 0 deletions x/rewards/types/message_fund_iprpc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package types

import (
"testing"

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/lavanet/lava/testutil/sample"
"github.com/stretchr/testify/require"
)

func TestFundIprpc_ValidateBasic(t *testing.T) {
tests := []struct {
name string
msg MsgFundIprpc
valid bool
}{
{
name: "valid",
msg: MsgFundIprpc{
Creator: sample.AccAddress(),
Duration: 2,
Amounts: sdk.NewCoins(sdk.NewCoin("denom", math.OneInt())),
Spec: "spec",
},
valid: true,
},
{
name: "invalid creator address",
msg: MsgFundIprpc{
Creator: "invalid_address",
Duration: 2,
Amounts: sdk.NewCoins(sdk.NewCoin("denom", math.OneInt())),
Spec: "spec",
},
valid: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.msg.ValidateBasic()
if tt.valid {
require.NoError(t, err)
return
}
require.Error(t, err)
})
}
}
Loading

0 comments on commit 227d6b8

Please sign in to comment.