Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,22 @@
# Image pins. Defaults live in docker-compose.yml as ${VAR:-tag} fallbacks;
# override per pin here to test a pre-release or pin a digest.
# To opt into a moving tag such as zfnd/zebra:latest, see docs/faq.md.
# Z3_ZEBRA_IMAGE=zfnd/zebra:5.0.0
# Z3_ZAINO_IMAGE=zingodevops/zainod:0.4.0-rc.2
# Z3_ZALLET_IMAGE=zodlinc/zallet:v0.1.0-alpha.4
# Z3_ZEBRA_IMAGE=zfnd/zebra:6.0.0-rc.0
# Z3_ZAINO_IMAGE=zingodevops/zainod:0.5.1-no-tls # indexer profile (--profile indexer)
# Z3_ZALLET_IMAGE=zodlinc/zallet:v0.1.0-alpha.4@sha256:3097751a2502857294d7031a360023834b53a6ebd31ed4520f6cd3a148074dbf
# Z3_ZEBRA_BUILD_FEATURES=default-release-binaries

# Platform pin. Zebra is multi-arch and selects the host arch automatically.
# Zaino and Zallet's zaino-backed binary are pinned to amd64 in compose because
# their upstream images publish the needed binaries for amd64 only; setting
# DOCKER_PLATFORM=linux/arm64 with the opt-in build overlay
# (docker-compose.build.yml) builds them native arm64.
# Platform pin. Zebra and Zallet are multi-arch and select the host arch
# automatically. Zaino is pinned to amd64 in compose because its upstream image
# publishes amd64 only; setting DOCKER_PLATFORM=linux/arm64 with the opt-in
# build overlay (docker-compose.build.yml) builds it native arm64.
# DOCKER_PLATFORM=linux/arm64

# Host ports (mainnet defaults; .env.testnet and .env.regtest override them)
# Z3_ZEBRA_HOST_RPC_PORT=8232
# Z3_ZEBRA_HOST_P2P_PORT=8233
# Z3_ZEBRA_HOST_HEALTH_PORT=8080
# Zaino indexer ports (published only with --profile indexer)
# Z3_ZAINO_HOST_GRPC_PORT=8137
# Z3_ZAINO_HOST_JSON_RPC_PORT=8237
# Z3_ZALLET_HOST_RPC_PORT=28232
Expand Down
76 changes: 50 additions & 26 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ jobs:
docker compose --env-file .env.mainnet --profile monitoring config --quiet
docker compose --env-file .env.testnet --profile monitoring config --quiet
docker compose --env-file .env.regtest --profile monitoring config --quiet
docker compose --env-file .env.mainnet --profile indexer config --quiet
docker compose --env-file .env.testnet --profile indexer config --quiet
docker compose --env-file .env.regtest --profile indexer config --quiet

- name: Install Python deps (contract parser + JSON Schema validator)
run: python3 -m pip install --quiet pyyaml jsonschema
Expand Down Expand Up @@ -158,32 +161,14 @@ jobs:
-d '{"jsonrpc":"2.0","method":"getblockchaininfo","params":[],"id":1}' \
http://127.0.0.1:29232 | jq .

- name: Mine block 1 (required for Zaino sync)
- name: Mine the activation blocks
# config/regtest activates NU5 at height 2; mine two blocks so the
# network is past activation before wallet init.
run: |
curl -sf -u zebra:zebra -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"generate","params":[1],"id":1}' \
-d '{"jsonrpc":"2.0","method":"generate","params":[2],"id":1}' \
http://127.0.0.1:29232 | jq .

- name: Start Zaino and verify JSON-RPC proxy
run: |
docker compose --env-file .env.regtest up -d zaino
echo "Waiting for Zaino JSON-RPC on host port 28237..."
for i in $(seq 1 30); do
if RESULT=$(curl -sf -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"getblockchaininfo","params":[],"id":1}' \
http://127.0.0.1:28237 2>/dev/null); then
echo "Zaino is ready"
echo "$RESULT" | jq .
break
fi
if [ "$i" -eq 30 ]; then
echo "Zaino failed to start"
docker compose --env-file .env.regtest logs zaino --tail 20
exit 1
fi
sleep 2
done

- name: Verify Zallet accepts compose command
run: docker compose --env-file .env.regtest run --rm --no-deps zallet --help

Expand All @@ -202,6 +187,39 @@ jobs:
test "$MODE" = "600"
echo "identity.txt present in z3-regtest-zallet volume, mode $MODE"

- name: Initialize the wallet and generate a mnemonic
run: |
docker compose --env-file .env.regtest run --rm zallet \
--datadir /var/lib/zallet --config /etc/zallet/zallet.toml \
init-wallet-encryption
docker compose --env-file .env.regtest run --rm zallet \
--datadir /var/lib/zallet --config /etc/zallet/zallet.toml \
generate-mnemonic

- name: Start Zallet and rpc-router, verify wallet RPC
run: |
docker compose --env-file .env.regtest up -d zallet rpc-router
echo "Waiting for Zallet wallet RPC on host port 50232..."
for i in $(seq 1 30); do
if RESULT=$(curl -sf -u zebra:zebra -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"getwalletinfo","params":[],"id":1}' \
http://127.0.0.1:50232 2>/dev/null); then
echo "Zallet wallet RPC is ready"
echo "$RESULT" | jq .
break
fi
if [ "$i" -eq 30 ]; then
echo "Zallet wallet RPC never responded"
docker compose --env-file .env.regtest logs zallet rpc-router --tail 50
exit 1
fi
sleep 2
done
echo "Asserting rpc-router proxies chain RPCs..."
curl -sf -u zebra:zebra -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"getblockchaininfo","params":[],"id":1}' \
http://127.0.0.1:8181 | jq .result.blocks

- name: Assert Zallet config is readable by the compose user
# Guards the config-readability contract at the same UID/GID declared
# for the zallet service. Catches script regressions and compose user
Expand All @@ -223,7 +241,7 @@ jobs:
- name: Verify the external network is attachable
run: |
docker run --rm --network "$EXTERNAL_NETWORK" alpine \
sh -c 'apk add --no-cache bind-tools >/dev/null 2>&1 && nslookup zebra && nslookup zaino'
sh -c 'apk add --no-cache bind-tools >/dev/null 2>&1 && nslookup zebra && nslookup zallet'

- name: Collect logs on failure
if: failure()
Expand All @@ -232,8 +250,14 @@ jobs:
docker compose --env-file .env.regtest ps 2>&1 || true
echo "=== Zebra logs ==="
docker compose --env-file .env.regtest logs zebra --tail 50 2>&1 || true
echo "=== Zaino logs ==="
docker compose --env-file .env.regtest logs zaino --tail 50 2>&1 || true
echo "=== Zallet logs ==="
docker compose --env-file .env.regtest logs zallet --tail 50 2>&1 || true
echo "=== rpc-router logs ==="
docker compose --env-file .env.regtest logs rpc-router --tail 50 2>&1 || true

- name: Tear down
if: always()
run: docker compose --env-file .env.regtest --profile "*" down -v --remove-orphans 2>&1 || true

cookie-permission-smoke:
# Regtest disables cookie auth, so the regtest smoke job does not exercise
Expand Down Expand Up @@ -288,7 +312,7 @@ jobs:

- name: Tear down
if: always()
run: docker compose --env-file .env.testnet down -v --remove-orphans 2>&1 || true
run: docker compose --env-file .env.testnet --profile "*" down -v --remove-orphans 2>&1 || true

rpc-router-tests:
name: rpc-router cargo test
Expand Down
51 changes: 31 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Z3: a Zcash node platform

Z3 runs **Zebra** (full node), **Zaino** (indexer), and **Zallet** (wallet) together with Docker Compose, on mainnet, testnet, or a local regtest network.
Z3 runs **Zebra** (full node) and **Zallet** (wallet) together with Docker Compose, on mainnet, testnet, or a local regtest network. An optional **Zaino** indexer adds a lightwalletd-compatible gRPC endpoint behind the `indexer` profile.

