The Linera banner

The Linera Developer Manual

Welcome to the developer manual of Linera, a decentralized protocol designed for highly scalable, low-latency Web3 applications.

This documentation is intended for developers who wish to learn more about Linera and its programming model by prototyping applications on top of the Linera Rust SDK.

NEW: Publish and test your Web3 application on the Linera Devnet!

Install the Linera CLI tool then follow the instructions on this page to claim a microchain and publish your first application on the current Devnet.

To join our community and get involved in the development of the Linera ecosystem, check out our GitHub repository, our website, and find us on social media channels such as youtube, twitter, telegram, and discord.

Let's get started!

Getting started

In this section, we will cover the necessary steps to install the Linera toolchain and give a short example to get started with the Linera SDK.

Installation

Let's start with the installation of the Linera development tools.

Overview

The Linera toolchain consist of two crates:

  • linera-sdk is the main library to program Linera applications in Rust. It also includes the Wasm test runner binary linera-wasm-test-runner.

  • linera-service defines a number of binaries, including:

    • linera -- the main client tool, used to operate development wallets,
    • linera-proxy -- the proxy service, acting as a public entrypoint for each validator,
    • linera-server -- the service run by each worker of a validator, hidden behind the proxy.

Requirements

The operating systems currently supported by the Linera toolchain can be summarized as follows:

Linux x86 64-bitMac OS (M1 / M2)Mac OS (x86)Windows
✓ Main platform✓ Working✓ WorkingUntested

The main prerequisites to install the Linera toolchain are Rust, Wasm, and Protoc. They can be installed as follows on Linux:

  • Rust and Wasm

    • curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    • rustup target add wasm32-unknown-unknown
  • Protoc

    • curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v21.11/protoc-21.11-linux-x86_64.zip
    • unzip protoc-21.11-linux-x86_64.zip -d $HOME/.local
    • If ~/.local is not in your path, add it: export PATH="$PATH:$HOME/.local/bin"
  • On certain Linux distributions, you may have to install development packages such as g++, libclang-dev and libssl-dev.

For MacOS support and for additional requirements needed to test the Linera protocol itself, see the installation section on GitHub.

This manual was tested with the following Rust toolchain:

[toolchain]
channel = "1.75.0"
components = [ "clippy", "rustfmt", "rust-src" ]
targets = [ "wasm32-unknown-unknown" ]
profile = "minimal"

Installing from crates.io

You may install the Linera binaries with

cargo install --locked linera-sdk@0.10.3
cargo install --locked linera-service@0.10.3

and use linera-sdk as a library for Linera Wasm applications:

cargo add linera-sdk@0.10.3

The version number 0.10.3 corresponds to the current Devnet of Linera and may change frequently.

Installing from GitHub

Download the source from GitHub:

git clone https://github.com/linera-io/linera-protocol.git
cd linera-protocol
git checkout -t origin/devnet_2024_03_26  # Current release branch

To install the Linera toolchain locally from source, you may run:

cargo install --locked --path linera-sdk
cargo install --locked --path linera-service

Alternatively, for developing and debugging, you may instead use the binaries compiled in debug mode, e.g. using export PATH="$PWD/target/debug:$PATH".

This manual was tested against the following commit of the repository:

687bb93985b6806ff2686f57cac968db0c7513dd

Bash helper (optional)

Consider adding the output of linera net helper to your ~/.bash_profile to help with automation.

Getting help

If installation fails, reach out to the team (e.g. on Discord) to help troubleshoot your issue or create an issue on GitHub.

Hello, Linera

This section is about interacting with the Devnet, running a local development network, then compiling and deploying your first application from scratch.

By the end of this section, you'll have a microchain on the Devnet and/or on your local network, and a working application that can be queried using GraphQL.

Using the Devnet

The Linera Devnet is a deployment of the Linera protocol that's useful for developers. It should not be considered stable, and can be restarted from a clean slate and new genesis at any time.

To interact with the Devnet, some tokens are needed. A Faucet service is available to create new microchains and obtain some test tokens. To do so, this must be configured when initializing the wallet:

linera wallet init --with-new-chain --faucet https://faucet.devnet-2024-03-26.linera.net

This creates a new microchain on Devnet with some initial test tokens, and the chain is automatically added to the newly instantiated wallet.

Make sure to use a Linera toolchain compatible with the current Devnet.

Starting a Local Test Network

Another option is to start your own local development network. A development network consists of a number of validators, each of which consist of an ingress proxy (aka. a "load balancer") and a number of workers (aka. "physical shards").

To start a local network, run the following command:

linera net up

This will start a validator with the default number of shards and create a temporary directory storing the entire network state.

This will set up a number of initial chains and create an initial wallet to operate them.

Using the Initial Test Wallet

linera net up prints Bash statements on its standard output to help you configure your terminal to use the initial wallet of the new test network, for instance:

export LINERA_WALLET="/var/folders/3d/406tbklx3zx2p3_hzzpfqdbc0000gn/T/.tmpvJ6lJI/wallet.json"
export LINERA_STORAGE="rocksdb:/var/folders/3d/406tbklx3zx2p3_hzzpfqdbc0000gn/T/.tmpvJ6lJI/linera.db"

This wallet is only valid for the lifetime of a single network. Every time a local network is restarted, the wallet needs to be reconfigured.

Interacting with the Network

In the following examples, we assume that either the wallet was initialized to interact with the Devnet or the variables LINERA_WALLET and LINERA_STORAGE are both set and point to the initial wallet of the running local network.

The main way of interacting with the network and deploying applications is using the linera client.

To check that the network is working, you can synchronize your default chain with the rest of the network and display the chain balance as follows:

linera sync
linera query-balance

You should see an output number, e.g. 10.

Building an Example Application

Applications running on Linera are Wasm bytecode. Each validator and client has a built-in Wasm virtual machine (VM) which can execute bytecode.

Let's build the counter application from the examples/ subdirectory:

cd examples/counter && cargo build --release --target wasm32-unknown-unknown

Publishing your Application

You can publish the bytecode and create an application using it on your local network using the linera client's publish-and-create command and provide:

  1. The location of the contract bytecode
  2. The location of the service bytecode
  3. The JSON encoded initialization arguments
linera publish-and-create \
  ../target/wasm32-unknown-unknown/release/counter_{contract,service}.wasm \
  --json-argument "42"

Congratulations! You've published your first application on Linera!

Querying your Application

Now let's query your application to get the current counter value. To do that, we need to use the client running in service mode. This will expose a bunch of APIs locally which we can use to interact with applications on the network.

linera service

Navigate to http://localhost:8080 in your browser to access the GraphiQL, the GraphQL IDE. We'll look at this in more detail in a later section; for now, list the applications deployed on your default chain e476… by running:

query {
  applications(
    chainId: "e476187f6ddfeb9d588c7b45d3df334d5501d6499b3f9ad5595cae86cce16a65"
  ) {
    id
    description
    link
  }
}

Since we've only deployed one application, the results returned have a single entry.

At the bottom of the returned JSON there is a field link. To interact with your application copy and paste the link into a new browser tab.

Finally, to query the counter value, run:

query {
  value
}

This will return a value of 42, which is the initialization argument we specified when deploying our application.

The Linera Protocol

We now describe the main concepts of the Linera protocol in more details.

Overview

Linera is a decentralized infrastructure optimized for Web3 applications that require guaranteed performance for an unlimited number of active users.

The core idea of the Linera protocol is to run many lightweight blockchains, called microchains, in parallel in a single set of validators.

How does it work?

In Linera, user wallets operate their own microchains. The owner of a chain chooses when to add new blocks to the chain and what goes inside the blocks. Such chains with a single user are called user chains.

Users may add new blocks to their chains in order to process incoming messages from other chains or to execute secure operations on their accounts, for instance to transfer assets to another user.

Importantly, validators ensure that all new blocks are valid. For instance, transfer operations must originate from accounts with sufficient funds; and incoming messages must have been actually sent from another chain. Blocks are verified by validators in the same way for every chain.

A Linera application is a Wasm program that defines its own state and operations. Users can publish bytecode and initialize an application on one chain, and it will be automatically deployed to all chains where it is needed, with a separate state on each chain.

To ensure coordination across chains, an application may rely on asynchronous cross-chain messages. Message payloads are application-specific and opaque to the rest of the system.

                               ┌───┐     ┌───┐     ┌───┐
                       Chain A │   ├────►│   ├────►│   │
                               └───┘     └───┘     └───┘
                                                     ▲
                                           ┌─────────┘
                                           │
                               ┌───┐     ┌─┴─┐     ┌───┐
                       Chain B │   ├────►│   ├────►│   │
                               └───┘     └─┬─┘     └───┘
                                           │         ▲
                                           │         │
                                           ▼         │
                               ┌───┐     ┌───┐     ┌─┴─┐
                       Chain C │   ├────►│   ├────►│   │
                               └───┘     └───┘     └───┘

The number of applications present on a single chain is not limited. On the same chain, applications are composed as usual using synchronous calls.

The current Linera SDK uses Rust as a source language to create Wasm applications. It relies on the normal Rust toolchains so that Rust programmers can work in their preferred environments.

How does Linera compare to existing multi-chain infrastructure?

Linera is the first infrastructure designed to support many chains in parallel, and notably an arbitrary number of user chains meant to be operated by user wallets.

In traditional multi-chain infrastructures, each chain usually runs a full blockchain protocol in a separate set of validators. Creating a new chain or exchanging messages between chains is expensive. As a result, the total number of chains is generally limited. Some chains may be specialized to a given use case: these are called "app chains".

