Developers Home»tutorials»Build a Proof of Existence dApp

Build a Proof of Existence dApp


In this tutorial, you will learn to create a custom "Proof of Existence" (PoE) dApp using the Substrate blockchain development framework and the FRAME library. From Wikipedia:

"Proof of Existence is an online service that verifies the existence of computer files as of a specific time via timestamped transactions in the bitcoin blockchain."

Rather than uploading the entire file to the blockchain to "prove its existence", users submit a hash of the file, known as a file digest or checksum. These hashes are powerful because huge files can be uniquely represented by a small hash value, which is efficient for storing on the blockchain. Any user with the original file can prove that this file matches the one on the blockchain by simply recomputing the hash of the file and comparing it with the hash stored on chain.

File Hash

To add to this, blockchains also provide a robust identity system through accounts that map to public keys, and associations built on top of those keys (see FRAME's identity pallet). So when a file digest is stored on the blockchain, we can also record which account uploaded that digest. This allows that controller of that account to later prove that they were the original person who claim the file.

This tutorial should take you about one hour to complete. We will be using the Rust programming language and ReactJS, but you do not need to know these to complete this guide. We will provide you with working code snippets and explain what all the code does at a high level.

We only expect that:

  • You have completed the Create Your First Substrate Chain Tutorial.
  • You are generally familiar with software development, writing code, and compiling your code.
  • You are open to learning about the bleeding edge of blockchain development.
We're here to help

If you run into an issue on this tutorial, we are here to help! You can ask a question on Stack Overflow using the substrate tag, or contact us on Element.

What you will be doing

1. Launch a Substrate blockchain based on Substrate Node Template.

2. Add our own custom PoE pallet from scratch and implement our PoE API.

3. Add a custom user interface that interacts with our PoE API.

Learning outcomes

  • Learn how to think of Substrate as a framework to create a decentralized application composed of pallets
  • Learn how to create and use a custom FRAME pallet
  • Learn how to modify the Front-end Node Template to match your node's runtime items

Prepare to build a dApp

For our PoE blockchain, we'll be customizing the Node Template so you'll need to make sure you have it installed. We'll also be customizing the Front-end Template so you'll need to make sure you have that too. This part steps you through just that and goes over our application design.

Install the Node Template

You should already have the latest version of Substrate Node Template compiled on your computer from when you completed the Create Your First Substrate Chain Tutorial. If you haven't, please complete that tutorial.

git clone -b latest --depth 1

Experienced developers may prefer to skip that tutorial and install the Node Template according to the instructions in its readme.

Install the Front-End Template

The Create Your First Substrate Chain Tutorial used the latest version of Front-end Template, so there is no additional set-up required if you have already completed that tutorial.


Refer directly to the front-end setup instructions for the Create Your First Chain Tutorial if necessary.

Application Design

Our PoE API will expose two callable functions:

  • create_claim() - allows a user to claim the existence of a file by uploading a file digest.
  • revoke_claim() - allows the current owner of a claim to revoke their ownership.

In order to implement this, we will only need to store information about the proofs that have been claimed, and who made those claims.

Sounds simple enough, right? Let's get coding!

Next steps

  1. Create a custom pallet from scratch
  2. Implement pallet events, errors, and storage items
  3. Implement callabe functions create_claim() and revoke_claim()

Building a Custom Pallet

The Substrate 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.

Runtime Composition

To give some examples, FRAME includes:

  • a Balances pallet that controls the underlying currency of your blockchain by managing the balances of all the accounts in your system.
  • a Contracts pallet, designed to add smart contract functionality to your blockchain.
  • pallets for on-chain governance capabilities such as Democracy, Elections, and Collective.

The goal of this tutorial is to teach you how to create your own FRAME pallet to be included in your custom blockchain. The Node Template comes with a simple pallet that we will use as a starting point to build our own custom runtime logic.

Scaffolding with FRAME


The following sections will start from scratch and assumes that you have cleared the contents inside pallet/template/src/ file.

Open the Node Template in your favorite code editor, then open the file pallets/template/src/

+-- node
+-- pallets
|   |
|   +-- template
|       |
|       +-- Cargo.toml           <-- *Modify* this file
|       |
|       +-- src
|           |
|           +--     <-- *Remove* (optionally modify)
|           |
|           +--           <-- *Remove* contents
|           |
|           +--          <-- *Remove* (optionally modify)
|           |
|           +--         <-- *Remove* (optionally modify)
+-- runtime
+-- scripts
+-- ...

You will see some pre-written code that acts as a template for a new pallet. When writing your own pallets in the future, you will likely find the scaffolding in this template pallet useful. But for the purposes of learning how a pallet is constructed, delete all contents of this file.


Have a look at the skeleton of a FRAME pallet from the docs to learn more about the basic structure of a FRAME pallet.

This tutorial is using the latest version of FRAME so be sure to refer to that. We can start by scaffolding our pallet using the following code:


#![cfg_attr(not(feature = "std"), no_std)]

// Re-export pallet items so that they can be accessed from the crate namespace.
pub use pallet::*;

pub mod pallet {
  use frame_support::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::generate_store(pub(super) trait Store)]
  pub struct Pallet<T>(_);

  #[pallet::storage] // <-- Step 5. code block will replace this.

  impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}

  #[pallet::call]   // <-- Step 6. code block will replace this.