Two kinds of people use Z3, and this README is split for them:

Expand Down Expand Up @@ -31,7 +31,7 @@ All three can run at once on one host; each gets its own ports and volumes. New

Each network has a one-time setup step and a start step.

**Mainnet (production).** Zebra must sync the whole chain before Zaino and Zallet can serve clients, so the boot is two-phase: start Zebra, wait for the sync, then start the rest.
**Mainnet (production).** Zebra must sync the whole chain before Zallet can serve clients, so the boot is two-phase: start Zebra, wait for the sync, then start the rest.

```bash
git clone https://github.com/ZcashFoundation/z3 && cd z3
Expand All @@ -43,14 +43,14 @@ git clone https://github.com/ZcashFoundation/z3 && cd z3
docker compose --env-file .env.mainnet up -d zebra
./scripts/check-zebra-readiness.sh

# 3. Start Zaino + Zallet once Zebra is synced.
# 3. Start Zallet once Zebra is synced.
docker compose --env-file .env.mainnet up -d
```

Images are pulled automatically; no build step or source checkout is needed.

> [!IMPORTANT]
> Running step 3 before Zebra reaches `/ready` makes Zaino and Zallet restart-loop until the sync catches up. The poller in step 2 exits only when Zebra is synced.
> Running step 3 before Zebra reaches `/ready` makes Zallet restart-loop until the sync catches up. The poller in step 2 exits only when Zebra is synced.

Your edits to the per-network config under `config/<network>/` stay local and survive `git pull`.

Expand Down Expand Up @@ -79,7 +79,7 @@ Each network keeps its chain state in a Docker named volume called `z3-<network>
- **Find the path:** `docker volume inspect z3-mainnet-chain -f '{{.Mountpoint}}'`
- **Put it on another disk:** set `Z3_CHAIN_DATA_PATH=/mnt/ssd/zebra-state` and run `./scripts/fix-permissions.sh zebra /mnt/ssd/zebra-state` before the first start.
- **Back up the wallet:** the only data worth backing up is the `z3-<network>-zallet` volume. It holds both the encrypted wallet database and the age identity that decrypts it, so the volume is self-contained. Chain state is re-syncable and the cookie is regenerated, so neither needs backup.
- **Stop vs wipe:** `docker compose --env-file .env.<network> down` stops the stack and keeps every volume; adding `-v` (`down -v`) deletes them, which means a full re-sync.
- **Stop vs wipe:** `docker compose --env-file .env.<network> --profile "*" down` stops the stack and keeps every volume; adding `-v` (`down -v`) deletes them, which means a full re-sync. `--profile "*"` includes profile-gated services (indexer, monitoring); without it their containers keep running and `down -v` leaves their volumes behind.

The volume table and bind-mount details are in the [Reference](#reference) section.

Expand All @@ -106,11 +106,22 @@ docker compose --env-file .env.<network> --profile monitoring up -d

Default UI host ports are globally unique across the three networks (mainnet Grafana `3000`, testnet `13000`, regtest `23000`, and so on). Each is overridable: `Z3_GRAFANA_PORT`, `Z3_PROMETHEUS_PORT`, `Z3_ALERTMANAGER_PORT`, `Z3_JAEGER_UI_PORT`.

### Indexer

The Zaino indexer ships behind a Compose profile. It adds a lightwalletd-compatible gRPC endpoint (mainnet `8137`) and a JSON-RPC proxy (mainnet `8237`) for explorers, faucets, and light-wallet backends. Zallet reaches Zebra directly, so the default stack does not need it.

```bash
docker compose --env-file .env.<network> --profile indexer up -d
```

> [!NOTE]
> The pinned `zingodevops/zainod` release cannot parse Zebra `6.0.0-rc.0` RPC responses. Until upstream ships Ironwood support, run the indexer profile against a Zebra 5.2-era image by setting `Z3_ZEBRA_IMAGE=zfnd/zebra:5.2.0`.

### Stopping the stack

```bash
docker compose --env-file .env.mainnet down # stop containers, keep data
docker compose --env-file .env.mainnet down -v # stop and delete all volumes (full reset)
docker compose --env-file .env.mainnet --profile "*" down # stop containers, keep data
docker compose --env-file .env.mainnet --profile "*" down -v # stop and delete all volumes (full reset)
```

## Building and testing against Z3
Expand Down Expand Up @@ -138,7 +149,7 @@ Mainnet pairs cleanly with either other network on one host. Testnet and regtest
graph LR
subgraph Z3["Z3 (per network)"]
Zebra["Zebra<br/>(full node)"]
Zaino["Zaino<br/>(indexer)"]
Zaino["Zaino<br/>(indexer, opt-in)"]

subgraph Zallet["Zallet (wallet)"]
EmbeddedZaino["Embedded<br/>Zaino libs"]
Expand All @@ -150,7 +161,7 @@ graph LR
Zaino -->|gRPC| LightClients["Light wallet<br/>clients"]
```

