Skip to main content

0.4 Introduction to canisters

Beginner
Tutorial

Smart contracts on the Internet Computer are known as canisters. A canister contains both the source code and software state. A canister's source code is compiled into a WebAssembly module and is associated with a module of stable memory.

When a dapp is written to be deployed on ICP, the source code is compiled into a WebAssembly module. Then, that WebAssembly module is deployed and executed inside of the canister. Once a canister is deployed, end-users can interact with the canister through the CLI or through a frontend client such as a web browser.

Architecture

The concept of a canister is similar to that of a container (such as a Docker container): both are deployed as a unit of software that contains both the dependencies for an application or service and the compiled code.

A canister differs from a container, however, in the fact that a canister also stores information about the current software state. A container may include information about the state of the environment in which it runs, but a canister is able to persist a record of state changes that have resulted from the software's functions being called.

Languages

Canisters can be developed in a variety of existing languages, such as Rust, JavaScript, Python, and TypeScript. There is also an SDK for Motoko, a language specifically for canister development on the Internet Computer with the focus on programming in a distributed asynchronous environment.

You'll dive further into Motoko and other languages in the next section, introduction to languages.

Actors

An actor is an object that processes messages within an isolated state, which enables messages to be handled remotely and asynchronously. Motoko uses an actor-based programming model.

Each canister includes the compiled code for one actor and may also include additional information such as interface descriptions and frontend assets. Many projects contain multiple canisters, but each canister can only contain one actor.

Why code is compiled into WebAssembly

WebAssembly is a low-level computer instruction format. It abstracts a program's execution cleanly over most modern hardware. WebAssembly is portable and broadly supported for programs that run on the internet, making it a natural fit for dapps intended to run on ICP.

Types of canisters

  • Backend canisters: The backend of an application is the portion that does not come in direct contact with the users. It is hosts the application's primary source code and functionality, and does not contain user interface assets such as HTML or CSS. As the name suggests, backend canisters are canisters that contain a dapp's backend code. This code typically includes the dapp's functionality. When a project is created with dfx, the file structure for a default backend canister is created within the project's directory.
  • Frontend canisters: The frontend of an application is the portion that is used to interact with the application's functions using a user interface. Frontend canisters contain the files and assets used to create the frontend interface used by end-users. These assets typically contain things such as CSS, HTML, JavaScript, or React elements. Since the frontend canister contains assets, it is often also referred to as the asset canister. When a project is created with dfx, the file structure for a default frontend canister is created within the project's directory.
  • Custom canisters: Custom canisters are canisters that don't fit into the frontend or backend canister type definitions. These may include a mix of backend and frontend functionalities, or they may be used for specific, individual functionalities within the dapp.

Using a single or multiple canister architecture

When designing a dapp, one of the first decisions you should make is how to structure your dapp. Should it be within a single canister or should it consist of multiple canisters?

If you're developing a simple service-based app that doesn't include a frontend interface, a single canister might be a good choice to simplify project management and maintenance.

If your dapp is intended to have both frontend assets and backend logic, your dapp should contain at least two canisters. This structure is the default structure that is generated by dfx when a new project is created.

It also may be beneficial to separate different reusable services into their own canisters so that they can be imported and called from other canisters, or be made available to other developers. For example, a dapp that provides a social media platform might split the backend functions into two canisters: one that contains the code used to establish social connections and one contains the code that is used to set up user profiles. Additionally, a third canister may be added that provides functionality to schedule social events or create user groups.

Canister communication

Canisters communicate with other canisters through the use of asynchronous messages. Each message is also executed in isolation, which allows for increased levels of concurrent execution. Canister messages are either requests or replies to other messages. When a canister processes a message, the result of that process could be a change to the canister's state, a reply message sent to another canister, or even the creation of a new canister.

If a canister processes a request that requires the canister to send additional requests to other canisters, the canister may wait for the replies from other canisters before producing a reply to the original request. If a canister fails to respond (referred to as 'trapping'), the canister's state is rolled back to the point right after it made the last outgoing request.

Canisters can also communicate with external entities, such as end-users or external services, through update calls and query calls. Update calls can modify the state of the canister, while query calls cannot. Updates are used to write changes to the state of the canister, and queries are used to read information from the canister's state.

Canister controllers

Canisters are managed by controllers, which may be a centralized entity, decentralized entity, or they may have no controller at all. Controllers can be a single user, a group of users, or another canister. If a canister has no controller, it is an immutable smart contract. A canister can have multiple controllers.

Controllers are responsible for deploying and maintaining the canister that it is a controller of. A controller is the only entity that has permission to update and manage the canister through workflows like deploying the canister to the mainnet or starting and stopping the canister. Controllers can also change the canister's parameters, add or remove additional controllers, or delete the canister.

Additionally, controllers can update the canister code by submitting a new Wasm module to replace the current, existing module. By default, when the Wasm module of a canister is updated, the canister clears out the Wasm's memory, but the content of the canister's stable memory remains the same. On ICP, the upgrade mechanism includes three actions that are atomically executed. These are the serializing of the Wasm memory and writing it to the stable memory, installing the new Wasm code, then deserializing the content of the stable memory. It is good practice that any data that needs to be persisted across upgrades should be stored in stable memory.

Cycles and resource charges

A canister's controller is responsible for ensuring the canister contains enough cycles. Cycles are used to pay for the canister's resources, such as memory, computational power, and network bandwidth. Each operation that is performed by a canister on the mainnet has a cost of cycles. A canister has a local cycles account that is used to store the canister's cycles.

For memory usage, the system keeps track of all memory used by the canister and regularly charges the canister's cycles account. This charging happens at regular intervals for efficiency.

For computational power, cycles are charged at the time computation is performed. Each canister contains instrumental code that allows ICP to count the number of instructions executed during the processing of a message. Each round, there is a limit on the number of executions that can be performed during that round. If that number is exceeded, the execution is paused and continued in the following round. Cycles for the computation are charged at the end of the round. For security and efficiency reasons, there is a limit on the total number of rounds the execution can use.

For network bandwidth, cycles are charged at the time of usage. When a canister goes to send a request to another canister, the system automatically calculates the total number of cycles that sending the message will cost. This cost consists of a fixed component and a component that varies based on the size of the message's payload. This cost is then deducted from the canister's cycles account. A charge is also deducted for sending a maximum sized reply to a callee, since for inter-canister messages, the caller pays for the reply. Any cost difference between the maximum size and the actual size of the reply are refunded to the canister when the reply arrives.

If a canister runs out of cycles, the canister is uninstalled. The code and state are deleted, but the remainder of the canister's information remains. To avoid unexpected deletion, canisters have a 'freezing threshold'. If a canister's balance dips below this threshold, then the canister will stop processing any new requests. Replies will still be processed. The system will throw an error if the canister attempts to perform any action that would result in the canister's cycles balance dipping below the freezing threshold.

Need help?

Did you get stuck somewhere in this tutorial, or feel like you need additional help understanding some of the concepts? The ICP community has several resources available for developers, like working groups and bootcamps, along with our Discord community, forum, and events such as hackathons. Here are a few to check out:

Next steps