logos-devnet 0.2.1

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.2.0 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`.

You need Docker (OrbStack, Docker Desktop or Colima). 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 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` and `--max-txs-per-block`, 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`), 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> <elf>` | Deploys a program and adds `LOGOS_PROGRAM_<NAME>` and `LOGOS_PROGRAM_<NAME>_HEX` to the env file. Names are lower-case letters, digits and `-`. |
| `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.

## 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

[programs]               # name = guest binary, relative to this file
counter = "target/riscv32im-risc0-zkvm-elf/docker/counter.bin"

[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"]
```

`up` deploys missing programs, makes and funds missing accounts and sessions, and changes nothing the second time (0.6 s). A program whose binary changed since it was deployed stays as it is, with a note: deployed programs are immutable, so `reset` deploys the new build. `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. 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
```

Its keys live in one LEZ wallet, `~/.logos-devnet/accounts/users/wallet`, which `lez_core` and the LEZ CLI wallet can both use. Its `accounts.json` lists every account, and `LOGOS_DEVNET_ACCOUNTS_USERS` in the env file 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 is quick, 500 in under 3 s. 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: `reset` first to deploy a new build.

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.

## 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. `session rm` deletes a session and its wallet.

The catalog's `lez_core` is built for the LEZ release the testnet runs, so sessions match the default `--lez` tag.

## 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`, the release `lez_core` 0.5.1 is built on. `--lez` picks another tag, and a chain only runs one tag: `reset --lez <tag>` to switch.

| | 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.

## Where things live

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

| Path | |
| --- | --- |
| `settings.json` | LEZ tag, runtime, port, block time, faucet |
| `env` | the env file |
| `chain/` | the sequencer's config, database and keys. `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 |
| `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`), `faucet` (`account`, `balance`), `programs`, `account_groups`, `sessions`, `env_file` |
| `deploy` | `name`, `id`, `hex`, `var`, `deployed_now` |
| `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.2.0
  with:
    args: --max-txs-per-block 100     # optional, more options for up
- run: cargo test                     # LOGOS_DEVNET_SEQUENCER, LOGOS_PROGRAM_*, LOGOS_DEVNET_ACCOUNTS_* are set
```

Inputs: `version` (default: the release tag you use the action at, so set it when you pin a commit SHA), `config`, `working-directory`, `args` (space-separated, without shell quoting. Set the home with `LOGOS_DEVNET_HOME`, not `--home`), and `start: "false"` to only install. Output: `sequencer`. macOS runners have no Docker, so it refuses them.

## 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.