title |
---|
Building a Custom Pallet |
The Substrate Developer Hub Node Template, which is used as the starting point for this tutorial, has a FRAME-based runtime. FRAME is a library of code that allows you to build a Substrate runtime by composing modules called "pallets". You can think of these pallets as individual pieces of logic that define what your blockchain can do! Substrate provides you with a number of pre-built pallets for use in FRAME-based runtimes.
For example, FRAME includes a Balances pallet that controls the underlying currency of your blockchain by managing the balance of all the accounts in your system.
If you want to add smart contract functionality to your blockchain, you simply need to include the Contracts pallet.
Even things like on-chain governance can be added to your blockchain by including pallets like Democracy, Elections, and Collective.
The goal of this tutorial is to teach you how to create your own FRAME pallet to include in your custom blockchain! The Substrate Developer Hub Node Template comes with a template pallet that you will use as a starting point to build custom runtime logic on top of.
Open the Node Template in your favorite code editor, then open the file
pallets/template/src/lib.rs
substrate-node-template
|
+-- node
|
+-- pallets
| |
| +-- template
| |
| +-- Cargo.toml <-- One change in this file
| |
| +-- src
| |
| +-- lib.rs <-- Most changes in this file
| |
| +-- mock.rs
| |
| +-- tests.rs
|
+-- runtime
|
+-- scripts
|
+-- ...
You will see some pre-written code that acts as a template for a new pallet. You can read over this file if you'd like, and then delete the contents since we will start from scratch for full transparency. When writing your own pallets in the future, you will likely find the scaffolding in this template pallet useful.
At a high level, a FRAME pallet can be broken down into six sections:
// 1. Imports
use frame_support::{decl_module, decl_storage, decl_event, decl_error, dispatch};
use frame_system::ensure_signed;
// 2. Configuration
pub trait Trait: frame_system::Trait { /* --snip-- */ }
// 3. Storage
decl_storage! { /* --snip-- */ }
// 4. Events
decl_event! { /* --snip-- */ }
// 5. Errors
decl_error! { /* --snip-- */ }
// 6. Callable Functions
decl_module! { /* --snip-- */ }
Things like events, storage, and callable functions may look familiar to you if you have done other blockchain development. We will show you what each of these components looks like for a basic Proof Of Existence pallet.
Since imports are pretty boring, you can start by pasting this at the top of your empty lib.rs
file:
#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::{
decl_module, decl_storage, decl_event, decl_error, ensure, StorageMap
};
use frame_system::ensure_signed;
use sp_std::vec::Vec;
Most of these imports are already available because they were used in the template pallet whose code
we just deleted. However, sp_std
is not available and we need to list it as a dependency.
Add this block to your pallets/template/Cargo.toml
file.
[dependencies]
#--snip--
sp-std = { default-features = false, version = '2.0.0' }
Then, Update the existing [features]
block to look like this. The last line is new. You will
learn more about why this is necessary in the next tutorial, the Add a Pallet
tutorial.
[features]
default = ['std']
std = [
'codec/std',
'frame-support/std',
'frame-system/std',
'sp-std/std', <-- This line is new
]
Every pallet has a component called Trait
that is used for configuration. This component is a
Rust "trait"; traits in Rust are similar to
interfaces in languages such as C++, Java and Go. For now, the only thing we will configure about
our pallet is that it will emit some Events. The Trait
interface is another topic that will be
covered in greater depth in the next tutorial, the Add a Pallet tutorial.
/// Configure the pallet by specifying the parameters and types on which it depends.
pub trait Trait: frame_system::Trait {
/// Because this pallet emits events, it depends on the runtime's definition of an event.
type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
}
After we've configured our pallet to emit events, let's go ahead and define which events:
// 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
decl_event! {
pub enum Event<T> where AccountId = <T as frame_system::Trait>::AccountId {
/// Event emitted when a proof has been claimed. [who, claim]
ClaimCreated(AccountId, Vec<u8>),
/// Event emitted when a claim is revoked by the owner. [who, claim]
ClaimRevoked(AccountId, Vec<u8>),
}
}
Our pallet will only emit an event in two circumstances:
- When a new proof is added to the blockchain.
- When a proof is removed.
The events can contain some additional data, in this case, each event will also display who
triggered the event (AccountId
), and the proof data (as Vec<u8>
) that is being stored or
removed. Note that convention is to include an array with descriptive names for these parameters at
the end of event documentation.
The events we defined previously indicate when calls to the pallet have completed successfully. Similarly, errors indicate when a call has failed, and why it has failed.
// Errors inform users that something went wrong.
decl_error! {
pub enum Error for Module<T: Trait> {
/// 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,
}
}
The first of these errors can occur when attempting to claim a new proof. Of course a user cannot claim a proof that has already been claimed. The latter two can occur when attempting to revoke a proof.
To add a new proof to the blockchain, we will simply store that proof in our pallet's storage. To store that value, we will create a hash map from the proof to the owner of that proof and the block number the proof was made.
// The pallet's runtime storage items.
// https://substrate.dev/docs/en/knowledgebase/runtime/storage
decl_storage! {
trait Store for Module<T: Trait> as TemplateModule {
/// The storage item for our proofs.
/// It maps a proof to the user who made the claim and when they made it.
Proofs: map hasher(blake2_128_concat) Vec<u8> => (T::AccountId, T::BlockNumber);
}
}
If a proof has an owner and a block number, then we know that it has been claimed! Otherwise, the proof is available to be claimed.
As implied by our pallet's events and errors, we will have two "dispatchable functions" the user can call in this FRAME pallet:
create_claim()
: Allow a user to claim the existence of a file with a proof.revoke_claim()
: Allow the owner of a claim to revoke their claim.
// 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.
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
// Errors must be initialized if they are used by the pallet.
type Error = Error<T>;
// Events must be initialized if they are used by the pallet.
fn deposit_event() = default;
/// Allow a user to claim ownership of an unclaimed proof.
#[weight = 10_000]
fn create_claim(origin, proof: Vec<u8>) {
// 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::Module<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(RawEvent::ClaimCreated(sender, proof));
}
/// Allow the owner to revoke their claim.
#[weight = 10_000]
fn revoke_claim(origin, proof: Vec<u8>) {
// 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(RawEvent::ClaimRevoked(sender, proof));
}
}
}
The functions you see here do not have return types explicitly stated. In reality they all return
DispatchResult
s. This return type is added on your behalf by thedecl_module!
macro.
After you've copied all of the parts of this pallet correctly into your pallets/template/lib.rs
file, you should be able to recompile your node without warning or error. Run this command in the
root directory of the substrate-node-template
repository to build and run the node:
WASM_BUILD_TOOLCHAIN=nightly-2020-10-05 cargo run --release -- --dev --tmp
Now it is time to interact with our new Proof of Existence pallet!