In contrast, Linera is optimized for a large number of user chains:

  • Users only create blocks in their chain when needed;

  • Creating a microchain does not require onboarding validators;

  • All chains have the same level of security;

  • Microchains communicate efficiently using the internal networks of validators;

  • Validators are internally sharded (like a regular web service) and may adjust their capacity elastically by adding or removing internal workers.

Besides user chains, the Linera protocol is designed to support other types of microchains, called "permissioned" and "public" chains. Public chains are operated by validators. In this regard, they are similar to classical blockchains. Permissioned chains are meant to be used for temporary interactions between users, such as atomic swaps.

Why build on top of Linera?

We believe that many high-value use cases are currently out of reach of existing Web3 infrastructures because of the challenges of serving many active users simultaneously without degrading user experience (unpredictable fees, latency, etc).

Examples of applications that require processing time-sensitive transactions created by many simultaneous users include:

  • real-time micro-payments and micro-rewards,

  • social data feeds,

  • real-time auction systems,

  • turn-based games,

  • version control systems for software, data pipelines, or AI training pipelines.

Lightweight user chains are instrumental in providing elastic scalability but they have other benefits as well. Because user chains have fewer blocks than traditional blockchains, in Linera, the full-nodes of user chains will be embedded into the users' wallets, typically deployed as a browser extension.

This means that Web UIs connected to a wallet will be able to query the state of the user chain directly (no API provider, no light client) using familiar frameworks (React/GraphQL). Furthermore, wallets will be able to leverage the full node as well for security purposes, including to display meaningful confirmation messages to users.

What is the current state of the development of Linera?

The reference open-source implementation of Linera is under active development. It already includes a Web3 SDK with the necessary features to prototype simple Web3 applications and test them locally on the same machine and deploying them to the Devnet.

Web UIs (possibly reactive) can already be built on top of Wasm-embedded GraphQL services, and tested locally in the browser.

The main limitations of our current Web3 SDK include:

  • Web UIs need to query a local HTTP service acting as a wallet. This setup is meant to be temporary and for testing only: in the future, web UIs will securely connect to a Wallet installed as a browser extension, as usual.

  • Only user chains are currently documented in this manual. Permissioned chains (aka "temporary" chains) were recently added. Support for public chains is in progress.

The main development workstreams of Linera, beyond its SDK, can be broken down as follows.

Core Protocol

  • User chains
  • Permissioned chains (core protocol only)
  • Cross-chain messages
  • Cross-chain pub/sub channels (initial version)
  • Bytecode publishing
  • Application creation
  • Reconfigurations of validators
  • Support for gas fees
  • Support for storage fees and storage limits
  • External service (aka. "Faucet") to help users create their first chain
  • Permissioned chains (adding operation access control, demo of atomic swaps, etc)
  • Avoid repeatedly loading chain states from storage
  • Blob storage usable by system and user applications (generalizing/replacing bytecode storage)
  • Support for easy onboarding of user chains into a new application (removing the need to accept requests)
  • Replace pub/sub channels by data streams (removing the need to accept subscriptions)
  • Allow chain clients to control which chains they track (lazily/actively) and execute (do not execute all tracked chains)
  • Multi-signed events to facilitate future bridges to external chains
  • Public chains (adding leader election, inbox constraints, etc)
  • Transaction scripts
  • Support for dynamic shard assignment
  • Support for archiving chains
  • Tokenomics and incentives for all stakeholders
  • Governance on the admin chain (e.g. DPoS, onboarding of validators)
  • Permissionless auditing protocol

Wasm VM integration

  • Support for the Wasmer VM
  • Support for the Wasmtime VM (experimental)
  • Test gas metering and deterministic execution across VMs
  • Composing Wasm applications on the same chain
  • Support for non-blocking (yet deterministic) calls to storage
  • Support for read-only GraphQL services in Wasm
  • Support for mocked system APIs
  • Improve host/guest stub generation to make mocks easier
  • Support for running Wasm applications in the browser

Storage

  • Object management library ("linera-views") on top of Key-Value store abstraction
  • Support for Rocksdb
  • Experimental support for DynamoDb
  • Derive macros for GraphQL
  • Support for ScyllaDb
  • Make library fully extensible by users (requires better GraphQL macros)
  • In-memory storage service for testing purposes
  • Support for Web storage (IndexedDB)
  • Performance benchmarks and improvements (including faster state hashing)
  • Better configuration management
  • Local Write-Ahead Log
  • Production-grade support for the chosen main database
  • Tooling for debugging
  • Make the storage library easy to use outside of Linera

Validator Infrastructure

  • Simple TCP/UDP networking (used for benchmarks only)
  • GRPC networking
  • Basic frontend (aka. proxy) supporting fixed internal shards
  • Observability
  • Kubernetes support in CI
  • Deployment using a cloud provider
  • Horizontally scalable frontend (aka. proxy)
  • Dynamic shard assignment
  • Cloud integration to demonstrate elastic scaling

Web3 SDK

  • Traits for contract and service interfaces
  • Support for unit testing
  • Support for integration testing
  • Local GraphQL service to query and browse system state
  • Local GraphQL service to query and browse application states
  • Use GraphQL mutations to execute operations and create blocks
  • ABIs for contract and service interfaces
  • Allowing message sender to pay for message execution fees
  • Wallet as a browser extension (no VM)
  • Wallet as a browser extension (with Wasm VM)
  • Easier communication with EVM chains
  • Bindings to use native cryptographic primitives from Wasm
  • Allowing applications to pay for user fees
  • Allowing applications to use permissioned chains and public chains

Microchains

This section provides an introduction to microchains, the main building block of the Linera Protocol. For a more formal treatment refer to the whitepaper.

Background

A microchain is a chain of blocks describing successive changes to a shared state. We will use the terms chain and microchain interchangeably. Linera microchains are similar to the familiar notion of blockchain, with the following important specificities:

  • An arbitrary number of microchains can coexist in a Linera network, all sharing the same set of validators and the same level of security. Creating a new microchain only takes one transaction on an existing chain.

  • The task of proposing new blocks in a microchain can be assumed either by validators or by end users (or rather their wallets) depending on the configuration of a chain. Specifically, microchains can be single-owner, permissioned, or public, depending on who is authorized to propose blocks.

Cross-Chain Messaging

In traditional networks with a single blockchain, every transaction can access the application's entire execution state. This is not the case in Linera where the state of an application is spread across multiple microchains, and the state on any individual microchain is only affected by the blocks of that microchain.

Cross-chain messaging is a way for different microchains to communicate with each other asynchronously. This method allows applications and data to be distributed across multiple chains for better scalability. When an application on one chain sends a message to itself on another chain, a cross-chain request is created. These requests are implemented using remote procedure calls (RPCs) within the validators' internal network, ensuring that each request is executed only once.

Instead of immediately modifying the target chain, messages are placed first in the target chain's inbox. When an owner of the target chain creates its next block in the future, they may reference a selection of messages taken from the current inbox in the new block. This executes the selected messages and applies their messages to the chain state.

Below is an example set of chains sending asynchronous messages to each other over consecutive blocks.

                               ┌───┐     ┌───┐     ┌───┐
                       Chain A │   ├────►│   ├────►│   │
                               └───┘     └───┘     └───┘
                                                     ▲
                                           ┌─────────┘
                                           │
                               ┌───┐     ┌─┴─┐     ┌───┐
                       Chain B │   ├────►│   ├────►│   │
                               └───┘     └─┬─┘     └───┘
                                           │         ▲
                                           │         │
                                           ▼         │
                               ┌───┐     ┌───┐     ┌─┴─┐
                       Chain C │   ├────►│   ├────►│   │
                               └───┘     └───┘     └───┘

The Linera protocol allows receivers to discard messages but not to change the ordering of selected messages inside the communication queue between two chains. If a selected message fails to execute, the wallet will automatically reject it when proposing the receiver's block. The current implementation of the Linera client always selects as many messages as possible from inboxes, and never discards messages unless they fail to execute.

Chain Ownership Semantics

Only single-owner chains are currently supported in the Linera SDK. However, microchains can create new microchains for other users, and control of a chain can be transferred to another user by changing the owner ID. A chain is permanently deactivated when its owner ID is set to None.

For more detail and examples on how to open and close chains, see the wallet section on chain management.

Wallets

As in traditional blockchains, Linera wallets are in charge of holding user private keys. However, instead of signing transactions, Linera wallets are meant to sign blocks and propose them to extend the chains owned by their users.

In practice, wallets include a node which tracks a subset of Linera chains. We will see in the next section how a Linera wallet can run a GraphQL service to expose the state of its chains to web frontends.

The command-line tool linera is the main way for developers to interact with a Linera network and manage the user wallets present locally on the system.

Note that this command-line tool is intended mainly for development purposes. Our goal is that end users eventually manage their wallets in a browser extension.

Selecting a Wallet

The private state of a wallet is conventionally stored in a file wallet.json, while the state of its node is stored in a file linera.db.

To switch between wallets, you may use the --wallet and --storage options of the linera tool, e.g. as in linera --wallet wallet2.json --storage rocksdb:linera2.db.

You may also define the environment variables LINERA_STORAGE and LINERA_WALLET to the same effect. E.g. LINERA_STORAGE=$PWD/wallet2.json and LINERA_WALLET=$PWD/wallet2.json.

Finally, if LINERA_STORAGE_$I and LINERA_WALLET_$I are defined for some number I, you may call linera --with-wallet $I (or linera -w $I for short).

Chain Management

