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:
- 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(){}
- 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(){}
- 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 loginfo
to print log with info levelwarn
to print log with warn levelerr
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 bitshash_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!!