Skip to main content

Write and deploy an LEZ program with logos-scaffold

Use logos-scaffold to create, build, and deploy a guest program on a local Logos Execution Zone sequencer.​

Version

This document is accurate for logos-scaffold 0.4.0 built from master at commit d4b15ff, which pins LEZ v0.1.2. It deploys to a local sequencer only; see Deploy to the testnet.

logos-scaffold is a project scaffold and CLI tool that manages the full lifecycle of a LEZ guest program—from project creation to deployment. It pins LEZ and SPEL dependencies, builds a project-local sequencer, and handles wallet interactions, so you can focus on writing your program logic.

This guide walks each stage separately so you can see what the tool does. Once the pieces are familiar, logos-scaffold run chains them into a single command. For what else scaffold manages, including Basecamp modules, see About Logos Scaffold.

Prerequisites
  • A supported OS:

    • Linux: x86_64
    • macOS
  • git, and Rust 1.81 or newer with cargo.

  • The Unix process helpers lsof, ps, and kill, and curl.

  • The RISC Zero toolchain, installed with rzup:

    curl -L https://risczero.com/install | bash # installs rzup; restart your shell afterwards
    rzup install rust
    rzup install cargo-risczero 3.0.5

    rust builds the guest programs. cargo-risczero provides r0vm, which the sequencer needs to execute transactions, and it must be the version LEZ v0.1.2 pins, 3.0.5: scaffold only looks for that version, under ~/.risc0/extensions/. Without it, localnet start still reports localnet ready, with a warning about RISC0_SERVER_PATH, and deploy then fails. If you installed rzup with a custom RISC0_HOME, export RISC0_SERVER_PATH with the full path to that r0vm.

  • Docker, with the daemon running. Scaffold builds guest programs inside the pinned risczero/risc0-guest-builder container by default, so that a program_id is the same on every machine. Without Docker, opt out as described in Step 5.

  • About 5 GB of free disk space for the pinned LEZ and spel builds.

Nix is not needed for this guide, only for scaffold's basecamp commands. If it is not installed, logos-scaffold doctor reports a tool nix warning that you can ignore.

What to expect​

  • You can create an LEZ program project and write a guest program that runs inside the RISC0 zkVM.
  • You can build your program and deploy it to a project-local LEZ sequencer.
  • You can call your deployed program and repeat the whole loop with one command.

Step 1: Install logos-scaffold​

  1. Install the CLI from the scaffold repository, pinned to the commit this guide was tested with:

    cargo install --locked --git https://github.com/logos-co/scaffold --rev d4b15ff logos-scaffold

    This installs two binaries on your PATH: logos-scaffold and the shorter alias lgs. They are functionally identical. The logos-scaffold 0.4.0 release on crates.io predates the Docker guest build in Step 5, so cargo install logos-scaffold gives you a CLI without --guest and with guest artifacts in a different place.

  2. Verify the installation:

    logos-scaffold --version

Step 2: Create a new project​

  1. Create a new LEZ program project. Replace my-program with your project name:

    logos-scaffold new my-program
    cd my-program

    This generates a project with the default template, which includes sample guest programs and runner scripts. For an Anchor-style framework with IDL and generated client bindings, pass --template lez-framework instead; this guide follows the default template.

    info

    new also clones the pinned LEZ source into scaffold's shared cache; with the builds from Step 3 the cache grows to about 3.5 GB. To keep it somewhere else, pass --cache-root <path> or set LOGOS_SCAFFOLD_CACHE_ROOT.

  2. Inspect the project layout:

    my-program/
    ├── scaffold.toml # Project configuration and dependency pins
    ├── methods/
    │ └── guest/
    │ └── src/bin/ # Guest programs run inside the RISC0 zkVM
    ├── src/
    │ └── bin/ # Runner scripts that submit transactions
    ├── AGENTS.md # Guidance for AI coding assistants
    ├── .claude/skills/ # The same guidance for Claude Code
    ├── .cursor/rules/ # The same guidance for Cursor
    └── .scaffold/ # Local state, wallet home, and build artifacts

Step 3: Set up the project​

  1. Run setup to sync the LEZ and SPEL repositories to their pinned commits, build the project-local sequencer and wallet binaries, and seed the default wallet:

    warning

    Scaffold unlocks the wallet it seeds with a deterministic local password, so the onboarding flow needs no prompts. To use your own, export it before the first setup (or the first run, which chains setup):

    export LOGOS_SCAFFOLD_WALLET_PASSWORD='<your-local-dev-password>'

    If the storage was already created under the default password, export the override and re-seed with logos-scaffold run --reset. These are development-only keys; never use them for real funds.

    logos-scaffold setup

    On a cold cache this step builds the sequencer, wallet, and spel from source, which takes about 20 minutes on a four-core machine. Later runs reuse the build.

  2. Check the project's environment. doctor reports missing tools and configuration problems, with a next step for each:

    logos-scaffold doctor

    At this point expect three WARN rows about the sequencer, because the localnet is not running yet. Step 6 clears them. A FAIL on the guest build row means docker or cargo-risczero is not on your PATH; see Step 5.

