#!/usr/bin/env bash # Entrypoint for running Zebra in Docker. # # The main script logic is at the bottom. # # ## Notes # # - `$ZEBRA_CONF_PATH` can point to an existing Zebra config file, or if not set, # the script will look for a default config at ${HOME}/.config/zebrad.toml, # or generate one using environment variables. set -eo pipefail # These are the default cached state directories for Zebra and lightwalletd. # # They are set to `${HOME}/.cache/zebra` and `${HOME}/.cache/lwd` # respectively, but can be overridden by setting the # `ZEBRA_CACHE_DIR` and `LWD_CACHE_DIR` environment variables. : "${ZEBRA_CACHE_DIR:=${HOME}/.cache/zebra}" : "${LWD_CACHE_DIR:=${HOME}/.cache/lwd}" : "${ZEBRA_COOKIE_DIR:=${HOME}/.cache/zebra}" # Use gosu to drop privileges and execute the given command as the specified UID:GID exec_as_user() { user=$(id -u) if [[ ${user} == '0' ]]; then exec gosu "${UID}:${GID}" "$@" else exec "$@" fi } # Modifies the Zebra config file using environment variables. # # This function generates a new config file from scratch at ZEBRA_CONF_PATH # using the provided environment variables. # # It creates a complete configuration with network settings, state, RPC, # metrics, tracing, and mining sections based on environment variables. prepare_conf_file() { # Base configuration cat >"${ZEBRA_CONF_PATH}" <&2 exit 1 } # Creates a directory if it doesn't exist and sets ownership to specified UID:GID. # Also ensures the parent directories have the correct ownership. # # ## Parameters # # - $1: Directory path to create and own create_owned_directory() { local dir="$1" # Skip if directory is empty [[ -z ${dir} ]] && return # Create directory with parents mkdir -p "${dir}" || exit_error "Failed to create directory: ${dir}" # Set ownership for the created directory chown -R "${UID}:${GID}" "${dir}" || exit_error "Failed to secure directory: ${dir}" # Set ownership for parent directory (but not if it's root or home) local parent_dir parent_dir="$(dirname "${dir}")" if [[ "${parent_dir}" != "/" && "${parent_dir}" != "${HOME}" ]]; then chown "${UID}:${GID}" "${parent_dir}" fi } # Create and own cache and config directories [[ -n ${ZEBRA_CACHE_DIR} ]] && create_owned_directory "${ZEBRA_CACHE_DIR}" [[ -n ${LWD_CACHE_DIR} ]] && create_owned_directory "${LWD_CACHE_DIR}" [[ -n ${ZEBRA_COOKIE_DIR} ]] && create_owned_directory "${ZEBRA_COOKIE_DIR}" [[ -n ${LOG_FILE} ]] && create_owned_directory "$(dirname "${LOG_FILE}")" # Runs cargo test with an arbitrary number of arguments. # # Positional Parameters # # - '$1' must contain cargo FEATURES as described here: # https://doc.rust-lang.org/cargo/reference/features.html#command-line-feature-options # - The remaining params will be appended to a command starting with # `exec_as_user cargo test ... -- ...` run_cargo_test() { # Shift the first argument, as it's already included in the cmd local features="$1" shift # Start constructing the command array local cmd=(cargo test --locked --release --features "${features}" --package zebrad --test acceptance -- --nocapture --include-ignored) # Loop through the remaining arguments for arg in "$@"; do if [[ -n ${arg} ]]; then # If the argument is non-empty, add it to the command cmd+=("${arg}") fi done echo "Running: ${cmd[*]}" # Execute directly to become PID 1 exec_as_user "${cmd[@]}" } # Runs tests depending on the env vars. # # ## Positional Parameters # # - $@: Arbitrary command that will be executed if no test env var is set. run_tests() { if [[ "${RUN_ALL_TESTS}" -eq "1" ]]; then # Run unit, basic acceptance tests, and ignored tests, only showing command # output if the test fails. If the lightwalletd environment variables are # set, we will also run those tests. exec_as_user cargo test --locked --release --workspace --features "${FEATURES}" \ -- --nocapture --include-ignored --skip check_no_git_refs_in_cargo_lock elif [[ "${RUN_CHECK_NO_GIT_REFS}" -eq "1" ]]; then # Run the check_no_git_refs_in_cargo_lock test. exec_as_user cargo test --locked --release --workspace --features "${FEATURES}" \ -- --nocapture --include-ignored check_no_git_refs_in_cargo_lock elif [[ "${TEST_FAKE_ACTIVATION_HEIGHTS}" -eq "1" ]]; then # Run state tests with fake activation heights. exec_as_user cargo test --locked --release --lib --features "zebra-test" \ --package zebra-state \ -- --nocapture --include-ignored with_fake_activation_heights elif [[ "${TEST_SCANNER}" -eq "1" ]]; then # Test the scanner. exec_as_user cargo test --locked --release --package zebra-scan \ -- --nocapture --include-ignored scan_task_commands scan_start_where_left elif [[ "${TEST_ZEBRA_EMPTY_SYNC}" -eq "1" ]]; then # Test that Zebra syncs and checkpoints a few thousand blocks from an empty # state. run_cargo_test "${FEATURES}" "sync_large_checkpoints_" elif [[ -n "${FULL_SYNC_MAINNET_TIMEOUT_MINUTES}" ]]; then # Run a Zebra full sync test on mainnet. run_cargo_test "${FEATURES}" "full_sync_mainnet" elif [[ -n "${FULL_SYNC_TESTNET_TIMEOUT_MINUTES}" ]]; then # Run a Zebra full sync test on testnet. run_cargo_test "${FEATURES}" "full_sync_testnet" elif [[ "${TEST_DISK_REBUILD}" -eq "1" ]]; then # Run a Zebra sync up to the mandatory checkpoint. run_cargo_test "${FEATURES} test_sync_to_mandatory_checkpoint_${NETWORK,,}" \ "sync_to_mandatory_checkpoint_${NETWORK,,}" echo "ran test_disk_rebuild" elif [[ "${TEST_UPDATE_SYNC}" -eq "1" ]]; then # Run a Zebra sync starting at the cached tip, and syncing to the latest # tip. run_cargo_test "${FEATURES}" "zebrad_update_sync" elif [[ "${TEST_CHECKPOINT_SYNC}" -eq "1" ]]; then # Run a Zebra sync starting at the cached mandatory checkpoint, and syncing # past it. run_cargo_test "${FEATURES} test_sync_past_mandatory_checkpoint_${NETWORK,,}" \ "sync_past_mandatory_checkpoint_${NETWORK,,}" elif [[ "${GENERATE_CHECKPOINTS_MAINNET}" -eq "1" ]]; then # Generate checkpoints after syncing Zebra from a cached state on mainnet. # # TODO: disable or filter out logs like: # test generate_checkpoints_mainnet has been running for over 60 seconds run_cargo_test "${FEATURES}" "generate_checkpoints_mainnet" elif [[ "${GENERATE_CHECKPOINTS_TESTNET}" -eq "1" ]]; then # Generate checkpoints after syncing Zebra on testnet. # # This test might fail if testnet is unstable. run_cargo_test "${FEATURES}" "generate_checkpoints_testnet" elif [[ "${TEST_LWD_RPC_CALL}" -eq "1" ]]; then # Starting at a cached Zebra tip, test a JSON-RPC call to Zebra. # Run both the fully synced RPC test and the subtree snapshot test, one test # at a time. Since these tests use the same cached state, a state problem in # the first test can fail the second test. run_cargo_test "${FEATURES}" "--test-threads" "1" "fully_synced_rpc_" elif [[ "${TEST_LWD_INTEGRATION}" -eq "1" ]]; then # Test launching lightwalletd with an empty lightwalletd and Zebra state. run_cargo_test "${FEATURES}" "lightwalletd_integration" elif [[ "${TEST_LWD_FULL_SYNC}" -eq "1" ]]; then # Starting at a cached Zebra tip, run a lightwalletd sync to tip. run_cargo_test "${FEATURES}" "lightwalletd_full_sync" elif [[ "${TEST_LWD_UPDATE_SYNC}" -eq "1" ]]; then # Starting with a cached Zebra and lightwalletd tip, run a quick update sync. run_cargo_test "${FEATURES}" "lightwalletd_update_sync" # These tests actually use gRPC. elif [[ "${TEST_LWD_GRPC}" -eq "1" ]]; then # Starting with a cached Zebra and lightwalletd tip, test all gRPC calls to # lightwalletd, which calls Zebra. run_cargo_test "${FEATURES}" "lightwalletd_wallet_grpc_tests" elif [[ "${TEST_LWD_TRANSACTIONS}" -eq "1" ]]; then # Starting with a cached Zebra and lightwalletd tip, test sending # transactions gRPC call to lightwalletd, which calls Zebra. run_cargo_test "${FEATURES}" "sending_transactions_using_lightwalletd" # These tests use mining code, but don't use gRPC. elif [[ "${TEST_GET_BLOCK_TEMPLATE}" -eq "1" ]]; then # Starting with a cached Zebra tip, test getting a block template from # Zebra's RPC server. run_cargo_test "${FEATURES}" "get_block_template" elif [[ "${TEST_SUBMIT_BLOCK}" -eq "1" ]]; then # Starting with a cached Zebra tip, test sending a block to Zebra's RPC # port. run_cargo_test "${FEATURES}" "submit_block" else exec_as_user "$@" fi } # Main Script Logic # # 1. First check if ZEBRA_CONF_PATH is explicitly set or if a file exists at that path # 2. If not set but default config exists, use that # 3. If neither exists, generate a default config at ${HOME}/.config/zebrad.toml # 4. Print environment variables and config for debugging # 5. Process command-line arguments and execute appropriate action if [[ -n ${ZEBRA_CONF_PATH} ]]; then if [[ -f ${ZEBRA_CONF_PATH} ]]; then echo "ZEBRA_CONF_PATH was set to ${ZEBRA_CONF_PATH} and a file exists." echo "Using user-provided config file" else echo "ERROR: ZEBRA_CONF_PATH was set and no config file found at ${ZEBRA_CONF_PATH}." echo "Please ensure a config file exists or set ZEBRA_CONF_PATH to point to your config file." exit 1 fi else if [[ -f "${HOME}/.config/zebrad.toml" ]]; then echo "ZEBRA_CONF_PATH was not set." echo "Using default config at ${HOME}/.config/zebrad.toml" ZEBRA_CONF_PATH="${HOME}/.config/zebrad.toml" else echo "ZEBRA_CONF_PATH was not set and no default config found at ${HOME}/.config/zebrad.toml" echo "Preparing a default one..." ZEBRA_CONF_PATH="${HOME}/.config/zebrad.toml" create_owned_directory "$(dirname "${ZEBRA_CONF_PATH}")" prepare_conf_file fi fi echo "INFO: Using the following environment variables:" printenv echo "Using Zebra config at ${ZEBRA_CONF_PATH}:" cat "${ZEBRA_CONF_PATH}" # - If "$1" is "--", "-", or "zebrad", run `zebrad` with the remaining params. # - If "$1" is "test": # - and "$2" is "zebrad", run `zebrad` with the remaining params, # - else run tests with the remaining params. # - TODO: If "$1" is "monitoring", start a monitoring node. # - If "$1" doesn't match any of the above, run "$@" directly. case "$1" in --* | -* | zebrad) shift exec_as_user zebrad --config "${ZEBRA_CONF_PATH}" "$@" ;; test) shift if [[ "$1" == "zebrad" ]]; then shift exec_as_user zebrad --config "${ZEBRA_CONF_PATH}" "$@" else run_tests "$@" fi ;; monitoring) # TODO: Impl logic for starting a monitoring node. : ;; *) exec_as_user "$@" ;; esac