Rock-n-rollup

Wow, you find this framework, that means you are intrigued with this awesome framework.

Let me introduce this framework for you.

Rock-n-rollup is an opiniated framework to develop smart rollups on Tezos. The goal of this framework is to ease the development of smart rollups for Rust developers. That's why this framework looks like actix/axum/bevy. We decided to use the "dependency injection at runtime pattern".

When you develop a webserver, you will develop what you call endpoint or routes, for smart rollup it's the same way but it's named transition.

In addition, this framework comes with many useful plugins to help you implement your deepest desires.

Finnaly, this framework also comes with tooling to setup/deploy your rollup.

It can happen we don't share the same opinion. If so, please have a look to this book. If you are convinced this framework is not what you are looking for, please use the official Tezos library.

Basics

rock-n-rollup uses the same semantic as major libraries of the Rust ecosystem. You can register transitions to the application (as handler for actix, or system for bevy, or route for axum, etc...).

This transition can take any parameters that implement the trait FromInput.

In this section, you will learn and discover how to write custom transitions to fullfill your needs.

Installing Rust

If you don't have Rust yet, we recommend you use rustup to manage your Rust installation. The official rust guide has a wonderful section on getting started.

Rock-N-Rollup currently has a minimum supported Rust version of 1.66. Running rustup default 1.66 will ensure you have the correct version of Rust. As such, this guide assumes you are running Rust 1.66.

To develop smart rollups on Tezos, you will also need to compile your Rust code to Wasm. To compile to Wasm just add the wasm32-unknown-unknown as a new target: rustup target add wasm32-unknown-unknown.

Hello kernel!

Start by creating a new library based Cargo project and changing into the new directory:

cargo new hello-kernel --lib
cd hello-kernel

Add a [lib] section to your Cargo.toml:

[lib]
crate-type = ["rlib", "cdylib"]

Add rock-n-rollup as a dependency of your project in Cargo.toml file.

[dependencies]
rock-n-rollup = "0.0.5"

Transition functions accept zero or more parameters. These parameters can be extracted from an input (see FromInput trait) and returns void.

Now let's start the kernel by replace the contents of src/lib.rs with the following:

extern crate rock_n_rollup;
use rock_n_rollup::core::Runtime;

fn hello<R: Runtime>(rt: &mut R) {
    rt.write_debug("Hello kernel!");
}
fn main(){}

Next, create a kernel_entry function, that accept an Application as parameter. Use application.register to add a transition to your application. Finally, the application is started by calling run on it.

extern crate rock_n_rollup;
use rock_n_rollup::core::Application;
use rock_n_rollup::core::Runtime;

fn hello<R: Runtime>(rt: &mut R) {
    rt.write_debug("Hello kernel!");
}

#[rock_n_rollup::main]
pub fn kernel_entry<R: Runtime>(application: &mut Application<R>) {
    application.register(hello).run();
}
fn main(){}

That's it! It should compile with cargo build --release --target wasm32-unknown-unknown

Internal Messages

Smart rollups always inject 4 messages in the inbox:

  • StartOfLevel that indicates the beginning of the inbox.
  • InfoPerLevel that gives you the current Tezos level of this execution.
  • EndOfLevel that indicates the end of the inbox.
  • Transfer that takes a transfer.

An easy way to execute a transition of this kind of message is to add a parameter to your transition function:

extern crate rock_n_rollup;
use rock_n_rollup::services::internal::*;
use rock_n_rollup::core::Runtime;

fn start_of_level<R: Runtime>(rt: &mut R, msg: Internal<StartOfLevel>) {
    // Only executed on StartOfLevel message
    // ...
}

fn info_per_level<R: Runtime>(rt: &mut R, msg: Internal<InfoPerLevel>) {
    // Only executed on InfoPerLevel message
    // ...
}

fn end_of_level<R: Runtime>(rt: &mut R, msg: Internal<EndOfLevel>) {
    // Only executed on EndOfLevel
    // ...
}

