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
9 changes: 5 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@
# override per pin here to test a pre-release or pin a digest.
# Z3_ZEBRA_IMAGE=zfnd/zebra:5.0.0
# Z3_ZAINO_IMAGE=zingodevops/zainod:0.4.0-rc.2
# Z3_ZALLET_IMAGE=electriccoinco/zallet:v0.1.0-alpha.3
# Z3_ZALLET_IMAGE=zodlinc/zallet:v0.1.0-alpha.4
# Z3_ZEBRA_BUILD_FEATURES=default-release-binaries

# Platform pin. Zebra is multi-arch and selects the host arch automatically.
# Zaino and Zallet are pinned to amd64 in compose because their upstream
# images publish amd64 only; setting DOCKER_PLATFORM=linux/arm64 with the
# opt-in build overlay (docker-compose.build.yml) builds them native arm64.
# 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.
# DOCKER_PLATFORM=linux/arm64

# Host ports (mainnet defaults; .env.testnet and .env.regtest override them)
Expand Down
84 changes: 25 additions & 59 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,13 @@ jobs:
persist-credentials: false

- name: Stage local config files for compose render
# The live config/<net>/{zallet,zaino}.toml files and identity files
# are gitignored, but docker compose config validates bind-mount source
# existence. Copy templates into place and touch identity stubs.
# The live config/<net>/{zallet,zaino}.toml files are gitignored, but
# docker compose config validates bind-mount source existence. Copy the
# templates into place.
run: |
for net in mainnet testnet regtest; do
cp config/$net/zallet.toml.example config/$net/zallet.toml
cp config/$net/zaino.toml.example config/$net/zaino.toml
touch config/$net/zallet_identity.txt
done

- name: Validate compose for every network
Expand Down Expand Up @@ -68,25 +67,6 @@ jobs:
with:
persist-credentials: false

- name: Install ACL tooling
run: sudo apt-get update -qq && sudo apt-get install -y -qq acl

- name: Provide fake rage-keygen
# This job validates z3's permission contract, not rage itself.
run: |
mkdir -p "$RUNNER_TEMP/fake-bin"
cat > "$RUNNER_TEMP/fake-bin/rage-keygen" <<'EOF'
#!/usr/bin/env sh
set -eu
if [ "$#" -ne 2 ] || [ "$1" != "-o" ]; then
echo "usage: rage-keygen -o <path>" >&2
exit 2
fi
printf '%s\n' '# dummy identity for CI permission smoke' > "$2"
EOF
chmod +x "$RUNNER_TEMP/fake-bin/rage-keygen"
echo "$RUNNER_TEMP/fake-bin" >> "$GITHUB_PATH"

- name: Fresh setup files are readable by Zallet's compose user
run: |
set -euo pipefail
Expand All @@ -95,39 +75,28 @@ jobs:

test "$(stat -c '%a' config/mainnet/zaino.toml)" = "644"
test "$(stat -c '%a' config/mainnet/zallet.toml)" = "644"
getfacl -cpn config/mainnet/zallet_identity.txt > /tmp/mainnet-zallet-identity.acl
grep -q '^user::rw-$' /tmp/mainnet-zallet-identity.acl
grep -q '^user:1000:r--$' /tmp/mainnet-zallet-identity.acl
grep -q '^group::---$' /tmp/mainnet-zallet-identity.acl
grep -q '^other::---$' /tmp/mainnet-zallet-identity.acl

ZALLET_USER="$(docker compose --env-file .env.mainnet config --format json \
| python3 -c 'import json, sys; print(json.load(sys.stdin)["services"]["zallet"]["user"])')"
docker run --rm --user "$ZALLET_USER" -v "$PWD/config/mainnet":/c:ro alpine \
sh -c 'cat /c/zallet.toml >/dev/null && cat /c/zallet_identity.txt >/dev/null'
sh -c 'cat /c/zallet.toml >/dev/null'

- name: Existing restrictive files are repaired by setup-network.sh
run: |
set -euo pipefail
umask 077
./scripts/setup-network.sh testnet

chmod 600 config/testnet/zaino.toml config/testnet/zallet.toml config/testnet/zallet_identity.txt
setfacl -b config/testnet/zallet_identity.txt
chmod 600 config/testnet/zaino.toml config/testnet/zallet.toml

