From a7200b121b0fa0ff4bddd20d415d1fea459cdcfa Mon Sep 17 00:00:00 2001 From: Shashank Date: Wed, 4 Nov 2026 14:05:41 +0530 Subject: [PATCH 1/4] running with curio --- .config/forest.dic | 3 +- .github/workflows/lotus-api-bump.yml | 2 +- .github/workflows/miner-tests.yml | 19 +- docs/docs/users/guides/running_with_curio.md | 11 +- docs/docs/users/reference/cli.md | 4 +- mise.toml | 8 +- scripts/devnet-forest-curio/.env | 17 ++ scripts/devnet-forest-curio/README.md | 17 ++ scripts/devnet-forest-curio/build_image.sh | 21 ++ scripts/devnet-forest-curio/check.sh | 19 ++ scripts/devnet-forest-curio/curio.dockerfile | 58 ++++ .../devnet-forest-curio/curio_entrypoint.sh | 130 +++++++++ scripts/devnet-forest-curio/devnet-layer.toml | 18 ++ .../devnet-forest-curio/docker-compose.yml | 148 ++++++++++ scripts/devnet-forest-curio/setup.sh | 47 +++ scripts/devnet-forest-miner/.env | 16 +- scripts/devnet-forest-miner/README.md | 5 +- scripts/devnet-forest-miner/check.sh | 46 +-- scripts/devnet-forest-miner/setup.sh | 21 +- scripts/devnet/.env | 1 + scripts/devnet/README.md | 4 +- scripts/devnet/chain_height.sh | 62 ++++ scripts/devnet/check.sh | 19 +- scripts/devnet/setup.sh | 21 +- scripts/devnet/test_harness.sh | 25 +- scripts/devnet/upgrade_heights.env | 16 ++ src/dev/subcommands/devnet_cmd/miner.rs | 43 ++- src/dev/subcommands/devnet_cmd/miner/api.rs | 46 +-- src/dev/subcommands/devnet_cmd/miner/curio.rs | 269 ++++++++++++++++++ .../devnet_cmd/miner/onboarding.rs | 77 +++-- 30 files changed, 1018 insertions(+), 175 deletions(-) create mode 100644 scripts/devnet-forest-curio/.env create mode 100644 scripts/devnet-forest-curio/README.md create mode 100755 scripts/devnet-forest-curio/build_image.sh create mode 100755 scripts/devnet-forest-curio/check.sh create mode 100644 scripts/devnet-forest-curio/curio.dockerfile create mode 100755 scripts/devnet-forest-curio/curio_entrypoint.sh create mode 100644 scripts/devnet-forest-curio/devnet-layer.toml create mode 100644 scripts/devnet-forest-curio/docker-compose.yml create mode 100755 scripts/devnet-forest-curio/setup.sh create mode 100644 scripts/devnet/chain_height.sh create mode 100644 scripts/devnet/upgrade_heights.env create mode 100644 src/dev/subcommands/devnet_cmd/miner/curio.rs diff --git a/.config/forest.dic b/.config/forest.dic index 47a8acc2cc4b..6bc3fc6bd79e 100644 --- a/.config/forest.dic +++ b/.config/forest.dic @@ -1,4 +1,4 @@ -303 +304 ABI Algorand/M API's @@ -64,6 +64,7 @@ coreos CPUs cron crypto +Curio daemon daemonize Datacap diff --git a/.github/workflows/lotus-api-bump.yml b/.github/workflows/lotus-api-bump.yml index 015510f53a69..f0c04817452f 100644 --- a/.github/workflows/lotus-api-bump.yml +++ b/.github/workflows/lotus-api-bump.yml @@ -26,7 +26,7 @@ jobs: for env in ./scripts/tests/api_compare/.env ./scripts/tests/bootstrapper/.env ./scripts/tests/snapshot_parity/.env; do sed -i "s/\(LOTUS_IMAGE=filecoin.*\):.*/\1:$TAG-calibnet/" "$env" done - for env in ./scripts/devnet/.env ./scripts/devnet-forest-miner/.env; do + for env in ./scripts/devnet/.env ./scripts/devnet-forest-miner/.env ./scripts/devnet-forest-curio/.env; do sed -i "s/\(LOTUS_IMAGE=filecoin.*\):.*/\1:$TAG-2k/" "$env" done else diff --git a/.github/workflows/miner-tests.yml b/.github/workflows/miner-tests.yml index 3cbe924663f2..95e0c5f982cf 100644 --- a/.github/workflows/miner-tests.yml +++ b/.github/workflows/miner-tests.yml @@ -77,18 +77,21 @@ jobs: if-no-files-found: error miner-tests: - name: Miner integration tests (${{ matrix.group }}) + name: Miner integration tests (${{ matrix.devnet }}, ${{ matrix.group }}) needs: build runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: - # The `forest-dev devnet miner --group` values. Add groups as more Lotus itests are ported. + # Add groups as more Lotus itests are ported. group: [onboarding] + devnet: [devnet-forest-miner, devnet-forest-curio] env: # We use a custom Dockerfile for CI to speed up the build process. FOREST_DOCKERFILE_OVERRIDE: scripts/devnet/forest_ci.dockerfile MINER_TEST_GROUP: ${{ matrix.group }} + DEVNET_DIR: scripts/${{ matrix.devnet }} + SCRIPT_TIMEOUT_MINUTES: 90 steps: - uses: actions/cache@v6 id: cache @@ -98,9 +101,9 @@ jobs: - name: Load cache into volume if: steps.cache.outputs.cache-hit == 'true' run: | - docker volume create devnet-forest-miner_filecoin-proofs + docker volume create "${{ matrix.devnet }}_filecoin-proofs" docker run --rm \ - -v devnet-forest-miner_filecoin-proofs:/proofs \ + -v "${{ matrix.devnet }}_filecoin-proofs":/proofs \ -v "$FIL_PROOFS_PARAMETER_CACHE":/cache \ "$SHELL_IMAGE" \ sh -c "cp /cache/* /proofs" @@ -111,10 +114,10 @@ jobs: with: name: "forest-${{ runner.os }}" - name: Devnet setup - run: ./scripts/devnet-forest-miner/setup.sh + run: ./${{ env.DEVNET_DIR }}/setup.sh timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }} - name: Devnet check - run: ./scripts/devnet-forest-miner/check.sh + run: ./${{ env.DEVNET_DIR }}/check.sh timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }} - name: Add forest binaries to PATH run: | @@ -123,7 +126,7 @@ jobs: - name: Miner tests run: | set -euo pipefail - export DEVNET_DIR=scripts/devnet-forest-miner + export DEVNET_DIR="${{ env.DEVNET_DIR }}" source ./scripts/devnet/test_harness.sh devnet_test_env_init forest-dev devnet miner --group "$MINER_TEST_GROUP" @@ -135,7 +138,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: | docker run --rm \ - -v devnet-forest-miner_filecoin-proofs:/proofs \ + -v "${{ matrix.devnet }}_filecoin-proofs":/proofs \ -v "$FIL_PROOFS_PARAMETER_CACHE":/cache \ "$SHELL_IMAGE" \ sh -c "cp /proofs/* /cache" diff --git a/docs/docs/users/guides/running_with_curio.md b/docs/docs/users/guides/running_with_curio.md index 2b54b4cac4cf..feb89c3c5905 100644 --- a/docs/docs/users/guides/running_with_curio.md +++ b/docs/docs/users/guides/running_with_curio.md @@ -3,8 +3,13 @@ title: Running Forest With Curio sidebar_position: 99 --- -:::note +[Curio](https://curiostorage.org/) is the successor to Lotus Miner. Forest can act as Curio's chain node (the same "Forest, Curio, YugabyteDB" layout Curio documents). -[Curio](https://curiostorage.org/) is the successor to Lotus-miner under active development by Curio Storage Inc. Compatibility with Forest is not yet confirmed. +Verified on the local stack in [`scripts/devnet-forest-curio`](../../../../scripts/devnet-forest-curio) against **Curio v1.29.0**: -::: +- Put an admin token in `FULLNODE_API_INFO` and keep the miner worker key in Forest's wallet (`MinerCreateBlock` and message signing use it). +- On a private network, the genesis name must start with `local` or `test` (Curio drops other chain nodes). The stack uses `localnet`. + +```shell +mise run test:devnet:miner:curio +``` diff --git a/docs/docs/users/reference/cli.md b/docs/docs/users/reference/cli.md index 5aefee220873..e73e9e2bc658 100644 --- a/docs/docs/users/reference/cli.md +++ b/docs/docs/users/reference/cli.md @@ -2931,7 +2931,7 @@ Commands: eth-gas `eth_estimateGas` parity tests eth-pending-tx `eth_getTransactionByHash` pending transaction tests eth-skip-sender Skip-sender integration tests that need a private chain with a miner - miner Lotus Miner integration tests on Forest (`scripts/devnet-forest-miner` only) + miner Miner integration tests on Forest (Lotus Miner or Curio; set `MINER_BACKEND` in the stack `.env`) state-call `Filecoin.StateCall` gas limit parity tests help Print this message or the help of the given subcommand(s) @@ -2975,7 +2975,7 @@ Options: ### `forest-dev devnet miner` ``` -Lotus Miner integration tests on Forest (`scripts/devnet-forest-miner` only) +Miner integration tests on Forest (Lotus Miner or Curio; set `MINER_BACKEND` in the stack `.env`) Usage: forest-dev devnet miner [OPTIONS] [FILTER] diff --git a/mise.toml b/mise.toml index 528841152d87..af091b44b3f6 100644 --- a/mise.toml +++ b/mise.toml @@ -249,10 +249,14 @@ run = "scripts/devnet/with_devnet.sh scripts/devnet devnet_integration_tests" description = "Run the integration test suites against the Forest-produced local docker devnet." run = "scripts/devnet/with_devnet.sh scripts/devnet-forest-miner devnet_integration_tests" -[tasks."test:devnet:miner"] -description = "Run the Lotus Miner integration tests against the Forest-produced local docker devnet." +[tasks."test:devnet:miner:lotus"] +description = "Run the Miner integration tests against the Forest-produced local docker devnet with Lotus Miner." run = "scripts/devnet/with_devnet.sh scripts/devnet-forest-miner forest-dev devnet miner" +[tasks."test:devnet:miner:curio"] +description = "Run the Miner integration tests against the Forest-produced local docker devnet with Curio." +run = "scripts/devnet/with_devnet.sh scripts/devnet-forest-curio forest-dev devnet miner" + [tasks."codecov:nextest"] description = "Run codecov with nextest" run = ''' diff --git a/scripts/devnet-forest-curio/.env b/scripts/devnet-forest-curio/.env new file mode 100644 index 000000000000..79db5a7350b8 --- /dev/null +++ b/scripts/devnet-forest-curio/.env @@ -0,0 +1,17 @@ +LOTUS_IMAGE=filecoin/lotus-all-in-one:v1.38.3-2k +# Curio rejects chain nodes whose network name does not start with local or test. +NETWORK_NAME=localnet +FOREST_DATA_DIR=/forest_data +LOTUS_DATA_DIR=/lotus_data +FIL_PROOFS_PARAMETER_CACHE=/var/tmp/filecoin-proof-parameters +MINER_ACTOR_ADDRESS=t01000 +MINER_BACKEND=curio +LOTUS_RPC_PORT=1234 +LOTUS_P2P_PORT=1235 +# Unused here; compose interpolates the extended forest-miner file which references it. +MINER_RPC_PORT=2345 +FOREST_RPC_PORT=3456 +FOREST_P2P_PORT=4903 +CURIO_COMMIT=v1.29.0 +CURIO_REPO_PATH=/var/lib/curio +YUGABYTE_IMAGE=yugabytedb/yugabyte:2025.2.2.2-b11@sha256:1fdda2ae928facf0b7b25b7c1183c540567c33730cce10a557c81c33a8e18422 diff --git a/scripts/devnet-forest-curio/README.md b/scripts/devnet-forest-curio/README.md new file mode 100644 index 000000000000..60705cc658ea --- /dev/null +++ b/scripts/devnet-forest-curio/README.md @@ -0,0 +1,17 @@ +# Forest-produced devnet (Curio seals and mines) + +Same chain topology as [`../devnet-forest-miner`](../devnet-forest-miner), with Curio in place of Lotus Miner: + +| | this stack | +| --------------- | ---------------------- | +| Block producer | **Forest** + **Curio** | +| Validating node | Lotus | +| HarmonyDB | Yugabyte | + +`NETWORK_NAME=localnet` — Curio rejects names that do not start with `local` or `test`. Upgrade heights are shared via [`../devnet/upgrade_heights.env`](../devnet/upgrade_heights.env) (a plain `2k` Curio binary assumes the wrong genesis schedule). + +`setup.sh` builds `forest/curio-devnet:` once from that release tag (about 15 minutes) and reuses it afterward. + +```shell +mise run test:devnet:miner:curio +``` diff --git a/scripts/devnet-forest-curio/build_image.sh b/scripts/devnet-forest-curio/build_image.sh new file mode 100755 index 000000000000..88f4f8cc5112 --- /dev/null +++ b/scripts/devnet-forest-curio/build_image.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Build forest/curio-devnet:${CURIO_COMMIT} when that tag is missing. +set -euo pipefail + +PARENT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +# shellcheck source=/dev/null +source "${PARENT_PATH}/.env" + +IMAGE="forest/curio-devnet:${CURIO_COMMIT}" +if docker image inspect "${IMAGE}" >/dev/null 2>&1; then + echo "Using existing ${IMAGE}" + exit 0 +fi + +echo "Building ${IMAGE} (linux/amd64) ..." +docker build \ + --platform linux/amd64 \ + --build-arg "CURIO_COMMIT=${CURIO_COMMIT}" \ + -f "${PARENT_PATH}/curio.dockerfile" \ + -t "${IMAGE}" \ + "${PARENT_PATH}" diff --git a/scripts/devnet-forest-curio/check.sh b/scripts/devnet-forest-curio/check.sh new file mode 100755 index 000000000000..f9d1d0768739 --- /dev/null +++ b/scripts/devnet-forest-curio/check.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Forest + Lotus past TARGET_HEIGHT (Curio mining via Forest). + +set -eu + +PARENT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +pushd "${PARENT_PATH}" +set -a +# shellcheck source=/dev/null +source .env +# shellcheck source=/dev/null +source "${PARENT_PATH}/../devnet/upgrade_heights.env" +set +a +# shellcheck source=/dev/null +source "${PARENT_PATH}/../devnet/chain_height.sh" + +wait_forest_and_lotus_past_target "Lotus validated Forest/Curio-produced blocks" "$(check_timeout_secs)" + +popd diff --git a/scripts/devnet-forest-curio/curio.dockerfile b/scripts/devnet-forest-curio/curio.dockerfile new file mode 100644 index 000000000000..ca83587f7b51 --- /dev/null +++ b/scripts/devnet-forest-curio/curio.dockerfile @@ -0,0 +1,58 @@ +# Curio 2k image for the Forest-produced Curio devnet. Tags and GOAMD64 must be +# set explicitly: Curio's Dockerfile clears CURIO_TAGS, and its Makefile hardcodes +# GOAMD64=v3 which SIGILLs under QEMU. +ARG CURIO_COMMIT +FROM rust:1.86.0-slim-bookworm AS rust-toolchain + +FROM golang:1.26-trixie AS curio-builder +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +ARG CURIO_COMMIT +RUN test -n "$CURIO_COMMIT" + +RUN apt-get update && \ + apt-get install --no-install-recommends -y ca-certificates build-essential clang \ + ocl-icd-opencl-dev ocl-icd-libopencl1 jq libhwloc-dev git && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=rust-toolchain /usr/local/cargo /usr/local/cargo +COPY --from=rust-toolchain /usr/local/rustup /usr/local/rustup +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:$PATH + +WORKDIR /opt/curio +RUN git clone https://github.com/filecoin-project/curio.git . && \ + git checkout "$CURIO_COMMIT" && \ + git submodule update --init + +# Portable BLST so the binary runs under QEMU; rewrite GOAMD64=v3 -> v1. +RUN sed -i 's/GOAMD64=v3/GOAMD64=v1/g' scripts/makefiles/30-build.mk + +ENV FFI_USE_OPENCL=1 \ + CURIO_TAGS="cunative nosupraseal 2k" \ + CGO_CFLAGS_ALLOW="-D__BLST_PORTABLE__" \ + CGO_CFLAGS="-D__BLST_PORTABLE__" \ + FFI_USE_BLST_PORTABLE=1 \ + FFI_USE_GPU=0 + +RUN make clean deps && make build && strip curio sptool + +FROM ubuntu:24.04 +RUN apt-get update && \ + apt-get install --no-install-recommends -y ca-certificates curl jq dnsutils && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=curio-builder /etc/ssl/certs /etc/ssl/certs +COPY --from=curio-builder /lib/*/libdl.so.2 /lib/ +COPY --from=curio-builder /lib/*/librt.so.1 /lib/ +COPY --from=curio-builder /lib/*/libgcc_s.so.1 /lib/ +COPY --from=curio-builder /lib/*/libutil.so.1 /lib/ +COPY --from=curio-builder /usr/lib/*/libltdl.so.7 /lib/ +COPY --from=curio-builder /usr/lib/*/libnuma.so.1 /lib/ +COPY --from=curio-builder /usr/lib/*/libhwloc.so.* /lib/ +COPY --from=curio-builder /usr/lib/*/libOpenCL.so.1 /lib/ +COPY --from=curio-builder /opt/curio/curio /opt/curio/sptool /usr/local/bin/ + +RUN curio --version && ! command -v lotus-miner + +CMD ["/bin/bash"] diff --git a/scripts/devnet-forest-curio/curio_entrypoint.sh b/scripts/devnet-forest-curio/curio_entrypoint.sh new file mode 100755 index 000000000000..69005f67f812 --- /dev/null +++ b/scripts/devnet-forest-curio/curio_entrypoint.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# Bootstrap Curio against Forest for genesis ${MINER_ACTOR_ADDRESS}, then run. +# +# Storage attach needs the live Curio API, so we start Curio first, cordon it so +# sealing/mining does not race ahead, attach genesis + seal storage (idempotent +# across restart:on-failure), then uncordon. `.init.ready` marks a healthy node +# for the compose healthcheck. +set -euo pipefail + +set +x +FULLNODE_API_INFO="$(cat "${FOREST_DATA_DIR}/token.jwt"):/dns/forest/tcp/${FOREST_RPC_PORT}/http" +export FULLNODE_API_INFO +set -x + +ADDR="$(hostname -I | tr ' ' '\n' | awk '$1 ~ /^[0-9]+\./ && $1 !~ /^127\./ {print $1; exit}')" +if [ -z "${ADDR}" ]; then + echo "failed to resolve container IPv4 for Curio --machine" >&2 + exit 1 +fi +MACHINE="${ADDR}:12300" + +LAYER_HASH="$(sha256sum /devnet-layer.toml | awk '{print $1}')" +if [ ! -f "${CURIO_REPO_PATH}/.init.config" ]; then + curio config new-cluster "${MINER_ACTOR_ADDRESS}" + curio config create --title devnet < /devnet-layer.toml + echo "${LAYER_HASH}" > "${CURIO_REPO_PATH}/.init.layer" + touch "${CURIO_REPO_PATH}/.init.config" +elif [ "$(cat "${CURIO_REPO_PATH}/.init.layer" 2>/dev/null || true)" != "${LAYER_HASH}" ]; then + echo "devnet-layer.toml changed since this curio-data volume was init'd; re-run setup.sh (it wipes curio-data)" >&2 + exit 1 +fi + +# Drop a stale ready marker until this boot finishes attach + uncordon. +rm -f "${CURIO_REPO_PATH}/.init.ready" + +CURIO_PID="" +cleanup() { + if [ -n "${CURIO_PID}" ] && kill -0 "${CURIO_PID}" 2>/dev/null; then + kill "${CURIO_PID}" 2>/dev/null || true + wait "${CURIO_PID}" 2>/dev/null || true + fi +} +trap cleanup EXIT TERM INT + +curio run --nosync --name forest-devnet --layers devnet & +CURIO_PID=$! + +until curio cli --machine "${MACHINE}" wait-api; do + if ! kill -0 "${CURIO_PID}" 2>/dev/null; then + echo "curio exited before its API came up" >&2 + wait "${CURIO_PID}" + exit 1 + fi + echo "waiting for curio API on ${MACHINE} ..." + sleep 2 +done + +# Hold scheduling until storage is ready (API is up, attach is next). +curio cli --machine "${MACHINE}" cordon + +# Always try to leave the machine schedulable if attach fails mid-boot. +uncordon() { + curio cli --machine "${MACHINE}" uncordon || true +} + +storage_path_listed() { + local path=$1 + local list + list=$(curio cli --machine "${MACHINE}" storage list 2>/dev/null) || return 1 + printf '%s\n' "${list}" | grep -qF -- "${path}" +} + +# A crash after --init leaves the path unlisted; retry without flags. +attach_storage() { + local label=$1 + shift + local out rc path + path=$(printf '%s\n' "$@" | awk '/^\// {p=$0} END {print p}') + if [ -z "${path}" ]; then + echo "storage attach ${label}: no filesystem path" >&2 + return 1 + fi + + set +e + out=$(curio cli --machine "${MACHINE}" storage attach "$@" 2>&1) + rc=$? + set -e + if [ "${rc}" -eq 0 ]; then + echo "attached ${label}" + return 0 + fi + if storage_path_listed "${path}"; then + echo "storage already listed (${label}: ${path}); continuing" + return 0 + fi + if printf '%s\n' "${out}" | grep -q 'path is already initialized'; then + echo "storage metadata present (${label}); attaching without --init" + set +e + out=$(curio cli --machine "${MACHINE}" storage attach "${path}" 2>&1) + rc=$? + set -e + if [ "${rc}" -eq 0 ] || storage_path_listed "${path}"; then + echo "attached ${label}" + return 0 + fi + fi + echo "${out}" >&2 + return "${rc}" +} + +if [ ! -f "${CURIO_REPO_PATH}/.init.storage" ]; then + # --store marks the pre-sealed replicas primary for Winning PoSt. + if ! attach_storage genesis-sectors --init --store "${LOTUS_DATA_DIR}/genesis-sectors"; then + uncordon + exit 1 + fi + if ! attach_storage seal-store --init --seal --store "${CURIO_REPO_PATH}/storage"; then + uncordon + exit 1 + fi + touch "${CURIO_REPO_PATH}/.init.storage" +fi + +curio cli --machine "${MACHINE}" uncordon +touch "${CURIO_REPO_PATH}/.init.ready" + +# Disarm EXIT trap so a clean `wait` exit does not re-kill a dead PID noisily. +trap - EXIT +trap cleanup TERM INT +wait "${CURIO_PID}" || exit 1 diff --git a/scripts/devnet-forest-curio/devnet-layer.toml b/scripts/devnet-forest-curio/devnet-layer.toml new file mode 100644 index 000000000000..ca066c1319ad --- /dev/null +++ b/scripts/devnet-forest-curio/devnet-layer.toml @@ -0,0 +1,18 @@ +# A layer title does not enable tasks; these flags do. +[Subsystems] +EnableWindowPost = true +EnableWinningPost = true +EnableSealSDR = true +EnableSealSDRTrees = true +EnableSendPrecommitMsg = true +EnablePoRepProof = true +EnableSendCommitMsg = true +EnableMoveStorage = true + +[Batching.PreCommit] +Timeout = "0h0m5s" +Slack = "6h0m0s" + +[Batching.Commit] +Timeout = "0h0m5s" +Slack = "1h0m0s" diff --git a/scripts/devnet-forest-curio/docker-compose.yml b/scripts/devnet-forest-curio/docker-compose.yml new file mode 100644 index 000000000000..9b85000d8dea --- /dev/null +++ b/scripts/devnet-forest-curio/docker-compose.yml @@ -0,0 +1,148 @@ +# Forest-produced chain with Curio as the only block producer and sealer. +# Chain services are reused from ../devnet-forest-miner; there is no lotus-miner. + +x-lotus-heights: &lotus-heights + LOTUS_GENESIS_NETWORK_VERSION: ${GENESIS_NETWORK_VERSION} + LOTUS_SHARK_HEIGHT: ${SHARK_HEIGHT} + LOTUS_HYGGE_HEIGHT: ${HYGGE_HEIGHT} + LOTUS_LIGHTNING_HEIGHT: ${LIGHTNING_HEIGHT} + LOTUS_THUNDER_HEIGHT: ${THUNDER_HEIGHT} + LOTUS_WATERMELON_HEIGHT: ${WATERMELON_HEIGHT} + LOTUS_DRAGON_HEIGHT: ${DRAGON_HEIGHT} + LOTUS_WAFFLE_HEIGHT: ${WAFFLE_HEIGHT} + LOTUS_TUKTUK_HEIGHT: ${TUKTUK_HEIGHT} + LOTUS_TEEP_HEIGHT: ${TEEP_HEIGHT} + LOTUS_TOCK_HEIGHT: ${TOCK_HEIGHT} + LOTUS_GOLDENWEEK_HEIGHT: ${GOLDEN_WEEK_HEIGHT} + LOTUS_FIREHORSE_HEIGHT: ${FIREHORSE_HEIGHT} + LOTUS_SOLSTICE_HEIGHT: ${SOLSTICE_HEIGHT} + +services: + lotus_init: + extends: + file: ../devnet-forest-miner/docker-compose.yml + service: lotus_init + env_file: + - ../devnet-forest-miner/lotus.env + + lotus_node: + extends: + file: ../devnet-forest-miner/docker-compose.yml + service: lotus_node + env_file: + - ../devnet-forest-miner/lotus.env + + lotus_config: + extends: + file: ../devnet-forest-miner/docker-compose.yml + service: lotus_config + env_file: + - ../devnet-forest-miner/lotus.env + + forest: + extends: + file: ../devnet-forest-miner/docker-compose.yml + service: forest + volumes: + - lotus-data:${LOTUS_DATA_DIR} + - filecoin-proofs:${FIL_PROOFS_PARAMETER_CACHE} + - forest-data:${FOREST_DATA_DIR} + - ../devnet-forest-miner/forest_config.toml.tpl:/forest/forest_config.toml.tpl + + forest_wallet_import: + extends: + file: ../devnet-forest-miner/docker-compose.yml + service: forest_wallet_import + + yugabyte: + container_name: yugabyte + image: ${YUGABYTE_IMAGE} + init: true + environment: + TINI_SUBREAPER: "true" + volumes: + - yugabyte-data:/home/yugabyte/yb_data + # Bind YSQL on all interfaces; default yugabyted only listens on the container hostname. + command: + - /bin/bash + - -lc + - | + ADDR=$$(hostname -I | tr ' ' '\n' | awk '$$1 ~ /^[0-9]+\./ && $$1 !~ /^127\./ {print $$1; exit}') + exec bin/yugabyted start \ + --base_dir=/home/yugabyte/yb_data \ + --background=false \ + --advertise_address=$$ADDR \ + --tserver_flags="pgsql_proxy_bind_address=0.0.0.0:5433,cql_proxy_bind_address=0.0.0.0:9042" + healthcheck: + test: + [ + "CMD", + "/home/yugabyte/bin/ysqlsh", + "-h", + "127.0.0.1", + "-U", + "yugabyte", + "-c", + "SELECT 1", + ] + interval: 5s + timeout: 5s + retries: 30 + start_period: 20s + networks: + - devnet + + curio: + container_name: curio + image: forest/curio-devnet:${CURIO_COMMIT} + platform: linux/amd64 + pull_policy: never + init: true + # Cap restarts so a broken attach does not loop forever while check.sh waits. + restart: on-failure:5 + depends_on: + forest_wallet_import: + condition: service_completed_successfully + yugabyte: + condition: service_healthy + environment: + <<: *lotus-heights + CURIO_REPO_PATH: ${CURIO_REPO_PATH} + CURIO_HARMONYDB_HOSTS: yugabyte + FIL_PROOFS_PARAMETER_CACHE: ${FIL_PROOFS_PARAMETER_CACHE} + FIL_PROOFS_USE_GPU_COLUMN_BUILDER: "0" + FIL_PROOFS_USE_GPU_TREE_BUILDER: "0" + BELLMAN_NO_GPU: "1" + # Winning PoSt registers an F3 task unless this is set. + CURIO_DISABLE_F3: "1" + FOREST_DATA_DIR: ${FOREST_DATA_DIR} + FOREST_RPC_PORT: ${FOREST_RPC_PORT} + LOTUS_DATA_DIR: ${LOTUS_DATA_DIR} + MINER_ACTOR_ADDRESS: ${MINER_ACTOR_ADDRESS} + volumes: + - curio-data:${CURIO_REPO_PATH} + - lotus-data:${LOTUS_DATA_DIR} + - forest-data:${FOREST_DATA_DIR}:ro + - filecoin-proofs:${FIL_PROOFS_PARAMETER_CACHE} + - ./curio_entrypoint.sh:/curio_entrypoint.sh:ro + - ./devnet-layer.toml:/devnet-layer.toml:ro + networks: + - devnet + entrypoint: ["/bin/bash", "/curio_entrypoint.sh"] + # Set after cordon → attach → uncordon in curio_entrypoint.sh. + healthcheck: + test: ["CMD", "test", "-f", "${CURIO_REPO_PATH}/.init.ready"] + interval: 5s + timeout: 3s + retries: 60 + start_period: 30s + +volumes: + filecoin-proofs: + lotus-data: + forest-data: + curio-data: + yugabyte-data: + +networks: + devnet: diff --git a/scripts/devnet-forest-curio/setup.sh b/scripts/devnet-forest-curio/setup.sh new file mode 100755 index 000000000000..3f9b930c4f51 --- /dev/null +++ b/scripts/devnet-forest-curio/setup.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Bring up Forest+Curio; build Curio image if missing. + +set -euxo pipefail + +PARENT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +pushd "${PARENT_PATH}" +set -a +# shellcheck source=/dev/null +source .env +# shellcheck source=/dev/null +source "${PARENT_PATH}/../devnet/upgrade_heights.env" +set +a +# shellcheck source=/dev/null +source "${PARENT_PATH}/../devnet/chain_height.sh" + +./build_image.sh + +docker compose down --remove-orphans +docker compose rm -f +docker volume rm -f \ + devnet-forest-curio_lotus-data \ + devnet-forest-curio_forest-data \ + devnet-forest-curio_curio-data \ + devnet-forest-curio_yugabyte-data + +# Curio image is prebuilt (`pull_policy: never`); --build only refreshes Forest. +docker compose up --build --force-recreate --detach + +wait_forest_rpc + +until docker exec curio test -f "${CURIO_REPO_PATH}/.init.ready"; do + # restart:on-failure briefly reports "restarting", not "running". + status="$(docker inspect -f '{{.State.Status}}' curio 2>/dev/null || echo missing)" + case "${status}" in + running|restarting) ;; + *) + echo "ERROR: curio container status is '${status}' while waiting for .init.ready" >&2 + docker logs curio >&2 || true + exit 1 + ;; + esac + echo "Curio is not ready - sleeping for 2s" + sleep 2 +done + +popd diff --git a/scripts/devnet-forest-miner/.env b/scripts/devnet-forest-miner/.env index 65e8ac7ce7a2..225b7faf23a8 100644 --- a/scripts/devnet-forest-miner/.env +++ b/scripts/devnet-forest-miner/.env @@ -7,23 +7,9 @@ FOREST_DATA_DIR=/forest_data LOTUS_DATA_DIR=/lotus_data FIL_PROOFS_PARAMETER_CACHE=/var/tmp/filecoin-proof-parameters MINER_ACTOR_ADDRESS=t01000 +MINER_BACKEND=lotus LOTUS_RPC_PORT=1234 LOTUS_P2P_PORT=1235 MINER_RPC_PORT=2345 FOREST_RPC_PORT=3456 FOREST_P2P_PORT=4903 -GENESIS_NETWORK_VERSION=18 -SHARK_HEIGHT=-10 -HYGGE_HEIGHT=-9 -LIGHTNING_HEIGHT=3 -THUNDER_HEIGHT=6 -WATERMELON_HEIGHT=9 -DRAGON_HEIGHT=12 -WAFFLE_HEIGHT=18 -TUKTUK_HEIGHT=20 -TEEP_HEIGHT=22 -TOCK_HEIGHT=24 -GOLDEN_WEEK_HEIGHT=26 -FIREHORSE_HEIGHT=28 -SOLSTICE_HEIGHT=30 -TARGET_HEIGHT=34 diff --git a/scripts/devnet-forest-miner/README.md b/scripts/devnet-forest-miner/README.md index 91f7c905e56c..6abd6935c9be 100644 --- a/scripts/devnet-forest-miner/README.md +++ b/scripts/devnet-forest-miner/README.md @@ -17,8 +17,9 @@ validates and follows. mise run test:devnet:forest-miner ``` -The Lotus Miner suites (`forest-dev devnet miner`) run only against this devnet: +The miner suites (`forest-dev devnet miner`) run against this Lotus Miner stack and against [`../devnet-forest-curio`](../devnet-forest-curio): ```shell -mise run test:devnet:miner +mise run test:devnet:miner:lotus +mise run test:devnet:miner:curio ``` diff --git a/scripts/devnet-forest-miner/check.sh b/scripts/devnet-forest-miner/check.sh index 20942dbbfead..6fdc291c1255 100755 --- a/scripts/devnet-forest-miner/check.sh +++ b/scripts/devnet-forest-miner/check.sh @@ -1,41 +1,19 @@ #!/bin/bash -# Assert that Lotus Miner drives Forest past the target height AND that the Lotus -# validating node follows it there. Both climbing proves block production through -# Forest's RPC works and that Lotus accepts (validates) Forest-produced blocks. +# Forest + Lotus past TARGET_HEIGHT (Lotus Miner mining via Forest). -set -eux +set -eu -# Path to the directory containing this script. -PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) +PARENT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) pushd "${PARENT_PATH}" +set -a +# shellcheck source=/dev/null source .env +# shellcheck source=/dev/null +source "${PARENT_PATH}/../devnet/upgrade_heights.env" +set +a +# shellcheck source=/dev/null +source "${PARENT_PATH}/../devnet/chain_height.sh" -function get_sync_height { - local port=$1 - curl --silent -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","id":2,"method":"Filecoin.ChainHead","param":"null"}' \ - "http://127.0.0.1:${port}/rpc/v1" | jq '.result.Height' -} +wait_forest_and_lotus_past_target "Lotus validated Forest-produced blocks" "$(check_timeout_secs)" -start_time=$(date +%s) -timeout=$((start_time + 360)) # 6 minutes - -# Target height chosen so that every migration in .env is crossed. -target_height=$TARGET_HEIGHT - -while true; do - forest_height=$(get_sync_height "${FOREST_RPC_PORT}") - lotus_height=$(get_sync_height "${LOTUS_RPC_PORT}") - if [ "$forest_height" -gt "$target_height" ] && [ "$lotus_height" -gt "$target_height" ]; then - echo "Both nodes past $target_height (Forest: $forest_height, Lotus: $lotus_height): Lotus validated Forest-produced blocks" - break - fi - - current_time=$(date +%s) - if [ "$current_time" -gt "$timeout" ]; then - echo "Timeout reached (Forest: $forest_height, Lotus: $lotus_height), target $target_height not reached by both" - exit 1 - fi - - sleep 1 -done +popd diff --git a/scripts/devnet-forest-miner/setup.sh b/scripts/devnet-forest-miner/setup.sh index f898918970ed..f4bf467c7ae4 100755 --- a/scripts/devnet-forest-miner/setup.sh +++ b/scripts/devnet-forest-miner/setup.sh @@ -3,10 +3,16 @@ set -euxo pipefail -# Path to the directory containing this script. -PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) +PARENT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) pushd "${PARENT_PATH}" +set -a +# shellcheck source=/dev/null source .env +# shellcheck source=/dev/null +source "${PARENT_PATH}/../devnet/upgrade_heights.env" +set +a +# shellcheck source=/dev/null +source "${PARENT_PATH}/../devnet/chain_height.sh" # Start from a clean slate, but keep the `filecoin-proofs` volume so CI's cached proof params # (and local re-runs) are reused rather than re-fetched. @@ -18,15 +24,6 @@ docker volume rm -f devnet-forest-miner_lotus-data devnet-forest-miner_forest-da # distinguish services from init containers. See https://github.com/docker/compose/issues/10596 docker compose up --build --force-recreate --detach -function call_forest_chain_head { - curl --silent -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","id":2,"method":"Filecoin.ChainHead","param":"null"}' \ - "http://127.0.0.1:${FOREST_RPC_PORT}/rpc/v1" -} - -until call_forest_chain_head; do - echo "Forest is unavailable - sleeping for 1s" - sleep 1 -done +wait_forest_rpc popd diff --git a/scripts/devnet/.env b/scripts/devnet/.env index f08b0d96960f..380efb8eb3a7 100644 --- a/scripts/devnet/.env +++ b/scripts/devnet/.env @@ -5,6 +5,7 @@ FOREST_DATA_DIR=/forest_data LOTUS_DATA_DIR=/lotus_data FIL_PROOFS_PARAMETER_CACHE=/var/tmp/filecoin-proof-parameters MINER_ACTOR_ADDRESS=t01000 +MINER_BACKEND=lotus LOTUS_RPC_PORT=1234 LOTUS_P2P_PORT=1235 MINER_RPC_PORT=2345 diff --git a/scripts/devnet/README.md b/scripts/devnet/README.md index 94d328dfc745..15a7e142fd1e 100644 --- a/scripts/devnet/README.md +++ b/scripts/devnet/README.md @@ -69,8 +69,8 @@ mise run test:devnet Under the hood this sources `test_harness.sh`, which reads the admin token and the funded genesis key from the running `forest` container, exports -`FULLNODE_API_INFO` (Forest RPC on port 3456), `MINER_API_INFO` (Lotus Miner RPC on -port 2345) and `FOREST_TEST_PRELOADED_ADDRESS`, +`FULLNODE_API_INFO` (Forest RPC on port 3456), `MINER_API_INFO` when +`MINER_BACKEND=lotus` (Lotus Miner RPC on port 2345), and `FOREST_TEST_PRELOADED_ADDRESS`, then runs `forest-dev tests mpool`, `forest-dev tests wallet`, `forest-dev devnet eth-gas`, `forest-dev devnet eth-skip-sender`, `forest-dev devnet state-call` and `forest-dev devnet eth-pending-tx`. diff --git a/scripts/devnet/chain_height.sh b/scripts/devnet/chain_height.sh new file mode 100644 index 000000000000..f84fdf57702c --- /dev/null +++ b/scripts/devnet/chain_height.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Sourced: Forest RPC wait + Forest/Lotus past TARGET_HEIGHT. + +# jq exits 0 on empty input; do not map a failed RPC to height 0. +function get_sync_height { + local port=$1 + local height + height=$(curl --silent --max-time 2 -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","id":2,"method":"Filecoin.ChainHead","params":null}' \ + "http://127.0.0.1:${port}/rpc/v1" | jq -r '.result.Height // empty') || return 1 + if [ -z "${height}" ]; then + return 1 + fi + printf '%s\n' "${height}" +} + +function wait_forest_rpc { + until curl --silent --max-time 2 -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","id":2,"method":"Filecoin.ChainHead","params":null}' \ + "http://127.0.0.1:${FOREST_RPC_PORT}/rpc/v1" \ + >/dev/null; do + echo "Forest is unavailable - sleeping for 1s" + sleep 1 + done +} + +# SCRIPT_TIMEOUT_MINUTES (default 15); leave 60s so we fail before the GHA killer. +function check_timeout_secs { + local timeout_secs=$(( ${SCRIPT_TIMEOUT_MINUTES:-15} * 60 )) + if [ "${timeout_secs}" -gt 60 ]; then + timeout_secs=$((timeout_secs - 60)) + fi + echo "${timeout_secs}" +} + +function wait_forest_and_lotus_past_target { + local success_msg=$1 + local timeout_secs=$2 + local start_time timeout target_height forest_height lotus_height current_time + start_time=$(date +%s) + timeout=$((start_time + timeout_secs)) + target_height=$TARGET_HEIGHT + + while true; do + forest_height=$(get_sync_height "${FOREST_RPC_PORT}") || true + lotus_height=$(get_sync_height "${LOTUS_RPC_PORT}") || true + if [[ "${forest_height}" =~ ^[0-9]+$ && "${lotus_height}" =~ ^[0-9]+$ ]] \ + && [ "${forest_height}" -gt "${target_height}" ] \ + && [ "${lotus_height}" -gt "${target_height}" ]; then + echo "Both nodes past $target_height (Forest: $forest_height, Lotus: $lotus_height): ${success_msg}" + return 0 + fi + + current_time=$(date +%s) + if [ "$current_time" -gt "$timeout" ]; then + echo "Timeout reached (Forest: $forest_height, Lotus: $lotus_height), target $target_height not reached by both" >&2 + return 1 + fi + + sleep 1 + done +} diff --git a/scripts/devnet/check.sh b/scripts/devnet/check.sh index 2327484fd49b..8bf0175c86e2 100755 --- a/scripts/devnet/check.sh +++ b/scripts/devnet/check.sh @@ -4,19 +4,12 @@ set -eux -# Path to the directory containing this script. -PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) +PARENT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) pushd "${PARENT_PATH}" +# shellcheck source=/dev/null source .env - -# Forest check - assert that we sync past the genesis block. -# Allow for 360 seconds of sync time. -function get_sync_height { - local port=$1 - curl --silent -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","id":2,"method":"Filecoin.ChainHead","param":"null"}' \ - "http://127.0.0.1:${port}/rpc/v1" | jq '.result.Height' -} +# shellcheck source=/dev/null +source "${PARENT_PATH}/chain_height.sh" function get_f3_latest_cert_instance { local port=$1 @@ -35,8 +28,8 @@ docker exec \ timeout 6m \ forest-cli sync wait --target "$target_height" -# Check the offline RPC, which should be initialized at that point. It should be at the genesis height, so 0. -height=$(get_sync_height ${FOREST_OFFLINE_RPC_PORT}) +# Offline RPC should be at genesis height 0. +height=$(get_sync_height "${FOREST_OFFLINE_RPC_PORT}") if [ "$height" != "0" ]; then echo "Offline RPC height is not zero: $height" exit 1 diff --git a/scripts/devnet/setup.sh b/scripts/devnet/setup.sh index 3b821ac37f1b..027fcc580453 100755 --- a/scripts/devnet/setup.sh +++ b/scripts/devnet/setup.sh @@ -4,10 +4,12 @@ set -euxo pipefail -# Path to the directory containing this script. -PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) +PARENT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) pushd "${PARENT_PATH}" +# shellcheck source=/dev/null source .env +# shellcheck source=/dev/null +source "${PARENT_PATH}/chain_height.sh" # This should not be needed in GH. It is useful for running locally. docker compose --profile "*" down --remove-orphans @@ -18,21 +20,10 @@ docker volume rm -f devnet_forest-data # Run it in the background so we can perform checks on it. # Ideally, we could use `--wait` and `--wait-timeout` to wait for services -# to be up. However, `compose` does not distinct between services and +# to be up. However, `compose` does not distinct between services and # init containers. See more: https://github.com/docker/compose/issues/10596 docker compose up --build --force-recreate --detach -# Wait for Forest to be ready. We can assume that it is ready when the -# RPC server is up. This checks if Forest's RPC endpoint is up. - function call_forest_chain_head { - curl --silent -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","id":2,"method":"Filecoin.ChainHead","param":"null"}' \ - "http://127.0.0.1:${FOREST_RPC_PORT}/rpc/v1" - } - - until call_forest_chain_head; do - echo "Forest is unavailable - sleeping for 1s" - sleep 1 - done +wait_forest_rpc popd diff --git a/scripts/devnet/test_harness.sh b/scripts/devnet/test_harness.sh index d975741b8194..8fe3719fabce 100644 --- a/scripts/devnet/test_harness.sh +++ b/scripts/devnet/test_harness.sh @@ -1,7 +1,7 @@ #!/bin/bash # Sourced (not executed) helpers for the devnet test suites. Run after the devnet is up (`setup.sh`) and synced (`check.sh`). # -# The genesis key is the Lotus miner's default wallet, so using it as the test +# The genesis/pre-seal key is the funded default wallet, so using it as the test # sender causes nonce contention. We fund a dedicated wallet instead. TEST_HARNESS_PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) @@ -12,6 +12,10 @@ export DEVNET_DIR="${DEVNET_DIR:-${TEST_HARNESS_PARENT_PATH}}" # as environment variables; `source` on its own would only set shell variables. set -a source "${DEVNET_DIR}/.env" +if [[ -f "${TEST_HARNESS_PARENT_PATH}/upgrade_heights.env" ]]; then + # shellcheck source=/dev/null + source "${TEST_HARNESS_PARENT_PATH}/upgrade_heights.env" +fi set +a export FOREST_CLI_PATH="${FOREST_CLI_PATH:-forest-cli}" @@ -22,10 +26,17 @@ function devnet_test_env_init { local token token=$(docker exec forest cat "${FOREST_DATA_DIR}/token.jwt") export FULLNODE_API_INFO="${token}:/ip4/127.0.0.1/tcp/${FOREST_RPC_PORT}/http" - # The repo token carries all permissions (https://github.com/filecoin-project/lotus/blob/v1.38.3/node/modules/core.go#L165-L173). - local miner_token - miner_token=$(docker exec lotus-miner sh -c 'cat "$LOTUS_MINER_PATH/token"') - export MINER_API_INFO="${miner_token}:/ip4/127.0.0.1/tcp/${MINER_RPC_PORT}/http" + + # Curio has no Miner JSON-RPC; skip MINER_API_INFO. + if [[ "${MINER_BACKEND:-}" == "lotus" ]]; then + # Repo token is admin: https://github.com/filecoin-project/lotus/blob/v1.38.3/node/modules/core.go#L165-L173 + local miner_token + miner_token=$(docker exec lotus-miner sh -c 'cat "$LOTUS_MINER_PATH/token"') + export MINER_API_INFO="${miner_token}:/ip4/127.0.0.1/tcp/${MINER_RPC_PORT}/http" + elif [[ "${MINER_BACKEND:-}" != "curio" ]]; then + echo "ERROR: MINER_BACKEND must be lotus or curio (got '${MINER_BACKEND:-}')" >&2 + return 1 + fi local tmp tmp="$(mktemp --directory)" @@ -47,7 +58,9 @@ function devnet_test_env_init { echo "Devnet test env initialised:" echo " FULLNODE_API_INFO=:/ip4/127.0.0.1/tcp/${FOREST_RPC_PORT}/http" - echo " MINER_API_INFO=:/ip4/127.0.0.1/tcp/${MINER_RPC_PORT}/http" + if [[ -n "${MINER_API_INFO:-}" ]]; then + echo " MINER_API_INFO=:/ip4/127.0.0.1/tcp/${MINER_RPC_PORT}/http" + fi echo " FOREST_TEST_PRELOADED_ADDRESS=${FOREST_TEST_PRELOADED_ADDRESS}" echo " Funding ${test_addr} with ${DEVNET_TEST_FUND_AMT} from ${genesis_addr}..." diff --git a/scripts/devnet/upgrade_heights.env b/scripts/devnet/upgrade_heights.env new file mode 100644 index 000000000000..f6ffda30f917 --- /dev/null +++ b/scripts/devnet/upgrade_heights.env @@ -0,0 +1,16 @@ +# Shared upgrade schedule for Forest-produced miner stacks (Lotus Miner + Curio). +GENESIS_NETWORK_VERSION=18 +SHARK_HEIGHT=-10 +HYGGE_HEIGHT=-9 +LIGHTNING_HEIGHT=3 +THUNDER_HEIGHT=6 +WATERMELON_HEIGHT=9 +DRAGON_HEIGHT=12 +WAFFLE_HEIGHT=18 +TUKTUK_HEIGHT=20 +TEEP_HEIGHT=22 +TOCK_HEIGHT=24 +GOLDEN_WEEK_HEIGHT=26 +FIREHORSE_HEIGHT=28 +SOLSTICE_HEIGHT=30 +TARGET_HEIGHT=34 diff --git a/src/dev/subcommands/devnet_cmd/miner.rs b/src/dev/subcommands/devnet_cmd/miner.rs index e1e0718ab90f..e1e26ed17568 100644 --- a/src/dev/subcommands/devnet_cmd/miner.rs +++ b/src/dev/subcommands/devnet_cmd/miner.rs @@ -1,23 +1,21 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -//! Lotus Miner integration tests, ported from Lotus `itests`. They need -//! `scripts/devnet-forest-miner`, where Lotus Miner seals and mines through Forest's RPC. +//! Miner integration tests (Lotus Miner or Curio via `MINER_BACKEND`). mod api; +mod curio; mod devnet; mod onboarding; use crate::dev::subcommands::tests_cmd::helpers::harness_env; use crate::lotus_json::HasLotusJson; use crate::rpc::{Client, Request, humanize_rpc_error}; -use anyhow::{Context as _, ensure}; +use anyhow::{Context as _, bail}; use clap::ValueEnum as _; use libtest_mimic::{Arguments, Trial}; use std::fmt::Debug; -use std::path::Path; -/// Lotus Miner integration tests on Forest (`scripts/devnet-forest-miner` only) #[derive(Debug, clap::Args)] pub struct MinerTestCommand { /// Run only this group; all groups run by default @@ -27,7 +25,7 @@ pub struct MinerTestCommand { filter: Option, } -/// Tests that share one devnet. CI runs each group as a separate job on its own devnet. +/// CI runs each group as its own job/devnet. #[derive(Debug, Clone, Copy, clap::ValueEnum)] enum Group { Onboarding, @@ -35,11 +33,7 @@ enum Group { impl MinerTestCommand { pub async fn run(self) -> anyhow::Result<()> { - let devnet_dir = harness_env("DEVNET_DIR")?; - ensure!( - Path::new(&devnet_dir).ends_with("devnet-forest-miner"), - "`devnet miner` drives lotus-miner over Forest's RPC, which only scripts/devnet-forest-miner does; DEVNET_DIR is `{devnet_dir}`" - ); + let _ = MinerBackend::from_env()?; let groups = match self.group { Some(group) => vec![group], None => Group::value_variants().to_vec(), @@ -59,7 +53,32 @@ fn tests(group: Group) -> Vec { } } -/// Calls `request` on `node`, naming the node and the method in the error. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum MinerBackend { + Lotus, + Curio, +} + +impl MinerBackend { + pub fn from_env() -> anyhow::Result { + let backend = harness_env("MINER_BACKEND")?; + match backend.as_str() { + "lotus" => Ok(Self::Lotus), + "curio" => Ok(Self::Curio), + _ => bail!("MINER_BACKEND must be lotus or curio (got `{backend}`)"), + } + } +} + +/// `FULLNODE_API_INFO` shell fragment for `docker exec` (token + `/dns/forest/...`). +pub(super) fn forest_fullnode_api_info_shell() -> anyhow::Result { + let port = harness_env("FOREST_RPC_PORT")?; + let data_dir = harness_env("FOREST_DATA_DIR")?; + Ok(format!( + "$(cat '{data_dir}/token.jwt'):/dns/forest/tcp/{port}/http" + )) +} + async fn call( client: &Client, node: &str, diff --git a/src/dev/subcommands/devnet_cmd/miner/api.rs b/src/dev/subcommands/devnet_cmd/miner/api.rs index 8094adf30ed7..3e810340ed56 100644 --- a/src/dev/subcommands/devnet_cmd/miner/api.rs +++ b/src/dev/subcommands/devnet_cmd/miner/api.rs @@ -1,9 +1,8 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -//! The miner the trials drive: a `lotus-miner` whose full node is Forest, reached through the -//! Lotus Miner JSON-RPC API, which Forest's RPC does not define. - +use super::MinerBackend; +use super::curio; use crate::dev::subcommands::tests_cmd::helpers::{harness_env, poll}; use crate::rpc::{ApiPaths, Client, Request}; use crate::shim::address::Address; @@ -16,30 +15,47 @@ use serde_json::json; use std::str::FromStr as _; use std::sync::atomic::{AtomicU8, Ordering}; -/// The miner's on-chain actor and its API. +enum Driver { + Lotus(Box), + Curio, +} + pub(super) struct TestMiner { pub actor: Address, - pub api: MinerApi, + driver: Driver, } impl TestMiner { - /// Returns the genesis miner the compose devnet runs. pub fn genesis() -> anyhow::Result { let raw = harness_env("MINER_ACTOR_ADDRESS")?; let actor = Address::from_str(&raw) .with_context(|| format!("invalid MINER_ACTOR_ADDRESS `{raw}`"))?; - Ok(Self { - actor, - api: MinerApi::connect()?, - }) + let driver = match MinerBackend::from_env()? { + MinerBackend::Lotus => Driver::Lotus(Box::new(MinerApi::connect()?)), + MinerBackend::Curio => Driver::Curio, + }; + Ok(Self { actor, driver }) + } + + pub async fn pledge_sector(&self) -> anyhow::Result { + match &self.driver { + Driver::Lotus(api) => api.pledge_sector().await, + Driver::Curio => curio::pledge_sector(self.actor).await, + } + } + + pub async fn wait_until_proving(&self, sector: u64) -> anyhow::Result<()> { + match &self.driver { + Driver::Lotus(api) => api.wait_until_proving(sector).await, + Driver::Curio => curio::wait_until_proving(self.actor, sector).await, + } } } -/// The Lotus Miner API; its token must be admin, since the batch flushes are admin methods. +/// Lotus Miner JSON-RPC; token must be admin (batch flush methods). pub(super) struct MinerApi(Client); impl MinerApi { - /// Connects to `MINER_API_INFO`. pub fn connect() -> anyhow::Result { let raw = harness_env("MINER_API_INFO")?; let UrlFromMultiAddr(url) = raw.parse().context("parsing MINER_API_INFO")?; @@ -61,7 +77,6 @@ impl MinerApi { .with_context(|| format!("decoding the Lotus Miner {method} result")) } - /// Starts sealing a new CC sector and returns its number. pub async fn pledge_sector(&self) -> anyhow::Result { let id: SectorId = self.call("Filecoin.PledgeSector", json!([])).await?; Ok(id.number) @@ -72,7 +87,7 @@ impl MinerApi { .await } - /// Flushes the pre-commit and commit batches, which Lotus may otherwise hold for up to a day + /// Lotus may hold batches up to a day without a flush /// (). async fn flush_sealing_batches(&self) -> anyhow::Result<()> { for method in [ @@ -84,10 +99,7 @@ impl MinerApi { Ok(()) } - /// Waits until `sector` is `Proving` or `Available`, flushing the sealing batches on every - /// poll; a state that never leads to `Proving` fails at once. pub async fn wait_until_proving(&self, sector: u64) -> anyhow::Result<()> { - // `poll` logs every attempt, so the state is printed only when it changes. let last_state = AtomicU8::new(u8::MAX); let waited = poll(&format!("sector {sector} to reach Proving"), || async { self.flush_sealing_batches().await?; diff --git a/src/dev/subcommands/devnet_cmd/miner/curio.rs b/src/dev/subcommands/devnet_cmd/miner/curio.rs new file mode 100644 index 000000000000..2a01486cb5bd --- /dev/null +++ b/src/dev/subcommands/devnet_cmd/miner/curio.rs @@ -0,0 +1,269 @@ +// Copyright 2019-2026 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use super::forest_fullnode_api_info_shell; +use crate::dev::subcommands::tests_cmd::helpers::{docker, forest_client, harness_env, poll}; +use crate::rpc::prelude::*; +use crate::rpc::types::ApiTipsetKey; +use crate::shim::address::Address; +use anyhow::{Context as _, bail}; +use serde::Deserialize; +use std::sync::atomic::{AtomicBool, Ordering}; + +pub(super) async fn pledge_sector(actor: Address) -> anyhow::Result { + let out = docker(&[ + "exec", + "curio", + "curio", + "seal", + "start", + "--now", + "--cc", + "--count", + "1", + "--actor", + &actor.to_string(), + ]) + .context("`curio seal start` failed")?; + parse_seal_start_sector(&out) +} + +/// Harmony-DB "Done" can lead the chain; still wait for Forest `StateSectorGetInfo`. +pub(super) async fn wait_until_proving(actor: Address, sector: u64) -> anyhow::Result<()> { + let sp_id = actor.id().context("miner actor is not an ID address")?; + let forest = forest_client()?; + let harmony_done = AtomicBool::new(false); + poll( + &format!("Curio sector {sector} sealed and on Forest"), + || async { + let status = if harmony_done.load(Ordering::Relaxed) { + SectorStatus::Done + } else { + let status = tokio::task::spawn_blocking(move || sector_status(sp_id, sector)) + .await + .context("sector_status join")??; + if matches!(status, SectorStatus::Done) { + harmony_done.store(true, Ordering::Relaxed); + } + status + }; + match status { + SectorStatus::Failed { + reason, + reason_msg, + } => { + let events = seal_events(actor, sector).unwrap_or_default(); + bail!( + "Curio sector {sector} failed: {reason}: {reason_msg}\nseal events:\n{events}" + ); + } + SectorStatus::Unknown | SectorStatus::InProgress => Ok(None), + SectorStatus::Done => { + let info = forest + .call(StateSectorGetInfo::request(( + actor, + sector, + ApiTipsetKey(None), + ))?) + .await + .with_context(|| format!("StateSectorGetInfo for sector {sector}"))?; + Ok(info.map(|_| ())) + } + } + }, + ) + .await + .map_err(|e| { + let events = seal_events(actor, sector).unwrap_or_default(); + e.context(format!("seal events:\n{events}")) + })?; + Ok(()) +} + +pub(super) fn create_miner(owner: Address, worker: Address) -> anyhow::Result<()> { + let api = forest_fullnode_api_info_shell()?; + // `sptool` needs a global `--actor` even for `new-miner` (genesis SP = CLI context). + let actor = harness_env("MINER_ACTOR_ADDRESS")?; + let script = format!( + "export FULLNODE_API_INFO=\"{api}\" && \ + exec sptool --actor {actor} actor new-miner \ + --owner='{owner}' --worker='{worker}' --from='{owner}' \ + --sector-size 2KiB --confidence 1" + ); + docker(&["exec", "curio", "sh", "-c", &script]).context("`sptool actor new-miner` failed")?; + Ok(()) +} + +/// Prefer a pure-numeric line from `curio seal start`, else the last integer token. +fn parse_seal_start_sector(out: &str) -> anyhow::Result { + if let Some(n) = out + .lines() + .map(str::trim) + .filter(|l| !l.is_empty()) + .rev() + .find_map(|l| l.parse().ok()) + { + return Ok(n); + } + out.split_whitespace() + .rev() + .find_map(|tok| tok.trim_matches(|c: char| !c.is_ascii_digit()).parse().ok()) + .with_context(|| format!("no sector number in `curio seal start` output:\n{out}")) +} + +#[derive(Debug, PartialEq, Eq)] +enum SectorStatus { + Done, + Failed { reason: String, reason_msg: String }, + InProgress, + Unknown, +} + +#[derive(Debug, Deserialize)] +struct StatusRow { + after_commit_msg_success: bool, + failed: bool, + failed_reason: String, + failed_reason_msg: String, + in_meta: bool, + in_pipeline: bool, +} + +fn sector_status(sp_id: u64, sector: u64) -> anyhow::Result { + // after_commit_msg_success / failed are NOT NULL booleans on sectors_sdr_pipeline. + let sql = format!( + "SELECT json_build_object(\ + 'after_commit_msg_success', COALESCE(p.after_commit_msg_success, false),\ + 'failed', COALESCE(p.failed, false),\ + 'failed_reason', COALESCE(p.failed_reason, ''),\ + 'failed_reason_msg', COALESCE(p.failed_reason_msg, ''),\ + 'in_meta', EXISTS (SELECT 1 FROM curio.sectors_meta m \ + WHERE m.sp_id = {sp_id} AND m.sector_num = {sector}),\ + 'in_pipeline', p.sp_id IS NOT NULL\ + ) FROM (SELECT {sp_id}::bigint AS sp_id, {sector}::bigint AS sector_number) s \ + LEFT JOIN curio.sectors_sdr_pipeline p \ + ON p.sp_id = s.sp_id AND p.sector_number = s.sector_number;" + ); + let out = docker(&[ + "exec", + "yugabyte", + "bin/ysqlsh", + "-h", + "yugabyte", + "-U", + "yugabyte", + "-At", + "-c", + &sql, + ]) + .context("querying Curio sector status")?; + let row: StatusRow = serde_json::from_str(out.trim()) + .with_context(|| format!("decoding Curio status JSON: {out}"))?; + Ok(status_from_row(row)) +} + +fn status_from_row(row: StatusRow) -> SectorStatus { + if row.failed { + return SectorStatus::Failed { + reason: row.failed_reason, + reason_msg: row.failed_reason_msg, + }; + } + if row.after_commit_msg_success || (!row.in_pipeline && row.in_meta) { + return SectorStatus::Done; + } + if !row.in_pipeline && !row.in_meta { + return SectorStatus::Unknown; + } + SectorStatus::InProgress +} + +fn seal_events(actor: Address, sector: u64) -> anyhow::Result { + docker(&[ + "exec", + "curio", + "curio", + "seal", + "events", + "--actor", + &actor.to_string(), + "--sector", + §or.to_string(), + ]) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn row( + after_commit_msg_success: bool, + failed: bool, + in_meta: bool, + in_pipeline: bool, + ) -> StatusRow { + StatusRow { + after_commit_msg_success, + failed, + failed_reason: if failed { "boom".into() } else { String::new() }, + failed_reason_msg: if failed { + "details".into() + } else { + String::new() + }, + in_meta, + in_pipeline, + } + } + + #[test] + fn status_from_row_branches() { + assert_eq!( + status_from_row(row(false, true, false, true)), + SectorStatus::Failed { + reason: "boom".into(), + reason_msg: "details".into(), + } + ); + assert_eq!( + status_from_row(row(true, false, true, true)), + SectorStatus::Done + ); + assert_eq!( + status_from_row(row(false, false, true, false)), + SectorStatus::Done + ); + assert_eq!( + status_from_row(row(false, false, false, false)), + SectorStatus::Unknown + ); + assert_eq!( + status_from_row(row(false, false, false, true)), + SectorStatus::InProgress + ); + assert_eq!( + status_from_row(row(false, false, true, true)), + SectorStatus::InProgress + ); + } + + #[test] + fn parse_seal_start_uses_last_numeric_line() { + let out = "starting seal\nnoise 99\n\n42\ndone\n"; + assert_eq!(parse_seal_start_sector(out).unwrap(), 42); + } + + #[test] + fn parse_seal_start_accepts_sector_in_prose() { + assert_eq!( + parse_seal_start_sector("Started sealing sector 42\n").unwrap(), + 42 + ); + } + + #[test] + fn parse_seal_start_rejects_when_no_numeric_line() { + let err = parse_seal_start_sector("ok\ndone").unwrap_err().to_string(); + assert!(err.contains("no sector number"), "unexpected error: {err}"); + } +} diff --git a/src/dev/subcommands/devnet_cmd/miner/onboarding.rs b/src/dev/subcommands/devnet_cmd/miner/onboarding.rs index 434934eccc51..7b77ceed1a81 100644 --- a/src/dev/subcommands/devnet_cmd/miner/onboarding.rs +++ b/src/dev/subcommands/devnet_cmd/miner/onboarding.rs @@ -1,11 +1,11 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -//! Lotus Miner creating a miner and pledging a sector through Forest's RPC. Mirrors the creation -//! step of Lotus `testNonGenesisMiner` (which also starts the new miner) and `TestPledgeSectors` -//! (single sector, real 2 KiB proofs). +//! Create-miner + pledge-sector (Lotus `testNonGenesisMiner` / `TestPledgeSectors`). +use super::MinerBackend; use super::api::TestMiner; +use super::curio; use super::devnet::Devnet; use crate::dev::subcommands::tests_cmd::helpers::*; use crate::key_management::generate_key; @@ -46,22 +46,34 @@ async fn create_miner() -> anyhow::Result<()> { .await?; let before = list_miners(&net.forest).await?; - let repo = format!( - "/tmp/forest-onboarding-{}", - SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() - ); - let init = init_miner(&repo, owner_key, worker_key); - let _ = docker(&["exec", "lotus-miner", "rm", "-rf", &repo]); - init?; + let backend = MinerBackend::from_env()?; + match backend { + MinerBackend::Curio => curio::create_miner(owner_key, worker_key)?, + MinerBackend::Lotus => { + let repo = format!( + "/tmp/forest-onboarding-{}", + SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() + ); + let init = init_lotus_miner(&repo, owner_key, worker_key); + let _ = docker(&["exec", "lotus-miner", "rm", "-rf", &repo]); + init?; + } + } - let created: Vec
= list_miners(&net.forest) - .await? - .difference(&before) - .copied() - .collect(); - let [miner] = created.as_slice() else { - bail!("expected exactly one new miner after `lotus-miner init`, found {created:?}"); - }; + // Wait until the new actor shows up (sptool --confidence 1 can still race a tipset). + let miner = poll("new miner to appear on Forest", || async { + let created: Vec
= list_miners(&net.forest) + .await? + .difference(&before) + .copied() + .collect(); + match created.as_slice() { + [] => Ok(None), + [miner] => Ok(Some(*miner)), + other => bail!("expected exactly one new miner, found {other:?}"), + } + }) + .await?; let worker_id = net .forest @@ -70,7 +82,7 @@ async fn create_miner() -> anyhow::Result<()> { let info = net .synced() .await? - .same(|tsk| StateMinerInfo::request((*miner, tsk))) + .same(|tsk| StateMinerInfo::request((miner, tsk))) .await?; ensure!( info.owner == owner_id && info.worker == worker_id, @@ -83,19 +95,25 @@ async fn create_miner() -> anyhow::Result<()> { "miner {miner}: sector size {:?}, expected 2 KiB", info.sector_size ); - ensure!( - !info.peer_id.is_empty(), - "miner {miner}: no peer ID recorded" - ); + match backend { + MinerBackend::Curio => ensure!( + info.peer_id.is_empty(), + "miner {miner}: Curio should leave peer_id empty, got {} bytes", + info.peer_id.len() + ), + MinerBackend::Lotus => ensure!( + !info.peer_id.is_empty(), + "miner {miner}: no peer ID recorded" + ), + } Ok(()) } -/// Pledges one CC sector; the chain verifies the seal proof on prove-commit. async fn pledge_sector() -> anyhow::Result<()> { let net = Devnet::connect()?; let miner = TestMiner::genesis()?; - let sector = miner.api.pledge_sector().await?; - miner.api.wait_until_proving(sector).await?; + let sector = miner.pledge_sector().await?; + miner.wait_until_proving(sector).await?; net.synced() .await? .same(|tsk| StateSectorGetInfo::request((miner.actor, sector, tsk))) @@ -112,12 +130,11 @@ async fn list_miners(client: &Client) -> anyhow::Result> { .collect()) } -fn init_miner(repo: &str, owner: Address, worker: Address) -> anyhow::Result<()> { - let port = harness_env("FOREST_RPC_PORT")?; - let data_dir = harness_env("FOREST_DATA_DIR")?; +fn init_lotus_miner(repo: &str, owner: Address, worker: Address) -> anyhow::Result<()> { + let api = super::forest_fullnode_api_info_shell()?; let script = format!( "export LOTUS_MINER_PATH='{repo}' \ - FULLNODE_API_INFO=\"$(cat '{data_dir}/token.jwt'):/dns/forest/tcp/{port}/http\" && \ + FULLNODE_API_INFO=\"{api}\" && \ exec lotus-miner init --owner='{owner}' --worker='{worker}' --sector-size=2KiB \ --no-local-storage --nosync" ); From bf73bc312f477e3ac46c2d5d017c4753fb7e57ea Mon Sep 17 00:00:00 2001 From: Shashank Date: Wed, 4 Nov 2026 15:12:50 +0530 Subject: [PATCH 2/4] docs fix --- docs/docs/users/guides/running_with_curio.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/users/guides/running_with_curio.md b/docs/docs/users/guides/running_with_curio.md index feb89c3c5905..f8a8a77c258a 100644 --- a/docs/docs/users/guides/running_with_curio.md +++ b/docs/docs/users/guides/running_with_curio.md @@ -5,7 +5,7 @@ sidebar_position: 99 [Curio](https://curiostorage.org/) is the successor to Lotus Miner. Forest can act as Curio's chain node (the same "Forest, Curio, YugabyteDB" layout Curio documents). -Verified on the local stack in [`scripts/devnet-forest-curio`](../../../../scripts/devnet-forest-curio) against **Curio v1.29.0**: +Verified on the local stack in `scripts/devnet-forest-curio` against **Curio v1.29.0**: - Put an admin token in `FULLNODE_API_INFO` and keep the miner worker key in Forest's wallet (`MinerCreateBlock` and message signing use it). - On a private network, the genesis name must start with `local` or `test` (Curio drops other chain nodes). The stack uses `localnet`. From a0127cdbd16dbb8c9293e429a22cf575158c597c Mon Sep 17 00:00:00 2001 From: Shashank Date: Wed, 4 Nov 2026 15:17:46 +0530 Subject: [PATCH 3/4] fix link --- docs/docs/users/guides/running_with_curio.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/users/guides/running_with_curio.md b/docs/docs/users/guides/running_with_curio.md index f8a8a77c258a..aa2f9b373338 100644 --- a/docs/docs/users/guides/running_with_curio.md +++ b/docs/docs/users/guides/running_with_curio.md @@ -5,7 +5,7 @@ sidebar_position: 99 [Curio](https://curiostorage.org/) is the successor to Lotus Miner. Forest can act as Curio's chain node (the same "Forest, Curio, YugabyteDB" layout Curio documents). -Verified on the local stack in `scripts/devnet-forest-curio` against **Curio v1.29.0**: +Verified on the local stack in [`scripts/devnet-forest-curio`](https://github.com/ChainSafe/forest/tree/main/scripts/devnet-forest-curio) against **Curio v1.29.0**: - Put an admin token in `FULLNODE_API_INFO` and keep the miner worker key in Forest's wallet (`MinerCreateBlock` and message signing use it). - On a private network, the genesis name must start with `local` or `test` (Curio drops other chain nodes). The stack uses `localnet`. From 908aad10eaf0ab3e4811111d1324b902e49c04e9 Mon Sep 17 00:00:00 2001 From: Shashank Date: Wed, 4 Nov 2026 16:43:46 +0530 Subject: [PATCH 4/4] fmt --- src/dev/subcommands/devnet_cmd/miner.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dev/subcommands/devnet_cmd/miner.rs b/src/dev/subcommands/devnet_cmd/miner.rs index e1e26ed17568..09dc88b44528 100644 --- a/src/dev/subcommands/devnet_cmd/miner.rs +++ b/src/dev/subcommands/devnet_cmd/miner.rs @@ -16,6 +16,7 @@ use clap::ValueEnum as _; use libtest_mimic::{Arguments, Trial}; use std::fmt::Debug; +/// Miner integration tests on Forest (Lotus Miner or Curio; set `MINER_BACKEND` in the stack `.env`) #[derive(Debug, clap::Args)] pub struct MinerTestCommand { /// Run only this group; all groups run by default