摘要路由问题:
  1. IBC 会根据资产跨越的 IBC 通道序列为其打标签,因此同一种资产如果经过两条不同路径转移,就会得到两个不同的 denom
  2. 通常情况下,每种资产在每条链上只有 1 个“正确”的版本(也就是高流动性的版本),而且很多时候一个都没有
Skip Go API 通过以下方式解决这个问题:
  1. 先把资产发送回其源链
  2. 找到从源链到目标链的最短路径;如果存在多条不同的最短路径,则使用流动性最高的那一条
  3. 同时,对少数异常情况保持灵活处理

路由问题

IBC 代币的名称和身份来自其路径

IBC 通过连接两条链的“通道”来传输数据。通道由人类可读的端口名(例如 “transfer”)和通道 ID(例如 channel-1)来标识。比如,来看一条连接 Terra2 和 Axelar 的转账通道:
注意,两条链都会为这条通道维护各自的通道 ID,而它们不一定相同。可以把不同的链类比为城市,把通道类比为连接城市的道路,IBC 数据包则像是在道路上行驶的汽车。 当一种同质化代币通过某条通道从一条链转移到另一条链时,它在目标链上的 denom 会由源 denom 加上该代币经过的通道路径唯一且可预测地决定。具体来说,denom 的算法如下:
Naming Algorithm
ibc_denom = 'ibc/' + hash('path' + 'base_denom')
hash 通常是 sha256 哈希函数 继续上面的例子,这个版本的 WETH.axl 在 Terra2 上的 denom 是:
axlWETH on Terra2
axlweth_on_terra2_denom = 'ibc/' + hash('transfer/channel-6/weth-wei')
axlweth_on_terra2_denom = 'ibc/BC8A77AFBD872FDC32A348D3FB10CC09277C266CFE52081DE341C7EC6752E674'

不同路径会产生不同的代币

既然你已经理解了 IBC denom 的名称来自其路径,那么也就理解了路由问题的核心:同一种资产如果通过两条不同路径转移到同一个目标链上,就会得到不同的 denom。 继续上面的例子,WETH.axl 如果直接从 Axelar 转到 Terra2,和先经过 Osmosis 再转到 Terra2,得到的 denom 会不同:
更麻烦的是,同一对链之间可以存在多条通道(毕竟 IBC 是无许可的),而 IBC 在构造 denom 时使用的是通道标识符,而不是链标识符。这意味着,即使代币来自同一个源链,只要它们是通过两条不同通道转移过来的,目标链上也会出现同一种资产的两个不同版本:
为什么不直接把它们视为等价资产,然后继续往下做?有些不经常接触跨链桥的人会把这种路径标记看作一个缺陷,或者觉得这些同一种资产的不同版本本来也应该被视为可互换。但这样做并不明智。基于路径的 denom 构造是一项关键的安全特性,因为代币被转入的那条链,实际上是在信任代币转出来源链的验证者集合。套用到这里的例子,使用紫色版本的 WETH.axl 意味着你同时信任 Osmosis 的验证者集合和 Axelar 的验证者集合;而使用蓝色版本的 WETH.axl 则只需要信任 Axelar 的验证者集合。

两条链之间可能有很多路径,但通常每种资产只有 1 个有用的版本

当前大约有 70 条支持 IBC 的链。这个集合中几乎任意两条链之间至少都存在一条通道。这样一个高密度的通道图,使得几乎任意两条链之间都存在大量不同路径,也就带来了很多“代币绕行”或“路径选错”的机会:用户可能会把资产沿着一条次优的通道路径从一条链发到另一条链,最终得到一个缺乏流动性、几乎无法使用的代币版本。 路径选错几乎总会让用户在目标链上得到一个实际上没什么用且流动性很差的代币版本,因为在给定目标链上,某种资产通常只有 1 个有用的版本(如果有的话)。(JUNO 上有超过 50 个版本的 ATOM!) 因此,我们必须非常谨慎,确保用户经过正确的通道序列。下一节会解释我们的代币路由算法。

路由算法

