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

feat: IPRPC over IBC #1446

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5008a8f
CNS-960: make rewards module ibc middleware
oren-lava May 21, 2024
d3c5144
CNS-960: implement memo ibc-transfer memo parsing
oren-lava May 22, 2024
10e1650
CNS-960: unit test for memo parsing
oren-lava May 22, 2024
14559ca
CNS-960: fix lint
oren-lava May 22, 2024
5f038ba
Merge branch 'main' into CNS-960-ibc-middleware
oren-lava May 27, 2024
e796926
Merge branch 'main' into CNS-960-ibc-middleware
oren-lava May 29, 2024
54f7b0d
Merge branch 'main' into CNS-960-ibc-middleware
oren-lava Jun 6, 2024
c5b5020
CNS-960: nil ack comment
oren-lava Jun 6, 2024
1723bb7
CNS-960: make iprpc memo a protobuf
oren-lava Jun 6, 2024
6a3ccb7
CNS-960: improve memo decoding code
oren-lava Jun 6, 2024
0ad3878
Merge branch 'main' into CNS-960-ibc-middleware
oren-lava Jun 10, 2024
a331cd9
CNS-960: create types/ibc_iprpc.go and PendingIprpcPool
oren-lava Jun 13, 2024
154a420
CNS-960: update middleware to work with pending pool and temp addrees…
oren-lava Jun 13, 2024
f1a7a37
Merge branch 'main' into CNS-960-ibc-middleware
oren-lava Jun 13, 2024
1676677
CNS-960: handling async packets
oren-lava Jun 18, 2024
fc25eb2
CNS-960: lint
oren-lava Jun 18, 2024
b9904e9
Merge branch 'main' into CNS-960-ibc-middleware
oren-lava Jun 18, 2024
98110fe
CNS-960: fix coderabbitai comments
oren-lava Jun 18, 2024
bbd5416
Merge branch 'main' into CNS-960-ibc-middleware
oren-lava Jun 19, 2024
0b5f338
feat: IPRPC over IBC: Part 2 - CNS-964: CLI for submitting an IPRPC o…
oren-lava Jul 8, 2024
4cedfe7
Merge branch 'main' into CNS-960-ibc-middleware
oren-lava Jul 8, 2024
04865f1
CNS-960: small fix send to pending iprpc pool without leftovers
oren-lava Jul 8, 2024
d853bff
CNS-960: code rabbit suggestions
oren-lava Aug 29, 2024
ee68fb7
Merge branch 'main' into CNS-960-ibc-middleware
oren-lava Aug 29, 2024
894dfee
CNS-960: lint fix
oren-lava Aug 29, 2024
b352fcd
Merge branch 'main' into CNS-960-ibc-middleware
oren-lava Sep 4, 2024
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
2 changes: 2 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ func New(
authtypes.FeeCollectorName,
app.TimerStoreKeeper,
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
app.IBCKeeper.ChannelKeeper,
)
rewardsModule := rewardsmodule.NewAppModule(appCodec, app.RewardsKeeper, app.AccountKeeper, app.BankKeeper)

Expand Down Expand Up @@ -714,6 +715,7 @@ func New(
packetforwardkeeper.DefaultForwardTransferPacketTimeoutTimestamp, // forward timeout
packetforwardkeeper.DefaultRefundTransferPacketTimeoutTimestamp, // refund timeout
)
transferStack = rewardsmodule.NewIBCMiddleware(transferStack, app.RewardsKeeper)
ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferStack)

// this line is used by starport scaffolding # ibc/app/router
Expand Down
6 changes: 6 additions & 0 deletions proto/lavanet/lava/rewards/iprpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@ message Specfund {
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins",
(gogoproto.nullable) = false
];
}

