自定义pallet

本教程的重点是如何使用 pallets 自定义运行时,包括如何将简单和复杂的 pallets 添加到运行时,以及如何将 pallets 与智能合约结合使用。

添加一个pallet到运行时

Nicks pallet 允许区块链用户支付押金,为他们控制的帐户保留昵称。它实现了以下功能:

  • set_name 函数用于收集押金,并为帐户设置名称,如果该名称尚未被使用。
  • clear_name 函数用于删除与帐户关联的名称并退还押金。
  • kill_name 函数用于强制删除帐户名称,但不会退还押金。

添加Nicks pallet的依赖

将 Nicks pallet 的依赖项添加到运行时,打开 runtime/Cargo.toml 文件,复制一个存在的 pallet 依赖项描述,然后把名字替换为 pallet-nicks,使该 pallet 在 node template 运行时是可用的。例如,添加如下类似的一行:

pallet-nicks = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.26" }
  • version 标识着你想导入的 crate 版本。
  • 使用标准 Rust 库编译运行时的时候,是否包括 pallet 特性的默认行为。
  • 用于检索 pallet-nicks crate 的存储库位置。
  • 用于检索 crate 的分支。

这些细节对于任何给定版本的 node tempalte 中的每个 pallet 都应该是相同的。

添加 pallet-nicks/std 特性到 features 列表中,以编译运行时的时候可以启用。

[features]
default = ["std"]
std = [
  ...
  "pallet-aura/std",
  "pallet-balances/std",
  "pallet-nicks/std",
  ...
]

如果你忘记更新 Cargo.tomlfeatures 部分,在编译运行时二进制文件时,你可能会看到 cannot find function 错误。

通过以下命令检查依赖项是否被正确解析:

cargo check -p node-template-runtime

审查余额的配置

例如,查看你需要实现的 nicks pallet,你可以参考关于 pallet_nicks::Config 的 Rust 文档或者 Nicks pallet 源码中的 trait 定义。

在本节教程中,你可以看到 nicks pallet 中的 Config trait 声明了以下类型: ``rust,editable,noplayground pub trait Config: Config { type Event: From<Event> + IsType<::Event>; type Currency: ReservableCurrencySelf::AccountId; type ReservationFee: Get<<::Currency as Currency<::AccountId>>::Balance>; type Slashed: OnUnbalanced<<::Currency as Currency<::AccountId>>::NegativeImbalance>; type ForceOrigin: EnsureOriginSelf::Origin; type MinLength: Get; type MaxLength: Get; }