By doing this, we've declared the dependencies and macros our pallet will require to function.

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 by providing you with the code blocks that go under each section.


The no_std feature is required for all pallets! This is because we are building a runtime module that must compile to WASM, and therefore cannot depend on rust's std dependencies. If you take a look at the pallets/template/Cargo.toml file, you will see that the template already has std default feature disabled which is necessary in order to compile the runtime to WASM. Learn more about why this is necessary in the Add a Pallet tutorial. You can only use std features in non-runtime components like and using [dev-dependencies]. Specifics and examples of this are outside the scope of this tutorial. Learn more about testing in Substrate here.

Pallet Configuration Trait

Every pallet has a component called Config that is used for configuration. It 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 Config trait is another topic that will be covered in greater depth in the Add a Pallet tutorial. To define the pallet's Config trait, replace the #[pallet::config] line with this block:


/// Configure the pallet by specifying the parameters and types on which it depends.
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>;

Implement pallet Events

Now that we've configured our pallet to emit events, let's go ahead and define those events. Our pallet will only emit an event in two circumstances:

  1. When a new proof is added to the blockchain.
  2. When a proof is removed.

Events can contain some additional data. In this case, each event will also display who triggers 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 when documenting an event.

To implement this, replace #[pallet::event] with:


// Pallets use events to inform users when important changes are made.
// Event documentation should end with an array that provides descriptive names for parameters.
#[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>),

Include sp-std library

Notice here that we are using the Vec<u8> type, which normally is included in the std Rust library. But we cannot use std in pallet development! So instead, we have included use sp_std::vec::Vec; in our mod pallet. The sp-std crate includes many common things that we desire from std, but are no_std compatible. To use this though, we must update our pallet's dependencies:


# add `sp-std` in the dependencies section of the toml file

# -- snip --
default-features = false
git = ''
tag = 'monthly-2021-10'  # or the latest monthly
version = '4.0.0-dev'    # or the latest version

# -- Scroll to the bottom section --

