Roast IBC Transport (TAO) Primitives

The transport (TAO) or core IBC specification is an attempt to define common abstractions for interoperability.

Here is a high level overview of the common abstractions defined in the IBC spec:

  • Clients: Arbitrary logic to prove state that lives on one chain on another
  • Connections: Chain <> chain conversations as an instance of a client type
  • Channels: Smart contract <> smart contract conversations over a connection instance
  • Ports: Chain agnostic addressing format
  • Packets: Data

To ensure that we’re building the right thing, it’s important to actively question everything.

This is an open call to anyone and everyone to come and roast IBC’s transport primitives. Let the roast begin!!!

3 Likes

Soft roast: I think we should question connections - why do we really need them and the way channels are right now could be improved.

For connections, from my understanding the main purpose with the connection handshake is to verify that the clients the connection is built on are as each chain is expecting their counterparty to be represented through the client and defining the possible channels that can be built on top of the connection - why can’t this just be done at the client layer?

For channels, it could be interesting to have more flexibility and allow packets sent and received from different application modules to use the same channel so there could be some kind of routing within a channel rather than requiring different channels for each application and this could be something for packets if you could send data for different applications within a single packet

3 Likes

For me, it’s the inflexibility of channels where only one endpoint can be connected to a channel. As a result, channels need to be initialised, opened, and confirmed with each pair-wise module/smart contract pairing. With the advent of ICA and other primitives that programmatically multiply the endpoints that require channel handshakes, this is unnecessarily burdensome to create channels at scale.

3 Likes

Completely agree. The notion of a client entails self-authentication and handshakes require a massive amount of machinery that needs to be rebuilt in each new environment. Version negotiation is great, but can be done at another layer.

1 Like

What I want to roast is that these IBC abstractions demand too much effort. The abstractions themselves are actually right. The problem is they are all necessary before a team can deliver any tangible user impact, prototype, or demo. Note that the list above doesn’t include a relayer, which might be the most complex piece of logic (it’s easy to maintain, but has a non-negligible initial cost).

The above is an important aspect of the problem. I’m less clear on the solution space. If I were to redo things, what I’d see as necessary is allow incrementality in IBC: allow a token transfer $DOT <> $ATOM without 2 years of upfront effort. How? Not sure, but it involves bypassing – temporarily and deliberately – a majority of the channel/conn/client machinery to allow an initial user base (i.e., people with risk appetite high enough to trust multi-sig bridges) to play with this prototype token transfer bridge. This initial deployment wouldn’t need ICA, neither ordered channels, nor connection delay features. If the users are there, this prototype would validate the need for the bridge and warrant further effort to implement the missing pieces.

LE: I put more thought into this.

On the solution space, I could reframe what I wrote as follows: If I could wave a magic wand and redo everything, I would look for ways to enable the deployment of a stripped-down IBC that proves validity and feasibility of this bridge. A stripped-down version could be: ICS20 + 23 + 07 (or appropriate light client); no connections, no channels.

I think the above is very important. IBC is an interoperability protocol, but most of its competitors offer token bridges (which is the use-case in most need). We should not be forced to pay upfront the full price of an interoperability protocol when all is needed is a secure ICS20.

2 Likes

I think this is missing test-and-set and/or mutex primitives.

I think of IBC as functionally a bus, and the interchain as a multi-core computer. The most important element that multi-core activity needs is data coherence, and coherence protocols need test-and-set tools.

If you have an affordance that you want to activate across IBC, the remote chain needs to be able to be confident it’s not entering into a race condition. For that it must do a test-and-set query to the chain it’s acting against so that it can execute its side of the transaction and then complete the remote side.

Coherence primitives like test-and-set will allow IBC to grow into complex and powerful interchain behavior, and allow us to leverage the decades of work that have been put into coherence models by hardware engineers in designing IBC transaction flow.

I think the concept of a client is generally applicable. It represents any arbitrary verification logic w/ arbitrary state. Even if the verification logic is merely checking a single signature (solo machine client) or multiple signatures.

The question we want to answer is - what data and abstractions do we want to be consistent across chains? The transport (TAO) layer produces a commitment to this common data and abstractions.

The connection abstraction is quite useful when defined at the transport layer from my POV. It defines an explicit link between two clients (A → B) & (B → A) in a common format.

This makes the network topology easy to traverse and it’s explicitly clear how a particular path is secured. It’s an integral part of multi-hop proofs which enable innovations such as multi-hop channels.

You could technically push the logic of linking clients into the clients themselves. But because you would want this to be in a consistent format - you end up with a common abstraction at a layer where the logic is expected to be inconsistent across client types.

Handshakes (both connections and channels) are implemented as a 2 phase commit which requires two round trips. You could replace the handshake with a single round trip.

But you end up in a situation where packets could be sent over one connection end while the other end has not been created yet. You’re also trusting the relayer to ensure that all of the versioning and middleware on both sides line up.

It’s possible to avoid handshakes while also staying consistent w/ the IBC transport commitment spec. You could do a one time handshake on initialization and use a universal channel for all applications. This moves routing logic from the transport layer to the app side. Not advisable but definitely doable.

