Developers HomeยปTutorialsยปStart a Permissioned Network

Start a Permissioned Network


In this tutorial, you will learn how to build a permissioned network with Substrate by using the node-authorization pallet. This tutorial should take you about 1 hour to complete.

You are probably already familiar with public or permissionless blockchain, where everyone is free to join the network by running a node. In a permissioned network, only authorized nodes are allowed to perform specific activities, like validate blocks and propagate transactions. Some examples of where permissioned blockchains may be desired:

  • Private (or consortium) networks
  • Highly regulated data environments (healthcare, B2B ledgers, etc.)
  • Testing pre-public networks at scale

Before you start, we expect that:

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 and use the substrate tag or contact us on Element.

What You Will Be Doing

Before we get started, let's layout the objectives of this tutorial:

1. Modify the node template project to add `node-authorization pallet`.

2. Launch multiple nodes and authorize new nodes to join.

Learning outcomes

  • Learn how to use the node-authorization pallet in your runtime
  • Learn how to create a permissioned network consisting of multiple nodes

Add node-authorization pallet

The node-authorization pallet is a built-in pallet in Substrate's FRAME, which manages a configurable set of nodes for a permissioned network. Each node is identified by a PeerId which is simply a wrapper on Vec<u8>. Each PeerId is owned by an AccountId that claims it (these are associated in a map ). With this pallet, you have two ways to authorize a node which wants to join the network:

  1. Join the set of well known nodes between which the connections are allowed. You need to be approved by the governance (or sudo) in the system for this.
  2. Ask for a paired peer connection from a specific node. This node can either be a well known node or a normal one.

A node associated with a PeerId must have one and only one owner. The owner of a well known node is specified when adding it. If it's a normal node, any user can claim a PeerId as its owner. To protect against false claims, the maintainer of the node should claim it before even starting the node and therefore revealing their PeerID to the network that anyone could subsequently claim.

The owner of a node can then add and remove connections for their node. To be clear, you can't change the connections between well known nodes, they are always allowed to connect with each other. Instead, you can manipulate the connection between a well known node and a normal node or between two normal nodes and sub-nodes.

The node-authorization pallet integrates an offchain worker to configure it's node connections. Make sure to enable offchain worker with the right CLI flag as offchain worker is disabled by default for non-authority nodes.


Your node may not be synced with the latest block, and thus not be aware of and published updates that are reflected in the node-authorization chain storage. You may need to disable offchain worker and manually set reachable reserved nodes for your node to sync up with the network if this is the case.

Build the node template

To get started:

  1. Clone the node template.

    git clone
    # We want to use the `latest` tag throughout all of this tutorial
    git checkout latest
  2. Build the node template.

    cd substrate-node-template/
    cargo build --release

    If you do run into issues building, checkout these helpful tips.

  3. Now open the code with your favorite editor, and let's make some changes.

Add the node-authorization pallet

First we must add the pallet to our runtime dependencies:


default-features = false
git = ''
tag = 'devhub/latest'
version = '4.0.0-dev'

