Substrate Developer Hub

Substrate Developer Hub

  • 教程
  • 知识库
  • 进阶菜谱
  • API 文档
  • Languages icon简体中文
    • English
    • 协助翻译
Translate

添加一个模块

Substrate Node Template 提供了一个最小可运行的 runtime,让你可快速打造自定义的区块链。 不过,为了让它保持细小,有许多 FRAME 里的模块都没有被包含在内。

This guide will show you how you can add the Contracts pallet to your runtime in order to allow your blockchain to support Wasm smart contracts. 你可按照类似的方式将其他 FRAME 模块添加到 runtime 里。但需要注意,每个模块在具体配置设定上都会略有差异。

安装 Node Template

You should already have version v3.0.0 of the Substrate Node Template compiled on your computer from when you completed the Create Your First Substrate Chain Tutorial. 如果您尚未完成,请完成该教程。

有经验的开发者可以跳过该教程,您可以参考readme里的步骤说明来安装 。 。

文件结构

我们现在可以修改 substrate-node-template 来添加 Contracts 模块。

在您喜欢的编辑器中打开 substrate-node-template。 我们将编辑两个文件:runtime/src/lib.rs,和 runtime/Cargo.toml。

substrate-node-template
|
+-- runtime
|   |
|   +-- Cargo.toml   <-- One change in this file
|   |
|   +-- build.rs
|   |
|   +-- src
|      |
|      +-- lib.rs   <-- Most changes in this file
|
+-- pallets
|
+-- scripts
|
+-- node            <-- changes in this directory
|
+-- ...

导入模块 Crate

要添加合约模块,首先应把 pallet-contracts 导入到 runtime 的 Cargo.toml 文件中。 如果您需要 Cargo 相关参考资料的入门导读,可查询 它的官方文档。

打开 substrate-node-template/runtime/Cargo.toml 文件,您将看到 runtime 的所有依赖组件。 For example, it depends on the Balances pallet:

runtime/Cargo.toml

[dependencies]
#--snip--
pallet-balances = { default-features = false, version = '2.0.0' }

Crate 特性

导入pallet crates时需要注意的一件事是确保正确设置crate里的 features内容。 在上面的代码中,你会注意到我们设置了default_features = false。 如果您更仔细地浏览Cargo.toml 文件,则会发现类似以下内容:

runtime/Cargo.toml

[features]
default = ['std']
std = [
    'codec/std',
    'frame-executive/std',
    'frame-support/std',
    'frame-system/std',
    'frame-system-rpc-runtime-api/std',
    'pallet-balances/std',
    #--snip--
]

第二行代码将runtime crate的 default 特性定义为 std。 您可以想象,每个pallet crate都有一个相似的配置定义该crate的默认特性。 每个 crate 的特性则决定了其下游依赖项的特性。 例如,上面代码应解读为:

此 Substrate runtime 的默认特性为 std。 当runtime启用 std 特性时, parity-scale-codec, primitives, client和所有其他列出的依赖项也将设置为 std。

这样使 Substrate runtime 既能编译成原生文件 (支持 Rust:std) 和 Wasm (不支持: no_std)。

要查看这些功能如何在运行时代码中实际使用,我们可以打开项目文件:

runtime/src/lib.rs

//! The Substrate Node Template runtime. This can be compiled with `#[no_std]`, ready for Wasm.

#![cfg_attr(not(feature = "std"), no_std)]
// `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256.
#![recursion_limit="256"]

// Make the WASM binary available.
#[cfg(feature = "std")]
include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));

// --snip--

文件头部定义了当我们 不 使用 std 特性的时候,no_std 特性将会被启用。 再往下走几行代码,可以看见在 wasm_binary.rs 导入代码行上面的 #[cfg(feature = "std")] 标志,它表示仅在启用了 std 特性后我们才能导入 WASM 二进制文件。

导入合约模块

既然我们已经对模块的特性有基本认识了,现在就可以开始动手导入合约模块啦。 合约模块可能是 FRAME 中最复杂的模块,所以它是一个很好的例子来说明在添加额外的模块时可能涉及的一些棘手问题。

首先,我们将通过简单地复制现有模块并更改里面的值来添加新的依赖关系。 因此,根据上面 balances 导入样式, contracts导入将看起来像:

runtime/Cargo.toml

[dependencies]
#--snip--
pallet-contracts = { version = '2.0.0', default_features = false }
pallet-contracts-primitives = { version = '2.0.0', default_features = false }

与其他钱包一样,Contracts 模块也具有 std 的特性。 当 runtime 是使用其自身的 std 特性构建时,Nicks pallet 也应该使用它的 std 功能来构建。 请将以下代码添加到 runtime 的 std 功能。

runtime/Cargo.toml