Maybe I’m missing something, but it seems like it would be pretty viable to just provide abstractions for verifying state from various clients, e.g. ibcVerify(client, state). A given client corresponds to a counterparty chain so this seems fine.

On top of this, other conveniences could be built such as a concept of messages, and niceties such as channel, port, etc could be application level libraries if needed.

The problem that we are talking about is not that any given feature of IBC can be unequivocally proven to be a bad idea, but just that there are so many things. So starting from the simplest possible base and figuring out what’s absolutely necessary and what’s optional could be a good way to address that.

2 Likes

Sharing a document that I’ve shared internally at Informal, as it’s relevant to this discussion!

Executive summary

This document explores the reasons why we see little to no adoption of IBC outside of Cosmos, and suggest some avenues for fixing the problem. After building a shared background, I deep dive into some technical reasons why IBC is not “universal”; that is, I identify parts of its design which make it unfit for some bridging use cases. We end with providing avenues for how to improve IBC, and identify future research questions.

The purpose of this document is to inform business decisions from mid- to high-level technical arguments. Minor technical inaccuracies are included to help readability.

Background

In this section, I provide background for the concepts that will be discussed. I will be referring back to them in the ensuing discussion. This can be safely skipped and referred back to if needed.

Review of bridges

Fundamentally, a bridge provides transport of messages from one chain to another. Transport refers to the mechanism by which a message sent from chain A is delivered to chain B. Every transport protocol defines how chain A sends a message, how a relayer picks up on that, and what on-chain verification chain B must perform when receiving a message.

In practice, there are 3 main categories of bridges: multisig-based, light client-based, and validating bridges. We will briefly go over the main ideas of each. This is not a comprehensive review; we only go over those to serve the ensuing discussion.

Multisig bridges

Multisig bridges implement transport by giving full authority to a set of keys to dictate what messages were sent by a counterparty chain. That is, from chain A’s perspective, to validate the claim that a message was sent to A, A needs to verify that a sufficient number of known keys signed the message to be convinced that the message was indeed sent.

Note that an interesting special case of multisig bridges is when there is only one trusted key. This is how centralized platforms such as Coinbase work. On Ethereum, they have a smart contract where that escrows tokens. When a token is escrowed, their centralized database is updated (e.g. if you escrowed 1 ETH, you will now see 1 ETH in your Coinbase account). Conversely, to send 1 ETH out of Coinbase, they first deduct 1 ETH from your account, and then send 1 ETH from the escrow contract back to your Ethereum address. Note that the contract only accepts such transactions if it is originated by a predetermined account, which they control. Note that this is a bridge just like any other given our original definition of bridges; we just described here how the Coinbase bridge implements transport.

Multisig bridges are in wide use today, for better or for worse.

Light client bridges

Light client bridges implement transport by having each chain run a light client of its counterparty. There is no one definition of what a “light client” is, but broadly speaking, it is an algorithm that verifies that the validators of the counterparty chain approved the state transition (i.e. the validators signed off on a new block), as opposed to verifying the state transition itself. To send a message, chain A must then commit the message to state; chain B will require a Merkle proof that the packet was indeed included in the state. Note that chain B can do that because it has a light client which tracks the state root of the other chain.

This is the effectively the sole use of IBC today.

Validating bridges

For the purpose of this discussion, I include all bridges which validate the state transition function under the name “validating bridge”. In essence, a validating bridge is similar to a light client bridge, except that the “light client” actually verifies the state transition as well. That is, it doesn’t blindly trust that the counterparty chain’s validators are honest; it verifies that the new blocks are indeed valid.

The two primary ways that this is is done today are either optimistically or using zero-knowledge proofs. In the optimistic scenario, new block headers are accepted, but given a “challenge period” (typically on the order of days) for anyone to publish a proof that the state transition function was violated. In the zero-knowledge proof scenario, to update the counterparty client, chain A must also submit a so-called “zero-knowledge proof”: a proof that turns out to be simultaneously cheap to verify, and at the same time prove that the state-transition function was not violated.

Why IBC is not a universal protocol

We begin our main discussion by exploring some technical reasons why IBC as currently designed is not always the best bridging solution.

The connection and channel primitives don’t apply to all bridges

In order to send a message (packet) in IBC, one must set up a connection and channel on top of the connection. IBC has handshakes to establish connections or channels, which serve 2 purposes:

  1. (connection only) Ensuring that the counterparty chain runs a proper light client
    • For example, A ensures that the root of trust on B actually refers to A (i.e. proper Merkle root of the state at some height H).
  2. Negotiating parameters
    • e.g. if channels using a given connection are ordered or unordered.

The connection and channel primitives make sense only in a subset of transport protocols. For example, they are not needed for multisig bridges. As previously explored, multisig bridges implement transport by giving full authority to a set of keys to dictate what messages were sent by a counterparty chain. Connections and channels are not needed in this scheme; they add complexity for no benefit.

