diff --git a/proto/vaas/provider/v1/tx.proto b/proto/vaas/provider/v1/tx.proto index 46ce2f75..e41fc6d4 100644 --- a/proto/vaas/provider/v1/tx.proto +++ b/proto/vaas/provider/v1/tx.proto @@ -105,16 +105,26 @@ message MsgUpdateParams { 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. +// MsgRemoveConsumer defines the message used to remove a consumer chain. Its +// effect depends on the consumer's phase. +// +// A consumer that has not launched yet (registered or initialized) is erased +// right away -- a chain no validator ever validated needs no unbonding delay -- +// its fee pool is refunded to its depositors and its chain id is released for +// immediate reuse. Either the consumer owner or the governance authority may +// sign: the owner to abandon a chain it no longer intends to launch, +// governance when the owner key is lost, which would otherwise pin the +// consumer -- and its chain id -- in place forever. +// +// A launched or paused consumer is stopped, and its state is erased only once +// the unbonding period has elapsed. Only the governance authority may sign. message MsgRemoveConsumer { - option (cosmos.msg.v1.signer) = "authority"; + option (cosmos.msg.v1.signer) = "signer"; - // the consumer id of the consumer chain to be stopped + // the consumer id of the consumer chain to be removed uint64 consumer_id = 1; - // authority is the address of the governance account. - string authority = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + // signer is the consumer owner (pre-launch only) or the governance authority + string signer = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; } // MsgRemoveConsumerResponse defines response type for MsgRemoveConsumer messages diff --git a/tests/e2e/e2e_consumer_liveness_test.go b/tests/e2e/e2e_consumer_liveness_test.go index 59bdd2ac..cc69b51d 100644 --- a/tests/e2e/e2e_consumer_liveness_test.go +++ b/tests/e2e/e2e_consumer_liveness_test.go @@ -180,7 +180,7 @@ func (s *IntegrationTestSuite) testLivenessRemoval() { removeJSON := fmt.Sprintf(`{ "messages": [{ "@type": "/vaas.provider.v1.MsgRemoveConsumer", - "authority": %q, + "signer": %q, "consumer_id": %q }], "metadata": "ipfs://test", diff --git a/x/vaas/provider/client/cli/tx.go b/x/vaas/provider/client/cli/tx.go index 433fc5d8..aa4db6ac 100644 --- a/x/vaas/provider/client/cli/tx.go +++ b/x/vaas/provider/client/cli/tx.go @@ -53,6 +53,7 @@ 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()) @@ -406,6 +407,48 @@ 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(`Remove a consumer chain. The effect depends on the consumer's phase. + +A consumer that has not launched (registered or initialized) is erased right +away, its fee-pool balance returns to its depositors, and its chain id frees +for reuse; the consumer owner may sign, or the governance authority when the +owner key is lost. A launched or paused consumer is stopped and erased after +the unbonding period, and only the governance authority may sign, so from the +CLI that path is a governance proposal rather than a direct transaction. + +Example: +%s tx vaasprovider remove-consumer 0 --from mykey +`, version.AppName)), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + consumerId, err := parseConsumerIdArg(args[0]) + if err != nil { + return err + } + msg := &types.MsgRemoveConsumer{ + Signer: clientCtx.GetFromAddress().String(), + ConsumerId: consumerId, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), 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]", diff --git a/x/vaas/provider/keeper/consumer_lifecycle.go b/x/vaas/provider/keeper/consumer_lifecycle.go index f95ccca0..dcfb867d 100644 --- a/x/vaas/provider/keeper/consumer_lifecycle.go +++ b/x/vaas/provider/keeper/consumer_lifecycle.go @@ -607,11 +607,68 @@ func (k Keeper) BeginBlockRemoveConsumers(ctx sdk.Context) error { return nil } +// RetireConsumerChain erases a consumer that has not launched, i.e. one still +// in the registered or initialized phase. +// +// Such a consumer has no counterpart to wind down: no IBC client was ever +// associated with it (that only happens once a consumer launches), no +// validator set was ever computed or sent, and evidence, +// downtime accusations and fee distribution all require phase LAUNCHED. No +// validator ever validated it, so there is nothing to keep slashable for an +// unbonding period either -- hence it does not go through STOPPED and the +// removal queue the way StopAndPrepareForConsumerRemoval does for a live +// chain, but straight to the shared teardown in DeleteConsumerChain. +// +// Two pieces of live state a pre-launch consumer can hold are worth naming: +// key assignments, which AssignConsumerKey accepts for registered and +// initialized consumers, and a funded fee pool, which anyone may deposit into +// before launch. DeleteConsumerChain clears the former and pays the latter +// back to its depositors. +func (k Keeper) RetireConsumerChain(ctx sdk.Context, consumerId uint64) error { + phase := k.GetConsumerPhase(ctx, consumerId) + if !k.IsConsumerPrelaunched(ctx, consumerId) { + return errorsmod.Wrapf(types.ErrInvalidPhase, + "cannot retire consumer %d: expected phase registered or initialized, got %s", consumerId, phase) + } + + // An initialized consumer waits in the spawn-time queue: drop its entry so + // the queue cannot hand an erased consumer to BeginBlockLaunchConsumers. + // The phase and the entry are always written together (see + // InitializeConsumer plus PrepareConsumerForLaunch, and the same derivation + // at InitGenesis), so a missing entry means inconsistent state and is + // reported rather than ignored. + if phase == types.CONSUMER_PHASE_INITIALIZED { + initializationParameters, err := k.GetConsumerInitializationParameters(ctx, consumerId) + if err != nil { + return fmt.Errorf("getting initialization parameters, consumerId(%d): %w", consumerId, err) + } + if err := k.RemoveConsumerToBeLaunched(ctx, consumerId, initializationParameters.SpawnTime); err != nil { + return errorsmod.Wrapf(vaastypes.ErrInvalidConsumerState, + "cannot remove consumer %d from the launch queue: %s", consumerId, err.Error()) + } + } + + return k.DeleteConsumerChain(ctx, consumerId) +} + // DeleteConsumerChain cleans up the state of the given consumer chain. +// +// It accepts a consumer in either of the two positions from which erasure is +// final: STOPPED, reached via StopAndPrepareForConsumerRemoval once +// BeginBlockRemoveConsumers has waited out the unbonding delay, or a pre-launch +// phase, reached via RetireConsumerChain, which needs no such delay (see there). func (k Keeper) DeleteConsumerChain(ctx sdk.Context, consumerId uint64) (err error) { + // The three deletable phases are named positively: STOPPED (the deferred + // route, its unbonding delay already served by BeginBlockRemoveConsumers) + // and the two pre-launch phases (the immediate route, see + // RetireConsumerChain). Both existing callers gate their phase before + // calling; this check is the teardown's own contract, independent of + // caller discipline, so no future caller can erase a launched consumer. phase := k.GetConsumerPhase(ctx, consumerId) - if phase != types.CONSUMER_PHASE_STOPPED { - return fmt.Errorf("cannot delete non-stopped chain: %d", consumerId) + if phase != types.CONSUMER_PHASE_STOPPED && + phase != types.CONSUMER_PHASE_REGISTERED && + phase != types.CONSUMER_PHASE_INITIALIZED { + return fmt.Errorf("cannot delete chain %d in phase %s", consumerId, phase) } // Auto-sweep the fee pool. This cannot fail under valid state; on state @@ -671,8 +728,22 @@ func (k Keeper) DeleteConsumerChain(ctx sdk.Context, consumerId uint64) (err err return fmt.Errorf("clearing downtime window floors for consumer %d: %w", consumerId, err) } - // Note that we do not delete ConsumerIdToChainIdKey and ConsumerIdToPhase, as well - // as consumer metadata and initialization parameters. + // Release the chain id. The provider stores it to keep two consumers from + // claiming the same chain (ChainIdInUse, consulted by MsgCreateConsumer and + // MsgUpdateConsumer), and by this point nothing can name this consumer's + // chain any more: its client mapping has just been removed, so an inbound + // packet can no longer be attributed to it (the provider resolves packets + // by destination client, see OnRecvPacket), and evidence, downtime + // accusations and fee distribution all require phase LAUNCHED. On the + // stop-then-remove path a full unbonding period has additionally passed + // since the consumer was stopped, so any infraction it could still be + // punished for is already outside the slashable window. Keeping the id past + // this point would reserve that chain id for good, since DELETED is + // terminal. + k.DeleteConsumerChainId(ctx, consumerId) + + // Note that we do not delete ConsumerIdToPhase, as well as consumer + // metadata, initialization parameters and owner address. // This is to enable block explorers and front ends to show information of // consumer chains that were removed without needing an archive node. diff --git a/x/vaas/provider/keeper/genesis.go b/x/vaas/provider/keeper/genesis.go index 5b73094c..8d63a140 100644 --- a/x/vaas/provider/keeper/genesis.go +++ b/x/vaas/provider/keeper/genesis.go @@ -48,7 +48,12 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState) []abc maxConsumerId = consumerId } - k.SetConsumerChainId(ctx, consumerId, cs.ChainId) + // A deleted consumer carries no chain id (see DeleteConsumerChain): + // writing one back would re-reserve a chain id that the deletion + // released, so the release survives a state-export restart. + if cs.ChainId != "" { + k.SetConsumerChainId(ctx, consumerId, cs.ChainId) + } k.SetConsumerPhase(ctx, consumerId, cs.Phase) if cs.OwnerAddress != "" { @@ -395,18 +400,21 @@ func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { for _, consumerId := range allConsumerIds { phase := k.GetConsumerPhase(ctx, consumerId) - chainId, err := k.GetConsumerChainId(ctx, consumerId) - if err != nil { - panic(fmt.Errorf("export: failed to read chain id for consumer %d: %w", consumerId, err)) - } - cs := types.ConsumerState{ ConsumerId: consumerId, - ChainId: chainId, Phase: phase, PendingValsetChanges: k.GetPendingVSCPackets(ctx, consumerId), } + // A deleted consumer has no chain id: the teardown released it so the + // chain id can be registered again (see DeleteConsumerChain). Every + // other phase must have one. + if chainId, err := k.GetConsumerChainId(ctx, consumerId); err == nil { + cs.ChainId = chainId + } else if !errors.Is(err, collections.ErrNotFound) { + panic(fmt.Errorf("export: failed to read chain id for consumer %d: %w", consumerId, err)) + } + if owner, err := k.GetConsumerOwnerAddress(ctx, consumerId); err == nil { cs.OwnerAddress = owner } else if !errors.Is(err, collections.ErrNotFound) { diff --git a/x/vaas/provider/keeper/genesis_test.go b/x/vaas/provider/keeper/genesis_test.go index 4da7b8e1..ba90d227 100644 --- a/x/vaas/provider/keeper/genesis_test.go +++ b/x/vaas/provider/keeper/genesis_test.go @@ -119,11 +119,13 @@ func TestExportGenesisIncludesNewFields(t *testing.T) { require.NoError(t, pk.SetConsumerGenesis(ctx, 3, cg)) require.NoError(t, pk.SetConsumerRemovalTime(ctx, 3, removalTime)) - // id 4 DELETED. + // id 4 DELETED: the teardown released its chain id (see + // DeleteConsumerChain), so the exported tombstone carries none. pk.SetConsumerPhase(ctx, 4, providertypes.CONSUMER_PHASE_DELETED) pk.SetConsumerOwnerAddress(ctx, 4, owner) require.NoError(t, pk.SetConsumerMetadata(ctx, 4, metadata)) require.NoError(t, pk.SetConsumerInitializationParameters(ctx, 4, initParams)) + pk.DeleteConsumerChainId(ctx, 4) pk.SetParams(ctx, providertypes.DefaultParams()) pk.SetValidatorSetUpdateId(ctx, 1) @@ -138,7 +140,8 @@ func TestExportGenesisIncludesNewFields(t *testing.T) { byID[cs.ChainId] = cs } - for _, id := range chainIDs { + // The deleted consumer no longer has a chain id, so it is keyed by "". + for _, id := range chainIDs[:4] { cs, ok := byID[id] require.True(t, ok, "consumer %s missing from export", id) require.Equal(t, owner, cs.OwnerAddress, "owner missing on consumer %s", id) @@ -150,7 +153,14 @@ func TestExportGenesisIncludesNewFields(t *testing.T) { require.Equal(t, "07-tendermint-0", byID["consumer-gamma"].ClientId, "LAUNCHED must have client_id") 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) + + deleted, ok := byID[""] + require.True(t, ok, "DELETED consumer missing from export") + require.Equal(t, uint64(4), deleted.ConsumerId) + require.Equal(t, providertypes.CONSUMER_PHASE_DELETED, deleted.Phase) + require.Equal(t, owner, deleted.OwnerAddress, "DELETED must keep its owner") + require.NotNil(t, deleted.Metadata, "DELETED must keep its metadata") + require.NotNil(t, deleted.InitParams, "DELETED must keep its init_params") // LAUNCHED consumer carries the liveness clock (last-ack + resync counters). require.NotNil(t, byID["consumer-gamma"].LastAckTime, "LAUNCHED must carry last_ack_time") @@ -212,8 +222,11 @@ func TestInitGenesisRestoresPerConsumerStateAndDerivedQueues(t *testing.T) { OwnerAddress: owner, Metadata: &md, InitParams: &ip, ClientId: "07-tendermint-1", ConsumerGenesis: cg, RemovalTime: &removeAt, }, + // A deleted consumer is imported without a chain id: its teardown + // released it (see DeleteConsumerChain) and importing one back would + // reserve that chain id again. { - ConsumerId: 4, ChainId: "consumer-epsilon", Phase: providertypes.CONSUMER_PHASE_DELETED, + ConsumerId: 4, Phase: providertypes.CONSUMER_PHASE_DELETED, OwnerAddress: owner, Metadata: &md, InitParams: &ip, }, }, @@ -231,11 +244,11 @@ func TestInitGenesisRestoresPerConsumerStateAndDerivedQueues(t *testing.T) { // ConsumerStates are imported in order; InitGenesis allocates numeric ids // starting at "0": - // "0" → consumer-alpha (REGISTERED) - // "1" → consumer-beta (INITIALIZED) - // "2" → consumer-gamma (LAUNCHED) - // "3" → consumer-delta (STOPPED) - // "4" → consumer-epsilon (DELETED) + // "0" -> consumer-alpha (REGISTERED) + // "1" -> consumer-beta (INITIALIZED) + // "2" -> consumer-gamma (LAUNCHED) + // "3" -> consumer-delta (STOPPED) + // "4" -> no chain id (DELETED) idChain := []struct { consumerId uint64 chainId string @@ -244,10 +257,10 @@ func TestInitGenesisRestoresPerConsumerStateAndDerivedQueues(t *testing.T) { {1, "consumer-beta"}, {2, "consumer-gamma"}, {3, "consumer-delta"}, - {4, "consumer-epsilon"}, } - // Per-consumer fields: chain id, owner, metadata must be present on all five. + // Per-consumer fields: chain id, owner, metadata must be present on all + // four consumers that still hold a chain id. for _, entry := range idChain { gotChain, err := pk.GetConsumerChainId(ctx, entry.consumerId) require.NoError(t, err, "chain id missing for consumer %d", entry.consumerId) @@ -262,6 +275,16 @@ func TestInitGenesisRestoresPerConsumerStateAndDerivedQueues(t *testing.T) { require.Equal(t, md, gotMd) } + // The deleted consumer keeps owner and metadata but no chain id. + _, err := pk.GetConsumerChainId(ctx, 4) + require.ErrorIs(t, err, collections.ErrNotFound, "DELETED must not hold a chain id") + gotOwner, err := pk.GetConsumerOwnerAddress(ctx, 4) + require.NoError(t, err) + require.Equal(t, owner, gotOwner) + gotMd, err := pk.GetConsumerMetadata(ctx, 4) + require.NoError(t, err) + require.Equal(t, md, gotMd) + // init_params are set on INITIALIZED, LAUNCHED, STOPPED, DELETED (ids 1–4). for _, consumerId := range []uint64{1, 2, 3, 4} { gotIp, err := pk.GetConsumerInitializationParameters(ctx, consumerId) @@ -347,6 +370,11 @@ func TestGenesisRoundTrip(t *testing.T) { if s.phase != providertypes.CONSUMER_PHASE_REGISTERED { require.NoError(t, pkA.SetConsumerInitializationParameters(ctxA, id, ip)) } + // A deleted consumer holds no chain id: its teardown released it (see + // DeleteConsumerChain), so seed the state deletion actually leaves. + if s.phase == providertypes.CONSUMER_PHASE_DELETED { + pkA.DeleteConsumerChainId(ctxA, id) + } if s.clientId != "" { pkA.SetConsumerClientId(ctxA, id, s.clientId) } diff --git a/x/vaas/provider/keeper/grpc_query.go b/x/vaas/provider/keeper/grpc_query.go index 5eb420ce..c3733f6f 100644 --- a/x/vaas/provider/keeper/grpc_query.go +++ b/x/vaas/provider/keeper/grpc_query.go @@ -76,10 +76,15 @@ func (k Keeper) QueryConsumerChains(goCtx context.Context, req *types.QueryConsu return &types.QueryConsumerChainsResponse{Chains: chains, Pagination: pageRes}, nil } -// GetConsumerChain returns a Chain data structure with all the necessary fields +// GetConsumerChain returns a Chain data structure with all the necessary fields. +// A deleted consumer comes back with an empty chain id: its teardown released +// the chain id for reuse (see DeleteConsumerChain), and the record is still +// listed so the deletion stays visible without an archive node. func (k Keeper) GetConsumerChain(ctx sdk.Context, consumerId uint64) (types.Chain, error) { + phase := k.GetConsumerPhase(ctx, consumerId) + chainID, err := k.GetConsumerChainId(ctx, consumerId) - if err != nil { + if err != nil && phase != types.CONSUMER_PHASE_DELETED { return types.Chain{}, fmt.Errorf("cannot find chainID for consumer (%d)", consumerId) } @@ -93,7 +98,7 @@ func (k Keeper) GetConsumerChain(ctx sdk.Context, consumerId uint64) (types.Chai return types.Chain{ ChainId: chainID, ClientId: clientID, - Phase: k.GetConsumerPhase(ctx, consumerId).String(), + Phase: phase.String(), Metadata: metadata, ConsumerId: consumerId, FeePoolAddress: k.GetConsumerFeePoolAddress(consumerId).String(), @@ -278,8 +283,15 @@ func (k Keeper) QueryConsumerChain(goCtx context.Context, req *types.QueryConsum consumerId := req.ConsumerId ctx := sdk.UnwrapSDKContext(goCtx) + phase := k.GetConsumerPhase(ctx, consumerId) + if phase == types.CONSUMER_PHASE_UNSPECIFIED { + return nil, status.Errorf(codes.InvalidArgument, "cannot retrieve phase for consumer id: %d", consumerId) + } + + // A deleted consumer answers with an empty chain id: the teardown released + // the chain id for reuse (see DeleteConsumerChain). chainId, err := k.GetConsumerChainId(ctx, consumerId) - if err != nil { + if err != nil && phase != types.CONSUMER_PHASE_DELETED { return nil, status.Errorf(codes.InvalidArgument, "cannot retrieve chain id for consumer id: %d", consumerId) } @@ -288,11 +300,6 @@ func (k Keeper) QueryConsumerChain(goCtx context.Context, req *types.QueryConsum return nil, status.Errorf(codes.InvalidArgument, "cannot retrieve owner address for consumer id: %d", consumerId) } - phase := k.GetConsumerPhase(ctx, consumerId) - if phase == types.CONSUMER_PHASE_UNSPECIFIED { - return nil, status.Errorf(codes.InvalidArgument, "cannot retrieve phase for consumer id: %d", consumerId) - } - metadata, err := k.GetConsumerMetadata(ctx, consumerId) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "cannot retrieve metadata for consumer id: %d", consumerId) diff --git a/x/vaas/provider/keeper/keeper.go b/x/vaas/provider/keeper/keeper.go index 43df7ecd..435b1795 100644 --- a/x/vaas/provider/keeper/keeper.go +++ b/x/vaas/provider/keeper/keeper.go @@ -2,6 +2,7 @@ package keeper import ( "context" + "errors" "fmt" "strings" "time" @@ -566,8 +567,13 @@ func (k Keeper) GetClientIdToConsumerId(ctx context.Context, clientId string) (u // DeleteConsumerClientId removes from the store the client id for the given consumer id. // The reverse index is automatically cleaned up by the indexed map. +// +// A consumer with no client mapping is not an error: the indexed map has to read +// the value to unreference the index, so it reports a missing entry, and a +// consumer that never launched never had a client adopted for it (see +// RetireConsumerChain). Only a genuine store failure panics. func (k Keeper) DeleteConsumerClientId(ctx context.Context, consumerId uint64) { - if err := k.ConsumerClients.Remove(ctx, consumerId); err != nil { + if err := k.ConsumerClients.Remove(ctx, consumerId); err != nil && !errors.Is(err, collections.ErrNotFound) { panic(fmt.Errorf("failed to remove consumer id to client id mapping: %w", err)) } } diff --git a/x/vaas/provider/keeper/msg_server.go b/x/vaas/provider/keeper/msg_server.go index b8b1c031..41fe37a2 100644 --- a/x/vaas/provider/keeper/msg_server.go +++ b/x/vaas/provider/keeper/msg_server.go @@ -575,37 +575,84 @@ func (k msgServer) UpdateConsumer(goCtx context.Context, msg *types.MsgUpdateCon return &resp, nil } -// RemoveConsumer defines an RPC handler method for MsgRemoveConsumer. -// Only the governance authority can remove a consumer chain. +// RemoveConsumer defines an RPC handler method for MsgRemoveConsumer. Its +// effect depends on the consumer's phase. +// +// A consumer that has not launched is erased immediately by +// RetireConsumerChain -- a chain no validator ever validated needs no +// unbonding delay -- under the owner-or-gov admission: the owner abandons a +// chain it no longer intends to launch, and governance is the remedy for a +// lost owner key, which would otherwise pin the consumer, and the chain id it +// holds, in place forever. +// +// A launched or paused consumer is stopped and prepared for a removal that +// erases state only once the unbonding period has elapsed; real validators +// are running such a chain, so ending it is governance's call alone. func (k msgServer) RemoveConsumer(goCtx context.Context, msg *types.MsgRemoveConsumer) (*types.MsgRemoveConsumerResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - - if k.GetAuthority() != msg.Authority { - return nil, errorsmod.Wrapf(govtypes.ErrInvalidSigner, "invalid authority; expected %s, got %s", k.authority, msg.Authority) - } - resp := types.MsgRemoveConsumerResponse{} - consumerId := msg.ConsumerId + exists, err := k.ConsumerPhase.Has(ctx, consumerId) + if err != nil { + return nil, err + } + if !exists { + return nil, errorsmod.Wrapf(types.ErrUnknownConsumerId, + "consumer %d does not exist", consumerId) + } + + // Read the chain id up front: the pre-launch teardown releases it, and the + // event should still report which chain was removed. chainId, err := k.GetConsumerChainId(ctx, consumerId) if err != nil { return &resp, errorsmod.Wrapf(vaastypes.ErrInvalidConsumerState, "cannot get consumer chain ID: %s", err.Error()) } - phase := k.Keeper.GetConsumerPhase(ctx, consumerId) - if phase != types.CONSUMER_PHASE_LAUNCHED && phase != types.CONSUMER_PHASE_PAUSED { - return &resp, errorsmod.Wrapf(types.ErrInvalidPhase, - "chain with consumer id: %d has to be in its launched or paused phase", consumerId) - } + isGov := k.GetAuthority() == msg.Signer + + switch { + case k.IsConsumerPrelaunched(ctx, consumerId): + if !isGov { + ownerAddress, err := k.GetConsumerOwnerAddress(ctx, consumerId) + if err != nil { + return nil, errorsmod.Wrapf(types.ErrNoOwnerAddress, + "consumer %d has no owner: %s", consumerId, err) + } + if !strings.EqualFold(msg.Signer, ownerAddress) { + return nil, errorsmod.Wrapf(types.ErrUnauthorized, + "only consumer owner %s or the gov authority may remove pre-launch consumer %d, got %s", + ownerAddress, consumerId, msg.Signer) + } + } + if err := k.Keeper.RetireConsumerChain(ctx, consumerId); err != nil { + return nil, err + } + k.Logger(ctx).Info("removed pre-launch consumer", + "consumerId", consumerId, + "chainId", chainId, + "signer", msg.Signer, + ) - err = k.Keeper.StopAndPrepareForConsumerRemoval(ctx, consumerId) + case phase == types.CONSUMER_PHASE_LAUNCHED || phase == types.CONSUMER_PHASE_PAUSED: + if !isGov { + return nil, errorsmod.Wrapf(govtypes.ErrInvalidSigner, + "invalid authority; a launched or paused consumer is removed only by the governance authority %s, got %s", + k.GetAuthority(), msg.Signer) + } + if err := k.Keeper.StopAndPrepareForConsumerRemoval(ctx, consumerId); err != nil { + return &resp, err + } + k.Logger(ctx).Info("stopped consumer", + "consumerId", consumerId, + "chainId", chainId, + "phase", phase, + ) - k.Logger(ctx).Info("stopped consumer", - "consumerId", consumerId, - "chainId", chainId, - "phase", phase, - ) + default: + return &resp, errorsmod.Wrapf(types.ErrInvalidPhase, + "consumer %d is %s; a stopped or deleted consumer cannot be removed again", consumerId, phase) + } ctx.EventManager().EmitEvent( sdk.NewEvent( @@ -613,11 +660,15 @@ func (k msgServer) RemoveConsumer(goCtx context.Context, msg *types.MsgRemoveCon sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute(types.AttributeConsumerId, strconv.FormatUint(consumerId, 10)), sdk.NewAttribute(types.AttributeConsumerChainId, chainId), - sdk.NewAttribute(types.AttributeSubmitterAddress, msg.Authority), + // The phase at removal tells the two arms apart for indexers: a + // pre-launch phase means immediate erasure, launched or paused a + // deferred one. + sdk.NewAttribute(types.AttributeConsumerPhase, phase.String()), + sdk.NewAttribute(types.AttributeSubmitterAddress, msg.Signer), ), ) - return &resp, err + return &resp, nil } // SetConsumerFeesPerBlock sets or clears the per-consumer override for the diff --git a/x/vaas/provider/keeper/msg_server_test.go b/x/vaas/provider/keeper/msg_server_test.go index 914049c3..e6cb649d 100644 --- a/x/vaas/provider/keeper/msg_server_test.go +++ b/x/vaas/provider/keeper/msg_server_test.go @@ -690,7 +690,7 @@ func TestRemoveConsumerGovAuth(t *testing.T) { // non-authority should be rejected _, err = msgServer.RemoveConsumer(ctx, &providertypes.MsgRemoveConsumer{ - Authority: "cosmos1notthegovauth000000000000000000000000", + Signer: "cosmos1notthegovauth000000000000000000000000", ConsumerId: consumerId, }) require.Error(t, err) @@ -699,7 +699,7 @@ func TestRemoveConsumerGovAuth(t *testing.T) { // correct authority succeeds _, err = msgServer.RemoveConsumer(ctx, &providertypes.MsgRemoveConsumer{ - Authority: providerKeeper.GetAuthority(), + Signer: providerKeeper.GetAuthority(), ConsumerId: consumerId, }) require.NoError(t, err) @@ -709,40 +709,6 @@ func TestRemoveConsumerGovAuth(t *testing.T) { require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, phase) } -func TestRemoveConsumerNonLaunchedRejected(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) - defer ctrl.Finish() - - providerKeeper.SetInfractionParams(ctx, providertypes.DefaultInfractionParameters()) - - 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) - createResp, err := msgServer.CreateConsumer(ctx, - &providertypes.MsgCreateConsumer{ - Submitter: "submitter", ChainId: "chainId", - Metadata: providertypes.ConsumerMetadata{ - Name: "name", - Description: "description", - }, - InitializationParameters: &providertypes.ConsumerInitializationParameters{ - UnbondingPeriod: 21 * 24 * time.Hour, - }, - }) - require.NoError(t, err) - - // gov authority cannot remove a non-launched chain - _, err = msgServer.RemoveConsumer(ctx, - &providertypes.MsgRemoveConsumer{ - Authority: providerKeeper.GetAuthority(), - ConsumerId: createResp.ConsumerId, - }) - require.Error(t, err) - require.ErrorIs(t, err, providertypes.ErrInvalidPhase) -} - // TestRemoveConsumerFromPaused verifies that MsgRemoveConsumer accepts a // PAUSED consumer (not just LAUNCHED), routes into // StopAndPrepareForConsumerRemoval, and clears the pause auto-stop schedule @@ -778,7 +744,7 @@ func TestRemoveConsumerFromPaused(t *testing.T) { _, err = msgServer.RemoveConsumer(ctx, &providertypes.MsgRemoveConsumer{ - Authority: providerKeeper.GetAuthority(), + Signer: providerKeeper.GetAuthority(), ConsumerId: consumerId, }) require.NoError(t, err) diff --git a/x/vaas/provider/keeper/permissionless.go b/x/vaas/provider/keeper/permissionless.go index b1bf4c30..9a8f96aa 100644 --- a/x/vaas/provider/keeper/permissionless.go +++ b/x/vaas/provider/keeper/permissionless.go @@ -131,15 +131,22 @@ func (k Keeper) GetConsumerInitializationParameters(ctx context.Context, consume return params, nil } -// SetConsumerInitializationParameters sets the initialization parameters associated with this consumer id +// SetConsumerInitializationParameters sets the initialization parameters associated with this consumer id. +// +// The initial height is cross-checked against the chain id's revision, except +// for a deleted consumer, which no longer has a chain id to check against: its +// teardown released it (see DeleteConsumerChain). Only genesis import writes +// parameters for a deleted consumer, since deletion keeps them for explorer UX. func (k Keeper) SetConsumerInitializationParameters(ctx context.Context, consumerId uint64, parameters types.ConsumerInitializationParameters) error { - chainId, err := k.GetConsumerChainId(ctx, consumerId) - if err != nil { - return fmt.Errorf("failed to get consumer chain ID for consumer id (%d): %w", consumerId, err) - } - // validate that the initial height matches the chain ID - if err := types.ValidateInitialHeight(parameters.InitialHeight, chainId); err != nil { - return fmt.Errorf("invalid initial height for consumer id (%d): %w", consumerId, err) + if k.GetConsumerPhase(ctx, consumerId) != types.CONSUMER_PHASE_DELETED { + chainId, err := k.GetConsumerChainId(ctx, consumerId) + if err != nil { + return fmt.Errorf("failed to get consumer chain ID for consumer id (%d): %w", consumerId, err) + } + // validate that the initial height matches the chain ID + if err := types.ValidateInitialHeight(parameters.InitialHeight, chainId); err != nil { + return fmt.Errorf("invalid initial height for consumer id (%d): %w", consumerId, err) + } } if err := k.ConsumerInitParams.Set(ctx, consumerId, parameters); err != nil { diff --git a/x/vaas/provider/keeper/retirement_test.go b/x/vaas/provider/keeper/retirement_test.go new file mode 100644 index 00000000..cd92ba0b --- /dev/null +++ b/x/vaas/provider/keeper/retirement_test.go @@ -0,0 +1,475 @@ +package keeper_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + + "cosmossdk.io/collections" + "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + + cryptotestutil "github.com/allinbits/vaas/testutil/crypto" + 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" +) + +// retirementInitParams returns initialization parameters whose initial-height +// revision matches a "-1"-suffixed chain id, with the spawn time supplied by +// the caller (zero leaves the consumer registered, a future time initializes +// it and enqueues it for launch). +func retirementInitParams(spawnTime time.Time) providertypes.ConsumerInitializationParameters { + ip := providertypes.DefaultConsumerInitializationParameters() + ip.InitialHeight = clienttypes.Height{RevisionNumber: 1, RevisionHeight: 1} + ip.SpawnTime = spawnTime + return ip +} + +// createRetirableConsumer registers a consumer owned by owner and returns its +// consumer id. A non-zero spawnTime leaves it in the initialized phase, queued +// for launch; a zero one leaves it registered. +func createRetirableConsumer( + t *testing.T, k providerkeeper.Keeper, ctx sdk.Context, + owner, chainId string, spawnTime time.Time, +) uint64 { + t.Helper() + + k.SetInfractionParams(ctx, providertypes.DefaultInfractionParameters()) + + ip := retirementInitParams(spawnTime) + ms := providerkeeper.NewMsgServerImpl(&k) + resp, err := ms.CreateConsumer(ctx, &providertypes.MsgCreateConsumer{ + Submitter: owner, + ChainId: chainId, + Metadata: providertypes.ConsumerMetadata{ + Name: "retirable", Description: "description", Metadata: "metadata", + }, + InitializationParameters: &ip, + }) + require.NoError(t, err) + return resp.ConsumerId +} + +// requireConsumerTornDown asserts that nothing is left of a retired consumer +// beyond the tombstone DeleteConsumerChain keeps on purpose (phase, owner, +// metadata and initialization parameters, for explorers). +func requireConsumerTornDown( + t *testing.T, k providerkeeper.Keeper, ctx sdk.Context, consumerId uint64, +) { + t.Helper() + + require.Equal(t, providertypes.CONSUMER_PHASE_DELETED, k.GetConsumerPhase(ctx, consumerId)) + + _, err := k.GetConsumerChainId(ctx, consumerId) + require.ErrorIs(t, err, collections.ErrNotFound, "chain id must be released") + + _, found := k.GetConsumerClientId(ctx, consumerId) + require.False(t, found, "client id must be deleted") + + _, err = k.FeePoolAddressToConsumerId.Get(ctx, k.GetConsumerFeePoolAddress(consumerId)) + require.ErrorIs(t, err, collections.ErrNotFound, "fee-pool reverse lookup must be deleted") + + require.Empty(t, k.GetAllValidatorConsumerPubKeys(ctx, &consumerId), "key assignments must be cleared") + require.Empty(t, k.GetAllValidatorsByConsumerAddr(ctx, &consumerId), "consumer-addr lookups must be cleared") + require.Empty(t, k.GetAllConsumerAddrsToPrune(ctx, consumerId), "addresses to prune must be cleared") + + _, found = k.GetConsumerGenesis(ctx, consumerId) + require.False(t, found, "consumer genesis must be deleted") + _, found = k.GetInitChainHeight(ctx, consumerId) + require.False(t, found, "init chain height must be deleted") + require.Zero(t, k.GetEquivocationEvidenceMinHeight(ctx, consumerId)) + require.Empty(t, k.GetPendingVSCPackets(ctx, consumerId), "pending VSC packets must be deleted") + + valSet, err := k.GetConsumerValSet(ctx, consumerId) + require.NoError(t, err) + require.Empty(t, valSet, "consumer valset must be deleted") + + _, err = k.GetConsumerRemovalTime(ctx, consumerId) + require.Error(t, err, "removal time must be deleted") + require.True(t, k.GetConsumerLastAckTime(ctx, consumerId).IsZero(), "last-ack time must be deleted") + require.Zero(t, k.GetConsumerHighestSentVscId(ctx, consumerId)) + require.Zero(t, k.GetConsumerHighestAckedVscId(ctx, consumerId)) + hasDebt, err := k.ConsumerDebt.Has(ctx, consumerId) + require.NoError(t, err) + require.False(t, hasDebt, "debt flag must be deleted") + + has, err := k.ConsumerFeesPerBlockOverride.Has(ctx, consumerId) + require.NoError(t, err) + require.False(t, has, "fees-per-block override must be deleted") + + // Nothing is left in any fee-pool share collection, so no balance can be + // stranded behind a claim nobody can exercise. + sharesIter, err := k.ConsumerFeePoolShares.Iterate(ctx, + collections.NewPrefixedTripleRange[uint64, string, sdk.AccAddress](consumerId)) + require.NoError(t, err) + defer sharesIter.Close() + require.False(t, sharesIter.Valid(), "fee-pool shares must be cleared") + + totalsIter, err := k.ConsumerFeePoolTotalShares.Iterate(ctx, + collections.NewPrefixedPairRange[uint64, string](consumerId)) + require.NoError(t, err) + defer totalsIter.Close() + require.False(t, totalsIter.Valid(), "fee-pool total shares must be cleared") + + // The tombstone the teardown keeps on purpose. + owner, err := k.GetConsumerOwnerAddress(ctx, consumerId) + require.NoError(t, err) + require.NotEmpty(t, owner, "owner must be kept for explorers") + _, err = k.GetConsumerMetadata(ctx, consumerId) + require.NoError(t, err, "metadata must be kept for explorers") +} + +// TestRemoveConsumerOwnerErasesPrelaunchedConsumer verifies the pre-launch arm +// of MsgRemoveConsumer: the owner of a consumer that has not launched can +// remove it from either pre-launch phase, the teardown is immediate and leaves +// nothing behind, and the chain id it held can be registered again afterwards. +func TestRemoveConsumerOwnerErasesPrelaunchedConsumer(t *testing.T) { + owner := sdk.AccAddress([]byte("consumer-owner-addr1")).String() + + testCases := []struct { + name string + spawnTime time.Time + phase providertypes.ConsumerPhase + }{ + { + name: "registered", + spawnTime: time.Time{}, + phase: providertypes.CONSUMER_PHASE_REGISTERED, + }, + { + name: "initialized", + spawnTime: time.Unix(2_000_000_000, 0).UTC(), + phase: providertypes.CONSUMER_PHASE_INITIALIZED, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(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 chainId = "retire-1" + consumerId := createRetirableConsumer(t, k, ctx, owner, chainId, tc.spawnTime) + require.Equal(t, tc.phase, k.GetConsumerPhase(ctx, consumerId)) + + // A validator may assign a consumer key before launch, so make sure + // the retirement has some to clear. + providerAddr := providertypes.NewProviderConsAddress([]byte("provider-cons-addr-1")) + consumerAddr := providertypes.NewConsumerConsAddress([]byte("consumer-cons-addr-1")) + k.SetValidatorConsumerPubKey(ctx, consumerId, providerAddr, + cryptotestutil.NewCryptoIdentityFromIntSeed(7).TMProtoCryptoPublicKey()) + k.SetValidatorByConsumerAddr(ctx, consumerId, consumerAddr, providerAddr) + + // Empty fee pool: the teardown sweeps it and finds nothing to move. + mocks.MockBankKeeper.EXPECT(). + GetAllBalances(ctx, k.GetConsumerFeePoolAddress(consumerId)). + Return(sdk.NewCoins()) + + ms := providerkeeper.NewMsgServerImpl(&k) + _, err := ms.RemoveConsumer(ctx, &providertypes.MsgRemoveConsumer{ + Signer: owner, ConsumerId: consumerId, + }) + require.NoError(t, err) + + requireConsumerTornDown(t, k, ctx, consumerId) + + // An initialized consumer was queued for launch: the entry is gone, + // so BeginBlockLaunchConsumers can never see the retired consumer. + if tc.phase == providertypes.CONSUMER_PHASE_INITIALIZED { + queued, err := k.GetConsumersToBeLaunched(ctx, tc.spawnTime) + require.NoError(t, err) + require.Empty(t, queued.Ids, "launch queue entry must be dropped") + } + + // The chain id is registrable again by anyone, including a different owner. + inUse, err := k.ChainIdInUse(ctx, chainId) + require.NoError(t, err) + require.False(t, inUse) + + otherOwner := sdk.AccAddress([]byte("consumer-owner-addr2")).String() + newConsumerId := createRetirableConsumer(t, k, ctx, otherOwner, chainId, time.Time{}) + require.NotEqual(t, consumerId, newConsumerId) + gotChainId, err := k.GetConsumerChainId(ctx, newConsumerId) + require.NoError(t, err) + require.Equal(t, chainId, gotChainId) + }) + } +} + +// TestRemoveConsumerGovernanceErasesPrelaunchedWhenOwnerKeyIsLost verifies the +// gov side of the pre-launch arm: a consumer whose owner can no longer sign is +// still removable, so neither its state nor its chain id is pinned forever. +func TestRemoveConsumerGovernanceErasesPrelaunchedWhenOwnerKeyIsLost(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() + + lostKeyOwner := sdk.AccAddress([]byte("lost-key-owner-addr1")).String() + const chainId = "stranded-1" + consumerId := createRetirableConsumer(t, k, ctx, lostKeyOwner, chainId, time.Time{}) + + mocks.MockBankKeeper.EXPECT(). + GetAllBalances(ctx, k.GetConsumerFeePoolAddress(consumerId)). + Return(sdk.NewCoins()) + + ms := providerkeeper.NewMsgServerImpl(&k) + _, err := ms.RemoveConsumer(ctx, &providertypes.MsgRemoveConsumer{ + Signer: k.GetAuthority(), ConsumerId: consumerId, + }) + require.NoError(t, err) + + requireConsumerTornDown(t, k, ctx, consumerId) + + inUse, err := k.ChainIdInUse(ctx, chainId) + require.NoError(t, err) + require.False(t, inUse) +} + +// TestRemoveConsumerRejectsUnauthorizedSigner verifies that a signer who is +// neither the owner nor the gov authority cannot remove a pre-launch consumer, +// so removal is not a way to destroy someone else's registration. +func TestRemoveConsumerRejectsUnauthorizedSigner(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() + + owner := sdk.AccAddress([]byte("consumer-owner-addr1")).String() + stranger := sdk.AccAddress([]byte("some-other-account11")).String() + const chainId = "guarded-1" + consumerId := createRetirableConsumer(t, k, ctx, owner, chainId, time.Time{}) + + ms := providerkeeper.NewMsgServerImpl(&k) + _, err := ms.RemoveConsumer(ctx, &providertypes.MsgRemoveConsumer{ + Signer: stranger, ConsumerId: consumerId, + }) + require.ErrorIs(t, err, providertypes.ErrUnauthorized) + + // Nothing moved: the consumer is intact and still holds its chain id. + require.Equal(t, providertypes.CONSUMER_PHASE_REGISTERED, k.GetConsumerPhase(ctx, consumerId)) + inUse, err := k.ChainIdInUse(ctx, chainId) + require.NoError(t, err) + require.True(t, inUse) +} + +// TestRemoveConsumerRejectsUnknownConsumer verifies that a consumer id that was +// never registered is reported as unknown, including for the gov authority. +func TestRemoveConsumerRejectsUnknownConsumer(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + ms := providerkeeper.NewMsgServerImpl(&k) + for _, signer := range []string{sdk.AccAddress([]byte("consumer-owner-addr1")).String(), k.GetAuthority()} { + _, err := ms.RemoveConsumer(ctx, &providertypes.MsgRemoveConsumer{ + Signer: signer, ConsumerId: 42, + }) + require.ErrorIs(t, err, providertypes.ErrUnknownConsumerId) + } +} + +// TestRemoveConsumerPastLaunchIsGovOnlyAndDeferred verifies the +// launched-and-later arms of MsgRemoveConsumer: a launched or paused consumer +// is removable only by the governance authority, and is stopped with erasure +// deferred by the unbonding period rather than erased immediately (its chain +// id stays reserved). A stopped or deleted consumer is rejected for any +// signer. +func TestRemoveConsumerPastLaunchIsGovOnlyAndDeferred(t *testing.T) { + owner := sdk.AccAddress([]byte("consumer-owner-addr1")).String() + + for _, phase := range []providertypes.ConsumerPhase{ + providertypes.CONSUMER_PHASE_LAUNCHED, + providertypes.CONSUMER_PHASE_PAUSED, + } { + t.Run(phase.String(), func(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 chainId = "live-1" + consumerId := createRetirableConsumer(t, k, ctx, owner, chainId, time.Time{}) + k.SetConsumerPhase(ctx, consumerId, phase) + + ms := providerkeeper.NewMsgServerImpl(&k) + + // Past launch the owner may not remove: real validators are + // running the chain, so ending it is governance's call. + _, err := ms.RemoveConsumer(ctx, &providertypes.MsgRemoveConsumer{ + Signer: owner, ConsumerId: consumerId, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid authority") + require.Equal(t, phase, k.GetConsumerPhase(ctx, consumerId), "a rejected removal must not move the phase") + + // Governance removes it: stopped now, erased after unbonding, the + // chain id stays reserved until the deferred deletion. + _, err = ms.RemoveConsumer(ctx, &providertypes.MsgRemoveConsumer{ + Signer: k.GetAuthority(), ConsumerId: consumerId, + }) + require.NoError(t, err) + require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, k.GetConsumerPhase(ctx, consumerId)) + inUse, err := k.ChainIdInUse(ctx, chainId) + require.NoError(t, err) + require.True(t, inUse) + }) + } + + terminalCases := []struct { + phase providertypes.ConsumerPhase + // A consumer past launch that has not been erased yet still holds its + // chain id. The deleted case below is set up by phase alone, so its + // chain id proves nothing here; the real release is covered by + // TestDeleteConsumerChainReleasesChainIdOnStoppedPath. + stillHoldsChainId bool + }{ + {providertypes.CONSUMER_PHASE_STOPPED, true}, + {providertypes.CONSUMER_PHASE_DELETED, false}, + } + for _, tc := range terminalCases { + phase := tc.phase + t.Run(phase.String(), func(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 chainId = "live-1" + consumerId := createRetirableConsumer(t, k, ctx, owner, chainId, time.Time{}) + k.SetConsumerPhase(ctx, consumerId, phase) + + ms := providerkeeper.NewMsgServerImpl(&k) + + // Both arms are rejected on the phase, not on the signer. + for _, signer := range []string{owner, k.GetAuthority()} { + _, err := ms.RemoveConsumer(ctx, &providertypes.MsgRemoveConsumer{ + Signer: signer, ConsumerId: consumerId, + }) + require.ErrorIs(t, err, providertypes.ErrInvalidPhase) + } + + require.Equal(t, phase, k.GetConsumerPhase(ctx, consumerId)) + + if tc.stillHoldsChainId { + // The chain id is not released while the consumer still exists + // in a non-terminal phase. + inUse, err := k.ChainIdInUse(ctx, chainId) + require.NoError(t, err) + require.True(t, inUse) + } + }) + } +} + +// TestRemoveConsumerReturnsFundedFeePool verifies that removing a pre-launch consumer +// whose fee pool holds a deposit pays the depositors back rather than leaving +// the balance behind an address nobody can withdraw from any more. +func TestRemoveConsumerReturnsFundedFeePool(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() + + owner := sdk.AccAddress([]byte("consumer-owner-addr1")).String() + depositor := sdk.AccAddress([]byte("fee-pool-depositor11")) + const chainId = "funded-1" + consumerId := createRetirableConsumer(t, k, ctx, owner, chainId, time.Time{}) + poolAddr := k.GetConsumerFeePoolAddress(consumerId) + + // A pre-launch deposit: shares for the depositor against a pool balance. + require.NoError(t, k.ConsumerFeePoolShares.Set(ctx, + collections.Join3(consumerId, "uphoton", depositor), math.NewInt(100))) + require.NoError(t, k.ConsumerFeePoolTotalShares.Set(ctx, + collections.Join(consumerId, "uphoton"), math.NewInt(100))) + + deposit := sdk.NewInt64Coin("uphoton", 500) + mocks.MockBankKeeper.EXPECT().GetAllBalances(ctx, poolAddr). + Return(sdk.NewCoins(deposit)) + mocks.MockBankKeeper.EXPECT().GetBalance(ctx, poolAddr, "uphoton").Return(deposit) + mocks.MockBankKeeper.EXPECT().SendCoinsFromAccountToModule( + ctx, poolAddr, providertypes.ModuleName, sdk.NewCoins(deposit)).Return(nil) + mocks.MockBankKeeper.EXPECT().SendCoinsFromModuleToAccount( + ctx, providertypes.ModuleName, depositor, sdk.NewCoins(deposit)).Return(nil) + + ms := providerkeeper.NewMsgServerImpl(&k) + _, err := ms.RemoveConsumer(ctx, &providertypes.MsgRemoveConsumer{ + Signer: owner, ConsumerId: consumerId, + }) + require.NoError(t, err) + + requireConsumerTornDown(t, k, ctx, consumerId) +} + +// TestDeleteConsumerChainReleasesChainIdOnStoppedPath verifies the other route +// into deletion: a consumer stopped by the liveness sweep (or by governance) +// keeps its chain id reserved while it is stopped, and only gives it up once +// BeginBlockRemoveConsumers erases it after the unbonding period. Without that +// release a swept consumer could never re-register under its own chain id. +func TestDeleteConsumerChainReleasesChainIdOnStoppedPath(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() + + owner := sdk.AccAddress([]byte("consumer-owner-addr1")).String() + const chainId = "swept-1" + consumerId := createRetirableConsumer(t, k, ctx, owner, chainId, time.Time{}) + k.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) + k.SetConsumerClientId(ctx, consumerId, "07-tendermint-0") + + // Seed the state a consumer accumulates once it is live, so the teardown + // assertions below are checking something that was actually there. + tmPubKey := cryptotestutil.NewCryptoIdentityFromIntSeed(11).TMProtoCryptoPublicKey() + require.NoError(t, k.SetConsumerValSet(ctx, consumerId, + []providertypes.ConsensusValidator{{PublicKey: &tmPubKey, Power: 10}})) + require.NoError(t, k.SetConsumerGenesis(ctx, consumerId, *vaastypes.DefaultConsumerGenesisState())) + k.SetInitChainHeight(ctx, consumerId, 100) + k.SetEquivocationEvidenceMinHeight(ctx, consumerId, 1) + k.AppendPendingVSCPackets(ctx, consumerId, + vaastypes.ValidatorSetChangePacketData{ValsetUpdateId: 3}) + require.NoError(t, k.SetConsumerLastAckTime(ctx, consumerId, ctx.BlockTime())) + k.SetConsumerHighestSentVscId(ctx, consumerId, 5) + k.SetConsumerHighestAckedVscId(ctx, consumerId, 3) + k.SetConsumerInDebt(ctx, consumerId, true) + require.NoError(t, k.ConsumerFeesPerBlockOverride.Set(ctx, consumerId, math.NewInt(9_999))) + providerAddr := providertypes.NewProviderConsAddress([]byte("provider-cons-addr-2")) + k.SetValidatorConsumerPubKey(ctx, consumerId, providerAddr, tmPubKey) + + // Stopped: the chain may still be producing blocks and its validators are + // still slashable, so the chain id stays reserved. + require.NoError(t, k.StopAndPrepareForConsumerRemoval(ctx, consumerId)) + inUse, err := k.ChainIdInUse(ctx, chainId) + require.NoError(t, err) + require.True(t, inUse, "chain id must stay reserved while the consumer is stopped") + + mocks.MockBankKeeper.EXPECT(). + GetAllBalances(ctx, k.GetConsumerFeePoolAddress(consumerId)). + Return(sdk.NewCoins()) + + require.NoError(t, k.DeleteConsumerChain(ctx, consumerId)) + + requireConsumerTornDown(t, k, ctx, consumerId) + + inUse, err = k.ChainIdInUse(ctx, chainId) + require.NoError(t, err) + require.False(t, inUse, "chain id must be released once the consumer is erased") + + newConsumerId := createRetirableConsumer(t, k, ctx, owner, chainId, time.Time{}) + require.NotEqual(t, consumerId, newConsumerId) +} diff --git a/x/vaas/provider/types/genesis.go b/x/vaas/provider/types/genesis.go index b400e9ce..d8b3cb01 100644 --- a/x/vaas/provider/types/genesis.go +++ b/x/vaas/provider/types/genesis.go @@ -447,7 +447,14 @@ func validateEpochDowntimeEntries(entries []EpochDowntimeEntry, knownConsumerIds // Each phase has different required and forbidden fields, mirroring the // invariants the keeper maintains (see x/vaas/provider/keeper/consumer_lifecycle.go). func (cs ConsumerState) Validate() error { - if cs.ChainId == "" { + // A deleted consumer is the one phase without a chain id: the teardown + // releases it so the chain id can be registered again, and importing one + // back would take it out of circulation a second time. + if cs.Phase == CONSUMER_PHASE_DELETED { + if cs.ChainId != "" { + return fmt.Errorf("chain id must be empty for phase %s", cs.Phase) + } + } else if cs.ChainId == "" { return errors.New("chain id cannot be empty") } if cs.OwnerAddress == "" { @@ -540,7 +547,7 @@ func (cs ConsumerState) Validate() error { case CONSUMER_PHASE_DELETED: // Tombstoned: keeper retains owner+metadata+init_params for explorer UX // (see consumer_lifecycle.go DeleteConsumerChain comment). - // Everything else is cleared. + // Everything else is cleared, including the chain id (checked above). if cs.InitParams == nil { return fmt.Errorf("init params required for phase %s", cs.Phase) } diff --git a/x/vaas/provider/types/genesis_test.go b/x/vaas/provider/types/genesis_test.go index 521bf956..84f51363 100644 --- a/x/vaas/provider/types/genesis_test.go +++ b/x/vaas/provider/types/genesis_test.go @@ -756,18 +756,28 @@ func TestConsumerStateValidatePerPhase(t *testing.T) { cs.RemovalTime = &rt }, "removal time must be empty"}, - // DELETED: chain_id + owner + init_params + metadata preserved; everything else cleared. + // DELETED: owner + init_params + metadata preserved; everything else + // cleared, including the chain id, which the teardown released so it can + // be registered again. {"DELETED valid", func(cs *types.ConsumerState) { *cs = base(types.CONSUMER_PHASE_DELETED) + cs.ChainId = "" cs.InitParams = validInit cs.Metadata = &validMetadata cs.ConsumerGenesis = vaastypes.ConsumerGenesisState{} // cleared }, ""}, {"DELETED missing metadata", func(cs *types.ConsumerState) { *cs = base(types.CONSUMER_PHASE_DELETED) + cs.ChainId = "" cs.InitParams = validInit cs.ConsumerGenesis = vaastypes.ConsumerGenesisState{} }, "metadata required"}, + {"DELETED with retained chain id", func(cs *types.ConsumerState) { + *cs = base(types.CONSUMER_PHASE_DELETED) + cs.InitParams = validInit + cs.Metadata = &validMetadata + cs.ConsumerGenesis = vaastypes.ConsumerGenesisState{} + }, "chain id must be empty"}, } for _, tc := range cases { diff --git a/x/vaas/provider/types/msg.go b/x/vaas/provider/types/msg.go index 15c53acc..33b7f94d 100644 --- a/x/vaas/provider/types/msg.go +++ b/x/vaas/provider/types/msg.go @@ -293,17 +293,20 @@ func (msg MsgUpdateConsumer) ValidateBasic() error { } // NewMsgRemoveConsumer creates a new MsgRemoveConsumer instance -func NewMsgRemoveConsumer(authority string, consumerId uint64) (*MsgRemoveConsumer, error) { +func NewMsgRemoveConsumer(signer string, consumerId uint64) (*MsgRemoveConsumer, error) { return &MsgRemoveConsumer{ - Authority: authority, + Signer: signer, ConsumerId: consumerId, }, nil } // ValidateBasic implements the sdk.HasValidateBasic interface. func (msg MsgRemoveConsumer) ValidateBasic() error { - if _, err := sdk.AccAddressFromBech32(msg.Authority); err != nil { - return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid authority address: %s", err) + // The signer is the consumer owner (pre-launch) or the gov authority; + // which one is acceptable depends on the consumer's phase, so + // RemoveConsumer decides against state. + if _, err := sdk.AccAddressFromBech32(msg.Signer); err != nil { + return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid signer: %s", err) } return nil } diff --git a/x/vaas/provider/types/msg_test.go b/x/vaas/provider/types/msg_test.go index 4b53fcad..b4c72683 100644 --- a/x/vaas/provider/types/msg_test.go +++ b/x/vaas/provider/types/msg_test.go @@ -648,6 +648,36 @@ func TestMsgSweepConsumerFeePool_ValidateBasic(t *testing.T) { } } +func TestMsgRemoveConsumer_ValidateBasic(t *testing.T) { + tests := []struct { + name string + msg types.MsgRemoveConsumer + wantErr bool + }{ + {"valid", types.MsgRemoveConsumer{ + Signer: sdk.AccAddress([]byte("alice___________")).String(), + ConsumerId: 0, + }, false}, + {"invalid signer", types.MsgRemoveConsumer{ + Signer: "not-bech32", + ConsumerId: 0, + }, true}, + {"empty signer", types.MsgRemoveConsumer{ + ConsumerId: 0, + }, true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + func TestValidateChainId(t *testing.T) { testCases := []struct { name string diff --git a/x/vaas/provider/types/tx.pb.go b/x/vaas/provider/types/tx.pb.go index a28717de..84a1d1de 100644 --- a/x/vaas/provider/types/tx.pb.go +++ b/x/vaas/provider/types/tx.pb.go @@ -374,14 +374,24 @@ func (m *MsgUpdateParamsResponse) XXX_DiscardUnknown() { var xxx_messageInfo_MsgUpdateParamsResponse proto.InternalMessageInfo -// 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. +// MsgRemoveConsumer defines the message used to remove a consumer chain. Its +// effect depends on the consumer's phase. +// +// A consumer that has not launched yet (registered or initialized) is erased +// right away -- a chain no validator ever validated needs no unbonding delay -- +// its fee pool is refunded to its depositors and its chain id is released for +// immediate reuse. Either the consumer owner or the governance authority may +// sign: the owner to abandon a chain it no longer intends to launch, +// governance when the owner key is lost, which would otherwise pin the +// consumer -- and its chain id -- in place forever. +// +// A launched or paused consumer is stopped, and its state is erased only once +// the unbonding period has elapsed. Only the governance authority may sign. type MsgRemoveConsumer struct { - // the consumer id of the consumer chain to be stopped + // the consumer id of the consumer chain to be removed ConsumerId uint64 `protobuf:"varint,1,opt,name=consumer_id,json=consumerId,proto3" json:"consumer_id,omitempty"` - // authority is the address of the governance account. - Authority string `protobuf:"bytes,2,opt,name=authority,proto3" json:"authority,omitempty"` + // signer is the consumer owner (pre-launch only) or the governance authority + Signer string `protobuf:"bytes,2,opt,name=signer,proto3" json:"signer,omitempty"` } func (m *MsgRemoveConsumer) Reset() { *m = MsgRemoveConsumer{} } @@ -424,9 +434,9 @@ func (m *MsgRemoveConsumer) GetConsumerId() uint64 { return 0 } -func (m *MsgRemoveConsumer) GetAuthority() string { +func (m *MsgRemoveConsumer) GetSigner() string { if m != nil { - return m.Authority + return m.Signer } return "" } @@ -1308,106 +1318,107 @@ func init() { func init() { proto.RegisterFile("vaas/provider/v1/tx.proto", fileDescriptor_a07778b1d094765c) } var fileDescriptor_a07778b1d094765c = []byte{ - // 1580 bytes of a gzipped FileDescriptorProto + // 1586 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0x4f, 0x6c, 0x1b, 0x45, - 0x17, 0xcf, 0xe6, 0x5f, 0x9b, 0x49, 0x9a, 0x36, 0xdb, 0xb4, 0x89, 0xdd, 0x7e, 0x76, 0xea, 0xaf, + 0x17, 0xcf, 0xe6, 0x5f, 0x9b, 0x71, 0x9a, 0x36, 0xdb, 0xb4, 0x89, 0xdd, 0x7e, 0x76, 0xea, 0xaf, 0x4d, 0xd3, 0x7e, 0xed, 0x6e, 0x93, 0x0f, 0x5a, 0x11, 0x55, 0x95, 0x9a, 0x84, 0xaa, 0x11, 0x8a, 0x88, 0xb6, 0xa2, 0x48, 0x08, 0x61, 0x8d, 0x77, 0xa7, 0xeb, 0x51, 0x76, 0x67, 0xcc, 0xce, 0xd8, - 0x6e, 0x10, 0x48, 0x08, 0x24, 0x04, 0x37, 0x38, 0x72, 0xeb, 0x09, 0x21, 0x24, 0xa4, 0x1e, 0x7a, + 0x69, 0x10, 0x48, 0x08, 0x24, 0x04, 0x37, 0x38, 0x72, 0xeb, 0x09, 0x21, 0x24, 0xa4, 0x1e, 0x7a, 0xe6, 0xdc, 0x63, 0xe9, 0x09, 0x71, 0x28, 0x28, 0x95, 0x28, 0x27, 0x0e, 0x1c, 0x39, 0xa1, 0x99, 0x9d, 0x5d, 0xaf, 0xed, 0x5d, 0xdb, 0x29, 0xb4, 0x97, 0xc4, 0x33, 0xef, 0x37, 0xef, 0xcf, 0xef, - 0xcd, 0xbc, 0x79, 0xb3, 0x20, 0xd7, 0x80, 0x90, 0x99, 0xb5, 0x80, 0x36, 0xb0, 0x83, 0x02, 0xb3, - 0xb1, 0x6c, 0xf2, 0xbb, 0x46, 0x2d, 0xa0, 0x9c, 0xea, 0x47, 0x84, 0xc8, 0x88, 0x44, 0x46, 0x63, - 0x39, 0x3f, 0x03, 0x7d, 0x4c, 0xa8, 0x29, 0xff, 0x86, 0xa0, 0x7c, 0xc1, 0xa6, 0xcc, 0xa7, 0xcc, - 0xac, 0x40, 0x86, 0xcc, 0xc6, 0x72, 0x05, 0x71, 0xb8, 0x6c, 0xda, 0x14, 0x13, 0x25, 0x9f, 0x53, - 0x72, 0x9f, 0xb9, 0x42, 0xb9, 0xcf, 0x5c, 0x25, 0xc8, 0x85, 0x82, 0xb2, 0x1c, 0x99, 0xe1, 0x40, - 0x89, 0x66, 0x5d, 0xea, 0xd2, 0x70, 0x5e, 0xfc, 0x52, 0xb3, 0x27, 0x5d, 0x4a, 0x5d, 0x0f, 0x99, - 0xb0, 0x86, 0x4d, 0x48, 0x08, 0xe5, 0x90, 0x63, 0x4a, 0xa2, 0x35, 0x39, 0x25, 0x95, 0xa3, 0x4a, - 0xfd, 0x8e, 0x09, 0xc9, 0x6e, 0xe4, 0x62, 0xa7, 0xc8, 0xa9, 0x07, 0x72, 0xad, 0x92, 0x17, 0x3b, - 0xe5, 0x1c, 0xfb, 0x88, 0x71, 0xe8, 0xd7, 0x22, 0x00, 0xae, 0xd8, 0xa6, 0x4d, 0x03, 0x64, 0xda, - 0x1e, 0x46, 0x84, 0x8b, 0x40, 0xc2, 0x5f, 0x0a, 0x60, 0x0a, 0x80, 0x87, 0xdd, 0x2a, 0x0f, 0xa7, + 0xcd, 0xbc, 0x79, 0xb3, 0x20, 0xdf, 0x84, 0x90, 0x99, 0xf5, 0x80, 0x36, 0xb1, 0x83, 0x02, 0xb3, + 0xb9, 0x64, 0xf2, 0xbb, 0x46, 0x3d, 0xa0, 0x9c, 0xea, 0x47, 0x84, 0xc8, 0x88, 0x44, 0x46, 0x73, + 0xa9, 0x30, 0x0d, 0x7d, 0x4c, 0xa8, 0x29, 0xff, 0x86, 0xa0, 0x42, 0xd1, 0xa6, 0xcc, 0xa7, 0xcc, + 0xac, 0x42, 0x86, 0xcc, 0xe6, 0x52, 0x15, 0x71, 0xb8, 0x64, 0xda, 0x14, 0x13, 0x25, 0x9f, 0x55, + 0x72, 0x9f, 0xb9, 0x42, 0xb9, 0xcf, 0x5c, 0x25, 0xc8, 0x87, 0x82, 0x8a, 0x1c, 0x99, 0xe1, 0x40, + 0x89, 0x66, 0x5c, 0xea, 0xd2, 0x70, 0x5e, 0xfc, 0x52, 0xb3, 0x27, 0x5d, 0x4a, 0x5d, 0x0f, 0x99, + 0xb0, 0x8e, 0x4d, 0x48, 0x08, 0xe5, 0x90, 0x63, 0x4a, 0xa2, 0x35, 0x79, 0x25, 0x95, 0xa3, 0x6a, + 0xe3, 0x8e, 0x09, 0xc9, 0x6e, 0xe4, 0x62, 0xa7, 0xc8, 0x69, 0x04, 0x72, 0xad, 0x92, 0x97, 0x3a, + 0xe5, 0x1c, 0xfb, 0x88, 0x71, 0xe8, 0xd7, 0x23, 0x00, 0xae, 0xda, 0xa6, 0x4d, 0x03, 0x64, 0xda, + 0x1e, 0x46, 0x84, 0x8b, 0x40, 0xc2, 0x5f, 0x0a, 0x60, 0x0a, 0x80, 0x87, 0xdd, 0x1a, 0x0f, 0xa7, 0x99, 0xc9, 0x11, 0x71, 0x50, 0xe0, 0xe3, 0x10, 0xdc, 0x1a, 0x45, 0x1a, 0x13, 0x72, 0xbe, 0x5b, - 0x43, 0xcc, 0x44, 0x82, 0x64, 0x62, 0xa3, 0x28, 0xd8, 0x2e, 0x80, 0xfc, 0x1b, 0x2d, 0xef, 0x4a, - 0x5a, 0x9c, 0x25, 0x09, 0x28, 0xfd, 0xac, 0x81, 0xd9, 0x2d, 0xe6, 0x5e, 0x67, 0x0c, 0xbb, 0x64, - 0x9d, 0x12, 0x56, 0xf7, 0x51, 0xf0, 0x06, 0xda, 0xd5, 0x8b, 0x60, 0xd2, 0x56, 0xc3, 0x32, 0x76, - 0xe6, 0xb5, 0x05, 0x6d, 0x69, 0xd4, 0x02, 0xd1, 0xd4, 0xa6, 0xa3, 0x5f, 0x01, 0x87, 0x22, 0x5d, - 0x65, 0xe8, 0x38, 0xc1, 0xfc, 0xf0, 0x82, 0xb6, 0x34, 0xb1, 0xa6, 0xff, 0xf9, 0xa4, 0x38, 0xbd, - 0x0b, 0x7d, 0x6f, 0xb5, 0x24, 0x66, 0x11, 0x63, 0x25, 0x6b, 0x2a, 0x02, 0x5e, 0x77, 0x9c, 0x40, - 0x3f, 0x05, 0xa6, 0x62, 0xcd, 0x3b, 0x68, 0x77, 0x7e, 0x44, 0xac, 0xb3, 0x62, 0x6b, 0xc2, 0xf8, - 0x25, 0x30, 0x2e, 0xfc, 0x41, 0xc1, 0xfc, 0xa8, 0x54, 0x3a, 0xff, 0xf8, 0xc1, 0xc5, 0x59, 0x95, - 0xf9, 0xeb, 0xa1, 0xd6, 0x5b, 0x3c, 0xc0, 0xc4, 0xb5, 0x14, 0x6e, 0xf5, 0xe8, 0xe7, 0xf7, 0x8a, - 0x43, 0xbf, 0xdf, 0x2b, 0x0e, 0x7d, 0xf2, 0xec, 0xfe, 0x79, 0x35, 0x59, 0x2a, 0x80, 0x93, 0x69, - 0xb1, 0x59, 0x88, 0xd5, 0x28, 0x61, 0xa8, 0xb4, 0xa7, 0x81, 0xff, 0x6c, 0x31, 0xf7, 0x56, 0xbd, - 0xe2, 0x63, 0x1e, 0x01, 0xb6, 0x30, 0xab, 0xa0, 0x2a, 0x6c, 0x60, 0x5a, 0x0f, 0xf4, 0xcb, 0x60, - 0x82, 0x49, 0x29, 0x47, 0x81, 0xe4, 0xa0, 0x97, 0x2f, 0x2d, 0xa8, 0xbe, 0x0d, 0xa6, 0xfc, 0x84, - 0x1e, 0xc9, 0xcd, 0xe4, 0xca, 0x05, 0x03, 0x57, 0x6c, 0x23, 0x99, 0x7e, 0x23, 0x91, 0xf0, 0xc6, - 0xb2, 0x91, 0xb4, 0x6d, 0xb5, 0x69, 0xe8, 0xcc, 0xc7, 0x48, 0x67, 0x3e, 0x56, 0x8f, 0x27, 0x19, - 0x68, 0xb9, 0x52, 0x3a, 0x0b, 0xce, 0xf4, 0x8c, 0x31, 0x66, 0xe3, 0xc7, 0xe1, 0x14, 0x36, 0x36, - 0x68, 0xbd, 0xe2, 0xa1, 0xdb, 0x94, 0x63, 0xe2, 0x3e, 0x37, 0x1b, 0x65, 0x30, 0xe7, 0xd4, 0x6b, - 0x1e, 0xb6, 0x21, 0x47, 0xe5, 0x06, 0xe5, 0xa8, 0x1c, 0x6d, 0x62, 0x45, 0xcc, 0xd9, 0x24, 0x0f, - 0xe1, 0xfe, 0xdd, 0x88, 0x16, 0xdc, 0xa6, 0x1c, 0xbd, 0xae, 0xe0, 0xd6, 0x31, 0x27, 0x6d, 0x5a, - 0x7f, 0x0f, 0xcc, 0x61, 0x72, 0x27, 0x80, 0xb6, 0x38, 0xac, 0xe5, 0x8a, 0x47, 0xed, 0x9d, 0x72, - 0x15, 0x41, 0x07, 0x05, 0x92, 0xa8, 0xc9, 0x95, 0xc5, 0x7e, 0xcc, 0xdf, 0x94, 0x68, 0xeb, 0x58, - 0x4b, 0xcd, 0x9a, 0xd0, 0x12, 0x4e, 0x77, 0x92, 0x3f, 0xfa, 0x8f, 0xc8, 0x4f, 0x52, 0x1a, 0x93, - 0xff, 0x95, 0x06, 0x0e, 0x6f, 0x31, 0xf7, 0xad, 0x9a, 0x03, 0x39, 0xda, 0x86, 0x01, 0xf4, 0x99, - 0xa0, 0x1b, 0xd6, 0x79, 0x95, 0x06, 0x98, 0xef, 0xf6, 0xa7, 0x3b, 0x86, 0xea, 0x97, 0xc1, 0x78, - 0x4d, 0x6a, 0x50, 0xec, 0xce, 0x1b, 0x9d, 0xf5, 0xd9, 0x08, 0x2d, 0xac, 0x8d, 0x3e, 0x7c, 0x52, - 0x1c, 0xb2, 0x14, 0x7a, 0x75, 0x5a, 0x3a, 0x1f, 0xeb, 0x29, 0xe5, 0xc0, 0x5c, 0x87, 0x4b, 0xb1, - 0xbb, 0x1f, 0x82, 0x99, 0x2d, 0xe6, 0x5a, 0xc8, 0xa7, 0x0d, 0x14, 0xc5, 0xd5, 0xbf, 0x64, 0xb4, - 0x05, 0x34, 0x3c, 0x70, 0x40, 0x5d, 0x8e, 0x9d, 0x00, 0xb9, 0x2e, 0xeb, 0xb1, 0x6b, 0xdf, 0x0f, - 0x4b, 0xdf, 0xd6, 0x03, 0x04, 0x79, 0xcb, 0xb7, 0xe7, 0xdd, 0xba, 0x39, 0x70, 0xd0, 0xae, 0x42, - 0x4c, 0x44, 0x40, 0xd2, 0x63, 0xeb, 0x80, 0x1c, 0x6f, 0x3a, 0xfa, 0x06, 0x38, 0xe8, 0x23, 0x0e, - 0x1d, 0xc8, 0xa1, 0xda, 0x65, 0xa5, 0x6e, 0xa2, 0xe3, 0x13, 0xa7, 0x90, 0x8a, 0xf2, 0x78, 0xa5, - 0x4e, 0x41, 0x0e, 0x13, 0xcc, 0x31, 0xf4, 0xf0, 0x07, 0xf2, 0xae, 0x29, 0xcb, 0x6c, 0x20, 0x8e, - 0x02, 0x26, 0x37, 0xda, 0xe4, 0xca, 0x4a, 0xb6, 0xda, 0xcd, 0xb6, 0xa5, 0xdb, 0xf1, 0x4a, 0x6b, - 0x1e, 0x67, 0x48, 0x14, 0x99, 0xad, 0x2d, 0x7a, 0x55, 0x92, 0xd9, 0x4e, 0x57, 0x44, 0x66, 0xdf, - 0x94, 0x96, 0xbe, 0x19, 0x91, 0x6c, 0x87, 0x9b, 0x24, 0x66, 0xdb, 0x00, 0x63, 0xb4, 0x49, 0x06, - 0x60, 0x3a, 0x84, 0x75, 0x9a, 0x19, 0xee, 0xda, 0x39, 0x1b, 0x60, 0x86, 0xa0, 0x66, 0x59, 0xa2, - 0xcb, 0xea, 0x5e, 0x09, 0x2f, 0x8e, 0x1e, 0xca, 0x0f, 0x13, 0xd4, 0x7c, 0x53, 0xac, 0x50, 0xd3, - 0xfa, 0xb5, 0x44, 0xc6, 0x46, 0x07, 0xcd, 0xd8, 0xa0, 0xb9, 0x1a, 0xfb, 0xf7, 0x73, 0xa5, 0x2f, - 0x80, 0x29, 0x11, 0x76, 0xbc, 0x03, 0xc7, 0xe5, 0x0e, 0x04, 0x04, 0x35, 0xd7, 0xd5, 0x26, 0x3c, - 0x01, 0x26, 0xc2, 0x82, 0x26, 0xc4, 0x07, 0xa4, 0xf8, 0x60, 0x38, 0xb1, 0xe9, 0xac, 0x02, 0x91, - 0xea, 0x90, 0x62, 0x75, 0x66, 0xda, 0xf3, 0xd4, 0x3a, 0x33, 0x1a, 0xc8, 0x8b, 0x3a, 0x85, 0xe2, - 0x22, 0x75, 0x03, 0x21, 0xb6, 0x8d, 0x02, 0x59, 0x04, 0x9f, 0xbb, 0x10, 0xf5, 0x4d, 0xeb, 0x22, - 0x18, 0x87, 0x3e, 0xad, 0x13, 0xae, 0x72, 0x39, 0xfd, 0xf8, 0xc1, 0x45, 0xa0, 0xb4, 0x6e, 0x12, - 0x6e, 0x29, 0x69, 0x57, 0x01, 0x38, 0x0d, 0x4a, 0xd9, 0xee, 0xc6, 0x51, 0xfd, 0xa0, 0x81, 0xe3, - 0x5b, 0xcc, 0xbd, 0x51, 0x27, 0x4e, 0x02, 0xb7, 0x4d, 0xa9, 0x97, 0x68, 0x30, 0xb4, 0xc1, 0x1a, - 0x8c, 0xfe, 0xb1, 0x5c, 0x6d, 0x8b, 0x65, 0x72, 0x25, 0x67, 0x28, 0x7d, 0xa2, 0xe1, 0x35, 0x54, - 0xc3, 0x6b, 0xac, 0x53, 0x4c, 0xd6, 0x26, 0x44, 0x0d, 0xf8, 0xf6, 0xd9, 0xfd, 0xf3, 0x5a, 0x1c, - 0x61, 0x6a, 0xff, 0xb2, 0x00, 0x0a, 0xe9, 0xfe, 0xc7, 0x21, 0xfe, 0x11, 0x26, 0xee, 0x6d, 0xcc, - 0xab, 0x4e, 0x00, 0x9b, 0x2f, 0x21, 0xcc, 0x6a, 0x22, 0xcc, 0x91, 0xde, 0x61, 0xbe, 0x2a, 0xc2, - 0xfc, 0xee, 0x97, 0xe2, 0x92, 0x8b, 0x79, 0xb5, 0x5e, 0x31, 0x6c, 0xea, 0xab, 0xf6, 0x5d, 0xfd, - 0xbb, 0xc8, 0x9c, 0x1d, 0xd5, 0xae, 0x8a, 0x05, 0x6c, 0x00, 0x4a, 0xbe, 0xd0, 0x64, 0xea, 0x33, - 0x02, 0x8e, 0xeb, 0x96, 0x1d, 0x7b, 0xa9, 0xf5, 0xf3, 0xf2, 0xd2, 0x7e, 0xbd, 0x8c, 0x1c, 0x2c, - 0x7d, 0xad, 0xc9, 0x0b, 0xf2, 0x56, 0x13, 0xa1, 0xda, 0x4b, 0x60, 0xfe, 0x38, 0x18, 0x77, 0x10, - 0xa1, 0x3e, 0x93, 0xcc, 0x4f, 0x58, 0x6a, 0x94, 0xce, 0xd3, 0x29, 0x50, 0xcc, 0x70, 0x2d, 0xde, - 0x3b, 0x7f, 0x0d, 0xcb, 0xf6, 0x78, 0xbd, 0x0a, 0x3d, 0x0f, 0x11, 0x17, 0xb5, 0xfa, 0x93, 0x26, - 0x11, 0x0f, 0x9b, 0x17, 0x11, 0xc3, 0x19, 0x30, 0xdd, 0x80, 0x1e, 0x76, 0x20, 0xa7, 0xea, 0xd5, - 0x20, 0x0e, 0xcb, 0x94, 0x75, 0x28, 0x9e, 0x95, 0x4f, 0x84, 0x33, 0x60, 0xda, 0xf6, 0x20, 0xf6, - 0x91, 0x53, 0xae, 0x22, 0xd1, 0xb4, 0xc9, 0x72, 0x3d, 0x62, 0x1d, 0x52, 0xb3, 0x37, 0xe5, 0xa4, - 0x7e, 0x0d, 0x8c, 0xab, 0x2e, 0x6f, 0x6c, 0x5f, 0x5d, 0x9e, 0x5a, 0xa5, 0xbf, 0x06, 0x26, 0x3d, - 0xc8, 0x78, 0xd9, 0xa6, 0xbe, 0x8f, 0xb9, 0xac, 0xae, 0xa2, 0x5b, 0xea, 0xea, 0x45, 0xd7, 0xa5, - 0xdc, 0x02, 0x02, 0x1c, 0xfe, 0xd6, 0xcf, 0x81, 0x23, 0xad, 0x40, 0x6a, 0xf5, 0x8a, 0x78, 0xc8, - 0x1c, 0x90, 0xa1, 0x1c, 0x8e, 0xe7, 0xb7, 0xe5, 0x74, 0x7a, 0x7e, 0x16, 0xc1, 0xe9, 0x5e, 0xdc, - 0x77, 0x35, 0x5a, 0x42, 0x9a, 0x6c, 0x66, 0x5e, 0x48, 0x3d, 0xce, 0x6c, 0xb4, 0x92, 0xd6, 0x23, - 0xd7, 0x56, 0x7e, 0x9b, 0x04, 0x23, 0x5b, 0xcc, 0xd5, 0x77, 0xc0, 0x4c, 0xf7, 0xf3, 0x71, 0xb1, - 0xfb, 0x1e, 0x4c, 0x7b, 0x8a, 0xe5, 0x8d, 0xc1, 0x70, 0xf1, 0xc1, 0xfe, 0x4c, 0x03, 0xf9, 0x1e, - 0xef, 0x35, 0x33, 0x55, 0x5d, 0xf6, 0x82, 0xfc, 0x95, 0x7d, 0x2e, 0xe8, 0xe1, 0x48, 0xdb, 0x53, - 0x69, 0x10, 0x47, 0x92, 0x0b, 0x06, 0x72, 0x24, 0xed, 0xe5, 0xa0, 0x57, 0xc0, 0x74, 0x47, 0xaf, - 0xfb, 0xdf, 0x54, 0x55, 0xed, 0xa0, 0xfc, 0xff, 0x06, 0x00, 0x25, 0x6d, 0x74, 0x74, 0x78, 0xe9, - 0x36, 0xda, 0x41, 0x19, 0x36, 0xd2, 0x7b, 0x10, 0x61, 0xa3, 0xe3, 0x3d, 0x91, 0x6e, 0xa3, 0x1d, - 0x94, 0x61, 0x23, 0xfd, 0x6d, 0xa0, 0xbf, 0x0b, 0xa6, 0xda, 0x5e, 0x58, 0xa7, 0x7a, 0x38, 0x18, - 0x42, 0xf2, 0xe7, 0xfa, 0x42, 0x62, 0xed, 0x1f, 0x81, 0xb9, 0xac, 0x0e, 0xea, 0x42, 0x7a, 0x76, - 0xd3, 0xd1, 0xf9, 0x57, 0xf6, 0x83, 0x8e, 0xcd, 0xbf, 0x0f, 0x8e, 0xa6, 0xb5, 0x3a, 0x4b, 0xa9, - 0xca, 0x52, 0x90, 0xf9, 0x4b, 0x83, 0x22, 0x93, 0x11, 0x67, 0xb5, 0x1e, 0xe9, 0x11, 0x67, 0xa0, - 0x33, 0x22, 0xee, 0x77, 0xcb, 0x73, 0x30, 0x9b, 0x7a, 0xf9, 0xa6, 0xe7, 0x2c, 0x0d, 0x9a, 0x5f, - 0x1e, 0x18, 0x1a, 0x5b, 0xfd, 0x54, 0x03, 0xb9, 0xec, 0x4b, 0x33, 0xbd, 0xa0, 0x65, 0xe2, 0xf3, - 0x97, 0xf7, 0x87, 0x6f, 0x3f, 0x2e, 0x6d, 0xb7, 0x42, 0xd6, 0x71, 0x49, 0x82, 0x32, 0x8f, 0x4b, - 0x5a, 0x85, 0xcf, 0x8f, 0x7d, 0x2c, 0x1a, 0xb2, 0xb5, 0xcd, 0x87, 0x7b, 0x05, 0xed, 0xd1, 0x5e, - 0x41, 0xfb, 0x75, 0xaf, 0xa0, 0x7d, 0xf9, 0xb4, 0x30, 0xf4, 0xe8, 0x69, 0x61, 0xe8, 0xa7, 0xa7, - 0x85, 0xa1, 0x77, 0xcc, 0x44, 0xcf, 0x04, 0x3d, 0x0f, 0x93, 0x0a, 0xe6, 0xcc, 0x94, 0xdf, 0x1c, - 0xef, 0x9a, 0xed, 0x9f, 0x1e, 0xe5, 0x4d, 0x5a, 0x19, 0x97, 0x5f, 0x1d, 0xff, 0xff, 0x77, 0x00, - 0x00, 0x00, 0xff, 0xff, 0xb4, 0xf2, 0x9d, 0x67, 0x4d, 0x16, 0x00, 0x00, + 0x47, 0xcc, 0x44, 0x82, 0x64, 0x62, 0xa3, 0x28, 0xd8, 0x2e, 0x80, 0xfc, 0x1b, 0x2d, 0xef, 0x4a, + 0x5a, 0x9c, 0x25, 0x09, 0x28, 0xff, 0xac, 0x81, 0x99, 0x4d, 0xe6, 0x5e, 0x67, 0x0c, 0xbb, 0x64, + 0x8d, 0x12, 0xd6, 0xf0, 0x51, 0xf0, 0x06, 0xda, 0xd5, 0x4b, 0x20, 0x67, 0xab, 0x61, 0x05, 0x3b, + 0x73, 0xda, 0xbc, 0xb6, 0x38, 0x6a, 0x81, 0x68, 0x6a, 0xc3, 0xd1, 0xaf, 0x80, 0x43, 0x91, 0xae, + 0x0a, 0x74, 0x9c, 0x60, 0x6e, 0x78, 0x5e, 0x5b, 0x9c, 0x58, 0xd5, 0xff, 0x7c, 0x52, 0x9a, 0xda, + 0x85, 0xbe, 0xb7, 0x52, 0x16, 0xb3, 0x88, 0xb1, 0xb2, 0x35, 0x19, 0x01, 0xaf, 0x3b, 0x4e, 0xa0, + 0x9f, 0x02, 0x93, 0xb1, 0xe6, 0x6d, 0xb4, 0x3b, 0x37, 0x22, 0xd6, 0x59, 0xb1, 0x35, 0x61, 0xfc, + 0x12, 0x18, 0x17, 0xfe, 0xa0, 0x60, 0x6e, 0x54, 0x2a, 0x9d, 0x7b, 0xfc, 0xe0, 0xe2, 0x8c, 0xca, + 0xfc, 0xf5, 0x50, 0xeb, 0x2d, 0x1e, 0x60, 0xe2, 0x5a, 0x0a, 0xb7, 0x72, 0xf4, 0xf3, 0x7b, 0xa5, + 0xa1, 0xdf, 0xef, 0x95, 0x86, 0x3e, 0x79, 0x76, 0xff, 0xbc, 0x9a, 0x2c, 0x17, 0xc1, 0xc9, 0xb4, + 0xd8, 0x2c, 0xc4, 0xea, 0x94, 0x30, 0x54, 0xde, 0xd3, 0xc0, 0x7f, 0x36, 0x99, 0x7b, 0xab, 0x51, + 0xf5, 0x31, 0x8f, 0x00, 0x9b, 0x98, 0x55, 0x51, 0x0d, 0x36, 0x31, 0x6d, 0x04, 0xfa, 0x65, 0x30, + 0xc1, 0xa4, 0x94, 0xa3, 0x40, 0x72, 0xd0, 0xcb, 0x97, 0x16, 0x54, 0xdf, 0x02, 0x93, 0x7e, 0x42, + 0x8f, 0xe4, 0x26, 0xb7, 0x7c, 0xc1, 0xc0, 0x55, 0xdb, 0x48, 0xa6, 0xdf, 0x48, 0x24, 0xbc, 0xb9, + 0x64, 0x24, 0x6d, 0x5b, 0x6d, 0x1a, 0x3a, 0xf3, 0x31, 0xd2, 0x99, 0x8f, 0x95, 0xe3, 0x49, 0x06, + 0x5a, 0xae, 0x94, 0xcf, 0x82, 0x33, 0x3d, 0x63, 0x8c, 0xd9, 0xf8, 0x71, 0x38, 0x85, 0x8d, 0x75, + 0xda, 0xa8, 0x7a, 0xe8, 0x36, 0xe5, 0x98, 0xb8, 0xcf, 0xcd, 0x46, 0x05, 0xcc, 0x3a, 0x8d, 0xba, + 0x87, 0x6d, 0xc8, 0x51, 0xa5, 0x49, 0x39, 0xaa, 0x44, 0x9b, 0x58, 0x11, 0x73, 0x36, 0xc9, 0x43, + 0xb8, 0x7f, 0xd7, 0xa3, 0x05, 0xb7, 0x29, 0x47, 0xaf, 0x2b, 0xb8, 0x75, 0xcc, 0x49, 0x9b, 0xd6, + 0xdf, 0x03, 0xb3, 0x98, 0xdc, 0x09, 0xa0, 0x2d, 0x0e, 0x6b, 0xa5, 0xea, 0x51, 0x7b, 0xbb, 0x52, + 0x43, 0xd0, 0x41, 0x81, 0x24, 0x2a, 0xb7, 0xbc, 0xd0, 0x8f, 0xf9, 0x9b, 0x12, 0x6d, 0x1d, 0x6b, + 0xa9, 0x59, 0x15, 0x5a, 0xc2, 0xe9, 0x4e, 0xf2, 0x47, 0xff, 0x11, 0xf9, 0x49, 0x4a, 0x63, 0xf2, + 0xbf, 0xd2, 0xc0, 0xe1, 0x4d, 0xe6, 0xbe, 0x55, 0x77, 0x20, 0x47, 0x5b, 0x30, 0x80, 0x3e, 0x13, + 0x74, 0xc3, 0x06, 0xaf, 0xd1, 0x00, 0xf3, 0xdd, 0xfe, 0x74, 0xc7, 0x50, 0xfd, 0x32, 0x18, 0xaf, + 0x4b, 0x0d, 0x8a, 0xdd, 0x39, 0xa3, 0xb3, 0x3e, 0x1b, 0xa1, 0x85, 0xd5, 0xd1, 0x87, 0x4f, 0x4a, + 0x43, 0x96, 0x42, 0xaf, 0x4c, 0x49, 0xe7, 0x63, 0x3d, 0xe5, 0x3c, 0x98, 0xed, 0x70, 0x29, 0x76, + 0x97, 0x81, 0xe9, 0x4d, 0xe6, 0x5a, 0xc8, 0xa7, 0x4d, 0x14, 0xc5, 0xd5, 0xbf, 0x64, 0xb4, 0x8e, + 0xf5, 0xf0, 0x80, 0xc7, 0x3a, 0x97, 0x3c, 0xce, 0x27, 0x40, 0xbe, 0xcb, 0x68, 0xec, 0xd1, 0xf7, + 0xc3, 0xd2, 0xa5, 0xb5, 0x00, 0x41, 0xde, 0x72, 0xe9, 0x79, 0x77, 0x6c, 0x1e, 0x1c, 0xb4, 0x6b, + 0x10, 0x13, 0x11, 0x87, 0xf4, 0xd5, 0x3a, 0x20, 0xc7, 0x1b, 0x8e, 0xbe, 0x0e, 0x0e, 0xfa, 0x88, + 0x43, 0x07, 0x72, 0xa8, 0x36, 0x57, 0xb9, 0x9b, 0xdf, 0xf8, 0xa0, 0x29, 0xa4, 0x62, 0x3a, 0x5e, + 0xa9, 0x53, 0x90, 0xc7, 0x04, 0x73, 0x0c, 0x3d, 0xfc, 0x81, 0xbc, 0x62, 0x2a, 0x32, 0x09, 0x88, + 0xa3, 0x80, 0xc9, 0xfd, 0x95, 0x5b, 0x5e, 0xce, 0x56, 0xbb, 0xd1, 0xb6, 0x74, 0x2b, 0x5e, 0x69, + 0xcd, 0xe1, 0x0c, 0x89, 0x4a, 0x6e, 0x6b, 0x67, 0x5e, 0x95, 0x64, 0xb6, 0xd3, 0x15, 0x91, 0xd9, + 0x37, 0x93, 0xe5, 0x6f, 0x46, 0x24, 0xdb, 0xe1, 0xde, 0x88, 0xd9, 0x36, 0xc0, 0x18, 0xdd, 0x21, + 0x03, 0x30, 0x1d, 0xc2, 0x3a, 0xcd, 0x0c, 0x77, 0x6d, 0x98, 0x75, 0x30, 0x4d, 0xd0, 0x4e, 0x45, + 0xa2, 0x2b, 0xea, 0x3a, 0x09, 0xef, 0x8b, 0x1e, 0xca, 0x0f, 0x13, 0xb4, 0xf3, 0xa6, 0x58, 0xa1, + 0xa6, 0xf5, 0x6b, 0x89, 0x8c, 0x8d, 0x0e, 0x9a, 0xb1, 0x41, 0x73, 0x35, 0xf6, 0xef, 0xe7, 0x4a, + 0x9f, 0x07, 0x93, 0x22, 0xec, 0x78, 0x07, 0x8e, 0xcb, 0x1d, 0x08, 0x08, 0xda, 0x59, 0x53, 0x9b, + 0xf0, 0x04, 0x98, 0x08, 0xeb, 0x98, 0x10, 0x1f, 0x90, 0xe2, 0x83, 0xe1, 0xc4, 0x86, 0xb3, 0x02, + 0x44, 0xaa, 0x43, 0x8a, 0xd5, 0x99, 0x69, 0xcf, 0x53, 0xeb, 0xcc, 0x68, 0xa0, 0x20, 0xca, 0x13, + 0x8a, 0x6b, 0xd3, 0x0d, 0x84, 0xd8, 0x16, 0x0a, 0x64, 0xed, 0x7b, 0xee, 0xfa, 0xd3, 0x37, 0xad, + 0x0b, 0x60, 0x1c, 0xfa, 0xb4, 0x41, 0xb8, 0xca, 0xe5, 0xd4, 0xe3, 0x07, 0x17, 0x81, 0xd2, 0xba, + 0x41, 0xb8, 0xa5, 0xa4, 0x5d, 0x05, 0xe9, 0x34, 0x28, 0x67, 0xbb, 0x1b, 0x47, 0xf5, 0x83, 0x06, + 0x8e, 0x6f, 0x32, 0xf7, 0x46, 0x83, 0x38, 0x09, 0xdc, 0x16, 0xa5, 0x5e, 0xa2, 0x00, 0x69, 0x83, + 0x15, 0xa0, 0xfe, 0xb1, 0x5c, 0x6d, 0x8b, 0x25, 0xb7, 0x9c, 0x37, 0x94, 0x3e, 0xd1, 0xe7, 0x1a, + 0xaa, 0xcf, 0x35, 0xd6, 0x28, 0x26, 0xab, 0x13, 0xa2, 0x06, 0x7c, 0xfb, 0xec, 0xfe, 0x79, 0x2d, + 0x8e, 0x30, 0xb5, 0x6d, 0x99, 0x07, 0xc5, 0x74, 0xff, 0xe3, 0x10, 0xff, 0x08, 0x13, 0xf7, 0x36, + 0xe6, 0x35, 0x27, 0x80, 0x3b, 0x2f, 0x21, 0xcc, 0x5a, 0x22, 0xcc, 0x91, 0xde, 0x61, 0xbe, 0x2a, + 0xc2, 0xfc, 0xee, 0x97, 0xd2, 0xa2, 0x8b, 0x79, 0xad, 0x51, 0x35, 0x6c, 0xea, 0xab, 0xae, 0x5d, + 0xfd, 0xbb, 0xc8, 0x9c, 0x6d, 0xd5, 0xa5, 0x8a, 0x05, 0x6c, 0x00, 0x4a, 0xbe, 0xd0, 0x64, 0xea, + 0x33, 0x02, 0x8e, 0xeb, 0x96, 0x1d, 0x7b, 0xa9, 0xf5, 0xf3, 0xf2, 0xd2, 0x7e, 0xbd, 0x8c, 0x1c, + 0x2c, 0x7f, 0xad, 0xc9, 0x7b, 0xf1, 0xd6, 0x0e, 0x42, 0xf5, 0x97, 0xc0, 0xfc, 0x71, 0x30, 0xee, + 0x20, 0x42, 0x7d, 0x26, 0x99, 0x9f, 0xb0, 0xd4, 0x28, 0x9d, 0xa7, 0x53, 0xa0, 0x94, 0xe1, 0x5a, + 0xbc, 0x77, 0xfe, 0x1a, 0x96, 0x5d, 0xf1, 0x5a, 0x0d, 0x7a, 0x1e, 0x22, 0x2e, 0x6a, 0xb5, 0x25, + 0x3b, 0x44, 0xbc, 0x67, 0x5e, 0x44, 0x0c, 0x67, 0xc0, 0x54, 0x13, 0x7a, 0xd8, 0x81, 0x9c, 0xaa, + 0xc7, 0x82, 0x38, 0x2c, 0x93, 0xd6, 0xa1, 0x78, 0x56, 0xbe, 0x0c, 0xce, 0x80, 0x29, 0xdb, 0x83, + 0xd8, 0x47, 0x4e, 0xa5, 0x86, 0x44, 0xaf, 0x26, 0xcb, 0xf5, 0x88, 0x75, 0x48, 0xcd, 0xde, 0x94, + 0x93, 0xfa, 0x35, 0x30, 0xae, 0x9a, 0xbb, 0xb1, 0x7d, 0x35, 0x77, 0x6a, 0x95, 0xfe, 0x1a, 0xc8, + 0x79, 0x90, 0xf1, 0x8a, 0x4d, 0x7d, 0x1f, 0x73, 0x59, 0x5d, 0x45, 0x93, 0xd4, 0xd5, 0x82, 0xae, + 0x49, 0xb9, 0x05, 0x04, 0x38, 0xfc, 0xad, 0x9f, 0x03, 0x47, 0x5a, 0x81, 0xd4, 0x1b, 0x55, 0xf1, + 0x7e, 0x39, 0x20, 0x43, 0x39, 0x1c, 0xcf, 0x6f, 0xc9, 0xe9, 0xf4, 0xfc, 0x2c, 0x80, 0xd3, 0xbd, + 0xb8, 0x8f, 0x93, 0xf4, 0xa1, 0xea, 0xaf, 0x84, 0x34, 0xd9, 0xcc, 0xbc, 0x90, 0x7a, 0xdc, 0x55, + 0x67, 0xa3, 0x46, 0x2b, 0x69, 0x3d, 0x72, 0x6d, 0xf9, 0xb7, 0x1c, 0x18, 0xd9, 0x64, 0xae, 0xbe, + 0x0d, 0xa6, 0xbb, 0x5f, 0x8d, 0x0b, 0xdd, 0xf7, 0x60, 0xda, 0x0b, 0xac, 0x60, 0x0c, 0x86, 0x8b, + 0x0f, 0xf6, 0x67, 0x1a, 0x28, 0xf4, 0x78, 0xa6, 0x99, 0xa9, 0xea, 0xb2, 0x17, 0x14, 0xae, 0xec, + 0x73, 0x41, 0x0f, 0x47, 0xda, 0x5e, 0x48, 0x83, 0x38, 0x92, 0x5c, 0x30, 0x90, 0x23, 0x69, 0x0f, + 0x06, 0xbd, 0x0a, 0xa6, 0x3a, 0x7a, 0xdd, 0xff, 0xa6, 0xaa, 0x6a, 0x07, 0x15, 0xfe, 0x37, 0x00, + 0x28, 0x69, 0xa3, 0xa3, 0xc3, 0x4b, 0xb7, 0xd1, 0x0e, 0xca, 0xb0, 0x91, 0xde, 0x83, 0x08, 0x1b, + 0x1d, 0xcf, 0x88, 0x74, 0x1b, 0xed, 0xa0, 0x0c, 0x1b, 0xe9, 0x6f, 0x03, 0xfd, 0x5d, 0x30, 0xd9, + 0xf6, 0xb0, 0x3a, 0xd5, 0xc3, 0xc1, 0x10, 0x52, 0x38, 0xd7, 0x17, 0x12, 0x6b, 0xff, 0x08, 0xcc, + 0x66, 0x75, 0x50, 0x17, 0xd2, 0xb3, 0x9b, 0x8e, 0x2e, 0xbc, 0xb2, 0x1f, 0x74, 0x6c, 0xfe, 0x7d, + 0x70, 0x34, 0xad, 0xd5, 0x59, 0x4c, 0x55, 0x96, 0x82, 0x2c, 0x5c, 0x1a, 0x14, 0x99, 0x8c, 0x38, + 0xab, 0xf5, 0x48, 0x8f, 0x38, 0x03, 0x9d, 0x11, 0x71, 0xbf, 0x5b, 0x9e, 0x83, 0x99, 0xd4, 0xcb, + 0x37, 0x3d, 0x67, 0x69, 0xd0, 0xc2, 0xd2, 0xc0, 0xd0, 0xd8, 0xea, 0xa7, 0x1a, 0xc8, 0x67, 0x5f, + 0x9a, 0xe9, 0x05, 0x2d, 0x13, 0x5f, 0xb8, 0xbc, 0x3f, 0x7c, 0xfb, 0x71, 0x69, 0xbb, 0x15, 0xb2, + 0x8e, 0x4b, 0x12, 0x94, 0x79, 0x5c, 0xd2, 0x2a, 0x7c, 0x61, 0xec, 0x63, 0xd1, 0x90, 0xad, 0x6e, + 0x3c, 0xdc, 0x2b, 0x6a, 0x8f, 0xf6, 0x8a, 0xda, 0xaf, 0x7b, 0x45, 0xed, 0xcb, 0xa7, 0xc5, 0xa1, + 0x47, 0x4f, 0x8b, 0x43, 0x3f, 0x3d, 0x2d, 0x0e, 0xbd, 0x63, 0x26, 0x7a, 0x26, 0xe8, 0x79, 0x98, + 0x54, 0x31, 0x67, 0xa6, 0xfc, 0xd4, 0x78, 0xd7, 0x6c, 0xff, 0xe2, 0x28, 0x6f, 0xd2, 0xea, 0xb8, + 0xfc, 0xd8, 0xf8, 0xff, 0xbf, 0x03, 0x00, 0x00, 0xff, 0xff, 0x76, 0x92, 0x78, 0xe6, 0x44, 0x16, + 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -2229,10 +2240,10 @@ func (m *MsgRemoveConsumer) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - if len(m.Authority) > 0 { - i -= len(m.Authority) - copy(dAtA[i:], m.Authority) - i = encodeVarintTx(dAtA, i, uint64(len(m.Authority))) + if len(m.Signer) > 0 { + i -= len(m.Signer) + copy(dAtA[i:], m.Signer) + i = encodeVarintTx(dAtA, i, uint64(len(m.Signer))) i-- dAtA[i] = 0x12 } @@ -3041,7 +3052,7 @@ func (m *MsgRemoveConsumer) Size() (n int) { if m.ConsumerId != 0 { n += 1 + sovTx(uint64(m.ConsumerId)) } - l = len(m.Authority) + l = len(m.Signer) if l > 0 { n += 1 + l + sovTx(uint64(l)) } @@ -4176,7 +4187,7 @@ func (m *MsgRemoveConsumer) Unmarshal(dAtA []byte) error { } case 2: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Authority", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Signer", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { @@ -4204,7 +4215,7 @@ func (m *MsgRemoveConsumer) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Authority = string(dAtA[iNdEx:postIndex]) + m.Signer = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex