diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..418f53a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,70 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +This is a Rust binary crate for the `aleo-devnode` CLI. The entrypoint is `src/main.rs`; command modules live in `src/accounts.rs`, `src/advance.rs`, `src/start.rs`, and `src/restore.rs`. REST routing is under `src/rest/`, with shared helpers in `src/rest/helpers/`. Integration tests are in `tests/integration.rs`, and the bundled genesis block is in `resources/`. + +## Architecture (the big picture) + +Single Rust binary crate built on **snarkVM `TestnetV0`**. The defining trait: **the devnode skips proof verification** (snarkVM `dev_skip_checks`/`test_*` features in `Cargo.toml`), so clients can broadcast transactions built with placeholder proofs/verifying keys. Any code touching transaction validation must preserve this "no real proofs" assumption. + +**CLI dispatch** (`src/main.rs`): a clap enum with four subcommands — `start`, `advance`, `restore`, `accounts` — each owning its module. `--private-key` is a *global* arg resolved from the flag or the `PRIVATE_KEY` env var (`start::resolve_private_key`). Keep each command's behavior inside its own module. + +**Ledger storage is generic over `ConsensusStorage`.** `src/start.rs` picks the concrete backend at runtime: `ConsensusMemory` (in-memory, default) or `ConsensusDB` (RocksDB, when `--storage ` is given). Everything downstream — including the whole REST layer — is written generically as `>` so both backends share one code path. + +**REST layer** (`src/rest/`): `Rest` (`mod.rs`) holds the ledger plus a `Mutex>` buffer, shutdown channel, and in-flight verification counters. `build_routes()` defines the route table **once** and is mounted under three prefixes in `spawn_server`: +- `/` and `/v1/` — wrapped in `v1_error_middleware`, which flattens any error into a plain-string HTTP 500 (legacy behavior). +- `/v2/` — returns structured JSON errors (`helpers/error.rs`, `RestError`). + +So a single handler change automatically affects all three prefixes; error *shape* differs only via the middleware. Route handlers live in `src/rest/routes.rs`; error types/helpers in `src/rest/helpers/`. + +**Block creation has two modes** (`routes.rs`): +- Default (auto): `transaction/broadcast` immediately calls `prepare_advance_to_next_beacon_block` + `advance_to_next_block`, producing one block per transaction. +- `--manual-block-creation`: broadcasts only push into the buffer; blocks are minted on demand via `POST /block/create` (`{"num_blocks": N}`), draining the buffer into the first block. + +On startup (auto mode only), `run_devnode` advances the ledger to the last height in `TEST_CONSENSUS_VERSION_HEIGHTS` so the node boots at the latest consensus version. Blocking ledger work is wrapped in `tokio::task::spawn_blocking`. + +**Snapshots & restore** (`routes.rs` + `src/restore.rs`): snapshots require `--storage` and are written to a sibling dir `{storage}-snapshots/{name}` (`snapshots_sibling_dir` is the shared source of that layout — reuse it, don't recompute the path). Restore is **offline**: the server must be stopped; `restore` clears the storage dir, copies the snapshot in, and with `--restart` re-execs the binary as `start` (via `exec` on Unix, same PID). + +**Genesis**: a 40-validator genesis block is embedded at compile time from `resources/` via `include_bytes!`; `--genesis-path` overrides it. + +## Build, Test, and Development Commands + +Native build prerequisite: `clang`/`libclang` (snarkVM's `rocks`/RocksDB feature links it). CI installs `libclang-dev`; on macOS it comes with the Xcode Command Line Tools (`xcode-select --install`). + +- `cargo build --release`: builds the production CLI binary. +- `cargo test --locked`: runs the same locked-dependency test command used by CI. +- `cargo test `: runs a single test by substring; `cargo test --test integration ` runs one integration test. +- `cargo fmt --all -- --check`: verifies formatting against `rustfmt.toml`. CI does **not** enforce fmt — run it yourself. +- `cargo run -- accounts`: lists built-in funded development accounts. +- `cargo run -- start --private-key `: starts a local node on `127.0.0.1:3030`. + +Use the standard development private key from `README.md` only for local testing. + +CI (`.github/workflows/ci.yml`) installs `libclang-dev`, verifies `Cargo.lock` is present, and runs `cargo test --locked` on stable. The release workflow refuses to build unless the git tag version matches the `Cargo.toml` version — bump `Cargo.toml` before tagging. + +## Coding Style & Naming Conventions + +Follow Rust 2021 crate semantics and `rustfmt.toml`: 4-space indentation, 120-column width, shorthand field/try syntax, and crate-granular imports. Use `snake_case` for modules, functions, and tests; `PascalCase` for types; and `SCREAMING_SNAKE_CASE` for constants. Keep command behavior in its owning command module. + +## Testing Guidelines + +Tests use Rust's built-in test harness plus `reqwest` and `tempfile`. Integration tests spawn the compiled `aleo-devnode` binary through `CARGO_BIN_EXE_aleo-devnode`, allocate local ports, and clean up child processes through `DevnodeGuard`. Name new integration tests `test_*` and avoid depending on port `3030`. + +## Domain Deep Dives + +Use these docs as selective disclosure. Read them only when touching the related area: + +- `docs/cli-usage.md`: commands, flags, env vars, and examples. +- `docs/rest-api.md`: route prefixes, endpoint patterns, and response behavior. +- `docs/storage-and-snapshots.md`: persistent storage, snapshot layout, and restore rules. +- `docs/testing.md`: integration harness, CI checks, ports, and child process cleanup. +- `docs/genesis-and-accounts.md`: bundled genesis data and development account limits. + +## Commit & Pull Request Guidelines + +Recent commits use short, imperative summaries, usually lowercase, such as `bump version to 0.1.1`. Keep commits focused and include `Cargo.lock` when dependency resolution changes. Pull requests should follow `.github/PULL_REQUEST_TEMPLATE.md`: motivation, test plan, and related PRs or issues. + +## Security & Configuration Tips + +The devnode skips proof verification by design. Treat all pre-funded keys and placeholder proof workflows as development-only. Never document or commit production keys, private ledgers, or generated snapshot data. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3ed435d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +See **@AGENTS.md** for everything: project structure, the big-picture architecture, build/test/dev commands, coding style, testing model, commit/PR conventions, and security notes. The `docs/` files it links are selective-disclosure reference — read the matching one only when touching that area. diff --git a/docs/cli-usage.md b/docs/cli-usage.md new file mode 100644 index 0000000..78dbc76 --- /dev/null +++ b/docs/cli-usage.md @@ -0,0 +1,112 @@ +# CLI Usage Deep Dive + +Read this when changing `src/main.rs`, any command module, README command examples, or user-facing flag behavior. + +## Command Map + +- `start`: starts the REST devnode and owns server, storage, genesis, logging, and block creation flags. +- `advance`: a thin client that POSTs to `/testnet/block/create`; defaults to advancing one block. It surfaces failures — a connection error or a non-success HTTP status returns an error and exits nonzero. +- `restore`: copies a named snapshot back into a storage directory and can re-exec as `start`. +- `accounts`: prints the 50 built-in development accounts for the bundled genesis block. +- `update`: self-updates the binary from GitHub releases (`ProvableHQ/aleo-devnode`). + +## Global flags + +These are defined on the top-level CLI (`src/main.rs`): + +- `-v` / `-V` / `--version`: print the version and exit. Top-level only (parsed before the subcommand). **Caution:** `-v` at the top level means *version*, but `-v` after `start` or `restore` means *verbosity* — clap disambiguates by position. +- `--private-key `: marked `global = true`, so clap accepts it positionally alongside `start` (e.g. `start --private-key ...` or `--private-key ... start`). However, `src/main.rs` only forwards it into `start`. **It is ignored by `restore`** — `restore --restart` reads its own `--private-key` (or `PRIVATE_KEY`), not the top-level one. See Private Key Rules below. + +## `start` flags + +| Flag | Short | Default | Notes | +| --- | --- | --- | --- | +| `--private-key ` | | `PRIVATE_KEY` env | Global. Required (flag or env). Used to sign every block. | +| `--verbosity <0-2>` | `-v` | `2` | Logging level; values outside `0..=2` are rejected by clap. | +| `--socket-addr ` | `-a` | `127.0.0.1:3030` | REST API bind address. Must parse as a `SocketAddr`. | +| `--genesis-path ` | `-g` | `blank` | `blank` is a sentinel meaning the bundled 40-validator genesis; any other value is read as a genesis block file. | +| `--manual-block-creation` | `-m` | off | Switches block creation from automatic to manual — see below. | +| `--storage [DIR]` | `-s` | in-memory | Persist the ledger to disk (RocksDB). See storage behavior below. | +| `--clear-storage` | `-c` | off | Wipe the storage dir before starting. **Requires `--storage`** (clap enforces this). | + +### Storage flag behavior (`--storage`) + +`--storage` takes an optional value (`num_args = 0..=1`): + +- **Omitted entirely** → ledger runs in memory (`ConsensusMemory`); nothing persists across restarts. +- **`-s` / `--storage` with no value** → defaults to the directory `devnode` (`default_missing_value`). +- **`--storage `** → persists to `` (`ConsensusDB` / RocksDB). + +Snapshots (`POST /snapshot`) and the `restore` command only work when `--storage` is set — in-memory nodes return `400` for snapshot routes. See `docs/storage-and-snapshots.md`. + +## Block creation + +This is the core of how clients drive the ledger. There are two modes, selected at `start` time: + +### Automatic (default) + +Every successful `POST /testnet/transaction/broadcast` **immediately mints one block** containing that transaction — no extra call needed. Additionally, on startup in this mode `run_devnode` auto-advances the ledger to the last height in `TEST_CONSENSUS_VERSION_HEIGHTS` (by calling `/block/create` once internally), so the node boots at the latest consensus version. + +You can still mint blocks explicitly in automatic mode — `advance ` (or `POST /block/create`) works here too. Since the broadcast buffer is always empty in this mode, those blocks are empty; use it to advance height or trigger height/time-gated logic without any transactions. + +### Manual (`--manual-block-creation` / `-m`) + +Broadcasts are only **buffered**; they do not land on the ledger until a block is minted explicitly. Use this when a test needs several transactions in one block, deterministic block boundaries, or control over block timing. Two ways to mint: + +- `cargo run -- advance ` — the `advance` subcommand (a thin client that POSTs to `/block/create`). +- `POST /testnet/block/create` with `{"num_blocks": N}` directly (see `docs/rest-api.md`). + +The buffered transactions are drained into the **first** block created; any further blocks in the same call are empty. `num_blocks` is bounded server-side to `1..=1000`. + +Note: in manual mode the startup consensus-version auto-advance is **skipped**, so the node remains at the loaded ledger height — genesis height for a fresh ledger, or the persisted height when restarting against existing `--storage`. Advance manually if you need a later consensus version. + +## `advance` flags + +| Arg | Default | Notes | +| --- | --- | --- | +| `` (positional) | `1` | Number of blocks to mint. | +| `--socket-addr ` | `127.0.0.1:3030` | Target devnode. | + +A thin client against a running server — it does not share the ledger, it just calls `/block/create`. Connection failures and non-success HTTP responses (status + body) are returned as errors and exit nonzero. + +## `restore` flags + +`restore` is **offline** — the server must be stopped first. Flags forwarded to `start` only apply with `--restart`, and **only the ones in the table below are forwarded** (`--storage`, `--private-key`, `--socket-addr`, `--verbosity`, `--manual-block-creation`). Notably `--genesis-path` and `--clear-storage` are **not** forwarded — if you restored a snapshot built from a custom genesis, restart with a manual `start --genesis-path ...` instead of `--restart`. + +| Flag | Short | Default | Notes | +| --- | --- | --- | --- | +| `--snapshot ` | | (required) | Snapshot directory name under `{storage}-snapshots/`. | +| `--storage ` | | `devnode` | Storage dir to restore into; must match the `--storage` used at `start`. | +| `--restart` | | off | Re-exec as `start` after restoring (via `exec` on Unix, same PID). | +| `--private-key ` | | `PRIVATE_KEY` env | Forwarded to `start` on `--restart`. | +| `--socket-addr ` | `-a` | `127.0.0.1:3030` | Forwarded to `start` on `--restart`. | +| `--verbosity <0-2>` | `-v` | `2` | Forwarded to `start` on `--restart`. | +| `--manual-block-creation` | `-m` | off | Forwarded to `start` on `--restart`. | + +## `update` flags + +| Flag | Short | Notes | +| --- | --- | --- | +| `--list` | `-l` | List available releases instead of updating. | +| `--name ` | `-n` | Update to a specific release tag rather than latest. | +| `--quiet` | `-q` | Suppress download/progress output. | + +## Private Key Rules + +`start` requires a private key. Prefer `--private-key` in examples because it is explicit and overrides the `PRIVATE_KEY` environment fallback. If neither is present, startup exits with an error. Invalid or blank keys should fail before the REST server starts. + +`restore --restart` has its own forwarded `--private-key`; without it, restart relies on `PRIVATE_KEY`. The key is passed to the re-exec'd process via the `PRIVATE_KEY` env var to keep it out of the process argument list. + +## Examples To Keep Current + +```sh +cargo run -- accounts +cargo run -- start --private-key +cargo run -- start --private-key --manual-block-creation +cargo run -- start --private-key --storage devnode --clear-storage +cargo run -- advance 5 --socket-addr 127.0.0.1:3030 +cargo run -- restore --snapshot before-deploy --storage devnode +cargo run -- update --list +``` + +When adding or renaming flags, update the owning command module, `README.md`, and this file together. diff --git a/docs/genesis-and-accounts.md b/docs/genesis-and-accounts.md new file mode 100644 index 0000000..733bb24 --- /dev/null +++ b/docs/genesis-and-accounts.md @@ -0,0 +1,29 @@ +# Genesis And Accounts Deep Dive + +Read this when changing `resources/`, `src/accounts.rs`, genesis loading, or documentation that lists funded development accounts. + +## Bundled Genesis + +The default genesis block is compiled into the binary from: + +```text +resources/genesis_8d710d7e2_40val_snarkos_dev_network.bin +``` + +`start --genesis-path ` replaces the built-in block with a custom genesis file. Do not assume the built-in funded account list applies when a custom genesis path is used. + +## Funded Accounts + +`src/accounts.rs` defines `FUNDED_ACCOUNTS`, the 50 development address/private-key pairs seeded by the bundled genesis block. The `accounts` command prints this list for local setup convenience. + +The first development key is also used by README examples and integration tests: + +```text +APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH +``` + +These keys are public development fixtures. Never present them as safe for production, private deployments, or funded real assets. + +## Documentation Rules + +If the bundled genesis block or funded account list changes, update `src/accounts.rs`, `README.md`, and tests that assume the standard development private key. For custom genesis workflows, direct users to inspect block 0 through the REST API instead of relying on `accounts`. diff --git a/docs/rest-api.md b/docs/rest-api.md new file mode 100644 index 0000000..0ee2843 --- /dev/null +++ b/docs/rest-api.md @@ -0,0 +1,108 @@ +# REST API Deep Dive + +Read this when changing `src/rest/`, route paths, request or response shapes, error mapping, or README endpoint examples. + +## Route Prefixes + +Routes are mounted under three prefixes: + +- `/testnet/...` +- `/v1/testnet/...` +- `/v2/testnet/...` + +The default and `v1` routers wrap responses in `v1_error_middleware` (`src/rest/mod.rs`): any non-success response is flattened to **HTTP 500** with a plain-text body — the error message and its cause chain joined by ` — `. The `v2` router skips that middleware and returns the structured `RestError` form (`src/rest/helpers/error.rs`): the real status code (400/404/422/429/500) and a JSON body `{message, error_type, chain}`. + +So the same handler error surfaces as a 500 string under `/testnet` and `/v1/testnet`, but as a typed JSON error under `/v2/testnet`. + +## Route Groups + +`src/rest/mod.rs` defines the route table. Handler implementations live in `src/rest/routes.rs`. Keep new endpoints grouped with nearby ledger, transaction, program, snapshot, or shutdown routes. + +Important endpoint families include: + +- Node info: `/consensus_version` (returns the active consensus version at the latest height; the devnode boots at the latest version — see the Architecture section in AGENTS.md) +- Blocks: `/block/height/latest`, `/block/latest`, `/block/{height_or_hash}`, `/block/create` +- Transactions: `/transaction/broadcast`, `/transaction/{id}`, confirmed and unconfirmed lookups +- Programs and mappings: `/program/{id}`, `/program/{id}/mapping/{name}/{key}`, `/program/{id}/mapping/{name}` +- State and find helpers: `/stateRoot/latest`, `/statePath/{commitment}`, `/find/...` +- Devnode controls: `/transaction/broadcast`, `/block/create`, `/snapshot`, `/snapshots`, `/shutdown` + +## Devnode Control Endpoints + +These are the endpoints that *drive* the devnode rather than just read from it — they are what distinguishes this tool from a read-only snarkVM node. All examples use the bare `/testnet` prefix; the same paths exist under `/v1/testnet` and `/v2/testnet` (only the error shape differs — see Route Prefixes). Handlers are in `src/rest/routes.rs`. + +### `POST /transaction/broadcast` — submit a transaction (the main entry point) + +The primary way to put state on the ledger. Body is a JSON-serialized snarkVM `Transaction`. Optional query `?check_transaction=false` skips structural validation (`check_transaction_basic`); it defaults to `true`. Note this is *structural* validation only — proof verification is always skipped by design, so placeholder-proof transactions are accepted either way. + +```sh +# auto mode (default): this single call also mints a block containing the tx +curl -X POST http://127.0.0.1:3030/testnet/transaction/broadcast \ + -H 'Content-Type: application/json' --data @tx.json +``` + +**When to use:** every time you want to commit a transaction. In **auto mode** (default) a successful broadcast *immediately mints one block* containing the transaction — nothing else is needed. In **`--manual-block-creation` mode** the transaction is only buffered; it does not land on the ledger until you call `POST /block/create`. + +Returns `200` with the transaction ID on success. Failure modes: `400` (transaction exceeds the byte limit), `422` (malformed JSON body or failed validation), `429` (too many in-flight verifications — bounded per type by `VM::MAX_PARALLEL_EXECUTE_VERIFICATIONS` / `MAX_PARALLEL_DEPLOY_VERIFICATIONS`; retry with backoff). + +### `POST /block/create` — mint blocks on demand + +A JSON body is **required** (the handler uses axum's `Json` extractor, which rejects requests with no body or missing `Content-Type: application/json`). Only the `num_blocks` field is optional: send `{}` to default to one block, or `{"num_blocks": N}` where `N` must be `1..=1000`. Drains *all* currently buffered transactions into the **first** block created; any further blocks in the same call are empty. + +```sh +# mint one block, sweeping up everything broadcast since the last block +curl -X POST http://127.0.0.1:3030/testnet/block/create \ + -H 'Content-Type: application/json' -d '{}' + +# mint three blocks (txs land in the first, the rest are empty) +curl -X POST http://127.0.0.1:3030/testnet/block/create \ + -H 'Content-Type: application/json' -d '{"num_blocks": 3}' +``` + +**When to use:** +- In **`--manual-block-creation` mode**: this is how buffered broadcasts actually get committed. Broadcast N transactions, then call this once to seal them into a block. Use this when a test needs several transactions in a *single* block, or precise control over block boundaries/timing. +- In **auto mode**: the buffer is always empty (broadcasts self-seal), so calling this just mints *empty* blocks — handy to advance height or trigger time/height-gated logic without any transactions. + +Returns the last created block as JSON. `400` if `num_blocks` is `0` or exceeds `1000`. + +### `POST /snapshot` — checkpoint the ledger (online) + +A JSON body is **required** (the handler uses axum's `Json` extractor — send `Content-Type: application/json` and a body every time, even for the default name). The `name` field is optional: send `{}` to default to `snapshot-{height}`, or `{"name": "my-snapshot"}` to choose one. **Requires `--storage`** (in-memory nodes return `400`). The name must not contain path separators or `..`. Writes a RocksDB backup to the sibling dir `{storage}-snapshots/{name}/`. + +```sh +# default name (snapshot-{height}) +curl -X POST http://127.0.0.1:3030/testnet/snapshot \ + -H 'Content-Type: application/json' -d '{}' + +# explicit name +curl -X POST http://127.0.0.1:3030/testnet/snapshot \ + -H 'Content-Type: application/json' -d '{"name": "before-upgrade"}' +# -> {"name":"before-upgrade","height":1234} +``` + +**When to use:** to capture a known-good ledger state mid-run that you can return to later. Snapshots are created *online* here, but **restoring is offline** — stop the node and use the `restore` CLI subcommand (see `docs/storage-and-snapshots.md`); there is no restore REST endpoint. + +### `GET /snapshots` — list snapshots + +Requires `--storage` (`400` otherwise). Returns a sorted JSON array of snapshot directory names (`[]` if none exist yet). Use it to discover names to pass to the `restore` CLI command. + +```sh +curl http://127.0.0.1:3030/testnet/snapshots +# -> ["before-upgrade","snapshot-1234"] +``` + +### `POST /shutdown` — graceful shutdown + +No body. **Loopback-only**: requests from non-local IPs get `403`. Triggers graceful shutdown via the server's one-shot channel and returns `200`. + +```sh +curl -X POST http://127.0.0.1:3030/testnet/shutdown +``` + +**When to use:** to cleanly stop a node you started — e.g. tear-down in a test harness, or before running an offline `restore`. The integration test `DevnodeGuard` kills the child process directly instead, so this is mainly for external scripts. + +## Behavior Notes + +Mapping list reads require `all=true`; metadata responses use `metadata=true`. The request body limit is set in `build_routes` to match the snarkVM binary transaction limit policy. + +A `tower_governor` rate-limit layer is wired into `build_routes`, but `start.rs` passes an effectively unlimited RPS, so it does not throttle in practice. Don't rely on it for limiting; the real backpressure is the verification-slot counter above. diff --git a/docs/storage-and-snapshots.md b/docs/storage-and-snapshots.md new file mode 100644 index 0000000..6feb2bc --- /dev/null +++ b/docs/storage-and-snapshots.md @@ -0,0 +1,26 @@ +# Storage And Snapshots Deep Dive + +Read this when changing `src/start.rs`, `src/restore.rs`, snapshot REST handlers, or tests that persist ledger state. + +## Storage Modes + +Without `--storage`, the devnode uses in-memory ledger storage and state is lost on shutdown. With `--storage`, it uses RocksDB-backed storage at the provided path. Passing `--storage` without a value uses `devnode/`. + +Use `--clear-storage` only with `--storage`. It clears entries inside the storage directory before loading the ledger and leaves the directory itself in place. + +## Snapshot Layout + +Snapshots require persistent storage. The snapshot directory is a sibling of the storage directory: + +```text +devnode/ # active ledger +devnode-snapshots/ # snapshots for that ledger +``` + +For a custom storage path, `snapshots_sibling_dir()` derives the snapshot root from the storage directory name, for example `tmp/ledger` maps to `tmp/ledger-snapshots`. + +## Restore Rules + +`restore --snapshot --storage ` clears the target storage directory and copies the named snapshot into it. The source snapshot is not deleted, so the same snapshot can be reused. + +The devnode should be stopped before restore. `restore --restart` re-executes the current binary as `start` with the storage path and forwarded runtime flags. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..32708f9 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,24 @@ +# Testing Deep Dive + +Read this when changing `tests/integration.rs`, process lifecycle behavior, REST readiness, storage persistence, or CI expectations. + +## Commands + +```sh +cargo test --locked +cargo fmt --all -- --check +``` + +CI currently runs `cargo test --locked` on stable Rust after installing `libclang-dev` and verifying `Cargo.lock` exists. Run formatting locally even though CI does not currently enforce it. + +## Integration Harness + +Integration tests spawn the compiled binary using `env!("CARGO_BIN_EXE_aleo-devnode")`. This mirrors real CLI behavior better than calling internal functions directly. + +`DevnodeGuard` owns the child process, a blocking `reqwest` client, and the base REST URL. It waits for REST readiness by polling `/testnet/block/height/latest`, then kills and waits for the child on drop. Prefer this guard for new tests. + +## Test Patterns + +Use `alloc_port()` instead of hard-coding `3030`. Use `tempfile::tempdir()` for persistent storage and snapshot cases. Keep test names in the `test_*` pattern. For manual block creation tests, pass `--manual-block-creation` and explicitly call `/block/create` through the helper methods. + +Long startup paths can take time because the ledger and consensus heights are initialized; keep polling timeouts realistic.