在上一章中介绍了如何使用node-template
运行substrate
私链,接下来将介绍如何在substrate
私链上部署并调用智能合约。在默认的node-template
中只包含以下几个runtime模块:
- sudo:用于由单账户执行一些需要root执行或者委托其他账户执行的函数。 -
balances:处理账号与余额相关 -
grandpa:用于扩展grandpa共识并且管理共识验证人 -
aura:扩展了aura共识算法 - timestamp:获取与设置链上时间 -
transaction-payment:用于计算交易费用的基础逻辑。
为了在node-template
中部署,执行合约,需要添加一个新的runtime
,在上图的FRAME pallets
中有一个contract pallet
可以用于部署和执行webAssembly
智能合约。
contracts pallet介绍
contract pallet
扩展了账户功能,在账户的基础上增加了智能合约功能。所以contract pallet
既具有普通账户的功能:“可以与其他账户进行转账操作”,也具备智能合约功能:“可以由外部交易调用合约方法,也可以调用其他合约”。
合约部署
编译后的智能合约代码通过put_code
上传到链上并返回code_hash
,通过返回的code_hash
可以多次实例化相同的合约。这里与以太坊做一个对比就可以看出substrate
现将代码存储到链上,然后在进行实例化的好处。在以太坊上有很多逻辑相同的合约,比如ERC20
,每个ERC20
的逻辑都是相同的,只是在代币名称,代币发行总量等参数的值上有些差别,但在以太坊上部署ERC20
合约的时候,每次都需要将相同的合约代码上传到链,这会浪费链上宝贵的存储资源。
ethereum | substrate |
---|---|
1. 上传编译后的合约代码,并执行初始化操作 | 1. 上传编译后的合约代码到链上,返回code_hash 2. 通过code_hash创建合约实力,同一份合约代码可以初始化多次 |
合约实例化
合约实例化通过instantiate
方法,instantiate
首先会创建一个合约账户,然后通过code_hash
获取合约代码,并调用合约代码中的deploy
函数对合约进行初始化。在初始化合约的时候需要向合约账户转一点币,由于系统会根据合约账户消耗的存储空间收取一定比例的租金,当前合约账户余额低于一个规定的额度的时候,合约账户状态会从活着("Alive")就会变成墓碑("tombstone"),并且合约的状态会被清空。让合约从墓碑状态恢复的办法是:“向合约提供被清理的数据,同时向合约账户中提供足够的资金用于支付租赁费用”。
合约执行
合约调用通过call
方法,当智能合约被调用的时候会通过code_hash
获取合约中的相关函数代码并执行,合约执行后会改变合约账户的存储状态。由于合约执行时需要消耗一定的gas
,所以当用户每次调用合约时需要指明gas limit
,未使用的gas
会返回给用户,当在合约执行的过程中发现gas
不够,那么合约仅仅会回滚当前的状态,不会向上回滚整个调用栈,例如:合约A
调用合约B
时,在执行合约B
的过程中发生gas
不足的错位,那么仅仅会回滚合约B
的状态,合约A
的状态不会被回滚。
添加contracts pallet
在上一章中介绍了如果编译node-template
,接下来我们将向node-template
中加入contracts pallet
模块,之前没有下载node-template
的可以执行以下命令,编译node-template
所需要的依赖库可以参考上一章。
1 | git clone -b v2.0.0-rc4 --depth 1 https://github.com/substrate-developer-hub/substrate-node-template |
添加contract pallet
一共需要修改两处地方:
- runtime:在现有runtime中加入
contract pallet
模块 - node:
- rpc:提供RPC接口给外部程序查询合约状态
- genesis:将合约模块的配置信息放到创世块中
下面是node-template
目录的结构,对需要修改的文件做了标注
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17...
├── node
│ ├── Cargo.toml <--这里需要修改
│ ├── build.rs
│ └── src
│ ├── chain_spec.rs <--这里需要修改
│ ├── cli.rs
│ ├── command.rs
│ ├── lib.rs
│ ├── main.rs
│ └── service.rs <--这里需要修改
├── runtime
│ ├── Cargo.toml <--这里需要修改
│ ├── build.rs
│ └── src
│ └── lib.rs <--这里需要修改
├── scripts
导入contract依赖
向runtime
中加入contract
依赖,将下面的代码拷贝到runtime/Cargo.toml
文件中。
文件:runtime/Cargo.toml
1 | [dependencies.contracts] |
在runtime/Cargo.toml
文件的[features]
中加入下面代码
文件:runtime/Cargo.toml 1
2
3
4
5
6
7
8[features]
default = ["std"]
std = [
#--snip--
'contracts/std',
'contracts-primitives/std',
#--snip--
]contract pallet
依赖包,接下来需要将contract
导入到runtime
中,在lib.rs
中通过pub use
语句导入contracts::Schedule
runtime/src/lib.rs 1
2
3/*** Add This Line ***/
/// Importing the contracts Schedule type.
pub use contracts::Schedule as ContractsSchedule;
在lib.rs
中实现contract
的相关Trait
,Trait
可以简单理解为面向对象中的接口
文件:runtime/src/lib.rs 1
2
3
4
5
6
7
8
9
10
// These time units are defined in number of blocks.
/* --snip-- */
/*** Add This Block ***/
// Contracts price units.
pub const MILLICENTS: Balance = 1_000_000_000;
pub const CENTS: Balance = 1_000 * MILLICENTS;
pub const DOLLARS: Balance = 100 * CENTS;
/*** End Added Block ***/
1 |
|
最后将contract
加入到construct_runtime!
宏中
文件:runtime/src/lib.rs 1
2
3
4
5
6
7
8
9
10
11
12construct_runtime!(
pub enum Runtime where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
/* --snip-- */
/*** Add This Line ***/
Contracts: contracts::{Module, Call, Config, Storage, Event<T>},
}
);
contract
模块需要暴露runtime API
接口给外部程序读取合约状态,首先在runtime/Cargo.toml
中导入API
依赖包
文件:runtime/Cargo.toml 1
2
3
4
5
6[dependencies.contracts-rpc-runtime-api]
git = 'https://github.com/paritytech/substrate.git'
default-features = false
package = 'pallet-contracts-rpc-runtime-api'
version = '0.8.0-rc4'
tag = 'v2.0.0-rc4'
文件:runtime/Cargo.toml 1
2
3
4
5
6[features]
default = ["std"]
std = [
#--snip--
'contracts-rpc-runtime-api/std',
]
合约runtime API
的返回值类型是ContractExecResult
文件:runtime/src/lib.rs 1
2
3/*** Add This Line ***/
use contracts_rpc_runtime_api::ContractExecResult;
/* --snip-- */
在impl_runtime_apis!
宏中实现runtime API
相关接口
1 | impl_runtime_apis! { |
node配置
将contracts
加入到runtime
后,接下来需要在node
中暴露RPC
端口,以及在创世块中加入contract
的配置
暴露RPC端口
在node
中添加RPC
服务,以便外部节点可以调用runtime API
,首先在node/Cargo.toml
中添加相关依赖包。
文件:node/Cargo.toml
1 | [dependencies] |
文件:node/src/service.rs 1
2
3
4macro_rules! new_full_start {
($config:expr) => {{
/*** Add This Line ***/
use jsonrpc_core::IoHandler;
substrate
中提供RPC
端口让外部与node
进行交互,但是在默认的RPC
实现中,并没有实现对contract
的访问,为了让外部能通过RPC
端口与contract
交互,我们需要扩展RPC
实现,增加对contracts
模块访问的实现。
1 | /* --snip-- */ |
创世区块配置
创世块配置在node/src/chain_spec.rs
文件中,我们需要修改这个文件,在文件头部中导入ContractsConfig
文件:node/src/chain_spec.rs 1
use node_template_runtime::{ContractsConfig, ContractsSchedule};
testnet_genesis
方法中添加contract
默认配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17fn testnet_genesis(initial_authorities: Vec<(AuraId, GrandpaId)>,
root_key: AccountId,
endowed_accounts: Vec<AccountId>,
_enable_println: bool) -> GenesisConfig {
GenesisConfig {
/* --snip-- */
/*** Add This Block ***/
contracts: Some(ContractsConfig {
current_schedule: ContractsSchedule {
..Default::default()
},
}),
/*** End Added Block ***/
}
}
编译,运行node-template
contract
模块添加完成后,执行以下命令,编译node-template
1 | cargo build --release |
编译完成后,运行node-template
1
2./target/release/node-template purge-chain --dev
./target/release/node-template --dev
现在contract
模块已经添加到node-template
中了,在下一章中将介绍如何编写substrate
智能合约,并部署在node-template
节点中。