message IprpcMemo {
string creator = 1;
string spec = 2;
uint64 duration = 3; // Iprpc fund period in months
}
2 changes: 1 addition & 1 deletion testutil/keeper/keepers_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ func InitAllKeepers(t testing.TB) (*Servers, *Keepers, context.Context) {
ks.Projects = *projectskeeper.NewKeeper(cdc, projectsStoreKey, projectsMemStoreKey, projectsparamsSubspace, ks.Epochstorage, ks.FixationStoreKeeper)
ks.Protocol = *protocolkeeper.NewKeeper(cdc, protocolStoreKey, protocolMemStoreKey, protocolparamsSubspace, authtypes.NewModuleAddress(govtypes.ModuleName).String())
ks.Downtime = downtimekeeper.NewKeeper(cdc, downtimeKey, downtimeParamsSubspace, ks.Epochstorage)
ks.Rewards = *rewardskeeper.NewKeeper(cdc, rewardsStoreKey, rewardsMemStoreKey, rewardsparamsSubspace, ks.BankKeeper, ks.AccountKeeper, ks.Spec, ks.Epochstorage, ks.Downtime, ks.StakingKeeper, ks.Dualstaking, ks.Distribution, authtypes.FeeCollectorName, ks.TimerStoreKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String())
ks.Rewards = *rewardskeeper.NewKeeper(cdc, rewardsStoreKey, rewardsMemStoreKey, rewardsparamsSubspace, ks.BankKeeper, ks.AccountKeeper, ks.Spec, ks.Epochstorage, ks.Downtime, ks.StakingKeeper, ks.Dualstaking, ks.Distribution, authtypes.FeeCollectorName, ks.TimerStoreKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), nil)
ks.Subscription = *subscriptionkeeper.NewKeeper(cdc, subscriptionStoreKey, subscriptionMemStoreKey, subscriptionparamsSubspace, &ks.BankKeeper, &ks.AccountKeeper, &ks.Epochstorage, ks.Projects, ks.Plans, ks.Dualstaking, ks.Rewards, ks.FixationStoreKeeper, ks.TimerStoreKeeper, ks.StakingKeeper)
ks.Pairing = *pairingkeeper.NewKeeper(cdc, pairingStoreKey, pairingMemStoreKey, pairingparamsSubspace, &ks.BankKeeper, &ks.AccountKeeper, ks.Spec, &ks.Epochstorage, ks.Projects, ks.Subscription, ks.Plans, ks.Downtime, ks.Dualstaking, &ks.StakingKeeper, ks.FixationStoreKeeper, ks.TimerStoreKeeper)
ks.ParamsKeeper = paramsKeeper
Expand Down
1 change: 1 addition & 0 deletions testutil/keeper/rewards.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func RewardsKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) {
authtypes.FeeCollectorName,
timerstorekeeper.NewKeeper(cdc),
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
nil,
)

// Initialize params
Expand Down
7 changes: 7 additions & 0 deletions testutil/sample/sample.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ func AccAddress() string {
return sdk.AccAddress(addr).String()
}

// AccAddress returns a sample account address of type sdk.AccAddress
func AccAddressObject() sdk.AccAddress {
pk := ed25519.GenPrivKey().PubKey()
addr := pk.Address()
return sdk.AccAddress(addr)
}