Listing Chains

To list the chains present in your wallet, you may use the command show:

linera wallet show
╭──────────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────╮
│ Chain ID                                                         ┆ Latest Block                                                                         │
╞══════════════════════════════════════════════════════════════════╪══════════════════════════════════════════════════════════════════════════════════════╡
│ 668774d6f49d0426f610ad0bfa22d2a06f5f5b7b5c045b84a26286ba6bce93b4 ┆ Public Key:         3812c2bf764e905a3b130a754e7709fe2fc725c0ee346cb15d6d261e4f30b8f1 │
│                                                                  ┆ Owner:              c9a538585667076981abfe99902bac9f4be93714854281b652d07bb6d444cb76 │
│                                                                  ┆ Block Hash:         -                                                                │
│                                                                  ┆ Timestamp:          2023-04-10 13:52:20.820840                                       │
│                                                                  ┆ Next Block Height:  0                                                                │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 91c7b394ef500cd000e365807b770d5b76a6e8c9c2f2af8e58c205e521b5f646 ┆ Public Key:         29c19718a26cb0d5c1d28102a2836442f53e3184f33b619ff653447280ccba1a │
│                                                                  ┆ Owner:              efe0f66451f2f15c33a409dfecdf76941cf1e215c5482d632c84a2573a1474e8 │
│                                                                  ┆ Block Hash:         51605cad3f6a210183ac99f7f6ef507d0870d0c3a3858058034cfc0e3e541c13 │
│                                                                  ┆ Timestamp:          2023-04-10 13:52:21.885221                                       │
│                                                                  ┆ Next Block Height:  1                                                                │
╰──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────╯

Each row represents a chain present in the wallet. On the left is the unique identifier on the chain, and on the right is metadata for that chain associated with the latest block.

Default Chain

Each wallet has a default chain that all commands apply to unless you specify another --chain on the command line.

The default chain is set initially, when the first chain is added to the wallet. You can check the default chain for your wallet by running:

linera wallet show

The Chain ID which is in green text instead of white text is your default chain.

To change the default chain for your wallet, user the set-default command:

linera wallet set-default <chain-id>

Opening a Chain

The Linera protocol defines semantics for how new chains are created, we call this "opening a chain". A chain cannot be opened in a vacuum, it needs to be created by an existing chain on the network.

Open a Chain for Your Own Wallet

To open a chain for your own wallet, you can use the open-chain command:

linera open-chain