./scripts/setup-network.sh testnet
test "$(stat -c '%a' config/testnet/zaino.toml)" = "644"
test "$(stat -c '%a' config/testnet/zallet.toml)" = "644"
getfacl -cpn config/testnet/zallet_identity.txt > /tmp/testnet-zallet-identity.acl
grep -q '^user::rw-$' /tmp/testnet-zallet-identity.acl
grep -q '^user:1000:r--$' /tmp/testnet-zallet-identity.acl
grep -q '^group::---$' /tmp/testnet-zallet-identity.acl
grep -q '^other::---$' /tmp/testnet-zallet-identity.acl

ZALLET_USER="$(docker compose --env-file .env.testnet config --format json \
| python3 -c 'import json, sys; print(json.load(sys.stdin)["services"]["zallet"]["user"])')"
docker run --rm --user "$ZALLET_USER" -v "$PWD/config/testnet":/c:ro alpine \
sh -c 'cat /c/zallet.toml >/dev/null && cat /c/zallet_identity.txt >/dev/null'
sh -c 'cat /c/zallet.toml >/dev/null'

- name: Regtest pwhash rewrite keeps files readable
run: |
Expand All @@ -142,16 +111,11 @@ jobs:
test "$(stat -c '%a' config/regtest/zaino.toml)" = "644"
test "$(stat -c '%a' config/regtest/zallet.toml)" = "644"
test "$(stat -c '%a' config/regtest/zebra.toml)" = "644"
getfacl -cpn config/regtest/zallet_identity.txt > /tmp/regtest-zallet-identity.acl
grep -q '^user::rw-$' /tmp/regtest-zallet-identity.acl
grep -q '^user:1000:r--$' /tmp/regtest-zallet-identity.acl
grep -q '^group::---$' /tmp/regtest-zallet-identity.acl
grep -q '^other::---$' /tmp/regtest-zallet-identity.acl

ZALLET_USER="$(docker compose --env-file .env.regtest config --format json \
| python3 -c 'import json, sys; print(json.load(sys.stdin)["services"]["zallet"]["user"])')"
docker run --rm --user "$ZALLET_USER" -v "$PWD/config/regtest":/c:ro alpine \
sh -c 'cat /c/zallet.toml >/dev/null && cat /c/zallet_identity.txt >/dev/null'
sh -c 'cat /c/zallet.toml >/dev/null'

regtest-smoke:
name: Regtest smoke test
Expand All @@ -170,20 +134,8 @@ jobs:

- name: Stage regtest files with setup scripts
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq acl
mkdir -p "$RUNNER_TEMP/fake-bin"
cat > "$RUNNER_TEMP/fake-bin/rage-keygen" <<'EOF'
#!/usr/bin/env sh
set -eu
if [ "$#" -ne 2 ] || [ "$1" != "-o" ]; then
echo "usage: rage-keygen -o <path>" >&2
exit 2
fi
printf '%s\n' '# dummy identity for CI regtest smoke' > "$2"
EOF
chmod +x "$RUNNER_TEMP/fake-bin/rage-keygen"
umask 077
PATH="$RUNNER_TEMP/fake-bin:$PATH" ./scripts/regtest-init.sh --prepare-only
./scripts/regtest-init.sh --prepare-only

- name: Start Zebra (regtest)
run: |
Expand Down Expand Up @@ -235,15 +187,30 @@ jobs:
- name: Verify Zallet accepts compose command
run: docker compose --env-file .env.regtest run --rm --no-deps zallet --help

- name: Generate the encryption identity in-container
# Exercises the real generate-encryption-identity command against the
# pinned image and asserts the key lands in the data volume owner-only
# (mode 0600). The distroless image ships no data dir, so a fresh named
# volume is root-owned; chown it to the zallet uid first, as the init
# scripts do.
run: |
docker run --rm -v z3-regtest-zallet:/d busybox chown 1000:1000 /d
docker compose --env-file .env.regtest run --rm --no-deps zallet \
--datadir /var/lib/zallet --config /etc/zallet/zallet.toml \
generate-encryption-identity
MODE="$(docker run --rm -v z3-regtest-zallet:/d busybox stat -c '%a' /d/identity.txt)"
test "$MODE" = "600"
echo "identity.txt present in z3-regtest-zallet volume, mode $MODE"

- 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
# changes that would make bind-mounted config unreadable.
# changes that would make the bind-mounted config unreadable.
run: |
ZALLET_USER="$(docker compose --env-file .env.regtest config --format json \
| python3 -c 'import json, sys; print(json.load(sys.stdin)["services"]["zallet"]["user"])')"
docker run --rm --user "$ZALLET_USER" -v "$PWD/config/regtest":/c:ro alpine \
sh -c 'cat /c/zallet.toml >/dev/null && cat /c/zallet_identity.txt >/dev/null && echo "compose user read OK"'
sh -c 'cat /c/zallet.toml >/dev/null && echo "compose user read OK"'

