XCM第三部分:执行和错误管理
XCM Part III: Execution and Error Management
XCM第三部分:执行和错误管理
September 30, 2021inPolkadot by Gavin Wood
https://polkadot.network/blog/xcm-part-three-execution-and-error-management/
在我写的关于 XCM 的前两篇文章(第一部分,第二部分)中,我介绍了其设计和版本结构的基本情况。在这篇文章中,我们将更深入地了解它的基本设计和执行模型。因为XCM基于的是XCVM(一种非常高级的虚拟机)指令集,这相当于熟悉其机器架构。
XCVM是一个非常高级的非图灵完备虚拟机。它是基于寄存器的(而不是基于堆栈的),并且含有几个专用寄存器,其中大部分保存高度结构化的数据。与通用处理器不同,XCVM的寄存器不能自由地设置为任意的值,而是有严格的机制来管理它们如何变化。除了与本地链状态进行交互的某些手段(如我们已经见过的WithdrawAsset和DepositAsset指令),没有额外的 "内存"。此情况下不会进行循环,也没有明确的分支指令。
我们已经了解了其中两个寄存器:持有寄存器,它能够暂时持有一个或多个资产,可以通过从本地链中提取资产,或者通过从可信的外部来源(例如另一个链)接收资产来填充;另一个是起始寄存器,该寄存器在执行之初保存着当前XCM执行起始的共识系统位置,并且只能被转变到内部位置或完全清除掉。
在其他的寄存器中,有三个与异常/错误管理有关,两个与跟踪执行权重有关。我们将在本文中了解所有有关这些寄存器的情况。
执行模型
如前所述,没有明确的条件性指令或循环基元可以多次重新执行同一条指令。这使得预先确定程序的控制流相当简单。鉴于我们想确定一条XCM消息在执行前可以利用多少执行时间(在Substrate/Polkadot中被称为权重),这一特性非常有用。
我们希望执行XCM的大多数共识平台将需要能够在开始执行之前确定最坏情况下的执行时间。这是因为区块链通常需要确保单个区块的处理时间不会超过某个预定限制,以免导致整个系统失速。此外,如果系统需要支付费用,则必须在进行支付的工作量之前完成,重要的是该费用必须涵盖最坏情况下的执行时间。
由于这种图灵完整性,允许图灵完备语言的系统(例如以太坊)实际上无法直接从程序中计算出最坏情况的执行时间。他们通过要求用户预先确定程序的执行资源来解决这个问题,然后在执行过程中对其进行计量,如果超过了所支付的费用就中断程序。有时在事务执行之前情况会发生变化,权重也会出错。好在像 XCVM 这样的虚拟机不是图灵完备的,它可以避免这种计量和权重规定的需要。
权重
权重通常表示为一个代表性硬件执行给定操作所需的整数皮秒。正如我们在BuyExecution指令中所看到的,XCVM在处理某些指令时包含了执行时间/重量的概念。
此处没有对权重进行计量,但考虑到XCVM程序最终采用低于最坏情况下权重预测的可能性,有一个称为剩余权重寄存器 (Surplus Weight Register)的寄存器来处理。大多数指令不会接触到它,因为我们可以准确预测将使用多少权重。然而在某些情况下,最坏情况下的权重预测是高估的,只有在执行时我们才知道具体是多少。当用XCM消息的高估权重来计算区块执行时间时,跟踪原始权重的高估量并从账目中减去,使链上的区块执行时间配额得到优化。
因此,剩余权重寄存器对区块执行时间核算是有用的,但它本身并不能解决另外一个确保支付金额不被高估的问题。为此,我们需要一个与BuyExecution相配套的指令,该指令将任何剩余的权重予以退还。这个指令是存在的,叫做RefundSurplus。它还利用了第二个寄存器,称为退还权重寄存器,以确保同一剩余权重不会被多次退还。
流量控制和异常情况
到目前为止,在我们对XCVM的处理中还有两个隐式寄存器,仍然需要对其进行了解。首先是程序寄存器 (Programme Register),它存储当前执行的 XCVM 程序。其次是程序计数器 (Programme Counter),它存储当前执行的指令索引。当程序寄存器更改时,计数器重置为零,在每个成功执行的指令结束时增加一。
在编写健壮的代码时,处理“异常”情况的能力至关重要。当远程系统上发生了没有预料到(或确实无法预测)的事情时,即使只是简单地向源系统发送一份报告进行说明,您也需要通过某种方法来管理它。
虽然XCVM指令集不包括任何明确的通用分支指令,但它确实在其执行模型中内置了一个通用异常处理框架。XCVM还包括两个代码寄存器,每个寄存器包含一个类似于程序寄存器的XCVM程序。这两个寄存器被称为附录寄存器和错误处理器寄存器。如果您熟悉几种流行语言中的try/catch/finally异常处理系统,接下来的内容可能会唤起联想。
如前所述,XCVM程序的执行是按照其中的每条指令一步步进行的。当它遵循这些指令直到程序结束时,会发生两种情况的其一:要么成功到达程序的终点,要么发生错误。在第一种成功执行的情况下,错误寄存器将清零,其权重被添加到剩余重量寄存器中。附录寄存器也被清空,其内容放入程序寄存器中。如果程序寄存器为空,那么就停止执行程序,否则程序计数器会重置为零。简单地说,我们丢弃当前程序和错误处理程序,并开始执行附录程序(如果有)。
这个功能本身并不那么有用,但如果与错误情况结合起来就会很有用。此时,任何尚未执行的指令的权重被添加到剩余权重寄存器。错误处理寄存器被清空,其内容被放入程序寄存器,程序计数器重置为零。简单地说,我们丢弃当前的程序,开始执行错误处理程序。因为没有清除附录寄存器,所以除非它被错误处理程序重置,否则将在成功完成后执行。
由于其组成结构,该功能允许错误处理程序的任意“嵌套”:如果需要,错误处理程序可以有自己的错误处理程序,附录也可以有自己的附录。
存在两条指令允许对这些寄存器进行操作,分别是SetAppendix和SetErrorHandler。其中一个设置附录寄存器,另一个设置错误处理程序寄存器。其中每个参数的预测权重都比其参数的权重略大。但是在执行时,即将遭替换的寄存器中XCM消息的权重添加到剩余权重寄存器中,从而允许回收任何附录或错误处理程序未使用的权重。
抛出错误
有时,确保发生错误并自定义该错误的某些方面可能很有用。这在编写测试代码时使用过,但它最终可能会在实际运行的链中找到用途也是可能的。这可以通过指令Trap在XCVM中完成,该指令总是导致错误发生。被抛出的错误类型与Trap名称相同。指令和错误都带有一个整数参数,允许某种形式的信息在错误抛出者和外部观察者之间传递。
下面是一个简单的例子:
WithdrawAsset((Here, 10_000_000_000).into()),
BuyExecution {
fees: (Here, 10_000_000_000).into(),
weight: Unlimited,
},
SetErrorHandler(Xcm(vec![
RefundSurplus,
DepositAsset {
assets: All.into(),
max_assets: 1,
beneficiary: Parachain(2000).into(),
},
])),
Trap(0),
DepositAsset {
assets: All.into(),
max_assets: 1,
beneficiary: Parachain(3000).into(),
},
Trap导致最后的DepositAsset被跳过,取而代之的是运行错误处理程序的DepositAsset,将 1 DOT(减去执行成本)置于parachain2000的所有权之下。我们总是倾向于在错误处理程序代码的开头使用RefundSurplus,因为如果它正在运行,我们知道使用的预测权重(以及因此购买的权重)很可能是高估的。
错误报告
能够引入代码来处理错误是非常有用的,但有一个要求的功能是能够将XCM消息的结果反馈给原发送端。我们在上一篇文章中见到了QueryResponse指令,它允许一个共识系统将一些信息反馈给另一个,剩下的就是能够以某种方式将XCM的结果插入QueryResponse中,并将其发送给希望被告知结果的一方。
事实证明,只有一条指令能执行此操作,即ReportError。它通过使用我们尚未接触过的错误寄存器来工作。错误寄存器是一种可选类型(可以设置或清除)。如果已进行设置,则它包含两条信息:数字索引和XCM错误类型。
ReportError的运行机制非常简单。首先,当一条指令产生错误时,总是对其进行设置;错误类型被设置为该错误的类型,数字索引被设置为程序计数器寄存器的值。其次,只有当ClearError指令被执行时,它才会被清除。这条指令是绝对正确的指令之一--它本身是不允许导致错误的。如此一来,它在发生错误时设置,在发出适当的指令时清除。
现在我们应该清楚地了解ReportError指令的工作原理:它只是用错误寄存器的内容组成一个QueryResponse指令,并将其发送到特定目标。当然,在它之前发生的任何错误都会导致指令被跳过,因为执行时首先跳转到错误处理寄存器的代码,随后跳转到附录寄存器的代码。然而,解决这个问题的方法很简单:将ReportError放在附录中可以确保它被执行,而不管主代码是否导致执行错误。
让我们看看一个简单的例子。我们将一笔资产(1 DOT)从中继链传送到Statemint(parachain1000),在那里购买一些执行时间,然后用Statemint作为储备,将资产存入parachain2000。原始(非报错)信息看起来是这样的:
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: Unlimited,
},
DepositReserveAsset {
assets: All.into(),
max_assets: 1,
dest: ParentThen(Parachain(2000)).into(),
xcm: Xcm(vec![
BuyExecution {
fees: (Parent, 10_000_000_000).into(),
weight: Unlimited,
},
DepositAsset {
assets: All.into(),
max_assets: 1,
beneficiary: Parent.into(),
},
]),
},
]),
}
在基本的错误报告中,我们会使用以下方式来代替:
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: Unlimited,
},
SetAppendix(Xcm(vec![
ReportError {
query_id: 42,
dest: Parent.into(),
max_response_weight: 10_000_000,
},
])),
DepositReserveAsset {
assets: All.into(),
max_assets: 1,
dest: ParentThen(Parachain(2000)).into(),
xcm: Xcm(vec![
BuyExecution {
fees: (Parent, 10_000_000_000).into(),
weight: Unlimited,
},
SetAppendix(Xcm(vec![
ReportError {
query_id: 42,
dest: Parent.into(),
max_response_weight: 10_000_000,
},
])),
DepositAsset {
assets: All.into(),
max_assets: 1,
beneficiary: ParentThen(Parachain(2000)).into(),
},
]),
},
]),
}
正如您所看到的,唯一的变化是引入了两条SetAppendix指令,确保Statemint和parachain 2000中的错误或缺失将被报告给中继链。此处假定中继链已设置为能够识别和处理来自Statemint和parachain2000的查询ID为42、权重限制为1000万的QueryResponse消息。好在这确实是Substrate所支持的,但现在已经超出了范围。
资产陷阱
当在处理资产的程序中发生错误时(因为大多数程序通常需要用BuyExecution来支付执行费用),就会出现很大的问题。在某些情况下,BuyExecution指令本身可能会导致错误,也许是因为权重限制不正确或用于支付的资产不足。也可能是资产被送到了一个无法对其进行有效处理的链上。在这些和其他许多情况下,当消息的XCVM执行结束时,资产仍在持有寄存器中,与其他寄存器一样是短暂的,我们希望能被遗忘。
团队及其用户喜闻Substrate 的 XCM 能让链完全避免这种损失。该机制分两步运作。首先,当清空持有寄存器中的任何资产时不会被完全遗忘。如果 XCVM 停止时持有寄存器不为空,则会发出包含三条信息的事件:持有寄存器的值;起始寄存器的原始值;以及这两条信息的哈希值。然后,Substrate 的 XCM 系统会将这个哈希值放入存储器。该机制的这一部分称为“资产陷阱”。
索赔系统
该机制的第二步是能够对持有寄存器中以前的一些内容进行索赔。这实际上并不是通过任何专门设计的东西来实现的,而是通过一个我们尚未遇到的通用指令,称为ClaimAsset。下面是它在 Rust 中的声明方式:
pub enum Instruction {
/* snip */
ClaimAsset { assets: MultiAssets, ticket: MultiLocation },
/* snip */
}
这条指令的名字可能会让人想起我们见过的其他一些 "资金 "指令,比如WithdrawAsset和ReceiveTeleportedAsset。如果是这样,那么有一个很好的理由:它和其他指令一样,试图将资产(由这里的资产参数给出)放入持有寄存器中。与WithdrawAsset会减少账户在链上的资产余额不同,ClaimAsset则是寻找这些资产的有效索赔,无论起始寄存器的价值是多少。为了帮助系统找到有效的索赔,可以通过ticket参数提供信息。如果找到有效的索赔,则从链中删除,并将资产添加到持有寄存器中。
现在,索赔的构成完全由链本身决定。不同的链可能支持不同种类的索赔,而Substrate允许您轻松地组合它们。但是正如您可能猜到的那样,有一种特殊的索赔是可以随时使用的,那就是先前放弃的持有寄存器内容。
让我们来看看这在实践中是如何运作的。假设我们用户的parachain2000向Statemint发送了一条消息,后者从其主权账户中提取了0.01DOT以支付费用,并通知它将100个单位的自身原生代币的储备资产转移到Statemint的主权账户。它看起来像这样:
WithdrawAsset((Parent, 100_000_000).into()),
BuyExecution {
fees: (Parent, 100_000_000).into(),
weight: Unlimited,
},
SetAppendix(Xcm(vec![
ReportError {
query_id: 42,
dest: ParentThen(Parachain(2000)).into(),
max_response_weight: 10_000_000,
},
RefundSurplus,
])),
ReserveAssetDeposited((ParentThen(Parachain(2000)), 100).into()),
DepositAsset {
assets: All.into(),
max_assets: 2,
beneficiary: ParentThen(Parachain(2000)).into(),
}
假设0.01 DOT足够支付此费用,并且Statemint支持parachain 2000原生资产的链上存款(以及使用parachain 2000作为其储备),这应该没问题。然而,也许Statemint还没有被设置为识别parachain 2000的原生资产。在这种情况下,DepositAsset将不知道如何处理该资产并相应地抛出错误。在执行将通知parachain 2000该故障的附录后,我们将剩下parachain 2000的100个单位的原生资产,以及持有寄存器中可能的一些DOT。
让我们假设费用仅需0.005DOT,剩下0.005DOT。
然后,Statemint 的 XCM pallet 会记录一个事件,涉及这些新的可索赔资产,类似于:
Event::AssetsTrapped(
/* snipped hash */,
ParentThen(Parachain(2000)),
vec![
(Parent, 50_000_000).into(),
(ParentThen(Parachain(2000)), 100),
].into(),
)
将向parachain 2000发送一条消息,如下所示:
QueryResponse {
query_id: 42,
response: ExecutionResult(Err((4, AssetNotFound))),
max_weight: 10_000_000,
}
Parachain 2000将在稍后的某个阶段(可能一旦确定Statemint能够接受其原生资产的存款时),能够以相当简单的方式收回这100个单位:
ClaimAsset {
assets: vec![
(Parent, 50_000_000).into(),
(ParentThen(Parachain(2000)), 100),
].into(),
ticket: Here,
}
BuyExecution {
fees: (Parent, 50_000_000).into(),
weight: Unlimited,
},
DepositAsset {
assets: All.into(),
max_assets: 2,
beneficiary: ParentThen(Parachain(2000)).into(),
}
这种情况下没有通过ticket参数提供特殊信息来帮助定位索赔。这通常适用于资产陷阱索赔,可能也有必要将其用于其他类型的索赔。
总结
以上就是本文的全部内容--我希望这篇文章能帮助您进一步了解XCM的底层虚拟机,以及它如何帮助您管理和从意外情况中恢复。本系列的下一篇文章将介绍XCM的未来发展方向、如何对该格式提出改进建议,并深入探讨Substrate的XCM在Rust中的实现,以及我们如何使用它提供一个能够轻松解释XCM的链。
I’m Lucas Yoda, a Jedi of the Crypto Community, enjoying Polkadot and all the parachains. Also a Validator within the Community to support the core infrastructure.
My Discord Channel: lucasyoda#6714 My Element Channel: @lucasyoda:matrix.org
0 comments