diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml deleted file mode 100644 index 9a7e45dc..00000000 --- a/.github/workflows/integration_test.yml +++ /dev/null @@ -1,578 +0,0 @@ -name: Ev-ABCI Integration & IBC Tests - -on: - push: - branches: ["main"] - pull_request: - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build-gm-image: - name: Build GM Image - runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: read - packages: write - outputs: - image_tag: ${{ steps.tag.outputs.tag }} - env: - IGNITE_VERSION: v29.3.0 # the gm build script depends on some annotations - IGNITE_EVOLVE_APP_VERSION: main - EVNODE_VERSION: v1.0.0-beta.9 - steps: - - uses: actions/checkout@v5 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Determine image tag - id: tag - run: | - if [ "${{ github.event_name }}" = "pull_request" ]; then - echo "tag=ghcr.io/01builders/evolve-gm:pr-${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" - else - echo "tag=ghcr.io/01builders/evolve-gm:${{ github.sha }}" >> "$GITHUB_OUTPUT" - fi - - - name: Build and push GM image - uses: docker/build-push-action@v6 - with: - context: . - file: tests/integration/docker/Dockerfile.gm - push: true - tags: ${{ steps.tag.outputs.tag }} - build-args: | - IGNITE_VERSION=${{ env.IGNITE_VERSION }} - IGNITE_EVOLVE_APP_VERSION=${{ env.IGNITE_EVOLVE_APP_VERSION }} - EVNODE_VERSION=${{ env.EVNODE_VERSION }} - - liveness-tastora: - name: Test with EV-ABCI Chain (Tastora) - runs-on: ubuntu-latest - timeout-minutes: 30 - env: - EVNODE_VERSION: "v1.0.0-beta.9" - IGNITE_VERSION: "v29.6.1" - IGNITE_EVOLVE_APP_VERSION: "main" # use tagged when apps has tagged (blocked on things) - EVOLVE_IMAGE_REPO: "evolve-gm" - EVOLVE_IMAGE_TAG: "latest" - - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version: "stable" - cache: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build Evolve Docker Image - run: | - docker build \ - --build-arg EVNODE_VERSION=${{ env.EVNODE_VERSION }} \ - --build-arg IGNITE_VERSION=${{ env.IGNITE_VERSION }} \ - --build-arg IGNITE_EVOLVE_APP_VERSION=${{ env.IGNITE_EVOLVE_APP_VERSION }} \ - -t ${{ env.EVOLVE_IMAGE_REPO }}:${{ env.EVOLVE_IMAGE_TAG }} \ - . - - - name: Run Liveness Test - run: | - cd tests/integration - go test -v -run TestDockerIntegrationTestSuite/TestLivenessWithCelestiaDA -timeout 30m - env: - EVOLVE_IMAGE_REPO: ${{ env.EVOLVE_IMAGE_REPO }} - EVOLVE_IMAGE_TAG: ${{ env.EVOLVE_IMAGE_TAG }} - - liveness: - name: Test with Evolve Chain - runs-on: ubuntu-latest - timeout-minutes: 30 - env: - DO_NOT_TRACK: true - EVNODE_VERSION: "v1.0.0-beta.9" - IGNITE_VERSION: "v29.6.1" - IGNITE_EVOLVE_APP_VERSION: "main" # use tagged when apps has tagged (blocked on things) - outputs: - carol_mnemonic: ${{ steps.save_mnemonic.outputs.carol_mnemonic }} - gmd_home: ${{ steps.paths.outputs.GMD_HOME }} - - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version: "stable" - cache: true - - - name: Install Ignite CLI - run: | - curl -sSL https://get.ignite.com/cli@$IGNITE_VERSION! | bash - - - name: Scaffold Evolve Chain - run: | - # scaffold a new chain - ignite scaffold chain gm --no-module --skip-git --address-prefix gm - cd gm - - # install evolve app - ignite app install github.com/ignite/apps/evolve@$IGNITE_EVOLVE_APP_VERSION - - # add evolve to the chain - ignite evolve add - - - name: Start Local DA - run: | - cd gm - # start the local da in the background - go tool github.com/evstack/ev-node/da/cmd/local-da & - # capture the background process PID - echo "DA_PID=$!" >> $GITHUB_ENV - # give it a moment to start - sleep 3 - - - name: Replace ABCI Module with Current Branch And Prepare Chain - run: | - # get the path to the current checkout of ev-abci - CURRENT_DIR=$(pwd) - GO_EXECUTION_ABCI_DIR=$CURRENT_DIR - - # enter the gm directory - cd gm - - # replace the github.com/evstack/ev-node module with tagged version - go mod edit -replace github.com/evstack/ev-node=github.com/evstack/ev-node@$EVNODE_VERSION - - # replace the github.com/evstack/ev-abci module with the local version - go mod edit -replace github.com/evstack/ev-abci=$GO_EXECUTION_ABCI_DIR - - # download dependencies and update go.mod/go.sum - go mod tidy - - # build the chain - ignite chain build --skip-proto - - # initialize evolve - ignite evolve init - - - name: Create extra accounts - id: save_mnemonic - run: | - MNEMONIC=$(gmd keys add carol --output json | jq -r .mnemonic) - echo "$MNEMONIC" > carol.mnemonic - echo "CAROL_MNEMONIC=$MNEMONIC" >> $GITHUB_ENV - echo "carol_mnemonic=$MNEMONIC" >> $GITHUB_OUTPUT - - - name: Get gm binary and gmd home paths - id: paths - run: | - GM_BINARY_PATH=$(which gmd) - echo "GM_BINARY_PATH=$GM_BINARY_PATH" - echo "GM_BINARY_PATH=$GM_BINARY_PATH" >> $GITHUB_ENV - echo "GM_BINARY_PATH=$GM_BINARY_PATH" >> $GITHUB_OUTPUT - GMD_HOME=$(gmd config home) - echo "GMD_HOME=$GMD_HOME" - echo "GMD_HOME=$GMD_HOME" >> $GITHUB_ENV - echo "GMD_HOME=$GMD_HOME" >> $GITHUB_OUTPUT - - - name: Upload gm Binary and gmd Home Directory - uses: actions/upload-artifact@v5 - with: - name: gmd - include-hidden-files: true - if-no-files-found: error - path: | - ${{ steps.paths.outputs.GM_BINARY_PATH }} - ${{ steps.paths.outputs.GMD_HOME }} - - - name: Start Chain and Wait for Blocks - run: | - # start the chain and send output to a log file - gmd start --rollkit.node.aggregator --log_format=json > chain.log 2>&1 & - CHAIN_PID=$! - echo "CHAIN_PID=$CHAIN_PID" >> $GITHUB_ENV - - echo "Waiting for chain to produce blocks..." - - # wait for chain to start and check for 5 blocks - BLOCKS_FOUND=0 - MAX_ATTEMPTS=60 - ATTEMPT=0 - - while [ $BLOCKS_FOUND -lt 5 ] && [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do - sleep 2 - ATTEMPT=$((ATTEMPT+1)) - - # check if the chain is still running - if ! ps -p $CHAIN_PID > /dev/null; then - echo "Chain process died unexpectedly" - cat chain.log - exit 1 - fi - - # query the node for the current block height - BLOCKS_FOUND=$(gmd query block --output json | tail -n +2 | jq -r '.header.height') - echo "Found $BLOCKS_FOUND blocks so far (attempt $ATTEMPT/$MAX_ATTEMPTS)" - done - - if [ $BLOCKS_FOUND -lt 5 ]; then - echo "Failed to find 5 blocks within time limit" - cat chain.log - exit 1 - fi - - echo "Success! Chain produced at least 5 blocks." - - - name: Test Transaction Submission and Query - run: | - # get Bob's and Carol's addresses - BOB_ADDRESS=$(gmd keys show bob -a) - CAROL_ADDRESS=$(gmd keys show carol -a) - echo "Bob's address: $BOB_ADDRESS" - echo "Carol's address: $CAROL_ADDRESS" - - # query bob's initial balance - echo "Querying Bob's initial balance..." - INITIAL_BALANCE=$(gmd query bank balances $BOB_ADDRESS --output json | jq '.balances[0].amount' -r) - echo "Bob's initial balance: $INITIAL_BALANCE stake" - - # check that bob has funds - if [ "$INITIAL_BALANCE" == "" ] || [ "$INITIAL_BALANCE" == "null" ] || [ "$INITIAL_BALANCE" -lt 100 ]; then - echo "Error: Bob's account not properly funded" - exit 1 - fi - - # send transaction from bob to carol and get tx hash - echo "Sending 100stake from Bob to Carol..." - TX_HASH=$(gmd tx bank send $BOB_ADDRESS $CAROL_ADDRESS 100stake -y --output json | jq -r .txhash) - - sleep 3 - - # query the transaction - TX_RESULT=$(gmd query tx $TX_HASH --output json) - TX_CODE=$(echo $TX_RESULT | jq -r '.code') - if [ "$TX_CODE" != "0" ]; then - echo "Error: Transaction failed with code $TX_CODE" - echo $TX_RESULT | jq - exit 1 - fi - - # query bob's balance after transaction - FINAL_BALANCE=$(gmd query bank balances $BOB_ADDRESS --output json | jq '.balances[0].amount' -r) - echo "Bob's final balance: $FINAL_BALANCE" - - # calculate and verify the expected balance - EXPECTED_BALANCE=$((INITIAL_BALANCE - 100)) - if [ "$FINAL_BALANCE" != "$EXPECTED_BALANCE" ]; then - echo "Error: Balance mismatch. Expected: $EXPECTED_BALANCE, Actual: $FINAL_BALANCE" - exit 1 - fi - - echo "✅ Transaction test successful! Balance correctly updated." - - - name: Cleanup Processes - if: always() - run: | - # kill chain process if it exists - if [[ -n "${CHAIN_PID}" ]]; then - kill -9 $CHAIN_PID || true - fi - - # kill DA process if it exists - if [[ -n "${DA_PID}" ]]; then - kill -9 $DA_PID || true - fi - - ibc: - name: Test IBC Connection Ev-ABCI <-> Cosmos Hub - runs-on: ubuntu-latest - timeout-minutes: 60 - needs: liveness - env: - DO_NOT_TRACK: true - CAROL_MNEMONIC: ${{ needs.liveness.outputs.carol_mnemonic }} - GMD_HOME: ${{ needs.liveness.outputs.gmd_home }} - HERMES_VERSION: "v1.13.1" - GAIA_VERSION: "v25.1.0" - EVNODE_VERSION: "v1.0.0-beta.9" - steps: - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version: stable - - - name: Download gm Binary and gmd Home Directory - uses: actions/download-artifact@v4 # keep v4. - with: - name: gmd - path: gmd - - - name: Download Gaia Binary - run: | - wget https://github.com/cosmos/gaia/releases/download/${GAIA_VERSION}/gaiad-${GAIA_VERSION}-linux-amd64 - chmod +x gaiad-${GAIA_VERSION}-linux-amd64 - sudo mv gaiad-${GAIA_VERSION}-linux-amd64 /usr/local/bin/gaiad - - - name: Download Hermes Binary - run: | - wget https://github.com/informalsystems/hermes/releases/download/${HERMES_VERSION}/hermes-${HERMES_VERSION}-x86_64-unknown-linux-gnu.tar.gz - tar -xzf hermes-${HERMES_VERSION}-x86_64-unknown-linux-gnu.tar.gz - sudo mv hermes /usr/local/bin/hermes - - - name: Start Cosmos Hub Chain - run: | - gaiad init cosmos-local --chain-id cosmos-local - echo "$CAROL_MNEMONIC" | gaiad keys add validator \ - --keyring-backend test \ - --recover > /dev/null 2>&1 - gaiad genesis add-genesis-account $(gaiad keys show validator -a --keyring-backend test) 1000000000stake - gaiad genesis gentx validator 100000000stake --fees 1stake --chain-id cosmos-local --keyring-backend test - gaiad genesis collect-gentxs - gaiad config set app minimum-gas-prices 0.025stake - gaiad config set app grpc.enable true - gaiad start --rpc.laddr tcp://0.0.0.0:26654 --rpc.pprof_laddr localhost:6061 --p2p.laddr tcp://0.0.0.0:26653 --grpc.address 0.0.0.0:9091 --log_format=json > cosmos.log 2>&1 & - echo "COSMOS_PID=$!" >> $GITHUB_ENV - sleep 5 - - - name: Start Local DA - run: | - # Create a temporary go module to use go tool (can't use go install due to replace directives) - mkdir -p /tmp/da-tool - cd /tmp/da-tool - go mod init temp - go mod edit -replace github.com/evstack/ev-node=github.com/evstack/ev-node@$EVNODE_VERSION - go get github.com/evstack/ev-node/da/cmd/local-da - # start the local da in the background - go tool github.com/evstack/ev-node/da/cmd/local-da & - # capture the background process PID - echo "DA_PID=$!" >> $GITHUB_ENV - # give it a moment to start - sleep 3 - - - name: Start Evolve Chain - run: | - chmod +x ./gmd/go/bin/gmd # restoring permissions after download - ./gmd/go/bin/gmd start --rollkit.node.aggregator --rpc.laddr tcp://0.0.0.0:26657 --grpc.address 0.0.0.0:9090 --log_format=json --home ./gmd/.gm > chain.log 2>&1 & - echo "CHAIN_PID=$!" >> $GITHUB_ENV - sleep 10 - CAROL_ADDRESS=$(./gmd/go/bin/gmd keys show carol -a --home ./gmd/.gm) - echo "Fund Carol's account ($CAROL_ADDRESS)" - ./gmd/go/bin/gmd tx bank send bob $CAROL_ADDRESS 200000stake -y --output json --home ./gmd/.gm - sleep 5 - - - name: Configure & Start Hermes Relayer - run: | - mkdir -p ~/.hermes - cat > ~/.hermes/config.toml < $tmp - hermes keys add --chain gm --mnemonic-file $tmp - hermes keys add --chain cosmos-local --mnemonic-file $tmp - hermes start > hermes.log 2>&1 & - echo "HERMES_PID=$!" >> $GITHUB_ENV - - - name: Create IBC Connection and Channel - run: | - hermes create channel --a-chain gm --a-port transfer --b-chain cosmos-local --b-port transfer --order unordered --new-client-connection --yes - - - name: ICS20 Transfer Evolve -> Cosmos Hub - run: | - ROLLKIT_ADDR=$(./gmd/go/bin/gmd keys show carol -a --home ./gmd/.gm) - COSMOS_ADDR=$(gaiad keys show validator -a --keyring-backend test) - ./gmd/go/bin/gmd tx ibc-transfer transfer transfer channel-0 $COSMOS_ADDR 100stake --from carol -y --home ./gmd/.gm - - # Wait for IBC transfer to complete with retry logic - echo "Waiting for IBC transfer to complete..." - MAX_ATTEMPTS=30 - ATTEMPT=0 - IBC_FOUND=false - - while [ $ATTEMPT -lt $MAX_ATTEMPTS ] && [ "$IBC_FOUND" = false ]; do - sleep 3 - ATTEMPT=$((ATTEMPT+1)) - - BALANCE=$(gaiad query bank balances $COSMOS_ADDR --output json --node http://localhost:26654 | jq '.balances') - echo "Attempt $ATTEMPT/$MAX_ATTEMPTS - Gm balance: $BALANCE" - - # Check if any denom starts with ibc/ - if echo "$BALANCE" | jq -e '.[] | select(.denom | startswith("ibc/"))' > /dev/null; then - IBC_FOUND=true - echo "✅ IBC transfer successful! IBC denom found in balance." - else - echo "IBC denom not found yet, retrying in 3 seconds..." - fi - done - - if [ "$IBC_FOUND" = false ]; then - echo "Error: No IBC denom found in balance after transfer within $MAX_ATTEMPTS attempts!" - echo "Final balance: $BALANCE" - exit 1 - fi - - - name: ICS20 Transfer Cosmos Hub -> Evolve - run: | - ROLLKIT_ADDR=$(./gmd/go/bin/gmd keys show carol -a --home ./gmd/.gm) - COSMOS_ADDR=$(gaiad keys show validator -a --keyring-backend test) - gaiad tx ibc-transfer transfer transfer channel-0 $ROLLKIT_ADDR 100stake --from validator --node http://localhost:26654 --fees 200000stake --keyring-backend test -y - - # Wait for IBC transfer to complete with retry logic - echo "Waiting for IBC transfer to complete..." - MAX_ATTEMPTS=30 - ATTEMPT=0 - IBC_FOUND=false - - while [ $ATTEMPT -lt $MAX_ATTEMPTS ] && [ "$IBC_FOUND" = false ]; do - sleep 3 - ATTEMPT=$((ATTEMPT+1)) - - BALANCE=$(./gmd/go/bin/gmd query bank balances $ROLLKIT_ADDR --output json --home ./gmd/.gm | jq '.balances') - echo "Attempt $ATTEMPT/$MAX_ATTEMPTS - Gm balance: $BALANCE" - - # Check if any denom starts with ibc/ - if echo "$BALANCE" | jq -e '.[] | select(.denom | startswith("ibc/"))' > /dev/null; then - IBC_FOUND=true - echo "✅ IBC transfer successful! IBC denom found in balance." - else - echo "IBC denom not found yet, retrying in 3 seconds..." - fi - done - - if [ "$IBC_FOUND" = false ]; then - echo "Error: No IBC denom found in balance after transfer within $MAX_ATTEMPTS attempts!" - echo "Final balance: $BALANCE" - exit 1 - fi - - - name: Print logs on failure - if: failure() - run: | - echo '--- chain.log ---' - cat chain.log || true - echo '--- cosmos.log ---' - cat cosmos.log || true - echo '--- hermes.log ---' - cat hermes.log || true - - - name: Cleanup Processes - if: always() - run: | - if [[ -n "${CHAIN_PID}" ]]; then - kill -9 $CHAIN_PID || true - fi - if [[ -n "${COSMOS_PID}" ]]; then - kill -9 $COSMOS_PID || true - fi - if [[ -n "${DA_PID}" ]]; then - kill -9 $DA_PID || true - fi - if [[ -n "${HERMES_PID}" ]]; then - kill -9 $HERMES_PID || true - fi - - attester-integration: - needs: build-gm-image - name: Attester Mode Integration Test - runs-on: ubuntu-latest - timeout-minutes: 30 - env: - DO_NOT_TRACK: true - EVNODE_VERSION: "v1.0.0-beta.9" - IGNITE_VERSION: "v29.6.1" - IGNITE_EVOLVE_APP_VERSION: "main" - GAIA_VERSION: "v25.1.0" - EVOLVE_IMAGE_REPO: "evabci/gm" - EVOLVE_IMAGE_TAG: "local" - - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version-file: go.mod - - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Pull GM image from GHCR - run: | - docker pull ${{ needs.build-gm-image.outputs.image_tag }} - docker tag ${{ needs.build-gm-image.outputs.image_tag }} evabci/gm:local - - - name: Run attester integration test - working-directory: tests/integration - env: - GOTOOLCHAIN: auto - VERBOSE: "true" - run: | - go test -v -run 'TestDockerIntegrationTestSuite/TestAttesterSystem' -count=1 diff --git a/.github/workflows/migration_test.yml b/.github/workflows/migration_test.yml index f06078ce..d607606d 100644 --- a/.github/workflows/migration_test.yml +++ b/.github/workflows/migration_test.yml @@ -89,141 +89,6 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max,ignore-error=true - docker-build-evolve-image: - name: Build Evolve Docker Images - runs-on: ubuntu-latest - timeout-minutes: 20 - needs: determine-tag - - strategy: - fail-fast: false - matrix: - include: - - enable_ibc: "true" - tag_suffix: "" # normal tag - - enable_ibc: "false" - tag_suffix: "-no-ibc" # appended to tag - - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Echo tag - run: | - echo "Base tag: ${{ needs.determine-tag.outputs.image_tag }}" - echo "Full tag: ${{ needs.determine-tag.outputs.image_tag }}${{ matrix.tag_suffix }}" - echo "ENABLE_IBC=${{ matrix.enable_ibc }}" - - - name: Build & push Evolve image - uses: docker/build-push-action@v6 - with: - context: . - file: Dockerfile - build-args: | - EVNODE_VERSION=${{ env.EVNODE_VERSION }} - IGNITE_VERSION=${{ env.IGNITE_VERSION }} - IGNITE_EVOLVE_APP_VERSION=${{ env.IGNITE_EVOLVE_APP_VERSION }} - ENABLE_IBC=${{ matrix.enable_ibc }} - push: true - tags: ${{ env.EVOLVE_IMAGE_REPO }}:${{ needs.determine-tag.outputs.image_tag }}${{ matrix.tag_suffix }} - cache-from: type=gha - cache-to: type=gha,mode=max,ignore-error=true - - migration-tastora-single-node: - name: Test Migration from Cosmos SDK to Evolve (Single Node) - runs-on: ubuntu-latest - timeout-minutes: 30 - needs: - - determine-tag - - docker-build-evolve-image - - docker-build-sdk-image - - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version-file: tests/integration/go.mod - cache: true - - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Pull images - run: | - docker pull ${{ env.EVOLVE_IMAGE_REPO }}:${{ needs.determine-tag.outputs.image_tag }} - docker pull ${{ env.COSMOS_SDK_IMAGE_REPO }}:${{ needs.determine-tag.outputs.image_tag }} - - - name: Run Migration Test - run: | - cd tests/integration - go test -v -run TestMigrationSuite/TestCosmosToEvolveMigration -timeout 30m - env: - EVOLVE_IMAGE_REPO: ${{ env.EVOLVE_IMAGE_REPO }} - EVOLVE_IMAGE_TAG: ${{ needs.determine-tag.outputs.image_tag }} - COSMOS_SDK_IMAGE_REPO: ${{ env.COSMOS_SDK_IMAGE_REPO }} - COSMOS_SDK_IMAGE_TAG: ${{ needs.determine-tag.outputs.image_tag }} - - migration-tastora-multi-node: - name: Test Migration from Cosmos SDK to Evolve - runs-on: ubuntu-latest - timeout-minutes: 30 - needs: - - determine-tag - - docker-build-evolve-image - - docker-build-sdk-image - - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version-file: tests/integration/go.mod - cache: true - - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Pull images - run: | - docker pull ${{ env.EVOLVE_IMAGE_REPO }}:${{ needs.determine-tag.outputs.image_tag }}-no-ibc - docker pull ${{ env.COSMOS_SDK_IMAGE_REPO }}:${{ needs.determine-tag.outputs.image_tag }}-no-ibc - - - name: Run Migration Test - run: | - cd tests/integration - go test -v -run TestMigrationSuite/TestCosmosToEvolveMigration_MultiValidator_GovSuccess -timeout 30m - env: - EVOLVE_IMAGE_REPO: ${{ env.EVOLVE_IMAGE_REPO }} - EVOLVE_IMAGE_TAG: ${{ needs.determine-tag.outputs.image_tag }}-no-ibc - COSMOS_SDK_IMAGE_REPO: ${{ env.COSMOS_SDK_IMAGE_REPO }} - COSMOS_SDK_IMAGE_TAG: ${{ needs.determine-tag.outputs.image_tag }}-no-ibc - migration-stay-on-comet: name: Test Migration To 1 Validator Staying on Comet runs-on: ubuntu-latest diff --git a/Dockerfile.cosmos-sdk b/Dockerfile.cosmos-sdk index 3a22b34d..7036803b 100644 --- a/Dockerfile.cosmos-sdk +++ b/Dockerfile.cosmos-sdk @@ -32,6 +32,10 @@ RUN ignite evolve add-migrate # Replace ev-abci with local version to get updated proto files RUN go mod edit -replace github.com/evstack/ev-node=github.com/evstack/ev-node@${EVNODE_VERSION} && \ go mod edit -replace github.com/evstack/ev-abci=/workspace/ev-abci && \ + go mod edit -replace github.com/libp2p/go-libp2p-quic-transport=github.com/libp2p/go-libp2p-quic-transport@v0.33.1 && \ + go mod edit -replace github.com/libp2p/go-libp2p=github.com/libp2p/go-libp2p@v0.43.0 && \ + go mod edit -replace github.com/quic-go/quic-go=github.com/quic-go/quic-go@v0.54.1 && \ + go mod edit -replace github.com/quic-go/webtransport-go=github.com/quic-go/webtransport-go@v0.9.0 && \ go mod tidy # TODO: replace this with proper ignite flag to skip IBC registration when available diff --git a/modules/migrationmngr/keeper/migration.go b/modules/migrationmngr/keeper/migration.go index a4a3f308..e06eda88 100644 --- a/modules/migrationmngr/keeper/migration.go +++ b/modules/migrationmngr/keeper/migration.go @@ -31,16 +31,32 @@ func (k Keeper) migrateNow( } if migrationData.StayOnComet { - // unbond delegations, let staking module handle validator updates - k.Logger(ctx).Info("Unbonding all validators immediately (StayOnComet, IBC not enabled)") - validatorsToRemove := getValidatorsToRemove(migrationData, lastValidatorSet) - for _, val := range validatorsToRemove { + // StayOnComet (IBC disabled): fully undelegate all validators' tokens and + // explicitly set the final CometBFT validator set to a single validator with power=1. + k.Logger(ctx).Info("StayOnComet: immediate undelegation and explicit valset update (IBC disabled)") + + // unbond all validator delegations + for _, val := range lastValidatorSet { if err := k.unbondValidatorDelegations(ctx, val); err != nil { return nil, err } } - // return empty updates - staking module will update CometBFT - return []abci.ValidatorUpdate{}, nil + + validatorsToRemove := getValidatorsToRemove(migrationData, lastValidatorSet) + + // Build ABCI updates: zeros for all non-sequencers. sequencer power 1 + var updates []abci.ValidatorUpdate + for _, val := range validatorsToRemove { + updates = append(updates, val.ABCIValidatorUpdateZero()) + } + + pk, err := migrationData.Sequencer.TmConsPublicKey() + if err != nil { + return nil, sdkerrors.ErrInvalidRequest.Wrapf("failed to get sequencer pubkey: %v", err) + } + updates = append(updates, abci.ValidatorUpdate{PubKey: pk, Power: 1}) + + return updates, nil } // rollup migration: build and return ABCI updates directly @@ -166,8 +182,66 @@ func (k Keeper) migrateOver( } if migrationData.StayOnComet { - // unbond delegations gradually, let staking module handle validator updates - return k.migrateOverWithUnbonding(ctx, migrationData, lastValidatorSet, step) + // StayOnComet with IBC enabled: from the very first smoothing step, keep + // membership constant and reweight CometBFT powers so that the sequencer + // alone has >1/3 voting power. This removes timing sensitivity for IBC + // client updates. + + // Final step: set sequencer power=1 and undelegate sequencer + if step+1 == IBCSmoothingFactor { + k.Logger(ctx).Info("StayOnComet: finalization step, setting sequencer power=1 and undelegating all delegations") + + for _, val := range lastValidatorSet { + if err := k.unbondValidatorDelegations(ctx, val); err != nil { + return nil, err + } + } + + // ABCI updates: zero all non-sequencers, set sequencer to 1 + var updates []abci.ValidatorUpdate + validatorsToRemove := getValidatorsToRemove(migrationData, lastValidatorSet) + for _, val := range validatorsToRemove { + updates = append(updates, val.ABCIValidatorUpdateZero()) + } + pk, err := migrationData.Sequencer.TmConsPublicKey() + if err != nil { + return nil, sdkerrors.ErrInvalidRequest.Wrapf("failed to get sequencer pubkey: %v", err) + } + updates = append(updates, abci.ValidatorUpdate{PubKey: pk, Power: 1}) + + // increment step to mark completion next block + if err := k.MigrationStep.Set(ctx, step+1); err != nil { + return nil, sdkerrors.ErrInvalidRequest.Wrapf("failed to set migration step: %v", err) + } + + return updates, nil + } + + // emit reweighting updates: ensure sequencer gets large power, others get 1. + n := len(lastValidatorSet) + if n == 0 { + return []abci.ValidatorUpdate{}, nil + } + seqPower := int64(2 * n) + var updates []abci.ValidatorUpdate + for _, val := range lastValidatorSet { + pk, err := val.CmtConsPublicKey() + if err != nil { + return nil, sdkerrors.ErrInvalidRequest.Wrapf("failed to get validator pubkey: %v", err) + } + power := int64(1) + if val.ConsensusPubkey.Equal(migrationData.Sequencer.ConsensusPubkey) { + power = seqPower + } + updates = append(updates, abci.ValidatorUpdate{PubKey: pk, Power: power}) + } + + // advance smoothing step + if err := k.MigrationStep.Set(ctx, step+1); err != nil { + return nil, sdkerrors.ErrInvalidRequest.Wrapf("failed to set migration step: %v", err) + } + + return updates, nil } // rollup migration: build and return ABCI updates directly @@ -329,44 +403,3 @@ func getValidatorsToRemove(migrationData types.EvolveMigration, lastValidatorSet } return validatorsToRemove } - -// migrateOverWithUnbonding unbonds validators gradually over the smoothing period. -// This is used when StayOnComet is true with IBC enabled. -func (k Keeper) migrateOverWithUnbonding( - ctx context.Context, - migrationData types.EvolveMigration, - lastValidatorSet []stakingtypes.Validator, - step uint64, -) ([]abci.ValidatorUpdate, error) { - validatorsToRemove := getValidatorsToRemove(migrationData, lastValidatorSet) - - if len(validatorsToRemove) == 0 { - k.Logger(ctx).Info("No validators to remove, migration complete") - return []abci.ValidatorUpdate{}, nil - } - - // unbond validators gradually - removePerStep := (len(validatorsToRemove) + int(IBCSmoothingFactor) - 1) / int(IBCSmoothingFactor) - startRemove := int(step) * removePerStep - endRemove := min(startRemove+removePerStep, len(validatorsToRemove)) - - k.Logger(ctx).Info("Unbonding validators gradually", - "step", step, - "start_index", startRemove, - "end_index", endRemove, - "total_to_remove", len(validatorsToRemove)) - - for _, val := range validatorsToRemove[startRemove:endRemove] { - if err := k.unbondValidatorDelegations(ctx, val); err != nil { - return nil, err - } - } - - // increment step - if err := k.MigrationStep.Set(ctx, step+1); err != nil { - return nil, sdkerrors.ErrInvalidRequest.Wrapf("failed to set migration step: %v", err) - } - - // return empty updates - let staking module handle validator set changes - return []abci.ValidatorUpdate{}, nil -} diff --git a/tests/integration/single_validator_comet_migration_test.go b/tests/integration/single_validator_comet_migration_test.go index 4f2a6d37..99c9cf9f 100644 --- a/tests/integration/single_validator_comet_migration_test.go +++ b/tests/integration/single_validator_comet_migration_test.go @@ -3,6 +3,7 @@ package integration_test import ( "bytes" "context" + "encoding/json" "fmt" "sync" "testing" @@ -36,6 +37,14 @@ import ( "google.golang.org/grpc/credentials/insecure" ) +const ( + // matches the variable of the same name in ev-abci. + IBCSmoothingFactor = 30 + // firstClientID is the name of the first client that is generated. NOTE: for this test it is always the same + // as only a single client is being created on each chain. + firstClientID = "07-tendermint-1" +) + // SingleValidatorSuite tests migration from N validators to 1 validator on CometBFT type SingleValidatorSuite struct { DockerIntegrationTestSuite @@ -52,6 +61,13 @@ type SingleValidatorSuite struct { preMigrationIBCBal sdk.Coin migrationHeight uint64 + // number of validators on the primary chain at test start + initialValidators int + + // last height on the primary chain (subject) for which we've + // successfully attempted a client update on the counterparty (host). + // Used to step updates height-by-height during migration. + lastUpdatedChainOnCounterparty int64 } func TestSingleValSuite(t *testing.T) { @@ -92,6 +108,7 @@ func (s *SingleValidatorSuite) TestNTo1StayOnCometMigration() { wg.Add(2) go func() { defer wg.Done() + // start with 5 validators on the primary chain s.chain = s.createAndStartChain(ctx, 5, "gm-1") }() @@ -107,6 +124,7 @@ func (s *SingleValidatorSuite) TestNTo1StayOnCometMigration() { s.setupIBCConnection(ctx) }) + // Establish initial IBC state and capture s.ibcDenom and pre-migration balance. t.Run("perform_ibc_transfers", func(t *testing.T) { s.performIBCTransfers(ctx) }) @@ -119,6 +137,16 @@ func (s *SingleValidatorSuite) TestNTo1StayOnCometMigration() { s.waitForMigrationCompletion(ctx) }) + // Ensure the light client on the counterparty has consensus states for + // every height across the migration window. This can be done AFTER the + // migration by backfilling one height at a time. + // The equivalent of this needs to be done for each counterparty. + t.Run("backfill_client_updates", func(t *testing.T) { + end := int64(s.migrationHeight + IBCSmoothingFactor) + err := s.backfillChainClientOnCounterpartyUntil(ctx, end) + s.Require().NoError(err) + }) + t.Run("validate_single_validator", func(t *testing.T) { s.validateSingleValidatorSet(ctx) }) @@ -290,7 +318,7 @@ func (s *SingleValidatorSuite) submitSingleValidatorMigrationProposal(ctx contex s.Require().NoError(err) // schedule migration 30 blocks in the future to allow governance - migrateAt := uint64(curHeight + 30) + migrateAt := uint64(curHeight + IBCSmoothingFactor) s.migrationHeight = migrateAt s.T().Logf("Current height: %d, Migration at: %d", curHeight, migrateAt) @@ -360,12 +388,13 @@ func (s *SingleValidatorSuite) waitForMigrationCompletion(ctx context.Context) { s.T().Log("Waiting for migration to complete...") // migration should complete at migrationHeight + IBCSmoothingFactor (30 blocks) - targetHeight := int64(s.migrationHeight + 30) + targetHeight := int64(s.migrationHeight + IBCSmoothingFactor) - err := wait.ForCondition(ctx, 5*time.Minute, 5*time.Second, func() (bool, error) { + err := wait.ForCondition(ctx, time.Hour, 10*time.Second, func() (bool, error) { h, err := s.chain.Height(ctx) if err != nil { - return false, err + s.T().Logf("Error getting height: %v", err) + return false, nil } s.T().Logf("Current height: %d, Target: %d", h, targetHeight) return h >= targetHeight, nil @@ -392,29 +421,60 @@ func (s *SingleValidatorSuite) validateSingleValidatorSet(ctx context.Context) { stakeQC := stakingtypes.NewQueryClient(conn) - // check bonded validators + // staking bonded validators should be zero because all tokens are undelegated bondedResp, err := stakeQC.Validators(ctx, &stakingtypes.QueryValidatorsRequest{ Status: stakingtypes.BondStatus_name[int32(stakingtypes.Bonded)], }) s.Require().NoError(err) s.T().Logf("Bonded validators: %d", len(bondedResp.Validators)) - s.Require().Len(bondedResp.Validators, 1, "should have exactly 1 bonded validator") + s.Require().Len(bondedResp.Validators, 0, "staking should report zero bonded validators after finalization") - // check unbonding validators + // check unbonding validators: after undelegation, validators enter unbonding state unbondingResp, err := stakeQC.Validators(ctx, &stakingtypes.QueryValidatorsRequest{ Status: stakingtypes.BondStatus_name[int32(stakingtypes.Unbonding)], }) s.Require().NoError(err) s.T().Logf("Unbonding validators: %d", len(unbondingResp.Validators)) + s.Require().Equal(len(s.chain.GetNodes()), len(unbondingResp.Validators), "all validators should be in unbonding state after finalization") - // check unbonded validators + // check unbonded validators: expect 0 since unbonding period has not elapsed unbondedResp, err := stakeQC.Validators(ctx, &stakingtypes.QueryValidatorsRequest{ Status: stakingtypes.BondStatus_name[int32(stakingtypes.Unbonded)], }) s.Require().NoError(err) s.T().Logf("Unbonded validators: %d", len(unbondedResp.Validators)) + s.Require().Len(unbondedResp.Validators, 0, "no validators should be fully unbonded yet") - s.T().Log("Validator set validated: 1 bonded validator") + // additionally assert that the remaining bonded validator (sequencer) has no delegations left + // find the operator address for validator 0 + val0 := s.chain.GetNode() + stdout, stderr, err := val0.Exec(ctx, []string{ + "gmd", "keys", "show", "--address", "validator", + "--home", val0.HomeDir(), + "--keyring-backend", "test", + "--bech", "val", + }, nil) + s.Require().NoError(err, "failed to get valoper address from node 0: %s", stderr) + val0Oper := string(bytes.TrimSpace(stdout)) + + // query delegations to the remaining validator; expect zero after finalization step + delResp, err := stakeQC.ValidatorDelegations(ctx, &stakingtypes.QueryValidatorDelegationsRequest{ + ValidatorAddr: val0Oper, + Pagination: nil, + }) + s.Require().NoError(err) + s.T().Logf("Delegations to remaining validator: %d", len(delResp.DelegationResponses)) + s.Require().Len(delResp.DelegationResponses, 0, "remaining validator should have zero delegations after final step") + + // Also verify CometBFT validator set has exactly one validator with power=1 + rpcClient, err := s.chain.GetNode().GetRPCClient() + s.Require().NoError(err) + vals, err := rpcClient.Validators(ctx, nil, nil, nil) + s.Require().NoError(err) + s.Require().Equal(1, len(vals.Validators), "CometBFT should have exactly 1 validator in the set") + s.Require().Equal(int64(1), vals.Validators[0].VotingPower, "CometBFT validator should have voting power 1") + + s.T().Log("Validator set validated: staking has 0 bonded, CometBFT has 1 validator with power=1") } // validateChainProducesBlocks validates the chain continues to produce blocks @@ -447,9 +507,10 @@ func (s *SingleValidatorSuite) validateIBCStatePreserved(ctx context.Context) { gmWallet.GetFormattedAddress(), s.ibcDenom) s.Require().NoError(err) - s.Require().Equal(s.preMigrationIBCBal.Amount, currentIBCBalance.Amount) - - s.T().Logf("IBC balance preserved: %s %s", currentIBCBalance.Amount, s.ibcDenom) + // With no background transfers during migration, expect exact equality. + s.Require().Equal(s.preMigrationIBCBal.Amount, currentIBCBalance.Amount, + "IBC balance should equal pre-migration balance") + s.T().Logf("IBC balance (pre-migration): %s %s", currentIBCBalance.Amount, s.ibcDenom) // perform IBC transfer back to verify IBC still works after migration s.T().Log("Performing IBC transfer back to verify IBC functionality...") @@ -457,6 +518,10 @@ func (s *SingleValidatorSuite) validateIBCStatePreserved(ctx context.Context) { transferAmount := math.NewInt(100_000) ibcChainWallet := s.counterpartyChain.GetFaucetWallet() + // wait a few blocks to ensure relayer has synced recent heights + err = wait.ForBlocks(ctx, 3, s.counterpartyChain, s.chain) + s.Require().NoError(err) + // get counterparty network info to query balance counterpartyNetworkInfo, err := s.counterpartyChain.GetNode().GetNetworkInfo(ctx) s.Require().NoError(err) @@ -480,7 +545,7 @@ func (s *SingleValidatorSuite) validateIBCStatePreserved(ctx context.Context) { "", ) - ctxTx, cancelTx := context.WithTimeout(ctx, 2*time.Minute) + ctxTx, cancelTx := context.WithTimeout(ctx, 3*time.Minute) defer cancelTx() resp, err := s.chain.BroadcastMessages(ctxTx, gmWallet, transferMsg) s.Require().NoError(err) @@ -489,7 +554,7 @@ func (s *SingleValidatorSuite) validateIBCStatePreserved(ctx context.Context) { s.T().Log("Waiting for IBC transfer to complete...") // wait for transfer to complete on counterparty chain - err = wait.ForCondition(ctx, 2*time.Minute, 2*time.Second, func() (bool, error) { + err = wait.ForCondition(ctx, 3*time.Minute, 2*time.Second, func() (bool, error) { balance, err := queryBankBalance(ctx, counterpartyNetworkInfo.External.GRPCAddress(), ibcChainWallet.GetFormattedAddress(), @@ -498,6 +563,7 @@ func (s *SingleValidatorSuite) validateIBCStatePreserved(ctx context.Context) { return false, nil } expectedBalance := initialCounterpartyBalance.Amount.Add(transferAmount) + s.T().Logf("Waiting for IBC transfer: current=%s expected>=%s denom=stake", balance.Amount.String(), expectedBalance.String()) return balance.Amount.GTE(expectedBalance), nil }) s.Require().NoError(err) @@ -519,3 +585,88 @@ func (s *SingleValidatorSuite) calculateIBCDenom(portID, channelID, baseDenom st prefixedDenom := transfertypes.GetPrefixedDenom(portID, channelID, baseDenom) return transfertypes.ParseDenomTrace(prefixedDenom).IBCDenom() } + +// queryClientRevisionHeight returns latest_height.revision_height for the client on the host chain. +func queryClientRevisionHeight(ctx context.Context, host *cosmos.Chain, clientID string) (int64, error) { + nodes := host.GetNodes() + if len(nodes) == 0 { + return 0, fmt.Errorf("no nodes for host chain") + } + node := nodes[0].(*cosmos.ChainNode) + + networkInfo, err := node.GetNetworkInfo(ctx) + if err != nil { + return 0, fmt.Errorf("failed to get host node network info: %w", err) + } + + stdout, stderr, err := node.Exec(ctx, []string{ + "gmd", "q", "ibc", "client", "state", clientID, "-o", "json", + "--grpc-addr", networkInfo.Internal.GRPCAddress(), "--grpc-insecure", "--prove=false", + }, nil) + if err != nil { + return 0, fmt.Errorf("query client state failed: %s", stderr) + } + var resp struct { + ClientState struct { + LatestHeight struct { + RevisionHeight json.Number `json:"revision_height"` + } `json:"latest_height"` + } `json:"client_state"` + } + if err := json.Unmarshal(stdout, &resp); err != nil { + return 0, fmt.Errorf("failed to decode client state JSON: %w", err) + } + if rh, err := resp.ClientState.LatestHeight.RevisionHeight.Int64(); err == nil { + return rh, nil + } + return 0, fmt.Errorf("could not parse client revision_height from host state JSON") +} + +// updateClientAtHeight updates the client by submitting a header for a specific +// subject-chain height. Hermes expects a numeric height; for single-revision +// test chains this is sufficient. +func updateClientAtHeight(ctx context.Context, hermes *relayer.Hermes, host *cosmos.Chain, clientID string, height int64) error { + hArg := fmt.Sprintf("%d", height) + cmd := []string{ + "hermes", "--json", "update", "client", + "--host-chain", host.GetChainID(), + "--client", clientID, + "--height", hArg, + } + _, _, err := hermes.Exec(ctx, hermes.Logger, cmd, nil) + return err +} + +// backfillChainClientOnCounterpartyUntil steps from the host client's current +// trusted height + 1 up to and including endHeight on the subject chain. +func (s *SingleValidatorSuite) backfillChainClientOnCounterpartyUntil(ctx context.Context, endHeight int64) error { + if s.chain == nil || s.counterpartyChain == nil || s.hermes == nil { + return fmt.Errorf("missing chain(s) or hermes") + } + + // Start from host client's current trusted height + 1 to ensure continuity. + trusted, err := queryClientRevisionHeight(ctx, s.counterpartyChain, firstClientID) + if err != nil { + return err + } + + // Always start from the client's current trusted height + 1 on the host chain + startHeight := trusted + 1 + if startHeight < 1 { + startHeight = 1 + } + + // Do not go past the requested endHeight + if endHeight < startHeight { + return nil + } + + for h := startHeight; h <= endHeight; h++ { + if err := updateClientAtHeight(ctx, s.hermes, s.counterpartyChain, firstClientID, h); err != nil { + s.T().Logf("backfill update at height %d failed: %v", h, err) + return err + } + s.lastUpdatedChainOnCounterparty = h + } + return nil +}