-
Notifications
You must be signed in to change notification settings - Fork 208
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
base: main
Are you sure you want to change the base?
feat: IPRPC over IBC #1446
Changes from 12 commits
5008a8f
d3c5144
10e1650
14559ca
5f038ba
e796926
54f7b0d
c5b5020
1723bb7
6a3ccb7
0ad3878
a331cd9
154a420
f1a7a37
1676677
fc25eb2
b9904e9
98110fe
bbd5416
0b5f338
4cedfe7
04865f1
d853bff
ee68fb7
894dfee
b352fcd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done. see 1676677 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
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
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?