Tutorials

Create a smart rollup

Last Updated: 5th October 2023

This tutorial covers how to deploy a smart rollup in a Tezos sandbox. To run this tutorial, you should have a basic understanding of how Tezos works and the ability to use the command-line terminal on your computer. If you are new to Tezos, try the tutorial Deploy a smart contract first.

In this tutorial, you will learn:

  • What a smart rollup is and how they help scale Tezos
  • How information passes between Tezos and smart rollups
  • How to respond to messages from Tezos in a smart rollup
  • How to send messages from a smart rollup to Tezos

What is a smart rollup?

Smart rollups are processing units that run outside the Tezos network but communicate with Tezos on a regular basis. These processing units can run arbitrarily large amounts of code without waiting for Tezos baking nodes to run and verify that code. Smart rollups use Tezos for information and transactions but can run large applications at their own speed, independently of the Tezos baking system.

In this way, smart rollups allow Tezos to scale to support large, complex applications without slowing Tezos itself. The processing that runs on Tezos itself is referred to as layer 1 and the processing that smart rollups run is referred to as layer 2.

Smart rollups can run any kind of applications that they want, such as:

  • Financial applications that use information and transactions from Tezos
  • Gaming applications that manipulate assets and keep them in sync with Tezos
  • Applications that run complex logic on NFTs or other types of tokens
  • Applications that communicate with other blockchains

Smart rollups stay in sync with Tezos by passing messages to Tezos and receiving messages from Tezos and other rollups. Each Tezos block contains a global rollups inbox that contains messages from Tezos layer 1 to all rollups. Anyone can add a message to this inbox and all messages are visible to all rollups. Rollups receive this inbox, filter it to the messages that they are interested in, and act on them accordingly.

Rollups also have an outbox, which consists of calls to smart contracts on layer 1. These calls are how rollups send messages back to Tezos.

Rollups maintain consensus by publishing the hash of their state to Tezos, which other nodes can use to verify the rollup. The specific way that rollups publish their states and maintain consensus is beyond the scope of this tutorial. For more information about rollups and their consensus mechanism, see Smart Optimistic Rollups.

This diagram shows a smart rollup interacting with layer 1 by receiving a message, running processing based on that message, and sending a transaction to layer 1:

Smart rollup analogy

Businesses talk about horizontal scaling versus vertical scaling. If a business is growing and its employees are being overworked, the business could use vertical scaling to hire more employees or use better tools to improve the productivity of each employee. Scaling Tezos in this way would mean using more processing power to process each new block, which would increase the cost to run baking nodes. Also, if the business hires more employees, the amount of communication between employees increases because, for example, they have to make sure that they are working in the same way and not doing duplicate jobs.

By contrast, smart rollups behave like horizontal scaling. In horizontal scaling, businesses create specialized teams that work on different portions of the workload. These teams can work independently of other teams and take advantage of efficiencies of being focused on a specific task. They also need to communicate less with other teams, which speeds up their work. Smart rollups are like separate horizontally scaled teams, with Tezos layer 1 as the source of communication between teams.

Prerequisites

To run this tutorial, make sure that the following tools are installed:

  • Docker

  • Rust

    The application in this tutorial uses Rust because of its support for WebAssembly (WASM), the language that smart rollups use to communicate. Rollups can use any language that has WASM compilation support.

    To install Rust via the rustup command, run this command:

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    

    You can see other ways of installing Rust at https://www.rust-lang.org.

  • Clang and LLVM

    Clang and LLVM are required for compilation to WebAssembly. Version 11 or later of Clang is required. Here are instructions for installing the appropriate tools on different operating systems:

    MacOS

    brew install llvm
    export CC="$(brew --prefix llvm)/bin/clang"
    

    Ubuntu

    sudo apt-get install clang-11
    export CC=clang-11
    

    Fedora

    dnf install clang
    export CC=clang
    

    Arch Linux

    pacman -S clang
    export CC=clang
    

    The export CC command sets Clang as the default C/C++ compiler.

    After you run these commands, run CC --version to verify that you have version 11 or greater installed.

    Also, ensure that your version of Clang wasm32 target with by running the command CC -print-targets | grep WebAssembly and verifying that the results include wasm32.

  • AR (macOS only)

    To compile to WebAssembly on macOS, you need to use the LLVM archiver. If you've used Homebrew to install LLVM, you can configure it to use the archiver by running this command:

    export AR="$(brew --prefix llvm)/bin/llvm-ar"
    
  • WebAssembly Toolkit

    The the WebAssembly Toolkit (wabt) provides tooling for reducing (or stripping) the size of WebAssembly binaries (with the wasm-strip command) and conversion utilities between the textual and binary representations of WebAssembly (including the wat2wasm and wasm2wat commands).

    Most distributions ship a wabt package, which you can install with the appropriate command for your operating system:

    MacOS

    brew install wabt
    

    Ubuntu

    sudo apt install wabt
    

    Fedora

    dnf install wabt
    

    Arch Linux

    pacman -S wabt
    

    To verify that wabt is installed, run the command wasm-strip --version and verify that the version is at least 1.0.31. If not, you can download this version directly and extract its files: https://github.com/WebAssembly/wabt/releases/tag/1.0.31. Then, whenever you have to use wasm-strip, you can use .<path_to_wabt_1.0.31>/bin/wasm-strip instead.

