diff --git a/modules/migrationmngr/depinject.go b/modules/migrationmngr/depinject.go index dde1f0fd..3c22d484 100644 --- a/modules/migrationmngr/depinject.go +++ b/modules/migrationmngr/depinject.go @@ -28,14 +28,10 @@ func init() { type ModuleInputs struct { depinject.In - Config *modulev1.Module - Cdc codec.Codec - StoreService store.KVStoreService - AddressCodec address.Codec - // optional, used to detect if IBC module is enabled. - // When IBC module is present, use `depinject.Provide(IBCStoreKey(ibcStoreKey))` - IBCStoreKey keeper.IbcKVStoreKeyAlias `optional:"true"` - + Config *modulev1.Module + Cdc codec.Codec + StoreService store.KVStoreService + AddressCodec address.Codec StakingKeeper types.StakingKeeper } @@ -59,7 +55,6 @@ func ProvideModule(in ModuleInputs) ModuleOutputs { in.StoreService, in.AddressCodec, in.StakingKeeper, - in.IBCStoreKey, authority.String(), ) m := NewAppModule(in.Cdc, k) diff --git a/modules/migrationmngr/keeper/abci.go b/modules/migrationmngr/keeper/abci.go index 7ce081f7..3f86197e 100644 --- a/modules/migrationmngr/keeper/abci.go +++ b/modules/migrationmngr/keeper/abci.go @@ -95,17 +95,9 @@ func (k Keeper) EndBlock(ctx context.Context) ([]abci.ValidatorUpdate, error) { } var updates []abci.ValidatorUpdate - if !k.isIBCEnabled(ctx) { - // if IBC is not enabled, we can migrate immediately - // but only return updates on the first block of migration (start height) - if uint64(sdkCtx.BlockHeight()) == start { - updates, err = k.migrateNow(ctx, migration, validatorSet) - if err != nil { - return nil, err - } - } - } else { - updates, err = k.migrateOver(sdkCtx, migration, validatorSet) + // Always perform immediate migration updates at the start height. + if uint64(sdkCtx.BlockHeight()) == start { + updates, err = k.migrateNow(ctx, migration, validatorSet) if err != nil { return nil, err } diff --git a/modules/migrationmngr/keeper/grpc_query_test.go b/modules/migrationmngr/keeper/grpc_query_test.go index 271bd64c..e803238a 100644 --- a/modules/migrationmngr/keeper/grpc_query_test.go +++ b/modules/migrationmngr/keeper/grpc_query_test.go @@ -112,7 +112,6 @@ func initFixture(tb testing.TB) *fixture { storeService, addressCodec, stakingKeeper, - nil, sdk.AccAddress(address.Module(types.ModuleName)).String(), ) @@ -165,41 +164,6 @@ func TestIsMigrating(t *testing.T) { require.Equal(t, uint64(2), resp.EndBlockHeight) } -func TestIsMigrating_IBCEnabled(t *testing.T) { - stakingKeeper := &mockStakingKeeper{} - key := storetypes.NewKVStoreKey(types.ModuleName) - storeService := runtime.NewKVStoreService(key) - encCfg := moduletestutil.MakeTestEncodingConfig(migrationmngr.AppModuleBasic{}) - addressCodec := addresscodec.NewBech32Codec("cosmos") - ibcKey := storetypes.NewKVStoreKey("ibc") - ctx := testutil.DefaultContextWithKeys(map[string]*storetypes.KVStoreKey{ - types.ModuleName: key, - "ibc": ibcKey, - }, nil, nil) - - k := keeper.NewKeeper( - encCfg.Codec, - storeService, - addressCodec, - stakingKeeper, - func() *storetypes.KVStoreKey { return key }, - sdk.AccAddress(address.Module(types.ModuleName)).String(), - ) - - // set up migration - require.NoError(t, k.Migration.Set(ctx, types.EvolveMigration{ - BlockHeight: 1, - Sequencer: types.Sequencer{Name: "foo"}, - })) - - ctx = ctx.WithBlockHeight(1) - resp, err := keeper.NewQueryServer(k).IsMigrating(ctx, &types.QueryIsMigratingRequest{}) - require.NoError(t, err) - require.True(t, resp.IsMigrating) - require.Equal(t, uint64(1), resp.StartBlockHeight) - require.Equal(t, 1+keeper.IBCSmoothingFactor, resp.EndBlockHeight) -} - func TestSequencer_Migrating(t *testing.T) { s := initFixture(t) diff --git a/modules/migrationmngr/keeper/keeper.go b/modules/migrationmngr/keeper/keeper.go index 0d05171f..ffe2aaee 100644 --- a/modules/migrationmngr/keeper/keeper.go +++ b/modules/migrationmngr/keeper/keeper.go @@ -8,24 +8,18 @@ import ( "cosmossdk.io/core/address" corestore "cosmossdk.io/core/store" "cosmossdk.io/log" - storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/evstack/ev-abci/modules/migrationmngr/types" ) -// IbcStoreKey is the store key used for IBC-related data. -// It is an alias for storetypes.StoreKey to allow depinject to resolve it as a dependency (as runtime assumes 1 module = 1 store key maximum). -type IbcKVStoreKeyAlias = func() *storetypes.KVStoreKey - type Keeper struct { storeService corestore.KVStoreService cdc codec.BinaryCodec addressCodec address.Codec authority string - ibcStoreKey IbcKVStoreKeyAlias stakingKeeper types.StakingKeeper Schema collections.Schema @@ -40,7 +34,6 @@ func NewKeeper( storeService corestore.KVStoreService, addressCodec address.Codec, stakingKeeper types.StakingKeeper, - ibcStoreKey IbcKVStoreKeyAlias, authority string, ) Keeper { // ensure that authority is a valid account address @@ -55,7 +48,6 @@ func NewKeeper( authority: authority, addressCodec: addressCodec, stakingKeeper: stakingKeeper, - ibcStoreKey: ibcStoreKey, Sequencer: collections.NewItem( sb, types.SequencerKey, @@ -103,13 +95,8 @@ func (k Keeper) IsMigrating(ctx context.Context) (start, end uint64, ok bool) { return 0, 0, false } - // smoothen the migration over IBCSmoothingFactor blocks, in order to migrate the validator set to the sequencer or attesters network when IBC is enabled. - migrationEndHeight := migration.BlockHeight + IBCSmoothingFactor - - // If IBC is not enabled, the migration can be done in one block. - if !k.isIBCEnabled(ctx) { - migrationEndHeight = migration.BlockHeight + 1 - } + // Migration is performed in a single step regardless of IBC. + migrationEndHeight := migration.BlockHeight + 1 sdkCtx := sdk.UnwrapSDKContext(ctx) currentHeight := uint64(sdkCtx.BlockHeight()) @@ -117,28 +104,3 @@ func (k Keeper) IsMigrating(ctx context.Context) (start, end uint64, ok bool) { return migration.BlockHeight, migrationEndHeight, migrationInProgress } - -// isIBCEnabled checks if IBC is enabled on the chain. -// In order to not import the IBC module, we only check if the IBC store exists, -// but not the ibc params. This should be sufficient for our use case. -func (k Keeper) isIBCEnabled(ctx context.Context) bool { - enabled := true - - if k.ibcStoreKey == nil { - return false - } - - sdkCtx := sdk.UnwrapSDKContext(ctx) - - ms := sdkCtx.MultiStore().CacheMultiStore() - defer func() { - if r := recover(); r != nil { - // If we panic, it means the store does not exist, so IBC is not enabled. - enabled = false - } - }() - ms.GetKVStore(k.ibcStoreKey()) - - // has not panicked, so store exists - return enabled -} diff --git a/modules/migrationmngr/keeper/migration.go b/modules/migrationmngr/keeper/migration.go index e06eda88..f3b17b13 100644 --- a/modules/migrationmngr/keeper/migration.go +++ b/modules/migrationmngr/keeper/migration.go @@ -2,9 +2,7 @@ package keeper import ( "context" - "errors" - "cosmossdk.io/collections" abci "github.com/cometbft/cometbft/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -33,7 +31,7 @@ func (k Keeper) migrateNow( if migrationData.StayOnComet { // 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)") + k.Logger(ctx).Info("StayOnComet: immediate undelegation and explicit valset update") // unbond all validator delegations for _, val := range lastValidatorSet { @@ -150,199 +148,6 @@ func migrateToAttesters( return initialValUpdates, nil } -// migrateOver migrates the chain to evolve over a period of blocks. -// this is to ensure ibc light client verification keep working while changing the whole validator set. -// the migration step is tracked in store. -// If StayOnComet is true, delegations are unbonded gradually and empty updates returned. -// Otherwise, ABCI ValidatorUpdates are returned directly for rollup migration. -func (k Keeper) migrateOver( - ctx context.Context, - migrationData types.EvolveMigration, - lastValidatorSet []stakingtypes.Validator, -) (initialValUpdates []abci.ValidatorUpdate, err error) { - step, err := k.MigrationStep.Get(ctx) - if err != nil && !errors.Is(err, collections.ErrNotFound) { - return nil, sdkerrors.ErrInvalidRequest.Wrapf("failed to get migration step: %v", err) - } - - if step >= IBCSmoothingFactor { - // migration complete - if err := k.MigrationStep.Remove(ctx); err != nil { - return nil, sdkerrors.ErrInvalidRequest.Wrapf("failed to remove migration step: %v", err) - } - - if migrationData.StayOnComet { - // unbonding was already completed gradually over previous blocks, just return empty updates - k.Logger(ctx).Info("Migration complete, all validators unbonded gradually") - return []abci.ValidatorUpdate{}, nil - } - - // rollup migration: return final ABCI validator updates - return k.migrateNow(ctx, migrationData, lastValidatorSet) - } - - if migrationData.StayOnComet { - // 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 - switch len(migrationData.Attesters) { - case 0: - // no attesters, migrate to a single sequencer over smoothing period - // remove all validators except the sequencer, add sequencer at the end - seq := migrationData.Sequencer - var oldValsToRemove []stakingtypes.Validator - for _, val := range lastValidatorSet { - if !val.ConsensusPubkey.Equal(seq.ConsensusPubkey) { - oldValsToRemove = append(oldValsToRemove, val) - } - } - removePerStep := (len(oldValsToRemove) + int(IBCSmoothingFactor) - 1) / int(IBCSmoothingFactor) - startRemove := int(step) * removePerStep - endRemove := min(startRemove+removePerStep, len(oldValsToRemove)) - for _, val := range oldValsToRemove[startRemove:endRemove] { - powerUpdate := val.ABCIValidatorUpdateZero() - initialValUpdates = append(initialValUpdates, powerUpdate) - } - default: - // attesters present, migrate as before - attesterPubKeys := make(map[string]struct{}) - for _, attester := range migrationData.Attesters { - attesterPubKeys[attester.ConsensusPubkey.String()] = struct{}{} - } - var oldValsToRemove []stakingtypes.Validator - for _, val := range lastValidatorSet { - if _, ok := attesterPubKeys[val.ConsensusPubkey.String()]; !ok { - oldValsToRemove = append(oldValsToRemove, val) - } - } - lastValPubKeys := make(map[string]struct{}) - for _, val := range lastValidatorSet { - lastValPubKeys[val.ConsensusPubkey.String()] = struct{}{} - } - var newAttestersToAdd []types.Attester - for _, attester := range migrationData.Attesters { - if _, ok := lastValPubKeys[attester.ConsensusPubkey.String()]; !ok { - newAttestersToAdd = append(newAttestersToAdd, attester) - } - } - removePerStep := (len(oldValsToRemove) + int(IBCSmoothingFactor) - 1) / int(IBCSmoothingFactor) - addPerStep := (len(newAttestersToAdd) + int(IBCSmoothingFactor) - 1) / int(IBCSmoothingFactor) - startRemove := int(step) * removePerStep - endRemove := min(startRemove+removePerStep, len(oldValsToRemove)) - for _, val := range oldValsToRemove[startRemove:endRemove] { - powerUpdate := val.ABCIValidatorUpdateZero() - initialValUpdates = append(initialValUpdates, powerUpdate) - } - startAdd := int(step) * addPerStep - endAdd := min(startAdd+addPerStep, len(newAttestersToAdd)) - for _, attester := range newAttestersToAdd[startAdd:endAdd] { - pk, err := attester.TmConsPublicKey() - if err != nil { - return nil, sdkerrors.ErrInvalidRequest.Wrapf("failed to get attester pubkey: %v", err) - } - attesterUpdate := abci.ValidatorUpdate{ - PubKey: pk, - Power: 1, - } - initialValUpdates = append(initialValUpdates, attesterUpdate) - } - } - - // increment and persist the step - if err := k.MigrationStep.Set(ctx, step+1); err != nil { - return nil, sdkerrors.ErrInvalidRequest.Wrapf("failed to set migration step: %v", err) - } - - // the first time, we set the whole validator set to the same validator power. This is to avoid a validator ends up with >= 33% or worse >= 66% - // vp during the migration. - // TODO: add a test - if step == 0 { - // Create a map of existing updates for O(1) lookup - existingUpdates := make(map[string]bool) - for _, powerUpdate := range initialValUpdates { - existingUpdates[powerUpdate.PubKey.String()] = true - } - - // set the whole validator set to the same power - for _, val := range lastValidatorSet { - valPubKey, err := val.CmtConsPublicKey() - if err != nil { - return nil, sdkerrors.ErrInvalidRequest.Wrapf("failed to get validator pubkey: %v", err) - } - - if !existingUpdates[valPubKey.String()] { - powerUpdate := abci.ValidatorUpdate{ - PubKey: valPubKey, - Power: 1, - } - initialValUpdates = append(initialValUpdates, powerUpdate) - } - } - } - - return initialValUpdates, nil -} - // unbondValidatorDelegations unbonds all delegations to a specific validator. // This is used when StayOnComet is true to properly return tokens to delegators. func (k Keeper) unbondValidatorDelegations(ctx context.Context, validator stakingtypes.Validator) error { diff --git a/tests/integration/single_validator_comet_migration_test.go b/tests/integration/single_validator_comet_migration_test.go index 99c9cf9f..0b273033 100644 --- a/tests/integration/single_validator_comet_migration_test.go +++ b/tests/integration/single_validator_comet_migration_test.go @@ -3,7 +3,6 @@ package integration_test import ( "bytes" "context" - "encoding/json" "fmt" "sync" "testing" @@ -38,8 +37,6 @@ import ( ) 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" @@ -61,13 +58,6 @@ 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) { @@ -137,13 +127,11 @@ 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) + // Perform client update at H + 1 to ensure the counterparty's light client can verify the new validator set. + // At H, the val updates are returned in EndBlock, by specifically sending a client update at H + 1, we ensure that + // subsequent ibc messages will succeed. + t.Run("client_updates_at_upgrade", func(t *testing.T) { + err := updateClientAtHeight(ctx, s.hermes, s.counterpartyChain, firstClientID, int64(s.migrationHeight+1)) s.Require().NoError(err) }) @@ -151,6 +139,10 @@ func (s *SingleValidatorSuite) TestNTo1StayOnCometMigration() { s.validateSingleValidatorSet(ctx) }) + t.Run("remove_old_validators", func(t *testing.T) { + s.removeOldValidators(ctx) + }) + t.Run("validate_chain_continues", func(t *testing.T) { s.validateChainProducesBlocks(ctx) }) @@ -317,8 +309,11 @@ func (s *SingleValidatorSuite) submitSingleValidatorMigrationProposal(ctx contex curHeight, err := s.chain.Height(ctx) s.Require().NoError(err) - // schedule migration 30 blocks in the future to allow governance - migrateAt := uint64(curHeight + IBCSmoothingFactor) + // schedule migration some blocks in the future to allow governance to pass + // keep this small to speed up the test + // allow enough blocks for deposit and voting to complete + const governanceBuffer = 30 + migrateAt := uint64(curHeight + governanceBuffer) s.migrationHeight = migrateAt s.T().Logf("Current height: %d, Migration at: %d", curHeight, migrateAt) @@ -383,12 +378,13 @@ func (s *SingleValidatorSuite) getValidatorPubKey(ctx context.Context, conn *grp return nil } -// waitForMigrationCompletion waits for the 30-block migration window to complete +// waitForMigrationCompletion waits for the migration to finalize on-chain 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 + IBCSmoothingFactor) + // migration updates are emitted at migrationHeight, and take effect at migrationHeight+1. + // wait until at least migrationHeight+2 to ensure finalization has occurred. + targetHeight := int64(s.migrationHeight + 2) err := wait.ForCondition(ctx, time.Hour, 10*time.Second, func() (bool, error) { h, err := s.chain.Height(ctx) @@ -477,6 +473,25 @@ func (s *SingleValidatorSuite) validateSingleValidatorSet(ctx context.Context) { s.T().Log("Validator set validated: staking has 0 bonded, CometBFT has 1 validator with power=1") } +// removeOldValidators removes all validator containers except the first one +func (s *SingleValidatorSuite) removeOldValidators(ctx context.Context) { + s.T().Log("Removing old validator containers...") + + nodes := s.chain.GetNodes() + s.Require().Greater(len(nodes), 1, "expected multiple validators to remove") + + // remove all validators except the first one (index 0) + for i := 1; i < len(nodes); i++ { + node := nodes[i].(*cosmos.ChainNode) + s.T().Logf("Stopping and removing node %d", i) + + err := node.Remove(ctx) + s.Require().NoError(err, "failed to remove container for node %d", i) + } + + s.T().Logf("Removed %d old validator containers", len(nodes)-1) +} + // validateChainProducesBlocks validates the chain continues to produce blocks func (s *SingleValidatorSuite) validateChainProducesBlocks(ctx context.Context) { s.T().Log("Validating chain produces blocks...") @@ -484,7 +499,7 @@ func (s *SingleValidatorSuite) validateChainProducesBlocks(ctx context.Context) initialHeight, err := s.chain.Height(ctx) s.Require().NoError(err) - err = wait.ForBlocks(ctx, 5, s.chain) + err = wait.ForBlocks(ctx, 10, s.chain) s.Require().NoError(err) finalHeight, err := s.chain.Height(ctx) @@ -586,42 +601,6 @@ func (s *SingleValidatorSuite) calculateIBCDenom(portID, channelID, baseDenom st 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. @@ -636,37 +615,3 @@ func updateClientAtHeight(ctx context.Context, hermes *relayer.Hermes, host *cos _, _, 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 -}