当我们应用区块链解决生活中问题的时候,它的价值就产生了。如果还不清楚Substrate的基本概念,在开始本文的阅读之前,我希望你能大概浏览Substrate开发者中心的文档或者参考之前的教程使用Substrate搭建你的第一条区块链来了解Substrate相关的基础知识。本文会从零开始开发一条承载具体业务的区块链
当我们应用区块链解决生活中问题的时候,它的价值就产生了。如果还不清楚Substrate的基本概念,在开始本文的阅读之前,我希望你能大概浏览Substrate开发者中心的文档或者参考之前的教程使用Substrate搭建你的第一条区块链来了解Substrate相关的基础知识。本文会从零开始开发一条承载具体业务的区块链应用,即抛硬币游戏。
快速安装Substrate依赖,详细内容参考开发者中心文档《Installing Substrate》:
curl https://getsubstrate.io -sSf | bash -s -- --fast
更新substrate-up脚本,它提供了初始化节点、创建新模块等功能:
git clone https://github.com/paritytech/substrate-up
cd substrate-up
cp -a substrate-* ~/.cargo/bin
cp -a polkadot-* ~/.cargo/bin
Substrate作为一个通用的区块链开发框架,Substrate提供了用于构建区块链的所有组件,开发者要做的只是将需要的组件组装起来。为了帮助开发者从繁杂的组装工作中解放出来,Substrate提供了两类的节点程序来快速实现组装工作:
这里所说的节点通常也被称为点对点节点或者全节点,承载了区块链的所有功能,你可以把它想象成传统互联网开发中的后端,但是它不是运行在中心化的服务器上,而是分布式的运行在世界的各个角落里。
本文我们将会用模板节点(Template Node)作为我们的节点程序,承载我们的抛硬币游戏。
substrate-up
脚本提供的初始化节点命令是substrate-node-new
,通过下载和编译模板节点(Template Node)来生成我们的节点程序。运行下面的命令来生成节点,替换demo-node
为你自己的节点名,替换yourname
为你的团队或个人名字:
substrate-node-new demo-node yourname
启动刚刚生成的节点:
cd demo-node
./target/release/demo-node --dev
如果在控制台看到这些内容,证明你的节点创建成功:
2019-07-27 18:03:45 Substrate Node
2019-07-27 18:03:45 version 1.0.0-2857a44-x86_64-macos
2019-07-27 18:03:45 by demo-author, 2017, 2018
2019-07-27 18:03:45 Chain specification: Development
2019-07-27 18:03:45 Node name: safe-tin-6167
2019-07-27 18:03:45 Roles: AUTHORITY
2019-07-27 18:03:45 Initializing Genesis block/state (state: 0x79b0…3c01, header-hash: 0xacb5…bb17)
2019-07-27 18:03:45 Loaded block-time = 10 seconds from genesis on first-launch
2019-07-27 18:03:45 Best block: #0
2019-07-27 18:03:45 Using default protocol ID "sup" because none is configured in the chain specs
2019-07-27 18:03:45 Local node identity is: QmZH4oHKH4nwaP4apeYCM7EJXkxAjv4AqnJt29MrMNhWBV
2019-07-27 18:03:45 Libp2p => Random Kademlia query has yielded empty results
2019-07-27 18:03:46 Listening for new connections on 127.0.0.1:9944.
2019-07-27 18:03:46 Using authority key 5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu
2019-07-27 18:03:48 Libp2p => Random Kademlia query has yielded empty results
2019-07-27 18:03:49 Accepted a new tcp connection from 127.0.0.1:62636.
2019-07-27 18:03:50 Starting consensus session on top of parent 0xacb55b52944dff23e2aa99326cc20b1f9c091556516d15db9ffcffd7d159bb17
2019-07-27 18:03:50 Prepared block for proposing at 1 [hash: 0x2d84be81477309b475af22c457f850174c498d1b0d19032f18fe7f7656233dad; parent_ha
sh: 0xacb5…bb17; extrinsics: [0xb1d4…9362]]
2019-07-27 18:03:50 Pre-sealed block for proposal at 1\. Hash now 0x1d70dc9d4299519880cc5824cee49ffa0c5a74ec5a9bb238012ae5ff65055302, previ
ously 0x2d84be81477309b475af22c457f850174c498d1b0d19032f18fe7f7656233dad.
2019-07-27 18:03:50 Imported #1 (0x1d70…5302)
2019-07-27 18:03:50 Idle (0 peers), best: #1 (0x1d70…5302), finalized #0 (0xacb5…bb17), ⬇ 0 ⬆ 0
以上输出的内容包含了一些有价值的信息如:
Development
,表明我们使用的是内置开发模式的chain spec。QmZH4oHKH4nwaP4apeYCM7EJXkxAjv4AqnJt29MrMNhWBV
, 节点ID。5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu
, 验证人的公钥。127.0.0.1:9944
。best: #1 (0x1d70…5302)
当前区块.finalized #0 (0xacb5…bb17)
。一直显示 0
是由于 Template Node 并没有引入最终性模块 GRANDPA finality gadget。Substrate 默认的共识机制是基于BABE和GRANDPA的混合共识,详细信息参考Polkadot Consensus。
启动之后,你就拥有了一个由单个节点维护的"区块链"网络。下面我们通过UI与刚创建的节点进行交互。
Substrate生态里提供了一个UI工具 Polkadot/Substrate UI 来帮助开发者与Substrate编写的区块链进行交互。你可以根据项目README的指示在本地运行,或者访问官方host的网页应用。
在 Settings页面,配置remote node
为之前的WebSocket RPC IP及端口127.0.0.1:9944
。保存配置后,会有更多的功能在侧边栏出现,供大家使用。
转到Extrinsics
页:
ALICE
用户,template
doSomething(something)
,接着,转到 Chain state 页面: 配置 selected state query 为 template
something(): Option<u32>
点击➕按钮,你会看到刚刚输进入的数字。
以上就是我们与节点程序的基本交互操作。如果你还不熟悉UI的其它功能,可以多多练习,有助于后面的操作和理解。
使用Substrate编写区块链应用,数据存储、可调用函数和事件都被封装在自定义的Runtime模块中。以刚刚创建的节点程序为例,预先定义的template模块,代码位于runtime/src/template.rs
, 内容包含:
Something get(something): Option<u32>
rust pub fn do_something(origin, something: u32) -> Result { // --snip-- }
SomethingStored(u32, AccountId)
下面我们在编写自定义的功能模块时会逐一对上面的内容进行介绍。
substrate-up
提供了命令substrate-module-new
来帮助我们创建一个template模块,里面包含了一些基本的依赖引入,以及上面提到的数据存储项、可调用函数、事件等示例代码,其中的一些注释可以很好地帮助初学者理解Substrate runtime模块的构成。在节点程序目录下执行如下命令(替换mymodule
为你自己的模块名):
cd runtime/src
substrate-module-new mymodule
执行完成后,你会看到一个新生成的mymodule.rs
文件,这就是你的模块程序文件。为了使用这一模块,我们还需要修改当前目录下的lib.rs
:
mod mymodule;
impl mymodule::Trait for Runtime {
type Event = Event;
}
construct_runtime!
宏:construct_runtime!(
pub enum Runtime with Log(InternalLog: DigestItem<Hash, AuthorityId, AuthoritySignature>) where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
// --snip--
MyModule: mymodule::{Module, Call, Storage, Event<T>},
}
);
宏通常被称为元编程,根据提供的代码可以生成新的代码,实现代码复用。Substrate使用了大量的宏来减轻开发人员的工作,让人"又爱又恨",更多可了解construct_runtime!。
接下来,重新编译我们的节点程序:
# 编译runtime的wasm版本
./scripts/build.sh
# 编译runtime的本地二进制版本,并构建可执行的客户端
cargo build --release
# 删除链上的历史数据
./target/release/demo-node purge-chain --dev
# 启动本地测试网络
./target/release/demo-node --dev
请通过 Polkadot/Substrate UI 简单测试一下新创建模块的功能。
本文,我们实现的业务是"抛硬币"游戏,用户可以付费玩游戏,如果抛出的结果为"正面朝上",则用户胜利,获取奖池里的奖金;如果用户失败,则什么都拿不到。无论胜负,用户支付的游戏费用都要存进奖池,以备后面的用户使用。
Runtime开发的第一步是设计你的存储数据结构,比如这里需要的游戏花费和奖池,在模块的decl_storage!
宏中添加如下存储项:
decl_storage! {
trait Store for Module<T: Trait> as mymodule {
// --snip--
Payment get(payment): Option<T::Balance>;
Pot get(pot): T::Balance;
Nonce get(nonce): u64;
}
}
这里我们使用的
decl_storage!
宏使代码变得简单易懂,由Substrate负责生成更多和数据库进行交互的辅助代码,开发者只需设计存储的数据模型。
这里有三个存储项:
Payment
类型为 Option<T::Balance>
,保存着游戏的花费。使用Option
表明该费用是否已经被初始化。Pot
类型为 T::Balance
,保存了上次获胜者之后累积的所有奖励。Nonce
为u64
类型的整数,我们将会在生成随机数的时候用到。Balance
类型是由 SRML balances 模块提供的,用来表示账户的余额。要使用它,需要将我们模块的配置接口修改为依赖 balances Trait:
pub trait Trait: balances::Trait {
// --snip--
}
代码中get(payment)
是用来定义Payment
存储项的另一种getter函数,下面的章节我们再介绍如何使用这些getter函数。
本节我们将会定义Runtime开发所需的可调用函数。这里所说的可调用函数,是那些可以被用户调用,并且与区块链系统进行交互的函数。函数本身是不可以被代码之外进行调用的,但是由于Substrate的封装开放了对应的RPC接口,更多细节这里我们不过多的讨论。我们为"抛硬币"游戏定义了两个函数:一个用来初始化游戏花费;另一个用来开始游戏并生成游戏结果。
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
fn set_payment(_origin, value: T::Balance) -> Result {
// Logic for setting the game payment
}
play(origin) -> Result {
// Logic for playing the game
}
}
}
上面的代码显示了我们的可调用函数位于Module
结构体中,下面我们将会为函数添加真正的逻辑。对于 set_payment
函数:
// This function initializes the `payment` storage item
// It also populates the pot with an initial value
fn set_payment(origin, value: T::Balance) -> Result {
// Ensure that the function call is a signed message (i.e. a transaction)
let _ = ensure_signed(origin)?;
// If `payment` is not initialized with some value
if Self::payment().is_none() {
// Set the value of `payment`
<Payment<T>>::put(value);
// Initialize the `pot` with the same value
<Pot<T>>::put(value);
}
// Return Ok(()) when everything happens successfully
Ok(())
}
我们的 set_payment
函数需要两个参数, origin
, 类型为 SRML system 模块定义的T::Origin
,包含了函数调用的发出方。这个参数总是作为可调用函数的第一个参数。Substrate允许我们为这个参数缺省类型签名来简化工作。参考这里Origin的定义。 value
,类型为 T::Balance
,用来初始化 Payment
和Pot
。
接下来,我们来实现 play
函数:
// This function is allows a user to play our coin-flip game
fn play(origin) -> Result {
// Ensure that the function call is a signed message (i.e. a transaction)
// Additionally, derive the sender address from the signed message
let sender = ensure_signed(origin)?;
// Ensure that `payment` storage item has been set
let payment = Self::payment().ok_or("Must have payment amount set")?;
// Read our storage values, and place them in memory variables
let mut nonce = Self::nonce();
let mut pot = Self::pot();
// Try to withdraw the payment from the account, making sure that it will not kill the account
let _ = <balances::Module<T> as Currency<_>>::withdraw(&sender, payment, WithdrawReason::Reserve, ExistenceRequirement::KeepAlive)?;
// Generate a random hash between 0-255 using a csRNG algorithm
if (<system::Module<T>>::random_seed(), &sender, nonce)
.using_encoded(<T as system::Trait>::Hashing::hash)
.using_encoded(|e| e[0] < 128)
{
// If the user won the coin flip, deposit the pot winnings; cannot fail
let _ = <balances::Module<T> as Currency<_>>::deposit_into_existing(&sender, pot)
.expect("`sender` must exist since a transaction is being made and withdraw will keep alive; qed.");
// Reduce the pot to zero
pot = Zero::zero();
}
// No matter the outcome, increase the pot by the payment amount
pot = pot.saturating_add(payment);
// Increment the nonce
nonce = nonce.wrapping_add(1);
// Store the updated values for our module
<Pot<T>>::put(pot);
<Nonce<T>>::put(nonce);
// Return Ok(()) when everything happens successfully
Ok(())
}
上面的 play
函数只接收 orgin
这一个参数。然后做一些预置条件检查如,交易应当被签名,并且payment
存储项不能为空。这里我们使用了 Self::payment()
来获取存储项中的具体值,这就是我们上面说到的getter函数的具体使用方法,另一种获取存储项的方法为 <Payment<T>>::get()
。
在真正"抛硬币"之前,我们需要将游戏所需的花费从用户账户取出,当游戏结束之后将这些费用放入奖池中。代码中使用的 withdraw
函数还需要引入下面的依赖: rust use support::traits::{Currency, WithdrawReason, ExistenceRequirement};
当硬币被抛出之后,用户有百分之五十的几率获胜。为了模拟这样的情况,首先生成一个0到255的随机数,再拿这个随机数跟128进行比较,如果小于128,那么用户获胜并获得奖池中的奖金;反之失败,用户什么都没有得到。最后更新存储项,为下一次游戏做准备。关于更多的随机数生成,请参考 Generating Random Data 页面
最后还需要引入的依赖有:
use runtime_primitives::traits::{Zero, Hash, Saturating};
use parity_codec::Encode;
客户端通过监听区块中的Event来更新链下的存储状态或与用户交互。
当Payment被更新之后我们希望产生一个包含Payment信息的Event,由于用到了Balance
类型,我们需要修改Event enum,添加泛型约束Balance = <T as balances::Trait>::Balance
:
decl_event!(
pub enum Event<T> where
AccountId = <T as system::Trait>::AccountId,
Balance = <T as balances::Trait>::Balance {
// --snip--
}
);
之后,在Event enum中定义我们的Event,并修改set_payment
函数来生成这一事件:
PaymentSet(Balance),
fn set_payment(origin, value: T::Balance) -> Result {
// --snip--
if Self::payment().is_none() {
// --snip--
Self::deposit_event(RawEvent::PaymentSet(value));
}
// --snip--
}
当用户完成游戏之后,我们希望产生一个包含用户信息以及获胜信息的事件,同样我们需要添加我们的Event到Event enum中,并在合适的时机触发事件:
PlayResult(AccountId, Balance),
// This function is allows a user to play our coin-flip game
fn play(origin) -> Result {
let sender = ensure_signed(origin)?;
// --snip--
let mut winnings = Zero::zero();
if (<system::Module<T>>::random_seed(), &sender, nonce)
.using_encoded(<T as system::Trait>::Hashing::hash)
.using_encoded(|e| e[0] < 128)
{
// --snip--
// Set the winnings
winnings = pot;
// Reduce the pot to zero
pot = Zero::zero();
}
// --snip--
// Raise event for the play result
Self::deposit_event(RawEvent::PlayResult(sender, winnings));
// Return Ok(()) when everything happens successfully
Ok(())
}
这里我们定义了新的变量winnings
保存获胜信息,初始值为0
,如果获胜则更新为pot
即奖池中的值。在函数返回Ok(())
之前触发该事件。
现在已经完成了所有的代码,可以进行简单的测试。同样地,访问 Polkadot/Substrate UI 在Extrinsics页面中调用上面定义的函数;之后在Chain state页面查询对应的存储项。遇到问题可以参考这里的 “抛硬币”完整代码。
后续文章将会介绍如何添加测试和编写UI。更多内容请关注,
本文来自孙凯超的知乎专栏:Substrate区块链开发,他的公众号: 沐风自语
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!