default = ['std']
std = [

We need to simulate the governance in our simple blockchain, so we just let a sudo admin rule, configuring the pallet's interface to EnsureRoot. In a production environment we should want to have governance based checking implemented here. More details of this Config can be found in the pallet's reference docs.


/* --snip-- */

use frame_system::EnsureRoot;

/* --snip-- */

parameter_types! {
    pub const MaxWellKnownNodes: u32 = 8;
    pub const MaxPeerIdLength: u32 = 128;

impl pallet_node_authorization::Config for Runtime {
    type Event = Event;
    type MaxWellKnownNodes = MaxWellKnownNodes;
    type MaxPeerIdLength = MaxPeerIdLength;
    type AddOrigin = EnsureRoot<AccountId>;
    type RemoveOrigin = EnsureRoot<AccountId>;
    type SwapOrigin = EnsureRoot<AccountId>;
    type ResetOrigin = EnsureRoot<AccountId>;
    type WeightInfo = ();

/* --snip-- */

Finally, we are ready to put our pallet in construct_runtime macro with following extra line of code:


    pub enum Runtime where
        Block = Block,
        NodeBlock = opaque::Block,
        UncheckedExtrinsic = UncheckedExtrinsic
        /* --snip-- */
        NodeAuthorization: pallet_node_authorization, // <-- add this line
        /* --snip-- */

Add genesis storage for our pallet

PeerId is encoded in bs58 format, so we need a new library bs58 in node/Cargo.toml to decode it to get its bytes.


bs58 = "0.4.0"

Now we add a proper genesis storage in node/src/ Similarly, import the necessary dependencies:


/* --snip-- */
use sp_core::OpaquePeerId; // A struct wraps Vec<u8>, represents as our `PeerId`.
use node_template_runtime::NodeAuthorizationConfig; // The genesis config that serves for our pallet.
/* --snip-- */

Adding our genesis config in the helper function testnet_genesis,


/// Configure initial storage state for FRAME modules.
fn testnet_genesis(
    wasm_binary: &[u8],
    initial_authorities: Vec<(AuraId, GrandpaId)>,
    root_key: AccountId,
    endowed_accounts: Vec<AccountId>,
    _enable_println: bool,
) -> GenesisConfig {

        /* --snip-- */

    /*** Add This Block Item ***/
        node_authorization: NodeAuthorizationConfig {
            nodes: vec![

    /* --snip-- */


NodeAuthorizationConfig contains a property named nodes, which is vector of tuple. The first element of the tuple is the OpaquePeerId and we use bs58::decode to convert the PeerId in human readable format to bytes. The second element of the tuple is AccountId and represents the owner of this node, here we are using one of the provided endowed accounts for demonstration: Alice and Bob.

You may wondering where the 12D3KooWBmAwcd4PJNJvfV89HwE48nwkRmAgo8Vy3uQEyNNHBox2 comes from. We can use subkey to generate the above human readable PeerId.

subkey generate-node-key


subkey is a CLI tool that comes bundled with substrate, and you can install it natively too! Refer to the install Instructions.

The output of the command is like:

12D3KooWBmAwcd4PJNJvfV89HwE48nwkRmAgo8Vy3uQEyNNHBox2 // this is PeerId.
c12b6d18942f5ee8528c8e2baf4e147b5c5c18710926ea492d09cbd9f6c9f82a // This is node-key.

Now all the code changes are finished, we are ready to launch our permissioned network!

Get stuck or need help? The solution with all required changes to the base template can be found here for your review. You can also checkout the working full solution to test against with:

# Run from your cloned node template
git checkout tutorials/solutions/permissioned-network
# Re-build the template with this source
cargo build --release
# Try out the functionality described to make sure it works

In the next section, we will use well-known node keys and Peer IDs to launch your permissioned network and allow access for other nodes to join.

Launch our Permissioned Network

In this part, we will demonstrate how to launch and add new nodes to our permissioned chain.

Let's first make sure everything compiles:

# from the root dir of your node template:
cargo build --release

For this demonstration, we'll launch 4 nodes: 3 well known nodes that are allowed to author and validate blocks, and 1 sub-node that only has read-only access to data from a selected well-known node (upon it's approval).

Obtaining Node Keys and PeerIDs

For Alice's well known node:

# Node Key

# Peer ID, generated from node key

# BS58 decoded Peer ID in hex:

For Bob's well known node:

# Node Key

# Peer ID, generated from node key

# BS58 decoded Peer ID in hex:

For Charlie's NOT well known node:

# Node Key

# Peer ID, generated from node key

# BS58 decoded Peer ID in hex:

For Dave's sub-node (to Charlie, more below):

# Node Key

# Peer ID, generated from node key

# BS58 decoded Peer ID in hex:

The nodes of Alice and Bob are already configured in genesis storage and serve as well known nodes. We will later add Charlie's node into the set of well known nodes. Finally we will add the connection between Charlie's node and Dave's node without making Dave's node as a well known node.


You can get the above bs58 decoded peer id by using bs58::decode similar', to how it was used in our genesis storage configuration. Alternatively, there are tools online like this one to en/decode bs58 IDs.

Alice and Bob Start the Network

Let's start Alice's node first:

./target/release/node-template \
--chain=local \
--base-path /tmp/validator1 \
--alice \
--node-key=c12b6d18942f5ee8528c8e2baf4e147b5c5c18710926ea492d09cbd9f6c9f82a \
--port 30333 \
--ws-port 9944

Here we are using --node-key to specify the key that are used for the security connection of the network. This key is also used internally to generate the human readable PeerId as shown in above section.

Other used CLI flags are:

  • --chain=local for a local testnet (not the same as the --dev flag!).
  • --alice to make the node an authority which can author and finalize block, also give the node a name which is alice.
  • --port assign a port for peer to peer connection.
  • --ws-port assign a listening port for WebSocket connection.

You can get the detailed description of above flags and more by running ./target/release/node-template -h.

Start Bob's node:

# In a new terminal, leave Alice running
./target/release/node-template \
--chain=local \
--base-path /tmp/validator2 \
--bob \
--node-key=6ce3be907dbcabf20a9a5a60a712b4256a54196000a8ed4050d352bc113f8c58 \
--port 30334 \
--ws-port 9945

After both nodes are started, you should be able to see new blocks authored and finalized in bother terminal logs. Now let's use the polkadot.js apps and check the well known nodes of our blockchain. Don't forget to switch to one of our local nodes running: or

Then, let's go to Developer page, Chain State sub-tab, and check the data stored in the nodeAuthorization pallet, wellKnownNodes storage. You should be able to see the peer ids of Alice and Bob's nodes, prefixed with 0x to show its bytes in hex format.

We can also check the owner of one node by querying the storage owners with the peer id of the node as input, you should get the account address of the owner.


Add Another Well Known Node

Let's start Charlie's node,

./target/release/node-template \
--chain=local \
--base-path /tmp/validator3 \
--name charlie  \
--node-key=3a9d5b35b9fb4c42aafadeca046f6bf56107bd2579687f069b42646684b94d9e \
--port 30335 \
--ws-port=9946 \
--offchain-worker always


The node-authorization pallet integrates an offchain worker to configure node connections. As Charlie is not yet a well-known node, and we intend to attach Dave's node, we require the offchain worker to be enabled.

After it was started, you should see there are no connected peers for this node. This is because we are trying to connect to a permissioned network, you need to get authorization to to be connectable! Alice and Bob were configured already in the genesis, where all others must be added manually via extrinsic.

Remember that we are using sudo pallet for our governance, we can make a sudo call on add_well_known_node dispatch call provided by node-authorization pallet to add our node. You can find more available calls in this reference doc.

Go to Developer page, Sudo tab, in apps and submit the nodeAuthorization - add_well_known_node call with the peer id in hex of Charlie's node and the owner is Charlie, of course. Note Alice is the valid sudo origin for this call.


After the transaction is included in the block, you should see Charlie's node is connected to Alice and Bob's nodes, and starts to sync blocks. Notice the reason the three nodes can find each other is mDNS discovery mechanism is enabled by default in a local network.


If your nodes are not on the same local network, you don't need mDNS and should use --no-mdns to disable it. If running node in a public internet, you may wish to specify bootnodes if a static IP or DNS entry is available in the Chain Spec, for convenience.

Now we have 3 well known nodes all validating blocks together!

Add Dave as a Sub-Node to Charlie

Let's add Dave's node, not as a well-known node, but a "sub-node" of Charlie. Dave will only be able to connect to Charlie to access the network. This is a security feature: as Charlie is therefore solely responsible for any connected sub-node peer. There is one point of access control for David in case they need to be removed or audited.

Start Dave's node with following command:

./target/release/node-template \
--chain=local \
--base-path /tmp/validator4 \
--name dave \
--node-key=a99331ff4f0e0a0434a6263da0a5823ea3afcfffe590c9f3014e6cf620f2b19a \
--port 30336 \
--ws-port 9947 \
--offchain-worker always

After it was started, there is no available connections. This is a permissioned network, so first, Charlie needs to configure his node to allow the connection from Dave's node.

In the Developer Extrinsics page, get Charlie to submit an addConnections extrinsic. The first PeerId is the peer id in hex of Charlie's node. The connections is a list of allowed peer ids for Charlie's node, here we only add Dave's.


Then, Dave needs to configure his node to allow the connection from Charlie's node. But before he adds this, Dave needs to claim his node, hopefully it's not too late!


Similarly, Dave can add connection from Charlie's node.


You should now see Dave is catching up blocks and only has one peer which belongs to Charlie! Restart Dave's node in case it's not connecting with Charlie right away.


Any node may issue extrinsics that effect other node's behavior, as long as it is on chain data that is used for reference, and you have the singing key in the keystore available for the account in question for the extrinsics' required origin. All nodes in this demonstration have access to the developer signing keys, thus we were able to issue commands that effected Charlie's sub-nodes from any connected node on our network on behalf of Charlie. In a real world application, node operators would only have access to their node keys, and would be the only ones able to sign and submit extrinsics properly, very likely from their own node where they have control of the key's security.


You are at the end of this tutorial and are already learned about how to build a permissioned network. You can also play with other dispatchable calls like remove_well_known_node, remove_connections.

Next steps

Last edit: on

Run into problems?
Let us Know