Building Custom Blockchains Using the Substrate Framework
In this final chapter, we'll build a complete Substrate-based blockchain from scratch. We'll create a custom pallet, configure the runtime, and deploy our own blockchain. This hands-on approach will solidify your understanding of Substrate development.
Let's start by setting up a new Substrate project:
# Install Substrate dependencies
curl https://get.substrate.io -sSf | bash
# Create a new Substrate node
substrate-node-new my-blockchain
# Navigate to the project
cd my-blockchain
# Build the project
cargo build --release
# Run the node
./target/release/my-blockchain --devLet's create a custom pallet for our blockchain:
// Create a new pallet
mkdir -p pallets/my-pallet/src
// pallets/my-pallet/Cargo.toml
[package]
name = "pallet-my-pallet"
version = "0.1.0"
edition = "2021"
[dependencies]
frame-support = { default-features = false, version = "4.0.0" }
frame-system = { default-features = false, version = "4.0.0" }
sp-std = { default-features = false, version = "4.0.0" }
sp-runtime = { default-features = false, version = "6.0.0" }
[features]
default = ["std"]
std = [
"frame-support/std",
"frame-system/std",
"sp-std/std",
"sp-runtime/std",
]
// pallets/my-pallet/src/lib.rs
use frame_support::{
decl_module, decl_storage, decl_event, decl_error,
traits::{Get, Randomness},
weights::Weight,
};
// Define the pallet
decl_storage! {
trait Store for Module<T: Config> as MyPallet {
// Storage items
pub Value get(fn value): Option<u32>;
pub Nonce get(fn nonce): u64;
}
}
// Define events
decl_event!(
pub enum Event<T> where AccountId = <T as frame_system::Config>::AccountId {
ValueStored(u32, AccountId),
}
);
// Define errors
decl_error! {
pub enum Error for Module<T: Config> {
NoneValue,
StorageOverflow,
}
}
// Define the module
decl_module! {
pub struct Module<T: Config> for enum Call where origin: T::RuntimeOrigin {
type Error = Error<T>;
type RuntimeEvent = Event<T>;
// Initialize the module
fn on_initialize(_n: T::BlockNumber) -> Weight {
Weight::from_parts(10_000, 0)
}
// Finalize the module
fn on_finalize(_n: T::BlockNumber) {
// Clean up
}
// Dispatchable function
#[weight = 10_000]
pub fn do_something(origin, something: u32) -> DispatchResult {
let who = ensure_signed(origin)?;
// Validate input
ensure!(something > 0, Error::<T>::NoneValue);
// Update storage
<Value<T>>::put(something);
<Nonce<T>>::mutate(|n| *n += 1);
// Emit event
Self::deposit_event(Event::ValueStored(something, who));
Ok(())
}
}
}
// Define the configuration trait
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}Now let's configure our runtime to include the custom pallet:
// runtime/src/lib.rs
use frame_support::{
construct_runtime, parameter_types,
traits::{Everything, OnInitialize, OnFinalize},
weights::Weight,
};
// Import our custom pallet
pub use pallet_my_pallet;
// Define the runtime
construct_runtime!(
pub enum Runtime where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system,
Timestamp: pallet_timestamp,
Balances: pallet_balances,
TransactionPayment: pallet_transaction_payment,
Sudo: pallet_sudo,
// Our custom pallet
MyPallet: pallet_my_pallet,
}
);
// Configure system pallet
impl frame_system::Config for Runtime {
type BaseCallFilter = Everything;
type BlockWeights = BlockWeights;
type BlockLength = BlockLength;
type DbWeight = RocksDbWeight;
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Nonce = u64;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = AccountId;
type Lookup = AccountIdLookup<AccountId, ()>;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = BlockHashCount;
type Version = Version;
type PalletInfo = PalletInfo;
type AccountData = AccountData<Balance>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = SS58Prefix;
type OnSetCode = ();
type MaxConsumers = ConstU32<16>;
}
// Configure our custom pallet
impl pallet_my_pallet::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
// Configure balances pallet
impl pallet_balances::Config for Runtime {
type MaxLocks = ConstU32<50>;
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = Balance;
type RuntimeEvent = RuntimeEvent;
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
type WeightInfo = pallet_balances::weights::SubstrateWeight<Runtime>;
type FreezeIdentifier = ();
type MaxFreezes = ();
type HoldIdentifier = ();
type MaxHolds = ();
}Let's create comprehensive tests for our custom pallet:
// pallets/my-pallet/src/tests.rs
use frame_support::{
assert_ok, assert_noop,
traits::{OnInitialize, OnFinalize},
};
#[test]
fn test_do_something() {
new_test_ext().execute_with(|| {
// Test initial state
assert_eq!(MyPallet::value(), None);
// Test successful execution
assert_ok!(MyPallet::do_something(Origin::signed(1), 42));
assert_eq!(MyPallet::value(), Some(42));
// Test error case
assert_noop!(
MyPallet::do_something(Origin::signed(1), 0),
Error::<Test>::NoneValue
);
});
}
#[test]
fn test_on_initialize() {
new_test_ext().execute_with(|| {
// Test initialization
MyPallet::on_initialize(1);
// Add assertions here
});
}
#[test]
fn test_on_finalize() {
new_test_ext().execute_with(|| {
// Test finalization
MyPallet::on_finalize(1);
// Add assertions here
});
}
// Test configuration
impl pallet_my_pallet::Config for Test {
type RuntimeEvent = ();
}
// Test externalities
pub fn new_test_ext() -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::default()
.build_storage::<Test>()
.unwrap();
t.into()
}Now let's deploy our custom blockchain:
# Build the project
cargo build --release
# Run the node in development mode
./target/release/my-blockchain --dev
# Run the node with custom configuration
./target/release/my-blockchain --chain=local --alice --validator --rpc-cors=all --ws-external
# Run the node with custom port
./target/release/my-blockchain --dev --rpc-port=9944 --ws-port=9945 --port=30333Let's create a simple frontend to interact with our blockchain:
// Frontend integration with Polkadot.js API
import { ApiPromise, WsProvider } from '@polkadot/api';
import { Keyring } from '@polkadot/keyring';
// Connect to our custom blockchain
const provider = new WsProvider('ws://localhost:9944');
const api = await ApiPromise.create({ provider });
// Create a keyring
const keyring = new Keyring({ type: 'sr25519' });
const alice = keyring.addFromUri('//Alice');
// Call our custom function
const tx = api.tx.myPallet.doSomething(42);
const hash = await tx.signAndSend(alice);
console.log('Transaction hash:', hash.toHex());
// Listen for events
api.query.system.events((events) => {
events.forEach(({ event }) => {
if (api.events.myPallet.ValueStored.is(event)) {
console.log('Value stored:', event.data.toString());
}
});
});
// Get the current value
const value = await api.query.myPallet.value();
console.log('Current value:', value.toString());Let's add some advanced features to our blockchain:
Add multi-signature functionality to our pallet.
Integrate governance functionality into our pallet.
Add XCM support for cross-chain communication.
Optimize your blockchain for better performance:
Ensure your blockchain is secure:
Different deployment strategies for different use cases:
In this final chapter, we've built a complete Substrate-based blockchain:
Congratulations! You've completed the Polkadot development curriculum. You now have the knowledge and skills to build sophisticated blockchain applications on Polkadot and create your own custom blockchains using Substrate. Continue exploring, building, and contributing to the Polkadot ecosystem!