From e5f3b45a34a1aac889a8a718e9690c8d067f5397 Mon Sep 17 00:00:00 2001 From: Carlos Rodriguez Date: Tue, 5 Jul 2022 21:49:33 +0200 Subject: [PATCH] ICS-29: updates based on current implementation (#779) * update spec based on current implementation * fix typo * remove duplicated word * review comments and some formatting * fix typo and add extra clarification * fix alignment * add quotes around placeholder values * review comments * formatting * nit Co-authored-by: Carlos Rodriguez --- spec/app/ics-029-fee-payment/README.md | 329 +++++++++++++++++-------- 1 file changed, 223 insertions(+), 106 deletions(-) diff --git a/spec/app/ics-029-fee-payment/README.md b/spec/app/ics-029-fee-payment/README.md index 69bd9e82c..10ca3377d 100644 --- a/spec/app/ics-029-fee-payment/README.md +++ b/spec/app/ics-029-fee-payment/README.md @@ -19,8 +19,8 @@ application, without forcing other applications to use this implementation. ### Motivation There has been much discussion on a general incentivization mechanism for relayers. A simple proposal was created to -[extend ICS20 to incentivize relaying](https://github.com/cosmos/ibc/pull/577) on the destination chain. However, -it was very specific to ICS20 and would not work for other protocols. This was then extended to a more +[extend ICS-20 to incentivize relaying](https://github.com/cosmos/ibc/pull/577) on the destination chain. However, +it was very specific to ICS-20 and would not work for other protocols. This was then extended to a more [general fee payment design](https://github.com/cosmos/ibc/issues/578) that could be adopted by any ICS application protocol. @@ -48,7 +48,13 @@ define a clear interface that can be easily adopted by any application, but not `timeout relayer`: The relayer that submits the `timeoutPacket` or `timeoutOnClose` message for a given packet -`source address`: The address of a relayer on the chain that sent the packet +`receive fee`: The fee paid for submitting the `recvPacket` message for a given packet + +`ack fee`: The fee paid for submitting the `acknowledgePacket` message for a given packet + +`timeout fee`: The fee paid for submitting the `timeoutPacket` or `timeoutOnClose` message for a given packet + +`source address`: The payee address selected by a relayer on the chain that sent the packet `destination address`: The address of a relayer on the chain that receives the packet @@ -88,7 +94,7 @@ Alternate flow: ### Fee details For an example implementation in the Cosmos SDK, we consider 3 potential fee payments, which may be defined. Each one may be -paid out in a different token. Imagine a connection between IrisNet and the Cosmos Hub. They may define: +paid out in a different token. Imagine a connection between IrisNet and the Cosmos Hub. To incentivize a packet from IrisNet to the Cosmos Hub, they may define: - ReceiveFee: 0.003 channel-7/ATOM vouchers (ATOMs already on IrisNet via ICS20) - AckFee: 0.001 IRIS @@ -96,15 +102,43 @@ paid out in a different token. Imagine a connection between IrisNet and the Cosm Ideally the fees can easily be redeemed in native tokens on both sides, but relayers may select others. In this example, the relayer collects a fair bit of IRIS, covering its costs there and more. It also collects channel-7/ATOM vouchers from many packets. After relaying a few thousand packets, the account on the Cosmos Hub is running low, so the relayer will send those channel-7/ATOM vouchers back over channel-7 to it's account on the Hub to replenish the supply there. -The sender chain will escrow 0.003 channel-7/ATOM and 0.002 IRIS from the fee payers' account. In the case that a forward relayer submits the `recvPacket` and a reverse relayer submits the `ackPacket`, the forward relayer is rewarded 0.003 channel-7/ATOM and the reverse relayer is rewarded 0.001 IRIS while 0.001 IRIS is refunded to the original fee payer. In the case where the packet times out, the timeout relayer receives 0.002 IRIS and 0.003 channel-7/ATOM is refunded to the original fee payer. +The sender chain will escrow 0.003 channel-7/ATOM and 0.002 IRIS from the fee payers' account. In the case that a forward relayer submits the `recvPacket` and a reverse relayer submits the `ackPacket`, the forward relayer is rewarded 0.003 channel-7/ATOM and the reverse relayer is rewarded 0.001 IRIS while 0.002 IRIS is refunded to the original fee payer. In the case where the packet times out, the timeout relayer receives 0.002 IRIS and 0.003 channel-7/ATOM is refunded to the original fee payer. The logic involved in collecting fees from users and then paying it out to the relevant relayers is encapsulated by a separate fee module and may vary between implementations. However, all fee modules must implement a uniform interface such that the ICS-4 handlers can correctly pay out fees to the right relayers, and so that relayers themselves can easily determine the fees they can expect for relaying a packet. + +### Data Structures + +The incentivized acknowledgment written on the destination chain includes: +- raw bytes of the acknowledgement from the underlying application, +- the source address of the forward relayer, +- and a boolean indicative of receive operation success on the underlying application. + +```typescript +interface Acknowledgement { + appAcknowledgement: []byte + forwardRelayerAddress: string + underlyingAppSuccess: boolean +} +``` + +### Store Paths + +#### Relayer Address for Async Ack Path + +The forward relayer addresses are stored under a store path prefix unique to a combination of port identifier, channel identifier and sequence. This may be stored in the private store. + +```typescript +function relayerAddressForAsyncAckPath(packet: Packet): Path { + return "forwardRelayer/{packet.destinationPort}/{packet.destinationChannel}/{packet.sequence}" +} +``` + ### Fee Middleware Contract -While the details may vary between fee modules, all Fee modules **must** ensure it does the following: +While the details may vary between fee modules, all fee modules **must** ensure they does the following: -- It must allow relayers to register their counterparty address. +- It must allow relayers to register their counterparty payee address (i.e. source address). - It must have in escrow the maximum fees that all outstanding packets may pay out (or it must have ability to mint required amount of tokens) - It must pay the receive fee for a packet to the forward relayer specified in `PayFee` callback (if unspecified, it must refund forward fee to original fee payer(s)) - It must pay the ack fee for a packet to the reverse relayer specified in `PayFee` callback @@ -112,23 +146,31 @@ While the details may vary between fee modules, all Fee modules **must** ensure - It must refund any remainder fees in escrow to the original fee payer(s) if applicable ```typescript -// RegisterCounterpartyAddress is called by the relayer on each channelEnd and allows them to specify their counterparty address before relaying -// This ensures they will be properly compensated for forward relaying since destination chain must send back relayer's source address (counterparty address) in acknowledgement -// This function may be called more than once by relayer, in which case, latest counterparty address is always used. -function RegisterCounterpartyAddress(address: string, counterPartyAddress: string) { - // set mapping between address and counterparty address +// RegisterCounterpartyPayee is called by the relayer on each channelEnd and +// allows them to specify their counterparty payee address before relaying. +// This ensures they will be properly compensated for forward relaying since +// destination chain must send back relayer's source address (counterparty +// payee address) in acknowledgement. +// This function may be called more than once by relayer, in which case, latest +// counterparty payee address is always used. +function RegisterCounterpartyPayee(relayer: string, counterPartyAddress: string) { + // set mapping between relayer address and counterparty payee address } -// EscrowPacketFee is an open callback that may be called by any module/user that wishes to escrow funds in order to -// incentivize the relaying of the given packet. -// NOTE: These fees are escrowed in addition to any previously escrowed amount for the packet. In the case where the previous amount is zero, -// the provided fees are the initial escrow amount. +// EscrowPacketFee is an open callback that may be called by any module/user +// that wishes to escrow funds in order to incentivize the relaying of the +// given packet. +// NOTE: These fees are escrowed in addition to any previously escrowed amount +// for the packet. In the case where the previous amount is zero, the provided +// fees are the initial escrow amount. // They may set a separate receiveFee, ackFee, and timeoutFee to be paid -// for each step in the packet flow. The caller must send max(receiveFee+ackFee, timeoutFee) to the fee module to be locked -// in escrow to provide payout for any potential packet flow. -// The caller may optionally specify an array of relayer addresses. This MAY be used by the fee module to modify fee payment logic -// based on ultimate relayer address. For example, fee module may choose to only pay out relayer if the relayer address was specified in -// the `EscrowPacketFee`. +// for each step in the packet flow. The caller must send max(receiveFee+ackFee, timeoutFee) +// to the fee module to be locked in escrow to provide payout for any potential +// packet flow. +// The caller may optionally specify an array of relayer addresses. This MAY be +// used by the fee module to modify fee payment logic based on ultimate relayer +// address. For example, fee module may choose to only pay out relayer if the +// relayer address was specified in the `EscrowPacketFee`. function EscrowPacketFee(packet: Packet, receiveFee: Fee, ackFee: Fee, timeoutFee: Fee, relayers: []string) { // escrow max(receiveFee+ackFee, timeoutFee) for this packet // do custom logic with provided relayer addresses if necessary @@ -153,7 +195,7 @@ function PayTimeoutFee(packet: Packet, timeout_relayer: string) { The fee module should also expose the following queries so that relayers may query their expected fee: ```typescript -// Gets the fee expected for submitting ReceivePacket msg for the given packet +// Gets the fee expected for submitting RecvPacket msg for the given packet // Caller should provide the intended relayer address in case the fee is dependent on specific relayer(s). function GetReceiveFee(portID, channelID, sequence, relayer) Fee @@ -185,11 +227,17 @@ In this way, custom fee-handling logic can be hooked up to the IBC packet flow l #### Fee Protocol Negotiation -The fee middleware will negotiate its fee protocol version with the counterparty module by prepending its own version to the application version. +The fee middleware will negotiate its fee protocol version with the counterparty module by including its own version next to the application version. The channel version will be a string of a JSON struct containing the fee middleware version and the application version. The application version may as well be a JSON-encoded string, possibly including further middleware and app versions, if the application stack consists of multiple milddlewares wrapping a base application. -Channel Version: `fee_v{fee_protocol_version}:{application_version}` +Channel Version: +```json +{"fee_version":"","app_version":""} +``` -Ex: `fee_v1:ics20-1` +Ex: +```json +{"fee_version":"ics29-1","app_version":"ics20-1"} +``` The fee middleware's handshake callbacks ensure that both modules agree on compatible fee protocol version(s), and then pass the application-specific version string to the embedded application's handshake callbacks. @@ -203,53 +251,101 @@ function onChanOpenInit( channelIdentifier: Identifier, counterpartyPortIdentifier: Identifier, counterpartyChannelIdentifier: Identifier, - version: string) { - // remove the prefix and pass the app-specific version to app callback. + version: string): (version: string, err: Error) { + // try to unmarshal JSON-encoded version string and pass + // the app-specific version to app callback. // otherwise, pass version directly to app callback. - feeVersion, appVersion = splitFeeVersion(version) + metadata, err = UnmarshalJSON(version) + if err != nil { + // call the underlying applications OnChanOpenInit callback + return app.onChanOpenInit( + order, + connectionHops, + portIdentifier, + channelIdentifier, + counterpartyPortIdentifier, + counterpartyChannelIdentifier, + version, + ) + } + // check that feeVersion is supported - if !isSupported(feeVersion) { - return error + if !isSupported(metadata.feeVersion) { + return "", error } - app.OnChanOpenInit( + // call the underlying applications OnChanOpenInit callback. + // if the version string is empty, OnChanOpenInit is expected to return + // a default version string representing the version(s) it supports + appVersion, err = app.onChanOpenInit( order, connectionHops, portIdentifier, channelIdentifier, counterpartyPortIdentifier, counterpartyChannelIdentifier, - appVersion, + metadata.appVersion, ) + if err != nil { + return "", err + } + + // a new version string is constructed with the app version returned + // by the underlying application, in case it is different than the + // one passed by the caller + version = constructVersion(metadata.MiddlewareVersion, appVersion) + + return version, nil } -function OnChanOpenTry( +function onChanOpenTry( order: ChannelOrder, connectionHops: [Identifier], portIdentifier: Identifier, channelIdentifier: Identifier, counterpartyPortIdentifier: Identifier, counterpartyChannelIdentifier: Identifier, - version: string, - counterpartyVersion: string) { - // select mutually compatible fee version - cpFeeVersion, cpAppVersion = splitFeeVersion(counterpartyVersion) - feeVersion, appVersion = splitFeeVersion(version) - if !isCompatible(cpFeeVersion, feeVersion) { - return error - } - selectFeeVersion(cpFeeVersion, feeVersion) - - // call the underlying applications OnChanOpenTry callback - app.OnChanOpenTry( - order, - connectionHops, - portIdentifier, - channelIdentifier, - counterpartyPortIdentifier, - counterpartyChannelIdentifier, - cpAppVersion, - appVersion, - ) + counterpartyVersion: string): (version: string, err: Error) { + // select mutually compatible fee version + cpMetadata, err = UnmarshalJSON(counterpartyVersion) + if err != nil { + // call the underlying applications OnChanOpenTry callback + return app.onChanOpenTry( + order, + connectionHops, + portIdentifier, + channelIdentifier, + counterpartyPortIdentifier, + counterpartyChannelIdentifier, + counterpartyVersion, + ) + } + + if !isCompatible(cpMetadata.feeVersion) { + return "", error + } + feeVersion = selectFeeVersion(cpMetadata.feeVersion) + + // call the underlying applications OnChanOpenTry callback + appVersion, err = app.onChanOpenTry( + order, + connectionHops, + portIdentifier, + channelIdentifier, + counterpartyPortIdentifier, + counterpartyChannelIdentifier, + cpMetadata.appVersion, + ) + if err != nil { + return "", err + } + + // a new version string is constructed with the finall fee version + // that is selected and the app version returned by the underlying + // application (which may be differents different than the one + // passed by the caller + version = constructVersion(feeVersion, appVersion) + + return version, nil } function onChanOpenAck( @@ -257,34 +353,35 @@ function onChanOpenAck( channelIdentifier: Identifier, counterpartyChannelIdentifier: Identifier, counterpartyVersion: string) { - feeVersion, appVersion = splitFeeVersion(counterpartyVersion) - if !isSupported(feeVersion) { - return error - } + cpMetadata, err = UnmarshalJSON(counterpartyVersion) + if err != nil { + // call the underlying applications OnChanOpenAck callback + return app.onChanOpenAck( + portIdentifier, + channelIdentifier, + counterpartyChannelIdentifier, + counterpartyVersion, + ) + } - // call the underlying applications OnChanOpenAck callback - app.OnChanOpenAck(portIdentifier, channelIdentifier, appVersion) + if !isSupported(cpMetadata.feeVersion) { + return error + } + // call the underlying applications OnChanOpenAck callback + return app.onChanOpenAck( + portIdentifier, + channelIdentifier, + counterpartyChannelIdentifier, + cpMetadata.appVersion, + ) } function onChanOpenConfirm( portIdentifier: Identifier, channelIdentifier: Identifier) { - // fee middleware performs no-op on ChanOpenConfirm, - // just call underlying callback - app.onChanOpenConfirm(portIdentifier, channelIdentifier) - } - -function splitFeeVersion(version: string): []string { - if hasPrefix(version, "fee") { - splitVersions = split(version, ":") - feeVersion = version[0] - appVersion = join(version[1:], ":") - // if version has fee prefix - // return first split as fee version and the rest of the string as app version - return []string{feeVersion, appVersion} - } - // otherwise return an empty fee version and full version as app version - return []string{"", version} + // fee middleware performs no-op on ChanOpenConfirm, + // just call underlying callback + return app.onChanOpenConfirm(portIdentifier, channelIdentifier) } ``` @@ -294,28 +391,36 @@ function splitFeeVersion(version: string): []string { function onRecvPacket(packet: Packet, relayer: string): bytes { app_acknowledgement = app.onRecvPacket(packet, relayer) - // get source address by retrieving counterparty address of this relayer stored in fee middleware. - // NOTE: source address may be empty or invalid, counterparty - // must refund fee in these cases - sourceAddress = getCounterpartyAddress(relayer) - - // in case of asynchronous acknowledgement, we must store the relayer address so that we can retrieve it later to write the acknowledgement. + // in case of asynchronous acknowledgement, we must store the relayer + // address. It will be retrieved later and used to get the source + // address that will be written in the acknowledgement. if app_acknowledgement == nil { - privateStore.set(forwardRelayerPath(packet), sourceAddress) + privateStore.set(relayerAddressForAsyncAckPath(packet), relayer) } + // get source address by retrieving counterparty payee address of + // this relayer stored in fee middleware. + // NOTE: source address may be empty or invalid, counterparty + // must refund fee in these cases + sourceAddress = getCounterpartyPayeeAddress(relayer) + // wrap the acknowledgement with forward relayer and return marshalled bytes - // constructIncentivizedAck takes the app-specific acknowledgement and receive-packet relayer (forward relayer) - // and constructs the incentivized acknowledgement struct with the forward relayer and app-specific acknowledgement embedded. - ack = constructIncentivizedAck(app_acknowledgment, sourceAddress) + // constructIncentivizedAck takes: + // - the app-specific acknowledgement, + // - the receive-packet relayer (forward relayer) + // - and a boolean indicative of receive operation success, + // and constructs the incentivized acknowledgement struct with + // the forward relayer and app-specific acknowledgement embedded. + ack = constructIncentivizedAck(app_acknowledgment, sourceAddress, app_acknowledgment.success) return marshal(ack) } function onAcknowledgePacket(packet: Packet, acknowledgement: bytes, relayer: string) { - // the acknowledgement is a marshalled struct containing the forward relayer address as a string (called forward_relayer), - // and the raw acknowledgement bytes returned by the counterparty application module (called app_ack). + // the acknowledgement is a marshalled struct containing: + // - the forward relayer address as a string (called forward_relayer) + // - and the raw acknowledgement bytes returned by the counterparty application module (called app_ack). - // get the forward relayer from the acknowledgement + // get the forward relayer from the (incentivized) acknowledgement // and pay fees to forward and reverse relayers. // reverse_relayer is submitter of acknowledgement message // provided in function arguments @@ -324,7 +429,8 @@ function onAcknowledgePacket(packet: Packet, acknowledgement: bytes, relayer: st forward_relayer = getForwardRelayer(ack) PayFee(packet, forward_relayer, relayer) - // unwrap the raw acknowledgement bytes sent by counterparty application and pass it to the application callback. + // unwrap the raw acknowledgement bytes sent by counterparty application + // and pass it to the application callback. app_ack = getAppAcknowledgement(acknowledgement) app.OnAcknowledgePacket(packet, app_ack, relayer) @@ -346,16 +452,23 @@ function onTimeoutPacketClose(packet: Packet, relayer: string) { app.onTimeoutPacketClose(packet, relayer) } -function constructIncentivizedAck(app_ack: bytes, forward_relayer: string): Acknowledgement { - // TODO: see https://github.com/cosmos/ibc/pull/582 +function constructIncentivizedAck( + app_ack: bytes, + forward_relayer: string, + success: boolean): Acknowledgement { + return Acknowledgement{ + appAcknowledgement: app_ack, + forwardRelayerAddress: relayer, + underlyingAppSuccess: success, + } } function getForwardRelayer(ack: Acknowledgement): string { - // TODO: see https://github.com/cosmos/ibc/pull/582 + ack.forwardRelayerAddress } function getAppAcknowledgement(ack: Acknowledgement): bytes { - // TODO: see https://github.com/cosmos/ibc/pull/582 + ack.appAcknowledgement } ``` @@ -368,10 +481,13 @@ Note that if the embedded application uses asynchronous acks then, the `WriteAck function writeAcknowledgement( packet: Packet, acknowledgement: bytes) { - // retrieve the forward relayer that was stored in `onRecvPacket` - relayer = privateStore.get(forwardRelayerPath(packet)) - ack = constructIncentivizedAck(acknowledgment, relayer) - ack_bytes marshal(ack) + // retrieve the relayer that was stored in `onRecvPacket` + relayer = privateStore.get(relayerAddressForAsyncAckPath(packet)) + // get source address by retrieving counterparty payee address + // of this relayer stored in fee middleware. + sourceAddress = getCounterpartyPayeeAddress(relayer) + ack = constructIncentivizedAck(acknowledgment, sourceAddress, acknowledgment.success) + ack_bytes = marshal(ack) // ics4Wrapper may be core IBC or higher-level middleware return ics4Wrapper.writeAcknowledgement(packet, ack_bytes) } @@ -399,7 +515,7 @@ function sendPacket( **User sending Packets** -A user may specify a fee to incentivize the relaying during packet submission, by submitting a fee payment message atomically with the application-specific "send packet" message (e.g. ICS-20 MsgTransfer). The fee middleware will escrow the fee for the packet that is created atomically with the escrow. The fee payment message itself is not specified in this document as it may vary greatly across implementations. In some middleware, there may be no fee payment message at all if the fees are being paid out from an altruistic pool. +A user may specify a fee to incentivize the relaying during packet submission, by submitting a fee payment message atomically with the application-specific "send packet" message (e.g. ICS-20 `MsgTransfer`). The fee middleware will escrow the fee for the packet that is created atomically with the escrow. The fee payment message itself is not specified in this document as it may vary greatly across implementations. In some middleware, there may be no fee payment message at all if the fees are being paid out from an altruistic pool. Since the fee middleware does not need to modify the outgoing packet, the fee payment message may be placed before or after the send packet message. However in order to maintain consistency with other middleware messages, it is recommended that fee middleware require their messages to be placed before the send packet message and escrow fees for the **next sequence** on the given channel. This way when the messages are atomically committed, the next sequence on the channel is the send packet message sent by the user, and the user escrows their fee for the created packet. @@ -410,17 +526,17 @@ In case a user wants to pay fees on a packet after it has already been created, Before a relayer starts relaying on a channel, they should register their counterparty message using the standardized message: ```typescript -interface RegisterCounterpartyAddressMsg { - channelID: string +interface RegisterCounterpartyPayeeMsg { portID: string - counterpartyAddress: string - address: string + channelID: string + relayer: string // destination address of the forward relayer + counterpartyPayee: string // source address of the forward relayer } ``` -It is the responsibility of the receiving chain to authenticate that the message was received from owner of `address`. The receiving chain must store the mapping from: `address -> counterpartyAddress` for the given channel. Then, `onRecvPacket` of the destination fee middleware can query for the counterparty address of the `recvPacket` message sender in order to get the source address of the forward relayer. This source address is what will get embedded in the acknowledgement. +It is the responsibility of the receiving chain to authenticate that the message was received from owner of `relayer`. The receiving chain must store the mapping from: `relayer -> counterpartyPayee` for the given channel. Then, `onRecvPacket` of the destination fee middleware can query for the counterparty payee address of the `recvPacket` message sender in order to get the source address of the forward relayer. This source address is what will get embedded in the acknowledgement. -If the relayer does not register their counterparty address, or registers an invalid address; the acknowledgment will still be received and processed but the forward fee will be refunded to the original fee payer(s). +If the relayer does not register their counterparty payee address (or registers an invalid address), then the acknowledgment will still be received and processed but the forward fee will be refunded to the original fee payer(s). #### Backwards Compatibility @@ -436,7 +552,7 @@ This proposal satisfies the desired properties. All parts of the packet flow (re ##### Correctness -The fee module is responsible for correctly escrowing and distributing funds to the provided relayers. The ack and timeout relayers are trivially retrievable since they are the senders of the acknowledgment and timeout message. The forward relayer is responsible for registering their sourcer address before sending `recvPacket` messages, so that the destination fee middleware can embed this address in the acknowledgement. The fee middleware on source will then use the address in acknowledgement to pay the forward relayer on the source chain. +The fee module is responsible for correctly escrowing and distributing funds to the provided relayers. The ack and timeout relayers are trivially retrievable since they are the senders of the acknowledgment and timeout message. The forward relayer is responsible for registering their source address before sending `recvPacket` messages, so that the destination fee middleware can embed this address in the acknowledgement. The fee middleware on source will then use the address in acknowledgement to pay the forward relayer on the source chain. The source chain will use a "best efforts" approach with regard to the forward relayer address. Since it is not verified directly by the counterparty and is instead just treated as a string to be passed back in the acknowledgement, the registered forward relayer source address may not be a valid source chain address. In this case, the invalid address is discarded, the receive fee is refunded, and the acknowledgement processing continues. It is incumbent on relayers to register their source addresses to the counterparty chain correctly. In the event that the counterparty chain itself incorrectly sends the forward relayer address, this will cause relayers to not collect fees on source chain for relaying packets. The incentivize-driven relayers will stop relaying for the chain until the acknowledgement logic is fixed, however the channel remains functional. @@ -464,6 +580,7 @@ Coming soon. ## History June 8 2021 - Switched to middleware solution from implementing callbacks in ICS-4 directly. + June 1 2021 - Draft written ## Copyright