fn transfer<R: Runtime>(rt: &mut R, msg: Internal<Transfer<Vec<u8>>>){
    // Only excuted on Transfer
    // ...
}
fn main(){}

External message

Message from users can come from the add_rollup_message Tezos operation. This message will be added to the inbox as an external message.

If you want to trigger a transition on a External message, you can add a parameter to your transition function as follow:

extern crate rock_n_rollup;
use rock_n_rollup::services::external::*;
use rock_n_rollup::core::Runtime;

fn transition<R: Runtime>(rt: &mut R, msg: External<Vec<u8>>) {
    // Only executed on external messages where the payload can be parsed as bytes
    // External<Vec<u8>> will match on any messages
    // ...
}
fn main(){}

At this point, you get all the features as the already provided kernel library provided by Tezos code dev. You can define transitions on any messages.

Reading custom external messages

If you want to define your custom external message, it's easy: First let's define a message type PingPong as an example:

enum PingPong {
    Ping,
    Pong
}
fn main(){}

The next step is to implement the FromExternal trait:

extern crate rock_n_rollup;
use rock_n_rollup::services::external::*;
enum PingPong {
   Ping,
   Pong
}

impl FromExternal for PingPong {
    fn from_external(input: Vec<u8>) -> Result<Self, ()> {
        // Notice that the magic byte is not there
        match input.as_slice() {
            [0x00] => Ok(PingPong::Ping),
            [0x01] => Ok(PingPong::Pong),
            _ => Err(())
        }
    }
}
fn main(){}

Then you can use the PingPong type in your transition:

extern crate rock_n_rollup;
use rock_n_rollup::core::Runtime;
use rock_n_rollup::services::external::*;

enum PingPong {
    Ping,
    Pong
}

impl FromExternal for PingPong {
   fn from_external(input: Vec<u8>) -> Result<Self, ()> {
       // Notice that the magic byte is not there
       match input.as_slice() {
           [0x00] => Ok(PingPong::Ping),
           [0x01] => Ok(PingPong::Pong),
           _ => Err(())
       }
   }
}

fn transition<R: Runtime>(rt: &mut R, ping_pong: External<PingPong>) {
    // process your stuff
}
fn main(){}

Your transition will be executed when the payload will be parsed: when there is an external message containing only 0x00 or only 0x01.

extern crate rock_n_rollup;
use rock_n_rollup::core::{Runtime, Application};
use rock_n_rollup::services::external::*;

enum PingPong {
    Ping,
    Pong
}

impl FromExternal for PingPong {
   fn from_external(input: Vec<u8>) -> Result<Self, ()> {
       // Notice that the magic byte is not there
       match input.as_slice() {
           [0x00] => Ok(PingPong::Ping),
           [0x01] => Ok(PingPong::Pong),
           _ => Err(())
       }
   }
}

fn external_transition<R: Runtime>(rt: &mut R, ping_pong: External<PingPong>) {
   // process your stuff
}

#[rock_n_rollup::main]
pub fn kernel_entry<R: Runtime>(application: &mut Application<R>) {
    application
        .register(external_transition)
        .run();
}
fn main(){}

Reading custom transfer message

An internal input message can also be a Transfer message with a Michelson payload. Let's say you want to receive some bytes ticket:

extern crate rock_n_rollup;
use rock_n_rollup::core::Runtime;
use rock_n_rollup::services::internal::*;
use rock_n_rollup::core::michelson::*;
use rock_n_rollup::core::michelson::ticket::*;

fn transfer<R: Runtime>(rt: &mut R, msg: Internal<Transfer<Ticket<MichelsonBytes>>>) {
    let transfer = msg.payload();
    let ticket = transfer.payload();
    let destination = transfer.destination();
    let source = transfer.source();
    let sender = transfer.sender();
}
fn main(){}

Your transition will be executed when the payload is a transfer of byte tickets:

extern crate rock_n_rollup;
use rock_n_rollup::core::{Runtime, Application};
use rock_n_rollup::services::internal::*;
use rock_n_rollup::core::michelson::*;
use rock_n_rollup::core::michelson::ticket::*;

fn transfer<R: Runtime>(rt: &mut R, msg: Internal<Transfer<Ticket<MichelsonBytes>>>) {
    let transfer = msg.payload();
    let ticket = transfer.payload();
    let destination = transfer.destination();
    let source = transfer.source();
    let sender = transfer.sender();
}

#[rock_n_rollup::main]
pub fn kernel_entry<R: Runtime>(application: &mut Application<R>) {
    application
        .register(transfer)
        .run();
}
fn main(){}

Plugins

Rock-N-Rollup provides many plugins to ease your developement.

First question, what is a plugin? A plugin is an augmented runtime. Basically a plugin is a superset of the runtime.

How to use plugins?

A plugin is a trait that is implemented for any runtime. So when you want to use the plugin you can add a constraint on the Runtime:

extern crate rock_n_rollup;
use rock_n_rollup::core::Runtime;
use rock_n_rollup::plugins::logger::Logger;

fn transition<R: Runtime + Logger>(rt: &mut R) {
    rt.log("Hello kernel");
}
fn main() {}

If you don't care of the Runtime, you can restrict your function to your plugin and only your plugin:

extern crate rock_n_rollup;
use rock_n_rollup::plugins::logger::Logger;

fn transition<R: Logger>(rt: &mut R) {
    rt.log("Hello kernel");
}
fn main() {}

And of course, you can compose plugins with each other

extern crate rock_n_rollup;
use rock_n_rollup::plugins::logger::Logger;
use rock_n_rollup::plugins::hasher::Hasher;


fn transition<R: Logger + Hasher>(rt: &mut R) {
    let data = "Hello world";
    rt.info("Hello world");
    let hash = rt.hash(data.as_bytes());
}
fn main() {}

Unfortunately there is one limitation, it's complicated to use two plugins that exposes the same feature.

How to develop a plugin?

Let's say you want to implement your custom plugin, you can implement it in 3 steps:

  1. Define a trait

This trait can define any function. Let's implement the Identity plugin: it returns the given parameter.

trait Identity {
    fn identity<P>(&mut self, param: P) -> P;
}
fn main(){}
  1. Implement this trait for any Runtime
extern crate rock_n_rollup;
use rock_n_rollup::core::Runtime;
trait Identity {
   fn identity<P>(&mut self, param: P) -> P;
}

impl <R> Identity for R
where
    R: Runtime
{
    fn identity<P>(&mut self, param: P) -> P {
        param
    }
}
fn main(){}

Tips, you can also compose plugins

extern crate rock_n_rollup;
use rock_n_rollup::core::Runtime;
use rock_n_rollup::plugins::logger::Logger;
trait Identity {
   fn identity<P>(&mut self, param: P) -> P;
}

impl <R> Identity for R
where
    R: Runtime + Logger
{
    fn identity<P>(&mut self, param: P) -> P {
        self.log("Hello identity plugin");
        param
    }
}
fn main(){}
  1. Let's write some tests

Because you have implemented your trait for all Runtime, you can directly use the MockRuntime to test your plugin.

extern crate rock_n_rollup;
use rock_n_rollup::core::Runtime;
use rock_n_rollup::plugins::logger::Logger;
trait Identity {
   fn identity<P>(&mut self, param: P) -> P;
}
impl <R> Identity for R
where
   R: Runtime + Logger
{
   fn identity<P>(&mut self, param: P) -> P {
       self.log("Hello identity plugin");
       param
   }
}
use rock_n_rollup::core::MockRuntime;

fn test() {
    let mut runtime = MockRuntime::default();
    let param = runtime.identity(32);
    assert_eq!(param, 32)
}
fn main(){}

Logger