Tutorial application

Despite the number of command-line tools needed, the code for the core of the rollup itself is relatively simple. This core is called the kernel and is responsible for accepting messages from layer 1 and sending messages to layer 1. It also updates its state to allow other rollup nodes to verify it.

The code for the tutorial application is here: https://gitlab.com/trili/hello-world-kernel.

The code for the kernel is in the src/lib.rs file. It is written in the Rust programming language and looks like this:

use tezos_smart_rollup::inbox::InboxMessage;
use tezos_smart_rollup::kernel_entry;
use tezos_smart_rollup::michelson::MichelsonBytes;
use tezos_smart_rollup::prelude::*;

kernel_entry!(hello_kernel);

fn handle_message(host: &mut impl Runtime, msg: impl AsRef<[u8]>) {
    if let Some((_, msg)) = InboxMessage::<MichelsonBytes>::parse(msg.as_ref()).ok() {
        debug_msg!(host, "Got message: {:?}\n", msg);
    }
}

pub fn hello_kernel(host: &mut impl Runtime) {
    debug_msg!(host, "Hello, kernel!\n");

    while let Some(msg) = host.read_input().unwrap() {
        handle_message(host, msg);
    }
}

This example kernel has these major parts:

  1. It imports resources that allow it to access and decode messages from layer 1.
  2. It runs the Rust macro kernel_entry! to set the main function for the kernel.
  3. It declares the handle_message function, which accepts, decodes, and processes messages from layer 1. In this case, the function decodes the message (which is sent as a sequence of bytes) and prints it to the log. The function could call any other logic that the application needs to run.
  4. It declares the hello_kernel function, which prints a logging message each time it is called and then runs the handle_message function on each message from layer 1.

You don't need to access the other files in the application directly, but here are descriptions of them:

  • src/lib.rs: The Rust code for the kernel
  • Cargo.toml: The dependencies for the build process
  • rustup-toolchain.toml: The required Rust version
  • sandbox_node.sh: A script that sets up a Tezos sandbox for testing the rollup

The tutorial repository also includes two files that represent example message inboxes in layer 1 blocks:

  • empty_input.json: An empty rollup message inbox
  • two_inputs.json: A rollup message inbox with two messages

Part 1: Configure the tutorial application

Follow these steps to get the application code and build it:

  1. Clone the repository with the tutorial application:

    git clone https://gitlab.com/trili/hello-world-kernel.git
    cd hello-world-kernel/
    
  2. Configure Rust to build WebAssembly applications:

    1. Verify that you have Rust version 1.66.0 or later installed by running cargo --version.

    2. If you have an earlier version of Rust, use the rustup command to use version 1.66.0:

      rustup override set 1.66.0
      
    3. Add WASM as a compilation target for Rust by running this command:

      rustup target add wasm32-unknown-unknown
      
  3. Build the application by running the command cargo build --target wasm32-unknown-unknown.

    If the application built correctly, the terminal shows a message that looks like "Finished dev [unoptimized + debuginfo] target(s) in 0.44s." You can see the compiled application in the target/wasm32-unknown-unknown/debug folder. In particular, the compiled kernel is in the hello_world_kernel.wasm file.

Part 2: Start a Tezos sandbox