Note that IBC supports multisigs the solomachine client is used. However, multisig bridges over IBC are made more complex by the connection and channel primitives. This also has a runtime cost; receiving messages is then more expensive due to all the extra checks imposed by the connection and channel primitives.

Many core types require on-chain parsing

A self-describing format is one where bytes in the data structure contain information about the structure of the rest of the data. The most common example is JSON.

IBC has many core types that need to be parsed on-chain.

For example, chain IDs are encoded as a string of the form "<client type>-<revision number>". Many IBC datagrams’ on-chain validation require parsing the revision number out of the chain ID.

Another example is the use of JSON as a packet data format. Notably, the token transfer app’s packet format roughly look like:

{
    "token": "a/b/denom",
    "amount": 42,
    "sender": "address",
    "receiver": "address",
    "memo": "some text"
}

Hence, processing token transfers requires parsing a JSON-encoded string.

While this seems to work fine on app chains, this is prohibitively expensive in resource-constrained environments. Two such notable environments are:

  1. EVM-based chains
  2. zk-based validating bridges
    • As previously discussed, validating a message on the receiving chain requires validating a SNARK (or “succinct proof”) attesting to the fact that the counterparty’s state transition function (or “chain logic”) was properly executed. Parsing JSON, or self-describing data types in general, drastically increases the size of SNARKs.
    • This is relevant in the context where an IBC-enabled rollup has a zk-bridge to its “settlement chain”. Everytime the rollup updates the zk-bridge, it must construct a SNARK attesting to the fact that it ran its state transition function properly. Some of the rollup’s transactions will contain IBC message processing, which often involves parsing JSON. This ends up drastically increasing the size of the SNARK sent to the rollup’s settlement chain.

In contrast, bridges commonly used on Ethereum understand this. They avoid self-describing formats, and use fixed-width types.

For example, this is the definition of a Wormhole packet:

// Wormhole's "packet" format
VAA struct {
	Version uint8
	GuardianSetIndex uint32
	LenSignatures uint8
	Signatures []*Signature
	Timestamp time.Time
	Nonce uint32
	EmitterChain uint8
	EmitterAddress [32]byte
	Sequence uint64
    ConsistencyLevel uint8
	Payload []byte
}

// Signature
Signature struct {
    Index uint8
    Signature [65]byte
}

The critical property of this packet format is that it is very efficient to parse.

Avenues for improving IBC

The key to getting IBC out of Cosmos is to make sending and receiving messages efficient in resource-constrained environments. Recommendations follow directly from problems identified in the previous section.

Avoid self-describing formats and use fixed-width types

Specifically, chain IDs should be, for example, a u64.

Also, IBC apps should carefully design their packet formats to use fixed-width types as much as possible. The previously mentioned Wormhole packet format is a good example.

To accomodate apps that must use strings, IBC should provide a standard efficient String representation, such as

struct String {
    len: u32,
    data: char[],
}

This format is efficient to parse: read the first 4 bytes interpreted as an unsigned integer. This tells indicates how many bytes the string contains. A parser which doesn’t care about the string value can simply jump the data field and keep parsing the rest of the data. Note that although this String structure is technically self-describing, the overhead of parsing it is very minimal (i.e. one unsigned integer read).

Strings should never contain structured data. That is, no parsing of the data field should be needed. Note that the token field of the ICS-20 packet data structure violates this principle and should be redesigned.

Future research

There are still many questions left to answer:

  • Is IBC a good protocol to implement a validating bridge? Specifically, are the connection & channel primitives (and their current handshakes) useful abstractions for validating bridges?
    • If not, can we redesign them in a way that they are?
  • Can IBC enable certain clients to not require connections & channels? As we previously saw, those are unnecessary for multisig bridges; can we get rid of them on a per-client basis?
  • Say we come up with a design which we believe solves all the problems previously described; how do we upgrade the ecosystem to this “IBC v2” protocol?
4 Likes

The IBC was originally designed (at least to my understanding and experience) bottom-up, from client to packets, with token transfer as an initial use case. This is also how people are normally exposed and onboarded into IBC.
What if we instead look at it more top down (this is actually how we onboard people now into IBC internally at Informal); I guess this is what some folks already pointed out here, suggesting for minimal start set.
I am personally not a big fan of an approach where we just remove abstractions to make things simpler; there is a reason why they exist in the first place there, and normally removing things, without moving functionality elsewhere could lead to short wins, but breaks very fast.
I would suggest to consider slightly different approach: what if instead of removing abstractions, we allow elements below not to be defined in some cases, or have simpler implementations.
For example, if you start from ICS20, you need to use packets and channels, and then you can have flexibility how you configure channels: maybe there is a way to create channel by only specifying simple multi-sig based validity predicate; in this case we would not necessarily use connection or client abstraction, but we are not removing it, as in some cases they bring value.
I feel that we need to agree on connection/channel API, and this is what we try to turn into standard, and then above this api we have IBC applications, and below we have different modules that allow IBC to be used in different scenarios, with different performance and security tradeoffs.

1 Like