default = ['std']
std = [
  # -- snip --

Include pallet errors

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.

The first of these errors can occur when attempting to claim a new proof. A user cannot claim a proof that has already been claimed. The other two can occur when attempting to revoke a proof.

To implement this, replace the #[pallet::error] line with:


pub enum Error<T> {
  /// The proof has already been claimed.
  /// The proof does not exist, so it cannot be revoked.
  /// The proof is claimed by another account, so caller can't revoke it.

Implement a StorageMap storage item

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. We'll be using FRAME's StorageMap to keep track of this information.

To implement this, replace the #[pallet::storage] line with:


pub(super) type Proofs<T: Config> = StorageMap<_, Blake2_128Concat, Vec<u8>, (T::AccountId, T::BlockNumber), ValueQuery>;

Implement callable functions

As implied by our pallet's events and errors, we will have two "dispatchable functions" the user can call in this FRAME pallet:

  1. create_claim(): Allow a user to claim the existence of a file with a proof.
  2. revoke_claim(): Allow the owner of a claim to revoke their claim.

These functions will be based on using the StorageMap based on the following logic: 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 (and written to storage).

To implement this, replace the #[pallet::call] line with:


// 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.
impl<T: Config> Pallet<T> {
  pub fn create_claim(
    origin: OriginFor<T>,
    proof: Vec<u8>,
  ) -> DispatchResult {
    // Check that the extrinsic was signed and get the signer.
    // This function will return an error if the extrinsic is not signed.
    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(Event::ClaimCreated(sender, proof));


  pub fn revoke_claim(
    origin: OriginFor<T>,
    proof: Vec<u8>,
  ) -> DispatchResult {
    // Check that the extrinsic was signed and get the signer.
    // This function will return an error if the extrinsic is not signed.
    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.

    // Emit an event that the claim was erased.
    Self::deposit_event(Event::ClaimRevoked(sender, proof));


Build Your New Pallet

After you've copied all of the parts of this pallet correctly into your pallets/template/ 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:

# Compile your node
cargo build --release
# Launch your chain in dev mode
./target/release/node-template --dev --tmp

If everything work out properly, your node should run and produce blocks. And now it is time to interact with our new Proof of Existence pallet!


There is a full Node Template solution here to use as a reference if you're stuck. Check the commit diff from the base latest template for the exact changes.

Next steps

  1. Install the Substrate Front-end Template
  2. Add a custom React component to interact with your node's runtime
  3. Submit a proof and check that your dApp works

Building a Custom Front End

If you have made it this far, that means you should have a brand new blockchain with custom functionality up and running.

In this section, we will get the front-end up and running and add a custom React component designed to make use of our PoE pallet's capabilities.

The first thing you'll need to do is to clone a copy of the the latest version of Front-End Template as a base, if you haven't already.

git clone -b latest --depth 1

Front-end Template requires Yarn Berry to run. Please ensure running yarn --version in your local machine is returning a version of v2 or above.

Explore the Front-End Template

To start the Front-End Template, navigate to its directory and run:

# Install dependencies if this is the first time you run front-end template
yarn install
# Start the template
yarn start

A new tab should open in your web browser and you should see the following interface.

Front End Template

You'll see a list of pre-funded accounts, and you can make token transfers between those accounts.

Balance Transfer

Add your custom react component

In the Front-End Template project, edit the TemplateModule.js file in the /src/ folder:


+-- src
|   |
|   +-- index.js
|   |
|   +-- App.js
|   |
|   +-- TemplateModule.js  <-- Edit this file
|   |
|   +-- ...
+-- ...

Delete the entire contents of that file, and replace it with the following:

// 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:
  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'))

    const hash = blake2AsHex(content, 256)

  // Callback function for when a new file is selected.
  const handleFileChosen = file => {
    fileReader = new FileReader()
    fileReader.onloadend = bufferToDigest

  // 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.
      .proofs(digest, result => {
        // Our storage item returns a tuple, which is represented as an array.
      .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 (
      <h1>Proof of Existence</h1>
      {/* Show warning or success message if the file is or is not claimed. */}
      <Form success={!!digest && !isClaimed()} warning={isClaimed()}>
          {/* File selector with a callback to `handleFileChosen`. */}
            label="Your File"
            onChange={e => handleFileChosen([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. */}
            header="File Digest Claimed"
            list={[digest, `Owner: ${owner}`, `Block: ${block}`]}
        {/* Buttons for interacting with the component. */}
          {/* Button to create a claim. Only active if a file is selected,
          and not already claimed. Updates the `status`. */}
            label={'Create Claim'}
            disabled={isClaimed() || !digest}
              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`. */}
            label="Revoke Claim"
            disabled={!isClaimed() || owner !== accountPair.address}
              palletRpc: 'templateModule',
              callable: 'revokeClaim',
              inputParams: [digest],
              paramFields: [true],
        {/* Status message about the transaction. */}
        <div style={{ overflowWrap: 'break-word' }}>{status}</div>

export default function TemplateModule(props) {
  const { api } = useSubstrate();
  return api.query.templateModule && api.query.templateModule.proofs
    ? <Main {...props} />
    : null;

We won't walk you step by step through the creation of this component, but do look over the code comments to learn what each part is doing.


There is a full Front-end Template solution here to use as a reference if you're stuck. Check the commit diff from the base latest template for the exact changes.

Submit a proof

Your Front-End Template should reload when you save your changes, and you'll notice our new component. Now we're ready to try out our new dApp. Select any file on your computer, and you will see that you can create a claim with its file digest:

Proof Of Existence Component

If you press "Create Claim", a transaction will be dispatched to your custom Proof of Existence pallet, where this digest and the selected user account will be stored on chain.

Claimed File

If all went well, you should see a new ClaimCreated event appear in the Events component. The front-end automatically recognizes that your file is now claimed, and even gives you the option to revoke the claim if you want.

Remember, only the owner can revoke the claim! If you select another user account at the top, and you will see that the revoke option is disabled!

Next steps

This is the end of our journey into creating a Proof of Existence dApp.

You have seen first-hand how simple it can be to develop a brand new pallet and launch a custom blockchain using Substrate and FRAME. Furthermore, we have shown you that the Substrate ecosystem provides you with the tools to quickly create responsive front-end experiences so users can interact with your blockchain.

This tutorial chooses to omit some of the specific details around development in order to keep this experience short and satisfying. However, we want you to keep learning!

Learn more

To learn more about building your own pallets, explore the FRAME documentation and the how-to guides.

Complete the Add a Pallet tutorial to learn how to extend the Node Template with additional capabilities from Substrate's set of core FRAME pallets.

Substrate is written in the Rust programming language, which has a great community and many helpful resources. If you would like to learn more about this powerful and beloved programming language, check out the information-packed Rust Book and the fun Rustlings course.

If you experienced any issues with this tutorial or want to provide feedback, you can ask a question on Stack Overflow (using the substrate tag), or ask any questions in the Substrate Technical Channel on Element. We're here to help!

We can't wait to see what you build next.

Last edit: on

Was This Tutorial Helpful?
Help us improve