From be104bb0a4ff4e6d3fb09dea21ed2bc45cf7800f Mon Sep 17 00:00:00 2001 From: Joel Smith Date: Thu, 19 Dec 2024 12:39:16 -0600 Subject: [PATCH] feat: allow validator hot swapping in mainnet --- wormchain/interchaintest/hot_swap_test.go | 231 ++++++++++++++++++ wormchain/interchaintest/ibc_receiver_test.go | 2 +- wormchain/interchaintest/setup.go | 32 ++- ...msg_server_register_account_as_guardian.go | 11 +- ...erver_register_account_as_guardian_test.go | 45 +++- 5 files changed, 291 insertions(+), 30 deletions(-) create mode 100644 wormchain/interchaintest/hot_swap_test.go diff --git a/wormchain/interchaintest/hot_swap_test.go b/wormchain/interchaintest/hot_swap_test.go new file mode 100644 index 0000000000..4ded5d0ba6 --- /dev/null +++ b/wormchain/interchaintest/hot_swap_test.go @@ -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 TestMultiValidatorHotSwap(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) +} diff --git a/wormchain/interchaintest/ibc_receiver_test.go b/wormchain/interchaintest/ibc_receiver_test.go index 38ec704d49..9334465f29 100644 --- a/wormchain/interchaintest/ibc_receiver_test.go +++ b/wormchain/interchaintest/ibc_receiver_test.go @@ -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{ { diff --git a/wormchain/interchaintest/setup.go b/wormchain/interchaintest/setup.go index 1302f76cd2..c545c55916 100644 --- a/wormchain/interchaintest/setup.go +++ b/wormchain/interchaintest/setup.go @@ -74,7 +74,7 @@ func CreateChains(t *testing.T, wormchainVersion string, guardians guardians.Val } // 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{ { @@ -171,9 +171,8 @@ func BuildInterchain(t *testing.T, chains []ibc.Chain) (context.Context, ibc.Rel // * Set Guardian Set List using new val set // * Set Guardian Validator List using new val set // * Allow list the faucet address -func ModifyGenesis(votingPeriod string, maxDepositPeriod string, guardians guardians.ValSet) func(ibc.ChainConfig, []byte) ([]byte, error) { +func ModifyGenesis(votingPeriod string, maxDepositPeriod string, guardians guardians.ValSet, numVals int, skipRelayers bool) func(ibc.ChainConfig, []byte) ([]byte, error) { return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { - numVals := len(guardians.Vals) g := make(map[string]interface{}) if err := json.Unmarshal(genbz, &g); err != nil { return nil, fmt.Errorf("failed to unmarshal genesis file: %w", err) @@ -206,10 +205,13 @@ func ModifyGenesis(votingPeriod string, maxDepositPeriod string, guardians guard return nil, fmt.Errorf("failed to get faucet address: %w", err) } - // Get relayer address - relayerAddress, err := dyno.Get(g, "app_state", "auth", "accounts", numVals+1, "address") - if err != nil { - return nil, fmt.Errorf("failed to get relayer address: %w", err) + var relayerAddress interface{} + if !skipRelayers { + // Get relayer address + relayerAddress, err = dyno.Get(g, "app_state", "auth", "accounts", numVals+1, "address") + if err != nil { + return nil, fmt.Errorf("failed to get relayer address: %w", err) + } } // Set guardian set list and validators @@ -219,7 +221,7 @@ func ModifyGenesis(votingPeriod string, maxDepositPeriod string, guardians guard Keys: [][]byte{}, } guardianValidators := []GuardianValidator{} - for i := 0; i < numVals; i++ { + for i := 0; i < len(guardians.Vals); i++ { guardianSet.Keys = append(guardianSet.Keys, guardians.Vals[i].Addr) guardianValidators = append(guardianValidators, GuardianValidator{ GuardianKey: guardians.Vals[i].Addr, @@ -240,11 +242,15 @@ func ModifyGenesis(votingPeriod string, maxDepositPeriod string, guardians guard AllowedAddress: faucetAddress.(string), Name: "Faucet", }) - allowedAddresses = append(allowedAddresses, ValidatorAllowedAddress{ - ValidatorAddress: sdk.MustBech32ifyAddressBytes(chainConfig.Bech32Prefix, validators[0]), - AllowedAddress: relayerAddress.(string), - Name: "Relayer", - }) + + if !skipRelayers { + allowedAddresses = append(allowedAddresses, ValidatorAllowedAddress{ + ValidatorAddress: sdk.MustBech32ifyAddressBytes(chainConfig.Bech32Prefix, validators[0]), + AllowedAddress: relayerAddress.(string), + Name: "Relayer", + }) + } + if err := dyno.Set(g, allowedAddresses, "app_state", "wormhole", "allowedAddresses"); err != nil { return nil, fmt.Errorf("failed to set guardian validator list: %w", err) } diff --git a/wormchain/x/wormhole/keeper/msg_server_register_account_as_guardian.go b/wormchain/x/wormhole/keeper/msg_server_register_account_as_guardian.go index 4cef14435b..de1431833f 100644 --- a/wormchain/x/wormhole/keeper/msg_server_register_account_as_guardian.go +++ b/wormchain/x/wormhole/keeper/msg_server_register_account_as_guardian.go @@ -25,6 +25,7 @@ func (k msgServer) RegisterAccountAsGuardian(goCtx context.Context, msg *types.M if err != nil { return nil, err } + // recover guardian key from signature signerHash := crypto.Keccak256Hash(wormholesdk.SignedWormchainAddressPrefix, signer) guardianKey, err := crypto.Ecrecover(signerHash.Bytes(), msg.Signature) @@ -50,16 +51,6 @@ func (k msgServer) RegisterAccountAsGuardian(goCtx context.Context, msg *types.M return nil, types.ErrGuardianSetNotFound } - consensusGuardianSetIndex, consensusIndexFound := k.GetConsensusGuardianSetIndex(ctx) - if !consensusIndexFound { - return nil, types.ErrConsensusSetUndefined - } - - // If the size of the guardian set is 1, allow hot-swapping the validator address. - if consensusIndexFound && latestGuardianSetIndex == consensusGuardianSetIndex.Index && len(latestGuardianSet.Keys) > 1 { - return nil, types.ErrConsensusSetNotUpdatable - } - if !latestGuardianSet.ContainsKey(guardianKeyAddr) { return nil, types.ErrGuardianNotFound } diff --git a/wormchain/x/wormhole/keeper/msg_server_register_account_as_guardian_test.go b/wormchain/x/wormhole/keeper/msg_server_register_account_as_guardian_test.go index 8011491b9d..7a5fc7f8fb 100644 --- a/wormchain/x/wormhole/keeper/msg_server_register_account_as_guardian_test.go +++ b/wormchain/x/wormhole/keeper/msg_server_register_account_as_guardian_test.go @@ -53,8 +53,8 @@ func TestRegisterAccountAsGuardianHotSwap(t *testing.T) { assert.Equal(t, newValAddr.Bytes(), newGuardian.ValidatorAddr) } -// disallow hot-swapping validator addresses when guardian set size is >1 -func TestRegisterAccountAsGuardianBlockHotSwap(t *testing.T) { +// test hot swapping with validator size > 1 +func TestRegisterAccountAsGuardianHotSwapMultipleValidators(t *testing.T) { // setup -- create guardian set of size 2 k, ctx := keepertest.WormholeKeeper(t) guardians, privateKeys := createNGuardianValidator(k, ctx, 2) @@ -64,8 +64,6 @@ func TestRegisterAccountAsGuardianBlockHotSwap(t *testing.T) { ChainId: uint32(vaa.ChainIDWormchain), GuardianSetExpiration: 86400, }) - newValAddr_bz := [20]byte{} - newValAddr := sdk.AccAddress(newValAddr_bz[:]) set := createNewGuardianSet(k, ctx, guardians) k.SetConsensusGuardianSetIndex(ctx, types.ConsensusGuardianSetIndex{Index: set.Index}) @@ -74,15 +72,50 @@ func TestRegisterAccountAsGuardianBlockHotSwap(t *testing.T) { context := sdk.WrapSDKContext(ctx) msgServer := keeper.NewMsgServerImpl(*k) + // store old val addr for later + + oldValAddr_bz := [20]byte{} + copy(oldValAddr_bz[:], guardians[0].ValidatorAddr) + oldValAddr := sdk.AccAddress(oldValAddr_bz[:]) + + // hot swap to new val addr + + newValAddr_bz := [20]byte{} + newValAddr := sdk.AccAddress(newValAddr_bz[:]) + // sign the new validator address as the new validator address addrHash := crypto.Keccak256Hash(wormholesdk.SignedWormchainAddressPrefix, newValAddr) sig, err := crypto.Sign(addrHash[:], privateKeys[0]) require.NoErrorf(t, err, "failed to sign wormchain address: %v", err) - // assert that we are unable to associate the guardian address with a new validator address when the set size is >1 + // assert we can hot swap when validators > 1 _, err = msgServer.RegisterAccountAsGuardian(context, &types.MsgRegisterAccountAsGuardian{ Signer: newValAddr.String(), Signature: sig, }) - assert.Error(t, types.ErrConsensusSetNotUpdatable, err) + assert.NoError(t, err) + + // assert that the guardian validator has the new validator address + newGuardian, newGuardianFound := k.GetGuardianValidator(ctx, guardians[0].GuardianKey) + require.Truef(t, newGuardianFound, "expected guardian not found in the keeper store") + assert.Equal(t, newValAddr.Bytes(), newGuardian.ValidatorAddr) + + // -- hot swap back to old val addr -- + + // sign the old validator address as the new validator address + addrHash = crypto.Keccak256Hash(wormholesdk.SignedWormchainAddressPrefix, oldValAddr) + sig, err = crypto.Sign(addrHash[:], privateKeys[0]) + require.NoErrorf(t, err, "failed to sign wormchain address: %v", err) + + // assert we can hot swap back to the old validator address + _, err = msgServer.RegisterAccountAsGuardian(context, &types.MsgRegisterAccountAsGuardian{ + Signer: oldValAddr.String(), + Signature: sig, + }) + assert.NoError(t, err) + + // assert that the guardian validator has the old validator address + oldGuardian, oldGuardianFound := k.GetGuardianValidator(ctx, guardians[0].GuardianKey) + require.Truef(t, oldGuardianFound, "expected guardian not found in the keeper store") + assert.Equal(t, oldValAddr.Bytes(), oldGuardian.ValidatorAddr) }