From 52a5c9a00cff7ddccfba56f981509184d12dc54f Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:37:58 +0200 Subject: [PATCH 01/20] liveness-based consumer removal: ack clock, block-clock sweep, and demoted IBC callbacks --- x/vaas/provider/ibc_module.go | 8 +- x/vaas/provider/ibc_module_test.go | 12 +- x/vaas/provider/keeper/consumer_lifecycle.go | 29 +++ .../keeper/ibc_v2_integration_test.go | 25 ++- x/vaas/provider/keeper/keeper.go | 76 ++++++++ x/vaas/provider/keeper/liveness_test.go | 177 ++++++++++++++++++ x/vaas/provider/keeper/params.go | 12 ++ x/vaas/provider/keeper/relay.go | 52 +++-- x/vaas/provider/keeper/relay_test.go | 30 ++- x/vaas/provider/module.go | 5 + x/vaas/provider/module_test.go | 11 +- x/vaas/provider/types/keys.go | 3 + x/vaas/provider/types/params.go | 7 + 13 files changed, 375 insertions(+), 72 deletions(-) create mode 100644 x/vaas/provider/keeper/liveness_test.go diff --git a/x/vaas/provider/ibc_module.go b/x/vaas/provider/ibc_module.go index e869b25..8237f33 100644 --- a/x/vaas/provider/ibc_module.go +++ b/x/vaas/provider/ibc_module.go @@ -173,7 +173,13 @@ func (im IBCModule) OnAcknowledgementPacket( ackError = "error acknowledgement received" } - if err := im.keeper.OnAcknowledgementPacketV2(ctx, sourceClient, ackError); err != nil { + ackVscId := uint64(0) + var vsc vaastypes.ValidatorSetChangePacketData + if err := vaastypes.ModuleCdc.UnmarshalJSON(payload.Value, &vsc); err == nil { + ackVscId = vsc.ValsetUpdateId + } + + if err := im.keeper.OnAcknowledgementPacketV2(ctx, sourceClient, ackVscId, ackError); err != nil { return err } diff --git a/x/vaas/provider/ibc_module_test.go b/x/vaas/provider/ibc_module_test.go index d56971a..74ab3db 100644 --- a/x/vaas/provider/ibc_module_test.go +++ b/x/vaas/provider/ibc_module_test.go @@ -2,7 +2,6 @@ package provider_test import ( "testing" - "time" testkeeper "github.com/allinbits/vaas/testutil/keeper" "github.com/allinbits/vaas/x/vaas/provider" @@ -35,7 +34,7 @@ func TestIBCModuleOnSendPacketRequiresAuthority(t *testing.T) { } func TestIBCModuleOnAcknowledgementPacketHandlesErrorSentinel(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() consumerID := uint64(0) @@ -45,8 +44,6 @@ func TestIBCModuleOnAcknowledgementPacketHandlesErrorSentinel(t *testing.T) { providerKeeper.SetConsumerPhase(ctx, consumerID, providertypes.CONSUMER_PHASE_LAUNCHED) providerKeeper.SetConsumerChainId(ctx, consumerID, "consumer-chain") - mocks.MockStakingKeeper.EXPECT().UnbondingTime(ctx).Return(21*24*time.Hour, nil).Times(1) - module := provider.NewIBCModule(&providerKeeper) err := module.OnAcknowledgementPacket( ctx, @@ -58,12 +55,13 @@ func TestIBCModuleOnAcknowledgementPacketHandlesErrorSentinel(t *testing.T) { sdk.AccAddress{}, ) require.NoError(t, err) - require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, providerKeeper.GetConsumerPhase(ctx, consumerID)) + // Consumer stays launched; liveness sweep owns removal + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, providerKeeper.GetConsumerPhase(ctx, consumerID)) require.Equal(t, "65", findEventAttributeValue(ctx.EventManager().Events(), vaastypes.EventTypePacket, "sequence")) } func TestIBCModuleOnTimeoutPacketFormatsSequence(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() consumerID := uint64(0) @@ -73,8 +71,6 @@ func TestIBCModuleOnTimeoutPacketFormatsSequence(t *testing.T) { providerKeeper.SetConsumerPhase(ctx, consumerID, providertypes.CONSUMER_PHASE_LAUNCHED) providerKeeper.SetConsumerChainId(ctx, consumerID, "consumer-chain") - mocks.MockStakingKeeper.EXPECT().UnbondingTime(ctx).Return(21*24*time.Hour, nil).Times(1) - module := provider.NewIBCModule(&providerKeeper) err := module.OnTimeoutPacket( ctx, diff --git a/x/vaas/provider/keeper/consumer_lifecycle.go b/x/vaas/provider/keeper/consumer_lifecycle.go index 37fa258..2e88858 100644 --- a/x/vaas/provider/keeper/consumer_lifecycle.go +++ b/x/vaas/provider/keeper/consumer_lifecycle.go @@ -254,6 +254,9 @@ func (k Keeper) LaunchConsumer( k.SetEquivocationEvidenceMinHeight(ctx, consumerId, initializationRecord.InitialHeight.RevisionHeight) k.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_LAUNCHED) + if err := k.SetConsumerLastAckTime(ctx, consumerId, ctx.BlockTime()); err != nil { + return err + } k.Logger(ctx).Info("consumer successfully launched", "consumerId", consumerId, @@ -413,6 +416,9 @@ func (k Keeper) DeleteConsumerChain(ctx sdk.Context, consumerId uint64) (err err k.DeleteConsumerValSet(ctx, consumerId) k.DeleteConsumerRemovalTime(ctx, consumerId) + k.DeleteConsumerLastAckTime(ctx, consumerId) + k.DeleteConsumerHighestSentVscId(ctx, consumerId) + k.DeleteConsumerHighestAckedVscId(ctx, consumerId) k.DeleteConsumerDebt(ctx, consumerId) if err := k.ConsumerFeesPerBlockOverride.Remove(ctx, consumerId); err != nil { @@ -438,3 +444,26 @@ func (k Keeper) DeleteConsumerChain(ctx sdk.Context, consumerId uint64) (err err return nil } + +// SweepUnresponsiveConsumers stops any launched consumer that has produced no +// successful VSC ack within the liveness grace period. It is the sole authority +// for removing unresponsive consumers and runs on the provider block clock, so +// it does not depend on a live IBC client or an available relayer. +func (k Keeper) SweepUnresponsiveConsumers(ctx sdk.Context) error { + grace, err := k.LivenessGracePeriod(ctx) + if err != nil { + return err + } + for _, consumerId := range k.GetAllLaunchedConsumerIds(ctx) { + lastAck := k.GetConsumerLastAckTime(ctx, consumerId) + if ctx.BlockTime().Sub(lastAck) <= grace { + continue + } + k.Logger(ctx).Info("consumer unresponsive past liveness grace, stopping", + "consumerId", consumerId, "lastAck", lastAck, "grace", grace) + if err := k.StopAndPrepareForConsumerRemoval(ctx, consumerId); err != nil { + k.Logger(ctx).Error("failed to stop unresponsive consumer", "consumerId", consumerId, "error", err.Error()) + } + } + return nil +} diff --git a/x/vaas/provider/keeper/ibc_v2_integration_test.go b/x/vaas/provider/keeper/ibc_v2_integration_test.go index 11dff43..5e71403 100644 --- a/x/vaas/provider/keeper/ibc_v2_integration_test.go +++ b/x/vaas/provider/keeper/ibc_v2_integration_test.go @@ -2,7 +2,6 @@ package keeper_test import ( "testing" - "time" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -97,9 +96,9 @@ func TestIBCV2MultipleConsumers(t *testing.T) { } } -// TestIBCV2ConsumerRemovalOnTimeout tests that a timeout triggers consumer removal. -func TestIBCV2ConsumerRemovalOnTimeout(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) +// TestIBCV2TimeoutIsLogOnly tests that a timeout is log-only; liveness sweep owns removal. +func TestIBCV2TimeoutIsLogOnly(t *testing.T) { + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() consumerId := uint64(0) @@ -116,18 +115,17 @@ func TestIBCV2ConsumerRemovalOnTimeout(t *testing.T) { require.True(t, found) require.Equal(t, consumerId, gotConsumerId) - mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(time.Hour*24*21, nil).Times(1) - err := providerKeeper.OnTimeoutPacketV2(ctx, clientId) require.NoError(t, err) + // Consumer stays launched; liveness sweep will handle removal phase = providerKeeper.GetConsumerPhase(ctx, consumerId) - require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, phase) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) } -// TestIBCV2ConsumerRemovalOnErrorAck tests that an error acknowledgement triggers consumer removal. -func TestIBCV2ConsumerRemovalOnErrorAck(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) +// TestIBCV2ErrorAckIsLogOnly tests that an error acknowledgement is log-only; liveness sweep owns removal. +func TestIBCV2ErrorAckIsLogOnly(t *testing.T) { + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() consumerId := uint64(0) @@ -140,13 +138,12 @@ func TestIBCV2ConsumerRemovalOnErrorAck(t *testing.T) { phase := providerKeeper.GetConsumerPhase(ctx, consumerId) require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) - mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(time.Hour*24*21, nil).Times(1) - - err := providerKeeper.OnAcknowledgementPacketV2(ctx, clientId, "packet decode error") + err := providerKeeper.OnAcknowledgementPacketV2(ctx, clientId, 0, "packet decode error") require.NoError(t, err) + // Consumer stays launched; liveness sweep will handle removal phase = providerKeeper.GetConsumerPhase(ctx, consumerId) - require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, phase) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) } // TestIBCV2DualModeRouting tests that the provider correctly selects v2 routing diff --git a/x/vaas/provider/keeper/keeper.go b/x/vaas/provider/keeper/keeper.go index 1993ee6..003b35f 100644 --- a/x/vaas/provider/keeper/keeper.go +++ b/x/vaas/provider/keeper/keeper.go @@ -74,6 +74,9 @@ type Keeper struct { ConsumerFeesPerBlockOverride collections.Map[uint64, math.Int] EquivocationEvidenceMinHeight collections.Map[uint64, uint64] ConsumerRemovalTime collections.Map[uint64, []byte] + ConsumerLastAckTime collections.Map[uint64, []byte] + ConsumerHighestSentVscId collections.Map[uint64, uint64] + ConsumerHighestAckedVscId collections.Map[uint64, uint64] SpawnTimeToConsumerIds collections.Map[[]byte, types.ConsumerIds] RemovalTimeToConsumerIds collections.Map[[]byte, types.ConsumerIds] @@ -184,6 +187,9 @@ func NewKeeper( ConsumerFeesPerBlockOverride: collections.NewMap(sb, types.ConsumerIdToFeesPerBlockOverridePrefix, types.ConsumerIdToFeesPerBlockOverrideKeyName, collections.Uint64Key, sdk.IntValue), EquivocationEvidenceMinHeight: collections.NewMap(sb, types.EquivocationEvidenceMinHeightPrefix, "equivocation_evidence_min_height", collections.Uint64Key, collections.Uint64Value), ConsumerRemovalTime: collections.NewMap(sb, types.ConsumerIdToRemovalTimePrefix, "consumer_removal_time", collections.Uint64Key, collections.BytesValue), + ConsumerLastAckTime: collections.NewMap(sb, types.ConsumerIdToLastAckTimePrefix, "consumer_last_ack_time", collections.Uint64Key, collections.BytesValue), + ConsumerHighestSentVscId: collections.NewMap(sb, types.ConsumerIdToHighestSentVscIdPrefix, "consumer_highest_sent_vsc_id", collections.Uint64Key, collections.Uint64Value), + ConsumerHighestAckedVscId: collections.NewMap(sb, types.ConsumerIdToHighestAckedVscIdPrefix, "consumer_highest_acked_vsc_id", collections.Uint64Key, collections.Uint64Value), SpawnTimeToConsumerIds: collections.NewMap(sb, types.SpawnTimeToConsumerIdsPrefix, "spawn_time_to_consumer_ids", collections.BytesKey, codec.CollValue[types.ConsumerIds](cdc)), RemovalTimeToConsumerIds: collections.NewMap(sb, types.RemovalTimeToConsumerIdsPrefix, "removal_time_to_consumer_ids", collections.BytesKey, codec.CollValue[types.ConsumerIds](cdc)), @@ -571,6 +577,76 @@ func (k Keeper) DeleteConsumerRemovalTime(ctx context.Context, consumerId uint64 } } +// SetConsumerLastAckTime records the block time of the consumer's most recent successful VSC ack. +func (k Keeper) SetConsumerLastAckTime(ctx context.Context, consumerId uint64, t time.Time) error { + buf, err := t.MarshalBinary() + if err != nil { + return fmt.Errorf("failed to marshal last ack time (%+v) for consumer id (%d): %w", t, consumerId, err) + } + return k.ConsumerLastAckTime.Set(ctx, consumerId, buf) +} + +// GetConsumerLastAckTime returns the consumer's last successful VSC ack time. +// When absent it returns the current block time, so a consumer is never treated +// as stale before its first recorded ack. +func (k Keeper) GetConsumerLastAckTime(ctx context.Context, consumerId uint64) time.Time { + buf, err := k.ConsumerLastAckTime.Get(ctx, consumerId) + if err != nil { + return sdk.UnwrapSDKContext(ctx).BlockTime() + } + var t time.Time + if err := t.UnmarshalBinary(buf); err != nil { + return sdk.UnwrapSDKContext(ctx).BlockTime() + } + return t +} + +func (k Keeper) DeleteConsumerLastAckTime(ctx context.Context, consumerId uint64) { + if err := k.ConsumerLastAckTime.Remove(ctx, consumerId); err != nil { + panic(fmt.Errorf("failed to delete last ack time for consumer id (%d): %w", consumerId, err)) + } +} + +func (k Keeper) SetConsumerHighestSentVscId(ctx context.Context, consumerId, vscId uint64) { + if err := k.ConsumerHighestSentVscId.Set(ctx, consumerId, vscId); err != nil { + panic(fmt.Errorf("failed to set highest sent vscId for consumer id (%d): %w", consumerId, err)) + } +} + +func (k Keeper) GetConsumerHighestSentVscId(ctx context.Context, consumerId uint64) uint64 { + v, err := k.ConsumerHighestSentVscId.Get(ctx, consumerId) + if err != nil { + return 0 + } + return v +} + +func (k Keeper) DeleteConsumerHighestSentVscId(ctx context.Context, consumerId uint64) { + if err := k.ConsumerHighestSentVscId.Remove(ctx, consumerId); err != nil { + panic(fmt.Errorf("failed to delete highest sent vscId for consumer id (%d): %w", consumerId, err)) + } +} + +func (k Keeper) SetConsumerHighestAckedVscId(ctx context.Context, consumerId, vscId uint64) { + if err := k.ConsumerHighestAckedVscId.Set(ctx, consumerId, vscId); err != nil { + panic(fmt.Errorf("failed to set highest acked vscId for consumer id (%d): %w", consumerId, err)) + } +} + +func (k Keeper) GetConsumerHighestAckedVscId(ctx context.Context, consumerId uint64) uint64 { + v, err := k.ConsumerHighestAckedVscId.Get(ctx, consumerId) + if err != nil { + return 0 + } + return v +} + +func (k Keeper) DeleteConsumerHighestAckedVscId(ctx context.Context, consumerId uint64) { + if err := k.ConsumerHighestAckedVscId.Remove(ctx, consumerId); err != nil { + panic(fmt.Errorf("failed to delete highest acked vscId for consumer id (%d): %w", consumerId, err)) + } +} + // timeToBytes converts a time.Time to bytes for use as a key func timeToBytes(t time.Time) []byte { bz, _ := t.MarshalBinary() diff --git a/x/vaas/provider/keeper/liveness_test.go b/x/vaas/provider/keeper/liveness_test.go new file mode 100644 index 0000000..f7b2244 --- /dev/null +++ b/x/vaas/provider/keeper/liveness_test.go @@ -0,0 +1,177 @@ +package keeper_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + sdk "github.com/cosmos/cosmos-sdk/types" + + testkeeper "github.com/allinbits/vaas/testutil/keeper" + providertypes "github.com/allinbits/vaas/x/vaas/provider/types" +) + +func TestConsumerLivenessState(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + const cid = uint64(0) + + // lastAck defaults to BlockTime when absent (defensive, not a migration crutch). + require.Equal(t, ctx.BlockTime(), k.GetConsumerLastAckTime(ctx, cid)) + + now := ctx.BlockTime().Add(time.Hour) + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, now)) + require.Equal(t, now.UTC(), k.GetConsumerLastAckTime(ctx, cid).UTC()) + + // counters default to 0. + require.Equal(t, uint64(0), k.GetConsumerHighestSentVscId(ctx, cid)) + require.Equal(t, uint64(0), k.GetConsumerHighestAckedVscId(ctx, cid)) + k.SetConsumerHighestSentVscId(ctx, cid, 5) + k.SetConsumerHighestAckedVscId(ctx, cid, 3) + require.Equal(t, uint64(5), k.GetConsumerHighestSentVscId(ctx, cid)) + require.Equal(t, uint64(3), k.GetConsumerHighestAckedVscId(ctx, cid)) + + k.DeleteConsumerLastAckTime(ctx, cid) + k.DeleteConsumerHighestSentVscId(ctx, cid) + k.DeleteConsumerHighestAckedVscId(ctx, cid) + require.Equal(t, ctx.BlockTime(), k.GetConsumerLastAckTime(ctx, cid)) + require.Equal(t, uint64(0), k.GetConsumerHighestSentVscId(ctx, cid)) +} + +func TestOnAckRecordsLiveness(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + const cid = uint64(0) + clientId := "07-tendermint-0" + k.SetConsumerClientId(ctx, cid, clientId) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + + future := ctx.BlockTime().Add(2 * time.Hour) + ctx = ctx.WithBlockTime(future) + + require.NoError(t, k.OnAcknowledgementPacketV2(ctx, clientId, 9, "")) + require.Equal(t, future.UTC(), k.GetConsumerLastAckTime(ctx, cid).UTC()) + require.Equal(t, uint64(9), k.GetConsumerHighestAckedVscId(ctx, cid)) +} + +func TestLivenessGracePeriod(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + unbonding := 21 * 24 * time.Hour + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).Times(1) + + grace, err := k.LivenessGracePeriod(ctx) + require.NoError(t, err) + require.Greater(t, grace, time.Duration(0)) + require.Less(t, grace, unbonding) // safety invariant + require.Equal(t, time.Duration(float64(unbonding)*0.66), grace) +} + +func TestSweepRemovesStaleConsumer(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + k.SetConsumerChainId(ctx, cid, "consumer-1") + + unbonding := 21 * 24 * time.Hour + // LivenessGracePeriod + StopAndPrepareForConsumerRemoval both read UnbondingTime. + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).AnyTimes() + + // Last ack far in the past -> beyond grace. + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime().Add(-30*24*time.Hour))) + + require.NoError(t, k.SweepUnresponsiveConsumers(ctx)) + require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, k.GetConsumerPhase(ctx, cid)) +} + +func TestSweepSparesLiveConsumer(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).AnyTimes() + + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime())) // fresh + require.NoError(t, k.SweepUnresponsiveConsumers(ctx)) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cid)) +} + +func TestTimeoutDoesNotRemove(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + const cid = uint64(0) + clientId := "07-tendermint-0" + k.SetConsumerClientId(ctx, cid, clientId) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + + require.NoError(t, k.OnTimeoutPacketV2(ctx, clientId)) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cid)) +} + +func TestErrorAckDoesNotRemove(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + const cid = uint64(0) + clientId := "07-tendermint-0" + k.SetConsumerClientId(ctx, cid, clientId) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + + require.NoError(t, k.OnAcknowledgementPacketV2(ctx, clientId, 1, "some error")) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cid)) +} + +func TestDeleteConsumerChainClearsLivenessState(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).AnyTimes() + + const cid = uint64(0) + poolAddr := k.GetConsumerFeePoolAddress(cid) + k.SetConsumerClientId(ctx, cid, "07-tendermint-0") + require.NoError(t, k.FeePoolAddressToConsumerId.Set(ctx, poolAddr, cid)) + mocks.MockBankKeeper.EXPECT().GetAllBalances(ctx, poolAddr).Return(sdk.NewCoins()) + + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_STOPPED) + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime())) + k.SetConsumerHighestSentVscId(ctx, cid, 4) + k.SetConsumerHighestAckedVscId(ctx, cid, 4) + + // confirm keys are present before deletion + hasAck, err := k.ConsumerLastAckTime.Has(ctx, cid) + require.NoError(t, err) + require.True(t, hasAck) + hasSent, err := k.ConsumerHighestSentVscId.Has(ctx, cid) + require.NoError(t, err) + require.True(t, hasSent) + hasAcked, err := k.ConsumerHighestAckedVscId.Has(ctx, cid) + require.NoError(t, err) + require.True(t, hasAcked) + + require.NoError(t, k.DeleteConsumerChain(ctx, cid)) + + // assert underlying keys are gone, not just that the getter returns a default + hasAck, err = k.ConsumerLastAckTime.Has(ctx, cid) + require.NoError(t, err) + require.False(t, hasAck) + hasSent, err = k.ConsumerHighestSentVscId.Has(ctx, cid) + require.NoError(t, err) + require.False(t, hasSent) + hasAcked, err = k.ConsumerHighestAckedVscId.Has(ctx, cid) + require.NoError(t, err) + require.False(t, hasAcked) + + // getters return their absent-defaults after deletion + require.Equal(t, ctx.BlockTime(), k.GetConsumerLastAckTime(ctx, cid)) + require.Equal(t, uint64(0), k.GetConsumerHighestSentVscId(ctx, cid)) + require.Equal(t, uint64(0), k.GetConsumerHighestAckedVscId(ctx, cid)) +} diff --git a/x/vaas/provider/keeper/params.go b/x/vaas/provider/keeper/params.go index fbd988b..72f6d60 100644 --- a/x/vaas/provider/keeper/params.go +++ b/x/vaas/provider/keeper/params.go @@ -7,6 +7,7 @@ import ( "time" "github.com/allinbits/vaas/x/vaas/provider/types" + vaastypes "github.com/allinbits/vaas/x/vaas/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" commitmenttypes "github.com/cosmos/ibc-go/v10/modules/core/23-commitment/types" @@ -165,3 +166,14 @@ func (k Keeper) SetInfractionParams(ctx context.Context, params types.Infraction panic(fmt.Sprintf("error setting infraction parameters: %v", err)) } } + +// LivenessGracePeriod returns the maximum time a launched consumer may go +// without a successful VSC ack before it is removed, derived from the provider +// unbonding period so it stays inside the slashable window. +func (k Keeper) LivenessGracePeriod(ctx context.Context) (time.Duration, error) { + unbonding, err := k.stakingKeeper.UnbondingTime(ctx) + if err != nil { + return 0, err + } + return vaastypes.CalculateTrustPeriod(unbonding, types.LivenessGraceFraction) +} diff --git a/x/vaas/provider/keeper/relay.go b/x/vaas/provider/keeper/relay.go index 21f8b91..42c8f54 100644 --- a/x/vaas/provider/keeper/relay.go +++ b/x/vaas/provider/keeper/relay.go @@ -19,17 +19,23 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -func (k Keeper) OnAcknowledgementPacketV2(ctx sdk.Context, sourceClientID string, ackError string) error { +func (k Keeper) OnAcknowledgementPacketV2(ctx sdk.Context, sourceClientID string, ackVscId uint64, ackError string) error { + consumerId, found := k.GetClientIdToConsumerId(ctx, sourceClientID) + if !found { + return errorsmod.Wrapf(providertypes.ErrInvalidConsumerClient, "recv acknowledgement on unknown client %s", sourceClientID) + } + if ackError != "" { - k.Logger(ctx).Error( - "recv ErrorAcknowledgement", - "clientID", sourceClientID, - "error", ackError, - ) - if consumerId, found := k.GetClientIdToConsumerId(ctx, sourceClientID); found { - return k.StopAndPrepareForConsumerRemoval(ctx, consumerId) - } - return errorsmod.Wrapf(providertypes.ErrInvalidConsumerClient, "recv ErrorAcknowledgement on unknown client %s", sourceClientID) + k.Logger(ctx).Error("recv ErrorAcknowledgement, retrying next epoch; liveness sweep owns removal", + "clientID", sourceClientID, "consumerId", consumerId, "error", ackError) + return nil + } + + if err := k.SetConsumerLastAckTime(ctx, consumerId, ctx.BlockTime()); err != nil { + return err + } + if ackVscId > k.GetConsumerHighestAckedVscId(ctx, consumerId) { + k.SetConsumerHighestAckedVscId(ctx, consumerId, ackVscId) } return nil } @@ -38,13 +44,11 @@ func (k Keeper) OnTimeoutPacketV2(ctx sdk.Context, sourceClientID string) error consumerId, found := k.GetClientIdToConsumerId(ctx, sourceClientID) if !found { k.Logger(ctx).Error("packet timeout, unknown client:", "clientID", sourceClientID) - return errorsmod.Wrapf( - providertypes.ErrInvalidConsumerClient, - "timeout on unknown client %s", sourceClientID, - ) + return errorsmod.Wrapf(providertypes.ErrInvalidConsumerClient, "timeout on unknown client %s", sourceClientID) } - k.Logger(ctx).Info("packet timeout, deleting the consumer:", "consumerId", consumerId, "clientId", sourceClientID) - return k.StopAndPrepareForConsumerRemoval(ctx, consumerId) + k.Logger(ctx).Info("packet timeout, retrying next epoch; liveness sweep owns removal", + "consumerId", consumerId, "clientId", sourceClientID) + return nil } // EndBlockVSU contains the EndBlock logic needed for @@ -235,17 +239,8 @@ func (k Keeper) SendVSCPacketsToChain(ctx sdk.Context, consumerId uint64, client return nil } - k.Logger(ctx).Error("cannot send VSC, removing consumer:", - "consumerId", consumerId, - "clientId", clientId, - "vscid", data.ValsetUpdateId, - "err", err.Error(), - ) - - err := k.StopAndPrepareForConsumerRemoval(ctx, consumerId) - if err != nil { - k.Logger(ctx).Info("consumer chain failed to stop:", "consumerId", consumerId, "error", err.Error()) - } + k.Logger(ctx).Error("cannot send VSC, leaving packet data stored; liveness sweep owns removal", + "consumerId", consumerId, "clientId", clientId, "vscid", data.ValsetUpdateId, "err", err.Error()) return nil } @@ -255,6 +250,9 @@ func (k Keeper) SendVSCPacketsToChain(ctx sdk.Context, consumerId uint64, client "vscid", data.ValsetUpdateId, "sequence", resp.Sequence, ) + if data.ValsetUpdateId > k.GetConsumerHighestSentVscId(ctx, consumerId) { + k.SetConsumerHighestSentVscId(ctx, consumerId, data.ValsetUpdateId) + } } k.DeletePendingVSCPackets(ctx, consumerId) diff --git a/x/vaas/provider/keeper/relay_test.go b/x/vaas/provider/keeper/relay_test.go index d76275a..62059e3 100644 --- a/x/vaas/provider/keeper/relay_test.go +++ b/x/vaas/provider/keeper/relay_test.go @@ -2,7 +2,6 @@ package keeper_test import ( "testing" - "time" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -18,7 +17,7 @@ import ( // TestOnAcknowledgementPacketV2 tests the IBC v2 acknowledgement handler. func TestOnAcknowledgementPacketV2(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() consumerId := uint64(0) @@ -30,24 +29,20 @@ func TestOnAcknowledgementPacketV2(t *testing.T) { providerKeeper.SetConsumerChainId(ctx, consumerId, "consumer-chain") // Test 1: Success acknowledgement (empty error) - no action needed - err := providerKeeper.OnAcknowledgementPacketV2(ctx, clientId, "") + err := providerKeeper.OnAcknowledgementPacketV2(ctx, clientId, 1, "") require.NoError(t, err) // Consumer should still be launched phase := providerKeeper.GetConsumerPhase(ctx, consumerId) require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) - // Setup mock expectation for StopAndPrepareForConsumerRemoval - // which calls UnbondingTime - mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(time.Hour*24*21, nil).Times(1) - - // Test 2: Error acknowledgement - should trigger consumer removal - err = providerKeeper.OnAcknowledgementPacketV2(ctx, clientId, "packet data could not be decoded") + // Test 2: Error acknowledgement - liveness sweep owns removal, consumer stays launched + err = providerKeeper.OnAcknowledgementPacketV2(ctx, clientId, 1, "packet data could not be decoded") require.NoError(t, err) - // Consumer should now be stopped + // Consumer stays launched; liveness sweep will handle removal phase = providerKeeper.GetConsumerPhase(ctx, consumerId) - require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, phase) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) } // TestOnAcknowledgementPacketV2UnknownClient tests error handling for unknown clients. @@ -58,14 +53,14 @@ func TestOnAcknowledgementPacketV2UnknownClient(t *testing.T) { unknownClientId := "07-tendermint-999" // Error ack with unknown client should return error - err := providerKeeper.OnAcknowledgementPacketV2(ctx, unknownClientId, "some error") + err := providerKeeper.OnAcknowledgementPacketV2(ctx, unknownClientId, 0, "some error") require.Error(t, err) require.Contains(t, err.Error(), "unknown client") } // TestOnTimeoutPacketV2 tests the IBC v2 timeout handler. func TestOnTimeoutPacketV2(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() consumerId := uint64(0) @@ -80,16 +75,13 @@ func TestOnTimeoutPacketV2(t *testing.T) { phase := providerKeeper.GetConsumerPhase(ctx, consumerId) require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) - // Setup mock expectation for StopAndPrepareForConsumerRemoval - mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(time.Hour*24*21, nil).Times(1) - - // Timeout should trigger consumer removal + // Timeout is now log-only; liveness sweep owns removal err := providerKeeper.OnTimeoutPacketV2(ctx, clientId) require.NoError(t, err) - // Consumer should now be stopped + // Consumer stays launched; liveness sweep will handle removal phase = providerKeeper.GetConsumerPhase(ctx, consumerId) - require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, phase) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) } // TestOnTimeoutPacketV2UnknownClient tests error handling for unknown clients. diff --git a/x/vaas/provider/module.go b/x/vaas/provider/module.go index b62e977..2225f14 100644 --- a/x/vaas/provider/module.go +++ b/x/vaas/provider/module.go @@ -155,6 +155,11 @@ func (am AppModule) BeginBlock(ctx context.Context) error { return err } + // Stop consumers that have gone silent past the liveness grace period. + if err := am.keeper.SweepUnresponsiveConsumers(sdkCtx); err != nil { + return err + } + // Collect and distribute fees once per epoch, not every block. if am.keeper.BlocksUntilNextEpoch(sdkCtx) == 0 { if err := am.keeper.DistributeConsumerFees(sdkCtx); err != nil { diff --git a/x/vaas/provider/module_test.go b/x/vaas/provider/module_test.go index dce592f..dc36c4c 100644 --- a/x/vaas/provider/module_test.go +++ b/x/vaas/provider/module_test.go @@ -3,6 +3,7 @@ package provider import ( "bytes" "testing" + "time" "cosmossdk.io/math" testkeeper "github.com/allinbits/vaas/testutil/keeper" @@ -43,6 +44,7 @@ func TestBeginBlockCommitsDebtStateWhenDistributionFails(t *testing.T) { // One bonded validator. valAddrCodec := address.NewBech32Codec("cosmosvaloper") mocks.MockStakingKeeper.EXPECT().ValidatorAddressCodec().Return(valAddrCodec).AnyTimes() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).AnyTimes() opBytes := bytes.Repeat([]byte{0xfe}, 20) op, err := valAddrCodec.BytesToString(opBytes) require.NoError(t, err) @@ -57,7 +59,7 @@ func TestBeginBlockCommitsDebtStateWhenDistributionFails(t *testing.T) { GetBondedValidatorsByPower(gomock.Any()). Return([]stakingtypes.Validator{val}, nil) - // consumerInDebt: balance too low → in debt, no InputOutputCoins + // consumerInDebt: balance too low -> in debt, no InputOutputCoins mocks.MockBankKeeper.EXPECT(). GetBalance(gomock.Any(), consumerInDebtPool, "uphoton"). Return(sdk.NewCoin("uphoton", feesPerEpoch.Amount.QuoRaw(2))) @@ -84,7 +86,7 @@ func TestBeginBlockCommitsDebtStateWhenDistributionFails(t *testing.T) { // per-epoch fee distribution only runs at epoch boundaries. func TestBeginBlockSkipsFeeCollectionWhenNotAtEpochBoundary(t *testing.T) { params := testkeeper.NewInMemKeeperParams(t) - k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, params) + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, params) defer ctrl.Finish() appModule := NewAppModule(&k) @@ -100,7 +102,9 @@ func TestBeginBlockSkipsFeeCollectionWhenNotAtEpochBoundary(t *testing.T) { // Height 100 is not an epoch boundary (blocks_per_epoch=600) ctx = ctx.WithBlockHeight(100) - // No bank or staking mock expectations — nothing should be called. + // SweepUnresponsiveConsumers calls UnbondingTime on every block. + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).AnyTimes() + require.NoError(t, appModule.BeginBlock(sdk.WrapSDKContext(ctx))) } @@ -129,6 +133,7 @@ func TestBeginBlockCollectsFeesAtEpochBoundary(t *testing.T) { valAddrCodec := address.NewBech32Codec("cosmosvaloper") mocks.MockStakingKeeper.EXPECT().ValidatorAddressCodec().Return(valAddrCodec).AnyTimes() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).AnyTimes() opBytes := bytes.Repeat([]byte{0xfe}, 20) op, err := valAddrCodec.BytesToString(opBytes) require.NoError(t, err) diff --git a/x/vaas/provider/types/keys.go b/x/vaas/provider/types/keys.go index 01c4722..1350169 100644 --- a/x/vaas/provider/types/keys.go +++ b/x/vaas/provider/types/keys.go @@ -115,5 +115,8 @@ var ( ConsumerFeePoolTotalSharesPrefix = collections.NewPrefix(26) FeePoolAddressToConsumerIdPrefix = collections.NewPrefix(27) EpochDowntimePrefix = collections.NewPrefix(28) + ConsumerIdToLastAckTimePrefix = collections.NewPrefix(29) + ConsumerIdToHighestSentVscIdPrefix = collections.NewPrefix(30) + ConsumerIdToHighestAckedVscIdPrefix = collections.NewPrefix(31) ParametersPrefix = collections.NewPrefix(0xFF) ) diff --git a/x/vaas/provider/types/params.go b/x/vaas/provider/types/params.go index 26818ea..6121c97 100644 --- a/x/vaas/provider/types/params.go +++ b/x/vaas/provider/types/params.go @@ -20,6 +20,13 @@ const ( // as UnbondingPeriod * TrustingPeriodFraction DefaultTrustingPeriodFraction = "0.66" + // LivenessGraceFraction sets the consumer liveness grace period as a + // fraction of the provider unbonding period. A consumer that produces no + // successful VSC ack for longer than this is removed. 0.66 mirrors the + // trusting-period fraction: the grace ends around the client recovery + // horizon, leaving margin below the unbonding (slashable) window. + LivenessGraceFraction = "0.66" + // DefaultBlocksPerEpoch defines the default blocks that constitute an epoch. Assuming we need 6 seconds per block, // an epoch corresponds to 1 hour (6 * 600 = 3600 seconds). // forcing int64 as the Params KeyTable expects an int64 and not int. From ab4505330ea98b30617bcdb25a1025bab075ec76 Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:37:58 +0200 Subject: [PATCH 02/20] consumer safe mode on VSC staleness --- x/vaas/consumer/ante/msg_filter_ante.go | 55 +++++++++++--------- x/vaas/consumer/ante/msg_filter_ante_test.go | 25 +++++++++ x/vaas/consumer/keeper/keeper.go | 37 +++++++++++++ x/vaas/consumer/keeper/relay.go | 2 + x/vaas/consumer/keeper/relay_test.go | 36 +++++++++++++ x/vaas/consumer/types/keys.go | 1 + x/vaas/consumer/types/safe_mode.go | 8 +++ 7 files changed, 139 insertions(+), 25 deletions(-) create mode 100644 x/vaas/consumer/types/safe_mode.go diff --git a/x/vaas/consumer/ante/msg_filter_ante.go b/x/vaas/consumer/ante/msg_filter_ante.go index a438daa..68e695d 100644 --- a/x/vaas/consumer/ante/msg_filter_ante.go +++ b/x/vaas/consumer/ante/msg_filter_ante.go @@ -18,19 +18,24 @@ type ( ConsumerKeeper interface { GetProviderClientID(ctx context.Context) (string, bool) IsConsumerInDebt(ctx context.Context) bool + IsVSCStale(ctx context.Context) bool } // MsgFilterDecorator is the consumer-side tx admission gate. It runs in - // three modes driven by consumer state: + // four modes driven by consumer state: // - // pre-CCV : provider IBC client not yet established → only /ibc.* msgs - // are accepted, so the chain can stand up its IBC stack. - // normal : provider client established, consumer is paying its fees → - // everything passes. - // in debt : provider client established, consumer has fallen behind on - // per-block fees → only /ibc.core.* and /cosmos.gov.* msgs - // pass, keeping CCV liveness and governance available so the - // chain can recover without off-chain coordination. + // pre-CCV : provider IBC client not yet established -> only /ibc.* msgs + // are accepted, so the chain can stand up its IBC stack. + // normal : provider client established, consumer is paying its fees + // and has a fresh validator set -> everything passes. + // in debt : provider client established, consumer has fallen behind on + // per-block fees -> restricted mode (see below). + // stale VSC : provider client established, no VSC packet received within + // the safe-mode threshold -> restricted mode (see below). + // + // Restricted mode: only /ibc.core.* and /cosmos.gov.* messages pass, + // keeping CCV liveness and governance available so the chain can recover + // without off-chain coordination. MsgFilterDecorator struct { ConsumerKeeper ConsumerKeeper } @@ -53,12 +58,9 @@ func (mfd MsgFilterDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bo return next(ctx, tx, simulate) } - // Hot path: post-CCV and not in debt → pass without walking the message list. - if !mfd.ConsumerKeeper.IsConsumerInDebt(ctx) { - return next(ctx, tx, simulate) - } - - // In debt: only let /ibc.core.* and /cosmos.gov.* messages through. + // Restricted mode: in debt, or no fresh VSC (validator set may be stale). + // + // Only /ibc.core.* and /cosmos.gov.* messages pass: // // /ibc.core.* preserves CCV liveness and keeps all IBC apps working. // The prefix covers both IBC v1 and v2 channel/connection/client @@ -69,16 +71,19 @@ func (mfd MsgFilterDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bo // their own keys; authz wrapping is not part of any realistic relayer // flow and is therefore treated as non-ibc.core and rejected here. // - // /cosmos.gov.* keeps governance alive during debt so the community can - // vote on recovery (e.g. emergency funding, param changes) without - // requiring off-chain coordination. - if isAllowedDuringDebtTx(tx.GetMsgs()) { - return next(ctx, tx, simulate) + // /cosmos.gov.* keeps governance alive so the community can vote on + // recovery (e.g. emergency funding, param changes) without requiring + // off-chain coordination. + if mfd.ConsumerKeeper.IsConsumerInDebt(ctx) || mfd.ConsumerKeeper.IsVSCStale(ctx) { + if isAllowedRestrictedTx(tx.GetMsgs()) { + return next(ctx, tx, simulate) + } + return ctx, errorsmod.Wrap( + consumertypes.ErrConsumerInDebt, + "consumer is in debt or has a stale validator set; only ibc.core and cosmos.gov messages are temporarily allowed", + ) } - return ctx, errorsmod.Wrap( - consumertypes.ErrConsumerInDebt, - "consumer chain is in debt; only ibc.core and cosmos.gov messages are temporarily allowed", - ) + return next(ctx, tx, simulate) } func hasOnlyPrefix(msgs []sdk.Msg, prefix string) bool { @@ -90,7 +95,7 @@ func hasOnlyPrefix(msgs []sdk.Msg, prefix string) bool { return true } -func isAllowedDuringDebtTx(msgs []sdk.Msg) bool { +func isAllowedRestrictedTx(msgs []sdk.Msg) bool { for _, msg := range msgs { url := sdk.MsgTypeURL(msg) if !strings.HasPrefix(url, "/ibc.core.") && !strings.HasPrefix(url, "/cosmos.gov.") { diff --git a/x/vaas/consumer/ante/msg_filter_ante_test.go b/x/vaas/consumer/ante/msg_filter_ante_test.go index 1d61778..c92cbc6 100644 --- a/x/vaas/consumer/ante/msg_filter_ante_test.go +++ b/x/vaas/consumer/ante/msg_filter_ante_test.go @@ -20,6 +20,7 @@ import ( type mockConsumerKeeper struct { providerClientFound bool inDebt bool + vscStale bool } func (m mockConsumerKeeper) GetProviderClientID(context.Context) (string, bool) { @@ -30,6 +31,10 @@ func (m mockConsumerKeeper) IsConsumerInDebt(context.Context) bool { return m.inDebt } +func (m mockConsumerKeeper) IsVSCStale(context.Context) bool { + return m.vscStale +} + type mockTx struct { msgs []sdk.Msg } @@ -153,6 +158,26 @@ func TestMsgFilterDecoratorRejectsAuthzWrappedIBCCoreTxWhenInDebt(t *testing.T) require.False(t, nextCalled) } +// Safe mode (stale VSC): non-IBC, non-gov msgs are rejected with ErrConsumerInDebt. +func TestSafeModeRejectsAppMsgWhenStale(t *testing.T) { + // provider client established, not in debt, but VSC stale + nextCalled, err := runDecorator(t, + mockConsumerKeeper{providerClientFound: true, inDebt: false, vscStale: true}, + []sdk.Msg{bankSendMsg()}, + ) + require.Error(t, err) + require.True(t, errorsmod.IsOf(err, consumertypes.ErrConsumerInDebt)) + require.False(t, nextCalled) + + // /ibc.core.* message must pass (recovery path stays open) + nextCalled, err = runDecorator(t, + mockConsumerKeeper{providerClientFound: true, inDebt: false, vscStale: true}, + []sdk.Msg{&channeltypes.MsgRecvPacket{}}, + ) + require.NoError(t, err) + require.True(t, nextCalled) +} + func testAccAddress(seed byte) sdk.AccAddress { return sdk.AccAddress(bytes.Repeat([]byte{seed}, 20)) } diff --git a/x/vaas/consumer/keeper/keeper.go b/x/vaas/consumer/keeper/keeper.go index bb62e1e..cf2dfc0 100644 --- a/x/vaas/consumer/keeper/keeper.go +++ b/x/vaas/consumer/keeper/keeper.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" "github.com/allinbits/vaas/x/vaas/consumer/types" vaastypes "github.com/allinbits/vaas/x/vaas/types" @@ -64,6 +65,7 @@ type Keeper struct { HistoricalInfos collections.Map[int64, stakingtypes.HistoricalInfo] HighestValsetUpdateID collections.Item[uint64] PendingEvidencePackets collections.Map[[]byte, []byte] + LastVSCRecvTime collections.Item[[]byte] } // NewKeeper creates a new Consumer Keeper instance @@ -110,6 +112,7 @@ func NewKeeper( HistoricalInfos: collections.NewMap(sb, types.HistoricalInfoPrefix, "historical_infos", collections.Int64Key, codec.CollValue[stakingtypes.HistoricalInfo](cdc)), HighestValsetUpdateID: collections.NewItem(sb, types.HighestValsetUpdateIDPrefix, "highest_valset_update_id", collections.Uint64Value), PendingEvidencePackets: collections.NewMap(sb, types.PendingEvidencePacketsPrefix, "pending_evidence_packets", collections.BytesKey, collections.BytesValue), + LastVSCRecvTime: collections.NewItem(sb, types.LastVSCRecvTimePrefix, "last_vsc_recv_time", collections.BytesValue), } schema, err := sb.Build() @@ -150,6 +153,7 @@ func NewNonZeroKeeper(cdc codec.BinaryCodec, storeService corestoretypes.KVStore HistoricalInfos: collections.NewMap(sb, types.HistoricalInfoPrefix, "historical_infos", collections.Int64Key, codec.CollValue[stakingtypes.HistoricalInfo](cdc)), HighestValsetUpdateID: collections.NewItem(sb, types.HighestValsetUpdateIDPrefix, "highest_valset_update_id", collections.Uint64Value), PendingEvidencePackets: collections.NewMap(sb, types.PendingEvidencePacketsPrefix, "pending_evidence_packets", collections.BytesKey, collections.BytesValue), + LastVSCRecvTime: collections.NewItem(sb, types.LastVSCRecvTimePrefix, "last_vsc_recv_time", collections.BytesValue), } schema, err := sb.Build() @@ -451,3 +455,36 @@ func (k Keeper) GetHighestValsetUpdateID(ctx context.Context) (uint64, bool, err return val, true, nil } + +// SetLastVSCRecvTime records the block time at which a VSC packet was last processed. +func (k Keeper) SetLastVSCRecvTime(ctx context.Context, t time.Time) { + buf, err := t.MarshalBinary() + if err != nil { + panic(err) + } + if err := k.LastVSCRecvTime.Set(ctx, buf); err != nil { + panic(err) + } +} + +// GetLastVSCRecvTime returns the block time of the last processed VSC packet. +// Falls back to the current block time when no packet has been received yet, +// so a freshly launched consumer is never considered stale. +func (k Keeper) GetLastVSCRecvTime(ctx context.Context) time.Time { + buf, err := k.LastVSCRecvTime.Get(ctx) + if err != nil { + return sdk.UnwrapSDKContext(ctx).BlockTime() + } + var t time.Time + if err := t.UnmarshalBinary(buf); err != nil { + return sdk.UnwrapSDKContext(ctx).BlockTime() + } + return t +} + +// IsVSCStale reports whether the consumer has not received a VSC packet within +// the safe-mode threshold. +func (k Keeper) IsVSCStale(ctx context.Context) bool { + sdkCtx := sdk.UnwrapSDKContext(ctx) + return sdkCtx.BlockTime().Sub(k.GetLastVSCRecvTime(ctx)) > types.DefaultSafeModeThreshold +} diff --git a/x/vaas/consumer/keeper/relay.go b/x/vaas/consumer/keeper/relay.go index 5953cb6..4cc160d 100644 --- a/x/vaas/consumer/keeper/relay.go +++ b/x/vaas/consumer/keeper/relay.go @@ -30,6 +30,8 @@ func (k Keeper) OnRecvVSCPacketV2(ctx sdk.Context, sourceClientID string, newCha return nil } + k.SetLastVSCRecvTime(ctx, sdk.UnwrapSDKContext(ctx).BlockTime()) + _, found = k.GetProviderClientID(ctx) if !found { k.SetProviderClientID(ctx, sourceClientID) diff --git a/x/vaas/consumer/keeper/relay_test.go b/x/vaas/consumer/keeper/relay_test.go index a2061f0..cb28232 100644 --- a/x/vaas/consumer/keeper/relay_test.go +++ b/x/vaas/consumer/keeper/relay_test.go @@ -3,6 +3,7 @@ package keeper_test import ( "sort" "testing" + "time" "github.com/stretchr/testify/require" @@ -13,6 +14,7 @@ import ( testcrypto "github.com/allinbits/vaas/testutil/crypto" testkeeper "github.com/allinbits/vaas/testutil/keeper" + consumertypes "github.com/allinbits/vaas/x/vaas/consumer/types" "github.com/allinbits/vaas/x/vaas/types" ) @@ -244,3 +246,37 @@ func TestOnRecvVSCPacketV2DebtStatus(t *testing.T) { require.NoError(t, consumerKeeper.OnRecvVSCPacketV2(ctx, providerClientID, pd2)) require.False(t, consumerKeeper.IsConsumerInDebt(ctx)) } + +func TestConsumerVSCStaleness(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + require.False(t, k.IsVSCStale(ctx)) // absent -> BlockTime -> fresh + + k.SetLastVSCRecvTime(ctx, ctx.BlockTime()) + require.False(t, k.IsVSCStale(ctx)) + + stale := ctx.WithBlockTime(ctx.BlockTime().Add(consumertypes.DefaultSafeModeThreshold + time.Minute)) + require.True(t, k.IsVSCStale(stale)) +} + +func TestOnRecvVSCRecordsRecvTime(t *testing.T) { + providerClientID := "07-tendermint-0" + + pk1, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + advancedTime := ctx.BlockTime().Add(10 * time.Minute) + ctx = ctx.WithBlockTime(advancedTime) + + changes := []abci.ValidatorUpdate{{PubKey: pk1, Power: 100}} + pd := types.NewValidatorSetChangePacketData(changes, 1) + err = consumerKeeper.OnRecvVSCPacketV2(ctx, providerClientID, pd) + require.NoError(t, err) + + got := consumerKeeper.GetLastVSCRecvTime(ctx) + require.Equal(t, advancedTime, got) +} diff --git a/x/vaas/consumer/types/keys.go b/x/vaas/consumer/types/keys.go index df51b4f..b3dc7cb 100644 --- a/x/vaas/consumer/types/keys.go +++ b/x/vaas/consumer/types/keys.go @@ -35,4 +35,5 @@ var ( HighestValsetUpdateIDPrefix = collections.NewPrefix(12) ConsumerDebtPrefix = collections.NewPrefix(13) PendingEvidencePacketsPrefix = collections.NewPrefix(14) + LastVSCRecvTimePrefix = collections.NewPrefix(15) ) diff --git a/x/vaas/consumer/types/safe_mode.go b/x/vaas/consumer/types/safe_mode.go new file mode 100644 index 0000000..5a4590c --- /dev/null +++ b/x/vaas/consumer/types/safe_mode.go @@ -0,0 +1,8 @@ +package types + +import "time" + +// DefaultSafeModeThreshold is how long the consumer may go without receiving a +// VSC packet before its tx admission gate enters safe mode (only ibc.core and +// gov messages pass). Set well below the provider liveness grace period. +const DefaultSafeModeThreshold = 3 * time.Hour From 1876511c474e79f21b5d462c33f213dc4c22d851 Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:37:58 +0200 Subject: [PATCH 03/20] snapshot resync for behind consumers --- proto/vaas/v1/wire.proto | 7 ++ x/vaas/consumer/keeper/relay.go | 16 ++-- x/vaas/consumer/keeper/relay_test.go | 32 +++++++ x/vaas/consumer/keeper/validators.go | 32 +++++++ x/vaas/provider/keeper/consumer_lifecycle.go | 2 +- x/vaas/provider/keeper/relay.go | 8 +- .../provider/keeper/validator_set_update.go | 27 ++++-- .../keeper/validator_set_update_test.go | 64 +++++++++++++ x/vaas/types/wire.pb.go | 92 ++++++++++++++----- x/vaas/types/wire_test.go | 20 ++++ 10 files changed, 261 insertions(+), 39 deletions(-) create mode 100644 x/vaas/provider/keeper/validator_set_update_test.go create mode 100644 x/vaas/types/wire_test.go diff --git a/proto/vaas/v1/wire.proto b/proto/vaas/v1/wire.proto index 946e99f..ae2080d 100644 --- a/proto/vaas/v1/wire.proto +++ b/proto/vaas/v1/wire.proto @@ -39,4 +39,11 @@ message ValidatorSetChangePacketData { // non-/ibc.core.* and non-/cosmos.gov.* messages until the provider // collects fees successfully and propagates false on the next VSC. bool consumer_in_debt = 3; + // is_snapshot indicates how the consumer must apply validator_updates. + // false: validator_updates is a diff against the consumer's previous set + // (the consumer accumulates). true: validator_updates is the provider's + // complete current set (the consumer replaces its set with it). The + // provider sends a snapshot while a consumer is behind on acks, so a + // consumer that missed packets converges to the exact set in one packet. + bool is_snapshot = 4; } diff --git a/x/vaas/consumer/keeper/relay.go b/x/vaas/consumer/keeper/relay.go index 4cc160d..293ca46 100644 --- a/x/vaas/consumer/keeper/relay.go +++ b/x/vaas/consumer/keeper/relay.go @@ -48,13 +48,17 @@ func (k Keeper) OnRecvVSCPacketV2(ctx sdk.Context, sourceClientID string, newCha k.SetConsumerInDebt(ctx, newChanges.ConsumerInDebt) - // Set pending changes by accumulating changes from this packet with all prior changes - currentValUpdates := []abci.ValidatorUpdate{} - currentChanges, exists := k.GetPendingChanges(ctx) - if exists { - currentValUpdates = currentChanges.ValidatorUpdates + // Set pending changes: snapshot packets replace the set; diff packets accumulate. + var pendingChanges []abci.ValidatorUpdate + if newChanges.IsSnapshot { + pendingChanges = k.computeReplaceUpdates(ctx, newChanges.ValidatorUpdates) + } else { + currentValUpdates := []abci.ValidatorUpdate{} + if currentChanges, exists := k.GetPendingChanges(ctx); exists { + currentValUpdates = currentChanges.ValidatorUpdates + } + pendingChanges = vaastypes.AccumulateChanges(currentValUpdates, newChanges.ValidatorUpdates) } - pendingChanges := vaastypes.AccumulateChanges(currentValUpdates, newChanges.ValidatorUpdates) k.SetPendingChanges(ctx, vaastypes.ValidatorSetChangePacketData{ ValidatorUpdates: pendingChanges, diff --git a/x/vaas/consumer/keeper/relay_test.go b/x/vaas/consumer/keeper/relay_test.go index cb28232..ced6fe9 100644 --- a/x/vaas/consumer/keeper/relay_test.go +++ b/x/vaas/consumer/keeper/relay_test.go @@ -260,6 +260,38 @@ func TestConsumerVSCStaleness(t *testing.T) { require.True(t, k.IsVSCStale(stale)) } +func TestSnapshotReplacesValidatorSet(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pkA := ed25519.GenPrivKey().PubKey() + pkB := ed25519.GenPrivKey().PubKey() + tmA, err := cryptocodec.ToCmtProtoPublicKey(pkA) + require.NoError(t, err) + tmB, err := cryptocodec.ToCmtProtoPublicKey(pkB) + require.NoError(t, err) + + // Seed current CC set: A=10, B=5. + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{{PubKey: tmA, Power: 10}, {PubKey: tmB, Power: 5}}) + require.NoError(t, k.SetHighestValsetUpdateID(ctx, 1)) + k.SetProviderClientID(ctx, "07-tendermint-0") + + snap := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: tmA, Power: 10}}, 2) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + + pending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + // snapshot must produce exactly 2 entries: A at 10 and B at 0 (explicit removal) + require.Len(t, pending.ValidatorUpdates, 2) + powers := map[string]int64{} + for _, u := range pending.ValidatorUpdates { + powers[u.PubKey.String()] = u.Power + } + require.Equal(t, int64(10), powers[tmA.String()]) + require.Equal(t, int64(0), powers[tmB.String()]) // B explicitly removed +} + func TestOnRecvVSCRecordsRecvTime(t *testing.T) { providerClientID := "07-tendermint-0" diff --git a/x/vaas/consumer/keeper/validators.go b/x/vaas/consumer/keeper/validators.go index 60f89d7..677b67d 100644 --- a/x/vaas/consumer/keeper/validators.go +++ b/x/vaas/consumer/keeper/validators.go @@ -308,3 +308,35 @@ func (k Keeper) GetAllValidators(ctx context.Context) ([]stakingtypes.Validator, func (k Keeper) GetBondedValidatorsByPower(goCtx context.Context) ([]stakingtypes.Validator, error) { return []stakingtypes.Validator{}, nil } + +// computeReplaceUpdates turns a complete target set into the updates needed to +// move the consumer's current cross-chain set to exactly that target: every +// target entry at its absolute power, plus a power-0 removal for each current +// validator absent from the target. +// +// Identity matching uses the cmt proto public key string, which is the same +// identity used by MustGetCurrentValidatorsAsABCIUpdates and ApplyCCValidatorChanges. +func (k Keeper) computeReplaceUpdates(ctx context.Context, target []abci.ValidatorUpdate) []abci.ValidatorUpdate { + inTarget := make(map[string]struct{}, len(target)) + for _, u := range target { + inTarget[u.PubKey.String()] = struct{}{} + } + + updates := make([]abci.ValidatorUpdate, 0, len(target)+len(k.GetAllCCValidator(ctx))) + updates = append(updates, target...) + + for _, v := range k.GetAllCCValidator(ctx) { + pk, err := v.ConsPubKey() + if err != nil { + continue + } + tmPk, err := cryptocodec.ToCmtProtoPublicKey(pk) + if err != nil { + continue + } + if _, ok := inTarget[tmPk.String()]; !ok { + updates = append(updates, abci.ValidatorUpdate{PubKey: tmPk, Power: 0}) + } + } + return updates +} diff --git a/x/vaas/provider/keeper/consumer_lifecycle.go b/x/vaas/provider/keeper/consumer_lifecycle.go index 2e88858..9cb97f4 100644 --- a/x/vaas/provider/keeper/consumer_lifecycle.go +++ b/x/vaas/provider/keeper/consumer_lifecycle.go @@ -232,7 +232,7 @@ func (k Keeper) LaunchConsumer( } // compute consumer initial validator set (all validators validate all consumers) - initialValUpdates, err := k.ComputeConsumerNextValSet(ctx, bondedValidators, consumerId, []types.ConsensusValidator{}) + initialValUpdates, err := k.ComputeConsumerNextValSet(ctx, bondedValidators, consumerId, []types.ConsensusValidator{}, false) if err != nil { return fmt.Errorf("computing consumer next validator set, consumerId(%d): %w", consumerId, err) } diff --git a/x/vaas/provider/keeper/relay.go b/x/vaas/provider/keeper/relay.go index 42c8f54..22688d6 100644 --- a/x/vaas/provider/keeper/relay.go +++ b/x/vaas/provider/keeper/relay.go @@ -277,8 +277,11 @@ func (k Keeper) QueueVSCPackets(ctx sdk.Context) error { return fmt.Errorf("getting consumer current validator set, consumerId(%d): %w", consumerId, err) } - // compute consumer next validator set (all validators validate all consumers) - valUpdates, err := k.ComputeConsumerNextValSet(ctx, bondedValidators, consumerId, currentValSet) + // Send a full snapshot when the consumer is behind (i.e. it has + // unacknowledged packets), otherwise send a diff. + isSnapshot := k.GetConsumerHighestAckedVscId(ctx, consumerId) < k.GetConsumerHighestSentVscId(ctx, consumerId) + + valUpdates, err := k.ComputeConsumerNextValSet(ctx, bondedValidators, consumerId, currentValSet, isSnapshot) if err != nil { return fmt.Errorf("computing consumer next validator set, consumerId(%d): %w", consumerId, err) } @@ -292,6 +295,7 @@ func (k Keeper) QueueVSCPackets(ctx sdk.Context) error { // bounded: at most one packet per consumer per epoch. packet := vaastypes.NewValidatorSetChangePacketData(valUpdates, valUpdateID) packet.ConsumerInDebt = k.IsConsumerInDebt(ctx, consumerId) + packet.IsSnapshot = isSnapshot k.AppendPendingVSCPackets(ctx, consumerId, packet) k.Logger(ctx).Info("VSCPacket enqueued:", "consumerId", consumerId, diff --git a/x/vaas/provider/keeper/validator_set_update.go b/x/vaas/provider/keeper/validator_set_update.go index 37dc218..f79a170 100644 --- a/x/vaas/provider/keeper/validator_set_update.go +++ b/x/vaas/provider/keeper/validator_set_update.go @@ -174,30 +174,41 @@ func (k Keeper) GetLastProviderConsensusActiveValidators(ctx sdk.Context) ([]sta return vaastypes.GetLastBondedValidatorsUtil(ctx, k.stakingKeeper, uint32(maxVals)) } +// FullValSetUpdates renders a complete validator set as absolute-power updates. +// Used for snapshot VSC packets: the consumer replaces its set with these, +// deriving removals against its own current set. +func FullValSetUpdates(validators []types.ConsensusValidator) []abci.ValidatorUpdate { + updates := make([]abci.ValidatorUpdate, 0, len(validators)) + for _, v := range validators { + updates = append(updates, abci.ValidatorUpdate{PubKey: *v.PublicKey, Power: v.Power}) + } + return updates +} + // ComputeConsumerNextValSet computes the consumer next validator set and returns // the validator updates to be sent to the consumer chain. -// PSS (Partial Set Security) has been removed - all validators validate all consumers. +// When isSnapshot is true, it returns the full set as absolute-power updates; +// otherwise it returns the diff against currentConsumerValSet. func (k Keeper) ComputeConsumerNextValSet( ctx sdk.Context, bondedValidators []stakingtypes.Validator, consumerId uint64, currentConsumerValSet []types.ConsensusValidator, + isSnapshot bool, ) ([]abci.ValidatorUpdate, error) { - // All validators validate all consumers (no opt-in/out, no power shaping) nextValidators, err := k.ComputeNextValidators(ctx, consumerId, bondedValidators) if err != nil { return []abci.ValidatorUpdate{}, fmt.Errorf("computing next validators, consumerId(%d): %w", consumerId, err) } - err = k.SetConsumerValSet(ctx, consumerId, nextValidators) - if err != nil { + if err = k.SetConsumerValSet(ctx, consumerId, nextValidators); err != nil { return []abci.ValidatorUpdate{}, fmt.Errorf("setting consumer validator set, consumerId(%d): %w", consumerId, err) } - // get the initial updates with the latest set consumer public keys - valUpdates := DiffValidators(currentConsumerValSet, nextValidators) - - return valUpdates, nil + if isSnapshot { + return FullValSetUpdates(nextValidators), nil + } + return DiffValidators(currentConsumerValSet, nextValidators), nil } diff --git a/x/vaas/provider/keeper/validator_set_update_test.go b/x/vaas/provider/keeper/validator_set_update_test.go new file mode 100644 index 0000000..13ee7e4 --- /dev/null +++ b/x/vaas/provider/keeper/validator_set_update_test.go @@ -0,0 +1,64 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + testkeeper "github.com/allinbits/vaas/testutil/keeper" + keeper "github.com/allinbits/vaas/x/vaas/provider/keeper" + providertypes "github.com/allinbits/vaas/x/vaas/provider/types" +) + +func TestFullValSetUpdatesReturnsCompleteSet(t *testing.T) { + pk1 := ed25519.GenPrivKey().PubKey() + tmPk1, err := cryptocodec.ToCmtProtoPublicKey(pk1) + require.NoError(t, err) + vals := []providertypes.ConsensusValidator{{PublicKey: &tmPk1, Power: 10}} + + updates := keeper.FullValSetUpdates(vals) + require.Len(t, updates, 1) + require.Equal(t, int64(10), updates[0].Power) +} + +func TestQueueVSCPacketsSnapshotWhenBehind(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + k.SetConsumerHighestSentVscId(ctx, cid, 5) + k.SetConsumerHighestAckedVscId(ctx, cid, 3) // behind: acked < sent: acked < sent + + mocks.MockStakingKeeper.EXPECT().MaxValidators(gomock.Any()).Return(uint32(100), nil).AnyTimes() + mocks.MockStakingKeeper.EXPECT().GetBondedValidatorsByPower(gomock.Any()).Return([]stakingtypes.Validator{}, nil).AnyTimes() + + require.NoError(t, k.QueueVSCPackets(ctx)) + pending := k.GetPendingVSCPackets(ctx, cid) + require.Len(t, pending, 1) + require.True(t, pending[0].IsSnapshot) +} + +func TestQueueVSCPacketsDiffWhenCaughtUp(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + // acked == sent == 0: caught up + k.SetConsumerHighestSentVscId(ctx, cid, 0) + k.SetConsumerHighestAckedVscId(ctx, cid, 0) + + mocks.MockStakingKeeper.EXPECT().MaxValidators(gomock.Any()).Return(uint32(100), nil).AnyTimes() + mocks.MockStakingKeeper.EXPECT().GetBondedValidatorsByPower(gomock.Any()).Return([]stakingtypes.Validator{}, nil).AnyTimes() + + require.NoError(t, k.QueueVSCPackets(ctx)) + pending := k.GetPendingVSCPackets(ctx, cid) + require.Len(t, pending, 1) + require.False(t, pending[0].IsSnapshot) +} diff --git a/x/vaas/types/wire.pb.go b/x/vaas/types/wire.pb.go index 10eb5ee..c0324c1 100644 --- a/x/vaas/types/wire.pb.go +++ b/x/vaas/types/wire.pb.go @@ -46,6 +46,13 @@ type ValidatorSetChangePacketData struct { // non-/ibc.core.* and non-/cosmos.gov.* messages until the provider // collects fees successfully and propagates false on the next VSC. ConsumerInDebt bool `protobuf:"varint,3,opt,name=consumer_in_debt,json=consumerInDebt,proto3" json:"consumer_in_debt,omitempty"` + // is_snapshot indicates how the consumer must apply validator_updates. + // false: validator_updates is a diff against the consumer's previous set + // (the consumer accumulates). true: validator_updates is the provider's + // complete current set (the consumer replaces its set with it). The + // provider sends a snapshot while a consumer is behind on acks, so a + // consumer that missed packets converges to the exact set in one packet. + IsSnapshot bool `protobuf:"varint,4,opt,name=is_snapshot,json=isSnapshot,proto3" json:"is_snapshot,omitempty"` } func (m *ValidatorSetChangePacketData) Reset() { *m = ValidatorSetChangePacketData{} } @@ -102,6 +109,13 @@ func (m *ValidatorSetChangePacketData) GetConsumerInDebt() bool { return false } +func (m *ValidatorSetChangePacketData) GetIsSnapshot() bool { + if m != nil { + return m.IsSnapshot + } + return false +} + func init() { proto.RegisterType((*ValidatorSetChangePacketData)(nil), "vaas.v1.ValidatorSetChangePacketData") } @@ -109,28 +123,29 @@ func init() { func init() { proto.RegisterFile("vaas/v1/wire.proto", fileDescriptor_17a432f87dfb1130) } var fileDescriptor_17a432f87dfb1130 = []byte{ - // 333 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x91, 0xc1, 0x4a, 0xeb, 0x40, - 0x14, 0x86, 0x33, 0xb7, 0x97, 0x7b, 0x2f, 0xb9, 0x50, 0x34, 0xb8, 0x88, 0x55, 0xd2, 0x50, 0x44, - 0xb2, 0xca, 0x10, 0xdd, 0xb9, 0x92, 0xda, 0x4d, 0x77, 0x52, 0xd1, 0x85, 0x9b, 0x70, 0x26, 0x19, - 0xd2, 0xa1, 0xc9, 0x4c, 0xc9, 0x9c, 0x46, 0xfb, 0x16, 0x3e, 0x56, 0x97, 0x5d, 0xba, 0x2a, 0xd2, - 0xbc, 0x81, 0x4f, 0x20, 0xc9, 0xb4, 0x8a, 0xb8, 0x9a, 0x73, 0xfe, 0xff, 0x3b, 0xff, 0x81, 0x33, - 0xb6, 0x53, 0x01, 0x68, 0x5a, 0x45, 0xf4, 0x49, 0x94, 0x3c, 0x9c, 0x97, 0x0a, 0x95, 0xf3, 0xb7, - 0xd1, 0xc2, 0x2a, 0xea, 0x9d, 0x25, 0x4a, 0x17, 0x4a, 0x53, 0x8d, 0x30, 0x13, 0x32, 0xa3, 0x55, - 0xc4, 0x38, 0x42, 0xb4, 0xef, 0x0d, 0xde, 0x3b, 0xca, 0x54, 0xa6, 0xda, 0x92, 0x36, 0xd5, 0x4e, - 0x3d, 0x41, 0x2e, 0x53, 0x5e, 0x16, 0x42, 0x22, 0x05, 0x96, 0x08, 0x8a, 0xcb, 0x39, 0xd7, 0x3b, - 0xf3, 0xd8, 0x04, 0xc7, 0x66, 0xca, 0x34, 0xc6, 0x1a, 0xd4, 0xc4, 0x3e, 0x7d, 0x80, 0x5c, 0xa4, - 0x80, 0xaa, 0xbc, 0xe3, 0x78, 0x33, 0x05, 0x99, 0xf1, 0x5b, 0x48, 0x66, 0x1c, 0x47, 0x80, 0xe0, - 0x28, 0xfb, 0xb0, 0xda, 0xfb, 0xf1, 0x62, 0x9e, 0x02, 0x72, 0xed, 0x12, 0xbf, 0x13, 0xfc, 0xbf, - 0xf0, 0xc3, 0xaf, 0xa5, 0x61, 0xb3, 0x34, 0xfc, 0x4c, 0xba, 0x6f, 0xc1, 0xa1, 0xbf, 0xda, 0xf4, - 0xad, 0xf7, 0x4d, 0xdf, 0x5d, 0x42, 0x91, 0x5f, 0x0d, 0x7e, 0x04, 0x0d, 0x26, 0x07, 0xd5, 0xf7, - 0x11, 0xed, 0x04, 0x76, 0xa3, 0x69, 0x8e, 0x3b, 0x28, 0x16, 0xa9, 0xfb, 0xcb, 0x27, 0xc1, 0xef, - 0x49, 0xd7, 0xe8, 0x06, 0x1c, 0xa7, 0x0d, 0x99, 0x28, 0xa9, 0x17, 0x05, 0x2f, 0x63, 0x21, 0xe3, - 0x94, 0x33, 0x74, 0x3b, 0x3e, 0x09, 0xfe, 0x4d, 0xba, 0x7b, 0x7d, 0x2c, 0x47, 0x9c, 0xe1, 0xf0, - 0x7a, 0xb5, 0xf5, 0xc8, 0x7a, 0xeb, 0x91, 0xb7, 0xad, 0x47, 0x5e, 0x6a, 0xcf, 0x5a, 0xd7, 0x9e, - 0xf5, 0x5a, 0x7b, 0xd6, 0xe3, 0x79, 0x26, 0x70, 0xba, 0x60, 0x61, 0xa2, 0x0a, 0x0a, 0x79, 0x2e, - 0x24, 0x13, 0xa8, 0x69, 0xfb, 0x4b, 0xcf, 0xe6, 0x69, 0x0f, 0xc9, 0xfe, 0xb4, 0xe7, 0xba, 0xfc, - 0x08, 0x00, 0x00, 0xff, 0xff, 0x0e, 0x4a, 0x54, 0xd8, 0xc1, 0x01, 0x00, 0x00, + // 352 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x91, 0xc1, 0x4a, 0xf3, 0x40, + 0x10, 0x80, 0x93, 0xb6, 0xfc, 0xff, 0x4f, 0x0a, 0xe5, 0x37, 0x78, 0x88, 0x55, 0xd2, 0x50, 0x44, + 0x72, 0xca, 0x12, 0xbd, 0x79, 0x92, 0xda, 0x4b, 0x6f, 0xd2, 0xa2, 0x07, 0x2f, 0x61, 0x92, 0x2c, + 0xe9, 0xd2, 0x64, 0x37, 0x64, 0xa7, 0xd1, 0xde, 0x7d, 0x00, 0x1f, 0xab, 0xc7, 0x1e, 0x3d, 0x15, + 0x69, 0xdf, 0xc0, 0x27, 0x90, 0x64, 0x53, 0x45, 0x3c, 0xed, 0xcc, 0x37, 0xdf, 0xcc, 0xc0, 0xac, + 0x61, 0x96, 0x00, 0x92, 0x94, 0x3e, 0x79, 0x62, 0x05, 0xf5, 0xf2, 0x42, 0xa0, 0x30, 0xff, 0x56, + 0xcc, 0x2b, 0xfd, 0xfe, 0x79, 0x24, 0x64, 0x26, 0x24, 0x91, 0x08, 0x0b, 0xc6, 0x13, 0x52, 0xfa, + 0x21, 0x45, 0xf0, 0x0f, 0xb9, 0xd2, 0xfb, 0xc7, 0x89, 0x48, 0x44, 0x1d, 0x92, 0x2a, 0x6a, 0xe8, + 0x29, 0x52, 0x1e, 0xd3, 0x22, 0x63, 0x1c, 0x09, 0x84, 0x11, 0x23, 0xb8, 0xca, 0xa9, 0x6c, 0x8a, + 0x27, 0x6a, 0x70, 0xa0, 0xba, 0x54, 0xa2, 0x4a, 0xc3, 0x97, 0x96, 0x71, 0xf6, 0x00, 0x29, 0x8b, + 0x01, 0x45, 0x31, 0xa3, 0x78, 0x3b, 0x07, 0x9e, 0xd0, 0x3b, 0x88, 0x16, 0x14, 0xc7, 0x80, 0x60, + 0x0a, 0xe3, 0xa8, 0x3c, 0xd4, 0x83, 0x65, 0x1e, 0x03, 0x52, 0x69, 0xe9, 0x4e, 0xdb, 0xed, 0x5e, + 0x3a, 0xde, 0xf7, 0x52, 0xaf, 0x5a, 0xea, 0x7d, 0x4d, 0xba, 0xaf, 0xc5, 0x91, 0xb3, 0xde, 0x0e, + 0xb4, 0x8f, 0xed, 0xc0, 0x5a, 0x41, 0x96, 0x5e, 0x0f, 0x7f, 0x0d, 0x1a, 0x4e, 0xff, 0x97, 0x3f, + 0x5b, 0xa4, 0xe9, 0x1a, 0x15, 0x93, 0x14, 0x1b, 0x29, 0x60, 0xb1, 0xd5, 0x72, 0x74, 0xb7, 0x33, + 0xed, 0x29, 0xae, 0xc4, 0x49, 0x5c, 0x99, 0x91, 0xe0, 0x72, 0x99, 0xd1, 0x22, 0x60, 0x3c, 0x88, + 0x69, 0x88, 0x56, 0xdb, 0xd1, 0xdd, 0x7f, 0xd3, 0xde, 0x81, 0x4f, 0xf8, 0x98, 0x86, 0x68, 0x0e, + 0x8c, 0x2e, 0x93, 0x81, 0xe4, 0x90, 0xcb, 0xb9, 0x40, 0xab, 0x53, 0x4b, 0x06, 0x93, 0xb3, 0x86, + 0x8c, 0x6e, 0xd6, 0x3b, 0x5b, 0xdf, 0xec, 0x6c, 0xfd, 0x7d, 0x67, 0xeb, 0xaf, 0x7b, 0x5b, 0xdb, + 0xec, 0x6d, 0xed, 0x6d, 0x6f, 0x6b, 0x8f, 0x17, 0x09, 0xc3, 0xf9, 0x32, 0xf4, 0x22, 0x91, 0x11, + 0x48, 0x53, 0xc6, 0x43, 0x86, 0x92, 0xd4, 0xdf, 0xf8, 0xac, 0x9e, 0xfa, 0xd2, 0xe1, 0x9f, 0xfa, + 0x9e, 0x57, 0x9f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x95, 0xa1, 0xfa, 0xe0, 0xe2, 0x01, 0x00, 0x00, } func (m *ValidatorSetChangePacketData) Marshal() (dAtA []byte, err error) { @@ -153,6 +168,16 @@ func (m *ValidatorSetChangePacketData) MarshalToSizedBuffer(dAtA []byte) (int, e _ = i var l int _ = l + if m.IsSnapshot { + i-- + if m.IsSnapshot { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x20 + } if m.ConsumerInDebt { i-- if m.ConsumerInDebt { @@ -214,6 +239,9 @@ func (m *ValidatorSetChangePacketData) Size() (n int) { if m.ConsumerInDebt { n += 2 } + if m.IsSnapshot { + n += 2 + } return n } @@ -325,6 +353,26 @@ func (m *ValidatorSetChangePacketData) Unmarshal(dAtA []byte) error { } } m.ConsumerInDebt = bool(v != 0) + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field IsSnapshot", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowWire + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.IsSnapshot = bool(v != 0) default: iNdEx = preIndex skippy, err := skipWire(dAtA[iNdEx:]) diff --git a/x/vaas/types/wire_test.go b/x/vaas/types/wire_test.go new file mode 100644 index 0000000..617c1ea --- /dev/null +++ b/x/vaas/types/wire_test.go @@ -0,0 +1,20 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/allinbits/vaas/x/vaas/types" +) + +func TestValidatorSetChangePacketData_IsSnapshotRoundTrip(t *testing.T) { + pkt := types.NewValidatorSetChangePacketData(nil, 7) + pkt.IsSnapshot = true + + bz := pkt.GetBytes() + var got types.ValidatorSetChangePacketData + require.NoError(t, types.ModuleCdc.UnmarshalJSON(bz, &got)) + require.True(t, got.IsSnapshot) + require.Equal(t, uint64(7), got.ValsetUpdateId) +} From 8eae4421cf9df464b0dcdeb0a493023f1b34d0b7 Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:37:58 +0200 Subject: [PATCH 04/20] bound vaas_timeout_period and consumer unbonding period --- x/vaas/provider/keeper/msg_server.go | 25 ++++ x/vaas/provider/keeper/msg_server_test.go | 141 ++++++++++++++++++---- x/vaas/provider/types/msg.go | 3 +- x/vaas/provider/types/msg_test.go | 12 +- x/vaas/provider/types/params.go | 3 +- x/vaas/provider/types/params_test.go | 17 ++- x/vaas/types/shared_params.go | 30 ++++- 7 files changed, 199 insertions(+), 32 deletions(-) diff --git a/x/vaas/provider/keeper/msg_server.go b/x/vaas/provider/keeper/msg_server.go index e92f9ff..954ee79 100644 --- a/x/vaas/provider/keeper/msg_server.go +++ b/x/vaas/provider/keeper/msg_server.go @@ -218,6 +218,25 @@ func (k msgServer) SubmitConsumerDoubleVoting(goCtx context.Context, msg *types. return &types.MsgSubmitConsumerDoubleVotingResponse{}, nil } +// validateConsumerUnbonding enforces floor <= consumer unbonding <= provider +// unbonding, so a consumer's relayer-derived trusting period stays sane and the +// consumer cannot outlive the provider's slashable window. +func (k Keeper) validateConsumerUnbonding(ctx sdk.Context, d time.Duration) error { + providerUnbonding, err := k.stakingKeeper.UnbondingTime(ctx) + if err != nil { + return err + } + if d < vaastypes.MinConsumerUnbondingPeriod { + return errorsmod.Wrapf(types.ErrInvalidConsumerInitializationParameters, + "unbonding period must be >= %s, got %s", vaastypes.MinConsumerUnbondingPeriod, d) + } + if d > providerUnbonding { + return errorsmod.Wrapf(types.ErrInvalidConsumerInitializationParameters, + "unbonding period %s must not exceed provider unbonding %s", d, providerUnbonding) + } + return nil +} + // CreateConsumer creates a consumer chain func (k msgServer) CreateConsumer(goCtx context.Context, msg *types.MsgCreateConsumer) (*types.MsgCreateConsumerResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) @@ -261,6 +280,9 @@ func (k msgServer) CreateConsumer(goCtx context.Context, msg *types.MsgCreateCon if msg.InitializationParameters != nil { initializationParameters = *msg.InitializationParameters } + if err := k.Keeper.validateConsumerUnbonding(ctx, initializationParameters.UnbondingPeriod); err != nil { + return &resp, err + } if err := k.Keeper.SetConsumerInitializationParameters(ctx, consumerId, initializationParameters); err != nil { return &resp, errorsmod.Wrapf(types.ErrInvalidConsumerInitializationParameters, "cannot set consumer initialization parameters: %s", err.Error()) @@ -457,6 +479,9 @@ func (k msgServer) UpdateConsumer(goCtx context.Context, msg *types.MsgUpdateCon sdk.NewAttribute(types.AttributeConsumerGenesisHash, string(msg.InitializationParameters.GenesisHash))) } + if err = k.Keeper.validateConsumerUnbonding(ctx, msg.InitializationParameters.UnbondingPeriod); err != nil { + return &resp, err + } if err = k.Keeper.SetConsumerInitializationParameters(ctx, msg.ConsumerId, *msg.InitializationParameters); err != nil { return &resp, errorsmod.Wrapf(types.ErrInvalidConsumerInitializationParameters, "cannot set consumer initialization parameters: %s", err.Error()) diff --git a/x/vaas/provider/keeper/msg_server_test.go b/x/vaas/provider/keeper/msg_server_test.go index 82b3ec5..ff610af 100644 --- a/x/vaas/provider/keeper/msg_server_test.go +++ b/x/vaas/provider/keeper/msg_server_test.go @@ -49,9 +49,11 @@ func validSubmitter() string { } func TestCreateConsumer(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).AnyTimes() + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) consumerMetadata := providertypes.ConsumerMetadata{ @@ -61,7 +63,9 @@ func TestCreateConsumer(t *testing.T) { response, err := msgServer.CreateConsumer(ctx, &providertypes.MsgCreateConsumer{ Submitter: "submitter", ChainId: "chainId", Metadata: consumerMetadata, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.NoError(t, err) require.Equal(t, uint64(0), response.ConsumerId) @@ -82,7 +86,9 @@ func TestCreateConsumer(t *testing.T) { response, err = msgServer.CreateConsumer(ctx, &providertypes.MsgCreateConsumer{ Submitter: "submitter2", ChainId: "chainId2", Metadata: consumerMetadata, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.NoError(t, err) // assert that the consumer id is different from the previously registered chain @@ -98,9 +104,12 @@ func TestCreateConsumer(t *testing.T) { } func TestCreateConsumerDuplicateChainId(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + // only the first CreateConsumer reaches validateConsumerUnbonding; the duplicate is rejected earlier + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(1) + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) consumerMetadata := providertypes.ConsumerMetadata{ @@ -112,16 +121,20 @@ func TestCreateConsumerDuplicateChainId(t *testing.T) { response, err := msgServer.CreateConsumer(ctx, &providertypes.MsgCreateConsumer{ Submitter: "submitter1", ChainId: "duplicateChainId", Metadata: consumerMetadata, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.NoError(t, err) require.Equal(t, uint64(0), response.ConsumerId) - // Attempt to register another consumer with the same chainId + // Attempt to register another consumer with the same chainId — rejected before unbonding check _, err = msgServer.CreateConsumer(ctx, &providertypes.MsgCreateConsumer{ Submitter: "submitter2", ChainId: "duplicateChainId", Metadata: consumerMetadata, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.Error(t, err) require.ErrorIs(t, err, providertypes.ErrDuplicateChainId) @@ -131,6 +144,8 @@ func TestUpdateConsumer(t *testing.T) { providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(1) + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) // try to update a non-existing consumer @@ -200,9 +215,11 @@ func TestUpdateConsumer(t *testing.T) { } func TestUpdateConsumerDuplicateChainId(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(2) + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) // create a chain that register chainId-1 @@ -638,6 +655,9 @@ func TestRemoveConsumerGovAuth(t *testing.T) { msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) + // CreateConsumer calls validateConsumerUnbonding; RemoveConsumer also calls UnbondingTime + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(2) + // create a consumer chain and set it to LAUNCHED createResp, err := msgServer.CreateConsumer(ctx, &providertypes.MsgCreateConsumer{ @@ -646,15 +666,15 @@ func TestRemoveConsumerGovAuth(t *testing.T) { Name: "name", Description: "description", }, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.NoError(t, err) consumerId := createResp.ConsumerId providerKeeper.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) - mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(time.Hour*24*7*3, nil).Times(1) - // non-authority should be rejected _, err = msgServer.RemoveConsumer(ctx, &providertypes.MsgRemoveConsumer{ @@ -678,9 +698,11 @@ func TestRemoveConsumerGovAuth(t *testing.T) { } func TestRemoveConsumerNonLaunchedRejected(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(1) + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) // create a consumer chain (stays in REGISTERED phase) @@ -691,7 +713,9 @@ func TestRemoveConsumerNonLaunchedRejected(t *testing.T) { Name: "name", Description: "description", }, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.NoError(t, err) @@ -709,6 +733,8 @@ func TestUpdateConsumerLaunchedOnlyMetadata(t *testing.T) { providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(1) + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) // create a consumer chain @@ -719,7 +745,9 @@ func TestUpdateConsumerLaunchedOnlyMetadata(t *testing.T) { Name: "name", Description: "description", }, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.NoError(t, err) consumerId := createResp.ConsumerId @@ -786,6 +814,8 @@ func TestUpdateConsumerPreLaunchAllowsAll(t *testing.T) { providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(1) + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) // create a consumer chain (REGISTERED phase) @@ -828,9 +858,11 @@ func TestUpdateConsumerPreLaunchAllowsAll(t *testing.T) { } func TestCreateConsumerEventsIncludeInitParams(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(1) + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) binaryHash := []byte("deadbeef") @@ -846,9 +878,10 @@ func TestCreateConsumerEventsIncludeInitParams(t *testing.T) { Description: "description", }, InitializationParameters: &providertypes.ConsumerInitializationParameters{ - BinaryHash: binaryHash, - GenesisHash: genesisHash, - SpawnTime: spawnTime, + BinaryHash: binaryHash, + GenesisHash: genesisHash, + SpawnTime: spawnTime, + UnbondingPeriod: 21 * 24 * time.Hour, }, }) require.NoError(t, err) @@ -880,14 +913,19 @@ func TestCreateConsumerEventsIncludeInitParams(t *testing.T) { } func TestCreateConsumer_PopulatesReverseLookup(t *testing.T) { - k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(1) + ms := providerkeeper.NewMsgServerImpl(&k) resp, err := ms.CreateConsumer(ctx, &providertypes.MsgCreateConsumer{ Submitter: "submitter", ChainId: "chainId", - Metadata: providertypes.ConsumerMetadata{Name: "n", Description: "d"}, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + Metadata: providertypes.ConsumerMetadata{Name: "n", Description: "d"}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.NoError(t, err) @@ -1270,6 +1308,67 @@ func TestWithdrawConsumerFeePool(t *testing.T) { } } +func TestCreateConsumerUnbondingBounds(t *testing.T) { + providerUnbonding := 21 * 24 * time.Hour + + tests := []struct { + name string + unbondingPeriod time.Duration + wantErr bool + }{ + { + name: "too long: exceeds provider unbonding", + unbondingPeriod: 30 * 24 * time.Hour, + wantErr: true, + }, + { + name: "too short: below minimum", + unbondingPeriod: 1 * 24 * time.Hour, + wantErr: true, + }, + { + name: "valid: equal to provider unbonding", + unbondingPeriod: 21 * 24 * time.Hour, + wantErr: false, + }, + { + name: "valid: equal to minimum", + unbondingPeriod: 5 * 24 * time.Hour, + wantErr: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) + + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(providerUnbonding, nil).Times(1) + + _, err := msgServer.CreateConsumer(ctx, + &providertypes.MsgCreateConsumer{ + Submitter: "submitter", + ChainId: "chainId", + Metadata: providertypes.ConsumerMetadata{ + Name: "name", + Description: "description", + }, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: tc.unbondingPeriod, + }, + }) + if tc.wantErr { + require.Error(t, err) + require.ErrorIs(t, err, providertypes.ErrInvalidConsumerInitializationParameters) + } else { + require.NoError(t, err) + } + }) + } +} + func TestSweepConsumerFeePool(t *testing.T) { owner := sdk.AccAddress([]byte("owner___________")) alice := sdk.AccAddress([]byte("alice___________")) diff --git a/x/vaas/provider/types/msg.go b/x/vaas/provider/types/msg.go index 5e53e98..11ab95a 100644 --- a/x/vaas/provider/types/msg.go +++ b/x/vaas/provider/types/msg.go @@ -12,6 +12,7 @@ import ( cmttypes "github.com/cometbft/cometbft/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" ibctmtypes "github.com/cosmos/ibc-go/v10/modules/light-clients/07-tendermint" errorsmod "cosmossdk.io/errors" @@ -428,7 +429,7 @@ func ValidateInitializationParameters(initializationParameters ConsumerInitializ return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "HistoricalEntries: %s", err.Error()) } - if err := vaastypes.ValidateDuration(initializationParameters.VaasTimeoutPeriod); err != nil { + if err := vaastypes.ValidateVAASTimeoutPeriod(initializationParameters.VaasTimeoutPeriod, channeltypesv2.MaxTimeoutDelta); err != nil { return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "VaasTimeoutPeriod: %s", err.Error()) } diff --git a/x/vaas/provider/types/msg_test.go b/x/vaas/provider/types/msg_test.go index 468a790..47fb7f5 100644 --- a/x/vaas/provider/types/msg_test.go +++ b/x/vaas/provider/types/msg_test.go @@ -165,7 +165,7 @@ func TestValidateInitializationParameters(t *testing.T) { BinaryHash: []byte{0x01}, SpawnTime: now, UnbondingPeriod: time.Duration(100000000000), - VaasTimeoutPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, HistoricalEntries: 10000, }, valid: true, @@ -178,7 +178,7 @@ func TestValidateInitializationParameters(t *testing.T) { BinaryHash: []byte{0x01}, SpawnTime: now, UnbondingPeriod: time.Duration(100000000000), - VaasTimeoutPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, HistoricalEntries: 10000, }, valid: false, @@ -191,7 +191,7 @@ func TestValidateInitializationParameters(t *testing.T) { BinaryHash: []byte{0x01}, SpawnTime: now, UnbondingPeriod: time.Duration(100000000000), - VaasTimeoutPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, HistoricalEntries: 10000, }, valid: false, @@ -204,7 +204,7 @@ func TestValidateInitializationParameters(t *testing.T) { BinaryHash: []byte{0x01}, SpawnTime: time.Time{}, UnbondingPeriod: time.Duration(100000000000), - VaasTimeoutPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, HistoricalEntries: 10000, }, valid: true, @@ -217,7 +217,7 @@ func TestValidateInitializationParameters(t *testing.T) { BinaryHash: []byte{0x01}, SpawnTime: now, UnbondingPeriod: 0, - VaasTimeoutPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, HistoricalEntries: 10000, }, valid: false, @@ -230,7 +230,7 @@ func TestValidateInitializationParameters(t *testing.T) { BinaryHash: []byte{0x01}, SpawnTime: now, UnbondingPeriod: time.Duration(100000000000), - VaasTimeoutPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, HistoricalEntries: 0, }, valid: false, diff --git a/x/vaas/provider/types/params.go b/x/vaas/provider/types/params.go index 6121c97..4c06839 100644 --- a/x/vaas/provider/types/params.go +++ b/x/vaas/provider/types/params.go @@ -7,6 +7,7 @@ import ( vaastypes "github.com/allinbits/vaas/x/vaas/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" "cosmossdk.io/math" ) @@ -162,7 +163,7 @@ func (p Params) Validate() error { if err := vaastypes.ValidateStringFractionNonZero(p.TrustingPeriodFraction); err != nil { return fmt.Errorf("trusting period fraction is invalid: %s", err) } - if err := vaastypes.ValidateDuration(p.VaasTimeoutPeriod); err != nil { + if err := vaastypes.ValidateVAASTimeoutPeriod(p.VaasTimeoutPeriod, channeltypesv2.MaxTimeoutDelta); err != nil { return fmt.Errorf("VAAS timeout period is invalid: %s", err) } if err := vaastypes.ValidatePositiveInt64(p.BlocksPerEpoch); err != nil { diff --git a/x/vaas/provider/types/params_test.go b/x/vaas/provider/types/params_test.go index 87fa59b..57255c8 100644 --- a/x/vaas/provider/types/params_test.go +++ b/x/vaas/provider/types/params_test.go @@ -4,10 +4,13 @@ import ( "testing" "time" - "github.com/allinbits/vaas/x/vaas/provider/types" "github.com/stretchr/testify/require" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" + "cosmossdk.io/math" + + "github.com/allinbits/vaas/x/vaas/provider/types" ) func TestValidateParams(t *testing.T) { @@ -35,6 +38,18 @@ func TestValidateParams(t *testing.T) { } } +func TestParamsRejectsTimeoutAboveMaxDelta(t *testing.T) { + p := types.DefaultParams() + p.VaasTimeoutPeriod = channeltypesv2.MaxTimeoutDelta + time.Hour + require.Error(t, p.Validate()) +} + +func TestParamsAcceptsTimeoutAtCap(t *testing.T) { + p := types.DefaultParams() + p.VaasTimeoutPeriod = channeltypesv2.MaxTimeoutDelta + require.NoError(t, p.Validate()) +} + func TestValidateInfractionParameters(t *testing.T) { testCases := []struct { name string diff --git a/x/vaas/types/shared_params.go b/x/vaas/types/shared_params.go index 933456b..cc0f0f4 100644 --- a/x/vaas/types/shared_params.go +++ b/x/vaas/types/shared_params.go @@ -9,8 +9,21 @@ import ( ) const ( - // DefaultVAASTimeoutPeriod is 4 weeks to ensure channel doesn't close on timeout. - DefaultVAASTimeoutPeriod = 4 * 7 * 24 * time.Hour + // DefaultVAASTimeoutPeriod is the IBC packet timeout for VAAS packets. + // One epoch-scale value: undelivered packets are superseded the next + // epoch and late ones are dropped by the consumer, so a long timeout + // buys nothing. Must be <= MaxTimeoutDelta (24h, ibc-go v2 hard cap). + DefaultVAASTimeoutPeriod = time.Hour + + // MinVAASTimeoutPeriod is the floor for VaasTimeoutPeriod, comfortably + // above realistic IBC relay latency so packets are not expired before + // they can be delivered. + MinVAASTimeoutPeriod = 10 * time.Minute + + // MinConsumerUnbondingPeriod floors the consumer unbonding so the + // relayer-derived trusting period (unbonding * fraction) is at least a + // few days. + MinConsumerUnbondingPeriod = 5 * 24 * time.Hour ) var KeyVAASTimeoutPeriod = []byte("VaasTimeoutPeriod") @@ -22,6 +35,19 @@ func ValidateDuration(d time.Duration) error { return nil } +// ValidateVAASTimeoutPeriod checks the VAAS packet timeout is within +// [MinVAASTimeoutPeriod, maxTimeoutDelta]. maxTimeoutDelta is passed in to +// avoid importing ibc-go from this shared package. +func ValidateVAASTimeoutPeriod(d, maxTimeoutDelta time.Duration) error { + if d < MinVAASTimeoutPeriod { + return fmt.Errorf("VAAS timeout period must be >= %s, got %s", MinVAASTimeoutPeriod, d) + } + if d > maxTimeoutDelta { + return fmt.Errorf("VAAS timeout period must be <= %s (MaxTimeoutDelta), got %s", maxTimeoutDelta, d) + } + return nil +} + func ValidatePositiveInt64(n int64) error { if n <= int64(0) { return errors.New("int must be positive") From 6e0d7cd5c61ec5d4770ad946eabf8d9cebf35ea9 Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:37:58 +0200 Subject: [PATCH 05/20] consumer liveness query --- proto/vaas/provider/v1/query.proto | 16 + x/vaas/provider/keeper/grpc_query.go | 25 + x/vaas/provider/keeper/grpc_query_test.go | 17 + x/vaas/provider/types/query.pb.go | 791 ++++++++++++++++++---- x/vaas/provider/types/query.pb.gw.go | 101 +++ 5 files changed, 812 insertions(+), 138 deletions(-) diff --git a/proto/vaas/provider/v1/query.proto b/proto/vaas/provider/v1/query.proto index 63331ba..f22afba 100644 --- a/proto/vaas/provider/v1/query.proto +++ b/proto/vaas/provider/v1/query.proto @@ -6,6 +6,7 @@ option go_package = "github.com/allinbits/vaas/x/vaas/provider/types"; import "google/api/annotations.proto"; import "gogoproto/gogo.proto"; import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; import "vaas/provider/v1/genesis.proto"; import "vaas/provider/v1/provider.proto"; import "vaas/v1/shared_consumer.proto"; @@ -132,6 +133,13 @@ service Query { rpc ConsumerFeePoolClaims(QueryConsumerFeePoolClaimsRequest) returns (QueryConsumerFeePoolClaimsResponse) { option (google.api.http).get = "/vaas/provider/v1/consumer_fee_pool_claims/{consumer_id}"; } + + // QueryConsumerLiveness returns the liveness state of a launched consumer. + rpc QueryConsumerLiveness(QueryConsumerLivenessRequest) + returns (QueryConsumerLivenessResponse) { + option (google.api.http).get = + "/vaas/provider/consumer_liveness/{consumer_id}"; + } } message QueryConsumerGenesisRequest { @@ -358,3 +366,11 @@ message QueryConsumerFeePoolClaimsResponse { repeated DepositorClaim claims = 1 [(gogoproto.nullable) = false]; cosmos.base.query.v1beta1.PageResponse pagination = 2; } + +message QueryConsumerLivenessRequest { uint64 consumer_id = 1; } +message QueryConsumerLivenessResponse { + google.protobuf.Timestamp last_ack_time = 1 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; + google.protobuf.Duration grace_period = 2 [(gogoproto.stdduration) = true, (gogoproto.nullable) = false]; + google.protobuf.Timestamp removal_eta = 3 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; + bool degraded = 4; +} diff --git a/x/vaas/provider/keeper/grpc_query.go b/x/vaas/provider/keeper/grpc_query.go index 7eb9445..5d34bd9 100644 --- a/x/vaas/provider/keeper/grpc_query.go +++ b/x/vaas/provider/keeper/grpc_query.go @@ -593,6 +593,31 @@ func (k Keeper) QueryConsumerFeesPerBlock( }, nil } +func (k Keeper) QueryConsumerLiveness(goCtx context.Context, req *types.QueryConsumerLivenessRequest) (*types.QueryConsumerLivenessResponse, error) { + if req == nil { + return nil, status.Errorf(codes.InvalidArgument, "empty request") + } + ctx := sdk.UnwrapSDKContext(goCtx) + + if k.GetConsumerPhase(ctx, req.ConsumerId) == types.CONSUMER_PHASE_UNSPECIFIED { + return nil, status.Errorf(codes.InvalidArgument, "unknown consumer: %d", req.ConsumerId) + } + + grace, err := k.LivenessGracePeriod(ctx) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + lastAck := k.GetConsumerLastAckTime(ctx, req.ConsumerId) + removalEta := lastAck.Add(grace) + + return &types.QueryConsumerLivenessResponse{ + LastAckTime: lastAck, + GracePeriod: grace, + RemovalEta: removalEta, + Degraded: ctx.BlockTime().After(lastAck.Add(grace / 2)), + }, nil +} + // QueryAllConsumerFeesPerBlockOverrides returns the full list of overrides, // paginated, ordered by consumer_id ascending. func (k Keeper) QueryAllConsumerFeesPerBlockOverrides( diff --git a/x/vaas/provider/keeper/grpc_query_test.go b/x/vaas/provider/keeper/grpc_query_test.go index f3a7bf1..25fda2c 100644 --- a/x/vaas/provider/keeper/grpc_query_test.go +++ b/x/vaas/provider/keeper/grpc_query_test.go @@ -2,10 +2,12 @@ package keeper_test import ( "testing" + "time" "cosmossdk.io/collections" "cosmossdk.io/math" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -256,3 +258,18 @@ func TestQueryConsumerFeePoolClaims_UnknownConsumer(t *testing.T) { require.Error(t, err) require.Equal(t, codes.NotFound, status.Code(err)) } + +func TestQueryConsumerLiveness(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).AnyTimes() + + const cid = uint64(0) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime())) + + resp, err := k.QueryConsumerLiveness(ctx, &providertypes.QueryConsumerLivenessRequest{ConsumerId: cid}) + require.NoError(t, err) + require.False(t, resp.Degraded) + require.Equal(t, ctx.BlockTime().Add(resp.GracePeriod).UTC(), resp.RemovalEta.UTC()) +} diff --git a/x/vaas/provider/types/query.pb.go b/x/vaas/provider/types/query.pb.go index 406ee51..0de0f2e 100644 --- a/x/vaas/provider/types/query.pb.go +++ b/x/vaas/provider/types/query.pb.go @@ -22,6 +22,7 @@ import ( grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" + _ "google.golang.org/protobuf/types/known/durationpb" _ "google.golang.org/protobuf/types/known/timestamppb" io "io" math "math" @@ -1804,6 +1805,118 @@ func (m *QueryConsumerFeePoolClaimsResponse) GetPagination() *query.PageResponse return nil } +type QueryConsumerLivenessRequest struct { + ConsumerId uint64 `protobuf:"varint,1,opt,name=consumer_id,json=consumerId,proto3" json:"consumer_id,omitempty"` +} + +func (m *QueryConsumerLivenessRequest) Reset() { *m = QueryConsumerLivenessRequest{} } +func (m *QueryConsumerLivenessRequest) String() string { return proto.CompactTextString(m) } +func (*QueryConsumerLivenessRequest) ProtoMessage() {} +func (*QueryConsumerLivenessRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_71b3f55ed407aa51, []int{34} +} +func (m *QueryConsumerLivenessRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryConsumerLivenessRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryConsumerLivenessRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryConsumerLivenessRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryConsumerLivenessRequest.Merge(m, src) +} +func (m *QueryConsumerLivenessRequest) XXX_Size() int { + return m.Size() +} +func (m *QueryConsumerLivenessRequest) XXX_DiscardUnknown() { + xxx_messageInfo_QueryConsumerLivenessRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryConsumerLivenessRequest proto.InternalMessageInfo + +func (m *QueryConsumerLivenessRequest) GetConsumerId() uint64 { + if m != nil { + return m.ConsumerId + } + return 0 +} + +type QueryConsumerLivenessResponse struct { + LastAckTime time.Time `protobuf:"bytes,1,opt,name=last_ack_time,json=lastAckTime,proto3,stdtime" json:"last_ack_time"` + GracePeriod time.Duration `protobuf:"bytes,2,opt,name=grace_period,json=gracePeriod,proto3,stdduration" json:"grace_period"` + RemovalEta time.Time `protobuf:"bytes,3,opt,name=removal_eta,json=removalEta,proto3,stdtime" json:"removal_eta"` + Degraded bool `protobuf:"varint,4,opt,name=degraded,proto3" json:"degraded,omitempty"` +} + +func (m *QueryConsumerLivenessResponse) Reset() { *m = QueryConsumerLivenessResponse{} } +func (m *QueryConsumerLivenessResponse) String() string { return proto.CompactTextString(m) } +func (*QueryConsumerLivenessResponse) ProtoMessage() {} +func (*QueryConsumerLivenessResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_71b3f55ed407aa51, []int{35} +} +func (m *QueryConsumerLivenessResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryConsumerLivenessResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryConsumerLivenessResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryConsumerLivenessResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryConsumerLivenessResponse.Merge(m, src) +} +func (m *QueryConsumerLivenessResponse) XXX_Size() int { + return m.Size() +} +func (m *QueryConsumerLivenessResponse) XXX_DiscardUnknown() { + xxx_messageInfo_QueryConsumerLivenessResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryConsumerLivenessResponse proto.InternalMessageInfo + +func (m *QueryConsumerLivenessResponse) GetLastAckTime() time.Time { + if m != nil { + return m.LastAckTime + } + return time.Time{} +} + +func (m *QueryConsumerLivenessResponse) GetGracePeriod() time.Duration { + if m != nil { + return m.GracePeriod + } + return 0 +} + +func (m *QueryConsumerLivenessResponse) GetRemovalEta() time.Time { + if m != nil { + return m.RemovalEta + } + return time.Time{} +} + +func (m *QueryConsumerLivenessResponse) GetDegraded() bool { + if m != nil { + return m.Degraded + } + return false +} + func init() { proto.RegisterType((*QueryConsumerGenesisRequest)(nil), "vaas.provider.v1.QueryConsumerGenesisRequest") proto.RegisterType((*QueryConsumerGenesisResponse)(nil), "vaas.provider.v1.QueryConsumerGenesisResponse") @@ -1839,149 +1952,160 @@ func init() { proto.RegisterType((*DepositorClaim)(nil), "vaas.provider.v1.DepositorClaim") proto.RegisterType((*QueryConsumerFeePoolClaimsRequest)(nil), "vaas.provider.v1.QueryConsumerFeePoolClaimsRequest") proto.RegisterType((*QueryConsumerFeePoolClaimsResponse)(nil), "vaas.provider.v1.QueryConsumerFeePoolClaimsResponse") + proto.RegisterType((*QueryConsumerLivenessRequest)(nil), "vaas.provider.v1.QueryConsumerLivenessRequest") + proto.RegisterType((*QueryConsumerLivenessResponse)(nil), "vaas.provider.v1.QueryConsumerLivenessResponse") } func init() { proto.RegisterFile("vaas/provider/v1/query.proto", fileDescriptor_71b3f55ed407aa51) } var fileDescriptor_71b3f55ed407aa51 = []byte{ - // 2187 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x59, 0x4d, 0x6c, 0xdc, 0xc6, - 0xf5, 0x17, 0xd7, 0x92, 0xbc, 0x1a, 0xd9, 0x8a, 0xff, 0xf3, 0x97, 0xa3, 0xd5, 0x5a, 0xd6, 0xda, - 0x74, 0x9c, 0x28, 0xb6, 0x97, 0xb4, 0x56, 0xb6, 0x11, 0xa8, 0xae, 0x53, 0xef, 0x3a, 0x72, 0x05, - 0x37, 0xf5, 0x96, 0x76, 0x73, 0x08, 0xd0, 0x12, 0xdc, 0xe5, 0x78, 0x35, 0x15, 0x97, 0x64, 0x48, - 0xee, 0xda, 0x5b, 0x41, 0x97, 0x1e, 0x8a, 0x1e, 0x52, 0x20, 0x40, 0x0f, 0x3d, 0xf4, 0x12, 0x14, - 0x3d, 0xf5, 0xe3, 0x66, 0x14, 0x41, 0x81, 0x5e, 0x8a, 0x1e, 0x72, 0xc8, 0x21, 0x70, 0x2e, 0x41, - 0x0f, 0x4a, 0x61, 0x17, 0x45, 0xcf, 0x41, 0x7b, 0xe9, 0xa1, 0x28, 0x38, 0x1f, 0x5c, 0x72, 0x96, - 0xdc, 0xa5, 0x1c, 0xf7, 0xa4, 0xe5, 0xcc, 0x9b, 0x37, 0xbf, 0xf7, 0xe6, 0xcd, 0x9b, 0xf7, 0x7b, - 0x02, 0x2b, 0x7d, 0xc3, 0xf0, 0x55, 0xd7, 0x73, 0xfa, 0xd8, 0x44, 0x9e, 0xda, 0x5f, 0x57, 0xdf, - 0xeb, 0x21, 0x6f, 0xa0, 0xb8, 0x9e, 0x13, 0x38, 0xf0, 0x44, 0x38, 0xab, 0xf0, 0x59, 0xa5, 0xbf, - 0x5e, 0x5e, 0xe9, 0x38, 0x4e, 0xc7, 0x42, 0xaa, 0xe1, 0x62, 0xd5, 0xb0, 0x6d, 0x27, 0x30, 0x02, - 0xec, 0xd8, 0x3e, 0x95, 0x2f, 0x2f, 0x76, 0x9c, 0x8e, 0x43, 0x7e, 0xaa, 0xe1, 0x2f, 0x36, 0x5a, - 0x61, 0x6b, 0xc8, 0x57, 0xab, 0xf7, 0x40, 0x0d, 0x70, 0x17, 0xf9, 0x81, 0xd1, 0x75, 0x99, 0xc0, - 0xea, 0x08, 0x88, 0x0e, 0xb2, 0x91, 0x8f, 0xb9, 0xda, 0xca, 0xc8, 0x7c, 0x04, 0x89, 0x0a, 0x9c, - 0x26, 0x02, 0xfd, 0x75, 0xd5, 0xdf, 0x31, 0x3c, 0x64, 0xea, 0x6d, 0xc7, 0xf6, 0x7b, 0xdd, 0x68, - 0x1a, 0xf2, 0xe9, 0x87, 0xd8, 0x43, 0x6c, 0x6c, 0x25, 0x40, 0xb6, 0x89, 0xbc, 0x2e, 0xb6, 0x03, - 0xb5, 0xed, 0x0d, 0xdc, 0xc0, 0x51, 0x77, 0xd1, 0x80, 0xef, 0xb8, 0xdc, 0x76, 0xfc, 0xae, 0xe3, - 0xeb, 0xd4, 0x16, 0xfa, 0xc1, 0xc1, 0xd2, 0x2f, 0xb5, 0x65, 0xf8, 0x48, 0xed, 0xaf, 0xb7, 0x50, - 0x60, 0xac, 0xab, 0x6d, 0x07, 0xdb, 0x6c, 0xfe, 0x15, 0x36, 0xef, 0x07, 0xc6, 0x2e, 0xb6, 0x3b, - 0x91, 0x08, 0xfb, 0x66, 0x52, 0x17, 0xe2, 0x5a, 0x88, 0xcb, 0x23, 0x41, 0xd7, 0xe8, 0x60, 0x9b, - 0xb8, 0x95, 0xca, 0xca, 0x37, 0xc0, 0xa9, 0xef, 0x84, 0x12, 0x0d, 0x66, 0xd5, 0x6d, 0xea, 0x1c, - 0x0d, 0xbd, 0xd7, 0x43, 0x7e, 0x00, 0x2b, 0x60, 0x9e, 0xdb, 0xab, 0x63, 0xb3, 0x24, 0x9d, 0x91, - 0xd6, 0xa6, 0x35, 0xc0, 0x87, 0xb6, 0x4d, 0x79, 0x07, 0xac, 0xa4, 0xaf, 0xf7, 0x5d, 0xc7, 0xf6, - 0x11, 0xfc, 0x26, 0x38, 0xce, 0xfc, 0xad, 0xfb, 0x81, 0x11, 0x20, 0xa2, 0x62, 0xbe, 0x76, 0x5a, - 0x21, 0xa7, 0xdf, 0x5f, 0x57, 0x84, 0x85, 0xf7, 0x42, 0xa1, 0xfa, 0xf4, 0xc7, 0x07, 0x95, 0x29, - 0xed, 0x58, 0x27, 0x36, 0x26, 0xff, 0x42, 0x02, 0xe5, 0xc4, 0x56, 0x8d, 0x1d, 0x03, 0xdb, 0x11, - 0xd2, 0xab, 0x60, 0xc6, 0xdd, 0x31, 0x7c, 0xba, 0xc1, 0x42, 0xad, 0xa2, 0x88, 0xe1, 0x15, 0xed, - 0xd4, 0x0c, 0xc5, 0x34, 0x2a, 0x0d, 0xb7, 0x00, 0x18, 0xfa, 0xa4, 0x54, 0x20, 0xe0, 0x5e, 0x55, - 0xd8, 0xa1, 0x84, 0x0e, 0x54, 0x68, 0xcc, 0x32, 0x07, 0x2a, 0x4d, 0xa3, 0x83, 0xd8, 0x96, 0x5a, - 0x6c, 0xa5, 0xfc, 0x73, 0x49, 0x70, 0x24, 0x47, 0xc7, 0xfc, 0xa0, 0x82, 0xd9, 0x36, 0x19, 0x29, - 0x49, 0x67, 0x8e, 0xac, 0xcd, 0xd7, 0x96, 0x52, 0xf0, 0x85, 0xf3, 0x1a, 0x13, 0x83, 0xb7, 0x53, - 0x80, 0xbd, 0x36, 0x11, 0x18, 0xdd, 0x2d, 0x81, 0xec, 0xef, 0x12, 0x98, 0x21, 0xaa, 0xe1, 0x32, - 0x28, 0x12, 0xe5, 0xfc, 0x24, 0xe7, 0xb4, 0xa3, 0xe4, 0x7b, 0xdb, 0x84, 0xa7, 0xc0, 0x5c, 0xdb, - 0xc2, 0xc8, 0x0e, 0xc2, 0xb9, 0x02, 0x99, 0x2b, 0xd2, 0x81, 0x6d, 0x13, 0x2e, 0x72, 0xd7, 0x1e, - 0x21, 0x13, 0xcc, 0x73, 0xb7, 0x40, 0xb1, 0x8b, 0x02, 0xc3, 0x34, 0x02, 0xa3, 0x34, 0x4d, 0xe0, - 0xc9, 0xd9, 0x3e, 0x7f, 0x9b, 0x49, 0xb2, 0x93, 0x8d, 0x56, 0x8a, 0x01, 0x36, 0x23, 0x06, 0x18, - 0x5c, 0x03, 0x27, 0x1e, 0x20, 0xa4, 0xbb, 0x8e, 0x63, 0xe9, 0x86, 0x69, 0x7a, 0xc8, 0xf7, 0x4b, - 0xb3, 0x04, 0xc7, 0xc2, 0x03, 0x84, 0x9a, 0x8e, 0x63, 0xdd, 0xa4, 0xa3, 0xf2, 0x4f, 0x25, 0x70, - 0x96, 0x1c, 0xc1, 0x3b, 0x86, 0x85, 0x4d, 0x23, 0x70, 0x3c, 0xbe, 0x7b, 0x28, 0xc1, 0xe3, 0xe4, - 0xeb, 0xe0, 0x04, 0x07, 0x18, 0xe9, 0x23, 0xce, 0xa8, 0xc3, 0x2f, 0x0f, 0x2a, 0x0b, 0x03, 0xa3, - 0x6b, 0x6d, 0xca, 0x6c, 0x42, 0xd6, 0x5e, 0xe2, 0xb2, 0x6c, 0x13, 0x11, 0x6f, 0x41, 0xc4, 0xbb, - 0x59, 0xfc, 0xc9, 0x87, 0x95, 0xa9, 0x7f, 0x7c, 0x58, 0x99, 0x92, 0xef, 0x02, 0x79, 0x1c, 0x1c, - 0x16, 0x18, 0xaf, 0x83, 0x13, 0x91, 0xc2, 0x04, 0x1e, 0xed, 0xa5, 0x76, 0x4c, 0x3e, 0xdd, 0xc0, - 0x66, 0x0c, 0x5d, 0xcc, 0xc0, 0x74, 0x85, 0xe9, 0x06, 0x0a, 0x9b, 0x7c, 0x25, 0x03, 0x93, 0x70, - 0x86, 0x06, 0xa6, 0x3b, 0x7c, 0xc4, 0xb9, 0xf2, 0xb7, 0xc0, 0xeb, 0x44, 0xe1, 0x4d, 0xcb, 0x6a, - 0x1a, 0xd8, 0xf3, 0xdf, 0x31, 0xac, 0xd0, 0x67, 0xe1, 0x74, 0x3d, 0xba, 0x58, 0xb9, 0x53, 0xd3, - 0xfb, 0x12, 0xb8, 0x90, 0x47, 0x1d, 0xc3, 0xf9, 0x7d, 0xf0, 0x7f, 0xae, 0x81, 0x3d, 0xbd, 0x6f, - 0x58, 0x61, 0x8e, 0x27, 0x58, 0xd9, 0x65, 0xdd, 0x18, 0x0d, 0xec, 0x50, 0x21, 0xd5, 0x17, 0xaa, - 0x8b, 0x0c, 0xb7, 0xcd, 0x48, 0xef, 0x82, 0x9b, 0x10, 0x91, 0xff, 0x29, 0x81, 0xb3, 0x13, 0x57, - 0xc1, 0xad, 0xcc, 0xf0, 0x3c, 0xf5, 0xe5, 0x41, 0x65, 0x89, 0x9e, 0x9e, 0x28, 0x91, 0x12, 0xa7, - 0x5b, 0x29, 0x51, 0x50, 0x10, 0xf5, 0x88, 0x12, 0x29, 0xe1, 0xf0, 0x26, 0x38, 0x16, 0x49, 0xed, - 0xa2, 0x01, 0x49, 0x01, 0xf3, 0xb5, 0x15, 0x65, 0xf8, 0xc2, 0x29, 0xf4, 0x85, 0x53, 0x9a, 0xbd, - 0x96, 0x85, 0xdb, 0x77, 0xd0, 0x40, 0x8b, 0xce, 0xe5, 0x0e, 0x1a, 0xc8, 0x8b, 0x00, 0x92, 0x43, - 0x68, 0x1a, 0x9e, 0xd1, 0xe5, 0xd9, 0x5a, 0x7e, 0x1b, 0xfc, 0x7f, 0x62, 0x94, 0x9d, 0xc1, 0x35, - 0x30, 0xeb, 0x92, 0x11, 0xf6, 0x4c, 0x94, 0xd2, 0x1c, 0x1f, 0xce, 0xb3, 0x3c, 0xc2, 0xa4, 0xe5, - 0x9b, 0x60, 0x35, 0x91, 0x7c, 0xa3, 0x88, 0xcc, 0xff, 0x90, 0xfd, 0x67, 0x1a, 0x9c, 0xc9, 0xd0, - 0x11, 0xfd, 0xfa, 0xaa, 0xc9, 0x43, 0x74, 0x66, 0xe1, 0x90, 0xce, 0x84, 0xe7, 0xc1, 0x42, 0xa4, - 0xc0, 0x75, 0x1e, 0x22, 0x8f, 0x9c, 0xc7, 0x11, 0xed, 0x38, 0x1f, 0x6d, 0x86, 0x83, 0xf0, 0x0e, - 0x98, 0x37, 0x91, 0xdf, 0xf6, 0xb0, 0x4b, 0x1e, 0x0f, 0x9a, 0x9d, 0xcf, 0xf1, 0xc7, 0x83, 0x17, - 0x0b, 0xfc, 0xe5, 0xb8, 0x35, 0x14, 0x65, 0x6e, 0x8d, 0xaf, 0x86, 0xdf, 0x03, 0xcb, 0x91, 0xcd, - 0x8e, 0x8b, 0xbc, 0xd0, 0x11, 0x91, 0xf1, 0x33, 0xc4, 0xf8, 0xb3, 0x4f, 0x1e, 0x57, 0x4f, 0x33, - 0xed, 0x91, 0xb3, 0x98, 0xd1, 0xf7, 0x02, 0x0f, 0xdb, 0x1d, 0x6d, 0x89, 0xeb, 0xb8, 0xcb, 0x54, - 0x70, 0x9f, 0xbc, 0x0c, 0x66, 0x7f, 0x60, 0x60, 0x0b, 0x99, 0x24, 0xab, 0x17, 0x35, 0xf6, 0x05, - 0x37, 0xc1, 0x6c, 0x58, 0x30, 0xf4, 0xfc, 0xd2, 0x51, 0xf2, 0xa0, 0xcb, 0x59, 0xf0, 0xeb, 0x8e, - 0x6d, 0xde, 0x23, 0x92, 0x1a, 0x5b, 0x01, 0xef, 0x83, 0xc8, 0xf5, 0x7a, 0xe0, 0xec, 0x22, 0xdb, - 0x2f, 0x15, 0x09, 0xd0, 0x8b, 0xa1, 0x79, 0x7f, 0x39, 0xa8, 0x9c, 0xa4, 0xba, 0x7c, 0x73, 0x57, - 0xc1, 0x8e, 0xda, 0x35, 0x82, 0x1d, 0x65, 0xdb, 0x0e, 0x9e, 0x3c, 0xae, 0x02, 0xb6, 0xc9, 0xb6, - 0x1d, 0x68, 0x0b, 0x5c, 0xc7, 0x7d, 0xa2, 0x22, 0x74, 0x7e, 0xa4, 0x95, 0x3a, 0x7f, 0x8e, 0x3a, - 0x9f, 0x8f, 0x52, 0xe7, 0x5f, 0x03, 0x4b, 0x7d, 0xea, 0x03, 0xe4, 0xeb, 0xed, 0x9e, 0xe7, 0x85, - 0xaf, 0x2a, 0x72, 0x9d, 0xf6, 0x4e, 0x09, 0x10, 0x0b, 0x4f, 0x46, 0xd3, 0x0d, 0x3a, 0xfb, 0x56, - 0x38, 0x29, 0xf7, 0x40, 0x25, 0x33, 0x86, 0xd9, 0xf5, 0xd0, 0x00, 0xe8, 0x47, 0xa3, 0x2c, 0x37, - 0xd5, 0x46, 0xaf, 0xc8, 0xa4, 0x30, 0xd6, 0x62, 0x5a, 0x64, 0x99, 0x85, 0x7d, 0xdd, 0x72, 0xda, - 0xbb, 0xfe, 0x77, 0xed, 0x00, 0x5b, 0xdf, 0x46, 0x8f, 0x28, 0x26, 0x7e, 0x5b, 0xdf, 0x65, 0xef, - 0x4e, 0xba, 0x0c, 0x03, 0x77, 0x15, 0x2c, 0xb5, 0xc8, 0xbc, 0xde, 0x0b, 0x05, 0x74, 0x1b, 0x3d, - 0xe2, 0x76, 0xd3, 0xdb, 0xb6, 0xd8, 0x4a, 0x59, 0x2e, 0xdf, 0x64, 0x8f, 0x48, 0x23, 0xba, 0x8a, - 0x5b, 0x9e, 0xd3, 0x6d, 0xb0, 0xda, 0x83, 0x5f, 0xdf, 0x44, 0x7d, 0x22, 0x25, 0xeb, 0x13, 0x79, - 0x0b, 0x9c, 0x1b, 0xab, 0x82, 0x01, 0x9c, 0x98, 0x02, 0xae, 0x83, 0xe5, 0xd1, 0x12, 0x2e, 0x77, - 0x02, 0xf9, 0x57, 0x21, 0xad, 0x3e, 0xcd, 0xbd, 0x7b, 0xa2, 0x3a, 0x2b, 0x24, 0xab, 0xb3, 0x73, - 0xe0, 0xb8, 0xf3, 0xd0, 0x8e, 0xe5, 0x1c, 0x5a, 0x88, 0x1d, 0x23, 0x83, 0xfc, 0x22, 0x45, 0x55, - 0xda, 0x74, 0x56, 0x95, 0x36, 0xf3, 0xdc, 0x55, 0xda, 0x3d, 0x30, 0x8f, 0x6d, 0x1c, 0xe8, 0x2c, - 0x39, 0xcf, 0x12, 0x45, 0xb5, 0x6c, 0x45, 0xdb, 0x36, 0x0e, 0xb0, 0x61, 0xe1, 0x1f, 0x92, 0x12, - 0x94, 0xa4, 0x6c, 0x14, 0x20, 0xcf, 0xd7, 0x40, 0xa8, 0x86, 0xa6, 0xf0, 0xe4, 0x99, 0x1e, 0x15, - 0x6a, 0xce, 0xb4, 0xb2, 0xaf, 0x98, 0x5a, 0xf6, 0xd5, 0x85, 0x7b, 0xc3, 0x88, 0xc4, 0x7d, 0xdc, - 0x45, 0xb9, 0xcf, 0x6e, 0x57, 0xc8, 0xfd, 0x09, 0x1d, 0xec, 0x00, 0x6f, 0x03, 0xce, 0x47, 0xf4, - 0x90, 0x63, 0xb2, 0x17, 0xaa, 0xac, 0x50, 0x02, 0xaa, 0x70, 0x02, 0xaa, 0xdc, 0xe7, 0x04, 0xb4, - 0x5e, 0x0c, 0xbd, 0xf8, 0xc1, 0x17, 0x15, 0x49, 0x9b, 0xef, 0x0c, 0x15, 0xca, 0x0d, 0x61, 0xb3, - 0x2d, 0x84, 0xfc, 0x26, 0xf2, 0xc8, 0xe5, 0xca, 0x8d, 0xf8, 0xf7, 0xbc, 0x16, 0x4c, 0xd7, 0xc2, - 0x30, 0xbb, 0x20, 0xf4, 0x96, 0xaf, 0xbb, 0xc8, 0xd3, 0xc9, 0xed, 0x63, 0xa8, 0x97, 0x13, 0x44, - 0x82, 0x67, 0xd2, 0x86, 0x83, 0xed, 0xba, 0x1a, 0x82, 0xfe, 0xf7, 0x41, 0xe5, 0xb5, 0x0e, 0x0e, - 0x76, 0x7a, 0x2d, 0xa5, 0xed, 0x74, 0x19, 0x47, 0x65, 0x7f, 0xaa, 0xbe, 0xb9, 0xab, 0x06, 0x03, - 0x17, 0xf9, 0x64, 0x81, 0x76, 0xec, 0x41, 0x6c, 0xe7, 0x10, 0x38, 0xf6, 0x75, 0xa7, 0x8f, 0x3c, - 0x0f, 0x9b, 0x88, 0x04, 0x72, 0x51, 0x03, 0xd8, 0xbf, 0xcb, 0x46, 0xe4, 0x3e, 0xb8, 0xc4, 0x8b, - 0xb2, 0x34, 0xe8, 0x5c, 0x2e, 0x7a, 0xb8, 0x93, 0x04, 0x4d, 0x7a, 0x6e, 0x82, 0xf6, 0x89, 0x04, - 0xaa, 0x39, 0x37, 0x8e, 0xb2, 0xed, 0x1c, 0xb7, 0x83, 0x27, 0x5b, 0x25, 0x3b, 0xe4, 0xd3, 0x74, - 0xb1, 0x7b, 0x34, 0x54, 0xf3, 0xe2, 0x58, 0xdd, 0xde, 0x68, 0x10, 0x85, 0x97, 0xa2, 0x61, 0x19, - 0xb8, 0x9b, 0x37, 0x88, 0xe0, 0x35, 0x30, 0x67, 0x22, 0xd7, 0xf1, 0x71, 0xe0, 0x78, 0xac, 0x3a, - 0x2c, 0x3d, 0x79, 0x5c, 0x5d, 0x64, 0x78, 0x92, 0x2f, 0xf8, 0x50, 0x54, 0xfe, 0x71, 0x4a, 0xf0, - 0xc5, 0x76, 0x67, 0xfe, 0x33, 0xc0, 0x4c, 0x3b, 0x1c, 0x60, 0xbe, 0x1b, 0x13, 0x73, 0x97, 0x43, - 0x37, 0xfd, 0xfa, 0x8b, 0xca, 0x5a, 0xce, 0x98, 0xf3, 0x35, 0xaa, 0x59, 0xfe, 0x8d, 0x04, 0x16, - 0x6e, 0x71, 0x58, 0x64, 0xf7, 0xa4, 0x4d, 0x52, 0x6e, 0x9b, 0x86, 0x68, 0x0b, 0xff, 0x33, 0xb4, - 0xef, 0x8f, 0x73, 0x5b, 0xee, 0x4a, 0xf5, 0x85, 0xb5, 0x2c, 0x7e, 0x27, 0x09, 0x4f, 0xaf, 0x00, - 0x87, 0x1d, 0xe3, 0x0d, 0x30, 0x4b, 0xe0, 0xf3, 0x3b, 0x70, 0x66, 0xf4, 0x0e, 0x24, 0x8f, 0x80, - 0xd7, 0xe6, 0x74, 0xd5, 0x0b, 0x0b, 0xf9, 0xda, 0x47, 0x2f, 0x83, 0x19, 0x82, 0x17, 0xfe, 0x56, - 0x02, 0x8b, 0x69, 0xf9, 0x1a, 0x56, 0x27, 0x14, 0x43, 0xc9, 0xee, 0x56, 0x59, 0xc9, 0x2b, 0x4e, - 0xd1, 0xc8, 0x57, 0x7f, 0xf4, 0xd9, 0xdf, 0x7e, 0x56, 0x50, 0x61, 0x55, 0x4d, 0x36, 0x0d, 0xa3, - 0xf3, 0x62, 0x69, 0x5e, 0xdd, 0x8b, 0x9d, 0xe0, 0x3e, 0xfc, 0xa5, 0xc4, 0xd8, 0x4e, 0xb2, 0x37, - 0x04, 0x2f, 0x4d, 0xd8, 0x3e, 0xd1, 0xe0, 0x2a, 0x57, 0x73, 0x4a, 0x33, 0xac, 0x0a, 0xc1, 0xba, - 0x06, 0x5f, 0xcd, 0xc2, 0x4a, 0xfb, 0x4c, 0xea, 0x1e, 0xa9, 0x13, 0xf6, 0xe1, 0xe7, 0xbc, 0xbd, - 0x96, 0xda, 0xae, 0x80, 0x1b, 0x19, 0xbb, 0x8f, 0xeb, 0xb5, 0x94, 0xaf, 0x1c, 0x6e, 0x11, 0x43, - 0x7e, 0x97, 0x20, 0xdf, 0x86, 0xb7, 0x05, 0xe4, 0x51, 0xd1, 0xaa, 0x27, 0x78, 0x6b, 0xd2, 0xd9, - 0xea, 0x9e, 0x48, 0xd0, 0xd2, 0x4c, 0x8b, 0x37, 0x2a, 0x26, 0x9b, 0x96, 0xd2, 0x65, 0x99, 0x6c, - 0x5a, 0x5a, 0x2f, 0x24, 0x87, 0x69, 0x09, 0xf4, 0xa2, 0x69, 0x22, 0x5f, 0xdf, 0x87, 0x9f, 0xf1, - 0x3b, 0x3c, 0xb6, 0xc7, 0x01, 0xbf, 0x96, 0x81, 0x36, 0x4f, 0xa3, 0xa5, 0x7c, 0xfd, 0xf9, 0x16, - 0x33, 0x93, 0x6b, 0xc4, 0xe4, 0x4b, 0xf0, 0x82, 0x60, 0x32, 0x33, 0x41, 0x77, 0x43, 0x15, 0xc2, - 0x85, 0x19, 0x80, 0xf9, 0x58, 0x77, 0x00, 0xbe, 0x92, 0x01, 0x20, 0xd1, 0x52, 0x28, 0x9f, 0x9f, - 0x20, 0xc5, 0xf0, 0x9c, 0x26, 0x78, 0x96, 0xe0, 0x49, 0x01, 0x0f, 0x2d, 0x6d, 0xe1, 0x47, 0x12, - 0x58, 0xca, 0xe0, 0x4f, 0xf0, 0x72, 0x6e, 0xaa, 0xc5, 0x31, 0xad, 0x1f, 0x62, 0x05, 0xc3, 0xf7, - 0x06, 0xc1, 0x57, 0x83, 0x97, 0xb3, 0xee, 0xed, 0x90, 0xbb, 0x09, 0x5e, 0x7b, 0x2c, 0x31, 0xfe, - 0x92, 0x46, 0xd3, 0x60, 0x16, 0x4f, 0x1c, 0xc3, 0xfb, 0xca, 0x1b, 0x87, 0x5a, 0x33, 0x21, 0xf1, - 0x64, 0x90, 0x43, 0xf8, 0x07, 0xb1, 0x73, 0x9e, 0xa4, 0x6f, 0xf0, 0xca, 0x04, 0x1f, 0xa6, 0x12, - 0xc6, 0xf2, 0xd5, 0x43, 0xae, 0xca, 0x9b, 0x35, 0xb1, 0xa9, 0xee, 0x45, 0xac, 0x65, 0x1f, 0xfe, - 0x4a, 0x62, 0xed, 0xad, 0x44, 0x16, 0x86, 0x17, 0xf3, 0xe4, 0x6a, 0x0e, 0xf5, 0x52, 0x3e, 0x61, - 0x86, 0x70, 0x83, 0x20, 0xac, 0xc2, 0x8b, 0x63, 0xf3, 0xba, 0x10, 0x1a, 0x7f, 0x94, 0x40, 0x29, - 0x8b, 0xe0, 0xc0, 0xf5, 0x7c, 0xaf, 0x60, 0x8c, 0x50, 0x95, 0x6b, 0x87, 0x59, 0xc2, 0x80, 0x6f, - 0x12, 0xe0, 0x57, 0x60, 0x6d, 0xc2, 0xe3, 0x49, 0xd8, 0x95, 0x80, 0xff, 0x4f, 0x92, 0x40, 0xcd, - 0xe3, 0xd5, 0x36, 0x9c, 0x84, 0x26, 0x85, 0x60, 0x65, 0x86, 0xf6, 0x38, 0x3a, 0x25, 0x5f, 0x27, - 0x26, 0x5c, 0x83, 0x57, 0xb2, 0x4c, 0x48, 0x92, 0x2d, 0xc1, 0x88, 0xa7, 0x12, 0x38, 0x9f, 0x8b, - 0x81, 0xc0, 0x1b, 0xd9, 0x19, 0x37, 0x0f, 0x67, 0x2a, 0xbf, 0xf9, 0xdc, 0xeb, 0xf3, 0x26, 0xa1, - 0xa4, 0xa1, 0xfa, 0x90, 0xe0, 0x7c, 0x22, 0x81, 0xc5, 0xb4, 0x7a, 0x32, 0xcf, 0x21, 0x89, 0x04, - 0x26, 0xcf, 0x21, 0x8d, 0xd0, 0x0e, 0xf9, 0x0e, 0xc1, 0xfe, 0x16, 0x6c, 0xa8, 0x23, 0xff, 0xd9, - 0x8d, 0xc3, 0xa7, 0xad, 0x05, 0x52, 0xa3, 0x8a, 0xcf, 0x6c, 0x44, 0x0a, 0xf6, 0xe1, 0x9f, 0x25, - 0x70, 0x32, 0xb5, 0x3c, 0x86, 0x87, 0xc1, 0xe6, 0x4f, 0xaa, 0x1a, 0xc6, 0x56, 0xe0, 0xf2, 0x37, - 0x88, 0x45, 0x9b, 0xf0, 0x8d, 0xdc, 0x16, 0x09, 0x4f, 0x43, 0x7d, 0xfb, 0xe3, 0xa7, 0xab, 0xd2, - 0xa7, 0x4f, 0x57, 0xa5, 0xbf, 0x3e, 0x5d, 0x95, 0x3e, 0x78, 0xb6, 0x3a, 0xf5, 0xe9, 0xb3, 0xd5, - 0xa9, 0xcf, 0x9f, 0xad, 0x4e, 0xbd, 0xab, 0xc6, 0x48, 0x8c, 0x61, 0x59, 0xd8, 0x6e, 0xe1, 0xc0, - 0xa7, 0xfb, 0x3c, 0x12, 0xb6, 0x23, 0x8c, 0xa6, 0x35, 0x4b, 0x1a, 0x1d, 0x1b, 0xff, 0x0d, 0x00, - 0x00, 0xff, 0xff, 0xb1, 0xa1, 0x45, 0xf4, 0xdd, 0x1f, 0x00, 0x00, + // 2327 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x59, 0x4d, 0x6c, 0x1c, 0x49, + 0x15, 0x76, 0x4f, 0x6c, 0x67, 0x5c, 0x76, 0xbc, 0xa1, 0xb0, 0xf1, 0x78, 0x62, 0x7b, 0x92, 0xce, + 0x66, 0xd7, 0x9b, 0xc4, 0xdd, 0xf1, 0x38, 0xb1, 0x56, 0x26, 0x24, 0xd8, 0x4e, 0x1c, 0xac, 0xec, + 0x92, 0xa1, 0x13, 0xf6, 0xb0, 0x12, 0xb4, 0xca, 0xd3, 0x95, 0x71, 0x31, 0x3d, 0xdd, 0xbd, 0xdd, + 0x3d, 0x93, 0x18, 0xcb, 0x17, 0x0e, 0x88, 0xc3, 0x22, 0xad, 0x04, 0x12, 0x48, 0x5c, 0x56, 0x88, + 0x13, 0x3f, 0x12, 0x87, 0x08, 0x21, 0x24, 0x2e, 0x88, 0xc3, 0x1e, 0xf6, 0xb0, 0xca, 0x5e, 0x56, + 0x1c, 0xbc, 0x28, 0x41, 0x08, 0x89, 0xdb, 0x0a, 0x2e, 0x1c, 0x10, 0xaa, 0xea, 0xaa, 0x9e, 0xee, + 0x9e, 0xee, 0x99, 0xb6, 0x37, 0x9c, 0x3c, 0x5d, 0xf5, 0xea, 0xd5, 0xf7, 0x5e, 0xbd, 0x7a, 0xf5, + 0xbe, 0x67, 0x30, 0xd7, 0x41, 0xc8, 0x53, 0x1d, 0xd7, 0xee, 0x10, 0x03, 0xbb, 0x6a, 0x67, 0x59, + 0x7d, 0xa7, 0x8d, 0xdd, 0x3d, 0xc5, 0x71, 0x6d, 0xdf, 0x86, 0xa7, 0xe9, 0xac, 0x22, 0x66, 0x95, + 0xce, 0x72, 0x79, 0xae, 0x61, 0xdb, 0x0d, 0x13, 0xab, 0xc8, 0x21, 0x2a, 0xb2, 0x2c, 0xdb, 0x47, + 0x3e, 0xb1, 0x2d, 0x2f, 0x90, 0x2f, 0x4f, 0x35, 0xec, 0x86, 0xcd, 0x7e, 0xaa, 0xf4, 0x17, 0x1f, + 0xad, 0xf0, 0x35, 0xec, 0x6b, 0xa7, 0xfd, 0x50, 0xf5, 0x49, 0x0b, 0x7b, 0x3e, 0x6a, 0x39, 0x5c, + 0x60, 0x21, 0x29, 0x60, 0xb4, 0x5d, 0xa6, 0x57, 0xcc, 0xf7, 0x80, 0x6c, 0x60, 0x0b, 0x7b, 0x44, + 0x6c, 0x5b, 0xe9, 0x99, 0x0f, 0x21, 0x07, 0x02, 0xf3, 0x4c, 0xa0, 0xb3, 0xac, 0x7a, 0xbb, 0xc8, + 0xc5, 0x86, 0x5e, 0xb7, 0x2d, 0xaf, 0xdd, 0x0a, 0xa7, 0xa1, 0x98, 0x7e, 0x44, 0x5c, 0xcc, 0xc7, + 0xe6, 0x7c, 0x6c, 0x19, 0xd8, 0x6d, 0x11, 0xcb, 0x57, 0xeb, 0xee, 0x9e, 0xe3, 0xdb, 0x6a, 0x13, + 0xef, 0x89, 0x1d, 0x67, 0xeb, 0xb6, 0xd7, 0xb2, 0x3d, 0x3d, 0xb0, 0x35, 0xf8, 0x10, 0x60, 0x83, + 0x2f, 0x75, 0x07, 0x79, 0x58, 0xed, 0x2c, 0xef, 0x60, 0x1f, 0x2d, 0xab, 0x75, 0x9b, 0x08, 0x63, + 0x5e, 0xe6, 0xf3, 0x9e, 0x8f, 0x9a, 0xc4, 0x6a, 0x84, 0x22, 0xfc, 0x9b, 0x4b, 0x5d, 0x8c, 0x6a, + 0x61, 0x47, 0x12, 0x0a, 0x3a, 0xa8, 0x41, 0xac, 0x88, 0x7b, 0xe4, 0x1b, 0xe0, 0xcc, 0x37, 0xa8, + 0xc4, 0x26, 0xb7, 0xea, 0x4e, 0xe0, 0x1c, 0x0d, 0xbf, 0xd3, 0xc6, 0x9e, 0x0f, 0x2b, 0x60, 0x5c, + 0xd8, 0xab, 0x13, 0xa3, 0x24, 0x9d, 0x95, 0x16, 0x87, 0x35, 0x20, 0x86, 0xb6, 0x0d, 0x79, 0x17, + 0xcc, 0xa5, 0xaf, 0xf7, 0x1c, 0xdb, 0xf2, 0x30, 0xfc, 0x1a, 0x38, 0xc5, 0xfd, 0xad, 0x7b, 0x3e, + 0xf2, 0x31, 0x53, 0x31, 0x5e, 0x9d, 0x57, 0x58, 0x74, 0x74, 0x96, 0x95, 0xc4, 0xc2, 0xfb, 0x54, + 0x68, 0x63, 0xf8, 0x83, 0xc3, 0xca, 0x90, 0x36, 0xd1, 0x88, 0x8c, 0xc9, 0x3f, 0x93, 0x40, 0x39, + 0xb6, 0xd5, 0xe6, 0x2e, 0x22, 0x56, 0x88, 0xf4, 0x1a, 0x18, 0x71, 0x76, 0x91, 0x17, 0x6c, 0x30, + 0x59, 0xad, 0x28, 0xc9, 0xf0, 0x0b, 0x77, 0xaa, 0x51, 0x31, 0x2d, 0x90, 0x86, 0x5b, 0x00, 0x74, + 0x7d, 0x52, 0x2a, 0x30, 0x70, 0xaf, 0x28, 0xfc, 0x50, 0xa8, 0x03, 0x95, 0x20, 0xa6, 0xb9, 0x03, + 0x95, 0x1a, 0x6a, 0x60, 0xbe, 0xa5, 0x16, 0x59, 0x29, 0xff, 0x44, 0x4a, 0x38, 0x52, 0xa0, 0xe3, + 0x7e, 0x50, 0xc1, 0x68, 0x9d, 0x8d, 0x94, 0xa4, 0xb3, 0x27, 0x16, 0xc7, 0xab, 0x33, 0x29, 0xf8, + 0xe8, 0xbc, 0xc6, 0xc5, 0xe0, 0x9d, 0x14, 0x60, 0xaf, 0x0e, 0x04, 0x16, 0xec, 0x16, 0x43, 0xf6, + 0x77, 0x09, 0x8c, 0x30, 0xd5, 0x70, 0x16, 0x14, 0x99, 0x72, 0x71, 0x92, 0x63, 0xda, 0x49, 0xf6, + 0xbd, 0x6d, 0xc0, 0x33, 0x60, 0xac, 0x6e, 0x12, 0x6c, 0xf9, 0x74, 0xae, 0xc0, 0xe6, 0x8a, 0xc1, + 0xc0, 0xb6, 0x01, 0xa7, 0x84, 0x6b, 0x4f, 0xb0, 0x09, 0xee, 0xb9, 0x5b, 0xa0, 0xd8, 0xc2, 0x3e, + 0x32, 0x90, 0x8f, 0x4a, 0xc3, 0x0c, 0x9e, 0x9c, 0xed, 0xf3, 0x37, 0xb9, 0x24, 0x3f, 0xd9, 0x70, + 0x65, 0x32, 0xc0, 0x46, 0x92, 0x01, 0x06, 0x17, 0xc1, 0xe9, 0x87, 0x18, 0xeb, 0x8e, 0x6d, 0x9b, + 0x3a, 0x32, 0x0c, 0x17, 0x7b, 0x5e, 0x69, 0x94, 0xe1, 0x98, 0x7c, 0x88, 0x71, 0xcd, 0xb6, 0xcd, + 0xf5, 0x60, 0x54, 0xfe, 0xa1, 0x04, 0xce, 0xb1, 0x23, 0x78, 0x0b, 0x99, 0xc4, 0x40, 0xbe, 0xed, + 0x8a, 0xdd, 0xa9, 0x84, 0x88, 0x93, 0xaf, 0x80, 0xd3, 0x02, 0x60, 0xa8, 0x8f, 0x39, 0x63, 0x03, + 0x7e, 0x76, 0x58, 0x99, 0xdc, 0x43, 0x2d, 0x73, 0x4d, 0xe6, 0x13, 0xb2, 0xf6, 0x92, 0x90, 0xe5, + 0x9b, 0x24, 0xf1, 0x16, 0x92, 0x78, 0xd7, 0x8a, 0x3f, 0x78, 0xbf, 0x32, 0xf4, 0x8f, 0xf7, 0x2b, + 0x43, 0xf2, 0x3d, 0x20, 0xf7, 0x83, 0xc3, 0x03, 0xe3, 0x35, 0x70, 0x3a, 0x54, 0x18, 0xc3, 0xa3, + 0xbd, 0x54, 0x8f, 0xc8, 0xa7, 0x1b, 0x58, 0x8b, 0xa0, 0x8b, 0x18, 0x98, 0xae, 0x30, 0xdd, 0xc0, + 0xc4, 0x26, 0x9f, 0xcb, 0xc0, 0x38, 0x9c, 0xae, 0x81, 0xe9, 0x0e, 0xef, 0x71, 0xae, 0xfc, 0x06, + 0x78, 0x8d, 0x29, 0x5c, 0x37, 0xcd, 0x1a, 0x22, 0xae, 0xf7, 0x16, 0x32, 0xa9, 0xcf, 0xe8, 0xf4, + 0x46, 0x78, 0xb1, 0x72, 0xa7, 0xa6, 0x77, 0x25, 0x70, 0x31, 0x8f, 0x3a, 0x8e, 0xf3, 0xdb, 0xe0, + 0x0b, 0x0e, 0x22, 0xae, 0xde, 0x41, 0x26, 0xcd, 0xf1, 0x0c, 0x2b, 0xbf, 0xac, 0x2b, 0xbd, 0x81, + 0x4d, 0x15, 0x06, 0xfa, 0xa8, 0xba, 0xd0, 0x70, 0xcb, 0x08, 0xf5, 0x4e, 0x3a, 0x31, 0x11, 0xf9, + 0x5f, 0x12, 0x38, 0x37, 0x70, 0x15, 0xdc, 0xca, 0x0c, 0xcf, 0x33, 0x9f, 0x1d, 0x56, 0x66, 0x82, + 0xd3, 0x4b, 0x4a, 0xa4, 0xc4, 0xe9, 0x56, 0x4a, 0x14, 0x14, 0x92, 0x7a, 0x92, 0x12, 0x29, 0xe1, + 0x70, 0x13, 0x4c, 0x84, 0x52, 0x4d, 0xbc, 0xc7, 0x52, 0xc0, 0x78, 0x75, 0x4e, 0xe9, 0xbe, 0x70, + 0x4a, 0xf0, 0xc2, 0x29, 0xb5, 0xf6, 0x8e, 0x49, 0xea, 0x77, 0xf1, 0x9e, 0x16, 0x9e, 0xcb, 0x5d, + 0xbc, 0x27, 0x4f, 0x01, 0xc8, 0x0e, 0xa1, 0x86, 0x5c, 0xd4, 0x12, 0xd9, 0x5a, 0x7e, 0x13, 0x7c, + 0x31, 0x36, 0xca, 0xcf, 0x60, 0x15, 0x8c, 0x3a, 0x6c, 0x84, 0x3f, 0x13, 0xa5, 0x34, 0xc7, 0xd3, + 0x79, 0x9e, 0x47, 0xb8, 0xb4, 0xbc, 0x0e, 0x16, 0x62, 0xc9, 0x37, 0x8c, 0xc8, 0xfc, 0x0f, 0xd9, + 0x7f, 0x87, 0xc1, 0xd9, 0x0c, 0x1d, 0xe1, 0xaf, 0xcf, 0x9b, 0x3c, 0x92, 0xce, 0x2c, 0x1c, 0xd1, + 0x99, 0xf0, 0x02, 0x98, 0x0c, 0x15, 0x38, 0xf6, 0x23, 0xec, 0xb2, 0xf3, 0x38, 0xa1, 0x9d, 0x12, + 0xa3, 0x35, 0x3a, 0x08, 0xef, 0x82, 0x71, 0x03, 0x7b, 0x75, 0x97, 0x38, 0xec, 0xf1, 0x08, 0xb2, + 0xf3, 0x79, 0xf1, 0x78, 0x88, 0x62, 0x41, 0xbc, 0x1c, 0xb7, 0xba, 0xa2, 0xdc, 0xad, 0xd1, 0xd5, + 0xf0, 0x5b, 0x60, 0x36, 0xb4, 0xd9, 0x76, 0xb0, 0x4b, 0x1d, 0x11, 0x1a, 0x3f, 0xc2, 0x8c, 0x3f, + 0xf7, 0xf4, 0xc9, 0xd2, 0x3c, 0xd7, 0x1e, 0x3a, 0x8b, 0x1b, 0x7d, 0xdf, 0x77, 0x89, 0xd5, 0xd0, + 0x66, 0x84, 0x8e, 0x7b, 0x5c, 0x85, 0xf0, 0xc9, 0x97, 0xc0, 0xe8, 0x77, 0x10, 0x31, 0xb1, 0xc1, + 0xb2, 0x7a, 0x51, 0xe3, 0x5f, 0x70, 0x0d, 0x8c, 0xd2, 0x82, 0xa1, 0xed, 0x95, 0x4e, 0xb2, 0x07, + 0x5d, 0xce, 0x82, 0xbf, 0x61, 0x5b, 0xc6, 0x7d, 0x26, 0xa9, 0xf1, 0x15, 0xf0, 0x01, 0x08, 0x5d, + 0xaf, 0xfb, 0x76, 0x13, 0x5b, 0x5e, 0xa9, 0xc8, 0x80, 0x5e, 0xa2, 0xe6, 0xfd, 0xe5, 0xb0, 0x32, + 0x1d, 0xe8, 0xf2, 0x8c, 0xa6, 0x42, 0x6c, 0xb5, 0x85, 0xfc, 0x5d, 0x65, 0xdb, 0xf2, 0x9f, 0x3e, + 0x59, 0x02, 0x7c, 0x93, 0x6d, 0xcb, 0xd7, 0x26, 0x85, 0x8e, 0x07, 0x4c, 0x05, 0x75, 0x7e, 0xa8, + 0x35, 0x70, 0xfe, 0x58, 0xe0, 0x7c, 0x31, 0x1a, 0x38, 0x7f, 0x15, 0xcc, 0x74, 0x02, 0x1f, 0x60, + 0x4f, 0xaf, 0xb7, 0x5d, 0x97, 0xbe, 0xaa, 0xd8, 0xb1, 0xeb, 0xbb, 0x25, 0xc0, 0x2c, 0x9c, 0x0e, + 0xa7, 0x37, 0x83, 0xd9, 0xdb, 0x74, 0x52, 0x6e, 0x83, 0x4a, 0x66, 0x0c, 0xf3, 0xeb, 0xa1, 0x01, + 0xd0, 0x09, 0x47, 0x79, 0x6e, 0xaa, 0xf6, 0x5e, 0x91, 0x41, 0x61, 0xac, 0x45, 0xb4, 0xc8, 0x32, + 0x0f, 0xfb, 0x0d, 0xd3, 0xae, 0x37, 0xbd, 0x6f, 0x5a, 0x3e, 0x31, 0xbf, 0x8e, 0x1f, 0x07, 0x98, + 0xc4, 0x6d, 0x7d, 0x9b, 0xbf, 0x3b, 0xe9, 0x32, 0x1c, 0xdc, 0x35, 0x30, 0xb3, 0xc3, 0xe6, 0xf5, + 0x36, 0x15, 0xd0, 0x2d, 0xfc, 0x58, 0xd8, 0x1d, 0xdc, 0xb6, 0xa9, 0x9d, 0x94, 0xe5, 0xf2, 0x3a, + 0x7f, 0x44, 0x36, 0xc3, 0xab, 0xb8, 0xe5, 0xda, 0xad, 0x4d, 0x5e, 0x7b, 0x88, 0xeb, 0x1b, 0xab, + 0x4f, 0xa4, 0x78, 0x7d, 0x22, 0x6f, 0x81, 0xf3, 0x7d, 0x55, 0x70, 0x80, 0x03, 0x53, 0xc0, 0x75, + 0x30, 0xdb, 0x5b, 0xc2, 0xe5, 0x4e, 0x20, 0xff, 0x2e, 0xa4, 0xd5, 0xa7, 0xb9, 0x77, 0x8f, 0x55, + 0x67, 0x85, 0x78, 0x75, 0x76, 0x1e, 0x9c, 0xb2, 0x1f, 0x59, 0x91, 0x9c, 0x13, 0x14, 0x62, 0x13, + 0x6c, 0x50, 0x5c, 0xa4, 0xb0, 0x4a, 0x1b, 0xce, 0xaa, 0xd2, 0x46, 0x8e, 0x5d, 0xa5, 0xdd, 0x07, + 0xe3, 0xc4, 0x22, 0xbe, 0xce, 0x93, 0xf3, 0x28, 0x53, 0x54, 0xcd, 0x56, 0xb4, 0x6d, 0x11, 0x9f, + 0x20, 0x93, 0x7c, 0x97, 0x95, 0xa0, 0x2c, 0x65, 0x63, 0x1f, 0xbb, 0x9e, 0x06, 0xa8, 0x9a, 0x20, + 0x85, 0xc7, 0xcf, 0xf4, 0x64, 0xa2, 0xe6, 0x4c, 0x2b, 0xfb, 0x8a, 0xa9, 0x65, 0xdf, 0x46, 0xe2, + 0xde, 0x70, 0x22, 0xf1, 0x80, 0xb4, 0x70, 0xee, 0xb3, 0x6b, 0x26, 0x72, 0x7f, 0x4c, 0x07, 0x3f, + 0xc0, 0x3b, 0x40, 0xf0, 0x11, 0x9d, 0x72, 0x50, 0xfe, 0x42, 0x95, 0x95, 0x80, 0x7f, 0x2a, 0x82, + 0x7f, 0x2a, 0x0f, 0x04, 0x41, 0xdd, 0x28, 0x52, 0x2f, 0xbe, 0xf7, 0x69, 0x45, 0xd2, 0xc6, 0x1b, + 0x5d, 0x85, 0xf2, 0x66, 0x62, 0xb3, 0x2d, 0x8c, 0xbd, 0x1a, 0x76, 0xd9, 0xe5, 0xca, 0x8d, 0xf8, + 0x77, 0xa2, 0x16, 0x4c, 0xd7, 0xc2, 0x31, 0x3b, 0x80, 0x7a, 0xcb, 0xd3, 0x1d, 0xec, 0xea, 0xec, + 0xf6, 0x71, 0xd4, 0xb3, 0x31, 0x22, 0x21, 0x32, 0xe9, 0xa6, 0x4d, 0xac, 0x0d, 0x95, 0x82, 0xfe, + 0xcf, 0x61, 0xe5, 0xd5, 0x06, 0xf1, 0x77, 0xdb, 0x3b, 0x4a, 0xdd, 0x6e, 0x71, 0x8e, 0xca, 0xff, + 0x2c, 0x79, 0x46, 0x53, 0xf5, 0xf7, 0x1c, 0xec, 0xb1, 0x05, 0xda, 0xc4, 0xc3, 0xc8, 0xce, 0x14, + 0x38, 0xf1, 0x74, 0xbb, 0x83, 0x5d, 0x97, 0x18, 0x98, 0x05, 0x72, 0x51, 0x03, 0xc4, 0xbb, 0xc7, + 0x47, 0xe4, 0x0e, 0xb8, 0x2c, 0x8a, 0xb2, 0x34, 0xe8, 0x42, 0x2e, 0x7c, 0xb8, 0xe3, 0x04, 0x4d, + 0x3a, 0x36, 0x41, 0xfb, 0x50, 0x02, 0x4b, 0x39, 0x37, 0x0e, 0xb3, 0xed, 0x98, 0xb0, 0x43, 0x24, + 0x5b, 0x25, 0x3b, 0xe4, 0xd3, 0x74, 0xf1, 0x7b, 0xd4, 0x55, 0xf3, 0xe2, 0x58, 0xdd, 0x7e, 0x6f, + 0x10, 0xd1, 0x4b, 0xb1, 0x69, 0x22, 0xd2, 0xca, 0x1b, 0x44, 0x70, 0x15, 0x8c, 0x19, 0xd8, 0xb1, + 0x3d, 0xe2, 0xdb, 0x2e, 0xaf, 0x0e, 0x4b, 0x4f, 0x9f, 0x2c, 0x4d, 0x71, 0x3c, 0xf1, 0x17, 0xbc, + 0x2b, 0x2a, 0x7f, 0x3f, 0x25, 0xf8, 0x22, 0xbb, 0x73, 0xff, 0x21, 0x30, 0x52, 0xa7, 0x03, 0xdc, + 0x77, 0x7d, 0x62, 0xee, 0x0a, 0x75, 0xd3, 0x2f, 0x3f, 0xad, 0x2c, 0xe6, 0x8c, 0x39, 0x4f, 0x0b, + 0x34, 0xcb, 0xbf, 0x92, 0xc0, 0xe4, 0x2d, 0x01, 0x8b, 0xed, 0x1e, 0xb7, 0x49, 0xca, 0x6d, 0x53, + 0x17, 0x6d, 0xe1, 0xff, 0x86, 0xf6, 0xdd, 0x7e, 0x6e, 0xcb, 0x5d, 0xa9, 0xbe, 0xb0, 0x96, 0xc5, + 0x6f, 0xa4, 0xc4, 0xd3, 0x9b, 0x80, 0xc3, 0x8f, 0xf1, 0x06, 0x18, 0x65, 0xf0, 0xc5, 0x1d, 0x38, + 0xdb, 0x7b, 0x07, 0xe2, 0x47, 0x20, 0x6a, 0xf3, 0x60, 0xd5, 0x8b, 0x0b, 0xf9, 0x9b, 0x89, 0x56, + 0xd3, 0x1b, 0xa4, 0x43, 0xb3, 0x6a, 0xfe, 0x12, 0xff, 0xc7, 0x05, 0x30, 0x9f, 0xa1, 0xa1, 0xdb, + 0xad, 0x32, 0x91, 0xe7, 0xeb, 0xa8, 0xde, 0x3c, 0x46, 0x92, 0xa7, 0x4b, 0xd7, 0xeb, 0x4d, 0x3a, + 0x07, 0xb7, 0xc0, 0x44, 0xc3, 0x45, 0x75, 0x4c, 0x53, 0x2f, 0xb1, 0x0d, 0x6e, 0xf7, 0x6c, 0x8f, + 0xa2, 0x5b, 0xbc, 0x5b, 0x19, 0xe8, 0xf9, 0x69, 0xf0, 0x58, 0xd0, 0x85, 0x35, 0xb6, 0x0e, 0xde, + 0x06, 0xe3, 0x2e, 0x6e, 0xd9, 0x94, 0x94, 0x62, 0x1f, 0x71, 0xfa, 0x95, 0x0f, 0x0f, 0xe0, 0x0b, + 0x6f, 0xfb, 0x08, 0x96, 0x41, 0xd1, 0xc0, 0x0d, 0x17, 0x19, 0xd8, 0x60, 0xf5, 0x41, 0x51, 0x0b, + 0xbf, 0xab, 0xff, 0x9c, 0x01, 0x23, 0xcc, 0x2d, 0xf0, 0xd7, 0x12, 0x98, 0x4a, 0x7b, 0x07, 0xe1, + 0xd2, 0x80, 0x22, 0x33, 0xde, 0x35, 0x2c, 0x2b, 0x79, 0xc5, 0x03, 0xb7, 0xcb, 0xd7, 0xbe, 0xf7, + 0xf1, 0xdf, 0x7e, 0x54, 0x50, 0xe1, 0x92, 0x1a, 0x6f, 0xc6, 0x86, 0xc7, 0xc9, 0x9f, 0x4f, 0x75, + 0x3f, 0x72, 0xc0, 0x07, 0xf0, 0xe7, 0x12, 0x67, 0x91, 0xf1, 0x9e, 0x1b, 0xbc, 0x3c, 0x60, 0xfb, + 0x58, 0xe3, 0xb0, 0xbc, 0x94, 0x53, 0x9a, 0x63, 0x55, 0x18, 0xd6, 0x45, 0xf8, 0x4a, 0x16, 0xd6, + 0xa0, 0x7f, 0xa7, 0xee, 0xb3, 0xfa, 0xeb, 0x00, 0x7e, 0x22, 0xda, 0x96, 0xa9, 0x6d, 0x20, 0xb8, + 0x92, 0xb1, 0x7b, 0xbf, 0x1e, 0x56, 0xf9, 0xea, 0xd1, 0x16, 0x71, 0xe4, 0xf7, 0x18, 0xf2, 0x6d, + 0x78, 0x27, 0x81, 0x3c, 0x24, 0x03, 0x7a, 0xac, 0x1f, 0x10, 0x77, 0xb6, 0xba, 0x9f, 0x24, 0xbe, + 0x69, 0xa6, 0x45, 0x1b, 0x40, 0x83, 0x4d, 0x4b, 0xe9, 0x5e, 0x0d, 0x36, 0x2d, 0xad, 0xc7, 0x94, + 0xc3, 0xb4, 0x18, 0xfa, 0xa4, 0x69, 0xc9, 0x3e, 0xc8, 0x01, 0xfc, 0x58, 0xe4, 0xc6, 0xbe, 0xbd, + 0x23, 0xf8, 0xe5, 0x0c, 0xb4, 0x79, 0x1a, 0x58, 0xe5, 0xeb, 0xc7, 0x5b, 0xcc, 0x4d, 0xae, 0x32, + 0x93, 0x2f, 0xc3, 0x8b, 0x09, 0x93, 0xb9, 0x09, 0xba, 0x43, 0x55, 0x24, 0x2e, 0xcc, 0x1e, 0x18, + 0x8f, 0x74, 0x5d, 0xe0, 0xcb, 0x19, 0x00, 0x62, 0xad, 0x9a, 0xf2, 0x85, 0x01, 0x52, 0x1c, 0xcf, + 0x3c, 0xc3, 0x33, 0x03, 0xa7, 0x13, 0x78, 0x02, 0xca, 0x00, 0x7f, 0x2f, 0x81, 0x99, 0x0c, 0x5e, + 0x0a, 0xaf, 0xe4, 0xa6, 0xb0, 0x02, 0xd3, 0xf2, 0x11, 0x56, 0x70, 0x7c, 0xaf, 0x33, 0x7c, 0x55, + 0x78, 0x25, 0xeb, 0xde, 0x76, 0x39, 0x71, 0xc2, 0x6b, 0x4f, 0x24, 0xce, 0x0b, 0xd3, 0xe8, 0x2f, + 0xcc, 0xe2, 0xdf, 0x7d, 0xf8, 0x74, 0x79, 0xe5, 0x48, 0x6b, 0x06, 0x24, 0x9e, 0x0c, 0xd2, 0x0d, + 0xff, 0x90, 0xfc, 0x8f, 0x44, 0x9c, 0x16, 0xc3, 0xab, 0x03, 0x7c, 0x98, 0x4a, 0xc4, 0xcb, 0xd7, + 0x8e, 0xb8, 0x2a, 0x6f, 0xd6, 0x24, 0x86, 0xba, 0x1f, 0xb2, 0xc1, 0x03, 0xf8, 0x0b, 0x89, 0xb7, + 0x0d, 0x63, 0x59, 0x18, 0x5e, 0xca, 0x93, 0xab, 0x05, 0xd4, 0xcb, 0xf9, 0x84, 0x39, 0xc2, 0x15, + 0x86, 0x70, 0x09, 0x5e, 0xea, 0x9b, 0xd7, 0x13, 0xa1, 0xf1, 0x47, 0x09, 0x94, 0xb2, 0x88, 0x23, + 0x5c, 0xce, 0xf7, 0x0a, 0x46, 0x88, 0x6a, 0xb9, 0x7a, 0x94, 0x25, 0x1c, 0xf8, 0x1a, 0x03, 0x7e, + 0x15, 0x56, 0x07, 0x3c, 0x9e, 0xac, 0xa0, 0x49, 0xe0, 0xff, 0x93, 0x94, 0x68, 0x79, 0x44, 0x59, + 0x0c, 0x1c, 0x84, 0x26, 0x85, 0xb8, 0x66, 0x86, 0x76, 0x3f, 0x9a, 0x2a, 0x5f, 0x67, 0x26, 0xac, + 0xc2, 0xab, 0x59, 0x26, 0xc4, 0x49, 0x6c, 0xc2, 0x88, 0x67, 0x12, 0xb8, 0x90, 0x8b, 0xd9, 0xc1, + 0x1b, 0xd9, 0x19, 0x37, 0x0f, 0x17, 0x2d, 0xdf, 0x3c, 0xf6, 0xfa, 0xbc, 0x49, 0x28, 0x6e, 0xa8, + 0xde, 0x25, 0x8e, 0x1f, 0x4a, 0x60, 0x2a, 0xad, 0x4e, 0xcf, 0x73, 0x48, 0x49, 0x62, 0x98, 0xe7, + 0x90, 0x7a, 0xe8, 0x9c, 0x7c, 0x97, 0x61, 0xbf, 0x0d, 0x37, 0xd5, 0x9e, 0xff, 0x98, 0x47, 0xe1, + 0x07, 0x2d, 0x1b, 0x56, 0xfb, 0x27, 0x9f, 0xd9, 0x90, 0x6c, 0x1d, 0xc0, 0x3f, 0x4b, 0x60, 0x3a, + 0x95, 0x76, 0xc0, 0xa3, 0x60, 0xf3, 0x06, 0x55, 0x0d, 0x7d, 0x99, 0x8d, 0xfc, 0x55, 0x66, 0xd1, + 0x1a, 0x7c, 0x3d, 0xb7, 0x45, 0xc9, 0xa7, 0xe1, 0xb7, 0x12, 0x98, 0x4e, 0x65, 0x14, 0x70, 0x50, + 0x09, 0x9c, 0x20, 0x2f, 0x65, 0x35, 0xb7, 0x3c, 0x07, 0xbf, 0xca, 0xc0, 0x5f, 0x81, 0x4a, 0x56, + 0x28, 0x99, 0x7c, 0x45, 0x1c, 0xf2, 0xc6, 0xf6, 0x07, 0xcf, 0x16, 0xa4, 0x8f, 0x9e, 0x2d, 0x48, + 0x7f, 0x7d, 0xb6, 0x20, 0xbd, 0xf7, 0x7c, 0x61, 0xe8, 0xa3, 0xe7, 0x0b, 0x43, 0x9f, 0x3c, 0x5f, + 0x18, 0x7a, 0x5b, 0x8d, 0xf0, 0x59, 0x64, 0x9a, 0xc4, 0xda, 0x21, 0xbe, 0x17, 0x68, 0x7f, 0x9c, + 0xd8, 0x84, 0x91, 0xdb, 0x9d, 0x51, 0x46, 0x3f, 0x56, 0xfe, 0x17, 0x00, 0x00, 0xff, 0xff, 0xe0, + 0x53, 0x5d, 0x63, 0x08, 0x22, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -2038,6 +2162,8 @@ type QueryClient interface { QueryAllConsumerFeesPerBlockOverrides(ctx context.Context, in *QueryAllConsumerFeesPerBlockOverridesRequest, opts ...grpc.CallOption) (*QueryAllConsumerFeesPerBlockOverridesResponse, error) ConsumerFeePoolClaim(ctx context.Context, in *QueryConsumerFeePoolClaimRequest, opts ...grpc.CallOption) (*QueryConsumerFeePoolClaimResponse, error) ConsumerFeePoolClaims(ctx context.Context, in *QueryConsumerFeePoolClaimsRequest, opts ...grpc.CallOption) (*QueryConsumerFeePoolClaimsResponse, error) + // QueryConsumerLiveness returns the liveness state of a launched consumer. + QueryConsumerLiveness(ctx context.Context, in *QueryConsumerLivenessRequest, opts ...grpc.CallOption) (*QueryConsumerLivenessResponse, error) } type queryClient struct { @@ -2183,6 +2309,15 @@ func (c *queryClient) ConsumerFeePoolClaims(ctx context.Context, in *QueryConsum return out, nil } +func (c *queryClient) QueryConsumerLiveness(ctx context.Context, in *QueryConsumerLivenessRequest, opts ...grpc.CallOption) (*QueryConsumerLivenessResponse, error) { + out := new(QueryConsumerLivenessResponse) + err := c.cc.Invoke(ctx, "/vaas.provider.v1.Query/QueryConsumerLiveness", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // QueryServer is the server API for Query service. type QueryServer interface { // ConsumerGenesis queries the genesis state needed to start a consumer chain @@ -2227,6 +2362,8 @@ type QueryServer interface { QueryAllConsumerFeesPerBlockOverrides(context.Context, *QueryAllConsumerFeesPerBlockOverridesRequest) (*QueryAllConsumerFeesPerBlockOverridesResponse, error) ConsumerFeePoolClaim(context.Context, *QueryConsumerFeePoolClaimRequest) (*QueryConsumerFeePoolClaimResponse, error) ConsumerFeePoolClaims(context.Context, *QueryConsumerFeePoolClaimsRequest) (*QueryConsumerFeePoolClaimsResponse, error) + // QueryConsumerLiveness returns the liveness state of a launched consumer. + QueryConsumerLiveness(context.Context, *QueryConsumerLivenessRequest) (*QueryConsumerLivenessResponse, error) } // UnimplementedQueryServer can be embedded to have forward compatible implementations. @@ -2278,6 +2415,9 @@ func (*UnimplementedQueryServer) ConsumerFeePoolClaim(ctx context.Context, req * func (*UnimplementedQueryServer) ConsumerFeePoolClaims(ctx context.Context, req *QueryConsumerFeePoolClaimsRequest) (*QueryConsumerFeePoolClaimsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ConsumerFeePoolClaims not implemented") } +func (*UnimplementedQueryServer) QueryConsumerLiveness(ctx context.Context, req *QueryConsumerLivenessRequest) (*QueryConsumerLivenessResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method QueryConsumerLiveness not implemented") +} func RegisterQueryServer(s grpc1.Server, srv QueryServer) { s.RegisterService(&_Query_serviceDesc, srv) @@ -2553,6 +2693,24 @@ func _Query_ConsumerFeePoolClaims_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _Query_QueryConsumerLiveness_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryConsumerLivenessRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QueryServer).QueryConsumerLiveness(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/vaas.provider.v1.Query/QueryConsumerLiveness", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QueryServer).QueryConsumerLiveness(ctx, req.(*QueryConsumerLivenessRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _Query_serviceDesc = grpc.ServiceDesc{ ServiceName: "vaas.provider.v1.Query", HandlerType: (*QueryServer)(nil), @@ -2617,6 +2775,10 @@ var _Query_serviceDesc = grpc.ServiceDesc{ MethodName: "ConsumerFeePoolClaims", Handler: _Query_ConsumerFeePoolClaims_Handler, }, + { + MethodName: "QueryConsumerLiveness", + Handler: _Query_QueryConsumerLiveness_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "vaas/provider/v1/query.proto", @@ -3948,6 +4110,91 @@ func (m *QueryConsumerFeePoolClaimsResponse) MarshalToSizedBuffer(dAtA []byte) ( return len(dAtA) - i, nil } +func (m *QueryConsumerLivenessRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryConsumerLivenessRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryConsumerLivenessRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.ConsumerId != 0 { + i = encodeVarintQuery(dAtA, i, uint64(m.ConsumerId)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *QueryConsumerLivenessResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryConsumerLivenessResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryConsumerLivenessResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Degraded { + i-- + if m.Degraded { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x20 + } + n17, err17 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(m.RemovalEta, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(m.RemovalEta):]) + if err17 != nil { + return 0, err17 + } + i -= n17 + i = encodeVarintQuery(dAtA, i, uint64(n17)) + i-- + dAtA[i] = 0x1a + n18, err18 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.GracePeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.GracePeriod):]) + if err18 != nil { + return 0, err18 + } + i -= n18 + i = encodeVarintQuery(dAtA, i, uint64(n18)) + i-- + dAtA[i] = 0x12 + n19, err19 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(m.LastAckTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(m.LastAckTime):]) + if err19 != nil { + return 0, err19 + } + i -= n19 + i = encodeVarintQuery(dAtA, i, uint64(n19)) + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + func encodeVarintQuery(dAtA []byte, offset int, v uint64) int { offset -= sovQuery(v) base := offset @@ -4502,6 +4749,36 @@ func (m *QueryConsumerFeePoolClaimsResponse) Size() (n int) { return n } +func (m *QueryConsumerLivenessRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.ConsumerId != 0 { + n += 1 + sovQuery(uint64(m.ConsumerId)) + } + return n +} + +func (m *QueryConsumerLivenessResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = github_com_cosmos_gogoproto_types.SizeOfStdTime(m.LastAckTime) + n += 1 + l + sovQuery(uint64(l)) + l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.GracePeriod) + n += 1 + l + sovQuery(uint64(l)) + l = github_com_cosmos_gogoproto_types.SizeOfStdTime(m.RemovalEta) + n += 1 + l + sovQuery(uint64(l)) + if m.Degraded { + n += 2 + } + return n +} + func sovQuery(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -8077,6 +8354,244 @@ func (m *QueryConsumerFeePoolClaimsResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *QueryConsumerLivenessRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryConsumerLivenessRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryConsumerLivenessRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ConsumerId", wireType) + } + m.ConsumerId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ConsumerId |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *QueryConsumerLivenessResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryConsumerLivenessResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryConsumerLivenessResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LastAckTime", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_cosmos_gogoproto_types.StdTimeUnmarshal(&m.LastAckTime, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field GracePeriod", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_cosmos_gogoproto_types.StdDurationUnmarshal(&m.GracePeriod, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RemovalEta", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_cosmos_gogoproto_types.StdTimeUnmarshal(&m.RemovalEta, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Degraded", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Degraded = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipQuery(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/x/vaas/provider/types/query.pb.gw.go b/x/vaas/provider/types/query.pb.gw.go index 57c6058..51b18f7 100644 --- a/x/vaas/provider/types/query.pb.gw.go +++ b/x/vaas/provider/types/query.pb.gw.go @@ -861,6 +861,60 @@ func local_request_Query_ConsumerFeePoolClaims_0(ctx context.Context, marshaler } +func request_Query_QueryConsumerLiveness_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryConsumerLivenessRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["consumer_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "consumer_id") + } + + protoReq.ConsumerId, err = runtime.Uint64(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "consumer_id", err) + } + + msg, err := client.QueryConsumerLiveness(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Query_QueryConsumerLiveness_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryConsumerLivenessRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["consumer_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "consumer_id") + } + + protoReq.ConsumerId, err = runtime.Uint64(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "consumer_id", err) + } + + msg, err := server.QueryConsumerLiveness(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterQueryHandlerServer registers the http handlers for service Query to "mux". // UnaryRPC :call QueryServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -1212,6 +1266,29 @@ func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, serv }) + mux.Handle("GET", pattern_Query_QueryConsumerLiveness_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Query_QueryConsumerLiveness_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_QueryConsumerLiveness_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -1553,6 +1630,26 @@ func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie }) + mux.Handle("GET", pattern_Query_QueryConsumerLiveness_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Query_QueryConsumerLiveness_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_QueryConsumerLiveness_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -1586,6 +1683,8 @@ var ( pattern_Query_ConsumerFeePoolClaim_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4, 1, 0, 4, 1, 5, 5}, []string{"vaas", "provider", "v1", "consumer_fee_pool_claim", "consumer_id", "depositor"}, "", runtime.AssumeColonVerbOpt(false))) pattern_Query_ConsumerFeePoolClaims_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"vaas", "provider", "v1", "consumer_fee_pool_claims", "consumer_id"}, "", runtime.AssumeColonVerbOpt(false))) + + pattern_Query_QueryConsumerLiveness_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"vaas", "provider", "consumer_liveness", "consumer_id"}, "", runtime.AssumeColonVerbOpt(false))) ) var ( @@ -1618,4 +1717,6 @@ var ( forward_Query_ConsumerFeePoolClaim_0 = runtime.ForwardResponseMessage forward_Query_ConsumerFeePoolClaims_0 = runtime.ForwardResponseMessage + + forward_Query_QueryConsumerLiveness_0 = runtime.ForwardResponseMessage ) From b6aeaca92a7bc68cc7eb44a12058a8b5d025d35a Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:44:07 +0200 Subject: [PATCH 06/20] make liveness thresholds configurable params and relax the unbonding bound - SafeModeThreshold becomes a per-consumer initialization parameter (was a hardcoded const), validated > 0. - LivenessGraceFraction becomes a provider module parameter (was a hardcoded const), default 0.66; the sweep grace is unbonding * fraction. - consumer unbonding is bounded only by <= provider unbonding; the hardcoded multi-day floor is dropped (an impractically short unbonding is an operator concern, and short values are useful for test/dev chains). - clarify the remove-consumer CLI help: removal is governance-only. Accompanying unit tests are in the following commit. --- proto/vaas/provider/v1/provider.proto | 20 +- proto/vaas/v1/shared_consumer.proto | 5 + x/vaas/consumer/keeper/keeper.go | 2 +- x/vaas/consumer/types/safe_mode.go | 8 - x/vaas/provider/client/cli/tx.go | 2 +- x/vaas/provider/keeper/consumer_lifecycle.go | 1 + x/vaas/provider/keeper/msg_server.go | 14 +- x/vaas/provider/keeper/params.go | 5 +- x/vaas/provider/types/msg.go | 4 + x/vaas/provider/types/params.go | 20 +- x/vaas/provider/types/provider.pb.go | 376 ++++++++++++------- x/vaas/types/params.go | 6 + x/vaas/types/shared_consumer.pb.go | 139 ++++--- x/vaas/types/shared_params.go | 11 +- 14 files changed, 403 insertions(+), 210 deletions(-) delete mode 100644 x/vaas/consumer/types/safe_mode.go diff --git a/proto/vaas/provider/v1/provider.proto b/proto/vaas/provider/v1/provider.proto index 4c008c8..9c4e253 100644 --- a/proto/vaas/provider/v1/provider.proto +++ b/proto/vaas/provider/v1/provider.proto @@ -26,21 +26,27 @@ message Params { // TrustingPeriodFraction is used to compute the consumer and provider IBC // client's TrustingPeriod from the chain defined UnbondingPeriod string trusting_period_fraction = 1; + + // LivenessGraceFraction sets the consumer liveness grace period as a fraction + // of the provider unbonding period (grace = unbonding * fraction). A consumer + // with no successful VSC acknowledgement for longer than the grace is removed. + string liveness_grace_fraction = 2; + // Sent IBC packets will timeout after this duration - google.protobuf.Duration vaas_timeout_period = 2 + google.protobuf.Duration vaas_timeout_period = 3 [ (gogoproto.nullable) = false, (gogoproto.stdduration) = true ]; // The number of blocks that comprise an epoch. - int64 blocks_per_epoch = 3; + int64 blocks_per_epoch = 4; // The maximal number of validators that will be passed // to the consensus engine on the provider. - int64 max_provider_consensus_validators = 4; + int64 max_provider_consensus_validators = 5; // The per-block fee amount charged for consumer chain operation. The denom // is not a parameter: it is fixed at module wiring (see Keeper.feeDenom) and // cannot be changed without a binary upgrade. - string fees_per_block_amount = 5 [ + string fees_per_block_amount = 6 [ (cosmos_proto.scalar) = "cosmos.Int", (gogoproto.customtype) = "cosmossdk.io/math.Int", (gogoproto.nullable) = false, @@ -51,7 +57,7 @@ message Params { // fees_per_block.Amount (the floor is fees_per_block.Amount * // min_deposit_blocks). Zero disables the check. The floor applies to every // funder including the gov authority. - uint64 min_deposit_blocks = 6; + uint64 min_deposit_blocks = 7; } // AddressList contains a list of consensus addresses @@ -174,6 +180,10 @@ message ConsumerInitializationParameters { // This param is a part of the cosmos sdk staking module. In the case of // a VAAS enabled consumer chain, the VAAS module acts as the staking module. int64 historical_entries = 7; + // How long the consumer may go without receiving a VSC packet before its + // tx admission gate enters safe mode (only ibc.core and gov messages pass). + google.protobuf.Duration safe_mode_threshold = 8 + [ (gogoproto.nullable) = false, (gogoproto.stdduration) = true ]; } // ConsumerIds contains consumer ids of chains diff --git a/proto/vaas/v1/shared_consumer.proto b/proto/vaas/v1/shared_consumer.proto index c7c676e..e8fced7 100644 --- a/proto/vaas/v1/shared_consumer.proto +++ b/proto/vaas/v1/shared_consumer.proto @@ -39,6 +39,11 @@ message ConsumerParams { // which should be smaller than that of the provider in general. google.protobuf.Duration unbonding_period = 4 [ (gogoproto.nullable) = false, (gogoproto.stdduration) = true ]; + + // How long the consumer may go without receiving a VSC packet before its + // tx admission gate enters safe mode (only ibc.core and gov messages pass). + google.protobuf.Duration safe_mode_threshold = 5 + [ (gogoproto.nullable) = false, (gogoproto.stdduration) = true ]; } // ConsumerGenesisState defines shared genesis information between provider and diff --git a/x/vaas/consumer/keeper/keeper.go b/x/vaas/consumer/keeper/keeper.go index cf2dfc0..6ca2d39 100644 --- a/x/vaas/consumer/keeper/keeper.go +++ b/x/vaas/consumer/keeper/keeper.go @@ -486,5 +486,5 @@ func (k Keeper) GetLastVSCRecvTime(ctx context.Context) time.Time { // the safe-mode threshold. func (k Keeper) IsVSCStale(ctx context.Context) bool { sdkCtx := sdk.UnwrapSDKContext(ctx) - return sdkCtx.BlockTime().Sub(k.GetLastVSCRecvTime(ctx)) > types.DefaultSafeModeThreshold + return sdkCtx.BlockTime().Sub(k.GetLastVSCRecvTime(ctx)) > k.GetConsumerParams(ctx).SafeModeThreshold } diff --git a/x/vaas/consumer/types/safe_mode.go b/x/vaas/consumer/types/safe_mode.go deleted file mode 100644 index 5a4590c..0000000 --- a/x/vaas/consumer/types/safe_mode.go +++ /dev/null @@ -1,8 +0,0 @@ -package types - -import "time" - -// DefaultSafeModeThreshold is how long the consumer may go without receiving a -// VSC packet before its tx admission gate enters safe mode (only ibc.core and -// gov messages pass). Set well below the provider liveness grace period. -const DefaultSafeModeThreshold = 3 * time.Hour diff --git a/x/vaas/provider/client/cli/tx.go b/x/vaas/provider/client/cli/tx.go index 7633401..fb1c68c 100644 --- a/x/vaas/provider/client/cli/tx.go +++ b/x/vaas/provider/client/cli/tx.go @@ -393,7 +393,7 @@ func NewRemoveConsumerCmd() *cobra.Command { Use: "remove-consumer [consumer-id]", Short: "remove a consumer chain", Long: strings.TrimSpace( - fmt.Sprintf(`Removes (and stops) a consumer chain. Note that only the owner of the chain can remove it. + fmt.Sprintf(`Removes (and stops) a consumer chain. Removal must be performed via governance: submit a proposal containing MsgRemoveConsumer with the governance module address as authority. Example: %s tx provider remove-consumer [consumer-id] `, version.AppName)), diff --git a/x/vaas/provider/keeper/consumer_lifecycle.go b/x/vaas/provider/keeper/consumer_lifecycle.go index 9cb97f4..8ca069a 100644 --- a/x/vaas/provider/keeper/consumer_lifecycle.go +++ b/x/vaas/provider/keeper/consumer_lifecycle.go @@ -284,6 +284,7 @@ func (k Keeper) MakeConsumerGenesis( initializationRecord.VaasTimeoutPeriod, initializationRecord.HistoricalEntries, initializationRecord.UnbondingPeriod, + initializationRecord.SafeModeThreshold, ) providerUnbondingPeriod, err := k.stakingKeeper.UnbondingTime(ctx) diff --git a/x/vaas/provider/keeper/msg_server.go b/x/vaas/provider/keeper/msg_server.go index 954ee79..52f4399 100644 --- a/x/vaas/provider/keeper/msg_server.go +++ b/x/vaas/provider/keeper/msg_server.go @@ -218,18 +218,18 @@ func (k msgServer) SubmitConsumerDoubleVoting(goCtx context.Context, msg *types. return &types.MsgSubmitConsumerDoubleVotingResponse{}, nil } -// validateConsumerUnbonding enforces floor <= consumer unbonding <= provider -// unbonding, so a consumer's relayer-derived trusting period stays sane and the -// consumer cannot outlive the provider's slashable window. +// validateConsumerUnbonding enforces the one safety-relevant bound on a +// consumer's unbonding period: it must not exceed the provider's, so the +// consumer cannot outlive the provider's slashable window. Positivity is +// already checked by ValidateInitializationParameters. No lower floor is +// imposed: choosing an unbonding short enough that the relayer-derived +// trusting period becomes impractical is an operator concern (and is needed +// for short-lived test/dev chains), not a protocol-enforced minimum. func (k Keeper) validateConsumerUnbonding(ctx sdk.Context, d time.Duration) error { providerUnbonding, err := k.stakingKeeper.UnbondingTime(ctx) if err != nil { return err } - if d < vaastypes.MinConsumerUnbondingPeriod { - return errorsmod.Wrapf(types.ErrInvalidConsumerInitializationParameters, - "unbonding period must be >= %s, got %s", vaastypes.MinConsumerUnbondingPeriod, d) - } if d > providerUnbonding { return errorsmod.Wrapf(types.ErrInvalidConsumerInitializationParameters, "unbonding period %s must not exceed provider unbonding %s", d, providerUnbonding) diff --git a/x/vaas/provider/keeper/params.go b/x/vaas/provider/keeper/params.go index 72f6d60..1ede481 100644 --- a/x/vaas/provider/keeper/params.go +++ b/x/vaas/provider/keeper/params.go @@ -169,11 +169,12 @@ func (k Keeper) SetInfractionParams(ctx context.Context, params types.Infraction // LivenessGracePeriod returns the maximum time a launched consumer may go // without a successful VSC ack before it is removed, derived from the provider -// unbonding period so it stays inside the slashable window. +// unbonding period and the LivenessGraceFraction param so it stays inside the +// slashable window. func (k Keeper) LivenessGracePeriod(ctx context.Context) (time.Duration, error) { unbonding, err := k.stakingKeeper.UnbondingTime(ctx) if err != nil { return 0, err } - return vaastypes.CalculateTrustPeriod(unbonding, types.LivenessGraceFraction) + return vaastypes.CalculateTrustPeriod(unbonding, k.GetParams(ctx).LivenessGraceFraction) } diff --git a/x/vaas/provider/types/msg.go b/x/vaas/provider/types/msg.go index 11ab95a..f5b12da 100644 --- a/x/vaas/provider/types/msg.go +++ b/x/vaas/provider/types/msg.go @@ -437,6 +437,10 @@ func ValidateInitializationParameters(initializationParameters ConsumerInitializ return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "UnbondingPeriod: %s", err.Error()) } + if err := vaastypes.ValidateDuration(initializationParameters.SafeModeThreshold); err != nil { + return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "SafeModeThreshold: %s", err.Error()) + } + return nil } diff --git a/x/vaas/provider/types/params.go b/x/vaas/provider/types/params.go index 4c06839..f4713d9 100644 --- a/x/vaas/provider/types/params.go +++ b/x/vaas/provider/types/params.go @@ -21,12 +21,13 @@ const ( // as UnbondingPeriod * TrustingPeriodFraction DefaultTrustingPeriodFraction = "0.66" - // LivenessGraceFraction sets the consumer liveness grace period as a - // fraction of the provider unbonding period. A consumer that produces no - // successful VSC ack for longer than this is removed. 0.66 mirrors the - // trusting-period fraction: the grace ends around the client recovery - // horizon, leaving margin below the unbonding (slashable) window. - LivenessGraceFraction = "0.66" + // DefaultLivenessGraceFraction is the default value of the LivenessGraceFraction + // param, which sets the consumer liveness grace period as a fraction of the + // provider unbonding period (grace = unbonding * fraction). A consumer that + // produces no successful VSC ack for longer than the grace is removed. 0.66 + // mirrors the trusting-period fraction: the grace ends around the client + // recovery horizon, leaving margin below the unbonding (slashable) window. + DefaultLivenessGraceFraction = "0.66" // DefaultBlocksPerEpoch defines the default blocks that constitute an epoch. Assuming we need 6 seconds per block, // an epoch corresponds to 1 hour (6 * 600 = 3600 seconds). @@ -66,6 +67,7 @@ const ( // NewParams creates new provider parameters with provided arguments func NewParams( trustingPeriodFraction string, + livenessGraceFraction string, vaasTimeoutPeriod time.Duration, blocksPerEpoch int64, maxProviderConsensusValidators int64, @@ -74,6 +76,7 @@ func NewParams( ) Params { return Params{ TrustingPeriodFraction: trustingPeriodFraction, + LivenessGraceFraction: livenessGraceFraction, VaasTimeoutPeriod: vaasTimeoutPeriod, BlocksPerEpoch: blocksPerEpoch, MaxProviderConsensusValidators: maxProviderConsensusValidators, @@ -85,6 +88,7 @@ func NewParams( func DefaultParams() Params { return NewParams( DefaultTrustingPeriodFraction, + DefaultLivenessGraceFraction, vaastypes.DefaultVAASTimeoutPeriod, DefaultBlocksPerEpoch, DefaultMaxProviderConsensusValidators, @@ -124,6 +128,7 @@ func DefaultConsumerInitializationParameters() ConsumerInitializationParameters UnbondingPeriod: vaastypes.DefaultConsumerUnbondingPeriod, VaasTimeoutPeriod: vaastypes.DefaultVAASTimeoutPeriod, HistoricalEntries: vaastypes.DefaultHistoricalEntries, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, } } @@ -175,6 +180,9 @@ func (p Params) Validate() error { if err := validateFeesPerBlockAmount(p.FeesPerBlockAmount); err != nil { return err } + if err := vaastypes.ValidateStringFractionNonZero(p.LivenessGraceFraction); err != nil { + return fmt.Errorf("liveness grace fraction is invalid: %s", err) + } return nil } diff --git a/x/vaas/provider/types/provider.pb.go b/x/vaas/provider/types/provider.pb.go index cd7c623..f6ff877 100644 --- a/x/vaas/provider/types/provider.pb.go +++ b/x/vaas/provider/types/provider.pb.go @@ -90,22 +90,26 @@ type Params struct { // TrustingPeriodFraction is used to compute the consumer and provider IBC // client's TrustingPeriod from the chain defined UnbondingPeriod TrustingPeriodFraction string `protobuf:"bytes,1,opt,name=trusting_period_fraction,json=trustingPeriodFraction,proto3" json:"trusting_period_fraction,omitempty"` + // LivenessGraceFraction sets the consumer liveness grace period as a fraction + // of the provider unbonding period (grace = unbonding * fraction). A consumer + // with no successful VSC acknowledgement for longer than the grace is removed. + LivenessGraceFraction string `protobuf:"bytes,2,opt,name=liveness_grace_fraction,json=livenessGraceFraction,proto3" json:"liveness_grace_fraction,omitempty"` // Sent IBC packets will timeout after this duration - VaasTimeoutPeriod time.Duration `protobuf:"bytes,2,opt,name=vaas_timeout_period,json=vaasTimeoutPeriod,proto3,stdduration" json:"vaas_timeout_period"` + VaasTimeoutPeriod time.Duration `protobuf:"bytes,3,opt,name=vaas_timeout_period,json=vaasTimeoutPeriod,proto3,stdduration" json:"vaas_timeout_period"` // The number of blocks that comprise an epoch. - BlocksPerEpoch int64 `protobuf:"varint,3,opt,name=blocks_per_epoch,json=blocksPerEpoch,proto3" json:"blocks_per_epoch,omitempty"` + BlocksPerEpoch int64 `protobuf:"varint,4,opt,name=blocks_per_epoch,json=blocksPerEpoch,proto3" json:"blocks_per_epoch,omitempty"` // The maximal number of validators that will be passed // to the consensus engine on the provider. - MaxProviderConsensusValidators int64 `protobuf:"varint,4,opt,name=max_provider_consensus_validators,json=maxProviderConsensusValidators,proto3" json:"max_provider_consensus_validators,omitempty"` + MaxProviderConsensusValidators int64 `protobuf:"varint,5,opt,name=max_provider_consensus_validators,json=maxProviderConsensusValidators,proto3" json:"max_provider_consensus_validators,omitempty"` // The per-block fee amount charged for consumer chain operation. The denom // is not a parameter: it is fixed at module wiring (see Keeper.feeDenom) and // cannot be changed without a binary upgrade. - FeesPerBlockAmount cosmossdk_io_math.Int `protobuf:"bytes,5,opt,name=fees_per_block_amount,json=feesPerBlockAmount,proto3,customtype=cosmossdk.io/math.Int" json:"fees_per_block_amount"` + FeesPerBlockAmount cosmossdk_io_math.Int `protobuf:"bytes,6,opt,name=fees_per_block_amount,json=feesPerBlockAmount,proto3,customtype=cosmossdk.io/math.Int" json:"fees_per_block_amount"` // Minimum deposit on MsgFundConsumerFeePool, expressed as a multiplier of // fees_per_block.Amount (the floor is fees_per_block.Amount * // min_deposit_blocks). Zero disables the check. The floor applies to every // funder including the gov authority. - MinDepositBlocks uint64 `protobuf:"varint,6,opt,name=min_deposit_blocks,json=minDepositBlocks,proto3" json:"min_deposit_blocks,omitempty"` + MinDepositBlocks uint64 `protobuf:"varint,7,opt,name=min_deposit_blocks,json=minDepositBlocks,proto3" json:"min_deposit_blocks,omitempty"` } func (m *Params) Reset() { *m = Params{} } @@ -148,6 +152,13 @@ func (m *Params) GetTrustingPeriodFraction() string { return "" } +func (m *Params) GetLivenessGraceFraction() string { + if m != nil { + return m.LivenessGraceFraction + } + return "" +} + func (m *Params) GetVaasTimeoutPeriod() time.Duration { if m != nil { return m.VaasTimeoutPeriod @@ -688,6 +699,9 @@ type ConsumerInitializationParameters struct { // This param is a part of the cosmos sdk staking module. In the case of // a VAAS enabled consumer chain, the VAAS module acts as the staking module. HistoricalEntries int64 `protobuf:"varint,7,opt,name=historical_entries,json=historicalEntries,proto3" json:"historical_entries,omitempty"` + // How long the consumer may go without receiving a VSC packet before its + // tx admission gate enters safe mode (only ibc.core and gov messages pass). + SafeModeThreshold time.Duration `protobuf:"bytes,8,opt,name=safe_mode_threshold,json=safeModeThreshold,proto3,stdduration" json:"safe_mode_threshold"` } func (m *ConsumerInitializationParameters) Reset() { *m = ConsumerInitializationParameters{} } @@ -772,6 +786,13 @@ func (m *ConsumerInitializationParameters) GetHistoricalEntries() int64 { return 0 } +func (m *ConsumerInitializationParameters) GetSafeModeThreshold() time.Duration { + if m != nil { + return m.SafeModeThreshold + } + return 0 +} + // ConsumerIds contains consumer ids of chains type ConsumerIds struct { Ids []uint64 `protobuf:"varint,1,rep,packed,name=ids,proto3" json:"ids,omitempty"` @@ -957,96 +978,99 @@ func init() { func init() { proto.RegisterFile("vaas/provider/v1/provider.proto", fileDescriptor_6404dd5d21545279) } var fileDescriptor_6404dd5d21545279 = []byte{ - // 1414 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x56, 0xcd, 0x6f, 0x1c, 0xc5, - 0x12, 0xf7, 0x78, 0x37, 0x8e, 0xdd, 0x6b, 0xfb, 0x6d, 0x3a, 0x4e, 0xb2, 0x76, 0x92, 0xb5, 0xb3, - 0x4f, 0x79, 0xb2, 0xf2, 0x5e, 0x66, 0x9e, 0x83, 0x84, 0x10, 0x1c, 0x22, 0xdb, 0x3b, 0x89, 0x17, - 0x3b, 0xce, 0x6a, 0x76, 0x03, 0x52, 0x04, 0x1a, 0xf5, 0xce, 0x74, 0x76, 0x3b, 0x9e, 0xe9, 0x1e, - 0x4d, 0xf7, 0xae, 0xb3, 0x9c, 0x39, 0x70, 0x0c, 0x37, 0x6e, 0x20, 0x71, 0xe1, 0xc8, 0x01, 0x71, - 0xe4, 0x1c, 0x6e, 0x11, 0x27, 0x84, 0x50, 0x40, 0xc9, 0x81, 0xff, 0x81, 0x13, 0xea, 0x8f, 0x19, - 0x8f, 0x3f, 0x22, 0x1c, 0x71, 0x19, 0x75, 0xff, 0xaa, 0x7e, 0xd5, 0x55, 0xd5, 0xd5, 0x55, 0x03, - 0x96, 0x47, 0x08, 0x71, 0x27, 0x49, 0xd9, 0x88, 0x84, 0x38, 0x75, 0x46, 0x6b, 0xf9, 0xda, 0x4e, - 0x52, 0x26, 0x18, 0xac, 0x4a, 0x05, 0x3b, 0x07, 0x47, 0x6b, 0x4b, 0xe7, 0x50, 0x4c, 0x28, 0x73, - 0xd4, 0x57, 0x2b, 0x2d, 0x2d, 0x06, 0x8c, 0xc7, 0x8c, 0xfb, 0x6a, 0xe7, 0xe8, 0x8d, 0x11, 0x2d, - 0xf4, 0x59, 0x9f, 0x69, 0x5c, 0xae, 0x0c, 0xfa, 0x1f, 0xad, 0xe3, 0x60, 0x69, 0x95, 0x06, 0xd8, - 0x19, 0xad, 0xf5, 0xb0, 0x40, 0x6b, 0x39, 0x60, 0xf4, 0xea, 0x7d, 0xc6, 0xfa, 0x11, 0x76, 0xd4, - 0xae, 0x37, 0x7c, 0xe4, 0x84, 0xc3, 0x14, 0x09, 0xc2, 0xa8, 0x91, 0x2f, 0x1f, 0x95, 0x0b, 0x12, - 0x63, 0x2e, 0x50, 0x9c, 0x64, 0x0a, 0xa4, 0x17, 0x38, 0x01, 0x4b, 0xb1, 0x13, 0x44, 0x04, 0x53, - 0x21, 0x23, 0xd4, 0x2b, 0xa3, 0xe0, 0x48, 0x85, 0x88, 0xf4, 0x07, 0x42, 0xc3, 0xdc, 0x11, 0x98, - 0x86, 0x38, 0x8d, 0x89, 0x56, 0x3e, 0xd8, 0x19, 0x02, 0x54, 0x19, 0x1b, 0xad, 0x39, 0xfb, 0x24, - 0xcd, 0xdc, 0xbc, 0x52, 0xe0, 0x04, 0xe9, 0x38, 0x11, 0xcc, 0xd9, 0xc3, 0x63, 0x93, 0x82, 0xc6, - 0x97, 0x25, 0x30, 0xd5, 0x46, 0x29, 0x8a, 0x39, 0x7c, 0x07, 0xd4, 0x44, 0x3a, 0xe4, 0x82, 0xd0, - 0xbe, 0x9f, 0xe0, 0x94, 0xb0, 0xd0, 0x7f, 0x94, 0xa2, 0x40, 0x46, 0x54, 0xb3, 0x56, 0xac, 0xd5, - 0x19, 0xef, 0x62, 0x26, 0x6f, 0x2b, 0xf1, 0x1d, 0x23, 0x85, 0x1d, 0x70, 0x5e, 0x1e, 0xec, 0xcb, - 0x00, 0xd9, 0x50, 0x18, 0x76, 0x6d, 0x72, 0xc5, 0x5a, 0xad, 0xdc, 0x5a, 0xb4, 0x75, 0x1e, 0xec, - 0x2c, 0x0f, 0x76, 0xd3, 0xe4, 0x69, 0x63, 0xfa, 0xd9, 0x8b, 0xe5, 0x89, 0x2f, 0x7e, 0x5b, 0xb6, - 0xbc, 0x73, 0x92, 0xdf, 0xd5, 0x74, 0x6d, 0x1c, 0xae, 0x82, 0x6a, 0x2f, 0x62, 0xc1, 0x1e, 0x97, - 0xe6, 0x7c, 0x9c, 0xb0, 0x60, 0x50, 0x2b, 0xad, 0x58, 0xab, 0x25, 0x6f, 0x5e, 0xe3, 0x6d, 0x9c, - 0xba, 0x12, 0x85, 0x2d, 0x70, 0x2d, 0x46, 0x4f, 0xfc, 0xac, 0x0e, 0xfc, 0x80, 0x51, 0x8e, 0x29, - 0x1f, 0x72, 0x7f, 0x84, 0x22, 0x12, 0x22, 0xc1, 0x52, 0x5e, 0x2b, 0x2b, 0x6a, 0x3d, 0x46, 0x4f, - 0xda, 0x46, 0x6f, 0x33, 0x53, 0xfb, 0x20, 0xd7, 0x82, 0x01, 0xb8, 0xf0, 0x08, 0x63, 0x7d, 0xa4, - 0x3a, 0xc5, 0x47, 0x31, 0x1b, 0x52, 0x51, 0x3b, 0x23, 0x13, 0xb0, 0xf1, 0x7f, 0xe9, 0xf0, 0x2f, - 0x2f, 0x96, 0x2f, 0xe8, 0x12, 0xe1, 0xe1, 0x9e, 0x4d, 0x98, 0x13, 0x23, 0x31, 0xb0, 0x5b, 0x54, - 0xfc, 0xf4, 0xdd, 0x4d, 0x60, 0xea, 0xab, 0x45, 0xc5, 0x37, 0x7f, 0x7c, 0x7b, 0xc3, 0xf2, 0xa0, - 0x34, 0xd7, 0xc6, 0xe9, 0x86, 0x34, 0xb6, 0xae, 0x6c, 0xc1, 0xff, 0x01, 0x18, 0x13, 0xea, 0x87, - 0x38, 0x61, 0x9c, 0x08, 0x7d, 0x0e, 0xaf, 0x4d, 0xad, 0x58, 0xab, 0x65, 0xaf, 0x1a, 0x13, 0xda, - 0xd4, 0x02, 0x45, 0xe1, 0x8d, 0xff, 0x82, 0xca, 0x7a, 0x18, 0xa6, 0x98, 0xf3, 0x1d, 0xc2, 0x05, - 0xbc, 0x02, 0x66, 0x90, 0xde, 0x62, 0x5e, 0xb3, 0x56, 0x4a, 0xab, 0xb3, 0xde, 0x01, 0xd0, 0xf8, - 0x08, 0x2c, 0xe6, 0xd1, 0x74, 0xb0, 0xd8, 0x1c, 0x20, 0xda, 0xc7, 0x6d, 0x14, 0xec, 0x61, 0xc1, - 0xe1, 0x6d, 0x50, 0x8e, 0x08, 0x17, 0x8a, 0x55, 0xb9, 0x75, 0xdd, 0x56, 0xaf, 0x67, 0xb4, 0x66, - 0xbf, 0x8e, 0xd1, 0x44, 0x02, 0x6d, 0x94, 0x65, 0xc8, 0x9e, 0x22, 0x36, 0x3e, 0xb7, 0x40, 0x6d, - 0x1b, 0x8f, 0xd7, 0x39, 0x27, 0x7d, 0x1a, 0x63, 0x2a, 0x3c, 0x9c, 0x44, 0x28, 0xc0, 0x72, 0x09, - 0xff, 0x0d, 0xe6, 0xf2, 0x1b, 0x90, 0x0e, 0xa9, 0x9a, 0x99, 0xf5, 0x66, 0x33, 0x50, 0x06, 0x01, - 0xdf, 0x05, 0x20, 0x49, 0xf1, 0xc8, 0x0f, 0xfc, 0x3d, 0x3c, 0x36, 0x05, 0x72, 0xc5, 0x2e, 0xd4, - 0xb1, 0xae, 0x50, 0xbb, 0x3d, 0xec, 0x45, 0x24, 0xd8, 0xc6, 0x63, 0x6f, 0x5a, 0xea, 0x6f, 0x6e, - 0xe3, 0x31, 0x5c, 0x00, 0x67, 0x12, 0xb6, 0x8f, 0x53, 0x53, 0x05, 0x7a, 0xd3, 0xf8, 0xca, 0x02, - 0x97, 0xf2, 0x00, 0xe4, 0x95, 0x0e, 0x63, 0x9c, 0xb6, 0x87, 0x3d, 0xc9, 0x58, 0x06, 0x95, 0xc0, - 0x20, 0x3e, 0x09, 0x95, 0x43, 0x65, 0x0f, 0x64, 0x50, 0x2b, 0x3c, 0xee, 0xf3, 0xe4, 0x09, 0x3e, - 0xdf, 0x06, 0xb3, 0xb9, 0x15, 0xe9, 0x75, 0xe9, 0x14, 0x5e, 0xe7, 0xe7, 0x6e, 0xe3, 0x71, 0xe3, - 0xd3, 0xa2, 0x8b, 0x1b, 0xe3, 0xcc, 0x49, 0x65, 0xfc, 0x34, 0x2e, 0xe6, 0x0a, 0x45, 0x17, 0x83, - 0xa2, 0x95, 0x63, 0x71, 0x94, 0x8e, 0xc7, 0xd1, 0xf8, 0xc1, 0x02, 0x0b, 0xc5, 0xb3, 0x79, 0x97, - 0xb5, 0xd3, 0x21, 0xc5, 0x7f, 0xef, 0xc3, 0x6d, 0x30, 0x9d, 0x48, 0x4d, 0x5f, 0x70, 0x73, 0x67, - 0x4b, 0xc7, 0x1e, 0x75, 0x37, 0x6b, 0x6e, 0xfa, 0x55, 0x3f, 0x95, 0xaf, 0xfa, 0xac, 0x62, 0x75, - 0x39, 0x6c, 0x82, 0xf9, 0x43, 0x41, 0x70, 0x93, 0xc4, 0xab, 0xf6, 0xd1, 0x0e, 0x6e, 0x17, 0x6a, - 0xdd, 0x9b, 0x2b, 0x06, 0xc9, 0x1b, 0xdf, 0x5b, 0x00, 0x1e, 0x7f, 0xb4, 0xf2, 0x39, 0x1d, 0x7a, - 0xfa, 0xc5, 0xea, 0xab, 0x26, 0x85, 0xc7, 0xae, 0x52, 0x95, 0x57, 0xd1, 0x64, 0xa1, 0x8a, 0xe0, - 0x7b, 0x00, 0x24, 0xea, 0xf2, 0x4e, 0x7d, 0xc3, 0x33, 0x49, 0xb6, 0x94, 0xf9, 0x7b, 0xcc, 0x08, - 0xf5, 0x07, 0x58, 0xb6, 0x6a, 0xd3, 0x69, 0x80, 0x84, 0xb6, 0x14, 0xd2, 0x08, 0x41, 0x35, 0x4b, - 0xfc, 0x3d, 0x2c, 0x50, 0x88, 0x04, 0x82, 0x10, 0x94, 0x29, 0x8a, 0xb1, 0xe9, 0xac, 0x6a, 0x0d, - 0x57, 0x40, 0x25, 0xc4, 0x3c, 0x48, 0x49, 0xa2, 0x9a, 0xee, 0xa4, 0x12, 0x15, 0x21, 0xb8, 0x04, - 0xa6, 0x63, 0x63, 0x41, 0x79, 0x39, 0xe3, 0xe5, 0xfb, 0xc6, 0xb3, 0x12, 0x58, 0xc9, 0x8e, 0x69, - 0x51, 0x22, 0x08, 0x8a, 0xc8, 0x27, 0xaa, 0xd1, 0xaa, 0x06, 0x8f, 0x05, 0x4e, 0x39, 0xbc, 0x0b, - 0xe6, 0x89, 0x96, 0x65, 0xee, 0x5a, 0xe6, 0x42, 0x49, 0x2f, 0xb0, 0xe5, 0x30, 0xb2, 0xcd, 0x08, - 0x1a, 0xad, 0xd9, 0xda, 0x7d, 0xd3, 0x02, 0xe6, 0x0c, 0x4f, 0x83, 0xf0, 0x1a, 0x98, 0xed, 0x63, - 0x8a, 0x39, 0xe1, 0xfe, 0x00, 0xf1, 0x81, 0x29, 0xcb, 0x8a, 0xc1, 0xb6, 0x10, 0x1f, 0xc8, 0xbc, - 0xf4, 0x08, 0x45, 0xe9, 0x58, 0x6b, 0xe8, 0x9a, 0x04, 0x1a, 0x52, 0x0a, 0x9b, 0x00, 0xf0, 0x04, - 0xed, 0x53, 0x35, 0x38, 0x54, 0xde, 0x4e, 0x5b, 0x59, 0x33, 0x8a, 0x27, 0x25, 0x70, 0x17, 0x54, - 0x87, 0xb4, 0xc7, 0x68, 0x78, 0x30, 0xb7, 0x54, 0xb7, 0x3e, 0xe5, 0xe4, 0xf9, 0x57, 0x4e, 0x36, - 0x73, 0xe7, 0x35, 0xc3, 0x6c, 0xea, 0x1f, 0x0d, 0xb3, 0x9b, 0x00, 0x0e, 0x08, 0x17, 0x2c, 0x25, - 0x01, 0x8a, 0x7c, 0x4c, 0x45, 0x4a, 0x30, 0xaf, 0x9d, 0x55, 0x95, 0x72, 0xee, 0x40, 0xe2, 0x6a, - 0x41, 0x63, 0x19, 0x54, 0xf2, 0x9b, 0x0c, 0x39, 0xac, 0x82, 0x12, 0x09, 0x75, 0xb7, 0x2f, 0x7b, - 0x72, 0xd9, 0xf8, 0xd3, 0x02, 0x0b, 0x2d, 0x9a, 0x8d, 0xe7, 0xc2, 0xfd, 0xde, 0x01, 0x95, 0x90, - 0x0d, 0x7b, 0x11, 0xf6, 0x65, 0x8f, 0x36, 0x97, 0x7b, 0xfd, 0xf8, 0x33, 0xeb, 0x44, 0x88, 0x0f, - 0xde, 0x47, 0x24, 0x3a, 0xe0, 0x7a, 0x40, 0x33, 0x3b, 0xa4, 0x4f, 0xe1, 0x3a, 0x98, 0x0e, 0xd9, - 0x3e, 0x55, 0x17, 0x33, 0xf9, 0x26, 0x46, 0x72, 0x1a, 0xfc, 0x10, 0x5c, 0xc8, 0xd6, 0x7e, 0x3f, - 0x45, 0x01, 0xce, 0x52, 0x59, 0x3a, 0x7d, 0x2a, 0xcf, 0x67, 0x16, 0xee, 0x4a, 0x03, 0x3a, 0x99, - 0x8d, 0x5f, 0x2d, 0x70, 0xfe, 0x84, 0xa3, 0xe1, 0xc7, 0x60, 0x9e, 0x4b, 0xf8, 0xf0, 0x6f, 0xcb, - 0xec, 0xc6, 0xdb, 0x66, 0x6a, 0x5f, 0x3e, 0x3e, 0xb5, 0x77, 0x70, 0x1f, 0x05, 0xe3, 0x26, 0x0e, - 0x0a, 0xb3, 0xbb, 0x89, 0x03, 0x3d, 0xbb, 0xe7, 0x94, 0xb5, 0xfc, 0x2f, 0x67, 0x0b, 0xcc, 0x3d, - 0x46, 0x24, 0xf2, 0xb3, 0xdf, 0xbc, 0x37, 0xf9, 0xbf, 0x99, 0x95, 0xcc, 0x0c, 0x97, 0x33, 0x5c, - 0xb0, 0xb8, 0xc7, 0x05, 0xa3, 0x58, 0x65, 0x63, 0xda, 0x3b, 0x00, 0x6e, 0xfc, 0x68, 0x81, 0xb9, - 0x7c, 0x90, 0x0d, 0x10, 0xc7, 0xb0, 0x0e, 0x96, 0x36, 0xef, 0xef, 0x76, 0x1e, 0xdc, 0x73, 0x3d, - 0xbf, 0xbd, 0xb5, 0xde, 0x71, 0xfd, 0x07, 0xbb, 0x9d, 0xb6, 0xbb, 0xd9, 0xba, 0xd3, 0x72, 0x9b, - 0xd5, 0x09, 0x78, 0x15, 0x2c, 0x1e, 0x91, 0x7b, 0xee, 0xdd, 0x56, 0xa7, 0xeb, 0x7a, 0x6e, 0xb3, - 0x6a, 0x9d, 0x40, 0x6f, 0xed, 0xb6, 0xba, 0xad, 0xf5, 0x9d, 0xd6, 0x43, 0xb7, 0x59, 0x9d, 0x84, - 0x97, 0xc1, 0xa5, 0x23, 0xf2, 0x9d, 0xf5, 0x07, 0xbb, 0x9b, 0x5b, 0x6e, 0xb3, 0x5a, 0x82, 0x4b, - 0xe0, 0xe2, 0x11, 0x61, 0xa7, 0x7b, 0xbf, 0xdd, 0x76, 0x9b, 0xd5, 0xf2, 0x09, 0xb2, 0xa6, 0xbb, - 0xe3, 0x76, 0xdd, 0x66, 0xf5, 0xcc, 0x52, 0xf9, 0xb3, 0xaf, 0xeb, 0x13, 0x1b, 0xad, 0x67, 0x2f, - 0xeb, 0xd6, 0xf3, 0x97, 0x75, 0xeb, 0xf7, 0x97, 0x75, 0xeb, 0xe9, 0xab, 0xfa, 0xc4, 0xf3, 0x57, - 0xf5, 0x89, 0x9f, 0x5f, 0xd5, 0x27, 0x1e, 0x3a, 0x7d, 0x22, 0x06, 0xc3, 0x9e, 0x1d, 0xb0, 0xd8, - 0x41, 0x51, 0x44, 0x68, 0x8f, 0x08, 0xee, 0xa8, 0xff, 0xd7, 0x27, 0xce, 0xe1, 0x1f, 0x7f, 0x31, - 0x4e, 0x30, 0xef, 0x4d, 0xa9, 0xfc, 0xbe, 0xf5, 0x57, 0x00, 0x00, 0x00, 0xff, 0xff, 0xea, 0xb8, - 0x71, 0x09, 0x16, 0x0c, 0x00, 0x00, + // 1464 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x57, 0xcf, 0x6f, 0x1b, 0xc5, + 0x17, 0xcf, 0xc6, 0x6e, 0x9a, 0x8c, 0x93, 0x7c, 0xdd, 0x69, 0xd2, 0x3a, 0x69, 0xeb, 0xa4, 0xfe, + 0xaa, 0x28, 0x2a, 0x74, 0x97, 0x14, 0xa9, 0x42, 0x70, 0xa8, 0x92, 0xd8, 0x6d, 0x4c, 0xd2, 0xd4, + 0x5a, 0xbb, 0x20, 0x55, 0xa0, 0xd5, 0x78, 0x77, 0x62, 0x4f, 0xb3, 0x3b, 0xb3, 0xda, 0x19, 0x3b, + 0x35, 0x67, 0x0e, 0x1c, 0xcb, 0x8d, 0x0b, 0x12, 0x12, 0x17, 0x8e, 0x1c, 0x10, 0x47, 0xce, 0xe5, + 0x56, 0x71, 0x01, 0x21, 0x54, 0x50, 0x7b, 0xe0, 0x7f, 0xe0, 0x84, 0xe6, 0xc7, 0x6e, 0x36, 0x3f, + 0x2a, 0x52, 0x71, 0xb1, 0x66, 0xdf, 0x7b, 0x9f, 0x37, 0x9f, 0xf7, 0xe6, 0xcd, 0xbc, 0x67, 0xb0, + 0x34, 0x44, 0x88, 0x3b, 0x71, 0xc2, 0x86, 0x24, 0xc0, 0x89, 0x33, 0x5c, 0xcd, 0xd6, 0x76, 0x9c, + 0x30, 0xc1, 0x60, 0x59, 0x1a, 0xd8, 0x99, 0x70, 0xb8, 0xba, 0x78, 0x0e, 0x45, 0x84, 0x32, 0x47, + 0xfd, 0x6a, 0xa3, 0xc5, 0x05, 0x9f, 0xf1, 0x88, 0x71, 0x4f, 0x7d, 0x39, 0xfa, 0xc3, 0xa8, 0xe6, + 0x7a, 0xac, 0xc7, 0xb4, 0x5c, 0xae, 0x8c, 0xf4, 0x0d, 0x6d, 0xe3, 0x60, 0xe9, 0x95, 0xfa, 0xd8, + 0x19, 0xae, 0x76, 0xb1, 0x40, 0xab, 0x99, 0xc0, 0xd8, 0x55, 0x7b, 0x8c, 0xf5, 0x42, 0xec, 0xa8, + 0xaf, 0xee, 0x60, 0xd7, 0x09, 0x06, 0x09, 0x12, 0x84, 0x51, 0xa3, 0x5f, 0x3a, 0xaa, 0x17, 0x24, + 0xc2, 0x5c, 0xa0, 0x28, 0x4e, 0x0d, 0x48, 0xd7, 0x77, 0x7c, 0x96, 0x60, 0xc7, 0x0f, 0x09, 0xa6, + 0x42, 0x46, 0xa8, 0x57, 0xc6, 0xc0, 0x91, 0x06, 0x21, 0xe9, 0xf5, 0x85, 0x16, 0x73, 0x47, 0x60, + 0x1a, 0xe0, 0x24, 0x22, 0xda, 0xf8, 0xe0, 0xcb, 0x00, 0xa0, 0xca, 0xd8, 0x70, 0xd5, 0xd9, 0x27, + 0x49, 0x4a, 0xf3, 0x72, 0x0e, 0xe3, 0x27, 0xa3, 0x58, 0x30, 0x67, 0x0f, 0x8f, 0x4c, 0x0a, 0x6a, + 0xbf, 0x14, 0xc0, 0x44, 0x0b, 0x25, 0x28, 0xe2, 0xf0, 0x5d, 0x50, 0x11, 0xc9, 0x80, 0x0b, 0x42, + 0x7b, 0x5e, 0x8c, 0x13, 0xc2, 0x02, 0x6f, 0x37, 0x41, 0xbe, 0x8c, 0xa8, 0x62, 0x2d, 0x5b, 0x2b, + 0x53, 0xee, 0x85, 0x54, 0xdf, 0x52, 0xea, 0x3b, 0x46, 0x0b, 0x6f, 0x81, 0x8b, 0x21, 0x19, 0x62, + 0x8a, 0x39, 0xf7, 0x7a, 0x09, 0xf2, 0xf1, 0x01, 0x70, 0x5c, 0x01, 0xe7, 0x53, 0xf5, 0x5d, 0xa9, + 0xcd, 0x70, 0x6d, 0x70, 0x5e, 0x12, 0xf6, 0x64, 0x62, 0xd8, 0x40, 0x98, 0x5d, 0x2b, 0x85, 0x65, + 0x6b, 0xa5, 0x74, 0x73, 0xc1, 0xd6, 0xf9, 0xb3, 0xd3, 0xfc, 0xd9, 0x75, 0x93, 0xdf, 0xf5, 0xc9, + 0xa7, 0xcf, 0x97, 0xc6, 0xbe, 0xfc, 0x63, 0xc9, 0x72, 0xcf, 0x49, 0x7c, 0x47, 0xc3, 0x35, 0x29, + 0xb8, 0x02, 0xca, 0xdd, 0x90, 0xf9, 0x7b, 0x5c, 0xba, 0xf3, 0x70, 0xcc, 0xfc, 0x7e, 0xa5, 0xb8, + 0x6c, 0xad, 0x14, 0xdc, 0x59, 0x2d, 0x6f, 0xe1, 0xa4, 0x21, 0xa5, 0xb0, 0x09, 0xae, 0x46, 0xe8, + 0xb1, 0x97, 0xd6, 0x8f, 0xe7, 0x33, 0xca, 0x31, 0xe5, 0x03, 0xee, 0x0d, 0x51, 0x48, 0x02, 0x24, + 0x58, 0xc2, 0x2b, 0x67, 0x14, 0xb4, 0x1a, 0xa1, 0xc7, 0x2d, 0x63, 0xb7, 0x91, 0x9a, 0x7d, 0x98, + 0x59, 0x41, 0x1f, 0xcc, 0xef, 0x62, 0xac, 0xb7, 0x54, 0xbb, 0x78, 0x28, 0x62, 0x03, 0x2a, 0x2a, + 0x13, 0x32, 0xfe, 0xf5, 0xb7, 0x25, 0xe1, 0xdf, 0x9e, 0x2f, 0xcd, 0xeb, 0xd2, 0xe2, 0xc1, 0x9e, + 0x4d, 0x98, 0x13, 0x21, 0xd1, 0xb7, 0x9b, 0x54, 0xfc, 0xfc, 0xfd, 0x0d, 0x60, 0xea, 0xb2, 0x49, + 0xc5, 0xb7, 0x7f, 0x7d, 0x77, 0xdd, 0x72, 0xa1, 0x74, 0xd7, 0xc2, 0xc9, 0xba, 0x74, 0xb6, 0xa6, + 0x7c, 0xc1, 0xb7, 0x00, 0x8c, 0x08, 0xf5, 0x02, 0x1c, 0x33, 0x4e, 0x84, 0xde, 0x87, 0x57, 0xce, + 0x2e, 0x5b, 0x2b, 0x45, 0xb7, 0x1c, 0x11, 0x5a, 0xd7, 0x0a, 0x05, 0xe1, 0xb5, 0x37, 0x41, 0x69, + 0x2d, 0x08, 0x12, 0xcc, 0xf9, 0x36, 0xe1, 0x02, 0x5e, 0x06, 0x53, 0x48, 0x7f, 0x62, 0x5e, 0xb1, + 0x96, 0x0b, 0x2b, 0xd3, 0xee, 0x81, 0xa0, 0xf6, 0x31, 0x58, 0xc8, 0xa2, 0x69, 0x63, 0xb1, 0xd1, + 0x47, 0xb4, 0x87, 0x5b, 0xc8, 0xdf, 0xc3, 0x82, 0xc3, 0xdb, 0xa0, 0x18, 0x12, 0x2e, 0x14, 0xaa, + 0x74, 0xf3, 0x9a, 0xad, 0x6e, 0xdd, 0x70, 0xd5, 0x7e, 0x15, 0xa2, 0x8e, 0x04, 0x5a, 0x2f, 0xca, + 0x90, 0x5d, 0x05, 0xac, 0x7d, 0x61, 0x81, 0xca, 0x16, 0x1e, 0xad, 0x71, 0x4e, 0x7a, 0x34, 0xc2, + 0x54, 0xb8, 0x38, 0x0e, 0x91, 0x8f, 0xe5, 0x12, 0xfe, 0x1f, 0xcc, 0x64, 0x27, 0x20, 0x09, 0xa9, + 0x5a, 0x9b, 0x76, 0xa7, 0x53, 0xa1, 0x0c, 0x02, 0xbe, 0x07, 0x40, 0x9c, 0xe0, 0xa1, 0xe7, 0x7b, + 0x7b, 0x78, 0xa4, 0x8a, 0xaa, 0x74, 0xf3, 0xb2, 0x9d, 0xab, 0x7f, 0x5d, 0xd9, 0x76, 0x6b, 0xd0, + 0x0d, 0x89, 0xbf, 0x85, 0x47, 0xee, 0xa4, 0xb4, 0xdf, 0xd8, 0xc2, 0x23, 0x38, 0x07, 0xce, 0xc4, + 0x6c, 0x1f, 0x27, 0xaa, 0xae, 0x0a, 0xae, 0xfe, 0xa8, 0x7d, 0x6d, 0x81, 0x8b, 0x59, 0x00, 0xf2, + 0x48, 0x07, 0x11, 0x4e, 0x5a, 0x83, 0xae, 0x44, 0x2c, 0x81, 0x92, 0x6f, 0x24, 0x1e, 0x09, 0x14, + 0xa1, 0xa2, 0x0b, 0x52, 0x51, 0x33, 0x38, 0xce, 0x79, 0xfc, 0x04, 0xce, 0xb7, 0xc1, 0x74, 0xe6, + 0x45, 0xb2, 0x2e, 0x9c, 0x82, 0x75, 0xb6, 0xef, 0x16, 0x1e, 0xd5, 0x3e, 0xcb, 0x53, 0x5c, 0x1f, + 0xa5, 0x24, 0x95, 0xf3, 0xd3, 0x50, 0xcc, 0x0c, 0xf2, 0x14, 0xfd, 0xbc, 0x97, 0x63, 0x71, 0x14, + 0x8e, 0xc7, 0x51, 0xfb, 0xd1, 0x02, 0x73, 0xf9, 0xbd, 0x79, 0x87, 0xb5, 0x92, 0x01, 0xc5, 0xff, + 0xce, 0xe1, 0x36, 0x98, 0x8c, 0xa5, 0xa5, 0x27, 0xb8, 0x39, 0xb3, 0xc5, 0x63, 0x97, 0xba, 0x93, + 0x3e, 0x8a, 0xfa, 0x56, 0x3f, 0x91, 0xb7, 0xfa, 0xac, 0x42, 0x75, 0x38, 0xac, 0x83, 0xd9, 0x43, + 0x41, 0x70, 0x93, 0xc4, 0x2b, 0xf6, 0xd1, 0x97, 0xdf, 0xce, 0xd5, 0xba, 0x3b, 0x93, 0x0f, 0x92, + 0xd7, 0x7e, 0xb0, 0x00, 0x3c, 0x7e, 0x69, 0xe5, 0x75, 0x3a, 0x74, 0xf5, 0xf3, 0xd5, 0x57, 0x8e, + 0x73, 0x97, 0x5d, 0xa5, 0x2a, 0xab, 0xa2, 0xf1, 0x5c, 0x15, 0xc1, 0xf7, 0x01, 0x88, 0xd5, 0xe1, + 0x9d, 0xfa, 0x84, 0xa7, 0xe2, 0x74, 0x29, 0xf3, 0xf7, 0x88, 0x11, 0xea, 0xf5, 0xb1, 0x7c, 0xe2, + 0xcd, 0x23, 0x05, 0xa4, 0x68, 0x53, 0x49, 0x6a, 0x01, 0x28, 0xa7, 0x89, 0xbf, 0x87, 0x05, 0x0a, + 0x90, 0x40, 0x10, 0x82, 0x22, 0x45, 0x11, 0x36, 0x2f, 0xb2, 0x5a, 0xc3, 0x65, 0x50, 0x0a, 0x30, + 0xf7, 0x13, 0x12, 0xe7, 0xde, 0xdc, 0xbc, 0x08, 0x2e, 0x82, 0xc9, 0xc8, 0x78, 0x50, 0x2c, 0xa7, + 0xdc, 0xec, 0xbb, 0xf6, 0x55, 0x11, 0x2c, 0xa7, 0xdb, 0x34, 0x29, 0x11, 0x04, 0x85, 0xe4, 0x53, + 0xf5, 0xd0, 0xaa, 0xc6, 0x80, 0x05, 0x4e, 0x38, 0xbc, 0x0b, 0x66, 0x89, 0xd6, 0xa5, 0x74, 0x2d, + 0x73, 0xa0, 0xa4, 0xeb, 0xdb, 0xb2, 0x89, 0xd9, 0xa6, 0x75, 0x0d, 0x57, 0x6d, 0x4d, 0xdf, 0x3c, + 0x01, 0x33, 0x06, 0xa7, 0x85, 0xf0, 0x2a, 0x98, 0xee, 0xc9, 0x56, 0x40, 0xb8, 0xd7, 0x47, 0xbc, + 0x6f, 0xca, 0xb2, 0x64, 0x64, 0x9b, 0x88, 0xf7, 0x65, 0x5e, 0xba, 0x84, 0xa2, 0x64, 0xa4, 0x2d, + 0x74, 0x4d, 0x02, 0x2d, 0x52, 0x06, 0x1b, 0x00, 0xf0, 0x18, 0xed, 0x53, 0xd5, 0x38, 0x54, 0xde, + 0x4e, 0x5b, 0x59, 0x53, 0x0a, 0x27, 0x35, 0x70, 0x07, 0x94, 0x07, 0xb4, 0xcb, 0x68, 0x70, 0xd0, + 0xef, 0xd4, 0x63, 0x7f, 0xca, 0xce, 0xf3, 0xbf, 0x0c, 0x6c, 0xfa, 0xce, 0x2b, 0x9a, 0xd9, 0xc4, + 0x7f, 0x6a, 0x66, 0x37, 0x00, 0xec, 0x13, 0x2e, 0x58, 0x42, 0x7c, 0x14, 0x7a, 0x98, 0x8a, 0x84, + 0x60, 0xfd, 0xe4, 0x17, 0xdc, 0x73, 0x07, 0x9a, 0x86, 0x56, 0x48, 0x0e, 0x1c, 0xed, 0x62, 0x2f, + 0x62, 0x01, 0xf6, 0x44, 0x3f, 0xc1, 0xbc, 0xcf, 0xc2, 0xa0, 0x32, 0xf9, 0x1a, 0x1c, 0x24, 0xfe, + 0x1e, 0x0b, 0x70, 0x27, 0x45, 0xd7, 0x96, 0x40, 0x29, 0x2b, 0x8f, 0x80, 0xc3, 0x32, 0x28, 0x90, + 0x40, 0xb7, 0x90, 0xa2, 0x2b, 0x97, 0xb5, 0xbf, 0x2d, 0x30, 0xd7, 0xa4, 0x69, 0xcb, 0xcf, 0x15, + 0xcd, 0x1d, 0x50, 0x0a, 0xd8, 0xa0, 0x1b, 0x62, 0x4f, 0x3e, 0xfc, 0xa6, 0x62, 0xae, 0x1d, 0xbf, + 0xbb, 0xed, 0x10, 0xf1, 0xfe, 0x07, 0x88, 0x84, 0x07, 0x58, 0x17, 0x68, 0x64, 0x9b, 0xf4, 0x28, + 0x5c, 0x03, 0x93, 0x01, 0xdb, 0xa7, 0xea, 0xb4, 0xc7, 0x5f, 0xc7, 0x49, 0x06, 0x83, 0x1f, 0x81, + 0xf9, 0x74, 0x6d, 0x46, 0x94, 0xd7, 0x1f, 0x36, 0xce, 0xa7, 0x1e, 0xd4, 0x14, 0xa3, 0x4f, 0xa8, + 0xf6, 0xbb, 0x05, 0xce, 0x9f, 0xb0, 0x35, 0xfc, 0x04, 0xcc, 0x72, 0x29, 0x3e, 0x3c, 0x43, 0x4d, + 0xaf, 0xdf, 0x32, 0xa3, 0xc0, 0xa5, 0xe3, 0xa3, 0xc0, 0x36, 0xee, 0x21, 0x7f, 0x54, 0xc7, 0x7e, + 0x6e, 0x20, 0xa8, 0x63, 0x5f, 0x0f, 0x04, 0x33, 0xca, 0x5b, 0x36, 0x3a, 0x6d, 0x82, 0x99, 0x47, + 0x88, 0x84, 0x5e, 0x3a, 0x73, 0x9a, 0xbc, 0x9c, 0x2a, 0x8e, 0x69, 0x89, 0x4c, 0xe5, 0x72, 0x30, + 0x10, 0x2c, 0xea, 0x72, 0xc1, 0x28, 0x56, 0xd9, 0x98, 0x74, 0x0f, 0x04, 0xd7, 0x7f, 0xb2, 0xc0, + 0x4c, 0xd6, 0x1d, 0xfb, 0x88, 0x63, 0x58, 0x05, 0x8b, 0x1b, 0xf7, 0x77, 0xda, 0x0f, 0xee, 0x35, + 0x5c, 0xaf, 0xb5, 0xb9, 0xd6, 0x6e, 0x78, 0x0f, 0x76, 0xda, 0xad, 0xc6, 0x46, 0xf3, 0x4e, 0xb3, + 0x51, 0x2f, 0x8f, 0xc1, 0x2b, 0x60, 0xe1, 0x88, 0xde, 0x6d, 0xdc, 0x6d, 0xb6, 0x3b, 0x0d, 0xb7, + 0x51, 0x2f, 0x5b, 0x27, 0xc0, 0x9b, 0x3b, 0xcd, 0x4e, 0x73, 0x6d, 0xbb, 0xf9, 0xb0, 0x51, 0x2f, + 0x8f, 0xc3, 0x4b, 0xe0, 0xe2, 0x11, 0xfd, 0xf6, 0xda, 0x83, 0x9d, 0x8d, 0xcd, 0x46, 0xbd, 0x5c, + 0x80, 0x8b, 0xe0, 0xc2, 0x11, 0x65, 0xbb, 0x73, 0xbf, 0xd5, 0x6a, 0xd4, 0xcb, 0xc5, 0x13, 0x74, + 0xf5, 0xc6, 0x76, 0xa3, 0xd3, 0xa8, 0x97, 0xcf, 0x2c, 0x16, 0x3f, 0xff, 0xa6, 0x3a, 0xb6, 0xde, + 0x7c, 0xfa, 0xa2, 0x6a, 0x3d, 0x7b, 0x51, 0xb5, 0xfe, 0x7c, 0x51, 0xb5, 0x9e, 0xbc, 0xac, 0x8e, + 0x3d, 0x7b, 0x59, 0x1d, 0xfb, 0xf5, 0x65, 0x75, 0xec, 0xa1, 0xd3, 0x23, 0xa2, 0x3f, 0xe8, 0xda, + 0x3e, 0x8b, 0x1c, 0x14, 0x86, 0x84, 0x76, 0x89, 0xe0, 0x8e, 0x1a, 0xa6, 0x1f, 0x3b, 0x87, 0xff, + 0x85, 0x88, 0x51, 0x8c, 0x79, 0x77, 0x42, 0xe5, 0xf7, 0x9d, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, + 0x1e, 0x3a, 0x91, 0xca, 0xa3, 0x0c, 0x00, 0x00, } func (m *Params) Marshal() (dAtA []byte, err error) { @@ -1072,7 +1096,7 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { if m.MinDepositBlocks != 0 { i = encodeVarintProvider(dAtA, i, uint64(m.MinDepositBlocks)) i-- - dAtA[i] = 0x30 + dAtA[i] = 0x38 } { size := m.FeesPerBlockAmount.Size() @@ -1083,16 +1107,16 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { i = encodeVarintProvider(dAtA, i, uint64(size)) } i-- - dAtA[i] = 0x2a + dAtA[i] = 0x32 if m.MaxProviderConsensusValidators != 0 { i = encodeVarintProvider(dAtA, i, uint64(m.MaxProviderConsensusValidators)) i-- - dAtA[i] = 0x20 + dAtA[i] = 0x28 } if m.BlocksPerEpoch != 0 { i = encodeVarintProvider(dAtA, i, uint64(m.BlocksPerEpoch)) i-- - dAtA[i] = 0x18 + dAtA[i] = 0x20 } n1, err1 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.VaasTimeoutPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.VaasTimeoutPeriod):]) if err1 != nil { @@ -1101,7 +1125,14 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { i -= n1 i = encodeVarintProvider(dAtA, i, uint64(n1)) i-- - dAtA[i] = 0x12 + dAtA[i] = 0x1a + if len(m.LivenessGraceFraction) > 0 { + i -= len(m.LivenessGraceFraction) + copy(dAtA[i:], m.LivenessGraceFraction) + i = encodeVarintProvider(dAtA, i, uint64(len(m.LivenessGraceFraction))) + i-- + dAtA[i] = 0x12 + } if len(m.TrustingPeriodFraction) > 0 { i -= len(m.TrustingPeriodFraction) copy(dAtA[i:], m.TrustingPeriodFraction) @@ -1481,34 +1512,42 @@ func (m *ConsumerInitializationParameters) MarshalToSizedBuffer(dAtA []byte) (in _ = i var l int _ = l - if m.HistoricalEntries != 0 { - i = encodeVarintProvider(dAtA, i, uint64(m.HistoricalEntries)) - i-- - dAtA[i] = 0x38 - } - n7, err7 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.VaasTimeoutPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.VaasTimeoutPeriod):]) + n7, err7 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.SafeModeThreshold, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.SafeModeThreshold):]) if err7 != nil { return 0, err7 } i -= n7 i = encodeVarintProvider(dAtA, i, uint64(n7)) i-- - dAtA[i] = 0x32 - n8, err8 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.UnbondingPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.UnbondingPeriod):]) + dAtA[i] = 0x42 + if m.HistoricalEntries != 0 { + i = encodeVarintProvider(dAtA, i, uint64(m.HistoricalEntries)) + i-- + dAtA[i] = 0x38 + } + n8, err8 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.VaasTimeoutPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.VaasTimeoutPeriod):]) if err8 != nil { return 0, err8 } i -= n8 i = encodeVarintProvider(dAtA, i, uint64(n8)) i-- - dAtA[i] = 0x2a - n9, err9 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(m.SpawnTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(m.SpawnTime):]) + dAtA[i] = 0x32 + n9, err9 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.UnbondingPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.UnbondingPeriod):]) if err9 != nil { return 0, err9 } i -= n9 i = encodeVarintProvider(dAtA, i, uint64(n9)) i-- + dAtA[i] = 0x2a + n10, err10 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(m.SpawnTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(m.SpawnTime):]) + if err10 != nil { + return 0, err10 + } + i -= n10 + i = encodeVarintProvider(dAtA, i, uint64(n10)) + i-- dAtA[i] = 0x22 if len(m.BinaryHash) > 0 { i -= len(m.BinaryHash) @@ -1558,20 +1597,20 @@ func (m *ConsumerIds) MarshalToSizedBuffer(dAtA []byte) (int, error) { var l int _ = l if len(m.Ids) > 0 { - dAtA12 := make([]byte, len(m.Ids)*10) - var j11 int + dAtA13 := make([]byte, len(m.Ids)*10) + var j12 int for _, num := range m.Ids { for num >= 1<<7 { - dAtA12[j11] = uint8(uint64(num)&0x7f | 0x80) + dAtA13[j12] = uint8(uint64(num)&0x7f | 0x80) num >>= 7 - j11++ + j12++ } - dAtA12[j11] = uint8(num) - j11++ + dAtA13[j12] = uint8(num) + j12++ } - i -= j11 - copy(dAtA[i:], dAtA12[:j11]) - i = encodeVarintProvider(dAtA, i, uint64(j11)) + i -= j12 + copy(dAtA[i:], dAtA13[:j12]) + i = encodeVarintProvider(dAtA, i, uint64(j12)) i-- dAtA[i] = 0xa } @@ -1598,12 +1637,12 @@ func (m *InfractionParameters) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - n13, err13 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.DowntimeGracePeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.DowntimeGracePeriod):]) - if err13 != nil { - return 0, err13 + n14, err14 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.DowntimeGracePeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.DowntimeGracePeriod):]) + if err14 != nil { + return 0, err14 } - i -= n13 - i = encodeVarintProvider(dAtA, i, uint64(n13)) + i -= n14 + i = encodeVarintProvider(dAtA, i, uint64(n14)) i-- dAtA[i] = 0x1a if m.Downtime != nil { @@ -1663,12 +1702,12 @@ func (m *SlashJailParameters) MarshalToSizedBuffer(dAtA []byte) (int, error) { i-- dAtA[i] = 0x18 } - n16, err16 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.JailDuration, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.JailDuration):]) - if err16 != nil { - return 0, err16 + n17, err17 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.JailDuration, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.JailDuration):]) + if err17 != nil { + return 0, err17 } - i -= n16 - i = encodeVarintProvider(dAtA, i, uint64(n16)) + i -= n17 + i = encodeVarintProvider(dAtA, i, uint64(n17)) i-- dAtA[i] = 0x12 { @@ -1705,6 +1744,10 @@ func (m *Params) Size() (n int) { if l > 0 { n += 1 + l + sovProvider(uint64(l)) } + l = len(m.LivenessGraceFraction) + if l > 0 { + n += 1 + l + sovProvider(uint64(l)) + } l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.VaasTimeoutPeriod) n += 1 + l + sovProvider(uint64(l)) if m.BlocksPerEpoch != 0 { @@ -1898,6 +1941,8 @@ func (m *ConsumerInitializationParameters) Size() (n int) { if m.HistoricalEntries != 0 { n += 1 + sovProvider(uint64(m.HistoricalEntries)) } + l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.SafeModeThreshold) + n += 1 + l + sovProvider(uint64(l)) return n } @@ -2020,6 +2065,38 @@ func (m *Params) Unmarshal(dAtA []byte) error { m.TrustingPeriodFraction = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LivenessGraceFraction", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProvider + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthProvider + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthProvider + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.LivenessGraceFraction = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field VaasTimeoutPeriod", wireType) } @@ -2052,7 +2129,7 @@ func (m *Params) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex - case 3: + case 4: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field BlocksPerEpoch", wireType) } @@ -2071,7 +2148,7 @@ func (m *Params) Unmarshal(dAtA []byte) error { break } } - case 4: + case 5: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field MaxProviderConsensusValidators", wireType) } @@ -2090,7 +2167,7 @@ func (m *Params) Unmarshal(dAtA []byte) error { break } } - case 5: + case 6: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field FeesPerBlockAmount", wireType) } @@ -2124,7 +2201,7 @@ func (m *Params) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex - case 6: + case 7: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field MinDepositBlocks", wireType) } @@ -3435,6 +3512,39 @@ func (m *ConsumerInitializationParameters) Unmarshal(dAtA []byte) error { break } } + case 8: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SafeModeThreshold", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProvider + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthProvider + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthProvider + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_cosmos_gogoproto_types.StdDurationUnmarshal(&m.SafeModeThreshold, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipProvider(dAtA[iNdEx:]) diff --git a/x/vaas/types/params.go b/x/vaas/types/params.go index f3e9344..9747387 100644 --- a/x/vaas/types/params.go +++ b/x/vaas/types/params.go @@ -24,12 +24,14 @@ func NewConsumerParams(enabled bool, vaasTimeoutPeriod time.Duration, historicalEntries int64, consumerUnbondingPeriod time.Duration, + safeModeThreshold time.Duration, ) ConsumerParams { return ConsumerParams{ Enabled: enabled, VaasTimeoutPeriod: vaasTimeoutPeriod, HistoricalEntries: historicalEntries, UnbondingPeriod: consumerUnbondingPeriod, + SafeModeThreshold: safeModeThreshold, } } @@ -40,6 +42,7 @@ func DefaultConsumerParams() ConsumerParams { DefaultVAASTimeoutPeriod, DefaultHistoricalEntries, DefaultConsumerUnbondingPeriod, + DefaultSafeModeThreshold, ) } @@ -54,5 +57,8 @@ func (p ConsumerParams) Validate() error { if err := ValidateDuration(p.UnbondingPeriod); err != nil { return err } + if err := ValidateDuration(p.SafeModeThreshold); err != nil { + return err + } return nil } diff --git a/x/vaas/types/shared_consumer.pb.go b/x/vaas/types/shared_consumer.pb.go index 3a265cd..82ddfb9 100644 --- a/x/vaas/types/shared_consumer.pb.go +++ b/x/vaas/types/shared_consumer.pb.go @@ -47,6 +47,9 @@ type ConsumerParams struct { // Unbonding period for the consumer, // which should be smaller than that of the provider in general. UnbondingPeriod time.Duration `protobuf:"bytes,4,opt,name=unbonding_period,json=unbondingPeriod,proto3,stdduration" json:"unbonding_period"` + // How long the consumer may go without receiving a VSC packet before its + // tx admission gate enters safe mode (only ibc.core and gov messages pass). + SafeModeThreshold time.Duration `protobuf:"bytes,5,opt,name=safe_mode_threshold,json=safeModeThreshold,proto3,stdduration" json:"safe_mode_threshold"` } func (m *ConsumerParams) Reset() { *m = ConsumerParams{} } @@ -110,6 +113,13 @@ func (m *ConsumerParams) GetUnbondingPeriod() time.Duration { return 0 } +func (m *ConsumerParams) GetSafeModeThreshold() time.Duration { + if m != nil { + return m.SafeModeThreshold + } + return 0 +} + // ConsumerGenesisState defines shared genesis information between provider and // consumer type ConsumerGenesisState struct { @@ -258,42 +268,44 @@ func init() { func init() { proto.RegisterFile("vaas/v1/shared_consumer.proto", fileDescriptor_58a750255ca5daa3) } var fileDescriptor_58a750255ca5daa3 = []byte{ - // 560 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x53, 0x41, 0x6f, 0xd4, 0x3c, - 0x10, 0xdd, 0x74, 0xab, 0x36, 0x9f, 0xb7, 0x5f, 0x4b, 0x4d, 0x11, 0x4b, 0x2b, 0xd2, 0x55, 0x0f, - 0x68, 0x25, 0x84, 0xad, 0x16, 0x21, 0xae, 0xb4, 0x05, 0x21, 0x2e, 0x55, 0x95, 0x85, 0x22, 0x71, - 0x89, 0x9c, 0x64, 0x9a, 0xb5, 0x94, 0xb5, 0x23, 0xdb, 0x49, 0xe1, 0x17, 0x70, 0xe5, 0xc8, 0x9f, - 0xe1, 0xde, 0x63, 0x8f, 0x9c, 0x00, 0x75, 0xff, 0x07, 0x42, 0x71, 0x9c, 0x6d, 0x7a, 0x82, 0x53, - 0xfc, 0x3c, 0xf3, 0x26, 0x6f, 0x9e, 0x67, 0xd0, 0xc3, 0x8a, 0x31, 0x4d, 0xab, 0x7d, 0xaa, 0xa7, - 0x4c, 0x41, 0x1a, 0x25, 0x52, 0xe8, 0x72, 0x06, 0x8a, 0x14, 0x4a, 0x1a, 0x89, 0x57, 0xeb, 0x30, - 0xa9, 0xf6, 0xb7, 0x77, 0x0c, 0x88, 0x14, 0xd4, 0x8c, 0x0b, 0x43, 0x59, 0x9c, 0x70, 0x6a, 0x3e, - 0x15, 0xa0, 0x9b, 0xac, 0x6d, 0xca, 0xe3, 0x84, 0xe6, 0x3c, 0x9b, 0x9a, 0x24, 0xe7, 0x20, 0x8c, - 0xa6, 0x9d, 0xec, 0x6a, 0xbf, 0x83, 0x1c, 0x21, 0xc8, 0xa4, 0xcc, 0x72, 0xa0, 0x16, 0xc5, 0xe5, - 0x39, 0x4d, 0x4b, 0xc5, 0x0c, 0x97, 0xc2, 0xc5, 0xb7, 0x32, 0x99, 0x49, 0x7b, 0xa4, 0xf5, 0xa9, - 0xb9, 0xdd, 0xfb, 0xed, 0xa1, 0xf5, 0x63, 0xa7, 0xef, 0x94, 0x29, 0x36, 0xd3, 0x78, 0x88, 0x56, - 0x41, 0xb0, 0x38, 0x87, 0x74, 0xe8, 0x8d, 0xbc, 0xb1, 0x1f, 0xb6, 0x10, 0x4f, 0xd0, 0xdd, 0x5a, - 0x7b, 0x64, 0xf8, 0x0c, 0x64, 0x69, 0xa2, 0x02, 0x14, 0x97, 0xe9, 0x70, 0x69, 0xe4, 0x8d, 0x07, - 0x07, 0x0f, 0x48, 0x23, 0x80, 0xb4, 0x02, 0xc8, 0x4b, 0x27, 0xe0, 0xc8, 0xbf, 0xfc, 0xb1, 0xdb, - 0xfb, 0xfa, 0x73, 0xd7, 0x0b, 0x37, 0x6b, 0xfe, 0xdb, 0x86, 0x7e, 0x6a, 0xd9, 0xf8, 0x09, 0xc2, - 0x53, 0xae, 0x8d, 0x54, 0x3c, 0x61, 0x79, 0x04, 0xc2, 0x28, 0x0e, 0x7a, 0xd8, 0x1f, 0x79, 0xe3, - 0x7e, 0xb8, 0x79, 0x13, 0x79, 0xd5, 0x04, 0xf0, 0x09, 0xba, 0x53, 0x8a, 0x58, 0x8a, 0x94, 0x8b, - 0xac, 0x15, 0xb0, 0xfc, 0xef, 0x02, 0x36, 0x16, 0xe4, 0xe6, 0xf7, 0x7b, 0xdf, 0x3c, 0xb4, 0xd5, - 0x1a, 0xf0, 0x1a, 0x04, 0x68, 0xae, 0x27, 0x86, 0x19, 0xc0, 0xcf, 0xd0, 0x4a, 0x61, 0x0d, 0xb1, - 0x2e, 0x0c, 0x0e, 0xee, 0x13, 0xf7, 0x6e, 0xe4, 0xb6, 0x5f, 0x47, 0xcb, 0x75, 0xf1, 0xd0, 0x25, - 0xe3, 0xe7, 0xc8, 0x2f, 0x94, 0xac, 0x78, 0x0a, 0xca, 0x19, 0x73, 0x6f, 0x41, 0x3c, 0x75, 0x81, - 0x37, 0xe2, 0x5c, 0x3a, 0xda, 0x22, 0x19, 0xef, 0xa0, 0xff, 0x04, 0x5c, 0x44, 0xc9, 0x94, 0x71, - 0x61, 0xdb, 0xf7, 0x43, 0x5f, 0xc0, 0xc5, 0x71, 0x8d, 0xeb, 0x37, 0x29, 0x14, 0x9c, 0x1d, 0x1e, - 0x4e, 0x6c, 0xb3, 0x7e, 0xd8, 0xc2, 0xbd, 0xcf, 0x4b, 0x68, 0xad, 0x5b, 0x17, 0x9f, 0xa0, 0xb5, - 0x66, 0x62, 0x22, 0x5d, 0xf7, 0xe1, 0xd4, 0x3f, 0x26, 0x3c, 0x4e, 0x48, 0x77, 0x9e, 0x48, 0x67, - 0x82, 0xea, 0xa6, 0xec, 0xad, 0x6d, 0x3d, 0x1c, 0x24, 0x37, 0x00, 0xbf, 0x47, 0x1b, 0xf5, 0x00, - 0x83, 0xd0, 0xa5, 0x76, 0x25, 0x9b, 0xbe, 0xc8, 0x5f, 0x4b, 0xb6, 0xb4, 0xa6, 0xea, 0x7a, 0x72, - 0x0b, 0xe3, 0x13, 0xb4, 0xc1, 0x05, 0x37, 0x9c, 0xe5, 0x51, 0xc5, 0xf2, 0x48, 0x83, 0x19, 0xf6, - 0x47, 0xfd, 0xf1, 0xe0, 0x60, 0xd4, 0xad, 0x53, 0x2f, 0x06, 0x39, 0x63, 0x39, 0x4f, 0x99, 0x91, - 0xea, 0x5d, 0x91, 0x32, 0x03, 0xce, 0xbb, 0xff, 0x1d, 0xfd, 0x8c, 0xe5, 0x13, 0x30, 0x47, 0x2f, - 0x2e, 0xaf, 0x03, 0xef, 0xea, 0x3a, 0xf0, 0x7e, 0x5d, 0x07, 0xde, 0x97, 0x79, 0xd0, 0xbb, 0x9a, - 0x07, 0xbd, 0xef, 0xf3, 0xa0, 0xf7, 0xe1, 0x51, 0xc6, 0xcd, 0xb4, 0x8c, 0x49, 0x22, 0x67, 0x94, - 0xe5, 0x39, 0x17, 0x31, 0x37, 0x9a, 0xda, 0x2d, 0xfd, 0xd8, 0x7c, 0xec, 0xe6, 0xc5, 0x2b, 0x76, - 0x72, 0x9e, 0xfe, 0x09, 0x00, 0x00, 0xff, 0xff, 0xa2, 0x7b, 0x1a, 0x0c, 0xc1, 0x03, 0x00, 0x00, + // 588 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0x41, 0x4f, 0xd4, 0x40, + 0x14, 0xde, 0xb2, 0x08, 0xeb, 0x2c, 0x82, 0x14, 0x8c, 0x2b, 0xc4, 0xb2, 0xe1, 0x60, 0x36, 0x31, + 0x4e, 0x03, 0xc6, 0x78, 0x15, 0xd0, 0x18, 0x0f, 0x12, 0xd2, 0x45, 0x4c, 0xbc, 0x34, 0xd3, 0xf6, + 0xd1, 0x4e, 0x32, 0x3b, 0xd3, 0xcc, 0x4c, 0x17, 0xfd, 0x05, 0x5e, 0x3d, 0xfa, 0x67, 0xbc, 0x73, + 0x24, 0xf1, 0xe2, 0x49, 0x0d, 0xfc, 0x11, 0x33, 0xd3, 0xe9, 0x52, 0x4e, 0xea, 0xa9, 0xf3, 0xcd, + 0x7b, 0xdf, 0xeb, 0xf7, 0xbe, 0x79, 0x0f, 0x3d, 0x9c, 0x12, 0xa2, 0xc2, 0xe9, 0x4e, 0xa8, 0x0a, + 0x22, 0x21, 0x8b, 0x53, 0xc1, 0x55, 0x35, 0x01, 0x89, 0x4b, 0x29, 0xb4, 0xf0, 0x17, 0x4d, 0x18, + 0x4f, 0x77, 0x36, 0x36, 0x35, 0xf0, 0x0c, 0xe4, 0x84, 0x72, 0x1d, 0x92, 0x24, 0xa5, 0xa1, 0xfe, + 0x54, 0x82, 0xaa, 0xb3, 0x36, 0x42, 0x9a, 0xa4, 0x21, 0xa3, 0x79, 0xa1, 0x53, 0x46, 0x81, 0x6b, + 0x15, 0xb6, 0xb2, 0xa7, 0x3b, 0x2d, 0xe4, 0x08, 0x41, 0x2e, 0x44, 0xce, 0x20, 0xb4, 0x28, 0xa9, + 0x4e, 0xc3, 0xac, 0x92, 0x44, 0x53, 0xc1, 0x5d, 0x7c, 0x3d, 0x17, 0xb9, 0xb0, 0xc7, 0xd0, 0x9c, + 0xea, 0xdb, 0xed, 0xef, 0x73, 0x68, 0xf9, 0xc0, 0xe9, 0x3b, 0x22, 0x92, 0x4c, 0x94, 0x3f, 0x40, + 0x8b, 0xc0, 0x49, 0xc2, 0x20, 0x1b, 0x78, 0x43, 0x6f, 0xd4, 0x8b, 0x1a, 0xe8, 0x8f, 0xd1, 0x9a, + 0xd1, 0x1e, 0x6b, 0x3a, 0x01, 0x51, 0xe9, 0xb8, 0x04, 0x49, 0x45, 0x36, 0x98, 0x1b, 0x7a, 0xa3, + 0xfe, 0xee, 0x03, 0x5c, 0x0b, 0xc0, 0x8d, 0x00, 0xfc, 0xd2, 0x09, 0xd8, 0xef, 0x9d, 0xff, 0xdc, + 0xea, 0x7c, 0xfd, 0xb5, 0xe5, 0x45, 0xab, 0x86, 0x7f, 0x5c, 0xd3, 0x8f, 0x2c, 0xdb, 0x7f, 0x82, + 0xfc, 0x82, 0x2a, 0x2d, 0x24, 0x4d, 0x09, 0x8b, 0x81, 0x6b, 0x49, 0x41, 0x0d, 0xba, 0x43, 0x6f, + 0xd4, 0x8d, 0x56, 0xaf, 0x23, 0xaf, 0xea, 0x80, 0x7f, 0x88, 0xee, 0x56, 0x3c, 0x11, 0x3c, 0xa3, + 0x3c, 0x6f, 0x04, 0xcc, 0xff, 0xbb, 0x80, 0x95, 0x19, 0xd9, 0xfd, 0x7e, 0x8c, 0xd6, 0x14, 0x39, + 0x85, 0x78, 0x22, 0x32, 0x88, 0x75, 0x21, 0x41, 0x15, 0x82, 0x65, 0x83, 0x5b, 0xff, 0xd1, 0x93, + 0xe1, 0xbf, 0x15, 0x19, 0x1c, 0x37, 0xec, 0xed, 0x6f, 0x1e, 0x5a, 0x6f, 0x5c, 0x7d, 0x0d, 0x1c, + 0x14, 0x55, 0x63, 0x4d, 0x34, 0xf8, 0xcf, 0xd0, 0x42, 0x69, 0x5d, 0xb6, 0xd6, 0xf6, 0x77, 0xef, + 0x63, 0x37, 0x0c, 0xf8, 0xe6, 0x23, 0xec, 0xcf, 0x9b, 0xf2, 0x91, 0x4b, 0xf6, 0x9f, 0xa3, 0x5e, + 0x29, 0xc5, 0x94, 0x66, 0x20, 0x9d, 0xdb, 0xf7, 0x66, 0xc4, 0x23, 0x17, 0x78, 0xc3, 0x4f, 0x85, + 0xa3, 0xcd, 0x92, 0xfd, 0x4d, 0x74, 0x9b, 0xc3, 0x59, 0x9c, 0x16, 0x84, 0x72, 0xeb, 0x69, 0x2f, + 0xea, 0x71, 0x38, 0x3b, 0x30, 0xd8, 0x3c, 0x74, 0x29, 0xe1, 0x64, 0x6f, 0x6f, 0x6c, 0x1d, 0xec, + 0x45, 0x0d, 0xdc, 0xfe, 0x3c, 0x87, 0x96, 0xda, 0x75, 0xfd, 0x43, 0xb4, 0x54, 0x8f, 0x61, 0xac, + 0x4c, 0x1f, 0x4e, 0xfd, 0x63, 0x4c, 0x93, 0x14, 0xb7, 0x87, 0x14, 0xb7, 0xc6, 0xd2, 0x34, 0x65, + 0x6f, 0x6d, 0xeb, 0x51, 0x3f, 0xbd, 0x06, 0xfe, 0x7b, 0xb4, 0x62, 0xb6, 0x02, 0xb8, 0xaa, 0x94, + 0x2b, 0x59, 0xf7, 0x85, 0xff, 0x5a, 0xb2, 0xa1, 0xd5, 0x55, 0x97, 0xd3, 0x1b, 0xd8, 0x3f, 0x44, + 0x2b, 0x94, 0x53, 0x4d, 0x09, 0x8b, 0xa7, 0x84, 0xc5, 0x0a, 0xf4, 0xa0, 0x3b, 0xec, 0x8e, 0xfa, + 0xbb, 0xc3, 0x76, 0x1d, 0xb3, 0x6d, 0xf8, 0x84, 0x30, 0x9a, 0x11, 0x2d, 0xe4, 0xbb, 0x32, 0x23, + 0x1a, 0x9c, 0x77, 0x77, 0x1c, 0xfd, 0x84, 0xb0, 0x31, 0xe8, 0xfd, 0x17, 0xe7, 0x97, 0x81, 0x77, + 0x71, 0x19, 0x78, 0xbf, 0x2f, 0x03, 0xef, 0xcb, 0x55, 0xd0, 0xb9, 0xb8, 0x0a, 0x3a, 0x3f, 0xae, + 0x82, 0xce, 0x87, 0x47, 0x39, 0xd5, 0x45, 0x95, 0xe0, 0x54, 0x4c, 0x42, 0xc2, 0x18, 0xe5, 0x09, + 0xd5, 0x2a, 0xb4, 0xab, 0xff, 0xb1, 0xfe, 0xd8, 0x75, 0x4e, 0x16, 0xec, 0xec, 0x3c, 0xfd, 0x13, + 0x00, 0x00, 0xff, 0xff, 0x73, 0xbe, 0x93, 0xe2, 0x16, 0x04, 0x00, 0x00, } func (m *ConsumerParams) Marshal() (dAtA []byte, err error) { @@ -316,25 +328,33 @@ func (m *ConsumerParams) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - n1, err1 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.UnbondingPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.UnbondingPeriod):]) + n1, err1 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.SafeModeThreshold, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.SafeModeThreshold):]) if err1 != nil { return 0, err1 } i -= n1 i = encodeVarintSharedConsumer(dAtA, i, uint64(n1)) i-- + dAtA[i] = 0x2a + n2, err2 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.UnbondingPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.UnbondingPeriod):]) + if err2 != nil { + return 0, err2 + } + i -= n2 + i = encodeVarintSharedConsumer(dAtA, i, uint64(n2)) + i-- dAtA[i] = 0x22 if m.HistoricalEntries != 0 { i = encodeVarintSharedConsumer(dAtA, i, uint64(m.HistoricalEntries)) i-- dAtA[i] = 0x18 } - n2, err2 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.VaasTimeoutPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.VaasTimeoutPeriod):]) - if err2 != nil { - return 0, err2 + n3, err3 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.VaasTimeoutPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.VaasTimeoutPeriod):]) + if err3 != nil { + return 0, err3 } - i -= n2 - i = encodeVarintSharedConsumer(dAtA, i, uint64(n2)) + i -= n3 + i = encodeVarintSharedConsumer(dAtA, i, uint64(n3)) i-- dAtA[i] = 0x12 if m.Enabled { @@ -501,6 +521,8 @@ func (m *ConsumerParams) Size() (n int) { } l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.UnbondingPeriod) n += 1 + l + sovSharedConsumer(uint64(l)) + l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.SafeModeThreshold) + n += 1 + l + sovSharedConsumer(uint64(l)) return n } @@ -686,6 +708,39 @@ func (m *ConsumerParams) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SafeModeThreshold", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowSharedConsumer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthSharedConsumer + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthSharedConsumer + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_cosmos_gogoproto_types.StdDurationUnmarshal(&m.SafeModeThreshold, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipSharedConsumer(dAtA[iNdEx:]) diff --git a/x/vaas/types/shared_params.go b/x/vaas/types/shared_params.go index cc0f0f4..c54e904 100644 --- a/x/vaas/types/shared_params.go +++ b/x/vaas/types/shared_params.go @@ -9,6 +9,12 @@ import ( ) const ( + // DefaultSafeModeThreshold is how long the consumer may go without + // receiving a VSC packet before its tx admission gate enters safe mode + // (only ibc.core and gov messages pass). Set well below the provider + // liveness grace period. + DefaultSafeModeThreshold = 3 * time.Hour + // DefaultVAASTimeoutPeriod is the IBC packet timeout for VAAS packets. // One epoch-scale value: undelivered packets are superseded the next // epoch and late ones are dropped by the consumer, so a long timeout @@ -19,11 +25,6 @@ const ( // above realistic IBC relay latency so packets are not expired before // they can be delivered. MinVAASTimeoutPeriod = 10 * time.Minute - - // MinConsumerUnbondingPeriod floors the consumer unbonding so the - // relayer-derived trusting period (unbonding * fraction) is at least a - // few days. - MinConsumerUnbondingPeriod = 5 * 24 * time.Hour ) var KeyVAASTimeoutPeriod = []byte("VaasTimeoutPeriod") From 0b39d9a45f6dd464dedabc4503b31bb41cbed698 Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:44:07 +0200 Subject: [PATCH 07/20] add liveness unit tests Provider: block-clock sweep (including boundary and per-consumer partial removal), ack/counter tracking, snapshot resync, timeout/error-ack are log-only, param and query coverage, and the relaxed unbonding bound. Consumer: VSC staleness, snapshot-replace apply, and the safe-mode tx filter. --- x/vaas/consumer/ante/msg_filter_ante_test.go | 30 ++ .../keeper/grpc_staking_query_test.go | 1 + x/vaas/consumer/keeper/params_test.go | 3 +- x/vaas/consumer/keeper/relay_test.go | 286 +++++++++++++++++- x/vaas/consumer/types/genesis_test.go | 2 + x/vaas/consumer/types/params_test.go | 12 +- x/vaas/provider/keeper/grpc_query_test.go | 41 +++ x/vaas/provider/keeper/liveness_test.go | 147 ++++++++- x/vaas/provider/keeper/msg_server_test.go | 89 +++++- x/vaas/provider/keeper/params_test.go | 1 + x/vaas/provider/keeper/relay_test.go | 119 ++++++++ .../keeper/validator_set_update_test.go | 122 ++++++++ x/vaas/provider/types/genesis_test.go | 4 +- x/vaas/provider/types/msg_test.go | 162 +++++++--- x/vaas/provider/types/params_test.go | 10 +- 15 files changed, 967 insertions(+), 62 deletions(-) diff --git a/x/vaas/consumer/ante/msg_filter_ante_test.go b/x/vaas/consumer/ante/msg_filter_ante_test.go index c92cbc6..3c9971f 100644 --- a/x/vaas/consumer/ante/msg_filter_ante_test.go +++ b/x/vaas/consumer/ante/msg_filter_ante_test.go @@ -181,3 +181,33 @@ func TestSafeModeRejectsAppMsgWhenStale(t *testing.T) { func testAccAddress(seed byte) sdk.AccAddress { return sdk.AccAddress(bytes.Repeat([]byte{seed}, 20)) } + +// TestSafeModeAllowsGovWhenStale verifies that when the consumer has an +// established provider client, is not in debt, but has a stale validator set, +// governance messages still pass (restricted mode allows /cosmos.gov.*). +func TestSafeModeAllowsGovWhenStale(t *testing.T) { + msg := &govtypes.MsgVote{ + ProposalId: 1, + Voter: testAccAddress(1).String(), + Option: govtypes.OptionYes, + } + nextCalled, err := runDecorator(t, + mockConsumerKeeper{providerClientFound: true, inDebt: false, vscStale: true}, + []sdk.Msg{msg}, + ) + require.NoError(t, err) + require.True(t, nextCalled) +} + +// TestRestrictedWhenBothDebtAndStale verifies that when the consumer is both +// in debt and has a stale validator set, app messages (e.g. bank) are rejected. +// The combined condition still routes to restricted mode. +func TestRestrictedWhenBothDebtAndStale(t *testing.T) { + nextCalled, err := runDecorator(t, + mockConsumerKeeper{providerClientFound: true, inDebt: true, vscStale: true}, + []sdk.Msg{bankSendMsg()}, + ) + require.Error(t, err) + require.True(t, errorsmod.IsOf(err, consumertypes.ErrConsumerInDebt)) + require.False(t, nextCalled) +} diff --git a/x/vaas/consumer/keeper/grpc_staking_query_test.go b/x/vaas/consumer/keeper/grpc_staking_query_test.go index 7f76f24..820a779 100644 --- a/x/vaas/consumer/keeper/grpc_staking_query_test.go +++ b/x/vaas/consumer/keeper/grpc_staking_query_test.go @@ -31,6 +31,7 @@ func TestStakingQueryServerParams(t *testing.T) { vaastypes.DefaultVAASTimeoutPeriod, 1234, 13*24*time.Hour, + vaastypes.DefaultSafeModeThreshold, )) server := consumerkeeper.NewStakingQueryServer(&consumerKeeper) diff --git a/x/vaas/consumer/keeper/params_test.go b/x/vaas/consumer/keeper/params_test.go index b9f7224..2c9e5d9 100644 --- a/x/vaas/consumer/keeper/params_test.go +++ b/x/vaas/consumer/keeper/params_test.go @@ -21,12 +21,13 @@ func TestParams(t *testing.T) { vaastypes.DefaultVAASTimeoutPeriod, vaastypes.DefaultHistoricalEntries, vaastypes.DefaultConsumerUnbondingPeriod, + vaastypes.DefaultSafeModeThreshold, ) // these are the default params, IBC suite independently sets enabled=true params := consumerKeeper.GetConsumerParams(ctx) require.Equal(t, expParams, params) - newParams := vaastypes.NewConsumerParams(false, 7*24*time.Hour, 500, 24*21*time.Hour) + newParams := vaastypes.NewConsumerParams(false, 7*24*time.Hour, 500, 24*21*time.Hour, vaastypes.DefaultSafeModeThreshold) consumerKeeper.SetParams(ctx, newParams) params = consumerKeeper.GetConsumerParams(ctx) require.Equal(t, newParams, params) diff --git a/x/vaas/consumer/keeper/relay_test.go b/x/vaas/consumer/keeper/relay_test.go index ced6fe9..0cde138 100644 --- a/x/vaas/consumer/keeper/relay_test.go +++ b/x/vaas/consumer/keeper/relay_test.go @@ -14,7 +14,6 @@ import ( testcrypto "github.com/allinbits/vaas/testutil/crypto" testkeeper "github.com/allinbits/vaas/testutil/keeper" - consumertypes "github.com/allinbits/vaas/x/vaas/consumer/types" "github.com/allinbits/vaas/x/vaas/types" ) @@ -251,12 +250,28 @@ func TestConsumerVSCStaleness(t *testing.T) { k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + // Use a non-default threshold so this proves IsVSCStale reads the param + // value (not a constant) and that the boundary tracks the param. + const threshold = 2 * time.Hour + k.SetParams(ctx, types.NewConsumerParams( + true, + types.DefaultVAASTimeoutPeriod, + types.DefaultHistoricalEntries, + types.DefaultConsumerUnbondingPeriod, + threshold, + )) + require.False(t, k.IsVSCStale(ctx)) // absent -> BlockTime -> fresh k.SetLastVSCRecvTime(ctx, ctx.BlockTime()) require.False(t, k.IsVSCStale(ctx)) - stale := ctx.WithBlockTime(ctx.BlockTime().Add(consumertypes.DefaultSafeModeThreshold + time.Minute)) + // Exactly at the threshold is not stale (the check is strict >). + atBoundary := ctx.WithBlockTime(ctx.BlockTime().Add(threshold)) + require.False(t, k.IsVSCStale(atBoundary)) + + // Past the threshold is stale. + stale := ctx.WithBlockTime(ctx.BlockTime().Add(threshold + time.Minute)) require.True(t, k.IsVSCStale(stale)) } @@ -312,3 +327,270 @@ func TestOnRecvVSCRecordsRecvTime(t *testing.T) { got := consumerKeeper.GetLastVSCRecvTime(ctx) require.Equal(t, advancedTime, got) } + +// TestDedupDoesNotResetLastVSCRecvTime verifies that an out-of-order (duplicate) +// packet -- one whose ValsetUpdateId <= HighestValsetUpdateID -- returns early +// before recording block time, so a stale replay cannot silently reset the clock. +func TestDedupDoesNotResetLastVSCRecvTime(t *testing.T) { + providerClientID := "07-tendermint-0" + + pk1, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + // Deliver packet at blockTime T1 -- records lastVSCRecvTime = T1. + t1 := ctx.BlockTime().Add(5 * time.Minute) + ctx1 := ctx.WithBlockTime(t1) + pd1 := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pk1, Power: 10}}, 5) + require.NoError(t, k.OnRecvVSCPacketV2(ctx1, providerClientID, pd1)) + require.Equal(t, t1, k.GetLastVSCRecvTime(ctx1)) + + // Deliver an out-of-order packet (vscId 3 < highest 5) at a later blockTime T2. + // The dedup early-return fires before SetLastVSCRecvTime, so the clock must NOT advance. + t2 := t1.Add(10 * time.Minute) + ctx2 := ctx.WithBlockTime(t2) + pd2 := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pk1, Power: 99}}, 3) + require.NoError(t, k.OnRecvVSCPacketV2(ctx2, providerClientID, pd2)) + + got := k.GetLastVSCRecvTime(ctx2) + require.Equal(t, t1, got, "dedup replay must not reset lastVSCRecvTime") +} + +// TestRecvPacketAfterStalenessLiftsStale drives the consumer into safe mode by +// making the last VSC receipt time appear far in the past, then delivers a +// higher-id packet at a fresh block time and verifies IsVSCStale returns false. +func TestRecvPacketAfterStalenessLiftsStale(t *testing.T) { + providerClientID := "07-tendermint-0" + + pk1, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + const threshold = 2 * time.Hour + k.SetParams(ctx, types.NewConsumerParams( + true, + types.DefaultVAASTimeoutPeriod, + types.DefaultHistoricalEntries, + types.DefaultConsumerUnbondingPeriod, + threshold, + )) + + // Seed the consumer with an initial packet delivered at blockTime now. + baseTime := ctx.BlockTime() + ctxBase := ctx.WithBlockTime(baseTime) + pd1 := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pk1, Power: 10}}, 1) + require.NoError(t, k.OnRecvVSCPacketV2(ctxBase, providerClientID, pd1)) + + // Fast-forward block time past the threshold so the consumer is stale. + staleCtx := ctx.WithBlockTime(baseTime.Add(threshold + time.Minute)) + require.True(t, k.IsVSCStale(staleCtx), "consumer should be stale before resync") + + // Deliver a higher-id packet at the stale block time -- this constitutes resync. + pd2 := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pk1, Power: 20}}, 2) + require.NoError(t, k.OnRecvVSCPacketV2(staleCtx, providerClientID, pd2)) + + // After the resync, IsVSCStale should be false because lastVSCRecvTime was updated. + require.False(t, k.IsVSCStale(staleCtx), "resync must lift safe mode") +} + +// TestSnapshotPowerChange seeds the CC set with A=10 and delivers a snapshot +// that changes A's power to 50. PendingChanges must contain A at power 50. +func TestSnapshotPowerChange(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pkA, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{{PubKey: pkA, Power: 10}}) + require.NoError(t, k.SetHighestValsetUpdateID(ctx, 1)) + k.SetProviderClientID(ctx, "07-tendermint-0") + + snap := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pkA, Power: 50}}, 2) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + + pending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + require.Len(t, pending.ValidatorUpdates, 1) + require.Equal(t, int64(50), pending.ValidatorUpdates[0].Power) +} + +// TestSnapshotAddsNewValidator seeds {A} and delivers a snapshot {A, B}. +// PendingChanges must contain both A and B at their snapshot powers, with no +// power-0 entry for A. +func TestSnapshotAddsNewValidator(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pkA, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + pkB, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{{PubKey: pkA, Power: 10}}) + require.NoError(t, k.SetHighestValsetUpdateID(ctx, 1)) + k.SetProviderClientID(ctx, "07-tendermint-0") + + snap := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{ + {PubKey: pkA, Power: 10}, + {PubKey: pkB, Power: 20}, + }, 2) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + + pending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + require.Len(t, pending.ValidatorUpdates, 2) + + powers := map[string]int64{} + for _, u := range pending.ValidatorUpdates { + powers[u.PubKey.String()] = u.Power + } + require.Equal(t, int64(10), powers[pkA.String()]) + require.Equal(t, int64(20), powers[pkB.String()]) +} + +// TestSnapshotMultipleRemovals seeds {A, B, C} and delivers a snapshot {A only}. +// PendingChanges must contain exactly 3 entries: A at its power, B=0, C=0. +func TestSnapshotMultipleRemovals(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pkA, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + pkB, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + pkC, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{ + {PubKey: pkA, Power: 30}, + {PubKey: pkB, Power: 20}, + {PubKey: pkC, Power: 10}, + }) + require.NoError(t, k.SetHighestValsetUpdateID(ctx, 1)) + k.SetProviderClientID(ctx, "07-tendermint-0") + + snap := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pkA, Power: 30}}, 2) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + + pending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + require.Len(t, pending.ValidatorUpdates, 3) + + powers := map[string]int64{} + for _, u := range pending.ValidatorUpdates { + powers[u.PubKey.String()] = u.Power + } + require.Equal(t, int64(30), powers[pkA.String()]) + require.Equal(t, int64(0), powers[pkB.String()]) + require.Equal(t, int64(0), powers[pkC.String()]) +} + +// TestSnapshotEmptyRemovesAll seeds {A, B} and delivers a snapshot with zero +// validator updates (IsSnapshot=true, empty list). PendingChanges must contain +// exactly two entries: A=0 and B=0. +func TestSnapshotEmptyRemovesAll(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pkA, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + pkB, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{ + {PubKey: pkA, Power: 10}, + {PubKey: pkB, Power: 5}, + }) + require.NoError(t, k.SetHighestValsetUpdateID(ctx, 1)) + k.SetProviderClientID(ctx, "07-tendermint-0") + + snap := types.NewValidatorSetChangePacketData(nil, 2) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + + pending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + require.Len(t, pending.ValidatorUpdates, 2) + + for _, u := range pending.ValidatorUpdates { + require.Equal(t, int64(0), u.Power, "all validators should be removed by empty snapshot") + } +} + +// TestSnapshotReplacesEarlierPendingChanges delivers a diff packet to populate +// PendingChanges, then immediately delivers a snapshot. The final PendingChanges +// must contain only snapshot-derived updates (snapshot replaces, does not merge). +func TestSnapshotReplacesEarlierPendingChanges(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pkA, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + pkB, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k.SetProviderClientID(ctx, "07-tendermint-0") + + // First: a diff packet introducing A and B. + diff := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{ + {PubKey: pkA, Power: 10}, + {PubKey: pkB, Power: 20}, + }, 1) + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", diff)) + + // Apply so the CC set reflects A and B. + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{ + {PubKey: pkA, Power: 10}, + {PubKey: pkB, Power: 20}, + }) + + // Second: a snapshot containing only A. B must be removed; the earlier diff + // entry for B must not survive in PendingChanges. + snap := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pkA, Power: 10}}, 2) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + + pending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + + powers := map[string]int64{} + for _, u := range pending.ValidatorUpdates { + powers[u.PubKey.String()] = u.Power + } + // Snapshot produced: A at 10, B at 0 (explicit removal). No other entries. + require.Len(t, pending.ValidatorUpdates, 2) + require.Equal(t, int64(10), powers[pkA.String()]) + require.Equal(t, int64(0), powers[pkB.String()]) +} + +// TestSnapshotNoDoubleEmitForUnchangedValidator seeds {A=10} and delivers a +// snapshot {A=10} (no power change). PendingChanges must contain exactly one +// entry for A -- the identity match must not duplicate the update. +func TestSnapshotNoDoubleEmitForUnchangedValidator(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pkA, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{{PubKey: pkA, Power: 10}}) + require.NoError(t, k.SetHighestValsetUpdateID(ctx, 1)) + k.SetProviderClientID(ctx, "07-tendermint-0") + + snap := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pkA, Power: 10}}, 2) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + + pending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + require.Len(t, pending.ValidatorUpdates, 1, "unchanged validator must appear exactly once in snapshot pending changes") + require.Equal(t, int64(10), pending.ValidatorUpdates[0].Power) +} diff --git a/x/vaas/consumer/types/genesis_test.go b/x/vaas/consumer/types/genesis_test.go index 7c84d23..168db28 100644 --- a/x/vaas/consumer/types/genesis_test.go +++ b/x/vaas/consumer/types/genesis_test.go @@ -126,6 +126,7 @@ func TestValidateInitialGenesisState(t *testing.T) { 0, vaastypes.DefaultHistoricalEntries, vaastypes.DefaultConsumerUnbondingPeriod, + vaastypes.DefaultSafeModeThreshold, )), true, }, @@ -216,6 +217,7 @@ func TestValidateRestartConsumerGenesisState(t *testing.T) { 0, vaastypes.DefaultHistoricalEntries, vaastypes.DefaultConsumerUnbondingPeriod, + vaastypes.DefaultSafeModeThreshold, )), true, }, diff --git a/x/vaas/consumer/types/params_test.go b/x/vaas/consumer/types/params_test.go index 8f3e587..1a00d64 100644 --- a/x/vaas/consumer/types/params_test.go +++ b/x/vaas/consumer/types/params_test.go @@ -19,19 +19,23 @@ func TestValidateParams(t *testing.T) { {"default params", vaastypes.DefaultConsumerParams(), true}, { "custom valid params", - vaastypes.NewConsumerParams(true, vaastypes.DefaultVAASTimeoutPeriod, 1000, 24*21*time.Hour), true, + vaastypes.NewConsumerParams(true, vaastypes.DefaultVAASTimeoutPeriod, 1000, 24*21*time.Hour, vaastypes.DefaultSafeModeThreshold), true, }, { "custom invalid params, VAAS timeout", - vaastypes.NewConsumerParams(true, 0, 1000, 24*21*time.Hour), false, + vaastypes.NewConsumerParams(true, 0, 1000, 24*21*time.Hour, vaastypes.DefaultSafeModeThreshold), false, }, { "custom invalid params, negative num historical entries", - vaastypes.NewConsumerParams(true, vaastypes.DefaultVAASTimeoutPeriod, -100, 24*21*time.Hour), false, + vaastypes.NewConsumerParams(true, vaastypes.DefaultVAASTimeoutPeriod, -100, 24*21*time.Hour, vaastypes.DefaultSafeModeThreshold), false, }, { "custom invalid params, negative unbonding period", - vaastypes.NewConsumerParams(true, vaastypes.DefaultVAASTimeoutPeriod, 1000, -24*21*time.Hour), false, + vaastypes.NewConsumerParams(true, vaastypes.DefaultVAASTimeoutPeriod, 1000, -24*21*time.Hour, vaastypes.DefaultSafeModeThreshold), false, + }, + { + "custom invalid params, zero safe mode threshold", + vaastypes.NewConsumerParams(true, vaastypes.DefaultVAASTimeoutPeriod, 1000, 24*21*time.Hour, 0), false, }, } diff --git a/x/vaas/provider/keeper/grpc_query_test.go b/x/vaas/provider/keeper/grpc_query_test.go index 25fda2c..bc7fcbe 100644 --- a/x/vaas/provider/keeper/grpc_query_test.go +++ b/x/vaas/provider/keeper/grpc_query_test.go @@ -273,3 +273,44 @@ func TestQueryConsumerLiveness(t *testing.T) { require.False(t, resp.Degraded) require.Equal(t, ctx.BlockTime().Add(resp.GracePeriod).UTC(), resp.RemovalEta.UTC()) } + +// TestQueryConsumerLiveness_DegradedFlag verifies that Degraded is true when +// the last ack time is more than grace/2 ago. +// +// The Degraded condition is: blockTime.After(lastAck + grace/2). +// We set lastAck = blockTime - grace/2 - 1ns so elapsed > grace/2 by 1ns. +func TestQueryConsumerLiveness_DegradedFlag(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + unbonding := 21 * 24 * time.Hour + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).AnyTimes() + + const cid = uint64(0) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + + // Compute grace first so we can position lastAck precisely. + grace, err := k.LivenessGracePeriod(ctx) + require.NoError(t, err) + + // lastAck is grace/2 + 1ns before blockTime: blockTime.After(lastAck + grace/2) == true. + lastAck := ctx.BlockTime().Add(-(grace/2 + time.Nanosecond)) + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, lastAck)) + + resp, err := k.QueryConsumerLiveness(ctx, &providertypes.QueryConsumerLivenessRequest{ConsumerId: cid}) + require.NoError(t, err) + require.True(t, resp.Degraded, "expected Degraded=true when lastAck is past the half-grace mark") +} + +// TestQueryConsumerLiveness_UnknownConsumer verifies that a consumer whose phase +// is UNSPECIFIED (never registered) causes QueryConsumerLiveness to return a +// codes.InvalidArgument gRPC error. +func TestQueryConsumerLiveness_UnknownConsumer(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + // Consumer 999 was never registered; its phase is UNSPECIFIED. + _, err := k.QueryConsumerLiveness(ctx, &providertypes.QueryConsumerLivenessRequest{ConsumerId: 999}) + require.Error(t, err) + require.Equal(t, codes.InvalidArgument, status.Code(err)) +} diff --git a/x/vaas/provider/keeper/liveness_test.go b/x/vaas/provider/keeper/liveness_test.go index f7b2244..6fc0453 100644 --- a/x/vaas/provider/keeper/liveness_test.go +++ b/x/vaas/provider/keeper/liveness_test.go @@ -63,13 +63,24 @@ func TestLivenessGracePeriod(t *testing.T) { defer ctrl.Finish() unbonding := 21 * 24 * time.Hour - mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).Times(1) + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).AnyTimes() + // Default fraction (0.66) from the params seeded by the test helper. grace, err := k.LivenessGracePeriod(ctx) require.NoError(t, err) require.Greater(t, grace, time.Duration(0)) require.Less(t, grace, unbonding) // safety invariant require.Equal(t, time.Duration(float64(unbonding)*0.66), grace) + + // The grace tracks the LivenessGraceFraction param: lowering it shortens + // the grace proportionally (this is what lets e2e/test chains sweep fast). + params := k.GetParams(ctx) + params.LivenessGraceFraction = "0.1" + k.SetParams(ctx, params) + + grace, err = k.LivenessGracePeriod(ctx) + require.NoError(t, err) + require.Equal(t, time.Duration(float64(unbonding)*0.1), grace) } func TestSweepRemovesStaleConsumer(t *testing.T) { @@ -89,6 +100,14 @@ func TestSweepRemovesStaleConsumer(t *testing.T) { require.NoError(t, k.SweepUnresponsiveConsumers(ctx)) require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, k.GetConsumerPhase(ctx, cid)) + + // The sweep does not just stop the consumer, it schedules its deletion at + // blockTime + unbonding (the same removal path a gov removal takes). The + // e2e suite asserts the LAUNCHED -> STOPPED edge end-to-end and relies on + // this assertion for the STOPPED -> DELETED scheduling. + removalTime, err := k.GetConsumerRemovalTime(ctx, cid) + require.NoError(t, err) + require.Equal(t, ctx.BlockTime().Add(unbonding), removalTime) } func TestSweepSparesLiveConsumer(t *testing.T) { @@ -175,3 +194,129 @@ func TestDeleteConsumerChainClearsLivenessState(t *testing.T) { require.Equal(t, uint64(0), k.GetConsumerHighestSentVscId(ctx, cid)) require.Equal(t, uint64(0), k.GetConsumerHighestAckedVscId(ctx, cid)) } + +// TestSweepBoundaryExact checks the boundary condition: lastAck exactly at the +// grace boundary is spared; one nanosecond earlier is stopped. +func TestSweepBoundaryExact(t *testing.T) { + unbonding := 21 * 24 * time.Hour + + t.Run("exactly at grace boundary - spared", func(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).AnyTimes() + grace, err := k.LivenessGracePeriod(ctx) + require.NoError(t, err) + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + + // lastAck == blockTime - grace: elapsed == grace, check is <= grace, so spared. + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime().Add(-grace))) + require.NoError(t, k.SweepUnresponsiveConsumers(ctx)) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cid)) + }) + + t.Run("one ns past grace - stopped", func(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).AnyTimes() + grace, err := k.LivenessGracePeriod(ctx) + require.NoError(t, err) + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + k.SetConsumerChainId(ctx, cid, "consumer-boundary") + + // lastAck == blockTime - grace - 1ns: elapsed > grace, should be stopped. + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime().Add(-grace-time.Nanosecond))) + require.NoError(t, k.SweepUnresponsiveConsumers(ctx)) + require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, k.GetConsumerPhase(ctx, cid)) + }) +} + +// TestSweepMultipleConsumers_PartialRemoval ensures only the stale consumer is +// stopped when three consumers are present: stale, fresh, and at-grace-boundary. +func TestSweepMultipleConsumers_PartialRemoval(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + unbonding := 21 * 24 * time.Hour + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).AnyTimes() + + grace, err := k.LivenessGracePeriod(ctx) + require.NoError(t, err) + + // stale: elapsed >> grace. + cidStale := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cidStale, providertypes.CONSUMER_PHASE_LAUNCHED) + k.SetConsumerChainId(ctx, cidStale, "consumer-stale") + require.NoError(t, k.SetConsumerLastAckTime(ctx, cidStale, ctx.BlockTime().Add(-30*24*time.Hour))) + + // fresh: lastAck == blockTime (elapsed = 0). + cidFresh := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cidFresh, providertypes.CONSUMER_PHASE_LAUNCHED) + require.NoError(t, k.SetConsumerLastAckTime(ctx, cidFresh, ctx.BlockTime())) + + // at boundary: elapsed == grace -> spared (check is <= grace). + cidBoundary := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cidBoundary, providertypes.CONSUMER_PHASE_LAUNCHED) + require.NoError(t, k.SetConsumerLastAckTime(ctx, cidBoundary, ctx.BlockTime().Add(-grace))) + + require.NoError(t, k.SweepUnresponsiveConsumers(ctx)) + + require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, k.GetConsumerPhase(ctx, cidStale)) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cidFresh)) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cidBoundary)) + + // The sweep is per-consumer: only the stale one is scheduled for removal; + // the spared consumers are left entirely untouched (no removal time). + staleRemoval, err := k.GetConsumerRemovalTime(ctx, cidStale) + require.NoError(t, err) + require.Equal(t, ctx.BlockTime().Add(unbonding), staleRemoval) + _, err = k.GetConsumerRemovalTime(ctx, cidFresh) + require.Error(t, err) + _, err = k.GetConsumerRemovalTime(ctx, cidBoundary) + require.Error(t, err) +} + +// TestSweepContinuesAfterStopError is SKIPPED. +// +// StopAndPrepareForConsumerRemoval sets the consumer phase to STOPPED unconditionally +// before it calls stakingKeeper.UnbondingTime. There is no mock lever that causes +// the stop to fail while leaving the consumer in LAUNCHED: by the time UnbondingTime +// is called, the phase change has already been committed to the store. Injecting an +// error via UnbondingTime only prevents the removal-time entry from being written, but +// the consumer phase is already STOPPED. A faithful per-consumer stop failure test +// would require refactoring production code, which is out of scope here. + +// TestSweepRecoversAfterAck confirms a consumer that receives a fresh ack just +// inside the grace window survives a subsequent sweep once more time elapses. +func TestSweepRecoversAfterAck(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + unbonding := 21 * 24 * time.Hour + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).AnyTimes() + + grace, err := k.LivenessGracePeriod(ctx) + require.NoError(t, err) + + cid := k.FetchAndIncrementConsumerId(ctx) + clientId := "07-tendermint-recover" + k.SetConsumerClientId(ctx, cid, clientId) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + + // Set lastAck to just inside the grace window (1ns before expiry). + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime().Add(-grace+time.Nanosecond))) + + // Simulate ack arriving at current block time, refreshing the liveness clock. + require.NoError(t, k.OnAcknowledgementPacketV2(ctx, clientId, 1, "")) + + // Advance block time by exactly grace; elapsed == grace which satisfies <= grace, so spared. + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(grace)) + + require.NoError(t, k.SweepUnresponsiveConsumers(ctx)) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cid)) +} diff --git a/x/vaas/provider/keeper/msg_server_test.go b/x/vaas/provider/keeper/msg_server_test.go index ff610af..3331aa1 100644 --- a/x/vaas/provider/keeper/msg_server_test.go +++ b/x/vaas/provider/keeper/msg_server_test.go @@ -1321,19 +1321,19 @@ func TestCreateConsumerUnbondingBounds(t *testing.T) { unbondingPeriod: 30 * 24 * time.Hour, wantErr: true, }, - { - name: "too short: below minimum", - unbondingPeriod: 1 * 24 * time.Hour, - wantErr: true, - }, { name: "valid: equal to provider unbonding", unbondingPeriod: 21 * 24 * time.Hour, wantErr: false, }, { - name: "valid: equal to minimum", - unbondingPeriod: 5 * 24 * time.Hour, + name: "short value accepted (no lower floor)", + unbondingPeriod: 1 * 24 * time.Hour, + wantErr: false, + }, + { + name: "very short value accepted (no lower floor)", + unbondingPeriod: time.Hour, wantErr: false, }, } @@ -1471,3 +1471,78 @@ func TestSweepConsumerFeePool(t *testing.T) { }) } } + +// TestUpdateConsumerUnbondingBounds mirrors TestCreateConsumerUnbondingBounds +// but exercises the UpdateConsumer path for a pre-launch (REGISTERED) consumer. +func TestUpdateConsumerUnbondingBounds(t *testing.T) { + providerUnbonding := 21 * 24 * time.Hour + + tests := []struct { + name string + unbondingPeriod time.Duration + wantErr bool + }{ + { + name: "above provider unbonding: rejected", + unbondingPeriod: 30 * 24 * time.Hour, + wantErr: true, + }, + { + name: "short value accepted (no lower floor)", + unbondingPeriod: 1 * 24 * time.Hour, + wantErr: false, + }, + { + name: "very short value accepted (no lower floor)", + unbondingPeriod: time.Hour, + wantErr: false, + }, + { + name: "exactly provider unbonding: ok", + unbondingPeriod: providerUnbonding, + wantErr: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + // CreateConsumer calls validateConsumerUnbonding once; UpdateConsumer + // calls it once more when InitializationParameters is non-nil. + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(providerUnbonding, nil).Times(2) + mocks.MockAccountKeeper.EXPECT().AddressCodec().Return(address.NewBech32Codec("cosmos")).AnyTimes() + + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) + + // Register a consumer with a valid unbonding period. + createResp, err := msgServer.CreateConsumer(ctx, &providertypes.MsgCreateConsumer{ + Submitter: "submitter", + ChainId: "chainId", + Metadata: providertypes.ConsumerMetadata{Name: "n", Description: "d"}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, + }) + require.NoError(t, err) + consumerId := createResp.ConsumerId + + // Consumer stays in REGISTERED (pre-launch), so InitializationParameters + // updates are allowed. Override only UnbondingPeriod. + _, err = msgServer.UpdateConsumer(ctx, &providertypes.MsgUpdateConsumer{ + Owner: "submitter", + ConsumerId: consumerId, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: tc.unbondingPeriod, + }, + }) + if tc.wantErr { + require.Error(t, err) + require.ErrorIs(t, err, providertypes.ErrInvalidConsumerInitializationParameters) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/x/vaas/provider/keeper/params_test.go b/x/vaas/provider/keeper/params_test.go index c1c3f4d..fd81451 100644 --- a/x/vaas/provider/keeper/params_test.go +++ b/x/vaas/provider/keeper/params_test.go @@ -27,6 +27,7 @@ func TestParams(t *testing.T) { newParams := providertypes.NewParams( "0.25", + "0.5", 7*24*time.Hour, 600, 10, diff --git a/x/vaas/provider/keeper/relay_test.go b/x/vaas/provider/keeper/relay_test.go index 62059e3..9650a83 100644 --- a/x/vaas/provider/keeper/relay_test.go +++ b/x/vaas/provider/keeper/relay_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "fmt" "testing" "github.com/stretchr/testify/require" @@ -9,6 +10,7 @@ import ( abci "github.com/cometbft/cometbft/abci/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" testkeeper "github.com/allinbits/vaas/testutil/keeper" providertypes "github.com/allinbits/vaas/x/vaas/provider/types" @@ -162,3 +164,120 @@ func TestSendVSCPacketsToChainNoHandler(t *testing.T) { pending := providerKeeper.GetPendingVSCPackets(ctx, consumerId) require.Len(t, pending, 1) } + +// TestOnAckHighestAckedDoesNotRegress asserts that a lower vscId arriving after +// a higher one does not regress the highestAcked counter. +func TestOnAckHighestAckedDoesNotRegress(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + consumerId := k.FetchAndIncrementConsumerId(ctx) + clientId := "07-tendermint-0" + k.SetConsumerClientId(ctx, consumerId, clientId) + k.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) + + // First ack: vscId 9. + require.NoError(t, k.OnAcknowledgementPacketV2(ctx, clientId, 9, "")) + require.Equal(t, uint64(9), k.GetConsumerHighestAckedVscId(ctx, consumerId)) + + // Out-of-order ack: vscId 5 must not regress the counter. + require.NoError(t, k.OnAcknowledgementPacketV2(ctx, clientId, 5, "")) + require.Equal(t, uint64(9), k.GetConsumerHighestAckedVscId(ctx, consumerId)) +} + +// TestHighestSentAdvancesOnlyOnSuccess verifies that highestSent and pending +// packets are unchanged when SendPacket returns a non-ErrClientNotActive error, +// and that they are updated correctly on success. +func TestHighestSentAdvancesOnlyOnSuccess(t *testing.T) { + consumerId := uint64(0) + clientId := "07-tendermint-0" + + t.Run("send error leaves state unchanged", func(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + k.SetConsumerClientId(ctx, consumerId, clientId) + k.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) + + k.AppendPendingVSCPackets(ctx, consumerId, vaastypes.ValidatorSetChangePacketData{ + ValidatorUpdates: []abci.ValidatorUpdate{}, + ValsetUpdateId: 3, + }) + k.AppendPendingVSCPackets(ctx, consumerId, vaastypes.ValidatorSetChangePacketData{ + ValidatorUpdates: []abci.ValidatorUpdate{}, + ValsetUpdateId: 5, + }) + + // Non-ErrClientNotActive error: function logs and returns nil, leaving pending intact. + mocks.MockChannelV2Keeper.EXPECT(). + SendPacket(gomock.Any(), gomock.Any()). + Return(nil, fmt.Errorf("some transient send error")) + + require.NoError(t, k.SendVSCPacketsToChain(ctx, consumerId, clientId)) + + require.Equal(t, uint64(0), k.GetConsumerHighestSentVscId(ctx, consumerId)) + require.Len(t, k.GetPendingVSCPackets(ctx, consumerId), 2) + }) + + t.Run("send success advances highestSent and clears pending", func(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + k.SetConsumerClientId(ctx, consumerId, clientId) + k.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) + + k.AppendPendingVSCPackets(ctx, consumerId, vaastypes.ValidatorSetChangePacketData{ + ValidatorUpdates: []abci.ValidatorUpdate{}, + ValsetUpdateId: 3, + }) + k.AppendPendingVSCPackets(ctx, consumerId, vaastypes.ValidatorSetChangePacketData{ + ValidatorUpdates: []abci.ValidatorUpdate{}, + ValsetUpdateId: 5, + }) + + // Both packets sent successfully. + mocks.MockChannelV2Keeper.EXPECT(). + SendPacket(gomock.Any(), gomock.Any()). + Return(&channeltypesv2.MsgSendPacketResponse{Sequence: 1}, nil).Times(1) + mocks.MockChannelV2Keeper.EXPECT(). + SendPacket(gomock.Any(), gomock.Any()). + Return(&channeltypesv2.MsgSendPacketResponse{Sequence: 2}, nil).Times(1) + + require.NoError(t, k.SendVSCPacketsToChain(ctx, consumerId, clientId)) + + require.Equal(t, uint64(5), k.GetConsumerHighestSentVscId(ctx, consumerId)) + require.Empty(t, k.GetPendingVSCPackets(ctx, consumerId)) + }) + + t.Run("partial send: first succeeds, second fails, advances to the sent id and leaves pending", func(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + k.SetConsumerClientId(ctx, consumerId, clientId) + k.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) + + k.AppendPendingVSCPackets(ctx, consumerId, vaastypes.ValidatorSetChangePacketData{ + ValidatorUpdates: []abci.ValidatorUpdate{}, + ValsetUpdateId: 3, + }) + k.AppendPendingVSCPackets(ctx, consumerId, vaastypes.ValidatorSetChangePacketData{ + ValidatorUpdates: []abci.ValidatorUpdate{}, + ValsetUpdateId: 5, + }) + + // First packet (vscId 3) sends and advances highestSent; the second + // (vscId 5) fails, so SendVSCPacketsToChain returns nil early without + // reaching DeletePendingVSCPackets -- all pending packets stay queued. + mocks.MockChannelV2Keeper.EXPECT(). + SendPacket(gomock.Any(), gomock.Any()). + Return(&channeltypesv2.MsgSendPacketResponse{Sequence: 1}, nil).Times(1) + mocks.MockChannelV2Keeper.EXPECT(). + SendPacket(gomock.Any(), gomock.Any()). + Return(nil, fmt.Errorf("some transient send error")).Times(1) + + require.NoError(t, k.SendVSCPacketsToChain(ctx, consumerId, clientId)) + + require.Equal(t, uint64(3), k.GetConsumerHighestSentVscId(ctx, consumerId)) + require.Len(t, k.GetPendingVSCPackets(ctx, consumerId), 2) + }) +} diff --git a/x/vaas/provider/keeper/validator_set_update_test.go b/x/vaas/provider/keeper/validator_set_update_test.go index 13ee7e4..8a5713e 100644 --- a/x/vaas/provider/keeper/validator_set_update_test.go +++ b/x/vaas/provider/keeper/validator_set_update_test.go @@ -10,11 +10,22 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + testcrypto "github.com/allinbits/vaas/testutil/crypto" testkeeper "github.com/allinbits/vaas/testutil/keeper" keeper "github.com/allinbits/vaas/x/vaas/provider/keeper" providertypes "github.com/allinbits/vaas/x/vaas/provider/types" ) +// makeConsensusValidator is a test helper that builds a ConsensusValidator +// from an ed25519 private key and a power value. +func makeConsensusValidator(t *testing.T, power int64) providertypes.ConsensusValidator { + t.Helper() + pk := ed25519.GenPrivKey().PubKey() + tmPk, err := cryptocodec.ToCmtProtoPublicKey(pk) + require.NoError(t, err) + return providertypes.ConsensusValidator{PublicKey: &tmPk, Power: power} +} + func TestFullValSetUpdatesReturnsCompleteSet(t *testing.T) { pk1 := ed25519.GenPrivKey().PubKey() tmPk1, err := cryptocodec.ToCmtProtoPublicKey(pk1) @@ -62,3 +73,114 @@ func TestQueueVSCPacketsDiffWhenCaughtUp(t *testing.T) { require.Len(t, pending, 1) require.False(t, pending[0].IsSnapshot) } + +// TestSnapshotAfterLostDiff is the core resync SAFETY property. +// +// When a consumer is BEHIND (acked < sent) the provider must send a full +// snapshot so the consumer can replace its entire set — no reliance on a +// diff that may never have arrived. This test verifies: +// - the queued packet is flagged as a snapshot (IsSnapshot == true), +// - the snapshot contains validator A with non-zero power (the survivor +// is present in the live set and kept in the snapshot), and +// - validator B is NOT present in the snapshot — proving the snapshot is +// computed from the live bonded set and not the stale consumer valset. +func TestSnapshotAfterLostDiff(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + // identityA is the surviving validator; identityB has departed. + identityA := testcrypto.NewCryptoIdentityFromIntSeed(0) + identityB := testcrypto.NewCryptoIdentityFromIntSeed(1) + + // Build the ConsensusValidator representations used to seed the consumer valset. + tmPkA := identityA.TMProtoCryptoPublicKey() + tmPkB := identityB.TMProtoCryptoPublicKey() + valAConsensus := providertypes.ConsensusValidator{PublicKey: &tmPkA, Power: 10} + valBConsensus := providertypes.ConsensusValidator{PublicKey: &tmPkB, Power: 5} + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + + // The consumer last knew about {A, B}. + require.NoError(t, k.SetConsumerValSet(ctx, cid, []providertypes.ConsensusValidator{valAConsensus, valBConsensus})) + + // The consumer is behind: some sent packets have not been acknowledged. + k.SetConsumerHighestSentVscId(ctx, cid, 5) + k.SetConsumerHighestAckedVscId(ctx, cid, 3) + + // Build a real staking validator for A to return from the bonded set. + // B is absent — it has departed from the bonded set. + stakingValA := identityA.SDKStakingValidator() + const powerA = int64(10) + + // GetLastBondedValidators calls MaxValidators then GetBondedValidatorsByPower. + // CreateConsumerValidator calls GetLastValidatorPower for each bonded validator. + mocks.MockStakingKeeper.EXPECT().MaxValidators(gomock.Any()).Return(uint32(100), nil).AnyTimes() + mocks.MockStakingKeeper.EXPECT().GetBondedValidatorsByPower(gomock.Any()).Return([]stakingtypes.Validator{stakingValA}, nil).AnyTimes() + mocks.MockStakingKeeper.EXPECT().GetLastValidatorPower(gomock.Any(), identityA.SDKValOpAddress()).Return(powerA, nil).AnyTimes() + + require.NoError(t, k.QueueVSCPackets(ctx)) + + pending := k.GetPendingVSCPackets(ctx, cid) + require.Len(t, pending, 1) + + pkt := pending[0] + require.True(t, pkt.IsSnapshot, "expected IsSnapshot=true when consumer is behind") + + // Validator A must appear in the snapshot with non-zero power. + aKeyStr := tmPkA.String() + foundA := false + for _, u := range pkt.ValidatorUpdates { + if u.PubKey.String() == aKeyStr { + require.Greater(t, u.Power, int64(0), "validator A must have non-zero power in the snapshot") + foundA = true + } + } + require.True(t, foundA, "surviving validator A must be present in the snapshot") + + // Validator B was in the stale consumer valset but departed from the bonded + // set, so it must NOT appear in the snapshot. + bKeyStr := tmPkB.String() + for _, u := range pkt.ValidatorUpdates { + require.NotEqual(t, bKeyStr, u.PubKey.String(), + "departed validator B must not appear in the snapshot") + } +} + +// TestDiffValidators_RemoveDeparted verifies that a validator present in the +// current set but absent from the next set generates exactly one power-0 +// update for that validator. +func TestDiffValidators_RemoveDeparted(t *testing.T) { + valA := makeConsensusValidator(t, 10) + valB := makeConsensusValidator(t, 5) + + current := []providertypes.ConsensusValidator{valA, valB} + next := []providertypes.ConsensusValidator{valA} + + updates := keeper.DiffValidators(current, next) + require.Len(t, updates, 1, "expected exactly one update (removal of B)") + require.Equal(t, valB.PublicKey.String(), updates[0].PubKey.String()) + require.Equal(t, int64(0), updates[0].Power, "departed validator must have power=0") +} + +// TestDiffValidators_EmptyNextSet verifies that when the next set is empty +// all current validators are removed (power-0 updates), with no additions. +func TestDiffValidators_EmptyNextSet(t *testing.T) { + valA := makeConsensusValidator(t, 10) + valB := makeConsensusValidator(t, 5) + + current := []providertypes.ConsensusValidator{valA, valB} + updates := keeper.DiffValidators(current, nil) + + require.Len(t, updates, 2, "expected two removals") + for _, u := range updates { + require.Equal(t, int64(0), u.Power, "all removals must have power=0") + } +} + +// TestFullValSetUpdates_EmptyInput verifies that FullValSetUpdates on a nil +// or empty slice returns an empty (non-nil, len 0) result. +func TestFullValSetUpdates_EmptyInput(t *testing.T) { + require.Empty(t, keeper.FullValSetUpdates(nil)) + require.Empty(t, keeper.FullValSetUpdates([]providertypes.ConsensusValidator{})) +} diff --git a/x/vaas/provider/types/genesis_test.go b/x/vaas/provider/types/genesis_test.go index d48629d..070d793 100644 --- a/x/vaas/provider/types/genesis_test.go +++ b/x/vaas/provider/types/genesis_test.go @@ -88,7 +88,7 @@ func TestValidateGenesisState(t *testing.T) { nil, []types.ConsumerState{launchedCS(0, "chainid-1", "client-id", false)}, types.NewParams( - types.DefaultTrustingPeriodFraction, time.Hour, 600, 180, math.NewInt(42), types.DefaultMinDepositBlocks), + types.DefaultTrustingPeriodFraction, types.DefaultLivenessGraceFraction, time.Hour, 600, 180, math.NewInt(42), types.DefaultMinDepositBlocks), nil, nil, nil, @@ -135,6 +135,7 @@ func TestValidateGenesisState(t *testing.T) { []types.ConsumerState{launchedCS(0, "chainid-1", "client-id", false)}, types.NewParams( "0.0", // 0 trusting period fraction here + types.DefaultLivenessGraceFraction, vaastypes.DefaultVAASTimeoutPeriod, 600, 180, math.NewInt(42), types.DefaultMinDepositBlocks), nil, nil, @@ -152,6 +153,7 @@ func TestValidateGenesisState(t *testing.T) { []types.ConsumerState{launchedCS(0, "chainid-1", "client-id", false)}, types.NewParams( types.DefaultTrustingPeriodFraction, + types.DefaultLivenessGraceFraction, 0, // 0 ccv timeout here 600, 180, math.NewInt(42), types.DefaultMinDepositBlocks), nil, diff --git a/x/vaas/provider/types/msg_test.go b/x/vaas/provider/types/msg_test.go index 47fb7f5..f465ae5 100644 --- a/x/vaas/provider/types/msg_test.go +++ b/x/vaas/provider/types/msg_test.go @@ -6,6 +6,7 @@ import ( "time" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" "github.com/stretchr/testify/require" "cosmossdk.io/math" @@ -16,6 +17,7 @@ import ( cryptoutil "github.com/allinbits/vaas/testutil/crypto" "github.com/allinbits/vaas/x/vaas/provider/types" + vaastypes "github.com/allinbits/vaas/x/vaas/types" ) func TestValidateStringField(t *testing.T) { @@ -160,78 +162,98 @@ func TestValidateInitializationParameters(t *testing.T) { { name: "valid", params: types.ConsumerInitializationParameters{ - InitialHeight: clienttypes.NewHeight(3, 4), - GenesisHash: []byte{0x01}, - BinaryHash: []byte{0x01}, - SpawnTime: now, - UnbondingPeriod: time.Duration(100000000000), - VaasTimeoutPeriod: 15 * time.Minute, - HistoricalEntries: 10000, + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, + HistoricalEntries: 10000, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, }, valid: true, }, { name: "invalid - zero height", params: types.ConsumerInitializationParameters{ - InitialHeight: clienttypes.ZeroHeight(), - GenesisHash: []byte{0x01}, - BinaryHash: []byte{0x01}, - SpawnTime: now, - UnbondingPeriod: time.Duration(100000000000), - VaasTimeoutPeriod: 15 * time.Minute, - HistoricalEntries: 10000, + InitialHeight: clienttypes.ZeroHeight(), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, + HistoricalEntries: 10000, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, }, valid: false, }, { name: "invalid - hash too long", params: types.ConsumerInitializationParameters{ - InitialHeight: clienttypes.NewHeight(3, 4), - GenesisHash: tooLongHash, - BinaryHash: []byte{0x01}, - SpawnTime: now, - UnbondingPeriod: time.Duration(100000000000), - VaasTimeoutPeriod: 15 * time.Minute, - HistoricalEntries: 10000, + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: tooLongHash, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, + HistoricalEntries: 10000, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, }, valid: false, }, { name: "invalid - zero spawn time", params: types.ConsumerInitializationParameters{ - InitialHeight: clienttypes.NewHeight(3, 4), - GenesisHash: []byte{0x01}, - BinaryHash: []byte{0x01}, - SpawnTime: time.Time{}, - UnbondingPeriod: time.Duration(100000000000), - VaasTimeoutPeriod: 15 * time.Minute, - HistoricalEntries: 10000, + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: time.Time{}, + UnbondingPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, + HistoricalEntries: 10000, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, }, valid: true, }, { name: "invalid - zero duration", params: types.ConsumerInitializationParameters{ - InitialHeight: clienttypes.NewHeight(3, 4), - GenesisHash: []byte{0x01}, - BinaryHash: []byte{0x01}, - SpawnTime: now, - UnbondingPeriod: 0, - VaasTimeoutPeriod: 15 * time.Minute, - HistoricalEntries: 10000, + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: 0, + VaasTimeoutPeriod: 15 * time.Minute, + HistoricalEntries: 10000, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, }, valid: false, }, { name: "invalid - HistoricalEntries zero", params: types.ConsumerInitializationParameters{ - InitialHeight: clienttypes.NewHeight(3, 4), - GenesisHash: []byte{0x01}, - BinaryHash: []byte{0x01}, - SpawnTime: now, - UnbondingPeriod: time.Duration(100000000000), - VaasTimeoutPeriod: 15 * time.Minute, - HistoricalEntries: 0, + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, + HistoricalEntries: 0, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, + }, + valid: false, + }, + { + name: "invalid - zero safe mode threshold", + params: types.ConsumerInitializationParameters{ + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, + HistoricalEntries: 10000, + SafeModeThreshold: 0, }, valid: false, }, @@ -752,3 +774,59 @@ func TestMsgSetConsumerFeesPerBlock_ValidateBasic(t *testing.T) { }) } } + +// TestValidateInitParams_VaasTimeoutFloor table-drives ValidateInitializationParameters +// focusing on the VaasTimeoutPeriod boundary at MinVAASTimeoutPeriod (10m). +// All other fields are set to valid values so only the timeout is under test. +func TestValidateInitParams_VaasTimeoutBounds(t *testing.T) { + base := types.ConsumerInitializationParameters{ + InitialHeight: clienttypes.NewHeight(1, 1), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: time.Now().UTC(), + UnbondingPeriod: 21 * 24 * time.Hour, + HistoricalEntries: 1000, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, + } + + tests := []struct { + name string + timeout time.Duration + wantErr bool + }{ + { + name: "exactly MinVAASTimeoutPeriod: ok", + timeout: vaastypes.MinVAASTimeoutPeriod, + wantErr: false, + }, + { + name: "1ns below floor: error", + timeout: vaastypes.MinVAASTimeoutPeriod - time.Nanosecond, + wantErr: true, + }, + { + name: "exactly MaxTimeoutDelta: ok", + timeout: channeltypesv2.MaxTimeoutDelta, + wantErr: false, + }, + { + name: "above MaxTimeoutDelta: error", + timeout: channeltypesv2.MaxTimeoutDelta + time.Hour, + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + params := base + params.VaasTimeoutPeriod = tc.timeout + err := types.ValidateInitializationParameters(params) + if tc.wantErr { + require.Error(t, err) + require.ErrorIs(t, err, types.ErrInvalidConsumerInitializationParameters) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/x/vaas/provider/types/params_test.go b/x/vaas/provider/types/params_test.go index 57255c8..057abae 100644 --- a/x/vaas/provider/types/params_test.go +++ b/x/vaas/provider/types/params_test.go @@ -20,12 +20,14 @@ func TestValidateParams(t *testing.T) { expPass bool }{ {"default params", types.DefaultParams(), true}, - {"custom valid params", types.NewParams("0.33", time.Hour, 1000, 180, math.NewInt(42), types.DefaultMinDepositBlocks), true}, - {"zero fees per block", types.NewParams("0.33", time.Hour, 1000, 180, math.NewInt(0), types.DefaultMinDepositBlocks), false}, + {"custom valid params", types.NewParams("0.33", "0.5", time.Hour, 1000, 180, math.NewInt(42), types.DefaultMinDepositBlocks), true}, + {"zero fees per block", types.NewParams("0.33", "0.5", time.Hour, 1000, 180, math.NewInt(0), types.DefaultMinDepositBlocks), false}, {"0 trusting period fraction", types.NewParams( - "0.00", time.Hour, 1000, 180, math.NewInt(1), types.DefaultMinDepositBlocks), false}, + "0.00", "0.5", time.Hour, 1000, 180, math.NewInt(1), types.DefaultMinDepositBlocks), false}, + {"0 liveness grace fraction", types.NewParams( + "0.33", "0.00", time.Hour, 1000, 180, math.NewInt(1), types.DefaultMinDepositBlocks), false}, {"0 ccv timeout period", types.NewParams( - "0.33", 0, 1000, 180, math.NewInt(1), types.DefaultMinDepositBlocks), false}, + "0.33", "0.5", 0, 1000, 180, math.NewInt(1), types.DefaultMinDepositBlocks), false}, } for _, tc := range testCases { From c1cc5c66f8c45d6efbf68ef52fd7271fe881dae3 Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:44:07 +0200 Subject: [PATCH 08/20] add e2e coverage for consumer liveness Short-unbonding Docker suite with fast blocks and a first-sync gate: exercises recover-before-grace, real safe mode (relayer paused -> VSC-stale consumer restricted, then recovers on resume), the liveness query, and the block-clock sweep to STOPPED under sustained IBC silence. --- tests/e2e/e2e_consumer_liveness_test.go | 291 ++++ tests/e2e/e2e_liveness_suite_test.go | 1301 +++++++++++++++++ tests/e2e/e2e_test.go | 11 + .../create_consumer_short_unbonding.json | 33 + 4 files changed, 1636 insertions(+) create mode 100644 tests/e2e/e2e_consumer_liveness_test.go create mode 100644 tests/e2e/e2e_liveness_suite_test.go create mode 100644 tests/e2e/testdata/create_consumer_short_unbonding.json diff --git a/tests/e2e/e2e_consumer_liveness_test.go b/tests/e2e/e2e_consumer_liveness_test.go new file mode 100644 index 0000000..9a4839e --- /dev/null +++ b/tests/e2e/e2e_consumer_liveness_test.go @@ -0,0 +1,291 @@ +package e2e + +// e2e_consumer_liveness_test.go exercises the three liveness-removal scenarios +// introduced by issue #36: +// +// 1. testLivenessRemoval - sustained outage causes LAUNCHED -> STOPPED -> DELETED. +// 2. testLivenessTransientOutage - brief outage + valset change; consumer +// stays LAUNCHED and re-syncs after grace. +// 3. testLivenessSafeMode - while VSC is stale, consumer rejects bank +// sends; after a fresh VSC it accepts them. +// +// HARNESS CONSTRAINTS +// +// These tests share the single IntegrationTestSuite spun up by TestVAAS in +// e2e_test.go. That consumer's unbonding_period is ~20 days (from +// testdata/create_consumer.json), so the derived liveness grace period +// (unbonding * 0.66) is ~13 days. Waiting for an automatic STOPPED transition +// in real-time is therefore impractical in CI. +// +// For testLivenessRemoval we approximate by issuing an explicit +// remove-consumer tx (which immediately stops the consumer), then asserting the +// provider transitions through STOPPED and eventually to DELETED. The STOPPED +// -> DELETED leg also relies on time elapsing (the removal_time set at stop), +// so in a normal CI run the consumer will stay STOPPED until a follow-up epoch +// advances the chain past removal_time. We therefore assert STOPPED as the +// terminal state within the test window, and note that a proper timing-faithful +// run would need a short unbonding_period consumer (e.g. 60s -> grace ~40s). +// +// For testLivenessTransientOutage we pause the consumer container (same +// mechanism as testDowntimeSlash) for a brief window and verify the consumer +// stays LAUNCHED and its VP re-converges after recovery. This exercises the +// snapshot-resync path introduced in issue #36. +// +// For testLivenessSafeMode we verify that the consumer's MsgFilterDecorator +// rejects bank sends while in restricted mode (debt or stale VSC), and accepts +// them once normal conditions are restored. The DefaultSafeModeThreshold for +// VSC staleness is 3 hours (hard-coded), so we trigger restricted mode via the +// fee-debt path instead, which exercises the same ante-handler code branch. +// +// Sequencing in TestVAAS: +// testLivenessTransientOutage runs after testValidatorSetSync (non-destructive VP change). +// testLivenessSafeMode runs after testConsumerDebtFlow (reuses the debt cycle). +// testLivenessRemoval runs second-to-last, before testGenesisRoundTrip. + +import ( + "encoding/json" + "fmt" + "strings" + "time" +) + +// queryConsumerPhase returns the phase string for a given consumer ID from the +// provider chain (e.g. "CONSUMER_PHASE_LAUNCHED"). +func (s *IntegrationTestSuite) queryConsumerPhase(consumerID string) string { + stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "provider", "consumer-chain", consumerID, + "--home", providerHomePath, + "--output", "json", + }) + if err != nil { + s.T().Logf("queryConsumerPhase(%s): exec error: %v", consumerID, err) + return "" + } + var res struct { + Phase string `json:"phase"` + } + if err := json.Unmarshal(stdout.Bytes(), &res); err != nil { + s.T().Logf("queryConsumerPhase(%s): decode error: %v (raw: %s)", consumerID, err, stdout.String()) + return "" + } + return res.Phase +} + +// testLivenessTransientOutage verifies snapshot resync: a brief consumer +// outage (shorter than the liveness grace period) does not stop the consumer. +// After recovery the consumer's consensus VP must re-converge with the +// provider's updated VP. +// +// Issue #36 context: the snapshot resync sends the full current valset (not +// just a delta) when a reconnecting consumer's last-known VSC ID lags behind. +func (s *IntegrationTestSuite) testLivenessTransientOutage() { + s.Run("liveness transient outage: consumer stays LAUNCHED and re-syncs valset", func() { + const consumerID = "0" + + // Record provider VP before the outage. + providerValsBefore, err := s.queryProviderNetValidators() + s.Require().NoError(err, "failed to query provider validators before outage") + s.Require().NotEmpty(providerValsBefore, "provider must have at least one validator") + _, vpsBefore := s.extractPubKeys(providerValsBefore) + s.Require().NotEmpty(vpsBefore) + initialProviderVP := vpsBefore[0] + + // Pause the consumer to simulate a transient network outage. + s.T().Log("pausing consumer container (transient outage starts)...") + err = s.dkrPool.Client.PauseContainer(s.consumerValRes[0].Container.ID) + s.Require().NoError(err, "failed to pause consumer container") + + // While the consumer is paused, delegate extra stake on the provider so + // it emits a VSC packet for the VP change. + s.T().Log("delegating extra stake on provider to generate a valset change...") + stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "keys", "show", "val", "--bech", "val", "-a", + "--home", providerHomePath, + "--keyring-backend", "test", + }) + s.Require().NoError(err, "failed to get validator operator address") + valoperAddr := strings.TrimSpace(stdout.String()) + + _, _, err = s.dockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "tx", "staking", "delegate", valoperAddr, "500000" + bondDenom, + "--from", "user", + "--home", providerHomePath, + "--keyring-backend", "test", + "--chain-id", providerChainID, + "--fees", "10000" + bondDenom, + "-y", + }) + s.Require().NoError(err, "failed to delegate extra stake on provider") + + // Wait for provider to reflect the new VP. + var newProviderVP uint64 + s.Require().Eventuallyf(func() bool { + vals, err := s.queryProviderNetValidators() + if err != nil || len(vals) == 0 { + return false + } + _, vps := s.extractPubKeys(vals) + if len(vps) == 0 { + return false + } + newProviderVP = vps[0] + return newProviderVP > initialProviderVP + }, 30*time.Second, 2*time.Second, + "provider VP did not increase after delegation (before=%d)", initialProviderVP) + s.T().Logf("provider VP changed: %d -> %d", initialProviderVP, newProviderVP) + + // Restore the consumer (outage ends well before grace expires). + s.T().Log("unpausing consumer container (outage ends)...") + err = s.dkrPool.Client.UnpauseContainer(s.consumerValRes[0].Container.ID) + s.Require().NoError(err, "failed to unpause consumer container") + + // The consumer must still be LAUNCHED after the transient outage. + phase := s.queryConsumerPhase(consumerID) + s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, + "consumer %s must remain LAUNCHED after a transient outage", consumerID) + + // Consumer VP must re-converge to the updated provider VP via snapshot resync. + s.T().Log("waiting for consumer valset to re-converge via snapshot resync...") + s.Require().Eventuallyf(func() bool { + consumerVals, err := s.queryConsumerNetValidators() + if err != nil || len(consumerVals) == 0 { + return false + } + _, consumerVPs := s.extractPubKeys(consumerVals) + if len(consumerVPs) == 0 { + return false + } + s.T().Logf("consumer VP: %d (want %d)", consumerVPs[0], newProviderVP) + return consumerVPs[0] == newProviderVP + }, 3*time.Minute, 3*time.Second, + "consumer VP did not converge to provider VP %d after snapshot resync", newProviderVP) + }) +} + +// testLivenessSafeMode verifies that the consumer's MsgFilterDecorator enters +// restricted mode when the consumer is in debt or has a stale validator set +// (the two conditions share the same code path in the ante handler), and exits +// restricted mode after recovery. +// +// Issue #36 context: the safe-mode gate allows only /ibc.core.* and +// /cosmos.gov.* messages through. A bank send is rejected in restricted mode +// but accepted in normal mode. +// +// Approximation note: VSC staleness (DefaultSafeModeThreshold = 3h) cannot be +// triggered within a short test run. Debt (empty fee pool) uses the same +// restricted-mode branch and can be triggered within epochs. This test +// therefore relies on the debt path to trigger restricted mode. +func (s *IntegrationTestSuite) testLivenessSafeMode() { + s.Run("liveness safe mode: bank send rejected while restricted, accepted after recovery", func() { + // Wait for the consumer to enter restricted mode (debt due to empty fee pool). + s.T().Log("waiting for consumer to enter restricted mode...") + s.Require().Eventuallyf(func() bool { + out, err := s.consumerBankSendDryRun() + return err != nil || strings.Contains(out, "consumer chain is in debt") || + strings.Contains(out, "stale validator set") + }, 3*time.Minute, 5*time.Second, + "consumer did not enter restricted mode; fee pool may not be empty") + s.T().Log("restricted mode confirmed; bank sends are now rejected") + + // A /cosmos.gov.* message must NOT be blocked by the admission gate in + // restricted mode. We probe this with a dry-run gov submit-proposal and + // verify the rejection reason (if any) does not come from the gate. + user := s.consumerUserBech32() + _, govStderr, _ := s.dockerExec(s.consumerValRes[0].Container.ID, []string{ + consumerBinary, "tx", "gov", "submit-proposal", "/dev/null", + "--from", user, + "--home", consumerHomePath, + "--keyring-backend", "test", + "--chain-id", consumerChainID, + "--fees", "1000" + bondDenom, + "--dry-run", + "-y", + }) + govErrStr := govStderr.String() + s.Require().False( + strings.Contains(govErrStr, "consumer chain is in debt") || + strings.Contains(govErrStr, "stale validator set"), + "gov tx must not be rejected by the admission gate in restricted mode; stderr=%q", + govErrStr, + ) + + // Fund the fee pool to restore normal mode. + s.T().Log("re-funding consumer fee pool to restore normal mode...") + s.providerFundConsumerFeePool("0", "20000000"+feeDenom) + + // After funding, bank sends must be accepted again. + s.T().Log("waiting for consumer to exit restricted mode after re-fund...") + s.Require().Eventuallyf(func() bool { + out, err := s.consumerBankSendDryRun() + if err != nil { + return false + } + return !strings.Contains(out, "consumer chain is in debt") && + !strings.Contains(out, "stale validator set") + }, 2*time.Minute, 5*time.Second, + "consumer did not exit restricted mode after fee pool was re-funded") + s.T().Log("consumer back in normal mode; bank sends accepted") + }) +} + +// testLivenessRemoval verifies the LAUNCHED -> STOPPED lifecycle when a +// consumer is explicitly removed via MsgRemoveConsumer. +// +// Issue #36 context: the redesign makes explicit removal (and the liveness +// sweep) the only way to stop a consumer. Timed-out VSC packets no longer +// trigger removal. This test exercises the explicit removal path. +// +// Ordering: this test must run after all other sub-tests that depend on +// consumer "0" being LAUNCHED, and before testGenesisRoundTrip (which still +// succeeds because verifyExportedGenesis only requires consumer fields to be +// present, not a specific phase). +// +// Approximation note: the STOPPED -> DELETED transition requires +// removal_time to elapse (unbonding * 0.66). With the 20-day unbonding period +// used by the shared suite this takes ~13 days, so within the test window the +// consumer will remain STOPPED. A timing-faithful CI test would use a +// short-unbonding consumer (60s -> grace ~40s) launched in a dedicated suite. +func (s *IntegrationTestSuite) testLivenessRemoval() { + s.Run("liveness removal: explicit remove-consumer transitions to STOPPED", func() { + const consumerID = "0" + + // Precondition: consumer must still be LAUNCHED. + phase := s.queryConsumerPhase(consumerID) + s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, + "consumer %s must be LAUNCHED before the liveness removal test", consumerID) + + // Issue an explicit remove-consumer via governance (gov authority is required + // since PR #53; a direct --from val tx would fail DeliverTx). + s.T().Log("submitting remove-consumer governance proposal...") + govAddr := s.queryGovAuthority() + removeJSON := fmt.Sprintf(`{ + "messages": [{ + "@type": "/vaas.provider.v1.MsgRemoveConsumer", + "authority": %q, + "consumer_id": %q + }], + "metadata": "ipfs://test", + "deposit": "10000000%s", + "title": "Remove consumer %s", + "summary": "e2e liveness removal test" +}`, govAddr, consumerID, bondDenom, consumerID) + s.submitAndPassProposal(removeJSON) + + // Wait for the provider to reflect the STOPPED phase. + s.T().Log("waiting for provider to move consumer to STOPPED or DELETED...") + s.Require().Eventuallyf(func() bool { + p := s.queryConsumerPhase(consumerID) + s.T().Logf("consumer %s phase: %s", consumerID, p) + return p == "CONSUMER_PHASE_STOPPED" || p == "CONSUMER_PHASE_DELETED" + }, 2*time.Minute, 5*time.Second, + "provider did not transition consumer %s to STOPPED/DELETED after remove-consumer", + consumerID) + + finalPhase := s.queryConsumerPhase(consumerID) + s.T().Logf("consumer %s terminal phase: %s", consumerID, finalPhase) + s.Require().True( + finalPhase == "CONSUMER_PHASE_STOPPED" || finalPhase == "CONSUMER_PHASE_DELETED", + "expected STOPPED or DELETED, got %q", finalPhase, + ) + }) +} diff --git a/tests/e2e/e2e_liveness_suite_test.go b/tests/e2e/e2e_liveness_suite_test.go new file mode 100644 index 0000000..822081c --- /dev/null +++ b/tests/e2e/e2e_liveness_suite_test.go @@ -0,0 +1,1301 @@ +package e2e + +// e2e_liveness_suite_test.go contains the LivenessIntegrationTestSuite, a +// Docker-based e2e suite that exercises liveness behaviour. The liveness grace +// period is unbonding * LivenessGraceFraction; this suite sets the fraction to +// 0.75 in provider genesis so the grace (~150s) is observable within a CI run, +// while keeping a long-enough provider/consumer unbonding (200s) that the +// relayer-derived IBC client trusting period (unbonding * 0.66 = ~132s) stays +// viable. Shrinking the grace via a short unbonding instead would collapse the +// trusting period and the relayer could never establish the clients. +// +// Two infrastructure measures keep the timing-sensitive assertions reliable: +// - Fast blocks (livenessPatchConfigToml lowers the CometBFT timeouts to +// ~1s blocks). The provider seeds lastAck at consumer launch and starts the +// grace clock then, so every setup step that precedes the first VSC sync -- +// relayer add-path, the first relay cycle, the recv/ack round-trip -- must +// finish inside the grace. At the default ~5s block time these add up to +// minutes and the consumer is swept before it ever syncs. +// - A first-sync gate (livenessWaitForConsumerSync) that blocks setup until +// the provider's lastAck has actually advanced past the launch seed, i.e. +// the relayer is delivering and the consumer is acking. Assertions then run +// from a known-synced state, independent of relayer startup jitter. +// +// The existing IntegrationTestSuite uses a ~21d provider unbonding, making the +// liveness sweep untestable in real time. This suite launches its own isolated +// set of containers (distinct chain IDs, Docker network, and host ports) and +// registers a single consumer via create_consumer_short_unbonding.json which +// carries a 200s consumer unbonding (200000000000 ns), 10m vaas_timeout +// (600000000000 ns, the MinVAASTimeoutPeriod floor), and 5s safe_mode_threshold +// (5000000000 ns). +// +// Test ordering within TestLivenessVAAS: +// 1. testRecoverBeforeGrace - brief pause < grace (~150s); consumer stays LAUNCHED. +// 2. testRealSafeMode - relayer paused > 5s; consumer enters restricted +// mode; bank send rejected; relayer unpaused; accepted. +// 3. testLivenessQuery - QueryConsumerLiveness via CLI; last_ack_time recent, +// non-zero grace, removal_eta present. +// 4. testAutoSweepRemoval - relayer stopped indefinitely; poll until STOPPED. +// +// The STOPPED -> DELETED edge (deletion at stop-time + unbonding) is unit-tested +// (TestSweepRemovesStaleConsumer), not exercised here -- see the note above +// testAutoSweepRemoval's trailing comment. + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + "time" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "github.com/stretchr/testify/suite" +) + +const ( + livenessProviderChainID = "provider-liveness" + livenessConsumerChainID = "consumer-liveness" + livenessDockerNetwork = "vaas-liveness-testnet" + + // Host port block: offset +10 from the existing suite (26657/9090/1317/26656) + // to avoid conflicts when both suites run in the same Docker host. + livenessProviderRPCPort = "26757" + livenessProviderGRPCPort = "9190" + livenessProviderRESTPort = "1417" + livenessProviderP2PPort = "26756" + + livenessConsumerRPCPort = "26767" + livenessConsumerGRPCPort = "9192" + livenessConsumerRESTPort = "1427" + livenessConsumerP2PPort = "26766" +) + +// LivenessIntegrationTestSuite mirrors IntegrationTestSuite with two key +// differences: +// - provider genesis is patched with unbonding_time "200s" and +// liveness_grace_fraction "0.75" (grace ~150s, trusting period ~132s) +// - consumer is registered via create_consumer_short_unbonding.json +// (200s unbonding, 10m vaas_timeout, 5s safe_mode_threshold) +type LivenessIntegrationTestSuite struct { + suite.Suite + + tmpDirs []string + provider *chain + consumer *chain + dkrPool *dockertest.Pool + dkrNet *dockertest.Network + providerValRes []*dockertest.Resource + consumerValRes []*dockertest.Resource + tsRelayerResource *dockertest.Resource +} + +// TestLivenessIntegrationTestSuite is the entry point for the short-unbonding +// liveness e2e suite. +func TestLivenessIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(LivenessIntegrationTestSuite)) +} + +// SetupSuite brings up provider + consumer + ts-relayer with short timers. +func (s *LivenessIntegrationTestSuite) SetupSuite() { + s.T().Log("setting up liveness e2e suite (short unbonding)...") + + var err error + + s.dkrPool, err = dockertest.NewPool("") + s.Require().NoError(err, "failed to create docker pool") + s.dkrPool.MaxWait = 5 * time.Minute + + s.livenessCleanupStaleContainers() + + s.dkrNet, err = s.dkrPool.CreateNetwork(livenessDockerNetwork) + s.Require().NoError(err, "failed to create liveness docker network") + + s.T().Log("step 1: initializing liveness provider chain...") + s.provider = &chain{id: livenessProviderChainID} + s.livenessInitAndStartProvider() + + s.T().Log("step 2: registering consumer on liveness provider...") + s.livenessRegisterConsumerOnProvider() + + s.T().Log("step 3: fetching consumer genesis from liveness provider...") + consumerGenesisJSON := s.livenessFetchConsumerGenesis() + + s.T().Log("step 4: initializing liveness consumer chain...") + s.consumer = &chain{id: livenessConsumerChainID} + s.livenessInitAndStartConsumer(consumerGenesisJSON) + + s.T().Log("step 5: starting ts-relayer for liveness suite...") + s.livenessSetupTSRelayer() + + s.T().Log("step 6: waiting for the consumer to sync its first VSC...") + s.livenessWaitForConsumerSync("0") + + s.T().Log("liveness e2e suite setup complete!") +} + +// livenessWaitForConsumerSync blocks until the provider has recorded a VSC ack +// from the consumer that is strictly newer than the launch seed, i.e. the +// relayer is actually delivering VSC packets and the consumer is acking them. +// +// This gate is what makes the timing-sensitive tests deterministic: the +// provider seeds lastAck at launch and starts the grace clock then, but the +// relayer needs time (add-path, then the first relay cycle) before any VSC is +// delivered. Without this gate a test could assert against a consumer that has +// launched but never synced. The grace (~150s) is sized to exceed this +// first-sync latency, so the consumer is not swept while we wait here. +func (s *LivenessIntegrationTestSuite) livenessWaitForConsumerSync(consumerID string) { + livenessURL := fmt.Sprintf("http://localhost:%s/vaas/provider/consumer_liveness/%s", + livenessProviderRESTPort, consumerID) + + var first string + s.Require().Eventuallyf(func() bool { + body, err := httpGet(livenessURL) + if err != nil { + return false + } + var res struct { + LastAckTime string `json:"last_ack_time"` + } + if json.Unmarshal(body, &res) != nil || res.LastAckTime == "" { + return false + } + // Require two distinct lastAck readings: the first records a baseline, + // the second proves lastAck is advancing (acks are flowing), not merely + // sitting at the launch seed. + if first == "" { + first = res.LastAckTime + s.T().Logf("consumer sync: baseline last_ack=%s", first) + return false + } + if res.LastAckTime != first { + s.T().Logf("consumer sync confirmed: last_ack advanced %s -> %s", first, res.LastAckTime) + return true + } + return false + }, 150*time.Second, 3*time.Second, + "consumer %s never synced a VSC ack (lastAck did not advance past the launch seed)", consumerID) +} + +// TearDownSuite cleans up all Docker resources. +func (s *LivenessIntegrationTestSuite) TearDownSuite() { + s.T().Log("tearing down liveness e2e suite...") + + if os.Getenv("VAAS_E2E_SKIP_CLEANUP") == "true" { + s.T().Log("skipping cleanup (VAAS_E2E_SKIP_CLEANUP=true)") + return + } + + s.livenessStopTSRelayer() + + for _, r := range s.consumerValRes { + if err := s.dkrPool.Purge(r); err != nil { + s.T().Logf("failed to purge consumer container: %v", err) + } + } + + for _, r := range s.providerValRes { + if err := s.dkrPool.Purge(r); err != nil { + s.T().Logf("failed to purge provider container: %v", err) + } + } + + if s.dkrNet != nil { + if err := s.dkrPool.RemoveNetwork(s.dkrNet); err != nil { + s.T().Logf("failed to remove liveness network: %v", err) + } + } + + for _, dir := range s.tmpDirs { + _ = os.RemoveAll(dir) + } +} + +// TestLivenessVAAS sequences the liveness tests in a safe order: non-destructive +// tests (recover, safe-mode, query) before the destructive sweep/delete tests. +func (s *LivenessIntegrationTestSuite) TestLivenessVAAS() { + s.testRecoverBeforeGrace() + s.testRealSafeMode() + s.testLivenessQuery() + s.testAutoSweepRemoval() +} + +// ---- infrastructure helpers ------------------------------------------------ + +func (s *LivenessIntegrationTestSuite) livenessCleanupStaleContainers() { + staleNames := []string{ + "liveness-provider-init", + "liveness-consumer-init", + fmt.Sprintf("%s-val0", livenessProviderChainID), + fmt.Sprintf("%s-val0", livenessConsumerChainID), + fmt.Sprintf("%s-%s-ts-relayer", livenessProviderChainID, livenessConsumerChainID), + } + for _, name := range staleNames { + c, err := s.dkrPool.Client.InspectContainer(name) + if err != nil { + continue + } + s.T().Logf("removing stale liveness container: %s", name) + _ = s.dkrPool.Client.RemoveContainer(docker.RemoveContainerOptions{ + ID: c.ID, + Force: true, + RemoveVolumes: true, + }) + } + _ = s.dkrPool.Client.RemoveNetwork(livenessDockerNetwork) +} + +func (s *LivenessIntegrationTestSuite) livenessRunInitContainer(name, scriptPath, containerScriptPath, dataDir, homePath string, env []string) { + initResource, err := s.dkrPool.RunWithOptions( + &dockertest.RunOptions{ + Name: name, + Repository: e2eChainImage, + NetworkID: s.dkrNet.Network.ID, + User: "nonroot", + Env: env, + Mounts: []string{ + fmt.Sprintf("%s:%s", dataDir, homePath), + fmt.Sprintf("%s:%s", scriptPath, containerScriptPath), + }, + Entrypoint: []string{"sh", containerScriptPath}, + }, + func(config *docker.HostConfig) { + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }, + ) + s.Require().NoError(err, "failed to start %s container", name) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + exitCode, err := s.dkrPool.Client.WaitContainerWithContext(initResource.Container.ID, ctx) + s.Require().NoError(err, "%s container wait failed", name) + + var logBuf bytes.Buffer + _ = s.dkrPool.Client.Logs(docker.LogsOptions{ + Container: initResource.Container.ID, + OutputStream: &logBuf, + ErrorStream: &logBuf, + Stdout: true, + Stderr: true, + }) + s.Require().Equal(0, exitCode, "%s container exited with code %d\noutput:\n%s", name, exitCode, logBuf.String()) + s.Require().NoError(s.dkrPool.Purge(initResource), "failed to purge %s container", name) +} + +func (s *LivenessIntegrationTestSuite) livenessInitAndStartProvider() { + providerDir, err := os.MkdirTemp("", "vaas-liveness-provider-") + s.Require().NoError(err) + s.tmpDirs = append(s.tmpDirs, providerDir) + s.provider.dataDir = providerDir + s.Require().NoError(os.Chmod(providerDir, 0o777)) + + scriptPath := filepath.Join(testDir(), "scripts", "provider-init.sh") + s.livenessRunInitContainer( + "liveness-provider-init", scriptPath, "/scripts/provider-init.sh", + providerDir, providerHomePath, + []string{ + "BINARY=" + providerBinary, + "HOME_DIR=" + providerHomePath, + "CHAIN_ID=" + livenessProviderChainID, + "DENOM=" + bondDenom, + "MNEMONIC=" + relayerMnemonic, + }, + ) + + genesisFile := filepath.Join(providerDir, "config", "genesis.json") + s.livenessPatchGenesisJSON(genesisFile, func(genesis map[string]any) { + appState := genesis["app_state"].(map[string]any) + + if gov, ok := appState["gov"].(map[string]any); ok { + if params, ok := gov["params"].(map[string]any); ok { + params["voting_period"] = "15s" + } + } + + if provider, ok := appState["provider"].(map[string]any); ok { + if params, ok := provider["params"].(map[string]any); ok { + // One block per epoch so a VSC packet (and its ack) flows every + // block. Combined with the fast block time (see + // livenessPatchConfigToml) this refreshes the provider's lastAck + // every few seconds, well inside the grace, so a healthy + // consumer is never swept between acks. + params["blocks_per_epoch"] = "1" + params["fees_per_block_amount"] = "1000" + // Shrink the liveness grace fraction so the grace period + // (unbonding * fraction = 200s * 0.75 = ~150s) is observable in + // a CI run, while keeping the unbonding itself long enough that + // the relayer-derived IBC client trusting period (unbonding * + // 0.66 = ~132s) survives the relayer add-path window. + // + // The grace must exceed the time from consumer launch to first + // VSC sync (the provider seeds lastAck at launch, so the clock + // starts before the relayer has delivered anything). With fast + // blocks that first sync is ~60-90s; 150s leaves margin. The + // suite also waits for that first sync explicitly before + // asserting (see livenessWaitForConsumerSync). + params["liveness_grace_fraction"] = "0.75" + } + } + + // Keep unbonding long enough for a viable IBC client trusting period + // (see liveness_grace_fraction note above); the short grace comes from + // the fraction, not from a short unbonding. + if staking, ok := appState["staking"].(map[string]any); ok { + if params, ok := staking["params"].(map[string]any); ok { + params["unbonding_time"] = "200s" + } + } + }) + + // Fast blocks so add-path, epochs, and VSC delivery all complete well + // inside the grace (see livenessPatchConfigToml). + s.livenessPatchConfigToml(filepath.Join(providerDir, "config", "config.toml")) + + s.T().Log("starting liveness provider chain container...") + resource, err := s.dkrPool.RunWithOptions( + &dockertest.RunOptions{ + Name: fmt.Sprintf("%s-val0", livenessProviderChainID), + Repository: e2eChainImage, + NetworkID: s.dkrNet.Network.ID, + Mounts: []string{ + fmt.Sprintf("%s:%s", providerDir, providerHomePath), + }, + PortBindings: map[docker.Port][]docker.PortBinding{ + "26657/tcp": {{HostIP: "", HostPort: livenessProviderRPCPort}}, + "9090/tcp": {{HostIP: "", HostPort: livenessProviderGRPCPort}}, + "1317/tcp": {{HostIP: "", HostPort: livenessProviderRESTPort}}, + "26656/tcp": {{HostIP: "", HostPort: livenessProviderP2PPort}}, + }, + Cmd: []string{providerBinary, "start", "--home", providerHomePath}, + }, + func(config *docker.HostConfig) { + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }, + ) + s.Require().NoError(err, "failed to start liveness provider container") + s.providerValRes = append(s.providerValRes, resource) + s.T().Logf("liveness provider container started: %s", resource.Container.ID[:12]) + + waitCtx, waitCancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer waitCancel() + err = s.livenessWaitForChainHeight(waitCtx, "http://localhost:"+livenessProviderRPCPort, 3) + s.Require().NoError(err, "liveness provider failed to produce blocks") + s.T().Log("liveness provider chain is producing blocks") +} + +func (s *LivenessIntegrationTestSuite) livenessRegisterConsumerOnProvider() { + templatePath := filepath.Join(testDir(), "testdata", "create_consumer_short_unbonding.json") + templateBytes, err := os.ReadFile(templatePath) + s.Require().NoError(err, "failed to read create_consumer_short_unbonding.json template") + + createConsumerJSON := strings.ReplaceAll(string(templateBytes), "CONSUMER_SHORT_CHAIN_ID", livenessConsumerChainID) + + s.livenessdockerExecMust(s.providerValRes[0].Container.ID, []string{ + "sh", "-c", fmt.Sprintf("echo '%s' > /tmp/create_consumer.json", createConsumerJSON), + }) + + _, _, err = s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "tx", "provider", "create-consumer", "/tmp/create_consumer.json", + "--from", "val", + "--home", providerHomePath, + "--keyring-backend", "test", + "--chain-id", livenessProviderChainID, + "--gas", "auto", + "--gas-adjustment", "1.5", + "--fees", "10000" + bondDenom, + "-y", + }) + s.Require().NoError(err, "failed to create consumer on liveness provider") + time.Sleep(10 * time.Second) +} + +func (s *LivenessIntegrationTestSuite) livenessFetchConsumerGenesis() []byte { + var output string + var lastErr error + + for range 30 { + stdout, _, err := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "provider", "consumer-genesis", "0", + "--home", providerHomePath, + "--output", "json", + }) + if err == nil { + output = stdout.String() + if output != "" && !strings.Contains(output, "not found") && !strings.Contains(output, "Error") { + break + } + } + lastErr = err + time.Sleep(3 * time.Second) + } + s.Require().NotEmpty(output, "consumer genesis is empty, last error: %v", lastErr) + s.T().Logf("fetched liveness consumer genesis (%d bytes)", len(output)) + return []byte(output) +} + +func (s *LivenessIntegrationTestSuite) livenessInitAndStartConsumer(consumerGenesisJSON []byte) { + consumerDir, err := os.MkdirTemp("", "vaas-liveness-consumer-") + s.Require().NoError(err) + s.tmpDirs = append(s.tmpDirs, consumerDir) + s.consumer.dataDir = consumerDir + s.Require().NoError(os.Chmod(consumerDir, 0o777)) + + scriptPath := filepath.Join(testDir(), "scripts", "consumer-init.sh") + s.livenessRunInitContainer( + "liveness-consumer-init", scriptPath, "/scripts/consumer-init.sh", + consumerDir, consumerHomePath, + []string{ + "BINARY=" + consumerBinary, + "HOME_DIR=" + consumerHomePath, + "CHAIN_ID=" + livenessConsumerChainID, + "DENOM=" + bondDenom, + "MNEMONIC=" + relayerMnemonic, + }, + ) + + genesisFile := filepath.Join(consumerDir, "config", "genesis.json") + err = patchConsumerGenesisWithProviderData(genesisFile, consumerGenesisJSON) + s.Require().NoError(err, "failed to patch liveness consumer genesis") + + s.livenessPatchConsumerSlashingParams() + + // Fast blocks on the consumer too, so its side of add-path and the VSC + // recv/ack round-trip keep pace with the provider (see livenessPatchConfigToml). + s.livenessPatchConfigToml(filepath.Join(consumerDir, "config", "config.toml")) + + providerDir := s.provider.dataDir + err = copyFile( + filepath.Join(providerDir, "config", "priv_validator_key.json"), + filepath.Join(consumerDir, "config", "priv_validator_key.json"), + ) + s.Require().NoError(err, "failed to copy priv_validator_key.json") + + err = copyFile( + filepath.Join(providerDir, "config", "node_key.json"), + filepath.Join(consumerDir, "config", "node_key.json"), + ) + s.Require().NoError(err, "failed to copy node_key.json") + + s.T().Log("starting liveness consumer chain container...") + resource, err := s.dkrPool.RunWithOptions( + &dockertest.RunOptions{ + Name: fmt.Sprintf("%s-val0", livenessConsumerChainID), + Repository: e2eChainImage, + NetworkID: s.dkrNet.Network.ID, + Mounts: []string{ + fmt.Sprintf("%s:%s", consumerDir, consumerHomePath), + }, + PortBindings: map[docker.Port][]docker.PortBinding{ + "26657/tcp": {{HostIP: "", HostPort: livenessConsumerRPCPort}}, + "9090/tcp": {{HostIP: "", HostPort: livenessConsumerGRPCPort}}, + "1317/tcp": {{HostIP: "", HostPort: livenessConsumerRESTPort}}, + "26656/tcp": {{HostIP: "", HostPort: livenessConsumerP2PPort}}, + }, + Cmd: []string{consumerBinary, "start", "--home", consumerHomePath}, + }, + func(config *docker.HostConfig) { + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }, + ) + s.Require().NoError(err, "failed to start liveness consumer container") + s.consumerValRes = append(s.consumerValRes, resource) + s.T().Logf("liveness consumer container started: %s", resource.Container.ID[:12]) + + waitCtx, waitCancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer waitCancel() + err = s.livenessWaitForChainHeight(waitCtx, "http://localhost:"+livenessConsumerRPCPort, 3) + s.Require().NoError(err, "liveness consumer failed to produce blocks") + s.T().Log("liveness consumer chain is producing blocks") +} + +func (s *LivenessIntegrationTestSuite) livenessSetupTSRelayer() { + s.livenessStartTSRelayer() + s.livenesstsRelayerAddMnemonic(livenessProviderChainID, relayerMnemonic) + s.livenesstsRelayerAddMnemonic(livenessConsumerChainID, relayerMnemonic) + s.livenesstsRelayerAddGasPrice(livenessProviderChainID, "0.025"+bondDenom) + s.livenesstsRelayerAddGasPrice(livenessConsumerChainID, "0.025"+bondDenom) + s.livenesstsRelayerAddPath(IBCv2) + s.livenesstsRelayerDumpPaths() + s.livenessStartTSRelayerRelay() + s.T().Log("liveness ts-relayer IBC v2 path configured") +} + +func (s *LivenessIntegrationTestSuite) livenessStartTSRelayer() { + s.T().Log("starting liveness ts-relayer container") + resource, err := s.dkrPool.RunWithOptions( + &dockertest.RunOptions{ + Name: fmt.Sprintf("%s-%s-ts-relayer", livenessProviderChainID, livenessConsumerChainID), + Repository: tsRelayerImage, + Tag: tsRelayerImageTag, + NetworkID: s.dkrNet.Network.ID, + User: "root", + CapAdd: []string{"IPC_LOCK"}, + }, + noRestart, + ) + s.Require().NoError(err, "failed to start liveness ts-relayer container") + s.tsRelayerResource = resource + s.T().Logf("liveness ts-relayer started: %s", resource.Container.ID[:12]) + + time.Sleep(3 * time.Second) + + providerURL := "http://" + s.providerValRes[0].Container.Name[1:] + ":26657" + consumerURL := "http://" + s.consumerValRes[0].Container.Name[1:] + ":26657" + + s.livenessVerifyTSRelayerConnectivity("liveness-provider", providerURL) + s.livenessVerifyTSRelayerConnectivity("liveness-consumer", consumerURL) +} + +func (s *LivenessIntegrationTestSuite) livenessVerifyTSRelayerConnectivity(chainName, rpcURL string) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + for attempt := range 10 { + exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{ + Context: ctx, + AttachStdout: true, + AttachStderr: true, + Container: s.tsRelayerResource.Container.ID, + User: "root", + Cmd: []string{"wget", "-qO-", fmt.Sprintf("%s/status", rpcURL)}, + }) + s.Require().NoError(err) + + var out bytes.Buffer + err = s.dkrPool.Client.StartExec(exec.ID, docker.StartExecOptions{ + Context: ctx, + Detach: false, + OutputStream: &out, + ErrorStream: &out, + }) + _ = err + + for { + inspectExec, err := s.dkrPool.Client.InspectExec(exec.ID) + s.Require().NoError(err) + if !inspectExec.Running { + if inspectExec.ExitCode == 0 && strings.Contains(out.String(), "latest_block_height") { + s.T().Logf("liveness ts-relayer can reach %s at %s", chainName, rpcURL) + return + } + break + } + } + s.T().Logf("liveness ts-relayer connectivity to %s attempt %d failed", chainName, attempt+1) + time.Sleep(2 * time.Second) + } + s.Require().Fail("liveness ts-relayer cannot reach %s at %s", chainName, rpcURL) +} + +func (s *LivenessIntegrationTestSuite) livenessExecuteTSRelayerCommand(ctx context.Context, args []string) []byte { + tsRelayerBinary := []string{"/bin/with_keyring", "ibc-v2-ts-relayer"} + cmd := append(tsRelayerBinary, args...) + exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{ + Context: ctx, + AttachStdout: true, + AttachStderr: true, + Container: s.tsRelayerResource.Container.ID, + User: "root", + Cmd: cmd, + }) + s.Require().NoError(err) + + var out bytes.Buffer + err = s.dkrPool.Client.StartExec(exec.ID, docker.StartExecOptions{ + Context: ctx, + Detach: false, + OutputStream: &out, + ErrorStream: &out, + }) + s.Require().NoError(err, "liveness ts-relayer startExec error: %s", out.String()) + + for { + inspectExec, err := s.dkrPool.Client.InspectExec(exec.ID) + s.Require().NoError(err) + if !inspectExec.Running { + s.Require().Equal(0, inspectExec.ExitCode, + "liveness ts-relayer cmd '%s' failed (exit=%d): %s", + strings.Join(cmd, " "), inspectExec.ExitCode, out.String()) + break + } + } + return out.Bytes() +} + +func (s *LivenessIntegrationTestSuite) livenesstsRelayerAddMnemonic(chainID, mnemonic string) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + s.livenessExecuteTSRelayerCommand(ctx, []string{"add-mnemonic", "-c", chainID, "--mnemonic", mnemonic}) +} + +func (s *LivenessIntegrationTestSuite) livenesstsRelayerAddGasPrice(chainID, gasPrice string) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + s.livenessExecuteTSRelayerCommand(ctx, []string{"add-gas-price", "-c", chainID, "--gas-adjustment", "2.0", gasPrice}) +} + +func (s *LivenessIntegrationTestSuite) livenesstsRelayerAddPath(ibcVersion string) { + s.T().Logf("liveness ts-relayer: adding IBCv%s path", ibcVersion) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + s.livenessExecuteTSRelayerCommand(ctx, []string{ + "add-path", + "-s", livenessProviderChainID, + "-d", livenessConsumerChainID, + "--surl", "http://" + s.providerValRes[0].Container.Name[1:] + ":26657", + "--durl", "http://" + s.consumerValRes[0].Container.Name[1:] + ":26657", + "--st", "cosmos", + "--dt", "cosmos", + "--ibc-version", ibcVersion, + }) +} + +func (s *LivenessIntegrationTestSuite) livenesstsRelayerDumpPaths() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + s.livenessExecuteTSRelayerCommand(ctx, []string{"dump-paths"}) +} + +func (s *LivenessIntegrationTestSuite) livenessStartTSRelayerRelay() { + s.T().Log("liveness ts-relayer: starting relay process") + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + cmd := []string{"/bin/with_keyring", "ibc-v2-ts-relayer", "relay"} + exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{ + Context: ctx, + AttachStdout: true, + AttachStderr: true, + Container: s.tsRelayerResource.Container.ID, + User: "root", + Cmd: cmd, + }) + s.Require().NoError(err, "failed to create liveness relay exec") + err = s.dkrPool.Client.StartExec(exec.ID, docker.StartExecOptions{Context: ctx, Detach: true}) + s.Require().NoError(err, "failed to start liveness relay process") + time.Sleep(3 * time.Second) + inspectExec, err := s.dkrPool.Client.InspectExec(exec.ID) + s.Require().NoError(err) + s.Require().True(inspectExec.Running, "liveness relay process is not running") +} + +func (s *LivenessIntegrationTestSuite) livenessStopTSRelayer() { + if s.tsRelayerResource != nil { + s.T().Log("tearing down liveness ts-relayer...") + s.Require().NoError(s.dkrPool.Purge(s.tsRelayerResource)) + s.tsRelayerResource = nil + } +} + +func (s *LivenessIntegrationTestSuite) livenessWaitForChainHeight(ctx context.Context, rpcEndpoint string, minHeight int64) error { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timeout waiting for chain at %s to reach height %d", rpcEndpoint, minHeight) + case <-ticker.C: + height, err := queryBlockHeight(rpcEndpoint) + if err != nil { + continue + } + if height >= minHeight { + return nil + } + } + } +} + +// ---- genesis helpers ------------------------------------------------------- + +func (s *LivenessIntegrationTestSuite) livenessPatchGenesisJSON(path string, mutate func(map[string]any)) { + bz, err := os.ReadFile(path) + s.Require().NoError(err, "failed to read genesis file") + var genesis map[string]any + s.Require().NoError(json.Unmarshal(bz, &genesis), "failed to unmarshal genesis") + mutate(genesis) + out, err := json.MarshalIndent(genesis, "", " ") + s.Require().NoError(err, "failed to marshal genesis") + s.Require().NoError(os.WriteFile(path, out, 0o600), "failed to write genesis") +} + +// livenessPatchConfigToml lowers the CometBFT consensus timeouts so the chain +// produces blocks roughly every second instead of the ~5s default. Fast blocks +// matter here because every time-bounded step in the suite -- relayer add-path +// (which waits for client header heights), epoch/VSC cadence, and the VSC +// recv/ack round-trip -- is measured against the liveness grace. At the default +// block time these add up to minutes and the grace (which starts ticking at +// consumer launch) can expire before the consumer ever syncs. +func (s *LivenessIntegrationTestSuite) livenessPatchConfigToml(path string) { + bz, err := os.ReadFile(path) + s.Require().NoError(err, "failed to read config.toml") + out := string(bz) + for from, to := range map[string]string{ + `timeout_propose = "3s"`: `timeout_propose = "500ms"`, + `timeout_commit = "5s"`: `timeout_commit = "1s"`, + `timeout_propose_delta = "500ms"`: `timeout_propose_delta = "200ms"`, + } { + s.Require().Containsf(out, from, "config.toml missing %q (default may have changed)", from) + out = strings.ReplaceAll(out, from, to) + } + s.Require().NoError(os.WriteFile(path, []byte(out), 0o600), "failed to write config.toml") +} + +func (s *LivenessIntegrationTestSuite) livenessPatchConsumerSlashingParams() { + s.livenessPatchGenesisJSON(s.consumer.dataDir+"/config/genesis.json", func(genesis map[string]any) { + appState, ok := genesis["app_state"].(map[string]any) + if !ok { + return + } + slashing, ok := appState["slashing"].(map[string]any) + if !ok { + slashing = make(map[string]any) + } + params, ok := slashing["params"].(map[string]any) + if !ok { + params = make(map[string]any) + } + params["signed_blocks_window"] = "5" + params["min_signed_per_window"] = "0.050000000000000000" + params["slash_fraction_downtime"] = "0.000000000000000000" + params["downtime_jail_duration"] = "60s" + slashing["params"] = params + appState["slashing"] = slashing + }) +} + +// ---- exec helpers ---------------------------------------------------------- + +func (s *LivenessIntegrationTestSuite) livenessdockerExec(containerID string, cmd []string) (bytes.Buffer, bytes.Buffer, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + var stdout, stderr bytes.Buffer + exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{ + Context: ctx, + AttachStdout: true, + AttachStderr: true, + Container: containerID, + User: "nonroot", + Cmd: cmd, + }) + if err != nil { + return stdout, stderr, fmt.Errorf("failed to create exec: %w", err) + } + err = s.dkrPool.Client.StartExec(exec.ID, docker.StartExecOptions{ + Context: ctx, + Detach: false, + OutputStream: &stdout, + ErrorStream: &stderr, + }) + if err != nil { + return stdout, stderr, fmt.Errorf("failed to start exec: %w", err) + } + return stdout, stderr, nil +} + +func (s *LivenessIntegrationTestSuite) livenessdockerExecMust(containerID string, cmd []string) { + stdout, stderr, err := s.livenessdockerExec(containerID, cmd) + if err != nil { + s.T().Logf("cmd: %v", cmd) + s.T().Logf("stdout: %s", stdout.String()) + s.T().Logf("stderr: %s", stderr.String()) + } + s.Require().NoError(err, "docker exec failed for cmd: %v", cmd) +} + +// ---- query helpers --------------------------------------------------------- + +func (s *LivenessIntegrationTestSuite) livenessQueryConsumerPhase(consumerID string) string { + stdout, _, err := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "provider", "consumer-chain", consumerID, + "--home", providerHomePath, + "--output", "json", + }) + if err != nil { + s.T().Logf("livenessQueryConsumerPhase(%s): exec error: %v", consumerID, err) + return "" + } + var res struct { + Phase string `json:"phase"` + } + if err := json.Unmarshal(stdout.Bytes(), &res); err != nil { + s.T().Logf("livenessQueryConsumerPhase(%s): decode error: %v (raw: %s)", consumerID, err, stdout.String()) + return "" + } + return res.Phase +} + +func (s *LivenessIntegrationTestSuite) livenessConsumerUserBech32() string { + stdout, _, err := s.livenessdockerExec(s.consumerValRes[0].Container.ID, []string{ + consumerBinary, "keys", "show", "user", "-a", + "--home", consumerHomePath, + "--keyring-backend", "test", + }) + s.Require().NoError(err, "failed to get liveness consumer user address") + return strings.TrimSpace(stdout.String()) +} + +func (s *LivenessIntegrationTestSuite) livenessConsumerBankSendDryRun() (string, error) { + user := s.livenessConsumerUserBech32() + _, stderr, err := s.livenessdockerExec(s.consumerValRes[0].Container.ID, []string{ + consumerBinary, "tx", "bank", "send", user, user, "1" + bondDenom, + "--home", consumerHomePath, + "--keyring-backend", "test", + "--chain-id", livenessConsumerChainID, + "--fees", "1000" + bondDenom, + "--dry-run", + "-y", + }) + return stderr.String(), err +} + +func (s *LivenessIntegrationTestSuite) livenessQueryGovAuthority() string { + stdout, _, err := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "auth", "module-account", "gov", + "--home", providerHomePath, + "--output", "json", + }) + s.Require().NoError(err, "failed to query gov module account") + + var res struct { + Account struct { + Address string `json:"address"` + BaseAccount struct { + Address string `json:"address"` + } `json:"base_account"` + Value struct { + Address string `json:"address"` + BaseAccount struct { + Address string `json:"address"` + } `json:"base_account"` + } `json:"value"` + } `json:"account"` + } + s.Require().NoError(json.Unmarshal(stdout.Bytes(), &res), + "failed to decode module-account response: %s", stdout.String()) + + candidates := []string{ + res.Account.BaseAccount.Address, + res.Account.Value.Address, + res.Account.Value.BaseAccount.Address, + res.Account.Address, + } + for _, addr := range candidates { + if addr != "" { + return addr + } + } + s.Require().Fail("gov authority address not found in module-account response") + return "" +} + +func (s *LivenessIntegrationTestSuite) livenessSubmitAndPassProposal(proposalJSON string) { + containerID := s.providerValRes[0].Container.ID + + payload := base64.StdEncoding.EncodeToString([]byte(proposalJSON)) + _, _, err := s.livenessdockerExec(containerID, []string{ + "sh", "-c", + fmt.Sprintf("echo %s | base64 -d > /tmp/proposal.json", payload), + }) + s.Require().NoError(err, "failed to write proposal.json") + + stdout, stderr, err := s.livenessdockerExec(containerID, []string{ + providerBinary, "tx", "gov", "submit-proposal", "/tmp/proposal.json", + "--from", "val", + "--home", providerHomePath, + "--keyring-backend", "test", + "--chain-id", livenessProviderChainID, + "--fees", "10000" + bondDenom, + "--yes", + "-o", "json", + }) + s.Require().NoErrorf(err, "failed to submit proposal: stdout=%s stderr=%s", + stdout.String(), stderr.String()) + + var submitRes struct { + TxHash string `json:"txhash"` + Code int `json:"code"` + RawLog string `json:"raw_log"` + } + s.Require().NoError(json.Unmarshal(stdout.Bytes(), &submitRes), + "failed to decode submit-proposal response: %s", stdout.String()) + s.Require().Equalf(0, submitRes.Code, "submit-proposal failed: %s", submitRes.RawLog) + s.Require().NotEmpty(submitRes.TxHash, "submit-proposal returned empty txhash") + + var proposalID uint64 + var lastTxOut string + s.Require().Eventuallyf(func() bool { + txOut, _, qerr := s.livenessdockerExec(containerID, []string{ + providerBinary, "query", "tx", submitRes.TxHash, + "--home", providerHomePath, + "--output", "json", + }) + if qerr != nil || txOut.Len() == 0 { + return false + } + lastTxOut = txOut.String() + proposalID = extractProposalID(txOut.Bytes()) + return proposalID != 0 + }, 30*time.Second, 2*time.Second, + "could not parse proposal_id from tx %s: last stdout=%s", + submitRes.TxHash, lastTxOut) + + voteStdout, voteStderr, err := s.livenessdockerExec(containerID, []string{ + providerBinary, "tx", "gov", "vote", fmt.Sprintf("%d", proposalID), "yes", + "--from", "val", + "--home", providerHomePath, + "--keyring-backend", "test", + "--chain-id", livenessProviderChainID, + "--fees", "10000" + bondDenom, + "--yes", + }) + s.Require().NoErrorf(err, "failed to vote on proposal %d: stdout=%s stderr=%s", + proposalID, voteStdout.String(), voteStderr.String()) + + s.Require().Eventuallyf(func() bool { + txOut, _, qerr := s.livenessdockerExec(containerID, []string{ + providerBinary, "query", "gov", "proposal", strconv.FormatUint(proposalID, 10), + "--home", providerHomePath, + "--output", "json", + }) + if qerr != nil || txOut.Len() == 0 { + return false + } + var res struct { + Status string `json:"status"` + Proposal struct { + Status string `json:"status"` + } `json:"proposal"` + } + if json.Unmarshal(txOut.Bytes(), &res) != nil { + return false + } + status := res.Status + if status == "" { + status = res.Proposal.Status + } + switch status { + case "PROPOSAL_STATUS_PASSED": + return true + case "PROPOSAL_STATUS_REJECTED", "PROPOSAL_STATUS_FAILED": + s.Require().Failf("proposal terminated unsuccessfully", + "proposal %d ended with status %s", proposalID, status) + return true + } + return false + }, 30*time.Second, 2*time.Second, "proposal %d did not pass within timeout", proposalID) +} + +func (s *LivenessIntegrationTestSuite) livenessFundConsumerFeePool(consumerID, amount string) { + stdout, stderr, err := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "tx", "provider", "fund-consumer-fee-pool", + consumerID, amount, + "--from", "val", + "--home", providerHomePath, + "--keyring-backend", "test", + "--chain-id", livenessProviderChainID, + "--fees", "10000" + bondDenom, + "-y", + "-o", "json", + }) + s.Require().NoErrorf(err, "failed to fund consumer %s fee pool: stdout=%s stderr=%s", + consumerID, stdout.String(), stderr.String()) + + var bres struct { + TxHash string `json:"txhash"` + Code int `json:"code"` + RawLog string `json:"raw_log"` + } + s.Require().NoError(json.Unmarshal(stdout.Bytes(), &bres)) + s.Require().Equalf(0, bres.Code, "fund-consumer-fee-pool failed: %s", bres.RawLog) +} + +// ---- test methods ---------------------------------------------------------- + +// testRecoverBeforeGrace pauses the consumer for ~10s (far less than the ~150s +// grace), unpauses it, and asserts the consumer remains LAUNCHED. This +// exercises the ack-refreshing code path: as long as an ack arrives before +// grace expires, the clock resets and the consumer is not swept. +func (s *LivenessIntegrationTestSuite) testRecoverBeforeGrace() { + s.Run("recover before grace: consumer stays LAUNCHED after brief outage", func() { + const consumerID = "0" + + // Non-fatal diagnostic: log the consumer's liveness state at the start of + // the run so the ack cadence (last_ack vs removal_eta) is visible even if + // a later assertion fails. The dedicated check is testLivenessQuery. + livenessURL := fmt.Sprintf("http://localhost:%s/vaas/provider/consumer_liveness/%s", + livenessProviderRESTPort, consumerID) + if body, err := httpGet(livenessURL); err == nil { + s.T().Logf("diagnostic: initial consumer liveness: %s", string(body)) + } + + phase := s.livenessQueryConsumerPhase(consumerID) + s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, + "consumer %s must be LAUNCHED before recover-before-grace test", consumerID) + + s.T().Log("pausing consumer container for ~10s (far less than ~150s grace)...") + err := s.dkrPool.Client.PauseContainer(s.consumerValRes[0].Container.ID) + s.Require().NoError(err, "failed to pause consumer container") + + time.Sleep(10 * time.Second) + + s.T().Log("unpausing consumer container (outage ends before grace expiry)...") + err = s.dkrPool.Client.UnpauseContainer(s.consumerValRes[0].Container.ID) + s.Require().NoError(err, "failed to unpause consumer container") + + // Allow a few epochs for an ack to arrive and reset the liveness clock. + time.Sleep(10 * time.Second) + + // Diagnostic: capture the liveness state at the moment of assertion so a + // failure shows lastAck/removal_eta vs the sweep, not just the phase. + if body, err := httpGet(livenessURL); err == nil { + s.T().Logf("diagnostic: post-recovery consumer liveness: %s", string(body)) + } + + phase = s.livenessQueryConsumerPhase(consumerID) + s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, + "consumer %s must remain LAUNCHED after a transient outage shorter than grace", consumerID) + s.T().Log("consumer remains LAUNCHED after recovery before grace") + }) +} + +// testRealSafeMode freezes VSC delivery so the consumer goes >5s without a VSC +// packet (the safe_mode_threshold). The consumer's MsgFilterDecorator then +// enters restricted mode and rejects bank sends. Once VSC delivery resumes, the +// consumer exits restricted mode. +// +// This exercises the VSC-staleness path directly (unlike the existing suite +// which relies on the fee-debt path as an approximation, because its +// safe_mode_threshold is 3h). +// +// Delivery is frozen by *pausing the relayer container* (docker pause), not by +// purging and rebuilding it. The distinction is essential: a rebuild runs +// add-path, which creates brand-new IBC clients. The provider, however, keeps +// using its already-discovered client as long as that client stays Active and +// has a counterparty (see discoverActiveConsumerClient) -- which it does for +// the ~132s trusting period. So a rebuild would leave the provider sending VSCs +// to the original, now-unrelayed client and the consumer would never recover +// within grace. Pausing the relayer leaves the original client untouched, so on +// unpause the relay loop resumes on the same client and delivery continues. It +// also keeps the outage short, decoupling this consumer-side test from the +// provider-side liveness grace. The consumer keeps producing blocks throughout +// (its staleness clock is wall-time on its own block height), which is why we +// pause the relayer rather than the consumer. +func (s *LivenessIntegrationTestSuite) testRealSafeMode() { + s.Run("real safe mode: VSC-stale consumer rejects bank sends, recovers when delivery resumes", func() { + const consumerID = "0" + + // Ensure the fee pool is funded so debt does not contaminate the test. + s.T().Log("funding consumer fee pool to avoid debt interference...") + s.livenessFundConsumerFeePool(consumerID, "20000000"+feeDenom) + + // Verify normal mode before freezing delivery. + s.Require().Eventuallyf(func() bool { + out, err := s.livenessConsumerBankSendDryRun() + if err != nil { + return false + } + return !strings.Contains(out, "consumer chain is in debt") && + !strings.Contains(out, "stale validator set") + }, 2*time.Minute, 5*time.Second, + "consumer did not enter normal mode after fee pool funding") + + s.T().Log("pausing relayer container so VSC packets are no longer relayed...") + err := s.dkrPool.Client.PauseContainer(s.tsRelayerResource.Container.ID) + s.Require().NoError(err, "failed to pause relayer container") + + // Wait for the consumer to enter restricted mode (VSC stale after >5s). + s.T().Log("waiting for consumer to enter restricted mode (VSC stale after ~5s)...") + s.Require().Eventuallyf(func() bool { + out, err := s.livenessConsumerBankSendDryRun() + return err != nil || strings.Contains(out, "stale validator set") || + strings.Contains(out, "consumer chain is in debt") + }, 2*time.Minute, 3*time.Second, + "consumer did not enter restricted mode after VSC staleness") + s.T().Log("restricted mode confirmed; bank sends are rejected") + + // Resume VSC delivery by unpausing the relayer (same client, no rebuild). + s.T().Log("unpausing relayer container to resume VSC delivery...") + err = s.dkrPool.Client.UnpauseContainer(s.tsRelayerResource.Container.ID) + s.Require().NoError(err, "failed to unpause relayer container") + + // Wait for the consumer to exit restricted mode. + s.T().Log("waiting for consumer to exit restricted mode after VSC delivery resumes...") + s.Require().Eventuallyf(func() bool { + out, err := s.livenessConsumerBankSendDryRun() + if err != nil { + return false + } + return !strings.Contains(out, "stale validator set") && + !strings.Contains(out, "consumer chain is in debt") + }, 3*time.Minute, 5*time.Second, + "consumer did not exit restricted mode after VSC delivery resumed") + s.T().Log("consumer back in normal mode after VSC delivery") + }) +} + +// testLivenessQuery queries the provider's QueryConsumerLiveness via the CLI +// (there is no dedicated liveness CLI subcommand; we use the gRPC-gateway REST +// path which is exercised by the main suite's httpGet helper) and asserts that +// last_ack_time is recent (within the last 5 minutes), grace_period is non-zero, +// and removal_eta is present. +// +// The gRPC-gateway REST path is: +// +// GET /vaas/provider/consumer_liveness/{consumer_id} +// +// The response JSON uses protobuf JSON encoding: Timestamps are RFC3339 strings +// and Durations are strings like "19.8s". +func (s *LivenessIntegrationTestSuite) testLivenessQuery() { + s.Run("liveness query: last_ack_time recent, grace_period non-zero, removal_eta present", func() { + const consumerID = "0" + + // Confirm the consumer is still LAUNCHED before checking liveness. + phase := s.livenessQueryConsumerPhase(consumerID) + s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, + "consumer must be LAUNCHED for liveness query test") + + // Diagnostic: log provider staking params and consumer chain init_params so + // the controller run can confirm the genesis patches actually took effect. + stakingOut, _, _ := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "staking", "params", + "--home", providerHomePath, "--output", "json", + }) + s.T().Logf("diagnostic: provider staking params: %s", stakingOut.String()) + + chainOut, _, _ := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "provider", "consumer-chain", consumerID, + "--home", providerHomePath, "--output", "json", + }) + s.T().Logf("diagnostic: consumer chain (init_params): %s", chainOut.String()) + + // Query liveness via the gRPC-gateway REST path. + // The response Timestamps are RFC3339 strings; Duration is a string like "19.8s". + livenessURL := fmt.Sprintf("http://localhost:%s/vaas/provider/consumer_liveness/%s", + livenessProviderRESTPort, consumerID) + + var livenessRes struct { + LastAckTime string `json:"last_ack_time"` + GracePeriod string `json:"grace_period"` + RemovalEta string `json:"removal_eta"` + } + + s.Require().Eventuallyf(func() bool { + body, err := httpGet(livenessURL) + if err != nil { + s.T().Logf("liveness REST query attempt failed: %v", err) + return false + } + s.T().Logf("liveness REST raw body: %s", string(body)) + if jsonErr := json.Unmarshal(body, &livenessRes); jsonErr != nil { + s.T().Logf("liveness REST decode failed: %v (body: %s)", jsonErr, string(body)) + return false + } + return livenessRes.LastAckTime != "" && livenessRes.GracePeriod != "" + }, 30*time.Second, 3*time.Second, + "liveness REST query did not return a valid response") + + s.T().Logf("diagnostic: consumer liveness: last_ack=%s grace=%s removal_eta=%s", + livenessRes.LastAckTime, livenessRes.GracePeriod, livenessRes.RemovalEta) + + // last_ack_time must be parseable and within 5 minutes of now. + lastAck, err := time.Parse(time.RFC3339Nano, livenessRes.LastAckTime) + if err != nil { + // gRPC-gateway may omit fractional seconds for round values. + lastAck, err = time.Parse(time.RFC3339, livenessRes.LastAckTime) + } + s.Require().NoError(err, "last_ack_time is not a valid RFC3339 timestamp: %s", livenessRes.LastAckTime) + s.Require().WithinDurationf(time.Now().UTC(), lastAck, 5*time.Minute, + "last_ack_time %s is not recent", livenessRes.LastAckTime) + + // grace_period must be non-empty and non-zero. + s.Require().NotEmpty(livenessRes.GracePeriod, "grace_period must be non-empty") + s.Require().NotEqualf("0s", livenessRes.GracePeriod, "grace_period must be non-zero") + + // removal_eta must be present and parseable. + s.Require().NotEmpty(livenessRes.RemovalEta, "removal_eta must be present") + retaAck, err := time.Parse(time.RFC3339Nano, livenessRes.RemovalEta) + if err != nil { + retaAck, err = time.Parse(time.RFC3339, livenessRes.RemovalEta) + } + s.Require().NoError(err, "removal_eta is not a valid RFC3339 timestamp: %s", livenessRes.RemovalEta) + s.Require().Truef(retaAck.After(time.Now().Add(-24*time.Hour)), + "removal_eta %s looks implausibly old", livenessRes.RemovalEta) + }) +} + +// testAutoSweepRemoval stops the relayer (and pauses the consumer) so no VSC +// acks return to the provider. After the liveness grace period (~150s) expires, +// the provider's SweepUnresponsiveConsumers moves the consumer to +// CONSUMER_PHASE_STOPPED. Polls with a ~2min window. +func (s *LivenessIntegrationTestSuite) testAutoSweepRemoval() { + s.Run("auto sweep removal: sustained outage causes LAUNCHED -> STOPPED", func() { + const consumerID = "0" + + phase := s.livenessQueryConsumerPhase(consumerID) + s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, + "consumer %s must be LAUNCHED before auto-sweep test", consumerID) + + // Diagnostic: log provider staking params and consumer liveness state so + // the controller run can confirm the genesis patches took effect. + stakingOut, _, _ := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "staking", "params", + "--home", providerHomePath, "--output", "json", + }) + s.T().Logf("diagnostic: provider staking params: %s", stakingOut.String()) + + chainOut, _, _ := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "provider", "consumer-chain", consumerID, + "--home", providerHomePath, "--output", "json", + }) + s.T().Logf("diagnostic: consumer chain (init_params): %s", chainOut.String()) + + livenessURL := fmt.Sprintf("http://localhost:%s/vaas/provider/consumer_liveness/%s", + livenessProviderRESTPort, consumerID) + if body, err := httpGet(livenessURL); err == nil { + s.T().Logf("diagnostic: consumer liveness: %s", string(body)) + } + + s.T().Log("purging ts-relayer so no VSC acks are returned to provider...") + s.livenessStopTSRelayer() + + s.T().Log("pausing consumer container to prevent acks...") + err := s.dkrPool.Client.PauseContainer(s.consumerValRes[0].Container.ID) + s.Require().NoError(err, "failed to pause consumer container for sweep test") + + // The grace period is provider_unbonding * liveness_grace_fraction = + // 200s * 0.75 = ~150s. Wait for the grace period to elapse before polling, + // so that the sweep has had time to fire by the first poll iteration. + const gracePlusBuffer = 160 * time.Second + s.T().Logf("outage started; waiting %s for grace period to elapse...", gracePlusBuffer) + time.Sleep(gracePlusBuffer) + + // Poll until the provider sweeps the consumer to STOPPED. + // Allow up to 2 minutes total (grace already elapsed above). + s.T().Log("polling for CONSUMER_PHASE_STOPPED (timeout 2min)...") + s.Require().Eventuallyf(func() bool { + p := s.livenessQueryConsumerPhase(consumerID) + s.T().Logf("consumer %s phase: %s", consumerID, p) + return p == "CONSUMER_PHASE_STOPPED" + }, 2*time.Minute, 5*time.Second, + "provider did not sweep consumer %s to STOPPED within 2 minutes", consumerID) + + s.T().Logf("consumer %s successfully swept to STOPPED", consumerID) + }) +} + +// Note on STOPPED -> DELETED: the sweep schedules deletion at +// blockTime + providerUnbonding, the same path a governance removal takes. +// With a viable (relayer-survivable) ~200s unbonding, the real DELETED edge +// would only fire ~200s after STOPPED and so is deliberately not exercised +// here -- it is covered by TestSweepRemovesStaleConsumer, which asserts the +// removal_time scheduling directly. This e2e suite's contribution is proving +// the LAUNCHED -> STOPPED sweep fires end-to-end under real IBC silence. diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index fe3f178..da6fe1d 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -6,11 +6,22 @@ func (s *IntegrationTestSuite) TestVAAS() { s.testConsumerOnProvider() s.testProviderOnConsumer() s.testValidatorSetSync() + // Pause consumer briefly while provider VP changes; verify consumer stays + // LAUNCHED and re-converges via snapshot resync after recovery. + s.testLivenessTransientOutage() s.testConsumerDebtFlow() + // Verify MsgFilterDecorator restricted mode (debt gate) rejects bank sends + // and allows gov/ibc.core messages; exercises the same code path as VSC + // staleness (safe mode). + s.testLivenessSafeMode() s.testDowntimeSlash() s.testFeePoolSendRestriction() s.testFeePoolFundAndLockEnforcement() s.testFeePoolGovSubsidyClawback() + // Explicitly remove consumer "0"; verify STOPPED (DELETED if removal_time + // has elapsed). Must run after all tests that rely on consumer "0" being + // LAUNCHED and before testGenesisRoundTrip (which tolerates any phase). + s.testLivenessRemoval() // Run last: stops the provider container and replaces it with a fresh // one started from the exported genesis. s.testGenesisRoundTrip() diff --git a/tests/e2e/testdata/create_consumer_short_unbonding.json b/tests/e2e/testdata/create_consumer_short_unbonding.json new file mode 100644 index 0000000..41301b1 --- /dev/null +++ b/tests/e2e/testdata/create_consumer_short_unbonding.json @@ -0,0 +1,33 @@ +{ + "chain_id": "CONSUMER_SHORT_CHAIN_ID", + "metadata": { + "name": "consumer-short", + "description": "e2e liveness test consumer with short unbonding period", + "metadata": "{}" + }, + "initialization_parameters": { + "initial_height": { + "revision_number": 0, + "revision_height": 1 + }, + "genesis_hash": "", + "binary_hash": "", + "spawn_time": "2024-01-01T00:00:00Z", + "unbonding_period": 200000000000, + "vaas_timeout_period": 600000000000, + "safe_mode_threshold": 5000000000, + "historical_entries": 10000 + }, + "infraction_parameters": { + "double_sign": { + "slash_fraction": "0.05", + "jail_duration": 9223372036854775807, + "tombstone": true + }, + "downtime": { + "slash_fraction": "0.0001", + "jail_duration": 600000000000, + "tombstone": false + } + } +} From c799535744119230cad5b721ea9f701bae9d9c41 Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:44:07 +0200 Subject: [PATCH 09/20] document consumer liveness, removal, and resync Add docs/consumer-liveness.md (liveness clock and sweep, snapshot resync, demoted IBC callbacks, consumer safe mode, params/bounds, operator query) and reconcile the lifecycle doc. --- docs/consumer-lifecycle.md | 15 ++- docs/consumer-liveness.md | 185 +++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 docs/consumer-liveness.md diff --git a/docs/consumer-lifecycle.md b/docs/consumer-lifecycle.md index 06f9119..620f68a 100644 --- a/docs/consumer-lifecycle.md +++ b/docs/consumer-lifecycle.md @@ -2,6 +2,8 @@ This document describes the full lifecycle of a consumer chain in VAAS, from registration to deletion. +For how an unresponsive or lagging launched consumer is handled -- the liveness grace period, removal sweep, snapshot resync, and consumer safe mode -- see [consumer-liveness.md](consumer-liveness.md). + ## Phases ``` @@ -104,13 +106,17 @@ is stored and used for all subsequent VSC packet delivery. 3. The relayer relays the packets to the consumer. 4. The consumer applies the validator set changes on `EndBlock`. +VSC packets are diffs by default. If a consumer falls behind on acknowledgements, the provider instead sends an absolute snapshot of the full validator set so the consumer resyncs in a single packet, and the consumer is removed only after a sustained liveness failure rather than on a single packet timeout. See [consumer-liveness.md](consumer-liveness.md). + --- ## Phase 4: STOPPED -**Trigger:** `MsgRemoveConsumer` submitted by the consumer owner. +**Triggers:** either +- `MsgRemoveConsumer` submitted by the governance authority (removing a consumer requires the gov authority), or +- the automatic liveness sweep, when a launched consumer has produced no successful VSC acknowledgement for longer than the liveness grace period (see [consumer-liveness.md](consumer-liveness.md)). -**Requirements:** consumer must be in `LAUNCHED` phase. Only the owner can submit. +**Requirements:** consumer must be in `LAUNCHED` phase. **What happens on-chain:** 1. Phase is set to `STOPPED`. @@ -129,7 +135,8 @@ is stored and used for all subsequent VSC packet delivery. 1. Up to 200 due consumers are dequeued per block. 2. For each consumer, `DeleteConsumerChain` runs: - Deletes: IBC client ID mapping, consumer genesis, key assignments, equivocation - evidence minimum height, pending VSC packets, validator set, removal time. + evidence minimum height, pending VSC packets, validator set, removal time, and the + liveness state (last-ack time and the sent/acked VSC-id counters). - **Preserves** (for block explorer use): chain ID, phase, owner address, metadata, initialization parameters. 3. Phase is set to `DELETED`. @@ -143,5 +150,5 @@ is stored and used for all subsequent VSC packet delivery. | `REGISTERED` | `MsgCreateConsumer` | Any account | Consumer created, owner assigned | | `INITIALIZED` | `spawn_time` set | On-chain (automatic) | Queued for launch at spawn_time | | `LAUNCHED` | `spawn_time` elapsed | On-chain (BeginBlock) | Genesis built; operator starts consumer; relayer creates IBC path | -| `STOPPED` | `MsgRemoveConsumer` | Owner | Queued for deletion after unbonding period | +| `STOPPED` | `MsgRemoveConsumer` (gov) or liveness sweep | Governance / on-chain | Queued for deletion after unbonding period | | `DELETED` | Unbonding period elapsed | On-chain (BeginBlock) | State cleaned up | diff --git a/docs/consumer-liveness.md b/docs/consumer-liveness.md new file mode 100644 index 0000000..801e0df --- /dev/null +++ b/docs/consumer-liveness.md @@ -0,0 +1,185 @@ +# Consumer Liveness, Removal, and Resync + +This document describes how the provider handles an unresponsive or lagging consumer +chain: when (and why) a consumer is removed, how a consumer that briefly falls behind +heals itself, and how a consumer protects itself while its validator set may be stale. + +It complements [consumer-lifecycle.md](consumer-lifecycle.md), which covers the overall +`REGISTERED -> INITIALIZED -> LAUNCHED -> STOPPED -> DELETED` progression. This document +zooms in on what keeps a `LAUNCHED` consumer healthy and what happens when it is not. + +## Why this exists + +The provider sends a Validator Set Change (VSC) packet to each launched consumer every +epoch. Earlier, a single failure to deliver one of those packets -- an IBC packet timeout, +an error acknowledgement, or a local send failure -- immediately and irreversibly moved the +consumer to `STOPPED`. Two facts made that too severe: + +1. **The packet timeout is effectively 24h, not the configured value.** VSC packets are + sent with a timeout of `min(VaasTimeoutPeriod, MaxTimeoutDelta)`, and IBC v2 hard-caps + `MaxTimeoutDelta` at 24h. So a 24h outage of a single relayer or RPC was enough to + permanently retire a consumer. +2. **VSC packets are diffs.** Each packet carries only the validators that changed since + the previous one, applied on top of the consumer's current set. If a packet is lost, its + change is never re-sent on its own -- so dropping the eager removal would, by itself, + leave a departed validator stuck in the consumer's active set (a validator that could + later unbond on the provider and validate the consumer with no slashable stake). + +The model below replaces the per-packet kill switch with a provider-local liveness clock, +pairs it with a self-healing resync so lost diffs cannot strand a stale validator, and adds +a consumer-side safe mode for the window in which a consumer's set may be out of date. + +## 1. Liveness clock and removal sweep + +The provider tracks, per consumer, the block time of the last **successful** VSC +acknowledgement (`lastAck`). It is seeded when the consumer enters `LAUNCHED` and refreshed +every time the consumer acknowledges a VSC packet. A successful ack is the strongest +possible progress signal: it proves the consumer received *and applied* the update. + +Each `BeginBlock`, the provider sweeps every launched consumer and stops any whose `lastAck` +is older than a **grace period**: + +``` +grace = providerUnbondingPeriod * LivenessGraceFraction +``` + +derived from the provider's own unbonding period (about 14 days at the 21-day default and the +`0.66` default fraction). `LivenessGraceFraction` is a provider module param: its default +mirrors the IBC client trusting-period fraction, so the grace ends around the point where +recovery stops being possible anyway while still leaving margin below the unbonding +(slashable) window, but operators can lower it -- on short-lived test or dev chains, for +instance -- to make the sweep fire in seconds rather than days. The sweep runs on the +provider's own block clock, so it does not depend +on a live IBC client or an available relayer -- it removes a consumer whether the silence is +caused by a dead consumer, a dead relayer, or an expired client. + +When the grace period is exceeded, the consumer is moved to `STOPPED` (then `DELETED` after +the unbonding period) exactly as a governance removal would do. See +[consumer-lifecycle.md](consumer-lifecycle.md) for the `STOPPED -> DELETED` mechanics. + +**Guarantee:** a launched-but-unresponsive consumer is removed within the grace period, +which is strictly less than the provider's unbonding period. So every validator that was in +the consumer's set as of its last successful sync is still slashable on the provider at the +time of removal. + +## 2. Snapshot resync (self-healing) + +Because VSC packets are diffs, a consumer that misses packets during an outage could diverge +from the provider's true validator set. To prevent that, the provider tracks two per-consumer +counters: the highest VSC id it has **sent** and the highest the consumer has **acked**. A +consumer is **behind** when: + +``` +highestAcked < highestSent +``` + +While a consumer is behind, the provider does not send a diff. It sends an **absolute +snapshot**: the complete current validator set, flagged with `is_snapshot = true` on the +packet. The consumer, on receiving a snapshot, **replaces** its set with it -- setting each +listed validator to its absolute power and removing (power 0) any validator it currently has +that the snapshot omits. Applying a snapshot is idempotent: a behind consumer converges to +exactly the provider's current set in one packet, regardless of which intermediate packets it +missed. Once an ack catches `highestAcked` up to `highestSent`, the provider resumes sending +ordinary diffs. + +The wire format gains only a single boolean (`is_snapshot`); the validator-update list is +reused (a diff when false, the full set when true). The global VSC id counter and the +consumer's out-of-order dedup are unchanged. + +**Guarantee:** a transient outage no longer corrupts the consumer's set. As soon as +connectivity returns, the next snapshot heals it -- including removing any validator that +left the provider during the outage. + +## 3. IBC callbacks no longer remove + +With the sweep as the single authority for removing unresponsive consumers, the IBC +callbacks that used to remove are demoted to logging: + +- **Packet timeout** (`OnTimeoutPacketV2`): logs and returns; the next epoch retries. +- **Error acknowledgement**: logs and returns; the consumer keeps its grace budget. +- **Local send failure** (including an expired client): logs, leaves the pending packets + queued, and returns; the next epoch retries. + +This also closes a latent gap: previously an expired client left a consumer `LAUNCHED` +forever with no removal path. Now the sweep removes it once its grace elapses, because an +expired client produces no acks. + +## 4. Consumer safe mode + +While a consumer is behind, its local validator set may not match the provider's. The +consumer protects itself during that window. It records the block time of the last VSC packet +it received, and considers itself **stale** when it has not received one for longer than a +consumer-local threshold (`DefaultSafeModeThreshold`, 3 hours -- set well below the provider's +grace period). + +While stale (or while flagged in debt for unpaid fees), the consumer's transaction admission +gate restricts incoming transactions to `/ibc.core.*` and `/cosmos.gov.*` messages only, +rejecting value-bearing application transactions. This bounds the damage of a possibly-stale +set: the consumer refuses to do value-bearing work under validators it can no longer trust, +while keeping the recovery path open -- `/ibc.core.*` still admits the incoming VSC packets +that let the consumer resync, and `/cosmos.gov.*` keeps governance available. A freshly +launched consumer is never treated as stale before its first VSC. + +## 5. Parameters and bounds + +| Parameter | Where | Bound | Default | +|---|---|---|---| +| `VaasTimeoutPeriod` | provider module param (VSC packet timeout) and per-consumer init param (consumer evidence packet timeout) | `[10m, MaxTimeoutDelta (24h)]` | 1h | +| consumer `UnbondingPeriod` | per-consumer init param | `(0, providerUnbondingPeriod]` | provider default minus 1 day | +| `LivenessGraceFraction` | provider module param | `(0, 1]` | `0.66` | +| consumer `SafeModeThreshold` | per-consumer init param | `> 0` | 3h | + +`VaasTimeoutPeriod` is validated to `[10m, 24h]` at both the provider module param boundary +and the per-consumer initialization-parameter boundary, so the configured value is honest +rather than silently capped. The default was lowered from four weeks (which collapsed to 24h +under the cap) to one epoch-scale hour: an undelivered packet is superseded by the next +epoch's packet and a late one is dropped by the consumer's dedup, so a long timeout buys +nothing. + +The consumer `UnbondingPeriod` is bounded against the provider's at `MsgCreateConsumer` / +`MsgUpdateConsumer` time (it must not exceed the provider's), so a consumer cannot be +configured to outlive the provider's slashable window. No lower floor is enforced: an +unbonding short enough to make the relayer-derived trusting period impractical is an +operator concern (and is useful for short-lived test or dev chains), not a protocol minimum. + +## 6. Operator guidance: the liveness query + +Operators can observe a consumer's liveness without waiting for removal: + +``` +providerd query provider consumer-liveness +``` + +It returns, all derived (no stored extra state): + +- `last_ack_time` -- block time of the consumer's last successful VSC ack. +- `grace_period` -- the derived grace for this provider. +- `removal_eta` -- `last_ack_time + grace_period`: when the sweep will remove the consumer + if no further ack arrives. +- `degraded` -- true once `last_ack_time` is older than half the grace period; an early + warning that a consumer is trending toward removal. + +A `degraded` consumer that is genuinely reachable should recover on its own once VSC packets +flow again (the next snapshot resyncs it and refreshes `lastAck`). If it does not recover +before `removal_eta`, it will be stopped. + +## 7. Guarantees and limitations + +**Guarantees** + +- A consumer is removed only after sustained ack-silence approaching, but staying within, + the provider's slashable window -- never on a single transient packet failure. +- A consumer that recovers within the grace period self-heals to the provider's exact + validator set via snapshot resync, with no operator or governance action. +- While a consumer's set may be stale, it refuses value-bearing transactions but keeps the + IBC recovery path open. + +**Out of scope (candidates for follow-up work)** + +- **Owner/governance force-resume and an explicit `DEGRADED` phase.** Automatic recovery + covers the common case; a manual "buy more time" path and a distinct degraded marker are + deferred. The `lastAck` field is the groundwork for them. +- **Reviving a consumer whose IBC client has expired** (beyond the trusting period). This + needs substitute-client / client-recovery governance and is a separate effort. +- **Relayer redundancy (multi-RPC / failover).** The single-RPC risk that motivated the + original concern is a relayer-tooling matter, not a chain-side one. From 2d1ef3875b84bb8d63c30f9b32e60a842eff8c4c Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:03:49 +0200 Subject: [PATCH 10/20] harden liveness sweep, fraction validation, and consumer param reads - sweep: run StopAndPrepareForConsumerRemoval in a cached context so a partial failure cannot commit the STOPPED phase without also scheduling removal (which would strand a consumer STOPPED-but-never-DELETED); a stop-error test now covers this. - fraction validation: require a fraction strictly in (0,1), rejecting exactly 1, so the liveness grace stays inside the unbonding/slashable window and the IBC trusting period stays below unbonding. - computeReplaceUpdates: read the current cross-chain validator set once, not twice. - GetConsumerParams: panic on a params read error instead of returning a zero-value struct, whose zero SafeModeThreshold would silently pin the consumer in safe mode and hide the corruption. - genesis export: document that the liveness clock is not exported and resets safely on import (no immediate sweep, no forced snapshot). --- docs/consumer-liveness.md | 2 +- x/vaas/consumer/keeper/params.go | 8 +++- x/vaas/consumer/keeper/validators.go | 5 ++- x/vaas/provider/keeper/consumer_lifecycle.go | 9 ++++- x/vaas/provider/keeper/genesis.go | 8 ++++ x/vaas/provider/keeper/liveness_test.go | 41 +++++++++++++++----- x/vaas/types/shared_params.go | 10 ++++- 7 files changed, 67 insertions(+), 16 deletions(-) diff --git a/docs/consumer-liveness.md b/docs/consumer-liveness.md index 801e0df..a538a32 100644 --- a/docs/consumer-liveness.md +++ b/docs/consumer-liveness.md @@ -126,7 +126,7 @@ launched consumer is never treated as stale before its first VSC. |---|---|---|---| | `VaasTimeoutPeriod` | provider module param (VSC packet timeout) and per-consumer init param (consumer evidence packet timeout) | `[10m, MaxTimeoutDelta (24h)]` | 1h | | consumer `UnbondingPeriod` | per-consumer init param | `(0, providerUnbondingPeriod]` | provider default minus 1 day | -| `LivenessGraceFraction` | provider module param | `(0, 1]` | `0.66` | +| `LivenessGraceFraction` | provider module param | `(0, 1)` | `0.66` | | consumer `SafeModeThreshold` | per-consumer init param | `> 0` | 3h | `VaasTimeoutPeriod` is validated to `[10m, 24h]` at both the provider module param boundary diff --git a/x/vaas/consumer/keeper/params.go b/x/vaas/consumer/keeper/params.go index b41025a..5ee4910 100644 --- a/x/vaas/consumer/keeper/params.go +++ b/x/vaas/consumer/keeper/params.go @@ -2,6 +2,7 @@ package keeper import ( "context" + "fmt" "time" vaastypes "github.com/allinbits/vaas/x/vaas/types" @@ -11,10 +12,15 @@ import ( // GetConsumerParams returns the params for the consumer VAAS module // NOTE: it is different from the GetParams method which is required to implement StakingKeeper interface +// +// Params are set at InitGenesis and never removed, so a read error indicates +// state corruption. Panic rather than return a zero-value ConsumerParams: a +// zero SafeModeThreshold would make IsVSCStale trivially true and silently pin +// the consumer in safe mode, hiding the corruption. func (k Keeper) GetConsumerParams(ctx context.Context) vaastypes.ConsumerParams { params, err := k.Params.Get(ctx) if err != nil { - return vaastypes.ConsumerParams{} + panic(fmt.Sprintf("error getting consumer module parameters: %v", err)) } return params } diff --git a/x/vaas/consumer/keeper/validators.go b/x/vaas/consumer/keeper/validators.go index 677b67d..eab6c1e 100644 --- a/x/vaas/consumer/keeper/validators.go +++ b/x/vaas/consumer/keeper/validators.go @@ -322,10 +322,11 @@ func (k Keeper) computeReplaceUpdates(ctx context.Context, target []abci.Validat inTarget[u.PubKey.String()] = struct{}{} } - updates := make([]abci.ValidatorUpdate, 0, len(target)+len(k.GetAllCCValidator(ctx))) + current := k.GetAllCCValidator(ctx) + updates := make([]abci.ValidatorUpdate, 0, len(target)+len(current)) updates = append(updates, target...) - for _, v := range k.GetAllCCValidator(ctx) { + for _, v := range current { pk, err := v.ConsPubKey() if err != nil { continue diff --git a/x/vaas/provider/keeper/consumer_lifecycle.go b/x/vaas/provider/keeper/consumer_lifecycle.go index 8ca069a..72f9067 100644 --- a/x/vaas/provider/keeper/consumer_lifecycle.go +++ b/x/vaas/provider/keeper/consumer_lifecycle.go @@ -462,9 +462,16 @@ func (k Keeper) SweepUnresponsiveConsumers(ctx sdk.Context) error { } k.Logger(ctx).Info("consumer unresponsive past liveness grace, stopping", "consumerId", consumerId, "lastAck", lastAck, "grace", grace) - if err := k.StopAndPrepareForConsumerRemoval(ctx, consumerId); err != nil { + // Stop in a cached context so a partial failure does not commit the + // STOPPED phase without also scheduling removal (which would strand the + // consumer STOPPED-but-never-DELETED). On error nothing is written and + // the next sweep retries, mirroring BeginBlockRemoveConsumers. + cachedCtx, writeFn := ctx.CacheContext() + if err := k.StopAndPrepareForConsumerRemoval(cachedCtx, consumerId); err != nil { k.Logger(ctx).Error("failed to stop unresponsive consumer", "consumerId", consumerId, "error", err.Error()) + continue } + writeFn() } return nil } diff --git a/x/vaas/provider/keeper/genesis.go b/x/vaas/provider/keeper/genesis.go index 2ba3193..3eecf8e 100644 --- a/x/vaas/provider/keeper/genesis.go +++ b/x/vaas/provider/keeper/genesis.go @@ -280,6 +280,14 @@ func (k Keeper) InitGenesisValUpdates(ctx sdk.Context) []abci.ValidatorUpdate { // Spawn-time queue, removal-time queue, equivocation-evidence-min-height, // and per-consumer debt are NOT exported because they are derivable from // the per-consumer fields above and / or other module state at InitGenesis. +// +// The liveness clock is also NOT exported and resets to its fresh state on +// import: the last-ack time defaults to the current block time (so no launched +// consumer is swept before it has had a fresh grace window), and the +// highest-sent / highest-acked VSC ids default to 0 and equal (so no consumer +// is treated as "behind", and the next epoch sends an ordinary diff rather than +// a snapshot). This is a deliberate reset for a state-export restart, not +// preserved audit state. func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { allConsumerIds := k.GetAllConsumerIds(ctx) diff --git a/x/vaas/provider/keeper/liveness_test.go b/x/vaas/provider/keeper/liveness_test.go index 6fc0453..316461b 100644 --- a/x/vaas/provider/keeper/liveness_test.go +++ b/x/vaas/provider/keeper/liveness_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "errors" "testing" "time" @@ -281,15 +282,37 @@ func TestSweepMultipleConsumers_PartialRemoval(t *testing.T) { require.Error(t, err) } -// TestSweepContinuesAfterStopError is SKIPPED. -// -// StopAndPrepareForConsumerRemoval sets the consumer phase to STOPPED unconditionally -// before it calls stakingKeeper.UnbondingTime. There is no mock lever that causes -// the stop to fail while leaving the consumer in LAUNCHED: by the time UnbondingTime -// is called, the phase change has already been committed to the store. Injecting an -// error via UnbondingTime only prevents the removal-time entry from being written, but -// the consumer phase is already STOPPED. A faithful per-consumer stop failure test -// would require refactoring production code, which is out of scope here. +// TestSweepStrandOnStopErrorPrevented verifies that a per-consumer stop failure +// during the sweep does not strand the consumer STOPPED-with-no-removal. The +// sweep runs StopAndPrepareForConsumerRemoval in a cached context and only +// commits on success, so an error (here: UnbondingTime failing when the stop +// reads it, after the phase write in the cache) discards everything and leaves +// the consumer LAUNCHED for the next sweep to retry. +func TestSweepStrandOnStopErrorPrevented(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + // LivenessGracePeriod (top of the sweep) reads UnbondingTime once and must + // succeed; StopAndPrepareForConsumerRemoval reads it again and here fails. + gomock.InOrder( + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil), + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(time.Duration(0), errors.New("boom")), + ) + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + k.SetConsumerChainId(ctx, cid, "consumer-x") + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime().Add(-30*24*time.Hour))) + + // The sweep logs the per-consumer error and returns nil (it does not abort). + require.NoError(t, k.SweepUnresponsiveConsumers(ctx)) + + // Nothing was committed: the consumer is still LAUNCHED (not stranded) and + // has no scheduled removal. + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cid)) + _, err := k.GetConsumerRemovalTime(ctx, cid) + require.Error(t, err) +} // TestSweepRecoversAfterAck confirms a consumer that receives a fresh ack just // inside the grace window survives a subsequent sweep once more time elapses. diff --git a/x/vaas/types/shared_params.go b/x/vaas/types/shared_params.go index c54e904..b3383ec 100644 --- a/x/vaas/types/shared_params.go +++ b/x/vaas/types/shared_params.go @@ -56,6 +56,12 @@ func ValidatePositiveInt64(n int64) error { return nil } +// ValidateStringFractionNonZero checks that str parses to a decimal strictly +// inside the open interval (0, 1). Its two users -- the IBC trusting-period +// fraction and the liveness grace fraction -- each scale the unbonding period +// into a duration that must stay strictly below it (the trusting period must be +// < unbonding for the light client, and the liveness grace must end inside the +// slashable window), so a fraction of exactly 1 is invalid, not just > 1. func ValidateStringFractionNonZero(str string) error { dec, err := math.LegacyNewDecFromStr(str) if err != nil { @@ -64,8 +70,8 @@ func ValidateStringFractionNonZero(str string) error { if dec.IsNegative() { return fmt.Errorf("param cannot be negative, got %s", str) } - if dec.Sub(math.LegacyNewDec(1)).IsPositive() { - return fmt.Errorf("param cannot be greater than 1, got %s", str) + if dec.GTE(math.LegacyOneDec()) { + return fmt.Errorf("param must be less than 1, got %s", str) } if dec.IsZero() { return fmt.Errorf("param cannot be zero, got %s", str) From f437fea681d85aca2b08834e45effbcf473ea16b Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Thu, 2 Jul 2026 11:25:45 +0200 Subject: [PATCH 11/20] fix the main e2e suite for the liveness changes The long-unbonding IntegrationTestSuite was never green on this branch because create-consumer failed the new init-parameter validation, and once that was fixed two of its never-run liveness tests needed adjustment. - testdata/create_consumer.json: add a safe_mode_threshold (now a required init param, must be > 0) and bring vaas_timeout_period within [10m, 24h] (it was four weeks). Either omission made create-consumer fail at CheckTx, leaving the consumer uncreated and its genesis empty. - consumer registration now broadcasts in sync mode and asserts the tx code, so an init-param rejection fails immediately instead of surfacing ~90s later as an empty consumer genesis. - testLivenessTransientOutage: delegate at least one unit of voting power (tokens / powerReduction) so the validator's integer power actually changes and the snapshot resync is exercised; a smaller delegation truncated to +0 power. Also unpause the consumer via defer so a failure cannot leave the container paused and cascade into later tests. - remove testLivenessSafeMode: its debt-based bank-send restriction duplicates testConsumerDebtFlow, its gov-allowlist probe duplicates the ante unit tests, and real VSC-staleness safe mode is covered end-to-end by the short-unbonding LivenessIntegrationTestSuite. It also could not trigger debt because the preceding debt test leaves the fee pool funded. --- tests/e2e/e2e_consumer_liveness_test.go | 115 ++++++------------------ tests/e2e/e2e_setup_test.go | 19 +++- tests/e2e/e2e_test.go | 4 - tests/e2e/testdata/create_consumer.json | 3 +- 4 files changed, 44 insertions(+), 97 deletions(-) diff --git a/tests/e2e/e2e_consumer_liveness_test.go b/tests/e2e/e2e_consumer_liveness_test.go index 9a4839e..b3638c7 100644 --- a/tests/e2e/e2e_consumer_liveness_test.go +++ b/tests/e2e/e2e_consumer_liveness_test.go @@ -1,13 +1,19 @@ package e2e -// e2e_consumer_liveness_test.go exercises the three liveness-removal scenarios -// introduced by issue #36: +// e2e_consumer_liveness_test.go exercises the liveness scenarios introduced by +// issue #36 that require a full provider+consumer network: // -// 1. testLivenessRemoval - sustained outage causes LAUNCHED -> STOPPED -> DELETED. -// 2. testLivenessTransientOutage - brief outage + valset change; consumer -// stays LAUNCHED and re-syncs after grace. -// 3. testLivenessSafeMode - while VSC is stale, consumer rejects bank -// sends; after a fresh VSC it accepts them. +// 1. testLivenessTransientOutage - brief outage + valset change; the consumer +// stays LAUNCHED and re-syncs via snapshot. +// 2. testLivenessRemoval - explicit governance removal transitions the +// consumer to STOPPED. +// +// Consumer-side safe mode (the MsgFilterDecorator restricting txs while the +// consumer is in debt or its VSC is stale) is not duplicated here: the ante +// gate's allow/block logic is covered deterministically by the ante unit tests +// (x/vaas/consumer/ante), the debt path end-to-end by testConsumerDebtFlow, and +// the real VSC-staleness safe mode end-to-end by the short-unbonding +// LivenessIntegrationTestSuite. // // HARNESS CONSTRAINTS // @@ -17,29 +23,18 @@ package e2e // (unbonding * 0.66) is ~13 days. Waiting for an automatic STOPPED transition // in real-time is therefore impractical in CI. // -// For testLivenessRemoval we approximate by issuing an explicit -// remove-consumer tx (which immediately stops the consumer), then asserting the -// provider transitions through STOPPED and eventually to DELETED. The STOPPED -// -> DELETED leg also relies on time elapsing (the removal_time set at stop), -// so in a normal CI run the consumer will stay STOPPED until a follow-up epoch -// advances the chain past removal_time. We therefore assert STOPPED as the -// terminal state within the test window, and note that a proper timing-faithful -// run would need a short unbonding_period consumer (e.g. 60s -> grace ~40s). +// For testLivenessRemoval we issue an explicit remove-consumer via governance +// (gov authority is required since PR #53), then assert the provider transitions +// to STOPPED. The STOPPED -> DELETED leg relies on removal_time elapsing, which +// needs a short-unbonding consumer -- exercised by LivenessIntegrationTestSuite. // // For testLivenessTransientOutage we pause the consumer container (same // mechanism as testDowntimeSlash) for a brief window and verify the consumer // stays LAUNCHED and its VP re-converges after recovery. This exercises the // snapshot-resync path introduced in issue #36. // -// For testLivenessSafeMode we verify that the consumer's MsgFilterDecorator -// rejects bank sends while in restricted mode (debt or stale VSC), and accepts -// them once normal conditions are restored. The DefaultSafeModeThreshold for -// VSC staleness is 3 hours (hard-coded), so we trigger restricted mode via the -// fee-debt path instead, which exercises the same ante-handler code branch. -// // Sequencing in TestVAAS: // testLivenessTransientOutage runs after testValidatorSetSync (non-destructive VP change). -// testLivenessSafeMode runs after testConsumerDebtFlow (reuses the debt cycle). // testLivenessRemoval runs second-to-last, before testGenesisRoundTrip. import ( @@ -94,6 +89,11 @@ func (s *IntegrationTestSuite) testLivenessTransientOutage() { s.T().Log("pausing consumer container (transient outage starts)...") err = s.dkrPool.Client.PauseContainer(s.consumerValRes[0].Container.ID) s.Require().NoError(err, "failed to pause consumer container") + // Safety net: always unpause on exit so a failure before the explicit + // unpause below cannot leave the container paused and cascade into the + // next test (a paused container rejects docker exec). Errors ignored: + // the happy path already unpaused it, so this is a no-op then. + defer func() { _ = s.dkrPool.Client.UnpauseContainer(s.consumerValRes[0].Container.ID) }() // While the consumer is paused, delegate extra stake on the provider so // it emits a VSC packet for the VP change. @@ -107,7 +107,10 @@ func (s *IntegrationTestSuite) testLivenessTransientOutage() { valoperAddr := strings.TrimSpace(stdout.String()) _, _, err = s.dockerExec(s.providerValRes[0].Container.ID, []string{ - providerBinary, "tx", "staking", "delegate", valoperAddr, "500000" + bondDenom, + // Delegate at least one unit of voting power (tokens / powerReduction, + // default 1e6) so the validator's integer power actually increases; a + // smaller delegation truncates to +0 power and the VSC never changes. + providerBinary, "tx", "staking", "delegate", valoperAddr, "100000000" + bondDenom, "--from", "user", "--home", providerHomePath, "--keyring-backend", "test", @@ -162,72 +165,6 @@ func (s *IntegrationTestSuite) testLivenessTransientOutage() { }) } -// testLivenessSafeMode verifies that the consumer's MsgFilterDecorator enters -// restricted mode when the consumer is in debt or has a stale validator set -// (the two conditions share the same code path in the ante handler), and exits -// restricted mode after recovery. -// -// Issue #36 context: the safe-mode gate allows only /ibc.core.* and -// /cosmos.gov.* messages through. A bank send is rejected in restricted mode -// but accepted in normal mode. -// -// Approximation note: VSC staleness (DefaultSafeModeThreshold = 3h) cannot be -// triggered within a short test run. Debt (empty fee pool) uses the same -// restricted-mode branch and can be triggered within epochs. This test -// therefore relies on the debt path to trigger restricted mode. -func (s *IntegrationTestSuite) testLivenessSafeMode() { - s.Run("liveness safe mode: bank send rejected while restricted, accepted after recovery", func() { - // Wait for the consumer to enter restricted mode (debt due to empty fee pool). - s.T().Log("waiting for consumer to enter restricted mode...") - s.Require().Eventuallyf(func() bool { - out, err := s.consumerBankSendDryRun() - return err != nil || strings.Contains(out, "consumer chain is in debt") || - strings.Contains(out, "stale validator set") - }, 3*time.Minute, 5*time.Second, - "consumer did not enter restricted mode; fee pool may not be empty") - s.T().Log("restricted mode confirmed; bank sends are now rejected") - - // A /cosmos.gov.* message must NOT be blocked by the admission gate in - // restricted mode. We probe this with a dry-run gov submit-proposal and - // verify the rejection reason (if any) does not come from the gate. - user := s.consumerUserBech32() - _, govStderr, _ := s.dockerExec(s.consumerValRes[0].Container.ID, []string{ - consumerBinary, "tx", "gov", "submit-proposal", "/dev/null", - "--from", user, - "--home", consumerHomePath, - "--keyring-backend", "test", - "--chain-id", consumerChainID, - "--fees", "1000" + bondDenom, - "--dry-run", - "-y", - }) - govErrStr := govStderr.String() - s.Require().False( - strings.Contains(govErrStr, "consumer chain is in debt") || - strings.Contains(govErrStr, "stale validator set"), - "gov tx must not be rejected by the admission gate in restricted mode; stderr=%q", - govErrStr, - ) - - // Fund the fee pool to restore normal mode. - s.T().Log("re-funding consumer fee pool to restore normal mode...") - s.providerFundConsumerFeePool("0", "20000000"+feeDenom) - - // After funding, bank sends must be accepted again. - s.T().Log("waiting for consumer to exit restricted mode after re-fund...") - s.Require().Eventuallyf(func() bool { - out, err := s.consumerBankSendDryRun() - if err != nil { - return false - } - return !strings.Contains(out, "consumer chain is in debt") && - !strings.Contains(out, "stale validator set") - }, 2*time.Minute, 5*time.Second, - "consumer did not exit restricted mode after fee pool was re-funded") - s.T().Log("consumer back in normal mode; bank sends accepted") - }) -} - // testLivenessRemoval verifies the LAUNCHED -> STOPPED lifecycle when a // consumer is explicitly removed via MsgRemoveConsumer. // diff --git a/tests/e2e/e2e_setup_test.go b/tests/e2e/e2e_setup_test.go index c4c288b..81303dc 100644 --- a/tests/e2e/e2e_setup_test.go +++ b/tests/e2e/e2e_setup_test.go @@ -3,6 +3,7 @@ package e2e import ( "bytes" "context" + "encoding/json" "fmt" "os" "os/exec" @@ -313,7 +314,7 @@ func (s *IntegrationTestSuite) registerConsumerOnProvider() { "sh", "-c", fmt.Sprintf("echo '%s' > /tmp/create_consumer.json", createConsumerJSON), }) - _, _, err = s.dockerExec(s.providerValRes[0].Container.ID, []string{ + stdout, stderr, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ providerBinary, "tx", "provider", "create-consumer", "/tmp/create_consumer.json", "--from", "val", "--home", providerHomePath, @@ -322,10 +323,22 @@ func (s *IntegrationTestSuite) registerConsumerOnProvider() { "--gas", "auto", "--gas-adjustment", "1.5", "--fees", "10000" + bondDenom, + "--broadcast-mode", "sync", "-y", + "-o", "json", }) - - s.Require().NoError(err, "failed to create consumer on provider") + s.Require().NoErrorf(err, "failed to run create-consumer: stderr=%s", stderr.String()) + + // Assert the tx was accepted. Without this, a tx rejected at CheckTx (e.g. + // an init-parameter validation failure) passes silently here and only + // surfaces later as an empty consumer genesis. + var res struct { + Code int `json:"code"` + RawLog string `json:"raw_log"` + } + s.Require().NoErrorf(json.Unmarshal(stdout.Bytes(), &res), + "failed to decode create-consumer response:\nstdout=%q\nstderr=%q", stdout.String(), stderr.String()) + s.Require().Equalf(0, res.Code, "create-consumer tx failed: %s", res.RawLog) // Wait for the tx to be included time.Sleep(10 * time.Second) diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index da6fe1d..7d7ee29 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -10,10 +10,6 @@ func (s *IntegrationTestSuite) TestVAAS() { // LAUNCHED and re-converges via snapshot resync after recovery. s.testLivenessTransientOutage() s.testConsumerDebtFlow() - // Verify MsgFilterDecorator restricted mode (debt gate) rejects bank sends - // and allows gov/ibc.core messages; exercises the same code path as VSC - // staleness (safe mode). - s.testLivenessSafeMode() s.testDowntimeSlash() s.testFeePoolSendRestriction() s.testFeePoolFundAndLockEnforcement() diff --git a/tests/e2e/testdata/create_consumer.json b/tests/e2e/testdata/create_consumer.json index 48aeb1c..0b7c2d6 100644 --- a/tests/e2e/testdata/create_consumer.json +++ b/tests/e2e/testdata/create_consumer.json @@ -14,7 +14,8 @@ "binary_hash": "", "spawn_time": "2024-01-01T00:00:00Z", "unbonding_period": 1728000000000000, - "vaas_timeout_period": 2419200000000000, + "vaas_timeout_period": 3600000000000, + "safe_mode_threshold": 10800000000000, "historical_entries": 10000 }, "infraction_parameters": { From 3d746ce8a21056666affb5bfa1eb56aa99d8fe25 Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:55:23 +0200 Subject: [PATCH 12/20] enforce the consumer safe-mode threshold below the provider liveness grace A consumer's SafeModeThreshold (a per-consumer init parameter) must be strictly shorter than the provider's liveness grace (unbonding * LivenessGraceFraction), so a lagging consumer enters safe mode -- and stops value-bearing work under a possibly-stale set -- before the provider's sweep removes it. Both values are cross-chain, but the provider holds each at create/update time (the threshold as an init param it ships to the consumer, the grace derived from its own unbonding and fraction), so the relationship is enforced provider-side rather than left to operator discipline, exactly as the consumer-unbonding bound already is. The unbonding and safe-mode checks are combined into a single validateConsumerInitParams that reads the provider unbonding once. --- docs/consumer-liveness.md | 16 +++++-- x/vaas/provider/keeper/msg_server.go | 41 ++++++++++++----- x/vaas/provider/keeper/msg_server_test.go | 54 +++++++++++++++++++++++ 3 files changed, 96 insertions(+), 15 deletions(-) diff --git a/docs/consumer-liveness.md b/docs/consumer-liveness.md index a538a32..1a9e4dd 100644 --- a/docs/consumer-liveness.md +++ b/docs/consumer-liveness.md @@ -109,8 +109,10 @@ expired client produces no acks. While a consumer is behind, its local validator set may not match the provider's. The consumer protects itself during that window. It records the block time of the last VSC packet it received, and considers itself **stale** when it has not received one for longer than a -consumer-local threshold (`DefaultSafeModeThreshold`, 3 hours -- set well below the provider's -grace period). +consumer-local threshold (`SafeModeThreshold`, default 3 hours). The threshold is required to +be strictly below the provider's liveness grace, enforced at `MsgCreateConsumer` / +`MsgUpdateConsumer` time (see section 5), so a lagging consumer always enters safe mode before +the provider's sweep would remove it. While stale (or while flagged in debt for unpaid fees), the consumer's transaction admission gate restricts incoming transactions to `/ibc.core.*` and `/cosmos.gov.*` messages only, @@ -127,7 +129,7 @@ launched consumer is never treated as stale before its first VSC. | `VaasTimeoutPeriod` | provider module param (VSC packet timeout) and per-consumer init param (consumer evidence packet timeout) | `[10m, MaxTimeoutDelta (24h)]` | 1h | | consumer `UnbondingPeriod` | per-consumer init param | `(0, providerUnbondingPeriod]` | provider default minus 1 day | | `LivenessGraceFraction` | provider module param | `(0, 1)` | `0.66` | -| consumer `SafeModeThreshold` | per-consumer init param | `> 0` | 3h | +| consumer `SafeModeThreshold` | per-consumer init param | `(0, provider liveness grace)` | 3h | `VaasTimeoutPeriod` is validated to `[10m, 24h]` at both the provider module param boundary and the per-consumer initialization-parameter boundary, so the configured value is honest @@ -142,6 +144,14 @@ configured to outlive the provider's slashable window. No lower floor is enforce unbonding short enough to make the relayer-derived trusting period impractical is an operator concern (and is useful for short-lived test or dev chains), not a protocol minimum. +The consumer `SafeModeThreshold` is bounded at the same `MsgCreateConsumer` / +`MsgUpdateConsumer` boundary: it must be strictly less than the provider's liveness grace +(`unbonding * LivenessGraceFraction`). Both values are cross-chain, but the provider holds +each at configuration time -- the threshold is an init parameter it ships to the consumer, +and the grace derives from its own unbonding and fraction -- so the relationship is enforced +provider-side rather than left to operator discipline. This guarantees the consumer's own +safe mode engages before the provider's sweep removes it. + ## 6. Operator guidance: the liveness query Operators can observe a consumer's liveness without waiting for removal: diff --git a/x/vaas/provider/keeper/msg_server.go b/x/vaas/provider/keeper/msg_server.go index 52f4399..38c308b 100644 --- a/x/vaas/provider/keeper/msg_server.go +++ b/x/vaas/provider/keeper/msg_server.go @@ -218,21 +218,38 @@ func (k msgServer) SubmitConsumerDoubleVoting(goCtx context.Context, msg *types. return &types.MsgSubmitConsumerDoubleVotingResponse{}, nil } -// validateConsumerUnbonding enforces the one safety-relevant bound on a -// consumer's unbonding period: it must not exceed the provider's, so the -// consumer cannot outlive the provider's slashable window. Positivity is -// already checked by ValidateInitializationParameters. No lower floor is -// imposed: choosing an unbonding short enough that the relayer-derived -// trusting period becomes impractical is an operator concern (and is needed -// for short-lived test/dev chains), not a protocol-enforced minimum. -func (k Keeper) validateConsumerUnbonding(ctx sdk.Context, d time.Duration) error { +// validateConsumerInitParams enforces the two bounds on a consumer's +// initialization parameters that can only be checked against provider state +// (positivity and format are already checked by ValidateInitializationParameters). +// Both are cross-chain settings the provider holds at create/update time, so it +// can validate them here against its own state -- it reads its unbonding once +// and derives the grace from it: +// +// - UnbondingPeriod must not exceed the provider's unbonding, so the consumer +// cannot outlive the provider's slashable window. No lower floor is imposed: +// an unbonding short enough to make the relayer-derived trusting period +// impractical is an operator concern (and useful for test/dev chains), not +// a protocol minimum. +// - SafeModeThreshold must be strictly shorter than the provider's liveness +// grace (unbonding * LivenessGraceFraction), so a lagging consumer enters +// safe mode -- and stops value-bearing work under a possibly-stale set -- +// before the provider's sweep removes it. +func (k Keeper) validateConsumerInitParams(ctx sdk.Context, p types.ConsumerInitializationParameters) error { providerUnbonding, err := k.stakingKeeper.UnbondingTime(ctx) if err != nil { return err } - if d > providerUnbonding { + if p.UnbondingPeriod > providerUnbonding { + return errorsmod.Wrapf(types.ErrInvalidConsumerInitializationParameters, + "unbonding period %s must not exceed provider unbonding %s", p.UnbondingPeriod, providerUnbonding) + } + grace, err := vaastypes.CalculateTrustPeriod(providerUnbonding, k.GetParams(ctx).LivenessGraceFraction) + if err != nil { + return err + } + if p.SafeModeThreshold >= grace { return errorsmod.Wrapf(types.ErrInvalidConsumerInitializationParameters, - "unbonding period %s must not exceed provider unbonding %s", d, providerUnbonding) + "safe mode threshold %s must be less than the provider liveness grace period %s", p.SafeModeThreshold, grace) } return nil } @@ -280,7 +297,7 @@ func (k msgServer) CreateConsumer(goCtx context.Context, msg *types.MsgCreateCon if msg.InitializationParameters != nil { initializationParameters = *msg.InitializationParameters } - if err := k.Keeper.validateConsumerUnbonding(ctx, initializationParameters.UnbondingPeriod); err != nil { + if err := k.Keeper.validateConsumerInitParams(ctx, initializationParameters); err != nil { return &resp, err } if err := k.Keeper.SetConsumerInitializationParameters(ctx, consumerId, initializationParameters); err != nil { @@ -479,7 +496,7 @@ func (k msgServer) UpdateConsumer(goCtx context.Context, msg *types.MsgUpdateCon sdk.NewAttribute(types.AttributeConsumerGenesisHash, string(msg.InitializationParameters.GenesisHash))) } - if err = k.Keeper.validateConsumerUnbonding(ctx, msg.InitializationParameters.UnbondingPeriod); err != nil { + if err = k.Keeper.validateConsumerInitParams(ctx, *msg.InitializationParameters); err != nil { return &resp, err } if err = k.Keeper.SetConsumerInitializationParameters(ctx, msg.ConsumerId, *msg.InitializationParameters); err != nil { diff --git a/x/vaas/provider/keeper/msg_server_test.go b/x/vaas/provider/keeper/msg_server_test.go index 3331aa1..d40645c 100644 --- a/x/vaas/provider/keeper/msg_server_test.go +++ b/x/vaas/provider/keeper/msg_server_test.go @@ -29,6 +29,7 @@ import ( testkeeper "github.com/allinbits/vaas/testutil/keeper" providerkeeper "github.com/allinbits/vaas/x/vaas/provider/keeper" providertypes "github.com/allinbits/vaas/x/vaas/provider/types" + vaastypes "github.com/allinbits/vaas/x/vaas/types" ) var keeperBech32CfgOnce sync.Once @@ -1369,6 +1370,59 @@ func TestCreateConsumerUnbondingBounds(t *testing.T) { } } +// TestCreateConsumerSafeModeThresholdBounds checks that CreateConsumer rejects a +// safe-mode threshold that is not strictly below the provider's liveness grace +// (grace = providerUnbonding * LivenessGraceFraction), so a lagging consumer +// enters safe mode before the provider sweep removes it. +func TestCreateConsumerSafeModeThresholdBounds(t *testing.T) { + providerUnbonding := 21 * 24 * time.Hour + // Grace uses the default fraction seeded into params by the test helper. + grace, err := vaastypes.CalculateTrustPeriod(providerUnbonding, providertypes.DefaultLivenessGraceFraction) + require.NoError(t, err) + + tests := []struct { + name string + threshold time.Duration + wantErr bool + }{ + {name: "well below grace", threshold: time.Hour, wantErr: false}, + {name: "just below grace", threshold: grace - time.Second, wantErr: false}, + {name: "equal to grace (rejected)", threshold: grace, wantErr: true}, + {name: "above grace (rejected)", threshold: grace + time.Hour, wantErr: true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) + + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(providerUnbonding, nil).Times(1) + + _, err := msgServer.CreateConsumer(ctx, + &providertypes.MsgCreateConsumer{ + Submitter: "submitter", + ChainId: "chainId", + Metadata: providertypes.ConsumerMetadata{ + Name: "name", + Description: "description", + }, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: providerUnbonding, + SafeModeThreshold: tc.threshold, + }, + }) + if tc.wantErr { + require.Error(t, err) + require.ErrorIs(t, err, providertypes.ErrInvalidConsumerInitializationParameters) + } else { + require.NoError(t, err) + } + }) + } +} + func TestSweepConsumerFeePool(t *testing.T) { owner := sdk.AccAddress([]byte("owner___________")) alice := sdk.AccAddress([]byte("alice___________")) From 02ed6ce6d2d0a9bd30a628bec36d852157e60b2d Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:55:37 +0200 Subject: [PATCH 13/20] round-trip the liveness clock through genesis export/import Export and re-import the per-consumer liveness state so a genesis export/import (a state-export restart) preserves it instead of resetting it: - provider ConsumerState gains last_ack_time and highest_sent / highest_acked vsc ids, exported for launched consumers and restored at InitGenesis. - consumer GenesisState gains last_vsc_recv_time, so the VSC-staleness clock survives a restart rather than falling back to the current block time. Previously these fields reset on import -- safe by fallback (no immediate sweep, no forced snapshot, never-stale until the first VSC) but not faithful. Genesis round-tripping is foundational state handling, not a version migration, so the liveness clock is now preserved end-to-end (covered by the provider and consumer genesis round-trip tests). --- proto/vaas/consumer/v1/genesis.proto | 5 + proto/vaas/provider/v1/genesis.proto | 11 + x/vaas/consumer/keeper/genesis.go | 18 ++ x/vaas/consumer/keeper/genesis_test.go | 38 ++++ x/vaas/consumer/types/genesis.pb.go | 128 +++++++++--- x/vaas/provider/keeper/genesis.go | 43 +++- x/vaas/provider/keeper/genesis_test.go | 16 ++ x/vaas/provider/types/genesis.pb.go | 270 +++++++++++++++++++------ 8 files changed, 426 insertions(+), 103 deletions(-) diff --git a/proto/vaas/consumer/v1/genesis.proto b/proto/vaas/consumer/v1/genesis.proto index 196aeb7..04c25b2 100644 --- a/proto/vaas/consumer/v1/genesis.proto +++ b/proto/vaas/consumer/v1/genesis.proto @@ -29,6 +29,11 @@ message GenesisState { // Flag indicating whether the consumer VAAS module starts in pre-VAAS state bool preVAAS = 5; vaas.v1.ProviderInfo provider = 6 [ (gogoproto.nullable) = false ]; + // LastVSCRecvTime is the block time of the last VSC packet the consumer + // received; it drives IsVSCStale (safe mode). Round-tripped on restart so a + // state-export upgrade preserves the staleness clock. Absent for a new chain + // that has not received a VSC yet. + google.protobuf.Timestamp last_vsc_recv_time = 7 [ (gogoproto.stdtime) = true ]; } // HeightValsetUpdateID represents a mapping internal to the consumer VAAS module diff --git a/proto/vaas/provider/v1/genesis.proto b/proto/vaas/provider/v1/genesis.proto index 0c5f8d6..15c67cf 100644 --- a/proto/vaas/provider/v1/genesis.proto +++ b/proto/vaas/provider/v1/genesis.proto @@ -86,6 +86,17 @@ message ConsumerState { // absent otherwise. Used to re-derive the keeper's removal-time queue. google.protobuf.Timestamp removal_time = 12 [ (gogoproto.stdtime) = true ]; + + // Liveness clock, present once a consumer has launched: LastAckTime is the + // block time of the consumer's most recent successful VSC acknowledgement + // (the provider's per-consumer grace clock), and HighestSentVscId / + // HighestAckedVscId are the resync counters that decide whether the consumer + // is behind. Round-tripped so a state-export restart preserves the grace + // window and resync state instead of resetting them. + google.protobuf.Timestamp last_ack_time = 13 + [ (gogoproto.stdtime) = true ]; + uint64 highest_sent_vsc_id = 14; + uint64 highest_acked_vsc_id = 15; } // ValsetUpdateIdToHeight defines the genesis information for the mapping diff --git a/x/vaas/consumer/keeper/genesis.go b/x/vaas/consumer/keeper/genesis.go index dd4567b..0c0d238 100644 --- a/x/vaas/consumer/keeper/genesis.go +++ b/x/vaas/consumer/keeper/genesis.go @@ -61,6 +61,12 @@ func (k Keeper) InitGenesis(ctx sdk.Context, state *types.GenesisState) []abci.V return []abci.ValidatorUpdate{} } + // Restore the VSC staleness clock on a restart (see ExportGenesis); absent + // (new chain / never received a VSC) leaves the never-stale default. + if state.LastVscRecvTime != nil { + k.SetLastVSCRecvTime(ctx, *state.LastVscRecvTime) + } + // populate cross chain validators states with initial valset k.ApplyCCValidatorChanges(ctx, state.Provider.InitialValSet) return state.Provider.InitialValSet @@ -88,5 +94,17 @@ func (k Keeper) ExportGenesis(ctx sdk.Context) (genesis *types.GenesisState) { params, ) + // Preserve the VSC staleness clock across a restart (see IsVSCStale): export + // the last-VSC-recv time only when actually recorded, so a consumer that has + // not received a VSC keeps the absent-default (never stale) on import. + has, err := k.LastVSCRecvTime.Has(ctx) + if err != nil { + panic(err) + } + if has { + t := k.GetLastVSCRecvTime(ctx) + genesis.LastVscRecvTime = &t + } + return genesis } diff --git a/x/vaas/consumer/keeper/genesis_test.go b/x/vaas/consumer/keeper/genesis_test.go index 66645ad..b1879fd 100644 --- a/x/vaas/consumer/keeper/genesis_test.go +++ b/x/vaas/consumer/keeper/genesis_test.go @@ -193,6 +193,44 @@ func TestExportGenesis(t *testing.T) { } } +// TestGenesisRoundTripLastVSCRecvTime verifies the consumer's VSC-staleness +// clock survives an export/import restart: ExportGenesis carries the recorded +// last-VSC-recv time, and InitGenesis restores it on a fresh keeper (rather than +// falling back to the current block time, which would reset the safe-mode clock). +func TestGenesisRoundTripLastVSCRecvTime(t *testing.T) { + provClientID := "tendermint-07" + params := vaastypes.DefaultConsumerParams() + params.Enabled = true + + pubKey := ed25519.GenPrivKey().PubKey() + tmPK, err := cryptocodec.ToCmtPubKeyInterface(pubKey) + require.NoError(t, err) + validator := tmtypes.NewValidator(tmPK, 1) + + lastRecv := time.Unix(1_850_000_000, 0).UTC() + + // Export half: a keeper with a recorded last-VSC-recv time exports it. + ck, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + ck.SetParams(ctx, params) + ck.SetProviderClientID(ctx, provClientID) + cVal, err := consumertypes.NewCCValidator(validator.Address.Bytes(), 1, pubKey) + require.NoError(t, err) + ck.SetCCValidator(ctx, cVal) + ck.SetHeightValsetUpdateID(ctx, 0, 0) + ck.SetLastVSCRecvTime(ctx, lastRecv) + + exported := ck.ExportGenesis(ctx) + require.NotNil(t, exported.LastVscRecvTime, "export must carry last_vsc_recv_time") + require.Equal(t, lastRecv, *exported.LastVscRecvTime) + + // Import half: a fresh keeper restores the exact time, not the block-time fallback. + ck2, ctx2, ctrl2, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl2.Finish() + ck2.InitGenesis(ctx2, exported) + require.Equal(t, lastRecv, ck2.GetLastVSCRecvTime(ctx2)) +} + func assertProviderClientID(t *testing.T, ctx sdk.Context, ck *consumerkeeper.Keeper, clientID string) { t.Helper() cid, ok := ck.GetProviderClientID(ctx) diff --git a/x/vaas/consumer/types/genesis.pb.go b/x/vaas/consumer/types/genesis.pb.go index 07a5546..1144204 100644 --- a/x/vaas/consumer/types/genesis.pb.go +++ b/x/vaas/consumer/types/genesis.pb.go @@ -9,17 +9,20 @@ import ( _ "github.com/cometbft/cometbft/abci/types" _ "github.com/cosmos/gogoproto/gogoproto" proto "github.com/cosmos/gogoproto/proto" + github_com_cosmos_gogoproto_types "github.com/cosmos/gogoproto/types" _ "github.com/cosmos/ibc-go/v10/modules/light-clients/07-tendermint" _ "google.golang.org/protobuf/types/known/timestamppb" io "io" math "math" math_bits "math/bits" + time "time" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf +var _ = time.Kitchen // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. @@ -43,6 +46,11 @@ type GenesisState struct { // Flag indicating whether the consumer VAAS module starts in pre-VAAS state PreVAAS bool `protobuf:"varint,5,opt,name=preVAAS,proto3" json:"preVAAS,omitempty"` Provider types.ProviderInfo `protobuf:"bytes,6,opt,name=provider,proto3" json:"provider"` + // LastVSCRecvTime is the block time of the last VSC packet the consumer + // received; it drives IsVSCStale (safe mode). Round-tripped on restart so a + // state-export upgrade preserves the staleness clock. Absent for a new chain + // that has not received a VSC yet. + LastVscRecvTime *time.Time `protobuf:"bytes,7,opt,name=last_vsc_recv_time,json=lastVscRecvTime,proto3,stdtime" json:"last_vsc_recv_time,omitempty"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -120,6 +128,13 @@ func (m *GenesisState) GetProvider() types.ProviderInfo { return types.ProviderInfo{} } +func (m *GenesisState) GetLastVscRecvTime() *time.Time { + if m != nil { + return m.LastVscRecvTime + } + return nil +} + // HeightValsetUpdateID represents a mapping internal to the consumer VAAS module // which links a block height to each recv valset update id. type HeightToValsetUpdateID struct { @@ -182,36 +197,39 @@ func init() { func init() { proto.RegisterFile("vaas/consumer/v1/genesis.proto", fileDescriptor_cc650057d919c311) } var fileDescriptor_cc650057d919c311 = []byte{ - // 456 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0x41, 0x6f, 0xd3, 0x30, - 0x14, 0x6e, 0xba, 0x52, 0x3a, 0x0f, 0xa1, 0xca, 0x82, 0x12, 0x75, 0x22, 0xab, 0x76, 0xca, 0x01, - 0xc5, 0xea, 0x10, 0xe2, 0xbc, 0x15, 0x09, 0x7a, 0x9b, 0x3a, 0xd8, 0x61, 0x97, 0xc8, 0x49, 0xde, - 0x12, 0xa3, 0xc4, 0x8e, 0x62, 0x37, 0x85, 0x7f, 0xc1, 0xcf, 0xda, 0x8d, 0x1d, 0x39, 0x21, 0xd4, - 0xfe, 0x11, 0x94, 0x57, 0xa7, 0x30, 0xe8, 0xcd, 0x9f, 0xbf, 0xf7, 0xf9, 0x7d, 0xdf, 0x7b, 0x26, - 0x5e, 0xcd, 0xb9, 0x66, 0xb1, 0x92, 0x7a, 0x59, 0x40, 0xc5, 0xea, 0x29, 0x4b, 0x41, 0x82, 0x16, - 0x3a, 0x28, 0x2b, 0x65, 0x14, 0x1d, 0x36, 0x7c, 0xd0, 0xf2, 0x41, 0x3d, 0x1d, 0xbf, 0x44, 0x45, - 0x3d, 0x65, 0x3a, 0xe3, 0x15, 0x24, 0xe1, 0x8e, 0x43, 0xc1, 0x98, 0x89, 0x28, 0x66, 0xb9, 0x48, - 0x33, 0x13, 0xe7, 0x02, 0xa4, 0xd1, 0xcc, 0x80, 0x4c, 0xa0, 0x2a, 0x84, 0x34, 0x8d, 0xea, 0x0f, - 0xb2, 0x82, 0x67, 0xa9, 0x4a, 0x15, 0x1e, 0x59, 0x73, 0xb2, 0xb7, 0xb4, 0xed, 0xb2, 0x12, 0x15, - 0xd8, 0xbb, 0x93, 0x54, 0xa9, 0x34, 0x07, 0x86, 0x28, 0x5a, 0xde, 0x32, 0x23, 0x0a, 0xd0, 0x86, - 0x17, 0xa5, 0x2d, 0x38, 0xfe, 0xab, 0x15, 0x8f, 0x62, 0xc1, 0xcc, 0xd7, 0x12, 0x6c, 0x92, 0xd3, - 0xef, 0x5d, 0xf2, 0xe4, 0xfd, 0x36, 0xdb, 0x95, 0xe1, 0x06, 0xe8, 0x1b, 0xd2, 0x2f, 0x79, 0xc5, - 0x0b, 0xed, 0x3a, 0x13, 0xc7, 0x3f, 0x3a, 0x7b, 0x11, 0x60, 0xd6, 0x7a, 0x1a, 0xcc, 0x6c, 0xa4, - 0x4b, 0xa4, 0x2f, 0x7a, 0x77, 0x3f, 0x4f, 0x3a, 0x0b, 0x5b, 0x4c, 0x5f, 0x11, 0x5a, 0x56, 0xaa, - 0x16, 0x09, 0x54, 0xe1, 0x36, 0x62, 0x28, 0x12, 0xb7, 0x3b, 0x71, 0xfc, 0xc3, 0xc5, 0xb0, 0x65, - 0x66, 0x48, 0xcc, 0x13, 0x7a, 0x4c, 0x0e, 0x25, 0xac, 0xc2, 0x38, 0xe3, 0x42, 0xba, 0x07, 0x13, - 0xc7, 0x1f, 0x2c, 0x06, 0x12, 0x56, 0xb3, 0x06, 0xd3, 0xcf, 0x64, 0x9c, 0x41, 0x33, 0xaa, 0xd0, - 0xa8, 0xb0, 0xe6, 0xb9, 0x06, 0x13, 0x2e, 0xcb, 0x84, 0x1b, 0x68, 0x9e, 0xec, 0x4d, 0x0e, 0xfc, - 0xa3, 0x33, 0x3f, 0xf8, 0x77, 0x03, 0xc1, 0x07, 0xd4, 0x7c, 0x54, 0xd7, 0xa8, 0xf8, 0x84, 0x82, - 0xf9, 0x3b, 0x6b, 0x73, 0x94, 0xed, 0x63, 0x13, 0xea, 0x92, 0xc7, 0x65, 0x05, 0xd7, 0xe7, 0xe7, - 0x57, 0xee, 0x23, 0xb4, 0xd1, 0x42, 0xfa, 0x96, 0x0c, 0x5a, 0xdb, 0x6e, 0x1f, 0x27, 0xf1, 0x7c, - 0x37, 0x89, 0x4b, 0x4b, 0xcc, 0xe5, 0xad, 0xb2, 0x0d, 0x76, 0xc5, 0xa7, 0x37, 0x64, 0xb4, 0xdf, - 0x0a, 0x1d, 0x91, 0xfe, 0xd6, 0x06, 0x8e, 0xb6, 0xb7, 0xb0, 0x88, 0xfa, 0x64, 0xf8, 0x5f, 0xcc, - 0x2e, 0x56, 0x3c, 0xad, 0x1f, 0xd8, 0xbd, 0x98, 0xdf, 0xad, 0x3d, 0xe7, 0x7e, 0xed, 0x39, 0xbf, - 0xd6, 0x9e, 0xf3, 0x6d, 0xe3, 0x75, 0xee, 0x37, 0x5e, 0xe7, 0xc7, 0xc6, 0xeb, 0xdc, 0xb0, 0x54, - 0x98, 0x6c, 0x19, 0x05, 0xb1, 0x2a, 0x18, 0xcf, 0x73, 0x21, 0x23, 0x61, 0x34, 0xc3, 0xef, 0xf2, - 0x85, 0x3d, 0xfc, 0xcd, 0xb8, 0xfe, 0xa8, 0x8f, 0xfb, 0x7f, 0xfd, 0x3b, 0x00, 0x00, 0xff, 0xff, - 0x93, 0x27, 0x57, 0x0f, 0xeb, 0x02, 0x00, 0x00, + // 505 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x53, 0x41, 0x6f, 0xd3, 0x30, + 0x14, 0x6e, 0xb6, 0xd2, 0x6d, 0x1e, 0x82, 0xc9, 0x82, 0x12, 0x75, 0x22, 0xad, 0x76, 0xca, 0x01, + 0xd9, 0xea, 0x10, 0xe2, 0xbc, 0x16, 0x09, 0x7a, 0x40, 0x9a, 0xb2, 0xd1, 0xc3, 0x2e, 0x91, 0x93, + 0xbc, 0x25, 0x46, 0x49, 0x1c, 0xc5, 0xae, 0x0b, 0xff, 0x62, 0xbf, 0x80, 0xdf, 0xb3, 0xe3, 0x8e, + 0x9c, 0x00, 0xb5, 0x7f, 0x04, 0xc5, 0x75, 0x0a, 0x83, 0xdd, 0xf2, 0xf9, 0x7b, 0x9f, 0xdf, 0xfb, + 0xde, 0xe7, 0x20, 0x4f, 0x33, 0x26, 0x69, 0x2c, 0x4a, 0xb9, 0x28, 0xa0, 0xa6, 0x7a, 0x4c, 0x53, + 0x28, 0x41, 0x72, 0x49, 0xaa, 0x5a, 0x28, 0x81, 0x8f, 0x1a, 0x9e, 0xb4, 0x3c, 0xd1, 0xe3, 0xc1, + 0x4b, 0xa3, 0xd0, 0x63, 0x2a, 0x33, 0x56, 0x43, 0x12, 0x6e, 0x39, 0x23, 0x18, 0x50, 0x1e, 0xc5, + 0x34, 0xe7, 0x69, 0xa6, 0xe2, 0x9c, 0x43, 0xa9, 0x24, 0x55, 0x50, 0x26, 0x50, 0x17, 0xbc, 0x54, + 0x8d, 0xea, 0x0f, 0xb2, 0x82, 0x67, 0xa9, 0x48, 0x85, 0xf9, 0xa4, 0xcd, 0x97, 0x3d, 0xc5, 0x6d, + 0x97, 0x25, 0xaf, 0xc1, 0x9e, 0x0d, 0x53, 0x21, 0xd2, 0x1c, 0xa8, 0x41, 0xd1, 0xe2, 0x9a, 0x2a, + 0x5e, 0x80, 0x54, 0xac, 0xa8, 0x6c, 0xc1, 0xf1, 0x5f, 0xad, 0x58, 0x14, 0x73, 0xaa, 0xbe, 0x56, + 0x60, 0x9d, 0x9c, 0x7c, 0xdb, 0x45, 0x8f, 0xdf, 0x6f, 0xbc, 0x5d, 0x28, 0xa6, 0x00, 0xbf, 0x41, + 0xbd, 0x8a, 0xd5, 0xac, 0x90, 0xae, 0x33, 0x72, 0xfc, 0xc3, 0xd3, 0x17, 0xc4, 0x78, 0xd5, 0x63, + 0x32, 0xb5, 0x96, 0xce, 0x0d, 0x3d, 0xe9, 0xde, 0xfe, 0x18, 0x76, 0x02, 0x5b, 0x8c, 0x5f, 0x21, + 0x5c, 0xd5, 0x42, 0xf3, 0x04, 0xea, 0x70, 0x63, 0x31, 0xe4, 0x89, 0xbb, 0x33, 0x72, 0xfc, 0x83, + 0xe0, 0xa8, 0x65, 0xa6, 0x86, 0x98, 0x25, 0xf8, 0x18, 0x1d, 0x94, 0xb0, 0x0c, 0xe3, 0x8c, 0xf1, + 0xd2, 0xdd, 0x1d, 0x39, 0xfe, 0x7e, 0xb0, 0x5f, 0xc2, 0x72, 0xda, 0x60, 0xfc, 0x19, 0x0d, 0x32, + 0x68, 0x56, 0x15, 0x2a, 0x11, 0x6a, 0x96, 0x4b, 0x50, 0xe1, 0xa2, 0x4a, 0x98, 0x82, 0xe6, 0xca, + 0xee, 0x68, 0xd7, 0x3f, 0x3c, 0xf5, 0xc9, 0xbf, 0x09, 0x90, 0x0f, 0x46, 0x73, 0x29, 0xe6, 0x46, + 0xf1, 0xc9, 0x08, 0x66, 0xef, 0xec, 0x98, 0xfd, 0xec, 0x21, 0x36, 0xc1, 0x2e, 0xda, 0xab, 0x6a, + 0x98, 0x9f, 0x9d, 0x5d, 0xb8, 0x8f, 0xcc, 0x18, 0x2d, 0xc4, 0x6f, 0xd1, 0x7e, 0x3b, 0xb6, 0xdb, + 0x33, 0x9b, 0x78, 0xbe, 0xdd, 0xc4, 0xb9, 0x25, 0x66, 0xe5, 0xb5, 0xb0, 0x0d, 0xb6, 0xc5, 0xf8, + 0x23, 0xc2, 0x39, 0x93, 0x2a, 0xd4, 0x32, 0x0e, 0x6b, 0x88, 0x75, 0xd8, 0xe4, 0xe1, 0xee, 0x99, + 0x2b, 0x06, 0x64, 0x13, 0x16, 0x69, 0xc3, 0x22, 0x97, 0x6d, 0x58, 0x93, 0xee, 0xcd, 0xcf, 0xa1, + 0x13, 0x3c, 0x6d, 0xb4, 0x73, 0x19, 0x07, 0x10, 0xeb, 0x86, 0x3b, 0xb9, 0x42, 0xfd, 0x87, 0x9d, + 0xe1, 0x3e, 0xea, 0x6d, 0x5c, 0x99, 0xa4, 0xba, 0x81, 0x45, 0xd8, 0x47, 0x47, 0xff, 0x6d, 0x6d, + 0xc7, 0x54, 0x3c, 0xd1, 0xf7, 0xdc, 0x4f, 0x66, 0xb7, 0x2b, 0xcf, 0xb9, 0x5b, 0x79, 0xce, 0xaf, + 0x95, 0xe7, 0xdc, 0xac, 0xbd, 0xce, 0xdd, 0xda, 0xeb, 0x7c, 0x5f, 0x7b, 0x9d, 0x2b, 0x9a, 0x72, + 0x95, 0x2d, 0x22, 0x12, 0x8b, 0x82, 0xb2, 0x3c, 0xe7, 0x65, 0xc4, 0x95, 0xa4, 0xe6, 0xf5, 0x7d, + 0xa1, 0xf7, 0x7f, 0x0e, 0xf3, 0x9a, 0xa2, 0x9e, 0x71, 0xf4, 0xfa, 0x77, 0x00, 0x00, 0x00, 0xff, + 0xff, 0x76, 0x02, 0x7a, 0x56, 0x3a, 0x03, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -234,6 +252,16 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.LastVscRecvTime != nil { + n1, err1 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(*m.LastVscRecvTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.LastVscRecvTime):]) + if err1 != nil { + return 0, err1 + } + i -= n1 + i = encodeVarintGenesis(dAtA, i, uint64(n1)) + i-- + dAtA[i] = 0x3a + } { size, err := m.Provider.MarshalToSizedBuffer(dAtA[:i]) if err != nil { @@ -368,6 +396,10 @@ func (m *GenesisState) Size() (n int) { } l = m.Provider.Size() n += 1 + l + sovGenesis(uint64(l)) + if m.LastVscRecvTime != nil { + l = github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.LastVscRecvTime) + n += 1 + l + sovGenesis(uint64(l)) + } return n } @@ -593,6 +625,42 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LastVscRecvTime", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.LastVscRecvTime == nil { + m.LastVscRecvTime = new(time.Time) + } + if err := github_com_cosmos_gogoproto_types.StdTimeUnmarshal(m.LastVscRecvTime, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/vaas/provider/keeper/genesis.go b/x/vaas/provider/keeper/genesis.go index 3eecf8e..a75a7b1 100644 --- a/x/vaas/provider/keeper/genesis.go +++ b/x/vaas/provider/keeper/genesis.go @@ -81,6 +81,19 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState) []abc panic(fmt.Errorf("init: set removal time for %d: %w", consumerId, err)) } } + // Restore the liveness clock (see ExportGenesis): only when present, so + // a consumer that never launched keeps the absent-defaults. + if cs.LastAckTime != nil { + if err := k.SetConsumerLastAckTime(ctx, consumerId, *cs.LastAckTime); err != nil { + panic(fmt.Errorf("init: set last ack time for %d: %w", consumerId, err)) + } + } + if cs.HighestSentVscId != 0 { + k.SetConsumerHighestSentVscId(ctx, consumerId, cs.HighestSentVscId) + } + if cs.HighestAckedVscId != 0 { + k.SetConsumerHighestAckedVscId(ctx, consumerId, cs.HighestAckedVscId) + } if len(cs.PendingValsetChanges) > 0 { k.AppendPendingVSCPackets(ctx, consumerId, cs.PendingValsetChanges...) } @@ -281,13 +294,10 @@ func (k Keeper) InitGenesisValUpdates(ctx sdk.Context) []abci.ValidatorUpdate { // and per-consumer debt are NOT exported because they are derivable from // the per-consumer fields above and / or other module state at InitGenesis. // -// The liveness clock is also NOT exported and resets to its fresh state on -// import: the last-ack time defaults to the current block time (so no launched -// consumer is swept before it has had a fresh grace window), and the -// highest-sent / highest-acked VSC ids default to 0 and equal (so no consumer -// is treated as "behind", and the next epoch sends an ordinary diff rather than -// a snapshot). This is a deliberate reset for a state-export restart, not -// preserved audit state. +// The liveness clock (last-ack time and the highest-sent / highest-acked VSC +// ids) IS exported per consumer and restored at InitGenesis, so a state-export +// restart preserves each consumer's grace window and resync counters rather +// than resetting them. func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { allConsumerIds := k.GetAllConsumerIds(ctx) @@ -339,6 +349,25 @@ func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { panic(fmt.Errorf("export: failed to read removal time for consumer %d: %w", consumerId, err)) } + // Liveness clock: export the last-ack time only when actually recorded + // (GetConsumerLastAckTime falls back to block time when absent, which we + // must not persist), and the resync counters only when non-zero (zero is + // their absent default). + hasAck, err := k.ConsumerLastAckTime.Has(ctx, consumerId) + if err != nil { + panic(fmt.Errorf("export: failed to check last ack time for consumer %d: %w", consumerId, err)) + } + if hasAck { + ackCopy := k.GetConsumerLastAckTime(ctx, consumerId) + cs.LastAckTime = &ackCopy + } + if v := k.GetConsumerHighestSentVscId(ctx, consumerId); v != 0 { + cs.HighestSentVscId = v + } + if v := k.GetConsumerHighestAckedVscId(ctx, consumerId); v != 0 { + cs.HighestAckedVscId = v + } + consumerStates = append(consumerStates, cs) } diff --git a/x/vaas/provider/keeper/genesis_test.go b/x/vaas/provider/keeper/genesis_test.go index 500d074..697cd82 100644 --- a/x/vaas/provider/keeper/genesis_test.go +++ b/x/vaas/provider/keeper/genesis_test.go @@ -74,6 +74,7 @@ func TestExportGenesisIncludesNewFields(t *testing.T) { HistoricalEntries: 10, } removalTime := time.Unix(1_800_000_000, 0).UTC() + lastAckTime := time.Unix(1_850_000_000, 0).UTC() cg := *vaastypes.DefaultConsumerGenesisState() cg.NewChain = true @@ -105,6 +106,9 @@ func TestExportGenesisIncludesNewFields(t *testing.T) { require.NoError(t, pk.SetConsumerInitializationParameters(ctx, 2, initParams)) pk.SetConsumerClientId(ctx, 2, "07-tendermint-0") require.NoError(t, pk.SetConsumerGenesis(ctx, 2, cg)) + require.NoError(t, pk.SetConsumerLastAckTime(ctx, 2, lastAckTime)) + pk.SetConsumerHighestSentVscId(ctx, 2, 9) + pk.SetConsumerHighestAckedVscId(ctx, 2, 8) // id 3 STOPPED. pk.SetConsumerPhase(ctx, 3, providertypes.CONSUMER_PHASE_STOPPED) @@ -147,6 +151,12 @@ func TestExportGenesisIncludesNewFields(t *testing.T) { require.NotNil(t, byID["consumer-delta"].RemovalTime, "STOPPED must carry removal_time") require.Equal(t, removalTime, *byID["consumer-delta"].RemovalTime) require.Equal(t, providertypes.CONSUMER_PHASE_DELETED, byID["consumer-epsilon"].Phase) + + // LAUNCHED consumer carries the liveness clock (last-ack + resync counters). + require.NotNil(t, byID["consumer-gamma"].LastAckTime, "LAUNCHED must carry last_ack_time") + require.Equal(t, lastAckTime, *byID["consumer-gamma"].LastAckTime) + require.Equal(t, uint64(9), byID["consumer-gamma"].HighestSentVscId) + require.Equal(t, uint64(8), byID["consumer-gamma"].HighestAckedVscId) } func TestInitGenesisRestoresPerConsumerStateAndDerivedQueues(t *testing.T) { @@ -361,6 +371,12 @@ func TestGenesisRoundTrip(t *testing.T) { pkA.SetValidatorByConsumerAddr(ctxA, keyedConsumerID, assignedConsumerConsAddr, assignedProviderConsAddr) pkA.AppendConsumerAddrsToPrune(ctxA, keyedConsumerID, pruneTs, prunedAddr) + // Seed the liveness clock on the LAUNCHED consumer so the round-trip covers + // the last-ack time and the resync counters (highest sent / acked). + require.NoError(t, pkA.SetConsumerLastAckTime(ctxA, keyedConsumerID, time.Unix(1_850_000_000, 0).UTC())) + pkA.SetConsumerHighestSentVscId(ctxA, keyedConsumerID, 7) + pkA.SetConsumerHighestAckedVscId(ctxA, keyedConsumerID, 5) + pkA.SetParams(ctxA, providertypes.DefaultParams()) pkA.SetValidatorSetUpdateId(ctxA, 1) diff --git a/x/vaas/provider/types/genesis.pb.go b/x/vaas/provider/types/genesis.pb.go index 1b2b7e3..0587c66 100644 --- a/x/vaas/provider/types/genesis.pb.go +++ b/x/vaas/provider/types/genesis.pb.go @@ -187,6 +187,15 @@ type ConsumerState struct { // scheduled to transition to DELETED. Set only when phase == STOPPED; // absent otherwise. Used to re-derive the keeper's removal-time queue. RemovalTime *time.Time `protobuf:"bytes,12,opt,name=removal_time,json=removalTime,proto3,stdtime" json:"removal_time,omitempty"` + // Liveness clock, present once a consumer has launched: LastAckTime is the + // block time of the consumer's most recent successful VSC acknowledgement + // (the provider's per-consumer grace clock), and HighestSentVscId / + // HighestAckedVscId are the resync counters that decide whether the consumer + // is behind. Round-tripped so a state-export restart preserves the grace + // window and resync state instead of resetting them. + LastAckTime *time.Time `protobuf:"bytes,13,opt,name=last_ack_time,json=lastAckTime,proto3,stdtime" json:"last_ack_time,omitempty"` + HighestSentVscId uint64 `protobuf:"varint,14,opt,name=highest_sent_vsc_id,json=highestSentVscId,proto3" json:"highest_sent_vsc_id,omitempty"` + HighestAckedVscId uint64 `protobuf:"varint,15,opt,name=highest_acked_vsc_id,json=highestAckedVscId,proto3" json:"highest_acked_vsc_id,omitempty"` } func (m *ConsumerState) Reset() { *m = ConsumerState{} } @@ -306,6 +315,27 @@ func (m *ConsumerState) GetRemovalTime() *time.Time { return nil } +func (m *ConsumerState) GetLastAckTime() *time.Time { + if m != nil { + return m.LastAckTime + } + return nil +} + +func (m *ConsumerState) GetHighestSentVscId() uint64 { + if m != nil { + return m.HighestSentVscId + } + return 0 +} + +func (m *ConsumerState) GetHighestAckedVscId() uint64 { + if m != nil { + return m.HighestAckedVscId + } + return 0 +} + // ValsetUpdateIdToHeight defines the genesis information for the mapping // of each valset update id to a block height type ValsetUpdateIdToHeight struct { @@ -490,70 +520,74 @@ func init() { func init() { proto.RegisterFile("vaas/provider/v1/genesis.proto", fileDescriptor_c9071b84cde652f9) } var fileDescriptor_c9071b84cde652f9 = []byte{ - // 1004 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xdd, 0x6e, 0xdc, 0x44, - 0x14, 0x8e, 0x9b, 0xcd, 0x36, 0x3b, 0x9b, 0x84, 0x68, 0x94, 0x06, 0x37, 0x25, 0xbb, 0x61, 0x51, - 0xab, 0x45, 0x50, 0x5b, 0x09, 0xa2, 0x17, 0x5c, 0x20, 0x65, 0x53, 0x15, 0x56, 0x08, 0x58, 0x39, - 0x81, 0x8b, 0xde, 0x58, 0xb3, 0x9e, 0x53, 0x7b, 0x58, 0xdb, 0x63, 0x79, 0x66, 0x1d, 0x96, 0x0b, - 0x9e, 0xa1, 0x4f, 0xc1, 0x13, 0xf4, 0x21, 0x2a, 0xae, 0xaa, 0x5e, 0x21, 0x2e, 0x02, 0x4a, 0x9e, - 0x00, 0x9e, 0x00, 0xcd, 0x78, 0xec, 0xe6, 0x67, 0x93, 0x88, 0xbb, 0xf5, 0xf9, 0xbe, 0xf9, 0xbe, - 0x33, 0x33, 0xe7, 0x9c, 0x59, 0xd4, 0x29, 0x08, 0x11, 0x6e, 0x96, 0xf3, 0x82, 0x51, 0xc8, 0xdd, - 0x62, 0xd7, 0x0d, 0x21, 0x05, 0xc1, 0x84, 0x93, 0xe5, 0x5c, 0x72, 0xbc, 0xae, 0x70, 0xa7, 0xc2, - 0x9d, 0x62, 0x77, 0xeb, 0x7e, 0xc0, 0x45, 0xc2, 0x85, 0xaf, 0x71, 0xb7, 0xfc, 0x28, 0xc9, 0x5b, - 0x1b, 0x21, 0x0f, 0x79, 0x19, 0x57, 0xbf, 0x4c, 0xb4, 0x1b, 0x72, 0x1e, 0xc6, 0xe0, 0xea, 0xaf, - 0xf1, 0xf4, 0x85, 0x2b, 0x59, 0x02, 0x42, 0x92, 0x24, 0x33, 0x84, 0x6d, 0x9d, 0x43, 0xb1, 0xeb, - 0x8a, 0x88, 0xe4, 0x40, 0xfd, 0x80, 0xa7, 0x62, 0x9a, 0x40, 0x6e, 0x60, 0x5c, 0xc1, 0xc7, 0x2c, - 0x87, 0x4a, 0xf3, 0x4a, 0xda, 0x75, 0x8a, 0x9a, 0xd0, 0xfb, 0xa7, 0x89, 0x56, 0xbe, 0x2a, 0x77, - 0x72, 0x28, 0x89, 0x04, 0xdc, 0x47, 0xeb, 0x05, 0x89, 0x05, 0x48, 0x7f, 0x9a, 0x51, 0x22, 0xc1, - 0x67, 0xd4, 0xb6, 0x76, 0xac, 0x7e, 0xc3, 0x5b, 0x2b, 0xe3, 0x3f, 0xe8, 0xf0, 0x90, 0xe2, 0x08, - 0xbd, 0x57, 0x65, 0xe0, 0x0b, 0xb5, 0x56, 0xd8, 0x77, 0x76, 0x16, 0xfb, 0xed, 0xbd, 0xae, 0x73, - 0xf9, 0x30, 0x9c, 0x03, 0x43, 0xd4, 0x1e, 0x83, 0xce, 0xeb, 0x93, 0xee, 0xc2, 0xbf, 0x27, 0xdd, - 0xcd, 0x19, 0x49, 0xe2, 0x2f, 0x7a, 0x97, 0x54, 0x7a, 0xde, 0x5a, 0x70, 0x9e, 0x2e, 0xf0, 0x4f, - 0x68, 0xeb, 0x72, 0x4e, 0xbe, 0xe4, 0x7e, 0x04, 0x2c, 0x8c, 0xa4, 0xbd, 0xa8, 0x4d, 0xfb, 0x57, - 0x4d, 0x7f, 0xbc, 0x90, 0xef, 0x11, 0xff, 0x5a, 0xf3, 0x07, 0x0d, 0xe5, 0xee, 0x6d, 0x16, 0x73, - 0x51, 0xfc, 0x04, 0x35, 0x33, 0x92, 0x93, 0x44, 0xd8, 0x8d, 0x1d, 0xab, 0xdf, 0xde, 0xb3, 0xaf, - 0xea, 0x8e, 0x34, 0x6e, 0x74, 0x0c, 0x1b, 0x27, 0x3a, 0x47, 0x46, 0x89, 0xe4, 0x79, 0x7d, 0x33, - 0x7e, 0x36, 0x1d, 0x4f, 0x60, 0x26, 0xec, 0x25, 0x9d, 0xe3, 0xc7, 0x73, 0x73, 0x2c, 0xd7, 0x54, - 0x27, 0x34, 0x9a, 0x8e, 0xbf, 0x81, 0x99, 0x11, 0xb7, 0x8b, 0x39, 0xb0, 0x12, 0xc4, 0x29, 0x7a, - 0x50, 0x63, 0xc2, 0x1f, 0xcf, 0xde, 0x59, 0x12, 0x4a, 0x73, 0xbb, 0x79, 0xab, 0xdf, 0x60, 0x56, - 0x49, 0xee, 0x53, 0x9a, 0x5f, 0xf1, 0x13, 0x17, 0x71, 0x1c, 0xa0, 0xf7, 0x2f, 0x38, 0x08, 0x75, - 0x01, 0x59, 0x3e, 0x4d, 0xc1, 0xbe, 0xab, 0xbd, 0x1e, 0x5d, 0x7f, 0xe9, 0x4a, 0x40, 0x1c, 0xf1, - 0x91, 0x62, 0x1b, 0xa3, 0x8d, 0x60, 0x0e, 0x86, 0x7f, 0x45, 0x1f, 0xd6, 0x26, 0x2f, 0x00, 0x84, - 0x9f, 0x41, 0xee, 0x8f, 0x63, 0x1e, 0x4c, 0x7c, 0x5e, 0x40, 0x9e, 0x33, 0x0a, 0xc2, 0x5e, 0xd6, - 0x76, 0xce, 0xf5, 0x76, 0xcf, 0x00, 0xc4, 0x08, 0xf2, 0x81, 0x5a, 0xf7, 0xbd, 0x59, 0x66, 0x6c, - 0xb7, 0x83, 0x1b, 0x38, 0x02, 0x03, 0xb2, 0xcf, 0xfb, 0xfb, 0x19, 0xe7, 0xb1, 0xaf, 0x9b, 0x4d, - 0xd8, 0xad, 0xdb, 0x76, 0xf9, 0x0c, 0x60, 0xc4, 0x79, 0x7c, 0xa8, 0xe8, 0xc6, 0xee, 0x5e, 0x30, - 0x07, 0x13, 0xbd, 0xdf, 0x96, 0xd0, 0xea, 0x85, 0x86, 0xc0, 0x5d, 0xd4, 0xae, 0x8d, 0xeb, 0x7e, - 0x43, 0x55, 0x68, 0x48, 0xf1, 0x7d, 0xb4, 0x1c, 0x44, 0x84, 0xa5, 0x0a, 0xbd, 0xb3, 0x63, 0xf5, - 0x5b, 0xde, 0x5d, 0xfd, 0x3d, 0xa4, 0xf8, 0x01, 0x6a, 0x05, 0x31, 0x83, 0x54, 0x2a, 0x6c, 0x51, - 0x63, 0xcb, 0x65, 0x60, 0x48, 0xf1, 0x43, 0xb4, 0xc6, 0x52, 0x26, 0x19, 0x89, 0xab, 0x6e, 0x69, - 0x68, 0xed, 0x55, 0x13, 0x35, 0x45, 0xff, 0x1d, 0x5a, 0xaf, 0xfd, 0xcd, 0x5c, 0xb3, 0x97, 0x74, - 0xf9, 0x6f, 0x97, 0x1b, 0x3e, 0xb7, 0xcf, 0xf3, 0xd3, 0xc2, 0xec, 0xb3, 0x9e, 0x03, 0x06, 0xc3, - 0x04, 0x6d, 0x66, 0x90, 0x52, 0x96, 0x86, 0xbe, 0x69, 0xdc, 0x20, 0x22, 0x69, 0x08, 0xc2, 0x14, - 0xe6, 0xc3, 0x5a, 0xb5, 0xae, 0xc7, 0x43, 0x90, 0x07, 0x9a, 0x33, 0x22, 0xc1, 0x04, 0xe4, 0x53, - 0x22, 0x49, 0x55, 0x2b, 0x46, 0xaa, 0x6c, 0xe7, 0x92, 0x24, 0xf0, 0xa7, 0x08, 0x8b, 0x98, 0x88, - 0xc8, 0xa7, 0xfc, 0x38, 0x55, 0x93, 0xd2, 0x27, 0xc1, 0x44, 0xd7, 0x62, 0xcb, 0x5b, 0xd7, 0xc8, - 0x53, 0x03, 0xec, 0x07, 0x13, 0xfc, 0x39, 0x5a, 0xca, 0x22, 0x22, 0xc0, 0x5e, 0xde, 0xb1, 0xfa, - 0x6b, 0x37, 0x4d, 0xa8, 0x91, 0xa2, 0x79, 0x25, 0x1b, 0x7f, 0x84, 0x56, 0xf9, 0x71, 0x6a, 0x4a, - 0x1e, 0x84, 0xaa, 0x02, 0x75, 0xbe, 0x2b, 0x3a, 0xb8, 0x5f, 0xc6, 0xf0, 0x97, 0x68, 0x39, 0x01, - 0x49, 0x28, 0x91, 0xc4, 0x46, 0xfa, 0xd0, 0x7a, 0xd7, 0xcb, 0x7f, 0x6b, 0x98, 0x5e, 0xbd, 0x06, - 0x1f, 0xa2, 0xb6, 0xba, 0x0d, 0xdf, 0x8c, 0x9d, 0xb6, 0x96, 0xd8, 0xbb, 0x5e, 0x62, 0x58, 0x5e, - 0x1d, 0xfb, 0x85, 0x48, 0xc6, 0x53, 0x3d, 0x8c, 0x40, 0x42, 0x2e, 0x3c, 0xa4, 0x64, 0xca, 0xe1, - 0x84, 0x0f, 0xd0, 0x4a, 0x0e, 0x09, 0x2f, 0x48, 0xec, 0xab, 0x33, 0xb0, 0x57, 0xb4, 0xea, 0x96, - 0x53, 0xbe, 0x31, 0x4e, 0xf5, 0xc6, 0x38, 0x47, 0xd5, 0x1b, 0x33, 0x68, 0xbc, 0xfc, 0xab, 0x6b, - 0x79, 0x6d, 0xb3, 0x4a, 0xc5, 0x7b, 0xcf, 0xd1, 0xe6, 0xfc, 0x19, 0xfa, 0x3f, 0x5e, 0x89, 0x4d, - 0xd4, 0x34, 0x95, 0x77, 0x47, 0xe3, 0xe6, 0xab, 0x17, 0xa2, 0x0f, 0x6e, 0x6a, 0xd8, 0xdb, 0x5b, - 0xe2, 0x11, 0x6a, 0x92, 0x84, 0x4f, 0xd3, 0x52, 0xb8, 0x35, 0x58, 0x7b, 0xfb, 0xea, 0x31, 0x32, - 0xcf, 0xec, 0x30, 0x95, 0x9e, 0x41, 0x7b, 0xbf, 0x5b, 0x68, 0x63, 0x5e, 0x8f, 0xde, 0xee, 0xf0, - 0x04, 0xb5, 0x28, 0x64, 0x5c, 0x30, 0xc9, 0x73, 0x63, 0x62, 0xbf, 0x7d, 0xf5, 0x78, 0xc3, 0x98, - 0x98, 0xfb, 0x3f, 0x94, 0x39, 0x4b, 0x43, 0xef, 0x1d, 0x15, 0x6f, 0xa0, 0x25, 0x0a, 0x29, 0x4f, - 0x4c, 0x37, 0x96, 0x1f, 0xf8, 0x00, 0x35, 0xcd, 0x28, 0x69, 0x68, 0xa9, 0x4f, 0x54, 0x71, 0xff, - 0x79, 0xd2, 0xbd, 0x57, 0xca, 0x09, 0x3a, 0x71, 0x18, 0x77, 0x13, 0x22, 0x23, 0x95, 0xfe, 0xe5, - 0xcd, 0x94, 0x4b, 0x07, 0xc3, 0xd7, 0xa7, 0x1d, 0xeb, 0xcd, 0x69, 0xc7, 0xfa, 0xfb, 0xb4, 0x63, - 0xbd, 0x3c, 0xeb, 0x2c, 0xbc, 0x39, 0xeb, 0x2c, 0xfc, 0x71, 0xd6, 0x59, 0x78, 0xee, 0x86, 0x4c, - 0x46, 0xd3, 0xb1, 0x13, 0xf0, 0xc4, 0x25, 0x71, 0xcc, 0xd2, 0x31, 0x93, 0xc2, 0xd5, 0xcf, 0xff, - 0xcf, 0xee, 0xc5, 0x7f, 0x01, 0x72, 0x96, 0x81, 0x18, 0x37, 0x75, 0x0d, 0x7c, 0xf6, 0x5f, 0x00, - 0x00, 0x00, 0xff, 0xff, 0xbf, 0x87, 0x0b, 0xc8, 0xda, 0x08, 0x00, 0x00, + // 1069 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xdd, 0x6e, 0x1b, 0x45, + 0x14, 0x8e, 0x13, 0xc7, 0x8d, 0xc7, 0x89, 0x1b, 0x06, 0x37, 0x6c, 0x53, 0x62, 0x07, 0xa3, 0x56, + 0x46, 0x10, 0xaf, 0x12, 0x44, 0x2f, 0xb8, 0x40, 0x8a, 0x13, 0x15, 0x2c, 0x04, 0x58, 0x76, 0xe8, + 0x45, 0x6f, 0x56, 0xe3, 0x9d, 0xd3, 0xdd, 0xc1, 0xbb, 0x3b, 0xab, 0x9d, 0xf1, 0x06, 0x73, 0xc1, + 0x33, 0xf4, 0x61, 0xfa, 0x10, 0x15, 0xe2, 0xa2, 0xea, 0x15, 0xe2, 0x22, 0xa0, 0xe4, 0x09, 0xe0, + 0x09, 0xd0, 0xcc, 0xce, 0x6e, 0xf3, 0xe3, 0x24, 0xf4, 0xce, 0x73, 0xbe, 0x6f, 0xbe, 0xef, 0xcc, + 0xcc, 0x39, 0x67, 0x8d, 0x9a, 0x29, 0x21, 0xc2, 0x8e, 0x13, 0x9e, 0x32, 0x0a, 0x89, 0x9d, 0xee, + 0xda, 0x1e, 0x44, 0x20, 0x98, 0xe8, 0xc6, 0x09, 0x97, 0x1c, 0xaf, 0x2b, 0xbc, 0x9b, 0xe3, 0xdd, + 0x74, 0x77, 0xf3, 0xbe, 0xcb, 0x45, 0xc8, 0x85, 0xa3, 0x71, 0x3b, 0x5b, 0x64, 0xe4, 0xcd, 0x86, + 0xc7, 0x3d, 0x9e, 0xc5, 0xd5, 0x2f, 0x13, 0x6d, 0x79, 0x9c, 0x7b, 0x01, 0xd8, 0x7a, 0x35, 0x9e, + 0x3e, 0xb7, 0x25, 0x0b, 0x41, 0x48, 0x12, 0xc6, 0x86, 0xb0, 0xa5, 0x73, 0x48, 0x77, 0x6d, 0xe1, + 0x93, 0x04, 0xa8, 0xe3, 0xf2, 0x48, 0x4c, 0x43, 0x48, 0x0c, 0x8c, 0x73, 0xf8, 0x98, 0x25, 0x90, + 0x6b, 0x5e, 0x49, 0xbb, 0x48, 0x51, 0x13, 0xda, 0xff, 0x54, 0xd0, 0xea, 0xd7, 0xd9, 0x49, 0x46, + 0x92, 0x48, 0xc0, 0x1d, 0xb4, 0x9e, 0x92, 0x40, 0x80, 0x74, 0xa6, 0x31, 0x25, 0x12, 0x1c, 0x46, + 0xad, 0xd2, 0x76, 0xa9, 0x53, 0x1e, 0xd6, 0xb3, 0xf8, 0x8f, 0x3a, 0xdc, 0xa7, 0xd8, 0x47, 0x77, + 0xf3, 0x0c, 0x1c, 0xa1, 0xf6, 0x0a, 0x6b, 0x71, 0x7b, 0xa9, 0x53, 0xdb, 0x6b, 0x75, 0x2f, 0x5f, + 0x46, 0xf7, 0xc0, 0x10, 0xb5, 0x47, 0xaf, 0xf9, 0xea, 0xa4, 0xb5, 0xf0, 0xef, 0x49, 0x6b, 0x63, + 0x46, 0xc2, 0xe0, 0xcb, 0xf6, 0x25, 0x95, 0xf6, 0xb0, 0xee, 0x9e, 0xa7, 0x0b, 0xfc, 0x13, 0xda, + 0xbc, 0x9c, 0x93, 0x23, 0xb9, 0xe3, 0x03, 0xf3, 0x7c, 0x69, 0x2d, 0x69, 0xd3, 0xce, 0x55, 0xd3, + 0xa7, 0x17, 0xf2, 0x3d, 0xe2, 0xdf, 0x68, 0x7e, 0xaf, 0xac, 0xdc, 0x87, 0x1b, 0xe9, 0x5c, 0x14, + 0x3f, 0x46, 0x95, 0x98, 0x24, 0x24, 0x14, 0x56, 0x79, 0xbb, 0xd4, 0xa9, 0xed, 0x59, 0x57, 0x75, + 0x07, 0x1a, 0x37, 0x3a, 0x86, 0x8d, 0x43, 0x9d, 0x23, 0xa3, 0x44, 0xf2, 0xa4, 0x78, 0x19, 0x27, + 0x9e, 0x8e, 0x27, 0x30, 0x13, 0xd6, 0xb2, 0xce, 0xf1, 0x93, 0xb9, 0x39, 0x66, 0x7b, 0xf2, 0x1b, + 0x1a, 0x4c, 0xc7, 0xdf, 0xc2, 0xcc, 0x88, 0x5b, 0xe9, 0x1c, 0x58, 0x09, 0xe2, 0x08, 0x3d, 0x28, + 0x30, 0xe1, 0x8c, 0x67, 0x6f, 0x2d, 0x09, 0xa5, 0x89, 0x55, 0xb9, 0xd5, 0xaf, 0x37, 0xcb, 0x25, + 0xf7, 0x29, 0x4d, 0xae, 0xf8, 0x89, 0x8b, 0x38, 0x76, 0xd1, 0x07, 0x17, 0x1c, 0x84, 0x7a, 0x80, + 0x38, 0x99, 0x46, 0x60, 0xdd, 0xd1, 0x5e, 0x8f, 0xae, 0x7f, 0x74, 0x25, 0x20, 0x8e, 0xf8, 0x40, + 0xb1, 0x8d, 0x51, 0xc3, 0x9d, 0x83, 0xe1, 0x5f, 0xd1, 0x47, 0x85, 0xc9, 0x73, 0x00, 0xe1, 0xc4, + 0x90, 0x38, 0xe3, 0x80, 0xbb, 0x13, 0x87, 0xa7, 0x90, 0x24, 0x8c, 0x82, 0xb0, 0x56, 0xb4, 0x5d, + 0xf7, 0x7a, 0xbb, 0x27, 0x00, 0x62, 0x00, 0x49, 0x4f, 0xed, 0xfb, 0xc1, 0x6c, 0x33, 0xb6, 0x5b, + 0xee, 0x0d, 0x1c, 0x81, 0x01, 0x59, 0xe7, 0xfd, 0x9d, 0x98, 0xf3, 0xc0, 0xd1, 0xcd, 0x26, 0xac, + 0xea, 0x6d, 0xa7, 0x7c, 0x02, 0x30, 0xe0, 0x3c, 0x18, 0x29, 0xba, 0xb1, 0xbb, 0xe7, 0xce, 0xc1, + 0x44, 0xfb, 0xf7, 0x0a, 0x5a, 0xbb, 0xd0, 0x10, 0xb8, 0x85, 0x6a, 0x85, 0x71, 0xd1, 0x6f, 0x28, + 0x0f, 0xf5, 0x29, 0xbe, 0x8f, 0x56, 0x5c, 0x9f, 0xb0, 0x48, 0xa1, 0x8b, 0xdb, 0xa5, 0x4e, 0x75, + 0x78, 0x47, 0xaf, 0xfb, 0x14, 0x3f, 0x40, 0x55, 0x37, 0x60, 0x10, 0x49, 0x85, 0x2d, 0x69, 0x6c, + 0x25, 0x0b, 0xf4, 0x29, 0x7e, 0x88, 0xea, 0x2c, 0x62, 0x92, 0x91, 0x20, 0xef, 0x96, 0xb2, 0xd6, + 0x5e, 0x33, 0x51, 0x53, 0xf4, 0xdf, 0xa3, 0xf5, 0xc2, 0xdf, 0xcc, 0x35, 0x6b, 0x59, 0x97, 0xff, + 0x56, 0x76, 0xe0, 0x73, 0xe7, 0x3c, 0x3f, 0x2d, 0xcc, 0x39, 0x8b, 0x39, 0x60, 0x30, 0x4c, 0xd0, + 0x46, 0x0c, 0x11, 0x65, 0x91, 0xe7, 0x98, 0xc6, 0x75, 0x7d, 0x12, 0x79, 0x20, 0x4c, 0x61, 0x3e, + 0x2c, 0x54, 0x8b, 0x7a, 0x1c, 0x81, 0x3c, 0xd0, 0x9c, 0x01, 0x71, 0x27, 0x20, 0x0f, 0x89, 0x24, + 0x79, 0xad, 0x18, 0xa9, 0xac, 0x9d, 0x33, 0x92, 0xc0, 0x9f, 0x21, 0x2c, 0x02, 0x22, 0x7c, 0x87, + 0xf2, 0xe3, 0x48, 0x4d, 0x4a, 0x87, 0xb8, 0x13, 0x5d, 0x8b, 0xd5, 0xe1, 0xba, 0x46, 0x0e, 0x0d, + 0xb0, 0xef, 0x4e, 0xf0, 0x17, 0x68, 0x39, 0xf6, 0x89, 0x00, 0x6b, 0x65, 0xbb, 0xd4, 0xa9, 0xdf, + 0x34, 0xa1, 0x06, 0x8a, 0x36, 0xcc, 0xd8, 0xf8, 0x63, 0xb4, 0xc6, 0x8f, 0x23, 0x53, 0xf2, 0x20, + 0x54, 0x15, 0xa8, 0xfb, 0x5d, 0xd5, 0xc1, 0xfd, 0x2c, 0x86, 0xbf, 0x42, 0x2b, 0x21, 0x48, 0x42, + 0x89, 0x24, 0x16, 0xd2, 0x97, 0xd6, 0xbe, 0x5e, 0xfe, 0x3b, 0xc3, 0x1c, 0x16, 0x7b, 0xf0, 0x08, + 0xd5, 0xd4, 0x6b, 0x38, 0x66, 0xec, 0xd4, 0xb4, 0xc4, 0xde, 0xf5, 0x12, 0xfd, 0xec, 0xe9, 0xd8, + 0x2f, 0x44, 0x32, 0x1e, 0xe9, 0x61, 0x04, 0x12, 0x12, 0x31, 0x44, 0x4a, 0x26, 0x1b, 0x4e, 0xf8, + 0x00, 0xad, 0x26, 0x10, 0xf2, 0x94, 0x04, 0x8e, 0xba, 0x03, 0x6b, 0x55, 0xab, 0x6e, 0x76, 0xb3, + 0x6f, 0x4c, 0x37, 0xff, 0xc6, 0x74, 0x8f, 0xf2, 0x6f, 0x4c, 0xaf, 0xfc, 0xe2, 0xaf, 0x56, 0x69, + 0x58, 0x33, 0xbb, 0x54, 0x1c, 0x1f, 0xa2, 0xb5, 0x80, 0x08, 0xa9, 0x6e, 0x36, 0x53, 0x59, 0xfb, + 0xbf, 0x2a, 0x6a, 0xdb, 0xbe, 0x3b, 0xd1, 0x2a, 0x3b, 0xe8, 0x7d, 0x9f, 0x79, 0x3e, 0x08, 0xe9, + 0x08, 0x55, 0xa6, 0xa9, 0x70, 0x55, 0xa9, 0xd6, 0x75, 0x21, 0xae, 0x1b, 0x68, 0x04, 0x91, 0x7c, + 0x2a, 0xdc, 0x3e, 0xc5, 0x36, 0x6a, 0xe4, 0x74, 0x55, 0x09, 0x34, 0xe7, 0xdf, 0xd5, 0xfc, 0xf7, + 0x0c, 0xb6, 0xaf, 0x20, 0xbd, 0xa1, 0xfd, 0x0c, 0x6d, 0xcc, 0x9f, 0xf4, 0xef, 0xf0, 0x2d, 0xdb, + 0x40, 0x15, 0xd3, 0x1f, 0x8b, 0x1a, 0x37, 0xab, 0xb6, 0x87, 0x3e, 0xbc, 0x69, 0xac, 0xdc, 0xde, + 0xb8, 0x8f, 0x50, 0x85, 0x84, 0x7c, 0x1a, 0x65, 0xc2, 0xd5, 0x5e, 0xfd, 0xcd, 0xcb, 0x1d, 0x64, + 0xfe, 0x0c, 0xf4, 0x23, 0x39, 0x34, 0x68, 0xfb, 0xb7, 0x12, 0x6a, 0xcc, 0x9b, 0x24, 0xb7, 0x3b, + 0x3c, 0x46, 0x55, 0x0a, 0x31, 0x17, 0x4c, 0xf2, 0xc4, 0x98, 0x58, 0x6f, 0x5e, 0xee, 0x34, 0x8c, + 0x89, 0xa9, 0xd2, 0x91, 0x4c, 0x58, 0xe4, 0x0d, 0xdf, 0x52, 0x71, 0x03, 0x2d, 0x53, 0x88, 0x78, + 0x68, 0x66, 0x46, 0xb6, 0xc0, 0x07, 0xa8, 0x62, 0x06, 0x5e, 0x59, 0x4b, 0x7d, 0xaa, 0x5a, 0xf0, + 0xcf, 0x93, 0xd6, 0xbd, 0x4c, 0x4e, 0xd0, 0x49, 0x97, 0x71, 0x3b, 0x24, 0xd2, 0x57, 0xe9, 0x5f, + 0x3e, 0x4c, 0xb6, 0xb5, 0xd7, 0x7f, 0x75, 0xda, 0x2c, 0xbd, 0x3e, 0x6d, 0x96, 0xfe, 0x3e, 0x6d, + 0x96, 0x5e, 0x9c, 0x35, 0x17, 0x5e, 0x9f, 0x35, 0x17, 0xfe, 0x38, 0x6b, 0x2e, 0x3c, 0xb3, 0x3d, + 0x26, 0xfd, 0xe9, 0xb8, 0xeb, 0xf2, 0xd0, 0x26, 0x41, 0xc0, 0xa2, 0x31, 0x93, 0xc2, 0xd6, 0x7f, + 0x52, 0x7e, 0xb6, 0x2f, 0xfe, 0x57, 0x91, 0xb3, 0x18, 0xc4, 0xb8, 0xa2, 0x6b, 0xec, 0xf3, 0xff, + 0x02, 0x00, 0x00, 0xff, 0xff, 0xc1, 0x70, 0xc0, 0x2f, 0x80, 0x09, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -712,14 +746,34 @@ func (m *ConsumerState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - if m.RemovalTime != nil { - n2, err2 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(*m.RemovalTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.RemovalTime):]) + if m.HighestAckedVscId != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.HighestAckedVscId)) + i-- + dAtA[i] = 0x78 + } + if m.HighestSentVscId != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.HighestSentVscId)) + i-- + dAtA[i] = 0x70 + } + if m.LastAckTime != nil { + n2, err2 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(*m.LastAckTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.LastAckTime):]) if err2 != nil { return 0, err2 } i -= n2 i = encodeVarintGenesis(dAtA, i, uint64(n2)) i-- + dAtA[i] = 0x6a + } + if m.RemovalTime != nil { + n3, err3 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(*m.RemovalTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.RemovalTime):]) + if err3 != nil { + return 0, err3 + } + i -= n3 + i = encodeVarintGenesis(dAtA, i, uint64(n3)) + i-- dAtA[i] = 0x62 } if m.InitParams != nil { @@ -1058,6 +1112,16 @@ func (m *ConsumerState) Size() (n int) { l = github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.RemovalTime) n += 1 + l + sovGenesis(uint64(l)) } + if m.LastAckTime != nil { + l = github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.LastAckTime) + n += 1 + l + sovGenesis(uint64(l)) + } + if m.HighestSentVscId != 0 { + n += 1 + sovGenesis(uint64(m.HighestSentVscId)) + } + if m.HighestAckedVscId != 0 { + n += 1 + sovGenesis(uint64(m.HighestAckedVscId)) + } return n } @@ -1849,6 +1913,80 @@ func (m *ConsumerState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 13: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LastAckTime", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.LastAckTime == nil { + m.LastAckTime = new(time.Time) + } + if err := github_com_cosmos_gogoproto_types.StdTimeUnmarshal(m.LastAckTime, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 14: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field HighestSentVscId", wireType) + } + m.HighestSentVscId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.HighestSentVscId |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 15: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field HighestAckedVscId", wireType) + } + m.HighestAckedVscId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.HighestAckedVscId |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) From 001d11b0c5959ecdfd9182b24c9b9813219fec0c Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:24:59 +0200 Subject: [PATCH 14/20] remove the non-functional remove-consumer CLI command MsgRemoveConsumer requires the governance module as authority, but the CLI signed it with the caller's address, so the broadcast tx always failed ErrInvalidSigner. Removal goes through a gov proposal (tx gov submit-proposal with MsgRemoveConsumer); the dedicated command could only ever fail, so drop it. --- x/vaas/provider/client/cli/tx.go | 48 -------------------------------- 1 file changed, 48 deletions(-) diff --git a/x/vaas/provider/client/cli/tx.go b/x/vaas/provider/client/cli/tx.go index fb1c68c..611fb2a 100644 --- a/x/vaas/provider/client/cli/tx.go +++ b/x/vaas/provider/client/cli/tx.go @@ -36,7 +36,6 @@ func GetTxCmd() *cobra.Command { cmd.AddCommand(NewSubmitConsumerDoubleVotingCmd()) cmd.AddCommand(NewCreateConsumerCmd()) cmd.AddCommand(NewUpdateConsumerCmd()) - cmd.AddCommand(NewRemoveConsumerCmd()) cmd.AddCommand(NewFundConsumerFeePoolCmd()) cmd.AddCommand(NewWithdrawConsumerFeePoolCmd()) cmd.AddCommand(NewSweepConsumerFeePoolCmd()) @@ -388,53 +387,6 @@ If one of the fields is missing, it will be set to its zero value. return cmd } -func NewRemoveConsumerCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "remove-consumer [consumer-id]", - Short: "remove a consumer chain", - Long: strings.TrimSpace( - fmt.Sprintf(`Removes (and stops) a consumer chain. Removal must be performed via governance: submit a proposal containing MsgRemoveConsumer with the governance module address as authority. -Example: -%s tx provider remove-consumer [consumer-id] -`, version.AppName)), - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - clientCtx, err := client.GetClientTxContext(cmd) - if err != nil { - return err - } - - txf, err := tx.NewFactoryCLI(clientCtx, cmd.Flags()) - if err != nil { - return err - } - txf = txf.WithTxConfig(clientCtx.TxConfig).WithAccountRetriever(clientCtx.AccountRetriever) - - owner := clientCtx.GetFromAddress().String() - consumerId, err := parseConsumerIdArg(args[0]) - if err != nil { - return err - } - - msg, err := types.NewMsgRemoveConsumer(owner, consumerId) - if err != nil { - return err - } - if err := msg.ValidateBasic(); err != nil { - return err - } - - return tx.GenerateOrBroadcastTxWithFactory(clientCtx, txf, msg) - }, - } - - flags.AddTxFlagsToCmd(cmd) - - _ = cmd.MarkFlagRequired(flags.FlagFrom) - - return cmd -} - func NewFundConsumerFeePoolCmd() *cobra.Command { cmd := &cobra.Command{ Use: "fund-consumer-fee-pool [consumer-id] [amount]", From 9a234feff620f7fa31c28d3fc5d637e7af9c81e7 Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:24:59 +0200 Subject: [PATCH 15/20] drop redundant sdk context unwrap in OnRecvVSCPacketV2 ctx is already an sdk.Context, so sdk.UnwrapSDKContext(ctx).BlockTime() is just ctx.BlockTime(). --- x/vaas/consumer/keeper/relay.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/vaas/consumer/keeper/relay.go b/x/vaas/consumer/keeper/relay.go index 293ca46..c2ce9be 100644 --- a/x/vaas/consumer/keeper/relay.go +++ b/x/vaas/consumer/keeper/relay.go @@ -30,7 +30,7 @@ func (k Keeper) OnRecvVSCPacketV2(ctx sdk.Context, sourceClientID string, newCha return nil } - k.SetLastVSCRecvTime(ctx, sdk.UnwrapSDKContext(ctx).BlockTime()) + k.SetLastVSCRecvTime(ctx, ctx.BlockTime()) _, found = k.GetProviderClientID(ctx) if !found { From 3a6d42615f1bc0487e2769efa312cc5dabf5e5e7 Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:24:59 +0200 Subject: [PATCH 16/20] drop the VaasTimeoutPeriod lower floor MinVAASTimeoutPeriod (10m) floored VaasTimeoutPeriod; validation is now (0, MaxTimeoutDelta]. The floor guarded against a too-short timeout expiring packets before delivery, but post-redesign that is not dangerous -- an undelivered packet is superseded next epoch, a late one is dropped by the consumer's dedup, and a persistently unreachable consumer is removed by the liveness sweep. The floor only blocked exercising the timeout path in tests. --- docs/consumer-liveness.md | 8 +++++--- tests/e2e/e2e_liveness_suite_test.go | 3 +-- x/vaas/provider/types/msg_test.go | 20 +++++++++++++------- x/vaas/types/shared_params.go | 17 ++++++++--------- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/docs/consumer-liveness.md b/docs/consumer-liveness.md index 1a9e4dd..bef8c9a 100644 --- a/docs/consumer-liveness.md +++ b/docs/consumer-liveness.md @@ -126,17 +126,19 @@ launched consumer is never treated as stale before its first VSC. | Parameter | Where | Bound | Default | |---|---|---|---| -| `VaasTimeoutPeriod` | provider module param (VSC packet timeout) and per-consumer init param (consumer evidence packet timeout) | `[10m, MaxTimeoutDelta (24h)]` | 1h | +| `VaasTimeoutPeriod` | provider module param (VSC packet timeout) and per-consumer init param (consumer evidence packet timeout) | `(0, MaxTimeoutDelta (24h)]` | 1h | | consumer `UnbondingPeriod` | per-consumer init param | `(0, providerUnbondingPeriod]` | provider default minus 1 day | | `LivenessGraceFraction` | provider module param | `(0, 1)` | `0.66` | | consumer `SafeModeThreshold` | per-consumer init param | `(0, provider liveness grace)` | 3h | -`VaasTimeoutPeriod` is validated to `[10m, 24h]` at both the provider module param boundary +`VaasTimeoutPeriod` is validated to `(0, 24h]` at both the provider module param boundary and the per-consumer initialization-parameter boundary, so the configured value is honest rather than silently capped. The default was lowered from four weeks (which collapsed to 24h under the cap) to one epoch-scale hour: an undelivered packet is superseded by the next epoch's packet and a late one is dropped by the consumer's dedup, so a long timeout buys -nothing. +nothing. No lower floor is imposed: a persistently unreachable consumer is handled by the +liveness sweep, so a short timeout is not dangerous -- and a floor would only prevent +exercising the timeout path in tests. The consumer `UnbondingPeriod` is bounded against the provider's at `MsgCreateConsumer` / `MsgUpdateConsumer` time (it must not exceed the provider's), so a consumer cannot be diff --git a/tests/e2e/e2e_liveness_suite_test.go b/tests/e2e/e2e_liveness_suite_test.go index 822081c..13bc509 100644 --- a/tests/e2e/e2e_liveness_suite_test.go +++ b/tests/e2e/e2e_liveness_suite_test.go @@ -26,8 +26,7 @@ package e2e // set of containers (distinct chain IDs, Docker network, and host ports) and // registers a single consumer via create_consumer_short_unbonding.json which // carries a 200s consumer unbonding (200000000000 ns), 10m vaas_timeout -// (600000000000 ns, the MinVAASTimeoutPeriod floor), and 5s safe_mode_threshold -// (5000000000 ns). +// (600000000000 ns), and 5s safe_mode_threshold (5000000000 ns). // // Test ordering within TestLivenessVAAS: // 1. testRecoverBeforeGrace - brief pause < grace (~150s); consumer stays LAUNCHED. diff --git a/x/vaas/provider/types/msg_test.go b/x/vaas/provider/types/msg_test.go index f465ae5..a0b08b5 100644 --- a/x/vaas/provider/types/msg_test.go +++ b/x/vaas/provider/types/msg_test.go @@ -775,9 +775,10 @@ func TestMsgSetConsumerFeesPerBlock_ValidateBasic(t *testing.T) { } } -// TestValidateInitParams_VaasTimeoutFloor table-drives ValidateInitializationParameters -// focusing on the VaasTimeoutPeriod boundary at MinVAASTimeoutPeriod (10m). -// All other fields are set to valid values so only the timeout is under test. +// TestValidateInitParams_VaasTimeoutBounds table-drives ValidateInitializationParameters +// focusing on the VaasTimeoutPeriod bound (0, MaxTimeoutDelta]: positive with no +// lower floor, capped at the ibc-go v2 hard cap. All other fields are valid so +// only the timeout is under test. func TestValidateInitParams_VaasTimeoutBounds(t *testing.T) { base := types.ConsumerInitializationParameters{ InitialHeight: clienttypes.NewHeight(1, 1), @@ -795,13 +796,18 @@ func TestValidateInitParams_VaasTimeoutBounds(t *testing.T) { wantErr bool }{ { - name: "exactly MinVAASTimeoutPeriod: ok", - timeout: vaastypes.MinVAASTimeoutPeriod, + name: "small positive value: ok (no lower floor)", + timeout: time.Second, wantErr: false, }, { - name: "1ns below floor: error", - timeout: vaastypes.MinVAASTimeoutPeriod - time.Nanosecond, + name: "zero: error", + timeout: 0, + wantErr: true, + }, + { + name: "negative: error", + timeout: -time.Second, wantErr: true, }, { diff --git a/x/vaas/types/shared_params.go b/x/vaas/types/shared_params.go index b3383ec..3b3daa2 100644 --- a/x/vaas/types/shared_params.go +++ b/x/vaas/types/shared_params.go @@ -20,11 +20,6 @@ const ( // epoch and late ones are dropped by the consumer, so a long timeout // buys nothing. Must be <= MaxTimeoutDelta (24h, ibc-go v2 hard cap). DefaultVAASTimeoutPeriod = time.Hour - - // MinVAASTimeoutPeriod is the floor for VaasTimeoutPeriod, comfortably - // above realistic IBC relay latency so packets are not expired before - // they can be delivered. - MinVAASTimeoutPeriod = 10 * time.Minute ) var KeyVAASTimeoutPeriod = []byte("VaasTimeoutPeriod") @@ -37,11 +32,15 @@ func ValidateDuration(d time.Duration) error { } // ValidateVAASTimeoutPeriod checks the VAAS packet timeout is within -// [MinVAASTimeoutPeriod, maxTimeoutDelta]. maxTimeoutDelta is passed in to -// avoid importing ibc-go from this shared package. +// (0, maxTimeoutDelta]. maxTimeoutDelta is passed in to avoid importing ibc-go +// from this shared package. No lower floor is imposed: an undelivered packet is +// superseded by the next epoch's packet and a late one is dropped by the +// consumer's dedup, and a persistently unreachable consumer is handled by the +// liveness sweep -- so a short timeout is not dangerous, and forbidding one only +// blocks testing the timeout path. func ValidateVAASTimeoutPeriod(d, maxTimeoutDelta time.Duration) error { - if d < MinVAASTimeoutPeriod { - return fmt.Errorf("VAAS timeout period must be >= %s, got %s", MinVAASTimeoutPeriod, d) + if d <= 0 { + return fmt.Errorf("VAAS timeout period must be positive, got %s", d) } if d > maxTimeoutDelta { return fmt.Errorf("VAAS timeout period must be <= %s (MaxTimeoutDelta), got %s", maxTimeoutDelta, d) From eef841306f44c96909821a2b37c72e46c17f68b2 Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:11:19 +0200 Subject: [PATCH 17/20] emit a snapshot-resync event; correct the transient-outage e2e's claims The consumer now emits EventTypeSnapshotResync when it applies a snapshot VSC packet (is_snapshot=true) -- not on ordinary diffs -- so a resync is observable by operators and assertable in e2e (rather than being indistinguishable from a resent diff). The main-suite testLivenessTransientOutage previously claimed to 'verify snapshot resync' and exercise the demoted timeout, but with a brief pause no packet times out and re-convergence could equally be a resent diff. Its doc and messages are corrected to claim only what it proves (re-converges after a transient outage); the snapshot path and demoted timeout are covered by unit tests (TestSnapshotResyncEmitsEvent, the TestSnapshot* set, TestTimeoutDoesNotRemove) and, end-to-end, by an upcoming forced-timeout test in the short-timer suite. --- tests/e2e/e2e_consumer_liveness_test.go | 35 +++++++++++++----------- x/vaas/consumer/keeper/relay.go | 13 +++++++++ x/vaas/consumer/keeper/relay_test.go | 36 +++++++++++++++++++++++++ x/vaas/types/events.go | 6 +++++ 4 files changed, 75 insertions(+), 15 deletions(-) diff --git a/tests/e2e/e2e_consumer_liveness_test.go b/tests/e2e/e2e_consumer_liveness_test.go index b3638c7..c3e48ce 100644 --- a/tests/e2e/e2e_consumer_liveness_test.go +++ b/tests/e2e/e2e_consumer_liveness_test.go @@ -4,7 +4,7 @@ package e2e // issue #36 that require a full provider+consumer network: // // 1. testLivenessTransientOutage - brief outage + valset change; the consumer -// stays LAUNCHED and re-syncs via snapshot. +// stays LAUNCHED and re-converges after recovery. // 2. testLivenessRemoval - explicit governance removal transitions the // consumer to STOPPED. // @@ -29,9 +29,14 @@ package e2e // needs a short-unbonding consumer -- exercised by LivenessIntegrationTestSuite. // // For testLivenessTransientOutage we pause the consumer container (same -// mechanism as testDowntimeSlash) for a brief window and verify the consumer -// stays LAUNCHED and its VP re-converges after recovery. This exercises the -// snapshot-resync path introduced in issue #36. +// mechanism as testDowntimeSlash) for a brief window while the provider's valset +// changes, then verify the consumer stays LAUNCHED and its VP re-converges after +// recovery. This is an end-to-end smoke of recovery-after-outage; it does not by +// itself prove *how* the consumer re-converged (the un-timed-out packet could be +// resent). That the demoted timeout no longer removes the consumer and that a +// behind consumer heals via a full snapshot are proven deterministically by unit +// tests (TestTimeoutDoesNotRemove, the TestSnapshot* set, TestSnapshotResyncEmitsEvent) +// and end-to-end by LivenessIntegrationTestSuite's forced-timeout snapshot test. // // Sequencing in TestVAAS: // testLivenessTransientOutage runs after testValidatorSetSync (non-destructive VP change). @@ -66,15 +71,15 @@ func (s *IntegrationTestSuite) queryConsumerPhase(consumerID string) string { return res.Phase } -// testLivenessTransientOutage verifies snapshot resync: a brief consumer -// outage (shorter than the liveness grace period) does not stop the consumer. -// After recovery the consumer's consensus VP must re-converge with the -// provider's updated VP. -// -// Issue #36 context: the snapshot resync sends the full current valset (not -// just a delta) when a reconnecting consumer's last-known VSC ID lags behind. +// testLivenessTransientOutage is an end-to-end smoke that a brief consumer +// outage (shorter than the liveness grace period) does not stop the consumer, +// and that after recovery the consumer's consensus VP re-converges with the +// provider's updated VP. It does not assert the *mechanism* of re-convergence +// (the un-timed-out VSC packet could be resent rather than replaced by a +// snapshot); the snapshot path and the demoted timeout are covered by unit tests +// and by LivenessIntegrationTestSuite's forced-timeout snapshot test. func (s *IntegrationTestSuite) testLivenessTransientOutage() { - s.Run("liveness transient outage: consumer stays LAUNCHED and re-syncs valset", func() { + s.Run("liveness transient outage: consumer stays LAUNCHED and re-converges", func() { const consumerID = "0" // Record provider VP before the outage. @@ -147,8 +152,8 @@ func (s *IntegrationTestSuite) testLivenessTransientOutage() { s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, "consumer %s must remain LAUNCHED after a transient outage", consumerID) - // Consumer VP must re-converge to the updated provider VP via snapshot resync. - s.T().Log("waiting for consumer valset to re-converge via snapshot resync...") + // Consumer VP must re-converge to the updated provider VP after recovery. + s.T().Log("waiting for consumer valset to re-converge...") s.Require().Eventuallyf(func() bool { consumerVals, err := s.queryConsumerNetValidators() if err != nil || len(consumerVals) == 0 { @@ -161,7 +166,7 @@ func (s *IntegrationTestSuite) testLivenessTransientOutage() { s.T().Logf("consumer VP: %d (want %d)", consumerVPs[0], newProviderVP) return consumerVPs[0] == newProviderVP }, 3*time.Minute, 3*time.Second, - "consumer VP did not converge to provider VP %d after snapshot resync", newProviderVP) + "consumer VP did not re-converge to provider VP %d after the transient outage", newProviderVP) }) } diff --git a/x/vaas/consumer/keeper/relay.go b/x/vaas/consumer/keeper/relay.go index c2ce9be..836b021 100644 --- a/x/vaas/consumer/keeper/relay.go +++ b/x/vaas/consumer/keeper/relay.go @@ -1,6 +1,8 @@ package keeper import ( + "strconv" + "github.com/allinbits/vaas/x/vaas/consumer/types" vaastypes "github.com/allinbits/vaas/x/vaas/types" @@ -52,6 +54,17 @@ func (k Keeper) OnRecvVSCPacketV2(ctx sdk.Context, sourceClientID string, newCha var pendingChanges []abci.ValidatorUpdate if newChanges.IsSnapshot { pendingChanges = k.computeReplaceUpdates(ctx, newChanges.ValidatorUpdates) + // Surface snapshot resyncs (not ordinary diffs) so operators -- and the + // e2e -- can observe that a behind consumer was healed by a full-set + // replacement rather than an accumulated diff. + ctx.EventManager().EmitEvent( + sdk.NewEvent( + vaastypes.EventTypeSnapshotResync, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(vaastypes.AttributeValSetUpdateID, strconv.FormatUint(newChanges.ValsetUpdateId, 10)), + sdk.NewAttribute(vaastypes.AttributeNumValidators, strconv.Itoa(len(newChanges.ValidatorUpdates))), + ), + ) } else { currentValUpdates := []abci.ValidatorUpdate{} if currentChanges, exists := k.GetPendingChanges(ctx); exists { diff --git a/x/vaas/consumer/keeper/relay_test.go b/x/vaas/consumer/keeper/relay_test.go index 0cde138..acc3a00 100644 --- a/x/vaas/consumer/keeper/relay_test.go +++ b/x/vaas/consumer/keeper/relay_test.go @@ -397,6 +397,42 @@ func TestRecvPacketAfterStalenessLiftsStale(t *testing.T) { require.False(t, k.IsVSCStale(staleCtx), "resync must lift safe mode") } +// TestSnapshotResyncEmitsEvent verifies the consumer emits EventTypeSnapshotResync +// when (and only when) it applies a snapshot packet, so the resync is observable +// (used by the e2e to distinguish a snapshot from a resent diff). +func TestSnapshotResyncEmitsEvent(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pk := ed25519.GenPrivKey().PubKey() + tm, err := cryptocodec.ToCmtProtoPublicKey(pk) + require.NoError(t, err) + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{{PubKey: tm, Power: 10}}) + require.NoError(t, k.SetHighestValsetUpdateID(ctx, 1)) + k.SetProviderClientID(ctx, "07-tendermint-0") + + countSnapshotEvents := func() int { + n := 0 + for _, ev := range ctx.EventManager().Events() { + if ev.Type == types.EventTypeSnapshotResync { + n++ + } + } + return n + } + + // An ordinary diff packet must NOT emit the snapshot-resync event. + diff := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: tm, Power: 12}}, 2) + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", diff)) + require.Zero(t, countSnapshotEvents(), "a diff packet must not emit a snapshot-resync event") + + // A snapshot packet must emit exactly one. + snap := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: tm, Power: 15}}, 3) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + require.Equal(t, 1, countSnapshotEvents(), "a snapshot packet must emit exactly one snapshot-resync event") +} + // TestSnapshotPowerChange seeds the CC set with A=10 and delivers a snapshot // that changes A's power to 50. PendingChanges must contain A at power 50. func TestSnapshotPowerChange(t *testing.T) { diff --git a/x/vaas/types/events.go b/x/vaas/types/events.go index 34ef5a6..1b7cc99 100644 --- a/x/vaas/types/events.go +++ b/x/vaas/types/events.go @@ -11,6 +11,11 @@ const ( EventTypeSubmitConsumerDoubleVoting = "submit_consumer_double_voting" EventTypeExecuteConsumerChainSlash = "execute_consumer_chain_slash" EventTypeConsumerEvidenceRequest = "consumer_evidence_request" + // EventTypeSnapshotResync is emitted by the consumer when it applies a + // snapshot VSC packet (is_snapshot=true), i.e. it replaces its cross-chain + // validator set rather than accumulating a diff. Emitted only on snapshots, + // not on ordinary diffs. + EventTypeSnapshotResync = "vaas_snapshot_resync" AttributeKeyAckSuccess = "success" AttributeKeyAck = "acknowledgement" @@ -34,4 +39,5 @@ const ( AttributeValidatorAddress = "validator_address" AttributeInfractionType = "infraction_type" AttributeValSetUpdateID = "valset_update_id" + AttributeNumValidators = "num_validators" ) From bccc5a05e61c614ff1e0089b2b1b609a9070fa17 Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:58:58 +0200 Subject: [PATCH 18/20] add a forced-timeout snapshot-resync e2e test The main suite's transient-outage test cannot prove that the demoted OnTimeout no longer removes the consumer, nor that a behind consumer heals via a snapshot rather than a resent diff, because with a brief pause no packet ever times out. This adds testForcedTimeoutSnapshotResync to the short-timer suite. It sets a short provider vaas_timeout_period (20s, now allowed since the floor was dropped) and pauses the *relayer* -- not the consumer -- so the consumer keeps producing blocks and its clock advances past the timeout with packets undelivered. On unpause the relayer submits MsgTimeout (provider OnTimeout fires) and delivers a snapshot to the now-behind consumer. The test asserts all three: the consumer stays LAUNCHED, the provider logged a real timeout (so the LAUNCHED assertion is not vacuous), and the consumer logged a snapshot resync (healed via a snapshot, not a resent diff). The consumer now also logs "applied snapshot resync" alongside the EventTypeSnapshotResync event so the e2e can assert on it reliably (a container log grep, like the provider-timeout assertion) rather than via tx_search. --- tests/e2e/e2e_liveness_suite_test.go | 99 +++++++++++++++++++++++++++- x/vaas/consumer/keeper/relay.go | 7 +- 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/tests/e2e/e2e_liveness_suite_test.go b/tests/e2e/e2e_liveness_suite_test.go index 13bc509..110ce17 100644 --- a/tests/e2e/e2e_liveness_suite_test.go +++ b/tests/e2e/e2e_liveness_suite_test.go @@ -34,7 +34,14 @@ package e2e // mode; bank send rejected; relayer unpaused; accepted. // 3. testLivenessQuery - QueryConsumerLiveness via CLI; last_ack_time recent, // non-zero grace, removal_eta present. -// 4. testAutoSweepRemoval - relayer stopped indefinitely; poll until STOPPED. +// 4. testForcedTimeoutSnapshotResync - relayer paused > the short (20s) provider +// vaas_timeout while the consumer keeps producing +// blocks, so a VSC packet genuinely times out; the +// consumer stays LAUNCHED (demoted OnTimeout) and +// heals via a snapshot resync (asserted via the +// provider timeout log and the consumer's +// snapshot-resync event). +// 5. testAutoSweepRemoval - relayer stopped indefinitely; poll until STOPPED. // // The STOPPED -> DELETED edge (deletion at stop-time + unbonding) is unit-tested // (TestSweepRemovesStaleConsumer), not exercised here -- see the note above @@ -222,6 +229,7 @@ func (s *LivenessIntegrationTestSuite) TestLivenessVAAS() { s.testRecoverBeforeGrace() s.testRealSafeMode() s.testLivenessQuery() + s.testForcedTimeoutSnapshotResync() s.testAutoSweepRemoval() } @@ -327,6 +335,13 @@ func (s *LivenessIntegrationTestSuite) livenessInitAndStartProvider() { // consumer is never swept between acks. params["blocks_per_epoch"] = "1" params["fees_per_block_amount"] = "1000" + // Short VSC packet timeout so testForcedTimeoutSnapshotResync can + // make a packet actually time out within a CI run (the relayer is + // paused while the consumer keeps producing blocks past this + // deadline). Now possible since the MinVAASTimeoutPeriod floor was + // dropped. Timeouts are log-only, so this does not perturb the + // other liveness tests. + params["vaas_timeout_period"] = "20s" // Shrink the liveness grace fraction so the grace period // (unbonding * fraction = 200s * 0.75 = ~150s) is observable in // a CI run, while keeping the unbonding itself long enough that @@ -1231,6 +1246,88 @@ func (s *LivenessIntegrationTestSuite) testLivenessQuery() { }) } +// testForcedTimeoutSnapshotResync proves the two behaviours the main suite's +// transient-outage smoke cannot: that a genuinely timed-out VSC packet does NOT +// remove the consumer (the demoted OnTimeout), and that a consumer that fell +// behind heals via a snapshot resync (not a resent diff). +// +// A real IBC timeout requires the packet to expire on the *consumer's* clock +// while still undelivered. Pausing the consumer would freeze that clock, so +// instead the relayer is paused while the consumer keeps producing blocks: its +// clock advances past the short vaas_timeout_period (20s) with packets +// undelivered. On unpause the relayer submits MsgTimeout for the expired packets +// (the provider's OnTimeout fires, log-only) and delivers a snapshot to the +// now-behind consumer. +func (s *LivenessIntegrationTestSuite) testForcedTimeoutSnapshotResync() { + s.Run("forced timeout: demoted OnTimeout keeps consumer LAUNCHED; behind consumer heals via snapshot", func() { + const consumerID = "0" + + s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", s.livenessQueryConsumerPhase(consumerID), + "consumer %s must be LAUNCHED before the forced-timeout test", consumerID) + + // Pause the relayer; the consumer keeps producing blocks, so its clock + // advances past the 20s packet timeout while VSC packets go undelivered. + s.T().Log("pausing relayer for ~35s (> 20s vaas_timeout) to force VSC packet timeouts...") + s.Require().NoError(s.dkrPool.Client.PauseContainer(s.tsRelayerResource.Container.ID), + "failed to pause relayer container") + time.Sleep(35 * time.Second) + + s.T().Log("unpausing relayer; it submits MsgTimeout for expired packets and delivers a snapshot...") + s.Require().NoError(s.dkrPool.Client.UnpauseContainer(s.tsRelayerResource.Container.ID), + "failed to unpause relayer container") + + // (a) The demoted OnTimeout must not have removed the consumer. + s.Require().Eventuallyf(func() bool { + return s.livenessQueryConsumerPhase(consumerID) == "CONSUMER_PHASE_LAUNCHED" + }, 30*time.Second, 3*time.Second, + "consumer %s must remain LAUNCHED despite VSC packet timeouts", consumerID) + s.T().Log("consumer remained LAUNCHED through the timeouts") + + // (b) Prove a real timeout actually fired -- otherwise (a) is vacuous. + // The provider logs the demoted OnTimeout handler. + s.Require().Eventuallyf(func() bool { + return strings.Contains(s.livenessProviderLogs(), "packet timeout, retrying next epoch") + }, 30*time.Second, 3*time.Second, + "provider never logged a VSC packet timeout; the timeout path was not exercised") + s.T().Log("provider processed a demoted VSC timeout (OnTimeout is log-only)") + + // (c) Prove the behind consumer healed via a SNAPSHOT (not a resent diff): + // the consumer logs "applied snapshot resync" (and emits the matching + // event) only when it applies an is_snapshot packet. + s.Require().Eventuallyf(func() bool { + return strings.Contains(s.livenessConsumerLogs(), "applied snapshot resync") + }, 2*time.Minute, 5*time.Second, + "consumer never applied a snapshot resync after recovery") + s.T().Log("consumer applied a snapshot resync after recovery") + }) +} + +// livenessProviderLogs returns the provider container's full stdout+stderr log. +func (s *LivenessIntegrationTestSuite) livenessProviderLogs() string { + var buf bytes.Buffer + _ = s.dkrPool.Client.Logs(docker.LogsOptions{ + Container: s.providerValRes[0].Container.ID, + OutputStream: &buf, + ErrorStream: &buf, + Stdout: true, + Stderr: true, + }) + return buf.String() +} + +// livenessConsumerLogs returns the consumer container's full stdout+stderr log. +func (s *LivenessIntegrationTestSuite) livenessConsumerLogs() string { + var buf bytes.Buffer + _ = s.dkrPool.Client.Logs(docker.LogsOptions{ + Container: s.consumerValRes[0].Container.ID, + OutputStream: &buf, + ErrorStream: &buf, + Stdout: true, + Stderr: true, + }) + return buf.String() +} + // testAutoSweepRemoval stops the relayer (and pauses the consumer) so no VSC // acks return to the provider. After the liveness grace period (~150s) expires, // the provider's SweepUnresponsiveConsumers moves the consumer to diff --git a/x/vaas/consumer/keeper/relay.go b/x/vaas/consumer/keeper/relay.go index 836b021..9bdacd4 100644 --- a/x/vaas/consumer/keeper/relay.go +++ b/x/vaas/consumer/keeper/relay.go @@ -56,7 +56,8 @@ func (k Keeper) OnRecvVSCPacketV2(ctx sdk.Context, sourceClientID string, newCha pendingChanges = k.computeReplaceUpdates(ctx, newChanges.ValidatorUpdates) // Surface snapshot resyncs (not ordinary diffs) so operators -- and the // e2e -- can observe that a behind consumer was healed by a full-set - // replacement rather than an accumulated diff. + // replacement rather than an accumulated diff. Emitted both as an event + // (structured/queryable) and a log line (the e2e asserts on the log). ctx.EventManager().EmitEvent( sdk.NewEvent( vaastypes.EventTypeSnapshotResync, @@ -65,6 +66,10 @@ func (k Keeper) OnRecvVSCPacketV2(ctx sdk.Context, sourceClientID string, newCha sdk.NewAttribute(vaastypes.AttributeNumValidators, strconv.Itoa(len(newChanges.ValidatorUpdates))), ), ) + k.Logger(ctx).Info("applied snapshot resync", + "vscID", newChanges.ValsetUpdateId, + "numValidators", len(newChanges.ValidatorUpdates), + ) } else { currentValUpdates := []abci.ValidatorUpdate{} if currentChanges, exists := k.GetPendingChanges(ctx); exists { From 8d817fcaeb0022bf5a4ec72dcf8c650c5c577ac8 Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:30:52 +0200 Subject: [PATCH 19/20] extract a shared baseTestSuite for the two e2e suites IntegrationTestSuite and LivenessIntegrationTestSuite duplicated ~29 near- identical Docker/relayer/exec helpers that differed only in constants (chain ids, ports, network, container names) and the genesis/config-toml patches applied at bring-up. Introduce baseTestSuite (embedded by both) parameterized by a baseSuiteConfig: the differing constants become config fields and the differing mutations become hook funcs (patchProviderGenesis, patchProviderConfigToml, patchConsumerConfigToml). The shared bring-up, ts-relayer wiring, and exec/query helpers move to the base; the liveness* duplicates are deleted; each suite keeps only its config and its own test methods. Behaviour is unchanged (identical ports, ids, genesis values, timeouts, sequencing) -- both suites pass. Also rename the main suite's queryConsumerPhase to queryProviderConsumerPhase (it queries the provider) and host it on the base. --- tests/e2e/base_suite_test.go | 549 ++++++++++++ tests/e2e/e2e_consumer_liveness_test.go | 31 +- tests/e2e/e2e_debt_test.go | 12 +- tests/e2e/e2e_downtime_slash_test.go | 2 +- tests/e2e/e2e_exec_test.go | 4 +- tests/e2e/e2e_liveness_suite_test.go | 1074 +++-------------------- tests/e2e/e2e_setup_test.go | 440 +--------- tests/e2e/e2e_tsrelayer_test.go | 30 +- tests/e2e/genesis_test.go | 2 +- tests/e2e/gov_proposal_helpers_test.go | 14 +- tests/e2e/query_test.go | 23 + 11 files changed, 766 insertions(+), 1415 deletions(-) create mode 100644 tests/e2e/base_suite_test.go diff --git a/tests/e2e/base_suite_test.go b/tests/e2e/base_suite_test.go new file mode 100644 index 0000000..d228449 --- /dev/null +++ b/tests/e2e/base_suite_test.go @@ -0,0 +1,549 @@ +package e2e + +// base_suite_test.go holds the shared Docker-based e2e scaffolding used by both +// IntegrationTestSuite (the main suite) and LivenessIntegrationTestSuite. The +// two suites differ only in a handful of constants (chain IDs, Docker network, +// host ports, container names) and in the genesis / config mutations they apply +// at bring-up. Those differences are captured in baseSuiteConfig; everything +// else -- container lifecycle, chain init, ts-relayer wiring, exec helpers -- +// lives here on baseTestSuite and is embedded by each concrete suite. + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "github.com/stretchr/testify/suite" +) + +// baseSuiteConfig captures everything that differs between the main and +// liveness suites. A concrete suite fills this in during its SetupSuite before +// invoking the shared bring-up helpers. +type baseSuiteConfig struct { + providerChainID string + consumerChainID string + dockerNetwork string + + // Init-container names and the MkdirTemp prefix keep the two suites' + // transient containers and host dirs from colliding on a shared host. + providerInitName string + consumerInitName string + tmpDirPrefix string + + // Host port bindings for the provider chain container. + providerRPCPort string + providerGRPCPort string + providerRESTPort string + providerP2PPort string + + // Host port bindings for the consumer chain container. + consumerRPCPort string + consumerGRPCPort string + consumerRESTPort string + consumerP2PPort string + + // create-consumer template file (under testdata) and the chain-id + // placeholder to substitute in it. + consumerTemplateFile string + consumerTemplatePlaceholder string + + // patchProviderGenesis mutates the provider's genesis app_state before the + // chain container starts. + patchProviderGenesis func(appState map[string]any) + + // patchProviderConfigToml / patchConsumerConfigToml, when non-nil, mutate + // the respective config.toml before the chain container starts (the + // liveness suite uses this for fast blocks). Leaving them nil skips the + // step. + patchProviderConfigToml func(path string) + patchConsumerConfigToml func(path string) +} + +// baseTestSuite carries the shared Docker state and helper methods. Concrete +// suites embed it and supply their own cfg and SetupSuite. +type baseTestSuite struct { + suite.Suite + + cfg baseSuiteConfig + + tmpDirs []string + provider *chain + consumer *chain + dkrPool *dockertest.Pool + dkrNet *dockertest.Network + providerValRes []*dockertest.Resource + consumerValRes []*dockertest.Resource + tsRelayerResource *dockertest.Resource +} + +// TearDownSuite cleans up all Docker resources and temp directories. +func (s *baseTestSuite) TearDownSuite() { + s.T().Log("tearing down e2e suite...") + + if os.Getenv("VAAS_E2E_SKIP_CLEANUP") == "true" { + s.T().Log("skipping cleanup (VAAS_E2E_SKIP_CLEANUP=true)") + return + } + + s.stopTSRelayer() + + // Purge consumer validators + for _, r := range s.consumerValRes { + if err := s.dkrPool.Purge(r); err != nil { + s.T().Logf("failed to purge consumer container: %v", err) + } + } + + // Purge provider validators + for _, r := range s.providerValRes { + if err := s.dkrPool.Purge(r); err != nil { + s.T().Logf("failed to purge provider container: %v", err) + } + } + + // Remove network + if s.dkrNet != nil { + if err := s.dkrPool.RemoveNetwork(s.dkrNet); err != nil { + s.T().Logf("failed to remove network: %v", err) + } + } + + // Remove temp dirs + for _, dir := range s.tmpDirs { + _ = os.RemoveAll(dir) + } +} + +// cleanupStaleContainers removes containers from previous failed test runs. +func (s *baseTestSuite) cleanupStaleContainers() { + staleNames := []string{ + s.cfg.providerInitName, + s.cfg.consumerInitName, + fmt.Sprintf("%s-val0", s.cfg.providerChainID), + fmt.Sprintf("%s-val0", s.cfg.consumerChainID), + fmt.Sprintf("%s-%s-ts-relayer", s.cfg.providerChainID, s.cfg.consumerChainID), + } + for _, name := range staleNames { + c, err := s.dkrPool.Client.InspectContainer(name) + if err != nil { + continue // container doesn't exist + } + s.T().Logf("removing stale container: %s", name) + _ = s.dkrPool.Client.RemoveContainer(docker.RemoveContainerOptions{ + ID: c.ID, + Force: true, + RemoveVolumes: true, + }) + } + // Also remove stale network + _ = s.dkrPool.Client.RemoveNetwork(s.cfg.dockerNetwork) +} + +// runInitContainer starts an init container with the given script mounted, +// waits for it to exit, checks the exit code, and purges it. +func (s *baseTestSuite) runInitContainer(name, scriptPath, containerScriptPath, dataDir, homePath string, env []string) { + initResource, err := s.dkrPool.RunWithOptions( + &dockertest.RunOptions{ + Name: name, + Repository: e2eChainImage, + NetworkID: s.dkrNet.Network.ID, + User: "nonroot", + Env: env, + Mounts: []string{ + fmt.Sprintf("%s:%s", dataDir, homePath), + fmt.Sprintf("%s:%s", scriptPath, containerScriptPath), + }, + Entrypoint: []string{"sh", containerScriptPath}, + }, + func(config *docker.HostConfig) { + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }, + ) + s.Require().NoError(err, "failed to start %s container", name) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + exitCode, err := s.dkrPool.Client.WaitContainerWithContext(initResource.Container.ID, ctx) + s.Require().NoError(err, "%s container wait failed", name) + + var logBuf bytes.Buffer + _ = s.dkrPool.Client.Logs(docker.LogsOptions{ + Container: initResource.Container.ID, + OutputStream: &logBuf, + ErrorStream: &logBuf, + Stdout: true, + Stderr: true, + }) + s.Require().Equal(0, exitCode, "%s container exited with code %d\noutput:\n%s", name, exitCode, logBuf.String()) + + s.Require().NoError(s.dkrPool.Purge(initResource), "failed to purge %s container", name) +} + +// initAndStartProvider initializes the provider chain using a temporary Docker +// container that runs provider-init.sh, then starts the actual chain container. +func (s *baseTestSuite) initAndStartProvider() { + // Create host directory for provider data + providerDir, err := os.MkdirTemp("", s.cfg.tmpDirPrefix+"provider-") + s.Require().NoError(err) + s.tmpDirs = append(s.tmpDirs, providerDir) + s.provider.dataDir = providerDir + + // Make writable + s.Require().NoError(os.Chmod(providerDir, 0o777)) + + // Run init script in a temporary container + scriptPath := filepath.Join(testDir(), "scripts", "provider-init.sh") + s.runInitContainer(s.cfg.providerInitName, scriptPath, "/scripts/provider-init.sh", providerDir, providerHomePath, []string{ + "BINARY=" + providerBinary, + "HOME_DIR=" + providerHomePath, + "CHAIN_ID=" + s.cfg.providerChainID, + "DENOM=" + bondDenom, + "MNEMONIC=" + relayerMnemonic, + }) + + // Apply the suite-specific genesis mutations. + genesisFile := filepath.Join(providerDir, "config", "genesis.json") + s.patchGenesisJSON(genesisFile, func(genesis map[string]any) { + appState := genesis["app_state"].(map[string]any) + s.cfg.patchProviderGenesis(appState) + }) + + // Apply the suite-specific config.toml mutations (e.g. fast blocks). + if s.cfg.patchProviderConfigToml != nil { + s.cfg.patchProviderConfigToml(filepath.Join(providerDir, "config", "config.toml")) + } + + // Now start the actual provider container + s.T().Log("starting provider chain container...") + + resource, err := s.dkrPool.RunWithOptions( + &dockertest.RunOptions{ + Name: fmt.Sprintf("%s-val0", s.cfg.providerChainID), + Repository: e2eChainImage, + NetworkID: s.dkrNet.Network.ID, + Mounts: []string{ + fmt.Sprintf("%s:%s", providerDir, providerHomePath), + }, + PortBindings: map[docker.Port][]docker.PortBinding{ + "26657/tcp": {{HostIP: "", HostPort: s.cfg.providerRPCPort}}, + "9090/tcp": {{HostIP: "", HostPort: s.cfg.providerGRPCPort}}, + "1317/tcp": {{HostIP: "", HostPort: s.cfg.providerRESTPort}}, + "26656/tcp": {{HostIP: "", HostPort: s.cfg.providerP2PPort}}, + }, + Cmd: []string{ + providerBinary, "start", + "--home", providerHomePath, + }, + }, + func(config *docker.HostConfig) { + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }, + ) + s.Require().NoError(err, "failed to start provider container") + + s.providerValRes = append(s.providerValRes, resource) + s.T().Logf("provider container started: %s", resource.Container.ID[:12]) + + // Wait for provider to produce blocks + waitCtx, waitCancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer waitCancel() + err = s.waitForChainHeight(waitCtx, "http://localhost:"+s.cfg.providerRPCPort, 3) + s.Require().NoError(err, "provider failed to produce blocks") + s.T().Log("provider chain is producing blocks") +} + +// registerConsumerOnProvider creates a consumer chain registration on the provider. +func (s *baseTestSuite) registerConsumerOnProvider() { + // Read consumer JSON template and substitute chain ID + templatePath := filepath.Join(testDir(), "testdata", s.cfg.consumerTemplateFile) + templateBytes, err := os.ReadFile(templatePath) + s.Require().NoError(err, "failed to read %s template", s.cfg.consumerTemplateFile) + + createConsumerJSON := strings.ReplaceAll(string(templateBytes), s.cfg.consumerTemplatePlaceholder, s.cfg.consumerChainID) + + // Write JSON to container and execute tx + s.dockerExecMust(s.providerValRes[0].Container.ID, []string{ + "sh", "-c", fmt.Sprintf("echo '%s' > /tmp/create_consumer.json", createConsumerJSON), + }) + + stdout, stderr, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "tx", "provider", "create-consumer", "/tmp/create_consumer.json", + "--from", "val", + "--home", providerHomePath, + "--keyring-backend", "test", + "--chain-id", s.cfg.providerChainID, + "--gas", "auto", + "--gas-adjustment", "1.5", + "--fees", "10000" + bondDenom, + "--broadcast-mode", "sync", + "-y", + "-o", "json", + }) + s.Require().NoErrorf(err, "failed to run create-consumer: stderr=%s", stderr.String()) + + // Assert the tx was accepted. Without this, a tx rejected at CheckTx (e.g. + // an init-parameter validation failure) passes silently here and only + // surfaces later as an empty consumer genesis. + var res struct { + Code int `json:"code"` + RawLog string `json:"raw_log"` + } + s.Require().NoErrorf(json.Unmarshal(stdout.Bytes(), &res), + "failed to decode create-consumer response:\nstdout=%q\nstderr=%q", stdout.String(), stderr.String()) + s.Require().Equalf(0, res.Code, "create-consumer tx failed: %s", res.RawLog) + + // Wait for the tx to be included + time.Sleep(10 * time.Second) +} + +// fetchConsumerGenesis queries the provider for the consumer genesis data. +func (s *baseTestSuite) fetchConsumerGenesis() []byte { + var output string + var lastErr error + + // Retry fetching consumer genesis (it may take a few blocks) + for range 30 { + stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "provider", "consumer-genesis", "0", + "--home", providerHomePath, + "--output", "json", + }) + if err == nil { + output = stdout.String() + if output != "" && !strings.Contains(output, "not found") && !strings.Contains(output, "Error") { + break + } + } + lastErr = err + time.Sleep(3 * time.Second) + } + s.Require().NotEmpty(output, "consumer genesis is empty, last error: %v", lastErr) + + s.T().Logf("fetched consumer genesis (%d bytes)", len(output)) + return []byte(output) +} + +// initAndStartConsumer initializes the consumer chain and starts it. +func (s *baseTestSuite) initAndStartConsumer(consumerGenesisJSON []byte) { + // Create host directory for consumer data + consumerDir, err := os.MkdirTemp("", s.cfg.tmpDirPrefix+"consumer-") + s.Require().NoError(err) + s.tmpDirs = append(s.tmpDirs, consumerDir) + s.consumer.dataDir = consumerDir + + s.Require().NoError(os.Chmod(consumerDir, 0o777)) + + // Run init script in a temporary container + scriptPath := filepath.Join(testDir(), "scripts", "consumer-init.sh") + s.runInitContainer(s.cfg.consumerInitName, scriptPath, "/scripts/consumer-init.sh", consumerDir, consumerHomePath, []string{ + "BINARY=" + consumerBinary, + "HOME_DIR=" + consumerHomePath, + "CHAIN_ID=" + s.cfg.consumerChainID, + "DENOM=" + bondDenom, + "MNEMONIC=" + relayerMnemonic, + }) + + // Patch consumer genesis with provider's consumer-genesis data on the host + genesisFile := filepath.Join(consumerDir, "config", "genesis.json") + err = patchConsumerGenesisWithProviderData(genesisFile, consumerGenesisJSON) + s.Require().NoError(err, "failed to patch consumer genesis") + + // Patch consumer slashing params for aggressive downtime detection + s.patchConsumerSlashingParams() + + // Apply the suite-specific config.toml mutations (e.g. fast blocks). + if s.cfg.patchConsumerConfigToml != nil { + s.cfg.patchConsumerConfigToml(filepath.Join(consumerDir, "config", "config.toml")) + } + + // Copy validator keys from provider to consumer + providerDir := s.provider.dataDir + err = copyFile( + filepath.Join(providerDir, "config", "priv_validator_key.json"), + filepath.Join(consumerDir, "config", "priv_validator_key.json"), + ) + s.Require().NoError(err, "failed to copy priv_validator_key.json") + + err = copyFile( + filepath.Join(providerDir, "config", "node_key.json"), + filepath.Join(consumerDir, "config", "node_key.json"), + ) + s.Require().NoError(err, "failed to copy node_key.json") + + // Start the actual consumer container + s.T().Log("starting consumer chain container...") + + resource, err := s.dkrPool.RunWithOptions( + &dockertest.RunOptions{ + Name: fmt.Sprintf("%s-val0", s.cfg.consumerChainID), + Repository: e2eChainImage, + NetworkID: s.dkrNet.Network.ID, + Mounts: []string{ + fmt.Sprintf("%s:%s", consumerDir, consumerHomePath), + }, + PortBindings: map[docker.Port][]docker.PortBinding{ + "26657/tcp": {{HostIP: "", HostPort: s.cfg.consumerRPCPort}}, + "9090/tcp": {{HostIP: "", HostPort: s.cfg.consumerGRPCPort}}, + "1317/tcp": {{HostIP: "", HostPort: s.cfg.consumerRESTPort}}, + "26656/tcp": {{HostIP: "", HostPort: s.cfg.consumerP2PPort}}, + }, + Cmd: []string{ + consumerBinary, "start", + "--home", consumerHomePath, + }, + }, + func(config *docker.HostConfig) { + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }, + ) + s.Require().NoError(err, "failed to start consumer container") + + s.consumerValRes = append(s.consumerValRes, resource) + s.T().Logf("consumer container started: %s", resource.Container.ID[:12]) + + // Wait for consumer to produce blocks + waitCtx, waitCancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer waitCancel() + err = s.waitForChainHeight(waitCtx, "http://localhost:"+s.cfg.consumerRPCPort, 3) + s.Require().NoError(err, "consumer failed to produce blocks") + s.T().Log("consumer chain is producing blocks") +} + +// setupTSRelayer starts the ts-relayer container, configures it with +// mnemonics and gas prices for both chains, and creates an IBC v2 path. +// The ts-relayer handles counterparty registration and packet relaying. +func (s *baseTestSuite) setupTSRelayer() { + s.startTSRelayer() + + s.tsRelayerAddMnemonic(s.cfg.providerChainID, relayerMnemonic) + s.tsRelayerAddMnemonic(s.cfg.consumerChainID, relayerMnemonic) + s.tsRelayerAddGasPrice(s.cfg.providerChainID, "0.025"+bondDenom) + s.tsRelayerAddGasPrice(s.cfg.consumerChainID, "0.025"+bondDenom) + + s.tsRelayerAddPath(IBCv2) + + s.tsRelayerDumpPaths() + s.startTSRelayerRelay() + s.T().Log("ts-relayer IBC v2 path configured") +} + +// waitForChainHeight polls a CometBFT RPC endpoint until the chain reaches +// the given block height. +func (s *baseTestSuite) waitForChainHeight(ctx context.Context, rpcEndpoint string, minHeight int64) error { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timeout waiting for chain at %s to reach height %d", rpcEndpoint, minHeight) + case <-ticker.C: + height, err := queryBlockHeight(rpcEndpoint) + if err != nil { + continue + } + if height >= minHeight { + return nil + } + } + } +} + +// waitForConsumerSync blocks until the provider has recorded a VSC ack from the +// consumer that is strictly newer than the launch seed, i.e. the relayer is +// actually delivering VSC packets and the consumer is acking them. +// +// This gate is what makes the timing-sensitive liveness tests deterministic: +// the provider seeds lastAck at launch and starts the grace clock then, but the +// relayer needs time (add-path, then the first relay cycle) before any VSC is +// delivered. Without this gate a test could assert against a consumer that has +// launched but never synced. The grace (~150s) is sized to exceed this +// first-sync latency, so the consumer is not swept while we wait here. +func (s *baseTestSuite) waitForConsumerSync(consumerID string) { + livenessURL := fmt.Sprintf("http://localhost:%s/vaas/provider/consumer_liveness/%s", + s.cfg.providerRESTPort, consumerID) + + var first string + s.Require().Eventuallyf(func() bool { + body, err := httpGet(livenessURL) + if err != nil { + return false + } + var res struct { + LastAckTime string `json:"last_ack_time"` + } + if json.Unmarshal(body, &res) != nil || res.LastAckTime == "" { + return false + } + // Require two distinct lastAck readings: the first records a baseline, + // the second proves lastAck is advancing (acks are flowing), not merely + // sitting at the launch seed. + if first == "" { + first = res.LastAckTime + s.T().Logf("consumer sync: baseline last_ack=%s", first) + return false + } + if res.LastAckTime != first { + s.T().Logf("consumer sync confirmed: last_ack advanced %s -> %s", first, res.LastAckTime) + return true + } + return false + }, 150*time.Second, 3*time.Second, + "consumer %s never synced a VSC ack (lastAck did not advance past the launch seed)", consumerID) +} + +// patchConfigToml lowers the CometBFT consensus timeouts so the chain produces +// blocks roughly every second instead of the ~5s default. Fast blocks matter +// for the liveness suite because every time-bounded step -- relayer add-path +// (which waits for client header heights), epoch/VSC cadence, and the VSC +// recv/ack round-trip -- is measured against the liveness grace. At the default +// block time these add up to minutes and the grace (which starts ticking at +// consumer launch) can expire before the consumer ever syncs. +func (s *baseTestSuite) patchConfigToml(path string) { + bz, err := os.ReadFile(path) + s.Require().NoError(err, "failed to read config.toml") + out := string(bz) + for from, to := range map[string]string{ + `timeout_propose = "3s"`: `timeout_propose = "500ms"`, + `timeout_commit = "5s"`: `timeout_commit = "1s"`, + `timeout_propose_delta = "500ms"`: `timeout_propose_delta = "200ms"`, + } { + s.Require().Containsf(out, from, "config.toml missing %q (default may have changed)", from) + out = strings.ReplaceAll(out, from, to) + } + s.Require().NoError(os.WriteFile(path, []byte(out), 0o600), "failed to write config.toml") +} + +// providerLogs returns the provider container's full stdout+stderr log. +func (s *baseTestSuite) providerLogs() string { + var buf bytes.Buffer + _ = s.dkrPool.Client.Logs(docker.LogsOptions{ + Container: s.providerValRes[0].Container.ID, + OutputStream: &buf, + ErrorStream: &buf, + Stdout: true, + Stderr: true, + }) + return buf.String() +} + +// consumerLogs returns the consumer container's full stdout+stderr log. +func (s *baseTestSuite) consumerLogs() string { + var buf bytes.Buffer + _ = s.dkrPool.Client.Logs(docker.LogsOptions{ + Container: s.consumerValRes[0].Container.ID, + OutputStream: &buf, + ErrorStream: &buf, + Stdout: true, + Stderr: true, + }) + return buf.String() +} diff --git a/tests/e2e/e2e_consumer_liveness_test.go b/tests/e2e/e2e_consumer_liveness_test.go index c3e48ce..5882fdb 100644 --- a/tests/e2e/e2e_consumer_liveness_test.go +++ b/tests/e2e/e2e_consumer_liveness_test.go @@ -43,34 +43,11 @@ package e2e // testLivenessRemoval runs second-to-last, before testGenesisRoundTrip. import ( - "encoding/json" "fmt" "strings" "time" ) -// queryConsumerPhase returns the phase string for a given consumer ID from the -// provider chain (e.g. "CONSUMER_PHASE_LAUNCHED"). -func (s *IntegrationTestSuite) queryConsumerPhase(consumerID string) string { - stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ - providerBinary, "query", "provider", "consumer-chain", consumerID, - "--home", providerHomePath, - "--output", "json", - }) - if err != nil { - s.T().Logf("queryConsumerPhase(%s): exec error: %v", consumerID, err) - return "" - } - var res struct { - Phase string `json:"phase"` - } - if err := json.Unmarshal(stdout.Bytes(), &res); err != nil { - s.T().Logf("queryConsumerPhase(%s): decode error: %v (raw: %s)", consumerID, err, stdout.String()) - return "" - } - return res.Phase -} - // testLivenessTransientOutage is an end-to-end smoke that a brief consumer // outage (shorter than the liveness grace period) does not stop the consumer, // and that after recovery the consumer's consensus VP re-converges with the @@ -148,7 +125,7 @@ func (s *IntegrationTestSuite) testLivenessTransientOutage() { s.Require().NoError(err, "failed to unpause consumer container") // The consumer must still be LAUNCHED after the transient outage. - phase := s.queryConsumerPhase(consumerID) + phase := s.queryProviderConsumerPhase(consumerID) s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, "consumer %s must remain LAUNCHED after a transient outage", consumerID) @@ -192,7 +169,7 @@ func (s *IntegrationTestSuite) testLivenessRemoval() { const consumerID = "0" // Precondition: consumer must still be LAUNCHED. - phase := s.queryConsumerPhase(consumerID) + phase := s.queryProviderConsumerPhase(consumerID) s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, "consumer %s must be LAUNCHED before the liveness removal test", consumerID) @@ -216,14 +193,14 @@ func (s *IntegrationTestSuite) testLivenessRemoval() { // Wait for the provider to reflect the STOPPED phase. s.T().Log("waiting for provider to move consumer to STOPPED or DELETED...") s.Require().Eventuallyf(func() bool { - p := s.queryConsumerPhase(consumerID) + p := s.queryProviderConsumerPhase(consumerID) s.T().Logf("consumer %s phase: %s", consumerID, p) return p == "CONSUMER_PHASE_STOPPED" || p == "CONSUMER_PHASE_DELETED" }, 2*time.Minute, 5*time.Second, "provider did not transition consumer %s to STOPPED/DELETED after remove-consumer", consumerID) - finalPhase := s.queryConsumerPhase(consumerID) + finalPhase := s.queryProviderConsumerPhase(consumerID) s.T().Logf("consumer %s terminal phase: %s", consumerID, finalPhase) s.Require().True( finalPhase == "CONSUMER_PHASE_STOPPED" || finalPhase == "CONSUMER_PHASE_DELETED", diff --git a/tests/e2e/e2e_debt_test.go b/tests/e2e/e2e_debt_test.go index e1396b7..0c20a98 100644 --- a/tests/e2e/e2e_debt_test.go +++ b/tests/e2e/e2e_debt_test.go @@ -29,7 +29,7 @@ func (s *IntegrationTestSuite) queryConsumerFeePoolAddress(consumerID string) st // consumerUserBech32 returns the bech32 address of the consumer's "user" // account. It queries the key ring inside the consumer container. -func (s *IntegrationTestSuite) consumerUserBech32() string { +func (s *baseTestSuite) consumerUserBech32() string { stdout, _, err := s.dockerExec(s.consumerValRes[0].Container.ID, []string{ consumerBinary, "keys", "show", "user", "-a", "--home", consumerHomePath, @@ -43,13 +43,13 @@ func (s *IntegrationTestSuite) consumerUserBech32() string { // consumer in simulate mode. The simulation traverses the ante chain, so the // debt gate fires here just like it would for a real broadcast. Returns the // stderr output so callers can inspect rejection reasons. -func (s *IntegrationTestSuite) consumerBankSendDryRun() (string, error) { +func (s *baseTestSuite) consumerBankSendDryRun() (string, error) { user := s.consumerUserBech32() _, stderr, err := s.dockerExec(s.consumerValRes[0].Container.ID, []string{ consumerBinary, "tx", "bank", "send", user, user, "1" + bondDenom, "--home", consumerHomePath, "--keyring-backend", "test", - "--chain-id", consumerChainID, + "--chain-id", s.cfg.consumerChainID, "--fees", "1000" + bondDenom, "--dry-run", "-y", @@ -84,14 +84,14 @@ func (s *IntegrationTestSuite) providerFundAddress(addr, amount string) { // providerFundConsumerFeePool deposits `amount` into the named consumer's // fee pool via MsgFundConsumerFeePool, signed by val. -func (s *IntegrationTestSuite) providerFundConsumerFeePool(consumerID, amount string) { +func (s *baseTestSuite) providerFundConsumerFeePool(consumerID, amount string) { stdout, stderr, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ providerBinary, "tx", "provider", "fund-consumer-fee-pool", consumerID, amount, "--from", "val", "--home", providerHomePath, "--keyring-backend", "test", - "--chain-id", providerChainID, + "--chain-id", s.cfg.providerChainID, "--fees", "10000" + bondDenom, "-y", "-o", "json", @@ -107,7 +107,7 @@ func (s *IntegrationTestSuite) providerFundConsumerFeePool(consumerID, amount st // This surfaces handler-level rejections (e.g. the min-deposit floor) right at // the call site with the on-chain raw_log, instead of as a distant downstream // timeout. -func (s *IntegrationTestSuite) requireTxCommitted(broadcastOut []byte) { +func (s *baseTestSuite) requireTxCommitted(broadcastOut []byte) { var bres struct { TxHash string `json:"txhash"` Code int `json:"code"` diff --git a/tests/e2e/e2e_downtime_slash_test.go b/tests/e2e/e2e_downtime_slash_test.go index 4e251b2..69a5de1 100644 --- a/tests/e2e/e2e_downtime_slash_test.go +++ b/tests/e2e/e2e_downtime_slash_test.go @@ -51,7 +51,7 @@ func (s *IntegrationTestSuite) testDowntimeSlash() { // consumer chain and verify the provider slashes and jails that validator. } -func (s *IntegrationTestSuite) patchConsumerSlashingParams() { +func (s *baseTestSuite) patchConsumerSlashingParams() { s.patchGenesisJSON(s.consumer.dataDir+"/config/genesis.json", func(genesis map[string]any) { appState, ok := genesis["app_state"].(map[string]any) if !ok { diff --git a/tests/e2e/e2e_exec_test.go b/tests/e2e/e2e_exec_test.go index acf6f42..14db4a9 100644 --- a/tests/e2e/e2e_exec_test.go +++ b/tests/e2e/e2e_exec_test.go @@ -10,7 +10,7 @@ import ( ) // dockerExec runs a command in the specified Docker container and returns stdout/stderr. -func (s *IntegrationTestSuite) dockerExec(containerID string, cmd []string) (bytes.Buffer, bytes.Buffer, error) { +func (s *baseTestSuite) dockerExec(containerID string, cmd []string) (bytes.Buffer, bytes.Buffer, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -42,7 +42,7 @@ func (s *IntegrationTestSuite) dockerExec(containerID string, cmd []string) (byt } // dockerExecMust runs a command in a Docker container, failing the test on error. -func (s *IntegrationTestSuite) dockerExecMust(containerID string, cmd []string) { +func (s *baseTestSuite) dockerExecMust(containerID string, cmd []string) { stdout, stderr, err := s.dockerExec(containerID, cmd) if err != nil { s.T().Logf("cmd: %v", cmd) diff --git a/tests/e2e/e2e_liveness_suite_test.go b/tests/e2e/e2e_liveness_suite_test.go index 110ce17..b8a1d41 100644 --- a/tests/e2e/e2e_liveness_suite_test.go +++ b/tests/e2e/e2e_liveness_suite_test.go @@ -10,16 +10,16 @@ package e2e // trusting period and the relayer could never establish the clients. // // Two infrastructure measures keep the timing-sensitive assertions reliable: -// - Fast blocks (livenessPatchConfigToml lowers the CometBFT timeouts to -// ~1s blocks). The provider seeds lastAck at consumer launch and starts the -// grace clock then, so every setup step that precedes the first VSC sync -- -// relayer add-path, the first relay cycle, the recv/ack round-trip -- must -// finish inside the grace. At the default ~5s block time these add up to -// minutes and the consumer is swept before it ever syncs. -// - A first-sync gate (livenessWaitForConsumerSync) that blocks setup until -// the provider's lastAck has actually advanced past the launch seed, i.e. -// the relayer is delivering and the consumer is acking. Assertions then run -// from a known-synced state, independent of relayer startup jitter. +// - Fast blocks (patchConfigToml lowers the CometBFT timeouts to ~1s blocks). +// The provider seeds lastAck at consumer launch and starts the grace clock +// then, so every setup step that precedes the first VSC sync -- relayer +// add-path, the first relay cycle, the recv/ack round-trip -- must finish +// inside the grace. At the default ~5s block time these add up to minutes +// and the consumer is swept before it ever syncs. +// - A first-sync gate (waitForConsumerSync) that blocks setup until the +// provider's lastAck has actually advanced past the launch seed, i.e. the +// relayer is delivering and the consumer is acking. Assertions then run from +// a known-synced state, independent of relayer startup jitter. // // The existing IntegrationTestSuite uses a ~21d provider unbonding, making the // liveness sweep untestable in real time. This suite launches its own isolated @@ -48,20 +48,13 @@ package e2e // testAutoSweepRemoval's trailing comment. import ( - "bytes" - "context" - "encoding/base64" "encoding/json" "fmt" - "os" - "path/filepath" - "strconv" "strings" "testing" "time" "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/suite" ) @@ -90,16 +83,7 @@ const ( // - consumer is registered via create_consumer_short_unbonding.json // (200s unbonding, 10m vaas_timeout, 5s safe_mode_threshold) type LivenessIntegrationTestSuite struct { - suite.Suite - - tmpDirs []string - provider *chain - consumer *chain - dkrPool *dockertest.Pool - dkrNet *dockertest.Network - providerValRes []*dockertest.Resource - consumerValRes []*dockertest.Resource - tsRelayerResource *dockertest.Resource + baseTestSuite } // TestLivenessIntegrationTestSuite is the entry point for the short-unbonding @@ -112,117 +96,114 @@ func TestLivenessIntegrationTestSuite(t *testing.T) { func (s *LivenessIntegrationTestSuite) SetupSuite() { s.T().Log("setting up liveness e2e suite (short unbonding)...") + s.cfg = baseSuiteConfig{ + providerChainID: livenessProviderChainID, + consumerChainID: livenessConsumerChainID, + dockerNetwork: livenessDockerNetwork, + providerInitName: "liveness-provider-init", + consumerInitName: "liveness-consumer-init", + tmpDirPrefix: "vaas-liveness-", + providerRPCPort: livenessProviderRPCPort, + providerGRPCPort: livenessProviderGRPCPort, + providerRESTPort: livenessProviderRESTPort, + providerP2PPort: livenessProviderP2PPort, + consumerRPCPort: livenessConsumerRPCPort, + consumerGRPCPort: livenessConsumerGRPCPort, + consumerRESTPort: livenessConsumerRESTPort, + consumerP2PPort: livenessConsumerP2PPort, + + consumerTemplateFile: "create_consumer_short_unbonding.json", + consumerTemplatePlaceholder: "CONSUMER_SHORT_CHAIN_ID", + + patchProviderGenesis: func(appState map[string]any) { + if gov, ok := appState["gov"].(map[string]any); ok { + if params, ok := gov["params"].(map[string]any); ok { + params["voting_period"] = "15s" + } + } + + if provider, ok := appState["provider"].(map[string]any); ok { + if params, ok := provider["params"].(map[string]any); ok { + // One block per epoch so a VSC packet (and its ack) flows every + // block. Combined with the fast block time (see patchConfigToml) + // this refreshes the provider's lastAck every few seconds, well + // inside the grace, so a healthy consumer is never swept between + // acks. + params["blocks_per_epoch"] = "1" + params["fees_per_block_amount"] = "1000" + // Short VSC packet timeout so testForcedTimeoutSnapshotResync can + // make a packet actually time out within a CI run (the relayer is + // paused while the consumer keeps producing blocks past this + // deadline). Now possible since the MinVAASTimeoutPeriod floor was + // dropped. Timeouts are log-only, so this does not perturb the + // other liveness tests. + params["vaas_timeout_period"] = "20s" + // Shrink the liveness grace fraction so the grace period + // (unbonding * fraction = 200s * 0.75 = ~150s) is observable in + // a CI run, while keeping the unbonding itself long enough that + // the relayer-derived IBC client trusting period (unbonding * + // 0.66 = ~132s) survives the relayer add-path window. + // + // The grace must exceed the time from consumer launch to first + // VSC sync (the provider seeds lastAck at launch, so the clock + // starts before the relayer has delivered anything). With fast + // blocks that first sync is ~60-90s; 150s leaves margin. The + // suite also waits for that first sync explicitly before + // asserting (see waitForConsumerSync). + params["liveness_grace_fraction"] = "0.75" + } + } + + // Keep unbonding long enough for a viable IBC client trusting period + // (see liveness_grace_fraction note above); the short grace comes from + // the fraction, not from a short unbonding. + if staking, ok := appState["staking"].(map[string]any); ok { + if params, ok := staking["params"].(map[string]any); ok { + params["unbonding_time"] = "200s" + } + } + }, + + // Fast blocks so add-path, epochs, and VSC delivery all complete well + // inside the grace (see patchConfigToml). + patchProviderConfigToml: s.patchConfigToml, + patchConsumerConfigToml: s.patchConfigToml, + } + var err error s.dkrPool, err = dockertest.NewPool("") s.Require().NoError(err, "failed to create docker pool") s.dkrPool.MaxWait = 5 * time.Minute - s.livenessCleanupStaleContainers() + s.cleanupStaleContainers() s.dkrNet, err = s.dkrPool.CreateNetwork(livenessDockerNetwork) s.Require().NoError(err, "failed to create liveness docker network") s.T().Log("step 1: initializing liveness provider chain...") - s.provider = &chain{id: livenessProviderChainID} - s.livenessInitAndStartProvider() + s.provider = &chain{id: s.cfg.providerChainID} + s.initAndStartProvider() s.T().Log("step 2: registering consumer on liveness provider...") - s.livenessRegisterConsumerOnProvider() + s.registerConsumerOnProvider() s.T().Log("step 3: fetching consumer genesis from liveness provider...") - consumerGenesisJSON := s.livenessFetchConsumerGenesis() + consumerGenesisJSON := s.fetchConsumerGenesis() s.T().Log("step 4: initializing liveness consumer chain...") - s.consumer = &chain{id: livenessConsumerChainID} - s.livenessInitAndStartConsumer(consumerGenesisJSON) + s.consumer = &chain{id: s.cfg.consumerChainID} + s.initAndStartConsumer(consumerGenesisJSON) s.T().Log("step 5: starting ts-relayer for liveness suite...") - s.livenessSetupTSRelayer() + s.setupTSRelayer() s.T().Log("step 6: waiting for the consumer to sync its first VSC...") - s.livenessWaitForConsumerSync("0") + s.waitForConsumerSync("0") s.T().Log("liveness e2e suite setup complete!") } -// livenessWaitForConsumerSync blocks until the provider has recorded a VSC ack -// from the consumer that is strictly newer than the launch seed, i.e. the -// relayer is actually delivering VSC packets and the consumer is acking them. -// -// This gate is what makes the timing-sensitive tests deterministic: the -// provider seeds lastAck at launch and starts the grace clock then, but the -// relayer needs time (add-path, then the first relay cycle) before any VSC is -// delivered. Without this gate a test could assert against a consumer that has -// launched but never synced. The grace (~150s) is sized to exceed this -// first-sync latency, so the consumer is not swept while we wait here. -func (s *LivenessIntegrationTestSuite) livenessWaitForConsumerSync(consumerID string) { - livenessURL := fmt.Sprintf("http://localhost:%s/vaas/provider/consumer_liveness/%s", - livenessProviderRESTPort, consumerID) - - var first string - s.Require().Eventuallyf(func() bool { - body, err := httpGet(livenessURL) - if err != nil { - return false - } - var res struct { - LastAckTime string `json:"last_ack_time"` - } - if json.Unmarshal(body, &res) != nil || res.LastAckTime == "" { - return false - } - // Require two distinct lastAck readings: the first records a baseline, - // the second proves lastAck is advancing (acks are flowing), not merely - // sitting at the launch seed. - if first == "" { - first = res.LastAckTime - s.T().Logf("consumer sync: baseline last_ack=%s", first) - return false - } - if res.LastAckTime != first { - s.T().Logf("consumer sync confirmed: last_ack advanced %s -> %s", first, res.LastAckTime) - return true - } - return false - }, 150*time.Second, 3*time.Second, - "consumer %s never synced a VSC ack (lastAck did not advance past the launch seed)", consumerID) -} - -// TearDownSuite cleans up all Docker resources. -func (s *LivenessIntegrationTestSuite) TearDownSuite() { - s.T().Log("tearing down liveness e2e suite...") - - if os.Getenv("VAAS_E2E_SKIP_CLEANUP") == "true" { - s.T().Log("skipping cleanup (VAAS_E2E_SKIP_CLEANUP=true)") - return - } - - s.livenessStopTSRelayer() - - for _, r := range s.consumerValRes { - if err := s.dkrPool.Purge(r); err != nil { - s.T().Logf("failed to purge consumer container: %v", err) - } - } - - for _, r := range s.providerValRes { - if err := s.dkrPool.Purge(r); err != nil { - s.T().Logf("failed to purge provider container: %v", err) - } - } - - if s.dkrNet != nil { - if err := s.dkrPool.RemoveNetwork(s.dkrNet); err != nil { - s.T().Logf("failed to remove liveness network: %v", err) - } - } - - for _, dir := range s.tmpDirs { - _ = os.RemoveAll(dir) - } -} - // TestLivenessVAAS sequences the liveness tests in a safe order: non-destructive // tests (recover, safe-mode, query) before the destructive sweep/delete tests. func (s *LivenessIntegrationTestSuite) TestLivenessVAAS() { @@ -233,805 +214,6 @@ func (s *LivenessIntegrationTestSuite) TestLivenessVAAS() { s.testAutoSweepRemoval() } -// ---- infrastructure helpers ------------------------------------------------ - -func (s *LivenessIntegrationTestSuite) livenessCleanupStaleContainers() { - staleNames := []string{ - "liveness-provider-init", - "liveness-consumer-init", - fmt.Sprintf("%s-val0", livenessProviderChainID), - fmt.Sprintf("%s-val0", livenessConsumerChainID), - fmt.Sprintf("%s-%s-ts-relayer", livenessProviderChainID, livenessConsumerChainID), - } - for _, name := range staleNames { - c, err := s.dkrPool.Client.InspectContainer(name) - if err != nil { - continue - } - s.T().Logf("removing stale liveness container: %s", name) - _ = s.dkrPool.Client.RemoveContainer(docker.RemoveContainerOptions{ - ID: c.ID, - Force: true, - RemoveVolumes: true, - }) - } - _ = s.dkrPool.Client.RemoveNetwork(livenessDockerNetwork) -} - -func (s *LivenessIntegrationTestSuite) livenessRunInitContainer(name, scriptPath, containerScriptPath, dataDir, homePath string, env []string) { - initResource, err := s.dkrPool.RunWithOptions( - &dockertest.RunOptions{ - Name: name, - Repository: e2eChainImage, - NetworkID: s.dkrNet.Network.ID, - User: "nonroot", - Env: env, - Mounts: []string{ - fmt.Sprintf("%s:%s", dataDir, homePath), - fmt.Sprintf("%s:%s", scriptPath, containerScriptPath), - }, - Entrypoint: []string{"sh", containerScriptPath}, - }, - func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }, - ) - s.Require().NoError(err, "failed to start %s container", name) - - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancel() - - exitCode, err := s.dkrPool.Client.WaitContainerWithContext(initResource.Container.ID, ctx) - s.Require().NoError(err, "%s container wait failed", name) - - var logBuf bytes.Buffer - _ = s.dkrPool.Client.Logs(docker.LogsOptions{ - Container: initResource.Container.ID, - OutputStream: &logBuf, - ErrorStream: &logBuf, - Stdout: true, - Stderr: true, - }) - s.Require().Equal(0, exitCode, "%s container exited with code %d\noutput:\n%s", name, exitCode, logBuf.String()) - s.Require().NoError(s.dkrPool.Purge(initResource), "failed to purge %s container", name) -} - -func (s *LivenessIntegrationTestSuite) livenessInitAndStartProvider() { - providerDir, err := os.MkdirTemp("", "vaas-liveness-provider-") - s.Require().NoError(err) - s.tmpDirs = append(s.tmpDirs, providerDir) - s.provider.dataDir = providerDir - s.Require().NoError(os.Chmod(providerDir, 0o777)) - - scriptPath := filepath.Join(testDir(), "scripts", "provider-init.sh") - s.livenessRunInitContainer( - "liveness-provider-init", scriptPath, "/scripts/provider-init.sh", - providerDir, providerHomePath, - []string{ - "BINARY=" + providerBinary, - "HOME_DIR=" + providerHomePath, - "CHAIN_ID=" + livenessProviderChainID, - "DENOM=" + bondDenom, - "MNEMONIC=" + relayerMnemonic, - }, - ) - - genesisFile := filepath.Join(providerDir, "config", "genesis.json") - s.livenessPatchGenesisJSON(genesisFile, func(genesis map[string]any) { - appState := genesis["app_state"].(map[string]any) - - if gov, ok := appState["gov"].(map[string]any); ok { - if params, ok := gov["params"].(map[string]any); ok { - params["voting_period"] = "15s" - } - } - - if provider, ok := appState["provider"].(map[string]any); ok { - if params, ok := provider["params"].(map[string]any); ok { - // One block per epoch so a VSC packet (and its ack) flows every - // block. Combined with the fast block time (see - // livenessPatchConfigToml) this refreshes the provider's lastAck - // every few seconds, well inside the grace, so a healthy - // consumer is never swept between acks. - params["blocks_per_epoch"] = "1" - params["fees_per_block_amount"] = "1000" - // Short VSC packet timeout so testForcedTimeoutSnapshotResync can - // make a packet actually time out within a CI run (the relayer is - // paused while the consumer keeps producing blocks past this - // deadline). Now possible since the MinVAASTimeoutPeriod floor was - // dropped. Timeouts are log-only, so this does not perturb the - // other liveness tests. - params["vaas_timeout_period"] = "20s" - // Shrink the liveness grace fraction so the grace period - // (unbonding * fraction = 200s * 0.75 = ~150s) is observable in - // a CI run, while keeping the unbonding itself long enough that - // the relayer-derived IBC client trusting period (unbonding * - // 0.66 = ~132s) survives the relayer add-path window. - // - // The grace must exceed the time from consumer launch to first - // VSC sync (the provider seeds lastAck at launch, so the clock - // starts before the relayer has delivered anything). With fast - // blocks that first sync is ~60-90s; 150s leaves margin. The - // suite also waits for that first sync explicitly before - // asserting (see livenessWaitForConsumerSync). - params["liveness_grace_fraction"] = "0.75" - } - } - - // Keep unbonding long enough for a viable IBC client trusting period - // (see liveness_grace_fraction note above); the short grace comes from - // the fraction, not from a short unbonding. - if staking, ok := appState["staking"].(map[string]any); ok { - if params, ok := staking["params"].(map[string]any); ok { - params["unbonding_time"] = "200s" - } - } - }) - - // Fast blocks so add-path, epochs, and VSC delivery all complete well - // inside the grace (see livenessPatchConfigToml). - s.livenessPatchConfigToml(filepath.Join(providerDir, "config", "config.toml")) - - s.T().Log("starting liveness provider chain container...") - resource, err := s.dkrPool.RunWithOptions( - &dockertest.RunOptions{ - Name: fmt.Sprintf("%s-val0", livenessProviderChainID), - Repository: e2eChainImage, - NetworkID: s.dkrNet.Network.ID, - Mounts: []string{ - fmt.Sprintf("%s:%s", providerDir, providerHomePath), - }, - PortBindings: map[docker.Port][]docker.PortBinding{ - "26657/tcp": {{HostIP: "", HostPort: livenessProviderRPCPort}}, - "9090/tcp": {{HostIP: "", HostPort: livenessProviderGRPCPort}}, - "1317/tcp": {{HostIP: "", HostPort: livenessProviderRESTPort}}, - "26656/tcp": {{HostIP: "", HostPort: livenessProviderP2PPort}}, - }, - Cmd: []string{providerBinary, "start", "--home", providerHomePath}, - }, - func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }, - ) - s.Require().NoError(err, "failed to start liveness provider container") - s.providerValRes = append(s.providerValRes, resource) - s.T().Logf("liveness provider container started: %s", resource.Container.ID[:12]) - - waitCtx, waitCancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer waitCancel() - err = s.livenessWaitForChainHeight(waitCtx, "http://localhost:"+livenessProviderRPCPort, 3) - s.Require().NoError(err, "liveness provider failed to produce blocks") - s.T().Log("liveness provider chain is producing blocks") -} - -func (s *LivenessIntegrationTestSuite) livenessRegisterConsumerOnProvider() { - templatePath := filepath.Join(testDir(), "testdata", "create_consumer_short_unbonding.json") - templateBytes, err := os.ReadFile(templatePath) - s.Require().NoError(err, "failed to read create_consumer_short_unbonding.json template") - - createConsumerJSON := strings.ReplaceAll(string(templateBytes), "CONSUMER_SHORT_CHAIN_ID", livenessConsumerChainID) - - s.livenessdockerExecMust(s.providerValRes[0].Container.ID, []string{ - "sh", "-c", fmt.Sprintf("echo '%s' > /tmp/create_consumer.json", createConsumerJSON), - }) - - _, _, err = s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ - providerBinary, "tx", "provider", "create-consumer", "/tmp/create_consumer.json", - "--from", "val", - "--home", providerHomePath, - "--keyring-backend", "test", - "--chain-id", livenessProviderChainID, - "--gas", "auto", - "--gas-adjustment", "1.5", - "--fees", "10000" + bondDenom, - "-y", - }) - s.Require().NoError(err, "failed to create consumer on liveness provider") - time.Sleep(10 * time.Second) -} - -func (s *LivenessIntegrationTestSuite) livenessFetchConsumerGenesis() []byte { - var output string - var lastErr error - - for range 30 { - stdout, _, err := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ - providerBinary, "query", "provider", "consumer-genesis", "0", - "--home", providerHomePath, - "--output", "json", - }) - if err == nil { - output = stdout.String() - if output != "" && !strings.Contains(output, "not found") && !strings.Contains(output, "Error") { - break - } - } - lastErr = err - time.Sleep(3 * time.Second) - } - s.Require().NotEmpty(output, "consumer genesis is empty, last error: %v", lastErr) - s.T().Logf("fetched liveness consumer genesis (%d bytes)", len(output)) - return []byte(output) -} - -func (s *LivenessIntegrationTestSuite) livenessInitAndStartConsumer(consumerGenesisJSON []byte) { - consumerDir, err := os.MkdirTemp("", "vaas-liveness-consumer-") - s.Require().NoError(err) - s.tmpDirs = append(s.tmpDirs, consumerDir) - s.consumer.dataDir = consumerDir - s.Require().NoError(os.Chmod(consumerDir, 0o777)) - - scriptPath := filepath.Join(testDir(), "scripts", "consumer-init.sh") - s.livenessRunInitContainer( - "liveness-consumer-init", scriptPath, "/scripts/consumer-init.sh", - consumerDir, consumerHomePath, - []string{ - "BINARY=" + consumerBinary, - "HOME_DIR=" + consumerHomePath, - "CHAIN_ID=" + livenessConsumerChainID, - "DENOM=" + bondDenom, - "MNEMONIC=" + relayerMnemonic, - }, - ) - - genesisFile := filepath.Join(consumerDir, "config", "genesis.json") - err = patchConsumerGenesisWithProviderData(genesisFile, consumerGenesisJSON) - s.Require().NoError(err, "failed to patch liveness consumer genesis") - - s.livenessPatchConsumerSlashingParams() - - // Fast blocks on the consumer too, so its side of add-path and the VSC - // recv/ack round-trip keep pace with the provider (see livenessPatchConfigToml). - s.livenessPatchConfigToml(filepath.Join(consumerDir, "config", "config.toml")) - - providerDir := s.provider.dataDir - err = copyFile( - filepath.Join(providerDir, "config", "priv_validator_key.json"), - filepath.Join(consumerDir, "config", "priv_validator_key.json"), - ) - s.Require().NoError(err, "failed to copy priv_validator_key.json") - - err = copyFile( - filepath.Join(providerDir, "config", "node_key.json"), - filepath.Join(consumerDir, "config", "node_key.json"), - ) - s.Require().NoError(err, "failed to copy node_key.json") - - s.T().Log("starting liveness consumer chain container...") - resource, err := s.dkrPool.RunWithOptions( - &dockertest.RunOptions{ - Name: fmt.Sprintf("%s-val0", livenessConsumerChainID), - Repository: e2eChainImage, - NetworkID: s.dkrNet.Network.ID, - Mounts: []string{ - fmt.Sprintf("%s:%s", consumerDir, consumerHomePath), - }, - PortBindings: map[docker.Port][]docker.PortBinding{ - "26657/tcp": {{HostIP: "", HostPort: livenessConsumerRPCPort}}, - "9090/tcp": {{HostIP: "", HostPort: livenessConsumerGRPCPort}}, - "1317/tcp": {{HostIP: "", HostPort: livenessConsumerRESTPort}}, - "26656/tcp": {{HostIP: "", HostPort: livenessConsumerP2PPort}}, - }, - Cmd: []string{consumerBinary, "start", "--home", consumerHomePath}, - }, - func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }, - ) - s.Require().NoError(err, "failed to start liveness consumer container") - s.consumerValRes = append(s.consumerValRes, resource) - s.T().Logf("liveness consumer container started: %s", resource.Container.ID[:12]) - - waitCtx, waitCancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer waitCancel() - err = s.livenessWaitForChainHeight(waitCtx, "http://localhost:"+livenessConsumerRPCPort, 3) - s.Require().NoError(err, "liveness consumer failed to produce blocks") - s.T().Log("liveness consumer chain is producing blocks") -} - -func (s *LivenessIntegrationTestSuite) livenessSetupTSRelayer() { - s.livenessStartTSRelayer() - s.livenesstsRelayerAddMnemonic(livenessProviderChainID, relayerMnemonic) - s.livenesstsRelayerAddMnemonic(livenessConsumerChainID, relayerMnemonic) - s.livenesstsRelayerAddGasPrice(livenessProviderChainID, "0.025"+bondDenom) - s.livenesstsRelayerAddGasPrice(livenessConsumerChainID, "0.025"+bondDenom) - s.livenesstsRelayerAddPath(IBCv2) - s.livenesstsRelayerDumpPaths() - s.livenessStartTSRelayerRelay() - s.T().Log("liveness ts-relayer IBC v2 path configured") -} - -func (s *LivenessIntegrationTestSuite) livenessStartTSRelayer() { - s.T().Log("starting liveness ts-relayer container") - resource, err := s.dkrPool.RunWithOptions( - &dockertest.RunOptions{ - Name: fmt.Sprintf("%s-%s-ts-relayer", livenessProviderChainID, livenessConsumerChainID), - Repository: tsRelayerImage, - Tag: tsRelayerImageTag, - NetworkID: s.dkrNet.Network.ID, - User: "root", - CapAdd: []string{"IPC_LOCK"}, - }, - noRestart, - ) - s.Require().NoError(err, "failed to start liveness ts-relayer container") - s.tsRelayerResource = resource - s.T().Logf("liveness ts-relayer started: %s", resource.Container.ID[:12]) - - time.Sleep(3 * time.Second) - - providerURL := "http://" + s.providerValRes[0].Container.Name[1:] + ":26657" - consumerURL := "http://" + s.consumerValRes[0].Container.Name[1:] + ":26657" - - s.livenessVerifyTSRelayerConnectivity("liveness-provider", providerURL) - s.livenessVerifyTSRelayerConnectivity("liveness-consumer", consumerURL) -} - -func (s *LivenessIntegrationTestSuite) livenessVerifyTSRelayerConnectivity(chainName, rpcURL string) { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - for attempt := range 10 { - exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{ - Context: ctx, - AttachStdout: true, - AttachStderr: true, - Container: s.tsRelayerResource.Container.ID, - User: "root", - Cmd: []string{"wget", "-qO-", fmt.Sprintf("%s/status", rpcURL)}, - }) - s.Require().NoError(err) - - var out bytes.Buffer - err = s.dkrPool.Client.StartExec(exec.ID, docker.StartExecOptions{ - Context: ctx, - Detach: false, - OutputStream: &out, - ErrorStream: &out, - }) - _ = err - - for { - inspectExec, err := s.dkrPool.Client.InspectExec(exec.ID) - s.Require().NoError(err) - if !inspectExec.Running { - if inspectExec.ExitCode == 0 && strings.Contains(out.String(), "latest_block_height") { - s.T().Logf("liveness ts-relayer can reach %s at %s", chainName, rpcURL) - return - } - break - } - } - s.T().Logf("liveness ts-relayer connectivity to %s attempt %d failed", chainName, attempt+1) - time.Sleep(2 * time.Second) - } - s.Require().Fail("liveness ts-relayer cannot reach %s at %s", chainName, rpcURL) -} - -func (s *LivenessIntegrationTestSuite) livenessExecuteTSRelayerCommand(ctx context.Context, args []string) []byte { - tsRelayerBinary := []string{"/bin/with_keyring", "ibc-v2-ts-relayer"} - cmd := append(tsRelayerBinary, args...) - exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{ - Context: ctx, - AttachStdout: true, - AttachStderr: true, - Container: s.tsRelayerResource.Container.ID, - User: "root", - Cmd: cmd, - }) - s.Require().NoError(err) - - var out bytes.Buffer - err = s.dkrPool.Client.StartExec(exec.ID, docker.StartExecOptions{ - Context: ctx, - Detach: false, - OutputStream: &out, - ErrorStream: &out, - }) - s.Require().NoError(err, "liveness ts-relayer startExec error: %s", out.String()) - - for { - inspectExec, err := s.dkrPool.Client.InspectExec(exec.ID) - s.Require().NoError(err) - if !inspectExec.Running { - s.Require().Equal(0, inspectExec.ExitCode, - "liveness ts-relayer cmd '%s' failed (exit=%d): %s", - strings.Join(cmd, " "), inspectExec.ExitCode, out.String()) - break - } - } - return out.Bytes() -} - -func (s *LivenessIntegrationTestSuite) livenesstsRelayerAddMnemonic(chainID, mnemonic string) { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - s.livenessExecuteTSRelayerCommand(ctx, []string{"add-mnemonic", "-c", chainID, "--mnemonic", mnemonic}) -} - -func (s *LivenessIntegrationTestSuite) livenesstsRelayerAddGasPrice(chainID, gasPrice string) { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - s.livenessExecuteTSRelayerCommand(ctx, []string{"add-gas-price", "-c", chainID, "--gas-adjustment", "2.0", gasPrice}) -} - -func (s *LivenessIntegrationTestSuite) livenesstsRelayerAddPath(ibcVersion string) { - s.T().Logf("liveness ts-relayer: adding IBCv%s path", ibcVersion) - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancel() - s.livenessExecuteTSRelayerCommand(ctx, []string{ - "add-path", - "-s", livenessProviderChainID, - "-d", livenessConsumerChainID, - "--surl", "http://" + s.providerValRes[0].Container.Name[1:] + ":26657", - "--durl", "http://" + s.consumerValRes[0].Container.Name[1:] + ":26657", - "--st", "cosmos", - "--dt", "cosmos", - "--ibc-version", ibcVersion, - }) -} - -func (s *LivenessIntegrationTestSuite) livenesstsRelayerDumpPaths() { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - s.livenessExecuteTSRelayerCommand(ctx, []string{"dump-paths"}) -} - -func (s *LivenessIntegrationTestSuite) livenessStartTSRelayerRelay() { - s.T().Log("liveness ts-relayer: starting relay process") - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - cmd := []string{"/bin/with_keyring", "ibc-v2-ts-relayer", "relay"} - exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{ - Context: ctx, - AttachStdout: true, - AttachStderr: true, - Container: s.tsRelayerResource.Container.ID, - User: "root", - Cmd: cmd, - }) - s.Require().NoError(err, "failed to create liveness relay exec") - err = s.dkrPool.Client.StartExec(exec.ID, docker.StartExecOptions{Context: ctx, Detach: true}) - s.Require().NoError(err, "failed to start liveness relay process") - time.Sleep(3 * time.Second) - inspectExec, err := s.dkrPool.Client.InspectExec(exec.ID) - s.Require().NoError(err) - s.Require().True(inspectExec.Running, "liveness relay process is not running") -} - -func (s *LivenessIntegrationTestSuite) livenessStopTSRelayer() { - if s.tsRelayerResource != nil { - s.T().Log("tearing down liveness ts-relayer...") - s.Require().NoError(s.dkrPool.Purge(s.tsRelayerResource)) - s.tsRelayerResource = nil - } -} - -func (s *LivenessIntegrationTestSuite) livenessWaitForChainHeight(ctx context.Context, rpcEndpoint string, minHeight int64) error { - ticker := time.NewTicker(2 * time.Second) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - return fmt.Errorf("timeout waiting for chain at %s to reach height %d", rpcEndpoint, minHeight) - case <-ticker.C: - height, err := queryBlockHeight(rpcEndpoint) - if err != nil { - continue - } - if height >= minHeight { - return nil - } - } - } -} - -// ---- genesis helpers ------------------------------------------------------- - -func (s *LivenessIntegrationTestSuite) livenessPatchGenesisJSON(path string, mutate func(map[string]any)) { - bz, err := os.ReadFile(path) - s.Require().NoError(err, "failed to read genesis file") - var genesis map[string]any - s.Require().NoError(json.Unmarshal(bz, &genesis), "failed to unmarshal genesis") - mutate(genesis) - out, err := json.MarshalIndent(genesis, "", " ") - s.Require().NoError(err, "failed to marshal genesis") - s.Require().NoError(os.WriteFile(path, out, 0o600), "failed to write genesis") -} - -// livenessPatchConfigToml lowers the CometBFT consensus timeouts so the chain -// produces blocks roughly every second instead of the ~5s default. Fast blocks -// matter here because every time-bounded step in the suite -- relayer add-path -// (which waits for client header heights), epoch/VSC cadence, and the VSC -// recv/ack round-trip -- is measured against the liveness grace. At the default -// block time these add up to minutes and the grace (which starts ticking at -// consumer launch) can expire before the consumer ever syncs. -func (s *LivenessIntegrationTestSuite) livenessPatchConfigToml(path string) { - bz, err := os.ReadFile(path) - s.Require().NoError(err, "failed to read config.toml") - out := string(bz) - for from, to := range map[string]string{ - `timeout_propose = "3s"`: `timeout_propose = "500ms"`, - `timeout_commit = "5s"`: `timeout_commit = "1s"`, - `timeout_propose_delta = "500ms"`: `timeout_propose_delta = "200ms"`, - } { - s.Require().Containsf(out, from, "config.toml missing %q (default may have changed)", from) - out = strings.ReplaceAll(out, from, to) - } - s.Require().NoError(os.WriteFile(path, []byte(out), 0o600), "failed to write config.toml") -} - -func (s *LivenessIntegrationTestSuite) livenessPatchConsumerSlashingParams() { - s.livenessPatchGenesisJSON(s.consumer.dataDir+"/config/genesis.json", func(genesis map[string]any) { - appState, ok := genesis["app_state"].(map[string]any) - if !ok { - return - } - slashing, ok := appState["slashing"].(map[string]any) - if !ok { - slashing = make(map[string]any) - } - params, ok := slashing["params"].(map[string]any) - if !ok { - params = make(map[string]any) - } - params["signed_blocks_window"] = "5" - params["min_signed_per_window"] = "0.050000000000000000" - params["slash_fraction_downtime"] = "0.000000000000000000" - params["downtime_jail_duration"] = "60s" - slashing["params"] = params - appState["slashing"] = slashing - }) -} - -// ---- exec helpers ---------------------------------------------------------- - -func (s *LivenessIntegrationTestSuite) livenessdockerExec(containerID string, cmd []string) (bytes.Buffer, bytes.Buffer, error) { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - var stdout, stderr bytes.Buffer - exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{ - Context: ctx, - AttachStdout: true, - AttachStderr: true, - Container: containerID, - User: "nonroot", - Cmd: cmd, - }) - if err != nil { - return stdout, stderr, fmt.Errorf("failed to create exec: %w", err) - } - err = s.dkrPool.Client.StartExec(exec.ID, docker.StartExecOptions{ - Context: ctx, - Detach: false, - OutputStream: &stdout, - ErrorStream: &stderr, - }) - if err != nil { - return stdout, stderr, fmt.Errorf("failed to start exec: %w", err) - } - return stdout, stderr, nil -} - -func (s *LivenessIntegrationTestSuite) livenessdockerExecMust(containerID string, cmd []string) { - stdout, stderr, err := s.livenessdockerExec(containerID, cmd) - if err != nil { - s.T().Logf("cmd: %v", cmd) - s.T().Logf("stdout: %s", stdout.String()) - s.T().Logf("stderr: %s", stderr.String()) - } - s.Require().NoError(err, "docker exec failed for cmd: %v", cmd) -} - -// ---- query helpers --------------------------------------------------------- - -func (s *LivenessIntegrationTestSuite) livenessQueryConsumerPhase(consumerID string) string { - stdout, _, err := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ - providerBinary, "query", "provider", "consumer-chain", consumerID, - "--home", providerHomePath, - "--output", "json", - }) - if err != nil { - s.T().Logf("livenessQueryConsumerPhase(%s): exec error: %v", consumerID, err) - return "" - } - var res struct { - Phase string `json:"phase"` - } - if err := json.Unmarshal(stdout.Bytes(), &res); err != nil { - s.T().Logf("livenessQueryConsumerPhase(%s): decode error: %v (raw: %s)", consumerID, err, stdout.String()) - return "" - } - return res.Phase -} - -func (s *LivenessIntegrationTestSuite) livenessConsumerUserBech32() string { - stdout, _, err := s.livenessdockerExec(s.consumerValRes[0].Container.ID, []string{ - consumerBinary, "keys", "show", "user", "-a", - "--home", consumerHomePath, - "--keyring-backend", "test", - }) - s.Require().NoError(err, "failed to get liveness consumer user address") - return strings.TrimSpace(stdout.String()) -} - -func (s *LivenessIntegrationTestSuite) livenessConsumerBankSendDryRun() (string, error) { - user := s.livenessConsumerUserBech32() - _, stderr, err := s.livenessdockerExec(s.consumerValRes[0].Container.ID, []string{ - consumerBinary, "tx", "bank", "send", user, user, "1" + bondDenom, - "--home", consumerHomePath, - "--keyring-backend", "test", - "--chain-id", livenessConsumerChainID, - "--fees", "1000" + bondDenom, - "--dry-run", - "-y", - }) - return stderr.String(), err -} - -func (s *LivenessIntegrationTestSuite) livenessQueryGovAuthority() string { - stdout, _, err := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ - providerBinary, "query", "auth", "module-account", "gov", - "--home", providerHomePath, - "--output", "json", - }) - s.Require().NoError(err, "failed to query gov module account") - - var res struct { - Account struct { - Address string `json:"address"` - BaseAccount struct { - Address string `json:"address"` - } `json:"base_account"` - Value struct { - Address string `json:"address"` - BaseAccount struct { - Address string `json:"address"` - } `json:"base_account"` - } `json:"value"` - } `json:"account"` - } - s.Require().NoError(json.Unmarshal(stdout.Bytes(), &res), - "failed to decode module-account response: %s", stdout.String()) - - candidates := []string{ - res.Account.BaseAccount.Address, - res.Account.Value.Address, - res.Account.Value.BaseAccount.Address, - res.Account.Address, - } - for _, addr := range candidates { - if addr != "" { - return addr - } - } - s.Require().Fail("gov authority address not found in module-account response") - return "" -} - -func (s *LivenessIntegrationTestSuite) livenessSubmitAndPassProposal(proposalJSON string) { - containerID := s.providerValRes[0].Container.ID - - payload := base64.StdEncoding.EncodeToString([]byte(proposalJSON)) - _, _, err := s.livenessdockerExec(containerID, []string{ - "sh", "-c", - fmt.Sprintf("echo %s | base64 -d > /tmp/proposal.json", payload), - }) - s.Require().NoError(err, "failed to write proposal.json") - - stdout, stderr, err := s.livenessdockerExec(containerID, []string{ - providerBinary, "tx", "gov", "submit-proposal", "/tmp/proposal.json", - "--from", "val", - "--home", providerHomePath, - "--keyring-backend", "test", - "--chain-id", livenessProviderChainID, - "--fees", "10000" + bondDenom, - "--yes", - "-o", "json", - }) - s.Require().NoErrorf(err, "failed to submit proposal: stdout=%s stderr=%s", - stdout.String(), stderr.String()) - - var submitRes struct { - TxHash string `json:"txhash"` - Code int `json:"code"` - RawLog string `json:"raw_log"` - } - s.Require().NoError(json.Unmarshal(stdout.Bytes(), &submitRes), - "failed to decode submit-proposal response: %s", stdout.String()) - s.Require().Equalf(0, submitRes.Code, "submit-proposal failed: %s", submitRes.RawLog) - s.Require().NotEmpty(submitRes.TxHash, "submit-proposal returned empty txhash") - - var proposalID uint64 - var lastTxOut string - s.Require().Eventuallyf(func() bool { - txOut, _, qerr := s.livenessdockerExec(containerID, []string{ - providerBinary, "query", "tx", submitRes.TxHash, - "--home", providerHomePath, - "--output", "json", - }) - if qerr != nil || txOut.Len() == 0 { - return false - } - lastTxOut = txOut.String() - proposalID = extractProposalID(txOut.Bytes()) - return proposalID != 0 - }, 30*time.Second, 2*time.Second, - "could not parse proposal_id from tx %s: last stdout=%s", - submitRes.TxHash, lastTxOut) - - voteStdout, voteStderr, err := s.livenessdockerExec(containerID, []string{ - providerBinary, "tx", "gov", "vote", fmt.Sprintf("%d", proposalID), "yes", - "--from", "val", - "--home", providerHomePath, - "--keyring-backend", "test", - "--chain-id", livenessProviderChainID, - "--fees", "10000" + bondDenom, - "--yes", - }) - s.Require().NoErrorf(err, "failed to vote on proposal %d: stdout=%s stderr=%s", - proposalID, voteStdout.String(), voteStderr.String()) - - s.Require().Eventuallyf(func() bool { - txOut, _, qerr := s.livenessdockerExec(containerID, []string{ - providerBinary, "query", "gov", "proposal", strconv.FormatUint(proposalID, 10), - "--home", providerHomePath, - "--output", "json", - }) - if qerr != nil || txOut.Len() == 0 { - return false - } - var res struct { - Status string `json:"status"` - Proposal struct { - Status string `json:"status"` - } `json:"proposal"` - } - if json.Unmarshal(txOut.Bytes(), &res) != nil { - return false - } - status := res.Status - if status == "" { - status = res.Proposal.Status - } - switch status { - case "PROPOSAL_STATUS_PASSED": - return true - case "PROPOSAL_STATUS_REJECTED", "PROPOSAL_STATUS_FAILED": - s.Require().Failf("proposal terminated unsuccessfully", - "proposal %d ended with status %s", proposalID, status) - return true - } - return false - }, 30*time.Second, 2*time.Second, "proposal %d did not pass within timeout", proposalID) -} - -func (s *LivenessIntegrationTestSuite) livenessFundConsumerFeePool(consumerID, amount string) { - stdout, stderr, err := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ - providerBinary, "tx", "provider", "fund-consumer-fee-pool", - consumerID, amount, - "--from", "val", - "--home", providerHomePath, - "--keyring-backend", "test", - "--chain-id", livenessProviderChainID, - "--fees", "10000" + bondDenom, - "-y", - "-o", "json", - }) - s.Require().NoErrorf(err, "failed to fund consumer %s fee pool: stdout=%s stderr=%s", - consumerID, stdout.String(), stderr.String()) - - var bres struct { - TxHash string `json:"txhash"` - Code int `json:"code"` - RawLog string `json:"raw_log"` - } - s.Require().NoError(json.Unmarshal(stdout.Bytes(), &bres)) - s.Require().Equalf(0, bres.Code, "fund-consumer-fee-pool failed: %s", bres.RawLog) -} - // ---- test methods ---------------------------------------------------------- // testRecoverBeforeGrace pauses the consumer for ~10s (far less than the ~150s @@ -1046,12 +228,12 @@ func (s *LivenessIntegrationTestSuite) testRecoverBeforeGrace() { // the run so the ack cadence (last_ack vs removal_eta) is visible even if // a later assertion fails. The dedicated check is testLivenessQuery. livenessURL := fmt.Sprintf("http://localhost:%s/vaas/provider/consumer_liveness/%s", - livenessProviderRESTPort, consumerID) + s.cfg.providerRESTPort, consumerID) if body, err := httpGet(livenessURL); err == nil { s.T().Logf("diagnostic: initial consumer liveness: %s", string(body)) } - phase := s.livenessQueryConsumerPhase(consumerID) + phase := s.queryProviderConsumerPhase(consumerID) s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, "consumer %s must be LAUNCHED before recover-before-grace test", consumerID) @@ -1074,7 +256,7 @@ func (s *LivenessIntegrationTestSuite) testRecoverBeforeGrace() { s.T().Logf("diagnostic: post-recovery consumer liveness: %s", string(body)) } - phase = s.livenessQueryConsumerPhase(consumerID) + phase = s.queryProviderConsumerPhase(consumerID) s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, "consumer %s must remain LAUNCHED after a transient outage shorter than grace", consumerID) s.T().Log("consumer remains LAUNCHED after recovery before grace") @@ -1109,11 +291,11 @@ func (s *LivenessIntegrationTestSuite) testRealSafeMode() { // Ensure the fee pool is funded so debt does not contaminate the test. s.T().Log("funding consumer fee pool to avoid debt interference...") - s.livenessFundConsumerFeePool(consumerID, "20000000"+feeDenom) + s.providerFundConsumerFeePool(consumerID, "20000000"+feeDenom) // Verify normal mode before freezing delivery. s.Require().Eventuallyf(func() bool { - out, err := s.livenessConsumerBankSendDryRun() + out, err := s.consumerBankSendDryRun() if err != nil { return false } @@ -1129,7 +311,7 @@ func (s *LivenessIntegrationTestSuite) testRealSafeMode() { // Wait for the consumer to enter restricted mode (VSC stale after >5s). s.T().Log("waiting for consumer to enter restricted mode (VSC stale after ~5s)...") s.Require().Eventuallyf(func() bool { - out, err := s.livenessConsumerBankSendDryRun() + out, err := s.consumerBankSendDryRun() return err != nil || strings.Contains(out, "stale validator set") || strings.Contains(out, "consumer chain is in debt") }, 2*time.Minute, 3*time.Second, @@ -1144,7 +326,7 @@ func (s *LivenessIntegrationTestSuite) testRealSafeMode() { // Wait for the consumer to exit restricted mode. s.T().Log("waiting for consumer to exit restricted mode after VSC delivery resumes...") s.Require().Eventuallyf(func() bool { - out, err := s.livenessConsumerBankSendDryRun() + out, err := s.consumerBankSendDryRun() if err != nil { return false } @@ -1173,19 +355,19 @@ func (s *LivenessIntegrationTestSuite) testLivenessQuery() { const consumerID = "0" // Confirm the consumer is still LAUNCHED before checking liveness. - phase := s.livenessQueryConsumerPhase(consumerID) + phase := s.queryProviderConsumerPhase(consumerID) s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, "consumer must be LAUNCHED for liveness query test") // Diagnostic: log provider staking params and consumer chain init_params so // the controller run can confirm the genesis patches actually took effect. - stakingOut, _, _ := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ + stakingOut, _, _ := s.dockerExec(s.providerValRes[0].Container.ID, []string{ providerBinary, "query", "staking", "params", "--home", providerHomePath, "--output", "json", }) s.T().Logf("diagnostic: provider staking params: %s", stakingOut.String()) - chainOut, _, _ := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ + chainOut, _, _ := s.dockerExec(s.providerValRes[0].Container.ID, []string{ providerBinary, "query", "provider", "consumer-chain", consumerID, "--home", providerHomePath, "--output", "json", }) @@ -1194,7 +376,7 @@ func (s *LivenessIntegrationTestSuite) testLivenessQuery() { // Query liveness via the gRPC-gateway REST path. // The response Timestamps are RFC3339 strings; Duration is a string like "19.8s". livenessURL := fmt.Sprintf("http://localhost:%s/vaas/provider/consumer_liveness/%s", - livenessProviderRESTPort, consumerID) + s.cfg.providerRESTPort, consumerID) var livenessRes struct { LastAckTime string `json:"last_ack_time"` @@ -1262,7 +444,7 @@ func (s *LivenessIntegrationTestSuite) testForcedTimeoutSnapshotResync() { s.Run("forced timeout: demoted OnTimeout keeps consumer LAUNCHED; behind consumer heals via snapshot", func() { const consumerID = "0" - s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", s.livenessQueryConsumerPhase(consumerID), + s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", s.queryProviderConsumerPhase(consumerID), "consumer %s must be LAUNCHED before the forced-timeout test", consumerID) // Pause the relayer; the consumer keeps producing blocks, so its clock @@ -1278,7 +460,7 @@ func (s *LivenessIntegrationTestSuite) testForcedTimeoutSnapshotResync() { // (a) The demoted OnTimeout must not have removed the consumer. s.Require().Eventuallyf(func() bool { - return s.livenessQueryConsumerPhase(consumerID) == "CONSUMER_PHASE_LAUNCHED" + return s.queryProviderConsumerPhase(consumerID) == "CONSUMER_PHASE_LAUNCHED" }, 30*time.Second, 3*time.Second, "consumer %s must remain LAUNCHED despite VSC packet timeouts", consumerID) s.T().Log("consumer remained LAUNCHED through the timeouts") @@ -1286,7 +468,7 @@ func (s *LivenessIntegrationTestSuite) testForcedTimeoutSnapshotResync() { // (b) Prove a real timeout actually fired -- otherwise (a) is vacuous. // The provider logs the demoted OnTimeout handler. s.Require().Eventuallyf(func() bool { - return strings.Contains(s.livenessProviderLogs(), "packet timeout, retrying next epoch") + return strings.Contains(s.providerLogs(), "packet timeout, retrying next epoch") }, 30*time.Second, 3*time.Second, "provider never logged a VSC packet timeout; the timeout path was not exercised") s.T().Log("provider processed a demoted VSC timeout (OnTimeout is log-only)") @@ -1295,39 +477,13 @@ func (s *LivenessIntegrationTestSuite) testForcedTimeoutSnapshotResync() { // the consumer logs "applied snapshot resync" (and emits the matching // event) only when it applies an is_snapshot packet. s.Require().Eventuallyf(func() bool { - return strings.Contains(s.livenessConsumerLogs(), "applied snapshot resync") + return strings.Contains(s.consumerLogs(), "applied snapshot resync") }, 2*time.Minute, 5*time.Second, "consumer never applied a snapshot resync after recovery") s.T().Log("consumer applied a snapshot resync after recovery") }) } -// livenessProviderLogs returns the provider container's full stdout+stderr log. -func (s *LivenessIntegrationTestSuite) livenessProviderLogs() string { - var buf bytes.Buffer - _ = s.dkrPool.Client.Logs(docker.LogsOptions{ - Container: s.providerValRes[0].Container.ID, - OutputStream: &buf, - ErrorStream: &buf, - Stdout: true, - Stderr: true, - }) - return buf.String() -} - -// livenessConsumerLogs returns the consumer container's full stdout+stderr log. -func (s *LivenessIntegrationTestSuite) livenessConsumerLogs() string { - var buf bytes.Buffer - _ = s.dkrPool.Client.Logs(docker.LogsOptions{ - Container: s.consumerValRes[0].Container.ID, - OutputStream: &buf, - ErrorStream: &buf, - Stdout: true, - Stderr: true, - }) - return buf.String() -} - // testAutoSweepRemoval stops the relayer (and pauses the consumer) so no VSC // acks return to the provider. After the liveness grace period (~150s) expires, // the provider's SweepUnresponsiveConsumers moves the consumer to @@ -1336,32 +492,32 @@ func (s *LivenessIntegrationTestSuite) testAutoSweepRemoval() { s.Run("auto sweep removal: sustained outage causes LAUNCHED -> STOPPED", func() { const consumerID = "0" - phase := s.livenessQueryConsumerPhase(consumerID) + phase := s.queryProviderConsumerPhase(consumerID) s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, "consumer %s must be LAUNCHED before auto-sweep test", consumerID) // Diagnostic: log provider staking params and consumer liveness state so // the controller run can confirm the genesis patches took effect. - stakingOut, _, _ := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ + stakingOut, _, _ := s.dockerExec(s.providerValRes[0].Container.ID, []string{ providerBinary, "query", "staking", "params", "--home", providerHomePath, "--output", "json", }) s.T().Logf("diagnostic: provider staking params: %s", stakingOut.String()) - chainOut, _, _ := s.livenessdockerExec(s.providerValRes[0].Container.ID, []string{ + chainOut, _, _ := s.dockerExec(s.providerValRes[0].Container.ID, []string{ providerBinary, "query", "provider", "consumer-chain", consumerID, "--home", providerHomePath, "--output", "json", }) s.T().Logf("diagnostic: consumer chain (init_params): %s", chainOut.String()) livenessURL := fmt.Sprintf("http://localhost:%s/vaas/provider/consumer_liveness/%s", - livenessProviderRESTPort, consumerID) + s.cfg.providerRESTPort, consumerID) if body, err := httpGet(livenessURL); err == nil { s.T().Logf("diagnostic: consumer liveness: %s", string(body)) } s.T().Log("purging ts-relayer so no VSC acks are returned to provider...") - s.livenessStopTSRelayer() + s.stopTSRelayer() s.T().Log("pausing consumer container to prevent acks...") err := s.dkrPool.Client.PauseContainer(s.consumerValRes[0].Container.ID) @@ -1378,7 +534,7 @@ func (s *LivenessIntegrationTestSuite) testAutoSweepRemoval() { // Allow up to 2 minutes total (grace already elapsed above). s.T().Log("polling for CONSUMER_PHASE_STOPPED (timeout 2min)...") s.Require().Eventuallyf(func() bool { - p := s.livenessQueryConsumerPhase(consumerID) + p := s.queryProviderConsumerPhase(consumerID) s.T().Logf("consumer %s phase: %s", consumerID, p) return p == "CONSUMER_PHASE_STOPPED" }, 2*time.Minute, 5*time.Second, diff --git a/tests/e2e/e2e_setup_test.go b/tests/e2e/e2e_setup_test.go index 81303dc..d5270b4 100644 --- a/tests/e2e/e2e_setup_test.go +++ b/tests/e2e/e2e_setup_test.go @@ -1,20 +1,15 @@ package e2e import ( - "bytes" - "context" - "encoding/json" "fmt" "os" "os/exec" "path/filepath" "runtime" - "strings" "testing" "time" "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/suite" "github.com/cosmos/cosmos-sdk/codec" @@ -31,17 +26,9 @@ const ( // IntegrationTestSuite is the main e2e test suite that orchestrates // provider chain and consumer chain containers. type IntegrationTestSuite struct { - suite.Suite + baseTestSuite - cdc codec.Codec - tmpDirs []string - provider *chain - consumer *chain - dkrPool *dockertest.Pool - dkrNet *dockertest.Network - providerValRes []*dockertest.Resource - consumerValRes []*dockertest.Resource - tsRelayerResource *dockertest.Resource + cdc codec.Codec } // makeCodec creates a proto codec with the standard cosmos SDK interfaces registered. @@ -72,6 +59,45 @@ func testDir() string { func (s *IntegrationTestSuite) SetupSuite() { s.T().Log("setting up e2e integration test suite...") + s.cfg = baseSuiteConfig{ + providerChainID: providerChainID, + consumerChainID: consumerChainID, + dockerNetwork: dockerNetwork, + providerInitName: "provider-init", + consumerInitName: "consumer-init", + tmpDirPrefix: "vaas-e2e-", + providerRPCPort: "26657", + providerGRPCPort: "9090", + providerRESTPort: "1317", + providerP2PPort: "26656", + consumerRPCPort: "26667", + consumerGRPCPort: "9092", + consumerRESTPort: "1327", + consumerP2PPort: "26666", + + consumerTemplateFile: "create_consumer.json", + consumerTemplatePlaceholder: "CONSUMER_CHAIN_ID", + + patchProviderGenesis: func(appState map[string]any) { + // Set fast voting period + if gov, ok := appState["gov"].(map[string]any); ok { + if params, ok := gov["params"].(map[string]any); ok { + params["voting_period"] = "15s" + } + } + + // Set fast epoch for VSC and a small per-block fee amount. The fee + // denom is fixed to feeDenom at module wiring, only the amount is + // configurable here. val is funded with feeDenom in provider-init.sh + if provider, ok := appState["provider"].(map[string]any); ok { + if params, ok := provider["params"].(map[string]any); ok { + params["blocks_per_epoch"] = "5" + params["fees_per_block_amount"] = "1000" + } + } + }, + } + s.cdc = makeCodec() var err error @@ -90,7 +116,7 @@ func (s *IntegrationTestSuite) SetupSuite() { s.Require().NoError(err, "failed to create docker network") s.T().Log("step 1: initializing provider chain...") - s.provider = &chain{id: providerChainID} + s.provider = &chain{id: s.cfg.providerChainID} s.initAndStartProvider() s.T().Log("step 2: registering consumer chain on provider...") @@ -100,7 +126,7 @@ func (s *IntegrationTestSuite) SetupSuite() { consumerGenesisJSON := s.fetchConsumerGenesis() s.T().Log("step 4: initializing consumer chain...") - s.consumer = &chain{id: consumerChainID} + s.consumer = &chain{id: s.cfg.consumerChainID} s.initAndStartConsumer(consumerGenesisJSON) s.T().Log("step 5: starting ts-relayer and creating IBC v2 path...") @@ -112,386 +138,6 @@ func (s *IntegrationTestSuite) SetupSuite() { s.T().Log("e2e test suite setup complete!") } -// TearDownSuite cleans up all Docker resources and temp directories. -func (s *IntegrationTestSuite) TearDownSuite() { - s.T().Log("tearing down e2e integration test suite...") - - if os.Getenv("VAAS_E2E_SKIP_CLEANUP") == "true" { - s.T().Log("skipping cleanup (VAAS_E2E_SKIP_CLEANUP=true)") - return - } - - s.stopTSRelayer() - - // Purge consumer validators - for _, r := range s.consumerValRes { - if err := s.dkrPool.Purge(r); err != nil { - s.T().Logf("failed to purge consumer container: %v", err) - } - } - - // Purge provider validators - for _, r := range s.providerValRes { - if err := s.dkrPool.Purge(r); err != nil { - s.T().Logf("failed to purge provider container: %v", err) - } - } - - // Remove network - if s.dkrNet != nil { - if err := s.dkrPool.RemoveNetwork(s.dkrNet); err != nil { - s.T().Logf("failed to remove network: %v", err) - } - } - - // Remove temp dirs - for _, dir := range s.tmpDirs { - _ = os.RemoveAll(dir) - } -} - -// cleanupStaleContainers removes containers from previous failed test runs. -func (s *IntegrationTestSuite) cleanupStaleContainers() { - staleNames := []string{ - "provider-init", - "consumer-init", - fmt.Sprintf("%s-val0", providerChainID), - fmt.Sprintf("%s-val0", consumerChainID), - fmt.Sprintf("%s-%s-ts-relayer", providerChainID, consumerChainID), - } - for _, name := range staleNames { - c, err := s.dkrPool.Client.InspectContainer(name) - if err != nil { - continue // container doesn't exist - } - s.T().Logf("removing stale container: %s", name) - _ = s.dkrPool.Client.RemoveContainer(docker.RemoveContainerOptions{ - ID: c.ID, - Force: true, - RemoveVolumes: true, - }) - } - // Also remove stale network - _ = s.dkrPool.Client.RemoveNetwork(dockerNetwork) -} - -// runInitContainer starts an init container with the given script mounted, -// waits for it to exit, checks the exit code, and purges it. -func (s *IntegrationTestSuite) runInitContainer(name, scriptPath, containerScriptPath, dataDir, homePath string, env []string) { - initResource, err := s.dkrPool.RunWithOptions( - &dockertest.RunOptions{ - Name: name, - Repository: e2eChainImage, - NetworkID: s.dkrNet.Network.ID, - User: "nonroot", - Env: env, - Mounts: []string{ - fmt.Sprintf("%s:%s", dataDir, homePath), - fmt.Sprintf("%s:%s", scriptPath, containerScriptPath), - }, - Entrypoint: []string{"sh", containerScriptPath}, - }, - func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }, - ) - s.Require().NoError(err, "failed to start %s container", name) - - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancel() - - exitCode, err := s.dkrPool.Client.WaitContainerWithContext(initResource.Container.ID, ctx) - s.Require().NoError(err, "%s container wait failed", name) - - var logBuf bytes.Buffer - _ = s.dkrPool.Client.Logs(docker.LogsOptions{ - Container: initResource.Container.ID, - OutputStream: &logBuf, - ErrorStream: &logBuf, - Stdout: true, - Stderr: true, - }) - s.Require().Equal(0, exitCode, "%s container exited with code %d\noutput:\n%s", name, exitCode, logBuf.String()) - - s.Require().NoError(s.dkrPool.Purge(initResource), "failed to purge %s container", name) -} - -// initAndStartProvider initializes the provider chain using a temporary Docker -// container that runs provider-init.sh, then starts the actual chain container. -func (s *IntegrationTestSuite) initAndStartProvider() { - // Create host directory for provider data - providerDir, err := os.MkdirTemp("", "vaas-e2e-provider-") - s.Require().NoError(err) - s.tmpDirs = append(s.tmpDirs, providerDir) - s.provider.dataDir = providerDir - - // Make writable - s.Require().NoError(os.Chmod(providerDir, 0o777)) - - // Run init script in a temporary container - scriptPath := filepath.Join(testDir(), "scripts", "provider-init.sh") - s.runInitContainer("provider-init", scriptPath, "/scripts/provider-init.sh", providerDir, providerHomePath, []string{ - "BINARY=" + providerBinary, - "HOME_DIR=" + providerHomePath, - "CHAIN_ID=" + providerChainID, - "DENOM=" + bondDenom, - "MNEMONIC=" + relayerMnemonic, - }) - - // Modify genesis on the host: set fast voting period and small blocks_per_epoch - genesisFile := filepath.Join(providerDir, "config", "genesis.json") - s.patchGenesisJSON(genesisFile, func(genesis map[string]any) { - appState := genesis["app_state"].(map[string]any) - - // Set fast voting period - if gov, ok := appState["gov"].(map[string]any); ok { - if params, ok := gov["params"].(map[string]any); ok { - params["voting_period"] = "15s" - } - } - - // Set fast epoch for VSC and a small per-block fee amount. The fee - // denom is fixed to feeDenom at module wiring, only the amount is - // configurable here. val is funded with feeDenom in provider-init.sh - if provider, ok := appState["provider"].(map[string]any); ok { - if params, ok := provider["params"].(map[string]any); ok { - params["blocks_per_epoch"] = "5" - params["fees_per_block_amount"] = "1000" - } - } - }) - - // Now start the actual provider container - s.T().Log("starting provider chain container...") - - resource, err := s.dkrPool.RunWithOptions( - &dockertest.RunOptions{ - Name: fmt.Sprintf("%s-val0", providerChainID), - Repository: e2eChainImage, - NetworkID: s.dkrNet.Network.ID, - Mounts: []string{ - fmt.Sprintf("%s:%s", providerDir, providerHomePath), - }, - PortBindings: map[docker.Port][]docker.PortBinding{ - "26657/tcp": {{HostIP: "", HostPort: "26657"}}, - "9090/tcp": {{HostIP: "", HostPort: "9090"}}, - "1317/tcp": {{HostIP: "", HostPort: "1317"}}, - "26656/tcp": {{HostIP: "", HostPort: "26656"}}, - }, - Cmd: []string{ - providerBinary, "start", - "--home", providerHomePath, - }, - }, - func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }, - ) - s.Require().NoError(err, "failed to start provider container") - - s.providerValRes = append(s.providerValRes, resource) - s.T().Logf("provider container started: %s", resource.Container.ID[:12]) - - // Wait for provider to produce blocks - waitCtx, waitCancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer waitCancel() - err = s.waitForChainHeight(waitCtx, "http://localhost:26657", 3) - s.Require().NoError(err, "provider failed to produce blocks") - s.T().Log("provider chain is producing blocks") -} - -// registerConsumerOnProvider creates a consumer chain registration on the provider. -func (s *IntegrationTestSuite) registerConsumerOnProvider() { - // Read consumer JSON template and substitute chain ID - templatePath := filepath.Join(testDir(), "testdata", "create_consumer.json") - templateBytes, err := os.ReadFile(templatePath) - s.Require().NoError(err, "failed to read create_consumer.json template") - - createConsumerJSON := strings.ReplaceAll(string(templateBytes), "CONSUMER_CHAIN_ID", consumerChainID) - - // Write JSON to container and execute tx - s.dockerExecMust(s.providerValRes[0].Container.ID, []string{ - "sh", "-c", fmt.Sprintf("echo '%s' > /tmp/create_consumer.json", createConsumerJSON), - }) - - stdout, stderr, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ - providerBinary, "tx", "provider", "create-consumer", "/tmp/create_consumer.json", - "--from", "val", - "--home", providerHomePath, - "--keyring-backend", "test", - "--chain-id", providerChainID, - "--gas", "auto", - "--gas-adjustment", "1.5", - "--fees", "10000" + bondDenom, - "--broadcast-mode", "sync", - "-y", - "-o", "json", - }) - s.Require().NoErrorf(err, "failed to run create-consumer: stderr=%s", stderr.String()) - - // Assert the tx was accepted. Without this, a tx rejected at CheckTx (e.g. - // an init-parameter validation failure) passes silently here and only - // surfaces later as an empty consumer genesis. - var res struct { - Code int `json:"code"` - RawLog string `json:"raw_log"` - } - s.Require().NoErrorf(json.Unmarshal(stdout.Bytes(), &res), - "failed to decode create-consumer response:\nstdout=%q\nstderr=%q", stdout.String(), stderr.String()) - s.Require().Equalf(0, res.Code, "create-consumer tx failed: %s", res.RawLog) - - // Wait for the tx to be included - time.Sleep(10 * time.Second) -} - -// fetchConsumerGenesis queries the provider for the consumer genesis data. -func (s *IntegrationTestSuite) fetchConsumerGenesis() []byte { - var output string - var lastErr error - - // Retry fetching consumer genesis (it may take a few blocks) - for range 30 { - stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ - providerBinary, "query", "provider", "consumer-genesis", "0", - "--home", providerHomePath, - "--output", "json", - }) - if err == nil { - output = stdout.String() - if output != "" && !strings.Contains(output, "not found") && !strings.Contains(output, "Error") { - break - } - } - lastErr = err - time.Sleep(3 * time.Second) - } - s.Require().NotEmpty(output, "consumer genesis is empty, last error: %v", lastErr) - - s.T().Logf("fetched consumer genesis (%d bytes)", len(output)) - return []byte(output) -} - -// initAndStartConsumer initializes the consumer chain and starts it. -func (s *IntegrationTestSuite) initAndStartConsumer(consumerGenesisJSON []byte) { - // Create host directory for consumer data - consumerDir, err := os.MkdirTemp("", "vaas-e2e-consumer-") - s.Require().NoError(err) - s.tmpDirs = append(s.tmpDirs, consumerDir) - s.consumer.dataDir = consumerDir - - s.Require().NoError(os.Chmod(consumerDir, 0o777)) - - // Run init script in a temporary container - scriptPath := filepath.Join(testDir(), "scripts", "consumer-init.sh") - s.runInitContainer("consumer-init", scriptPath, "/scripts/consumer-init.sh", consumerDir, consumerHomePath, []string{ - "BINARY=" + consumerBinary, - "HOME_DIR=" + consumerHomePath, - "CHAIN_ID=" + consumerChainID, - "DENOM=" + bondDenom, - "MNEMONIC=" + relayerMnemonic, - }) - - // Patch consumer genesis with provider's consumer-genesis data on the host - genesisFile := filepath.Join(consumerDir, "config", "genesis.json") - err = patchConsumerGenesisWithProviderData(genesisFile, consumerGenesisJSON) - s.Require().NoError(err, "failed to patch consumer genesis") - - // Patch consumer slashing params for aggressive downtime detection - s.patchConsumerSlashingParams() - - // Copy validator keys from provider to consumer - providerDir := s.provider.dataDir - err = copyFile( - filepath.Join(providerDir, "config", "priv_validator_key.json"), - filepath.Join(consumerDir, "config", "priv_validator_key.json"), - ) - s.Require().NoError(err, "failed to copy priv_validator_key.json") - - err = copyFile( - filepath.Join(providerDir, "config", "node_key.json"), - filepath.Join(consumerDir, "config", "node_key.json"), - ) - s.Require().NoError(err, "failed to copy node_key.json") - - // Start the actual consumer container - s.T().Log("starting consumer chain container...") - - resource, err := s.dkrPool.RunWithOptions( - &dockertest.RunOptions{ - Name: fmt.Sprintf("%s-val0", consumerChainID), - Repository: e2eChainImage, - NetworkID: s.dkrNet.Network.ID, - Mounts: []string{ - fmt.Sprintf("%s:%s", consumerDir, consumerHomePath), - }, - PortBindings: map[docker.Port][]docker.PortBinding{ - "26657/tcp": {{HostIP: "", HostPort: "26667"}}, - "9090/tcp": {{HostIP: "", HostPort: "9092"}}, - "1317/tcp": {{HostIP: "", HostPort: "1327"}}, - "26656/tcp": {{HostIP: "", HostPort: "26666"}}, - }, - Cmd: []string{ - consumerBinary, "start", - "--home", consumerHomePath, - }, - }, - func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }, - ) - s.Require().NoError(err, "failed to start consumer container") - - s.consumerValRes = append(s.consumerValRes, resource) - s.T().Logf("consumer container started: %s", resource.Container.ID[:12]) - - // Wait for consumer to produce blocks - waitCtx, waitCancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer waitCancel() - err = s.waitForChainHeight(waitCtx, "http://localhost:26667", 3) - s.Require().NoError(err, "consumer failed to produce blocks") - s.T().Log("consumer chain is producing blocks") -} - -// setupTSRelayer starts the ts-relayer container, configures it with -// mnemonics and gas prices for both chains, and creates an IBC v2 path. -// The ts-relayer handles counterparty registration and packet relaying. -func (s *IntegrationTestSuite) setupTSRelayer() { - s.startTSRelayer() - - s.tsRelayerAddMnemonic(providerChainID, relayerMnemonic) - s.tsRelayerAddMnemonic(consumerChainID, relayerMnemonic) - s.tsRelayerAddGasPrice(providerChainID, "0.025"+bondDenom) - s.tsRelayerAddGasPrice(consumerChainID, "0.025"+bondDenom) - - s.tsRelayerAddPath(IBCv2) - - s.tsRelayerDumpPaths() - s.startTSRelayerRelay() - s.T().Log("ts-relayer IBC v2 path configured") -} - -// waitForChainHeight polls a CometBFT RPC endpoint until the chain reaches -// the given block height. -func (s *IntegrationTestSuite) waitForChainHeight(ctx context.Context, rpcEndpoint string, minHeight int64) error { - ticker := time.NewTicker(2 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return fmt.Errorf("timeout waiting for chain at %s to reach height %d", rpcEndpoint, minHeight) - case <-ticker.C: - height, err := queryBlockHeight(rpcEndpoint) - if err != nil { - continue - } - if height >= minHeight { - return nil - } - } - } -} - // chmodRecursive changes permissions on a directory recursively. func chmodRecursive(path string, mode os.FileMode) error { cmd := exec.Command("chmod", "-R", fmt.Sprintf("%o", mode), path) diff --git a/tests/e2e/e2e_tsrelayer_test.go b/tests/e2e/e2e_tsrelayer_test.go index 87fb3d0..f782e15 100644 --- a/tests/e2e/e2e_tsrelayer_test.go +++ b/tests/e2e/e2e_tsrelayer_test.go @@ -34,12 +34,12 @@ func noRestart(config *docker.HostConfig) { config.RestartPolicy = docker.RestartPolicy{Name: "no"} } -func (s *IntegrationTestSuite) startTSRelayer() { +func (s *baseTestSuite) startTSRelayer() { s.T().Log("starting ts-relayer container") resource, err := s.dkrPool.RunWithOptions( &dockertest.RunOptions{ - Name: fmt.Sprintf("%s-%s-ts-relayer", providerChainID, consumerChainID), + Name: fmt.Sprintf("%s-%s-ts-relayer", s.cfg.providerChainID, s.cfg.consumerChainID), Repository: tsRelayerImage, Tag: tsRelayerImageTag, NetworkID: s.dkrNet.Network.ID, @@ -61,7 +61,7 @@ func (s *IntegrationTestSuite) startTSRelayer() { s.verifyTSRelayerConnectivity("consumer", consumerURL) } -func (s *IntegrationTestSuite) verifyTSRelayerConnectivity(chainName, rpcURL string) { +func (s *baseTestSuite) verifyTSRelayerConnectivity(chainName, rpcURL string) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -104,7 +104,7 @@ func (s *IntegrationTestSuite) verifyTSRelayerConnectivity(chainName, rpcURL str s.Require().Fail("ts-relayer cannot reach %s RPC at %s", chainName, rpcURL) } -func (s *IntegrationTestSuite) startTSRelayerRelay() { +func (s *baseTestSuite) startTSRelayerRelay() { s.T().Log("ts-relayer: starting relay process") ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -134,7 +134,7 @@ func (s *IntegrationTestSuite) startTSRelayerRelay() { s.T().Log("ts-relayer: relay process started") } -func (s *IntegrationTestSuite) stopTSRelayer() { +func (s *baseTestSuite) stopTSRelayer() { if s.tsRelayerResource != nil { s.T().Log("tearing down ts-relayer...") s.Require().NoError(s.dkrPool.Purge(s.tsRelayerResource)) @@ -142,7 +142,7 @@ func (s *IntegrationTestSuite) stopTSRelayer() { } } -func (s *IntegrationTestSuite) executeTSRelayerCommand(ctx context.Context, args []string) []byte { +func (s *baseTestSuite) executeTSRelayerCommand(ctx context.Context, args []string) []byte { tsRelayerBinary := []string{"/bin/with_keyring", "ibc-v2-ts-relayer"} cmd := append(tsRelayerBinary, args...) exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{ @@ -184,7 +184,7 @@ func (s *IntegrationTestSuite) executeTSRelayerCommand(ctx context.Context, args return out.Bytes() } -func (s *IntegrationTestSuite) tsRelayerAddMnemonic(chainID, mnemonic string) { +func (s *baseTestSuite) tsRelayerAddMnemonic(chainID, mnemonic string) { s.T().Logf("ts-relayer: adding mnemonic for chain %s", chainID) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -195,7 +195,7 @@ func (s *IntegrationTestSuite) tsRelayerAddMnemonic(chainID, mnemonic string) { }) } -func (s *IntegrationTestSuite) tsRelayerAddGasPrice(chainID, gasPrice string) { +func (s *baseTestSuite) tsRelayerAddGasPrice(chainID, gasPrice string) { s.T().Logf("ts-relayer: adding gas-price for chain %s", chainID) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -207,14 +207,14 @@ func (s *IntegrationTestSuite) tsRelayerAddGasPrice(chainID, gasPrice string) { }) } -func (s *IntegrationTestSuite) tsRelayerAddPath(ibcVersion string) { - s.T().Logf("ts-relayer: adding IBCv%s path between %s and %s", ibcVersion, providerChainID, consumerChainID) +func (s *baseTestSuite) tsRelayerAddPath(ibcVersion string) { + s.T().Logf("ts-relayer: adding IBCv%s path between %s and %s", ibcVersion, s.cfg.providerChainID, s.cfg.consumerChainID) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() s.executeTSRelayerCommand(ctx, []string{ "add-path", - "-s", providerChainID, - "-d", consumerChainID, + "-s", s.cfg.providerChainID, + "-d", s.cfg.consumerChainID, "--surl", "http://" + s.providerValRes[0].Container.Name[1:] + ":26657", "--durl", "http://" + s.consumerValRes[0].Container.Name[1:] + ":26657", "--st", "cosmos", @@ -225,7 +225,7 @@ func (s *IntegrationTestSuite) tsRelayerAddPath(ibcVersion string) { var ansiEscapeRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`) -func (s *IntegrationTestSuite) tsRelayerDumpPaths() []tsRelayerPath { +func (s *baseTestSuite) tsRelayerDumpPaths() []tsRelayerPath { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() out := s.executeTSRelayerCommand(ctx, []string{"dump-paths"}) @@ -241,7 +241,7 @@ func (s *IntegrationTestSuite) tsRelayerDumpPaths() []tsRelayerPath { return paths } -func (s *IntegrationTestSuite) collectChainDiagnostics() string { +func (s *baseTestSuite) collectChainDiagnostics() string { var sb strings.Builder for _, chain := range []struct { name string @@ -299,7 +299,7 @@ func (s *IntegrationTestSuite) collectIBCDiagnosticsLog() { } } -func (s *IntegrationTestSuite) collectIBCDiagnostics() string { +func (s *baseTestSuite) collectIBCDiagnostics() string { var sb strings.Builder chains := []struct { name string diff --git a/tests/e2e/genesis_test.go b/tests/e2e/genesis_test.go index 79641e1..ee2608f 100644 --- a/tests/e2e/genesis_test.go +++ b/tests/e2e/genesis_test.go @@ -43,7 +43,7 @@ func patchConsumerGenesisWithProviderData(genesisFilePath string, consumerGenesi // patchGenesisJSON reads a genesis.json file, applies a mutation function, // and writes it back. -func (s *IntegrationTestSuite) patchGenesisJSON(path string, mutate func(map[string]any)) { +func (s *baseTestSuite) patchGenesisJSON(path string, mutate func(map[string]any)) { bz, err := os.ReadFile(path) s.Require().NoError(err, "failed to read genesis file") diff --git a/tests/e2e/gov_proposal_helpers_test.go b/tests/e2e/gov_proposal_helpers_test.go index 15ed76d..be69c0e 100644 --- a/tests/e2e/gov_proposal_helpers_test.go +++ b/tests/e2e/gov_proposal_helpers_test.go @@ -13,7 +13,7 @@ import ( // queryModuleAccountAddress returns the bech32 address of the named module // account on the provider chain. -func (s *IntegrationTestSuite) queryModuleAccountAddress(name string) string { +func (s *baseTestSuite) queryModuleAccountAddress(name string) string { stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ providerBinary, "query", "auth", "module-account", name, "--home", providerHomePath, @@ -57,7 +57,7 @@ func (s *IntegrationTestSuite) queryModuleAccountAddress(name string) string { } // queryGovAuthority is a thin wrapper for queryModuleAccountAddress("gov"). -func (s *IntegrationTestSuite) queryGovAuthority() string { +func (s *baseTestSuite) queryGovAuthority() string { return s.queryModuleAccountAddress("gov") } @@ -97,7 +97,7 @@ func (s *IntegrationTestSuite) queryCommunityPoolBalance(denom string) int64 { // // proposalJSON must be a valid gov v1 proposal body. The submitter and voting // fees are paid in bondDenom by val. -func (s *IntegrationTestSuite) submitAndPassProposal(proposalJSON string) uint64 { +func (s *baseTestSuite) submitAndPassProposal(proposalJSON string) uint64 { containerID := s.providerValRes[0].Container.ID // 1. Write the proposal body to /tmp/proposal.json via base64 to avoid @@ -115,7 +115,7 @@ func (s *IntegrationTestSuite) submitAndPassProposal(proposalJSON string) uint64 "--from", "val", "--home", providerHomePath, "--keyring-backend", "test", - "--chain-id", providerChainID, + "--chain-id", s.cfg.providerChainID, "--fees", "10000" + bondDenom, "--yes", "-o", "json", @@ -158,7 +158,7 @@ func (s *IntegrationTestSuite) submitAndPassProposal(proposalJSON string) uint64 "--from", "val", "--home", providerHomePath, "--keyring-backend", "test", - "--chain-id", providerChainID, + "--chain-id", s.cfg.providerChainID, "--fees", "10000" + bondDenom, "--yes", }) @@ -188,7 +188,7 @@ func (s *IntegrationTestSuite) submitAndPassProposal(proposalJSON string) uint64 // dumpProposal returns the raw JSON of a gov proposal query, used to surface // the failed_reason / messages on terminal-bad status. -func (s *IntegrationTestSuite) dumpProposal(proposalID uint64) string { +func (s *baseTestSuite) dumpProposal(proposalID uint64) string { stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ providerBinary, "query", "gov", "proposal", fmt.Sprintf("%d", proposalID), "--home", providerHomePath, @@ -202,7 +202,7 @@ func (s *IntegrationTestSuite) dumpProposal(proposalID uint64) string { // queryProposalStatus returns the textual status of a gov proposal (e.g. // "PROPOSAL_STATUS_PASSED"). -func (s *IntegrationTestSuite) queryProposalStatus(proposalID uint64) string { +func (s *baseTestSuite) queryProposalStatus(proposalID uint64) string { stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ providerBinary, "query", "gov", "proposal", fmt.Sprintf("%d", proposalID), "--home", providerHomePath, diff --git a/tests/e2e/query_test.go b/tests/e2e/query_test.go index d7d3638..6df08e4 100644 --- a/tests/e2e/query_test.go +++ b/tests/e2e/query_test.go @@ -1,6 +1,7 @@ package e2e import ( + "encoding/json" "fmt" "strings" @@ -9,6 +10,28 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) +// queryProviderConsumerPhase returns the phase string for a given consumer ID +// from the provider chain (e.g. "CONSUMER_PHASE_LAUNCHED"). +func (s *baseTestSuite) queryProviderConsumerPhase(consumerID string) string { + stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "provider", "consumer-chain", consumerID, + "--home", providerHomePath, + "--output", "json", + }) + if err != nil { + s.T().Logf("queryProviderConsumerPhase(%s): exec error: %v", consumerID, err) + return "" + } + var res struct { + Phase string `json:"phase"` + } + if err := json.Unmarshal(stdout.Bytes(), &res); err != nil { + s.T().Logf("queryProviderConsumerPhase(%s): decode error: %v (raw: %s)", consumerID, err, stdout.String()) + return "" + } + return res.Phase +} + // providerRESTEndpoint returns the provider chain's REST HTTP endpoint using the Docker-assigned host port. func (s *IntegrationTestSuite) providerRESTEndpoint() string { return fmt.Sprintf("http://%s", s.providerValRes[0].GetHostPort("1317/tcp")) From d52ebfac53d33dbedc62cd5007a8ec2bdc4144c4 Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:50:53 +0200 Subject: [PATCH 20/20] describe the liveness design in the present tense Rewrite docs/consumer-liveness.md and the liveness test comments to describe the current design only, dropping narration of how things used to behave (it is confusing context for unreleased software): the "why this exists" framing becomes a present-tense design rationale, the IBC-callbacks section states they are log-only rather than "no longer remove", and the VaasTimeoutPeriod note drops the "lowered from four weeks" history. Also drop the mention of the short-lived vaas_timeout floor from a test comment (added and removed within this branch). --- docs/consumer-liveness.md | 59 +++++++++++-------------- tests/e2e/e2e_consumer_liveness_test.go | 14 +++--- tests/e2e/e2e_liveness_suite_test.go | 15 +++---- x/vaas/provider/keeper/relay_test.go | 2 +- 4 files changed, 42 insertions(+), 48 deletions(-) diff --git a/docs/consumer-liveness.md b/docs/consumer-liveness.md index bef8c9a..b5f4b7c 100644 --- a/docs/consumer-liveness.md +++ b/docs/consumer-liveness.md @@ -8,26 +8,24 @@ It complements [consumer-lifecycle.md](consumer-lifecycle.md), which covers the `REGISTERED -> INITIALIZED -> LAUNCHED -> STOPPED -> DELETED` progression. This document zooms in on what keeps a `LAUNCHED` consumer healthy and what happens when it is not. -## Why this exists +## Design rationale The provider sends a Validator Set Change (VSC) packet to each launched consumer every -epoch. Earlier, a single failure to deliver one of those packets -- an IBC packet timeout, -an error acknowledgement, or a local send failure -- immediately and irreversibly moved the -consumer to `STOPPED`. Two facts made that too severe: - -1. **The packet timeout is effectively 24h, not the configured value.** VSC packets are - sent with a timeout of `min(VaasTimeoutPeriod, MaxTimeoutDelta)`, and IBC v2 hard-caps - `MaxTimeoutDelta` at 24h. So a 24h outage of a single relayer or RPC was enough to - permanently retire a consumer. -2. **VSC packets are diffs.** Each packet carries only the validators that changed since - the previous one, applied on top of the consumer's current set. If a packet is lost, its - change is never re-sent on its own -- so dropping the eager removal would, by itself, - leave a departed validator stuck in the consumer's active set (a validator that could - later unbond on the provider and validate the consumer with no slashable stake). - -The model below replaces the per-packet kill switch with a provider-local liveness clock, -pairs it with a self-healing resync so lost diffs cannot strand a stale validator, and adds -a consumer-side safe mode for the window in which a consumer's set may be out of date. +epoch. Two facts about that delivery shape the design: + +1. **The packet timeout is effectively 24h.** VSC packets are sent with a timeout of + `min(VaasTimeoutPeriod, MaxTimeoutDelta)`, and IBC v2 hard-caps `MaxTimeoutDelta` at 24h. + A packet timeout is therefore a weak signal -- a 24h outage of a single relayer or RPC + produces one -- so removing a consumer must not hinge on a single delivery failure. +2. **VSC packets are diffs.** Each packet carries only the validators that changed since the + previous one, applied on top of the consumer's current set. A lost diff is never re-sent + on its own, so a departed validator could otherwise linger in the consumer's active set -- + able to unbond on the provider and validate the consumer with no slashable stake. + +The model has three parts that answer these facts: a provider-local liveness clock that +decides removal on the provider's own block time rather than on per-packet delivery; a +self-healing snapshot resync so a lost diff cannot strand a stale validator; and a +consumer-side safe mode for the window in which a consumer's set may be out of date. ## 1. Liveness clock and removal sweep @@ -86,23 +84,22 @@ The wire format gains only a single boolean (`is_snapshot`); the validator-updat reused (a diff when false, the full set when true). The global VSC id counter and the consumer's out-of-order dedup are unchanged. -**Guarantee:** a transient outage no longer corrupts the consumer's set. As soon as +**Guarantee:** a transient outage does not corrupt the consumer's set. As soon as connectivity returns, the next snapshot heals it -- including removing any validator that left the provider during the outage. -## 3. IBC callbacks no longer remove +## 3. IBC callbacks are log-only -With the sweep as the single authority for removing unresponsive consumers, the IBC -callbacks that used to remove are demoted to logging: +The sweep is the single authority for removing an unresponsive consumer, so the IBC +delivery-failure callbacks only log and return: - **Packet timeout** (`OnTimeoutPacketV2`): logs and returns; the next epoch retries. - **Error acknowledgement**: logs and returns; the consumer keeps its grace budget. - **Local send failure** (including an expired client): logs, leaves the pending packets queued, and returns; the next epoch retries. -This also closes a latent gap: previously an expired client left a consumer `LAUNCHED` -forever with no removal path. Now the sweep removes it once its grace elapses, because an -expired client produces no acks. +This keeps an expired client from stranding a consumer: an expired client produces no acks, +so the sweep removes such a consumer once its grace elapses. ## 4. Consumer safe mode @@ -117,7 +114,7 @@ the provider's sweep would remove it. While stale (or while flagged in debt for unpaid fees), the consumer's transaction admission gate restricts incoming transactions to `/ibc.core.*` and `/cosmos.gov.*` messages only, rejecting value-bearing application transactions. This bounds the damage of a possibly-stale -set: the consumer refuses to do value-bearing work under validators it can no longer trust, +set: the consumer refuses to do value-bearing work under a validator set it cannot trust, while keeping the recovery path open -- `/ibc.core.*` still admits the incoming VSC packets that let the consumer resync, and `/cosmos.gov.*` keeps governance available. A freshly launched consumer is never treated as stale before its first VSC. @@ -133,12 +130,10 @@ launched consumer is never treated as stale before its first VSC. `VaasTimeoutPeriod` is validated to `(0, 24h]` at both the provider module param boundary and the per-consumer initialization-parameter boundary, so the configured value is honest -rather than silently capped. The default was lowered from four weeks (which collapsed to 24h -under the cap) to one epoch-scale hour: an undelivered packet is superseded by the next -epoch's packet and a late one is dropped by the consumer's dedup, so a long timeout buys -nothing. No lower floor is imposed: a persistently unreachable consumer is handled by the -liveness sweep, so a short timeout is not dangerous -- and a floor would only prevent -exercising the timeout path in tests. +rather than silently capped. The default is one epoch-scale hour: an undelivered packet is +superseded by the next epoch's packet and a late one is dropped by the consumer's dedup, so +a long timeout buys nothing. No lower floor is imposed either: a persistently unreachable +consumer is handled by the liveness sweep, so a short timeout is not dangerous. The consumer `UnbondingPeriod` is bounded against the provider's at `MsgCreateConsumer` / `MsgUpdateConsumer` time (it must not exceed the provider's), so a consumer cannot be diff --git a/tests/e2e/e2e_consumer_liveness_test.go b/tests/e2e/e2e_consumer_liveness_test.go index 5882fdb..59bdd2a 100644 --- a/tests/e2e/e2e_consumer_liveness_test.go +++ b/tests/e2e/e2e_consumer_liveness_test.go @@ -33,9 +33,9 @@ package e2e // changes, then verify the consumer stays LAUNCHED and its VP re-converges after // recovery. This is an end-to-end smoke of recovery-after-outage; it does not by // itself prove *how* the consumer re-converged (the un-timed-out packet could be -// resent). That the demoted timeout no longer removes the consumer and that a -// behind consumer heals via a full snapshot are proven deterministically by unit -// tests (TestTimeoutDoesNotRemove, the TestSnapshot* set, TestSnapshotResyncEmitsEvent) +// resent). That a timeout does not remove the consumer and that a behind consumer +// heals via a full snapshot are proven deterministically by unit tests +// (TestTimeoutDoesNotRemove, the TestSnapshot* set, TestSnapshotResyncEmitsEvent) // and end-to-end by LivenessIntegrationTestSuite's forced-timeout snapshot test. // // Sequencing in TestVAAS: @@ -53,7 +53,7 @@ import ( // and that after recovery the consumer's consensus VP re-converges with the // provider's updated VP. It does not assert the *mechanism* of re-convergence // (the un-timed-out VSC packet could be resent rather than replaced by a -// snapshot); the snapshot path and the demoted timeout are covered by unit tests +// snapshot); the snapshot path and the log-only timeout are covered by unit tests // and by LivenessIntegrationTestSuite's forced-timeout snapshot test. func (s *IntegrationTestSuite) testLivenessTransientOutage() { s.Run("liveness transient outage: consumer stays LAUNCHED and re-converges", func() { @@ -150,9 +150,9 @@ func (s *IntegrationTestSuite) testLivenessTransientOutage() { // testLivenessRemoval verifies the LAUNCHED -> STOPPED lifecycle when a // consumer is explicitly removed via MsgRemoveConsumer. // -// Issue #36 context: the redesign makes explicit removal (and the liveness -// sweep) the only way to stop a consumer. Timed-out VSC packets no longer -// trigger removal. This test exercises the explicit removal path. +// Explicit governance removal and the liveness sweep are the only ways to stop a +// consumer; a timed-out VSC packet does not. This test exercises the explicit +// removal path. // // Ordering: this test must run after all other sub-tests that depend on // consumer "0" being LAUNCHED, and before testGenesisRoundTrip (which still diff --git a/tests/e2e/e2e_liveness_suite_test.go b/tests/e2e/e2e_liveness_suite_test.go index b8a1d41..1551c46 100644 --- a/tests/e2e/e2e_liveness_suite_test.go +++ b/tests/e2e/e2e_liveness_suite_test.go @@ -37,7 +37,7 @@ package e2e // 4. testForcedTimeoutSnapshotResync - relayer paused > the short (20s) provider // vaas_timeout while the consumer keeps producing // blocks, so a VSC packet genuinely times out; the -// consumer stays LAUNCHED (demoted OnTimeout) and +// consumer stays LAUNCHED (log-only OnTimeout) and // heals via a snapshot resync (asserted via the // provider timeout log and the consumer's // snapshot-resync event). @@ -134,8 +134,7 @@ func (s *LivenessIntegrationTestSuite) SetupSuite() { // Short VSC packet timeout so testForcedTimeoutSnapshotResync can // make a packet actually time out within a CI run (the relayer is // paused while the consumer keeps producing blocks past this - // deadline). Now possible since the MinVAASTimeoutPeriod floor was - // dropped. Timeouts are log-only, so this does not perturb the + // deadline). Timeouts are log-only, so this does not perturb the // other liveness tests. params["vaas_timeout_period"] = "20s" // Shrink the liveness grace fraction so the grace period @@ -430,7 +429,7 @@ func (s *LivenessIntegrationTestSuite) testLivenessQuery() { // testForcedTimeoutSnapshotResync proves the two behaviours the main suite's // transient-outage smoke cannot: that a genuinely timed-out VSC packet does NOT -// remove the consumer (the demoted OnTimeout), and that a consumer that fell +// remove the consumer (the log-only OnTimeout), and that a consumer that fell // behind heals via a snapshot resync (not a resent diff). // // A real IBC timeout requires the packet to expire on the *consumer's* clock @@ -441,7 +440,7 @@ func (s *LivenessIntegrationTestSuite) testLivenessQuery() { // (the provider's OnTimeout fires, log-only) and delivers a snapshot to the // now-behind consumer. func (s *LivenessIntegrationTestSuite) testForcedTimeoutSnapshotResync() { - s.Run("forced timeout: demoted OnTimeout keeps consumer LAUNCHED; behind consumer heals via snapshot", func() { + s.Run("forced timeout: log-only OnTimeout keeps consumer LAUNCHED; behind consumer heals via snapshot", func() { const consumerID = "0" s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", s.queryProviderConsumerPhase(consumerID), @@ -458,7 +457,7 @@ func (s *LivenessIntegrationTestSuite) testForcedTimeoutSnapshotResync() { s.Require().NoError(s.dkrPool.Client.UnpauseContainer(s.tsRelayerResource.Container.ID), "failed to unpause relayer container") - // (a) The demoted OnTimeout must not have removed the consumer. + // (a) The log-only OnTimeout must not have removed the consumer. s.Require().Eventuallyf(func() bool { return s.queryProviderConsumerPhase(consumerID) == "CONSUMER_PHASE_LAUNCHED" }, 30*time.Second, 3*time.Second, @@ -466,12 +465,12 @@ func (s *LivenessIntegrationTestSuite) testForcedTimeoutSnapshotResync() { s.T().Log("consumer remained LAUNCHED through the timeouts") // (b) Prove a real timeout actually fired -- otherwise (a) is vacuous. - // The provider logs the demoted OnTimeout handler. + // The provider logs the log-only OnTimeout handler. s.Require().Eventuallyf(func() bool { return strings.Contains(s.providerLogs(), "packet timeout, retrying next epoch") }, 30*time.Second, 3*time.Second, "provider never logged a VSC packet timeout; the timeout path was not exercised") - s.T().Log("provider processed a demoted VSC timeout (OnTimeout is log-only)") + s.T().Log("provider processed a VSC timeout (OnTimeout is log-only)") // (c) Prove the behind consumer healed via a SNAPSHOT (not a resent diff): // the consumer logs "applied snapshot resync" (and emits the matching diff --git a/x/vaas/provider/keeper/relay_test.go b/x/vaas/provider/keeper/relay_test.go index 9650a83..41e112d 100644 --- a/x/vaas/provider/keeper/relay_test.go +++ b/x/vaas/provider/keeper/relay_test.go @@ -77,7 +77,7 @@ func TestOnTimeoutPacketV2(t *testing.T) { phase := providerKeeper.GetConsumerPhase(ctx, consumerId) require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) - // Timeout is now log-only; liveness sweep owns removal + // Timeout is log-only; liveness sweep owns removal err := providerKeeper.OnTimeoutPacketV2(ctx, clientId) require.NoError(t, err)