The Logger plugin is a simple plugin. It adds to the runtime 5 functions:

  • log to print normal log
  • info to print log with info level
  • warn to print log with warn level
  • err to print log with error level

How to use the Logger plugin

Let's say you have a transition. If you want to use the logger, you just add to add the Logger trait to the Runtime constraint:

extern crate rock_n_rollup;
use rock_n_rollup::plugins::logger::Logger;

fn transition<R: Logger>(rt: &mut R) {
    rt.log("Normal log with \n at the end");
    rt.info("The log will start by [INFO]");
    rt.warn("The log will start by [WARN]");
    rt.err("The log will start by [ERR]");
}
fn main(){}

Hasher

The Hasher plugin gives you access to blake2b hashing algorithm with the following function:

  • hash to hash any data with Blake2b 256 bits
  • hash_512 to hash any data with Blake2b 512 bits

How to use the Hasher plugin

Let's say you have a transition. If you want to use the hasher, you just add to add the Hasher trait to the Runtime constraint:

extern crate rock_n_rollup;
use rock_n_rollup::plugins::hasher::Hasher;

fn transition<R: Hasher>(rt: &mut R) {
    let data = b"Hello world";
    let hash = rt.hash(data);
    let string: String = hash.to_string();
}
fn main(){}

Cryptography

The Crypto plugin gives you access to cryptography primitive, like public key and signature verification.

New types

It defines the PublicKey and the Signature types. These types can be constructed from a String:

extern crate rock_n_rollup;
use rock_n_rollup::plugins::crypto::*;

fn my_function() {
    let pkey = "edpkuDMUm7Y53wp4gxeLBXuiAhXZrLn8XB1R83ksvvesH8Lp8bmCfK".to_string();
    let pkey = PublicKey::try_from(pkey).unwrap();

    let signature = "edsigtuU5nUqBniorqTFXFixkG6ZkfvEPrfc9aT9DnMAeims2AX2yjpgYaedXBoKzAGHE3ZXSi1hZz6piZ3itTE7f2F4FoaxXtM".to_string();
    let signature = Signature::try_from(signature).unwrap();
}
fn main(){}

How to use the Crypto plugin

Let's say you have a transition. If you want to use the crypto plugin, you just have to add the Crypto trait to the Runtime constraint:

extern crate rock_n_rollup;
use rock_n_rollup::plugins::crypto::*;

fn transition<R: Verifier>(rt: &mut R) {
    let pkey = "edpkuDMUm7Y53wp4gxeLBXuiAhXZrLn8XB1R83ksvvesH8Lp8bmCfK".to_string();
    let pkey = PublicKey::try_from(pkey).unwrap();

    let signature = "edsigtuU5nUqBniorqTFXFixkG6ZkfvEPrfc9aT9DnMAeims2AX2yjpgYaedXBoKzAGHE3ZXSi1hZz6piZ3itTE7f2F4FoaxXtM".to_string();
    let signature = Signature::try_from(signature).unwrap();

    let data = b"hello world";
    // verifies the signature
    let is_correct: bool = rt.verify_signature(&signature, &pkey, data);
}
fn main(){}

Database

The Database plugin gives you an easier way to read and write data to the durable state.

You only need to use the database to derive Serialize and Deserialize on your custom types, and you ready to go.

extern crate rock_n_rollup;
use rock_n_rollup::plugins::database::{Database, Json};

fn transition<R: Database<Json>>(rt: &mut R) {
    let greetings = "Hello world!".to_string();

    let _ = rt.save("/greet", &greetings);
    let greetings = rt.get::<String>("/greet");
}
fn main(){}

Backends

Rock-N-Rollup gives you 2 backends to handle the serialization and deserialization of your data:

The JSON one, useful when you want to access this data directly from the browser:

extern crate rock_n_rollup;
use rock_n_rollup::plugins::database::{Database, Json};

