通过本文,你已经了解到了如何修改 node-template 发布一个可用的公开测试网节点程序,chain spec 的组成和配置方法,最后我们模拟了如何渐进地启动一个公开测试网络。
通过本文,你会了解到:
Substrate node template 是一个快速开发Substrate应用链的节点程序,它内置的是权威证明(Proof of Authority, PoA)共识算法,出块算法是 Aura,也就是出块的节点和顺序是固定不变的。一个线上的公开区块链应用,需要实现一定程度的去中心化,并且通过随机出块来保证安全性,Substrate内置的权益证明(PoS)共识算法就是为了满足此类的需求。
使用 Substrate 添加 PoS 共识算法,通常有两种方法,
这里我们以第二种即修改 node template 为例,介绍一下具体的流程,这么做的好处是,通过一步步添加新的功能,熟悉各个模块的作用以及它们之间的关系。
Babe 是Substrate/Polkadot内置的默认出块算法,它的特点是:
更多关于Substrate的共识算法,参考官方文档 Advanced - Consensus。
相应的将 Aura 切换为 Babe 的代码修改如下:
在 runtime/lib.rs 中添加 Babe 模块接口的实现,
parameter_types! {
pub const EpochDuration: u64 = EPOCH_DURATION_IN_SLOTS;
pub const ExpectedBlockTime: u64 = MILLISECS_PER_BLOCK;
}
impl babe::Trait for Runtime {
type EpochDuration = EpochDuration;
type ExpectedBlockTime = ExpectedBlockTime;
type EpochChangeTrigger = babe::SameAuthoritiesForever;
}
在 construct_runtime 时引入 Babe 模块,
Babe: babe::{Module, Call, Storage, Config, Inherent(Timestamp)},
实现对应的runtime api,
impl sp_consensus_babe::BabeApi<Block> for Runtime {
fn configuration() -> sp_consensus_babe::BabeGenesisConfiguration {
sp_consensus_babe::BabeGenesisConfiguration {
slot_duration: Babe::slot_duration(),
epoch_length: EpochDuration::get(),
c: PRIMARY_PROBABILITY,
genesis_authorities: Babe::authorities(),
randomness: Babe::randomness(),
allowed_slots: sp_consensus_babe::AllowedSlots::PrimaryAndSecondaryPlainSlots,
}
}
fn authorities() -> Vec<AuraId> {
Aura::authorities()
}
fn current_epoch_start() -> sp_consensus_babe::SlotNumber {
Babe::current_epoch_start()
}
}
在 Chain spec (chain_spec.rs) 中添加 Babe 的初始区块配置,节点服务启动代码 (service.rs) 修改为 Babe 的启动方式,具体代码请参考这里。
Substrate 在共识算法中使用了提名权益证明(Nominated Proof-of-Stake, NPoS),节点可以申请成为验证人,网络中的其它用户可以提名不同的验证人和候选人。每隔一段时间进行选举,使用的选举算法为 Phragmén method,胜出的节点会成为下一轮正式的验证人,参与区块生产、区块最终性投票等。
NPoS 功能依赖一系列的模块,
你如果对如何一步步添加以上不同的模块感兴趣,可以参考这里的代码提交记录。
Substrate / Polkadot 的一个创新点在于链上治理,网络中持有 token 的用户可以对链的升级操作进行投票。只有投票通过,才会在一段时间之后更新链上的逻辑即 runtime。相关的模块有:
也可以查看添加以上模块的示例代码。
Chain Spec 包含了一系列配置信息,节点程序用它来连接指定的区块链网络,可连接的引导节点信息,以及初始区块的状态,具体信息如下:
以下是 Chain Spec 的一个示例:
{
"name": "My Staging Testnet",
"id": "my_staging",
"chainType": "Live",
"bootNodes": [
"/ip4/127.0.0.1/tcp/30333/p2p/12D3KooWMmsXriTjqeiw4Us9LLgzRGiUmq8f5frBvyJgaYAwNcrU"
],
"telemetryEndpoints": [
[
"/dns/telemetry.polkadot.io/tcp/443/x-parity-wss/%2Fsubmit%2F",
0
]
],
"protocolId": "my_staging",
"properties": {
"tokenDecimals": 15,
"tokenSymbol": "MYS"
},
"consensusEngine": null,
"genesis": {
"runtime": {
"system": {
"changesTrieConfig": null,
"code": "... ..."
},
"babe": {
"authorities": []
},
"grandpa": {
"authorities": []
},
"balances": {
"balances": [
[
"5FemZuvaJ7wVy4S49X7Y9mj7FyTR4caQD5mZo2rL7MXQoXMi",
100000000000000000000
],
... ...
]
},
"sudo": {
"key": "5FemZuvaJ7wVy4S49X7Y9mj7FyTR4caQD5mZo2rL7MXQoXMi"
},
"staking": {
"historyDepth": 84,
"validatorCount": 8,
"minimumValidatorCount": 4,
"invulnerables": [
"5Grpw9i5vNyF6pbbvw7vA8pC5Vo8GMUbG8zraLMmAn32kTNH",
... ...
],
"forceEra": "ForceNone",
"slashRewardFraction": 100000000,
"canceledPayout": 0,
"stakers": [
[
"5Grpw9i5vNyF6pbbvw7vA8pC5Vo8GMUbG8zraLMmAn32kTNH",
"5DLMZF33f61KvPDbJU5c2dPNQZ3jJyptsacpvsDhwNS1wUuU",
10000000000000000,
"Validator"
],
... ...
]
},
"session": {
"keys": [
[
"5Grpw9i5vNyF6pbbvw7vA8pC5Vo8GMUbG8zraLMmAn32kTNH",
"5Grpw9i5vNyF6pbbvw7vA8pC5Vo8GMUbG8zraLMmAn32kTNH",
{
"babe": "5Dhd2QbrSE4dyNn3YUg8j5TY3fG7ZAWZMoRRF9KUc7VPVGmC",
"grandpa": "5C6rkxAZB437B5Bf1yS4B4qjW4HZPeBp8Kzx2Se9FLKhfyHY",
"im_online": "5DscuovXyY1o7DxYroYjYgipn87eqYLyQA3HJ21Utb7TqAai"
}
],
... ...
]
},
"imOnline": {
"keys": []
},
"treasury": {},
"collectiveInstance1": {
"phantom": null,
"members": []
},
"collectiveInstance2": {
"phantom": null,
"members": [
"5FemZuvaJ7wVy4S49X7Y9mj7FyTR4caQD5mZo2rL7MXQoXMi",
... ...
]
},
"electionsPhragmen": {
"members": [
[
"5FemZuvaJ7wVy4S49X7Y9mj7FyTR4caQD5mZo2rL7MXQoXMi",
10000000000000000
],
... ...
]
},
"membershipInstance1": {
"members": [],
"phantom": null
},
"democracy": {}
}
}
}
上面的例子中,为了易读性缺省了一些必要的信息,如system的code、其他的初始账户和验证人信息等。
那么,如何生成 Chain Spec 呢?
fn staging_testnet_genesis() -> GenesisConfig {
// subkey inspect "$SECRET"
let endowed_accounts = vec![
// 5FemZuvaJ7wVy4S49X7Y9mj7FyTR4caQD5mZo2rL7MXQoXMi
hex!["9eaf896d76b55e04616ff1e1dce7fc5e4a417967c17264728b3fd8fee3b12f3c"].into(),
... ...
];
// for i in 1 2 3 4; do for j in stash controller; do subkey inspect "$SECRET//$i//$j"; done; done
// for i in 1 2 3 4; do for j in babe; do subkey --sr25519 inspect "$SECRET//$i//$j"; done; done
// for i in 1 2 3 4; do for j in grandpa; do subkey --ed25519 inspect "$SECRET//$i//$j"; done; done
// for i in 1 2 3 4; do for j in im_online; do subkey --sr25519 inspect "$SECRET//$i//$j"; done; done
let initial_authorities: Vec<(
AccountId,
AccountId,
BabeId,
GrandpaId,
ImOnlineId,
)> = vec![(
// 5Grpw9i5vNyF6pbbvw7vA8pC5Vo8GMUbG8zraLMmAn32kTNH
hex!["d41e0bf1d76de368bdb91896b0d02d758950969ea795b1e7154343ee210de649"].into(),
// 5DLMZF33f61KvPDbJU5c2dPNQZ3jJyptsacpvsDhwNS1wUuU
hex!["382bd29103cf3af5f7c032bbedccfb3144fe672ca2c606147974bc2984ca2b14"].into(),
// 5Dhd2QbrSE4dyNn3YUg8j5TY3fG7ZAWZMoRRF9KUc7VPVGmC
hex!["48640c12bc1b351cf4b051ac1cf7b5740765d02e34989d0a9dd935ce054ebb21"].unchecked_into(),
// 5C6rkxAZB437B5Bf1yS4B4qjW4HZPeBp8Kzx2Se9FLKhfyHY
hex!["01a474a93a0cf830fb40b1d17fd1fc7c6b4a95fa11f90345558574a72da0d4b1"].unchecked_into(),
// 5DscuovXyY1o7DxYroYjYgipn87eqYLyQA3HJ21Utb7TqAai
hex!["50041e469c63c994374a2829b0b0829213abd53be5113e751043318a9d7c0757"].unchecked_into(),
),
... ...
];
const ENDOWMENT: u128 = 1_000_000 * DOLLARS;
const STASH: u128 = 100 * DOLLARS;
let num_endowed_accounts = endowed_accounts.len();
GenesisConfig {
system: Some(SystemConfig {
code: WASM_BINARY.to_vec(),
changes_trie_config: Default::default(),
}),
balances: Some(BalancesConfig {
balances: endowed_accounts.iter()
.map(|k: &AccountId| (k.clone(), ENDOWMENT))
.chain(initial_authorities.iter().map(|x| (x.0.clone(), STASH)))
.collect(),
}),
babe: Some(BabeConfig {
authorities: vec![],
}),
grandpa: Some(GrandpaConfig {
authorities: vec![],
}),
sudo: Some(SudoConfig {
key: endowed_accounts[0].clone(),
}),
session: Some(SessionConfig {
keys: initial_authorities.iter().map(|x| {
(
x.0.clone(),
x.0.clone(),
session_keys(x.2.clone(), x.3.clone(), x.4.clone())
)
}).collect::<Vec<_>>(),
}),
staking: Some(StakingConfig {
validator_count: initial_authorities.len() as u32 * 2,
minimum_validator_count: initial_authorities.len() as u32,
stakers: initial_authorities
.iter()
.map(|x| (x.0.clone(), x.1.clone(), STASH, StakerStatus::Validator))
.collect(),
invulnerables: initial_authorities.iter().map(|x| x.0.clone()).collect(),
force_era: Forcing::ForceNone,
slash_reward_fraction: Perbill::from_percent(10),
.. Default::default()
}),
im_online: Some(ImOnlineConfig {
keys: vec![],
}),
democracy: Some(DemocracyConfig::default()),
elections_phragmen: Some(ElectionsConfig {
members: endowed_accounts.iter()
.take((num_endowed_accounts + 1) / 2)
.cloned()
.map(|member| (member, STASH))
.collect(),
}),
collective_Instance1: Some(CouncilConfig::default()),
collective_Instance2: Some(TechnicalCommitteeConfig {
members: endowed_accounts.iter()
.take((num_endowed_accounts + 1) / 2)
.cloned()
.collect(),
phantom: Default::default(),
}),
membership_Instance1: Some(Default::default()),
treasury: Some(Default::default()),
}
}
初始区块的账户可以是网络的管理员、发起者,或者其他社区成员,初始余额可以根据对网络的贡献进行分配。我们可以使用 subkey 工具生成所需的初始验证人信息,如stash和controller 账户,以及由Babe、GRANDPA、im-online组成的Session Keys,其中GRANDPA使用的是ed25519加密算法,其它则使用sr25519加密算法。subkey的使用方法可以参考官方文档 The subkey Tool,这里用到的命令在代码的注释中已经给出。
接着,就可以使用 ChainSpec::from_genesis
将通用的配置信息和 GenesisConfig 组合成Chain Spec,这里bootnodes的信息可以为空,在我们启动bootnode并获取到它的PeerId(打印在命令行输出)之后,修改Chain Spec 的 JSON 文件即可:
pub fn tao_staging_testnet_config() -> ChainSpec {
let boot_nodes = vec![];
ChainSpec::from_genesis(
"My Staging Testnet",
"my_staging",
ChainType::Live,
staging_testnet_genesis,
boot_nodes,
Some(
TelemetryEndpoints::new(vec![("wss://telemetry.polkadot.io/submit/".to_string(), 0)])
.expect("Westend Staging telemetry url is valid; qed")
),
Some("my_staging"),
None,
Default::default(),
)
}
2. 修改 command.rs 里的 load_spec,添加公开测试网络的解析方式:
fn load_spec(&self, id: &str) -> Result<Box<dyn sc_service::ChainSpec>, String> {
Ok(match id {
"dev" => Box::new(chain_spec::development_config()),
"" | "local" => Box::new(chain_spec::local_testnet_config()),
"my-staging" => Box::new(chain_spec::staging_testnet_config()),
path => Box::new(chain_spec::ChainSpec::from_json_file(
std::path::PathBuf::from(path),
)?),
})
}
3. 使用build-spec子命令生成Chain Spec 配置文件,命令为./target/release/node-template build-spec --chain my-staging > my-staging.json
;
4. 如果要发布公开网络,需要使用编码后的Chain Spec文件,从而确保网络中的各个节点使用相同的初始状态即genesis hash相同,命令为:./target/release/node-template build-spec --chain=my-staging.json --raw > my-staging-raw.json
,将此文件分发即可,也可以修改 staging_testnet_config,添加以配置文件加载网络的解析方式,
/// Staging testnet generator
pub fn staging_testnet_config() -> Result<ChainSpec, String> {
ChainSpec::from_json_bytes(&include_bytes!("../res/my-staging-raw.json")[..])
}
5. 配置bootnodes,启动某个bootnode的命令为:
./target/release/node-template \
--node-key c12b6d18942f5ee8528c8e2baf4e147b5c5c18710926ea492d09cbd9f6c9f82a \
--base-path /tmp/bootnode1 \
--chain my-staging-raw.json \
--name bootnode1
node-key 可使用 subkey ed25519的方式生成;如果默认的chain就是my-staging-raw.json所指定的,--chain
也可以缺省。
记录打印的PeerId(即这里的12D3KooWMmsXriTjqeiw4Us9LLgzRGiUmq8f5frBvyJgaYAwNcrU
),结合bootnode的IP地址或者域名更新 Chain Spec 文件并重新分发。最好有多个bootnodes,防止出现单点故障。
{
"name": "My Staging Testnet",
"id": "my_staging",
"chainType": "Live",
"bootNodes": [
"/ip4/your-ip-address/tcp/30333/p2p/12D3KooWMmsXriTjqeiw4Us9LLgzRGiUmq8f5frBvyJgaYAwNcrU"
],
... ...
}
为了确保公开网络的发布过程顺利,不会出现大范围的故障,这里我们借鉴 Kusama/Polkadot 正式网络的发布流程,具体可以分为下面几个步骤:
在部署和升级的过程中,可以借助一些监控和辅助工具如:
在这一阶段,要确保:
Staking模块的初始配置即 StakingConfig 的 force_era 设置为 ForceNone,这样的话,网络的验证人不会变化;
使用 system 模块提供的 BaseCallFilter
过滤掉当前阶段非必须的功能模块,从而不会因为网络不稳定导致无效交易的发生,示例代码参考这里;
启动初始验证人,命令为:
./target/release/node-template \
--base-path /tmp/validator1 \
--chain my-staging-raw.json \
--bootnodes /ip4/your-ip/tcp/30333/p2p/12D3KooWBmAwcd4PJNJvfV89HwE48nwkRmAgo8Vy3uQEyNNHBox2 \
--name validator1 \
--validator
--chain
和--bootnodes
如果已经配置在默认启动模式下,可缺省;需要将初始验证人集合所指定的验证人都启动,并且不少于3个。
启动之后,给每个验证人设置 Session Keys,这里以curl发送http请求进行添加为例,
// 添加Babe的密钥
curl http://localhost:9933 -H "Content-Type:application/json;charset=utf-8" -d "@babe1"
// babe1文件的内容
{
"jsonrpc":"2.0",
"id":1,
"method":"author_insertKey",
"params": [
"babe",
"own word vocal dog decline set bitter example forget excite gesture water//1//babe",
"0x48640c12bc1b351cf4b051ac1cf7b5740765d02e34989d0a9dd935ce054ebb21"
]
}
GRANDPA,im-online 密钥的设置方式类似,更多内容可以参考文档 Creating Your Private Network。注意:GRANDPA密钥设置之后,需要重启节点。
网络在PoA阶段稳定并且没有可见的故障后,可以使用 sudo 权限调用 staking 模块的 force_new_era,开启验证人的选举,一段时间之后,选举得出的新验证人将会开始生产和验证区块,在这一阶段:
通过修改BaseCallFilter
的逻辑,可以启用所需的治理功能,包括:
在网络的早期,合理地使用 sudo 权限,可以高效地管理网络的运转;但是在一个去中心化的网络中,这样的一个超级管理员,不符合去中心的价值观,在合适的时间点要进行删除,通常是在开启治理的一段时间之后。
删除 sudo 需要使用链上升级的方式,也就要求对此升级进行全民公投,由公投结果来决定是否删除 sudo 模块。
接着就可以一步步地引入和开启更多的功能模块,如转账、链上身份注册等等。
通过本文,你已经了解到了如何修改 node-template 发布一个可用的公开测试网节点程序,chain spec 的组成和配置方法,最后我们模拟了如何渐进地启动一个公开测试网络。注意,本文所列代码未经过审计,不可直接应用于生产环境。
Substrate官方文档:
Official Substrate Documentation for Blockchain Developers · Substrate Developer Hubsubstrate.dev
Parity介绍:
https://www.parity.io/www.parity.io
Substrate源码:
https://github.com/paritytech/substrategithub.com
Polkadot源码:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!