substrate学习笔记4:substrate之构建一个PoE去中心化的应用
1 说明
在本节,我们将学习substrat官方手册的第三个例子,使用substrate来创建自定义的存在证明dapp。
2 学习内容
我们本节的主要内容分为以下三步:
- 基于node template启动一条substrate的链。
- 修改node template来添加我们自己定制的Poe pallet,并实现我们的PoE API。
- 修改前端模板以添加与PoE API交互的自定义用户界面。
3 准备工作
3.1 关于存在证明
我们将要构建的dapp是一个存在证明的应用。
应用的原理为:用户将文件的哈希值上传到区块链“证明其存在”,其它人校验时可以通过原始文件重新生成哈希和链上的比对来获知此文件没有被篡改过。
3.2 安装note-template
安装命令如下:
git clone -b latest --depth 1 https://github.com/substrate-developer-hub/substrate-node-template
3.3 安装front-end template
git clone -b latest --depth 1 https://github.com/substrate-developer-hub/substrate-front-end-template
cd substrate-front-end-template
yarn install
3.4 接口设计
PoE API将暴露两个功能接口:
- create_claim()
用户通过此接口上传文件的摘要证明存在该文件。
- revoke_claim()
允许当前文件的所有者撤销这个声明。
4 创建自定义pallet
node template的运行时是基于FRAME来实现的。FRAME是一个代码库,允许我们使用一系列pallet来构建底层的运行时。pallet可以看出是定义区块链功能的单个逻辑模块。subtrate为我们提供了一些已经构建好的pallet,我们在定义运行时时可以直接使用。
4.1 substrate构建的链的文件目录结构
目录如下:
substrate-node-template
|
+-- node
|
+-- pallets
| |
| +-- template
| |
| +-- Cargo.toml <-- *Modify* this file
| |
| +-- src
| |
| +-- lib.rs <-- *Remove* contents
| |
| +-- mock.rs <-- *Remove* (optionally modify)
| |
| +-- tests.rs <-- *Remove* (optionally modify)
|
+-- runtime
|
+-- scripts
|
+-- ...
4.2 从头写pallet
4.2.1、pallet脚手架
在此之前,我们可以通过frame结构了解FRAME pallet的基本结构。
下面开始写我们的代码,在pallet/template/src/lib.rs中:
#![cfg_attr(not(feature = "std"), no_std)]
// Re-export pallet items so that they can be accessed from the crate namespace.
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
use frame_support::{dispatch::DispatchResultWithPostInfo, pallet_prelude::*};
use frame_system::pallet_prelude::*;
use sp_std::vec::Vec; // Step 3.1 will include this in `Cargo.toml`
#[pallet::config] // <-- Step 2. code block will replace this.
#[pallet::event] // <-- Step 3. code block will replace this.
#[pallet::error] // <-- Step 4. code block will replace this.
#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
pub struct Pallet<T>(_);
#[pallet::storage] // <-- Step 5. code block will replace this.
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
#[pallet::call] // <-- Step 6. code block will replace this.
}
通过上述代码,我们声明了pallet运行所需的依赖和宏。
4.2.2、Pallet Configure trait
每个pallet都有一个Config的组件用于配置。此处,我们需要对pallet的配置就是发出一些事件。我们需要将#[pallet::config] 替换成以下代码:
/// Configure the pallet by specifying the parameters and types on which it depends.
#[pallet::config]
pub trait Config: frame_system::Config {
/// Because this pallet emits events, it depends on the runtime's definition of an event.
type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
}
4.2.3. pallet 事件
下面我们来定义事件。我们的pallet仅两种情况下需要发出事件,分别如下:
- 当向区块链添加新的证明时;
- 当证明被移除时。
我们将替换#[pallet::event]下面的内容:
// Pallets use events to inform users when important changes are made.
// Event documentation should end with an array that provides descriptive names for parameters.
// https://substrate.dev/docs/en/knowledgebase/runtime/events
#[pallet::event]
#[pallet::metadata(T::AccountId = "AccountId")]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Event emitted when a proof has been claimed. [who, claim]
ClaimCreated(T::AccountId, Vec<u8>),
/// Event emitted when a claim is revoked by the owner. [who, claim]
ClaimRevoked(T::AccountId, Vec<u8>),
}
注意,此处我们使用的use sp_std::Vec,而不是Rust中标准的std,因此我们需要在Cargo.toml中添加如下依赖:
# add `sp-std` in the dependencies section of the toml file
[dependencies]
# -- snip --
sp-std = { default-features = false, version = '3.0.0' }
# -- snip --
4.2.4 Pallet errors
替换#[pallet::error]内容:
#[pallet::error]
pub enum Error<T> {
/// The proof has already been claimed.
ProofAlreadyClaimed,
/// The proof does not exist, so it cannot be revoked.
NoSuchProof,
/// The proof is claimed by another account, so caller can't revoke it.
NotProofOwner,
}
4.2.5、pallet 存储
在向区块链中添加新的证明时,我们需要将证明内容存储到pallet的storage中。为了存储这些内容,我们创建一个hash map来存储内容。
我们替换#[pallet::storage]内容如下:
#[pallet::storage]
pub(super) type Proofs<T: Config> = StorageMap<_, Blake2_128Concat, Vec<u8>, (T::AccountId, T::BlockNumber), ValueQuery>;
4.2.6、pallet的调用函数
此处我们主要有两个函数:
- create_claim()
- revoke_claim()
实现这两个函数,我们替换 #[pallet::call] 如下:
// Dispatchable functions allows users to interact with the pallet and invoke state changes.
// These functions materialize as "extrinsics", which are often compared to transactions.
// Dispatchable functions must be annotated with a weight and must return a DispatchResult.
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(1_000)]
pub fn create_claim(
origin: OriginFor<T>,
proof: Vec<u8>,
) -> DispatchResultWithPostInfo {
// Check that the extrinsic was signed and get the signer.
// This function will return an error if the extrinsic is not signed.
// https://substrate.dev/docs/en/knowledgebase/runtime/origin
let sender = ensure_signed(origin)?;
// Verify that the specified proof has not already been claimed.
ensure!(!Proofs::<T>::contains_key(&proof), Error::<T>::ProofAlreadyClaimed);
// Get the block number from the FRAME System module.
let current_block = <frame_system::Pallet<T>>::block_number();
// Store the proof with the sender and block number.
Proofs::<T>::insert(&proof, (&sender, current_block));
// Emit an event that the claim was created.
Self::deposit_event(Event::ClaimCreated(sender, proof));
Ok(().into())
}
#[pallet::weight(10_000)]
pub fn revoke_claim(
origin: OriginFor<T>,
proof: Vec<u8>,
) -> DispatchResultWithPostInfo {
// Check that the extrinsic was signed and get the signer.
// This function will return an error if the extrinsic is not signed.
// https://substrate.dev/docs/en/knowledgebase/runtime/origin
let sender = ensure_signed(origin)?;
// Verify that the specified proof has been claimed.
ensure!(Proofs::<T>::contains_key(&proof), Error::<T>::NoSuchProof);
// Get owner of the claim.
let (owner, _) = Proofs::<T>::get(&proof);
// Verify that sender of the current call is the claim owner.
ensure!(sender == owner, Error::<T>::NotProofOwner);
// Remove claim from storage.
Proofs::<T>::remove(&proof);
// Emit an event that the claim was erased.
Self::deposit_event(Event::ClaimRevoked(sender, proof));
Ok(().into())
}
}
4.3 编译&启动
执行如下命令:
cargo build --release
./target/release/node-template --dev --tmp
5 创建前端
5.1 启动前端
启动前端的命令如下:
yarn install
yarn start
5.2 添加自定义react 组件
在src目录下编辑TemplateModule.js如下:
// React and Semantic UI elements.
import React, { useState, useEffect } from 'react';
import { Form, Input, Grid, Message } from 'semantic-ui-react';
// Pre-built Substrate front-end utilities for connecting to a node
// and making a transaction.
import { useSubstrate } from './substrate-lib';
import { TxButton } from './substrate-lib/components';
// Polkadot-JS utilities for hashing data.
import { blake2AsHex } from '@polkadot/util-crypto';
// Our main Proof Of Existence Component which is exported.
export function Main (props) {
// Establish an API to talk to our Substrate node.
const { api } = useSubstrate();
// Get the selected user from the `AccountSelector` component.
const { accountPair } = props;
// React hooks for all the state variables we track.
// Learn more at: https://reactjs.org/docs/hooks-intro.html
const [status, setStatus] = useState('');
const [digest, setDigest] = useState('');
const [owner, setOwner] = useState('');
const [block, setBlock] = useState(0);
// Our `FileReader()` which is accessible from our functions below.
let fileReader;
// Takes our file, and creates a digest using the Blake2 256 hash function.
const bufferToDigest = () => {
// Turns the file content to a hexadecimal representation.
const content = Array.from(new Uint8Array(fileReader.result))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
const hash = blake2AsHex(content, 256);
setDigest(hash);
};
// Callback function for when a new file is selected.
const handleFileChosen = (file) => {
fileReader = new FileReader();
fileReader.onloadend = bufferToDigest;
fileReader.readAsArrayBuffer(file);
};
// React hook to update the owner and block number information for a file.
useEffect(() => {
let unsubscribe;
// Polkadot-JS API query to the `proofs` storage item in our pallet.
// This is a subscription, so it will always get the latest value,
// even if it changes.
api.query.templateModule
.proofs(digest, (result) => {
// Our storage item returns a tuple, which is represented as an array.
setOwner(result[0].toString());
setBlock(result[1].toNumber());
})
.then((unsub) => {
unsubscribe = unsub;
});
return () => unsubscribe && unsubscribe();
// This tells the React hook to update whenever the file digest changes
// (when a new file is chosen), or when the storage subscription says the
// value of the storage item has updated.
}, [digest, api.query.templateModule]);
// We can say a file digest is claimed if the stored block number is not 0.
function isClaimed () {
return block !== 0;
}
// The actual UI elements which are returned from our component.
return (
<Grid.Column>
<h1>Proof Of Existence</h1>
{/* Show warning or success message if the file is or is not claimed. */}
<Form success={!!digest && !isClaimed()} warning={isClaimed()}>
<Form.Field>
{/* File selector with a callback to `handleFileChosen`. */}
<Input
type='file'
id='file'
label='Your File'
onChange={ e => handleFileChosen(e.target.files[0]) }
/>
{/* Show this message if the file is available to be claimed */}
<Message success header='File Digest Unclaimed' content={digest} />
{/* Show this message if the file is already claimed. */}
<Message
warning
header='File Digest Claimed'
list={[digest, `Owner: ${owner}`, `Block: ${block}`]}
/>
</Form.Field>
{/* Buttons for interacting with the component. */}
<Form.Field>
{/* Button to create a claim. Only active if a file is selected,
and not already claimed. Updates the `status`. */}
<TxButton
accountPair={accountPair}
label={'Create Claim'}
setStatus={setStatus}
type='SIGNED-TX'
disabled={isClaimed() || !digest}
attrs={{
palletRpc: 'templateModule',
callable: 'createClaim',
inputParams: [digest],
paramFields: [true]
}}
/>
{/* Button to revoke a claim. Only active if a file is selected,
and is already claimed. Updates the `status`. */}
<TxButton
accountPair={accountPair}
label='Revoke Claim'
setStatus={setStatus}
type='SIGNED-TX'
disabled={!isClaimed() || owner !== accountPair.address}
attrs={{
palletRpc: 'templateModule',
callable: 'revokeClaim',
inputParams: [digest],
paramFields: [true]
}}
/>
</Form.Field>
{/* Status message about the transaction. */}
<div style={{ overflowWrap: 'break-word' }}>{status}</div>
</Form>
</Grid.Column>
);
}
export default function TemplateModule (props) {
const { api } = useSubstrate();
return (api.query.templateModule && api.query.templateModule.proofs
? <Main {...props} /> : null);
}
6 交互运行
- 启动node-template
cd substrate-node-template
./target/release/node-template --dev --tmp
- 启动前端
cd substrate-front-end-template
yarn install
yarn start
至此,我们可以使用proof-of-existence。
本作品采用《CC 协议》,转载必须注明作者和本文链接