[features]
default = ['std']
std = [
    #--snip--
    'pallet-contracts/std',
    'pallet-contracts-primitives/std',
    #--snip--
]

现在正是时候来检查到目前为止所有的改动是否能正确编译:

SKIP_WASM_BUILD=1 cargo check

添加合约模块

现在,我们已成功导入 Contracts 模块,我们现在需要把它添加到我们的 Runtime 中。

实现 Contract Trait

Every pallet has a configuration trait called Config that the runtime must implement.

To figure out what we need to implement for this pallet specifically, you can take a look to the FRAME pallet_contracts::Config documentation. 对于我们的 runtime,实现代码大概看起来这样:

runtime/src/lib.rs


// 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 ***/

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

At this point, it is recommended to explore the Contracts pallet source code if things don't make sense or you want to gain a deeper understanding.

将合约模块添加到 construct_runtime! 宏

Next, we need to add the pallet to the construct_runtime! macro. For this, we need to determine the types that the pallet exposes so that we can tell the our runtime that they exist. The complete list of possible types can be found in the construct_runtime! macro documentation.

If we look at the Contracts pallet in detail, we know it has:

  • Storage 模块:因为它使用了 decl_storage! 宏。
  • Event模块:因为它使用了decl_event! 宏。
  • Callable 函数:因为它在 decl_module!宏中定义了可调用函数。
  • 可配置 值:因为 decl_storage! 宏有 config() 参数。
  • 在 decl_module! 宏里定义的 Module 类型。

因此当我们添加pallet时,它看起来就像:

runtime/src/lib.rs

construct_runtime!(
    pub enum Runtime where
        Block = Block,
        NodeBlock = opaque::Block,
        UncheckedExtrinsic = UncheckedExtrinsic
    {
        /* --snip-- */

        /*** Add This Line ***/
        Contracts: pallet_contracts::{Module, Call, Config, Storage, Event<T>},
    }
);

请注意,并非所有的pallet都会暴露这些runtime类型,有些可能会暴露更多! You always look at the source code of a pallet or the documentation of the pallet to determine which of these types you need to expose.

This is another good time to check that your runtime compiles correctly so far. Although the runtime should compile, the entire node will not (yet). So we will use this command to check just the runtime.

SKIP_WASM_BUILD=1 cargo check -p node-template-runtime

开放出 Contracts API

Some pallets, including the Contracts pallet, expose custom runtime APIs and RPC endpoints. In the case of the Contracts pallet, this enables reading contracts state from off chain.

It's not required to enable the RPC calls on the contracts pallet to use it in our chain. However, we'll do it to make calls to our node's storage without making a transaction.

We start by adding the required API dependencies in our Cargo.toml.

runtime/Cargo.toml

[dependencies]
#--snip--
pallet-contracts-rpc-runtime-api = { version = '0.8.0', default-features = false }

runtime/Cargo.toml

[features]
default = ['std']
std = [
    #--snip--
    'pallet-contracts-rpc-runtime-api/std',
]

To get the state of a contract variable, we have to call a getter function that will return a ContractExecResult wrapper with the current state of the execution.

We need to add the return type to our runtime. Add this with the other use statements.

runtime/src/lib.rs

/*** Add This Line ***/
use pallet_contracts_rpc_runtime_api::ContractExecResult;
/* --snip-- */

We're now ready to implement the contracts runtime API. This happens in the impl_runtime_apis! macro near the end of your runtime.

impl_runtime_apis! {
   /* --snip-- */

   /*** Add This Block ***/
    impl pallet_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, gas_consumed) =
                Contracts::bare_call(origin, dest.into(), value, gas_limit, input_data);
            match exec_result {
                Ok(v) => ContractExecResult::Success {
                    flags: v.flags.bits(),
                    data: v.data,
                    gas_consumed: gas_consumed,
                },
                Err(_) => ContractExecResult::Error,
            }
        }

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

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

This is another good time to check that your runtime compiles correctly so far.

SKIP_WASM_BUILD=1 cargo check -p node-template-runtime

更新外部节点

At this point we have finished adding a pallet to the runtime. We now turn our attention to the outer node which will often need some corresponding updates. In the case of the Contracts pallet we will add the custom RPC endpoint and a genesis configuration.

添加 RPC API 扩展

With the proper runtime API exposed, now we can add the RPC to the node's service to call into that runtime API. Because we are now working in the outer node, we are not building to no_std and we don't have to maintain a dedicated std feature.

node/Cargo.toml

[dependencies]
jsonrpc-core = '15.0.0'
structopt = '0.3.8'
#--snip--
# *** Add this 2 lines ***
pallet-contracts = '2.0.0'
pallet-contracts-rpc = '0.8.0'

