Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion proto/vaas/provider/v1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ message ConsumerState {
// the phase of the consumer chain
ConsumerPhase phase = 8;
// OwnerAddress is the bech32 address that authorises owner-only operations
// (UpdateConsumer, RemoveConsumer, key assignment, etc.) on the consumer.
// (UpdateConsumer, key assignment, etc.) on the consumer.
// Always non-empty for any existing consumer record; set at REGISTERED.
string owner_address = 9;
// Metadata is the owner-supplied descriptor (name/description/repo).
Expand Down
6 changes: 6 additions & 0 deletions proto/vaas/provider/v1/provider.proto
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ enum ConsumerPhase {
message InfractionParameters {
SlashJailParameters double_sign = 1;
SlashJailParameters downtime = 2;
// downtime_grace_period is the duration after a consumer chain launches
// during which downtime slashing is suppressed. This gives validators time
// to spin up their consumer chain nodes without being penalized for early
// downtime. Set to 0 to disable the grace period.
google.protobuf.Duration downtime_grace_period = 3
[ (gogoproto.nullable) = false, (gogoproto.stdduration) = true ];
}

//
Expand Down
7 changes: 4 additions & 3 deletions proto/vaas/provider/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,14 @@ message MsgUpdateParamsResponse {}

// MsgRemoveConsumer defines the message used to remove (and stop) a consumer chain.
// If it passes, all the consumer chain's state is eventually removed from the provider chain.
// Only the governance authority can remove a consumer chain.
message MsgRemoveConsumer {
option (cosmos.msg.v1.signer) = "owner";
option (cosmos.msg.v1.signer) = "authority";

// the consumer id of the consumer chain to be stopped
uint64 consumer_id = 1;
// the address of the owner of the consumer chain to be stopped
string owner = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// authority is the address of the governance account.
string authority = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
}

// MsgRemoveConsumerResponse defines response type for MsgRemoveConsumer messages
Expand Down
4 changes: 2 additions & 2 deletions x/vaas/provider/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,8 @@ where create_consumer.json has the following structure:
}

Note that both 'chain_id' and 'metadata' are mandatory;
and 'initialization_parameters' is optional.
The parameters not provided are set to their zero value.
and 'initialization_parameters' is optional.
The parameters not provided are set to their zero value.
`, version.AppName)),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down
35 changes: 30 additions & 5 deletions x/vaas/provider/keeper/consumer_equivocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,17 +203,44 @@ func (k Keeper) HandleConsumerEvidencePacket(ctx sdk.Context, consumerId uint64,

// HandleConsumerDowntime slashes a validator that was offline on a consumer chain.
// The provider verifies the downtime claim by checking:
// 1. The infraction height is not older than the minimum evidence height for this consumer.
// 2. The provider's IBC client for the consumer has a consensus state at the infraction height,
// 1. The consumer chain is outside its downtime grace period (if configured).
// 2. The infraction height is not older than the minimum evidence height for this consumer.
// 3. The provider's IBC client for the consumer has a consensus state at the infraction height,
// proving the consumer chain actually reached that height.
// 3. The validator was part of the consumer's validator set at the time of the infraction.
// 4. The validator was part of the consumer's validator set at the time of the infraction.
//
// CONTRACT: A downtime infraction must never jail a validator.
func (k Keeper) HandleConsumerDowntime(ctx sdk.Context, consumerId uint64, evidencePacket vaastypes.EvidencePacketData) error {
consumerAddr := types.NewConsumerConsAddress(evidencePacket.ValidatorAddr)

providerAddr := k.GetProviderAddrFromConsumerAddr(ctx, consumerId, consumerAddr)

// Check that the consumer chain is outside its downtime grace period.
// During the grace period after launch, downtime evidence is suppressed to give
// validators time to spin up their consumer chain nodes.
infractionParams := k.GetInfractionParams(ctx)
if infractionParams.DowntimeGracePeriod > 0 {
initParams, err := k.GetConsumerInitializationParameters(ctx, consumerId)
if err != nil {
return errorsmod.Wrapf(
vaastypes.ErrInvalidConsumerState,
"cannot get initialization parameters for consumer chain %d: %s",
consumerId, err,
)
}
gracePeriodEnd := initParams.SpawnTime.Add(infractionParams.DowntimeGracePeriod)
Comment thread
julienrbrt marked this conversation as resolved.
if ctx.BlockTime().Before(gracePeriodEnd) {
Comment thread
julienrbrt marked this conversation as resolved.
return errorsmod.Wrapf(
vaastypes.ErrInvalidPacketData,
"consumer chain %d is still in downtime grace period (launched %s, grace ends %s, now %s)",
consumerId,
initParams.SpawnTime.UTC(),
gracePeriodEnd.UTC(),
ctx.BlockTime().UTC(),
)
}
}

// Verify the infraction height is not too old.
minHeight := k.GetEquivocationEvidenceMinHeight(ctx, consumerId)
if uint64(evidencePacket.InfractionHeight) < minHeight {
Expand Down Expand Up @@ -270,8 +297,6 @@ func (k Keeper) HandleConsumerDowntime(ctx sdk.Context, consumerId uint64, evide
)
}

infractionParams := k.GetInfractionParams(ctx)

if err := k.SlashValidator(ctx, providerAddr, infractionParams.Downtime, stakingtypes.Infraction_INFRACTION_DOWNTIME); err != nil {
return err
}
Expand Down
152 changes: 151 additions & 1 deletion x/vaas/provider/keeper/consumer_equivocation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,9 @@ func TestHandleConsumerEvidencePacket(t *testing.T) {
providerKeeper.SetConsumerChainId(ctx, consumerId, "consumer-chain")
providerKeeper.SetConsumerClientId(ctx, consumerId, "07-tendermint-0")
providerKeeper.SetEquivocationEvidenceMinHeight(ctx, consumerId, 1)
providerKeeper.SetInfractionParams(ctx, types.DefaultInfractionParameters())
noGraceParams := types.DefaultInfractionParameters()
noGraceParams.DowntimeGracePeriod = 0
providerKeeper.SetInfractionParams(ctx, noGraceParams)

pubKey, _ := cryptocodec.FromCmtPubKeyInterface(tmtypes.NewMockPV().PrivKey.PubKey())
validator, err := stakingtypes.NewValidator(
Expand Down Expand Up @@ -909,6 +911,9 @@ func TestHandleConsumerDowntimeRejectsTooOldEvidence(t *testing.T) {
providerKeeper.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_LAUNCHED)
providerKeeper.SetConsumerChainId(ctx, consumerId, "consumer-chain")
providerKeeper.SetEquivocationEvidenceMinHeight(ctx, consumerId, 200)
noGraceParams := types.DefaultInfractionParameters()
noGraceParams.DowntimeGracePeriod = 0
providerKeeper.SetInfractionParams(ctx, noGraceParams)

evidencePacket := vaastypes.NewEvidencePacketData(
sdk.ConsAddress([]byte{0x01, 0x02, 0x03}),
Expand All @@ -930,6 +935,9 @@ func TestHandleConsumerDowntimeRejectsNoClient(t *testing.T) {
providerKeeper.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_LAUNCHED)
providerKeeper.SetConsumerChainId(ctx, consumerId, "consumer-chain")
providerKeeper.SetEquivocationEvidenceMinHeight(ctx, consumerId, 1)
noGraceParams := types.DefaultInfractionParameters()
noGraceParams.DowntimeGracePeriod = 0
providerKeeper.SetInfractionParams(ctx, noGraceParams)

evidencePacket := vaastypes.NewEvidencePacketData(
sdk.ConsAddress([]byte{0x01, 0x02, 0x03}),
Expand All @@ -952,6 +960,9 @@ func TestHandleConsumerDowntimeRejectsNoConsensusState(t *testing.T) {
providerKeeper.SetConsumerChainId(ctx, consumerId, "consumer-chain")
providerKeeper.SetConsumerClientId(ctx, consumerId, "07-tendermint-0")
providerKeeper.SetEquivocationEvidenceMinHeight(ctx, consumerId, 1)
noGraceParams := types.DefaultInfractionParameters()
noGraceParams.DowntimeGracePeriod = 0
providerKeeper.SetInfractionParams(ctx, noGraceParams)

evidencePacket := vaastypes.NewEvidencePacketData(
sdk.ConsAddress([]byte{0x01, 0x02, 0x03}),
Expand Down Expand Up @@ -981,6 +992,9 @@ func TestHandleConsumerDowntimeRejectsValidatorNotInSet(t *testing.T) {
providerKeeper.SetConsumerChainId(ctx, consumerId, "consumer-chain")
providerKeeper.SetConsumerClientId(ctx, consumerId, "07-tendermint-0")
providerKeeper.SetEquivocationEvidenceMinHeight(ctx, consumerId, 1)
noGraceParams := types.DefaultInfractionParameters()
noGraceParams.DowntimeGracePeriod = 0
providerKeeper.SetInfractionParams(ctx, noGraceParams)

// Use a validator that is NOT in the consumer's validator set
evidencePacket := vaastypes.NewEvidencePacketData(
Expand Down Expand Up @@ -1011,6 +1025,9 @@ func TestHandleConsumerDowntimeRejectsInfractionBeforeJoin(t *testing.T) {
providerKeeper.SetConsumerChainId(ctx, consumerId, "consumer-chain")
providerKeeper.SetConsumerClientId(ctx, consumerId, "07-tendermint-0")
providerKeeper.SetEquivocationEvidenceMinHeight(ctx, consumerId, 1)
noGraceParams := types.DefaultInfractionParameters()
noGraceParams.DowntimeGracePeriod = 0
providerKeeper.SetInfractionParams(ctx, noGraceParams)

pubKey, _ := cryptocodec.FromCmtPubKeyInterface(tmtypes.NewMockPV().PrivKey.PubKey())
validator, err := stakingtypes.NewValidator(
Expand Down Expand Up @@ -1099,3 +1116,136 @@ func TestEvidencePacketDataJSONRoundTrip(t *testing.T) {
require.Equal(t, packet.InfractionHeight, decoded.InfractionHeight)
require.Equal(t, packet.Infraction, decoded.Infraction)
}

func TestHandleConsumerDowntimeRejectsDuringGracePeriod(t *testing.T) {
keeperParams := testkeeper.NewInMemKeeperParams(t)
providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, keeperParams)
defer ctrl.Finish()

consumerId := uint64(0)
providerKeeper.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_LAUNCHED)
providerKeeper.SetConsumerChainId(ctx, consumerId, "consumer-chain")

spawnTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
gracePeriod := 24 * time.Hour
now := spawnTime.Add(12 * time.Hour) // still within grace period
ctx = ctx.WithBlockTime(now)

providerKeeper.SetConsumerInitializationParameters(ctx, consumerId, types.ConsumerInitializationParameters{
SpawnTime: spawnTime,
})

providerKeeper.SetInfractionParams(ctx, types.InfractionParameters{
DoubleSign: &types.SlashJailParameters{
SlashFraction: math.LegacyMustNewDecFromStr("0.05"),
JailDuration: time.Duration(1<<63 - 1),
Tombstone: true,
},
Downtime: &types.SlashJailParameters{
SlashFraction: math.LegacyMustNewDecFromStr("0.0005"),
JailDuration: 0,
Tombstone: false,
},
DowntimeGracePeriod: gracePeriod,
})

evidencePacket := vaastypes.NewEvidencePacketData(
sdk.ConsAddress([]byte{0x01, 0x02, 0x03}),
100,
stakingtypes.Infraction_INFRACTION_DOWNTIME,
)

err := providerKeeper.HandleConsumerEvidencePacket(ctx, consumerId, evidencePacket)
require.Error(t, err)
require.Contains(t, err.Error(), "grace period")
}

func TestHandleConsumerDowntimePassesAfterGracePeriod(t *testing.T) {
keeperParams := testkeeper.NewInMemKeeperParams(t)
providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, keeperParams)
defer ctrl.Finish()

consumerId := uint64(0)
providerKeeper.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_LAUNCHED)
providerKeeper.SetConsumerChainId(ctx, consumerId, "consumer-chain")

spawnTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
gracePeriod := 24 * time.Hour
now := spawnTime.Add(48 * time.Hour) // past grace period
ctx = ctx.WithBlockTime(now)

providerKeeper.SetConsumerInitializationParameters(ctx, consumerId, types.ConsumerInitializationParameters{
SpawnTime: spawnTime,
})

providerKeeper.SetInfractionParams(ctx, types.InfractionParameters{
DoubleSign: &types.SlashJailParameters{
SlashFraction: math.LegacyMustNewDecFromStr("0.05"),
JailDuration: time.Duration(1<<63 - 1),
Tombstone: true,
},
Downtime: &types.SlashJailParameters{
SlashFraction: math.LegacyMustNewDecFromStr("0.0005"),
JailDuration: 0,
Tombstone: false,
},
DowntimeGracePeriod: gracePeriod,
})

evidencePacket := vaastypes.NewEvidencePacketData(
sdk.ConsAddress([]byte{0x01, 0x02, 0x03}),
100,
stakingtypes.Infraction_INFRACTION_DOWNTIME,
)

// Grace period check passes; the next validation (too-old evidence) should
// fail because no min-height is set (defaults to 0).
err := providerKeeper.HandleConsumerEvidencePacket(ctx, consumerId, evidencePacket)
require.Error(t, err)
require.NotContains(t, err.Error(), "grace period")
}

func TestHandleConsumerDowntimeNoGracePeriodSkipsCheck(t *testing.T) {
keeperParams := testkeeper.NewInMemKeeperParams(t)
providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, keeperParams)
defer ctrl.Finish()

consumerId := uint64(0)
providerKeeper.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_LAUNCHED)
providerKeeper.SetConsumerChainId(ctx, consumerId, "consumer-chain")

// spawn time is very recent but grace period is 0 (disabled)
spawnTime := time.Now()
ctx = ctx.WithBlockTime(spawnTime)

providerKeeper.SetConsumerInitializationParameters(ctx, consumerId, types.ConsumerInitializationParameters{
SpawnTime: spawnTime,
})

providerKeeper.SetInfractionParams(ctx, types.InfractionParameters{
DoubleSign: &types.SlashJailParameters{
SlashFraction: math.LegacyMustNewDecFromStr("0.05"),
JailDuration: time.Duration(1<<63 - 1),
Tombstone: true,
},
Downtime: &types.SlashJailParameters{
SlashFraction: math.LegacyMustNewDecFromStr("0.0005"),
JailDuration: 0,
Tombstone: false,
},
DowntimeGracePeriod: 0, // grace period disabled
})

evidencePacket := vaastypes.NewEvidencePacketData(
sdk.ConsAddress([]byte{0x01, 0x02, 0x03}),
100,
stakingtypes.Infraction_INFRACTION_DOWNTIME,
)

// Grace period is 0, so the check is skipped; should fail on the next
// validation (no IBC client) rather than grace period.
err := providerKeeper.HandleConsumerEvidencePacket(ctx, consumerId, evidencePacket)
require.Error(t, err)
require.NotContains(t, err.Error(), "grace period")
require.Contains(t, err.Error(), "no IBC client")
}
Loading
Loading