Photo by Kanchanara on Unsplash
Building a Decentralized Job Board Application on ICP with Rust: A Step-by-Step Guide
Rust developers have many benefits in building on the Internet Computer blockchain (ICP).
Reading this article will guide you through the process of building a decentralized job board using Rust and deploying it on the Internet Computer Protocol (ICP).
The decentralized nature of ICP allows for the development of apps that run fully on the blockchain, without relying on traditional cloud infrastructure. This article also digs into the technical details of creating a decentralized job board with Rust and deploying it over the Internet Computer Protocol (ICP).
The provided Rust code sets up a canister that manages job postings, applications, and job statuses. We’ll break down the code into its core components and explain how they contribute to the functionality.
How ICP Differs from Other Blockchains
Before we get started, let's quickly review the reasons why the computer internet protocol differs from other blockchains and why transactions on ICP don't require faucets.
While many blockchains measure their efficiency based on transactions per second (TX/s), the Internet Computer Protocol (ICP) redefines what a "transaction" means.
While conventional blockchains, such as Ethereum or Bitcoin, are frequently used for financial transactions or straightforward state updates, ICP is intended to function as a universal computer that can host complete web services.
The main distinctions between ICP and other blockchain networks are broken down technically below, using Ethereum as a benchmark.
Rich Transactions on ICP
Transactions in the context of ICP encompass more than just value transfers or state modifications. They are usually more sophisticated and need intricate calculations.
For instance, ICP transactions may comprise whole web service requests that deliver content straight to consumers' browsers, while Ethereum handles transactions that mostly involve balance transfers and calls to smart contracts.
This makes it difficult to compare blockchains solely by their TX/s or TX/d metrics because not all transactions carry the same computational load.
Because ICP can perform general-purpose computations more quickly than Ethereum, a single ICP transaction can perform more computational work than multiple Ethereum transactions.
Execution Throughput: EVM vs. Wasm
Both Ethereum (ETH) and ICP execute smart contracts, although they use separate virtual machines. Ethereum employs a runtime compatible with WebAssembly (Wasm), while ICP uses the Ethereum Virtual Machine (EVM).
The performance characteristics of the two virtual machines are very different, although they both transform high-level code into lower-level instructions like memory reads and writes and arithmetic operations. In Ethereum, "gas," a measure of computational expense, is used to determine the complexity of an instruction.
A comparable statistic called "cycles" is used in ICP to compensate network nodes for their labor. A cursory comparison reveals that Ethereum processes about 1.25 million instructions per second, whereas ICP manages an estimated 20 billion Wasm commands per second.
The execution throughput gives ICP a distinct performance edge when it comes to handling general-purpose computations.
Efficiency of Computational Work
ICP processes about 6.6 million instructions each update call, compared to Ethereum's approximately 83,333 instructions per transaction. This suggests that ICP executes 80 times more computational effort per transaction on average than Ethereum.
Furthermore, Ethereum has a considerably bigger replication factor of about 550,000, whereas ICP only manages 13 replication factors.
This indicates that ICP is more than 3.4 million times more efficient than Ethereum in terms of raw efficiency, enabling it to scale to a degree that is unrivaled by conventional blockchains.
Real-World Use Case: EdDSA Signature Verification
A specific example of how ICP outperforms Ethereum in practical applications is EdDSA signature verification.
Validating an EdDSA signature on Ethereum costs about 500,000 gas units, or about USD 36 at the current gas price. In comparison, the cost of doing the identical process on ICP is approximately 4.2 million cycles, or USD 0.00000567, or a fraction of a cent. As a result, ICP is now 6.3 million times less expensive for this typical calculation task.
The Internet Computer Protocol (ICP) radically expands the computational richness of blockchain transactions while also providing web-speed performance, hence redefining the capabilities of blockchain technology.
Compared to Ethereum and other conventional blockchains, ICP allows for the execution of more complicated processes and applications at a far lower cost and time. Without requiring outside cloud providers, its efficiency, scalability, and lower costs make it a feasible choice for developing next-generation decentralized applications and services.
ICP is different from other blockchains because of its processing capacity and structural efficiency, which enable it to function as more than just a decentralized ledger and eventually become a completely decentralized internet.
Why we do not need a faucet for the Internet Computer Protocol (ICP)
What is a Faucet in the context of blockchain?
A faucet is a program or service that gives users access to small amounts of money, usually for free or in return for doing easy tasks (such as passing CAPTCHAs).
Faucet's main objective is usually to enable users to engage with a blockchain network, try out functionality, and experiment with transactions without having first to buy bitcoin. Faucets are commonly used on Testnet or in the early stages of a blockchain’s life cycle.
Tokens are needed by users in conventional blockchains such as Ethereum or Bitcoin to pay transaction costs, also called "gas fees". Faucets are frequently used to deliver modest quantities of tokens for free to new developers and users testing on these platforms, allowing them to execute smart contracts or complete transactions without paying any money.
But on ICP, a faucet is not necessary for the reasons listed below:
Cycles Instead of Gas: In ICP, the idea of "cycles" is utilized to cover the cost of computation and storage as opposed to gas charges. Developers buy cycles in advance to fuel their canisters (smart contracts).
To engage with Dapps installed on the Internet Computer, end users do not need to possess ICP tokens. Because the developers are already covering the operating costs of utilizing the dApp, this removes the need for faucets.
Token Economics: ICP tokens are primarily used for governance (voting on network proposals) and also used for converting into cycles (the unit used to power computation on the network).
The usage of cycles instead of conventional gas prices to fund computation reduces the need for faucets by reducing the reliance on giving users tiny amounts of ICP tokens
Self-Sustaining Dapps: In order to ensure that their canisters continue to operate, developers pre-purchase cycles. This implies that users won't have to worry about supplying the resources required for the dApp to run after it is released or requesting free tokens from a faucet.
The cycle-based paradigm guarantees that developers bear the infrastructure expenses by severing the transaction costs from end-user actions.
End-User-Friendly Model: Users can engage with decentralized applications using ICP without having to worry about paying transaction fees or managing tokens.
In addition to setting ICP apart from traditional blockchains and making it more efficient and accessible for developers as well as end users.
The End-User-Friendly model makes transactions much smoother and easier for users to use than on other Blockchains, where users must keep tokens in their wallets to pay for each transaction.
This guide will cover the following:
Setting up the Development Environment.
Understanding the Architecture.
Writing and Structuring Code.
Deploying on ICP.
Testing and Maintenance.
Conclusion.
Let's get started!
Setting up the Development Environment
Before diving into development, ensure your system is prepared with all the necessary tools and dependencies.
Install Rust
Rust is our main programming language, known for its memory safety and performance.
Visit Rust's Official site to download and install Rust.
After installation, verify it by running:
Install DFX
The DFX tool is essential for building, testing, and deploying applications on ICP.
Install DFX by following the official installation guide.
Verify the installation by running.
ICP SDK and Canister Development
DFX allows you to work with canisters, the core units of computation on ICP. You’ll use Canister SDK to deploy both backend and frontend components.
For further details, you can explore the ICP SDK repository.
Understanding the Architecture
A decentralized job board requires both a backend and frontend, but unlike traditional apps, both run on the blockchain through ICP canisters.
Backend (Rust-based Canister): Manages job data, including job postings, employers, and job details.
Frontend: Built with HTML/CSS/JavaScript and served via ICP canisters. It provides a user interface for adding and viewing job listings.
With ICP, the same application can handle both data and interface hosting, avoiding the need for third-party cloud services.
To understand more about the ICP architecture, visit the ICP Developer Documentation
Writing and Structuring Code
With the environment set up, you can start coding the job board application. Begin by setting up a new project using DFX:
Core Logic (Rust)
The backend will be written in Rust, allowing you to store job postings on the ICP network.
You will define a Job struct and write functions to:
Add a new job.
Retrieve all job listings.
These functionalities will be handled within a Rust canister that interacts with the ICP through the IC Canister SDK.
If you need to explore Rust tutorials or deepen your understanding of the language, check out:
- Rust Official Docs: link to site
Deploying on ICP
Once the backend logic and frontend interface are in place, you can deploy your job board canister onto ICP.
Local Deployment
Start by running your application locally to ensure everything is working:
Your canister will now be available for local testing. You can interact with it through the command line or a Frontend.
Deploying on ICP Mainnet
To deploy your job board on the ICP mainnet:
Set up your Internet Identity to authenticate users securely.
Update your dfx.json to include deployment settings.
Run the below deployment command:
Running the above command will generate a canister ID, which you can use to access your decentralized job board online.
You can read more about deployment steps in the official ICP Deployment Documentation.
Setting Up Dependencies
Add the following to the dependency session of cargo.toml found inside the backend folder.
serde: Provides serialization and deserialization capabilities for Rust structures.
candid: Facilitates encoding and decoding of data in the Candid format, used in ICP.
ic_cdk: Provides the core functionalities for interacting with the ICP environment.
ic_stable_structures: Supplies stable storage mechanisms for persisting data on ICP canisters.
Ic-cdk-timers
Importing Dependencies
Next, we import dependencies in the lib.rs file inside the src folder located inside the backend folder.
Below is the explanation of the imported dependencies
Serde: This crate provides the necessary macros and traits for serializing and deserializing Rust data structures.
Candid: Used for encoding and decoding data in the Candid format, which is essential for interactions with ICP.
ic_cdk::api::time: Provides access to time functions within the ICP environment.
ic_stable_structures: Offers stable data structures and memory management utilities for ICP canisters.
std::collections: Contains data structures like HashMap.
std::{borrow::Cow, cell::RefCell}: Provides utilities for memory management and mutable access within the canister.
ic_cdk::storage: A module for accessing the canister’s storage.
We Define the Memory State and ID-Counter
Memory and ID Types
The memory type alias represents the virtual memory managed by the DefaultMemoryImpl. It's used to handle stable memory in ICP canisters.
Also, the IdCell type alias is used for Cell<u64, Memory>. It represents a cell in stable memory used to store a u64 value, specifically for managing IDs.
The Thread-Local Storage for State Management
This code uses thread_local! to manage states within the canister. In this approach, we ensure that memory and state are properly managed across different threads and operations.
Below is the explanation of the above code:
MEMORY_MANAGER: A thread-local variable managing the virtual memory for the canister. It is initialized with DefaultMemoryImpl, providing a default implementation of memory management.
ID_COUNTER: A thread-local variable holding the ID counter for the canister. It uses the IdCell to store and manage the next available ID. MemoryId::new(0) is used to create a memory ID for this counter.
STORAGE: A thread-local variable for persistent storage using StableBTreeMap. This map stores job data with u64 keys and Job values. MemoryId::new(1) specifies a different memory ID for this storage.
Defining the Data Structures:
Job Struct
The Job struct represents a job posting and includes the following fields:
id: Unique identifier for the job.
title: Title of the job.
description: Description of the job.
created_at: Timestamp when the job was created.
applicant_name: List of names of applicants who applied for the job.
accepted_applicants: Optionally stores the name of the accepted applicant.
CreateJob Struct Function
The CreateJob struct is used to create new job postings, including:
title: Title of the job.
description: Description of the job.
JobStatus Enum Function
JobStatus is an enumeration to represent various statuses a job can have, such as:
AcceptJob
JobWithdrawn
JobCancelled
Defining the Canister Functions
Create Job function
The create_job function creates a new job posting. It increments the ID counter and stores the new job in the STORAGE map.
Apply to Job Function
The apply_to_job function allows users to apply for a job by appending their name to the applicant_name list for the specified job.
Withdraw Application Function
The withdraw_application function allows an applicant to withdraw their application by removing their name from the applicant_name list.
Cancel Job Function
The cancel_job function removes a job posting from the storage, effectively canceling it.
Accept Job Function
The accept_job function marks an applicant as accepted for a job, updating the accepted_applicants field.
Fetch Job Function
The fetch_job function retrieves a job posting by its ID.
Finally, we export the candid interface to the canister using the below command:
Testing and Maintenance
After deployment, it’s essential to test your application to ensure that it performs as expected.
Testing Locally
For testing your Rust backend locally, run the following command:
Testing the frontend can be done by opening the URL provided during local deployment. You can also copy and paste the URL into the browser of your choice. The ICP provides a unique URL where your Dapp will be hosted.
Upgrading the Canister
One of the significant benefits of building on ICP is the ease of upgrading your canisters. You can deploy updates without disturbing users.
To upgrade, modify the code and redeploy by running the following command:
Conclusion
In this tutorial, we’ve been through the fundamental stages of developing a decentralized job board application using Rust and deploying it on the Internet Computer Protocol (ICP).
We have developed a strong, safe, and censorship-resistant application that represents the future of decentralized platforms by utilizing the strength of Rust's performance, safety, and memory management skills alongside the scalability and decentralization offered by ICP.
During this process, we looked at several important ideas, including handling job posts, user applications, persistent data with stable storage, and maintaining stable data structures with ICP's canisters.