// ValAddress returns a sample validator account address
func ValAddress() string {
pk := ed25519.GenPrivKey().PubKey()
Expand Down
174 changes: 174 additions & 0 deletions x/rewards/ibc_middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package rewards

import (
"errors"
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types"
clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types"
channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types"
porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types"
"github.com/cosmos/ibc-go/v7/modules/core/exported"
"github.com/lavanet/lava/utils"
"github.com/lavanet/lava/x/rewards/keeper"
"github.com/lavanet/lava/x/rewards/types"
)

var _ porttypes.Middleware = &IBCMiddleware{}

// IBCMiddleware implements the ICS26 callbacks for the transfer middleware given
// the rewards keeper and the underlying application.
type IBCMiddleware struct {
app porttypes.IBCModule // transfer stack
keeper keeper.Keeper
}

// NewIBCMiddleware creates a new IBCMiddleware given the keeper and underlying application
func NewIBCMiddleware(app porttypes.IBCModule, k keeper.Keeper) IBCMiddleware {
return IBCMiddleware{
app: app,
keeper: k,
}
}

// IBCModule interface implementation. Only OnRecvPacket() calls a callback from the keeper. The rest have default implementations

func (im IBCMiddleware) OnChanOpenInit(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID string,
channelID string,
chanCap *capabilitytypes.Capability,
counterparty channeltypes.Counterparty,
version string,
) (string, error) {
return im.app.OnChanOpenInit(ctx, order, connectionHops, portID, channelID, chanCap, counterparty, version)
}

func (im IBCMiddleware) OnChanOpenTry(
ctx sdk.Context,
order channeltypes.Order,
connectionHops []string,
portID, channelID string,
chanCap *capabilitytypes.Capability,
counterparty channeltypes.Counterparty,
counterpartyVersion string,
) (version string, err error) {
return im.app.OnChanOpenTry(ctx, order, connectionHops, portID, channelID, chanCap, counterparty, counterpartyVersion)
}

func (im IBCMiddleware) OnChanOpenAck(
ctx sdk.Context,
portID, channelID string,
counterpartyChannelID string,
counterpartyVersion string,
) error {
return im.app.OnChanOpenAck(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion)
}

func (im IBCMiddleware) OnChanOpenConfirm(ctx sdk.Context, portID, channelID string) error {
return im.app.OnChanOpenConfirm(ctx, portID, channelID)
}

func (im IBCMiddleware) OnChanCloseInit(ctx sdk.Context, portID, channelID string) error {
return im.app.OnChanCloseInit(ctx, portID, channelID)
}

func (im IBCMiddleware) OnChanCloseConfirm(ctx sdk.Context, portID, channelID string) error {
return im.app.OnChanCloseConfirm(ctx, portID, channelID)
}

// OnRecvPacket checks the packet's memo and funds the IPRPC pool accordingly. If the memo is not the expected JSON,
// the packet is transferred normally to the next IBC module in the transfer stack
func (im IBCMiddleware) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) exported.Acknowledgement {
// unmarshal the packet's data with the transfer module codec (expect an ibc-transfer packet)
var data transfertypes.FungibleTokenPacketData
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is breaking other functionality of ibc

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's not because the middleware is only registered under the ibc-transfer stack (see app/app.go lines 711-720). For this reason, the packet must by of type FungibleTokenPacketData. Instead of returning an error I can call the Transfer keeper's OnRecvPacket and it'll return the error (in case the data is not FungibleTokenPacketData). Is it preferable?

if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil {
return channeltypes.NewErrorAcknowledgement(err)
}
oren-lava marked this conversation as resolved.
Show resolved Hide resolved

// extract the packet's memo
memo, err := im.keeper.ExtractIprpcMemoFromPacket(ctx, data)
if errors.Is(err, types.ErrMemoNotIprpcOverIbc) {
// not a packet that should be handled as IPRPC over IBC (not considered as error)
utils.LavaFormatDebug("rewards module IBC middleware processing skipped, memo is invalid for IPRPC over IBC funding",
utils.LogAttr("memo", memo),
)
return im.app.OnRecvPacket(ctx, packet, relayer)
} else if errors.Is(err, types.ErrIprpcMemoInvalid) {
// memo is in the right format of IPRPC over IBC but the data is invalid
utils.LavaFormatWarning("rewards module IBC middleware processing failed, memo data is invalid", err,
utils.LogAttr("memo", memo))
return channeltypes.NewErrorAcknowledgement(err)
}
oren-lava marked this conversation as resolved.
Show resolved Hide resolved

// change the ibc-transfer packet receiver address to be the rewards module address and empty the memo
data.Receiver = im.keeper.GetModuleAddress()
oren-lava marked this conversation as resolved.
Show resolved Hide resolved
data.Memo = ""
marshelledData, err := transfertypes.ModuleCdc.MarshalJSON(&data)
if err != nil {
utils.LavaFormatError("rewards module IBC middleware processing failed, cannot marshal packet data", err,
utils.LogAttr("data", data))
return channeltypes.NewErrorAcknowledgement(err)
}
packet.Data = marshelledData

// call the next OnRecvPacket() of the transfer stack to make the rewards module get the IBC tokens
ack := im.app.OnRecvPacket(ctx, packet, relayer)
omerlavanet marked this conversation as resolved.
Show resolved Hide resolved
if ack == nil || !ack.Success() {
// we check for ack == nil because it means that IBC transfer module did not return an acknowledgement.
// This isn't necessarily an error, but it could indicate unexpected behavior or asynchronous processing
// on the IBC transfer module's side (which returns a non-nil ack when executed without errors). Asynchronous
// processing can be queued processing of packets, interacting with external APIs and more. These can cause
// delays in the IBC-transfer's processing which will make the module return a nil ack until the processing is done.
return ack
Copy link
Collaborator

Choose a reason for hiding this comment

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

in case of ack == nil and async transfer we have funds left over in the account and no pending fund state writes

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done. see 1676677

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

just FYI, after extensive research, the Transfer module itself always return sync acks. Your comment is still correct because we might have in the future an IBC middleware in the transfer stack that could use async acks

}

// set pending IPRPC over IBC requests on-chain
amountInt, ok := sdk.NewIntFromString(data.Amount)
if !ok {
utils.LavaFormatError("rewards module IBC middleware processing failed", fmt.Errorf("cannot decode coin amount"),
utils.LogAttr("data", data))
return channeltypes.NewErrorAcknowledgement(err)
}
amount := sdk.NewCoin(data.Denom, amountInt)
err = im.keeper.SetPendingIprpcOverIbcFunds(ctx, memo, amount)
if err != nil {
return channeltypes.NewErrorAcknowledgement(err)
}

return channeltypes.NewResultAcknowledgement([]byte{byte(1)})
}
oren-lava marked this conversation as resolved.
Show resolved Hide resolved

func (im IBCMiddleware) OnAcknowledgementPacket(
ctx sdk.Context,
packet channeltypes.Packet,
acknowledgement []byte,
relayer sdk.AccAddress,
) error {
return im.app.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer)
}

func (im IBCMiddleware) OnTimeoutPacket(
ctx sdk.Context,
packet channeltypes.Packet,
relayer sdk.AccAddress,
) error {
return im.app.OnTimeoutPacket(ctx, packet, relayer)
}