**Zebra** syncs and validates the Zcash blockchain. **Zaino** provides a lightwalletd-compatible gRPC interface for light wallet clients. **Zallet** embeds Zaino's indexer libraries internally and connects directly to Zebra's JSON-RPC; it does not use the standalone Zaino service. The Zallet image also ships a zebra-state backend binary, but z3 runs the `zallet-zaino` binary.
**Zebra** syncs and validates the Zcash blockchain. **Zallet** embeds Zaino's indexer libraries internally and connects directly to Zebra's JSON-RPC; it does not use the standalone Zaino service. The Zallet image also ships a zebra-state backend binary, but z3 runs the `zallet-zaino` binary. **Zaino** is optional, behind the `indexer` profile: it exposes a standalone lightwalletd-compatible gRPC interface for external light wallet clients.

Image pins live as `${VAR:-tag}` defaults in `docker-compose.yml`; override any pin with `Z3_ZEBRA_IMAGE`, `Z3_ZAINO_IMAGE`, or `Z3_ZALLET_IMAGE`. Upstream sources: [Zebra](https://github.com/ZcashFoundation/zebra), [Zaino](https://github.com/zingolabs/zaino), [Zallet](https://github.com/zcash/wallet).

Expand All @@ -163,8 +174,8 @@ Published host ports are chosen per `.env.<network>` so all networks coexist on
| Zebra RPC | `http://localhost:<port>` | `Z3_ZEBRA_HOST_RPC_PORT` |
| Zebra p2p (inbound peers) | `localhost:<port>` | `Z3_ZEBRA_HOST_P2P_PORT` |
| Zebra health | `http://localhost:<port>/ready` | `Z3_ZEBRA_HOST_HEALTH_PORT` |
| Zaino gRPC (plaintext, no TLS) | `localhost:<port>` | `Z3_ZAINO_HOST_GRPC_PORT` |
| Zaino JSON-RPC | `http://localhost:<port>` | `Z3_ZAINO_HOST_JSON_RPC_PORT` |
| Zaino gRPC (plaintext, no TLS; indexer profile) | `localhost:<port>` | `Z3_ZAINO_HOST_GRPC_PORT` |
| Zaino JSON-RPC (indexer profile) | `http://localhost:<port>` | `Z3_ZAINO_HOST_JSON_RPC_PORT` |
| Zallet RPC | `http://localhost:<port>` | `Z3_ZALLET_HOST_RPC_PORT` |

Inside the network, services resolve by DNS name (`zebra`, `zaino`, `zallet`) on Zebra's per-network container ports.
Expand Down Expand Up @@ -261,14 +272,14 @@ diff config/mainnet/zallet.toml config/mainnet/zallet.toml.example

### Platform configuration (ARM64)

Zebra is multi-arch; Docker picks the host's native arch automatically, no override needed. Zaino and Zallet are pinned to `linux/amd64` because their upstream images publish amd64 only. On Apple Silicon those two run under emulation by default; the workload is light enough that this rarely matters.
Zebra and Zallet are multi-arch; Docker picks the host's native arch automatically, no override needed. Zaino is pinned to `linux/amd64` because its upstream image publishes amd64 only. On Apple Silicon it runs under emulation by default; the workload is light enough that this rarely matters.

To run Zaino and Zallet natively on arm64, build them from source:
To run Zaino natively on arm64, build it from source:

```bash
scripts/vendor.sh zaino zallet
DOCKER_PLATFORM=linux/arm64 docker compose -f docker-compose.yml -f docker-compose.build.yml build zaino zallet
docker compose --env-file .env.mainnet up -d
scripts/vendor.sh zaino
DOCKER_PLATFORM=linux/arm64 docker compose -f docker-compose.yml -f docker-compose.build.yml build zaino
docker compose --env-file .env.mainnet --profile indexer up -d
```

</details>
Expand Down Expand Up @@ -307,7 +318,7 @@ Z3_ZEBRA_RUST_LOG=debug
Z3_ZAINO_RUST_LOG=debug

# Pin a different image version, or use zfnd/zebra:latest to track Zebra releases
Z3_ZEBRA_IMAGE=zfnd/zebra:5.0.0
Z3_ZEBRA_IMAGE=zfnd/zebra:5.2.0

# Move chain state to an external SSD
Z3_CHAIN_DATA_PATH=/mnt/ssd/zebra-state
Expand All @@ -330,7 +341,7 @@ Z3 declares each volume with an explicit `name:` so the external Docker identifi
| Suffix | Contents |
|--------|----------|
| `chain` | Zebra blockchain state (~300 GB mainnet, ~30 GB testnet) |
| `zaino` | Zaino indexer database |
| `zaino` | Zaino indexer database (indexer profile) |
| `zallet` | Zallet wallet database (contains keys) |
| `cookie` | RPC authentication cookie for mainnet/testnet (regtest disables cookie auth) |

Expand Down Expand Up @@ -385,11 +396,11 @@ Zebra exposes two endpoints on its health port:
```
Zebra (/ready: synced near tip)
-> Cookie permissions (.cookie readable on cookie-auth networks)
-> Zaino (gRPC port responding)
-> Zallet (RPC responding)
-> Zaino (gRPC port responding; indexer profile)
```

The default compose gates Zaino and Zallet on Zebra's `/ready` endpoint and on the cookie-permissions sidecar when cookie auth is enabled. For development, copy `docker-compose.override.yml.example` to `docker-compose.override.yml` to switch Zebra gating to `/healthy` (allows services to start during sync, but they may error until Zebra catches up).
The default compose gates Zallet on Zebra's `/ready` endpoint and on the cookie-permissions sidecar when cookie auth is enabled; the indexer profile adds Zaino under the same gates. For development, copy `docker-compose.override.yml.example` to `docker-compose.override.yml` to switch Zebra gating to `/healthy` (allows services to start during sync, but they may error until Zebra catches up).

### Monitoring sync progress

Expand Down
1 change: 0 additions & 1 deletion docker-compose.regtest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ services:
ZAINO_NETWORK: Regtest
ZAINO_VALIDATOR_SETTINGS__VALIDATOR_JSONRPC_LISTEN_ADDRESS: zebra:${Z3_ZEBRA_RPC_PORT:-18232}
ZAINO_GRPC_SETTINGS__LISTEN_ADDRESS: 0.0.0.0:8137
ZAINO_JSON_SERVER_SETTINGS__JSON_RPC_LISTEN_ADDRESS: 0.0.0.0:8237
volumes: !override
- ${Z3_ZAINO_DATA_PATH:-zaino}:/app/data
- ${Z3_CONFIG_DIR:-./config/regtest}/zaino.toml:/etc/zaino/zindexer.toml:ro
Expand Down
Loading
Loading