Substrate provides an RPC to interact with our node. However, it does not contain access to the contracts pallet by default. To interact with this pallet, we have to extend the existing RPC and add the contracts pallet along with its API.

node/src/rpc.rs

use node_template_runtime::{opaque::Block, AccountId, Balance, Index, BlockNumber}; // NOTE THIS IS AN ADJUSTMENT TO AN EXISTING LINE
use pallet_contracts_rpc::{Contracts, ContractsApi};
/// Instantiate all full RPC extensions.
pub fn create_full<C, P>(
    deps: FullDeps<C, P>,
) -> jsonrpc_core::IoHandler<sc_rpc::Metadata> where
    /* --snip-- */
    C: Send + Sync + 'static,
    C::Api: substrate_frame_rpc_system::AccountNonceApi<Block, AccountId, Index>,
    /*** Add This Line ***/
    C::Api: pallet_contracts_rpc::ContractsRuntimeApi<Block, AccountId, Balance, BlockNumber>,
    /* --snip-- */
{
    /* --snip-- */

    // Extend this RPC with a custom API by using the following syntax.
    // `YourRpcStruct` should have a reference to a client, which is needed
    // to call into the runtime.
    // `io.extend_with(YourRpcTrait::to_delegate(YourRpcStruct::new(ReferenceToClient, ...)));`

    /*** Add This Block ***/
    io.extend_with(
        ContractsApi::to_delegate(Contracts::new(client.clone()))
    );
    /*** End Added Block ***/
    io
}

创世配置

Not all pallets will have a genesis configuration, but if yours does, you can use its documentation to learn about it. For example, pallet_contracts::GenesisConfig documentation describes all the fields you need to define for the Contracts pallet.

Genesis configurations are controlled in node/src/chain_spec.rs. We need to modify this file to include the ContractsConfig type and the contract price units at the top:

node/src/chain_spec.rs

use node_template_runtime::ContractsConfig;

Then inside the testnet_genesis function we need to add the contract configuration to the returned GenesisConfig object as followed:

重要:我们正在从函数参数中获取 _enable_println 的值。 确保移除参数名称前面的下划线。

/// Configure initial storage state for FRAME modules.
fn testnet_genesis(
    wasm_binary: &[u8],
    initial_authorities: Vec<(AuraId, GrandpaId)>,
    root_key: AccountId,
    endowed_accounts: Vec<AccountId>,
    enable_println: bool, // Update this line
) -> GenesisConfig {
    GenesisConfig {
        /* --snip-- */

        /*** Add This Block ***/
        pallet_contracts: Some(ContractsConfig {
            current_schedule: pallet_contracts::Schedule {
                    enable_println,
                    ..Default::default()
            },
        }),
        /*** End Added Block ***/
    }
}

启动您升级过的链

Now you are ready to compile and run your contract-capable node. Compile the node in release mode with

WASM_BUILD_TOOLCHAIN=nightly-2020-10-05 cargo build --release

Now launch the executable you just built by running this command

# 在开发模式运行一个临时节点
./target/release/node-template --dev --tmp

添加其它 FRAME Pallets

In this guide, we walked through specifically how to import the Contracts pallet, but as mentioned in the beginning of this guide, each pallet will be a little different. Have no fear, you can always refer to the demonstration Substrate node runtime which includes nearly every pallet in the FRAME.

In the Cargo.toml file of the Substrate node runtime, you will see an example of how to import each of the different pallets, and in the lib.rs file you will find how to add each pallet to your runtime. You can basically copy what was done there to your own runtime.

进一步学习

  • 最简指南:在您自己的 package 中编写 runtime。
  • 您的节点现在能够运行智能合约,学习 Substrate ink! 智能合约。
  • Substrate Recipes offers detailed tutorials about writing Runtime APIs and Custom RPCs like the ones explored in this tutorial.
  • 了解 Chain Spec 文件以自定义创世配置。

参考文档

  • FRAME Contract 模块 API
  • 安装 Node Template
  • 文件结构
  • 导入模块 Crate
    • Crate 特性
    • 导入合约模块
  • 添加合约模块
    • 实现 Contract Trait
    • 将合约模块添加到 construct_runtime! 宏
    • 开放出 Contracts API
  • 更新外部节点
    • 添加 RPC API 扩展
    • 创世配置
  • 启动您升级过的链
  • 添加其它 FRAME Pallets
    • 进一步学习
    • 参考文档
Substrate Developer Hub
开发者中心
教程知识库进阶菜谱API 文档
社区
社区主页通讯Substrate 技术聊天室Substrate 研讨会Stack Overflow推特聚会活动
更多
Substrate Builders 计划BlogSubstrate GitHub开发者中心 GitHub隐私政策使用条款Cookie 设置
Copyright © 2021 Parity Technologies