Building with Substrate

Building Custom Blockchains Using the Substrate Framework

Building a Complete Substrate Blockchain

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.

Project Setup

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 --dev

Custom Pallet Development

Let'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>;
}

Runtime Configuration

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 = ();
}

Testing

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()
}

Deployment

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=30333

Frontend Integration

Let'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());

Advanced Features

Let's add some advanced features to our blockchain:

Multi-Signature Support

Add multi-signature functionality to our pallet.

  • • Create multi-signature accounts
  • • Require multiple signatures for transactions
  • • Manage signatory sets

Governance Integration

Integrate governance functionality into our pallet.

  • • Proposal submission
  • • Voting mechanisms
  • • Execution of approved proposals

Cross-Chain Communication

Add XCM support for cross-chain communication.

  • • XCM message handling
  • • Cross-chain asset transfers
  • • Remote function calls

Performance Optimization

Optimize your blockchain for better performance:

Performance Optimization Tips:

  • Storage Optimization: Use efficient storage patterns
  • Weight Management: Optimize function weights
  • Batch Operations: Use utility pallet for batch operations
  • State Pruning: Implement state pruning for old data
  • Database Optimization: Use appropriate database backends
  • Network Optimization: Optimize network protocols
  • Memory Management: Efficient memory usage

Security Considerations

Ensure your blockchain is secure:

Security Best Practices:

  • Input Validation: Validate all inputs
  • Access Control: Implement proper access controls
  • Error Handling: Handle errors gracefully
  • Testing: Comprehensive testing
  • Auditing: Regular security audits
  • Upgrades: Plan for secure upgrades
  • Monitoring: Monitor for security issues

Deployment Strategies

Different deployment strategies for different use cases:

Development

  • • Single node setup
  • • Local testing
  • • Rapid iteration
  • • Debug mode

Production

  • • Multi-node setup
  • • Validator nodes
  • • Monitoring
  • • Backup strategies

Summary

In this final chapter, we've built a complete Substrate-based blockchain:

  • • Created a custom pallet with storage, events, and errors
  • • Configured the runtime to include our custom pallet
  • • Implemented comprehensive testing
  • • Deployed the blockchain and integrated with frontend
  • • Explored advanced features and optimizations
  • • Considered security and deployment strategies

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!