关于路由的一个关键认识:正确路径取决于链和资产本身请注意,某个特定资产 A 从某条链 Chain-1 路由到另一条链 Chain-2 的正确路径,不仅取决于 Chain-1 和 Chain-2 之间有哪些通道,也取决于资产 A 本身是什么。这是因为资产 A 是由它从源头开始的路径定义的。请看下面两种情况:
  • 如果资产 A 原生于 Chain-1,那么它也许可以直接通过某条通道路由到 Chain-2。这样会得到一个较为简单的资产版本,其路径为 Chain-1->Chain-2
  • 如果资产 A 源自另一条链(例如 Chain-3),那么直接通过某条通道转到 Chain-2,极不可能得到 Chain-2 上正确的资产 A 版本。这样会得到一个更复杂的 denom,其路径为 Chain-3->Chain-1->Chain-2;如果 Chain-3 和 Chain-2 本身就是直连的,这个结果大概率是错误的。
    • 更合理的做法通常是,先让资产通过连接 Chain-1 和 Chain-3 的通道返回去,再通过通道发送到 Chain-2。这样得到的路径是 Chain-1->Chain-3->Chain-2,最终 denom 则由路径 Chain-3->Chain-2 决定
归根结底,我们使用的是一个非常简单的路由算法:
  1. 先把给定资产路由回源链(也就是“解开缠绕”)
  2. 检查对于该资产在目标链上是否存在任何 高优先级 的手动覆盖规则。如果存在,就推荐从源头出发、能够生成这一 高优先级 资产版本的路径
  3. 如果不存在 高优先级 的手动覆盖规则:
    1. 如果到目标链至少存在 1 条单跳路径(也就是源链和目标链之间存在 IBC 直连),则推荐流动性最高的直连路径。
    2. 如果不存在直连路径(或者直连路径上的 client 已过期,或者没有任何该资产曾经通过这条直连路径转移过),则不推荐任何资产
关于我们的数据采集,还有几点说明:
  • 我们为每条支持的链运行节点,以确保始终能够低延迟访问高质量数据
  • 我们每隔几个小时索引一次所有支持链上每条通道及其 client 的状态,以确保绝不会推荐那些已经被 relayer 放弃或遗忘的路径。
  • 我们每隔几个小时索引一次每条 Cosmos 链上每个通道所转移过的每种代币的流动性,以确保流动性数据保持最新。同时,我们也会密切监控异常的短期流动性变化,以防止攻击

tl;drThe routing problem:
  1. IBC tags assets based on the sequence of IBC channels they have been transferred over, so the same asset transferred over two different paths will have two different denoms
  2. Usually, there’s only 1 “correct” (i.e. highly liquid) version of each asset on each chain (and frequently there are none)
Skip Go API solves this problem by:
  1. Sending assets to their origin chain
  2. Find the shortest path from the origin chain to the destination chain, and using the most liquid path when there are multiple distinct shortest paths.
  3. Plus, staying flexible to unusual exceptions

Routing Problem

IBC Tokens Get Their Names & Identities from Their Paths

IBC transfers data over “channels” that connect two chains. Channels are identified by human-readable port names (e.g. “transfer”) and channel IDs (e.g. channel-1). For example, consider a transfer channel between Terra2 and Axelar:
Notice that both chains maintain their own channel IDs for the channel, which might not be the same. As an analogy, you might think of the different chains as cities and the channel as a road connecting them. IBC packets are cars driving across the road When transferring a fungible token from one chain to another over a channel, the denomination of the token on the destination chain is uniquely and predictably determined by the source denomination + the channel(s) over which the token was transferred. Specifically the denomination algorithm is:
Naming Algorithm
ibc_denom = 'ibc/' + hash('path' + 'base_denom')
hash is typically the sha256 hash function Continuing the example from above, the denom of this version of WETH.axl on Terra2 is:
axlWETH on Terra2
axlweth_on_terra2_denom = 'ibc/' + hash('transfer/channel-6/weth-wei')
axlweth_on_terra2_denom = 'ibc/BC8A77AFBD872FDC32A348D3FB10CC09277C266CFE52081DE341C7EC6752E674'

