logos-devnet 0.3.8

A local LEZ chain for Logos development: one command to start it, fund accounts and deploy programs
# logos-devnet

A local LEZ chain for Logos development. One command starts a sequencer on your machine with 1 second blocks and a funded faucet, and a project file in your repo says what else it needs: programs, a hundred funded test accounts, `logosctl` sessions. A transfer lands in about 2 seconds and a program deploy in about 10, and you never run out of testnet funds.

Keep the public testnet for final checks. Messaging (chat, discovery) isn't part of this: it stays on the Logos fleets.

## Install

```bash
cargo install --locked logos-devnet
```

Or take a binary from the [latest release](https://github.com/drptbl/logos-devnet/releases/latest), built for macOS (`aarch64-apple-darwin`, `x86_64-apple-darwin`) and Linux (`x86_64-unknown-linux-musl`, `aarch64-unknown-linux-musl`, static, so any distribution), and check it against its `.sha256`:

```bash
v=v0.3.8 t=aarch64-apple-darwin
curl -fsSL --remote-name-all https://github.com/drptbl/logos-devnet/releases/download/$v/logos-devnet-$v-$t.tar.gz{,.sha256}
shasum -a 256 -c logos-devnet-$v-$t.tar.gz.sha256 \
  && tar -xzf logos-devnet-$v-$t.tar.gz \
  && mkdir -p ~/.local/bin && install logos-devnet-$v-$t/logos-devnet ~/.local/bin/
```

Put `~/.local/bin` on your `PATH` if it isn't. `cargo install --locked --git https://github.com/drptbl/logos-devnet` builds the latest `main`.

It runs on macOS and Linux, and needs Docker (OrbStack, Docker Desktop or Colima). There's no Windows build. With `--native` you need git, a Rust toolchain and LEZ's build dependencies instead: clang, pkg-config and OpenSSL (`brew install pkg-config openssl` on macOS), plus `libpcsclite-dev` on Linux. The first native build takes a few minutes.

## Use it

```bash
logos-devnet doctor                                # what's missing, and how to fix it
logos-devnet up                                    # start, or resume
logos-devnet fund Public/<your account id>         # 10B lepta from the faucet
logos-devnet deploy my-program my_program.bin      # prints LOGOS_PROGRAM_MY_PROGRAM=<id>
logos-devnet wallet-config ~/my-app/devnet-wallet  # a wallet_config.json for lez_core
source ~/.logos-devnet/env                         # LOGOS_DEVNET_SEQUENCER, LOGOS_PROGRAM_*
```

| Command | What it does |
| --- | --- |
| `up` | Starts the devnet, or resumes it, and makes it match your `logos-devnet.toml`. Takes `--docker` or `--native`, `--lez <tag>`, `--port`, `--block-time`, `--max-txs-per-block`, `--dev-mode` and `--log-filter`, and remembers them. `--rebuild` builds the image or binaries again. |
| `down` | Stops it. The chain is kept. |
| `reset` | Deletes the chain and starts a new one, every account group funded at genesis. Takes the same options as `up`. |
| `status` | What runs, the chain head, the faucet, programs, account groups and sessions. |
| `doctor` | Checks the project file, Docker and the image (or git and cargo with `--native`), whether the chain takes the project's settings, the port, the faucet and, when sessions need it, `logosctl`, and says how to fix what fails. |
| `fund <account> [amount]` | Sends lepta from the faucet to a public account, 10B by default. |
| `deploy <name> <bin> [--mutable]` | Deploys a program, from its `.bin` (what `cargo risczero build` makes, not the bare ELF), and adds `LOGOS_PROGRAM_<NAME>` and `LOGOS_PROGRAM_<NAME>_HEX` to the env file. Names are lower-case letters, digits and `-`. With `--mutable`, a new build updates it in place. |
| `wallet-config <dir>` | Writes a `wallet_config.json` pointing at the devnet into a new wallet directory. |
| `env` | Prints the env file. |
| `logs [-f]` | Prints the sequencer's log. |
| `session new <name> [--module <m>]` | Makes a `logosctl` session with a funded devnet wallet, see below. |
| `session ls`, `session env <name>`, `session rm <name>` | Lists sessions, prints one's env to `source`, deletes one. |
| `accounts new <name> --count <n> [--amount <a>]` | Makes a group of funded accounts, see below. |
| `accounts ls` | Lists account groups. |

Every command takes `--json` for scripts and agents, `--config <file>` for a project file elsewhere, and `--home <dir>`. A bad option changes nothing: `up` and `reset` check it, and build what they need, before they stop the running chain.

[`examples/`](examples) has LEZ's hello-world tutorial running on a devnet, two `logosctl` sessions paying each other, and a GitHub Actions workflow to copy. For a whole project on it, [logos-agents](https://github.com/drptbl/logos-agents) deploys a released program and runs two sessions from its [`logos-devnet.toml`](https://github.com/drptbl/logos-agents/blob/main/logos-devnet.toml).

## For AI agents

[`skills/logos-devnet`](skills/logos-devnet) is an agent skill: the loop to run, the choices that matter, and what to do when `logos-devnet` refuses. For Claude Code, copy it into `~/.claude/skills/` for every project, or into a project's `.claude/skills/`:

```bash
git clone --depth 1 https://github.com/drptbl/logos-devnet.git /tmp/logos-devnet
mkdir -p ~/.claude/skills && cp -R /tmp/logos-devnet/skills/logos-devnet ~/.claude/skills/
```

The files are plain Markdown, so other agents can read `SKILL.md` from their own instructions.

## Set up a project

Commit a `logos-devnet.toml` at your project's root, and `logos-devnet up` anywhere below it makes the devnet match:

```toml
[devnet]                 # optional, the same settings as up's flags
block_time = "1s"
max_txs_per_block = 100
dev_mode = true          # fake proofs: private calls take a second
log_filter = "info,sequencer_core=debug"   # the sequencer's RUST_LOG, for `logs`

[programs]               # name = guest binary, relative to this file
counter = "target/riscv32im-risc0-zkvm-elf/docker/counter.bin"
shop = { path = "target/riscv32im-risc0-zkvm-elf/docker/shop.bin", mutable = true }
token = { url = "https://github.com/<owner>/<repo>/releases/download/v1.0.0/token.bin", sha256 = "<its 64 hex digits>" }

[accounts.users]         # a group of funded accounts
count = 100
amount = 5_000_000_000   # lepta each, 10B when left out

[sessions.alice]         # a logosctl session, as session new
modules = ["chat_module"]
```

A program can come from a release instead of a path: `up` downloads its `url` once, with `curl`, into the home, and deploys it only if its SHA-256 is the `sha256` given (as `shasum -a 256` prints it).

`up` deploys missing programs, makes and funds missing accounts and sessions, and changes nothing the second time (0.6 s). A `mutable` program whose binary changed is updated in place, with the same id. An immutable one keeps its old build, and `up` and `deploy` fail saying so, so tests don't run against it: `reset` deploys the new build. A module added to a session's `modules` is installed into it. `up` checks the whole file before it touches the devnet.

Projects share one devnet, `~/.logos-devnet` on port 3040, unless the file sets `home`, e.g. `home = ".logos-devnet"`, for a devnet of the project's own (give it its own `[devnet] port` too, if another one runs). On a shared devnet, a `[devnet]` section that disagrees with the running chain is refused rather than restarting the chain under the other projects: `down` and `up` apply it, or `reset` for `lez` and `dev_mode`, which only a new chain takes. A `reset` there funds every project's account groups again, and each project's next `up` deploys its programs on the new chain.

## Many accounts

An account group is any number of funded accounts for tests with many users: a hundred parties calling one program, say. Declare it in the project file or make one by hand:

```bash
logos-devnet accounts new users --count 100 --amount 5000000000
```

Group names follow the rules for program names, and can't end in `-wallet`. A group's keys live in one LEZ wallet, `~/.logos-devnet/accounts/users/wallet`, which `lez_core` and the LEZ CLI wallet can both use: `LOGOS_DEVNET_ACCOUNTS_USERS_WALLET` in the env file points at it, for `LEE_WALLET_HOME_DIR`. A new chain, from `reset` or a first `up`, makes it forget the old one, as it does for sessions: stop an app that has it open first, or it writes the old chain back. Its `accounts.json` lists every account, and `LOGOS_DEVNET_ACCOUNTS_USERS` points at it:

```json
{
  "amount": 5000000000,
  "accounts": [
    { "index": 0, "id": "CfcghzvosNM114Akg71vuQyDApHPYpPVC9p4uPFQygye", "hex": "ad54f9c3…" }
  ]
}
```

A group that exists when a chain starts is funded at genesis, so a hundred accounts, or a thousand, are ready at once. One added to a running chain is funded one transfer at a time, about 2.4 s each (100 accounts took 237 s), so declare groups before the first `up`, or `reset`. A group never shrinks: a lower `count` keeps the accounts it has. A new `amount` applies to new accounts, and to every account after `reset`. Making the keys takes longer as a group grows, since each one is written to the group's wallet: 500 took 3 s, and growing a group from 2,000 to 6,000 took about 80 s. It happens once, as keys outlive `reset`. Each `up` checks every account is funded, which took about 1 s with 6,000. Accounts that send need more than they move: every call holds about 1.6M lepta of fees up front, so give a sending group at least a few million each (the default is 10B).

How many transactions fit is set by `max_txs_per_block`. Upstream allows 20 per block, so at 1 s blocks about 20 per second. Measured with every account of a group sending one transfer at the same moment:

| `max_txs_per_block` | Burst | Landed |
| --- | --- | --- |
| 20 | 100 transfers | 5 blocks of 20, 5.5 s |
| 100 | 100 transfers | 1 block, 1.6 s |
| 200 | 200 transfers | 197 in 1 block. 3 were refused by the RPC, which takes 100 connections at a time |

Up to 1000 is accepted. Above 100 clients at the same instant, the RPC answers 429 to the rest, so retry or spread them.

## Point your project at it

The env file is the contract. A project reads `LOGOS_DEVNET_SEQUENCER` for the RPC URL, and `LOGOS_PROGRAM_<NAME>` (base58) or `LOGOS_PROGRAM_<NAME>_HEX` (what `lez_core` takes as `program_id_hex`) for its programs, falling back to the testnet values when they're not set.

Program ids stay the same. `deploy my-program` always deploys from the dev wallet's `program-my-program` header key, and a program id is that key's account id, so `reset` and redeploy give you the same `LOGOS_PROGRAM_MY_PROGRAM`. A second `deploy` of the same name on the same chain does nothing, unless the program is mutable and the binary changed: then it's updated in place.

Programs are immutable unless you ask, because undisclosed private calls only run against a program that was immutable from the start. A mutable one is for iterating: `up` after a rebuild takes about 10 s, against a `reset`. Each update writes new segment accounts, and the old ones stay. A program keeps its mutability on a chain, so a missing `--mutable` can't freeze it by accident: `reset` deploys it the other way.

For a wallet, give it its own directory. `wallet-config` refuses a directory that already holds a wallet, because repointing a testnet wallet at the devnet mixes up its sync state. Then create the wallet the usual way, e.g. `logosctl call lez_core create_new str:<dir>/wallet_config.json str:<dir>/storage.json str:`, and fund its public account. After a `reset`, such a wallet still holds the old chain's sync position and private notes, and skips the new chain's blocks up to the old height: make a new one.

## Sessions

A session is a throwaway `logosctl` session wired to the devnet, for an app or an agent that talks to Logos through `logosctl`. You need `logosctl` on your `PATH`.

```bash
logos-devnet session new alice                          # lez_core and a funded wallet
logos-devnet session new bob --module chat_module       # plus any module from the catalog
logos-devnet session new carol --amount 5000000         # fund it with less than 10B lepta
source <(logos-devnet session env alice)                # LOGOSCTL_CONFIG_DIR and the wallet
```

Each session runs its own daemon in `~/.logos-devnet/sessions/<name>`, with `lez_core` and any `--module` installed from the official catalog. Its wallet points at the devnet, is open in that daemon, and holds a public account the faucet funded. `session env` prints the devnet's env plus:

| Variable | |
| --- | --- |
| `LOGOSCTL_CONFIG_DIR` | the session, so `logosctl` and anything built on it talk to its daemon |
| `LOGOS_DEVNET_WALLET_CONFIG` | the wallet's `wallet_config.json` |
| `LOGOS_DEVNET_WALLET_STORAGE` | the wallet's `storage.json` |
| `LOGOS_DEVNET_ACCOUNT` | its funded account, `Public/<id>` |

Sessions follow the chain. `down` stops their daemons. `up` starts them again, loads their modules and opens their wallets, also after a `--port` change. `reset` funds each account again with what it was created with, and makes each wallet forget the old chain: its sync position and the state of its private accounts. Its keys and accounts stay. `session rm` deletes a session and its wallet.

The catalog's `lez_core` 0.5.1 is built on LEZ `v0.3.1`, the default `--lez` tag, so sessions match it.

## What runs

LEZ's own `sequencer_service` runs, built with two features. `standalone` replaces Bedrock, the L1, with an in-process mock that accepts every block. `prove` runs programs in-process, so you don't need `r0vm`. The default is `v0.3.1`. `--lez` picks another `v0.3` tag, and a chain only runs one tag: `reset --lez <tag>` to switch. LEZ `v0.4` changes the sequencer's config and commands, and isn't supported yet.

| | devnet | testnet |
| --- | --- | --- |
| Block time | 1 s (`--block-time`) | 60 s, often several minutes |
| Funding | faucet, funded at genesis | testnet faucet |
| Base fee | 8 | 8 |
| Block timestamp | your clock | the sequencer's clock |
| L1 finality, indexer | none | yes |

Fees are the testnet's. Every call holds `gas_limit × base fee` up front, plus a little for its size, so about 1.6M lepta at a `gas_limit` of 200,000. The faucet makes that irrelevant.

Private calls still prove on your machine, so they take as long as on the testnet: a shielded transfer from a session took 118 s on an Apple M-series laptop. Dev mode skips that.

## Dev mode

`up --dev-mode`, or `dev_mode = true` under `[devnet]`, runs the chain with `RISC0_DEV_MODE=1`: provers make fake proofs and the sequencer accepts them. The same transfer lands in 1 s.

The env file sets `RISC0_DEV_MODE` for your app and tests, `1` or `0`, so a value left in your shell can't send fake proofs to a chain that checks them. Sessions' daemons get it too. Real proofs still work in dev mode, so an app that doesn't set it is only slow.

A chain keeps its dev mode: one holding fake proofs crashes when it restarts without it. `reset --dev-mode` and `reset --dev-mode false` start a new chain with or without it, and `status` shows which runs. Don't use dev mode for anything that should prove a private call is valid.

## Where things live

Everything is in `~/.logos-devnet` (or `LOGOS_DEVNET_HOME`):

| Path | |
| --- | --- |
| `settings.json` | LEZ tag, runtime, port, block time, transactions per block, dev mode, log filter, faucet |
| `env` | the env file |
| `chain/` | the sequencer's config, database and keys, and whether the chain runs in dev mode. `reset` deletes it. |
| `wallet/` | the dev wallet: the faucet and each program's header key. Kept across resets. |
| `compose.yaml` | the Docker runtime's compose file |
| `native/` | the native runtime's LEZ checkout, build, log and pid file |
| `sessions/` | one directory per session: its `logosctl` home, wallet and `session.json` |
| `accounts/` | one directory per account group: its wallet and `accounts.json` |
| `programs.json` | the binary hash each program was deployed from |
| `downloads/` | program binaries the project file names by URL, by SHA-256 |
| `lock` | held while a command changes the devnet, so two projects sharing the home take turns |

The RPC listens on `127.0.0.1` only. It has no authentication, so don't publish the port.

## JSON

With `--json`, a command prints one JSON document and nothing else on stdout. Notes go to stderr. The fields:

| Command | Fields |
| --- | --- |
| `up`, `reset`, `status` | `lez`, `runtime`, `sequencer` (`url`, `running`, `block`, `max_txs_per_block`, `block_time`, `dev_mode`), `faucet` (`account`, `balance`), `programs`, `account_groups`, `sessions`, `env_file` |
| `deploy` | `name`, `id`, `hex`, `var`, `deployed_now`, `updated` |
| `fund` | `account`, `balance` |
| `accounts new`, `accounts ls` | `name`, `accounts`, `amount`, `file`, `wallet`, `var` (a list for `ls`) |
| `session new` | `name`, `modules`, `vars` |
| `session ls` | a list of `name`, `running`, `account`, `balance`, `error` |
| `env`, `session env` | an object of variable to value |
| `wallet-config` | `path` |
| `down`, `session rm` | `message` |
| `doctor` | `ok`, `checks` (`name`, `ok`, `detail`, `fix`) |

## In CI

The repo is a GitHub Action. On a Linux runner it installs `logos-devnet`, starts the devnet from your project file and exports the env into the job:

```yaml
- uses: drptbl/logos-devnet@v0.3.8
  with:
    args: --max-txs-per-block 100     # optional, more options for up
- run: cargo test                     # LOGOS_DEVNET_SEQUENCER, LOGOS_PROGRAM_*, LOGOS_DEVNET_ACCOUNTS_*, RISC0_DEV_MODE are set
- if: failure()
  run: logos-devnet logs              # the sequencer's log, when a test fails
```

Inputs: `version` (default: the release tag you use the action at, or, pinned to a commit, the release that commit's `Cargo.toml` names), `config`, `working-directory`, `args` (space-separated, without shell quoting. Set the home with `LOGOS_DEVNET_HOME`, not `--home`), `start: "false"` to only install, which also works on macOS runners, and `logosctl-version` (default `0.3.2`). When the project file has sessions and no `logosctl` is on `PATH`, the action installs that release of the Logos Core CLI, checked against its SHA-256. Output: `sequencer`. When `up` itself fails, the action prints the sequencer's log. Starting the devnet needs a Linux runner, for Docker.

## The image

`ghcr.io/drptbl/logos-devnet-sequencer:<lez tag>` holds the sequencer and the LEZ `wallet` for `linux/amd64` and `linux/arm64`. `up` pulls it, and builds it locally from [`docker/Dockerfile`](docker/Dockerfile) when no image is published for your tag. To build it yourself:

```bash
docker build --build-arg LEZ_TAG=v0.3.1 -t ghcr.io/drptbl/logos-devnet-sequencer:v0.3.1 - < docker/Dockerfile
```

## License

MIT or Apache-2.0, at your option.