Substrate 区块链开发实战:构建与测试 PoE Pallet
Table of Contents
Substrate 区块链开发实战:构建与测试 PoE Pallet
在区块链技术的发展中,Substrate作为一个模块化、可定制化的区块链开发框架,为开发者提供了强大的功能支持。本文通过实操案例,带领读者学习如何在Substrate上开发一个Proof of Existence(PoE)模块。PoE是数字内容存证的有效方式,应用场景包括版权保护、司法证据保存、供应链追溯等。
本文介绍了如何使用Substrate框架开发一个Proof of Existence(PoE)模块,包括从环境搭建到模块实现的全过程。通过该模块,用户可以创建、撤销和转移数字文件的存证信息。文章详细介绍了从代码克隆、编译、开发到测试的每一步骤,并展示了如何在Polkadot JS应用中进行操作测试。最终,通过本项目,读者将掌握Substrate框架在实际应用中的使用技巧。
Build a Blockchain with Substrate
Substrate overview
- 定制化、模块化
- 升级容易
- Runtime 开发基于宏 macro ,标准化、节省开发时间 rust 宏 链上存储
Introduction to On-chain PoE Proof of existence is an online service that can be used to verify the existence of a digital file at a certain point in time. It was originally implemented through transactions with timestamps on the Bitcoin network. The use cases includes
Proof of existence 是一种在线服务,可以用来验证某个数字文件在某个特定时间点的存在。它最初是通过比特币网络上的带时间戳的交易来实现的。
- copyright of digital contents 数字内容的版权
- judicial evidence preservation 司法证据保存
- supply chain traceability 供应链可追溯性
- digital invoicing 数字发票
Homework
- Continue unimplemented parts for PoE pallet
- Implement a function to transfer a claim, which contains two parameters, one is the hash of the content, and the other is the receiving account id for the transfer.
- Implement the unit tests for all extrinsics, verify all events and errors.
- Update the runtime, start the chain in dev mode, test PoE via Polkadot JS App
实操
第一步:克隆代码
git clone https://github.com/papermoonio/polkadot-sdk-solo-template-dev-courses.git
第二步:进入目录
cd polkadot-sdk-solo-template-dev-courses
第三步:编译
cargo build --release
# check
cargo check
第四步:进入pallets目录将 template 目录复制到 poe 目录
# 切换到当前工作目录中的 pallets 目录
cd pallets
# 将 template 目录(及其内容)复制到当前工作目录下的新目录 poe 中。如果 poe 目录不存在,cp -r 会自动创建它并将内容复制进去。
cp -r template poe
第五步:查看目录结构
➜ tree . -L 6 -I 'target|coverage|coverage_report|node_modules'
.
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── docs
│ └── rust-setup.md
├── env-setup
│ ├── README.md
│ ├── flake.lock
│ ├── flake.nix
│ └── rust-toolchain.toml
├── localSpec.json
├── node
│ ├── Cargo.toml
│ ├── build.rs
│ └── src
│ ├── benchmarking.rs
│ ├── chain_spec.rs
│ ├── cli.rs
│ ├── command.rs
│ ├── main.rs
│ ├── rpc.rs
│ └── service.rs
├── pallets
│ ├── poe
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ └── src
│ │ ├── benchmarking.rs
│ │ ├── lib.rs
│ │ ├── mock.rs
│ │ ├── tests.rs
│ │ └── weights.rs
│ └── template
│ ├── Cargo.toml
│ ├── README.md
│ └── src
│ ├── benchmarking.rs
│ ├── lib.rs
│ ├── mock.rs
│ ├── tests.rs
│ └── weights.rs
├── runtime
│ ├── Cargo.toml
│ ├── build.rs
│ └── src
│ └── lib.rs
└── rust-toolchain.toml
12 directories, 37 files
第六步:在项目根目录下的 Cargo.toml 文件中的 members 项,添加 pallets/poe 到列表中,如下所示
members = ["node", "pallets/template", "pallets/poe", "runtime"]
第七步:在 pallets/poe/Cargo.toml 文件中,将 pallet-template 替换为 pallet-poe,如下所示
[package]
name = "pallet-poe"
第八步:在 pallets/poe/src/lib.rs 文件中,添加以下代码
#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
pub use pallet::*;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {
#[pallet::constant]
type MaxClaimLength: Get<u32>;
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
#[pallet::storage]
pub type Proofs<T: Config> = StorageMap<
_,
Blake2_128Concat,
BoundedVec<u8, T::MaxClaimLength>,
(T::AccountId, BlockNumberFor<T>),
>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
ClaimCreated(T::AccountId, BoundedVec<u8, T::MaxClaimLength>),
ClaimRevoked(T::AccountId, BoundedVec<u8, T::MaxClaimLength>),
}
#[pallet::error]
pub enum Error<T> {
ProofAlreadyExists,
ClaimTooLong,
ClaimNotExist,
NotClaimOwner,
NewOwnerIsCurrentOwner,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(0)]
// #[pallet::weight(T::WeightInfo::create_claim(claim.len() as u32))]
pub fn create_claim(
origin: OriginFor<T>,
claim: BoundedVec<u8, T::MaxClaimLength>,
) -> DispatchResult {
// Check that the extrinsic was signed and get the signer.
let sender = ensure_signed(origin)?;
ensure!(
!Proofs::<T>::contains_key(&claim),
Error::<T>::ProofAlreadyExists
);
Proofs::<T>::insert(
&claim,
(sender.clone(), frame_system::Pallet::<T>::block_number()),
);
// Emit an event.
Self::deposit_event(Event::ClaimCreated(sender, claim));
// Return a successful `DispatchResult`
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(0)]
// #[pallet::weight(T::WeightInfo::revoke_claim(claim.len() as u32))]
pub fn revoke_claim(
origin: OriginFor<T>,
claim: BoundedVec<u8, T::MaxClaimLength>,
) -> DispatchResultWithPostInfo {
let sender = ensure_signed(origin)?;
let (owner, _) = Proofs::<T>::get(&claim).ok_or(Error::<T>::ClaimNotExist)?;
ensure!(owner == sender, Error::<T>::NotClaimOwner);
Proofs::<T>::remove(&claim);
Self::deposit_event(Event::ClaimRevoked(sender, claim));
Ok(().into()) // 撤销存证功能完成
}
#[pallet::call_index(2)]
#[pallet::weight(0)]
// #[pallet::weight(T::WeightInfo::transfer_claim(claim.len() as u32))]
pub fn transfer_claim(
origin: OriginFor<T>,
claim: BoundedVec<u8, T::MaxClaimLength>,
new_owner: T::AccountId,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
let (owner, _) = Proofs::<T>::get(&claim).ok_or(Error::<T>::ClaimNotExist)?;
ensure!(owner == sender, Error::<T>::NotClaimOwner);
ensure!(new_owner != owner, Error::<T>::NewOwnerIsCurrentOwner);
Proofs::<T>::insert(
&claim,
(new_owner.clone(), frame_system::Pallet::<T>::block_number()),
);
Self::deposit_event(Event::ClaimCreated(new_owner, claim));
Ok(())
}
}
}
第九步:在 runtime/Cargo.toml 文件中,添加 pallets-poe 相关依赖,如下所示
[dependencies]
pallet-poe = { path = "../pallets/poe", default-features = false }
[features]
default = ["std"]
std = ["pallet-poe/std",]
第十步:在 runtime/src/lib.rs 文件中,添加以下代码
pub use pallet_poe;
impl pallet_poe::Config for Runtime {
type MaxClaimLength = ConstU32<100>;
type RuntimeEvent = RuntimeEvent;
}
#[frame_support::runtime]
mod runtime {
...
#[runtime::pallet_index(8)]
pub type PoeModule = pallet_poe;
}
注意:在开发过程中,代码更新后要及时在相关目录下运行 cargo build --release
,以确保代码没有问题。例如,在 pallets/poe
目录下运行 cargo build --release
,在 runtime
目录下也要运行 cargo build --release
。如果编译速度过慢,可以尝试使用 cargo check
来进行快速检查
第十一步:编写测试代码
pallets/poe/src/mock.rs
文件
use crate as pallet_poe;
use frame_support::{
derive_impl,
traits::{ConstU16, ConstU32, ConstU64},
};
use sp_core::H256;
use sp_runtime::{
traits::{BlakeTwo256, IdentityLookup},
BuildStorage,
};
type Block = frame_system::mocking::MockBlock<Test>;
// Configure a mock runtime to test the pallet.
frame_support::construct_runtime!(
pub enum Test
{
System: frame_system,
PoeModule: pallet_poe,
}
);
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Test {
type BaseCallFilter = frame_support::traits::Everything;
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Nonce = u64;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = u64;
type Lookup = IdentityLookup<Self::AccountId>;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = ConstU64<250>;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = ();
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ConstU16<42>;
type OnSetCode = ();
type MaxConsumers = frame_support::traits::ConstU32<16>;
}
impl pallet_poe::Config for Test {
type MaxClaimLength = ConstU32<100>;
type RuntimeEvent = RuntimeEvent;
}
// Build genesis storage according to the mock runtime.
pub fn new_test_ext() -> sp_io::TestExternalities {
frame_system::GenesisConfig::<Test>::default()
.build_storage()
.unwrap()
.into()
}
pallets/poe/src/tests.rs
文件
use super::*;
use crate as pallet_poe;
use crate::{mock::*, Error};
use frame_support::{assert_noop, assert_ok, BoundedVec};
// use sp_runtime::BoundedVec;
// 测试 create_claim
#[test]
fn it_works_for_create_claim() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
let claim = BoundedVec::try_from(vec![1, 2, 3]).unwrap();
assert_ok!(PoeModule::create_claim(
RuntimeOrigin::signed(1),
claim.clone()
));
assert_eq!(pallet_poe::Proofs::<Test>::get(&claim), Some((1, 1)));
assert_eq!(
pallet_poe::Proofs::<Test>::get(&claim),
Some((1_u64, 1_u64))
);
// Go past genesis block so events get deposited
System::set_block_number(1);
});
}
#[test]
fn create_claim_works() {
new_test_ext().execute_with(|| {
let claim = BoundedVec::try_from(vec![1, 1]).unwrap();
assert_ok!(PoeModule::create_claim(
RuntimeOrigin::signed(2),
claim.clone()
));
assert_eq!(
Proofs::<Test>::get(&claim),
Some((2, frame_system::Pallet::<Test>::block_number()))
);
assert_eq!(<<Test as Config>::MaxClaimLength as Get<u32>>::get(), 100);
})
}
#[test]
fn create_claim_failed_when_claim_already_exist() {
new_test_ext().execute_with(|| {
let claim = BoundedVec::try_from(vec![1, 1]).unwrap();
let _ = PoeModule::create_claim(RuntimeOrigin::signed(1), claim.clone());
assert_noop!(
PoeModule::create_claim(RuntimeOrigin::signed(1), claim.clone()),
Error::<Test>::ProofAlreadyExists
);
})
}
#[test]
fn revoke_claim_works() {
new_test_ext().execute_with(|| {
let claim = BoundedVec::try_from(vec![1, 1]).unwrap();
let _ = PoeModule::create_claim(RuntimeOrigin::signed(1), claim.clone());
assert_ok!(PoeModule::revoke_claim(
RuntimeOrigin::signed(1),
claim.clone()
));
})
}
#[test]
fn revoke_claim_failed_when_claim_is_not_exist() {
new_test_ext().execute_with(|| {
let claim = BoundedVec::try_from(vec![1, 1]).unwrap();
assert_noop!(
PoeModule::revoke_claim(RuntimeOrigin::signed(1), claim.clone()),
Error::<Test>::ClaimNotExist
);
})
}
#[test]
fn revoke_claim_failed_with_wrong_owner() {
new_test_ext().execute_with(|| {
let claim = BoundedVec::try_from(vec![1, 1]).unwrap();
let _ = PoeModule::create_claim(RuntimeOrigin::signed(1), claim.clone());
assert_noop!(
PoeModule::revoke_claim(RuntimeOrigin::signed(2), claim.clone()),
Error::<Test>::NotClaimOwner
);
})
}
#[test]
fn transfer_claim_works() {
new_test_ext().execute_with(|| {
let claim = BoundedVec::try_from(vec![1, 1]).unwrap();
let _ = PoeModule::create_claim(RuntimeOrigin::signed(1), claim.clone());
assert_ok!(PoeModule::transfer_claim(
RuntimeOrigin::signed(1),
claim.clone(),
2
));
let bounded_claim =
BoundedVec::<u8, <Test as Config>::MaxClaimLength>::try_from(claim.clone()).unwrap();
assert_eq!(
Proofs::<Test>::get(&bounded_claim),
Some((2, frame_system::Pallet::<Test>::block_number()))
);
})
}
#[test]
fn transfer_claim_failed_when_claim_is_not_exist() {
new_test_ext().execute_with(|| {
let claim = BoundedVec::try_from(vec![1, 1]).unwrap();
assert_noop!(
PoeModule::transfer_claim(RuntimeOrigin::signed(1), claim.clone(), 2),
Error::<Test>::ClaimNotExist
);
})
}
#[test]
fn transfer_claim_failed_with_wrong_owner() {
new_test_ext().execute_with(|| {
let claim = BoundedVec::try_from(vec![1, 1]).unwrap();
let _ = PoeModule::create_claim(RuntimeOrigin::signed(1), claim.clone());
assert_noop!(
PoeModule::transfer_claim(RuntimeOrigin::signed(2), claim.clone(), 3),
Error::<Test>::NotClaimOwner
);
})
}
#[test]
fn transfer_claim_failed_with_same_owner() {
new_test_ext().execute_with(|| {
let claim = BoundedVec::try_from(vec![1, 1]).unwrap();
let _ = PoeModule::create_claim(RuntimeOrigin::signed(1), claim.clone());
assert_noop!(
PoeModule::transfer_claim(RuntimeOrigin::signed(1), claim.clone(), 1),
Error::<Test>::NewOwnerIsCurrentOwner
);
})
}
第十二步:运行测试
第十三步:运行项目
启动一个 Substrate 区块链节点,运行在开发模式下,并且使用临时存储。每次启动时都会从一个全新的状态开始,适用于本地开发和测试,不会影响到之前的数据或状态。
第十四步:连接 polkadot.js 网站,通过 Polkadot JS 应用程序测试 PoE
第十五步:测试 createClaim
第十六步:测试 revokeClaim
第十七步:测试 transferClaim
查看确认是否成功:
总结
通过本次实操,读者不仅学习了如何在Substrate框架上实现PoE模块,还了解了如何进行模块化开发、代码调试与单元测试。Substrate的灵活性和可扩展性为区块链应用开发提供了新的可能,特别是像PoE这样的创新应用,能够在多个行业领域中发挥重要作用。在未来的开发中,开发者可以根据需求定制更多功能模块,扩展Substrate的应用范围。