So Different Paths Produce Different Tokens

Now that you understand that IBC denoms get their names from their paths, you understand the crux of the routing problem: The same asset transferred to the same destination over two different paths will have different denominations. Continuing the example from above, WETH.axl transferred directly from Axelar to Terra2 will have a different denom than WETH.axl transferred through Osmosis:
To make matters worse, multiple channels can exist between the same two chains (IBC is permissionless afterall), and IBC uses channel identifiers—not chain identifiers—to construct denoms. That means two different versions of the same asset will exist on the destination chain even when tokens are transferred from the same source chain, if they’re transferred over two different channels:
Why don’t we just consider them equivalent anyway and move on?Some folks who don’t work with bridges on a regular basis view this path tagging as a bug, or might think we should just consider these different versions of the same asset as fungible anyway. But that’s not advisable!The route-based denom construction is a critical security feature because the chain where the token has been transferred to is effectively trusting the validator set of the chain from which the token was transferred.Applied to the example here, this trust model means using the purple version of WETH.axl implies trusting the Osmosis validator set AND the Axelar validator set, while using the blue version of WETH.axl only requires trusting the Axelar validator set.

There are many paths between two chains, but usually only 1 useful version of each asset

Right now, there are about 70 IBC-enabled chains. At least one channel exists between almost every pair in this set. This dense graph of channels contains a very large number of different paths between almost any two chains, which creates many opportunities for “token winding” or “mis-pathing”, where a user sends an asset through a suboptimal path of channels from one chain to another and ends up with an illiquid / unusable version of their token. Mis-pathing almost always produces a practically useless + illiquid version of their token on the destination chain because there’s usually only 1 useful version of a given asset on a given destination chain (if that). (There are over 50 versions of ATOM on JUNO!) As a result, we need to be very careful to send the user through the correct sequence of channels. The next section explains our token routing algorithm.

Routing Algorithm

Insight about routing: The correct route depends on the chains + the asset in questionNotice that the correct route for a particular asset A on a particular chain Chain-1 to another chain Chain-2 depends not only on the channels that exist between chain-1 and chain-2, but also on what asset-A is.This is because asset A is defined by its path from its origin. Consider the following two cases:
  • If asset-A is native to Chain-1, perhaps it can be routed directly over a channel to Chain-2. This would yield a simple asset given by path of Chain-1->Chain-2
  • If asset-A originated on another chain (e.g. Chain-3), it’s very unlikely that transferring directly over a channel to Chain-2 would give the correct version of asset A on Chain-2. This would yield a more complex denom given by path of Chain-3->Chain-1->Chain-2, which is probably wrong if Chain-3 and Chain-2 are directly connected.
    • Instead, the asset should probably be routed back through the channel that connects Chain-1 to Chain-3 first, then sent over the channel to Chain-2. This yields a path of Chain-1->Chain-3->Chain-2, and a final denom given by the path Chain-3->Chain-2
Ultimately, we use a very simple routing algorithm:
  1. Route the given asset back to origin chain (i.e. “unwind” it)
  2. Check whether any high-priority manual overrides exist for the given asset on the given destination chain. If so, recommend the path from the source that produces this high-priority version of the asset
  3. If no high priority manual overrides exist:
    1. If at least 1 single-hop path to the destination chain exists (i.e. if origin chain and destination chain are directly connected over IBC), recommend the most liquid direct path.
    2. If no direct path exists (or if the client on the direct path is expired, or none of the asset has been transferred over the direct path), do not recommend any asset
A few notes about our data collection:
  • We run nodes for every supported chain to ensure we always have low-latency access to high quality data
  • We index client + channel status of every channel + client on all the chains we support every couple of hours to ensure we never recommend a path that relayers have abandoned or forgotten about.
  • We index the liquidity of every token transferred over every channel on every Cosmos chain every few hours to ensure our liquidity data is up to date. And we closely monitor anomalous, short-term liquidity movements to prevent attacks