Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mainnet validator hot swaps #4203

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/wormchain-icts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,45 @@ permissions:

env:
GO_VERSION: 1.21
TAR_PATH: /tmp/wormchain-docker-image.tar
IMAGE_NAME: wormchain-docker-image

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build-docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: wormchain/interchaintest/go.sum

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build and export
uses: docker/build-push-action@v5
with:
context: .
file: wormchain/Dockerfile.ict
tags: wormchain:local
outputs: type=docker,dest=${{ env.TAR_PATH }}

- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: ${{ env.IMAGE_NAME }}
path: ${{ env.TAR_PATH }}

e2e-tests:
needs: build-docker
runs-on: ubuntu-latest
strategy:
matrix:
Expand All @@ -30,6 +62,7 @@ jobs:
- "ictest-upgrade"
- "ictest-wormchain"
- "ictest-ibc-receiver"
- "ictest-validator-hotswap"
fail-fast: false

steps:
Expand All @@ -42,6 +75,17 @@ jobs:
- name: checkout chain
uses: actions/checkout@v4

- name: Download Tarball Artifact
uses: actions/download-artifact@v3
with:
name: ${{ env.IMAGE_NAME }}
path: /tmp

- name: Load Docker Image
run: |
docker image load -i ${{ env.TAR_PATH }}
docker image ls -a

- name: Run Test
id: run_test
continue-on-error: true
Expand Down
40 changes: 40 additions & 0 deletions wormchain/Dockerfile.ict
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
FROM golang:1.22.5@sha256:86a3c48a61915a8c62c0e1d7594730399caa3feb73655dfe96c7bc17710e96cf AS builder

WORKDIR /app

# Install dependencies
RUN apt update && \
apt-get install -y \
build-essential \
ca-certificates \
curl

# Enable faster module downloading.
ENV GOPROXY https://proxy.golang.org

COPY ./wormchain/go.mod .
COPY ./wormchain/go.sum .
COPY ./sdk /sdk
RUN go mod download

COPY ./wormchain .

RUN make build/wormchaind

FROM golang:1.22.5@sha256:86a3c48a61915a8c62c0e1d7594730399caa3feb73655dfe96c7bc17710e96cf

WORKDIR /home/heighliner

COPY --from=builder /app/build/wormchaind /usr/bin