Follow these steps to set up a Tezos sandbox that you can use to test sending messages to the smart rollup. These steps use the Octez command-line client to set up a sandbox in a Docker container:

  1. Pull the most recent Tezos Docker image, which contains the most recent version of Octez:

    docker pull tezos/tezos:master
    

    You can also install Octez directly on your system, but keeping it in Docker is faster and more convenient for running the tutorial application.

  2. Make sure you are in the hello-world-kernel folder, at the same level as the Cargo.toml and sandbox_node.sh files.

  3. Run this command to start the Docker image, open a command-line terminal in that image, and mount the hello-world-kernel folder in it:

    docker run -it --rm --volume $(pwd):/home/tezos/hello-world-kernel --entrypoint /bin/sh --name octez-container tezos/tezos:master
    

    Your command-line prompt changes to indicate that it is now inside the running Docker container. This image includes the Octez command-line client and other Tezos tools. It also uses the docker --volume argument to mount the contents of the hello-world-kernel folder in the container so you can use those files within the container.

  4. Verify that the container has the necessary tools by running these commands:

    octez-node --version
    octez-smart-rollup-wasm-debugger --version
    octez-smart-rollup-node-alpha --version
    octez-client --version
    

    Each of these commands should print a version number. The specific version number is not important as long as you retrieved the latest image with the docker pull tezos/tezos:master command.

    Don't close this terminal window or exit the Docker terminal session, because Docker will close the container. If you accidentally close the container, you can run the docker run -it --rm --volume $(pwd):/home/tezos/hello-world-kernel --entrypoint /bin/sh --name octez-container tezos/tezos:master again to restart it.

  5. Within the container, go to the hello-world-kernel folder:

    cd hello-world-kernel
    
  6. Also inside the container, start the Tezos sandbox by running this command:

    ./sandbox_node.sh
    

    This command starts a Tezos testing environment, including a baking node running in sandbox mode and a group of test accounts. The console shows repeated messages that show that the node is baking blocks. For more information about sandbox mode, see sandbox mode.

    If you see an error that says "Unable to connect to the node," you can ignore it because it happens only once while the node is starting.

  7. Leave that terminal instance running for the rest of the tutorial.

  8. Open a new terminal window.

  9. In the new terminal window, enter the Docker container by running this command:

    docker exec -it octez-container /bin/sh
    

    Now the second terminal window is running inside the container just like the first one.

  10. In the second terminal window, run this command to verify that the sandbox is running with the correct protocol:

    octez-client rpc get /chains/main/blocks/head/metadata | grep protocol
    

    The response shows the protocol that the sandbox is running, as in this example:

    { "protocol": "ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK",
      "next_protocol": "ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK",
    

    If you don't see a message that looks like this one, check for errors in the first terminal window.

4. Processing the Kernel

4.1. Debugging the Kernel

Before originating a rollup, it can be helpful to observe the behavior of its kernel. To facilitate this, there is a dedicated Octez binary called octez-smart-rollup-wasm-debugger. However, before using it, it is important to understand how the rollup receives its inputs. Each block at every level of the blockchain has a specific section dedicated to the shared and unique smart rollup inbox. Consequently, the inputs of a rollup can be seen as a list of inboxes for each level, or more precisely, a list of lists. Let us start with a trivial inbox, which is stored in the empty_input.json file. We can debug the "Hello, World!" kernel with:

docker session 1

cd hello-world-kernel

docker session 1

octez-smart-rollup-wasm-debugger --kernel target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm --inputs empty_input.json

Now you are in debugging mode, which is very well documented and explained in the documentation. Similar to how the rollup awaits internal messages from Layer 1 or external sources, the debugger also waits for inputs.

Once we're in the debugger REPL (read–eval–print loop), you can run the kernel for one level using the step inbox command:

docker session 1

> step inbox
# Loaded 0 inputs at level 0
# Hello, kernel!
# Got message: Internal(StartOfLevel)!
# Got message: Internal(InfoPerLevel(InfoPerLevel { predecessor_timestamp: 1970-01-01T00:00:00Z, predecessor: BlockHash("BKiHLREqU3JkXfzEDYAkmmfX48gBDtYhMrpA98s7Aq4SzbUAB6M") }))!
# Got message: Internal(EndOfLevel)!
# Evaluation took 11000000000 ticks so far
# Status: Waiting for input
# Internal_status: Collect

Let us explain what our kernel is supposed to do:

  • whenever it receives an input, it prints the "Hello, kernel!" message.
  • whenever there is a message in the input, it is printed, because of the handle_message function.

It is important to understand that the shared rollup inbox has at each level at least the following internal messages:

  • StartOfLevel -- marks the beginning of the inbox level and does not have any payload.
  • InfoPerLevel -- provides the timestamp and block hash of the predecessor of the current Tezos block as payload.
  • EndOfLevel -- pushed after the application of the operations of the Tezos block and does not have any payload.

You will notice that the behavior aligns with the expectations. You can also experiment with a non-empty input, such as two_inputs.json:

docker session 1

octez-smart-rollup-wasm-debugger --kernel target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm --inputs two_inputs.json

docker session 1

> step inbox
# Loaded 2 inputs at level 0
# Hello, kernel!
# Got message: Internal(StartOfLevel)
# Got message: Internal(InfoPerLevel(InfoPerLevel { predecessor_timestamp: 1970-01-01T00:00:00Z, predecessor: BlockHash("BKiHLREqU3JkXfzEDYAkmmfX48gBDtYhMrpA98s7Aq4SzbUAB6M") }))
# Got message: External([26, 84, 104, 105, 115, 32, 109, 101, 115, 115, 97, 103, 101, 32, 105, 115, 32, 102, 111, 114, 32, 109, 101])
# Got message: External([5, 84, 104, 105, 115, 32, 111, 110, 101, 32, 105, 115, 110, 39, 116])
# Got message: Internal(EndOfLevel)
# Evaluation took 11000000000 ticks so far
# Status: Waiting for input
# Internal_status: Collect

As expected, the two messages from the input are also displayed as debug messages. Feel free to explore additional examples from the dedicated kernel gallery or create your own!

4.2. Reducing the Size of the Kernel

The origination process is similar to that of smart contracts. To originate a smart rollup, we have to consider the size of the kernel that will be deployed. The size of the kernel needs to be smaller than the manager operation size limit.

Regrettably, the size of the .wasm file is currently too large:

docker session 1

du -h target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm 
# 17.3M target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm

To address this, we can use wasm-strip, a tool designed to reduce the size of kernels. It accomplishes this by removing unused parts of the WebAssembly module (e.g. dead code), which are not required for the execution of the rollups. Open a new terminal session and navigate to the "Hello, world!" kernel directory since you do not have wasm-strip in your Docker session:

outside docker session - hello-world-kernel

wasm-strip target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm 

du -h target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm
# 532.0K target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm

The modifications from outside will get propagated to the interactive Docker session thanks to the --volume command option.

Undoubtedly, this process has effectively reduced the size of the kernel. However, there is still additional work required to ensure compliance with the manager operation size limit.

4.3. The Installer Kernel

Instead of using a kernel file for origination in the aforementioned format, an alternative approach is to utilize the installer version of the kernel. This installer kernel can be upgraded to the original version if provided with additional information in the form of preimages, which can be provided to the rollup node later on as part of its reveal data channel.

There are two ways to communicate with smart rollups:

  1. global inbox -- allows Layer 1 to transmit information to all rollups. This unique inbox contains two kinds of messages: external messages are pushed through a Layer 1 manager operation, while internal messages are pushed by Layer 1 smart contracts or the protocol itself (e.g. StartOfLevel, InfoPerLevel, EndOfLevel).
  2. reveal data channel -- allows the rollup to retrieve data (e.g. preimages) coming from data sources external to Layer 1.

The main benefit of the installer kernel is that it is small enough to be used in origination regardless of the kernel that it will be upgraded to.

There is an installer kernel origination topic for this; please consult it for further clarifications. To generate the installer kernel, the smart-rollup-installer tool is required:

outside docker session - hello-world-kernel

cargo install tezos-smart-rollup-installer

To create the installer kernel from the initial kernel:

outside docker session - hello-world-kernel

smart-rollup-installer get-reveal-installer --upgrade-to target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm --output hello_world_kernel_installer.hex --preimages-dir preimages/

This command creates the following:

  • hello_world_kernel_installer.hex -- the hexadecimal representation of the installer kernel to be used in the origination.
  • preimages/ -- a directory containing the preimages necessary for upgrading from the installer kernel to the original kernel. These preimages are transmitted to the rollup node that runs the installer kernel with the help of the reveal data channel.

Notice the reduced dimensions of the installer kernel:

outside docker session - hello-world-kernel

du -h hello_world_kernel_installer.hex
# 36.0K hello_world_kernel_installer.hex

Because of the size of this installer kernel, you are now ready for deployment.

Note that this shows the size of the hex encoded file, which is larger than the actual binary size of the kernel that we originate.

5. Deploying the Kernel

5.1. Sandboxed Mode

Our goal now is to create a testing environment for originating our rollup with the created kernel. In the hello-world-kernel repository, we offer the sandbox-node.sh file, which does the following:

  • configures the Octez node to operate in sandbox mode.
  • activates the alpha protocol by using an activator account.
  • creates five test (bootstrapping) accounts used for manual baking.
  • creates a loop of continuous baking.

Run the file with:

docker session 1

./sandbox_node.sh

Ignore the "Unable to connect to the node" error, as it only comes once because the octez-client command was used while the node was not yet bootstrapped. The result should be a permanent loop containing:

docker session 1

# Injected block at minimal timestamp

Leave that process running. Open a new Docker session, which works in the same container named octez-container:

outside docker session - hello-world-kernel

docker exec -it octez-container /bin/sh

It is very important to remember to open a new terminal session and run the command above whenever we mention a "new Docker session" or when you see that the docker session counter has increased.

To check that the network has the correctly configured protocol:

docker session 2

octez-client rpc get /chains/main/blocks/head/metadata | grep protocol

# "protocol": "ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK",
# "next_protocol": "ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK"

You are now ready for the Smart Rollup origination process.

5.2. Smart Rollup Origination

To originate a smart rollup using the hello_world_kernel_installer created above:

docker session 2

octez-client originate smart rollup "test_smart_rollup" from "bootstrap1" of kind wasm_2_0_0 of type bytes with kernel file:hello-world-kernel/hello_world_kernel_installer.hex --burn-cap 3
# > Node is bootstrapped.
# ...
# Smart rollup sr1B8HjmEaQ1sawZtnPU3YNEkYZavkv54M4z memorized as "test_smart_rollup"

In the command above, the --burn-cap option specifies the amount of ꜩ you are willing to "burn" (lose) to allocate storage in the global context of the blockchain for each rollup.

To run a rollup node for the rollup using the installer kernel, you need to copy the contents of the preimages directory to ${ROLLUP_NODE_DIR}/wasm_2_0_0/. You can set $ROLLUP_NODE_DIR to ~/.tezos-rollup-node, for instance:

docker session 2

mkdir -p ~/.tezos-rollup-node/wasm_2_0_0

cp hello-world-kernel/preimages/* ~/.tezos-rollup-node/wasm_2_0_0/

You should now be able to run your rollup node:

docker session 2

octez-smart-rollup-node-alpha run operator for "test_smart_rollup" with operators "bootstrap2" --data-dir ~/.tezos-rollup-node/ --log-kernel-debug --log-kernel-debug-file hello_kernel.debug

Leave this running as well, and open another Docker session, as already explained, with the octez-container.

Each time a block is baked, a new "Hello, kernel!" message should appear in the hello_kernel.debug file:

docker session 3

tail -f hello_kernel.debug 
# Hello, kernel!
# Got message: Internal(StartOfLevel)
# Got message: Internal(InfoPerLevel(InfoPerLevel { predecessor_timestamp: 2023-06-07T15:31:09Z, predecessor: BlockHash("BLQucC2rFyNhoeW4tuh1zS1g6H6ukzs2DQDUYArWNALGr6g2Jdq") }))
# Got message: Internal(EndOfLevel)
# ... (repeats)

Finally, you have successfully deployed a very basic yet functional smart rollup.

5.3. Sending an Inbox Message to the Smart Rollup

We now want to send an external message into the rollup inbox, which should be read by our kernel and sent as a debug message. First, we will wait for it to appear using:

docker session 3

tail -f hello_kernel.debug | grep External

Open yet another Docker session and send an external message into the rollup inbox. You can use the Octez client:

docker session 4

octez-client send smart rollup message '[ "test" ]' from "bootstrap3"

Once you send the Smart Rollup message, you will notice that in the debug trace, you get:

docker session 3

Got message: External([116, 101, 115, 116])

116, 101, 115, 116 represent the bytes of "test".

5.4. Test Networks

In the above section, we proposed how to create your Octez binaries in sandbox mode. Here, we propose a different approach to that, using test networks. We encourage the reader to try at least one of the following linked tutorials:

  • Ghostnet -- uses the protocol that Mainnet follows as well.
  • Nairobinet -- uses the Nairobi protocol.
  • Mondaynet -- uses the alpha protocol and resets every Monday.

The workflow should be similar to the one presented for the sandbox mode:

  • configure the network;
  • run a node (needs to synchronize with the network -- can make use of snapshots);
  • create test accounts (which should be funded by the appropriate Faucet);
  • originate the rollup;
  • run the rollup node;
  • check the debug file.

6. Further References and Documentation

  1. Smart Rollup Documentation
  2. Smart Rollup Kernel SDK Tutorial
  3. Smart Rollup Kernel Examples
  4. Ghostnet Indexer
  5. Blockchain Explorer
  6. Tezos Smart Rollups Resources
  7. Tezos Testnets
  8. Origination of the Installer Kernel
  9. Docker Documentation
Previous
Adding and removing liquidity