fn transition<R: Database<Json>>(rt: &mut R) {
    let greetings = "Hello world!".to_string();

    let _ = rt.save("/greet", &greetings);
    let greetings = rt.get::<String>("/greet");
}
fn main(){}

The bincode one, which is faster and consume less ticks:

extern crate rock_n_rollup;
use rock_n_rollup::plugins::database::{Database, Bincode};

fn transition<R: Database<Bincode>>(rt: &mut R) {
    let greetings = "Hello world!".to_string();

    let _ = rt.save("/greet", &greetings);
    let greetings = rt.get::<String>("/greet");
}
fn main(){}

How to read the data from the browser?

If you want to read the data from the browser, we highly recommend to use the JSON backend.

Then let's say you have saved the data "Hello world" under the path "/state". Then you can query this endpoint:

curl "https://rollup.address/global/block/head/durable/wasm_2_0_0/value?key=/state"

It should returns you an array of bytes.

The first 4 bytes represent the size of the data. The remaining bytes represent the JSON. Then you can deserialize these bytes into a string, and then you can use JSON.parse onto this string.

Dac

The Dac plugin gives you a way to read data from the reveal data channel (populated by the DAC).

extern crate rock_n_rollup;
use rock_n_rollup::plugins::dac::*;

fn transition<R: Dac>(rt: &mut R) {
    let hash: PreimageHash = PreimageHash::try_from("00D49798B2E23FF9F48680793A649FE7B787DB5C5649ACF8FC1C950CDA12E3AC82").unwrap();

    let data: Vec<u8> = rt.read_from_dac(&hash).unwrap();
}
fn main() {}

Installer

You may want to upgrade your kernel. The Installer plugin exposes a function to install properly a new kernel.

extern crate rock_n_rollup;
use rock_n_rollup::plugins::installer::*;

fn transition<R: Installer>(rt: &mut R) {
    let kernel: Vec<u8> = Vec::default(); // let's say you have some bytes
    let result: Result<(), ()> = rt.install(&kernel);
}
fn main(){}

Services

A service is a set of transition.

User can define their own services or use some services provided by the library.

TicketUpgrade

A simple strategy to upgrade your smart rollup is the "byte ticket strategy".

The idea is simple:

  • split your kernel into chunks
  • send the root hash to a smart contract
  • this smart contract sends a byte ticket representing the root hash to your rollup
  • your rollup proceed to the upgrade

Requirement

  • Your rollup should have the type byte ticket
  • Your rollup should be deployed with the TicketUpgrade service
  • Deploying a smart contract on L1 that acts as a proxy

Deploy a smart contract

TODO: provide a minimal example:

Here is the minimal specification of the smart contract:

  • one entrypoint that accepts bytes
  • check the signature of the sender (you can also check the authencity of the bytes with a multisig or what you prefer)
  • mint a byte ticket, the content of the bytes is the input of the entrypoint
  • send the byte ticket to your rollup address

How to install the service

Let's say you have your application, if you want to add the service, the only things you have to do is the following:

extern crate rock_n_rollup;
use rock_n_rollup::core::{Runtime, Application};
use rock_n_rollup::services::ticket_upgrade::TicketUpgrade;

#[rock_n_rollup::main]
fn kernel_entry<R: Runtime>(application: &mut Application<R>) {
    application
        .service(TicketUpgrade::new("KT1...")) // Put the address of your L1 contract
        .run()
}
fn main(){}

Then when your kernel will receive a root hash from this contract, it will proceed to the installation of your new kernel.

Split your kernel

As you did to originate your kernel, you will have to split your new kernel with the tezos-smart-rollup-installer.

$ smart-rollup-installer get-reveal-installer --upgrade-to my_kernel.wasm --output installer.hex --preimages-dir wasm_2_0_0

Place the generated chunks in the good folder.

Then you need to retrieve the root hash:

$ cat installer.hex | tail -c 33

You can send the resulting output directly to your smart contract and your kernel will be upgraded!!