- name: Verify the cookie volume is attachable
# Regtest disables cookie auth (ZEBRA_RPC__ENABLE_COOKIE_AUTH=false) so
Expand Down Expand Up @@ -288,7 +255,6 @@ jobs:
run: |
cp config/testnet/zallet.toml.example config/testnet/zallet.toml
cp config/testnet/zaino.toml.example config/testnet/zaino.toml
echo "# dummy identity for CI" > config/testnet/zallet_identity.txt

- name: Start testnet zebra and cookie-permissions sidecar
# The sidecar chmods the cookie 0644 so services that mount the shared
Expand Down
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ Two kinds of people use Z3, and this README is split for them:
## Prerequisites

- [Docker Engine](https://docs.docker.com/engine/install/) with [Docker Compose](https://docs.docker.com/compose/install/) (v2.24.4+)
- [rage](https://github.com/str4d/rage/releases) for generating Zallet encryption keys (`brew install rage` on macOS, or download a release)
- Git, to clone this repository
- `openssl`, only for regtest (it hashes the regtest wallet RPC password); pre-installed on macOS and most Linux distros

Expand Down Expand Up @@ -79,7 +78,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 wallet, and it needs **two** pieces kept together: the `z3-<network>-zallet` volume **and** `config/<network>/zallet_identity.txt` (the age key the wallet database is encrypted with). One without the other cannot be restored. Chain state is re-syncable and the cookie is regenerated, so neither needs backup.
- **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.

The volume table and bind-mount details are in the [Reference](#reference) section.
Expand All @@ -89,7 +88,7 @@ The volume table and bind-mount details are in the [Reference](#reference) secti
Z3 ships production-shaped defaults, but a few choices are yours to make before running mainnet for real:

- **Pick where the chain lives.** The default named volume sits under `/var/lib/docker` (~300 GB on mainnet). To use a dedicated disk, set `Z3_CHAIN_DATA_PATH` and run `fix-permissions.sh` before the first start (see [Where your data lives](#where-your-data-lives)).
- **Plan the wallet backup.** Keep the `z3-<network>-zallet` volume and `config/<network>/zallet_identity.txt` together; nothing else needs backup.
- **Plan the wallet backup.** Back up the `z3-<network>-zallet` volume; it holds the wallet and its encryption identity, and nothing else needs backup.
- **Set a log rotation policy.** Z3 does not pin a logging driver, so containers use your Docker daemon default. Add size limits in `/etc/docker/daemon.json` (see the [FAQ](docs/faq.md)); otherwise logs grow unbounded on a 24/7 node.
- **Decide p2p exposure.** Mainnet and testnet publish Zebra's p2p port for inbound peers. Behind NAT or a firewall, set `ZEBRA_NETWORK__EXTERNAL_ADDR` to the address peers should dial. Regtest is peerless and publishes no p2p.
- **Tune the host network (Linux).** On a busy mainnet node, default kernel TCP buffer and connection-backlog limits can cap Zebra's peer throughput. See [Zebra's TCP tuning notes](https://github.com/ZcashFoundation/zebra/pull/10513) for the `sysctl` values worth raising.
Expand Down Expand Up @@ -151,7 +150,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.
**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.

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 Down Expand Up @@ -247,9 +246,8 @@ docker compose -f docker-compose.yml -f docker-compose.build.yml build

- Copies `config/<network>/zallet.toml.example` -> `config/<network>/zallet.toml` (local, gitignored)
- Copies `config/<network>/zaino.toml.example` → `config/<network>/zaino.toml` (same)
- Generates `config/<network>/zallet_identity.txt` via `rage-keygen` if missing

Subsequent runs leave existing file contents untouched, but still reconcile generated TOML modes and the Zallet identity permissions described below. Back up `zallet_identity.txt` together with the `z3-<network>-zallet` volume; without the identity file the wallet database cannot be decrypted.
Subsequent runs leave existing file contents untouched, but still reconcile the generated TOML modes described below. The Zallet encryption identity is no longer a host file: it is generated in-container into the `z3-<network>-zallet` volume by `zallet-zaino generate-encryption-identity` (see [the wallet setup FAQ](docs/faq.md)). Back up that volume; it holds both the wallet database and the identity that decrypts it.

### Per-network Zallet config

Expand Down Expand Up @@ -358,7 +356,7 @@ Fix permissions before starting:

Zebra, Zaino, and Zallet each run as a specific non-root user. Directories must have correct ownership (set by the script) and `700` permissions. Never use `755` or `777`.

Operators whose host uid is not `1000` do **not** need to coordinate uids for Zallet's bind-mounted config. Zallet runs as uid 1000 (distroless image), and `setup-network.sh` makes generated TOMLs readable by service containers (`0644`) and grants the age key `zallet_identity.txt` read access for uid 1000 via a POSIX ACL (`setfacl -m u:1000:r`) while keeping group and other access closed. The script re-applies this permission contract on every run, including existing files. When your host uid is already `1000` the owner-only key is readable as-is and no ACL is needed; otherwise install the `acl` package on Linux. If your host uid is not `1000` and `setfacl` is unavailable, `setup-network.sh` stops with instructions rather than leaving Zallet to crash at startup — install `acl` and run `setfacl -m u:1000:r` on the identity file (or `chmod 644` it to allow all local users).
Operators whose host uid is not `1000` do **not** need to coordinate uids for Zallet's bind-mounted config. Zallet runs as uid 1000 (distroless image), and `setup-network.sh` makes the generated TOMLs readable by service containers (`0644`), re-applying that mode on every run including existing files. The encryption identity needs no such handling: it lives in the `z3-<network>-zallet` volume, created by uid 1000 in-container, so Zallet reads it directly.

</details>

Expand Down
10 changes: 5 additions & 5 deletions config/mainnet/zallet.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# JSON-RPC endpoint. It does NOT use the standalone Zaino service (which
# serves gRPC to external light-wallet clients).
#
# For full configuration options: `zallet example-config`.
# For full configuration options: `zallet-zaino example-config`.

[builder]
# Transaction builder: defaults.
Expand All @@ -22,7 +22,7 @@ network = "main"
# External settings: defaults.

[features]
as_of_version = "0.1.0-alpha.3"
as_of_version = "0.1.0-alpha.4"

[features.deprecated]

Expand All @@ -35,9 +35,9 @@ validator_address = "zebra:8232"
validator_cookie_path = "/var/run/auth/.cookie"

[keystore]
# Age encryption identity file, mounted by docker-compose from
# ${Z3_CONFIG_DIR}/zallet_identity.txt.
encryption_identity = "/etc/zallet/identity.txt"
# Age encryption identity, generated in-container into the Zallet data volume
# by `zallet-zaino generate-encryption-identity`.
encryption_identity = "/var/lib/zallet/identity.txt"

[note_management]
# Note management: defaults.
Expand Down
4 changes: 2 additions & 2 deletions config/regtest/zallet.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ regtest_nuparams = [
[external]

[features]
as_of_version = "0.1.0-alpha.3"
as_of_version = "0.1.0-alpha.4"

[features.deprecated]

Expand All @@ -37,7 +37,7 @@ validator_user = "zebra"
validator_password = "zebra"

[keystore]
encryption_identity = "/etc/zallet/identity.txt"
encryption_identity = "/var/lib/zallet/identity.txt"

[note_management]

Expand Down
10 changes: 5 additions & 5 deletions config/testnet/zallet.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# JSON-RPC endpoint. It does NOT use the standalone Zaino service (which
# serves gRPC to external light-wallet clients).
#
# For full configuration options: `zallet example-config`.
# For full configuration options: `zallet-zaino example-config`.

[builder]
# Transaction builder: defaults.
Expand All @@ -22,7 +22,7 @@ network = "test"
# External settings: defaults.

[features]
as_of_version = "0.1.0-alpha.3"
as_of_version = "0.1.0-alpha.4"

[features.deprecated]

Expand All @@ -35,9 +35,9 @@ validator_address = "zebra:18232"
validator_cookie_path = "/var/run/auth/.cookie"

[keystore]
# Age encryption identity file, mounted by docker-compose from
# ${Z3_CONFIG_DIR}/zallet_identity.txt.
encryption_identity = "/etc/zallet/identity.txt"
# Age encryption identity, generated in-container into the Zallet data volume
# by `zallet-zaino generate-encryption-identity`.
encryption_identity = "/var/lib/zallet/identity.txt"

[note_management]
# Note management: defaults.
Expand Down
1 change: 0 additions & 1 deletion docker-compose.regtest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ services:
volumes: !override
- ${Z3_ZALLET_DATA_PATH:-zallet}:/var/lib/zallet
- ${Z3_CONFIG_DIR:-./config/regtest}/zallet.toml:/etc/zallet/zallet.toml:ro
- ${Z3_CONFIG_DIR:-./config/regtest}/zallet_identity.txt:/etc/zallet/identity.txt:ro

rpc-router:
build:
Expand Down
Loading
Loading