确定 pallet 所需的类型后,需要向运行时添加代码以实现 `Config` 特性。查看为一个 pallet 怎样实现 `Config` trait,使用 **Balances** pallet 作为一个例子,打开 `runtime/src/lib.rs` 文件,找到 `Balances` pallet 的位置并注意它由以下实现(`impl`)代码块组成:
```rust,editable,noplayground
impl pallet_balances::Config for Runtime {
  type MaxLocks = ConstU32<50>;
  type MaxReserves = ();
  type ReserveIdentifier = [u8; 8];
  /// The type for recording an account's balance.
  type Balance = Balance;
  /// The ubiquitous event type.
  type Event = Event;
  /// The empty value, (), is used to specify a no-op callback function.
  type DustRemoval = ();
  /// Set the minimum balanced required for an account to exist on-chain
  type ExistentialDeposit = ConstU128<500>;
  /// The FRAME runtime system is used to track the accounts that hold balances.
  type AccountStore = System;
  /// Weight information is supplied to the Balances pallet by the node template runtime.
  type WeightInfo = pallet_balances::weights::SubstrateWeight<Runtime>;
}

正如你看到的这个例子,impl pallet_balances::Config 代码块允许你为指定的 Balances pallet Config trait 配置类型和参数。例如,此 impl 代码块配置 Balances pallet 使用 u128 类型来追述 balances。

为Nicks实现配置

要实现 nicks pallet 在你的运行时,打开 runtime/src/libs.rs 文件,找到 Balances 代码块的最后一行,为 Nicks pallet 添加下面的代码块:

impl pallet_nicks::Config for Runtime {
// The Balances pallet implements the ReservableCurrency trait.
// `Balances` is defined in `construct_runtime!` macro.
type Currency = Balances;

// Set ReservationFee to a value.
type ReservationFee = ConstU128<100>;

// No action is taken when deposits are forfeited.
type Slashed = ();

// Configure the FRAME System Root origin as the Nick pallet admin.
// https://paritytech.github.io/substrate/master/frame_system/enum.RawOrigin.html#variant.Root
type ForceOrigin = frame_system::EnsureRoot<AccountId>;

// Set MinLength of nick name to a desired value.
type MinLength = ConstU32<8>;

// Set MaxLength of nick name to a desired value.
type MaxLength = ConstU32<32>;

// The ubiquitous event type.
type Event = Event;
}

添加 Nicks 到 construct_runtime! 宏中,如下代码:

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

   /*** Add This Line ***/
   Nicks: pallet_nicks,
 }
);

通过运行下面的命令检查新的依赖是否被正确解析:

cargo check -p node-template-runtime

通过运行下面的命令以 release 模式编译 node:

cargo build --release

启动区块链节点

通过运行下面的命令以开发模式启动节点

./target/release/node-template --dev

在上面这种例子中,--dev 选项使用预构建的 development chain specification 使节点运行在开发模式下。默认情况下,当你通过按下 Control-c 停止节点时,这个选项会删除所有活动数据,如 keys、区块链的数据库、网络信息。使用 --dev 选项确保在任何时候你停止或重启节点时都会有一个干净的网络状态。

通过查看终端中显示的输出,验证节点是否已启动并成功运行。如果块的最终确认后控制台输出中的数字还在增加,则你的区块链正在生成新的区块,并就它们所描述的状态达成共识。

启动front-end template

现在你已经添加一个新的 pallet 到你的运行时,你可以使用 Substrate front-end template 跟 node template 进行交互,并且访问 Nicks pallet。在你已安装的 front-end template 目录中,通过运行下面的命令为 front-end template 启动 web server:

yarn start

在浏览器中打开 http://localhost:8000 去查看 front-end template。

使用Nicks pallet设置昵称

在账户列表选择 Alice 账户,在 Pallet Interactor 组件确认 Extrinsic 是已经被选择的,从可调用的 pallets 列表中选择 nicks,选择 setname 作为从 nicks pallet 中调用的函数,输入一个不少于 MinNickLength(8字符)且不超过 MaxNickLength(32字符)的昵称。

点击 Signed 执行函数。

观察调用改变的状态,从 Ready 到 InBlock 到 Finalized,并且注意 Nicks pallet 发出的事件

使用Nicks pallet查询账户的信息

在账户列表选择 Alice 账户,在 Pallet Interactor 组件中选择 Query 作为交互的类型,从可调用的 pallets 列表中选择 nicks,选择 nameOf 作为要调用的函数,在 AccountId 字段中复制并粘贴 alice 账户的地址,然后点击 Query

返回的类型是一个包含两个值的元组:

  • Alice 账户的16进制编码的昵称 53756273747261746520737570657273746172202d20416c696365
  • 从 Alice 的账户中预留的用于保存昵称的金额(100)。

如果你要查询 Nicks pallet 中 Bob 账户的昵称,你将看到返回一个 None 值,因为 Bob 没有调用 setname 函数去预留一个昵称。

探索其他功能

本节教程演示了如何向运行时添加一个简单的 pallet,并演示了如何使用前端模板与新 pallet 交互,你将 nicks pallet 添加到运行时,并使用前端模板调用 set_namenameOf 函数。nicks pallet 还提供了两个附加函数:clear_name 函数和 kill_name 函数,使帐户所有者能够删除或保留名称,或者一个 root-level 用户能够强制删除帐户名称。通过探索这些功能的工作方式,你可以继续了解其他功能,如 Sudo pallet 和 origin accounts 的使用。

配置合约pallet

添加contracts pallet依赖

将 contracts pallet 的依赖项添加到运行时,导入 pallet-contracts crate,打开 runtime/Cargo.toml 文件,复制一个存在的 pallet 依赖项描述,然后把名字替换为 pallet-contracts,使该 pallet 在 node template 运行时是可用的。例如,添加如下类似的一行:

pallet-contracts = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.26" }

导入 pallet-contracts-primitives crate,通过将其添加到依赖项列表中,使其可用于 node template 运行时。在大多数情况下,你在任何给定版本的 node template 中为每个托盘指定相同的信息。然而,如果编译器指定的版本与你所指定的版本不同,你可能需要修改依赖项以匹配编译器识别的版本。例如,如果编译器发现 pallet-contracts-primitives crate 的版本是 6.0.0:

pallet-contracts-primitives = { version = "6.0.0", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.26" }

将 Contracts pallet 添加到 std 特性列表中,这样当运行时作为平台原生二进制(非 Wasm 二进制)文件构建时,它的特性就包括在内了。

[features]
default = ["std"]
std = [
  "codec/std",
  "scale-info/std",
  "frame-executive/std",
  "frame-support/std",
  "frame-system-rpc-runtime-api/std",
  "frame-system/std",
  "pallet-aura/std",
  "pallet-balances/std",
  "pallet-contracts/std",
  "pallet-contracts-primitives/std",
]

保存代码并关闭 runtime/Cargo.toml 文件。

通过运行下面的命令检查你的运行时编译是否正确:

cargo check -p node-template-runtime

实现 Contracts configuration trait

如果你查看 pallet_contracts::Config Rust API 文档,你将会注意到,该 pallet 不像 Nicks pallet 一样,有很多关联类型,因此本节教程的代码比之前的要更复杂。

为了在运行时给 Contracts pallet 实现 Config trait,打开 runtime/src/lib.rs,找到 pub use frame_support 代码块并添加 Nothing 到 traits 列表之中,如下:

pub use frame_support::{
construct_runtime, parameter_types,
traits::{
  ConstU128, ConstU32, ConstU64, ConstU8, KeyOwnerProofSystem, Randomness, StorageInfo,Nothing
},
weights::{
 constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND},
 IdentityFee, Weight,
},
StorageValue,
};

添加一行从 Contracts pallet 导入默认的 contract 权重,如下:

pub use frame_system::Call as SystemCall;
pub use pallet_balances::Call as BalancesCall;
pub use pallet_timestamp::Call as TimestampCall;
use pallet_transaction_payment::CurrencyAdapter;
use pallet_contracts::DefaultContractAccessWeight; // Add this line

添加 Contracts pallet 所需的常量添加到运行时,如下:

/* After this block */
// Time is measured by number of blocks.
pub const MINUTES: BlockNumber = 60_000 / (MILLISECS_PER_BLOCK as BlockNumber);
pub const HOURS: BlockNumber = MINUTES * 60;
pub const DAYS: BlockNumber = HOURS * 24;

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

const fn deposit(items: u32, bytes: u32) -> Balance {
items as Balance * 15 * CENTS + (bytes as Balance) * 6 * CENTS
}
const AVERAGE_ON_INITIALIZE_RATIO: Perbill = Perbill::from_percent(10);
/*** End Added Block ***/

在运行时中为 pallet_contracts 添加参数类型和实现 Config trait,如下

/*** Add a block similar to the following ***/
parameter_types! {
  pub const DepositPerItem: Balance = deposit(1, 0);
  pub const DepositPerByte: Balance = deposit(0, 1);
  pub const DeletionQueueDepth: u32 = 128;
  pub DeletionWeightLimit: Weight = AVERAGE_ON_INITIALIZE_RATIO * BlockWeights::get().max_block;
  pub Schedule: pallet_contracts::Schedule<Runtime> = Default::default();
}

impl pallet_contracts::Config for Runtime {
  type Time = Timestamp;
  type Randomness = RandomnessCollectiveFlip;
  type Currency = Balances;
  type Event = Event;
  type Call = Call;
  type CallFilter = frame_support::traits::Nothing;
  type WeightPrice = pallet_transaction_payment::Pallet<Self>;
  type WeightInfo = pallet_contracts::weights::SubstrateWeight<Self>;
  type ChainExtension = ();
  type Schedule = Schedule;
  type CallStack = [pallet_contracts::Frame<Self>; 31];
  type DeletionQueueDepth = DeletionQueueDepth;
  type DeletionWeightLimit = DeletionWeightLimit;
  type DepositPerByte = DepositPerByte;
  type DepositPerItem = DepositPerItem;
  type AddressGenerator = pallet_contracts::DefaultAddressGenerator;
  type ContractAccessWeight = DefaultContractAccessWeight<BlockWeights>;
  type MaxCodeLen = ConstU32<{ 256 * 1024 }>;
  type RelaxedMaxCodeLen = ConstU32<{ 512 * 1024 }>;
  type MaxStorageKeyLen = ConstU32<{ 512 * 1024 }>;
}
/*** End added block ***/

关于 Contracts pallet 配置的更多信息以及类型和参数如何被使用的,可参考 Contracts pallet source code

添加 pallet_contractsconstruct_runtime! 宏中,如下:

// Create the runtime by composing the FRAME pallets that were previously configured
construct_runtime!(
  pub enum Runtime where
    Block = Block,
    NodeBlock = opaque::Block,
    UncheckedExtrinsic = UncheckedExtrinsic
  {
     System: frame_system,
     RandomnessCollectiveFlip: pallet_randomness_collective_flip,
     Timestamp: pallet_timestamp,
     Aura: pallet_aura,
     Grandpa: pallet_grandpa,
     Balances: pallet_balances,
     TransactionPayment: pallet_transaction_payment,
     Sudo: pallet_sudo,
     Contracts: pallet_contracts,
  }
);

保存代码并关闭 runtime/src/lib.rs 文件。

通过运行下面的命令检查你的运行时编译是否正确:

cargo check -p node-template-runtime

暴露合约的API

一些 pallets,包括 Contracts pallet,暴露了自定义运行时 APIs 和 RPC 端点。你无需在 Contracts pallet 上启用 RPC 调用就可以在链上使用它。然而,暴露 Contracts pallet 的 API 和 endpoints 很有用,因为这样做可以使你执行以下任务:

  • 从链下读取合约状态。
  • 在不进行交易的情况下调用节点存储。

要暴露合约 RPC API,打开 runtime/Cargo.toml 文件,使用与其他 pallets 相同的版本和分支信息,将 pallet-contracts-rpc-runtime-api pallet 的相关代码描述添加到 [dependencies] 部分,如下:

pallet-contracts-rpc-runtime-api = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.26" }

pallet-contracts-rpc-runtime-api 添加到 std 特性列表中,以便在构建运行时为原生二进制时包含其特性。

[features]
default = ["std"]
std = [
  "codec/std",
  ...
  "pallet-contracts/std",
  "pallet-contracts-primitives/std",
  "pallet-contracts-rpc-runtime-api/std",
  ...
]

保存代码并关闭 runtime/Cargo.toml 文件。

打开 runtime/src/lib.rs 文件,并通过添加以下常量启用合约调试:

const CONTRACTS_DEBUG_OUTPUT: bool = true;

impl_runtime_apis! 宏中,接近运行时 lib.rs 文件末尾的地方,实现合约运行时 API,如在 impl_runtime_apis! { } 的部分:

/*** Add this block ***/
impl pallet_contracts_rpc_runtime_api::ContractsApi<Block, AccountId, Balance, BlockNumber, Hash>
 for Runtime
 {
  fn call(
     origin: AccountId,
     dest: AccountId,
     value: Balance,
     gas_limit: u64,
     storage_deposit_limit: Option<Balance>,
     input_data: Vec<u8>,
  ) -> pallet_contracts_primitives::ContractExecResult<Balance> {
     Contracts::bare_call(origin, dest, value, gas_limit, storage_deposit_limit, input_data, CONTRACTS_DEBUG_OUTPUT)
  }
  
  fn instantiate(
     origin: AccountId,
     value: Balance,
     gas_limit: u64,
     storage_deposit_limit: Option<Balance>,
     code: pallet_contracts_primitives::Code<Hash>,
     data: Vec<u8>,
     salt: Vec<u8>,
  ) -> pallet_contracts_primitives::ContractInstantiateResult<AccountId, Balance> {
     Contracts::bare_instantiate(origin, value, gas_limit, storage_deposit_limit, code, data, salt, CONTRACTS_DEBUG_OUTPUT)
     }
     
  fn upload_code(
     origin: AccountId,
     code: Vec<u8>,
     storage_deposit_limit: Option<Balance>,
  ) -> pallet_contracts_primitives::CodeUploadResult<Hash, Balance> {
     Contracts::bare_upload_code(origin, code, storage_deposit_limit)
  }
  
  fn get_storage(
     address: AccountId,
     key: Vec<u8>,
     ) -> pallet_contracts_primitives::GetStorageResult {
     Contracts::get_storage(address, key)
     }
  }

保存代码并关闭 runtime/src/lib.rs 文件。

通过运行下面的命令检查你的运行时编译是否正确:

cargo check -p node-template-runtime

更新外部节点

要与 Contracts pallet 交互,必须扩展现有的 RPC server 以包括 Contracts pallet 和 Contracts RPC API。为了使 Contracts pallet 能够利用RPC endpoint API,你需要将自定义 RPC endpoint 添加到外部节点配置中。

将 RPC API 扩展添加到外部节点,打开 node/Cargo.toml 并且添加 pallet-contractspallet-contracts-rpc[dependencies] 部分,如下:

pallet-contracts = { version = "4.0.0-dev", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.26" }
pallet-contracts-rpc = { version = "4.0.0-dev", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.26" }

由于你已经暴露了运行时API,并且现在正在为外部节点编写代码,不需要使用 no_std 配置,所以你不必维护专用的 std 特性列表。

保存代码并关闭 node/Cargo.toml 文件。

打开 node/src/rpc.rs 文件,并找到下面这一行。

use node_template_runtime::{opaque::Block, AccountId, Balance, Index};

use pallet_contracts_rpc 添加到文件中,如下:

use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer};
use substrate_frame_rpc_system::{System, SystemApiServer};
use pallet_contracts_rpc::{Contracts, ContractsApiServer}; // Add this line

为 RPC 扩展添加 Contracts RPC pallet 到 create_full 函数中,如下:

/// Instantiate all full RPC extensions.
pub fn create_full<C, P>(
  deps: FullDeps<C, P>,
) -> Result<RpcModule<()>, Box<dyn std::error::Error + Send + Sync>>
where
  C: ProvideRuntimeApi<Block>,
  C: HeaderBackend<Block> + HeaderMetadata<Block, Error = BlockChainError> + 'static,
  C: Send + Sync + 'static,
  C::Api: substrate_frame_rpc_system::AccountNonceApi<Block, AccountId, Index>,
  C::Api: pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi<Block, Balance>,
  C::Api: pallet_contracts_rpc::ContractsRuntimeApi<Block, AccountId, Balance, BlockNumber, Hash>, // Add this line
  C::Api: BlockBuilder<Block>,
  P: TransactionPool + 'static,

添加 Contracts RPC API 的扩展。

module.merge(System::new(client.clone(), pool.clone(), deny_unsafe).into_rpc())?;
module.merge(TransactionPayment::new(client.clone()).into_rpc())?;
module.merge(Contracts::new(client.clone()).into_rpc())?; // Add this line

保存代码并关闭 node/src/rpc.rs 文件。

通过运行下面的命令检查你的运行时编译是否正确:

cargo check -p node-template

如果检查没有错误,你可以通过运行下面的命令在 release 模式下编译节点:

cargo build --release

启动本地Substrate节点

节点编译完成之后,你可以从 Contracts pallet 增强功能的智能合约启动 Substrate 节点,并且使用 front-end template 与之进行交互。

在开发模式下启动本地节点,运行以下命令:

./target/release/node-template --dev

通过查看终端中显示的输出,验证节点是否已启动并成功运行。如果在 finalized 后数字不断在增加,你的区块链正在生成新的块,并且就他们描述的状态达成新的共识,说明是成功运行了。

通过运行下面的命令启动 front-end template 的 web 服务:

yarn start

在浏览器中打开 http://localhost:8000/ 去访问 front-end template。

在 Pallet Interactor 组件中验证 Extrinsic 是被选中的,然后从可供调用的 pallets 列表中选择 contracts

在自定义pallet中使用宏

指定调用的来源