Step 4: Write your guest program​

Guest programs run inside the RISC0 zkVM and define the on-chain logic of your LEZ program. Each guest program in methods/guest/src/bin/ becomes a deployable program with its own program_id.

  1. Open the sample guest program:

    $EDITOR methods/guest/src/bin/hello_world.rs
  2. Read how the program works. It receives a ProgramInput struct via the zkVM environment, applies your logic, and writes a ProgramOutput struct to the journal. The sequencer verifies the proof and updates the on-chain account state.

    Key concepts:

    • Instructions are passed as bytes. hello_world takes its instruction as a raw Vec<u8> and appends it to the account's data.
    • Account data is stored in AccountWithMetadata structs.
    • Use RISC0_DEV_MODE=1 during development to skip ZK proof generation for faster iteration.

Step 5: Build the project​

  1. Build the workspace. In development, use RISC0_DEV_MODE=1 to skip proof generation:

    RISC0_DEV_MODE=1 logos-scaffold build

    The build compiles the workspace, then builds each guest program inside the pinned risczero/risc0-guest-builder Docker image and writes the .bin artifacts to target/riscv-guest-docker/riscv32im-risc0-zkvm-elf/docker/. The same source then produces the same bytes, and therefore the same program_id, on any machine. The first build takes several minutes.

    If the Docker daemon is not running, build stops straight away:

    error: [build].guest = "docker" needs a running Docker daemon; `docker info` failed.
  2. To build without Docker, use the host RISC Zero toolchain instead. For one build, pass --guest local:

    RISC0_DEV_MODE=1 logos-scaffold build --guest local

    To make it the project default, which logos-scaffold run needs because it has no --guest flag, add this to scaffold.toml:

    [build]
    guest = "local"

    A local build writes the artifacts to target/riscv-guest/…/riscv32im-risc0-zkvm-elf/release/ instead. Its program_id depends on your host's Rust and clang versions, so it can differ from the one another machine builds from the same source. Each build removes the other mode's artifacts, so deploy always ships the output of the last build. logos-scaffold doctor shows the active mode in its guest build row.

Step 6: Start a local sequencer​

  1. Start a project-local sequencer to deploy your program to:

    RISC0_DEV_MODE=1 logos-scaffold localnet start

    start returns once the sequencer process is alive and its RPC port answers. The sequencer is daemonised and survives terminal or tmux session closure. Use logos-scaffold localnet status to check that it is running and logos-scaffold localnet stop to stop it.

    warning

    The local sequencer always uses port 3040. If something else already listens there, such as another scaffold project's sequencer, localnet start fails with Address already in use (os error 98), and localnet status reports ownership: foreign. Stop the other process first. Changing port under [localnet] in scaffold.toml does not help: the sequencer still binds 3040 and the wallet keeps pointing at it, so deploy and wallet topup would talk to the other sequencer.

    For any other failure, read the sequencer log with logos-scaffold localnet logs --tail 200. To wipe chain state and start over, run logos-scaffold localnet reset --yes.

Step 7: Deploy your program​

  1. Deploy all guest programs to the running sequencer:

    RISC0_DEV_MODE=1 logos-scaffold deploy
    • After each successful submission, logos-scaffold prints program_id: <hex>, the RISC0 image ID computed locally from the submitted ELF. It prints program_id: unavailable if the project's spel binary has not been built yet. The example runner scripts in Step 8 load the program from its embedded ELF, so you do not need to copy a program_id to complete this guide.
    • deploy checks that the sequencer is reachable before submitting, and stops with a hint if it is not. It submits one program per block, so deploying the five sample programs takes about a minute.
    • Add --json for machine-readable output: one object per program under {"deploys": [...]}, or a single object with --program-path … --json.
    info

    deploy confirms submission, not inclusion. Redeploying a program that is already on the sequencer still reports submitted, while the sequencer skips the transaction and logs ProgramAlreadyExists, which you can see with logos-scaffold localnet logs.

  2. To deploy a specific program by name:

    RISC0_DEV_MODE=1 logos-scaffold deploy hello_world

Step 8: Interact with your program​