# copy over c bindings (libwasmvm.x86_64.so, etc)
COPY --from=builder /go/pkg/mod/github.com/!cosm!wasm/[email protected]/internal/api/* /usr/lib

EXPOSE 26657
EXPOSE 26656
EXPOSE 6060
EXPOSE 9090
EXPOSE 1317
EXPOSE 4500

ENTRYPOINT [ "wormchaind" ]
9 changes: 8 additions & 1 deletion wormchain/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ clean:
## INTERCHAINTESTS ##
#####################

# Generate Wormchain Image
local-image: build/wormchaind
docker build -t wormchain:local -f Dockerfile.ict ..

# Individual Tests ($$ is interpreted as $)
rm-testcache:
go clean -testcache
Expand All @@ -105,4 +109,7 @@ ictest-wormchain: rm-testcache
ictest-ibc-receiver: rm-testcache
cd interchaintest && go test -race -v -run ^TestIbcReceiver ./...

.PHONY: ictest-cancel-upgrade ictest-malformed-payload ictest-upgrade-failure ictest-upgrade ictest-wormchain ictest-ibc-receiver
ictest-validator-hotswap: rm-testcache
cd interchaintest && go test -race -v -run ^TestValidatorHotswap$$ ./...

.PHONY: ictest-cancel-upgrade ictest-malformed-payload ictest-upgrade-failure ictest-upgrade ictest-wormchain ictest-ibc-receiver ictest-validator-hotswap
231 changes: 231 additions & 0 deletions wormchain/interchaintest/hot_swap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package ictest

import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"strings"
"testing"

"github.com/strangelove-ventures/interchaintest/v4"
"github.com/strangelove-ventures/interchaintest/v4/chain/cosmos"
"github.com/strangelove-ventures/interchaintest/v4/ibc"
"github.com/strangelove-ventures/interchaintest/v4/testreporter"
"github.com/strangelove-ventures/interchaintest/v4/testutil"
"github.com/stretchr/testify/require"
"github.com/wormhole-foundation/wormchain/interchaintest/guardians"
"github.com/wormhole-foundation/wormchain/interchaintest/helpers"
"go.uber.org/zap/zaptest"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/crypto"
wormholetypes "github.com/wormhole-foundation/wormchain/x/wormhole/types"
wormholesdk "github.com/wormhole-foundation/wormhole/sdk"
)

func SetupHotSwapChain(t *testing.T, wormchainVersion string, guardians guardians.ValSet, numVals int) ibc.Chain {
wormchainConfig.Images[0].Version = wormchainVersion

if wormchainVersion == "local" {
wormchainConfig.Images[0].Repository = "wormchain"
}

// Create chain factory with wormchain
wormchainConfig.ModifyGenesis = ModifyGenesis(votingPeriod, maxDepositPeriod, guardians, numVals, true)

numFullNodes := 0
cf := interchaintest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*interchaintest.ChainSpec{
{
ChainName: "wormchain",
ChainConfig: wormchainConfig,
NumValidators: &numVals,
NumFullNodes: &numFullNodes,
},
})

// Get chains from the chain factory
chains, err := cf.Chains(t.Name())
require.NoError(t, err)

return chains[0]
}

type ValidatorInfo struct {
Validator *cosmos.ChainNode
Bech32Addr string
AccAddr sdk.AccAddress
}

type QueryAllGuardianValidatorResponse struct {
GuardianValidators []GuardianValidator `json:"guardianValidator"`
}

type QueryGetGuardianValidatorResponse struct {
GuardianValidator GuardianValidator `json:"guardianValidator"`
}

func TestValidatorHotswap(t *testing.T) {
// Base setup
numGuardians := 2
numVals := 3
guardians := guardians.CreateValSet(t, numGuardians)
chain := SetupHotSwapChain(t, "local", *guardians, numVals)

ic := interchaintest.NewInterchain().AddChain(chain)
ctx := context.Background()
rep := testreporter.NewNopReporter()
eRep := rep.RelayerExecReporter(t)
client, network := interchaintest.DockerSetup(t)

err := ic.Build(ctx, eRep, interchaintest.InterchainBuildOptions{
TestName: t.Name(),
Client: client,
NetworkID: network,
SkipPathCreation: true,
})
require.NoError(t, err)

t.Cleanup(func() {
_ = ic.Close()
})

wormchain := chain.(*cosmos.CosmosChain)

// ============================

// Query active guardian validators (returns both keys & sdk acc address)
res, _, err := wormchain.Validators[0].ExecQuery(ctx, "wormhole", "list-guardian-validator")
require.NoError(t, err)

// Validate response
var guardianValidators QueryAllGuardianValidatorResponse
err = json.Unmarshal(res, &guardianValidators)
require.NoError(t, err)
require.Equal(t, numGuardians, len(guardianValidators.GuardianValidators))

// ============================

// NOTE:
//
// wormchain.Validators & the guardan query do not guarantee order, so we need to map the validators to match the order
// of the guardian set reference.

// First guardian key refs - will swap from using first validator to last validator, then back again
firstGuardianKey := guardianValidators.GuardianValidators[0].GuardianKey
firstGuardianPrivKey := guardians.Vals[0].Priv
if !bytes.Equal(firstGuardianKey, guardians.Vals[0].Addr) {
firstGuardianPrivKey = guardians.Vals[1].Priv
}

// Guardian validatore sdk addresses
firstGuardianValAddr := sdk.AccAddress(guardianValidators.GuardianValidators[0].ValidatorAddr)
secondGuardianValAddr := sdk.AccAddress(guardianValidators.GuardianValidators[1].ValidatorAddr)

// Map validators to guardian set order
var validators [3]ValidatorInfo
for _, val := range wormchain.Validators {
valBech32Addr, err := val.AccountKeyBech32(ctx, "validator")
require.NoError(t, err)

valInfo := ValidatorInfo{
Validator: val,
Bech32Addr: valBech32Addr,
AccAddr: helpers.MustAccAddressFromBech32(valBech32Addr, "wormhole"),
}

if strings.Contains(valInfo.AccAddr.String(), firstGuardianValAddr.String()) {
validators[0] = valInfo
} else if strings.Contains(valInfo.AccAddr.String(), secondGuardianValAddr.String()) {
validators[1] = valInfo
} else {
validators[2] = valInfo
}
}

// Ensure all validators are mapped
require.NotNil(t, validators[0])
require.NotNil(t, validators[1])
require.NotNil(t, validators[2])

// References to first & last validator
firstVal := validators[0]
newVal := validators[2]

// ============================

// Ensure chain can produce blocks with the last validator shut down,
// as it is not in the active set
newVal.Validator.StopContainer(ctx)
err = testutil.WaitForBlocks(ctx, 10, wormchain)
require.NoError(t, err)
newVal.Validator.StartContainer(ctx)

// ============================

// Query the first guardian's validator
guardianKey := hex.EncodeToString(firstGuardianKey)
res, _, err = newVal.Validator.ExecQuery(ctx, "wormhole", "show-guardian-validator", guardianKey)
require.NoError(t, err)

// Ensure the first guardian's validator is set to the first validator
var valResponse wormholetypes.QueryGetGuardianValidatorResponse
err = json.Unmarshal(res, &valResponse)
require.NoError(t, err)
require.Equal(t, firstGuardianKey, valResponse.GuardianValidator.GuardianKey)
require.Equal(t, firstVal.AccAddr.Bytes(), valResponse.GuardianValidator.ValidatorAddr)

// ============================

// Use first validator to allow list the last validator (as it is not in active set)
_, err = firstVal.Validator.ExecTx(ctx, "validator", "wormhole", "create-allowed-address", newVal.Bech32Addr, "newVal")
require.NoError(t, err)

// Migrate first guardian to use last validator
addrHash := crypto.Keccak256Hash(wormholesdk.SignedWormchainAddressPrefix, newVal.AccAddr)
sig, err := crypto.Sign(addrHash[:], firstGuardianPrivKey)
require.NoErrorf(t, err, "failed to sign wormchain address: %v", err)
_, err = newVal.Validator.ExecTx(ctx, "validator", "wormhole", "register-account-as-guardian", hex.EncodeToString(sig))
require.NoError(t, err)

// Query the first guardian's validator
res, _, err = newVal.Validator.ExecQuery(ctx, "wormhole", "show-guardian-validator", guardianKey)
require.NoError(t, err)

// Ensure the first guardian's validator is set to the last validator
err = json.Unmarshal(res, &valResponse)
require.NoError(t, err)
require.Equal(t, firstGuardianKey, valResponse.GuardianValidator.GuardianKey)
require.Equal(t, newVal.AccAddr.Bytes(), valResponse.GuardianValidator.ValidatorAddr)

// Wait 10 blocks to ensure blocks are being produced
err = testutil.WaitForBlocks(ctx, 10, wormchain)
require.NoError(t, err)

// ============================

// Use last validator to allow list the first validator (as it is not in active set *anymore)
_, err = newVal.Validator.ExecTx(ctx, "validator", "wormhole", "create-allowed-address", firstVal.Bech32Addr, "firstVal")
require.NoError(t, err)

// Migrate first guardian back to use first validator
addrHash = crypto.Keccak256Hash(wormholesdk.SignedWormchainAddressPrefix, firstVal.AccAddr)
sig, err = crypto.Sign(addrHash[:], firstGuardianPrivKey)
require.NoErrorf(t, err, "failed to sign wormchain address: %v", err)
_, err = firstVal.Validator.ExecTx(ctx, "validator", "wormhole", "register-account-as-guardian", hex.EncodeToString(sig))
require.NoError(t, err)

// Query the first guardian's validator
res, _, err = firstVal.Validator.ExecQuery(ctx, "wormhole", "show-guardian-validator", guardianKey)
require.NoError(t, err)

// Ensure the first guardian's validator is set to the first validator
err = json.Unmarshal(res, &valResponse)
require.NoError(t, err)
require.Equal(t, firstGuardianKey, valResponse.GuardianValidator.GuardianKey)
require.Equal(t, firstVal.AccAddr.Bytes(), valResponse.GuardianValidator.ValidatorAddr)

// Wait 10 blocks to ensure blocks are being produced
err = testutil.WaitForBlocks(ctx, 10, wormchain)
require.NoError(t, err)
}
2 changes: 1 addition & 1 deletion wormchain/interchaintest/ibc_receiver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func createChains(t *testing.T, wormchainVersion string, guardians guardians.Val
wormchainConfig.Images[0].Version = wormchainVersion

// Create chain factory with wormchain
wormchainConfig.ModifyGenesis = ModifyGenesis(votingPeriod, maxDepositPeriod, guardians)
wormchainConfig.ModifyGenesis = ModifyGenesis(votingPeriod, maxDepositPeriod, guardians, len(guardians.Vals), false)

cf := interchaintest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*interchaintest.ChainSpec{
{
Expand Down
Loading
Loading