This will create a new chain (using the wallet's default chain) and add it to the wallet. Use the wallet show command to see your existing chains.

Open a Chain for Another Wallet

Opening a chain for another wallet requires an extra two steps. Let's initialize a second wallet:

linera --wallet wallet2.json --storage rocksdb:linera2.db wallet init --genesis target/debug/genesis.json

First wallet2 must create an unassigned keypair. The public part of that keypair is then sent to the wallet who is the chain creator.

linera --wallet wallet2.json keygen
6443634d872afbbfcc3059ac87992c4029fa88e8feb0fff0723ac6c914088888 # this is the public key for the unassigned keypair

Next, using the public key, wallet can open a chain for wallet2.

linera open-chain --to-public-key 6443634d872afbbfcc3059ac87992c4029fa88e8feb0fff0723ac6c914088888
e476187f6ddfeb9d588c7b45d3df334d5501d6499b3f9ad5595cae86cce16a65010000000000000000000000
fc9384defb0bcd8f6e206ffda32599e24ba715f45ec88d4ac81ec47eb84fa111

The first line is the message ID specifying the cross-chain message that creates the new chain. The second line is the new chain's ID.

Finally, to add the chain to wallet2 for the given unassigned key we use the assign command:

 linera --wallet wallet2.json assign --key 6443634d872afbbfcc3059ac87992c4029fa88e8feb0fff0723ac6c914088888 --message-id e476187f6ddfeb9d588c7b45d3df334d5501d6499b3f9ad5595cae86cce16a65010000000000000000000000

Setting up Extra Wallets Automatically with linera net up

For testing, rather than using linera open-chain and linera assign as above, it is often more convenient to pass the option --extra-wallets N to linera net up.

This option will create create N additional user wallets and output Bash commands to define the environment variables LINERA_{WALLET,STORAGE}_$I where I ranges over 0..=N (I=0 being the wallet for the initial chains).

Once all the environment variables are defined, you may switch between wallets using linera --with-wallet I or linera -w I for short.

Automation in Bash

To automate the process of setting the variables LINERA_WALLET* and LINERA_STORAGE* after creating a local test network in a shell, we provide a Bash helper function linera_spawn_and_read_wallet_variables.

To define the function linera_spawn_and_read_wallet_variables in your shell, run source /dev/stdin <<<"$(linera net helper 2>/dev/null)". You may also add the output of linera net helper to your ~/.bash_profile for future sessions.

Once the function is defined, call linera_spawn_and_read_wallet_variables linera net up instead of linera net up.

Node Service

So far we've seen how to use the Linera client treating it as a binary in your terminal. However, the client also acts as a node which:

  1. Executes blocks
  2. Exposes an GraphQL API and IDE for dynamically interacting with applications and the system
  3. Listens for notifications from validators and automatically updates local chains.

To interact with the node service, run linera in service mode:

linera service

This will run the node service on port 8080 by default (this can be overridden using the --port flag).

A Note on GraphQL

Linera uses GraphQL as the query language for interfacing with different parts of the system. GraphQL enables clients to craft queries such that they receive exactly what they want and nothing more.

GraphQL is used extensively during application development, especially to query the state of an application from a front-end for example.

To learn more about GraphQL check out the official docs.

GraphiQL IDE

Conveniently, the node service exposes a GraphQL IDE called GraphiQL. To use GraphiQL start the node service and navigate to localhost:8080/.

Using the schema explorer on the left of the GraphiQL IDE you can dynamically explore the state of the system and your applications.

graphiql.png

GraphQL System API

The node service also exposes a GraphQL API which corresponds to the set of system operations. You can explore the full set of operations by clicking on MutationRoot.

GraphQL Application API

To interact with an application, we run the Linera client in service mode. It exposes a GraphQL API for every application running on any owned chain at localhost:8080/chains/<chain-id>/applications/<application-id>.

Navigating there with your browser will open a GraphiQL interface which enables you to graphically explore the state of your application.

Applications

The programming model of Linera is designed so that developers can take advantage of microchains to scale their applications.

Linera uses the WebAssembly (Wasm) Virtual Machine to execute user applications. Currently, the Linera SDK is focused on the Rust programming language.

Linera applications are structured using the familiar notion of Rust crate: the external interfaces of an application (including initialization parameters, operations and messages) generally go into the library part of its crate, while the core of each application is compiled into binary files for the Wasm architecture.

The Application Deployment Lifecycle

Linera Applications are designed to be powerful yet re-usable. For this reason there is a distinction between the bytecode and an application instance on the network.

Applications undergo a lifecycle transition aimed at making development easy and flexible:

  1. The bytecode is built from a Rust project with the linera-sdk dependency.
  2. The bytecode is published to the network on a microchain, and assigned an identifier.
  3. A user can create a new application instance, by providing the bytecode identifier and initialization arguments. This process returns an application identifier which can be used to reference and interact with the application.
  4. The same bytecode identifier can be used as many times needed by as many users needed to create distinct applications.

Importantly, the application deployment lifecycle is abstracted from the user, and an application can be published with a single command:

linera publish-and-create <contract-path> <service-path> <init-args>

This will publish the bytecode as well as initialize the application for you.

Anatomy of an Application

An application is broken into two major components, the contract and the service.

The contract is gas-metered, and is the part of the application which executes operations and messages, make cross-application calls and modifies the application's state. The details are covered in more depth in the SDK docs.

The service is non-metered and read-only. It is used primarily to query the state of an application and populate the presentation layer (think front-end) with the data required for a user interface.

Operations and Messages

For this section we'll be using a simplified version of the example application called "fungible" where users can send tokens to each other.

At the system-level, interacting with an application can be done via operations and messages.

Operations are defined by an application developer and each application can have a completely different set of operations. Chain owners then actively create operations and put them in their block proposals to interact with an application. Other applications may also call the application by providing an operation for it to execute, this is called a cross-application call and always happens within the same chain. Operations for cross-application calls may return a response value back to the caller.

Taking the "fungible token" application as an example, an operation for a user to transfer funds to another user would look like this:

extern crate serde;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub enum Operation {
    /// A transfer from a (locally owned) account to a (possibly remote) account.
    Transfer {
        owner: AccountOwner,
        amount: Amount,
        target_account: Account,
    },
    // Meant to be extended here
}

Messages result from the execution of operations or other messages. Messages can be sent from one chain to another, always within the same application. Block proposers also actively include messages in their block proposal, but unlike with operations, they are only allowed to include them in the right order (possibly skipping some), and only if they were actually created by another chain (or by a previous block of the same chain).

In our "fungible token" application, a message to credit an account would look like this:

extern crate serde;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub enum Message {
    Credit { owner: AccountOwner, amount: Amount },
    // Meant to be extended here
}

Authentication

Operations in a block are always authenticated and messages may be authenticated. The signer of a block becomes the authenticator of all the operations in that block. As operations are being executed by applications, messages can be created to be sent to other chains. When they are created, they can be configured to be authenticated. In that case, the message receives the same authentication as the operation that created it. If handling an incoming message creates new messages, those may also be configured to have the same authentication as the received message.

In other words, the block signer can have its authority propagated across chains through series of messages. This allows applications to safely store user state on chains that the user may not have the authority to produce blocks. The application may also allow only the authorized user to change that state, and not even the chain owner is able to override that.

The figure below shows four chains (A, B, C, D) and some blocks produced in them. In this example, each chain is owned by a single owner (aka. address). Owners are in charge of producing blocks and sign new blocks using their signing keys. Some blocks show the operations and incoming messages they accept, where the authentication is shown inside parenthesis. All operations produced are authenticated by the block proposer, and if these are all single user chains, the proposer is always the chain owner. Messages that have authentication use the one from the operation or message that created it.

One example in the figure is that chain A produced a block with Operation 1, which is authenticated by the owner of chain A (written (a)). That operation sent a message to chain B, and assuming the message was sent with the authentication forwarding enabled, it is received and executed in chain B with the authentication of (a). Another example is that chain D produced a block with Operation 2, which is authenticated by the owner of chain D (written (d)). That operation sent a message to chain C, which is executed with authentication of (d) like the example before. Handling that message in chain C produced a new message, which was sent to chain B. That message, when received by chain B is executed with the authentication of (d).

                            ┌───┐     ┌─────────────────┐     ┌───┐
       Chain A owned by (a) │   ├────►│ Operation 1 (a) ├────►│   │
                            └───┘     └────────┬────────┘     └───┘
                                               │
                                               └────────────┐
                                                            ▼
                                                ┌──────────────────────────┐
                            ┌───┐     ┌───┐     │ Message from chain A (a) │
       Chain B owned by (b) │   ├────►│   ├────►│ Message from chain C (d) |
                            └───┘     └───┘     │ Operation 3 (b)          │
                                                └──────────────────────────┘
                                                            ▲
                                                   ┌────────┘
                                                   │
                            ┌───┐     ┌──────────────────────────┐     ┌───┐
       Chain C owned by (c) │   ├────►│ Message from chain D (d) ├────►│   │
                            └───┘     └──────────────────────────┘     └───┘
                                                 ▲
                                     ┌───────────┘
                                     │
                            ┌─────────────────┐     ┌───┐     ┌───┐
       Chain D owned by (d) │ Operation 2 (d) ├────►│   ├────►│   │
                            └─────────────────┘     └───┘     └───┘

An example where this is used is in the Fungible application, where a Claim operation allows retrieving money from a chain the user does not control (but the user still trusts will produce a block receiving their message). Without the Claim operation, users would only be able to store their tokens on their own chains, and multi-owner and public chains would have their tokens shared between anyone able to produce a block.

With the Claim operation, users can store their tokens on another chain where they're able to produce blocks or where they trust the owner will produce blocks receiving their messages. Only they are able to move their tokens, even on chains where ownership is shared or where they are not able to produce blocks.

Registering an Application across Chains

If Alice is using an application on her chain and starts interacting with Bob via the application, e.g. sends him some tokens using the fungible example, the application automatically gets registered on Bob's chain, too, as soon as he handles the incoming cross-chain messages. After that, he can execute the application's operations on his chain, too, and e.g. send tokens to someone.

But there are also cases where Bob may want to start using an application he doesn't have yet. E.g. maybe Alice regularly makes posts using the social example, and Bob wants to subscribe to her.

In that case, trying to execute an application-specific operation would fail, because the application is not registered on his chain. He needs to request it from Alice first:

linera request-application <application-id> --target-chain-id <alices-chain-id>

Once Alice processes his message (which happens automatically if she is running the client in service mode), he can start using the application.

Writing Linera Applications

In this section, we'll be exploring how to create Web3 applications using the Linera SDK.

We'll use a simple "counter" application as a running example.

We'll focus on the back end of the application, which consists of two main parts: a smart contract and its GraphQL service.

Both the contract and the service of an application are written in Rust using the crate linera-sdk, and compiled to Wasm bytecode.

This section should be seen as a guide versus a reference manual for the SDK. For the reference manual, refer to the documentation of the crate.

Creating a Linera Project

To create your Linera project, use the linera project new command. The command should be executed outside the linera-protocol folder. It sets up the scaffolding and requisite files:

linera project new my-counter

linera project new bootstraps your project by creating the following key files:

  • Cargo.toml: your project's manifest filled with the necessary dependencies to create an app;
  • src/lib.rs: the application's ABI definition;
  • src/state.rs: the application's state;
  • src/contract.rs: the application's contract, and the binary target for the contract bytecode;
  • src/service.rs: the application's service, and the binary target for the service bytecode.
  • .cargo/config.toml: modifies the default target used by cargo to be wasm32-unknown-unknown

Creating the Application State

The struct which defines your application's state can be found in src/state.rs. An application state is the data that is persisted on storage between transactions.

To represent our counter, we're going to need a single u64. To persist the counter we'll be using Linera's view paradigm.

Views are a little like an ORM, however instead of mapping data structures to a relational database like Postgres, they are instead mapped onto key-value stores like RocksDB.

In vanilla Rust, we might represent our Counter as so:

// do not use this
struct Counter {
  value: u64
}

However, to persist your data, you'll need to replace the existing Application state struct in src/state.rs with the following view:

/// The application state.
#[derive(RootView, async_graphql::SimpleObject)]
#[view(context = "ViewStorageContext")]
pub struct Counter {
    pub value: RegisterView<u64>,
}

and all other occurrences of Application in your app.

The RegisterView<T> supports modifying a single value of type T. There are different types of views for different use-cases, but the majority of common data structures have already been implemented:

  • A Vec or VecDeque corresponds to a LogView
  • A BTreeMap corresponds to a MapView if its values are primitive, or to CollectionView if its values are other views;
  • A Queue corresponds to a QueueView

For an exhaustive list refer to the Views documentation.

Finally, run cargo check to ensure that your changes compile.

Defining the ABI

The Application Binary Interface (ABI) of a Linera application defines how to interact with this application from other parts of the system. It includes the data structures, data types, and functions exposed by on-chain contracts and services.

ABIs are usually defined in src/lib.rs and compiled across all architectures (Wasm and native).

For a reference guide, check out the documentation of the crate.

Defining a marker struct

The library part of your application (generally in src/lib.rs) must define a public empty struct that implements the Abi trait.

struct CounterAbi;

The Abi trait combines the ContractAbi and ServiceAbi traits to include the types that your application exports.

/// A trait that includes all the types exported by a Linera application (both contract
/// and service).
pub trait Abi: ContractAbi + ServiceAbi {}

Next, we're going to implement each of the two traits.

Contract ABI

The ContractAbi trait defines the data types that your application uses in a contract. Each type represents a specific part of the contract's behavior:

/// A trait that includes all the types exported by a Linera application contract.
pub trait ContractAbi {
    /// Immutable parameters specific to this application (e.g. the name of a token).
    type Parameters: Serialize + DeserializeOwned + Send + Sync + Clone + Debug + 'static;

    /// Initialization argument passed to a new application on the chain that created it
    /// (e.g. an initial amount of tokens minted).
    ///
    /// To share configuration data on every chain, use [`ContractAbi::Parameters`]
    /// instead.
    type InitializationArgument: Serialize + DeserializeOwned + Send + Sync + Debug + 'static;

    /// The type of operation executed by the application.
    ///
    /// Operations are transactions directly added to a block by the creator (and signer)
    /// of the block. Users typically use operations to start interacting with an
    /// application on their own chain.
    type Operation: Serialize + DeserializeOwned + Send + Sync + Debug + 'static;

    /// The response type of an application call.
    type Response: Serialize + DeserializeOwned + Send + Sync + Debug + 'static;
}

All these types must implement the Serialize, DeserializeOwned, Send, Sync, Debug traits, and have a 'static lifetime.

In our example, we would like to change our InitializationArgument, Operation to u64, like so:

extern crate linera_base;
use linera_base::abi::ContractAbi;
struct CounterAbi;
impl ContractAbi for CounterAbi {
    type InitializationArgument = u64;
    type Parameters = ();
    type Operation = u64;
    type Response = ();
}

Service ABI

The ServiceAbi is in principle very similar to the ContractAbi, just for the service component of your application.

The ServiceAbi trait defines the types used by the service part of your application:

/// A trait that includes all the types exported by a Linera application service.
pub trait ServiceAbi {
    /// Immutable parameters specific to this application (e.g. the name of a token).
    type Parameters: Serialize + DeserializeOwned + Send + Sync + Clone + Debug + 'static;

    /// The type of a query receivable by the application's service.
    type Query: Serialize + DeserializeOwned + Send + Sync + Debug + 'static;

    /// The response type of the application's service.
    type QueryResponse: Serialize + DeserializeOwned + Send + Sync + Debug + 'static;
}

For our Counter example, we'll be using GraphQL to query our application so our ServiceAbi should reflect that:

extern crate linera_base;
extern crate async_graphql;
use linera_base::abi::ServiceAbi;
struct CounterAbi;
impl ServiceAbi for CounterAbi {
    type Query = async_graphql::Request;
    type QueryResponse = async_graphql::Response;
    type Parameters = ();
}

Writing the Contract Binary

The contract binary is the first component of a Linera application. It can actually change the state of the application.

To create a contract, we need to create a new type and implement the Contract trait for it, which is as follows:

#[async_trait]
pub trait Contract: WithContractAbi + ContractAbi + Send + Sized {
    /// The type used to report errors to the execution environment.
    type Error: Error + From<serde_json::Error> + From<bcs::Error> + 'static;

    /// The type used to store the persisted application state.
    type State: Sync;

    /// The desired storage backend used to store the application's state.
    type Storage: ContractStateStorage<Self> + Send + 'static;

    /// The type of message executed by the application.
    type Message: Serialize + DeserializeOwned + Send + Sync + Debug + 'static;

    /// Creates an in-memory instance of the contract handler from the application's `state`.
    async fn new(state: Self::State, runtime: ContractRuntime<Self>) -> Result<Self, Self::Error>;

    /// Returns the current state of the application so that it can be persisted.
    fn state_mut(&mut self) -> &mut Self::State;

    /// Initializes the application on the chain that created it.
    async fn initialize(
        &mut self,
        argument: Self::InitializationArgument,
    ) -> Result<(), Self::Error>;

    /// Applies an operation from the current block.
    async fn execute_operation(
        &mut self,
        operation: Self::Operation,
    ) -> Result<Self::Response, Self::Error>;

    /// Applies a message originating from a cross-chain message.
    async fn execute_message(&mut self, message: Self::Message) -> Result<(), Self::Error>;

    /// Finishes the execution of the current transaction.
    async fn finalize(&mut self) -> Result<(), Self::Error> {
        Self::Storage::store(self.state_mut()).await;
        Ok(())
    }
}

The full trait definition can be found here.

There's quite a bit going on here, so let's break it down and take one method at a time.

For this application, we'll be using the initialize and execute_operation methods.

The Contract Lifecycle

To implement the application contract, we first create a type for the contract:

pub struct CounterContract {
    state: Counter,
    runtime: ContractRuntime<Self>,
}

This type usually contains at least two fields: the persistent state defined earlier and a handle to the runtime. The runtime provides access to information about the current execution and also allows sending messages, among other things. Other fields can be added, and they can be used to store volatile data that only exists while the current transaction is being executed, and discarded afterwards.

When a transaction is executed, first the application's state is loaded, then the contract type is created by calling the Contract::new method. This method receives the state and a handle to the runtime that the contract can use. For our implementation, we will just store the received parameters:

    async fn new(state: Counter, runtime: ContractRuntime<Self>) -> Result<Self, Self::Error> {
        CounterContract { state, runtime }
    }

When the transaction finishes executing successfully, there's a final step where all loaded application contracts are allowed to finalize, similarly to executing a destructor. The default implementation of finalize just persists the application's state, and that's why we must provide it access to the state through the state_mut method:

    fn state_mut(&mut self) -> &mut Self::State {
        &mut self.state
    }

Applications may want to override the finalize method in more advanced scenarios, but they must ensure they don't forget to persist their state if they do so. For more information see the Contract finalization section.

Initializing our Application

The first thing that happens when an application is created from a bytecode is that it is initialized. This is done by calling the contract's Contract::initialize method.

Contract::initialize is only called once when the application is created and only on the microchain that created the application.

Deployment on other microchains will use the Default implementation of the application state if SimpleStateStorage is used, or the Default value of all sub-views in the state if the ViewStateStorage is used.

For our example application, we'll want to initialize the state of the application to an arbitrary number that can be specified on application creation using its initialization parameters:

    async fn initialize(&mut self, value: u64) -> Result<(), Self::Error> {
        self.state.value.set(value);
        Ok(())
    }

Implementing the Increment Operation

Now that we have our counter's state and a way to initialize it to any value we would like, we need a way to increment our counter's value. Execution requests from block proposers or other applications are broadly called 'operations'.

To create a new operation, we need to use the method Contract::execute_operation. In the counter's case, it will be receiving a u64 which is used to increment the counter:

    async fn execute_operation(&mut self, operation: u64) -> Result<(), Self::Error> {
        let current = self.value.get();
        self.value.set(current + operation);
        Ok(())
    }

Declaring the ABI

Finally, to link our Contract trait implementation with the ABI of the application, the following code is added:

impl WithContractAbi for CounterContract {
    type Abi = counter::CounterAbi;
}

Writing the Service Binary

The service binary is the second component of a Linera application. It is compiled into a separate Bytecode from the contract and is run independently. It is not metered (meaning that querying an application's service does not consume gas), and can be thought of as a read-only view into your application.

Application states can be arbitrarily complex, and most of the time you don't want to expose this state in its entirety to those who would like to interact with your app. Instead, you might prefer to define a distinct set of queries that can be made against your application.

The Service trait is how you define the interface into your application. The Service trait is defined as follows:

/// The service interface of a Linera application.
#[async_trait]
pub trait Service: WithServiceAbi + ServiceAbi {
    /// Type used to report errors to the execution environment.
    type Error: Error + From<serde_json::Error>;

    /// The type used to store the persisted application state.
    type State;

    /// The desired storage backend used to store the application's state.
    type Storage: ServiceStateStorage;

    /// Creates a in-memory instance of the service handler from the application's `state`.
    async fn new(state: Self::State, runtime: ServiceRuntime<Self>) -> Result<Self, Self::Error>;

    /// Executes a read-only query on the state of this application.
    async fn handle_query(&self, query: Self::Query) -> Result<Self::QueryResponse, Self::Error>;
}

The full service trait definition can be found here.

Let's implement Service for our counter application.

First, we create a new type for the service, similarly to the contract:

pub struct CounterService {
    state: Counter,
}

Just like with the CounterContract type, this type usually has two types: the application state and the runtime. We can omit the fields if we don't use them, so in this example we're omitting the runtime field.

We need to generate the necessary boilerplate for implementing the service WIT interface, export the necessary resource types and functions so that the service can be executed. Fortunately, there is a macro to perform this code generation, so just add the following to service.rs:

linera_sdk::service!(CounterService);

Next, we need to implement the Service trait for CounterService type. The first step is to define the Service's associated types:

#[async_trait]
impl Service for CounterService {
    type Error = Error;
    type Storage = ViewStateStorage<Self>;
    type State = Counter;
}

The only type specified above that isn't yet defined is the Error type, so let's create it below the trait implementation:

/// An error that can occur during the contract execution.
#[derive(Debug, Error)]
pub enum Error {
    /// Invalid query argument; could not deserialize GraphQL request.
    #[error("Invalid query argument; could not deserialize GraphQL request")]
    InvalidQuery(#[from] serde_json::Error),
}

Also like in contracts, we must implement a new constructor when implementing the Service trait. The constructor receives the state and the runtime handle:

    async fn new(state: Self::State, _runtime: ServiceRuntime<Self>) -> Result<Self, Self::Error> {
        Ok(CounterService { state })
    }

The actual functionality of the service starts in the handle_query method. We will accept GraphQL queries and handle them using the async-graphql crate. To forward the queries to custom GraphQL handlers we will implement in the next section, we use the following code:

    async fn handle_query(&mut self, request: Request) -> Result<Response, Self::Error> {
        let schema = Schema::build(
            // implemented in the next section
            QueryRoot { value: *self.value.get() },
            // implemented in the next section
            MutationRoot {},
            EmptySubscription,
        )
        .finish();
        Ok(schema.execute(request).await)
    }
}

Finally, as before, the following code is needed to incorporate the ABI definitions into your Service implementation:

impl WithServiceAbi for Counter {
    type Abi = counter::CounterAbi;
}

Adding GraphQL compatibility

Finally, we want our application to have GraphQL compatibility. To achieve this we need a QueryRoot to respond to queries and a MutationRoot for creating serialized Operation values that can be placed in blocks.

In the QueryRoot, we only create a single value query that returns the counter's value:

struct QueryRoot {
    value: u64,
}

#[Object]
impl QueryRoot {
    async fn value(&self) -> &u64 {
        &self.value
    }
}

In the MutationRoot, we only create one increment method that returns a serialized operation to increment the counter by the provided value:

struct MutationRoot;

#[Object]
impl MutationRoot {
    async fn increment(&self, value: u64) -> Vec<u8> {
        bcs::to_bytes(&value).unwrap()
    }
}

We haven't included the imports in the above code; they are left as an exercise to the reader (but remember to import async_graphql::Object). If you want the full source code and associated tests check out the examples section on GitHub.

Deploying the Application

The first step to deploy your application is to configure a wallet. This will determine where the application will be deployed: either to a local net or to the devnet.

Local Net

To configure the local network, follow the steps in the Getting Started section.

Afterwards, the LINERA_WALLET and the LINERA_STORAGE environment variables should be set and can be used in the publish-and-create command to deploy the application while also specifying:

  1. The location of the contract bytecode
  2. The location of the service bytecode
  3. The JSON encoded initialization arguments
linera publish-and-create \
  target/wasm32-unknown-unknown/release/my-counter_{contract,service}.wasm \
  --json-argument "42"

Devnet

To configure the wallet for the devnet while creating a new microchain, the following command can be used:

linera wallet init --with-new-chain --faucet https://faucet.devnet-2024-03-26.linera.net

The Faucet will provide the new chain with some tokens, which can then be used to deploy the application with the publish-and-create command. It requires specifying:

  1. The location of the contract bytecode
  2. The location of the service bytecode
  3. The JSON encoded initialization arguments
linera publish-and-create \
  target/wasm32-unknown-unknown/release/my-counter_{contract,service}.wasm \
  --json-argument "42"

Interacting with the Application

To interact with the deployed application, a node service must be used.

Cross-Chain Messages

On Linera, applications are meant to be multi-chain: They are instantiated on every chain where they are used. An application has the same application ID and bytecode everywhere, but a separate state on every chain. To coordinate, the instances can send cross-chain messages to each other. A message sent by an application is always handled by the same application on the target chain: The handling code is guaranteed to be the same as the sending code, but the state may be different.

For your application, you can specify any serializable type as the Message type in your ContractAbi implementation. To send a message, use the ContractRuntime made available as an argument to the contract's [Contract::new] constructor. The runtime is usually stored inside the contract object, as we did when writing the contract binary. We can then call ContractRuntime::prepare_message to start preparing a message, and then send_to to send it to a destination chain.

    self.runtime
        .prepare_message(message_contents)
        .send_to(destination_chain_id);

It is also possible to send a message to a subscription channel, so that the message is forwarded to the subscribers of that channel. All that has to be done is specify a ChannelName as the destination parameter to send_to.

After block execution in the sending chain, sent messages are placed in the target chains' inboxes for processing. There is no guarantee that it will be handled: For this to happen, an owner of the target chain needs to include it in the incoming_messages in one of their blocks. When that happens, the contract's execute_message method gets called on their chain.

While preparing the message to be sent, it is possible to enable authentication forwarding and/or tracking. Authentication forwarding means that the message is executed with the same authenticated signer as the sender of the message, while tracking means that the message is sent back to the sender if the receiver skips it. The example below enables both flags:

    self.runtime
        .prepare_message(message_contents)
        .with_tracking()
        .with_authentication()
        .send_to(destination_chain_id);

Example: Fungible Token

In the fungible example application, such a message can be the transfer of tokens from one chain to another. If the sender includes a Transfer operation on their chain, it decreases their account balance and sends a Credit message to the recipient's chain:

async fn execute_operation(
    &mut self,
    operation: Self::Operation,
) -> Result<Self::Response, Self::Error> {
    match operation {
        // ...
        Operation::Transfer {
            owner,
            amount,
            target_account,
        } => {
            self.check_account_authentication(owner)?;
            self.state.debit(owner, amount).await?;
            self.finish_transfer_to_account(amount, target_account, owner)
                .await;
            Ok(FungibleResponse::Ok)
        }
        // ...
    }
}

async fn finish_transfer_to_account(
    &mut self,
    amount: Amount,
    target_account: Account,
    source: AccountOwner,
) {
    if target_account.chain_id == self.runtime.chain_id() {
        self.state.credit(target_account.owner, amount).await;
    } else {
        let message = Message::Credit {
            target: target_account.owner,
            amount,
            source,
        };
        self.runtime
            .prepare_message(message)
            .with_authentication()
            .send_to(target_account.chain_id);
    }
}

On the recipient's chain, execute_message is called, which increases their account balance.

async fn execute_message(&mut self, message: Message) -> Result<(), Self::Error> {
    match message {
        Message::Credit {
            amount,
            target,
            source,
        } => {
            // ...
            self.state.credit(receiver, amount).await;
        }
        // ...
    }

    Ok(())
}

Calling other Applications

We have seen that cross-chain messages sent by an application on one chain are always handled by the same application on the target chain.

This section is about calling other applications using cross-application calls.

Such calls happen on the same chain and are made with the ContractRuntime::call_application method:

pub fn call_application<A: ContractAbi + Send>(
    &mut self,
    authenticated: bool,
    application: ApplicationId<A>,
    call: &A::Operation,
) -> A::Response {

The authenticated argument specifies whether the callee is allowed to perform actions that require authentication on behalf of the signer of the original block that caused this call.

The application argument is the callee's application ID, and A is the callee's ABI.

The call argument is the operation requested by the application call.

Example: Crowd-Funding

The crowd-funding example application allows the application creator to launch a campaign with a funding target. That target can be an amount specified in any type of token based on the fungible application. Others can then pledge tokens of that type to the campaign, and if the target is not reached by the deadline, they are refunded.

If Alice used the fungible example to create a Pugecoin application (with an impressionable pug as its mascot), then Bob can create a crowd-funding application, use Pugecoin's application ID as CrowdFundingAbi::Parameters, and specify in CrowdFundingAbi::InitializationArgument that his campaign will run for one week and has a target of 1000 Pugecoins.

Now let's say Carol wants to pledge 10 Pugecoin tokens to Bob's campaign.

First she needs to make sure she has his crowd-funding application on her chain, e.g. using the linera request-application command. This will automatically also register Alice's application on her chain, because it is a dependency of Bob's.

Now she can make her pledge by running the linera service and making a query to Bob's application:

mutation { pledge(owner: "User:841…6c0", amount: "10") }

This will add a block to Carol's chain containing the pledge operation that gets handled by CrowdFunding::execute_operation, resulting in one cross-application call and two cross-chain messages:

First CrowdFunding::execute_operation calls the fungible application on Carol's chain to transfer 10 tokens to Carol's account on Bob's chain:

// ...
let call = fungible::Operation::Transfer {
    owner,
    amount,
    target_account,
};
// ...
self.runtime
    .call_application(/* authenticated by owner */ true, fungible_id, &call);

This causes Fungible::execute_operation to be run, which will create a cross-chain message sending the amount 10 to the Pugecoin application instance on Bob's chain.

After the cross-application call returns, CrowdFunding::execute_operation continues to create another cross-chain message crowd_funding::Message::PledgeWithAccount, which informs the crowd-funding application on Bob's chain that the 10 tokens are meant for the campaign.

When Bob now adds a block to his chain that handles the two incoming messages, first Fungible::execute_message gets executed, and then CrowdFunding::execute_message. The latter makes another cross-application call to transfer the 10 tokens from Carol's account to the crowd-funding application's account (both on Bob's chain). That is successful because Carol does now have 10 tokens on this chain and she authenticated the transfer indirectly by signing her block. The crowd-funding application now makes a note in its application state on Bob's chain that Carol has pledged 10 Pugecoin tokens.

For the complete code please take a look at the crowd-funding and fungible applications in the examples folder in linera-protocol.

Printing Logs from an Application

Applications can use the log crate to print log messages with different levels of importance. Log messages are useful during development, but they may also be useful for end users. By default the linera service command will log the messages from an application if they are of the "info" importance level or higher (briefly, log::info!, log::warn! and log::error!).

During development it is often useful to log messages of lower importance (such as log::debug! and log::trace!). To enable them, the RUST_LOG environment variable must be set before running linera service. The example below enables trace level messages from applications and enables warning level messages from other parts of the linera binary:

export RUST_LOG="warn,linera_execution::wasm=trace"

Writing Tests

Linera applications can be tested using unit tests or integration tests. Both are a bit different than usual Rust tests. Unit tests are executed inside a WebAssembly virtual machine in an environment that simulates a single microchain and a single application. System APIs are only available if they are mocked using helper functions from linera_sdk::test.

Integration tests run outside a WebAssembly virtual machine, and use a simulated validator for testing. This allows creating chains and adding blocks to them in order to test interactions between multiple microchains and multiple applications.

Applications should consider having both types of tests. Unit tests should be used to focus on the application's internals and core functionality. Integration tests should be used to test how the application behaves on a more complex environment that's closer to the real network.

In most cases, the simplest way to run both unit tests and integration tests is to call linera project test from the project's directory.

Unit tests

Unit tests are written beside the application's source code (i.e., inside the src directory of the project). There are several differences to normal Rust unit tests:

  • the target wasm32-unknown-unknown must be selected;

  • the custom test runner linera-wasm-test-runner must be used;

  • the #[webassembly_test] attribute must be used instead of the usual #[test] attribute.

The first two items are done automatically by linera project test.

Alternatively, one may set up the environment and run cargo test directly as described below.

Example

A simple unit test is shown below, which tests if the application's do_something method changes the application state.

#[cfg(test)]
mod tests {
    use crate::state::ApplicationState;
    use webassembly_test::webassembly_test;

    #[webassembly_test]
    fn test_do_something() {
        let mut application = ApplicationState {
            // Configure the application's initial state
            ..ApplicationState::default()
        };

        let result = application.do_something();

        assert!(result.is_ok());
        assert_eq!(application, ApplicationState {
            // Define the application's expected final state
            ..ApplicationState::default()
        });
    }
}

Mocking System APIs

Unit tests run in a constrained environment, so things like access to the key-value store, cross-chain messages and cross-application calls can't be executed. However, they can be simulated using mock APIs. The linera-sdk::test module provides some helper functions to mock the system APIs.

Here's an example mocking the key-value store.

#[cfg(test)]
mod tests {
    use crate::state::ApplicationState;
    use linera_sdk::test::mock_key_value_store;
    use webassembly_test::webassembly_test;

    #[webassembly_test]
    fn test_state_is_not_persisted() {
        let mut storage = mock_key_value_store();

        // Assuming the application uses views
        let mut application = ApplicationState::load(storage.clone())
            .now_or_never()
            .expect("Mock key-value store returns immediately")
            .expect("Failed to load view from mock key-value store");

        // Assuming `do_something` changes the view, but does not persist it
        let result = application.do_something();

        assert!(result.is_ok());

        // Check that the state in memory is different from the state in storage
        assert_ne!(application, ApplicatonState::load(storage));
    }
}

Running Unit Tests with cargo test

Running linera project test is easier, but if there's a need to run cargo test explicitly to run the unit tests, Cargo must be configured to use the custom test runner linera-wasm-test-runner. This binary can be built from the repository or installed with cargo install --locked linera-sdk.

cd linera-protocol
cargo build -p linera-sdk --bin linera-wasm-test-runner --release

The steps above build the linera-wasm-test-runner and places the resulting binary at linera-protocol/target/release/linera-wasm-test-runner.

With the binary available, the last step is to configure Cargo. There are a few ways to do this. A quick way is to set the CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER environment variable to the path of the binary.

A more persistent way is to change one of Cargo's configuration files. As an example, the following file can be placed inside the project's directory at PROJECT_DIR/.cargo/config.toml:

[target.wasm32-unknown-unknown]
runner = "PATH_TO/linera-wasm-test-runner"

After configuring the test runner, unit tests can be executed with

cargo test --target wasm32-unknown-unknown

Optionally, wasm32-unknown-unknown can be made the default build target with the following lines in PROJECT_DIR/.cargo/config.toml:

[build]
target = "wasm32-unknown-unknown"

Integration Tests

Integration tests are usually written separately from the application's source code (i.e., inside a tests directory that's beside the src directory).

Integration tests are normal Rust integration tests, and they are compiled to the host target instead of the wasm32-unknown-unknown target used for unit tests. This is because unit tests run inside a WebAssembly virtual machine and integration tests run outside a virtual machine, starting isolated virtual machines to run each operation of each block added to each chain.

Integration tests can be run with linera project test or simply cargo test.

If you wish to use cargo test and have overridden your default target to be in wasm32-unknown-unknown in .cargo/config.toml, you will have to pass a native target to cargo, for instance cargo test --target aarch64-apple-darwin.

Integration tests use the helper types from linera_sdk::test to set up a simulated Linera network, and publish blocks to microchains in order to execute the application.

Example

A simple test that sends a message between application instances on different chains is shown below.

#[tokio::test]
async fn test_cross_chain_message() {
    let (validator, application_id) = TestValidator::with_current_application(vec![], vec![]).await;

    let mut sender_chain = validator.get_chain(application_id.creation.chain_id).await;
    let mut receiver_chain = validator.new_chain().await;

    sender_chain
        .add_block(|block| {
            block.with_operation(
                application_id,
                Operation::SendMessageTo(receiver_chain.id()),
            )
        })
        .await;

    receiver_chain.handle_received_messages().await;

    assert_eq!(
        receiver_chain
            .query::<ChainId>(application_id, Query::LastSender)
            .await,
        sender_chain.id(),
    );
}

Advanced Topics

In this section, we present additional topics related to the Linera protocol.

Views

Views are a specific functionality of the Linera system that allow to have data in memory and then seamlessly flush it to an underlying persistent datastore.

The full documentation is available on the crate documentation with all functions having examples.

Concretely, what is provided is the following:

  • A trait View that provides load, rollback, clear, flush, delete. The idea is that we can do operation on the data and then flush it to the database storing them.

  • Several other traits HashableView, RootView, CryptoHashView, CryptoHashRootView that are important for computing hash.

  • A number of standard containers: MapView, SetView, LogView, QueueView, RegisterView that implement the View and HashableView traits.

  • Two containers CollectionView and ReentrantCollectionView that are similar to MapView but whose values are views themselves.

  • Derive macros that allow to implement the above mentioned traits on struct data types whose entries are views.

Persistent storage

Validators run the servers and the data is stored in persistent storage. As a consequence we need a tool for working with persistent storage and so we have added linera-db for that purpose.

Available persistent storage

The persistent storage that are available right now are RocksDB, DynamoDB and ScyllaDB. Each has its own strengths and weaknesses.

  • RocksDB: Data is stored on disk and cannot be shared between shards but is very fast.

  • DynamoDB: Data is stored on a remote storage, that has to be on AWS. Data can be shared between shards.

  • ScyllaDB: Data is stored on a remote storage. Data can be shared between shards.

There is no fundamental obstacle to the addition of other persistent storage solutions.

In addition, the DynamoDB and ScyllaDB have the notion of a table which means that a given remote location can be used for several completely independent purposes.

The linera-db tool

When operating on a persistent storage some global operations can be required. The command line tool linera-db helps in making them work.

The functionalities are the following:

  • list_tables(DynamoDB and ScyllaDB): It lists all the tables that have been created on the persistent storage

  • initialize(RocksDB, DynamoDB and ScyllaDB): It initializes a persistent storage.

  • check_existence(RocksDB, DynamoDB and ScyllaDB): It tests the existence of a persistent storage. If the error code is 0 then the table exists, if the error code is 1 then the table is absent.

  • check_absence(RocksDB, DynamoDB and ScyllaDB): It tests the absence of a persistent storage. If the error code is 0 then the table is absent, if the error code is 1 then the table does not exist.

  • delete_all(RocksDB, DynamoDB and ScyllaDB): It deletes all the table of a persistent storage.

  • delete_single(DynamoDB and ScyllaDB): It deletes a single table of a persistent storage.

If some error occurs during the operation, then the error code 2 is returned and 0 if everything went fine with the exception of check_existence and check_absence for which the value 1 can occur if the connection with the database was established correctly but the result is not what we expected.

Contract Finalization

When a transaction finishes executing successfully, there's a final step where all loaded application contracts are allowed to finalize, similarly to executing a destructor. The default implementation of finalize just persists the application's state:

    /// Finishes the execution of the current transaction.
    async fn finalize(&mut self) -> Result<(), Self::Error> {
        Self::Storage::store(self.state_mut()).await;
        Ok(())
    }

Applications may want to override the finalize method, which allows performing some clean-up operations after execution finished. While finalizing, contracts may send messages, read and write to the state, but are not allowed to call other applications, because they are all also finalizing.

If finalize is overriden, the default implementation is not executed, so the developer must ensure that the application's state is persisted correctly.

While finalizing, contracts can force the transaction to fail by panicking or returning an error. The block is then rejected, even if the entire transaction's operation had succeeded before finalize was called. This allows a contract to reject transactions if other applications don't follow any required constraints it establishes after it responds to a cross-application call.

As an example, a contract that executes a cross-application call with Operation::StartSession may require the same caller to perform another cross-application call with Operation::EndSession before the transaction ends.

pub struct MyContract {
    state: MyState;
    runtime: ContractRuntime<Self>;
    active_sessions: HashSet<ApplicationId>;
}

#[async_trait]
impl Contract for MyContract {
    type Error = MyError;
    type State = MyState;
    type Storage = ViewStateStorage<Self>;
    type Message = ();

    async fn new(state: Self::State, runtime: ContractRuntime<Self>) -> Result<Self, Self::Error> {
        MyContract {
            state,
            runtime,
            active_sessions: HashSet::new(),
        }
    }

    fn state_mut(&mut self) -> &mut Self::State {
        &mut self.state
    }

    async fn initialize(
        &mut self,
        argument: Self::InitializationArgument,
    ) -> Result<(), Self::Error> {
        Ok(())
    }

    async fn execute_operation(
        &mut self,
        operation: Self::Operation,
    ) -> Result<Self::Response, Self::Error> {
        let caller = self.runtime
            .authenticated_caller_id()
            .expect("Missing caller ID");

        match operation {
            Operation::StartSession => {
                assert!(
                    self.active_sessions.insert(caller_id),
                    "Can't start more than one session for the same caller"
                );
            }
            Operation::EndSession => {
                assert!(
                    self.active_sessions.remove(&caller_id),
                    "Session was not started"
                );
            }
        }
    }

    async fn execute_message(&mut self, message: Self::Message) -> Result<(), Self::Error> {
        unreachable!("This example doesn't support messages");
    }

    async fn finalize(&mut self) -> Result<(), Self::Error> {
        assert!(
            self.active_sessions.is_empty(),
            "Some sessions have not ended"
        );

        Self::Storage::store(self.state_mut()).await;

        Ok(())
    }
}

Validators

Validators run the servers that allow users to download and create blocks. They validate, execute and cryptographically certify the blocks of all the chains.

In Linera, every chain is backed by the same set of validators and has the same level of security.

The main function of validators is to guarantee the integrity of the infrastructure in the sense that:

  • Each block is valid, i.e. it has the correct format, its operations are allowed, the received messages are in the correct order, and e.g. the balance was correctly computed.

  • Every message received by one chain was actually sent by another chain.

  • If one block on a particular height is certified, no other block on the same height is.

These properties are guaranteed to hold as long as two third of the validators (weighted by their stake) follow the protocol. In the future, deviating from the protocol may cause a validator to be considered malicious and to lose their stake.

Validators also play a role in the liveness of the system by making sure that the history of the chains stays available. However, since validators do not propose blocks on most chains (see next section), they do not guarantee that any particular operation or message will eventually be executed on a chain. Instead, chain owners decide whether and when to propose new blocks, and which operations and messages to include. The current implementation of the Linera client automatically includes all incoming messages in new blocks. The operations are the actions the chain owner explicitly adds, e.g. transfer.

Architecture of a validator

Since every chain uses the same validators, adding more chains does not require adding validators. Instead, it requires each individual validator to scale out by adding more computation units, also known as "workers" or "physical shards".

In the end, a Linera validator resembles a Web2 service made of

  • a load balancer (aka. ingress/egress), currently implemented by the binary linera-proxy,

  • a number of workers, currently implemented by the binary linera-server,

  • a shared database, currently implemented by the abstract interface linera-storage.

Example of Linera network

                    │                                             │
                    │                                             │
┌───────────────────┼───────────────────┐     ┌───────────────────┼───────────────────┐
│ validator 1       │                   │     │ validator N       │                   │
│             ┌─────┴─────┐             │     │             ┌─────┴─────┐             │
│             │   load    │             │     │             │   load    │             │
│       ┌─────┤  balancer ├────┐        │     │       ┌─────┤  balancer ├──────┐      │
│       │     └───────────┘    │        │     │       │     └─────┬─────┘      │      │
│       │                      │        │     │       │           │            │      │
│       │                      │        │     │       │           │            │      │
│  ┌────┴─────┐           ┌────┴─────┐  │     │  ┌────┴───┐  ┌────┴────┐  ┌────┴───┐  │
│  │  worker  ├───────────┤  worker  │  │ ... │  │ worker ├──┤  worker ├──┤ worker │  │
│  │    1     │           │    2     │  │     │  │    1   │  │    2    │  │    3   │  │
│  └────┬─────┘           └────┬─────┘  │     │  └────┬───┘  └────┬────┘  └────┬───┘  │
│       │                      │        │     │       │           │            │      │
│       │                      │        │     │       │           │            │      │
│       │     ┌───────────┐    │        │     │       │     ┌─────┴─────┐      │      │
│       └─────┤  shared   ├────┘        │     │       └─────┤  shared   ├──────┘      │
│             │ database  │             │     │             │ database  │             │
│             └───────────┘             │     │             └───────────┘             │
└───────────────────────────────────────┘     └───────────────────────────────────────┘

Inside a validator, components communicate using the internal network of the validator. Notably, workers use direct Remote Procedure Calls (RPCs) with each other to deliver cross-chain messages.

Note that the number of workers may vary for each validator. Both the load balancer and the shared database are represented as a single entity but are meant to scale out in production.

For local testing during development, we currently use a single worker and RocksDB as a database.

Creating New Blocks

In Linera, the responsibility of proposing blocks is separate from the task of validating blocks.

While all chains are validated in the same way, the Linera protocol defines several types of chains, depending on how new blocks are produced.

  • The simplest and lowest-latency type of chain is called single-owner chain.

  • Other types of Linera chains not currently supported in the SDK include permissioned chains and public chains (see the whitepaper for more context).

For most types of chains (all but public chains), Linera validators do not need to exchange messages with each other.

Instead, the wallets (aka. linera clients) of chain owners make the system progress by proposing blocks and actively providing any additional required data to the validators. For instance, client commands such as transfer, publish-bytecode, or open-chain perform multiple steps to append a block containing the token transfer, application publishing, or chain creation operation:

  • The Linera client creates a new block containing the desired operation and new incoming messages, if there are any. It also contains the most recent block's hash to designate its parent. The client sends the new block to all validators.

  • The validators validate the block, i.e. check that the block satisfies the conditions listed above, and send a cryptographic signature to the client, indicating that they vote to append the new block. But only if they have not voted for a different block on the same height earlier!

  • The client ideally receives a vote from every validator, but only a quorum of votes (say, two thirds) are required: These constitute a "certificate", proving that the block was confirmed. The client sends the certificate to every validator.

  • The validators "execute" the block: They update their own view of the most recent state of the chain by applying all messages and operations, and if it generated any cross-chain messages, they send these to the appropriate workers.

To guarantee that each incoming message in a block was actually sent by another chain, a validator will, in the second step, only vote for a block if it has already executed the block that sent it. However, when receiving a valid certificate for a block that receives a message it has not seen yet, it will accept and execute the block anyway. The certificate is proof that most other validators have seen the message, so it must be correct.

In the case of single-owner chains, clients must be carefully implemented so that they never propose multiple blocks at the same height. Otherwise, the chain may be stuck: once each of the two conflicting blocks has been signed by enough validators, it becomes impossible to collect a quorum of votes for either block.

In the future, we anticipate that most users will use permissioned chains even if they are the only owners of their chains. Permissioned chains have two confirmation steps instead of one, but it is not possible to accidentally make a chain unextendable. They also allow users to delegate certain admistrative tasks to third-parties, notably to help with epoch changes (i.e. when the validators change if reconfigured).

Applications that Handle Assets

In general, if you send tokens to a chain owned by someone else, you rely on them for asset availability: if they don't handle your messages, you don't have access to your tokens.

Fortunately, Linera provides a solution based on temporary chains: if the number of parties who want to participate is limited and known in advance, we can:

  • make them all chain owners using the linera change-ownership command,
  • allow only one application's operations on the chain,
  • and allow only that operation to close the chain, using linera change-application-permissions.

Such an application should have a designated operation or message that causes it to close the chain: when that operation is executed, it should send back all remaining assets, and call the runtime's close_chain method.

Once the chain is closed, owners can still create blocks rejecting messages. That way, even assets that are in flight can be returned.

The matching-engine example application does this:

    async fn execute_operation(&mut self, operation: Operation) -> Result<(), Self::Error> {
        match operation {
            // ...
            Operation::CloseChain => {
                for order_id in self.state.orders.indices().await? {
                    match self.modify_order(order_id, ModifyAmount::All).await {
                        Ok(transfer) => self.send_to(transfer),
                        // Orders with amount zero may have been cleared in an earlier iteration.
                        Err(MatchingEngineError::OrderNotPresent) => continue,
                        Err(error) => return Err(error),
                    }
                }
                self.runtime
                    .close_chain()
                    .map_err(|_| MatchingEngineError::CloseChainError)?;
            }
        }
    }

This enables doing atomic swaps using the Matching Engine: if you make a bid, you are guaranteed that at any point in time you can get back either the tokens you are offering or the tokens you bought.

Appendix

Congrats on reading this manual to the end! In this section, we will be sharing additional material.

Glossary

  • Address: A unique public alphanumeric identifier used to designate the identity of an entity on the Linera network.

  • Admin Chain: The Linera Network has one designated admin chain where validators can join or leave and where new epochs are defined.

  • Application: Similar to a smart-contract on Ethereum, an application is code deployed on the Linera network which is executed by all validators. An application has a metered contract which executes 'business logic' and modifies state and an unmetered 'service' which is a read-only view into an application's state.

  • Byzantine Fault-Tolerant (BFT): A system which can operate correctly and achieve consensus even if components of the system fail or act maliciously.

  • Block Height: The number of blocks preceding a given block on a specific microchain.

  • Block Proposal: A candidate block proposed by a chain owner which may be selected at the next block height.

  • Bytecode: A collection of bytes corresponding to a program that can be run by the Wasm virtual machine.

  • Client: The linera program, which is a local node and wallet operated by users to make requests to the network. In Linera, clients drive the network by proposing new blocks and validators are mostly reactive.

  • Certificate: A value with signatures from a quorum of validators. Values can be confirmed blocks, meaning that the block has been added to the chain and is final. There are other values that are used for reaching consensus, before certifying a confirmed block.

  • Committee: The set of all validators for a particular epoch, together with their voting weights.

  • Chain Owner: The owner of a user chain or permissioned chain. This is represented as the alphanumeric identifier derived from the hash of the owner's public key.

  • Channel: A broadcast mechanism enabling publish-subscribe behavior across chains.

  • Contract: The metered part of an application which executes business logic and can modify the application's state.

  • Cross-Application Call: A call from one application to another on the same chain.

  • Cross-Chain Message: A message containing a data payload which is sent from one chain to another. Cross-Chain messages are the asynchronous communication primitive which enable communication on the same application running on different chains.

  • Epoch: A period of time when a particular set of validators with particular voting weights can certify new blocks. Since each chain has to transition explicitly from one epoch to the next, epochs can overlap.

  • Genesis Configuration: The configuration determining the state of a newly created network; the voting weights of the initial set of validators, the initial fee structure, and initial chains that the network starts with.

  • Inbox: A commutative data structure storing incoming messages for a given chain.

  • Message: See 'Cross-Chain Message'.

  • Microchain: A lightweight chain of blocks holding a subset of the network's state running on every validator. This is used interchangeably with 'chain'. All Linera chains are microchains.

  • Network: The totality of all protocol participants. A network is the combination of committee, clients and auditors.

  • Operation: Operations are either transactions directly added to a block by the creator (and signer) of the block, or calls to an application from another. Users typically use operations to start interacting with an application on their own chain.

  • Permissioned Chain: A microchain which is owned by more than one user. Users take turns proposing blocks and the likelihood of selection is proportional to their weight.

  • Project: The collection of files and dependencies which are built into the bytecode which is instantiated as an application on the Linera Network.

  • Public Chain: A microchain with full BFT consensus with a strict set of permissions relied on for the operation of the network,

  • Quorum: A set of validators representing > ⅔ of the total stake. A quorum is required to create a certificate.

  • Single-Owner Chain: See 'User Chain'.

  • Service: An unmetered read-only view into an application's state.

  • Shard: A logical subset of all microchains on a given validator. This corresponds directly to a physical worker.

  • Stake: An amount of tokens pledged by a validator or auditor, as a collateral to guarantee their honest and correct participation in the network.

  • User Chain: Used interchangeably with Single-Owner Chain. User chains are chains which are owned by a single user on the network. Only the chain owner can propose blocks, and therefore only the chain owner can forcibly advance the state of a user chain.

  • Validator: Validators run the servers that allow users to download and create blocks. They validate, execute and cryptographically certify the blocks of all the chains.

  • View: Views are like an Object-Relational Mapping (ORM) for mapping complex types onto key-value stores. Views group complex state changes into a set of elementary operations and commit them atomically. They are full or partial in-memory representations of complex types saved on disk in a key-value store

  • Wallet: A file containing a user's public and private keys along with configuration and information regarding the chains they own.

  • WebAssembly (Wasm): A binary compilation target and instruction format that runs on a stack-based VM. Linera applications are compiled to Wasm and run on Wasm VMs inside validators and clients.

  • Web3: A natural evolution of the internet focusing on decentralization by leveraging blockchains and smart contracts.

  • Worker: A process which runs a subset of all microchains on a given validator. This corresponds directly to a logical shard.

Videos

We're consistently improving the SDK and creating new material. We recorded the following tutorial videos for the Linera Developer Summer School 2023:

Happy viewing!