Use the project-local wallet CLI to submit transactions to your deployed program. The wallet is available at logos-scaffold wallet.

  1. With the sequencer from Step 6 running, top up the default wallet on your local network, then list your accounts (wallet list shows accounts, not a balance):

    logos-scaffold wallet topup
    logos-scaffold wallet list

    topup initialises the account first if it is new, then claims from the Piñata faucet. To confirm the wallet can reach the sequencer, run logos-scaffold wallet -- check-health. Everything after -- goes to the project's wallet binary unchanged.

  2. Create a fresh public account for the program to claim. hello_world only writes to an account that is uninitialised or already owned by the program, so the topped-up default account does not work:

    logos-scaffold wallet -- account new public

    The output includes account_id Public/<ID>. Pass the part after Public/ to the runner.

  3. Run one of the example runner scripts that submit transactions to your program:

    export NSSA_WALLET_HOME_DIR="$(pwd)/.scaffold/wallet"
    export LEE_WALLET_HOME_DIR="$(pwd)/.scaffold/wallet"
    RISC0_DEV_MODE=1 cargo run --bin run_hello_world -- <ID>

    The LEZ v0.1.2 wallet that scaffold pins by default reads NSSA_WALLET_HOME_DIR, and LEZ v0.2.0 and later read LEE_WALLET_HOME_DIR. Exporting both keeps the runner working on either pin.

    The runner prints the transaction hash. Once the next block is produced, the account holds the greeting Hola mundo!, hex-encoded in data:

    logos-scaffold wallet -- account get --account-id Public/<ID>
    {"balance":0,"program_owner":"...","data":"486f6c61206d756e646f21","nonce":1}

    The runner scripts in src/bin/ demonstrate how to construct and sign a PublicTransaction, set the program_id, encode an instruction, and submit the transaction via the sequencer RPC.

Use logos-scaffold run for the inner loop​

Steps 5 to 8 are often repeated when working on developing an LEZ program. logos-scaffold run chains them into one command, and works with no configuration:

RISC0_DEV_MODE=1 logos-scaffold run

It runs these steps in order, printing progress for each:

  1. Build, which chains setup internally.
  2. Build the IDL, a no-op for projects that are not built on the lez-framework template.
  3. Start localnet, or reuse the one already running.
  4. Top up the default wallet.
  5. Deploy, skipped when the guest binaries, IDL, config, and sequencer are all unchanged since the last run. A manual logos-scaffold deploy does not count, so the first run deploys again.
  6. Run post_deploy hooks, if the project configures any.

Useful flags:

FlagEffect
--profile NAMESelect a named pipeline from [run.profiles.<name>].
--reset / --no-resetWipe sequencer state and wallet and re-seed before the run, or override a config-set default.
--post-deploy <cmd>Replace the configured hooks for this run. Repeatable.
--no-post-deploySkip hooks entirely.
--watchRe-run the pipeline when files change.

Configure the loop in scaffold.toml:

[run]
post_deploy = ["cargo run --bin run_hello_world -- <ID>"]

[run.profiles.demo]
topup = false
deploy = false
post_deploy = ["scripts/demo.sh"]
  • topup = false suits a project that funds its own accounts, and deploy = false one that deploys from a hook. Both default to true.
warning

A named profile is used as-is: selecting one shadows the inline [run] values instead of inheriting them. A key the profile does not state falls back to its own default, not to your [run] value. Set every key you need in each profile.

Hooks run through sh -c from the project root with these variables set:

VariableValue
SEQUENCER_URLThe localnet RPC URL.
NSSA_WALLET_HOME_DIR, LEE_WALLET_HOME_DIRThe project wallet directory, under both the name LEZ v0.1.2 reads and the one v0.2.0 and later read.
SCAFFOLD_PROJECT_ROOT, SCAFFOLD_IDL_DIRAbsolute paths to the project root and the IDL output directory.
SCAFFOLD_TOPUP_SKIPPED, SCAFFOLD_DEPLOY_SKIPPED1 or 0. Always set, so branch on the value rather than on whether the variable exists.
SCAFFOLD_PROGRAMSSpace-separated names of the project's deployable programs.
SCAFFOLD_PROGRAM_ID, SCAFFOLD_GUEST_BINThe deployed program's image ID and guest binary. Set only when the project has exactly one deployable program, so a multi-program project fails loudly instead of picking the wrong one.

run covers the deploy loop only. It does not run any basecamp command; for that side of scaffold see Develop a Logos module with Logos Scaffold.

Deploy to the testnet​

warning

logos-scaffold cannot deploy to the current LEZ public testnet. Use the local sequencer from this guide.

The public testnet runs LEZ v0.3, and scaffold pins v0.1.2 by default. Pointing the project wallet at https://testnet.lez.logos.co/ fails the compatibility check: logos-scaffold wallet -- check-health panics with Missing authenticated transfer ID from remote, and logos-scaffold doctor reports FAIL | wallet usability.

To deploy to the testnet today, set up a standalone wallet as described in Run an LEZ wallet via the CLI, built from the LEZ release the testnet runs, and use its deploy-program command with a guest program built against that same release. Check the wallet with wallet check-health first: at the time of writing, a wallet built from LEZ v0.3.1 passes against the testnet and one built from scaffold's v0.1.2 pin does not.