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

Update Middleware with Best Practices #903

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
57 changes: 57 additions & 0 deletions spec/app/ics-030-middleware/BEST_PRACTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Best Practices When Designing Middleware
Copy link
Contributor

Choose a reason for hiding this comment

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

Does the spec repo have any style conventions? We use the google docs style convention on ibc-go which states only the first word in a title is capitalized


## Namespacing the Stack

Each layer of the middleware should reserve a unique name in the stack so that it can key into specific information it needs in the version, packet data, or acknowledgement. Since this will not be in the portID which is shared across the stack, it will not be enforced by IBC. The Cosmos-SDK, for example, can accomplish uniqueness by using the ModuleName to reference the different middleware in the stack.

## Version Negotiation For Middleware

Middleware will only need to negotiate versions if they expect the counterparty chain to have compatible middleware on the stack as well. Middleware that can execute unilaterally **do not** need version negotiation.

Middleware must take care to ensure that the application logic can execute its own version negotiation without interference from the nesting middleware. In order to do this, the middleware will format the version in a JSON-encoded string. Any information intended for the middleware will be keyed on the middleware namespace. The version intended for the underlying app will be keyed on `"app_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. The version values themselves may be more complex than static strings, they can be JSON encoded structs with multiple fields that the counterparties must agree on. The format of the version string is as follows:

```json
{
"<middleware_name>": "<middleware_version_value>",
"app_version": "<application_version_value>",
}
```
Comment on lines +13 to +18
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there an issue tracking the work of making the version/packet data/ack a flat map? I think that improvement would be a really nice dev UX improvement


The `<middleware_name>` key in the JSON struct should be replaced by the actual name of the key for the corresponding middleware (e.g. `fee` for ICS-29 fee middleware).

In the application callbacks, the middleware can unmarshal the version string and retrieve the middleware and application versions. It must do its own version negotiation on `<middleware_version_value>` and then hand over `<application_version_value>` to the nested application's callback. This is only relevant if the middleware expects a compatible counterparty middleware at the same level on the counterparty stack. Middleware that only executes on a single side of the channel MUST NOT modify the channel version.

## Packet Data Structuring For Middleware

Packet senders may choose to send input data not just to the base application but also the middlewares in the stack. The packet data should be structured in the same way that the middleware stack is; i.e. nested from the top level middleware to the base application.

Similar to the version negotiation, the `app_packet_data` may be marshalling packet data for underlying middleware as well.

```json
{
"<middleware_name>": "<middleware_packet_data>",
"app_packet_data": "<application_packet_data>",
}
```
Comment on lines +24 to +35
Copy link
Member

Choose a reason for hiding this comment

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

The OnRecvPacket callbacks for ibc apps accept the arg channeltypes.Packet. That means we would have to reassign packet.Data bytes to AppPacketData before calling the next handler in the stack.

If we consider the OnRecvPacket callback of ics29 fee middleware and imagine that there is some middleware specific packet data encoded within the packet we would have to do something like:

var packetData types.FeePacketData
if err := cdc.Unmarshal(packet.GetData(), packetData); err != nil {
    // handle err or pass through to underlying app
}

// do whatever logic with fee specific packet data
handleFeePacketData(packetData.FeePacketData)

// reassign packet data bytes before passing packet to next handler
packet.Data = packetData.AppPacketData

return im.app.OnRecvPacket(ctx, packet, relayer)

Would this be the intended usage here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm yes, this is a good point. Correct this will be necessary, but again only for middleware that wish to modify the packet data

Copy link
Contributor

Choose a reason for hiding this comment

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

The OnRecvPacket packet interface would need to be updated within the spec if we are going to allow packet data nesting as a best practice?


On `SendPacket`, the execution flow goes from the base application to the top layer middleware. Thus at each step, the middleware can choose to wrap the application packet data with its own packet data before returning to the `ICS4Wrapper`.

On `ReceivePacket`, the middleware must process its own packet data, and then pass the `app_packet_data` to the underlying application. It may also choose to use the `app_packet_data` as part of its logic execution if it knows how to unmarshal the underlying data.

The state machine must provide a way for users to pass in input data that can be marshalled into the full nested packet data. This will enable users to send data not just to the base application but also to higher-level middleware(s). Since this is state-machine specific, it will not be specified here.

## Acknowledgement Structuring For Middleware

Middleware may also add on to acknowledgements. This will be done in the exact same way as version and packet data.

```json
{
"<middleware_name>": "<middleware_acknowledgement",
"app_acknowledgement": "<app_acknowledgement>",
}
```

On `WriteAcknowledgment`, the execution flow goes from the base application to the top layer middleware. Thus at each step, the middleware can choose to wrap the application acknowledgement with its own acknowledgement before returning to the `ICS4Wrapper`.

On `AcknowledgePacket`, the middleware must process its own acknowledgement, and then pass the `app_acknowledgement` to the underlying application. It may also choose to use the `app_acknowledgement` as part of its logic execution if it knows how to unmarshal the underlying data.

28 changes: 14 additions & 14 deletions spec/app/ics-030-middleware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,6 @@ In order to function as IBC Middleware, a module must implement the IBC applicat

When nesting an application, the module must make sure that it is in the middle of communication between core IBC and the application in both directions. Developers should do this by registering the top-level module directly with the IBC router (not any nested applications). The nested applications in turn, must be given access only to the middleware's `WriteAcknowledgement` and `SendPacket` rather than to the core IBC handlers directly.

Additionally, the middleware must take care to ensure that the application logic can execute its own version negotiation without interference from the nesting middleware. In order to do this, the middleware will format the version in a JSON-encoded string containing the middleware version and the application version (and potentially also other custom parameter fields). 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. The format of the version string is as follows:

```json
{
"<middleware_version_key>": "<middleware_version_value>",
"app_version": "<application_version_value>",
// ... other custom parameter fields
}
```

The `<middleware_version_key>` key in the JSON struct should be replaced by the actual name of the key for the corresponding middleware (e.g. `fee_version` for ICS-29 fee middleware).

In the application callbacks, the middleware can unmarshal the version string and retrieve the middleware and application versions. It must do its own version negotiation on `<middleware_version_value>` and then hand over `<application_version_value>` to the nested application's callback. This is only relevant if the middleware expects a compatible counterparty middleware at the same level on the counterparty stack. Middleware that only executes on a single side of the channel MUST NOT modify the channel version.

Each application stack must reserve its own unique port with core IBC. Thus two stacks with the same base application must bind to separate ports.

#### Interfaces
Expand All @@ -74,6 +60,9 @@ interface Middleware extends ICS26Module {
// middleware has access to ICS4Wrapper which may be core IBC Channel Handler
// or a higher-level middleware that wraps this middleware.
ics4Wrapper: ICS4Wrapper
// app wrapper provides access to the base layer application in order to retreive
// the base application's version, packet data, and acknowledgement
appWrapper: AppWrapper
}
```

Expand All @@ -94,6 +83,17 @@ interface ICS4Wrapper {
}
```

```typescript
// AppWrapper is an interface designed to get information on the base IBC application
// regardless of where you are in the current stack. This will allow middleware to act
// on the base application data
interface AppWrapper {
getAppVersion(portID: Identifier, channelId: Identifier, version: string): (string)
getAppPacketData(portID: Identifier, channelId: Identifier, packetData: bytes): (bytes)
getAppAcknowledgement(portID: Identifier, channelId: Identifier, acknowledgement: bytes): (bytes)
}
Comment on lines +90 to +94
Copy link
Contributor

Choose a reason for hiding this comment

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

How do these functions work exactly? any app in the stack would call the app above it?

```

#### Handshake Callbacks

```typescript
Expand Down