Gavin Wood:详解跨共识消息格式 XCM 设计原理与运转机制

XCM 是一种消息格式,它不能用于在系统之间实际「发送」任何消息,它的作用仅在于表达接收者应该做什么。

原文标题:《Gavin Wood: 跨共识消息格式 XCM》

撰文:Gavin Wood,以太坊的联合创始人、波卡和 Parity 科技创始人

翻译:PolkaWorld

随着最终的 Polkadot 1.0 版本和平行链的临近,跨共识消息格式(简称 XCM)即将发布其第一个生产就绪版本。这是对格式、其目标、工作原理的介绍,可用于实现典型的跨链任务。

我们从一个有趣的事实讲起。XCM 是“跨共识”消息格式,而不仅仅是“跨链”。这种差异是该格式在最终实现的目标上的标志,该格式不仅在链之间交流,而且在智能合约和模块之间,以及通过桥和分片(如 Polkadot 的 Spree)中发送各种想法。

? 一种格式,而不是一个协议

为了更好地理解 XCM,重要的是要了解它的边界以及它在 Polkadot 技术堆栈中的位置。XCM 是一种消息格式。它不是消息传递协议。它不能用于在系统之间实际“发送”任何消息,它的作用仅在于表达接收者应该做什么。

不包括桥和合约模块,Polkadot 带有三个不同的系统,用于在其组成链之间实际通信 XCM 消息:UMP、DMP 和 XCMP。UMP(向上消息传递)允许平行链向它们的中继链发送消息。DMP(向下消息传递)允许中继链将消息向下传递到其平行链。XCMP 可能是其中最著名的,这允许平行链之间发送消息。XCM 可用于通过这三个通信通道中的任意一个来表达消息的含义。

资产除了在链之间发送消息之外,XCM 在其他语境也很有用,比如,可以用于在你之前不是很了解它交易格式的链上进行交易。对于业务逻辑变化很小的链(例如比特币),交易格式 —— 或者钱包用来向链发送指令的格式 —— 往往会无限期地保持完全相同,或者至少兼容。使用高度可进化的基于元协议的链,例如 Polkadot 及其组成的平行链,业务逻辑可以通过单个交易跨网络升级。这可以改变任何事情,包括交易格式,给钱包维护者带来潜在的问题,特别是对于需要离线保存的钱包(例如 Parity Signer)。由于 XCM 版本良好、抽象且通用,因此它可以用作一种为钱包提供持久交易格式的手段,用于创建许多常见交易。

? 目标

XCM 旨在成为共识系统之间交流思想的语言。它应该足够通用,以便在整个不断发展的生态系统中正确并有用。它应该是可扩展的,由于可扩展性不可避免地意味着变化,它也应该是面向未来和向前兼容的。最后,它应该足够高效可以在链上运行,并且还能在计量环境中运行。

像所有语言一样,有些人会比其他人更倾向于使用某些元素。XCM 的设计方式并不是让每个支持 XCM 的系统都能够解释任何可能的 XCM 消息。有些消息在某些系统下不会有合理的解释。其他的可能是合理的,但由于资源限制或因为可以以更清晰、更规范的方式表达相同的内容,解释器仍然故意不支持。系统将不可避免地只支持可能消息的一个子集。资源严重受限的系统(如智能合约)可能只支持非常有限的“方言”。

这种普遍性甚至延伸到诸如为执行 XCM 消息支付费用之类的概念。由于我们知道 XCM 可用于多种系统,包括 gas 计量的智能合约平台和社区平行链,一直到系统平行链与其中继链之间的可信交互,因此我们不想将费用支付等元素烤得太深和在协议中变得不可逆转。

? 为什么不直接使用本地消息格式

在某些情况下,捎带在本地信息/交易格式的链或智能合约可能很有用,但确实有一些很大的缺点,使其对 XCM 的目标不太有用。首先,链之间缺乏兼容性,因此打算向多个目的地发送消息的系统需要了解如何为每个目的地编写消息。在这一点上,即使是单个目的地也可能随着时间的推移改变其本地交易/消息格式。智能合约可能会升级,区块链可能会引入新功能或改变现有功能,从而改变其交易格式。

其次,链上的常见用例不容易适合单个交易;可能需要特殊的技巧来提取资金,交换资金,然后将结果全部存入单个交易中。连贯的储备资产框架所需的后续转移通知并不存在于不知道其他人的链中。

第三,诸如支付费用之类的操作不容易适应假设已经像智能合约消息一样协商费用支付的模型。相比之下,交易信封提供了一些支付处理的系统,但通常也被设计为包含一个签名,这在共识系统之间进行通信时没有意义。

? 一些初始用例

虽然 XCM 的目标是通用的、灵活的和面向未来的,但它当然必须满足实际需求,尤其是链之间的代币转移。可选的费用支付(可能使用这些代币)是另一种方式,就像进行交换服务的通用接口一样,在整个 DeFi 世界中都很常见。最后,应该可以使用 XCM 语言进行一些特定于平台的操作。例如,在 Substrate 链中,可能需要将远程调用分派到其模块之一以访问专门的功能。

最重要的是,有许多我们希望支持的代币转移模型:我们可能只想简单地控制远程链上的帐户,允许本地链在远程链上拥有一个地址以接收资金并最终将其控制的资金转移到该远程链上的其他账户中。

我们可能有两个共识系统,它们都是特定代币的“本地家园”。想象一下像 USDT 或 USDC 这样的代币,它在几个不同的链上都有实例,并且完全可以互换。应该可以在一条链上销毁这样的代币,并在另一条支持的链上铸造相应的代币。在 XCM 的说法中,我们之所以称之为传送(teleport),是因为资产的转移实际上是通过在一侧销毁它,并在另一侧创建一个克隆来实现的。

最后,可能有两条链想要提名第三条链,其中一条链上的资产可能被视为本地资产,用作该资产的储备。每个链上资产的衍生形式将得到完全支持,允许衍生资产交换为支持它的储备链上的基础资产。这可能是两条链不一定相互信任的情况,但(至少就相关资产而言)愿意信任资产的本地链。这里的一个例子是我们有几个社区平行链想要在彼此之间发送 DOT。它们每个都有一个本地形式的 DOT,由 Statemint 链(DOT 的本地中心)上的平行链控制的 DOT 完全支持。当在链之间发送本地形式的 DOT 时,在后台,“真正的”DOT 在 Statemint 上的平行链帐户之间移动。

即使这种看似适度的功能水平也有相对大量的配置,它们的使用可能是可取的,并且需要一些有趣的设计来避免过度拟合。

XCM 剖析

XCM 格式的核心在于 XCVM。与某些人的看法相反,这不是(有效的)罗马数字(尽管如果是,它可能意味着 905)。事实上,这代表跨共识虚拟机。这是一台超高级别的非图灵完备计算机,其指令设计为与交易大致处于同一级别。

XCM 中的“消息”实际上只是一个运行在 XCVM 上的程序。它是一个或多个 XCM 指令。程序会一直执行,直到它运行到最后或遇到错误为止,此时它会结束(我现在有意不解释这一点)并停止。

XCVM 包括许多 Register,以及访问托管它的共识系统的整体状态。指令可能会改变一个 Register,它们可能会改变共识系统的状态,或者两者兼而有之。

这种指令的一个例子是 TransferAsset,它用于将资产转移到远程系统上的某个其他地址。需要告知要转让哪些资产以及资产要转让给谁/在哪里。在 Rust 中,它是这样声明的:

enum Instruction {

TransferAsset {

assets: MultiAssets,

beneficiary: MultiLocation,

}

/* snip */

}