// ICS4Wrapper interface (default implementations)
func (im IBCMiddleware) SendPacket(ctx sdk.Context, chanCap *capabilitytypes.Capability, sourcePort string, sourceChannel string,
timeoutHeight clienttypes.Height, timeoutTimestamp uint64, data []byte,
) (sequence uint64, err error) {
return im.keeper.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data)
}

func (im IBCMiddleware) WriteAcknowledgement(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet exported.PacketI,
ack exported.Acknowledgement,
) error {
return im.keeper.WriteAcknowledgement(ctx, chanCap, packet, ack)
}

func (im IBCMiddleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) {
return im.keeper.GetAppVersion(ctx, portID, channelID)
}
6 changes: 6 additions & 0 deletions x/rewards/keeper/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import (
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types"
"github.com/lavanet/lava/testutil/common"
testkeeper "github.com/lavanet/lava/testutil/keeper"
"github.com/lavanet/lava/testutil/sample"
commontypes "github.com/lavanet/lava/utils/common/types"
"github.com/lavanet/lava/utils/sigs"
planstypes "github.com/lavanet/lava/x/plans/types"
Expand Down Expand Up @@ -140,6 +142,10 @@ func (ts *tester) setupForIprpcTests(fundIprpcPool bool) {
}
}

func (ts *tester) createIbcTransferPacketData(memo string) transfertypes.FungibleTokenPacketData {
return transfertypes.NewFungibleTokenPacketData(ts.TokenDenom(), "100000", sample.AccAddress(), sample.AccAddress(), memo)
}
oren-lava marked this conversation as resolved.
Show resolved Hide resolved

// getConsumersForIprpcSubTest is a helper function specifically for the TestIprpcEligibleSubscriptions unit test
// this function returns two consumer addresses to test depending on the input mode:
//
Expand Down
79 changes: 79 additions & 0 deletions x/rewards/keeper/ibc_iprpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package keeper

import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"
transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types"
"github.com/lavanet/lava/utils"
"github.com/lavanet/lava/utils/decoder"
"github.com/lavanet/lava/x/rewards/types"
)

/*

The rewards module (which acts as an IBC middleware) analyzes incoming ibc-transfer packets and checks their memo field.
If the memo field is in the IPRPC over IBC format, it uses the tokens from the packet and saves them for a future fund to
the IPRPC pool.

An example of the expected IPRPC over IBC memo field:
{
"iprpc": {
"creator": "my-moniker",
"spec": "ETH1",
"duration": 3
}
}

The tokens will be transferred to the pool once the minimum IPRPC funding fee is paid. In the meantime, the IPRPC over IBC
funds are saved in the IbcIprpcFund scaffolded map.

*/

// ExtractIprpcMemoFromPacket extracts the memo field from an ibc-transfer packet and verifies that it's in the right format
// and holds valid values. If the memo is not in the right format, a custom error is returned so the packet will be skipped and
// passed to the next IBC module in the transfer stack normally (and not return an error ack)
func (k Keeper) ExtractIprpcMemoFromPacket(ctx sdk.Context, transferData transfertypes.FungibleTokenPacketData) (types.IprpcMemo, error) {
var memo types.IprpcMemo
err := decoder.Decode(transferData.Memo, "iprpc", &memo, nil, nil, nil)
if err != nil {
// memo is not for IPRPC over IBC, return custom error to skip processing for this packet
return types.IprpcMemo{}, types.ErrMemoNotIprpcOverIbc
}
err = k.validateIprpcMemo(ctx, memo)
if err != nil {
return types.IprpcMemo{}, err
}

return memo, nil
}
oren-lava marked this conversation as resolved.
Show resolved Hide resolved

func (k Keeper) validateIprpcMemo(ctx sdk.Context, memo types.IprpcMemo) error {
if _, found := k.specKeeper.GetSpec(ctx, memo.Spec); !found {
return printInvalidMemoWarning(memo, "memo's spec does not exist on chain")
}

if memo.Creator == "" {
return printInvalidMemoWarning(memo, "memo's creator cannot be empty")
} else if _, found := k.specKeeper.GetSpec(ctx, memo.Creator); found {
return printInvalidMemoWarning(memo, "memo's creator cannot be an on-chain spec index")
}

if memo.Duration == uint64(0) {
return printInvalidMemoWarning(memo, "memo's duration cannot be zero")
}

return nil
}
oren-lava marked this conversation as resolved.
Show resolved Hide resolved

func printInvalidMemoWarning(memo types.IprpcMemo, description string) error {
utils.LavaFormatWarning("invalid ibc over iprpc memo", fmt.Errorf(description),
utils.LogAttr("memo", memo.String()),
)
return types.ErrIprpcMemoInvalid
}

func (k Keeper) SetPendingIprpcOverIbcFunds(ctx sdk.Context, memo types.IprpcMemo, amount sdk.Coin) error {
// TODO: implement
return nil
}
oren-lava marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading