学习和实践Polkadot XCM
指引:
Parachain Development · Polkadot Wiki
XCM: The Cross-Consensus Message Format
XCM is related to cross-chain in the same way that REST is related RESTful.
XCM是一种消息格式,而不是消息传递协议, 因为它无法在系统之间发送任何消息,仅仅是一种应如何执行消息传输的格式,也就是表达接收者应该做什么。XCM还可用于通过UMP(向上信息传递)、DMP(向下消息传递)、XCMP(跨链消息传递)通信通道中的任意一个来表发消息的含义。
xcm消息里带有执行消息和位置信息
例如:链和链之间,合约与合约之间的xcm消息传递都可以用这套规范来实现,这些东西不能依托于链来实现,因为这套规范,如果依托于链本身,那每次链的升级都相当于做了一次xcm版本的升级。这很影响兼容性。
XCM 不仅仅是跨链,而是 " 跨共识 " 消息格式!-技术圈
XCM format 的核心是 XCVM(交叉共识虚拟机),它是一种超高级非图灵完备计算机,它的指令设计和交易大致处于同一水平。
XCM中的”消息“(就是上文的messages/消息)实际上只是XCVM上运行的程序。它是一个或多个 XCM指令。程序一直执行,直到它运行到最后或遇到错误,此时它完成并停止。
XCVM 包括许多寄存器,以及对托管它的共识系统的整体状态的访问。指令可能会更改寄存器,它们可能会更改共识系统的状态或两者兼而有之。
可以理解为共识系統语义下的“位置”
需要注意的是,所有位置都是从消息解释器的相对角度来看的。就如下面的例如...
从 Parachain A 的⻆度来看:
Polkadot Messaging Guide - HackMD
Cross-Consensus Protocols
随着 XCM Format 的正式建立,这些 messages 需要协议的通用模式,Polkadot实现了两个,用于在其组成的 parachain 之间处理XCM messages。
(分别是VMP和XCMP)
注:下文中的“消息” 意指上面提到的messages。
实际上,有两种垂直消息传递传输协议。
注意, DMP
的消息也可能来自平行链,例如:平行链A转给平行链B的情况,这个过程是这样子的。
首先平行链A将使用UMP
,将消息传递给中继链,其次中继链再根据DMP
将消息向下传递给平行链B
XCMP 允许平行链与其中继链上其他的平行链交换消息。
跨链交易使用基于 Merkle 树的简单排队机制来解决,以确保fidelity(保真度)。中继链验证人的任务是将一个平行链的输出队列上的交易移动到目标平行链的输入队列中。但是,只有关联的 metadata 作为 hash 存储在中继链的存储中。
输入和输出队列有时在 Polkadot 代码库和相关文档中分别称为ingress
(输入队列)和egress
(输出队列)消息。(这里队列的概念下文会提到。)
水平中继路由消息传递协议 (HRMP) 协议是未完全完成的功能的临时替代品。虽然 XCMP 本身仍在开发中,但 HRMP 是一个有效的替代品。
HRMP 具有与 XCMP 相同的接口和功能,但对资源的要求更高,因为它将所有消息存储在中继链存储中。当 XCMP 实施后,HRMP 计划被弃用并逐步淘汰以支持它。
(思想:中继链只保存相关元数据的hash值,只做关于xcm的相关验证。)
由于现在XCMP还没有被完全开发出来,现在主要使用的是HRMP,上文也提到了,HRMP需要用到许多的资源,现在吞吐量 可能会是42条平行链上下。不过现在parity的开发者已经在把其他模块交易移到其他平行链 比如国库财政部分来提高吞吐能力和承载链数量。
小问题: xcm消息执行失败了,怎么办呢(因为中继链最后敲定区块,所以能够解决回滚的问题)题外话:波卡网络上平行链只负责出块,所以没有grandpa共识,最后敲定区块上交给中继链决定的。
平⾏链阶段中,收集⼈打包区块的同时,也会将跨链交易放到平⾏链的出队列中。跨链交易通过 XCMP 协议进⾏传输,根据收集⼈和验证⼈的⽹络连接情况,
具体的传输⽅法有三种:
如果A和B不共享全节点,则需要依靠中继链上的验证⼈来传递消息。
中继链验证者应负责将某⼀平⾏链上输出队列中的交易移⾄⽬标平⾏链上的输⼊队列中。
cumulus 的 pallets 中两种队列:
消息队列链是由验证人创建的一个通用哈希链,用于跟踪从发送方发送到单个接收方的每条消息以及顺序。
MQC本身不保存在任何地方,而是只提供所有接受到的消息的最终证明。当验证器接收到候选消息时,它从放置在upward_messages中的消息中按升序生成MQC
上方都是关于XCM的基础部分,有了上面的知识,我们就进一步扩展讲讲其中的一些应用实现,例如跨链资产转账。
关于平行链之间进行资产转账会有一些细节。
XCM其实定义了两种转账的方式,一种是Asset Teleportation一种是Reserve Asset Transfer。 参考:How can I transfer assets using XCM?
这个转账模型只有两个参与者,源(Source)和目的地(Destination)。 例子(伪代码):
// 链A的Alice向链B的Bob转账100个链A的native token_a
Transfer_teleport_asset(source_alice, dest_bob, token_a, 100);
过程:
缺点:
它要求来源和目的地都具有高度的相互信任。目的地必须相信来源已经烧毁了发送过来的资产,并且来源还必须相信目的地铸造在来源处被烧毁的资产。不支持这两个条件中的任何一个都将导致资产的总发行量发生变化(在可替代代币的情况下)或 NFT 的完全丢失/重复。
过程:chain A 上的 account1 想转移某个资产到 chain B 上的 account2账⼾⾥,那⾸先将 account 1的资产转移⾄ chain A 上 的 chain B 代表账⼾,再发送⼀条通知消息给 chain B,chain B 将对应的资产分配给 account2。
ps: 其实业界里更推崇后者(reserve),相较于前者会更有保障。像orml-xtokens其实就是基于reserve方式实现的平行链多资产转账模块。
我们接下来的目的就是创建两条平行链,让这两条平行链支持多资产
并且实现跨链资产转账
。
实验环境
我们会准备4个中继链的验证人节点以支持两条平行链。因为我们要模拟链A到链B的跨链资产转移以及平行链到中继链的跨链资产转移。
- 4个验证人的中继链
- 平行链A
- 平行链B
平行链的跨链转账一共有两种场景:
orml
模块就行。接下来我们直接分析整个完整的runtime配置来介绍一下配置跨链资产转账时需要注意的配置项以及其含义。
为两条平行链添加支持向中继链进行跨链转账的功能。
其实是关于XcmExecutor
的配置,其中一项XcmConfig就是指定XcmSender. 这是你需要包含实现 XCMP 协议的pallet的地方。根据您要将 XCM 发送给谁,如果是要发送到中继链,则需要包含parachain-system-pallet,或者如果你要发送到同级平行链,则需要包含 xcmp-queue-pallet。
/// queues.
pub type XcmRouter = (
// Two routers - use UMP to communicate with the relay chain:
// ================================
// 需要修改的地方:最后我们是需要支持平行链到中继链,平行链到平行链,所以两个配置我们都要加。
cumulus_primitives_utility::ParentAsUmp<ParachainSystem, PolkadotXcm>,
// ..and XCMP to communicate with the sibling chains.
XcmpQueue,
// ================================
);
// ......
// ......
// ......
pub struct XcmConfig;
impl xcm_executor::Config for XcmConfig {
type Call = Call;
type XcmSender = XcmRouter;
// How to withdraw and deposit an asset.
type AssetTransactor = LocalAssetTransactor;
type OriginConverter = XcmOriginToTransactDispatchOrigin;
type IsReserve = NativeAsset;
type IsTeleporter = (); // Teleporting is disabled.
type LocationInverter = LocationInverter<Ancestry>;
type Barrier = Barrier;
type Weigher = FixedWeightBounds<UnitWeightCost, Call, MaxInstructions>;
type Trader = UsingComponents<IdentityFee<Balance>, RelayLocation, AccountId, Balances, ()>;
type ResponseHandler = PolkadotXcm;
type AssetTrap = PolkadotXcm;
type AssetClaims = PolkadotXcm;
type SubscriptionService = PolkadotXcm;
}
为平行链A和平行链B配置ORML相关库以实现平行链之间的跨链资产转移
将会用到ORML
的一些依赖库:
BasicCurrency
、MultiCurrency
、Auction
等trait。最后实现的效果: 链A的Alice通过
xtokens
模块进行跨链资产转移将 token_a 转移给链B的Bob, 转账成功后,链B上的Bob通过tokens
模块查看 token_a 的余额,然后通过currencies
模块将一部分的token_a转账给链B上的Alice。
为了平行链能够支持多资产转移,我们除了需要添加上面的ORML依赖库
,还需要做一些定制化的配置。
CurrencyId
和CurrencyIdConvert
,一个是代币的tokenSymbol
的list一个是将tokenSymbol转换成multilocation的转换器。1. 配置 CurrencyId 和 CurrencyIdConvert
假设条件:
平行链A -> TokenSymbol: AA -> ParachainId:1000
平行链B -> TokenSymbol:BB -> ParachainId:2000
#[derive(
Encode,
Decode,
Eq,
PartialEq,
Copy,
Clone,
RuntimeDebug,
PartialOrd,
Ord,
codec::MaxEncodedLen,
TypeInfo,
)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum CurrencyId {
// / Relay chain token.
ROC,
// Native TokenSymbol
AA,
// ===============================
// 1. 添加支持的代币tokenSymbol
// ===============================
BB
}
pub type Amount = i128;
// ===============================
// 2. 设置 CurrencyIdConvert,(CurrencyId转MultiLocation的转换方式)
// ===============================
pub struct CurrencyIdConvert;
impl Convert<CurrencyId, Option<MultiLocation>> for CurrencyIdConvert {
fn convert(id: CurrencyId) -> Option<MultiLocation> {
match id {
CurrencyId::ROC => Some(Parent.into()), // 支持中继链代币的转换
CurrencyId::AA => Some((Parent, Parachain(1000), GeneralKey("AA".into())).into()),
CurrencyId::BB => Some((Parent, Parachain(2000), GeneralKey("BB".into())).into()),
}
}
}
impl Convert<MultiLocation, Option<CurrencyId>> for CurrencyIdConvert {
fn convert(l: MultiLocation) -> Option<CurrencyId> {
let aa: Vec<u8> = "AA".into();
let bb: Vec<u8> = "BB".into();
if l == MultiLocation::parent() {
return Some(CurrencyId::ROC);
}
match l {
MultiLocation { parents, interior } if parents == 1 => match interior {
X2(Parachain(1000), GeneralKey(k)) if k == aa => Some(CurrencyId::AA),
X2(Parachain(2000), GeneralKey(k)) if k == bb => Some(CurrencyId::BB),
_ => None,
},
MultiLocation { parents, interior } if parents == 0 => match interior {
X1(GeneralKey(k)) if k == aa => Some(CurrencyId::AA),
X1(GeneralKey(k)) if k == bb => Some(CurrencyId::BB),
_ => None,
},
_ => None,
}
}
}
impl Convert<MultiAsset, Option<CurrencyId>> for CurrencyIdConvert {
fn convert(asset: MultiAsset) -> Option<CurrencyId> {
if let MultiAsset {
id: Concrete(id),
..
} = asset
{
Self::convert(id)
} else {
Option::None
}
}
}
2. 引入orml标准库
配置完CurrencyId和CurrencyIdConvert之后就可以进行引入orml标准库的工作了。
// xcm_config.rs
use orml_currencies::BasicCurrencyAdapter;
use orml_traits::parameter_type_with_key;
use orml_xcm_support::{
DepositToAlternative, IsNativeConcrete, MultiCurrencyAdapter, MultiNativeAsset,
};
//
// ......
//
parameter_types! {
pub const GetNativeCurrencyId: CurrencyId = CurrencyId::AA;
}
impl orml_currencies::Config for Runtime {
type Event = Event;
type MultiCurrency = Tokens;
type NativeCurrency = BasicCurrencyAdapter<Runtime, Balances, Amount, BlockNumber>;
type GetNativeCurrencyId = GetNativeCurrencyId;
type WeightInfo = ();
}
pub struct AccountIdToMultiLocation;
impl Convert<AccountId, MultiLocation> for AccountIdToMultiLocation {
fn convert(account: AccountId) -> MultiLocation {
X1(AccountId32 { network: NetworkId::Any, id: account.into() }).into()
}
}
parameter_types! {
pub SelfLocation: MultiLocation = MultiLocation::new(1, X1(Parachain(ParachainInfo::parachain_id().into())));
pub const BaseXcmWeight: Weight = 100_000_000;
pub const MaxAssetsForTransfer: usize = 2;
pub const TreasuryPalletId: PalletId = PalletId(*b"aa/trsry");
}
parameter_type_with_key! {
pub ParachainMinFee: |location: MultiLocation| -> u128 {
#[allow(clippy::match_ref_pats)] // false positive
match (location.parents, location.first_interior()) {
(1, Some(Parachain(3000))) => 4_000_000_000,
_ => u128::MAX,
}
};
}
impl orml_xtokens::Config for Runtime {
type Event = Event;
type Balance = Balance;
type CurrencyId = CurrencyId;
type CurrencyIdConvert = CurrencyIdConvert;
type AccountIdToMultiLocation = AccountIdToMultiLocation;
type SelfLocation = SelfLocation;
type MinXcmFee = ParachainMinFee;
type XcmExecutor = XcmExecutor<XcmConfig>;
type Weigher = FixedWeightBounds<UnitWeightCost, Call, MaxInstructions>;
type BaseXcmWeight = BaseXcmWeight;
type LocationInverter = LocationInverter<Ancestry>;
type MaxAssetsForTransfer = MaxAssetsForTransfer;
}
parameter_type_with_key! {
pub ExistentialDeposits: |currency_id: CurrencyId| -> Balance {
// every currency has a zero existential deposit
match currency_id {
_ => 0,
}
};
}
parameter_types! {
pub ORMLMaxLocks: u32 = 2;
pub NativeTreasuryAccount: AccountId = TreasuryPalletId::get().into_account();
}
impl orml_tokens::Config for Runtime {
type Event = Event;
type Balance = Balance;
type Amount = Amount;
type CurrencyId = CurrencyId;
type WeightInfo = ();
type ExistentialDeposits = ExistentialDeposits;
// type OnDust = orml_tokens::TransferDust<Runtime, NativeTreasuryAccount>;
type OnDust = ();
type MaxLocks = ORMLMaxLocks;
type DustRemovalWhitelist = Nothing;
}
// orml unknown tokens
impl orml_unknown_tokens::Config for Runtime {
type Event = Event;
}
impl orml_xcm::Config for Runtime {
type Event = Event;
type SovereignOrigin = EnsureRoot<AccountId>;
}
// =====================================
// =====================================
// =====================================
// runtime/src/lib.rs
// Create the runtime by composing the FRAME pallets that were previously configured.
construct_runtime!(
pub enum Runtime where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
// ......
Tokens: orml_tokens::{Pallet, Storage, Event<T>, Config<T>},
XTokens: orml_xtokens::{Pallet, Storage, Call, Event<T>},
UnknownTokens: orml_unknown_tokens::{Pallet, Storage, Event},
Currencies: orml_currencies::{Pallet, Call, Event<T>},
OrmlXcm: orml_xcm::{Pallet, Call, Event<T>},
// ......
}
);
3. 配置XcmConfig
// ==================================
// Barrier 起到过滤xcm消息的作用,如果不满足要求就会报 `Error: Barrier` 的错误
// ps: 如果在执行交易的时候,destweight太小会导致 `Error: Barrier`.
// ==================================
/// 配置parachain1000和parachain2000之间可以进行消息传递
match_type! {
pub type SpecParachain: impl Contains<MultiLocation> = {
// 当前上一级中继链下的parachain 1000
MultiLocation {parents: 1, interior: X1(Parachain(1000))} |
// 当前上一级中继链下的parachain 2000
MultiLocation {parents: 1, interior: X1(Parachain(2000))}
};
}
pub type Barrier = (
TakeWeightCredit,
AllowTopLevelPaidExecutionFrom<Everything>,
AllowUnpaidExecutionFrom<ParentOrParentsExecutivePlurality>,
// ^^^ Parent and its exec plurality get free execution
AllowUnpaidExecutionFrom<SpecParachain>,
);
// ==================================
// AssetTransactor 设置支持的资产类型
// ==================================
pub type LocalAssetTransactor = MultiCurrencyAdapter<
Currencies,
UnknownTokens,
IsNativeConcrete<CurrencyId, CurrencyIdConvert>,
AccountId,
LocationToAccountId,
CurrencyId,
CurrencyIdConvert,
DepositToAlternative<NativeTreasuryAccount, Currencies, CurrencyId, AccountId, Balance>,
>;
// ==================================
// Trader 配置跨链转账手续费的收费规则,这个手续费是其他链给我们链进行跨链转账的时候,我们平行链会收取一定的手续费。
// ==================================
use frame_support::{ExtrinsicBaseWeight, WEIGHT_PER_SECOND};
pub const MICROUNIT: Balance = 1_000_000;
pub const MILLICENTS: Balance = 1_000 * MICROUNIT;
pub const CENTS: Balance = 1_000 * MILLICENTS; // assume this is worth about a cent.
pub const DOLLARS: Balance = 100 * CENTS;
pub fn roc_per_second() -> u128 {
let base_weight = Balance::from(ExtrinsicBaseWeight::get());
let base_tx_fee = DOLLARS / 1000;
let base_tx_per_second = (WEIGHT_PER_SECOND as u128) / base_weight;
let fee_per_second = base_tx_per_second * base_tx_fee;
fee_per_second / 100
}
/// Trader - The means of purchasing weight credit for XCM execution.
/// We need to ensure we have at least one rule per token we want to handle or else
/// the xcm executor won't know how to charge fees for a transfer of said token.
pub type Trader = (
FixedRateOfFungible<RocPerSecond, ()>,
FixedRateOfFungible<NativePerSecond, ()>,
FixedRateOfFungible<NativeNewPerSecond, ()>,
FixedRateOfFungible<BbPerSecond, ()>,
);
parameter_types! {
pub RocPerSecond: (AssetId, u128) = (MultiLocation::parent().into(), roc_per_second());
pub NativePerSecond: (AssetId, u128) = (
MultiLocation::new(
1,
X2(Parachain(1000), GeneralKey(b"AA".to_vec()))
).into(),
// AA:ROC = 80:1
roc_per_second() * 80
);
pub NativeNewPerSecond: (AssetId, u128) = (
MultiLocation::new(
0,
X1(GeneralKey(b"AA".to_vec()))
).into(),
// AA:ROC = 80:1
roc_per_second() * 80
);
pub BbPerSecond: (AssetId, u128) = (
MultiLocation::new(
1,
X2(Parachain(2000), GeneralKey(b"BB".to_vec()))
).into(),
// BB:ROC = 100:1
roc_per_second() * 100
);
}
// ======================================
// XcmConfig
// ======================================
pub struct XcmConfig;
impl xcm_executor::Config for XcmConfig {
type Call = Call;
type XcmSender = XcmRouter;
// How to withdraw and deposit an asset.
type AssetTransactor = LocalAssetTransactor;
type OriginConverter = XcmOriginToTransactDispatchOrigin;
type IsReserve = MultiNativeAsset;
type IsTeleporter = (); // Teleporting is disabled.
type LocationInverter = LocationInverter<Ancestry>;
type Barrier = Barrier;
type Weigher = FixedWeightBounds<UnitWeightCost, Call, MaxInstructions>;
type Trader = Trader;
type ResponseHandler = PolkadotXcm;
type AssetTrap = PolkadotXcm;
type AssetClaims = PolkadotXcm;
type SubscriptionService = PolkadotXcm;
}
ps: 关于Trader机制的解释。
转账的手续费,一般是把转账的手续费充到国库。(转账的成本)
需要开发者自己设置weight的比例,这个weight的意思就是一秒钟的时间消耗大概多少的手续费,这个weight其实就是时间复杂度。这里需要注意一下:如果不调整的话,大伙转账消耗的都是一个代币,但是一个ksm得几百u,一个平行链代币却是几u,这个相差就有点大,就有可能被攻击(比如疯狂的转账,导致交易堵塞,类似DDOS的攻击)。
如果是非平行链的话,这个手续费会给验证人,但是平行链没有验证人,只能给国库。
如果Trader不指定任何账户,
类似这样子的写法,gas就相当于burn掉了。
如果是给国库,那生态的人可以通过治理模块支配国库内的额度。
如果是直接burn掉,就类似通缩模型。转的越多,代币越少。
在这里之前,请确保你链A和链B都进行了上面的配置。
我们本地需要启动4个验证人的一条中继链,然后把两条平行链注册上去(一条1000一条2000)
打开Hrmp通道有两种方法,一种是在中继链上直接通过sudo打开,另外一种则是在平行链上利用orml-xcm打开hrmp。
注意的是,hrmp是一个单向的通道,我们需要实现双向打通,就必须打通两次(1000->2000, 2000->1000)
这里以中继链上通过sudo为例,后者可以根据acala的wiki为参考: Open HRMP Channel.
在 Developer/Sudo
下 通过 parasSudoWrapper.sudoEstablishHrmpChannel
来打开 1000->2000 和 2000->1000 的hrmp通道。
到这所有的准备工作都准备好了,我们可以进行xcm消息传递了也就是说可以进行跨链资产转移了。
这里需要注意的是 只能往中继链转中继链代币,因为我用的是rococo-local,所以中继链代币tokenSymbol为ROC。
需要注意的是 这里的AccountId32需要我们将ss58的地址hex一下(另外一提,这个hex的内容其实就是账户的公钥)
转换工具:Substrate Utilities
同理也可以把CurrencyId切换成平行链代币,比如链A的native token(AA)。
执行成功之后,我们可以去链B通过 Developer/ChainState
下的 tokens
模块查看余额。
在实践之前需要先吃一些基础的知识,可以囫囵吞枣但是不能不去了解。
配置的时候确实牵扯到需要的配置项,不过细心的理解每个选项的含义,也能把问题修复好。
这是今天链A的代码地址,版本是polkadot-v0.9.17的版本。
Parachain A: https://github.com/99kies/Demo-Chain
Parachain Development · Polkadot Wiki
XCM: The Cross-Consensus Message Format
XCM Part II: Versioning and Compatibility
XCM Part III: Execution and Error Management
Polkadot 的跨链消息传递方案(XCMP) Polkadot Messaging Guide - HackMD
Sub0 Online: Getting Started with XCM - Your First Cross Chain Messages
<div align=center><a href="https://blog.csdn.net/qq_19381989" target="_blank"><img src="https://img-blog.csdnimg.cn/2f3c3c6e37964e62a4b2758a5fdf6541.png" width="40%" /></a></div>
作者的联系方式:
微信:thf056
qq:1290017556
邮箱:1290017556@qq.com
你也可以通过 <strong><a href="https://github.com/99kies" target="_blank">github</a></strong> | <strong><a href="https://blog.csdn.net/qq_19381989" target="_blank">csdn</a></strong> | <strong><a href="https://weibo.com/99kies" target="_blank">@新浪微博</a></strong> 关注我的动态
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!