正如你可能猜到的那样,资产是表示要转让哪些资产的参数,而受益人则说明这些资产要交给谁/在哪里。当然,我们还缺少另一条信息,即从谁/何处获取资产。这是从原 Register自动推断出来的。程序开始时,这个 Register 一般是根据传输系统(网、XCMP 或者其他什么)来设置的,以反映消息实际来自哪里,和受益人是同一类型的信息。Origin Register 作为一个受保护的 Register 运行 —— 程序不能任意设置它,尽管有两条指令可以用来以某种方式改变它。

使用的类型是 XCM 中非常基本的思想:资产,由 MultiAsset 表示,共识内的位置,由 MultiLocation 表示。Origin Register 是一个可选的 MultiLocation(可选,因为如果需要它可以完全清除)。

? 在 XCM 的位置

MultiLocation 类型标识存在于共识世界中的任何单个位置。这是一个相当抽象的想法,可以代表共识中存在的所有事物,从可扩展的多分片区块链(如 Polkadot)一直到平行链上的低级 ERC-20 资产账户。在计算机科学术语中,它实际上只是一个全局单例数据结构,无论其大小或复杂性如何。

MultiLocation 始终表示相对于当前位置的位置。你可以把它想象成一个文件系统路径,但是没有办法直接表达文件系统树的“根”。这有一个简单的原因:在 Polkadot 的世界中,区块链可以合并到其他区块链中,也可以从其他区块链中分离出来。区块链可以非常独立地开始生命,并最终被提升为更大共识中的平行链。如果这样做,那么 “root” 的含义将在一夜之间改变,这可能会给 XCM 消息和其他任何使用 MultiLocation 的消息带来混乱。为了简单起见,我们完全排除了这种可能性。

XCM 中的位置是分层的,共识中的一些地方完全封装在共识中的其他地方。Polkadot 的平行链完全存在于整个 Polkadot 共识中,我们称之为内部位置。更严格地说,我们可以说,只要有一个共识系统的任何变化都意味着另一个共识系统的变化,那么前一个系统是后者的内部系统。例如,Canvas 智能合约位于托管它的合约模块的内部。比特币中的 UTXO 是比特币区块链的内部。

这意味着 XCM 没有区分 “谁” 和 “在哪里” 两个问题。从像 XCM 这样相当抽象的东西的角度来看,区别并不重要 —— 两者模糊并成为本质上相同的东西。

MultiLocations 用于识别发送 XCM 消息的位置,可以接收资产的位置,然后甚至可以帮助描述资产本身的类型,正如我们将看到的。非常有用的东西。

当用本文这样的文本写下来时,它们表示为一些 .. (或“父”,封装共识系统)组件,后跟一些连接点,所有连接点都用 / 分隔。(当我们用 Rust 之类的语言表达它们时,通常不会发生这种情况,但它在书面上是有意义的,因为它很像广泛使用的熟悉的目录路径。)连接在其封装共识中标识了一个内部位置系统。如果根本没有父节点/连接点,那么我们只说位置是这里。

例如:

  • ../Parachain(1000): 在平行链中进行评估,这将识别我们索引为 1000 的兄弟平行链。(在 Rust 中,我们将编写 ParentThen(Parachain(1000)).into())
  • ../AccountId32(0x1234…cdef): 在平行链中进行评估,这将识别中继链上的 32 字节帐户 0x1234…cdef。
  • Parachain(42)/AccountKey20(0x1234…abcd): 在中继链上进行评估,这将识别平行链编号 42 上的 20 字节帐户 0x1234…abcd (大概类似于承载以太坊兼容帐户的 Moonbeam)。

有许多不同类型的连接点,用于以各种方式识别你可能在链上找到的位置,例如键、索引、二进制 blob 和复数描述。

? XCM 中的资产

在 XCM 中工作时,通常需要引用某种资产。这是因为几乎所有现有的公共区块链都依赖于一些原生数字资产来为其内部经济和安全机制提供支柱。对于比特币等工作量证明区块链,原生资产(BTC)用于奖励开发区块链的矿工并防止双重支出。对于 Polkadot 等权益证明区块链,原生资产 (DOT) 用作一种抵押形式,网络管理员(称为权益人)必须承担风险才能生成有效区块并获得实物奖励。

一些区块链管理多种资产,例如以太坊的 ERC-20 框架允许在链上管理许多不同的资产。一些管理不可替代的资产,例如以太坊的 ETH,而是不可替代的 —— 独一无二的实例;Crypto-kitties 是此类不可替代代币或 NFT 的早期示例。

XCM 旨在能够毫不费力地处理所有此类资产。为此,有数据类型 MultiAsset 及其关联类型 MultiAssets、WildMultiAsset 和 MultiAssetFilter。让我们看看 Rust 中的 MultiAsset:

struct MultiAsset {

id: AssetId,

fun: Fungibility,

}

所以有两个字段定义了我们的资产:id 和 fun,这很好地表明了 XCM 如何处理资产。首先,必须提供整体资产身份。对于可替代资产,这只是标识资产。对于 NFT,这标识了整个资产“类别” —— 不同的资产实例可能在这个类别中。

plain

enum AssetId {

   Concrete(MultiLocation),

   Abstract(BinaryBlob),

}

资产身份以两种方式之一表示;无论是具体的还是抽象的。Abstract 并没有真正使用,但它允许通过名称指定资产 ID。这很方便,但依赖于接收者以发送者期望的方式解释名称,这可能并不总是那么容易。Concrete 在一般用途中使用并使用位置来明确地识别资产。对于原生资产(例如 DOT),资产往往被识别为铸造资产的链(在这种情况下是 Polkadot 中继链,这将是其平行链的位置 .. )。主要在链模块内管理的资产可以通过包括其在该模块内的索引的位置来识别。例如,Karura 平行链可能指的是 Statemine 平行链上的资产,位置为 ../Parachain(1000)/PalletInstance(50)/GeneralIndex(42) 。

enum Fungibility {

Fungible(NonZeroAmount),

NonFungible(AssetInstance),

}

其次,它们必须是可替代的或不可替代的。如果它们是可替代的,那么应该有一些相关的非零数量。如果它们不可替代,那么应该有一些指示它们是哪个实例而不是数量。(这通常用索引表示,但 XCM 还允许使用各种其他数据类型,例如数组和二进制 blob。) 这涵盖了 MultiAsset,但我们有时会使用其他三种相关类型。MultiAssets 就是其中之一,实际上只是意味着一组 MultiAsset 项目。然后我们有 WildMultiAsset;这是一个通配符,可用于匹配一个或多个 MultiAsset 项目。它实际上只支持两种通配符:All(匹配所有资产)和 AllOf 匹配特定身份 (AssetId) 和可替代性的所有资产。值得注意的是,对于后者,不需要指定数量(在可替代的情况下)或实例(对于非可替代的),并且全部匹配。

最后,还有 MultiAssetFilter。这是最常用的,实际上只是 MultiAssets 和 WildMultiAsset 的组合,允许指定通配符或明确(即非通配符)资产列表。

在 Rust XCM API 中,我们提供了很多转换,以尽可能轻松地处理这些数据类型。例如,当我们在 Polkadot 中继链上时,要指定等于 100 个不可分割的 DOT 资产单位的可替代 MultiAsset(普朗克,对于那些知道的人),那么我们将使用 (Here, 100).into()。

? Holding Register

我们再来看看另一个 XCM 指令:WithdrawAsset。从表面上看,这有点像 TransferAsset 的前半部分:它从起源的账户中提取了一些资产。但这与他们有什么关系?如果他们没有在任何地方存入,那么这肯定是一个非常无用的操作。如果我们查看它的 Rust 声明,我们会发现更多有关其用法的线索:

WithdrawAsset(MultiAssets),

所以,这次只有一个参数(MultiAssets 类型,它规定哪些资产必须从 Origin Register 的所有权中撤出)。但是没有指定放置资产的位置。

这些暂时持有的未动用资产称为持有 Register。(“Holding”是因为它们处于不能无限期持续的临时位置,“Register”是因为它有点像 CPU Register,是一个存放工作数据的地方。)有许多指令对持有寄存器进行操作。一种非常简单的指令是 DepositAsset 指令。让我们来看看它:

enum Instruction {

DepositAsset {

assets: MultiAssetFilter,

max_assets: u32,

beneficiary: MultiLocation,

},

/* snip */

}

啊哈!精明的读者会发现这看起来很像 TransferAsset 指令中缺失的一半。我们有 assets 参数,该参数指定应从持有寄存器中删除哪些资产以存放在链上。max_assets 让 XCM 作者通知接收者打算存入多少独特的资产。(这在知道 Holding Register 的内容之前计算费用时很有帮助,因为存入资产可能是一项代价高昂的操作。)最后是受益人,这与我们之前在 TransferAsset 操作中遇到的参数相同。有许多指令表示要在 Holding Register 上执行的操作,而 DepositAsset 是最简单的指令之一。其他一些则更复杂。

? XCM 中的费用支付

XCM 中的费用支付是一个相当重要的用例。Polkadot 社区中的大多数平行链都会要求其对话者为他们希望进行的任何操作付费,以免为 “交易垃圾” 和拒绝服务攻击敞开大门。当链有充分的理由相信它们的对话者会表现良好时,也存在例外情况 —— 当 Polkadot 中继链与 Polkadot Statemint 公共利益链通信时就是这种情况。但是,对于一般情况而言,费用是确保 XCM 消息及其传输协议不会被过度使用的好方法。我们来看看 XCM 消息到达 Polkadot 时如何支付费用。

正如前文提到的一样,XCM 不包括作为一等公民的费用和费用支付:与以太坊交易模型不同,对于费用支付不是协议中不需要的东西,就必须进行规避。就像 Rust 的零成本抽象一样,费用支付在 XCM 中没有很大的设计开销。

对于确实需要支付一定费用的系统,XCM 提供了使用资产购买执行资源的能力。概括来讲,这包括了三个部分:

  • 首先,需要提供一些资产。
  • 其次,必须就计算时间(用 Substrate 中的说法就是 weight)交换资产。
  • 最后,XCM 操作将按照指示执行。

第一部分由提供资产的多个 XCM 指令之一管理。我们已经知道其中的一个指令了( WithdrawAsset ),但还有其他几个我们稍后会看到。Holding Register中的所得资产当然将用于支付与执行 XCM 相关的费用。任何未用于支付费用的资产都将被存入某个目的地账户。在我们的示例中,我们假设 XCM 发生在 Polkadot 中继链上,交易的是 1 个 DOT(即 10,000,000,000 个不可分割的单位)。

目前我们的 XCM 指令是这样的:

WithdrawAsset((Here, 10_000_000_000).into()),

这将我们带到了第二部分,交换(一部分)这些资产以换取计算时间来支付我们的 XCM。为此,我们有 XCM 指令 BuyExecution 。我们来看看它是什么样的:

enum Instruction {

/* snip */

BuyExecution {

fees: MultiAsset,

weight: u64,

},

}

第一个项目 fees 是应从 Holding Register 中提取并用于支付费用的金额。从技术上讲,这只是最大值,因为任何未使用的余额都会立即退还。

最终花费的金额由解释系统决定—— fees 只是限制它,如果解释系统需要为所需的执行支付更多费用,那么 BuyExecution 指令将导致错误。第二个项目指定要购买的执行时间量。这一般不应小于 XCM 程序的总 weight。

在我们的示例中,我们假设所有 XCM 指令的 weight 为 100 万,因此到目前为止,我们的两个项目(WithdrawAsset 和 BuyExecution)为 200 万,接下来还有一个。我们将只使用我们必须支付这些费用的所有 DOT(仅当我们相信目的地链没有疯狂的费用时,这样做才合理 – 假设情况如此)。到这里,让我们看看我们的 XCM:

WithdrawAsset((Here, 10_000_000_000).into()),

BuyExecution {

fees: (Here, 10_000_000_000).into(),

weight: 3_000_000,

},

我们 XCM 的第三部分是存入 Holding Register 中剩余的资金。为此,我们将只使用 DepositAsset 指令。我们实际上并不知道 Holding Register 中还剩下多少,但这并不重要,因为我们可以为应该存入的资产指定一个通配符。我们将它们放在 Statemint 的主权账户中(标识为 Parachain(1000))。

所以我们最终的 XCM 指令是这样的:

WithdrawAsset((Here, 10_000_000_000).into()),

