Building a Decentralized Job Board Application on ICP with Rust: 
A Step-by-Step Guide

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.

  1. 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.

  1. 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.

  1. 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.

  1. 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:

  1. Setting up the Development Environment.

  2. Understanding the Architecture.

  3. Writing and Structuring Code.

  4. Deploying on ICP.

  5. Testing and Maintenance.

  6. 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.

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:

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.