diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f2aed88 --- /dev/null +++ b/.env.example @@ -0,0 +1,95 @@ +# Z3 environment variable reference. +# +# Pick a network and start the stack: +# docker compose --env-file .env.mainnet up -d +# docker compose --env-file .env.testnet up -d +# docker compose --env-file .env.regtest up -d +# +# Every line below is a commented-out override with its default value. Copy +# this file to .env (or any file you load with --env-file) and uncomment only +# the lines you want to change. See z3-contract.yaml for the env-var schema. +# +# Naming convention: Z3_* variables configure stack-level settings such as +# host ports, image pins, and volume paths. Native service variables +# (ZEBRA_*, ZAINO_*, GF_*) pass through unchanged. + +# Network selection (set by the .env. files; rarely overridden) +# COMPOSE_PROJECT_NAME=z3-mainnet +# Z3_NETWORK=Mainnet +# Z3_CONFIG_DIR=./config/mainnet + +# 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. +# 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_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. +# 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 +# Z3_ZAINO_HOST_GRPC_PORT=8137 +# Z3_ZAINO_HOST_JSON_RPC_PORT=8237 +# Z3_ZALLET_HOST_RPC_PORT=28232 + +# Zebra container ports (vary per network) +# Z3_ZEBRA_RPC_PORT=8232 +# Z3_ZEBRA_P2P_PORT=8233 + +# Data paths (default: Docker named volumes; override to a bind-mount path) +# Z3_CHAIN_DATA_PATH=/mnt/ssd/zebra-state +# Z3_ZAINO_DATA_PATH=/mnt/ssd/zaino-data +# Z3_ZALLET_DATA_PATH=/mnt/ssd/zallet-data +# Z3_COOKIE_PATH=cookie + +# Log levels. +# Per-service Z3__RUST_LOG wins; falls back to global RUST_LOG; falls +# back to a sensible per-service default. Setting `RUST_LOG=debug` in your +# shell or .env flips the whole stack to debug. +# Z3_ZEBRA_RUST_LOG=info +# Z3_ZAINO_RUST_LOG=info,reqwest=warn,hyper_util=warn +# Z3_ZALLET_RUST_LOG=info,hyper_util=warn,reqwest=warn +# RUST_BACKTRACE=full # Zaino reads this directly; default is full + +# RPC router (regtest only) +# Z3_REGTEST_RPC_ROUTER_USER=zebra +# Z3_REGTEST_RPC_ROUTER_PASSWORD=zebra +# Z3_REGTEST_RPC_ROUTER_HOST_PORT=8181 + +# Zebra configuration overrides (native config-rs) +# Advertise a reachable address to peers when behind NAT or a firewall, so +# inbound p2p works (forward the published p2p host port to this host). +# ZEBRA_NETWORK__EXTERNAL_ADDR=203.0.113.10:8233 +# ZEBRA_RPC__ENABLE_COOKIE_AUTH=true +# ZEBRA_HEALTH__MIN_CONNECTED_PEERS=1 +# ZEBRA_HEALTH__READY_MAX_BLOCKS_BEHIND=2 +# ZEBRA_HEALTH__ENFORCE_ON_TEST_NETWORKS=false +# ZEBRA_TRACING__FILTER=info +# Miner address shape depends on the network: tm* for regtest/testnet, +# t1*/t3*/u1* for mainnet. Required for regtest mining. +# ZEBRA_MINING__MINER_ADDRESS=tmSRd1r8gs77Ja67Fw1JcdoXytxsyrLTPJm + +# Monitoring (enable with --profile monitoring; Zebra metrics are on by default) +# Z3_PROMETHEUS_PORT=9094 +# Z3_GRAFANA_PORT=3000 +# GF_SECURITY_ADMIN_PASSWORD=admin +# Z3_JAEGER_UI_PORT=16686 +# Z3_ALERTMANAGER_PORT=9093 +# Z3_JAEGER_OTLP_GRPC_PORT=4317 +# Z3_JAEGER_OTLP_HTTP_PORT=4318 +# Z3_JAEGER_SPANMETRICS_PORT=8889 +# ZEBRA_METRICS__ENDPOINT_ADDR=0.0.0.0:9999 + +# OpenTelemetry tracing. The monitoring profile starts Jaeger, but Zebra only +# exports spans when these vars are set. The default Zebra build features include +# opentelemetry; keep that feature if you override Z3_ZEBRA_BUILD_FEATURES. +# ZEBRA_TRACING__OPENTELEMETRY_ENDPOINT=http://jaeger:4318 +# ZEBRA_TRACING__OPENTELEMETRY_SERVICE_NAME=zebra +# ZEBRA_TRACING__OPENTELEMETRY_SAMPLE_PERCENT=100 diff --git a/.env.mainnet b/.env.mainnet new file mode 100644 index 0000000..b074d37 --- /dev/null +++ b/.env.mainnet @@ -0,0 +1,12 @@ +# Z3 mainnet environment. +# +# Usage: +# docker compose --env-file .env.mainnet up -d +# +# Sets the Compose project name, network selection, and config directory for +# the mainnet stack. docker-compose.yml defaults are already mainnet-shaped +# (Zebra ports 8232 RPC and 8233 p2p), so no port overrides are needed here. + +COMPOSE_PROJECT_NAME=z3-mainnet +Z3_NETWORK=Mainnet +Z3_CONFIG_DIR=./config/mainnet diff --git a/.env.regtest b/.env.regtest new file mode 100644 index 0000000..f458b4b --- /dev/null +++ b/.env.regtest @@ -0,0 +1,48 @@ +# Z3 regtest environment. +# +# Usage: +# docker compose --env-file .env.regtest up -d +# ./scripts/regtest-init.sh (first time, to generate keys + mine block 1) +# +# COMPOSE_FILE merges the regtest overlay on top of the base compose. It does +# NOT reference an override file, so a fresh clone boots without running setup. +# Per-host overrides are opt-in: create docker-compose.regtest.override.yml and +# append it to COMPOSE_FILE (or pass it with -f) only if you need one. +# Regtest inherits Zebra's testnet container-port defaults. Host ports are +# explicit and globally unique so all three networks can coexist. +# Cookie auth is disabled; Zaino and Zallet authenticate to Zebra via +# username/password configured in config/regtest/*.toml. + +COMPOSE_PROJECT_NAME=z3-regtest +COMPOSE_FILE=docker-compose.yml:docker-compose.regtest.yml + +Z3_NETWORK=Regtest +Z3_CONFIG_DIR=./config/regtest + +ZEBRA_RPC__ENABLE_COOKIE_AUTH=false +ZEBRA_HEALTH__MIN_CONNECTED_PEERS=0 +ZEBRA_MINING__MINER_ADDRESS=tmSRd1r8gs77Ja67Fw1JcdoXytxsyrLTPJm + +# Zebra container ports (regtest inherits testnet defaults) +Z3_ZEBRA_RPC_PORT=18232 + +# Zebra host ports +Z3_ZEBRA_HOST_RPC_PORT=29232 +Z3_ZEBRA_HOST_HEALTH_PORT=28080 + +# Zaino / Zallet host ports +Z3_ZAINO_HOST_GRPC_PORT=28137 +Z3_ZAINO_HOST_JSON_RPC_PORT=28237 +Z3_ZALLET_HOST_RPC_PORT=50232 + +# rpc-router host port (regtest-only unified Zebra+Zallet JSON-RPC) +Z3_REGTEST_RPC_ROUTER_HOST_PORT=8181 + +# Monitoring (only used with --profile monitoring) +Z3_PROMETHEUS_PORT=29094 +Z3_GRAFANA_PORT=23000 +Z3_JAEGER_UI_PORT=36686 +Z3_JAEGER_OTLP_GRPC_PORT=25317 +Z3_JAEGER_OTLP_HTTP_PORT=25318 +Z3_JAEGER_SPANMETRICS_PORT=28889 +Z3_ALERTMANAGER_PORT=29093 diff --git a/.env.testnet b/.env.testnet new file mode 100644 index 0000000..c3dffb7 --- /dev/null +++ b/.env.testnet @@ -0,0 +1,45 @@ +# Z3 testnet environment. +# +# Usage: +# docker compose --env-file .env.testnet up -d +# +# Container ports follow Zebra's testnet defaults (18232 RPC, 18233 p2p). +# Host ports for services without a per-network upstream convention (Zaino, +# Zallet, metrics) use a +10000 offset relative to mainnet so testnet and +# mainnet can run concurrently on the same host without port collisions. +# +# Testnet uses the base compose with the testnet values below; there is no +# testnet overlay. Per-host overrides are opt-in: create +# docker-compose.testnet.override.yml and load it explicitly with +# `-f docker-compose.yml -f docker-compose.testnet.override.yml`, or append it +# to COMPOSE_FILE in your operator-local .env. + +COMPOSE_PROJECT_NAME=z3-testnet +COMPOSE_FILE=docker-compose.yml +Z3_NETWORK=Testnet +Z3_CONFIG_DIR=./config/testnet + +ZEBRA_HEALTH__ENFORCE_ON_TEST_NETWORKS=true + +# Zebra container ports (testnet defaults from zebra-chain/parameters/network.rs) +Z3_ZEBRA_RPC_PORT=18232 +Z3_ZEBRA_P2P_PORT=18233 + +# Zebra host ports (match container; testnet ports do not collide with mainnet's) +Z3_ZEBRA_HOST_RPC_PORT=18232 +Z3_ZEBRA_HOST_P2P_PORT=18233 +Z3_ZEBRA_HOST_HEALTH_PORT=18080 + +# Zaino / Zallet host ports +Z3_ZAINO_HOST_GRPC_PORT=18137 +Z3_ZAINO_HOST_JSON_RPC_PORT=18237 +Z3_ZALLET_HOST_RPC_PORT=40232 + +# Monitoring (only used with --profile monitoring) +Z3_PROMETHEUS_PORT=19094 +Z3_GRAFANA_PORT=13000 +Z3_JAEGER_UI_PORT=26686 +Z3_JAEGER_OTLP_GRPC_PORT=15317 +Z3_JAEGER_OTLP_HTTP_PORT=15318 +Z3_JAEGER_SPANMETRICS_PORT=18889 +Z3_ALERTMANAGER_PORT=19093 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..40975a8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + groups: + github-actions: + patterns: ["*"] + cooldown: + default-days: 7 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..68e234c --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,248 @@ +name: CI + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +on: + pull_request: + push: + branches: [main] + +permissions: {} + +jobs: + contract-validation: + name: Contract validation (all networks) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Stage local config files for compose render + # The live config//{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. + 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 + run: | + docker compose --env-file .env.mainnet config --quiet + docker compose --env-file .env.testnet config --quiet + docker compose --env-file .env.regtest config --quiet + 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 + + - name: Install Python deps (contract parser + JSON Schema validator) + run: python3 -m pip install --quiet pyyaml jsonschema + + - name: Validate z3-contract.yaml against z3-contract.schema.json + run: | + python3 -c ' + import json, sys, yaml, jsonschema + contract = yaml.safe_load(open("z3-contract.yaml")) + schema = json.load(open("z3-contract.schema.json")) + jsonschema.validate(contract, schema) + print("PASS: z3-contract.yaml validates against z3-contract.schema.json") + ' + + - name: Validate platform contract against resolved compose + run: ./scripts/validate-contract.py + + - name: Validate contract inventory parity (env vars across compose and .env.example) + run: ./scripts/validate-contract-parity.py + + regtest-smoke: + name: Regtest smoke test + runs-on: ubuntu-latest + needs: contract-validation + # External identifiers for the regtest stack. Keep aligned with + # z3-contract.yaml networks.regtest.{external_network,volumes.cookie}. + # Values are literal constants, not user input — safe in run: blocks. + env: + COOKIE_VOLUME: z3-regtest-cookie + EXTERNAL_NETWORK: z3-regtest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Stage local files (live TOMLs from templates, identity) + # Mirrors the file setup step without requiring rage in CI. The sed + # command patches the live gitignored copy, not the tracked template. + run: | + cp config/regtest/zallet.toml.example config/regtest/zallet.toml + cp config/regtest/zaino.toml.example config/regtest/zaino.toml + cp config/regtest/zebra.toml.example config/regtest/zebra.toml + echo "# dummy identity for CI" > config/regtest/zallet_identity.txt + SALT=$(openssl rand -hex 16) + HASH=$(printf 'zebra' | openssl dgst -sha256 -mac HMAC -macopt "key:$SALT" | awk '{print $NF}') + sed -i "s|__GENERATED_BY_INIT_SH__|${SALT}\$${HASH}|" config/regtest/zallet.toml + # Mirror the permission end-state of setup-network.sh / regtest-init.sh + # so the readability assertion below exercises the real contract: + # non-secret TOMLs world-readable, the age key 0600 + ACL for uid 1000. + chmod 644 config/regtest/zallet.toml config/regtest/zaino.toml config/regtest/zebra.toml + chmod 600 config/regtest/zallet_identity.txt + setfacl -m u:1000:r config/regtest/zallet_identity.txt + + - name: Start Zebra (regtest) + run: | + docker compose --env-file .env.regtest up -d zebra + echo "Waiting for Zebra RPC on host port 29232..." + for i in $(seq 1 60); do + if curl -sf -X POST -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"getblockchaininfo","params":[],"id":1}' \ + http://127.0.0.1:29232 > /dev/null 2>&1; then + echo "Zebra is ready" + break + fi + if [ "$i" -eq 60 ]; then echo "Zebra failed to start" && exit 1; fi + sleep 2 + done + + - name: Verify Zebra is serving RPC + run: | + curl -sf -X POST -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"getblockchaininfo","params":[],"id":1}' \ + http://127.0.0.1:29232 | jq . + + - name: Mine block 1 (required for Zaino sync) + run: | + curl -sf -u zebra:zebra -X POST -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"generate","params":[1],"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 + + - name: Assert Zallet config is readable by the container uid (1000) + # Zallet runs as uid 1000 (distroless image, no runtime chown). Guards + # the config-readability contract: zallet.toml is world-readable and the + # age key is reachable by uid 1000 via ACL. Catches a regression of the + # mktemp-0600 mode leaking back onto the rewritten config. + run: | + docker run --rm --user 1000:1000 -v "$PWD/config/regtest":/c:ro alpine \ + sh -c 'cat /c/zallet.toml >/dev/null && cat /c/zallet_identity.txt >/dev/null && echo "uid 1000 read OK"' + + - name: Verify the cookie volume is attachable + # Regtest disables cookie auth (ZEBRA_RPC__ENABLE_COOKIE_AUTH=false) so + # Zebra does not write /auth/.cookie. This check only verifies that the + # published volume can be mounted. + run: | + docker run --rm -v "$COOKIE_VOLUME":/auth:ro alpine \ + sh -c 'test -d /auth && echo "Cookie volume mounted read-only at /auth"' + + - 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' + + - name: Collect logs on failure + if: failure() + run: | + echo "=== Container status ===" + 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 + + cookie-permission-smoke: + # Regtest disables cookie auth, so the regtest smoke job does not exercise + # the cookie path. This job boots testnet Zebra and checks that the cookie + # file is readable by service runtime users. + name: Cookie permission smoke (testnet) + runs-on: ubuntu-latest + needs: contract-validation + # External cookie volume for the testnet stack. Keep aligned with + # z3-contract.yaml networks.testnet.volumes.cookie. + env: + COOKIE_VOLUME: z3-testnet-cookie + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Stage operator files for testnet boot + 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 + # volume can read it. + run: | + docker compose --env-file .env.testnet up -d zebra cookie-permissions + echo "Waiting up to 60s for Zebra to write the cookie file..." + for i in $(seq 1 30); do + if docker run --rm -v "$COOKIE_VOLUME":/auth:ro alpine test -f /auth/.cookie 2>/dev/null; then + echo "Cookie file present after ~$((i*2))s" + break + fi + if [ "$i" -eq 30 ]; then + echo "Cookie file never appeared" + docker compose --env-file .env.testnet logs zebra --tail 50 + exit 1 + fi + sleep 2 + done + # Give the sidecar one polling cycle to chmod the cookie. + sleep 6 + + - name: Assert cookie file is readable by both Zaino (uid 1000) and Zallet (uid 1000) + # Test the runtime UIDs used by the services that read the shared + # cookie volume. + run: | + docker run --rm --user 1000:1000 -v "$COOKIE_VOLUME":/auth:ro alpine \ + sh -c 'cat /auth/.cookie >/dev/null && echo "uid 1000 read OK"' + docker run --rm --user 10001:10001 -v "$COOKIE_VOLUME":/auth:ro alpine \ + sh -c 'cat /auth/.cookie >/dev/null && echo "uid 10001 read OK"' + + - name: Tear down + if: always() + run: docker compose --env-file .env.testnet down -v --remove-orphans 2>&1 || true + + rpc-router-tests: + name: rpc-router cargo test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Install Rust toolchain + run: | + rustup toolchain install 1.85 --profile minimal --no-self-update + rustup default 1.85 + + - name: cargo test + working-directory: rpc-router + run: cargo test --locked --all-targets diff --git a/.github/workflows/zizmor.yaml b/.github/workflows/zizmor.yaml new file mode 100644 index 0000000..81223bd --- /dev/null +++ b/.github/workflows/zizmor.yaml @@ -0,0 +1,33 @@ +name: GitHub Actions security analysis + +on: + push: + branches: [main] + paths: + - '.github/workflows/**' + pull_request: + paths: + - '.github/workflows/**' + +permissions: {} + +jobs: + zizmor: + name: zizmor + runs-on: ubuntu-latest + permissions: + security-events: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + - name: Run zizmor + run: uvx zizmor --format sarif . > results.sarif + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@5c8a8a642e79153f5d047b10ec1cba1d1cc65699 # v3.35.1 + with: + sarif_file: results.sarif + category: zizmor diff --git a/.gitignore b/.gitignore index 0104787..e83caca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# macOS +.DS_Store + # Generated by Cargo # will have compiled files and executables debug/ @@ -14,4 +17,41 @@ target/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ + +# Config directory: tracked defaults live in .example files; active config, +# identity files, and TLS keys are local and gitignored. +# ============================================================================= +config/** +!config/.gitkeep +# Per-network: track only .example templates; live .toml files are local +!config/mainnet/ +!config/mainnet/zaino.toml.example +!config/mainnet/zallet.toml.example +!config/testnet/ +!config/testnet/zaino.toml.example +!config/testnet/zallet.toml.example +!config/regtest/ +!config/regtest/zaino.toml.example +!config/regtest/zallet.toml.example +!config/regtest/zebra.toml.example + +# Runtime environment overrides (see .env.example for available variables) +.env + +# Local comparator state +.tmp/ + +# Mainnet auto-loads docker-compose.override.yml when present (Compose's native +# behavior; absent is not an error). Per-network overrides are opt-in via -f or +# COMPOSE_FILE, so they are gitignored too if an operator creates one. +docker-compose.override.yml +docker-compose.testnet.override.yml +docker-compose.regtest.override.yml + +# Python bytecode caches (from scripts/validate-contract*.py) +__pycache__/ +*.pyc + +# Opt-in source-build checkouts (scripts/vendor.sh) +vendor/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f2f2775 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "yaml.customTags": [ + "!override sequence", + "!override mapping", + "!reset sequence", + "!reset mapping" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index bc322a6..e10ae0a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,427 @@ -# z3 -Repository to iterate on the grand unification of Zebra, Zaino and Zallet to replace the venerable Zcashd +# 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. + +Two kinds of people use Z3, and this README is split for them: + +- **Operators** run the stack as infrastructure. Start at [Choose your network](#choose-your-network); everything you need to run Z3 is in that half. +- **Developers, testers, and researchers** build or test against Z3, often running several networks at once. Start at [Building and testing against Z3](#building-and-testing-against-z3). + +## 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 + +## For operators + +### Choose your network + +Z3 runs as one of three independent Compose projects. Pick by what you are doing: + +| Network | Use it for | First sync | Real funds | +|---------|-----------|------------|------------| +| **mainnet** | Production: the real Zcash chain | 24-72 hours | Yes | +| **testnet** | Staging against the public test network | 2-12 hours | No (test ZEC) | +| **regtest** | Local practice and development: instant blocks, no peers, no sync | Seconds | No | + +All three can run at once on one host; each gets its own ports and volumes. New to Z3? Start with **regtest** to watch the whole stack come up in seconds, then move to mainnet. + +### Quick start + +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. + +```bash +git clone https://github.com/ZcashFoundation/z3 && cd z3 + +# 1. One-time setup: creates local config files and the Zallet wallet identity. +./scripts/setup-network.sh mainnet + +# 2. Start Zebra and wait for it to sync. The poller exits when Zebra is ready. +docker compose --env-file .env.mainnet up -d zebra +./scripts/check-zebra-readiness.sh + +# 3. Start Zaino + 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. + +Your edits to the per-network config under `config//` stay local and survive `git pull`. + +**Testnet (public test network).** The same two-phase flow; pass the testnet health port to the poller: + +```bash +./scripts/setup-network.sh testnet +docker compose --env-file .env.testnet up -d zebra +./scripts/check-zebra-readiness.sh 18080 +docker compose --env-file .env.testnet up -d +``` + +**Regtest (local practice and development).** Regtest mines blocks on demand with no peers, so it boots in seconds. One command does setup, starts Zebra, and mines the activation blocks: + +```bash +./scripts/regtest-init.sh +docker compose --env-file .env.regtest up -d +``` + +After the first run, the `up -d` line alone is enough. See [docs/regtest.md](docs/regtest.md) for test commands and the full workflow. + +### Where your data lives + +Each network keeps its chain state in a Docker named volume called `z3--chain`, which lands under Docker's data root (on Linux, `/var/lib/docker/volumes/`). Mainnet is roughly **300 GB**; size the disk before you start. + +- **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--zallet` volume **and** `config//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. +- **Stop vs wipe:** `docker compose --env-file .env. 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. + +### Running in production + +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--zallet` volume and `config//zallet_identity.txt` together; 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. +- **Bound resources on a shared host.** No CPU or memory limits are set by default: right for a dedicated node, easy to get wrong on a shared box. Add `deploy.resources.limits` in an override file if you need them. + +Z3 ships safe defaults: pinned image versions (no surprise upgrades), non-root containers with Linux capabilities dropped, health checks that hold the wallet back until the node is synced, and automatic restart. Upgrades stay deliberate: bump the version pin in a reviewed change, or set `Z3__IMAGE`. + +### Monitoring + +Prometheus, Grafana, Jaeger, and AlertManager ship behind a Compose profile. Zebra's metrics are on by default at the in-network `zebra:9999` scrape target. + +```bash +docker compose --env-file .env. --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`. + +### 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) +``` + +## Building and testing against Z3 + +You do not need this section to operate Z3. It covers running several networks for test scenarios, how the networks differ, the published service endpoints, and attaching your own services. + +### How the networks differ + +Operators usually run one network; testers often run several and need to know where they diverge from a deployment standpoint: + +| Aspect | mainnet | testnet | regtest | +|--------|---------|---------|---------| +| Peers / p2p | Real peers; p2p published (`8233`) | Test peers; p2p published (`18233`) | Peerless; no p2p | +| Blocks | From the network | From the network | Mined on demand (`generate`) | +| Sync wait before use | Hours to days | Hours | None | +| RPC auth | Cookie file | Cookie file | Username / password | +| In-network Zebra RPC | `zebra:8232` | `zebra:18232` | `zebra:18232` | +| Coexists on one host with | testnet, regtest | mainnet, regtest | mainnet, testnet | + +Mainnet pairs cleanly with either other network on one host. Testnet and regtest reuse Zebra's testnet container ports, so they differ only by published host port (the env files keep those unique). The rpc-router, a unified Zebra+Zallet JSON-RPC endpoint, runs only on regtest today. + +### Architecture + +```mermaid +graph LR + subgraph Z3["Z3 (per network)"] + Zebra["Zebra
(full node)"] + Zaino["Zaino
(indexer)"] + + subgraph Zallet["Zallet (wallet)"] + EmbeddedZaino["Embedded
Zaino libs"] + end + end + + Zebra -->|JSON-RPC| Zaino + Zebra -->|JSON-RPC| EmbeddedZaino + Zaino -->|gRPC| LightClients["Light wallet
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. + +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). + +### Service endpoints + +Published host ports are chosen per `.env.` so all networks coexist on one host. The full per-network matrix lives in [`z3-contract.yaml`](z3-contract.yaml). + +| Service | Endpoint shape | Env var | +|---------|----------------|---------| +| Zebra RPC | `http://localhost:` | `Z3_ZEBRA_HOST_RPC_PORT` | +| Zebra p2p (inbound peers) | `localhost:` | `Z3_ZEBRA_HOST_P2P_PORT` | +| Zebra health | `http://localhost:/ready` | `Z3_ZEBRA_HOST_HEALTH_PORT` | +| Zaino gRPC (plaintext, no TLS) | `localhost:` | `Z3_ZAINO_HOST_GRPC_PORT` | +| Zaino JSON-RPC | `http://localhost:` | `Z3_ZAINO_HOST_JSON_RPC_PORT` | +| Zallet RPC | `http://localhost:` | `Z3_ZALLET_HOST_RPC_PORT` | + +Inside the network, services resolve by DNS name (`zebra`, `zaino`, `zallet`) on Zebra's per-network container ports. + +### Attaching your own services + +> Operators can skip this. It exists for downstream services, wallets, and tooling that attach to a running Z3 stack, and for agents that consume the API programmatically. + +Z3 publishes a stable set of identifiers (network names, volume names, ports, and auth surfaces) so your service can attach by name and keep working across networks. A mainnet or testnet peer attaches over the external network and reads the RPC cookie from the shared volume: + +```yaml +networks: + z3: + external: true + name: z3-testnet +volumes: + z3-cookie: + external: true + name: z3-testnet-cookie +services: + my-app: + networks: [z3] + volumes: + - z3-cookie:/var/run/auth:ro + environment: + ZEBRA_RPC_URL: http://zebra:18232 + ZEBRA_COOKIE_PATH: /var/run/auth/.cookie +``` + +Regtest uses username/password instead of cookie auth; do not mount `z3-regtest-cookie` expecting a readable cookie. For the three attachment patterns (Compose peer, host-side pointer, lightwalletd client), see [docs/integrations/](docs/integrations/). For the full identifier inventory and stability promise, see [docs/contract.md](docs/contract.md) and [`z3-contract.yaml`](z3-contract.yaml). + +## Reference + +
+System requirements + +### Minimum + +- **CPU:** 2 cores (4+ recommended) +- **RAM:** 4 GB for Zebra alone; 8+ GB for the full stack +- **Disk:** Mainnet ~300 GB, Testnet ~30 GB (SSD strongly recommended) +- **Network:** Reliable internet; initial mainnet sync downloads ~300 GB + +### Recommended + +- **CPU:** 4+ cores +- **RAM:** 16+ GB +- **Disk:** 500+ GB with room for blockchain growth +- **Network:** 100+ Mbps, ~300 GB/month bandwidth + +### Sync times + +| Network | First sync | With existing data | +|---------|-----------|-------------------| +| Mainnet | 24-72 hours | Minutes | +| Testnet | 2-12 hours | Minutes | + +Based on [Zebra's official requirements](https://zebra.zfnd.org/user/requirements.html). + +
+ +
+Setup details + +### Building from source + +Pre-built images are pulled by default. To build Zebra, Zaino, and Zallet from upstream source instead, fetch the sources and add the opt-in build overlay: + +```bash +scripts/vendor.sh +docker compose -f docker-compose.yml -f docker-compose.build.yml build +``` + +`scripts/vendor.sh` clones each upstream repo into a gitignored `vendor/` directory at the tag matching its image pin. + +### First-run setup (`setup-network.sh`) + +`./scripts/setup-network.sh ` is idempotent and does everything needed before the first `docker compose up`: + +- Copies `config//zallet.toml.example` -> `config//zallet.toml` (local, gitignored) +- Copies `config//zaino.toml.example` → `config//zaino.toml` (same) +- Generates `config//zallet_identity.txt` via `rage-keygen` if missing + +Subsequent runs print which steps were skipped. Back up `zallet_identity.txt` together with the `z3--zallet` volume; without the identity file the wallet database cannot be decrypted. + +### Per-network Zallet config + +Zallet config lives at `config//zallet.toml` (local, gitignored). The tracked `.example` template carries the default. The `[indexer]` section points at the per-network Zebra RPC port (8232 mainnet, 18232 testnet, 18232 regtest). Edit your live `.toml` freely; pulls won't conflict. + +To compare your copy against a refreshed template after `git pull`: + +```bash +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. + +To run Zaino and Zallet natively on arm64, build them 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 +``` + +
+ +
+Configuration reference + +### Defaults in compose + +Every variable in `docker-compose.yml` has a default via `${VAR:-default}`. The stack works with zero configuration files; the per-network env files override only what differs from mainnet. + +Precedence (highest wins): + +1. Shell environment variables +2. `--env-file ` arguments +3. `.env` file values (auto-loaded) +4. Compose file defaults + +### Variable naming + +Two namespaces keep stack-level settings separate from service-native settings: + +| Namespace | Scope | Examples | +|-----------|-------|----------| +| `Z3_*` | Stack-level settings: port matrix, image pins, volume paths, per-service log split, monitoring port matrix | `Z3_NETWORK`, `Z3_ZEBRA_HOST_RPC_PORT`, `Z3_ZEBRA_IMAGE`, `Z3_ZEBRA_RUST_LOG` | +| `ZEBRA_*` / `ZAINO_*` | Service-native config-rs vars (double-underscore is config-rs nesting) | `ZEBRA_RPC__ENABLE_COOKIE_AUTH`, `ZEBRA_HEALTH__MIN_CONNECTED_PEERS` | +| `DOCKER_PLATFORM`, `COMPOSE_*`, `RUST_LOG` | Ecosystem / shell standards | `DOCKER_PLATFORM=linux/arm64` | + +z3 sets the service-internal vars (`ZEBRA_RPC__LISTEN_ADDR`, `ZAINO_VALIDATOR_SETTINGS__*`, etc.) inside the compose `environment:` blocks based on the public knobs above. Operators should not set those directly. + +### Common overrides + +```bash +# Per-service log levels +Z3_ZEBRA_RUST_LOG=debug +Z3_ZAINO_RUST_LOG=debug + +# Pin a different image version +Z3_ZEBRA_IMAGE=zfnd/zebra:5.0.0 + +# Move chain state to an external SSD +Z3_CHAIN_DATA_PATH=/mnt/ssd/zebra-state + +# Disable Zebra cookie auth (advanced; native Zebra config-rs var) +ZEBRA_RPC__ENABLE_COOKIE_AUTH=false +``` + +When using the documented `--env-file .env.` commands, put these in the shell environment or pass `.env` as a second `--env-file`; Compose does not auto-load `.env` in that mode. See `.env.example` for the full reference. + +
+ +
+Data storage and volumes + +### Docker named volumes (default) + +Z3 declares each volume with an explicit `name:` so the external Docker identifier is `${COMPOSE_PROJECT_NAME}-`. Volume contents per network: + +| Suffix | Contents | +|--------|----------| +| `chain` | Zebra blockchain state (~300 GB mainnet, ~30 GB testnet) | +| `zaino` | Zaino indexer database | +| `zallet` | Zallet wallet database (contains keys) | +| `cookie` | RPC authentication cookie for mainnet/testnet (regtest disables cookie auth) | + +Example concrete names: `z3-mainnet-chain`, `z3-testnet-cookie`, `z3-regtest-zallet`. + +### Local directories (advanced) + +For backups, external SSDs, or shared storage, override volume paths in `.env`: + +```bash +Z3_CHAIN_DATA_PATH=/mnt/ssd/zebra-state +Z3_ZAINO_DATA_PATH=/mnt/ssd/zaino-data +Z3_ZALLET_DATA_PATH=/mnt/ssd/zallet-data +``` + +Fix permissions before starting: + +```bash +./scripts/fix-permissions.sh zebra /mnt/ssd/zebra-state +./scripts/fix-permissions.sh zaino /mnt/ssd/zaino-data +./scripts/fix-permissions.sh zallet /mnt/ssd/zallet-data +``` + +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 `zallet.toml` readable by that uid (`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 it `0600` for everyone else. Install the `acl` package on Linux for this; if `setfacl` is unavailable the script warns and you can fall back to `chmod 644` on the identity file. + +
+ +
+Health checks and sync strategy + +### Two-phase deployment + +Zebra's blockchain sync takes hours to days. Docker Compose healthcheck timeouts cannot accommodate this, so the stack uses a two-phase approach: + +1. Start Zebra alone: `docker compose --env-file .env.mainnet up -d zebra` +2. Wait for sync: `curl http://localhost:8080/ready` returns `ok` +3. Start the full stack: `docker compose --env-file .env.mainnet up -d` + +### Health endpoints + +Zebra exposes two endpoints on its health port: + +| Endpoint | Returns 200 when | Use for | +|----------|-------------------|---------| +| `/healthy` | Minimum peer connections present | Liveness monitoring, restart decisions | +| `/ready` | Synced within 2 blocks of tip | Production readiness, dependency gating | + +### Service dependency chain + +``` +Zebra (/ready: synced near tip) + -> Cookie permissions (.cookie readable on cookie-auth networks) + -> Zaino (gRPC port responding) + -> Zallet (RPC responding) +``` + +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). + +### Monitoring sync progress + +```bash +curl http://localhost:8080/ready # "ok" when synced +docker compose --env-file .env.mainnet logs -f zebra +./scripts/check-zebra-readiness.sh # polls until synced, prints status every 30s +``` + +### Tuning health checks + +Three Zebra healthcheck thresholds are operator-tunable. Defaults live in `docker-compose.yml`; override in `.env`: + +```bash +# How far behind tip /ready tolerates (raise during catch-up syncs) +ZEBRA_HEALTH__READY_MAX_BLOCKS_BEHIND=10 + +# Minimum peers required for /healthy (set 0 for regtest) +ZEBRA_HEALTH__MIN_CONNECTED_PEERS=3 + +# Make /ready always return 200 on testnet even during sync +ZEBRA_HEALTH__ENFORCE_ON_TEST_NETWORKS=true +``` + +
+ +## Further reading + +- **Operators:** [docs/faq.md](docs/faq.md) (the "For operators" section) +- **Developers and testers:** [docs/faq.md](docs/faq.md) (the "For developers and testers" section), [docs/regtest.md](docs/regtest.md), [docs/integrations/](docs/integrations/) +- **Integrators and agents:** [docs/contract.md](docs/contract.md), [`z3-contract.yaml`](z3-contract.yaml) +- **Internals:** [docs/docker-architecture.md](docs/docker-architecture.md): Compose patterns, overlay merge rules, security hardening rationale +- **Every env var:** [.env.example](.env.example) diff --git a/config/.gitkeep b/config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/config/mainnet/zaino.toml.example b/config/mainnet/zaino.toml.example new file mode 100644 index 0000000..7babd6d --- /dev/null +++ b/config/mainnet/zaino.toml.example @@ -0,0 +1,5 @@ +# Zaino configuration: Mainnet. +# +# All runtime settings come from environment variables in docker-compose.yml. +# This file exists so the compose mount has a target; do not add values here +# unless an operator-specific override is needed that env vars cannot express. diff --git a/config/mainnet/zallet.toml.example b/config/mainnet/zallet.toml.example new file mode 100644 index 0000000..cee7340 --- /dev/null +++ b/config/mainnet/zallet.toml.example @@ -0,0 +1,49 @@ +# Zallet configuration: Mainnet. +# +# Zallet embeds Zaino's indexer libraries and connects directly to Zebra's +# 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`. + +[builder] +# Transaction builder: defaults. + +[builder.limits] +# Transaction limits: defaults. + +[consensus] +network = "main" + +[database] +# Wallet database at /wallet.db. + +[external] +# External settings: defaults. + +[features] +as_of_version = "0.1.0-alpha.3" + +[features.deprecated] + +[features.experimental] + +[indexer] +# Mainnet Zebra RPC port: 8232 (Zebra's canonical mainnet port). The service +# DNS name `zebra` resolves on the z3-mainnet network. +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" + +[note_management] +# Note management: defaults. + +[rpc] +# Zallet RPC listen address inside the container. +# Default port 28232; for a different host port set Z3_ZALLET_HOST_RPC_PORT. +bind = ["0.0.0.0:28232"] +timeout = 30 diff --git a/config/regtest/zaino.toml.example b/config/regtest/zaino.toml.example new file mode 100644 index 0000000..1b218ae --- /dev/null +++ b/config/regtest/zaino.toml.example @@ -0,0 +1,6 @@ +# Zaino config for regtest — auth via username/password (no cookie auth) +backend = "fetch" + +[validator_settings] +validator_user = "zebra" +validator_password = "zebra" diff --git a/config/regtest/zallet.toml.example b/config/regtest/zallet.toml.example new file mode 100644 index 0000000..a24770b --- /dev/null +++ b/config/regtest/zallet.toml.example @@ -0,0 +1,54 @@ +[builder.limits] +orchard_actions = 250 + +[consensus] +network = "regtest" +# NU activation heights must match Zebra's regtest config in +# config/regtest/zebra.toml and Zaino's hardcoded ActivationHeights +# (Canopy at 1, NU5 at 2, NU6 at 2, NU6.1 at 1000). Diverging in either +# direction breaks the wallet's view of the chain. NU6.2 is intentionally +# absent: zcash_protocol 0.7.2 (this Zallet's dep) has no NU6.2 branch id, +# so a "5437f330:..." entry would fail to parse ("Unknown consensus branch ID"). +regtest_nuparams = [ + "5ba81b19:1", # Overwinter + "76b809bb:1", # Sapling + "2bb40e60:1", # Blossom + "f5b9230b:1", # Heartwood + "e9ff75a6:1", # Canopy + "c2d6d0b4:2", # NU5 (Orchard); matches Zaino's hardcoded expectation + "c8e71055:2", # NU6; matches Zaino's hardcoded NU6=2 and Zebra's NU6=2 + "4dec4df0:1000", # NU6.1; matches Zaino's hardcoded NU6.1=1000 +] + +[database] + +[external] + +[features] +as_of_version = "0.1.0-alpha.3" + +[features.deprecated] + +[features.experimental] + +[indexer] +validator_address = "zebra:18232" +validator_user = "zebra" +validator_password = "zebra" + +[keystore] +encryption_identity = "/etc/zallet/identity.txt" + +[note_management] + +[rpc] +# Zallet RPC listen address inside the container. +# Default port 28232; for a different host port set Z3_ZALLET_HOST_RPC_PORT. +bind = ["0.0.0.0:28232"] +timeout = 30 + +[[rpc.auth]] +user = "zebra" +# Generated by scripts/regtest-init.sh from RPC_PASSWORD (default: "zebra"). +# Run ./scripts/regtest-init.sh to generate after cloning. +pwhash = "__GENERATED_BY_INIT_SH__" diff --git a/config/regtest/zebra.toml.example b/config/regtest/zebra.toml.example new file mode 100644 index 0000000..2181d9f --- /dev/null +++ b/config/regtest/zebra.toml.example @@ -0,0 +1,22 @@ +# Zebra regtest configuration. `scripts/setup-network.sh regtest` copies this +# template into a live gitignored config file on first run. +# +# Zebra's default regtest stops at Canopy. Activating NU5, NU6, and NU6.1 +# here matches Zaino's hardcoded regtest ActivationHeights +# (ZEBRAD_DEFAULT_ACTIVATION_HEIGHTS: NU5=2, NU6=2, NU6.1=1000) so its sync +# loop can read the chain Zebra produces. zebrad reads ~/.config/zebrad.toml +# automatically. NU6.1 must be 1000 (not 2) to match Zaino's chain view. + +[network] +network = "Regtest" + +[network.testnet_parameters.activation_heights] +BeforeOverwinter = 1 +Overwinter = 1 +Sapling = 1 +Blossom = 1 +Heartwood = 1 +Canopy = 1 +NU5 = 2 +NU6 = 2 +"NU6.1" = 1000 diff --git a/config/testnet/zaino.toml.example b/config/testnet/zaino.toml.example new file mode 100644 index 0000000..4f77757 --- /dev/null +++ b/config/testnet/zaino.toml.example @@ -0,0 +1,5 @@ +# Zaino configuration: Testnet. +# +# All runtime settings come from environment variables in docker-compose.yml. +# This file exists so the compose mount has a target; do not add values here +# unless an operator-specific override is needed that env vars cannot express. diff --git a/config/testnet/zallet.toml.example b/config/testnet/zallet.toml.example new file mode 100644 index 0000000..5018f0c --- /dev/null +++ b/config/testnet/zallet.toml.example @@ -0,0 +1,49 @@ +# Zallet configuration: Testnet. +# +# Zallet embeds Zaino's indexer libraries and connects directly to Zebra's +# 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`. + +[builder] +# Transaction builder: defaults. + +[builder.limits] +# Transaction limits: defaults. + +[consensus] +network = "test" + +[database] +# Wallet database at /wallet.db. + +[external] +# External settings: defaults. + +[features] +as_of_version = "0.1.0-alpha.3" + +[features.deprecated] + +[features.experimental] + +[indexer] +# Testnet Zebra RPC port: 18232 (Zebra's canonical testnet port). The service +# DNS name `zebra` resolves on the z3-testnet network. +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" + +[note_management] +# Note management: defaults. + +[rpc] +# Zallet RPC listen address inside the container. +# Default port 28232; for a different host port set Z3_ZALLET_HOST_RPC_PORT. +bind = ["0.0.0.0:28232"] +timeout = 30 diff --git a/docker-compose.build.yml b/docker-compose.build.yml new file mode 100644 index 0000000..6c759a2 --- /dev/null +++ b/docker-compose.build.yml @@ -0,0 +1,40 @@ +# Opt-in source-build overlay. +# +# The default stack pulls pinned images and needs no source checkout. This +# overlay re-adds build: contexts so contributors can build Zebra, Zaino, and +# Zallet from upstream source (for native arm64, or to test a local patch). +# +# Usage: +# scripts/vendor.sh # clone upstream into vendor/ at the pinned tags +# docker compose -f docker-compose.yml -f docker-compose.build.yml build +# +# or append it to COMPOSE_FILE for the session: +# COMPOSE_FILE=docker-compose.yml:docker-compose.build.yml docker compose build zaino +# +# The build args here mirror the published pins (Zebra's FEATURES, Zaino's +# NO_TLS guard) so a local build matches the image it replaces. Bump the tags +# in scripts/vendor.sh together with the image pins in docker-compose.yml. + +services: + zebra: + build: + context: ./vendor/zebra + dockerfile: docker/Dockerfile + target: runtime + args: + FEATURES: ${Z3_ZEBRA_BUILD_FEATURES:-default-release-binaries} + + zaino: + build: + context: ./vendor/zaino + dockerfile: Dockerfile + args: + NO_TLS: "true" + + zallet: + build: + context: ./vendor/zallet + dockerfile: Dockerfile + target: runtime + tags: + - z3_zallet:local diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example new file mode 100644 index 0000000..2ae97cd --- /dev/null +++ b/docker-compose.override.yml.example @@ -0,0 +1,29 @@ +# docker-compose.override.yml.example +# +# Development override for quick iteration without waiting for Zebra sync +# +# Usage: +# cp docker-compose.override.yml.example docker-compose.override.yml +# docker compose up -d +# +# This override changes Zebra's healthcheck from /ready to /healthy, +# allowing dependent services (Zaino, Zallet) to start immediately +# once Zebra has peer connections, without waiting for full sync. +# +# ⚠️ WARNING: Services may experience delays or errors while Zebra syncs +# ⚠️ Only use for development/testing, NOT production +# +# For production or production-like testing, follow the two-phase deployment: +# 1. docker compose up -d zebra # Sync Zebra first +# 2. docker compose up -d # Start full stack + +services: + zebra: + healthcheck: + # Use /healthy (has peers) instead of /ready (synced to tip) + # Allows Zaino and Zallet to start immediately + test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8080/healthy || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s diff --git a/docker-compose.regtest.yml b/docker-compose.regtest.yml new file mode 100644 index 0000000..e7c68a4 --- /dev/null +++ b/docker-compose.regtest.yml @@ -0,0 +1,79 @@ +# Regtest overlay merged on top of docker-compose.yml. +# +# Usage: +# docker compose --env-file .env.regtest up -d +# ./scripts/regtest-init.sh (first time, to generate keys + mine block 1) +# +# Only contains regtest-specific structural overrides. Network selection, +# port matrix, project name, and config dir come from .env.regtest. + +services: + zebra: + # ZEBRA_MINING__MINER_ADDRESS is required for regtest mining. The :? guard + # fails the compose substitution with a clear message if the var is unset, + # rather than passing an empty string that crashes Zebra at startup + # (config-rs treats empty strings as values; ZcashAddress::from_str("") + # is a parse error). + environment: + ZEBRA_MINING__MINER_ADDRESS: ${ZEBRA_MINING__MINER_ADDRESS:?must be set in .env.regtest or shell environment} + # Zebra's default regtest stops at Canopy; this config activates NU5/NU6 + # at the heights Zaino's regtest defaults expect. zebrad reads + # ~/.config/zebrad.toml automatically. + volumes: + - ${Z3_CONFIG_DIR:-./config/regtest}/zebra.toml:/home/zebra/.config/zebrad.toml:ro + # Regtest is peerless, so p2p is not published (its container port also + # reuses testnet's 18233). Re-list the published ports without p2p. + ports: !override + - "${Z3_ZEBRA_HOST_RPC_PORT:-29232}:${Z3_ZEBRA_RPC_PORT:-18232}" + - "${Z3_ZEBRA_HOST_HEALTH_PORT:-28080}:8080" + # Regtest has no peers; check RPC directly instead of /ready. + healthcheck: + test: ["CMD-SHELL", "curl -sf -X POST -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"method\":\"getblockchaininfo\",\"params\":[],\"id\":1}' http://127.0.0.1:${Z3_ZEBRA_RPC_PORT:-18232} || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + + zaino: + # Regtest uses username/password auth (set in config/regtest/zaino.toml), + # not cookie auth. Override the full environment to drop the cookie path + # inherited from the base. + environment: !override + RUST_LOG: ${Z3_ZAINO_RUST_LOG:-${RUST_LOG:-info}} + 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 + + zallet: + # Regtest uses username/password auth. Override the volumes list to drop + # the cookie volume mount (the distroless image lacks a shell to create + # /var/run dirs at runtime). + 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: + context: ./rpc-router + dockerfile: Dockerfile + restart: unless-stopped + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + environment: + ZEBRA_URL: http://zebra:${Z3_ZEBRA_RPC_PORT:-18232} + ZALLET_URL: http://zallet:28232 + RPC_USER: ${Z3_REGTEST_RPC_ROUTER_USER:-zebra} + RPC_PASSWORD: ${Z3_REGTEST_RPC_ROUTER_PASSWORD:-zebra} + LISTEN_PORT: "8181" + ports: + - "${Z3_REGTEST_RPC_ROUTER_HOST_PORT:-8181}:8181" + depends_on: + zebra: + condition: service_healthy + zallet: + condition: service_started diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1d842ea --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,304 @@ +# Z3: a Zcash node platform. +# +# Run a network with: +# docker compose --env-file .env.mainnet up -d +# docker compose --env-file .env.testnet up -d +# docker compose --env-file .env.regtest up -d +# +# Default invocation (no --env-file) brings up mainnet. +# Operator-specific overrides go in .env (auto-loaded, gitignored). +# +# Public contract identifiers (network, volume, port names) are defined in +# z3-contract.yaml. Image pins below have inline fallbacks and can be +# overridden per service via Z3_*_IMAGE. Downstream services attach via the +# documented network and cookie volume; see docs/integrations/. +# +# Shared defaults applied to every service via <<: *common: +# cap_drop removes all Linux capabilities (services run as non-root) +# security_opt prevents privilege escalation via setuid binaries +# +# Logging is deliberately not set here. Pinning a driver in Compose overrides +# the operator's Docker daemon default (journald, local, a remote collector). +# To bound log growth, set a rotating default in /etc/docker/daemon.json; see +# docs/faq.md ("Why doesn't z3 set a logging driver"). + +name: z3-mainnet + +x-common: &common + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + +services: + zebra: + image: ${Z3_ZEBRA_IMAGE:-zfnd/zebra:5.0.0} + # Zebra is multi-arch; Docker selects the host's native variant. + # DOCKER_PLATFORM forces a specific arch for cross-architecture testing. + platform: ${DOCKER_PLATFORM:-} + restart: unless-stopped + stop_grace_period: 30s + <<: *common + # Zebra's entrypoint runs mkdir/chown before dropping privileges via setpriv. + # Those operations need capabilities that cap_drop: [ALL] removes. + cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID] + # Optional operator overrides (e.g., OpenTelemetry endpoints) load from .env. + # config-rs treats empty strings as values, so empty-defaulted vars cannot + # appear in the explicit environment: block; they must be omitted instead. + env_file: + - path: ./.env + required: false + environment: + RUST_LOG: ${Z3_ZEBRA_RUST_LOG:-${RUST_LOG:-info}} + ZEBRA_NETWORK__NETWORK: ${Z3_NETWORK:-Mainnet} + ZEBRA_RPC__LISTEN_ADDR: 0.0.0.0:${Z3_ZEBRA_RPC_PORT:-8232} + ZEBRA_RPC__ENABLE_COOKIE_AUTH: ${ZEBRA_RPC__ENABLE_COOKIE_AUTH:-true} + ZEBRA_RPC__COOKIE_DIR: /var/run/auth + ZEBRA_TRACING__FILTER: ${ZEBRA_TRACING__FILTER:-info} + ZEBRA_STATE__CACHE_DIR: /home/zebra/.cache/zebra + ZEBRA_HEALTH__LISTEN_ADDR: 0.0.0.0:8080 + ZEBRA_HEALTH__MIN_CONNECTED_PEERS: ${ZEBRA_HEALTH__MIN_CONNECTED_PEERS:-1} + ZEBRA_HEALTH__READY_MAX_BLOCKS_BEHIND: ${ZEBRA_HEALTH__READY_MAX_BLOCKS_BEHIND:-2} + ZEBRA_HEALTH__ENFORCE_ON_TEST_NETWORKS: ${ZEBRA_HEALTH__ENFORCE_ON_TEST_NETWORKS:-false} + ZEBRA_METRICS__ENDPOINT_ADDR: ${ZEBRA_METRICS__ENDPOINT_ADDR:-0.0.0.0:9999} + volumes: + - ${Z3_CHAIN_DATA_PATH:-chain}:/home/zebra/.cache/zebra + - ${Z3_COOKIE_PATH:-cookie}:/var/run/auth + # p2p (8233 mainnet / 18233 testnet) is published so the node accepts + # inbound peer connections; without it Zebra makes only outbound peers. + # Behind NAT or a firewall, also set ZEBRA_NETWORK__EXTERNAL_ADDR to the + # address peers should dial. Regtest is peerless and omits this (see overlay). + ports: + - "${Z3_ZEBRA_HOST_RPC_PORT:-8232}:${Z3_ZEBRA_RPC_PORT:-8232}" + - "${Z3_ZEBRA_HOST_P2P_PORT:-8233}:${Z3_ZEBRA_P2P_PORT:-8233}" + - "${Z3_ZEBRA_HOST_HEALTH_PORT:-8080}:8080" + healthcheck: + test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8080/ready || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 90s + start_interval: 5s + + # Sidecar: makes Zebra's RPC cookie readable inside the shared auth volume. + # Zebra writes /var/run/auth/.cookie with mode 0600 owned by uid 10001; + # Zaino, Zallet, and attached services may run as different users. + cookie-permissions: + image: alpine:3 + restart: unless-stopped + <<: *common + # FOWNER is needed to chmod Zebra's cookie after cap_drop: [ALL]. + cap_add: [FOWNER] + environment: + ZEBRA_RPC__ENABLE_COOKIE_AUTH: ${ZEBRA_RPC__ENABLE_COOKIE_AUTH:-true} + volumes: + - ${Z3_COOKIE_PATH:-cookie}:/var/run/auth + command: + - sh + - -c + - | + while true; do + if [ -f /var/run/auth/.cookie ]; then + chmod 0644 /var/run/auth/.cookie 2>/dev/null + fi + sleep 5 + done + depends_on: + zebra: + condition: service_started + healthcheck: + test: ["CMD-SHELL", "if [ \"$${ZEBRA_RPC__ENABLE_COOKIE_AUTH:-true}\" = \"false\" ]; then exit 0; fi; test -r /var/run/auth/.cookie"] + interval: 5s + timeout: 2s + retries: 12 + start_period: 5s + + zaino: + # The -no-tls tag compiles out Zaino's "TLS required on a non-private bind" + # guard, so intra-container gRPC on 0.0.0.0:8137 runs as plaintext h2c. + # Terminate edge TLS at a reverse proxy if Zaino is exposed beyond the host. + image: ${Z3_ZAINO_IMAGE:-zingodevops/zainod:0.4.0-rc.2-no-tls} + # Zaino publishes linux/amd64 only; arm64 hosts run under emulation unless + # the operator builds from source (see docker-compose.build.yml). + platform: ${DOCKER_PLATFORM:-linux/amd64} + restart: unless-stopped + stop_grace_period: 15s + <<: *common + cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID] + command: ["start", "--config", "/etc/zaino/zindexer.toml"] + depends_on: + zebra: + condition: service_healthy + cookie-permissions: + condition: service_healthy + environment: + RUST_LOG: ${Z3_ZAINO_RUST_LOG:-${RUST_LOG:-info,reqwest=warn,hyper_util=warn}} + RUST_BACKTRACE: ${RUST_BACKTRACE:-full} + ZAINO_NETWORK: ${Z3_NETWORK:-Mainnet} + ZAINO_VALIDATOR_SETTINGS__VALIDATOR_JSONRPC_LISTEN_ADDRESS: zebra:${Z3_ZEBRA_RPC_PORT:-8232} + ZAINO_VALIDATOR_SETTINGS__VALIDATOR_COOKIE_PATH: /var/run/auth/.cookie + ZAINO_GRPC_SETTINGS__LISTEN_ADDRESS: 0.0.0.0:8137 + ZAINO_JSON_SERVER_SETTINGS__JSON_RPC_LISTEN_ADDRESS: 0.0.0.0:8237 + volumes: + - ${Z3_ZAINO_DATA_PATH:-zaino}:/app/data + - ${Z3_COOKIE_PATH:-cookie}:/var/run/auth:ro + - ${Z3_CONFIG_DIR:-./config/mainnet}/zaino.toml:/etc/zaino/zindexer.toml:ro + ports: + - "${Z3_ZAINO_HOST_GRPC_PORT:-8137}:8137" + - "${Z3_ZAINO_HOST_JSON_RPC_PORT:-8237}:8237" + healthcheck: + test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/127.0.0.1/8137' 2>/dev/null || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 60s + start_interval: 5s + + zallet: + image: ${Z3_ZALLET_IMAGE:-electriccoinco/zallet:v0.1.0-alpha.3} + platform: ${DOCKER_PLATFORM:-linux/amd64} + restart: unless-stopped + stop_grace_period: 15s + <<: *common + user: "1000:1000" + command: ["--datadir", "/var/lib/zallet", "--config", "/etc/zallet/zallet.toml", "start"] + depends_on: + zebra: + condition: service_healthy + cookie-permissions: + condition: service_healthy + environment: + RUST_LOG: ${Z3_ZALLET_RUST_LOG:-${RUST_LOG:-info,hyper_util=warn,reqwest=warn}} + volumes: + - ${Z3_ZALLET_DATA_PATH:-zallet}:/var/lib/zallet + - ${Z3_COOKIE_PATH:-cookie}:/var/run/auth:ro + - ${Z3_CONFIG_DIR:-./config/mainnet}/zallet.toml:/etc/zallet/zallet.toml:ro + - ${Z3_CONFIG_DIR:-./config/mainnet}/zallet_identity.txt:/etc/zallet/identity.txt:ro + ports: + - "${Z3_ZALLET_HOST_RPC_PORT:-28232}:28232" + # No healthcheck: the distroless image has no shell or curl. + + # =========================================================================== + # Monitoring stack (--profile monitoring) + # =========================================================================== + # Mainnet defaults: Grafana 3000, Prometheus 9094, Jaeger UI 16686, + # Jaeger OTLP 4317/4318, Jaeger spanmetrics 8889, AlertManager 9093. + # Testnet and regtest env files offset every published monitoring port so + # multiple networks coexist on one host without binding collisions. + + jaeger: + image: jaegertracing/jaeger:2.1.0 + profiles: [monitoring] + restart: unless-stopped + <<: *common + volumes: + - ./observability/jaeger/config.yaml:/etc/jaeger/config.yaml:ro + command: + - --config=/etc/jaeger/config.yaml + ports: + - "${Z3_JAEGER_UI_PORT:-16686}:16686" + - "${Z3_JAEGER_OTLP_GRPC_PORT:-4317}:4317" + - "${Z3_JAEGER_OTLP_HTTP_PORT:-4318}:4318" + - "${Z3_JAEGER_SPANMETRICS_PORT:-8889}:8889" + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:16686/ || exit 1"] + interval: 5s + timeout: 3s + retries: 10 + + prometheus: + image: prom/prometheus:v3.2.0 + profiles: [monitoring] + restart: unless-stopped + <<: *common + volumes: + - prometheus_data:/prometheus + - ./observability/prometheus/rules:/etc/prometheus/rules:ro + configs: + - source: prometheus_config + target: /etc/prometheus/prometheus.yml + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.enable-lifecycle' + - '--web.enable-admin-api' + ports: + - "${Z3_PROMETHEUS_PORT:-9094}:9090" + depends_on: + jaeger: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:9090/-/healthy || exit 1"] + interval: 10s + timeout: 3s + retries: 3 + + grafana: + image: grafana/grafana:11.5.1 + profiles: [monitoring] + restart: unless-stopped + <<: *common + volumes: + - ./observability/grafana/dashboards:/var/lib/grafana/dashboards:ro + - ./observability/grafana/provisioning:/etc/grafana/provisioning:ro + - grafana_data:/var/lib/grafana + environment: + GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH: /var/lib/grafana/dashboards/zebra_overview.json + GF_SECURITY_ADMIN_PASSWORD: ${GF_SECURITY_ADMIN_PASSWORD:-admin} + ports: + - "${Z3_GRAFANA_PORT:-3000}:3000" + depends_on: + prometheus: + condition: service_healthy + jaeger: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/health || exit 1"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 10s + + alertmanager: + image: prom/alertmanager:v0.28.1 + profiles: [monitoring] + restart: unless-stopped + <<: *common + volumes: + - ./observability/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro + - alertmanager_data:/alertmanager + command: + - '--config.file=/etc/alertmanager/alertmanager.yml' + - '--storage.path=/alertmanager' + ports: + - "${Z3_ALERTMANAGER_PORT:-9093}:9093" + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:9093/-/healthy || exit 1"] + interval: 10s + timeout: 3s + retries: 3 + +# Named volumes are declared with explicit name: so the external Docker +# identifier is `${COMPOSE_PROJECT_NAME}-` rather than the default +# `${COMPOSE_PROJECT_NAME}_`. Downstream consumers reference these +# external names directly; see docs/contract.md for the public contract. +volumes: + chain: + name: ${COMPOSE_PROJECT_NAME:-z3-mainnet}-chain + cookie: + name: ${COMPOSE_PROJECT_NAME:-z3-mainnet}-cookie + zaino: + name: ${COMPOSE_PROJECT_NAME:-z3-mainnet}-zaino + zallet: + name: ${COMPOSE_PROJECT_NAME:-z3-mainnet}-zallet + prometheus_data: + grafana_data: + alertmanager_data: + +# The default network is named explicitly so consumers attach via a stable +# identifier (not subject to Compose's project-prefix behavior). +networks: + default: + name: ${COMPOSE_PROJECT_NAME:-z3-mainnet} + +configs: + prometheus_config: + file: ./observability/prometheus/prometheus.yaml diff --git a/docs/contract.md b/docs/contract.md new file mode 100644 index 0000000..913003a --- /dev/null +++ b/docs/contract.md @@ -0,0 +1,182 @@ +# Z3 Platform Contract + +> This is for **integrators and tooling** that attach to a running Z3 stack, and for agents that consume the API. If you only run Z3, you do not need any of this; go to the [README operator guide](../README.md#for-operators). + +This document is the human-facing version of [`z3-contract.yaml`](../z3-contract.yaml). The YAML is the source of truth for integration identifiers and is validated against [`z3-contract.schema.json`](../z3-contract.schema.json). This doc explains what each identifier means, what consumers can rely on, and what is explicitly out of scope. + +For copy-paste integration examples, see [`docs/integrations/`](integrations/). + +## Stability promise + +Identifiers in the contract are SemVer-stable. The shipped contract version is `1.0.0`: + +- **Patch** (1.0.x): documentation-only changes; no consumer impact. +- **Minor** (1.x.0): adds optional fields, new env vars, new optional services. Existing consumers continue working. +- **Major** (x.0.0): renames, removed identifiers, or port changes. Existing consumers need to migrate. + +Identifiers NOT in this contract (Compose service container names, internal config-rs env vars, monitoring volumes, scrape labels) are z3's implementation. They can change without a contract bump. + +### Contract version and image pins + +The contract version (`z3-contract.yaml` → `contract_version`) is the SemVer-stable API surface bumped per the policy above. Image pins are not part of the contract: they live as `${VAR:-tag}` defaults in `docker-compose.yml` and bump independently of the contract version (a Zebra patch release `4.4.1` → `4.4.2` does not require a contract bump). Operators override any pin with `Z3_ZEBRA_IMAGE`, `Z3_ZAINO_IMAGE`, or `Z3_ZALLET_IMAGE`. Platform constraints per image live in `z3-contract.yaml` under `image_platforms:`. + +## What z3 publishes + +### Networks + +Z3 runs as one of three Compose projects, one per Zcash network. The full set of identifiers (project name, external network name, `Z3_NETWORK` value) lives in `z3-contract.yaml` under `networks.`. + +`Z3_NETWORK` is PascalCase (it passes through to Zebra's `serde` deserializer). `COMPOSE_PROJECT_NAME` is lowercase (Docker Compose accepts only lowercase letters, digits, dashes, and underscores). Both are required; the `.env.` files set both. + +### Volumes + +Every per-network resource is a named Docker volume with an explicit `name:` declaration, so it is not subject to Compose's project-prefix behavior. Names follow the pattern `z3--` (e.g., `z3-mainnet-cookie`); the full set is in `z3-contract.yaml` under `networks..volumes`. + +Of these, `cookie` is part of the consumer-facing attachment surface only when the network's `rpc_auth.mode` is `cookie` (mainnet and testnet). Regtest carries the named volume as an internal Compose artifact, but its `rpc_auth.mode` is `username_password`; consumers should not mount `z3-regtest-cookie` expecting a readable RPC cookie. The other volumes are z3's storage and should not be mounted. + +### In-network DNS + +Inside the Docker network, services resolve at their bare service name (`zebra`, `zaino`, `zallet`). The YAML's `service_dns:` block carries the full list. + +No per-network suffix: the network itself is the discriminator. A consumer attached to `z3-testnet` uses `http://zebra:18232`; on `z3-mainnet` it uses `http://zebra:8232`. + +### Cookie file + +The path is `/var/run/auth/.cookie` on every container that uses cookie auth. Mainnet and testnet publish `rpc_auth.mode: cookie`; Z3's Zebra writes the cookie, and Z3's Zaino, Zallet, and any consumer attached to the cookie volume mount it read-only at this path. + +Regtest is the exception: it publishes `rpc_auth.mode: username_password`, sets `ZEBRA_RPC__ENABLE_COOKIE_AUTH=false`, and the regtest overlay removes the cookie mount from Zaino and Zallet. Zebra does not guarantee `/var/run/auth/.cookie` on regtest. Regtest consumers should use the documented username/password path, usually through the rpc-router defaults (`zebra` / `zebra`) or the per-service regtest config. + +### Port matrix + +Container ports follow Zebra's per-network defaults from upstream (`zebra-chain/src/parameters/network.rs:239`, `zebra-rpc/src/config/rpc.rs:17-47`). Host ports are explicit and globally unique across all published services and optional profiles, so all three networks coexist on one host. The full matrix (including the `monitoring` profile) is in `z3-contract.yaml` under `networks..ports`. + +For non-mainnet networks, host ports for services without an upstream per-network convention (Zaino, Zallet, monitoring) use a +10000 offset relative to mainnet, with two exceptions. Regtest Zebra RPC moves to host `29232` because `28232` is already mainnet Zallet's host port. Zebra's metrics port stays in-network only on every network (no host publication). + +Regtest also uses Zebra's testnet container-port defaults. + +### Healthchecks + +Per-service health surface. The YAML's `healthchecks:` block carries the per-service transport, port, and endpoint. + +Operational details worth knowing: + +- **Zaino's check is a TCP probe on the gRPC port.** It confirms a listener exists; it does not validate the gRPC handler. Do not use it for production routing decisions. +- **Zallet has no healthcheck.** Its distroless image has no shell or probe binary, so `depends_on: condition: service_healthy` against Zallet hangs. Gate on Zebra's `/ready` instead and assume Zallet follows. +- **`/ready` is sync-strict; `/healthy` is peer-only.** `/healthy` returns success when Zebra has at least `ZEBRA_HEALTH__MIN_CONNECTED_PEERS` peers; `/ready` additionally requires sync within `ZEBRA_HEALTH__READY_MAX_BLOCKS_BEHIND` of tip. Use `/ready` for production; the tracked `docker-compose.override.yml.example` flips to `/healthy` for development. + +Inside the Docker network, consumers wait on Zebra via `depends_on` with `condition: service_healthy`. Outside the network, consumers poll the published health port (per-network number in `z3-contract.yaml`). + +### Env var schema + +Three namespaces keep stack-level settings separate from service-native settings. + +**`Z3_*` (stack-level settings).** Used for host port mappings, image pins, volume path overrides, the per-service `RUST_LOG` split, multi-network selection (`Z3_NETWORK`, `Z3_CONFIG_DIR`), and scoping wrappers such as `Z3_REGTEST_RPC_ROUTER_USER`. Image-pin defaults are `${VAR:-tag}` fallbacks in `docker-compose.yml`. + +**Service-native names (passed through as-is).** Used when the underlying service has a documented convention. Operators map straight from the service's docs without learning a z3 wrapper. Examples: `GF_SECURITY_ADMIN_PASSWORD` (Grafana), `ZEBRA_*` (Zebra config-rs, including `ZEBRA_HEALTH__*`, `ZEBRA_RPC__ENABLE_COOKIE_AUTH`, `ZEBRA_NETWORK__EXTERNAL_ADDR`, `ZEBRA_TRACING__*`, `ZEBRA_MINING__MINER_ADDRESS`). + +**Ecosystem-standard names (inherited; not part of the z3 contract).** Documented for completeness because operators encounter them in our docs: `COMPOSE_FILE`, `RUST_LOG`, `RUST_BACKTRACE`. The YAML lists them under `ecosystem_vars:`. + +**Internal vars (not in the contract).** Z3 sets these inside the compose `environment:` block; operators set the public knobs above instead. Examples: `ZEBRA_RPC__LISTEN_ADDR`, `ZAINO_VALIDATOR_SETTINGS__*`. Also hardcoded in the compose (no operator override): Zaino and Zallet container ports, which do not differ per network. + +For the canonical machine-readable inventory (every variable, its namespace tag, and profile gating), see [`z3-contract.yaml`](../z3-contract.yaml) under `env_vars:` and `ecosystem_vars:`. + +### Profiles + +One Compose profile is available across every network: `monitoring` (Prometheus, Grafana, Jaeger, AlertManager). Enable with `docker compose --env-file .env. --profile monitoring up -d`. Profile-gated identifiers in the YAML carry an explicit `profile:` field. + +## What z3 does NOT publish + +These are explicitly OUT of the contract; they may change without a major version bump: + +- **Container names** (e.g., `z3-mainnet-zebra-1`). Compose autogenerates them; consumers should reference services by their in-network DNS name, not the container name. +- **Per-network config file contents** (the live `config//zallet.toml`, `config//zaino.toml`). These are local files (see "File ownership" below); their contents are not part of the consumer-facing contract. +- **Internal env vars** (`ZEBRA_RPC__LISTEN_ADDR`, `ZAINO_VALIDATOR_SETTINGS__*`, etc.). Z3 sets them inside the compose `environment:` block; operators set the public knobs documented above instead. +- **Monitoring identifiers** (Prometheus job labels, Grafana datasource UIDs, dashboard UIDs). Internal. +- **Compose configs** (`prometheus_config`). Internal. + +## File ownership + +Files in this repository fall into two categories. The separation lets operators iterate locally without merge conflicts and lets maintainers ship template updates without disturbing operator copies. + +### Maintainer-owned (tracked in git) + +These files are tracked defaults. Operators should not edit them; doing so creates merge conflicts on repository updates. + +| Path | Purpose | +|------|---------| +| `docker-compose.yml`, `docker-compose.regtest.yml`, `docker-compose.build.yml` | Stack topology and the opt-in source-build overlay. Override via `docker-compose.override.yml` (see below). | +| `z3-contract.yaml`, `z3-contract.schema.json`, `docs/contract.md` | The contract, its schema, and this guide. | +| `.env.example` | Reference for every public env var. | +| `.env.mainnet`, `.env.testnet`, `.env.regtest` | Per-network defaults. Override via `.env`. | +| `config//zallet.toml.example` | Zallet config template. | +| `config//zaino.toml.example` | Zaino config template. | + +### Local (gitignored) + +These files are created locally by `scripts/setup-network.sh ` and edited freely. Repository updates never overwrite them. + +| Path | Created by | Purpose | +|------|------------|---------| +| `.env` | Local user | Per-host overrides; see "How `.env` is loaded" below. | +| `docker-compose.override.yml` | Local user (template in `.example`) | Per-host overrides for compose service definitions. Auto-loaded for mainnet. | +| `docker-compose..override.yml` | Local user (optional) | Per-host testnet/regtest overrides. **Not auto-loaded;** pass with `-f` or append to `COMPOSE_FILE` explicitly. | +| `config//zallet.toml` | `setup-network.sh` (cp from `.example`) | Active Zallet config. Edit to taste. | +| `config//zaino.toml` | `setup-network.sh` (cp from `.example`) | Active Zaino config. Edit to taste. | +| `config//zallet_identity.txt` | `setup-network.sh` (`rage-keygen`) | Per-operator wallet encryption key. Back this up. | + +After `git pull`, diff your live `.toml` against the refreshed `.example`; apply any desired changes by hand: + +```bash +diff config/mainnet/zallet.toml config/mainnet/zallet.toml.example +``` + +### How `.env` is loaded + +Docker Compose auto-loads `.env` from the working directory ONLY when no `--env-file` flag is given. The documented invocation pattern (`docker compose --env-file .env. up -d`) replaces that auto-load: variables in `.env.` are used for compose-level interpolation, and `.env` is not consulted. + +To make values in `.env` take effect under the `--env-file` pattern, use one of: + +```bash +# Option 1: Pass .env as a second --env-file (later wins for collisions) +docker compose --env-file .env.mainnet --env-file .env up -d + +# Option 2: Export in the shell before running compose +export Z3_JAEGER_OTLP_GRPC_PORT=14317 +docker compose --env-file .env.mainnet up -d + +# Option 3: Use a wrapper alias / script that always passes both files +``` + +Variables consumed through a service-level `env_file:` directive are still loaded from `.env` regardless of `--env-file`. Zebra uses this for optional config-rs settings. These variables reach the container's environment but do not influence compose's `${VAR}` interpolation. + +## Versioning + +`contract_version` in `z3-contract.yaml` is the SemVer for the API surface, decoupled from the z3 git tag. A z3 release may keep the same contract version or bump it for contract changes such as renamed identifiers, removed fields, or new ports. Image pins are not versioned: they're `${VAR:-tag}` defaults in `docker-compose.yml` that bump per upstream release. + +When the contract bumps, z3 publishes: + +- A `CHANGELOG.md` entry naming what changed. +- Release notes describing any required operator and consumer updates. +- A new SemVer tag on the z3 repository. + +Consumers should pin to a contract major version and only adopt minor or patch bumps automatically. + +## Validation + +The repository CI validates this contract on every PR. The job parses `z3-contract.yaml`, runs `docker compose --env-file .env. config` for each network, and asserts that every documented identifier (network name, volume name, port, env var) exists in the resolved compose output. A drift between this doc and the compose file is a CI failure. + +Operators can run the same validation locally: + +```bash +./scripts/validate-contract.py # port matrix and volume names per network +./scripts/validate-contract-parity.py # env var inventory across compose and .env.example +``` + +Consumers in any language can validate `z3-contract.yaml` against the shipped JSON Schema: + +```bash +# Example using `check-jsonschema` (any JSON Schema validator works). +pip install check-jsonschema pyyaml +python -c 'import yaml,json,sys; json.dump(yaml.safe_load(open("z3-contract.yaml")), sys.stdout)' \ + | check-jsonschema --schemafile z3-contract.schema.json /dev/stdin +``` diff --git a/docs/docker-architecture.md b/docs/docker-architecture.md new file mode 100644 index 0000000..d99073f --- /dev/null +++ b/docs/docker-architecture.md @@ -0,0 +1,295 @@ +# Docker Compose Architecture + +This document explains the Docker Compose patterns and runtime behavior used in the z3 stack. It is a reference for contributors and operators who need to understand how the stack is structured. + +For the public contract (network names, volume names, port matrix), see [`contract.md`](contract.md). + +## Overview + +```text +docker-compose.yml Base stack (Zebra + Zaino + Zallet + optional profiles) +docker-compose.regtest.yml Regtest overlay (structural differences only) +docker-compose.build.yml Opt-in source-build overlay (scripts/vendor.sh) +.env.mainnet Mainnet selection + canonical ports +.env.testnet Testnet selection + offset ports +.env.regtest Regtest selection + overlay loader +.env.example Reference for every public override +.env Operator-specific overrides (gitignored, optional) +config/mainnet/ Mainnet Zallet + Zaino configs + identity file +config/testnet/ Testnet equivalents +config/regtest/ Regtest equivalents +scripts/ Operational scripts (regtest-init, fix-permissions, etc.) +``` + +The core principle: **`docker-compose.yml` is self-sufficient for mainnet**. Every variable uses `${VAR:-default}` syntax, so `docker compose up` works on a fresh clone with zero configuration files. The per-network env files exist to switch networks; `.env` is purely optional operator overrides. + +## Defaults-in-compose pattern + +Every variable reference in `docker-compose.yml` includes a default value: + +```yaml +image: ${Z3_ZEBRA_IMAGE:-zfnd/zebra:5.0.0} +environment: + ZEBRA_NETWORK__NETWORK: ${Z3_NETWORK:-Mainnet} +volumes: + - ${Z3_CHAIN_DATA_PATH:-chain}:/home/zebra/.cache/zebra +ports: + - "${Z3_ZEBRA_HOST_RPC_PORT:-8232}:${Z3_ZEBRA_RPC_PORT:-8232}" +``` + +Docker Compose resolves `${VAR:-default}` as: use `VAR` if set and non-empty, otherwise use `default`. Values come from (highest precedence first): + +1. Shell environment variables (`Z3_ZEBRA_IMAGE=custom docker compose up`) +2. `--env-file ` arguments +3. `.env` file in the project root (auto-loaded) +4. The `:-default` fallback in the compose file + +The mainnet env file (`.env.mainnet`) sets only `COMPOSE_PROJECT_NAME`, `Z3_NETWORK`, and `Z3_CONFIG_DIR` because the compose defaults already match mainnet. Testnet and regtest env files set additional overrides (container ports, host ports, network selection) to switch the stack. + +## Multi-environment support + +### Per-network Compose projects + +Z3 runs as one of three Compose projects: `z3-mainnet`, `z3-testnet`, `z3-regtest`. Each is a separate logical instance with its own resources. Project name comes from `COMPOSE_PROJECT_NAME` in the env file. + +| Network | Project | Selected via | +|---------|---------|--------------| +| Mainnet | `z3-mainnet` | `docker compose --env-file .env.mainnet up` | +| Testnet | `z3-testnet` | `docker compose --env-file .env.testnet up` | +| Regtest | `z3-regtest` | `docker compose --env-file .env.regtest up` | + +Compose's native project boundary handles isolation: each project has its own containers, network, and volumes. Mainnet, testnet, and regtest can run concurrently on one host without collisions because their published host ports differ. Testnet keeps Zebra's canonical host ports; regtest uses explicit host ports where a simple offset would collide with another service. + +### Explicit `name:` declarations + +Compose's default behavior prefixes every volume and network with `${COMPOSE_PROJECT_NAME}_`. For internal services this is fine; for the public contract identifiers that consumers attach to, it is brittle: a project rename silently breaks every consumer. + +Z3 declares external-facing resources with explicit `name:`: + +```yaml +volumes: + chain: + name: ${COMPOSE_PROJECT_NAME:-z3-mainnet}-chain + cookie: + name: ${COMPOSE_PROJECT_NAME:-z3-mainnet}-cookie + +networks: + default: + name: ${COMPOSE_PROJECT_NAME:-z3-mainnet} +``` + +The `name:` field is documented as "used as-is and not scoped with the project name" (Docker Compose reference). This makes `z3-testnet-cookie` and `z3-testnet` the stable external identifiers consumers reference via `external: true, name: ...`. Renaming the Compose project would not affect them. + +Volumes and networks not part of the contract (Prometheus data, Compose configs) keep the default prefix; they are internal. + +### `COMPOSE_FILE` for overlay loading + +`.env.regtest` includes `COMPOSE_FILE` so the regtest overlay is loaded with +the base compose: + +```env +COMPOSE_FILE=docker-compose.yml:docker-compose.regtest.yml +``` + +When run with `--env-file .env.regtest`, Compose loads both files and merges them. The colon-separated list is processed left to right; later files override earlier ones. The regtest overlay contains the peerless healthcheck, username/password auth on Zaino, and the optional rpc-router. Testnet needs no overlay: `.env.testnet` sets `COMPOSE_FILE=docker-compose.yml` and selects the network through `Z3_NETWORK=Testnet`. + +### Compose file merge rules + +When an overlay defines the same service as the base, attributes merge as follows: + +| Attribute type | Merge behavior | +|----------------|---------------| +| Scalars (`image`, `command`) | Override replaces | +| `environment` | Merge by key name; override wins on conflict | +| `volumes` (service-level) | Merge by mount target path; same target = override wins | +| `ports` | Append | +| `healthcheck` | Override replaces entirely | +| `build.args` | Merge by key name; override wins on conflict | + +### `!override` YAML tag + +Standard YAML merge can add and overwrite keys but cannot *remove* them. Docker Compose v2.24.4+ supports the `!override` tag used here: + +```yaml +services: + zaino: + environment: !override + RUST_LOG: info + ZAINO_NETWORK: Regtest + # only regtest-relevant vars; cookie path from base is dropped +``` + +`!override` fully replaces the attribute instead of merging. The regtest overlay uses this on Zaino's `environment` to switch from cookie-based authentication (base compose) to username/password authentication (credentials in `config/regtest/zaino.toml`). + +A related tag, `!reset`, clears an attribute to its default value. This stack requires Docker Compose v2.24.4 or later for these tags. + +## Per-network configuration + +### Zallet config files + +Zallet's `[indexer]` block hardcodes the validator address it connects to (Zebra's JSON-RPC). Because the JSON-RPC port differs per network (Mainnet 8232, Testnet 18232, Regtest 18232), Z3 ships one Zallet config per network: + +``` +config/mainnet/zallet.toml validator_address = "zebra:8232" +config/testnet/zallet.toml validator_address = "zebra:18232" +config/regtest/zallet.toml validator_address = "zebra:18232" (username/password auth) +``` + +The compose mount path is templated: + +```yaml +volumes: + - ${Z3_CONFIG_DIR:-./config/mainnet}/zallet.toml:/etc/zallet/zallet.toml:ro +``` + +`Z3_CONFIG_DIR` is set per env file (`./config/mainnet`, `./config/testnet`, `./config/regtest`). To change Zallet behavior, edit the per-network file directly. + +### Zaino config files + +Zaino's config is empty for mainnet and testnet (all settings come from env vars); regtest needs a non-empty file because it uses username/password auth that cannot be set via env vars (Zaino blocks env vars containing "password" for security). The compose mounts `${Z3_CONFIG_DIR}/zaino.toml` for symmetry with Zallet. + +### Zallet identity files + +Each network has its own age-encryption identity at `config//zallet_identity.txt`. The file is gitignored. Identities are generated by `scripts/setup-network.sh ` (which `scripts/regtest-init.sh` delegates to for regtest). See the README Quick start. + +## Extension fields and YAML anchors + +### `x-common`: shared service configuration + +```yaml +x-common: &common + cap_drop: [ALL] + security_opt: [no-new-privileges:true] +``` + +Services reference this with `<<: *common` for consistent security hardening without repeating the configuration. Logging is intentionally absent (see "Log rotation" below). + +Top-level keys starting with `x-` are *extension fields*; Compose ignores them during processing but they serve as anchor sources for YAML reuse. + +### Zebra's `setpriv` entrypoint + +Zebra's Docker entrypoint starts as root, runs `mkdir` and `chown` to set up mounted volume directories, then uses `setpriv` (part of `util-linux`, included in Debian trixie) to drop to a non-root user. These pre-privilege-drop operations need capabilities that `cap_drop: [ALL]` removes, so Zebra adds back only the five it needs: + +```yaml +cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID] +``` + +Zaino and Zallet run as non-root from the start and work with `cap_drop: [ALL]` alone. + +## Healthchecks + +### `start_interval`: two-speed healthchecks + +Docker Engine 25.0+ supports `start_interval`, which checks more frequently during startup and then backs off: + +```yaml +healthcheck: + test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8080/ready || exit 1"] + interval: 30s # steady-state: every 30s + start_interval: 5s # during startup: every 5s + start_period: 90s # grace period before failures count +``` + +During the `start_period`, the check runs every `start_interval` (5s). After the first success or after `start_period` expires, it switches to `interval` (30s). A service that becomes ready in 10 seconds is detected in ~15 seconds instead of waiting up to 120 seconds. + +### Zaino: port check instead of process check + +Zaino's image (`debian:bookworm-slim`) does not include `curl` or `netcat`. The healthcheck uses bash's built-in TCP socket capability: + +```yaml +test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/127.0.0.1/8137' 2>/dev/null || exit 1"] +``` + +This verifies the gRPC port is actually accepting connections. The previous check (`zainod --version`) only confirmed the binary existed on disk; it would pass even if the service had crashed after startup. + +### Regtest Zebra: RPC check instead of `/ready` + +The base compose uses Zebra's `/ready` endpoint, which verifies the node is synced near the network tip. In regtest mode there are no peers and no network tip to sync to, so `/ready` would never succeed. The regtest overlay replaces this with a direct RPC call (`getblockchaininfo`) that confirms the RPC server is responding. + +### Development override + +`docker-compose.override.yml.example` provides a ready-made override that switches Zebra's healthcheck from `/ready` to `/healthy`, allowing dependent services to start during sync. Copy it to `docker-compose.override.yml` (gitignored) for local development. + +## Security hardening + +### `cap_drop: [ALL]` + +Linux containers receive ~14 capabilities by default (including `CHOWN`, `DAC_OVERRIDE`, `NET_RAW`). Most applications don't need any of them. `cap_drop: [ALL]` removes all capabilities, reducing the attack surface if a container is compromised. + +### `security_opt: [no-new-privileges:true]` + +Prevents processes inside the container from gaining additional privileges through setuid binaries or capability inheritance. Even if an attacker writes a setuid binary into a writable tmpfs, it won't escalate privileges. + +### Log rotation + +z3 does not pin a `logging:` driver on any service. A Compose `logging:` block overrides the driver the operator configured on the Docker daemon (journald, local, a remote collector), so forcing `json-file` would silently undo that choice. Bounding log growth is the daemon's job: set a rotating default once in `/etc/docker/daemon.json`, which applies to every container on the host. + +```json +{ + "log-driver": "local", + "log-opts": { "max-size": "50m", "max-file": "5" } +} +``` + +The `local` driver rotates by default and is more efficient than `json-file`. Operators who want per-service control add a `logging:` block in their override file instead. + +## Image override variables + +All service images are overridable. Compose references each image as `${Z3__IMAGE:-}`; the default tags are inline in `docker-compose.yml` itself and bump per upstream release. This allows operators to: + +- Pin to a specific version or digest for reproducibility. +- Test a pre-release candidate without editing the compose file. +- Use a private registry mirror in air-gapped environments. +- Run CI with custom-built images via shell variables. + +Tags are pinned, never floating (`:latest`). On a consensus-critical node platform a silent major bump on the next `pull` or recreate could fork the operator off the network, so upgrades are deliberate: bump the inline default in a reviewed change, set `Z3__IMAGE` to move a single service, or let Renovate (`renovate.json`) raise an auditable bump PR. Dependabot stays scoped to GitHub Actions because it cannot parse the `${VAR:-tag}` default form. + +The `Z3_*_IMAGE` prefix marks these as part of the public contract; `z3-contract.yaml` lists the env-var schema in full. + +## Environment variable strategy + +### Explicit mapping via `environment:` + +All services declare their environment variables explicitly in the `environment:` block. This prevents unintended variables from crossing service boundaries. Zallet does not support environment variable configuration at all, so it only receives `RUST_LOG`. + +### Zebra's `env_file` exception + +Zebra is the only service that also uses `env_file: [{path: ./.env, required: false}]`. Zebra uses config-rs, which auto-reads any `ZEBRA_*` environment variable. Optional config-rs variables like `ZEBRA_TRACING__OPENTELEMETRY_*` cannot be listed in the explicit `environment:` block with empty defaults, because config-rs treats empty strings as values and crashes when parsing `""` as a socket address. `ZEBRA_METRICS__ENDPOINT_ADDR` is the exception: z3 sets a non-empty default so the monitoring profile always has a Zebra scrape target. + +The `env_file` passthrough allows these optional variables to reach Zebra only when the operator explicitly sets them in `.env`. When `.env` does not exist, Zebra receives only the explicit `environment:` variables and uses its built-in defaults. + +Non-`ZEBRA_*` variables from `env_file` are ignored because config-rs reads only variables that match its configured prefix. + +## Cookie-permissions sidecar + +Zebra writes the RPC cookie at `/var/run/auth/.cookie` with mode `0600` owned by uid 10001. Z3's consumer-attachment surface includes the cookie volume, so any service or downstream container that mounts it needs to read the cookie. + +The base compose includes a small `cookie-permissions` sidecar (`alpine:3` with `cap_add: [FOWNER]`) that polls every 5 seconds and chmods the cookie to `0644` once it appears. The cookie volume is already the consumer attachment surface, so loosening the file mode within that volume does not change the security boundary; anyone with access to mount the volume already has access to the cookie. + +Zaino and Zallet depend on the sidecar's healthcheck, so targeted starts such as `docker compose up -d zaino` also start the sidecar and wait until the cookie is readable. On mainnet and testnet the healthcheck waits for `/var/run/auth/.cookie`; on regtest it exits successfully because cookie auth is disabled and username/password auth is used instead. + +## Zallet config readability and operator uid + +Zallet uses a distroless image with no shell, entrypoint script, or Linux capabilities, so the base compose pins it with `user: "1000:1000"` and it runs as that uid from PID 1 — unlike Zebra (root entrypoint that drops to uid 10001) and Zaino (root entrypoint with `cap_add: [DAC_OVERRIDE, ...]`, which bypasses file-permission checks before dropping privileges). Zallet therefore can only read its two bind-mounted host config files — `config//zallet.toml` → `/etc/zallet/zallet.toml` and `config//zallet_identity.txt` → `/etc/zallet/identity.txt` — if they are readable by uid 1000. + +The same "no UID coordination required" property that the cookie sidecar provides applies here: the setup scripts make this config readable by uid 1000 regardless of the operator's host uid. `scripts/setup-network.sh` writes `zallet.toml` (and the other non-secret TOMLs) mode `0644`; `scripts/regtest-init.sh` restores `0644` after its `mktemp`+`mv` pwhash rewrite (which would otherwise leave the file `0600`). The age key `zallet_identity.txt` is a long-lived wallet secret, so instead of widening it to all local users it stays mode `0600` with a POSIX ACL granting read to uid 1000 only (`setfacl -m u:1000:r`). `setfacl` (the `acl` package on Linux, with an ACL-capable filesystem such as ext4/xfs) is a soft prerequisite; if it is unavailable the script warns and the operator can fall back to `chmod 644` on the identity file. + +## Regtest overlay constraints + +### Zaino authentication + +The base compose configures Zaino with cookie-based authentication (shared cookie volume with Zebra). Regtest disables cookie auth (`ZEBRA_RPC__ENABLE_COOKIE_AUTH=false`), so the regtest overlay uses `environment: !override` on Zaino to replace the full environment block, removing the cookie path and other base vars. + +Regtest instead uses username/password authentication configured in `config/regtest/zaino.toml`. These credentials cannot be set via environment variables because Zaino blocks sensitive keys (containing "password") in env vars for security. + +### Config file vs environment variable conflicts + +Zaino's config-rs merges values from both TOML config files and environment variables. If the same field is set in both places, config-rs panics with a "duplicate field" error. The regtest Zaino config must contain only settings that are not set via environment variables. Currently it contains only `backend` and the auth credentials. + +### `docker compose run` and the `--config` flag + +When using `docker compose run` to execute one-off commands (for example, wallet initialization), the arguments replace the service's `command` from the compose file. The `--config /etc/zallet/zallet.toml` flag from the base service definition is not inherited. The init script must pass `--config` explicitly in every `compose run` invocation. + +## `stop_grace_period` + +When Docker sends `SIGTERM` to stop a container, it waits 10 seconds by default before sending `SIGKILL`. Blockchain nodes may need more time to flush state to disk. Zebra gets 30 seconds; other services get 15 seconds. This prevents potential state corruption during planned shutdowns. diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..5986cbc --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,195 @@ +# Z3 FAQ + +Answers to behaviors that look like bugs but are working as designed, plus a few real footguns. This FAQ answers "why is the thing I'm running behaving this way?". For how the stack is built internally, see [docker-architecture.md](docker-architecture.md). + +Questions are grouped by what you are doing: + +- **[For operators](#for-operators)**: running the stack as infrastructure. +- **[For developers and testers](#for-developers-and-testers)**: building or testing against Z3, often across several networks at once. + +When an entry says "see README", the fix lives there; this FAQ only adds the diagnostic context. + +--- + +## For operators + +### Q: Why is my Zebra container marked `unhealthy` right after start? + +That's the `/ready` probe doing its job, not a fault. `/ready` requires the node to have at least `ZEBRA_HEALTH__MIN_CONNECTED_PEERS` (default `1`) and to be within `ZEBRA_HEALTH__READY_MAX_BLOCKS_BEHIND` (default `2`) of the network tip. A fresh start from a cold chain, or a restart on a cache that's a few minutes behind, will report `unhealthy` until both thresholds are met. + +For development, `/healthy` is a looser signal that only checks peer connectivity. The tracked `docker-compose.override.yml.example` flips the healthcheck to `/healthy` so Zaino and Zallet can start without waiting for full sync. Use it for dev, never for production where you want consumers to wait for a synced node. + +Don't run `docker compose up -d` (it starts Zaino and Zallet) until Zebra reports `/ready`. The poller `scripts/check-zebra-readiness.sh` waits for exactly that. README Quick start step 3 explains the two-phase boot. + +--- + +### Q: Where does z3 store chain state, and what do I actually back up? + +Chain state for each network is a Docker named volume, `z3--chain`, under Docker's data root (`/var/lib/docker/volumes/` on Linux). Mainnet is roughly 300 GB. Find the exact path with: + +```bash +docker volume inspect z3-mainnet-chain -f '{{.Mountpoint}}' +``` + +To keep it off the OS 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. + +For backups, the only thing worth keeping is the wallet, and it takes two pieces **together**: the `z3--zallet` volume **and** the matching `config//zallet_identity.txt`. The wallet database in the volume is age-encrypted with that identity file, which lives outside the volume; restoring the volume without the identity leaves you with ciphertext you cannot open. Chain state is re-syncable and the RPC cookie is regenerated on boot, so neither belongs in a backup. + +`docker compose --env-file .env. down` keeps all volumes; `down -v` deletes them and forces a full re-sync. + +--- + +### Q: How do I set up the Zallet wallet on mainnet or testnet? + +`scripts/regtest-init.sh` initializes the regtest wallet automatically. On mainnet and testnet, the node and indexer run fine without a wallet; initialize Zallet's wallet encryption yourself, once per network, when you are ready to manage keys: + +```bash +docker compose --env-file .env. run --rm --no-deps zallet \ + --datadir /var/lib/zallet --config /etc/zallet/zallet.toml init-wallet-encryption +``` + +`--no-deps` skips starting Zebra (encryption setup does not need it). Each network keeps its Zallet data in a separate volume, so run this once per network you use. The wallet is encrypted with `config//zallet_identity.txt`, generated by `setup-network.sh`; back it up with the volume (see the data question above), because the wallet cannot be opened without it. Afterwards, use the Zallet RPC or `generate-mnemonic` to create or import keys. + +--- + +### Q: Why doesn't z3 set a logging driver, and how do I cap log size? + +By design. Pinning `logging.driver` in the compose file would override whatever logging driver you set on the Docker daemon (journald, local, awslogs, a remote collector). z3 leaves logging unset so your daemon default wins. + +To bound disk growth on a 24/7 node, set a rotating default once in `/etc/docker/daemon.json` and restart the daemon: + +```json +{ + "log-driver": "local", + "log-opts": { "max-size": "50m", "max-file": "5" } +} +``` + +The `local` driver rotates by default and is more efficient than `json-file`. This applies to every container on the host, not just z3. If you want per-service control, add a `logging:` block in your operator-local override file. + +--- + +### Q: Why is my container free to use all of my host's CPU and RAM? + +Because `docker-compose.yml` doesn't set `deploy.resources.limits` on any service. The choice is deliberate for a node platform: a constrained limit that makes sense on a 4-core laptop will silently throttle a 32-core production host, and the bound that's right for one operator is wrong for the next. + +If you want to bound a single noisy service (a bulk indexer, a one-off backfill) without hand-tuning everything else, add the limit in your operator-local override file rather than the tracked compose: + +```yaml +# docker-compose.override.yml (mainnet) or docker-compose.testnet.override.yml (testnet) +services: + zebra: + deploy: + resources: + limits: + cpus: "8" + memory: 8g +``` + +Keep the limit generous on the service you want to win contention (Zebra), tight on the service you want to lose it (the noisy consumer). Don't add limits to services you haven't actually seen misbehave, since under-sized limits cause more outages than they prevent. + +--- + +## For developers and testers + +### Q: How do per-network compose overrides work? + +Overrides are opt-in. A fresh clone boots with no override file, so nothing breaks before you run any setup. + +- **Mainnet:** Compose natively auto-loads `docker-compose.override.yml` when it is present and you pass no `-f` or `COMPOSE_FILE`. An absent file is not an error. +- **Testnet and regtest:** `.env.` sets `COMPOSE_FILE=docker-compose.yml:docker-compose..yml`, with no override entry, so the stack renders on a fresh clone. To add per-host customizations (pinning Zebra to `linux/arm64`, adding `deploy.resources.limits`), create `docker-compose..override.yml` and load it explicitly, either by passing `-f docker-compose.yml -f docker-compose..yml -f docker-compose..override.yml`, or by appending the override to `COMPOSE_FILE` in your operator-local `.env`. + +The compose merge order is left-to-right, so the override comes last and wins. The live override file is gitignored, so `git pull` never touches it. + +--- + +### Q: Why doesn't `DOCKER_PLATFORM` in my `.env` take effect? + +Because `docker compose --env-file .env.` *replaces* the auto-loaded `.env` for variable interpolation rather than layering on top of it. When you use `--env-file`, anything set only in `.env` is ignored for `${DOCKER_PLATFORM:-linux/amd64}` substitution. This is documented inside `.env` itself and in [docs/contract.md → How env is loaded](contract.md), but it bites everyone at least once. + +Two reliable workarounds: + +```bash +# (a) export before the call (shell env beats env-file in compose precedence) +export DOCKER_PLATFORM=linux/arm64 +docker compose --env-file .env.mainnet up -d + +# (b) pass .env as a second --env-file (loaded after the network one, so its values win) +docker compose --env-file .env.mainnet --env-file .env up -d +``` + +The mainnet stack also has a third path: `docker-compose.override.yml` (gitignored, auto-loaded for mainnet only) can hold an explicit `services.zebra.platform: linux/arm64`. That bypasses env-var interpolation entirely. For testnet and regtest the override file isn't auto-loaded; see the overrides question above. + +--- + +### Q: Why is my Zebra container running under emulation on Apple Silicon? + +The default `Z3_ZEBRA_IMAGE` is multi-arch and resolves to native arm64 on Apple Silicon. If `uname -m` inside the Zebra container reports `x86_64` instead of `aarch64`, you've either pinned `Z3_ZEBRA_IMAGE` to an amd64-only tag or exported `DOCKER_PLATFORM=linux/amd64`. Emulation pushes Halo2/Groth16 verification past its internal deadline and surfaces as `Transaction(InternalDowncastError("...Elapsed(())"))` followed by chain-tip sync stalls. + +Confirm what's actually running: + +```bash +docker exec z3-mainnet-zebra-1 uname -m # aarch64 = native, x86_64 = emulated +docker top z3-mainnet-zebra-1 -o cmd | head -3 # /usr/bin/qemu-x86_64 wrapper = emulated +``` + +Zaino and Zallet are pinned to amd64 by default (their upstream images publish amd64 only) and run under emulation; the workload is light enough that the CPU drain is barely noticeable next to Zebra's verifier. + +--- + +### Q: Can I run Zaino or Zallet natively on Apple Silicon? + +Not from the pinned tags. The default Zaino and Zallet images publish `linux/amd64` only (declared in [`z3-contract.yaml`](../z3-contract.yaml) under `image_platforms:`). Confirm with `docker buildx imagetools inspect `. The `unknown/unknown` entries in that output are OCI attestation manifests (SBOM/provenance), not real platform variants. + +Two ways forward if you need native arm64: + +1. **Build locally.** Fetch the upstream sources with `scripts/vendor.sh zaino zallet`, then build with the opt-in overlay: `DOCKER_PLATFORM=linux/arm64 docker compose -f docker-compose.yml -f docker-compose.build.yml build zaino zallet`. +2. **Wait for the upstream tag to gain a multi-arch publish, then bump the pin.** Existing tags never gain new platform variants after the fact; only new tags do. + +Leaving these two services under emulation is fine in practice; the workload is light compared to Zebra's verifier, which runs natively. + +Zaino's canonical upstream is [zingolabs/zaino](https://github.com/zingolabs/zaino), published to Docker Hub as `zingodevops/zainod` (matching the daemon binary name). `zingodevops/zaino` is an alias publishing identical digests. + +--- + +### Q: Why does `docker image inspect` report a different architecture than what's running? + +Because `docker image inspect ` returns metadata for whichever variant your local store currently has cached under that tag, not the variant the running container was launched from. On an arm64 host the local cache often holds the arm64 metadata for a multi-arch tag even when a `platform: linux/amd64`-pinned container is actively running the amd64 variant out of the same manifest list. The tag → arch mapping is not the container → arch mapping. + +For the actual running architecture, use runtime signals: + +```bash +docker exec uname -m # reports x86_64 or aarch64 +docker top -o cmd | head -3 # qemu-x86_64 wrapper = emulated +docker exec sh -c 'od -An -tx1 -N20 /path/to/binary' # ELF e_machine at offset 0x12 +``` + +The ELF `e_machine` field at offset `0x12` is `0x3e` for x86_64 and `0xb7` for aarch64. That's the definitive answer when uname or `/proc` aren't available. + +--- + +### Q: Why does my Zebra slow down when I add a heavy local RPC consumer? + +Because Zebra serves block fetches and indexer streaming from the same Tokio worker pool that drives consensus block verification. There is no built-in per-client RPC rate limit and no priority queue between request handlers and the verifier. A bulk indexer in catch-up mode (Zinder, lightwalletd, a fresh Zaino) with high `fetch_concurrency` can sustain enough RPC pressure to slow tip sync, and in extreme cases push transaction verification past its internal deadline. + +The CPU saturation is usually the consumer's choice, not Zebra's: turn the consumer's parallelism down before you reach for compose-level limits. For an indexer that's behind, a `fetch_concurrency` in the low single digits while it catches up is much friendlier than the defaults most clients ship with. + +If you can't control the consumer, the next lever is compose-level CPU limits (the resource-limits question under [For operators](#q-why-is-my-container-free-to-use-all-of-my-hosts-cpu-and-ram)) so the consumer can never starve the verifier. + +--- + +### Q: Why does regtest use username/password instead of cookie auth? + +Because regtest is meant to look like classic local dev, where username/password auth is the convention every existing tutorial and client library assumes. The regtest overlay (`docker-compose.regtest.yml`) disables Zebra's cookie auth and adds an `rpc-router` sidecar that authenticates with `zebra` / `zebra` against Zebra, so the same RPC client works without juggling cookie files. + +Cookie auth stays the default for mainnet and testnet, where Zaino and Zallet read the shared cookie volume directly. See [docs/regtest.md](regtest.md) for the full regtest workflow and the curl/grpcurl examples that use the regtest credentials. + +--- + +## Where to file something that isn't here + +If you hit a behavior that looks wrong and the FAQ doesn't cover it, the fastest path to triage is: + +1. **Check the container's actual runtime state** (the `uname -m` / `docker top` / ELF-header trio in the architecture-detection question above). Most "why is this slow?" reports trace back to a platform or resource-limit assumption that wasn't true. +2. **Look at the compose-resolved config**, not the source YAML: `docker compose --env-file .env. config ` shows you the variables after interpolation, which is what Docker actually receives. +3. **Open an issue** with the resolved config, the runtime signals, and the symptom. Without the resolved config, every triage round restarts from "is your env var actually set." diff --git a/docs/integrations/README.md b/docs/integrations/README.md new file mode 100644 index 0000000..6cef48a --- /dev/null +++ b/docs/integrations/README.md @@ -0,0 +1,35 @@ +# Z3 integration examples + +How downstream services attach to a running Z3 stack. Pick the example that matches how your service deploys. + +| Integration type | When to use it | Example | +|-----------|----------------|---------| +| Compose-peer | Your service runs as a Docker container in the same logical stack as Z3 | [compose-peer.md](compose-peer.md) | +| Host-side pointer | Your service runs outside Docker (a host process, a CLI tool, a developer-laptop dev server) | [host-side-pointer.md](host-side-pointer.md) | +| Lightwalletd-compatible client | Your service is a wallet or block explorer that speaks the `CompactTxStreamer` gRPC protocol | [lightwalletd-client.md](lightwalletd-client.md) | +| Ephemeral test fixture | Your test suite needs a controlled, deterministic chain | NOT a Z3 attachment; see [the ephemeral-fixture note](#ephemeral-test-fixtures) below | + +All examples assume a running Z3 stack on the relevant network. Bring one up first: + +```bash +cd +docker compose --env-file .env.mainnet up -d # or testnet/regtest +``` + +See [`contract.md`](../contract.md) for the public identifiers each example references. + +## Conventions + +Examples use `` as a placeholder for the network you're targeting. Cookie-auth examples apply to mainnet and testnet; regtest disables cookie auth and uses username/password through its regtest configs and rpc-router. + +Volume and network names follow the contract pattern `z3--` (and `z3-` for the external network). Verify the exact names with: + +```bash +./scripts/validate-contract.py +``` + +## Ephemeral test fixtures + +If your test suite needs a fresh chain on every run, a fixed tip, or full control over consensus parameters, **don't** attach to a shared Z3 stack. Spin up an ephemeral Zebra (the existing `zebra-test` harness or a per-test `zebrad` binary) and tear it down with the test. + +If your tests need an indexer or wallet behind that ephemeral Zebra, run them per-test too. Sharing chain state across parallel tests is what makes them flaky. diff --git a/docs/integrations/compose-peer.md b/docs/integrations/compose-peer.md new file mode 100644 index 0000000..de7467d --- /dev/null +++ b/docs/integrations/compose-peer.md @@ -0,0 +1,137 @@ +# Compose-peer integration + +Your service runs as a Docker container in the same logical stack as Z3. On mainnet and testnet, you attach to Z3's external network for DNS and to Z3's cookie volume for RPC auth. + +This is the most efficient integration type: services talk over the Docker network without going through the host, and the RPC cookie is mounted directly into your container. Regtest disables cookie auth; for regtest, omit the cookie volume and use the username/password path (see [Regtest auth](#regtest-auth) below). + +## Prerequisites + +- A running Z3 stack: `docker compose --env-file .env. up -d` in the Z3 repo. +- Your service's compose file lives in your repo (not in Z3). + +## Compose snippet + +The contract publishes the external network and cookie volume names per network as `z3-` and `z3--cookie` ([contract.md](../contract.md#volumes)). Reuse them by name from your own env file so the same compose works on every network: + +```yaml +# your-service/docker-compose.yml + +name: my-service-${Z3_NETWORK_SHORT:-mainnet} + +networks: + z3: + external: true + name: ${Z3_EXTERNAL_NETWORK:-z3-mainnet} + +volumes: + z3-cookie: + external: true + name: ${Z3_COOKIE_VOLUME:-z3-mainnet-cookie} + +services: + my-service: + image: my-service:latest + networks: [z3] + volumes: + - z3-cookie:/var/run/auth:ro + environment: + MY_SERVICE_ZEBRA_RPC_URL: http://zebra:${Z3_ZEBRA_CONTAINER_PORT:-8232} + MY_SERVICE_ZEBRA_COOKIE_PATH: /var/run/auth/.cookie + MY_SERVICE_NETWORK: ${Z3_NETWORK:-Mainnet} +``` + +The four `${Z3_*}` substitutions come from your service's per-network env file (below). The volume and network names match the contract's published identifiers exactly. + +## Per-network env files + +Mirror Z3's own pattern: every per-network env file sets `Z3_NETWORK` (PascalCase, what Zebra's serde wants) plus the lowercase / numeric values your compose snippet substitutes. `Z3_EXTERNAL_NETWORK` and `Z3_COOKIE_VOLUME` come straight from the [contract](../contract.md#volumes); `Z3_ZEBRA_CONTAINER_PORT` from the [port matrix](../contract.md#port-matrix). + +```bash +# my-service/.env.mainnet +Z3_NETWORK=Mainnet +Z3_NETWORK_SHORT=mainnet +Z3_EXTERNAL_NETWORK=z3-mainnet +Z3_COOKIE_VOLUME=z3-mainnet-cookie +Z3_ZEBRA_CONTAINER_PORT=8232 +``` + +```bash +# my-service/.env.testnet +Z3_NETWORK=Testnet +Z3_NETWORK_SHORT=testnet +Z3_EXTERNAL_NETWORK=z3-testnet +Z3_COOKIE_VOLUME=z3-testnet-cookie +Z3_ZEBRA_CONTAINER_PORT=18232 +``` + +```bash +# my-service/.env.regtest +Z3_NETWORK=Regtest +Z3_NETWORK_SHORT=regtest +Z3_EXTERNAL_NETWORK=z3-regtest +Z3_COOKIE_VOLUME=z3-regtest-cookie +Z3_ZEBRA_CONTAINER_PORT=18232 +``` + +## Per-network container ports + +Container ports are not host ports. Inside the network, services use Zebra's per-network defaults; Zaino and Zallet container ports do not vary by network. Regtest reuses Zebra's testnet container port, so testnet and regtest share the same in-network address (`zebra:18232`); only their published host ports differ (testnet `18232`, regtest `29232`). + +| Network | Zebra RPC | Zaino gRPC | Zallet RPC | +|---------|-----------|-------------|-------------| +| Mainnet | `zebra:8232` | `zaino:8137` | `zallet:28232` | +| Testnet | `zebra:18232` | `zaino:8137` | `zallet:28232` | +| Regtest | `zebra:18232` | `zaino:8137` | `zallet:28232` | + +The full per-network matrix lives in [`z3-contract.yaml`](../../z3-contract.yaml) under `networks..ports`. + +## Regtest auth + +Regtest disables cookie auth (the contract sets `rpc_auth.mode: username_password` for that network). The cookie volume `z3-regtest-cookie` exists but holds no readable cookie. Two ways to authenticate: + +- **Direct to Zebra:** point at `http://zebra:18232` with HTTP Basic, user/password = `zebra` / `zebra` by default. Override via `Z3_REGTEST_RPC_ROUTER_USER` / `Z3_REGTEST_RPC_ROUTER_PASSWORD` on the Z3 side; the same credentials gate the rpc-router and Zebra in regtest. +- **Through rpc-router:** point at `http://rpc-router:8181` for a unified JSON-RPC endpoint that proxies to Zebra or Zallet by method name. Same credentials. + +Drop the cookie volume from your service's compose on regtest, or guard the mount with a per-network override file. + +## Bring it up + +```bash +# Bring up Z3 first. +cd ../z3 && docker compose --env-file .env.mainnet up -d + +# Then bring up your service, pointing at the same network. +cd ../my-service +docker compose --env-file .env.mainnet up -d +``` + +## Verify the attachment + +`docker compose exec` references your service by its Compose service name (the key under `services:`), so you never need to know the container name Compose autogenerated: + +```bash +# Your service is on the z3 network +docker network inspect z3-mainnet --format '{{json .Containers}}' | jq + +# Your service can resolve zebra by DNS +docker compose --env-file .env.mainnet exec my-service nslookup zebra + +# Your service can read the cookie +docker compose --env-file .env.mainnet exec my-service head -c 20 /var/run/auth/.cookie +``` + +## Common pitfalls + +- **Attaching before Z3 is up:** Compose fails with `network z3- declared as external, but could not be found`. Run Z3 first. +- **Hardcoding `z3-mainnet` in your compose:** Template via `${Z3_EXTERNAL_NETWORK}` and `${Z3_COOKIE_VOLUME}` so the same compose file works against testnet and regtest by swapping the env file. +- **Mounting the regtest cookie volume:** Regtest does not publish a readable cookie file. Omit the mount or use the regtest auth path above. +- **Forgetting `:ro` on the cookie mount:** Mounting read-write means a bug in your service could corrupt Zebra's auth state. Always `:ro`. +- **Adding your own `depends_on: zebra`:** Only works if you and Z3 are in the same Compose project (rare). In separate projects, use a startup wait loop in your service's entrypoint that polls `http://zebra:` until it responds. +- **Worrying about cookie file UID:** Zebra writes the cookie as uid 10001 with mode 0600. Z3 runs a small `cookie-permissions` sidecar that chmods it to 0644 within the shared volume so your container can read it regardless of its own uid. No UID coordination required. + +## Tear down + +```bash +docker compose --env-file .env.mainnet down +# Your service's volumes go; Z3's volumes stay because they're declared external. +``` diff --git a/docs/integrations/host-side-pointer.md b/docs/integrations/host-side-pointer.md new file mode 100644 index 0000000..7437a00 --- /dev/null +++ b/docs/integrations/host-side-pointer.md @@ -0,0 +1,108 @@ +# Host-side pointer integration + +Your service runs outside Docker: a host process, a CLI tool, a developer-laptop dev server. On mainnet and testnet, you connect to Z3 via the published host ports and inject the cookie text because you can't mount a Docker volume into a non-container process. + +This integration type is less efficient than Compose-peer because every connection goes through the host network stack, but it fits development, scripting, and any service that lives outside Docker's lifecycle. + +## Prerequisites + +- A running Z3 stack: `docker compose --env-file .env. up -d` in the Z3 repo. + +## Connect via host ports + +Host ports per network (full matrix in [`z3-contract.yaml`](../../z3-contract.yaml) under `networks..ports`): + +| Service | Mainnet | Testnet | Regtest | +|---------|---------|---------|---------| +| Zebra RPC | `http://127.0.0.1:8232` | `http://127.0.0.1:18232` | `http://127.0.0.1:29232` | +| Zebra `/ready` | `http://127.0.0.1:8080/ready` | `http://127.0.0.1:18080/ready` | `http://127.0.0.1:28080/ready` | +| Zaino gRPC (plaintext h2c) | `127.0.0.1:8137` | `127.0.0.1:18137` | `127.0.0.1:28137` | +| Zaino JSON-RPC | `http://127.0.0.1:8237` | `http://127.0.0.1:18237` | `http://127.0.0.1:28237` | +| Zallet RPC | `http://127.0.0.1:28232` | `http://127.0.0.1:40232` | `http://127.0.0.1:50232` | +| rpc-router (regtest only) | n/a | n/a | `http://127.0.0.1:8181` | + +Testnet uses a `+10000` host-port offset from mainnet for services without an upstream per-network convention; regtest uses explicit host ports to avoid collisions with mainnet/testnet. + +## Read the RPC cookie + +A host process cannot mount a Docker volume. On mainnet and testnet, copy the cookie text out: + +```bash +# One-liner cookie reader +docker run --rm -v z3-mainnet-cookie:/auth:ro alpine \ + cat /auth/.cookie +``` + +Wrap it in a small helper your service runs at startup: + +```bash +#!/usr/bin/env bash +# get-z3-cookie.sh: print the Z3 cookie text for a given network. +# Usage: ./get-z3-cookie.sh mainnet +set -euo pipefail +NETWORK="${1:-mainnet}" +docker run --rm -v "z3-${NETWORK}-cookie:/auth:ro" alpine cat /auth/.cookie +``` + +Your service then reads it via env: + +```bash +export ZEBRA_RPC_URL=http://127.0.0.1:8232 +export ZEBRA_COOKIE=$(./get-z3-cookie.sh mainnet) +./my-service +``` + +## Use the cookie in an HTTP request + +Zebra's RPC auth expects HTTP Basic with the cookie as the password (Bitcoin Core / Zcash convention): + +```bash +COOKIE="$(./get-z3-cookie.sh mainnet)" +curl -sf -u "$COOKIE" -X POST -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"getblockchaininfo","params":[],"id":1}' \ + http://127.0.0.1:8232 | jq . +``` + +The cookie is in the format `__cookie__:`; the colon is the username/password separator that `curl -u` expects. Regtest disables cookie auth; see the next section. + +## Regtest auth + +Regtest's contract declares `rpc_auth.mode: username_password`. The cookie volume exists but holds no readable cookie. Authenticate with HTTP Basic using the rpc-router credentials (defaults: `zebra` / `zebra`, override via `Z3_REGTEST_RPC_ROUTER_USER` / `Z3_REGTEST_RPC_ROUTER_PASSWORD` on the Z3 side): + +```bash +# Direct to Zebra +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:29232 | jq . + +# Through rpc-router (unified Zebra + Zallet JSON-RPC endpoint) +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 . +``` + +## Health check before connecting + +Wait for Zebra to report ready before your service tries to talk to it: + +```bash +# Block until Zebra reports ready (or fail after 10 minutes) +for i in $(seq 1 200); do + if curl -sf http://127.0.0.1:8080/ready > /dev/null 2>&1; then + echo "Zebra ready"; break + fi + if [ "$i" -eq 200 ]; then echo "Zebra never became ready"; exit 1; fi + sleep 3 +done +``` + +## When to use this integration vs Compose-peer + +| Choose host-side pointer when | Choose Compose-peer when | +|--------------------------------|---------------------------| +| Your service is a dev server, REPL, or one-off script | Your service is a long-running daemon you ship in production | +| Your service is in a language without easy Docker integration | Your service deploys via Compose | +| You want to debug with native tools (gdb, dlv, language debuggers) | You want startup ordering via `depends_on` | +| You're iterating fast on the service itself | You're testing the consumer/platform integration | + +Both integration types can coexist for the same service. A common pattern: production uses Compose-peer; local development uses host-side pointer with `pnpm dev` / `cargo run` / etc. diff --git a/docs/integrations/lightwalletd-client.md b/docs/integrations/lightwalletd-client.md new file mode 100644 index 0000000..d673e7c --- /dev/null +++ b/docs/integrations/lightwalletd-client.md @@ -0,0 +1,100 @@ +# Lightwalletd-compatible client integration + +Your service is a wallet, block explorer, or scanner that speaks the lightwalletd `CompactTxStreamer` gRPC protocol. Z3's Zaino exposes that protocol as plaintext HTTP/2 (h2c) on the documented port. Terminate TLS at a reverse proxy if you expose Zaino beyond the host (see [Edge TLS in production](#edge-tls-in-production)). + +## Prerequisites + +- A running Z3 stack: `docker compose --env-file .env. up -d` in the Z3 repo. +- A gRPC client for your language (Tonic for Rust, grpcio for Python, grpc-java, etc.) or the `grpcurl` CLI for ad-hoc calls. +- The lightwalletd / Zaino `.proto` files. Fetch them into the Z3 repo with `scripts/vendor.sh zaino` (`vendor/zaino/zaino-proto/proto/service.proto`). + +## Endpoint per network + +Zaino's gRPC port is plaintext h2c and follows the contract's port matrix: + +| Network | Host endpoint | +|---------|----------------| +| Mainnet | `127.0.0.1:8137` | +| Testnet | `127.0.0.1:18137` | +| Regtest | `127.0.0.1:28137` | + +There is no TLS on this listener. The Z3 stack runs Zaino with its TLS guard compiled out (the `-no-tls` image), so intra-stack and host-side gRPC is unencrypted. Anything exposed to a network you do not control should sit behind a TLS-terminating reverse proxy. + +## Regtest auth + +Regtest's contract declares `rpc_auth.mode: username_password` for Zebra and Zallet; Zaino authenticates internally to Zebra using the same username/password (from `config/regtest/zaino.toml`). The Zaino gRPC surface itself does not require client auth on any network. The cookie volume `z3-regtest-cookie` exists but holds no readable cookie. If your test setup needs to talk to Zebra directly (mine blocks, inspect state), use the rpc-router host endpoint at `http://127.0.0.1:8181` with HTTP Basic, defaults `zebra` / `zebra` (override on the Z3 side via `Z3_REGTEST_RPC_ROUTER_USER` / `Z3_REGTEST_RPC_ROUTER_PASSWORD`). + +## Quick test with `grpcurl` + +Fetch the upstream Zaino source (proto files included) and point grpcurl at that path: + +```bash +# One-time: fetch the Zaino proto files into vendor/ +scripts/vendor.sh zaino + +# Probe the endpoint (mainnet example). -plaintext skips TLS. +grpcurl -plaintext \ + -import-path vendor/zaino/zaino-proto/proto \ + -proto service.proto \ + 127.0.0.1:8137 \ + cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLightdInfo + +# Get the latest block +grpcurl -plaintext \ + -import-path vendor/zaino/zaino-proto/proto \ + -proto service.proto \ + -d '{}' \ + 127.0.0.1:8137 \ + cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLatestBlock +``` + +## Rust (Tonic) example + +```rust +use tonic::transport::{Channel, Endpoint}; +use compact_tx_streamer_client::CompactTxStreamerClient; + +// Plaintext h2c: use http:// and no TLS config. +let channel = Endpoint::from_static("http://127.0.0.1:8137") + .connect() + .await?; + +let mut client = CompactTxStreamerClient::new(channel); +let info = client.get_lightd_info(()).await?.into_inner(); +println!("Lightd info: {info:?}"); +``` + +## TypeScript (Connect-ES) example + +```typescript +import { createGrpcTransport } from "@connectrpc/connect-node"; +import { createClient } from "@connectrpc/connect"; +import { CompactTxStreamer } from "./gen/service_pb"; + +const transport = createGrpcTransport({ + httpVersion: "2", + baseUrl: "http://127.0.0.1:8137", // plaintext h2c; no TLS +}); + +const client = createClient(CompactTxStreamer, transport); +const info = await client.getLightdInfo({}); +``` + +## What's the difference between Zaino and Zallet? + +| Service | Audience | Protocol | +|---------|----------|----------| +| **Zaino** | External light-wallet clients (mobile wallets, scanners) | gRPC `CompactTxStreamer` over plaintext h2c | +| **Zallet** | Operator wallet (the operator's own keys, full RPC surface) | JSON-RPC over HTTP | + +If you're writing a mobile wallet or a public-facing scanner, use Zaino. If you're administering the operator's wallet (creating addresses, sending transactions, importing keys), use Zallet. + +## Edge TLS in production + +The gRPC listener is plaintext, which is fine inside the Docker network and for local development. Wallet clients on the public internet expect TLS, so for any deployment exposed beyond `127.0.0.1`: + +1. Put a reverse proxy (nginx, Caddy, Envoy, or your cloud load balancer) in front of Zaino and terminate TLS there with a certificate from a trusted CA. +2. Proxy decrypted h2c traffic to Zaino's gRPC port on the internal network. +3. Point clients at the proxy's `https://` endpoint; the proxy handles TLS and forwards plaintext gRPC to Zaino. + +This keeps certificate management at the edge where it belongs, rather than baking a self-signed cert into the node stack. diff --git a/docs/regtest.md b/docs/regtest.md new file mode 100644 index 0000000..607a20a --- /dev/null +++ b/docs/regtest.md @@ -0,0 +1,156 @@ +# Z3 Regtest Environment + +Local end-to-end testing of the full Z3 stack (Zebra, Zaino, Zallet, and the rpc-router) in regtest mode. + +Uses the base `docker-compose.yml` with `docker-compose.regtest.yml` overlay and `.env.regtest` for regtest-specific configuration. Volumes are isolated via `COMPOSE_PROJECT_NAME=z3-regtest`, so regtest data never conflicts with mainnet or testnet. + +## Prerequisites + +- Docker with [Docker Compose](https://docs.docker.com/compose/install/) (v2.24.4+) +- [rage](https://github.com/str4d/rage/releases) for generating Zallet encryption keys +- For gRPC testing: [grpcurl](https://github.com/fullstorydev/grpcurl) and the Zaino proto files (`scripts/vendor.sh zaino`) + +## First-time setup + +From the repo root: + +```bash +./scripts/regtest-init.sh +``` + +This will: + +1. Copy the per-network config templates (`zebra.toml`, `zaino.toml`, `zallet.toml`) into live gitignored files +2. Generate a Zallet encryption identity (if not already present) +3. Generate and inject the Zallet RPC password hash in `config/regtest/zallet.toml` +4. Start Zebra in regtest mode with the activation heights Zaino expects (Canopy at 1, NU5/Orchard at 2) +5. Mine 2 blocks to activate Orchard +6. Initialize the Zallet wallet (`init-wallet-encryption` + `generate-mnemonic`) + +Optionally override the rpc-router password (default is `zebra`). The `REGTEST_` infix marks the var as regtest-scoped: + +```bash +Z3_REGTEST_RPC_ROUTER_PASSWORD='your-password' ./scripts/regtest-init.sh +``` + +## Start the stack + +From the repo root: + +```bash +docker compose --env-file .env.regtest up -d +``` + +Zebra, Zaino, and Zallet use pre-built images. The rpc-router builds from source on first run (takes a few minutes; subsequent runs use the Docker layer cache). + +> [!NOTE] +> Regtest host ports are explicit and globally unique so all three networks (mainnet, testnet, regtest) can run concurrently on one host without binding collisions. + +| Service | Endpoint | Description | +|---------|----------|-------------| +| rpc-router | http://localhost:8181 | JSON-RPC router (Zebra + Zallet) | +| Zaino gRPC | localhost:28137 | lightwalletd-compatible gRPC (plaintext h2c) | +| Zebra RPC | http://localhost:29232 | Direct Zebra JSON-RPC | +| Zallet RPC | http://localhost:50232 | Direct Zallet JSON-RPC | + +## Test routing + +These commands go through the rpc-router, which forwards to Zebra or Zallet based on the method: + +```bash +# Route to Zebra (full node) +curl -s -X POST -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"getblockchaininfo","params":[],"id":1}' \ + http://127.0.0.1:8181 + +# Route to Zallet (wallet) +curl -s -X POST -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"getwalletinfo","params":[],"id":2}' \ + http://127.0.0.1:8181 + +# Merged OpenRPC schema +curl -s -X POST -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"rpc.discover","params":[],"id":3}' \ + http://127.0.0.1:8181 | grep -o '"title":"[^"]*"' +``` + +## Test Zaino gRPC + +Zaino exposes the [lightwalletd-compatible gRPC protocol](https://github.com/zcash/lightwalletd/blob/master/walletrpc/service.proto) as plaintext h2c (no TLS). In regtest the host port is `28137` (`Z3_ZAINO_HOST_GRPC_PORT`); the `-plaintext` flag tells grpcurl to skip TLS. + +Fetch the Zaino proto files if you haven't already: + +```bash +scripts/vendor.sh zaino +``` + +Test with `GetLightdInfo` (from the repo root): + +```bash +grpcurl -plaintext \ + -import-path vendor/zaino/zaino-proto/proto \ + -proto service.proto \ + 127.0.0.1:28137 \ + cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLightdInfo +``` + +Get the latest block height: + +```bash +grpcurl -plaintext \ + -import-path vendor/zaino/zaino-proto/proto \ + -proto service.proto \ + -d '{}' \ + 127.0.0.1:28137 \ + cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLatestBlock +``` + +## OpenRPC Playground + +Open the playground pointed at your locally running router: + + + +The playground calls `rpc.discover` on `http://127.0.0.1:8181` to load the live merged schema. + +## Stop and clean up + +```bash +# Stop containers (keeps volumes/wallet data) +docker compose --env-file .env.regtest down + +# Full reset (deletes all regtest data; re-run scripts/regtest-init.sh afterwards) +docker compose --env-file .env.regtest down -v +``` + +## Expected output + +**`getblockchaininfo`** (routed to Zebra, truncated): + +```json +{"jsonrpc":"2.0","id":1,"result":{"chain":"test","blocks":1,"headers":1,...,"upgrades":{"5ba81b19":{"name":"Overwinter","activationheight":1,"status":"active"},...}}} +``` + +**`getwalletinfo`** (routed to Zallet): + +```json +{"jsonrpc":"2.0","result":{"walletversion":0,"balance":0.00000000,"unconfirmed_balance":0.00000000,"immature_balance":0.00000000,"shielded_balance":"0.00","shielded_unconfirmed_balance":"0.00","txcount":0,"keypoololdest":0,"keypoolsize":0,"mnemonic_seedfp":"TODO"},"id":1} +``` + +## Monitoring in regtest + +Zebra's Prometheus endpoint is enabled by default on the internal `zebra:9999` scrape target. Start the monitoring profile: + +```bash +docker compose --env-file .env.regtest --profile monitoring up -d +``` + +Regtest monitoring UI ports are Grafana `23000`, Prometheus `29094`, Jaeger UI `36686`, and AlertManager `29093`. Jaeger also publishes OTLP gRPC `25317`, OTLP HTTP `25318`, and spanmetrics `28889`. + +## Notes + +- Credentials: `zebra` / `zebra` (hardcoded for regtest only) +- Regtest activates upgrades through Canopy at block 1 and NU5/Orchard at block 2 (Zebra, Zaino, and Zallet all agree) +- Zaino uses username/password auth in regtest (not cookie auth) +- Zaino gRPC is plaintext h2c on all networks; terminate edge TLS at a reverse proxy if exposed beyond the host +- The rpc-router source is in `rpc-router/`; it is built automatically on first `docker compose up` diff --git a/observability/README.md b/observability/README.md new file mode 100644 index 0000000..43f2e17 --- /dev/null +++ b/observability/README.md @@ -0,0 +1,207 @@ +# Z3 Observability Stack + +Metrics, alerting, and dashboards for the Z3 stack (Zebra, Zaino, Zallet). + +## Quick Start + +```bash +# 1. Optional: enable Zebra trace export in .env +ZEBRA_TRACING__OPENTELEMETRY_ENDPOINT=http://jaeger:4318 +ZEBRA_TRACING__OPENTELEMETRY_SERVICE_NAME=zebra-mainnet +ZEBRA_TRACING__OPENTELEMETRY_SAMPLE_PERCENT=100 + +# 2. Start the full stack with monitoring. +# Passing .env as a second env file also applies local port and image overrides. +docker compose --env-file .env.mainnet --env-file .env --profile monitoring up -d + +# 3. View logs +docker compose --env-file .env.mainnet --env-file .env logs -f zebra +``` + +> **Note**: The monitoring profile starts Jaeger, but Zebra only exports spans +> after `ZEBRA_TRACING__OPENTELEMETRY_ENDPOINT` is set. The pinned Zebra image +> and the default local build use `default-release-binaries`, which includes +> OpenTelemetry. If you override `Z3_ZEBRA_BUILD_FEATURES`, keep +> `opentelemetry` in the feature list. + +## Components + +| Component | Purpose | +|-----------|---------| +| **Zebra** | Zcash node with metrics and tracing (in-network scrape on `:9999`) | +| **Prometheus** | Metrics collection and storage | +| **Grafana** | Dashboards and visualization | +| **Jaeger** | Distributed tracing UI | +| **AlertManager** | Alert routing | + +Published host ports for these components are per-network and live in [`z3-contract.yaml`](../z3-contract.yaml) under `networks..ports` (the `monitoring` profile). + +Default Grafana credentials: `admin` / `admin` (you'll be prompted to change on first login) + +## Architecture + +```text +┌─────────────────────────────────────────────────────────────────────┐ +│ Zebra Node │ +│ ┌─────────────────┐ ┌─────────────────────────────┐ │ +│ │ Metrics │ │ Tracing (OpenTelemetry) │ │ +│ │ :9999/metrics │ │ OTLP HTTP → Jaeger │ │ +│ └────────┬────────┘ └──────────────┬──────────────┘ │ +└───────────│──────────────────────────────────────│──────────────────┘ + │ │ + ▼ ▼ +┌───────────────────┐ ┌───────────────────────────┐ +│ Prometheus │ │ Jaeger │ +│ :9094 │ │ :16686 (UI) │ +│ │◄─────────────────│ :8889 (spanmetrics) │ +│ Scrapes metrics │ Span metrics │ :4318 (OTLP HTTP) │ +└─────────┬─────────┘ └───────────────────────────┘ + │ │ + ▼ │ +┌───────────────────┐ │ +│ Grafana │◄─────────────────────────────┘ +│ :3000 │ Trace queries +│ │ +│ Dashboards for │ +│ metrics + traces │ +└─────────┬─────────┘ + │ + ▼ +┌───────────────────┐ +│ AlertManager │ +│ :9093 │ +│ │ +│ Routes alerts │ +└───────────────────┘ +``` + +## What Each Component Provides + +### Metrics (Prometheus + Grafana) + +Quantitative data about Zebra's behavior over time: + +- **Network health**: Peer connections, bandwidth, message rates +- **Sync progress**: Block height, checkpoint verification, chain tip +- **Performance**: Block/transaction verification times +- **Resources**: Memory, connections, queue depths + +See [grafana/README.md](grafana/README.md) for dashboard details. + +### Tracing (Jaeger) + +Distributed tracing uses Zebra's OpenTelemetry exporter and the Jaeger collector +from the `monitoring` profile. Jaeger can run without Zebra traces; Zebra begins +exporting spans only after the OTLP endpoint is configured. + +Enable tracing in `.env`: + +```bash +ZEBRA_TRACING__OPENTELEMETRY_ENDPOINT=http://jaeger:4318 +ZEBRA_TRACING__OPENTELEMETRY_SERVICE_NAME=zebra-mainnet +ZEBRA_TRACING__OPENTELEMETRY_SAMPLE_PERCENT=100 +``` + +Then recreate Zebra with the local env file loaded: + +```bash +docker compose --env-file .env.mainnet --env-file .env --profile monitoring up -d --force-recreate zebra +``` + +`ZEBRA_TRACING__OPENTELEMETRY_*` values reach the Zebra container through its +service-level `env_file`. `Z3_*` values such as `Z3_ZEBRA_IMAGE` and +`Z3_JAEGER_OTLP_HTTP_PORT` are Compose interpolation inputs, so they need to be +exported in the shell or loaded with `--env-file .env`. + +If you build a custom Zebra image with `Z3_ZEBRA_BUILD_FEATURES`, include +`opentelemetry` or use `default-release-binaries`. + +Jaeger provides: + +- **Distributed traces**: Follow a request through all components +- **Latency breakdown**: See where time is spent in each operation +- **Error analysis**: Identify failure points and error propagation +- **Service Performance Monitoring (SPM)**: RED metrics for RPC endpoints + +See [jaeger/README.md](jaeger/README.md) for tracing details. + +### Alerts (AlertManager) + +Automated notifications for operational issues: + +- Critical: Negative value pools (ZIP-209 violation) +- Warning: High RPC latency, sync stalls, peer connection issues + +Configure alert destinations in [alertmanager/alertmanager.yml](alertmanager/alertmanager.yml). + +## Configuration + +### Environment Variables + +Add this to your `.env` file to enable Zebra metrics: + +| Variable | Default | Description | +|----------|---------|-------------| +| `ZEBRA_METRICS__ENDPOINT_ADDR` | - | Prometheus metrics endpoint (e.g., `0.0.0.0:9999`) | + +### Port Customization + +Override default monitoring host ports via the `Z3_*` env vars in your env file or `.env`: + +```bash +Z3_GRAFANA_PORT=3000 +Z3_PROMETHEUS_PORT=9094 +Z3_JAEGER_UI_PORT=16686 +Z3_ALERTMANAGER_PORT=9093 +``` + +See [`z3-contract.yaml`](../z3-contract.yaml) for the full env-var schema. + +## Common Tasks + +### View Zebra's current metrics + +```bash +docker compose --env-file .env.mainnet exec zebra \ + curl -sf http://127.0.0.1:9999/metrics | grep zcash +``` + +Zebra's metrics port is intentionally in-network only; Prometheus scrapes it on +the Compose network. + +### Query Prometheus directly + +```bash +# Current block height +curl -s 'http://localhost:9094/api/v1/query?query=zcash_state_tip_height' +``` + +## Troubleshooting + +### No metrics in Grafana + +1. Verify `ZEBRA_METRICS__ENDPOINT_ADDR=0.0.0.0:9999` is set in `.env` +2. Restart Zebra: `docker compose --env-file .env.mainnet --env-file .env restart zebra` +3. Check Zebra is exposing metrics: `docker compose --env-file .env.mainnet exec zebra curl -sf http://127.0.0.1:9999/metrics | head` +4. Check Prometheus targets: + +### No traces in Jaeger + +1. Verify Zebra has the OTLP env vars: `docker inspect z3-mainnet-zebra-1 --format '{{range .Config.Env}}{{println .}}{{end}}' | grep ZEBRA_TRACING__OPENTELEMETRY` +2. Check Zebra installed the tracing layer: `docker logs z3-mainnet-zebra-1 | grep 'installed OpenTelemetry tracing layer'` +3. Check Jaeger has the Zebra service: `curl -s http://127.0.0.1:16686/api/services` +4. Check Prometheus has span metrics: `curl -sG http://127.0.0.1:9094/api/v1/query --data-urlencode 'query=traces_span_metrics_calls_total{service_name="zebra-mainnet"}'` + +## Running Without Monitoring + +To run the Z3 stack without monitoring: + +```bash +docker compose up -d # Only starts zebra, zaino, zallet +``` + +To add monitoring later: + +```bash +docker compose --profile monitoring up -d +``` diff --git a/observability/alertmanager/alertmanager.yml b/observability/alertmanager/alertmanager.yml new file mode 100644 index 0000000..5b3351e --- /dev/null +++ b/observability/alertmanager/alertmanager.yml @@ -0,0 +1,28 @@ +global: + resolve_timeout: 5m + +route: + group_by: ['alertname', 'severity'] + group_wait: 10s + group_interval: 10s + repeat_interval: 1h + receiver: 'default' + routes: + - match: + severity: critical + receiver: 'critical' + - match: + severity: warning + receiver: 'warning' + +receivers: + - name: 'default' + - name: 'critical' + - name: 'warning' + +inhibit_rules: + - source_match: + severity: 'critical' + target_match: + severity: 'warning' + equal: ['alertname'] diff --git a/observability/grafana/README.md b/observability/grafana/README.md new file mode 100644 index 0000000..ed4455d --- /dev/null +++ b/observability/grafana/README.md @@ -0,0 +1,123 @@ +# Zebra Grafana Dashboards + +Pre-built dashboards for monitoring Zebra nodes. + +## Quick Start + +```bash +# From repository root - starts Zebra + all observability tools +docker compose -f docker/docker-compose.observability.yml up -d +``` + +Access Grafana at (admin/admin - you'll be prompted to change on first login). + +For full stack documentation, see the [Observability README](../README.md). + +## Dashboards + +| Dashboard | Description | +|-----------|-------------| +| `network_health.json` | Peer connections, bandwidth (default home) | +| `syncer.json` | Sync progress, block downloads | +| `mempool.json` | Transaction pool metrics | +| `peers.json` | Peer connection details | +| `block_verification.json` | Block verification stats | +| `checkpoint_verification.json` | Checkpoint sync progress | +| `transaction-verification.json` | Transaction verification | +| `network_messages.json` | P2P protocol messages | +| `errors.json` | Error tracking | + +## Datasources + +Grafana is provisioned with two datasources: + +| Datasource | UID | Description | +|------------|-----|-------------| +| Prometheus | `zebra-prometheus` | Metrics storage and queries | +| Jaeger | `zebra-jaeger` | Distributed tracing | + +Configuration: `provisioning/datasources/datasources.yml` + +### Using Jaeger in Grafana + +The Jaeger datasource allows you to: + +- Search traces by service name +- View trace details and span timelines +- Correlate traces with metrics (via trace IDs) + +To explore traces: + +1. Go to **Explore** in Grafana +2. Select **Jaeger** datasource +3. Search for service `zebra` + +Or access Jaeger UI directly at for full trace exploration. +See the [Jaeger README](../jaeger/README.md) for detailed tracing documentation. + +## Dashboard Configuration + +### Rate Window Requirements + +Dashboards use `rate()` functions for per-second metrics. The rate window must +contain at least 2 data points to calculate a rate. + +| Scrape Interval | Minimum Rate Window | +|-----------------|---------------------| +| 500ms | 1s | +| 15s (default) | 30s | +| 30s | 1m | + +Current dashboards use `[1m]` windows, compatible with the default 15s scrape interval. + +If you modify `../prometheus/prometheus.yaml` scrape_interval, update dashboard queries accordingly. + +### Job Label + +The `$job` variable in dashboards is populated from Prometheus. The default job +name is `zebra` (configured in `../prometheus/prometheus.yaml`). + +## Creating New Dashboards + +### Option 1: Grafana UI Export (Recommended) + +1. Create panel in Grafana UI +2. Click panel title → "Inspect" → "Panel JSON" +3. Add to dashboard file +4. Commit + +### Option 2: Copy Existing Panel + +1. Find similar panel in existing dashboard +2. Copy JSON, update metric names and titles +3. Test in Grafana + +### Panel Template + +```json +{ + "title": "Your Metric", + "type": "timeseries", + "targets": [ + { + "expr": "rate(your_metric_total[1m])", + "legendFormat": "{{label}}" + } + ], + "fieldConfig": { + "defaults": { + "unit": "reqps" + } + } +} +``` + +## Validation + +```bash +# Check JSON syntax +for f in dashboards/*.json; do jq . "$f" > /dev/null && echo "$f: OK"; done + +# List all metrics used +jq -r '.panels[].targets[]?.expr' dashboards/*.json | sort -u +``` diff --git a/observability/grafana/dashboards/block_verification.json b/observability/grafana/dashboards/block_verification.json new file mode 100644 index 0000000..0c99019 --- /dev/null +++ b/observability/grafana/dashboards/block_verification.json @@ -0,0 +1,529 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "liveNow": false, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "decimals": 0, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 4, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.1.6", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "state_full_verifier_committed_block_height{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "full verified", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "state_checkpoint_finalized_block_height{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "checkpoint verified", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "editorMode": "code", + "expr": "state_memory_queued_max_height{job=\"$job\"}", + "hide": false, + "legendFormat": "full queued max", + "range": true, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "editorMode": "code", + "expr": "state_memory_queued_min_height{job=\"$job\"}", + "hide": false, + "legendFormat": "full queued min", + "range": true, + "refId": "I" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "editorMode": "code", + "expr": "state_checkpoint_queued_max_height{job=\"$job\"}", + "hide": false, + "legendFormat": "checkpoint queued max", + "range": true, + "refId": "F" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "editorMode": "code", + "expr": "state_checkpoint_queued_min_height{job=\"$job\"}", + "hide": false, + "legendFormat": "checkpoint queued min", + "range": true, + "refId": "J" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "editorMode": "code", + "expr": "state_memory_sent_block_height{job=\"$job\"}", + "hide": false, + "legendFormat": "full sent", + "range": true, + "refId": "H" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "editorMode": "code", + "expr": "state_checkpoint_sent_block_height{job=\"$job\"}", + "hide": false, + "legendFormat": "checkpoint sent", + "range": true, + "refId": "G" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "zcash_chain_verified_block_height{job=\"$job\"}", + "interval": "", + "legendFormat": "committed", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "state_finalized_block_height{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "finalized", + "range": true, + "refId": "D" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Verified Block Height - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:84", + "format": "none", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:85", + "format": "none", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 7 + }, + "hiddenSeries": false, + "id": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.1.6", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "repeatDirection": "h", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "rate(zcash_chain_verified_block_total{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "zcash_chain_verified_block_total[1m]", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "rate(sync_downloaded_block_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "sync_downloaded_block_count", + "refId": "H" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sync_downloads_in_flight{job=\"$job\"}", + "interval": "", + "legendFormat": "sync_downloads_in_flight", + "refId": "I" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "rate(sync_verified_block_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "sync_verified_block_count", + "refId": "J" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Block Sync Count - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:167", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:168", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 14 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.1.6", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "repeatDirection": "h", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "rate(zcash_chain_verified_block_total{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "zcash_chain_verified_block_total[1m]", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "rate(gossip_downloaded_block_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "gossip_downloaded_block_count[1m]", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "rate(gossip_verified_block_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "gossip_verified_block_count[1m]", + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "gossip_queued_block_count{job=\"$job\"}", + "interval": "", + "legendFormat": "gossip_queued_block_count", + "refId": "E" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Block Gossip Count - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:252", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:253", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + ], + "refresh": "1m", + "schemaVersion": 37, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "definition": "label_values(zcash_chain_verified_block_height, job)", + "hide": 0, + "includeAll": true, + "multi": true, + "name": "job", + "options": [], + "query": { + "query": "label_values(zcash_chain_verified_block_height, job)", + "refId": "Prometheus-Zebra-job-Variable-Query" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "block verification", + "uid": "rO_Cl5tGz", + "version": 18, + "weekStart": "" +} diff --git a/observability/grafana/dashboards/checkpoint_verification.json b/observability/grafana/dashboards/checkpoint_verification.json new file mode 100644 index 0000000..3ab5d3e --- /dev/null +++ b/observability/grafana/dashboards/checkpoint_verification.json @@ -0,0 +1,2084 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 2, + "iteration": 1633652714499, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "decimals": null, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "avg": false, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": null, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "repeatDirection": "h", + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "checkpoint_processing_next_height{job=\"$job\"}", + "interval": "", + "legendFormat": "next_check", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "checkpoint_queued_continuous_height{job=\"$job\"}", + "interval": "", + "legendFormat": "queue_cont", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "checkpoint_verified_height{job=\"$job\"}", + "interval": "", + "legendFormat": "verified", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "checkpoint_queued_max_height{job=\"$job\"}", + "interval": "", + "legendFormat": "queue_max", + "refId": "D", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "state_checkpoint_committed_block_height{job=\"$job\"}", + "interval": "", + "legendFormat": "state_commit", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "state_checkpoint_queued_max_height{job=\"$job\"}", + "interval": "", + "legendFormat": "state_q_max", + "refId": "F", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Checkpoint Height - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:84", + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:85", + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "decimals": null, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 0 + }, + "hiddenSeries": false, + "id": 9, + "legend": { + "avg": false, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": null, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "repeatIteration": 1633652714499, + "repeatPanelId": 2, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet-tmp", + "value": "zebrad-mainnet-tmp" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "checkpoint_processing_next_height{job=\"$job\"}", + "interval": "", + "legendFormat": "next_check", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "checkpoint_queued_continuous_height{job=\"$job\"}", + "interval": "", + "legendFormat": "queue_cont", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "checkpoint_verified_height{job=\"$job\"}", + "interval": "", + "legendFormat": "verified", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "checkpoint_queued_max_height{job=\"$job\"}", + "interval": "", + "legendFormat": "queue_max", + "refId": "D", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "state_checkpoint_committed_block_height{job=\"$job\"}", + "interval": "", + "legendFormat": "state_commit", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "state_checkpoint_queued_max_height{job=\"$job\"}", + "interval": "", + "legendFormat": "state_q_max", + "refId": "F", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Checkpoint Height - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:84", + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:85", + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "decimals": null, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 0 + }, + "hiddenSeries": false, + "id": 10, + "legend": { + "avg": false, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": null, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "repeatIteration": 1633652714499, + "repeatPanelId": 2, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet", + "value": "zebrad-testnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "checkpoint_processing_next_height{job=\"$job\"}", + "interval": "", + "legendFormat": "next_check", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "checkpoint_queued_continuous_height{job=\"$job\"}", + "interval": "", + "legendFormat": "queue_cont", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "checkpoint_verified_height{job=\"$job\"}", + "interval": "", + "legendFormat": "verified", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "checkpoint_queued_max_height{job=\"$job\"}", + "interval": "", + "legendFormat": "queue_max", + "refId": "D", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "state_checkpoint_committed_block_height{job=\"$job\"}", + "interval": "", + "legendFormat": "state_commit", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "state_checkpoint_queued_max_height{job=\"$job\"}", + "interval": "", + "legendFormat": "state_q_max", + "refId": "F", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Checkpoint Height - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:84", + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:85", + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "decimals": null, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 0 + }, + "hiddenSeries": false, + "id": 11, + "legend": { + "avg": false, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": null, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "repeatIteration": 1633652714499, + "repeatPanelId": 2, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet-tmp", + "value": "zebrad-testnet-tmp" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "checkpoint_processing_next_height{job=\"$job\"}", + "interval": "", + "legendFormat": "next_check", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "checkpoint_queued_continuous_height{job=\"$job\"}", + "interval": "", + "legendFormat": "queue_cont", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "checkpoint_verified_height{job=\"$job\"}", + "interval": "", + "legendFormat": "verified", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "checkpoint_queued_max_height{job=\"$job\"}", + "interval": "", + "legendFormat": "queue_max", + "refId": "D", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "state_checkpoint_committed_block_height{job=\"$job\"}", + "interval": "", + "legendFormat": "state_commit", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "state_checkpoint_queued_max_height{job=\"$job\"}", + "interval": "", + "legendFormat": "state_q_max", + "refId": "F", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Checkpoint Height - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:84", + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:85", + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 7 + }, + "hiddenSeries": false, + "id": 8, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "repeatDirection": "h", + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(checkpoint_verified_block_count[1m])", + "interval": "", + "legendFormat": "checkpoint verify rate [1m]", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "rate(state_checkpoint_committed_block_count[1m])", + "interval": "", + "legendFormat": "state commit rate [1m]", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(sync_downloaded_block_count[1m])", + "interval": "", + "legendFormat": "sync download rate [1m]", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(gossip_downloaded_block_count[1m])", + "interval": "", + "legendFormat": "gossip download rate [1m]", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Checkpoint Pipeline Throughput - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:252", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:253", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 7 + }, + "hiddenSeries": false, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "repeatIteration": 1633652714499, + "repeatPanelId": 8, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet-tmp", + "value": "zebrad-mainnet-tmp" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(checkpoint_verified_block_count[1m])", + "interval": "", + "legendFormat": "checkpoint verify rate [1m]", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "rate(state_checkpoint_committed_block_count[1m])", + "interval": "", + "legendFormat": "state commit rate [1m]", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(sync_downloaded_block_count[1m])", + "interval": "", + "legendFormat": "sync download rate [1m]", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(gossip_downloaded_block_count[1m])", + "interval": "", + "legendFormat": "gossip download rate [1m]", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Checkpoint Pipeline Throughput - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:252", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:253", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 7 + }, + "hiddenSeries": false, + "id": 13, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "repeatIteration": 1633652714499, + "repeatPanelId": 8, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet", + "value": "zebrad-testnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(checkpoint_verified_block_count[1m])", + "interval": "", + "legendFormat": "checkpoint verify rate [1m]", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "rate(state_checkpoint_committed_block_count[1m])", + "interval": "", + "legendFormat": "state commit rate [1m]", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(sync_downloaded_block_count[1m])", + "interval": "", + "legendFormat": "sync download rate [1m]", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(gossip_downloaded_block_count[1m])", + "interval": "", + "legendFormat": "gossip download rate [1m]", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Checkpoint Pipeline Throughput - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:252", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:253", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 7 + }, + "hiddenSeries": false, + "id": 14, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "repeatIteration": 1633652714499, + "repeatPanelId": 8, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet-tmp", + "value": "zebrad-testnet-tmp" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(checkpoint_verified_block_count[1m])", + "interval": "", + "legendFormat": "checkpoint verify rate [1m]", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "rate(state_checkpoint_committed_block_count[1m])", + "interval": "", + "legendFormat": "state commit rate [1m]", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(sync_downloaded_block_count[1m])", + "interval": "", + "legendFormat": "sync download rate [1m]", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(gossip_downloaded_block_count[1m])", + "interval": "", + "legendFormat": "gossip download rate [1m]", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Checkpoint Pipeline Throughput - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:252", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:253", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 14 + }, + "hiddenSeries": false, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "repeatDirection": "h", + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "checkpoint_queued_slots{job=\"$job\"}", + "interval": "", + "legendFormat": "checkpoint_queued_slots", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "state_checkpoint_queued_block_count{job=\"$job\"}", + "interval": "", + "legendFormat": "state_finalized_queued_block_count", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sync_prospective_tips_len{job=\"$job\"}", + "interval": "", + "legendFormat": "sync_prospective_tips_len", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sync_downloads_in_flight{job=\"$job\"}", + "interval": "", + "legendFormat": "sync_downloads_in_flight", + "refId": "D", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sync_pending_blocks_len{job=\"$job\"}", + "interval": "", + "legendFormat": "sync_pending_blocks_len", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(sync_cancelled_download_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "sync_cancelled_download_count[1m]", + "refId": "F", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(sync_cancelled_verify_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "sync_cancelled_verify_count[1m]", + "refId": "G", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(gossip_queued_block_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "gossip_queued_block_count[1m]", + "refId": "H", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Checkpoint Queues - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:337", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:338", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 14 + }, + "hiddenSeries": false, + "id": 15, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "repeatIteration": 1633652714499, + "repeatPanelId": 4, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet-tmp", + "value": "zebrad-mainnet-tmp" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "checkpoint_queued_slots{job=\"$job\"}", + "interval": "", + "legendFormat": "checkpoint_queued_slots", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "state_checkpoint_queued_block_count{job=\"$job\"}", + "interval": "", + "legendFormat": "state_finalized_queued_block_count", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sync_prospective_tips_len{job=\"$job\"}", + "interval": "", + "legendFormat": "sync_prospective_tips_len", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sync_downloads_in_flight{job=\"$job\"}", + "interval": "", + "legendFormat": "sync_downloads_in_flight", + "refId": "D", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sync_pending_blocks_len{job=\"$job\"}", + "interval": "", + "legendFormat": "sync_pending_blocks_len", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(sync_cancelled_download_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "sync_cancelled_download_count[1m]", + "refId": "F", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(sync_cancelled_verify_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "sync_cancelled_verify_count[1m]", + "refId": "G", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(gossip_queued_block_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "gossip_queued_block_count[1m]", + "refId": "H", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Checkpoint Queues - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:337", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:338", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 14 + }, + "hiddenSeries": false, + "id": 16, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "repeatIteration": 1633652714499, + "repeatPanelId": 4, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet", + "value": "zebrad-testnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "checkpoint_queued_slots{job=\"$job\"}", + "interval": "", + "legendFormat": "checkpoint_queued_slots", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "state_checkpoint_queued_block_count{job=\"$job\"}", + "interval": "", + "legendFormat": "state_finalized_queued_block_count", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sync_prospective_tips_len{job=\"$job\"}", + "interval": "", + "legendFormat": "sync_prospective_tips_len", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sync_downloads_in_flight{job=\"$job\"}", + "interval": "", + "legendFormat": "sync_downloads_in_flight", + "refId": "D", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sync_pending_blocks_len{job=\"$job\"}", + "interval": "", + "legendFormat": "sync_pending_blocks_len", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(sync_cancelled_download_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "sync_cancelled_download_count[1m]", + "refId": "F", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(sync_cancelled_verify_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "sync_cancelled_verify_count[1m]", + "refId": "G", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(gossip_queued_block_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "gossip_queued_block_count[1m]", + "refId": "H", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Checkpoint Queues - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:337", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:338", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 14 + }, + "hiddenSeries": false, + "id": 17, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "repeatIteration": 1633652714499, + "repeatPanelId": 4, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet-tmp", + "value": "zebrad-testnet-tmp" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "checkpoint_queued_slots{job=\"$job\"}", + "interval": "", + "legendFormat": "checkpoint_queued_slots", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "state_checkpoint_queued_block_count{job=\"$job\"}", + "interval": "", + "legendFormat": "state_finalized_queued_block_count", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sync_prospective_tips_len{job=\"$job\"}", + "interval": "", + "legendFormat": "sync_prospective_tips_len", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sync_downloads_in_flight{job=\"$job\"}", + "interval": "", + "legendFormat": "sync_downloads_in_flight", + "refId": "D", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sync_pending_blocks_len{job=\"$job\"}", + "interval": "", + "legendFormat": "sync_pending_blocks_len", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(sync_cancelled_download_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "sync_cancelled_download_count[1m]", + "refId": "F", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(sync_cancelled_verify_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "sync_cancelled_verify_count[1m]", + "refId": "G", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "rate(gossip_queued_block_count{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "gossip_queued_block_count[1m]", + "refId": "H", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Checkpoint Queues - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:337", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:338", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "5s", + "schemaVersion": 27, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "definition": "label_values(sync_prospective_tips_len, job)", + "description": null, + "error": null, + "hide": 0, + "includeAll": true, + "label": null, + "multi": true, + "name": "job", + "options": [], + "query": { + "query": "label_values(sync_prospective_tips_len, job)", + "refId": "Prometheus-Zebra-job-Variable-Query" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "checkpoint verification", + "uid": "o4LmN_OMk", + "version": 12 +} diff --git a/observability/grafana/dashboards/errors.json b/observability/grafana/dashboards/errors.json new file mode 100644 index 0000000..c5bdfde --- /dev/null +++ b/observability/grafana/dashboards/errors.json @@ -0,0 +1,469 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 6, + "iteration": 1616480577841, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.2.2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "scopedVars": { + "job": { + "selected": true, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "zebra_error_sapling_binding{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "zebra_sighash_error_sapling_spend{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "zebra_sighash_error_sprout_joinsplit{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Sighash Errors - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 0 + }, + "hiddenSeries": false, + "id": 3, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.2.2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatIteration": 1616480577841, + "repeatPanelId": 2, + "scopedVars": { + "job": { + "selected": true, + "text": "zebrad-mainnet-tmp-1", + "value": "zebrad-mainnet-tmp-1" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "zebra_error_sapling_binding{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "zebra_sighash_error_sapling_spend{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "zebra_sighash_error_sprout_joinsplit{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Sighash Errors - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 0 + }, + "hiddenSeries": false, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.2.2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatIteration": 1616480577841, + "repeatPanelId": 2, + "scopedVars": { + "job": { + "selected": true, + "text": "zebrad-testnet-tmp-1", + "value": "zebrad-testnet-tmp-1" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "zebra_error_sapling_binding{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "zebra_sighash_error_sapling_spend{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "zebra_sighash_error_sprout_joinsplit{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Sighash Errors - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "selected": true, + "tags": [], + "text": [ + "zebrad-mainnet", + "zebrad-mainnet-tmp-1", + "zebrad-testnet-tmp-1" + ], + "value": [ + "zebrad-mainnet", + "zebrad-mainnet-tmp-1", + "zebrad-testnet-tmp-1" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "definition": "label_values(zcash_net_peers, job)", + "hide": 0, + "includeAll": true, + "label": null, + "multi": true, + "name": "job", + "options": [], + "query": "label_values(zcash_net_peers, job)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "errors", + "uid": "IhbO11wGk", + "version": 4 +} diff --git a/observability/grafana/dashboards/mempool.json b/observability/grafana/dashboards/mempool.json new file mode 100644 index 0000000..606eb00 --- /dev/null +++ b/observability/grafana/dashboards/mempool.json @@ -0,0 +1,943 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "", + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 15, + "iteration": 1634239984015, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Transactions" + }, + "properties": [ + { + "id": "displayName", + "value": "transactions" + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "Serialized Bytes" + }, + "properties": [ + { + "id": "displayName", + "value": "serialized bytes" + } + ] + } + ] + }, + "fill": 1, + "fillGradient": 1, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.1.2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "seriesOverrides": [ + { + "$$hashKey": "object:232", + "alias": "transactions", + "yaxis": 1 + }, + { + "$$hashKey": "object:239", + "alias": "serialized bytes", + "yaxis": 2 + }, + { + "alias": "rejected serialized bytes", + "yaxis": 2 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "zcash_mempool_size_transactions{job=\"$job\"}", + "interval": "", + "legendFormat": "transactions", + "refId": "Transactions", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "zcash_mempool_size_bytes{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": " serialized bytes", + "refId": "Serialized Bytes", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "mempool_currently_queued_transactions{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "queued transactions", + "refId": "Queued Transactions", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Mempool Storage - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "none", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "decimals": null, + "format": "decbytes", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Transactions" + }, + "properties": [ + { + "id": "displayName", + "value": "transactions" + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "Serialized Bytes" + }, + "properties": [ + { + "id": "displayName", + "value": "serialized bytes" + } + ] + } + ] + }, + "fill": 1, + "fillGradient": 1, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 9 + }, + "hiddenSeries": false, + "id": 9, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.1.2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "$$hashKey": "object:232", + "alias": "transactions", + "yaxis": 1 + }, + { + "$$hashKey": "object:239", + "alias": "serialized bytes", + "yaxis": 2 + }, + { + "alias": "rejected serialized bytes", + "yaxis": 2 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "mempool_rejected_transaction_ids{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "rejected transactions", + "refId": "Rejected Transactions IDs", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "mempool_rejected_transaction_ids_bytes{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "rejected serialized bytes", + "refId": "Rejected Serialized TXID Bytes", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Mempool Rejected Storage - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "none", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "decimals": null, + "format": "decbytes", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 18 + }, + "hiddenSeries": false, + "id": 7, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.1.2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "rate(sync_downloaded_block_count{job=\"$job\"}[1m])", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "sync download", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "rate(zcash_chain_verified_block_total{job=\"$job\"}[1m])", + "interval": "", + "legendFormat": "state commit", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Block Rates", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:80", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:81", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "description": "", + "fill": 1, + "fillGradient": 1, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 26 + }, + "hiddenSeries": false, + "id": 8, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.1.2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "$$hashKey": "object:232", + "alias": "transactions", + "yaxis": 1 + }, + { + "$$hashKey": "object:239", + "alias": "serialized bytes", + "yaxis": 2 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "mempool_queued_transactions_total{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "queued", + "refId": "Queued", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "mempool_downloaded_transactions_total{job=\"$job\"}", + "interval": "", + "legendFormat": "downloaded", + "refId": "Downloaded", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "mempool_pushed_transactions_total{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "pushed", + "refId": "Pushed", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "mempool_verified_transactions_total{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "verified", + "refId": "Verified", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "mempool_cancelled_verify_tasks_total{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "cancelled", + "refId": "Cancelled", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "mempool_failed_verify_tasks_total{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "failed - {{reason}}", + "refId": "Failed", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "mempool_gossiped_transactions_total{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "gossiped", + "refId": "Gossiped", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Transaction Downloader and Verifier, Gossiper - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "none", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "decimals": null, + "format": "decbytes", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 35 + }, + "hiddenSeries": false, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.1.2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "rate(mempool_downloaded_transactions_total{job=\"$job\"}[1m]) * 60", + "interval": "", + "legendFormat": "downloaded per min", + "refId": "Downloaded", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "rate(mempool_verified_transactions_total{job=\"$job\"}[1m]) * 60", + "interval": "", + "legendFormat": "verified per min", + "refId": "Verified", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "rate(mempool_queued_transactions_total{job=\"$job\"}[1m]) * 60", + "interval": "", + "legendFormat": "queued per min", + "refId": "Queued", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Transaction Downloader and Verifier (Rates) - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:1174", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:1175", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "description": "", + "fill": 1, + "fillGradient": 1, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 42 + }, + "hiddenSeries": false, + "id": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.1.2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "$$hashKey": "object:232", + "alias": "transactions", + "yaxis": 1 + }, + { + "$$hashKey": "object:239", + "alias": "serialized bytes", + "yaxis": 2 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum by (version) (mempool_downloaded_transactions_total{job=\"$job\"})", + "hide": false, + "interval": "", + "legendFormat": "{{version}}", + "refId": "Downloaded", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Downloaded Txs by Version - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "none", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "decimals": null, + "format": "decbytes", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "5s", + "schemaVersion": 30, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": null, + "definition": "label_values(zcash_net_in_bytes_total, job)", + "description": null, + "error": null, + "hide": 0, + "includeAll": true, + "label": null, + "multi": true, + "name": "job", + "options": [], + "query": { + "query": "label_values(zcash_net_in_bytes_total, job)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "mempool", + "uid": "wVXGE6v7z", + "version": 8 +} diff --git a/observability/grafana/dashboards/network_health.json b/observability/grafana/dashboards/network_health.json new file mode 100644 index 0000000..cf3035f --- /dev/null +++ b/observability/grafana/dashboards/network_health.json @@ -0,0 +1,2186 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 4, + "iteration": 1639361606831, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "repeatDirection": "h", + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(zcash_net_in_bytes_total{job=\"$job\"}[1m]))", + "hide": false, + "interval": "", + "legendFormat": "bytes read [1m]", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sum(rate(zcash_net_out_bytes_total{job=\"$job\"}[1m]))", + "interval": "", + "legendFormat": "bytes written [1m]", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "bytes - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:93", + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:94", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 6, + "x": 6, + "y": 0 + }, + "hiddenSeries": false, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "repeatIteration": 1639361606831, + "repeatPanelId": 2, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet-tmp", + "value": "zebrad-mainnet-tmp" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(zcash_net_in_bytes_total{job=\"$job\"}[1m]))", + "hide": false, + "interval": "", + "legendFormat": "bytes read [1m]", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sum(rate(zcash_net_out_bytes_total{job=\"$job\"}[1m]))", + "interval": "", + "legendFormat": "bytes written [1m]", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "bytes - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:93", + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:94", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 6, + "x": 12, + "y": 0 + }, + "hiddenSeries": false, + "id": 13, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "repeatIteration": 1639361606831, + "repeatPanelId": 2, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet", + "value": "zebrad-testnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(zcash_net_in_bytes_total{job=\"$job\"}[1m]))", + "hide": false, + "interval": "", + "legendFormat": "bytes read [1m]", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sum(rate(zcash_net_out_bytes_total{job=\"$job\"}[1m]))", + "interval": "", + "legendFormat": "bytes written [1m]", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "bytes - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:93", + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:94", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 0 + }, + "hiddenSeries": false, + "id": 14, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "repeatIteration": 1639361606831, + "repeatPanelId": 2, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet-tmp", + "value": "zebrad-testnet-tmp" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(zcash_net_in_bytes_total{job=\"$job\"}[1m]))", + "hide": false, + "interval": "", + "legendFormat": "bytes read [1m]", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "sum(rate(zcash_net_out_bytes_total{job=\"$job\"}[1m]))", + "interval": "", + "legendFormat": "bytes written [1m]", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "bytes - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:93", + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:94", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 6 + }, + "hiddenSeries": false, + "id": 6, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "repeatDirection": "h", + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "zcash_net_peers{job=\"$job\"}", + "interval": "", + "legendFormat": "total peers", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "pool_num_ready{job=\"$job\"}", + "interval": "", + "legendFormat": "ready peers", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "pool_num_unready{job=\"$job\"}", + "interval": "", + "legendFormat": "unready peers", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "peer readiness - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 6, + "x": 6, + "y": 6 + }, + "hiddenSeries": false, + "id": 15, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "repeatIteration": 1639361606831, + "repeatPanelId": 6, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet-tmp", + "value": "zebrad-mainnet-tmp" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "zcash_net_peers{job=\"$job\"}", + "interval": "", + "legendFormat": "total peers", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "pool_num_ready{job=\"$job\"}", + "interval": "", + "legendFormat": "ready peers", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "pool_num_unready{job=\"$job\"}", + "interval": "", + "legendFormat": "unready peers", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "peer readiness - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 6, + "x": 12, + "y": 6 + }, + "hiddenSeries": false, + "id": 16, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "repeatIteration": 1639361606831, + "repeatPanelId": 6, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet", + "value": "zebrad-testnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "zcash_net_peers{job=\"$job\"}", + "interval": "", + "legendFormat": "total peers", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "pool_num_ready{job=\"$job\"}", + "interval": "", + "legendFormat": "ready peers", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "pool_num_unready{job=\"$job\"}", + "interval": "", + "legendFormat": "unready peers", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "peer readiness - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 6 + }, + "hiddenSeries": false, + "id": 17, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatDirection": "h", + "repeatIteration": 1639361606831, + "repeatPanelId": 6, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet-tmp", + "value": "zebrad-testnet-tmp" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "zcash_net_peers{job=\"$job\"}", + "interval": "", + "legendFormat": "total peers", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "pool_num_ready{job=\"$job\"}", + "interval": "", + "legendFormat": "ready peers", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "pool_num_unready{job=\"$job\"}", + "interval": "", + "legendFormat": "unready peers", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "peer readiness - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 12 + }, + "hiddenSeries": false, + "id": 7, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "candidate_set_disconnected{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "recently stopped peers", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_failed{job=\"$job\"}", + "interval": "", + "legendFormat": "failed candidates", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_gossiped{job=\"$job\"}", + "interval": "", + "legendFormat": "never attempted candidates", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_pending{job=\"$job\"}", + "interval": "", + "legendFormat": "connection attempt pending", + "refId": "D", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_responded{job=\"$job\"}", + "interval": "", + "legendFormat": "recent peers", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_recently_live{job=\"$job\"}", + "interval": "", + "legendFormat": "recently live peers", + "refId": "F", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "candidate set - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 6, + "x": 6, + "y": 12 + }, + "hiddenSeries": false, + "id": 18, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatIteration": 1639361606831, + "repeatPanelId": 7, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet-tmp", + "value": "zebrad-mainnet-tmp" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "candidate_set_disconnected{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "recently stopped peers", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_failed{job=\"$job\"}", + "interval": "", + "legendFormat": "failed candidates", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_gossiped{job=\"$job\"}", + "interval": "", + "legendFormat": "never attempted candidates", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_pending{job=\"$job\"}", + "interval": "", + "legendFormat": "connection attempt pending", + "refId": "D", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_responded{job=\"$job\"}", + "interval": "", + "legendFormat": "recent peers", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_recently_live{job=\"$job\"}", + "interval": "", + "legendFormat": "recently live peers", + "refId": "F", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "candidate set - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 6, + "x": 12, + "y": 12 + }, + "hiddenSeries": false, + "id": 19, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatIteration": 1639361606831, + "repeatPanelId": 7, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet", + "value": "zebrad-testnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "candidate_set_disconnected{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "recently stopped peers", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_failed{job=\"$job\"}", + "interval": "", + "legendFormat": "failed candidates", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_gossiped{job=\"$job\"}", + "interval": "", + "legendFormat": "never attempted candidates", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_pending{job=\"$job\"}", + "interval": "", + "legendFormat": "connection attempt pending", + "refId": "D", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_responded{job=\"$job\"}", + "interval": "", + "legendFormat": "recent peers", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_recently_live{job=\"$job\"}", + "interval": "", + "legendFormat": "recently live peers", + "refId": "F", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "candidate set - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 12 + }, + "hiddenSeries": false, + "id": 20, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatIteration": 1639361606831, + "repeatPanelId": 7, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet-tmp", + "value": "zebrad-testnet-tmp" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "candidate_set_disconnected{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "recently stopped peers", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_failed{job=\"$job\"}", + "interval": "", + "legendFormat": "failed candidates", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_gossiped{job=\"$job\"}", + "interval": "", + "legendFormat": "never attempted candidates", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_pending{job=\"$job\"}", + "interval": "", + "legendFormat": "connection attempt pending", + "refId": "D", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_responded{job=\"$job\"}", + "interval": "", + "legendFormat": "recent peers", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "expr": "candidate_set_recently_live{job=\"$job\"}", + "interval": "", + "legendFormat": "recently live peers", + "refId": "F", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "candidate set - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 5, + "w": 6, + "x": 0, + "y": 18 + }, + "hiddenSeries": false, + "id": 11, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": false, + "expr": "sum by(command) (zebra_net_connection_state{job=\"$job\"})", + "hide": false, + "interval": "", + "legendFormat": "", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "connection state - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:76", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:77", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 5, + "w": 6, + "x": 6, + "y": 18 + }, + "hiddenSeries": false, + "id": 21, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatIteration": 1639361606831, + "repeatPanelId": 11, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet-tmp", + "value": "zebrad-mainnet-tmp" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": false, + "expr": "sum by(command) (zebra_net_connection_state{job=\"$job\"})", + "hide": false, + "interval": "", + "legendFormat": "", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "connection state - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:76", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:77", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 5, + "w": 6, + "x": 12, + "y": 18 + }, + "hiddenSeries": false, + "id": 22, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatIteration": 1639361606831, + "repeatPanelId": 11, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet", + "value": "zebrad-testnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": false, + "expr": "sum by(command) (zebra_net_connection_state{job=\"$job\"})", + "hide": false, + "interval": "", + "legendFormat": "", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "connection state - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:76", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:77", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 5, + "w": 6, + "x": 18, + "y": 18 + }, + "hiddenSeries": false, + "id": 23, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatIteration": 1639361606831, + "repeatPanelId": 11, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet-tmp", + "value": "zebrad-testnet-tmp" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": false, + "expr": "sum by(command) (zebra_net_connection_state{job=\"$job\"})", + "hide": false, + "interval": "", + "legendFormat": "", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "connection state - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:76", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:77", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "5s", + "schemaVersion": 27, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "definition": "label_values(zcash_net_in_bytes_total, job)", + "description": null, + "error": null, + "hide": 0, + "includeAll": true, + "label": null, + "multi": true, + "name": "job", + "options": [], + "query": { + "query": "label_values(zcash_net_in_bytes_total, job)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s" + ] + }, + "timezone": "", + "title": "network health", + "uid": "320aS_dMk", + "version": 6 +} diff --git a/observability/grafana/dashboards/network_messages.json b/observability/grafana/dashboards/network_messages.json new file mode 100644 index 0000000..5b91950 --- /dev/null +++ b/observability/grafana/dashboards/network_messages.json @@ -0,0 +1,868 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 6, + "iteration": 1639360549666, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "scopedVars": { + "job": { + "selected": true, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum by (command) (zebra_net_in_requests{job=\"$job\"})", + "interval": "", + "legendFormat": "Req::{{command}}", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sum by (command) (zebra_net_out_responses{job=\"$job\"})", + "hide": false, + "interval": "", + "legendFormat": "Rsp::{{command}}", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "inbound requests & responses - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 9 + }, + "hiddenSeries": false, + "id": 13, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "scopedVars": { + "job": { + "selected": true, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum by (command) (zebra_net_out_requests{job=\"$job\"})", + "interval": "", + "legendFormat": "Req::{{command}}", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sum by (command) (zebra_net_in_responses{job=\"$job\"})", + "hide": false, + "interval": "", + "legendFormat": "Rsp::{{command}}", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "outbound requests & responses - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 18 + }, + "hiddenSeries": false, + "id": 14, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum by (command) (zebra_net_out_requests_canceled{job=\"$job\"})", + "interval": "", + "legendFormat": "Req::{{command}}", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "canceled outbound requests - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 27 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "scopedVars": { + "job": { + "selected": true, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum by (command) (zcash_net_in_messages{job=\"$job\"})", + "interval": "", + "legendFormat": "{{command}}", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "inbound message types - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 36 + }, + "hiddenSeries": false, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "scopedVars": { + "job": { + "selected": true, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum by (command) (zcash_net_out_messages{job=\"$job\"})", + "interval": "", + "legendFormat": "{{command}}", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "outbound message types - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 45 + }, + "hiddenSeries": false, + "id": 7, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "scopedVars": { + "job": { + "selected": true, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum by (addr) (zcash_net_in_messages{job=\"$job\"})", + "interval": "", + "legendFormat": "{{command}}", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "inbound message peers - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 54 + }, + "hiddenSeries": false, + "id": 11, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "scopedVars": { + "job": { + "selected": true, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum by (addr) (zcash_net_out_messages{job=\"$job\"})", + "interval": "", + "legendFormat": "{{command}}", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "outbound message peers - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "schemaVersion": 27, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "selected": false, + "text": [ + "zebrad-mainnet" + ], + "value": [ + "zebrad-mainnet" + ] + }, + "datasource": null, + "definition": "label_values(zcash_net_in_bytes_total, job)", + "description": null, + "error": null, + "hide": 0, + "includeAll": true, + "label": null, + "multi": true, + "name": "job", + "options": [], + "query": { + "query": "label_values(zcash_net_in_bytes_total, job)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "network messages", + "uid": "YQ3yxiVnk", + "version": 9 +} diff --git a/observability/grafana/dashboards/peers.json b/observability/grafana/dashboards/peers.json new file mode 100644 index 0000000..5a7574d --- /dev/null +++ b/observability/grafana/dashboards/peers.json @@ -0,0 +1,620 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": null, + "graphTooltip": 0, + "id": 3, + "iteration": 1635278363376, + "links": [], + "liveNow": false, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "hideEmpty": false, + "hideZero": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.2.0", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum by (remote_version, seed) (label_replace(zcash_net_peers_initial{job=\"$job\",seed=~\"$seed\"}, \"seed\", \"$1\", \"seed\", \"(.*):1?8233\") * on(remote_ip) group_left(remote_version) (count_values by (remote_ip) (\"remote_version\", zcash_net_peers_version_connected{job=\"$job\"})))", + "instant": false, + "interval": "", + "legendFormat": "{{remote_version}} - {{seed}}", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Compatible Seed Peers - $job", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 9 + }, + "hiddenSeries": false, + "id": 11, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.2.0", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum by (remote_version, seed) (label_replace(zcash_net_peers_initial{job=\"$job\",seed=~\"$seed\"}, \"seed\", \"$1\", \"seed\", \"(.*):1?8233\") * on(remote_ip) group_left(remote_version) (count_values by (remote_ip) (\"remote_version\", zcash_net_peers_version_obsolete{job=\"$job\"})))", + "instant": false, + "interval": "", + "legendFormat": "{{remote_version}} - {{seed}}", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Obsolete Seed Peers - $job", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 17 + }, + "hiddenSeries": false, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.2.0", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum by (remote_version) (zcash_net_peers_connected{job=\"$job\"})", + "interval": "", + "legendFormat": "{{remote_version}}", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Compatible Peers - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 25 + }, + "hiddenSeries": false, + "id": 7, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.2.0", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum by (remote_version) (zcash_net_peers_obsolete{job=\"$job\"})", + "interval": "", + "legendFormat": "{{remote_version}}", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Obsolete Peers - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 33 + }, + "hiddenSeries": false, + "id": 13, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.2.0", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum by (user_agent) (zcash_net_peers_connected{job=\"$job\"})", + "interval": "", + "legendFormat": "{{user_agent}}", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Peer User Agents - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "5s", + "schemaVersion": 31, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": null, + "definition": "label_values(zcash_net_in_bytes_total, job)", + "description": null, + "error": null, + "hide": 0, + "includeAll": true, + "label": null, + "multi": true, + "name": "job", + "options": [], + "query": { + "query": "label_values(zcash_net_in_bytes_total, job)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".+", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": null, + "definition": "label_values(zcash_net_peers_initial, seed)", + "description": null, + "error": null, + "hide": 0, + "includeAll": true, + "label": null, + "multi": true, + "name": "seed", + "options": [], + "query": { + "query": "label_values(zcash_net_peers_initial, seed)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "peers", + "uid": "S29TgUH7k", + "version": 6 +} diff --git a/observability/grafana/dashboards/rocksdb.json b/observability/grafana/dashboards/rocksdb.json new file mode 100644 index 0000000..c93a2f1 --- /dev/null +++ b/observability/grafana/dashboards/rocksdb.json @@ -0,0 +1,925 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "RocksDB database metrics for Zebra", + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "zebra_state_rocksdb_total_disk_size_bytes{job=~\"$job\"}", + "legendFormat": "Total Disk Size", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "zebra_state_rocksdb_live_data_size_bytes{job=~\"$job\"}", + "legendFormat": "Live Data Size", + "refId": "B" + } + ], + "title": "Database Disk Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "zebra_state_rocksdb_total_memory_size_bytes{job=~\"$job\"}", + "legendFormat": "Total Memory Size", + "refId": "A" + } + ], + "title": "Database Memory Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "topk(10, zebra_state_rocksdb_cf_disk_size_bytes{job=~\"$job\"})", + "legendFormat": "{{cf}}", + "refId": "A" + } + ], + "title": "Top 10 Column Families by Disk Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "topk(10, zebra_state_rocksdb_cf_memory_size_bytes{job=~\"$job\"})", + "legendFormat": "{{cf}}", + "refId": "A" + } + ], + "title": "Top 10 Column Families by Memory Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "noValue": "NO DATA", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 18 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "zebra_state_rocksdb_total_disk_size_bytes{job=~\"$job\"}", + "legendFormat": "Total Disk", + "refId": "A" + } + ], + "title": "Total Disk Size", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 18 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "noValue": "NO DATA", + "color": { "mode": "thresholds" }, + "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] }, + "unit": "bytes" + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "zebra_state_rocksdb_live_data_size_bytes{job=~\"$job\"}", + "legendFormat": "Live Data", + "refId": "A" + } + ], + "title": "Live Data Size", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1000000000 + }, + { + "color": "red", + "value": 2000000000 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 18 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "zebra_state_rocksdb_total_memory_size_bytes{job=~\"$job\"}", + "legendFormat": "Memory", + "refId": "A" + } + ], + "title": "Total Memory Size", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "noValue": "NO DATA", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 0.7 + }, + { + "color": "green", + "value": 0.9 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 18 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "zebra_state_rocksdb_live_data_size_bytes{job=~\"$job\"} / zebra_state_rocksdb_total_disk_size_bytes{job=~\"$job\"}", + "legendFormat": "Compaction Efficiency", + "refId": "A" + } + ], + "title": "Compaction Efficiency", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 22 }, + "id": 100, + "panels": [], + "title": "I/O Performance", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "RocksDB batch commit latency percentiles (p95, p99)", + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "line" } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 0.1 }, + { "color": "red", "value": 0.5 } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 23 }, + "id": 10, + "options": { + "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "desc" } + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "zebra_state_rocksdb_batch_commit_duration_seconds{job=~\"$job\", quantile=\"0.95\"}", + "legendFormat": "p95", + "refId": "A" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "zebra_state_rocksdb_batch_commit_duration_seconds{job=~\"$job\", quantile=\"0.99\"}", + "legendFormat": "p99", + "refId": "B" + } + ], + "title": "Batch Commit Latency", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Block cache memory usage", + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 23 }, + "id": 11, + "options": { + "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "desc" } + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "zebra_state_rocksdb_block_cache_usage_bytes{job=~\"$job\"}", + "legendFormat": "Block Cache", + "refId": "A" + } + ], + "title": "Block Cache Usage", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 31 }, + "id": 101, + "panels": [], + "title": "Compaction", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Compaction status - pending bytes and running compactions", + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { "id": "byName", "options": "Running Compactions" }, + "properties": [ + { "id": "custom.axisPlacement", "value": "right" }, + { "id": "unit", "value": "none" } + ] + } + ] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 32 }, + "id": 12, + "options": { + "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "desc" } + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "zebra_state_rocksdb_compaction_pending_bytes{job=~\"$job\"}", + "legendFormat": "Pending Bytes", + "refId": "A" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "zebra_state_rocksdb_compaction_running{job=~\"$job\"}", + "legendFormat": "Running Compactions", + "refId": "B" + } + ], + "title": "Compaction Status", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Number of SST files at each RocksDB level (L0-L6)", + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 32 }, + "id": 13, + "options": { + "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "desc" } + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "zebra_state_rocksdb_num_files_at_level{job=~\"$job\"}", + "legendFormat": "Level {{level}}", + "refId": "A" + } + ], + "title": "SST Files by Level", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 27, + "style": "dark", + "tags": ["rocksdb", "database"], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "selected": true, + "text": ["All"], + "value": ["$__all"] + }, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "definition": "label_values(zebra_state_rocksdb_total_disk_size_bytes, job)", + "description": null, + "error": null, + "hide": 0, + "includeAll": true, + "label": null, + "multi": true, + "name": "job", + "options": [], + "query": { + "query": "label_values(zebra_state_rocksdb_total_disk_size_bytes, job)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "RocksDB Database", + "uid": "zebra-rocksdb", + "version": 1 +} diff --git a/observability/grafana/dashboards/rpc_metrics.json b/observability/grafana/dashboards/rpc_metrics.json new file mode 100644 index 0000000..a433d11 --- /dev/null +++ b/observability/grafana/dashboards/rpc_metrics.json @@ -0,0 +1,820 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "JSON-RPC endpoint metrics - request rates, latency, and errors", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "red", + "value": 100 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "rpc_active_requests", + "legendFormat": "Active Requests", + "refId": "A" + } + ], + "title": "Active Requests", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 0 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sum(rate(rpc_requests_total[5m]))", + "legendFormat": "Requests/s", + "refId": "A" + } + ], + "title": "Request Rate (5m)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.05 + }, + { + "color": "red", + "value": 0.1 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 0 + }, + "id": 3, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sum(rate(rpc_requests_total{status=\"error\"}[5m])) / sum(rate(rpc_requests_total[5m]))", + "legendFormat": "Error Rate", + "refId": "A" + } + ], + "title": "Error Rate (5m)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.5 + }, + { + "color": "red", + "value": 2 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 0 + }, + "id": 4, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "histogram_quantile(0.99, sum(rate(rpc_request_duration_seconds_bucket[5m])) by (le))", + "legendFormat": "p99 Latency", + "refId": "A" + } + ], + "title": "p99 Latency (5m)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "req/s", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 4 + }, + "id": 5, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sum(rate(rpc_requests_total[5m])) by (method)", + "legendFormat": "{{ method }}", + "refId": "A" + } + ], + "title": "Request Rate by Method", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "seconds", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 4 + }, + "id": 6, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "histogram_quantile(0.50, sum(rate(rpc_request_duration_seconds_bucket[5m])) by (le, method))", + "legendFormat": "{{ method }} p50", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "histogram_quantile(0.95, sum(rate(rpc_request_duration_seconds_bucket[5m])) by (le, method))", + "legendFormat": "{{ method }} p95", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "histogram_quantile(0.99, sum(rate(rpc_request_duration_seconds_bucket[5m])) by (le, method))", + "legendFormat": "{{ method }} p99", + "refId": "C" + } + ], + "title": "Request Latency Percentiles by Method", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "errors/s", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 12 + }, + "id": 7, + "options": { + "legend": { + "calcs": ["sum"], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sum(rate(rpc_errors_total[5m])) by (method, error_code)", + "legendFormat": "{{ method }} ({{ error_code }})", + "refId": "A" + } + ], + "title": "Errors by Method and Code", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 12 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "rpc_active_requests", + "legendFormat": "Active Requests", + "refId": "A" + } + ], + "title": "Active Requests Over Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Rate" + }, + "properties": [ + { + "id": "unit", + "value": "reqps" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "p50" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "p99" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 9, + "options": { + "footer": { + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Rate" + } + ] + }, + "pluginVersion": "9.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sum(rate(rpc_requests_total[5m])) by (method)", + "format": "table", + "instant": true, + "legendFormat": "", + "refId": "Rate" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "histogram_quantile(0.50, sum(rate(rpc_request_duration_seconds_bucket[5m])) by (le, method))", + "format": "table", + "instant": true, + "legendFormat": "", + "refId": "p50" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "histogram_quantile(0.99, sum(rate(rpc_request_duration_seconds_bucket[5m])) by (le, method))", + "format": "table", + "instant": true, + "legendFormat": "", + "refId": "p99" + } + ], + "title": "Method Statistics", + "transformations": [ + { + "id": "seriesToColumns", + "options": { + "byField": "method" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Time 1": true, + "Time 2": true, + "Time 3": true + }, + "indexByName": {}, + "renameByName": { + "Value #Rate": "Rate", + "Value #p50": "p50", + "Value #p99": "p99", + "method": "Method" + } + } + } + ], + "type": "table" + } + ], + "refresh": "10s", + "schemaVersion": 37, + "style": "dark", + "tags": ["zebra", "zcash", "rpc", "json-rpc"], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Zebra RPC Metrics", + "uid": "zebra-rpc-metrics", + "version": 1, + "weekStart": "" +} diff --git a/observability/grafana/dashboards/rpc_tracing.json b/observability/grafana/dashboards/rpc_tracing.json new file mode 100644 index 0000000..8e6585b --- /dev/null +++ b/observability/grafana/dashboards/rpc_tracing.json @@ -0,0 +1,639 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "title": "RPC Request Rate by Method", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sum by (rpc_method) (rate(traces_spanmetrics_calls_total{service=\"zebra\", span_name=\"rpc_request\"}[1m]))", + "legendFormat": "{{rpc_method}}", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 0.01 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "title": "RPC Error Rate by Method", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sum by (rpc_method) (rate(traces_spanmetrics_calls_total{service=\"zebra\", span_name=\"rpc_request\", otel_status_code=\"ERROR\"}[1m])) / sum by (rpc_method) (rate(traces_spanmetrics_calls_total{service=\"zebra\", span_name=\"rpc_request\"}[1m]))", + "legendFormat": "{{rpc_method}}", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "title": "RPC Latency P50 by Method", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "histogram_quantile(0.50, sum by (rpc_method, le) (rate(traces_spanmetrics_latency_bucket{service=\"zebra\", span_name=\"rpc_request\"}[1m])))", + "legendFormat": "{{rpc_method}}", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "title": "RPC Latency P99 by Method", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "histogram_quantile(0.99, sum by (rpc_method, le) (rate(traces_spanmetrics_latency_bucket{service=\"zebra\", span_name=\"rpc_request\"}[1m])))", + "legendFormat": "{{rpc_method}}", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "title": "Total RPC Requests", + "type": "stat", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sum(increase(traces_spanmetrics_calls_total{service=\"zebra\", span_name=\"rpc_request\"}[1h]))", + "legendFormat": "Total", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 16 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "title": "Total RPC Errors", + "type": "stat", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sum(increase(traces_spanmetrics_calls_total{service=\"zebra\", span_name=\"rpc_request\", otel_status_code=\"ERROR\"}[1h]))", + "legendFormat": "Errors", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "red", + "value": 0.5 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 12, + "y": 16 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "title": "Average Latency", + "type": "stat", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sum(rate(traces_spanmetrics_latency_sum{service=\"zebra\", span_name=\"rpc_request\"}[5m])) / sum(rate(traces_spanmetrics_latency_count{service=\"zebra\", span_name=\"rpc_request\"}[5m]))", + "legendFormat": "Avg", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.5 + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 18, + "y": 16 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "title": "P99 Latency", + "type": "stat", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "histogram_quantile(0.99, sum(rate(traces_spanmetrics_latency_bucket{service=\"zebra\", span_name=\"rpc_request\"}[5m])) by (le))", + "legendFormat": "P99", + "refId": "A" + } + ] + } + ], + "schemaVersion": 39, + "tags": ["zebra", "rpc", "tracing"], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "RPC Tracing", + "uid": "zebra-rpc-tracing", + "version": 1, + "weekStart": "" +} diff --git a/observability/grafana/dashboards/syncer.json b/observability/grafana/dashboards/syncer.json new file mode 100644 index 0000000..9441540 --- /dev/null +++ b/observability/grafana/dashboards/syncer.json @@ -0,0 +1,1319 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "", + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 5, + "iteration": 1633496321106, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 200, + "panels": [], + "title": "Key Metrics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "description": "Current blockchain height", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 1 + }, + "id": 201, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "state_finalized_block_height", + "legendFormat": "Height", + "refId": "A" + } + ], + "title": "Block Height", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "description": "Total blocks downloaded during sync", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 1 + }, + "id": 202, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sync_downloaded_block_count", + "legendFormat": "Downloaded", + "refId": "A" + } + ], + "title": "Downloaded Blocks", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "description": "Total blocks verified during sync", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 1 + }, + "id": 203, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sync_verified_block_count", + "legendFormat": "Verified", + "refId": "A" + } + ], + "title": "Verified Blocks", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "description": "Block downloads currently in progress", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 100 + }, + { + "color": "red", + "value": 200 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 1 + }, + "id": 204, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sync_downloads_in_flight", + "legendFormat": "In-Flight", + "refId": "A" + } + ], + "title": "In-Flight Downloads", + "type": "stat" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 5 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sync_obtain_queued_hash_count{job=\"$job\"}", + "interval": "", + "legendFormat": "obtain tips", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sync_extend_queued_hash_count{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "extend tips", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Sync Queued Downloads - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 5 + }, + "hiddenSeries": false, + "id": 8, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatIteration": 1633496321106, + "repeatPanelId": 2, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet", + "value": "zebrad-testnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sync_obtain_queued_hash_count{job=\"$job\"}", + "interval": "", + "legendFormat": "obtain tips", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sync_extend_queued_hash_count{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "extend tips", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Sync Queued Downloads - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "cards": { + "cardPadding": null, + "cardRound": null + }, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateOranges", + "exponent": 0.5, + "mode": "spectrum" + }, + "dataFormat": "timeseries", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 14 + }, + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "id": 4, + "legend": { + "show": false + }, + "pluginVersion": "7.5.7", + "repeat": "job", + "reverseYBuckets": false, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "sync_obtain_response_hash_count{job=\"$job\"}", + "format": "heatmap", + "interval": "", + "legendFormat": "obtain tips", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sync_extend_response_hash_count{job=\"$job\"}", + "format": "heatmap", + "hide": false, + "interval": "", + "legendFormat": "extend tips", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Sync Peer Responses - $job", + "tooltip": { + "show": true, + "showHistogram": false + }, + "type": "heatmap", + "xAxis": { + "show": true + }, + "xBucketNumber": null, + "xBucketSize": null, + "yAxis": { + "decimals": null, + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true, + "splitFactor": null + }, + "yBucketBound": "auto", + "yBucketNumber": null, + "yBucketSize": null + }, + { + "cards": { + "cardPadding": null, + "cardRound": null + }, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateOranges", + "exponent": 0.5, + "mode": "spectrum" + }, + "dataFormat": "timeseries", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 14 + }, + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "id": 9, + "legend": { + "show": false + }, + "pluginVersion": "7.5.7", + "repeatIteration": 1633496321106, + "repeatPanelId": 4, + "reverseYBuckets": false, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet", + "value": "zebrad-testnet" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "sync_obtain_response_hash_count{job=\"$job\"}", + "format": "heatmap", + "interval": "", + "legendFormat": "obtain tips", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sync_extend_response_hash_count{job=\"$job\"}", + "format": "heatmap", + "hide": false, + "interval": "", + "legendFormat": "extend tips", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Sync Peer Responses - $job", + "tooltip": { + "show": true, + "showHistogram": false + }, + "type": "heatmap", + "xAxis": { + "show": true + }, + "xBucketNumber": null, + "xBucketSize": null, + "yAxis": { + "decimals": null, + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true, + "splitFactor": null + }, + "yBucketBound": "auto", + "yBucketNumber": null, + "yBucketSize": null + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 23 + }, + "hiddenSeries": false, + "id": 7, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": "job", + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-mainnet", + "value": "zebrad-mainnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sync_cancelled_download_count{job=\"$job\"}", + "interval": "", + "legendFormat": "cancelled downloads", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sync_cancelled_verify_count{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "cancelled verifications", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sync_downloads_in_flight{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "in-flight downloads", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sync_prospective_tips_len{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "prospective tips", + "refId": "D", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sync_downloaded_block_count{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "downloaded blocks", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sync_verified_block_count{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "verified blocks", + "refId": "F", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Sync Status - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 23 + }, + "hiddenSeries": false, + "id": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeatIteration": 1633496321106, + "repeatPanelId": 7, + "scopedVars": { + "job": { + "selected": false, + "text": "zebrad-testnet", + "value": "zebrad-testnet" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sync_cancelled_download_count{job=\"$job\"}", + "interval": "", + "legendFormat": "cancelled downloads", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sync_cancelled_verify_count{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "cancelled verifications", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sync_downloads_in_flight{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "in-flight downloads", + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sync_prospective_tips_len{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "prospective tips", + "refId": "D", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sync_downloaded_block_count{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "downloaded blocks", + "refId": "E", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "sync_verified_block_count{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "verified blocks", + "refId": "F", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Sync Status - $job", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:65", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:66", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 100, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sync_stage_duration_seconds{quantile=\"0.5\"}", + "legendFormat": "{{stage}} p50", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sync_stage_duration_seconds{quantile=\"0.95\"}", + "legendFormat": "{{stage}} p95", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sync_stage_duration_seconds{quantile=\"0.99\"}", + "legendFormat": "{{stage}} p99", + "refId": "C" + } + ], + "title": "Sync Stage Duration Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 101, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sync_block_download_duration_seconds{quantile=\"0.95\"}", + "legendFormat": "download {{result}} p95", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sync_block_verify_duration_seconds{quantile=\"0.95\"}", + "legendFormat": "verify {{result}} p95", + "refId": "B" + } + ], + "title": "Block Download/Verify Duration (p95)", + "type": "timeseries" + } + ], + "refresh": "5s", + "schemaVersion": 27, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": null, + "definition": "label_values(zcash_net_in_bytes_total, job)", + "description": null, + "error": null, + "hide": 0, + "includeAll": true, + "label": null, + "multi": true, + "name": "job", + "options": [], + "query": { + "query": "label_values(zcash_net_in_bytes_total, job)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "syncer", + "uid": "Sl3h19Gnk", + "version": 11 +} diff --git a/observability/grafana/dashboards/transaction-verification.json b/observability/grafana/dashboards/transaction-verification.json new file mode 100644 index 0000000..21fb739 --- /dev/null +++ b/observability/grafana/dashboards/transaction-verification.json @@ -0,0 +1,978 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 6, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.5.5", + "targets": [ + { + "exemplar": true, + "expr": "zcash_chain_verified_block_height{}", + "hide": false, + "interval": "", + "legendFormat": "", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "title": "Block height", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "{instance=\"localhost:9999\", job=\"zebrad\"}" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RedPallas" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RedJubjub" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Ed25519" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(signatures_ed25519_validated{}[$__interval])", + "hide": false, + "interval": "", + "legendFormat": "Ed25519", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(signatures_redjubjub_validated{}[$__interval])", + "hide": false, + "interval": "", + "legendFormat": "RedJubjub", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(signatures_redpallas_validated{}[$__interval])", + "hide": false, + "legendFormat": "RedPallas", + "range": true, + "refId": "C" + } + ], + "title": "Signatures validated", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 12, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.5.5", + "targets": [ + { + "exemplar": true, + "expr": "rate(state_finalized_cumulative_transactions{}[$__interval])", + "interval": "", + "legendFormat": "Transactions finalized", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "title": "Transactions finalized", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Halo2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 9 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "exemplar": true, + "expr": "rate(proofs_groth16_verified{}[$__interval])", + "interval": "", + "legendFormat": "Groth16", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "editorMode": "code", + "expr": "rate(proofs_halo2_verified{}[$__interval])", + "hide": false, + "legendFormat": "Halo2", + "range": true, + "refId": "B" + } + ], + "title": "Proofs verified", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Transparent newOuts" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "semi-dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Transparent prevOuts" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "semi-dark-yellow", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "exemplar": true, + "expr": "rate(state_finalized_cumulative_transparent_newouts{}[$__interval])", + "interval": "", + "legendFormat": "Transparent newOuts", + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + }, + { + "exemplar": true, + "expr": "rate(state_finalized_cumulative_transparent_prevouts{}[$__interval])", + "hide": false, + "interval": "", + "legendFormat": "Transparent prevOuts", + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + } + } + ], + "title": "Transparent Outpoints", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Orchard" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Sapling" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Sprout" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 17 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "exemplar": true, + "expr": "rate(state_finalized_cumulative_sprout_nullifiers{}[$__interval])", + "hide": false, + "interval": "", + "legendFormat": "Sprout", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "exemplar": true, + "expr": "rate(state_finalized_cumulative_sapling_nullifiers{}[$__interval])", + "hide": false, + "interval": "", + "legendFormat": "Sapling", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "exemplar": true, + "expr": "rate(state_finalized_cumulative_orchard_nullifiers{}[$__interval])", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "Orchard", + "refId": "C" + } + ], + "title": "Nullifiers revealed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 25 + }, + "id": 100, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "zebra_consensus_batch_duration_seconds{quantile=\"0.5\"}", + "legendFormat": "{{verifier}} p50", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "zebra_consensus_batch_duration_seconds{quantile=\"0.95\"}", + "legendFormat": "{{verifier}} p95", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "zebra_consensus_batch_duration_seconds{quantile=\"0.99\"}", + "legendFormat": "{{verifier}} p99", + "refId": "C" + } + ], + "title": "Batch Verification Duration by Verifier", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 25 + }, + "id": 101, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sum(rate(zebra_consensus_batch_duration_seconds_count{result=\"success\"}[5m])) by (verifier)", + "legendFormat": "{{verifier}} success/s", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "sum(rate(zebra_consensus_batch_duration_seconds_count{result=\"failure\"}[5m])) by (verifier)", + "legendFormat": "{{verifier}} failure/s", + "refId": "B" + } + ], + "title": "Verification Throughput by Verifier", + "type": "timeseries" + } + ], + "refresh": "5s", + "schemaVersion": 36, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-12h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "🔎", + "uid": "UXVRR1v7z", + "version": 23, + "weekStart": "" +} diff --git a/observability/grafana/dashboards/value_pools.json b/observability/grafana/dashboards/value_pools.json new file mode 100644 index 0000000..b1d8a5a --- /dev/null +++ b/observability/grafana/dashboards/value_pools.json @@ -0,0 +1,725 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "Value Pool Monitoring - Track shielded pool balances and total supply. Zebra enforces ZIP-209 internally.", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "ZEC", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": ["last"], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "state_finalized_chain_supply_total / 100000000", + "legendFormat": "Total Supply (ZEC)", + "refId": "A" + } + ], + "title": "Total Chain Supply", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "ZEC", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Transparent" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#73BF69", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Sprout" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F2CC0C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Sapling" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#5794F2", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Orchard" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B877D9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Deferred" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FF9830", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 2, + "options": { + "legend": { + "calcs": ["last", "mean"], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "state_finalized_value_pool_transparent / 100000000", + "legendFormat": "Transparent", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "state_finalized_value_pool_sprout / 100000000", + "legendFormat": "Sprout", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "state_finalized_value_pool_sapling / 100000000", + "legendFormat": "Sapling", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "state_finalized_value_pool_orchard / 100000000", + "legendFormat": "Orchard", + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "state_finalized_value_pool_deferred / 100000000", + "legendFormat": "Deferred", + "refId": "E" + } + ], + "title": "Value Pools by Type (Stacked)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 18 + }, + "id": 3, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "state_finalized_value_pool_transparent / 100000000", + "legendFormat": "Transparent ZEC", + "refId": "A" + } + ], + "title": "Transparent Pool", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "#F2CC0C", + "value": 0 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 18 + }, + "id": 4, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "state_finalized_value_pool_sprout / 100000000", + "legendFormat": "Sprout ZEC", + "refId": "A" + } + ], + "title": "Sprout Pool", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "blue", + "value": 0 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 18 + }, + "id": 5, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "state_finalized_value_pool_sapling / 100000000", + "legendFormat": "Sapling ZEC", + "refId": "A" + } + ], + "title": "Sapling Pool", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "purple", + "value": 0 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 18 + }, + "id": 6, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "state_finalized_value_pool_orchard / 100000000", + "legendFormat": "Orchard ZEC", + "refId": "A" + } + ], + "title": "Orchard Pool", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "description": "Pool Health Check: All pools should be non-negative (enforced internally by Zebra)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "0": { + "color": "green", + "index": 0, + "text": "HEALTHY" + } + }, + "type": "value" + }, + { + "options": { + "from": 1, + "result": { + "color": "red", + "index": 1, + "text": "VIOLATION" + }, + "to": 999999 + }, + "type": "range" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 12, + "x": 0, + "y": 22 + }, + "id": 7, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "(state_finalized_value_pool_transparent < 0) + (state_finalized_value_pool_sprout < 0) + (state_finalized_value_pool_sapling < 0) + (state_finalized_value_pool_orchard < 0)", + "legendFormat": "Pool Health Status", + "refId": "A" + } + ], + "title": "Pool Health Status", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 12, + "x": 12, + "y": 22 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "expr": "state_finalized_block_height", + "legendFormat": "Block Height", + "refId": "A" + } + ], + "title": "Current Block Height", + "type": "stat" + } + ], + "refresh": "10s", + "schemaVersion": 37, + "style": "dark", + "tags": ["zebra", "zcash", "value-pools"], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Zebra Value Pools", + "uid": "zebra-value-pools", + "version": 1, + "weekStart": "" +} diff --git a/observability/grafana/dashboards/zebra_overview.json b/observability/grafana/dashboards/zebra_overview.json new file mode 100644 index 0000000..2b23bc5 --- /dev/null +++ b/observability/grafana/dashboards/zebra_overview.json @@ -0,0 +1,1047 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Zebra Node Overview - At-a-glance health and status for node operators", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, + "id": 100, + "panels": [], + "title": "Node Identity", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Zebra node software version", + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [{ "color": "blue", "value": null }] + } + }, + "overrides": [] + }, + "gridPos": { "h": 3, "w": 8, "x": 0, "y": 1 }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "/^version$/", "values": false }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "zebrad_build_info", + "format": "table", + "instant": true, + "refId": "A" + } + ], + "title": "Version", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Current finalized blockchain height", + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "noValue": "NO DATA", + "thresholds": { + "mode": "absolute", + "steps": [{ "color": "blue", "value": null }] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 3, "w": 8, "x": 8, "y": 1 }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "state_finalized_block_height", + "legendFormat": "Height", + "refId": "A" + } + ], + "title": "Block Height", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Total ZEC supply on the blockchain", + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [{ "color": "orange", "value": null }] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 3, "w": 8, "x": 16, "y": 1 }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "state_finalized_chain_supply_total / 100000000", + "legendFormat": "ZEC", + "refId": "A" + } + ], + "title": "Total Supply (ZEC)", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 4 }, + "id": 101, + "panels": [], + "title": "Health Status", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Peer connectivity - gauge shows current count relative to healthy thresholds", + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "max": 50, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "red", "value": null }, + { "color": "#EAB839", "value": 8 }, + { "color": "green", "value": 20 } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 5, "w": 4, "x": 0, "y": 5 }, + "id": 10, + "options": { + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showThresholdLabels": true, + "showThresholdMarkers": true + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "zcash_net_peers", + "legendFormat": "Peers", + "refId": "A" + } + ], + "title": "Peer Count", + "type": "gauge" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Block verification activity (ACTIVE = processing blocks, IDLE = waiting for new blocks)", + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [ + { + "options": { "from": 0.001, "result": { "color": "green", "index": 0, "text": "ACTIVE" }, "to": 1000000 }, + "type": "range" + }, + { + "options": { "from": 0, "result": { "color": "blue", "index": 1, "text": "IDLE" }, "to": 0.001 }, + "type": "range" + } + ], + "noValue": "NO DATA", + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "blue", "value": null }, + { "color": "green", "value": 0.001 } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 5, "w": 5, "x": 4, "y": 5 }, + "id": 11, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "rate(zcash_chain_verified_block_total[5m])", + "legendFormat": "Rate", + "refId": "A" + } + ], + "title": "Block Activity", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Blocks behind estimated network tip", + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [ + { + "options": { "from": 0, "result": { "color": "green", "index": 0, "text": "SYNCED" }, "to": 2 }, + "type": "range" + } + ], + "noValue": "NO DATA", + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 3 }, + { "color": "orange", "value": 50 }, + { "color": "red", "value": 100 } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 5, "w": 5, "x": 9, "y": 5 }, + "id": 14, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "value" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "sync_estimated_distance_to_tip{job=~\"$job\"}", + "legendFormat": "Distance", + "refId": "A" + } + ], + "title": "Sync Distance", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "ZIP-209 compliance - all value pools must be non-negative", + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [ + { "options": { "0": { "color": "green", "index": 0, "text": "HEALTHY" } }, "type": "value" }, + { + "options": { "from": 1, "result": { "color": "red", "index": 1, "text": "VIOLATION" }, "to": 10 }, + "type": "range" + } + ], + "noValue": "NO DATA", + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "red", "value": 1 } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 5, "w": 5, "x": 14, "y": 5 }, + "id": 12, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "count(state_finalized_value_pool_transparent < 0 or state_finalized_value_pool_sprout < 0 or state_finalized_value_pool_sapling < 0 or state_finalized_value_pool_orchard < 0) or vector(0)", + "legendFormat": "Violations", + "refId": "A" + } + ], + "title": "Value Pools", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "RPC endpoint health based on error rate (no data = RPC disabled)", + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [ + { "options": { "from": 0, "result": { "color": "green", "index": 0, "text": "HEALTHY" }, "to": 0.05 }, "type": "range" }, + { "options": { "from": 0.05, "result": { "color": "yellow", "index": 1, "text": "DEGRADED" }, "to": 0.1 }, "type": "range" }, + { "options": { "from": 0.1, "result": { "color": "red", "index": 2, "text": "UNHEALTHY" }, "to": 1 }, "type": "range" } + ], + "noValue": "DISABLED", + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 0.05 }, + { "color": "red", "value": 0.1 } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { "h": 5, "w": 5, "x": 19, "y": 5 }, + "id": 13, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "sum(rate(rpc_requests_total{status=\"error\"}[5m])) / sum(rate(rpc_requests_total[5m])) or vector(0)", + "legendFormat": "Error Rate", + "refId": "A" + } + ], + "title": "RPC Health", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 10 }, + "id": 102, + "panels": [], + "title": "Network", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Network bandwidth - bytes transferred per second", + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "binBps" + }, + "overrides": [ + { "matcher": { "id": "byName", "options": "Inbound" }, "properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }] }, + { "matcher": { "id": "byName", "options": "Outbound" }, "properties": [{ "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }] } + ] + }, + "gridPos": { "h": 8, "w": 8, "x": 0, "y": 11 }, + "id": 20, + "options": { + "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "none" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, "expr": "sum(rate(zcash_net_in_bytes_total[1m]))", "legendFormat": "Inbound", "refId": "A" }, + { "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, "expr": "sum(rate(zcash_net_out_bytes_total[1m]))", "legendFormat": "Outbound", "refId": "B" } + ], + "title": "Network Traffic", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "P2P message rate - messages per second", + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "msg/s", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "ops" + }, + "overrides": [ + { "matcher": { "id": "byName", "options": "Inbound" }, "properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }] }, + { "matcher": { "id": "byName", "options": "Outbound" }, "properties": [{ "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }] } + ] + }, + "gridPos": { "h": 8, "w": 8, "x": 8, "y": 11 }, + "id": 21, + "options": { + "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "none" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, "expr": "sum(rate(zcash_net_in_messages[1m]))", "legendFormat": "Inbound", "refId": "A" }, + { "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, "expr": "sum(rate(zcash_net_out_messages[1m]))", "legendFormat": "Outbound", "refId": "B" } + ], + "title": "Message Rate", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Distribution of connected peers by client software", + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 8, "x": 16, "y": 11 }, + "id": 22, + "options": { + "legend": { "displayMode": "table", "placement": "right", "showLegend": true, "values": ["value", "percent"] }, + "pieType": "pie", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "tooltip": { "mode": "single", "sort": "none" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "sum by (user_agent) (zcash_net_peers_connected)", + "legendFormat": "{{user_agent}}", + "refId": "A" + } + ], + "title": "Peer Distribution", + "type": "piechart" + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 19 }, + "id": 103, + "panels": [], + "title": "Consensus & Mempool", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Mempool transaction count and size", + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } + }, + "overrides": [ + { "matcher": { "id": "byName", "options": "Transactions" }, "properties": [{ "id": "custom.axisPlacement", "value": "left" }] }, + { "matcher": { "id": "byName", "options": "Size" }, "properties": [{ "id": "custom.axisPlacement", "value": "right" }, { "id": "unit", "value": "bytes" }] } + ] + }, + "gridPos": { "h": 8, "w": 8, "x": 0, "y": 20 }, + "id": 30, + "options": { + "legend": { "calcs": ["lastNotNull", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "none" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, "expr": "zcash_mempool_size_transactions", "legendFormat": "Transactions", "refId": "A" }, + { "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, "expr": "zcash_mempool_size_bytes", "legendFormat": "Size", "refId": "B" } + ], + "title": "Mempool Size", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Cryptographic proof verification rate (Halo2 + Groth16)", + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "proofs/s", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 8, "x": 8, "y": 20 }, + "id": 31, + "options": { + "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "desc" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, "expr": "rate(proofs_halo2_verified[1m])", "legendFormat": "Halo2", "refId": "A" }, + { "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, "expr": "rate(proofs_groth16_verified[1m])", "legendFormat": "Groth16", "refId": "B" } + ], + "title": "Proof Verification Rate", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Block download and verification rate", + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "blocks/s", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 8, "x": 16, "y": 20 }, + "id": 32, + "options": { + "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "none" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, "expr": "rate(zcash_chain_verified_block_total[1m])", "legendFormat": "Verified", "refId": "A" }, + { "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, "expr": "rate(sync_downloaded_block_count[1m])", "legendFormat": "Downloaded", "refId": "B" } + ], + "title": "Block Rate", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 28 }, + "id": 104, + "panels": [], + "title": "Infrastructure", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "RocksDB database disk usage", + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 80000000000 }, + { "color": "red", "value": 150000000000 } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { "h": 4, "w": 8, "x": 0, "y": 29 }, + "id": 40, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "zebra_state_rocksdb_total_disk_size_bytes", + "legendFormat": "DB Size", + "refId": "A" + } + ], + "title": "Database Size", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "RPC endpoint p99 latency (5m window)", + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 0.5 }, + { "color": "red", "value": 2 } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { "h": 4, "w": 8, "x": 8, "y": 29 }, + "id": 41, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "histogram_quantile(0.99, sum(rate(rpc_request_duration_seconds_bucket[5m])) by (le))", + "legendFormat": "p99", + "refId": "A" + } + ], + "title": "RPC p99 Latency", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Peer count trend over time with health thresholds", + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "line" } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "red", "value": null }, + { "color": "yellow", "value": 8 }, + { "color": "green", "value": 20 } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 4, "w": 8, "x": 16, "y": 29 }, + "id": 42, + "options": { + "legend": { "calcs": ["lastNotNull", "min", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "none" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "zcash_net_peers", + "legendFormat": "Peers", + "refId": "A" + } + ], + "title": "Peer Count Trend", + "type": "timeseries" + }, + { + "collapsed": true, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 33 }, + "id": 106, + "panels": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Process CPU usage rate", + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { "h": 6, "w": 8, "x": 0, "y": 34 }, + "id": 60, + "options": { + "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "none" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "rate(process_cpu_seconds_total{job=~\"$job\"}[1m])", + "legendFormat": "CPU", + "refId": "A" + } + ], + "title": "Process CPU Usage", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Process memory usage (resident and virtual)", + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { "h": 6, "w": 8, "x": 8, "y": 34 }, + "id": 61, + "options": { + "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "none" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "process_resident_memory_bytes{job=~\"$job\"}", + "legendFormat": "Resident", + "refId": "A" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "process_virtual_memory_bytes{job=~\"$job\"}", + "legendFormat": "Virtual", + "refId": "B" + } + ], + "title": "Process Memory", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "description": "Open and maximum file descriptors", + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { "h": 6, "w": 8, "x": 16, "y": 34 }, + "id": 62, + "options": { + "legend": { "calcs": ["lastNotNull", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "none" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "process_open_fds{job=~\"$job\"}", + "legendFormat": "Open", + "refId": "A" + }, + { + "datasource": { "type": "prometheus", "uid": "zebra-prometheus" }, + "expr": "process_max_fds{job=~\"$job\"}", + "legendFormat": "Max", + "refId": "B" + } + ], + "title": "Open File Descriptors", + "type": "timeseries" + } + ], + "title": "System Resources", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 34 }, + "id": 105, + "panels": [], + "title": "Dashboard Links", + "type": "row" + }, + { + "datasource": { "type": "datasource", "uid": "grafana" }, + "gridPos": { "h": 3, "w": 24, "x": 0, "y": 35 }, + "id": 50, + "options": { + "code": { "language": "plaintext", "showLineNumbers": false, "showMiniMap": false }, + "content": "### Detailed Dashboards\n\n| [Syncer](/d/Sl3h19Gnk/syncer) | [Network Health](/d/320aS_dMk/network-health) | [Peers](/d/S29TgUH7k/peers) | [RPC Metrics](/d/zebra-rpc-metrics/zebra-rpc-metrics) | [RPC Tracing](/d/zebra-rpc-tracing/zebra-rpc-tracing) | [Value Pools](/d/zebra-value-pools/zebra-value-pools) | [Mempool](/d/wVXGE6v7z/mempool) | [Database](/d/zebra-rocksdb/rocksdb-database) | [Tx Verification](/d/UXVRR1v7z/transaction-verification) | [Block Verification](/d/rO_Cl5tGz/block-verification) |", + "mode": "markdown" + }, + "pluginVersion": "10.0.0", + "title": "", + "transparent": true, + "type": "text" + } + ], + "refresh": "10s", + "schemaVersion": 38, + "style": "dark", + "tags": ["zebra", "zcash", "overview"], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "selected": true, + "text": ["All"], + "value": ["$__all"] + }, + "datasource": { + "type": "prometheus", + "uid": "zebra-prometheus" + }, + "definition": "label_values(state_finalized_block_height, job)", + "description": "Filter by Prometheus job label", + "hide": 0, + "includeAll": true, + "label": "Job", + "multi": true, + "name": "job", + "options": [], + "query": { + "query": "label_values(state_finalized_block_height, job)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": { "from": "now-1h", "to": "now" }, + "timepicker": {}, + "timezone": "", + "title": "Zebra Overview", + "uid": "zebra-overview", + "version": 1, + "weekStart": "" +} diff --git a/observability/grafana/provisioning/dashboards/default.yml b/observability/grafana/provisioning/dashboards/default.yml new file mode 100644 index 0000000..c364c8b --- /dev/null +++ b/observability/grafana/provisioning/dashboards/default.yml @@ -0,0 +1,13 @@ +apiVersion: 1 + +providers: + - name: 'Zebra Dashboards' + orgId: 1 + folder: 'Zebra' + folderUid: 'zebra-dashboards' + type: file + disableDeletion: false + updateIntervalSeconds: 30 + allowUiUpdates: true + options: + path: /var/lib/grafana/dashboards diff --git a/observability/grafana/provisioning/datasources/datasources.yml b/observability/grafana/provisioning/datasources/datasources.yml new file mode 100644 index 0000000..114fcb0 --- /dev/null +++ b/observability/grafana/provisioning/datasources/datasources.yml @@ -0,0 +1,19 @@ +apiVersion: 1 + +datasources: + # Prometheus for metrics (Zebra metrics + Jaeger spanmetrics) + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false + uid: zebra-prometheus + + # Jaeger for distributed tracing + - name: Jaeger + type: jaeger + access: proxy + url: http://jaeger:16686 + editable: false + uid: zebra-jaeger diff --git a/observability/jaeger/README.md b/observability/jaeger/README.md new file mode 100644 index 0000000..c4c5907 --- /dev/null +++ b/observability/jaeger/README.md @@ -0,0 +1,356 @@ +# Jaeger Distributed Tracing + +Jaeger shows how requests flow through Zebra, where time is spent, and where errors occur. + +## Why Jaeger? + +While Prometheus metrics show **what** is happening (counters, gauges, histograms), Jaeger traces show **how** it's happening: + +| Metrics (Prometheus) | Traces (Jaeger) | +|---------------------|-----------------| +| "RPC latency P95 is 500ms" | "This specific getblock call spent 400ms in state lookup" | +| "10 requests/second" | "Request X called service A → B → C with these timings" | +| "5% error rate" | "This error started in component Y and propagated to Z" | + +Jaeger is used by other blockchain clients including Lighthouse, Reth, Hyperledger Besu, and Hyperledger Fabric for similar observability needs. + +## Accessing Jaeger + +Open http://localhost:16686 in your browser. + +## Concepts + +### Traces and Spans + +- **Trace**: A complete request journey through the system (e.g., an RPC call from start to finish) +- **Span**: A single operation within a trace (e.g., "verify block", "read from database") +- **Parent-child relationships**: Spans form a tree showing how operations nest + +### Span Kinds + +Jaeger categorizes spans by their role: + +| Kind | Description | Example in Zebra | +|------|-------------|------------------| +| **SERVER** | Entry point handling external requests | JSON-RPC endpoints (`getblock`, `getinfo`) | +| **INTERNAL** | Internal operations within the service | Block verification, state operations | +| **CLIENT** | Outgoing calls to other services | (Not currently used) | + +This distinction is important for the Monitor tab (see below). + +## Monitor Tab (Service Performance Monitoring) + +The Monitor tab provides RED metrics (Rate, Errors, Duration) aggregated from traces. + +### Understanding the Dashboard + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Service: zebra Span Kind: [Internal ▼] │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ Latency (s) Error rate (%) Request rate (req/s) │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ /\ │ │ │ │ ___ │ │ +│ │ / \ │ │ ────── │ │ / \ │ │ +│ │ / \ │ │ │ │ / \ │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +├─────────────────────────────────────────────────────────────────────┤ +│ Operations metrics │ +│ ┌────────────────────────────────────────────────────────────────┐│ +│ │ Name P95 Latency Request rate Error ││ +│ │ state 10s 10.27 req/s < 0.1% ││ +│ │ connector 4.52s 0.24 req/s < 0.1% ││ +│ │ checkpoint 95.88ms 6.69 req/s < 0.1% ││ +│ │ best_tip_height 4.75ms 58.84 req/s < 0.1% ││ +│ │ download_and_verify 2.32s 0.11 req/s < 0.1% ││ +│ └────────────────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Columns Explained + +| Column | Meaning | What to Look For | +|--------|---------|------------------| +| **Name** | Span/operation name | Group of related operations | +| **P95 Latency** | 95th percentile duration | High values indicate slow operations | +| **Request rate** | Operations per second | Throughput of each operation type | +| **Error rate** | Percentage of failures | Any value > 0% needs investigation | +| **Impact** | Relative contribution to overall latency | Focus optimization on high-impact operations | + +### Span Kind Filter + +The "Span Kind" dropdown filters which operations you see: + +- **Internal**: All internal Zebra operations (default view) + - Block verification, state operations, network handling + - High volume during sync + +- **Server**: External-facing request handlers + - JSON-RPC endpoints (`getblock`, `getinfo`, `sendrawtransaction`) + - Use this for RPC performance monitoring + +### Interpreting Common Operations + +| Operation | Description | Normal Latency | +|-----------|-------------|----------------| +| `state` | State database operations | 1-10s during sync | +| `connector` | Peer connection establishment | 1-5s | +| `dial` | Network dial attempts | 1-5s | +| `checkpoint` | Checkpoint verification | 50-200ms | +| `best_tip_height` | Chain tip queries | < 10ms | +| `download_and_verify` | Block download + verification | 1-5s | +| `block_commitment_is_valid_for_chain_history` | Block validation | 50-200ms | +| `rpc_request` | JSON-RPC handler (SERVER span) | Method-dependent | + +## Search Tab + +Use the Search tab to find specific traces. + +### Finding Traces + +1. **Service**: Select `zebra` +2. **Operation**: Choose a specific operation or "all" +3. **Tags**: Filter by attributes (e.g., `rpc.method=getblock`) +4. **Min/Max Duration**: Find slow or fast requests +5. **Limit**: Number of results (default: 20) + +### Useful Search Queries + +**Find slow RPC calls:** +``` +Service: zebra +Operation: rpc_request +Min Duration: 1s +``` + +**Find failed operations:** +``` +Service: zebra +Tags: error=true +``` + +**Find specific RPC method:** +``` +Service: zebra +Tags: rpc.method=getblock +``` + +## Trace Detail View + +Clicking a trace opens the detail view showing: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Trace: abc123 (5 spans, 234ms) │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ ├─ rpc_request [SERVER] ─────────────────────────────── 234ms ────┤│ +│ │ rpc.method: getblock ││ +│ │ rpc.system: jsonrpc ││ +│ │ ││ +│ │ ├─ state::read_block ─────────────────────── 180ms ───────────┤││ +│ │ │ block.height: 2000000 │││ +│ │ │ │││ +│ │ │ ├─ db::get ───────────────────── 150ms ──────────────────┤│││ +│ │ │ │ cf: blocks ││││ +│ │ │ └─────────────────────────────────────────────────────────┘│││ +│ │ └─────────────────────────────────────────────────────────────┘││ +│ └─────────────────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Reading the Waterfall + +- **Horizontal bars**: Duration of each span +- **Nesting**: Parent-child relationships +- **Colors**: Different services/components +- **Tags**: Attributes attached to each span + +### Span Attributes + +| Attribute | Meaning | +|-----------|---------| +| `otel.kind` | Span type (server, internal) | +| `rpc.method` | JSON-RPC method name | +| `rpc.system` | Protocol (jsonrpc) | +| `otel.status_code` | ERROR on failure | +| `rpc.error_code` | JSON-RPC error code | +| `error.message` | Error description | + +## Debugging with Traces + +### Performance Issues + +1. **Find slow traces**: Use Min Duration filter in Search +2. **Identify bottleneck**: Look for the longest span in the waterfall +3. **Check children**: See if parent is slow due to child operations +4. **Compare traces**: Compare fast vs slow traces for same operation + +### Error Investigation + +1. **Find failed traces**: Search with `error=true` tag +2. **Locate error span**: Look for spans with `otel.status_code=ERROR` +3. **Read error message**: Check `error.message` attribute +4. **Trace propagation**: See how error affected parent operations + +### Example: Debugging Slow RPC + +``` +Problem: getblock calls are slow + +1. Search: + - Service: zebra + - Operation: rpc_request + - Tags: rpc.method=getblock + - Min Duration: 500ms + +2. Open a slow trace + +3. Examine waterfall: + - rpc_request: 800ms total + └─ state::read_block: 750ms ← Most time spent here + └─ db::get: 700ms ← Database is the bottleneck + +4. Conclusion: Database read is slow, possibly due to: + - Disk I/O + - Large block size + - Missing cache +``` + +## RPC Tracing + +Zebra's JSON-RPC endpoints are instrumented with `SPAN_KIND_SERVER` spans, enabling: + +- **Jaeger SPM**: View RPC metrics in the Monitor tab (select "Server" span kind) +- **Per-method analysis**: Filter by `rpc.method` to see specific endpoint performance +- **Error tracking**: Failed RPC calls have `otel.status_code=ERROR` + +### RPC Span Attributes + +Each RPC request includes: + +| Attribute | Example | Description | +|-----------|---------|-------------| +| `otel.kind` | `server` | Marks as server-side handler | +| `rpc.method` | `getblock` | JSON-RPC method name | +| `rpc.system` | `jsonrpc` | Protocol identifier | +| `otel.status_code` | `ERROR` | Present on failure | +| `rpc.error_code` | `-8` | JSON-RPC error code | + +## Compare Tab + +Use Compare to diff two traces: + +1. Select two trace IDs +2. View side-by-side comparison +3. Identify differences in timing or structure + +Useful for: +- Comparing fast vs slow versions of same request +- Before/after optimization comparisons +- Debugging intermittent issues + +## System Architecture Tab + +Shows service dependencies based on trace data: + +- Nodes: Services (currently just `zebra`) +- Edges: Communication between services +- Useful when Zebra calls external services + +## Configuration + +Jaeger v2 configuration is in `config.yaml`: + +```yaml +# Key settings +receivers: + otlp: + protocols: + http: + endpoint: 0.0.0.0:4318 # OTLP HTTP (used by Zebra) + +processors: + batch: {} # Batches spans for efficiency + +connectors: + spanmetrics: # Generates Prometheus metrics from spans + namespace: traces.spanmetrics + +exporters: + prometheus: + endpoint: 0.0.0.0:8889 # Spanmetrics for Prometheus +``` + +### Spanmetrics + +Jaeger automatically generates Prometheus metrics from spans: + +```bash +# View spanmetrics +curl -s http://localhost:8889/metrics | grep traces_spanmetrics +``` + +These power the Monitor tab and can be scraped by Prometheus for Grafana dashboards. + +## Usage Tips + +### During Initial Sync + +- **Reduce sampling** to 1-10% to avoid overwhelming Jaeger +- **Focus on errors** rather than complete traces +- **Use metrics** (Prometheus) for high-level sync progress + +### Steady State Operation + +- **Increase sampling** to 10-50% for better visibility +- **Monitor RPC latency** in the Monitor tab (Server spans) +- **Set up alerts** for high error rates + +### Debugging Sessions + +- **Set sampling to 100%** temporarily +- **Use specific searches** to find relevant traces +- **Compare traces** to identify anomalies +- **Remember to reduce sampling** after debugging + +## Troubleshooting + +### No traces appearing + +1. Check tracing env vars are set in Zebra's container: + `docker inspect z3-mainnet-zebra-1 --format '{{range .Config.Env}}{{println .}}{{end}}' | grep ZEBRA_TRACING__OPENTELEMETRY` +2. Recreate Zebra after env changes: + `docker compose --env-file .env.mainnet --env-file .env --profile monitoring up -d --force-recreate zebra` +3. Check Zebra installed the exporter: + `docker logs z3-mainnet-zebra-1 | grep 'installed OpenTelemetry tracing layer'` +4. Verify Jaeger health: +5. Check Jaeger service discovery: + `curl -s http://127.0.0.1:16686/api/services` +6. Check Jaeger logs: `docker compose --profile monitoring logs jaeger` + +The pinned Zebra image and the default local build use `default-release-binaries`, +which includes OpenTelemetry. If you override `Z3_ZEBRA_BUILD_FEATURES`, keep +`opentelemetry` in the feature list. + +### Monitor tab shows "No Data" + +1. Ensure spanmetrics connector is configured +2. Wait for traces to accumulate (needs a few minutes) +3. Select the correct Span Kind filter +4. Confirm Prometheus has spanmetrics: + `curl -sG http://127.0.0.1:9094/api/v1/query --data-urlencode 'query=traces_span_metrics_calls_total{service_name="zebra-mainnet"}'` + +### High memory usage + +1. Reduce sampling: `OTEL_TRACES_SAMPLER_ARG=10` +2. Restart Jaeger to clear memory +3. Consider using external storage for production + +## Further Reading + +- [Jaeger Documentation](https://www.jaegertracing.io/docs/) +- [OpenTelemetry Tracing Concepts](https://opentelemetry.io/docs/concepts/signals/traces/) +- [Jaeger v2 Architecture](https://www.jaegertracing.io/docs/2.0/architecture/) diff --git a/observability/jaeger/config.yaml b/observability/jaeger/config.yaml new file mode 100644 index 0000000..21c4f1e --- /dev/null +++ b/observability/jaeger/config.yaml @@ -0,0 +1,86 @@ +# Jaeger v2 Configuration +# Based on OpenTelemetry Collector architecture +# Reference: https://www.jaegertracing.io/docs/2.0/getting-started/ + +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + timeout: 1s + send_batch_size: 1024 + +connectors: + # Spanmetrics connector generates metrics from spans for Jaeger SPM + # Reference: https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/connector/spanmetricsconnector + spanmetrics: + namespace: span_metrics + metrics_flush_interval: 15s + # Histogram buckets for latency distribution + histogram: + explicit: + buckets: [1ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s] + dimensions: + - name: rpc.method + - name: rpc.system + - name: otel.status_code + +exporters: + # Jaeger storage (in-memory for development) + jaeger_storage_exporter: + trace_storage: memstore + + # Prometheus metrics endpoint for spanmetrics (used by Jaeger SPM) + prometheus: + endpoint: 0.0.0.0:8889 + namespace: traces + const_labels: + service: zebra + +extensions: + jaeger_storage: + backends: + memstore: + memory: + max_traces: 100000 + # Metrics storage for SPM - reads from Prometheus + metric_backends: + prometheus: + prometheus: + endpoint: http://prometheus:9090 + normalize_calls: true + normalize_duration: true + + jaeger_query: + storage: + traces: memstore + metrics: prometheus + ui: + config_file: "" + + healthcheckv2: + use_v2: true + http: + endpoint: 0.0.0.0:13133 + +service: + extensions: + - jaeger_storage + - jaeger_query + - healthcheckv2 + + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [jaeger_storage_exporter, spanmetrics] + + # Spanmetrics pipeline - exports to Prometheus + metrics/spanmetrics: + receivers: [spanmetrics] + exporters: [prometheus] diff --git a/observability/prometheus/prometheus.yaml b/observability/prometheus/prometheus.yaml new file mode 100644 index 0000000..9c56994 --- /dev/null +++ b/observability/prometheus/prometheus.yaml @@ -0,0 +1,42 @@ +# Prometheus configuration for Zebra metrics and tracing +# +# IMPORTANT: The scrape_interval affects Grafana dashboard queries. +# The rate() function requires at least 2 data points, so rate windows +# must be > 2x scrape_interval. With 15s scrape_interval: +# - rate(...[1m]) works (4 samples) +# - rate(...[30s]) works (2 samples minimum) +# - rate(...[1s]) does NOT work (insufficient samples) +# +# If you change scrape_interval, update dashboard rate() windows accordingly. + +global: + scrape_interval: 15s + evaluation_interval: 15s + +rule_files: + - /etc/prometheus/rules/*.yml + +alerting: + alertmanagers: + - static_configs: + - targets: ['alertmanager:9093'] + +scrape_configs: + # Zebra node metrics (z3 stack uses z3_zebra container name) + - job_name: "zebra" + scrape_interval: 15s + scrape_timeout: 10s + metrics_path: "/metrics" + static_configs: + - targets: ["zebra:9999"] + labels: + stack: "z3" + + # Jaeger spanmetrics for RPC tracing + # These metrics are generated by the spanmetrics connector in Jaeger + # and provide RED metrics (Rate, Errors, Duration) for RPC calls + - job_name: "jaeger-spanmetrics" + scrape_interval: 15s + scrape_timeout: 10s + static_configs: + - targets: ["jaeger:8889"] diff --git a/observability/prometheus/rules/zebra_alerts.yml b/observability/prometheus/rules/zebra_alerts.yml new file mode 100644 index 0000000..688ea07 --- /dev/null +++ b/observability/prometheus/rules/zebra_alerts.yml @@ -0,0 +1,149 @@ +# Zebra Alerting Rules +# +# These rules are evaluated by Prometheus and sent to AlertManager. +# AlertManager handles routing, grouping, and notifications. +# +# To receive notifications, configure receivers in: +# docker/observability/alertmanager/alertmanager.yml + +groups: + - name: zebra + rules: + # Node is down or metrics endpoint unreachable + - alert: ZebraDown + expr: up{job="zebra"} == 0 + for: 2m + labels: + severity: critical + annotations: + summary: "Zebra node is down" + description: "Zebra metrics endpoint has been unreachable for 2 minutes." + + # Block height not increasing (sync may be stalled) + - alert: ZebraSyncStalled + expr: changes(zcash_chain_verified_block_height[15m]) == 0 + for: 5m + labels: + severity: warning + annotations: + summary: "Block height stalled" + description: "Zebra block height has not increased in the last 15 minutes. Node may be stuck or network may be partitioned." + + # Low peer count (degraded connectivity) + - alert: ZebraLowPeers + expr: zcash_net_peers < 3 + for: 5m + labels: + severity: warning + annotations: + summary: "Low peer count" + description: "Zebra has fewer than 3 peers for 5 minutes. Network connectivity may be degraded." + + # No peers at all (network isolation) + - alert: ZebraNoPeers + expr: zcash_net_peers == 0 + for: 2m + labels: + severity: critical + annotations: + summary: "No peers connected" + description: "Zebra has 0 peers for 2 minutes. Check network connectivity and firewall rules." + + # High error rate + - alert: ZebraHighErrorRate + expr: rate(zebra_errors_total[5m]) > 0.1 + for: 5m + labels: + severity: warning + annotations: + summary: "High error rate" + description: "Zebra is experiencing elevated error rates (> 0.1/s for 5 minutes)." + + # Value Pool Alerts (Monitoring) + - name: zebra-value-pools + rules: + # Critical: Negative value pool would indicate a bug (should never happen - Zebra rejects such blocks) + - alert: ValuePoolNegative + expr: state_finalized_value_pool_transparent < 0 or state_finalized_value_pool_sprout < 0 or state_finalized_value_pool_sapling < 0 or state_finalized_value_pool_orchard < 0 + for: 0m + labels: + severity: critical + annotations: + summary: "Negative value pool detected" + description: "A pool has a negative balance in metrics. Zebra enforces ZIP-209 internally, so this should not occur in normal operation." + + # Warning: Value pool not updating (may indicate sync issues) + - alert: ValuePoolStale + expr: changes(state_finalized_chain_supply_total[15m]) == 0 and state_finalized_block_height > 0 + for: 5m + labels: + severity: warning + annotations: + summary: "Value pool not updating" + description: "Chain supply has not changed in 15 minutes while blocks exist. Sync may be stalled." + + # RPC Alerts + - name: zebra-rpc + rules: + # High RPC latency + - alert: RPCHighLatency + expr: histogram_quantile(0.99, sum(rate(rpc_request_duration_seconds_bucket[5m])) by (le, method)) > 2 + for: 5m + labels: + severity: warning + annotations: + summary: "High RPC latency detected" + description: "RPC method {{ $labels.method }} has p99 latency > 2 seconds for 5 minutes." + + # High RPC error rate + - alert: RPCHighErrorRate + expr: sum(rate(rpc_errors_total[5m])) by (method) / sum(rate(rpc_requests_total[5m])) by (method) > 0.1 + for: 5m + labels: + severity: warning + annotations: + summary: "High RPC error rate" + description: "RPC method {{ $labels.method }} has error rate > 10% for 5 minutes." + + # RPC endpoint overloaded + - alert: RPCOverloaded + expr: rpc_active_requests > 100 + for: 2m + labels: + severity: warning + annotations: + summary: "RPC endpoint overloaded" + description: "More than 100 concurrent RPC requests for 2 minutes. Consider rate limiting." + + # Peer Health Alerts + - name: zebra-peer-health + rules: + # High handshake failure rate + - alert: HandshakeFailureRateHigh + expr: sum(rate(zcash_net_peer_handshake_failures_total[5m])) / (sum(rate(zcash_net_peer_handshake_duration_seconds_count[5m])) + 0.001) > 0.5 + for: 5m + labels: + severity: warning + annotations: + summary: "High peer handshake failure rate" + description: "More than 50% of peer handshakes are failing for 5 minutes. Check network connectivity." + + # Specific failure reason spike (e.g., obsolete version) + - alert: ObsoleteVersionHandshakes + expr: rate(zcash_net_peer_handshake_failures_total{reason="obsolete_version"}[5m]) > 0.1 + for: 5m + labels: + severity: info + annotations: + summary: "Many obsolete version handshakes" + description: "Elevated rate of peer handshake failures due to obsolete protocol versions. May indicate network upgrade in progress." + + # Slow handshakes + - alert: SlowHandshakes + expr: histogram_quantile(0.95, sum(rate(zcash_net_peer_handshake_duration_seconds_bucket{result="success"}[5m])) by (le)) > 10 + for: 5m + labels: + severity: warning + annotations: + summary: "Slow peer handshakes" + description: "p95 handshake duration exceeds 10 seconds. Network latency may be high." diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..6f96e8f --- /dev/null +++ b/renovate.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "github-actions": { + "enabled": false + }, + "packageRules": [ + { + "description": "Image pins are deliberate; raise PRs but never auto-merge a node-platform image bump.", + "matchManagers": ["docker-compose", "custom.regex"], + "automerge": false + } + ], + "customManagers": [ + { + "customType": "regex", + "description": "Bump the ${VAR:-repo:tag} image-pin defaults in the compose files.", + "managerFilePatterns": ["/(^|/)docker-compose[^/]*\\.ya?ml$/"], + "matchStrings": [ + "\\$\\{[A-Z0-9_]+:-(?[^:}\\s]+):(?[^}\\s]+)\\}" + ], + "datasourceTemplate": "docker" + } + ] +} diff --git a/rpc-router/Cargo.lock b/rpc-router/Cargo.lock new file mode 100644 index 0000000..a78c6b6 --- /dev/null +++ b/rpc-router/Cargo.lock @@ -0,0 +1,2714 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-object-pool" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "333c456b97c3f2d50604e8b2624253b7f787208cb72eb75e64b0ad11b221652c" +dependencies = [ + "async-std", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel 2.5.0", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.1", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-attributes", + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "basic-cookies" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7" +dependencies = [ + "lalrpop", + "lalrpop-util", + "regex", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "httpmock" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ec9586ee0910472dec1a1f0f8acf52f0fdde93aea74d70d4a3107b4be0fd5b" +dependencies = [ + "assert-json-diff", + "async-object-pool", + "async-std", + "async-trait", + "base64 0.21.7", + "basic-cookies", + "crossbeam-utils", + "form_urlencoded", + "futures-util", + "hyper 0.14.32", + "lazy_static", + "levenshtein", + "log", + "regex", + "serde", + "serde_json", + "serde_regex", + "similar", + "tokio", + "url", +] + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.8.1", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.8.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.3", + "system-configuration", + "tokio", + "tower-layer", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lalrpop" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "levenshtein" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower 0.5.3", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rpc-router" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.22.1", + "http-body-util", + "httpmock", + "hyper 1.8.1", + "hyper-util", + "reqwest", + "serde", + "serde_json", + "serial_test", + "tokio", + "tower 0.4.13", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serial_test" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +dependencies = [ + "futures-executor", + "futures-util", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/rpc-router/Cargo.toml b/rpc-router/Cargo.toml new file mode 100644 index 0000000..5e39c9a --- /dev/null +++ b/rpc-router/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "rpc-router" +description = "Z3 RPC request router" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0" +base64 = "0.22" +http-body-util = "0.1" +hyper = { version = "1.2", features = ["full"] } +hyper-util = { version = "0.1", features = ["full"] } +reqwest = { version = "0.12", features = ["json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tokio = { version = "1.36", features = ["full"] } +tower = { version = "0.4", features = ["util"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[dev-dependencies] +httpmock = "0.7" +serial_test = "3" diff --git a/rpc-router/Dockerfile b/rpc-router/Dockerfile new file mode 100644 index 0000000..50f7b00 --- /dev/null +++ b/rpc-router/Dockerfile @@ -0,0 +1,19 @@ +FROM lukemathwalker/cargo-chef:latest-rust-1.85 AS chef +WORKDIR /app + +FROM chef AS planner +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + +FROM chef AS builder +COPY --from=planner /app/recipe.json recipe.json +# Build dependencies - this is the caching Docker layer! +RUN cargo chef cook --release --recipe-path recipe.json +# Build application +COPY . . +RUN cargo build --release --bin rpc-router + +# We do not need the Rust toolchain to run the binary! +FROM gcr.io/distroless/cc-debian12 AS runtime +COPY --from=builder /app/target/release/rpc-router /usr/local/bin/ +ENTRYPOINT ["/usr/local/bin/rpc-router"] diff --git a/rpc-router/README.md b/rpc-router/README.md new file mode 100644 index 0000000..752a866 --- /dev/null +++ b/rpc-router/README.md @@ -0,0 +1,111 @@ +# Z3 RPC Router + +The **Z3 RPC Router** (`rpc-router` crate) provides a single JSON-RPC endpoint on top of the Zebra and Zallet RPC servers. + +It exposes a new port where incoming RPC requests are transparently routed to the appropriate backend: + +- RPC methods belonging to Zebra are forwarded to the Zebra RPC endpoint. +- RPC methods belonging to Zallet are forwarded to the Zallet RPC endpoint. + +From the point of view of a dApp built on top of the Zcash Z3 stack, this means there is a single RPC endpoint to interact with, while all routing happens in the background. + +In addition to request routing, the router also: + +- Calls `rpc.discover` on both Zebra and Zallet at startup +- Builds a merged OpenRPC schema for the Z3 stack +- Exposes the merged schema via its own `rpc.discover` method + +## Developer Usage + +To run the router, Zebra and Zallet must already be running, fully synced, and responsive on known ports **before** starting the router. At startup the router calls `rpc.discover` on both backends to build the merged schema — if either is unreachable the router exits immediately with an error. + +The router accepts this configuration via environment variables, or alternatively you can modify the values directly in main.rs. + +### Running Zebra and Zallet for Development + +The easiest way to get Zebra and Zallet running locally is via the Docker Compose stack at the root of this repository: + +```bash +docker compose up -d zebra zallet +``` + +Wait until both services are healthy, then note the RPC ports from your `.env` file (or the defaults in `docker-compose.yml`) and pass them to the router. + +> **Note:** A `openrpc.py` QA helper that spawned Zebra and Zallet in regtest mode previously existed in the Zebra repository but was removed. It has not been ported to the [zcash/integration-tests](https://github.com/zcash/integration-tests) repository. + +> **Note:** For experimental Z3 regtest mode of the router see [Regtest Environment](../docs/regtest.md). + +### Running the RPC Router + +```bash +RUST_LOG=info \ +ZEBRA_URL=http://localhost:/ \ +ZALLET_URL=http://localhost:/ \ +cargo run +``` + +Example output: + +```bash +INFO rpc_router: RPC Router listening on 0.0.0.0:8080 +INFO rpc_router: You can use the following playground URL: +INFO rpc_router: https://playground.open-rpc.org/?... +``` + +At this point, the router is listening on `http://localhost:8080`. + +### Querying the Router + +You can use standard JSON-RPC clients (such as `curl`) to call methods exposed by either Zebra or Zallet through the router. + +#### Example: Zebra RPC Call + +```bash +curl --silent -X POST \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"getinfo","params":[],"id":123}' \ + http://127.0.0.1:8080 | jq +``` + +On the router side, you should see: + +```bash +INFO rpc_router: Routing getinfo to Zebra +``` + +#### Example: Zallet RPC Call + +```bash +curl --silent -X POST \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"getwalletinfo","params":[],"id":123}' \ + http://127.0.0.1:8080 | jq +``` + +And on the router logs: + +```bash +INFO rpc_router: Routing getwalletinfo to Zallet +``` + +### OpenRPC Playground + +You can interact with the merged OpenRPC schema using the OpenRPC Playground, pointed at your running router: + +[Playground](https://playground.open-rpc.org/?uiSchema[appBar][ui:title]=Zcash&uiSchema[appBar][ui:logoUrl]=https://z.cash/wp-content/uploads/2023/03/zcash-logo.gif&schemaUrl=http://127.0.0.1:8080&uiSchema[appBar][ui:splitView]=false&uiSchema[appBar][ui:edit]=false&uiSchema[appBar][ui:input]=false&uiSchema[appBar][ui:examplesDropdown]=false&uiSchema[appBar][ui:transports]=false) + +When using the inspector, make sure the target server URL is set to: + +``` +http://localhost:8080/ +``` + +The RPC router automatically sets the required CORS headers, allowing the playground (or other browser-based tools) to call the local endpoint directly. + +By default the `Access-Control-Allow-Origin` header is set to `https://playground.open-rpc.org`. To allow a different origin (e.g. a dApp frontend or any browser client), set the `CORS_ORIGIN` environment variable: + +```bash +CORS_ORIGIN=https://myapp.example.com cargo run +``` + +For local development you can use `CORS_ORIGIN=*` to allow all origins. Avoid using `*` in production as it allows any website to call your local node. diff --git a/rpc-router/src/defaults.rs b/rpc-router/src/defaults.rs new file mode 100644 index 0000000..ec7ff16 --- /dev/null +++ b/rpc-router/src/defaults.rs @@ -0,0 +1,22 @@ +//! Default constants +use std::net::SocketAddr; + +pub(crate) const ZEBRA_URL: &str = "http://127.0.0.1:20251"; + +pub(crate) const ZALLET_URL: &str = "http://127.0.0.1:25251"; + +pub(crate) const ZAINO_URL: &str = "http://zaino:8237"; + +pub(crate) const RPC_USER: &str = "zebra"; + +pub(crate) const RPC_PASSWORD: &str = "zebra"; + +pub(crate) const CORS_ORIGIN: &str = "https://playground.open-rpc.org"; + +pub(crate) const LISTEN_PORT: u16 = 8080; + +pub(crate) const PLAYGROUND_URL: &str = "https://playground.open-rpc.org/?uiSchema[appBar][ui:title]=Zcash&uiSchema[appBar][ui:logoUrl]=https://z.cash/wp-content/uploads/2023/03/zcash-logo.gif&schemaUrl={{addr}}&uiSchema[appBar][ui:splitView]=false&uiSchema[appBar][ui:edit]=false&uiSchema[appBar][ui:input]=false&uiSchema[appBar][ui:examplesDropdown]=false&uiSchema[appBar][ui:transports]=false"; + +pub(crate) fn playground_url(addr: SocketAddr) -> String { + PLAYGROUND_URL.replace("{{addr}}", &addr.to_string()) +} \ No newline at end of file diff --git a/rpc-router/src/integration_tests.rs b/rpc-router/src/integration_tests.rs new file mode 100644 index 0000000..29ada87 --- /dev/null +++ b/rpc-router/src/integration_tests.rs @@ -0,0 +1,285 @@ +use httpmock::prelude::*; +use reqwest::Client; +use serde_json::{json, Value}; +use std::sync::Arc; +use tokio::net::TcpListener; + +use super::*; + +// --- Mock backend helpers --- + +/// Starts a mock Zebra backend that handles rpc.discover and method forwarding. +/// Returns a distinct "zebra-response" result so routing can be verified. +async fn start_zebra_mock() -> MockServer { + let server = MockServer::start_async().await; + + server + .mock_async(|when, then| { + when.method(POST).body_contains("rpc.discover"); + then.status(200).json_body(json!({ + "jsonrpc": "2.0", "id": 1, + "result": { + "openrpc": "1.2.6", + "info": { "title": "Zebra", "version": "1.0.0" }, + "methods": [ + { "name": "getblock", "params": [] }, + { "name": "getinfo", "params": [] } + ], + "components": { "schemas": { "BlockHash": { "type": "string" } } } + } + })); + }) + .await; + + server + .mock_async(|when, then| { + when.method(POST); + then.status(200) + .json_body(json!({ "jsonrpc": "2.0", "id": 1, "result": "zebra-response" })); + }) + .await; + + server +} + +/// Starts a mock Zallet backend that handles rpc.discover and method forwarding. +async fn start_zallet_mock() -> MockServer { + let server = MockServer::start_async().await; + + server + .mock_async(|when, then| { + when.method(POST).body_contains("rpc.discover"); + then.status(200).json_body(json!({ + "jsonrpc": "2.0", "id": 1, + "result": { + "openrpc": "1.2.6", + "info": { "title": "Zallet", "version": "1.0.0" }, + "methods": [ + { "name": "getwalletinfo", "params": [] }, + { "name": "z_sendmany", "params": [] } + ], + "components": { "schemas": { "WalletInfo": { "type": "object" } } } + } + })); + }) + .await; + + server + .mock_async(|when, then| { + when.method(POST); + then.status(200) + .json_body(json!({ "jsonrpc": "2.0", "id": 1, "result": "zallet-response" })); + }) + .await; + + server +} + +/// Starts a mock Zaino backend (generic fallback). +async fn start_zaino_mock() -> MockServer { + let server = MockServer::start_async().await; + + server + .mock_async(|when, then| { + when.method(POST); + then.status(200) + .json_body(json!({ "jsonrpc": "2.0", "id": 1, "result": "zaino-response" })); + }) + .await; + + server +} + +// --- Router startup helper --- + +struct RouterHandle { + pub port: u16, + task: tokio::task::JoinHandle<()>, +} + +impl Drop for RouterHandle { + fn drop(&mut self) { + self.task.abort(); + } +} + +async fn start_router(zebra_url: &str, zallet_url: &str, zaino_url: &str) -> RouterHandle { + let config = Arc::new(Config { + zebra_url: zebra_url.to_string(), + zallet_url: zallet_url.to_string(), + _zaino_url: zaino_url.to_string(), + listen_port: 0, + rpc_user: "zebra".to_string(), + rpc_password: "zebra".to_string(), + cors_origin: "*".to_string(), + }); + + let zebra_schema = call_rpc_discover(&config.zebra_url, &config.rpc_user, &config.rpc_password) + .await + .unwrap()["result"] + .clone(); + let zallet_schema = + call_rpc_discover(&config.zallet_url, &config.rpc_user, &config.rpc_password) + .await + .unwrap()["result"] + .clone(); + let z3 = merge_openrpc_schemas(zebra_schema, zallet_schema).unwrap(); + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + + let task = tokio::spawn(async move { + if let Err(e) = run(config, listener, z3).await { + eprintln!("Router error in test: {}", e); + } + }); + + // Let the router start accepting connections. + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + + RouterHandle { port, task } +} + +// --- Tests --- + +#[tokio::test] +async fn test_health_check() { + let zebra = start_zebra_mock().await; + let zallet = start_zallet_mock().await; + let zaino = start_zaino_mock().await; + let router = start_router(&zebra.base_url(), &zallet.base_url(), &zaino.base_url()).await; + + let resp = Client::new() + .get(format!("http://127.0.0.1:{}/health", router.port)) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + assert_eq!(resp.text().await.unwrap(), "OK"); +} + +#[tokio::test] +async fn test_non_post_returns_405() { + let zebra = start_zebra_mock().await; + let zallet = start_zallet_mock().await; + let zaino = start_zaino_mock().await; + let router = start_router(&zebra.base_url(), &zallet.base_url(), &zaino.base_url()).await; + + let resp = Client::new() + .get(format!("http://127.0.0.1:{}/", router.port)) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 405); +} + +#[tokio::test] +async fn test_cors_preflight_returns_204_with_headers() { + let zebra = start_zebra_mock().await; + let zallet = start_zallet_mock().await; + let zaino = start_zaino_mock().await; + let router = start_router(&zebra.base_url(), &zallet.base_url(), &zaino.base_url()).await; + + let resp = Client::new() + .request( + reqwest::Method::OPTIONS, + format!("http://127.0.0.1:{}/", router.port), + ) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 204); + assert!(resp.headers().contains_key("access-control-allow-origin")); + assert!(resp.headers().contains_key("access-control-allow-methods")); +} + +#[tokio::test] +async fn test_rpc_discover_returns_merged_schema() { + let zebra = start_zebra_mock().await; + let zallet = start_zallet_mock().await; + let zaino = start_zaino_mock().await; + let router = start_router(&zebra.base_url(), &zallet.base_url(), &zaino.base_url()).await; + + let resp = Client::new() + .post(format!("http://127.0.0.1:{}/", router.port)) + .json(&json!({ "jsonrpc": "2.0", "id": 1, "method": "rpc.discover", "params": [] })) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + + let body: Value = resp.json().await.unwrap(); + let methods = body["methods"].as_array().unwrap(); + let names: Vec<&str> = methods + .iter() + .map(|m| m["name"].as_str().unwrap()) + .collect(); + + assert!(names.contains(&"getblock")); + assert!(names.contains(&"getinfo")); + assert!(names.contains(&"getwalletinfo")); + assert!(names.contains(&"z_sendmany")); +} + +#[tokio::test] +async fn test_zebra_method_routing() { + let zebra = start_zebra_mock().await; + let zallet = start_zallet_mock().await; + let zaino = start_zaino_mock().await; + let router = start_router(&zebra.base_url(), &zallet.base_url(), &zaino.base_url()).await; + + let resp = Client::new() + .post(format!("http://127.0.0.1:{}/", router.port)) + .json(&json!({ "jsonrpc": "2.0", "id": 1, "method": "getblock", "params": [] })) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + let body: Value = resp.json().await.unwrap(); + assert_eq!(body["result"], "zebra-response"); +} + +#[tokio::test] +async fn test_zallet_method_routing() { + let zebra = start_zebra_mock().await; + let zallet = start_zallet_mock().await; + let zaino = start_zaino_mock().await; + let router = start_router(&zebra.base_url(), &zallet.base_url(), &zaino.base_url()).await; + + let resp = Client::new() + .post(format!("http://127.0.0.1:{}/", router.port)) + .json(&json!({ "jsonrpc": "2.0", "id": 1, "method": "getwalletinfo", "params": [] })) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + let body: Value = resp.json().await.unwrap(); + assert_eq!(body["result"], "zallet-response"); +} + +#[tokio::test] +async fn test_unknown_method_falls_back_to_zebra() { + let zebra = start_zebra_mock().await; + let zallet = start_zallet_mock().await; + let zaino = start_zaino_mock().await; + let router = start_router(&zebra.base_url(), &zallet.base_url(), &zaino.base_url()).await; + + let resp = Client::new() + .post(format!("http://127.0.0.1:{}/", router.port)) + .json( + &json!({ "jsonrpc": "2.0", "id": 1, "method": "z_getaddressforaccount", "params": [] }), + ) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + let body: Value = resp.json().await.unwrap(); + assert_eq!(body["result"], "zebra-response"); +} diff --git a/rpc-router/src/main.rs b/rpc-router/src/main.rs new file mode 100644 index 0000000..6ab3e76 --- /dev/null +++ b/rpc-router/src/main.rs @@ -0,0 +1,390 @@ +//! RPC Router for Zebra, Zallet and Zaino. +//! +//! This service listens for JSON-RPC requests and routes them to the appropriate backend +//! based on the method being called. It merges the OpenRPC schemas from Zebra and Zallet +//! to determine which methods belong to which service. +use std::{env, net::SocketAddr, sync::Arc}; + + +use anyhow::Result; +use base64::{engine::general_purpose, Engine as _}; +use http_body_util::{BodyExt, Full}; +use hyper::{ + body::Bytes, + header::{HeaderName, HeaderValue}, + server::conn::http1, + service::service_fn, + Request, Response, StatusCode, Uri, +}; +use hyper_util::{ + client::legacy::Client as HyperClient, + rt::{TokioExecutor, TokioIo}, +}; +use reqwest::Client as ReqwestClient; +use serde::Deserialize; +use serde_json::{json, Value}; +use tokio::net::TcpListener; +use tracing::{error, info, warn}; + +mod defaults; + +#[cfg(test)] +mod unit_tests; + +#[cfg(test)] +mod integration_tests; + +/// Structure to parse incoming JSON-RPC requests. +#[derive(Deserialize, Debug)] +struct RpcRequest { + method: String, +} + +/// Configuration for the RPC router. +#[derive(Clone)] +struct Config { + zebra_url: String, + zallet_url: String, + _zaino_url: String, + rpc_user: String, + rpc_password: String, + cors_origin: String, + listen_port: u16, +} + +impl Config { + fn from_env() -> Self { + Self { + zebra_url: env::var("ZEBRA_URL") + .unwrap_or_else(|_| defaults::ZEBRA_URL.to_string()), + zallet_url: env::var("ZALLET_URL") + .unwrap_or_else(|_| defaults::ZALLET_URL.to_string()), + _zaino_url: env::var("ZAINO_URL").unwrap_or_else(|_| defaults::ZAINO_URL.to_string()), + rpc_user: env::var("RPC_USER").unwrap_or_else(|_| defaults::RPC_USER.to_string()), + rpc_password: env::var("RPC_PASSWORD").unwrap_or_else(|_| defaults::RPC_PASSWORD.to_string()), + cors_origin: env::var("CORS_ORIGIN") + .unwrap_or_else(|_| defaults::CORS_ORIGIN.to_string()), + listen_port: env::var("LISTEN_PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or(defaults::LISTEN_PORT), + } + } +} + +/// Merged schema and method lists for routing. +#[derive(Clone)] +struct Z3Schema { + zebra_methods: Vec, + zallet_methods: Vec, + merged: Value, +} + +/// Forwards the incoming request to the specified target URL with basic auth. +async fn forward_request( + req: Request>, + target_url: &str, + rpc_user: &str, + rpc_password: &str, +) -> Result>> { + let client = HyperClient::builder(TokioExecutor::new()).build_http(); + + let uri_string = format!( + "{}{}", + target_url, + req.uri() + .path_and_query() + .map(|x| x.as_str()) + .unwrap_or("/") + ); + let uri: Uri = uri_string.parse()?; + + let (parts, body) = req.into_parts(); + let mut new_req = Request::builder() + .method(parts.method) + .uri(uri) + .version(parts.version); + + // Inject auth header + let auth = general_purpose::STANDARD.encode(format!("{}:{}", rpc_user, rpc_password)); + new_req = new_req.header( + hyper::header::AUTHORIZATION, + hyper::header::HeaderValue::from_str(&format!("Basic {}", auth))?, + ); + + // Copy other headers + for (k, v) in parts.headers { + if let Some(key) = k { + new_req = new_req.header(key, v); + } + } + + let new_req = new_req.body(body)?; + let res = client.request(new_req).await?; + + let (parts, body) = res.into_parts(); + let body_bytes = body.collect().await?.to_bytes(); + let new_res = Response::from_parts(parts, Full::new(body_bytes)); + + Ok(new_res) +} + +/// Adds CORS headers to the response. +fn add_cors_headers(mut resp: Response>, cors_origin: &str) -> Response> { + let headers = resp.headers_mut(); + headers.insert( + HeaderName::from_static("access-control-allow-origin"), + HeaderValue::from_str(cors_origin).unwrap_or(HeaderValue::from_static("*")), + ); + for &(name, value) in &[ + ("access-control-allow-methods", "POST, OPTIONS"), + ("access-control-allow-headers", "Content-Type"), + ("access-control-max-age", "86400"), + ] { + headers.insert( + HeaderName::from_static(name), + HeaderValue::from_static(value), + ); + } + + resp +} + +/// Main request handler. +async fn handler( + req: Request, + config: Arc, + z3: Z3Schema, +) -> Result>> { + // Health check + if req.uri().path() == "/health" { + return Ok(Response::new(Full::new(Bytes::from("OK")))); + } + + // Handle CORS preflight + if req.method() == hyper::Method::OPTIONS { + let resp = add_cors_headers( + Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Full::new(Bytes::new())) + .unwrap(), + &config.cors_origin, + ); + return Ok(resp); + } + + // Only handle POST requests for JSON-RPC + if req.method() != hyper::Method::POST { + return Ok(Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .body(Full::new(Bytes::from("Method Not Allowed"))) + .unwrap()); + } + + // Buffer the body to parse it + let (parts, body) = req.into_parts(); + let body_bytes = body.collect().await?.to_bytes(); + + // Attempt to parse method from body + let target_url = if let Ok(rpc_req) = serde_json::from_slice::(&body_bytes) { + if rpc_req.method == "rpc.discover" { + info!("Routing rpc.discover to merged schema"); + + return Ok(add_cors_headers( + Response::builder() + .status(StatusCode::OK) + .header(hyper::header::CONTENT_TYPE, "application/json") + .body(Full::new(Bytes::from(serde_json::to_string(&z3.merged)?))) + .expect("z3 merged schema response should be valid"), + &config.cors_origin, + )); + } + + if z3.zebra_methods.iter().any(|m| m["name"] == rpc_req.method) { + info!("Routing {} to Zebra", rpc_req.method); + &config.zebra_url + } else if z3 + .zallet_methods + .iter() + .any(|m| m["name"] == rpc_req.method) + { + info!("Routing {} to Zallet", rpc_req.method); + &config.zallet_url + } else { + warn!( + "Method '{}' not found in Zebra or Zallet schema, falling back to Zebra", + rpc_req.method + ); + &config.zebra_url + } + } else { + warn!("Failed to parse JSON-RPC body, defaulting to Zebra"); + &config.zebra_url + }; + + // Reconstruct request with buffered body + let new_req = Request::from_parts(parts, Full::new(body_bytes)); + + match forward_request(new_req, target_url, &config.rpc_user, &config.rpc_password).await { + Ok(res) => Ok(add_cors_headers(res, &config.cors_origin)), + Err(e) => { + error!("Forwarding error: {}", e); + + Ok(Response::builder() + .status(StatusCode::BAD_GATEWAY) + .body(Full::new(Bytes::from(format!("Bad Gateway: {}", e)))) + .unwrap()) + } + } +} + +/// Calls rpc.discover on the given URL and returns the parsed JSON response. +async fn call_rpc_discover( + url: &str, + rpc_user: &str, + rpc_password: &str, +) -> Result { + let client = ReqwestClient::new(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "rpc.discover", + "params": [] + }); + + let text = client + .post(url) + .basic_auth(rpc_user, Some(rpc_password)) + .header(reqwest::header::CONTENT_TYPE, "application/json") + .body(body.to_string()) + .send() + .await? + .error_for_status()? + .text() + .await?; + + let resp = serde_json::from_str::(&text)?; + + Ok(resp) +} + +/// Extracts the methods array from the OpenRPC schema. +fn extract_methods_array(schema: &Value) -> Vec { + schema["methods"].as_array().cloned().unwrap_or_default() +} + +/// Annotates each method with its origin server. +fn annotate_methods_with_server(methods: &mut Vec, server_name: &str) { + for m in methods { + if let Some(obj) = m.as_object_mut() { + obj.insert("x-server".to_string(), json!(server_name)); + } else { + warn!( + "Skipping non-object method entry while annotating for {}", + server_name + ); + } + } +} + +/// Merges the components.schemas from the given schema into the combined map. +fn merge_components_schemas(schema: &Value, combined: &mut serde_json::Map) { + if let Some(obj) = schema["components"]["schemas"].as_object() { + for (k, v) in obj { + combined.insert(k.clone(), v.clone()); + } + } +} + +/// Merges the OpenRPC schemas from Zebra and Zallet. +fn merge_openrpc_schemas(zebra: Value, zallet: Value) -> Result { + // Extract method arrays + let mut zebra_methods = extract_methods_array(&zebra); + let mut zallet_methods = extract_methods_array(&zallet); + + // Annotate each method with its origin + annotate_methods_with_server(&mut zebra_methods, "zebra"); + annotate_methods_with_server(&mut zallet_methods, "zallet"); + + // Merge schemas under components.schemas + let mut combined_schemas = serde_json::Map::new(); + merge_components_schemas(&zebra, &mut combined_schemas); + merge_components_schemas(&zallet, &mut combined_schemas); + + let mut combined_methods = Vec::new(); + combined_methods.extend(zebra_methods.clone()); + combined_methods.extend(zallet_methods.clone()); + + // Build final merged schema + let merged = json!({ + "openrpc": zebra["openrpc"].clone(), + "info": { + "title": env!("CARGO_PKG_NAME"), + "description": env!("CARGO_PKG_DESCRIPTION"), + "version": env!("CARGO_PKG_VERSION"), + }, + "servers": [ + { "name": "router", "url": "http://localhost:8080/" }, + ], + "methods": combined_methods, + "components": { + "schemas": combined_schemas + } + }); + + let z3 = Z3Schema { + zebra_methods, + zallet_methods, + merged, + }; + + Ok(z3) +} + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + + let config = Arc::new(Config::from_env()); + let addr = SocketAddr::from(([0, 0, 0, 0], config.listen_port)); + + let zebra_schema = call_rpc_discover(&config.zebra_url, &config.rpc_user, &config.rpc_password) + .await?["result"] + .clone(); + let zallet_schema = + call_rpc_discover(&config.zallet_url, &config.rpc_user, &config.rpc_password).await? + ["result"] + .clone(); + + let z3 = merge_openrpc_schemas(zebra_schema, zallet_schema)?; + + let listener = TcpListener::bind(addr).await?; + info!("RPC Router listening on {}", addr); + info!("You can use the following playground URL:"); + info!("{}", defaults::playground_url(addr)); + + run(config, listener, z3).await +} + +/// Accepts connections and dispatches requests. Extracted for testability. +async fn run(config: Arc, listener: TcpListener, z3: Z3Schema) -> Result<()> { + loop { + let (stream, _) = listener.accept().await?; + let io = TokioIo::new(stream); + let config = config.clone(); + + let z3 = z3.clone(); + tokio::task::spawn(async move { + if let Err(err) = http1::Builder::new() + .serve_connection( + io, + service_fn(move |req| handler(req, config.clone(), z3.clone())), + ) + .await + { + error!("Error serving connection: {:?}", err); + } + }); + } +} diff --git a/rpc-router/src/unit_tests.rs b/rpc-router/src/unit_tests.rs new file mode 100644 index 0000000..ee67ceb --- /dev/null +++ b/rpc-router/src/unit_tests.rs @@ -0,0 +1,160 @@ +use super::*; +use serial_test::serial; + +fn zebra_schema() -> Value { + json!({ + "openrpc": "1.2.6", + "methods": [ + { "name": "getblock", "params": [] }, + { "name": "getinfo", "params": [] } + ], + "components": { + "schemas": { + "BlockHash": { "type": "string" } + } + } + }) +} + +fn zallet_schema() -> Value { + json!({ + "openrpc": "1.2.6", + "methods": [ + { "name": "getwalletinfo", "params": [] }, + { "name": "z_sendmany", "params": [] } + ], + "components": { + "schemas": { + "WalletInfo": { "type": "object" } + } + } + }) +} + +// --- extract_methods_array --- + +#[test] +fn test_extract_methods_array_returns_methods() { + let schema = zebra_schema(); + let methods = extract_methods_array(&schema); + assert_eq!(methods.len(), 2); + assert_eq!(methods[0]["name"], "getblock"); + assert_eq!(methods[1]["name"], "getinfo"); +} + +#[test] +fn test_extract_methods_array_missing_key_returns_empty() { + let schema = json!({ "openrpc": "1.2.6" }); + let methods = extract_methods_array(&schema); + assert!(methods.is_empty()); +} + +// --- annotate_methods_with_server --- + +#[test] +fn test_annotate_methods_sets_x_server() { + let mut methods = extract_methods_array(&zebra_schema()); + annotate_methods_with_server(&mut methods, "zebra"); + for m in &methods { + assert_eq!(m["x-server"], "zebra"); + } +} + +#[test] +fn test_annotate_methods_non_object_entry_does_not_panic() { + // A schema that contains a non-object method entry (e.g. a bare string). + let schema = json!({ "methods": ["not-an-object", { "name": "getblock" }] }); + let mut methods = extract_methods_array(&schema); + // Must not panic — the non-object entry is silently skipped. + annotate_methods_with_server(&mut methods, "zebra"); + assert_eq!(methods[1]["x-server"], "zebra"); +} + +// --- merge_components_schemas --- + +#[test] +fn test_merge_components_schemas_combines_keys() { + let mut combined = serde_json::Map::new(); + merge_components_schemas(&zebra_schema(), &mut combined); + merge_components_schemas(&zallet_schema(), &mut combined); + assert!(combined.contains_key("BlockHash")); + assert!(combined.contains_key("WalletInfo")); +} + +#[test] +fn test_merge_components_schemas_missing_components_is_noop() { + let schema = json!({ "methods": [] }); + let mut combined = serde_json::Map::new(); + merge_components_schemas(&schema, &mut combined); + assert!(combined.is_empty()); +} + +#[test] +fn test_merge_components_schemas_last_write_wins_on_conflict() { + let schema_a = json!({ "components": { "schemas": { "Foo": { "type": "string" } } } }); + let schema_b = json!({ "components": { "schemas": { "Foo": { "type": "integer" } } } }); + let mut combined = serde_json::Map::new(); + merge_components_schemas(&schema_a, &mut combined); + merge_components_schemas(&schema_b, &mut combined); + assert_eq!(combined["Foo"]["type"], "integer"); +} + +// --- merge_openrpc_schemas --- + +#[test] +fn test_merge_openrpc_schemas_combined_method_count() { + let z3 = merge_openrpc_schemas(zebra_schema(), zallet_schema()).unwrap(); + assert_eq!(z3.zebra_methods.len(), 2); + assert_eq!(z3.zallet_methods.len(), 2); + assert_eq!(z3.merged["methods"].as_array().unwrap().len(), 4); +} + +#[test] +fn test_merge_openrpc_schemas_methods_annotated() { + let z3 = merge_openrpc_schemas(zebra_schema(), zallet_schema()).unwrap(); + for m in &z3.zebra_methods { + assert_eq!(m["x-server"], "zebra"); + } + for m in &z3.zallet_methods { + assert_eq!(m["x-server"], "zallet"); + } +} + +#[test] +fn test_merge_openrpc_schemas_components_merged() { + let z3 = merge_openrpc_schemas(zebra_schema(), zallet_schema()).unwrap(); + let schemas = &z3.merged["components"]["schemas"]; + assert!(schemas.get("BlockHash").is_some()); + assert!(schemas.get("WalletInfo").is_some()); +} + +#[test] +fn test_merge_openrpc_schemas_info_fields_present() { + let z3 = merge_openrpc_schemas(zebra_schema(), zallet_schema()).unwrap(); + assert!(z3.merged["info"]["title"].is_string()); + assert!(z3.merged["info"]["version"].is_string()); +} + +// --- Config::from_env --- + +#[test] +#[serial] +fn test_config_from_env_uses_defaults() { + env::remove_var("RPC_USER"); + env::remove_var("RPC_PASSWORD"); + let config = Config::from_env(); + assert_eq!(config.rpc_user, "zebra"); + assert_eq!(config.rpc_password, "zebra"); +} + +#[test] +#[serial] +fn test_config_from_env_reads_rpc_credentials() { + env::set_var("RPC_USER", "alice"); + env::set_var("RPC_PASSWORD", "s3cr3t"); + let config = Config::from_env(); + assert_eq!(config.rpc_user, "alice"); + assert_eq!(config.rpc_password, "s3cr3t"); + env::remove_var("RPC_USER"); + env::remove_var("RPC_PASSWORD"); +} diff --git a/scripts/check-zebra-readiness.sh b/scripts/check-zebra-readiness.sh new file mode 100755 index 0000000..535fb94 --- /dev/null +++ b/scripts/check-zebra-readiness.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Polls Zebra's readiness endpoint until it returns "ok". +# Run during initial sync to know when Zebra is ready for the rest of the stack. +# +# Usage: +# ./scripts/check-zebra-readiness.sh # mainnet (port 8080) +# ./scripts/check-zebra-readiness.sh 18080 # testnet +# ./scripts/check-zebra-readiness.sh 28080 # regtest + +set -euo pipefail + +PORT="${1:-8080}" +URL="http://127.0.0.1:${PORT}/ready" + +# Map the health port to the matching env file so the success message tells +# the operator the right docker compose invocation. +case "$PORT" in + 8080) ENV_FILE=".env.mainnet" ;; + 18080) ENV_FILE=".env.testnet" ;; + 28080) ENV_FILE=".env.regtest" ;; + *) ENV_FILE=".env." ;; +esac + +echo "Polling ${URL} every 30s." +echo "Initial sync takes hours: mainnet 24-72h, testnet 2-12h." +echo "Safe to Ctrl+C and re-run later; Zebra keeps syncing in the background." +echo + +while true; do + response="$(curl -s "$URL" || true)" + if [ "$response" = "ok" ]; then + echo + echo "Zebra is ready. Bring up the rest of the stack with:" + echo " docker compose --env-file ${ENV_FILE} up -d" + exit 0 + fi + echo "$(date '+%H:%M:%S') - not ready yet: ${response:-no response}" + sleep 30 +done diff --git a/scripts/fix-permissions.sh b/scripts/fix-permissions.sh new file mode 100755 index 0000000..8fc0ca5 --- /dev/null +++ b/scripts/fix-permissions.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# +# Permission fix utility for Z3 stack local volumes +# This script sets correct ownership and permissions on user-specified directories +# +# Usage: +# ./fix-permissions.sh +# +# Services: zebra, zaino, zallet, cookie +# +# Examples: +# ./fix-permissions.sh zebra /mnt/ssd/zebra-state +# ./fix-permissions.sh zaino /home/user/data/zaino +# ./fix-permissions.sh zallet ~/Documents/zallet-data +# ./fix-permissions.sh cookie /var/lib/z3/cookies +# + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Container UIDs/GIDs +ZEBRA_UID=10001 +ZEBRA_GID=10001 +ZAINO_UID=1000 +ZAINO_GID=1000 +ZALLET_UID=1000 +ZALLET_GID=1000 + +# Show usage +usage() { + echo "Usage: $0 " + echo "" + echo "Services:" + echo " zebra - Zebra blockchain state (UID:GID 10001:10001, perms 700)" + echo " zaino - Zaino indexer data (UID:GID 1000:1000, perms 700)" + echo " zallet - Zallet wallet data (UID:GID 1000:1000, perms 700)" + echo " cookie - Shared cookie directory (UID:GID 10001:10001, perms 750)" + echo "" + echo "Examples:" + echo " $0 zebra /mnt/ssd/zebra-state" + echo " $0 zaino /home/user/data/zaino" + echo " $0 zallet ~/Documents/zallet-data" + exit 1 +} + +# Check arguments +if [[ $# -ne 2 ]]; then + usage +fi + +SERVICE="$1" +DIR_PATH="$2" + +# Validate service +case "$SERVICE" in + zebra) + OWNER_UID=$ZEBRA_UID + OWNER_GID=$ZEBRA_GID + PERMS=700 + ;; + zaino) + OWNER_UID=$ZAINO_UID + OWNER_GID=$ZAINO_GID + PERMS=700 + ;; + zallet) + OWNER_UID=$ZALLET_UID + OWNER_GID=$ZALLET_GID + PERMS=700 + ;; + cookie) + OWNER_UID=$ZEBRA_UID + OWNER_GID=$ZEBRA_GID + PERMS=755 + echo -e "${YELLOW}NOTE: cookie permissions are handled by the cookie-permissions${NC}" + echo "sidecar in docker-compose.yml at runtime. It chmods the .cookie file to" + echo "0644 inside the volume so any consumer uid can read it. Bind-mounting" + echo "the cookie path is advanced; the default Docker named volume is preferred." + echo "" + ;; + *) + echo -e "${RED}Error: Unknown service '$SERVICE'${NC}" + usage + ;; +esac + +# Check if directory exists +if [[ ! -d "$DIR_PATH" ]]; then + echo -e "${RED}Error: Directory does not exist: ${DIR_PATH}${NC}" + echo "Please create the directory first:" + echo " mkdir -p ${DIR_PATH}" + exit 1 +fi + +# Check if running with sudo +if [[ $EUID -ne 0 ]]; then + echo -e "${YELLOW}This script needs sudo to set ownership.${NC}" + echo "Re-running with sudo..." + echo "" + exec sudo "$0" "$@" +fi + +echo -e "${GREEN}Z3 Stack - Fixing Permissions${NC}" +echo "Service: $SERVICE" +echo "Directory: $DIR_PATH" +echo "UID:GID: ${OWNER_UID}:${OWNER_GID}" +echo "Permissions: $PERMS" +echo "" + +# Set ownership and permissions +chown "${OWNER_UID}:${OWNER_GID}" "$DIR_PATH" +chmod "$PERMS" "$DIR_PATH" + +echo -e "${GREEN}✓ Permissions set successfully${NC}" +echo "" +echo "To use this directory, update your .env file:" +case "$SERVICE" in + zebra) + echo " Z3_CHAIN_DATA_PATH=${DIR_PATH}" + ;; + zaino) + echo " Z3_ZAINO_DATA_PATH=${DIR_PATH}" + ;; + zallet) + echo " Z3_ZALLET_DATA_PATH=${DIR_PATH}" + ;; + cookie) + echo " Z3_COOKIE_PATH=${DIR_PATH}" + ;; +esac +echo "" diff --git a/scripts/regtest-init.sh b/scripts/regtest-init.sh new file mode 100755 index 0000000..0df7f8d --- /dev/null +++ b/scripts/regtest-init.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# regtest-init.sh: initialize the regtest wallet for the first time. +# +# Delegates first-run file setup to scripts/setup-network.sh, then performs +# the regtest-specific steps: write the Zallet RPC password hash, mine the +# activation blocks, and generate the wallet mnemonic. +# +# Run this once before starting the regtest stack. Safe to re-run. +# +# Requirements: +# - Docker with Docker Compose v2.24.4+ +# - rage-keygen, openssl (used by setup-network.sh) +# - No running z3-regtest-* containers + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +ENV_FILE="$REPO_ROOT/.env.regtest" +COMPOSE="docker compose --env-file $ENV_FILE" + +# Source the env file so port + project vars are available in this shell. +set -a +# shellcheck disable=SC1090 +. "$ENV_FILE" +set +a + +PROJECT="${COMPOSE_PROJECT_NAME:-z3-regtest}" +ZEBRA_HOST_RPC="${Z3_ZEBRA_HOST_RPC_PORT:-29232}" +ZAINO_HOST_GRPC="${Z3_ZAINO_HOST_GRPC_PORT:-28137}" +CONFIG_DIR="${Z3_CONFIG_DIR:-./config/regtest}" + +log() { + printf '%s\n' "$*" +} + +require_compose_v2() { + # This stack relies on the colon-separated COMPOSE_FILE merge and the + # !override tag, both Docker Compose v2.24.4+ features. The legacy v1 + # `docker-compose` binary cannot load them, so we require the v2 plugin + # and do not fall back to v1. + local ver need="2.24.4" + # A single `docker compose version --short` both proves the v2 plugin + # exists (non-zero exit otherwise) and yields the version to gate on. + if ! ver="$(docker compose version --short 2>/dev/null)"; then + log "Docker Compose v2 is required (the 'docker compose' plugin)." + log "The legacy 'docker-compose' v1 binary cannot load this stack's" + log "COMPOSE_FILE merge and !override tag. Install Compose v2.24.4+:" + log " https://docs.docker.com/compose/install/" + exit 1 + fi + ver="${ver#v}" + if [ -n "$ver" ] && [ "$(printf '%s\n%s\n' "$need" "$ver" | sort -V | head -n1)" != "$need" ]; then + log "Docker Compose $ver found, but >= $need is required (the regtest" + log "overlay uses the !override tag). Upgrade Docker Compose." + exit 1 + fi +} + +ensure_openssl() { + if command -v openssl > /dev/null 2>&1; then + return + fi + + log "openssl is required to generate the zallet RPC password hash." + log "Install OpenSSL, then re-run this script." + exit 1 +} + +update_zallet_rpc_pwhash() { + local config_path="$REPO_ROOT/${CONFIG_DIR#./}/zallet.toml" + # Keep Zallet's RPC password hash aligned with the rpc-router credential. + local rpc_password="${Z3_REGTEST_RPC_ROUTER_PASSWORD:-zebra}" + local placeholder="__GENERATED_BY_INIT_SH__" + local salt + local hash + local pwhash + local tmp + + if [ ! -f "$config_path" ]; then + log "Missing zallet config: $config_path" + exit 1 + fi + + if ! grep -q '^pwhash = "' "$config_path"; then + log "Could not find a pwhash entry in $config_path" + exit 1 + fi + + if ! grep -q "pwhash = \"${placeholder}\"" "$config_path"; then + log "==> Zallet RPC pwhash already generated, skipping." + return + fi + + ensure_openssl + + salt="$(openssl rand -hex 16)" + hash="$(printf '%s' "$rpc_password" | openssl dgst -sha256 -mac HMAC -macopt "key:$salt" | awk '{print $NF}')" + + if [ -z "$hash" ]; then + log "Failed to generate zallet RPC password hash" + exit 1 + fi + + pwhash="${salt}\$${hash}" + tmp="$(mktemp "${TMPDIR:-/tmp}/zallet.toml.XXXXXX")" + + sed -E "s|^pwhash = \".*\"$|pwhash = \"${pwhash}\"|" "$config_path" > "$tmp" + mv "$tmp" "$config_path" + # mktemp creates the temp file mode 0600 and mv carries that mode onto the + # config; restore world-read so the zallet container (uid 1000) can read it. + chmod 644 "$config_path" + + log "==> Generated zallet RPC pwhash in ${CONFIG_DIR}/zallet.toml" +} + +require_compose_v2 + +DOCKER="docker" +if ! docker info > /dev/null 2>&1; then + DOCKER="sudo -E docker" + COMPOSE="sudo -E $COMPOSE" +fi + +cd "$REPO_ROOT" + +"$SCRIPT_DIR/setup-network.sh" regtest +update_zallet_rpc_pwhash + +# Clean up any leftover containers from previous runs. +$COMPOSE down --remove-orphans 2>/dev/null || true + +echo "==> Starting Zebra in regtest mode..." +$COMPOSE up -d zebra + +echo "==> Waiting for Zebra RPC to be ready..." +until curl -sf -X POST \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"getblockchaininfo","params":[],"id":1}' \ + "http://127.0.0.1:${ZEBRA_HOST_RPC}" > /dev/null 2>&1; do + echo " Zebra not ready yet, retrying..." + sleep 2 +done +echo " Zebra is ready." + +echo "==> Mining 2 blocks (NU5/Orchard activates at height 2 to match Zaino's regtest defaults)..." +curl -s -u zebra:zebra \ + -X POST -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"generate","params":[2],"id":1}' \ + "http://127.0.0.1:${ZEBRA_HOST_RPC}" | grep -q '"result"' +echo " Blocks mined." + +# Compose names the Zallet data volume as ${COMPOSE_PROJECT_NAME}-zallet. +ZALLET_VOLUME="${PROJECT}-zallet" + +echo "==> Running init-wallet-encryption..." +# Remove stale lock file and wallet database if present (left by a previous +# interrupted run). wallet.db will be recreated with the correct schema by +# init-wallet-encryption; leaving a stale one causes a schema mismatch error. +$DOCKER run --rm -v "${ZALLET_VOLUME}:/data" busybox \ + sh -c 'rm -f /data/.lock /data/wallet.db' + +# generate-mnemonic stores an age-encrypted file; if one exists the full init +# sequence has already completed successfully. +ALREADY_INIT=$($DOCKER run --rm -v "${ZALLET_VOLUME}:/data" busybox \ + sh -c 'ls /data/*.age 2>/dev/null | wc -l') +if [ "${ALREADY_INIT:-0}" -gt 0 ]; then + echo " Wallet already initialized, skipping." +else + $COMPOSE run --rm zallet --datadir /var/lib/zallet --config /etc/zallet/zallet.toml init-wallet-encryption + echo "==> Running generate-mnemonic..." + $COMPOSE run --rm zallet --datadir /var/lib/zallet --config /etc/zallet/zallet.toml generate-mnemonic +fi + +echo "==> Stopping Zebra (will be restarted by docker compose up -d)..." +$COMPOSE down + +echo "" +echo "Wallet initialized. Now run:" +echo " docker compose --env-file .env.regtest up -d" +echo " Router available at http://127.0.0.1:8181" +echo " Zaino gRPC available at 127.0.0.1:${ZAINO_HOST_GRPC}" diff --git a/scripts/setup-network.sh b/scripts/setup-network.sh new file mode 100755 index 0000000..81aa45f --- /dev/null +++ b/scripts/setup-network.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# setup-network.sh: idempotent first-run setup for a z3 network. +# +# Copies per-network .example TOML templates into the live gitignored paths +# that docker-compose.yml mounts; generates a Zallet identity and shared TLS +# cert if missing. +# +# Usage: +# ./scripts/setup-network.sh +# +# Safe to re-run: every step skips if its output already exists. The live +# TOMLs and identity file are local and gitignored. +# +# For regtest, this script only handles file setup; the operational steps +# (mining the activation block, generating the wallet mnemonic) live in +# scripts/regtest-init.sh, which delegates here first. + +set -euo pipefail + +NETWORK="${1:-}" +case "$NETWORK" in + mainnet|testnet|regtest) ;; + *) + echo "Usage: $0 " >&2 + exit 1 + ;; +esac + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +CONFIG_DIR="$REPO_ROOT/config/$NETWORK" + +log() { printf '%s\n' "$*"; } + +copy_template() { + local file="$1" + local example="$CONFIG_DIR/$file.example" + local active="$CONFIG_DIR/$file" + + if [ -f "$active" ]; then + log "==> $NETWORK/$file: present, leaving operator copy untouched." + return + fi + + if [ ! -f "$example" ]; then + log "FAIL: missing template $example" >&2 + exit 1 + fi + + cp "$example" "$active" + # Make the config world-readable so the pinned zallet container uid (1000) + # can read it regardless of the operator's host uid/umask. These TOMLs are + # not secret; the age key is handled separately in ensure_identity. + chmod 644 "$active" + log "==> $NETWORK/$file: created from .example template." +} + +ensure_identity() { + local identity="$CONFIG_DIR/zallet_identity.txt" + + if [ -f "$identity" ]; then + log "==> $NETWORK/zallet_identity.txt: present." + return + fi + + if ! command -v rage-keygen >/dev/null 2>&1; then + log "FAIL: rage-keygen not found." >&2 + log " Install rage from https://github.com/str4d/rage/releases" >&2 + exit 1 + fi + + rage-keygen -o "$identity" + chmod 600 "$identity" + # Zallet runs as uid 1000 (distroless image, no runtime chown). Grant that + # uid read access to the age key without widening it to other host users. + if command -v setfacl >/dev/null 2>&1; then + setfacl -m u:1000:r "$identity" \ + || log "WARN: setfacl failed on $identity; zallet (uid 1000) may not be able to read it." + else + log "WARN: setfacl not found. Grant uid 1000 read on $identity before starting zallet" + log " (install the 'acl' package, or 'chmod 644 $identity' to allow all local users)." + fi + log "==> $NETWORK/zallet_identity.txt: generated." +} + +mkdir -p "$CONFIG_DIR" + +copy_template zaino.toml +copy_template zallet.toml +# Regtest needs a Zebra TOML to activate NU5/NU6 at heights Zaino expects. +# Mainnet and testnet use Zebra's built-in network defaults. +if [ "$NETWORK" = "regtest" ]; then + copy_template zebra.toml +fi +ensure_identity + +log +log "Setup complete for $NETWORK." +log "Next: docker compose --env-file .env.$NETWORK up -d zebra" diff --git a/scripts/validate-contract-parity.py b/scripts/validate-contract-parity.py new file mode 100755 index 0000000..78aee14 --- /dev/null +++ b/scripts/validate-contract-parity.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""validate-contract-parity.py: env-var inventory parity test. + +Asserts that the set of operator-facing environment variables in +z3-contract.yaml matches reality: + + compose substitutions -> env_vars (every ${VAR} must be contracted) + env_vars <-> .env.example (mutual: no orphans either way) + +Complements validate-contract.py, which asserts the per-network port +matrix against resolved compose output. + +Exit codes: + 0 all parity checks pass + 1 one or more parity checks failed + 2 prerequisite missing (PyYAML, files) +""" + +from __future__ import annotations + +import pathlib +import re +import sys + +try: + import yaml +except ImportError: + print("FAIL: PyYAML is required. Install with: pip install pyyaml", file=sys.stderr) + sys.exit(2) + +ROOT = pathlib.Path(__file__).resolve().parent.parent + +COMPOSE_FILES = [ + ROOT / "docker-compose.yml", + ROOT / "docker-compose.regtest.yml", +] +CONTRACT_FILE = ROOT / "z3-contract.yaml" +ENV_EXAMPLE = ROOT / ".env.example" + +VAR_NAME = r"[A-Z][A-Z0-9_]+" + +# $${VAR} is Compose's literal-escape (resolved by the container shell, not +# Compose), so the negative lookbehind skips it. ${VAR} and ${VAR:-default} +# both match. +COMPOSE_SUB_RE = re.compile(rf"(? str: + return path.read_text() if path.exists() else "" + + +def collect_compose_vars() -> set[str]: + found: set[str] = set() + for f in COMPOSE_FILES: + found |= {m.group(1) for m in COMPOSE_SUB_RE.finditer(read_text(f))} + return found + + +def collect_contract_vars(contract: dict) -> set[str]: + env_vars: set[str] = set() + for _, entries in contract.get("env_vars", {}).items(): + env_vars |= {e["name"] for e in entries} + return env_vars + + +def collect_ecosystem_vars(contract: dict) -> set[str]: + return {e["name"] for e in contract.get("ecosystem_vars", [])} + + +def collect_env_example_vars() -> set[str]: + return {m.group(1) for m in ENV_LINE_RE.finditer(read_text(ENV_EXAMPLE))} + + +def report_diff(subset_label: str, subset: set[str], + superset_label: str, superset: set[str]) -> int: + missing = subset - superset + if not missing: + print(f" OK every var in {subset_label} is present in {superset_label}") + return 0 + print(f" FAIL {len(missing)} var(s) in {subset_label} missing from {superset_label}:") + for v in sorted(missing): + print(f" - {v}") + return 1 + + +def main() -> int: + for f in [*COMPOSE_FILES, CONTRACT_FILE, ENV_EXAMPLE]: + if not f.exists(): + print(f"FAIL: missing {f.relative_to(ROOT)}") + return 1 + + contract = yaml.safe_load(CONTRACT_FILE.read_text()) + compose = collect_compose_vars() + env_vars = collect_contract_vars(contract) + ecosystem_vars = collect_ecosystem_vars(contract) + env_example = collect_env_example_vars() + + documented = env_vars | ecosystem_vars + + failures = 0 + + # Compose substitutions may include ecosystem-standard names (RUST_LOG, + # RUST_BACKTRACE) used as fallback chains; allow them. + print("== Compose substitutions vs contract (env_vars u ecosystem_vars) ==") + failures += report_diff("compose substitutions", compose, + "z3-contract.yaml (env + ecosystem)", documented) + + # .env.example documents both z3 surface and ecosystem fallbacks the + # operator may want to set. + print() + print("== Contract (env_vars u ecosystem_vars) vs .env.example ==") + failures += report_diff("z3-contract.yaml env_vars", env_vars, ".env.example", env_example) + failures += report_diff(".env.example", env_example, + "z3-contract.yaml (env + ecosystem)", documented) + + print() + if failures == 0: + print("PASS: contract inventory is in sync with compose and .env.example.") + return 0 + print(f"FAIL: {failures} parity check(s) failed.") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/validate-contract.py b/scripts/validate-contract.py new file mode 100755 index 0000000..819a077 --- /dev/null +++ b/scripts/validate-contract.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python3 +"""validate-contract.py: assert resolved Compose output matches z3-contract.yaml. + +Parses the platform contract's port matrix, volume names, RPC auth mode, and +service DNS directly from z3-contract.yaml, runs `docker compose --env-file +.env. --profile monitoring config` for each declared +network, and asserts that the resolved values match. + +Volumes and ports can be plain values or {name, profile} / {container, host, +profile} dicts. Profile-gated entries are checked the same way: the renderer +runs with all profiles enabled, so every contracted identifier is expected +in the output. + +Requires PyYAML. Pre-installed on GitHub Actions ubuntu-latest. For local +runs: pip install pyyaml. + +Exit codes: + 0 every assertion passes + 1 one or more assertions failed + 2 prerequisite missing (docker, PyYAML, env files, contract) +""" + +from __future__ import annotations + +import pathlib +import re +import subprocess +import sys + +try: + import yaml +except ImportError: + print("FAIL: PyYAML is required. Install with: pip install pyyaml", file=sys.stderr) + sys.exit(2) + +ROOT = pathlib.Path(__file__).resolve().parent.parent +CONTRACT_FILE = ROOT / "z3-contract.yaml" +PROMETHEUS_CONFIG = ROOT / "observability" / "prometheus" / "prometheus.yaml" + + +def volume_name(entry: str | dict) -> str: + """volumes.zaino can be a string or {name, profile?} dict.""" + return entry if isinstance(entry, str) else entry["name"] + + +class Asserter: + def __init__(self) -> None: + self.failures = 0 + + def present(self, label: str, pattern: str, haystack: str) -> None: + if re.search(pattern, haystack, re.MULTILINE): + print(f" OK {label}") + else: + print(f" FAIL {label} (expected pattern: {pattern})") + self.failures += 1 + + def absent(self, label: str, pattern: str, haystack: str) -> None: + if re.search(pattern, haystack, re.MULTILINE): + print(f" FAIL {label} (unexpected pattern: {pattern})") + self.failures += 1 + else: + print(f" OK {label}") + + def fail(self, label: str) -> None: + print(f" FAIL {label}") + self.failures += 1 + + +def load_contract() -> dict: + if not CONTRACT_FILE.exists(): + print(f"FAIL: missing {CONTRACT_FILE.relative_to(ROOT)}", file=sys.stderr) + sys.exit(2) + return yaml.safe_load(CONTRACT_FILE.read_text()) + + +def compose_files_for_network(network_name: str) -> list[str]: + files = ["docker-compose.yml"] + overlay = ROOT / f"docker-compose.{network_name}.yml" + if overlay.exists(): + files.append(overlay.name) + return files + + +def render_compose(network_name: str, env_file: pathlib.Path) -> str: + # Pass -f explicitly so local override files do not affect contract checks. + compose_args: list[str] = [] + for compose_file in compose_files_for_network(network_name): + compose_args.extend(["-f", compose_file]) + + try: + proc = subprocess.run( + [ + "docker", "compose", + *compose_args, + "--env-file", str(env_file), + "--profile", "monitoring", + "config", + ], + cwd=ROOT, capture_output=True, text=True, check=True, + ) + return proc.stdout + except FileNotFoundError: + print("FAIL: docker not found in PATH. Install Docker Engine with the", + file=sys.stderr) + print(" Compose v2 plugin: https://docs.docker.com/compose/install/", + file=sys.stderr) + sys.exit(2) + except subprocess.CalledProcessError as e: + stderr = e.stderr or "" + if "is not a docker command" in stderr or "'compose'" in stderr: + print("FAIL: the 'docker compose' v2 plugin is not available.", + file=sys.stderr) + print(" This stack requires Compose v2.24.4+; the legacy v1", + file=sys.stderr) + print(" 'docker-compose' binary is not supported. Install v2:", + file=sys.stderr) + print(" https://docs.docker.com/compose/install/", file=sys.stderr) + sys.exit(2) + print(f"FAIL: docker compose config failed for {env_file.name}:") + print(stderr) + sys.exit(1) + + +def validate_no_undeclared_host_ports(asserter: Asserter, ports: dict, config: str) -> None: + """Assert compose publishes no host port the contract omits (compose -> contract).""" + contract_hosts = {p["host"] for p in ports.values() if p.get("host") is not None} + rendered_hosts = {int(p) for p in re.findall(r'published: "(\d+)"', config)} + undeclared = sorted(rendered_hosts - contract_hosts) + for extra in undeclared: + asserter.fail(f"published host port {extra} is not declared in the contract") + if not undeclared: + print(" OK no undeclared published host ports") + + +def validate_network(asserter: Asserter, network_name: str, spec: dict) -> None: + env_file = ROOT / f".env.{network_name}" + if not env_file.exists(): + print(f" FAIL: missing {env_file.relative_to(ROOT)}") + asserter.failures += 1 + return + + print(f"== Network: {network_name} ({env_file.name}) ==") + config = render_compose(network_name, env_file) + + project = spec["compose_project"] + asserter.present(f"project name = {project}", + rf"^name: {re.escape(project)}$", config) + asserter.present(f"external network = {spec['external_network']}", + rf"^ name: {re.escape(spec['external_network'])}$", config) + + for _, vol_entry in spec["volumes"].items(): + vol = volume_name(vol_entry) + asserter.present(f"volume = {vol}", + rf"name: {re.escape(vol)}$", config) + + ports = spec["ports"] + + # Zebra container ports appear in the service environment block. + asserter.present( + f"Zebra RPC listen = {ports['zebra_rpc']['container']}", + rf"ZEBRA_RPC__LISTEN_ADDR: 0\.0\.0\.0:{ports['zebra_rpc']['container']}$", + config, + ) + asserter.present( + f"Zebra metrics listen = {ports['zebra_metrics']['container']}", + rf"ZEBRA_METRICS__ENDPOINT_ADDR: 0\.0\.0\.0:{ports['zebra_metrics']['container']}$", + config, + ) + + auth_mode = spec.get("rpc_auth", {}).get("mode") + if auth_mode == "cookie": + asserter.present( + "Zebra cookie auth enabled", + r"ZEBRA_RPC__ENABLE_COOKIE_AUTH: ['\"]?true['\"]?$", + config, + ) + asserter.present( + "Zaino cookie path configured", + r"ZAINO_VALIDATOR_SETTINGS__VALIDATOR_COOKIE_PATH: /var/run/auth/\.cookie$", + config, + ) + elif auth_mode == "username_password": + asserter.present( + "Zebra cookie auth disabled", + r"ZEBRA_RPC__ENABLE_COOKIE_AUTH: ['\"]?false['\"]?$", + config, + ) + asserter.absent( + "Zaino cookie path omitted", + r"ZAINO_VALIDATOR_SETTINGS__VALIDATOR_COOKIE_PATH:", + config, + ) + + # Host ports: every contracted port that has a host mapping must be + # published. Entries without a host key (zebra_metrics on every network, + # zebra_p2p on regtest) are container-only, so the assertion is skipped. + for key, port_spec in ports.items(): + host = port_spec.get("host") + if host is None: + continue + asserter.present(f"{key} host = {host}", + rf'published: "{host}"', config) + + # Bidirectional guard: compose must not publish a host port the contract + # omits (the loop above already covers contract -> compose). + validate_no_undeclared_host_ports(asserter, ports, config) + + # Zaino must point at Zebra's per-network RPC container port. + asserter.present( + f"Zaino -> Zebra RPC = {ports['zebra_rpc']['container']}", + rf"ZAINO_VALIDATOR_SETTINGS__VALIDATOR_JSONRPC_LISTEN_ADDRESS: " + rf"zebra:{ports['zebra_rpc']['container']}", + config, + ) + + +def validate_healthchecks(asserter: Asserter, network_name: str, + config: str, healthchecks: dict) -> None: + """Spot-check rendered healthcheck.test shape against contract.healthchecks. + + Catches drift like flipping Zebra's healthcheck from /ready to /healthy, + removing Zaino's TCP probe, or accidentally adding a healthcheck to a + service the contract declares as transport: none. + + Regtest's Zebra healthcheck diverges (peerless network uses an RPC probe + instead of /ready); the contract documents this so it is skipped here. + """ + for service, spec in healthchecks.items(): + transport = spec.get("transport") + port = spec.get("port") + + if service == "zebra" and network_name == "regtest": + asserter.present( + "Regtest Zebra healthcheck = getblockchaininfo RPC", + r"getblockchaininfo", config, + ) + continue + + if transport == "http" and spec.get("readiness"): + pattern = rf"http://127\.0\.0\.1:{port}{re.escape(spec['readiness'])}" + asserter.present( + f"{service} healthcheck = http {port}{spec['readiness']}", + pattern, config, + ) + elif transport == "tcp": + pattern = rf"/dev/tcp/127\.0\.0\.1/{port}" + asserter.present( + f"{service} healthcheck = tcp {port}", + pattern, config, + ) + # transport: none is not asserted positively (Zallet has no probe binary). + + +def validate_unique_host_ports(asserter: Asserter, contract: dict) -> None: + seen: dict[int, str] = {} + for network_name, spec in contract["networks"].items(): + for key, port_spec in spec["ports"].items(): + host = port_spec.get("host") + if host is None: + continue + label = f"{network_name}.{key}" + existing = seen.get(host) + if existing is not None: + print(f" FAIL host port {host} used by both {existing} and {label}") + asserter.failures += 1 + else: + seen[host] = label + + +def validate_prometheus_scrape_target(asserter: Asserter, contract: dict) -> None: + """Assert the Prometheus zebra job targets the contract's zebra_metrics container port.""" + if not PROMETHEUS_CONFIG.exists(): + print(" SKIP observability/prometheus/prometheus.yaml not found") + return + + expected_port = contract["networks"]["mainnet"]["ports"]["zebra_metrics"]["container"] + config = PROMETHEUS_CONFIG.read_text() + pattern = rf'job_name:\s*["\']?zebra["\']?[\s\S]*?targets:\s*\[\s*["\']zebra:{expected_port}["\']' + if re.search(pattern, config): + print(f" OK Prometheus zebra scrape target = zebra:{expected_port}") + else: + print(f" FAIL Prometheus zebra scrape target does not match contract") + print(f" (expected zebra:{expected_port} in {PROMETHEUS_CONFIG.relative_to(ROOT)})") + asserter.failures += 1 + + +def main() -> int: + contract = load_contract() + asserter = Asserter() + + healthchecks = contract.get("healthchecks", {}) + for net_name, net_spec in contract["networks"].items(): + validate_network(asserter, net_name, net_spec) + if healthchecks: + env_file = ROOT / f".env.{net_name}" + if env_file.exists(): + rendered = render_compose(net_name, env_file) + print(f"-- Healthchecks ({net_name}) --") + validate_healthchecks(asserter, net_name, rendered, healthchecks) + + print("== Cross-network host ports ==") + validate_unique_host_ports(asserter, contract) + + print() + print("== Prometheus scrape target ==") + validate_prometheus_scrape_target(asserter, contract) + + print() + if asserter.failures == 0: + print("PASS: all contract assertions hold.") + return 0 + print(f"FAIL: {asserter.failures} assertion(s) did not hold.") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/vendor.sh b/scripts/vendor.sh new file mode 100755 index 0000000..477ca28 --- /dev/null +++ b/scripts/vendor.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Clone the upstream node sources into vendor/ at the tags matching the pinned +# images in docker-compose.yml, for the opt-in source-build overlay +# (docker-compose.build.yml). vendor/ is gitignored. +# +# Usage: +# scripts/vendor.sh # fetch all three (zebra, zaino, zallet) +# scripts/vendor.sh zaino # fetch one +# Re-run after bumping an image pin to update the matching checkout. +# +# Tags track the image pins in docker-compose.yml; bump them together. +set -euo pipefail +cd "$(dirname "$0")/.." + +declare -a ALL=(zebra zaino zallet) +declare -A URL=( + [zebra]="https://github.com/ZcashFoundation/zebra" + [zaino]="https://github.com/zingolabs/zaino" + [zallet]="https://github.com/zcash/wallet" +) +declare -A REF=( + [zebra]="v5.0.0" + [zaino]="0.4.0-rc.2" + [zallet]="v0.1.0-alpha.3" +) + +vendor_one() { + local name="$1" dir="vendor/$1" url="${URL[$1]}" ref="${REF[$1]}" + if [ -d "$dir/.git" ]; then + echo "==> $name: fetching $ref" + git -C "$dir" fetch --depth 1 origin "$ref" + git -C "$dir" checkout --quiet --recurse-submodules FETCH_HEAD + git -C "$dir" submodule update --init --recursive --depth 1 + else + echo "==> $name: cloning $ref" + git clone --depth 1 --branch "$ref" --recurse-submodules --shallow-submodules \ + "$url" "$dir" + fi +} + +targets=("$@") +[ ${#targets[@]} -eq 0 ] && targets=("${ALL[@]}") +for t in "${targets[@]}"; do + [ -n "${URL[$t]:-}" ] || { echo "unknown component: $t (choose: ${ALL[*]})" >&2; exit 1; } + vendor_one "$t" +done + +echo +echo "Done. Build with:" +echo " docker compose -f docker-compose.yml -f docker-compose.build.yml build ${targets[*]}" diff --git a/z3-contract.schema.json b/z3-contract.schema.json new file mode 100644 index 0000000..10bc3e6 --- /dev/null +++ b/z3-contract.schema.json @@ -0,0 +1,204 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/ZcashFoundation/z3/blob/main/z3-contract.schema.json", + "title": "Z3 Platform Contract", + "description": "Schema for z3-contract.yaml. Consumers validate the parsed YAML against this schema before relying on its identifiers.", + "type": "object", + "required": [ + "contract_version", + "versioning", + "service_dns", + "cookie_path", + "networks", + "healthchecks", + "env_vars", + "profiles" + ], + "additionalProperties": false, + "properties": { + "contract_version": { + "type": "string", + "pattern": "^\\d+\\.\\d+\\.\\d+$", + "description": "SemVer string. Bumped per the policy under versioning:." + }, + "versioning": { + "type": "object", + "required": ["policy", "major", "minor", "patch"], + "additionalProperties": false, + "properties": { + "policy": {"const": "semver"}, + "major": {"type": "string"}, + "minor": {"type": "string"}, + "patch": {"type": "string"} + } + }, + "service_dns": { + "type": "object", + "description": "Map of service name to its in-network DNS identifier. Entries with a profile: key only resolve when that Compose profile is active.", + "additionalProperties": {"$ref": "#/$defs/dnsEntry"} + }, + "cookie_path": { + "type": "string", + "pattern": "^/", + "description": "Absolute path of the RPC cookie file inside every container that needs auth." + }, + "networks": { + "type": "object", + "required": ["mainnet", "testnet", "regtest"], + "additionalProperties": false, + "properties": { + "mainnet": {"$ref": "#/$defs/network"}, + "testnet": {"$ref": "#/$defs/network"}, + "regtest": {"$ref": "#/$defs/network"} + } + }, + "healthchecks": { + "type": "object", + "additionalProperties": {"$ref": "#/$defs/healthcheck"} + }, + "env_vars": { + "type": "object", + "description": "Public env-var schema, grouped by function.", + "additionalProperties": { + "type": "array", + "items": {"$ref": "#/$defs/envVar"} + } + }, + "ecosystem_vars": { + "type": "array", + "description": "Ecosystem-standard env vars documented for completeness but not part of the z3 contract.", + "items": {"$ref": "#/$defs/envVar"} + }, + "profiles": { + "type": "object", + "description": "Compose profiles available across all networks.", + "additionalProperties": {"type": "string"} + }, + "image_platforms": { + "type": "object", + "description": "Platform constraints per image. Each value is the list of platforms the upstream image publishes; entries with only linux/amd64 run under emulation on arm64 hosts.", + "additionalProperties": { + "type": "array", + "items": {"type": "string", "pattern": "^linux/(amd64|arm64)$"}, + "minItems": 1 + } + } + }, + "$defs": { + "profileName": { + "type": "string", + "enum": ["monitoring"] + }, + "networkName": { + "type": "string", + "enum": ["mainnet", "testnet", "regtest"] + }, + "namespaceName": { + "type": "string", + "enum": ["z3", "zebra", "zaino", "grafana", "compose", "docker", "rust"] + }, + "dnsEntry": { + "type": "object", + "required": ["dns"], + "additionalProperties": false, + "properties": { + "dns": {"type": "string"}, + "profile": {"$ref": "#/$defs/profileName"} + } + }, + "volumeEntry": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "required": ["name"], + "additionalProperties": false, + "properties": { + "name": {"type": "string"}, + "profile": {"$ref": "#/$defs/profileName"} + } + } + ] + }, + "portEntry": { + "type": "object", + "required": ["container"], + "additionalProperties": false, + "properties": { + "container": {"type": "integer", "minimum": 1, "maximum": 65535}, + "host": {"type": "integer", "minimum": 1, "maximum": 65535}, + "profile": {"$ref": "#/$defs/profileName"} + } + }, + "network": { + "type": "object", + "required": ["z3_network", "compose_project", "external_network", "rpc_auth", "volumes", "ports"], + "additionalProperties": false, + "properties": { + "z3_network": {"type": "string", "enum": ["Mainnet", "Testnet", "Regtest"]}, + "compose_project": {"type": "string", "pattern": "^z3-(mainnet|testnet|regtest)$"}, + "external_network": {"type": "string", "pattern": "^z3-(mainnet|testnet|regtest)$"}, + "rpc_auth": {"$ref": "#/$defs/rpcAuth"}, + "volumes": { + "type": "object", + "additionalProperties": {"$ref": "#/$defs/volumeEntry"} + }, + "ports": { + "type": "object", + "additionalProperties": {"$ref": "#/$defs/portEntry"} + } + } + }, + "rpcAuth": { + "type": "object", + "required": ["mode"], + "additionalProperties": false, + "properties": { + "mode": {"type": "string", "enum": ["cookie", "username_password"]}, + "credential_env_vars": { + "type": "object", + "required": ["user", "password"], + "additionalProperties": false, + "properties": { + "user": {"type": "string", "pattern": "^[A-Z][A-Z0-9_]*$"}, + "password": {"type": "string", "pattern": "^[A-Z][A-Z0-9_]*$"} + } + }, + "note": {"type": "string"} + }, + "allOf": [ + { + "if": { "properties": { "mode": { "const": "username_password" } }, "required": ["mode"] }, + "then": { "required": ["credential_env_vars"] } + } + ] + }, + "healthcheck": { + "type": "object", + "required": ["transport"], + "additionalProperties": false, + "properties": { + "transport": {"type": "string", "enum": ["http", "tcp", "cli", "none"]}, + "port": {"type": "integer", "minimum": 1, "maximum": 65535}, + "liveness": {"type": ["string", "null"]}, + "readiness": {"type": ["string", "null"]}, + "profile": {"$ref": "#/$defs/profileName"}, + "note": {"type": "string"} + } + }, + "envVar": { + "type": "object", + "required": ["name", "namespace"], + "additionalProperties": false, + "properties": { + "name": {"type": "string", "pattern": "^[A-Z][A-Z0-9_]*$"}, + "namespace": {"$ref": "#/$defs/namespaceName"}, + "description": {"type": "string"}, + "scope": {"type": "string", "enum": ["host", "container"]}, + "values": {"type": "array", "items": {"type": "string"}}, + "profile": {"$ref": "#/$defs/profileName"}, + "network": {"$ref": "#/$defs/networkName"} + } + } + } +} diff --git a/z3-contract.yaml b/z3-contract.yaml new file mode 100644 index 0000000..f55349e --- /dev/null +++ b/z3-contract.yaml @@ -0,0 +1,254 @@ +# Z3 platform contract. +# +# This file is the canonical machine-readable inventory of stable identifiers: +# networks, volumes, ports, env vars, service DNS, healthchecks, and image +# platforms. Consumers can validate it against z3-contract.schema.json. The +# repository validators check it against the resolved Compose output: +# scripts/validate-contract.py per-network port matrix + volumes +# scripts/validate-contract-parity.py env-var inventory across compose / .env.example +# +# Companion artifacts: +# z3-contract.schema.json JSON Schema for this file +# docs/contract.md concept guide (stability promise, file ownership, .env loading) +# docs/integrations/ integration examples + +contract_version: "1.0.0" + +# Version-bump policy. Independent from the z3 git tag. +versioning: + policy: semver + major: "Breaking: renamed/removed identifier, port change, removed service" + minor: "Additive: new optional field, new env var, new optional service" + patch: "Documentation-only or clarification" + +# In-network DNS names. Same on every Z3 network; the network itself +# is the discriminator at the Compose-project level. Entries with a +# `profile:` key only resolve when that Compose profile is active. +service_dns: + zebra: {dns: zebra} + zaino: {dns: zaino} + zallet: {dns: zallet} + +# Cookie file path inside containers on networks whose rpc_auth.mode is cookie. +cookie_path: "/var/run/auth/.cookie" + +# Per-network identifiers and port matrix. +# +# Container ports follow Zebra's per-network defaults from upstream +# (zebra-chain/src/parameters/network.rs and zebra-rpc/src/config/rpc.rs). +# Host ports are explicit so multiple networks coexist on one host. +# Port entries with a `profile:` key only appear when that profile is active. +networks: + mainnet: + z3_network: "Mainnet" + compose_project: "z3-mainnet" + external_network: "z3-mainnet" + rpc_auth: + mode: cookie + volumes: + chain: "z3-mainnet-chain" + cookie: "z3-mainnet-cookie" + zaino: "z3-mainnet-zaino" + zallet: "z3-mainnet-zallet" + ports: + zebra_rpc: {container: 8232, host: 8232} + zebra_p2p: {container: 8233, host: 8233} # published for inbound peers + zebra_health: {container: 8080, host: 8080} + zebra_metrics: {container: 9999} # in-network Prometheus scrape endpoint + zaino_grpc: {container: 8137, host: 8137} + zaino_json_rpc: {container: 8237, host: 8237} + zallet_rpc: {container: 28232, host: 28232} + prometheus: {container: 9090, host: 9094, profile: monitoring} + grafana: {container: 3000, host: 3000, profile: monitoring} + jaeger_ui: {container: 16686, host: 16686, profile: monitoring} + jaeger_otlp_grpc: {container: 4317, host: 4317, profile: monitoring} + jaeger_otlp_http: {container: 4318, host: 4318, profile: monitoring} + jaeger_spanmetrics: {container: 8889, host: 8889, profile: monitoring} + alertmanager: {container: 9093, host: 9093, profile: monitoring} + + testnet: + z3_network: "Testnet" + compose_project: "z3-testnet" + external_network: "z3-testnet" + rpc_auth: + mode: cookie + volumes: + chain: "z3-testnet-chain" + cookie: "z3-testnet-cookie" + zaino: "z3-testnet-zaino" + zallet: "z3-testnet-zallet" + ports: + # Health, Zaino, Zallet host ports use a +10000 offset relative to + # mainnet so the two networks can run concurrently on the same host. + zebra_rpc: {container: 18232, host: 18232} + zebra_p2p: {container: 18233, host: 18233} + zebra_health: {container: 8080, host: 18080} + zebra_metrics: {container: 9999} + zaino_grpc: {container: 8137, host: 18137} + zaino_json_rpc: {container: 8237, host: 18237} + zallet_rpc: {container: 28232, host: 40232} + prometheus: {container: 9090, host: 19094, profile: monitoring} + grafana: {container: 3000, host: 13000, profile: monitoring} + jaeger_ui: {container: 16686, host: 26686, profile: monitoring} + jaeger_otlp_grpc: {container: 4317, host: 15317, profile: monitoring} + jaeger_otlp_http: {container: 4318, host: 15318, profile: monitoring} + jaeger_spanmetrics: {container: 8889, host: 18889, profile: monitoring} + alertmanager: {container: 9093, host: 19093, profile: monitoring} + + regtest: + z3_network: "Regtest" + compose_project: "z3-regtest" + external_network: "z3-regtest" + rpc_auth: + mode: username_password + credential_env_vars: + user: Z3_REGTEST_RPC_ROUTER_USER + password: Z3_REGTEST_RPC_ROUTER_PASSWORD + note: "Cookie auth is disabled; rpc-router and service configs use username/password. The cookie volume may exist, but it does not carry a readable RPC cookie." + volumes: + chain: "z3-regtest-chain" + cookie: "z3-regtest-cookie" + zaino: "z3-regtest-zaino" + zallet: "z3-regtest-zallet" + ports: + # Regtest inherits Zebra's testnet container-port defaults. + # Host ports are explicit to avoid cross-service collisions. + zebra_rpc: {container: 18232, host: 29232} + zebra_p2p: {container: 18233} + zebra_health: {container: 8080, host: 28080} + zebra_metrics: {container: 9999} + zaino_grpc: {container: 8137, host: 28137} + zaino_json_rpc: {container: 8237, host: 28237} + zallet_rpc: {container: 28232, host: 50232} + rpc_router: {container: 8181, host: 8181} + prometheus: {container: 9090, host: 29094, profile: monitoring} + grafana: {container: 3000, host: 23000, profile: monitoring} + jaeger_ui: {container: 16686, host: 36686, profile: monitoring} + jaeger_otlp_grpc: {container: 4317, host: 25317, profile: monitoring} + jaeger_otlp_http: {container: 4318, host: 25318, profile: monitoring} + jaeger_spanmetrics: {container: 8889, host: 28889, profile: monitoring} + alertmanager: {container: 9093, host: 29093, profile: monitoring} + +# Per-service health endpoints. Consumers attached to the external network +# can wait via depends_on with condition: service_healthy. +healthchecks: + zebra: + transport: http + port: 8080 + liveness: "/healthy" # peer connectivity (>= ZEBRA_HEALTH__MIN_CONNECTED_PEERS) + readiness: "/ready" # synced within ZEBRA_HEALTH__READY_MAX_BLOCKS_BEHIND of tip + zaino: + transport: tcp + port: 8137 + note: "TCP probe on the gRPC port. Confirms a listener exists; does not validate the gRPC handler. Consumers should not rely on this for production routing decisions." + zallet: + transport: none + note: "No healthcheck: distroless image has no shell or probe binary. A consumer using depends_on: condition: service_healthy against zallet will hang. Wait for Zebra's /ready instead and assume Zallet is up shortly after." + +# Public env-var schema. +# +# Grouped by function (what the var configures). Each entry carries a +# `namespace:` tag identifying the convention the name follows: +# +# z3 Stack-level setting +# zebra Zebra config-rs (double-underscore is config-rs nesting separator) +# zaino Zaino config-rs +# grafana Grafana GF_* convention +# compose Docker Compose's own knobs +# docker Docker / Compose ecosystem convention +# +# Native-namespace entries (zebra, zaino, grafana) are passed through +# unchanged. +env_vars: + network: + - {name: COMPOSE_PROJECT_NAME, namespace: compose, description: "Compose project name; selects the per-network instance."} + - {name: Z3_NETWORK, namespace: z3, values: [Mainnet, Testnet, Regtest], description: "Network selector. PascalCase per Zebra's serde."} + - {name: Z3_CONFIG_DIR, namespace: z3, description: "Per-network config dir (zallet.toml, zaino.toml, zallet_identity.txt)."} + + images: + # Image-pin defaults live in docker-compose.yml as ${VAR:-tag} fallbacks. + # The env vars below override those defaults at runtime; see image_platforms + # at the end of this file for per-image platform constraints. + - {name: Z3_ZEBRA_IMAGE, namespace: z3} + - {name: Z3_ZAINO_IMAGE, namespace: z3} + - {name: Z3_ZALLET_IMAGE, namespace: z3} + - {name: Z3_ZEBRA_BUILD_FEATURES, namespace: z3, description: "Build arg when building Zebra from source."} + - {name: DOCKER_PLATFORM, namespace: docker, description: "Optional platform pin. Zebra 5.x is multi-arch (no pin needed); Zaino and Zallet publish amd64 only and are pinned in compose. Override to build Zaino/Zallet native arm64 from source (docker-compose.build.yml)."} + + ports: + # Host ports (z3 controls the per-network port matrix; testnet/regtest offset). + - {name: Z3_ZEBRA_HOST_RPC_PORT, namespace: z3, scope: host} + - {name: Z3_ZEBRA_HOST_P2P_PORT, namespace: z3, scope: host, description: "Published p2p port for inbound peers (mainnet/testnet; regtest is peerless and omits it)."} + - {name: Z3_ZEBRA_HOST_HEALTH_PORT, namespace: z3, scope: host} + - {name: Z3_ZAINO_HOST_GRPC_PORT, namespace: z3, scope: host} + - {name: Z3_ZAINO_HOST_JSON_RPC_PORT, namespace: z3, scope: host} + - {name: Z3_ZALLET_HOST_RPC_PORT, namespace: z3, scope: host} + - {name: Z3_REGTEST_RPC_ROUTER_HOST_PORT, namespace: z3, scope: host, network: regtest, description: "rpc-router host port (regtest only)."} + # Container ports: only Zebra's vary per network (upstream Zebra defaults + # 8232 mainnet / 18232 testnet). Zaino and Zallet container ports + # are hardcoded in the compose because they do not differ per network. + - {name: Z3_ZEBRA_RPC_PORT, namespace: z3, scope: container} + - {name: Z3_ZEBRA_P2P_PORT, namespace: z3, scope: container, description: "Zebra p2p port; network default 8233 mainnet / 18233 testnet."} + - {name: ZEBRA_NETWORK__EXTERNAL_ADDR, namespace: zebra, description: "Address advertised to peers for inbound p2p when behind NAT or a firewall."} + + paths: + - {name: Z3_CHAIN_DATA_PATH, namespace: z3, description: "Override chain volume with a bind-mount path."} + - {name: Z3_ZAINO_DATA_PATH, namespace: z3} + - {name: Z3_ZALLET_DATA_PATH, namespace: z3} + - {name: Z3_COOKIE_PATH, namespace: z3, description: "Override cookie volume (advanced; breaks the shared-volume pattern)."} + + auth: + - {name: ZEBRA_RPC__ENABLE_COOKIE_AUTH, namespace: zebra, description: "Toggles Zebra RPC cookie auth. Disabled in regtest."} + # rpc-router runs only under the regtest overlay; the REGTEST infix marks scope. + - {name: Z3_REGTEST_RPC_ROUTER_USER, namespace: z3, network: regtest, description: "rpc-router credential (regtest only)."} + - {name: Z3_REGTEST_RPC_ROUTER_PASSWORD, namespace: z3, network: regtest, description: "rpc-router credential (regtest only)."} + + health: + - {name: ZEBRA_HEALTH__MIN_CONNECTED_PEERS, namespace: zebra} + - {name: ZEBRA_HEALTH__READY_MAX_BLOCKS_BEHIND, namespace: zebra} + - {name: ZEBRA_HEALTH__ENFORCE_ON_TEST_NETWORKS, namespace: zebra} + + logging: + # Per-service RUST_LOG split. Each compose entry chains + # Z3__RUST_LOG -> RUST_LOG -> default, so a global RUST_LOG=debug + # flips the whole stack and per-service overrides still win. + - {name: Z3_ZEBRA_RUST_LOG, namespace: z3} + - {name: Z3_ZAINO_RUST_LOG, namespace: z3} + - {name: Z3_ZALLET_RUST_LOG, namespace: z3} + - {name: ZEBRA_TRACING__FILTER, namespace: zebra, description: "Advanced: Zebra's own tracing-subscriber filter (independent of RUST_LOG)."} + + mining: + - {name: ZEBRA_MINING__MINER_ADDRESS, namespace: zebra, description: "Required for regtest mining."} + + observability: + - {name: Z3_PROMETHEUS_PORT, namespace: z3, profile: monitoring} + - {name: Z3_GRAFANA_PORT, namespace: z3, profile: monitoring} + - {name: GF_SECURITY_ADMIN_PASSWORD, namespace: grafana, profile: monitoring, description: "Grafana admin password. Native GF_* name (no z3 wrapper)."} + - {name: Z3_JAEGER_UI_PORT, namespace: z3, profile: monitoring} + - {name: Z3_ALERTMANAGER_PORT, namespace: z3, profile: monitoring} + - {name: Z3_JAEGER_OTLP_GRPC_PORT, namespace: z3, profile: monitoring} + - {name: Z3_JAEGER_OTLP_HTTP_PORT, namespace: z3, profile: monitoring} + - {name: Z3_JAEGER_SPANMETRICS_PORT, namespace: z3, profile: monitoring, description: "Jaeger spanmetrics Prometheus scrape port (Jaeger SPM)."} + - {name: ZEBRA_METRICS__ENDPOINT_ADDR, namespace: zebra, description: "Zebra Prometheus scrape endpoint. Default: 0.0.0.0:9999."} + - {name: ZEBRA_TRACING__OPENTELEMETRY_ENDPOINT, namespace: zebra} + - {name: ZEBRA_TRACING__OPENTELEMETRY_SERVICE_NAME, namespace: zebra} + - {name: ZEBRA_TRACING__OPENTELEMETRY_SAMPLE_PERCENT, namespace: zebra} + +# Ecosystem-standard names z3 inherits but does not own. Documented because +# operators may set them; parity checkers skip these. +ecosystem_vars: + - {name: COMPOSE_FILE, namespace: compose, description: "Compose's overlay loader. .env.regtest sets this to load the regtest overlay."} + - {name: RUST_LOG, namespace: rust, description: "Global Rust log level."} + - {name: RUST_BACKTRACE, namespace: rust, description: "Global Rust backtrace toggle. Zaino reads it directly (default: full); set in shell or .env to enable for Zebra/Zallet too."} + +# Compose profiles available across all networks. +profiles: + monitoring: "Prometheus + Grafana + Jaeger + AlertManager" + +# Platform constraints per image. Entries listed as amd64-only run under +# emulation on arm64 hosts even when DOCKER_PLATFORM=linux/arm64 is set. +# For native arm64 of Zaino and Zallet, build locally from source (docker-compose.build.yml). +image_platforms: + zebra: [linux/amd64, linux/arm64] + zaino: [linux/amd64] + zallet: [linux/amd64]