BuyExecution {

fees: (Here, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositAsset {

assets: All.into(),

max_assets: 1,

beneficiary: Parachain(1000).into(),

},

⛓ 使用 XCM 在链之间移动资产

将资产发送到另一条链可能是链间消息传递的最常见用例。允许一条链管理另一条链的本地资产,将允许各种衍生用例(无双关语),最简单的是去中心化交易所,但通常归为去中心化金融或 De-Fi。

一般来说,资产在链之间移动有两种方式,这取决于链之间是否信任彼此的安全性和逻辑。

✨ 传送(Teleporting)

对于相互信任的链(例如在相同的整体共识和安全保护伞下的同质分片),我们可以使用 Polkadot 称为传送的框架,这基本上就意味着在发送方销毁资产并在接收方铸造它。这种国防法既简单又高效——它只需要两条链的协调,并且每一侧只涉及一个动作。遗憾的是,如果接收链不能 100% 信任发送链实际销毁它正在铸造的资产(并且确实不铸造超出资产约定规则的资产),那么发送链确实没有根据消息铸造资产的理由。

我们来看看 XCM 将(大部分的)1 个 DOT 从 Polkadot 中继链传送到它在 Statemint 上的主权帐户时是什么样的。我们假设 Polkadot 这边已经支付了费用。

WithdrawAsset((Here, 10_000_000_000).into()),

InitiateTeleport {

assets: All.into(),

dest: Parachain(1000).into(),

xcm: Xcm(vec![

BuyExecution {

fees: (Parent, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositAsset {

assets: All.into(),

max_assets: 1,

beneficiary: Parent.into(),

},

]),

}

如你所见,这看起来和我们上次看到的直接 “提款-购买-存入” 的模式非常相似。不同之处在于 InitiateTeleport 指令,它插入在最后两条指令(BuyExecution 和 DepositAsset)周围。在幕后,发送链(Polkadot 中继链)在执行InitiateTeleport指令时正在创建一条全新的消息;它获取 xcm 字段并将其放入新的 XCMReceiveTeleportedAsset中,然后将此 XCM 发送到接收链 (Statemint) 。Statemint 相信 Polkadot 中继链在发送消息之前已经销毁了其一侧的 1 个 DOT。(事实确实如此!)

beneficiary (受益人)被声明为 Parent.into()  ,精明的读者可能想知道这在 Polkadot 中继链的上下文中指的是什么。答案是 “什么也不是”,但这里没有错误。xcm 参数中的所有内容都是从接收方的角度编写的,因此尽管这是整个 XCM 的一部分,它被馈送到 Polkadot 中继链,但它实际上只在 Statemint 上执行,因此它的上下文是跟着 Statemint 走的。

当 Statemint 最终收到这条消息时,它长这样:

ReceiveTeleportedAsset((Parent, 10_000_000_000).into()),

BuyExecution {

fees: (Parent, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositAsset {

assets: All.into(),

max_assets: 1,

beneficiary: Parent.into(),

},

你可能注意到了,这看起来跟之前的 WithdrawAsset XCM 非常像。唯一的主要区别是,它不是通过从本地账户提款来为费用和存款提供资金,而是通过相信 DOT 在发送方(Polkadot 中继链)上确实被销毁并尊重 ReceiveTeleportedAsset 消息。值得注意的是,我们在 Polkadot 中继链上发送的 1 个 DOT 的资产标识符(Here,指的是中继链本身是 DOT 的原生环境)已自动变异为它在 Statemint 上的表示: Parent.into(),即 Statemint 上下文中中继链的位置。

beneficiary 也被指定为 Polkadot 中继链,因此其(在 Statemint 上的)主权账户被记入新铸造的 1 DOT 减去费用。XCM 可能只是轻松地为beneficiary 指定了一个帐户或其他地方。实际上,可以使用从中继链发送的后续 TransferAsset 来移动这 1 个 DOT。

? 准备金(Reserves)

跨链转移资产的另一个方式稍微复杂一些。用到了称为准备金(reserve)的第三方。这个名字来自银行的准备金制度,也就是资产被 “储备” 起来,来让某些已发布的价值承诺具有可信度。例如,如果我们有理由相信在一条独立的平行链上发行的每个 “衍生” DOT 恰好可以兑换 1 个 “真” DOT(例如 Statemint 或中继链上的 DOT),那么我们就可以将平行链的 DOT 视为在经济上等同于真实的 DOT 。(大多数银行都做部分准备金银行业务,这意味着他们保留的准备金少于面值。这种做法平时没什么问题,但是当太多人都希望赎回,就会很快出问题。)所以,准备金是存储 “真实” 资产的地方,出于传输目的,其逻辑和安全性受到发送方和接收方的信任。发送方和接收方的任何相应资产都将是衍生品,但它们将得到 100% 的 “真实” 储备资产支持。假设平行链表现良好(即它没有漏洞,并且其治理没有决定偷走准备金跑路),这将使衍生品 DOT 与基础储备 DOT 的价值基本相同。储备资产保存在储备链上的发送者/接收者的主权账户(即由发送者或接收者链控制的账户)中,所以除非平行链出现问题,否则有充分的理由相信这些资产会受到很好的保护。

说回到转账机制,发送方将指示准备金把发送方拥有的资产(并将其用作其自己版本的相同资产的准备金)转移到接收方的主权账户中,而准备金(而不是发送方!)通知接收方他们的新资产。这意味着发送方和接收方不需要信任彼此的逻辑或安全性,而只需信任用作准备金的链的逻辑或安全性。然而,这确实意味着三方需要协调,这增加了整体成本、时间和复杂性。

让我们看看所需的 XCM。这次我们将从平行链 2000 发送 1 个 DOT 到平行链 2001,它在平行链 1000 上使用准备金支持的 DOT。同样,我们假设费用已经在发送方支付过了。

WithdrawAsset((Parent, 10_000_000_000).into()),

InitiateReserveWithdraw {

assets: All.into(),

dest: ParentThen(Parachain(1000)).into(),

xcm: Xcm(vec![

BuyExecution {

fees: (Parent, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositReserveAsset {

assets: All.into(),

max_assets: 1,

dest: ParentThen(Parachain(2001)).into(),

xcm: Xcm(vec![

BuyExecution {

fees: (Parent, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositAsset {

assets: All.into(),

max_assets: 1,

beneficiary: ParentThen(Parachain(2000)).into(),

},

]),

},

]),

},

就像我之前说的,这会有点复杂。让我们来看看这个过程。外部部分负责在发送方(平行链 2000)上提取 1 个 DOT 并撤回 Statemint(平行链 1000)上相应的 1 个 DOT —— 为此它使用了 InitiateReserveWithdraw ,其作用就是字面意思(开启准备金提取)。

WithdrawAsset((Parent, 10_000_000_000).into()),

InitiateReserveWithdraw {

assets: All.into(),

dest: ParentThen(Parachain(1000)).into(),

xcm: /* snip */

}

现在我们在 Statemint 的 Holding Register 中持有 1 个 DOT。在我们可以做其他事情之前,我们需要在 Statemint 上购买一些执行时间。这个过程看起来也很眼熟:

/*snip*/

xcm: Xcm(vec![

BuyExecution {

fees: (Parent, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositReserveAsset {

assets: All.into(),

max_assets: 1,

dest: ParentThen(Parachain(2001)).into(),

xcm: /* snip */

},

]),

/*snip*/

我们使用自己的 1 DOT 来支付费用,我们假设每个 XCM 操作有 100 万。支付了这一项操作后,我们将 1 个 DOT(减去费用,而且我们很懒,所以我们只使用 All.into())存入平行链 2001 的主权账户,但这样做是作为储备资产,这意味着我们也要求 Statemint 向该接收链发送通知 XCM,通知它传输以及要对生成的衍生资产执行的一些指令。DepositReserveAsset 指令并不总是奏效;为了让它奏效, dest  必须是一个可以在准备金链上合理持有资金的位置,而且也是储备链可以向其发送 XCM 的位置。兄弟平行链恰好符合要求。

/*snip*/

xcm: Xcm(vec![

BuyExecution {

fees: (Parent, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositAsset {

assets: All.into(),

max_assets: 1,

beneficiary: ParentThen(Parachain(2000)).into(),

},

]),

/*snip*/

最后一部分定义了到达平行链 2001 的消息的一部分。就像启动传送操作一样, DepositReserveAsset  编写并发送一条新消息,在这里是 ReserveAssetDeposited 。正是这个消息,虽然包含我们定义的 XCM 程序,但它到达了接收平行链。它看起来像这样:

ReserveAssetDeposited((Parent, 10_000_000_000).into()),

BuyExecution {

fees: (Parent, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositAsset {

assets: All.into(),

max_assets: 1,

beneficiary: ParentThen(Parachain(2000)).into(),

},

(这假设 Statemint 实际上没有收取任何费用,并且整个 1 DOT 都传过去了。这不是特别现实,因此 assets 这一行的数字可能会更低。) 这条消息中的大部分地方看起来应该很熟悉;与我们在上一节中看到的 ReceiveTeleportedAsset  消息的唯一显著区别是顶层指令 ReserveAssetDeposited ,它实现了类似的目的,只是表示 “发送链销毁了资产,以便你可以铸造等价资产”,它的意思是 “发送链收到了资产并为你保留它们,因此你可以铸造有全额资产背书的衍生品”。无论哪种方式,目的地链都会将它们铸造到 Holding Reserve 中,然后我们将它们存入接收链上的发送者主权账户中。?

? 结论

以上就是本文的内容。希望它有助于解释 XCM 是什么,以及它是如何设计以运转的基础知识。在下一篇文章中,我们将深入研究 XCVM 的架构、其执行模型及其错误处理、XCM 的版本控制系统;如何在连接良好的、相互依赖的生态系统中管理格式升级;其查询-响应系统;以及 XCM 在 Substrate 中的工作原理。我们还将讨论 XCM 的一些未来发展方向、计划的功能以及发展它的过程。

原文链接:medium.com

转载声明:本文 由CoinON抓取收录,观点仅代表作者本人,不代表CoinON资讯立场,CoinON不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。若以此作为投资依据,请自行承担全部责任。

声明:图文来源于网络,如有侵权请联系删除

风险提示:投资有风险,入市需谨慎。本资讯不作为投资理财建议。

(0)
打赏 微信扫一扫 微信扫一扫
上一篇 2021年9月7日 下午9:25
下一篇 2021年9月7日 下午11:14

相关推荐

Gavin Wood:详解跨共识消息格式 XCM 设计原理与运转机制

星期二 2021-09-07 21:34:27

原文标题:《Gavin Wood: 跨共识消息格式 XCM》

撰文:Gavin Wood,以太坊的联合创始人、波卡和 Parity 科技创始人

翻译:PolkaWorld

随着最终的 Polkadot 1.0 版本和平行链的临近,跨共识消息格式(简称 XCM)即将发布其第一个生产就绪版本。这是对格式、其目标、工作原理的介绍,可用于实现典型的跨链任务。

我们从一个有趣的事实讲起。XCM 是“跨共识”消息格式,而不仅仅是“跨链”。这种差异是该格式在最终实现的目标上的标志,该格式不仅在链之间交流,而且在智能合约和模块之间,以及通过桥和分片(如 Polkadot 的 Spree)中发送各种想法。

? 一种格式,而不是一个协议

为了更好地理解 XCM,重要的是要了解它的边界以及它在 Polkadot 技术堆栈中的位置。XCM 是一种消息格式。它不是消息传递协议。它不能用于在系统之间实际“发送”任何消息,它的作用仅在于表达接收者应该做什么。

不包括桥和合约模块,Polkadot 带有三个不同的系统,用于在其组成链之间实际通信 XCM 消息:UMP、DMP 和 XCMP。UMP(向上消息传递)允许平行链向它们的中继链发送消息。DMP(向下消息传递)允许中继链将消息向下传递到其平行链。XCMP 可能是其中最著名的,这允许平行链之间发送消息。XCM 可用于通过这三个通信通道中的任意一个来表达消息的含义。

资产除了在链之间发送消息之外,XCM 在其他语境也很有用,比如,可以用于在你之前不是很了解它交易格式的链上进行交易。对于业务逻辑变化很小的链(例如比特币),交易格式 —— 或者钱包用来向链发送指令的格式 —— 往往会无限期地保持完全相同,或者至少兼容。使用高度可进化的基于元协议的链,例如 Polkadot 及其组成的平行链,业务逻辑可以通过单个交易跨网络升级。这可以改变任何事情,包括交易格式,给钱包维护者带来潜在的问题,特别是对于需要离线保存的钱包(例如 Parity Signer)。由于 XCM 版本良好、抽象且通用,因此它可以用作一种为钱包提供持久交易格式的手段,用于创建许多常见交易。

? 目标

XCM 旨在成为共识系统之间交流思想的语言。它应该足够通用,以便在整个不断发展的生态系统中正确并有用。它应该是可扩展的,由于可扩展性不可避免地意味着变化,它也应该是面向未来和向前兼容的。最后,它应该足够高效可以在链上运行,并且还能在计量环境中运行。

像所有语言一样,有些人会比其他人更倾向于使用某些元素。XCM 的设计方式并不是让每个支持 XCM 的系统都能够解释任何可能的 XCM 消息。有些消息在某些系统下不会有合理的解释。其他的可能是合理的,但由于资源限制或因为可以以更清晰、更规范的方式表达相同的内容,解释器仍然故意不支持。系统将不可避免地只支持可能消息的一个子集。资源严重受限的系统(如智能合约)可能只支持非常有限的“方言”。

这种普遍性甚至延伸到诸如为执行 XCM 消息支付费用之类的概念。由于我们知道 XCM 可用于多种系统,包括 gas 计量的智能合约平台和社区平行链,一直到系统平行链与其中继链之间的可信交互,因此我们不想将费用支付等元素烤得太深和在协议中变得不可逆转。

? 为什么不直接使用本地消息格式

在某些情况下,捎带在本地信息/交易格式的链或智能合约可能很有用,但确实有一些很大的缺点,使其对 XCM 的目标不太有用。首先,链之间缺乏兼容性,因此打算向多个目的地发送消息的系统需要了解如何为每个目的地编写消息。在这一点上,即使是单个目的地也可能随着时间的推移改变其本地交易/消息格式。智能合约可能会升级,区块链可能会引入新功能或改变现有功能,从而改变其交易格式。

其次,链上的常见用例不容易适合单个交易;可能需要特殊的技巧来提取资金,交换资金,然后将结果全部存入单个交易中。连贯的储备资产框架所需的后续转移通知并不存在于不知道其他人的链中。

第三,诸如支付费用之类的操作不容易适应假设已经像智能合约消息一样协商费用支付的模型。相比之下,交易信封提供了一些支付处理的系统,但通常也被设计为包含一个签名,这在共识系统之间进行通信时没有意义。

? 一些初始用例

虽然 XCM 的目标是通用的、灵活的和面向未来的,但它当然必须满足实际需求,尤其是链之间的代币转移。可选的费用支付(可能使用这些代币)是另一种方式,就像进行交换服务的通用接口一样,在整个 DeFi 世界中都很常见。最后,应该可以使用 XCM 语言进行一些特定于平台的操作。例如,在 Substrate 链中,可能需要将远程调用分派到其模块之一以访问专门的功能。

最重要的是,有许多我们希望支持的代币转移模型:我们可能只想简单地控制远程链上的帐户,允许本地链在远程链上拥有一个地址以接收资金并最终将其控制的资金转移到该远程链上的其他账户中。

我们可能有两个共识系统,它们都是特定代币的“本地家园”。想象一下像 USDT 或 USDC 这样的代币,它在几个不同的链上都有实例,并且完全可以互换。应该可以在一条链上销毁这样的代币,并在另一条支持的链上铸造相应的代币。在 XCM 的说法中,我们之所以称之为传送(teleport),是因为资产的转移实际上是通过在一侧销毁它,并在另一侧创建一个克隆来实现的。

最后,可能有两条链想要提名第三条链,其中一条链上的资产可能被视为本地资产,用作该资产的储备。每个链上资产的衍生形式将得到完全支持,允许衍生资产交换为支持它的储备链上的基础资产。这可能是两条链不一定相互信任的情况,但(至少就相关资产而言)愿意信任资产的本地链。这里的一个例子是我们有几个社区平行链想要在彼此之间发送 DOT。它们每个都有一个本地形式的 DOT,由 Statemint 链(DOT 的本地中心)上的平行链控制的 DOT 完全支持。当在链之间发送本地形式的 DOT 时,在后台,“真正的”DOT 在 Statemint 上的平行链帐户之间移动。

即使这种看似适度的功能水平也有相对大量的配置,它们的使用可能是可取的,并且需要一些有趣的设计来避免过度拟合。

XCM 剖析

XCM 格式的核心在于 XCVM。与某些人的看法相反,这不是(有效的)罗马数字(尽管如果是,它可能意味着 905)。事实上,这代表跨共识虚拟机。这是一台超高级别的非图灵完备计算机,其指令设计为与交易大致处于同一级别。

XCM 中的“消息”实际上只是一个运行在 XCVM 上的程序。它是一个或多个 XCM 指令。程序会一直执行,直到它运行到最后或遇到错误为止,此时它会结束(我现在有意不解释这一点)并停止。

XCVM 包括许多 Register,以及访问托管它的共识系统的整体状态。指令可能会改变一个 Register,它们可能会改变共识系统的状态,或者两者兼而有之。

这种指令的一个例子是 TransferAsset,它用于将资产转移到远程系统上的某个其他地址。需要告知要转让哪些资产以及资产要转让给谁/在哪里。在 Rust 中,它是这样声明的:

enum Instruction {

TransferAsset {

assets: MultiAssets,

beneficiary: MultiLocation,

}

/* snip */

}

正如你可能猜到的那样,资产是表示要转让哪些资产的参数,而受益人则说明这些资产要交给谁/在哪里。当然,我们还缺少另一条信息,即从谁/何处获取资产。这是从原 Register自动推断出来的。程序开始时,这个 Register 一般是根据传输系统(网、XCMP 或者其他什么)来设置的,以反映消息实际来自哪里,和受益人是同一类型的信息。Origin Register 作为一个受保护的 Register 运行 —— 程序不能任意设置它,尽管有两条指令可以用来以某种方式改变它。

使用的类型是 XCM 中非常基本的思想:资产,由 MultiAsset 表示,共识内的位置,由 MultiLocation 表示。Origin Register 是一个可选的 MultiLocation(可选,因为如果需要它可以完全清除)。

? 在 XCM 的位置

MultiLocation 类型标识存在于共识世界中的任何单个位置。这是一个相当抽象的想法,可以代表共识中存在的所有事物,从可扩展的多分片区块链(如 Polkadot)一直到平行链上的低级 ERC-20 资产账户。在计算机科学术语中,它实际上只是一个全局单例数据结构,无论其大小或复杂性如何。

MultiLocation 始终表示相对于当前位置的位置。你可以把它想象成一个文件系统路径,但是没有办法直接表达文件系统树的“根”。这有一个简单的原因:在 Polkadot 的世界中,区块链可以合并到其他区块链中,也可以从其他区块链中分离出来。区块链可以非常独立地开始生命,并最终被提升为更大共识中的平行链。如果这样做,那么 “root” 的含义将在一夜之间改变,这可能会给 XCM 消息和其他任何使用 MultiLocation 的消息带来混乱。为了简单起见,我们完全排除了这种可能性。

XCM 中的位置是分层的,共识中的一些地方完全封装在共识中的其他地方。Polkadot 的平行链完全存在于整个 Polkadot 共识中,我们称之为内部位置。更严格地说,我们可以说,只要有一个共识系统的任何变化都意味着另一个共识系统的变化,那么前一个系统是后者的内部系统。例如,Canvas 智能合约位于托管它的合约模块的内部。比特币中的 UTXO 是比特币区块链的内部。

这意味着 XCM 没有区分 “谁” 和 “在哪里” 两个问题。从像 XCM 这样相当抽象的东西的角度来看,区别并不重要 —— 两者模糊并成为本质上相同的东西。

MultiLocations 用于识别发送 XCM 消息的位置,可以接收资产的位置,然后甚至可以帮助描述资产本身的类型,正如我们将看到的。非常有用的东西。

当用本文这样的文本写下来时,它们表示为一些 .. (或“父”,封装共识系统)组件,后跟一些连接点,所有连接点都用 / 分隔。(当我们用 Rust 之类的语言表达它们时,通常不会发生这种情况,但它在书面上是有意义的,因为它很像广泛使用的熟悉的目录路径。)连接在其封装共识中标识了一个内部位置系统。如果根本没有父节点/连接点,那么我们只说位置是这里。

例如:

  • ../Parachain(1000): 在平行链中进行评估,这将识别我们索引为 1000 的兄弟平行链。(在 Rust 中,我们将编写 ParentThen(Parachain(1000)).into())
  • ../AccountId32(0x1234…cdef): 在平行链中进行评估,这将识别中继链上的 32 字节帐户 0x1234…cdef。
  • Parachain(42)/AccountKey20(0x1234…abcd): 在中继链上进行评估,这将识别平行链编号 42 上的 20 字节帐户 0x1234…abcd (大概类似于承载以太坊兼容帐户的 Moonbeam)。

有许多不同类型的连接点,用于以各种方式识别你可能在链上找到的位置,例如键、索引、二进制 blob 和复数描述。

? XCM 中的资产

在 XCM 中工作时,通常需要引用某种资产。这是因为几乎所有现有的公共区块链都依赖于一些原生数字资产来为其内部经济和安全机制提供支柱。对于比特币等工作量证明区块链,原生资产(BTC)用于奖励开发区块链的矿工并防止双重支出。对于 Polkadot 等权益证明区块链,原生资产 (DOT) 用作一种抵押形式,网络管理员(称为权益人)必须承担风险才能生成有效区块并获得实物奖励。

一些区块链管理多种资产,例如以太坊的 ERC-20 框架允许在链上管理许多不同的资产。一些管理不可替代的资产,例如以太坊的 ETH,而是不可替代的 —— 独一无二的实例;Crypto-kitties 是此类不可替代代币或 NFT 的早期示例。

XCM 旨在能够毫不费力地处理所有此类资产。为此,有数据类型 MultiAsset 及其关联类型 MultiAssets、WildMultiAsset 和 MultiAssetFilter。让我们看看 Rust 中的 MultiAsset:

struct MultiAsset {

id: AssetId,

fun: Fungibility,

}

所以有两个字段定义了我们的资产:id 和 fun,这很好地表明了 XCM 如何处理资产。首先,必须提供整体资产身份。对于可替代资产,这只是标识资产。对于 NFT,这标识了整个资产“类别” —— 不同的资产实例可能在这个类别中。

plain

enum AssetId {

   Concrete(MultiLocation),

   Abstract(BinaryBlob),

}

资产身份以两种方式之一表示;无论是具体的还是抽象的。Abstract 并没有真正使用,但它允许通过名称指定资产 ID。这很方便,但依赖于接收者以发送者期望的方式解释名称,这可能并不总是那么容易。Concrete 在一般用途中使用并使用位置来明确地识别资产。对于原生资产(例如 DOT),资产往往被识别为铸造资产的链(在这种情况下是 Polkadot 中继链,这将是其平行链的位置 .. )。主要在链模块内管理的资产可以通过包括其在该模块内的索引的位置来识别。例如,Karura 平行链可能指的是 Statemine 平行链上的资产,位置为 ../Parachain(1000)/PalletInstance(50)/GeneralIndex(42) 。

enum Fungibility {

Fungible(NonZeroAmount),

NonFungible(AssetInstance),

}

其次,它们必须是可替代的或不可替代的。如果它们是可替代的,那么应该有一些相关的非零数量。如果它们不可替代,那么应该有一些指示它们是哪个实例而不是数量。(这通常用索引表示,但 XCM 还允许使用各种其他数据类型,例如数组和二进制 blob。) 这涵盖了 MultiAsset,但我们有时会使用其他三种相关类型。MultiAssets 就是其中之一,实际上只是意味着一组 MultiAsset 项目。然后我们有 WildMultiAsset;这是一个通配符,可用于匹配一个或多个 MultiAsset 项目。它实际上只支持两种通配符:All(匹配所有资产)和 AllOf 匹配特定身份 (AssetId) 和可替代性的所有资产。值得注意的是,对于后者,不需要指定数量(在可替代的情况下)或实例(对于非可替代的),并且全部匹配。

最后,还有 MultiAssetFilter。这是最常用的,实际上只是 MultiAssets 和 WildMultiAsset 的组合,允许指定通配符或明确(即非通配符)资产列表。

在 Rust XCM API 中,我们提供了很多转换,以尽可能轻松地处理这些数据类型。例如,当我们在 Polkadot 中继链上时,要指定等于 100 个不可分割的 DOT 资产单位的可替代 MultiAsset(普朗克,对于那些知道的人),那么我们将使用 (Here, 100).into()。

? Holding Register

我们再来看看另一个 XCM 指令:WithdrawAsset。从表面上看,这有点像 TransferAsset 的前半部分:它从起源的账户中提取了一些资产。但这与他们有什么关系?如果他们没有在任何地方存入,那么这肯定是一个非常无用的操作。如果我们查看它的 Rust 声明,我们会发现更多有关其用法的线索:

WithdrawAsset(MultiAssets),

所以,这次只有一个参数(MultiAssets 类型,它规定哪些资产必须从 Origin Register 的所有权中撤出)。但是没有指定放置资产的位置。

这些暂时持有的未动用资产称为持有 Register。(“Holding”是因为它们处于不能无限期持续的临时位置,“Register”是因为它有点像 CPU Register,是一个存放工作数据的地方。)有许多指令对持有寄存器进行操作。一种非常简单的指令是 DepositAsset 指令。让我们来看看它:

enum Instruction {

DepositAsset {

assets: MultiAssetFilter,

max_assets: u32,

beneficiary: MultiLocation,

},

/* snip */

}

啊哈!精明的读者会发现这看起来很像 TransferAsset 指令中缺失的一半。我们有 assets 参数,该参数指定应从持有寄存器中删除哪些资产以存放在链上。max_assets 让 XCM 作者通知接收者打算存入多少独特的资产。(这在知道 Holding Register 的内容之前计算费用时很有帮助,因为存入资产可能是一项代价高昂的操作。)最后是受益人,这与我们之前在 TransferAsset 操作中遇到的参数相同。有许多指令表示要在 Holding Register 上执行的操作,而 DepositAsset 是最简单的指令之一。其他一些则更复杂。

? XCM 中的费用支付

XCM 中的费用支付是一个相当重要的用例。Polkadot 社区中的大多数平行链都会要求其对话者为他们希望进行的任何操作付费,以免为 “交易垃圾” 和拒绝服务攻击敞开大门。当链有充分的理由相信它们的对话者会表现良好时,也存在例外情况 —— 当 Polkadot 中继链与 Polkadot Statemint 公共利益链通信时就是这种情况。但是,对于一般情况而言,费用是确保 XCM 消息及其传输协议不会被过度使用的好方法。我们来看看 XCM 消息到达 Polkadot 时如何支付费用。

正如前文提到的一样,XCM 不包括作为一等公民的费用和费用支付:与以太坊交易模型不同,对于费用支付不是协议中不需要的东西,就必须进行规避。就像 Rust 的零成本抽象一样,费用支付在 XCM 中没有很大的设计开销。

对于确实需要支付一定费用的系统,XCM 提供了使用资产购买执行资源的能力。概括来讲,这包括了三个部分:

  • 首先,需要提供一些资产。
  • 其次,必须就计算时间(用 Substrate 中的说法就是 weight)交换资产。
  • 最后,XCM 操作将按照指示执行。

第一部分由提供资产的多个 XCM 指令之一管理。我们已经知道其中的一个指令了( WithdrawAsset ),但还有其他几个我们稍后会看到。Holding Register中的所得资产当然将用于支付与执行 XCM 相关的费用。任何未用于支付费用的资产都将被存入某个目的地账户。在我们的示例中,我们假设 XCM 发生在 Polkadot 中继链上,交易的是 1 个 DOT(即 10,000,000,000 个不可分割的单位)。

目前我们的 XCM 指令是这样的:

WithdrawAsset((Here, 10_000_000_000).into()),

这将我们带到了第二部分,交换(一部分)这些资产以换取计算时间来支付我们的 XCM。为此,我们有 XCM 指令 BuyExecution 。我们来看看它是什么样的:

enum Instruction {

/* snip */

BuyExecution {

fees: MultiAsset,

weight: u64,

},

}

第一个项目 fees 是应从 Holding Register 中提取并用于支付费用的金额。从技术上讲,这只是最大值,因为任何未使用的余额都会立即退还。

最终花费的金额由解释系统决定—— fees 只是限制它,如果解释系统需要为所需的执行支付更多费用,那么 BuyExecution 指令将导致错误。第二个项目指定要购买的执行时间量。这一般不应小于 XCM 程序的总 weight。

在我们的示例中,我们假设所有 XCM 指令的 weight 为 100 万,因此到目前为止,我们的两个项目(WithdrawAsset 和 BuyExecution)为 200 万,接下来还有一个。我们将只使用我们必须支付这些费用的所有 DOT(仅当我们相信目的地链没有疯狂的费用时,这样做才合理 – 假设情况如此)。到这里,让我们看看我们的 XCM:

WithdrawAsset((Here, 10_000_000_000).into()),

BuyExecution {

fees: (Here, 10_000_000_000).into(),

weight: 3_000_000,

},

我们 XCM 的第三部分是存入 Holding Register 中剩余的资金。为此,我们将只使用 DepositAsset 指令。我们实际上并不知道 Holding Register 中还剩下多少,但这并不重要,因为我们可以为应该存入的资产指定一个通配符。我们将它们放在 Statemint 的主权账户中(标识为 Parachain(1000))。

所以我们最终的 XCM 指令是这样的:

WithdrawAsset((Here, 10_000_000_000).into()),

BuyExecution {

fees: (Here, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositAsset {

assets: All.into(),

max_assets: 1,

beneficiary: Parachain(1000).into(),

},

⛓ 使用 XCM 在链之间移动资产

将资产发送到另一条链可能是链间消息传递的最常见用例。允许一条链管理另一条链的本地资产,将允许各种衍生用例(无双关语),最简单的是去中心化交易所,但通常归为去中心化金融或 De-Fi。

一般来说,资产在链之间移动有两种方式,这取决于链之间是否信任彼此的安全性和逻辑。

✨ 传送(Teleporting)

对于相互信任的链(例如在相同的整体共识和安全保护伞下的同质分片),我们可以使用 Polkadot 称为传送的框架,这基本上就意味着在发送方销毁资产并在接收方铸造它。这种国防法既简单又高效——它只需要两条链的协调,并且每一侧只涉及一个动作。遗憾的是,如果接收链不能 100% 信任发送链实际销毁它正在铸造的资产(并且确实不铸造超出资产约定规则的资产),那么发送链确实没有根据消息铸造资产的理由。

我们来看看 XCM 将(大部分的)1 个 DOT 从 Polkadot 中继链传送到它在 Statemint 上的主权帐户时是什么样的。我们假设 Polkadot 这边已经支付了费用。

WithdrawAsset((Here, 10_000_000_000).into()),

InitiateTeleport {

assets: All.into(),

dest: Parachain(1000).into(),

xcm: Xcm(vec![

BuyExecution {

fees: (Parent, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositAsset {

assets: All.into(),

max_assets: 1,

beneficiary: Parent.into(),

},

]),

}

如你所见,这看起来和我们上次看到的直接 “提款-购买-存入” 的模式非常相似。不同之处在于 InitiateTeleport 指令,它插入在最后两条指令(BuyExecution 和 DepositAsset)周围。在幕后,发送链(Polkadot 中继链)在执行InitiateTeleport指令时正在创建一条全新的消息;它获取 xcm 字段并将其放入新的 XCMReceiveTeleportedAsset中,然后将此 XCM 发送到接收链 (Statemint) 。Statemint 相信 Polkadot 中继链在发送消息之前已经销毁了其一侧的 1 个 DOT。(事实确实如此!)

beneficiary (受益人)被声明为 Parent.into()  ,精明的读者可能想知道这在 Polkadot 中继链的上下文中指的是什么。答案是 “什么也不是”,但这里没有错误。xcm 参数中的所有内容都是从接收方的角度编写的,因此尽管这是整个 XCM 的一部分,它被馈送到 Polkadot 中继链,但它实际上只在 Statemint 上执行,因此它的上下文是跟着 Statemint 走的。

当 Statemint 最终收到这条消息时,它长这样:

ReceiveTeleportedAsset((Parent, 10_000_000_000).into()),

BuyExecution {

fees: (Parent, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositAsset {

assets: All.into(),

max_assets: 1,

beneficiary: Parent.into(),

},

你可能注意到了,这看起来跟之前的 WithdrawAsset XCM 非常像。唯一的主要区别是,它不是通过从本地账户提款来为费用和存款提供资金,而是通过相信 DOT 在发送方(Polkadot 中继链)上确实被销毁并尊重 ReceiveTeleportedAsset 消息。值得注意的是,我们在 Polkadot 中继链上发送的 1 个 DOT 的资产标识符(Here,指的是中继链本身是 DOT 的原生环境)已自动变异为它在 Statemint 上的表示: Parent.into(),即 Statemint 上下文中中继链的位置。

beneficiary 也被指定为 Polkadot 中继链,因此其(在 Statemint 上的)主权账户被记入新铸造的 1 DOT 减去费用。XCM 可能只是轻松地为beneficiary 指定了一个帐户或其他地方。实际上,可以使用从中继链发送的后续 TransferAsset 来移动这 1 个 DOT。

? 准备金(Reserves)

跨链转移资产的另一个方式稍微复杂一些。用到了称为准备金(reserve)的第三方。这个名字来自银行的准备金制度,也就是资产被 “储备” 起来,来让某些已发布的价值承诺具有可信度。例如,如果我们有理由相信在一条独立的平行链上发行的每个 “衍生” DOT 恰好可以兑换 1 个 “真” DOT(例如 Statemint 或中继链上的 DOT),那么我们就可以将平行链的 DOT 视为在经济上等同于真实的 DOT 。(大多数银行都做部分准备金银行业务,这意味着他们保留的准备金少于面值。这种做法平时没什么问题,但是当太多人都希望赎回,就会很快出问题。)所以,准备金是存储 “真实” 资产的地方,出于传输目的,其逻辑和安全性受到发送方和接收方的信任。发送方和接收方的任何相应资产都将是衍生品,但它们将得到 100% 的 “真实” 储备资产支持。假设平行链表现良好(即它没有漏洞,并且其治理没有决定偷走准备金跑路),这将使衍生品 DOT 与基础储备 DOT 的价值基本相同。储备资产保存在储备链上的发送者/接收者的主权账户(即由发送者或接收者链控制的账户)中,所以除非平行链出现问题,否则有充分的理由相信这些资产会受到很好的保护。

说回到转账机制,发送方将指示准备金把发送方拥有的资产(并将其用作其自己版本的相同资产的准备金)转移到接收方的主权账户中,而准备金(而不是发送方!)通知接收方他们的新资产。这意味着发送方和接收方不需要信任彼此的逻辑或安全性,而只需信任用作准备金的链的逻辑或安全性。然而,这确实意味着三方需要协调,这增加了整体成本、时间和复杂性。

让我们看看所需的 XCM。这次我们将从平行链 2000 发送 1 个 DOT 到平行链 2001,它在平行链 1000 上使用准备金支持的 DOT。同样,我们假设费用已经在发送方支付过了。

WithdrawAsset((Parent, 10_000_000_000).into()),

InitiateReserveWithdraw {

assets: All.into(),

dest: ParentThen(Parachain(1000)).into(),

xcm: Xcm(vec![

BuyExecution {

fees: (Parent, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositReserveAsset {

assets: All.into(),

max_assets: 1,

dest: ParentThen(Parachain(2001)).into(),

xcm: Xcm(vec![

BuyExecution {

fees: (Parent, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositAsset {

assets: All.into(),

max_assets: 1,

beneficiary: ParentThen(Parachain(2000)).into(),

},

]),

},

]),

},

就像我之前说的,这会有点复杂。让我们来看看这个过程。外部部分负责在发送方(平行链 2000)上提取 1 个 DOT 并撤回 Statemint(平行链 1000)上相应的 1 个 DOT —— 为此它使用了 InitiateReserveWithdraw ,其作用就是字面意思(开启准备金提取)。

WithdrawAsset((Parent, 10_000_000_000).into()),

InitiateReserveWithdraw {

assets: All.into(),

dest: ParentThen(Parachain(1000)).into(),

xcm: /* snip */

}

现在我们在 Statemint 的 Holding Register 中持有 1 个 DOT。在我们可以做其他事情之前,我们需要在 Statemint 上购买一些执行时间。这个过程看起来也很眼熟:

/*snip*/

xcm: Xcm(vec![

BuyExecution {

fees: (Parent, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositReserveAsset {

assets: All.into(),

max_assets: 1,

dest: ParentThen(Parachain(2001)).into(),

xcm: /* snip */

},

]),

/*snip*/

我们使用自己的 1 DOT 来支付费用,我们假设每个 XCM 操作有 100 万。支付了这一项操作后,我们将 1 个 DOT(减去费用,而且我们很懒,所以我们只使用 All.into())存入平行链 2001 的主权账户,但这样做是作为储备资产,这意味着我们也要求 Statemint 向该接收链发送通知 XCM,通知它传输以及要对生成的衍生资产执行的一些指令。DepositReserveAsset 指令并不总是奏效;为了让它奏效, dest  必须是一个可以在准备金链上合理持有资金的位置,而且也是储备链可以向其发送 XCM 的位置。兄弟平行链恰好符合要求。

/*snip*/

xcm: Xcm(vec![

BuyExecution {

fees: (Parent, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositAsset {

assets: All.into(),

max_assets: 1,

beneficiary: ParentThen(Parachain(2000)).into(),

},

]),

/*snip*/

最后一部分定义了到达平行链 2001 的消息的一部分。就像启动传送操作一样, DepositReserveAsset  编写并发送一条新消息,在这里是 ReserveAssetDeposited 。正是这个消息,虽然包含我们定义的 XCM 程序,但它到达了接收平行链。它看起来像这样:

ReserveAssetDeposited((Parent, 10_000_000_000).into()),

BuyExecution {

fees: (Parent, 10_000_000_000).into(),

weight: 3_000_000,

},

DepositAsset {

assets: All.into(),

max_assets: 1,

beneficiary: ParentThen(Parachain(2000)).into(),

},

(这假设 Statemint 实际上没有收取任何费用,并且整个 1 DOT 都传过去了。这不是特别现实,因此 assets 这一行的数字可能会更低。) 这条消息中的大部分地方看起来应该很熟悉;与我们在上一节中看到的 ReceiveTeleportedAsset  消息的唯一显著区别是顶层指令 ReserveAssetDeposited ,它实现了类似的目的,只是表示 “发送链销毁了资产,以便你可以铸造等价资产”,它的意思是 “发送链收到了资产并为你保留它们,因此你可以铸造有全额资产背书的衍生品”。无论哪种方式,目的地链都会将它们铸造到 Holding Reserve 中,然后我们将它们存入接收链上的发送者主权账户中。?

? 结论

以上就是本文的内容。希望它有助于解释 XCM 是什么,以及它是如何设计以运转的基础知识。在下一篇文章中,我们将深入研究 XCVM 的架构、其执行模型及其错误处理、XCM 的版本控制系统;如何在连接良好的、相互依赖的生态系统中管理格式升级;其查询-响应系统;以及 XCM 在 Substrate 中的工作原理。我们还将讨论 XCM 的一些未来发展方向、计划的功能以及发展它的过程。

原文链接:medium.com