0%

在substrate上添加contract runtime

在上一章中介绍了如何使用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
2
3
4
5
6
7
8
9
10
11
12
13
[dependencies.contracts]
git = 'https://github.com/paritytech/substrate.git'
default-features = false
package = 'pallet-contracts'
tag = 'v2.0.0-rc4'
version = '2.0.0-rc4'

[dependencies.contracts-primitives]
git = 'https://github.com/paritytech/substrate.git'
default-features = false
package = 'pallet-contracts-primitives'
tag = 'v2.0.0-rc4'
version = '2.0.0-rc4'

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到runtime 现在我们已经成功导入了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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

impl timestamp::Trait for Runtime {
/* --snip-- */
}

/*** Add This Block ***/
parameter_types! {
pub const TombstoneDeposit: Balance = 16 * MILLICENTS;
pub const RentByteFee: Balance = 4 * MILLICENTS;
pub const RentDepositOffset: Balance = 1000 * MILLICENTS;
pub const SurchargeReward: Balance = 150 * MILLICENTS;
}

impl contracts::Trait for Runtime {
type Time = Timestamp;
type Randomness = RandomnessCollectiveFlip;
type Currency = Balances;
type Event = Event;
type DetermineContractAddress = contracts::SimpleAddressDeterminer<Runtime>;
type TrieIdGenerator = contracts::TrieIdFromParentCounter<Runtime>;
type RentPayment = ();
type SignedClaimHandicap = contracts::DefaultSignedClaimHandicap;
type TombstoneDeposit = TombstoneDeposit;
type StorageSizeOffset = contracts::DefaultStorageSizeOffset;
type RentByteFee = RentByteFee;
type RentDepositOffset = RentDepositOffset;
type SurchargeReward = SurchargeReward;
type MaxDepth = contracts::DefaultMaxDepth;
type MaxValueSize = contracts::DefaultMaxValueSize;
type WeightPrice = transaction_payment::Module<Self>;
}
/*** End Added Block ***/

最后将contract加入到construct_runtime!宏中

文件:runtime/src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
construct_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
impl_runtime_apis! {
/* --snip-- */

/*** Add This Block ***/
impl contracts_rpc_runtime_api::ContractsApi<Block, AccountId, Balance, BlockNumber>
for Runtime
{
fn call(
origin: AccountId,
dest: AccountId,
value: Balance,
gas_limit: u64,
input_data: Vec<u8>,
) -> ContractExecResult {
let exec_result =
Contracts::bare_call(origin, dest.into(), value, gas_limit, input_data);
match exec_result {
Ok(v) => ContractExecResult::Success {
status: v.status,
data: v.data,
},
Err(_) => ContractExecResult::Error,
}
}

fn get_storage(
address: AccountId,
key: [u8; 32],
) -> contracts_primitives::GetStorageResult {
Contracts::get_storage(address, key)
}

fn rent_projection(
address: AccountId,
) -> contracts_primitives::RentProjectionResult<BlockNumber> {
Contracts::rent_projection(address)
}
}
/*** End Added Block ***/
}

node配置

contracts加入到runtime后,接下来需要在node中暴露RPC端口,以及在创世块中加入contract的配置

暴露RPC端口

node中添加RPC服务,以便外部节点可以调用runtime API,首先在node/Cargo.toml中添加相关依赖包。

文件:node/Cargo.toml

1
2
3
4
5
6
7
8
9
10
11
12
[dependencies]
#--snip--
jsonrpc-core = '14.0.5'

[dependencies.pallet-contracts-rpc]
git = 'https://github.com/paritytech/substrate.git'
version = '0.8.0-rc4'
tag = 'v2.0.0-rc4'

[dependencies.sc-rpc]
git = 'https://github.com/paritytech/substrate.git'
tag = 'v2.0.0-rc4'

文件:node/src/service.rs

1
2
3
4
macro_rules! new_full_start {
($config:expr) => {{
/*** Add This Line ***/
use jsonrpc_core::IoHandler;

substrate中提供RPC端口让外部与node进行交互,但是在默认的RPC实现中,并没有实现对contract的访问,为了让外部能通过RPC端口与contract交互,我们需要扩展RPC实现,增加对contracts模块访问的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
        /* --snip-- */
Ok(import_queue)
})? // <- Remove semi-colon
/*** Add This Block ***/
.with_rpc_extensions(|builder| -> Result<IoHandler<sc_rpc::Metadata>, _> {
let handler = pallet_contracts_rpc::Contracts::new(builder.client().clone());
let delegate = pallet_contracts_rpc::ContractsApi::to_delegate(handler);

let mut io = IoHandler::default();
io.extend_with(delegate);
Ok(io)
})?;
/*** End Added Block ***/
(builder, import_setup, inherent_data_providers)
}}

创世区块配置

创世块配置在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
17
fn 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节点中。