diff --git a/docs/consumer-lifecycle.md b/docs/consumer-lifecycle.md index 06f9119..620f68a 100644 --- a/docs/consumer-lifecycle.md +++ b/docs/consumer-lifecycle.md @@ -2,6 +2,8 @@ This document describes the full lifecycle of a consumer chain in VAAS, from registration to deletion. +For how an unresponsive or lagging launched consumer is handled -- the liveness grace period, removal sweep, snapshot resync, and consumer safe mode -- see [consumer-liveness.md](consumer-liveness.md). + ## Phases ``` @@ -104,13 +106,17 @@ is stored and used for all subsequent VSC packet delivery. 3. The relayer relays the packets to the consumer. 4. The consumer applies the validator set changes on `EndBlock`. +VSC packets are diffs by default. If a consumer falls behind on acknowledgements, the provider instead sends an absolute snapshot of the full validator set so the consumer resyncs in a single packet, and the consumer is removed only after a sustained liveness failure rather than on a single packet timeout. See [consumer-liveness.md](consumer-liveness.md). + --- ## Phase 4: STOPPED -**Trigger:** `MsgRemoveConsumer` submitted by the consumer owner. +**Triggers:** either +- `MsgRemoveConsumer` submitted by the governance authority (removing a consumer requires the gov authority), or +- the automatic liveness sweep, when a launched consumer has produced no successful VSC acknowledgement for longer than the liveness grace period (see [consumer-liveness.md](consumer-liveness.md)). -**Requirements:** consumer must be in `LAUNCHED` phase. Only the owner can submit. +**Requirements:** consumer must be in `LAUNCHED` phase. **What happens on-chain:** 1. Phase is set to `STOPPED`. @@ -129,7 +135,8 @@ is stored and used for all subsequent VSC packet delivery. 1. Up to 200 due consumers are dequeued per block. 2. For each consumer, `DeleteConsumerChain` runs: - Deletes: IBC client ID mapping, consumer genesis, key assignments, equivocation - evidence minimum height, pending VSC packets, validator set, removal time. + evidence minimum height, pending VSC packets, validator set, removal time, and the + liveness state (last-ack time and the sent/acked VSC-id counters). - **Preserves** (for block explorer use): chain ID, phase, owner address, metadata, initialization parameters. 3. Phase is set to `DELETED`. @@ -143,5 +150,5 @@ is stored and used for all subsequent VSC packet delivery. | `REGISTERED` | `MsgCreateConsumer` | Any account | Consumer created, owner assigned | | `INITIALIZED` | `spawn_time` set | On-chain (automatic) | Queued for launch at spawn_time | | `LAUNCHED` | `spawn_time` elapsed | On-chain (BeginBlock) | Genesis built; operator starts consumer; relayer creates IBC path | -| `STOPPED` | `MsgRemoveConsumer` | Owner | Queued for deletion after unbonding period | +| `STOPPED` | `MsgRemoveConsumer` (gov) or liveness sweep | Governance / on-chain | Queued for deletion after unbonding period | | `DELETED` | Unbonding period elapsed | On-chain (BeginBlock) | State cleaned up | diff --git a/docs/consumer-liveness.md b/docs/consumer-liveness.md new file mode 100644 index 0000000..b5f4b7c --- /dev/null +++ b/docs/consumer-liveness.md @@ -0,0 +1,192 @@ +# Consumer Liveness, Removal, and Resync + +This document describes how the provider handles an unresponsive or lagging consumer +chain: when (and why) a consumer is removed, how a consumer that briefly falls behind +heals itself, and how a consumer protects itself while its validator set may be stale. + +It complements [consumer-lifecycle.md](consumer-lifecycle.md), which covers the overall +`REGISTERED -> INITIALIZED -> LAUNCHED -> STOPPED -> DELETED` progression. This document +zooms in on what keeps a `LAUNCHED` consumer healthy and what happens when it is not. + +## Design rationale + +The provider sends a Validator Set Change (VSC) packet to each launched consumer every +epoch. Two facts about that delivery shape the design: + +1. **The packet timeout is effectively 24h.** VSC packets are sent with a timeout of + `min(VaasTimeoutPeriod, MaxTimeoutDelta)`, and IBC v2 hard-caps `MaxTimeoutDelta` at 24h. + A packet timeout is therefore a weak signal -- a 24h outage of a single relayer or RPC + produces one -- so removing a consumer must not hinge on a single delivery failure. +2. **VSC packets are diffs.** Each packet carries only the validators that changed since the + previous one, applied on top of the consumer's current set. A lost diff is never re-sent + on its own, so a departed validator could otherwise linger in the consumer's active set -- + able to unbond on the provider and validate the consumer with no slashable stake. + +The model has three parts that answer these facts: a provider-local liveness clock that +decides removal on the provider's own block time rather than on per-packet delivery; a +self-healing snapshot resync so a lost diff cannot strand a stale validator; and a +consumer-side safe mode for the window in which a consumer's set may be out of date. + +## 1. Liveness clock and removal sweep + +The provider tracks, per consumer, the block time of the last **successful** VSC +acknowledgement (`lastAck`). It is seeded when the consumer enters `LAUNCHED` and refreshed +every time the consumer acknowledges a VSC packet. A successful ack is the strongest +possible progress signal: it proves the consumer received *and applied* the update. + +Each `BeginBlock`, the provider sweeps every launched consumer and stops any whose `lastAck` +is older than a **grace period**: + +``` +grace = providerUnbondingPeriod * LivenessGraceFraction +``` + +derived from the provider's own unbonding period (about 14 days at the 21-day default and the +`0.66` default fraction). `LivenessGraceFraction` is a provider module param: its default +mirrors the IBC client trusting-period fraction, so the grace ends around the point where +recovery stops being possible anyway while still leaving margin below the unbonding +(slashable) window, but operators can lower it -- on short-lived test or dev chains, for +instance -- to make the sweep fire in seconds rather than days. The sweep runs on the +provider's own block clock, so it does not depend +on a live IBC client or an available relayer -- it removes a consumer whether the silence is +caused by a dead consumer, a dead relayer, or an expired client. + +When the grace period is exceeded, the consumer is moved to `STOPPED` (then `DELETED` after +the unbonding period) exactly as a governance removal would do. See +[consumer-lifecycle.md](consumer-lifecycle.md) for the `STOPPED -> DELETED` mechanics. + +**Guarantee:** a launched-but-unresponsive consumer is removed within the grace period, +which is strictly less than the provider's unbonding period. So every validator that was in +the consumer's set as of its last successful sync is still slashable on the provider at the +time of removal. + +## 2. Snapshot resync (self-healing) + +Because VSC packets are diffs, a consumer that misses packets during an outage could diverge +from the provider's true validator set. To prevent that, the provider tracks two per-consumer +counters: the highest VSC id it has **sent** and the highest the consumer has **acked**. A +consumer is **behind** when: + +``` +highestAcked < highestSent +``` + +While a consumer is behind, the provider does not send a diff. It sends an **absolute +snapshot**: the complete current validator set, flagged with `is_snapshot = true` on the +packet. The consumer, on receiving a snapshot, **replaces** its set with it -- setting each +listed validator to its absolute power and removing (power 0) any validator it currently has +that the snapshot omits. Applying a snapshot is idempotent: a behind consumer converges to +exactly the provider's current set in one packet, regardless of which intermediate packets it +missed. Once an ack catches `highestAcked` up to `highestSent`, the provider resumes sending +ordinary diffs. + +The wire format gains only a single boolean (`is_snapshot`); the validator-update list is +reused (a diff when false, the full set when true). The global VSC id counter and the +consumer's out-of-order dedup are unchanged. + +**Guarantee:** a transient outage does not corrupt the consumer's set. As soon as +connectivity returns, the next snapshot heals it -- including removing any validator that +left the provider during the outage. + +## 3. IBC callbacks are log-only + +The sweep is the single authority for removing an unresponsive consumer, so the IBC +delivery-failure callbacks only log and return: + +- **Packet timeout** (`OnTimeoutPacketV2`): logs and returns; the next epoch retries. +- **Error acknowledgement**: logs and returns; the consumer keeps its grace budget. +- **Local send failure** (including an expired client): logs, leaves the pending packets + queued, and returns; the next epoch retries. + +This keeps an expired client from stranding a consumer: an expired client produces no acks, +so the sweep removes such a consumer once its grace elapses. + +## 4. Consumer safe mode + +While a consumer is behind, its local validator set may not match the provider's. The +consumer protects itself during that window. It records the block time of the last VSC packet +it received, and considers itself **stale** when it has not received one for longer than a +consumer-local threshold (`SafeModeThreshold`, default 3 hours). The threshold is required to +be strictly below the provider's liveness grace, enforced at `MsgCreateConsumer` / +`MsgUpdateConsumer` time (see section 5), so a lagging consumer always enters safe mode before +the provider's sweep would remove it. + +While stale (or while flagged in debt for unpaid fees), the consumer's transaction admission +gate restricts incoming transactions to `/ibc.core.*` and `/cosmos.gov.*` messages only, +rejecting value-bearing application transactions. This bounds the damage of a possibly-stale +set: the consumer refuses to do value-bearing work under a validator set it cannot trust, +while keeping the recovery path open -- `/ibc.core.*` still admits the incoming VSC packets +that let the consumer resync, and `/cosmos.gov.*` keeps governance available. A freshly +launched consumer is never treated as stale before its first VSC. + +## 5. Parameters and bounds + +| Parameter | Where | Bound | Default | +|---|---|---|---| +| `VaasTimeoutPeriod` | provider module param (VSC packet timeout) and per-consumer init param (consumer evidence packet timeout) | `(0, MaxTimeoutDelta (24h)]` | 1h | +| consumer `UnbondingPeriod` | per-consumer init param | `(0, providerUnbondingPeriod]` | provider default minus 1 day | +| `LivenessGraceFraction` | provider module param | `(0, 1)` | `0.66` | +| consumer `SafeModeThreshold` | per-consumer init param | `(0, provider liveness grace)` | 3h | + +`VaasTimeoutPeriod` is validated to `(0, 24h]` at both the provider module param boundary +and the per-consumer initialization-parameter boundary, so the configured value is honest +rather than silently capped. The default is one epoch-scale hour: an undelivered packet is +superseded by the next epoch's packet and a late one is dropped by the consumer's dedup, so +a long timeout buys nothing. No lower floor is imposed either: a persistently unreachable +consumer is handled by the liveness sweep, so a short timeout is not dangerous. + +The consumer `UnbondingPeriod` is bounded against the provider's at `MsgCreateConsumer` / +`MsgUpdateConsumer` time (it must not exceed the provider's), so a consumer cannot be +configured to outlive the provider's slashable window. No lower floor is enforced: an +unbonding short enough to make the relayer-derived trusting period impractical is an +operator concern (and is useful for short-lived test or dev chains), not a protocol minimum. + +The consumer `SafeModeThreshold` is bounded at the same `MsgCreateConsumer` / +`MsgUpdateConsumer` boundary: it must be strictly less than the provider's liveness grace +(`unbonding * LivenessGraceFraction`). Both values are cross-chain, but the provider holds +each at configuration time -- the threshold is an init parameter it ships to the consumer, +and the grace derives from its own unbonding and fraction -- so the relationship is enforced +provider-side rather than left to operator discipline. This guarantees the consumer's own +safe mode engages before the provider's sweep removes it. + +## 6. Operator guidance: the liveness query + +Operators can observe a consumer's liveness without waiting for removal: + +``` +providerd query provider consumer-liveness +``` + +It returns, all derived (no stored extra state): + +- `last_ack_time` -- block time of the consumer's last successful VSC ack. +- `grace_period` -- the derived grace for this provider. +- `removal_eta` -- `last_ack_time + grace_period`: when the sweep will remove the consumer + if no further ack arrives. +- `degraded` -- true once `last_ack_time` is older than half the grace period; an early + warning that a consumer is trending toward removal. + +A `degraded` consumer that is genuinely reachable should recover on its own once VSC packets +flow again (the next snapshot resyncs it and refreshes `lastAck`). If it does not recover +before `removal_eta`, it will be stopped. + +## 7. Guarantees and limitations + +**Guarantees** + +- A consumer is removed only after sustained ack-silence approaching, but staying within, + the provider's slashable window -- never on a single transient packet failure. +- A consumer that recovers within the grace period self-heals to the provider's exact + validator set via snapshot resync, with no operator or governance action. +- While a consumer's set may be stale, it refuses value-bearing transactions but keeps the + IBC recovery path open. + +**Out of scope (candidates for follow-up work)** + +- **Owner/governance force-resume and an explicit `DEGRADED` phase.** Automatic recovery + covers the common case; a manual "buy more time" path and a distinct degraded marker are + deferred. The `lastAck` field is the groundwork for them. +- **Reviving a consumer whose IBC client has expired** (beyond the trusting period). This + needs substitute-client / client-recovery governance and is a separate effort. +- **Relayer redundancy (multi-RPC / failover).** The single-RPC risk that motivated the + original concern is a relayer-tooling matter, not a chain-side one. diff --git a/proto/vaas/consumer/v1/genesis.proto b/proto/vaas/consumer/v1/genesis.proto index 196aeb7..04c25b2 100644 --- a/proto/vaas/consumer/v1/genesis.proto +++ b/proto/vaas/consumer/v1/genesis.proto @@ -29,6 +29,11 @@ message GenesisState { // Flag indicating whether the consumer VAAS module starts in pre-VAAS state bool preVAAS = 5; vaas.v1.ProviderInfo provider = 6 [ (gogoproto.nullable) = false ]; + // LastVSCRecvTime is the block time of the last VSC packet the consumer + // received; it drives IsVSCStale (safe mode). Round-tripped on restart so a + // state-export upgrade preserves the staleness clock. Absent for a new chain + // that has not received a VSC yet. + google.protobuf.Timestamp last_vsc_recv_time = 7 [ (gogoproto.stdtime) = true ]; } // HeightValsetUpdateID represents a mapping internal to the consumer VAAS module diff --git a/proto/vaas/provider/v1/genesis.proto b/proto/vaas/provider/v1/genesis.proto index 0c5f8d6..15c67cf 100644 --- a/proto/vaas/provider/v1/genesis.proto +++ b/proto/vaas/provider/v1/genesis.proto @@ -86,6 +86,17 @@ message ConsumerState { // absent otherwise. Used to re-derive the keeper's removal-time queue. google.protobuf.Timestamp removal_time = 12 [ (gogoproto.stdtime) = true ]; + + // Liveness clock, present once a consumer has launched: LastAckTime is the + // block time of the consumer's most recent successful VSC acknowledgement + // (the provider's per-consumer grace clock), and HighestSentVscId / + // HighestAckedVscId are the resync counters that decide whether the consumer + // is behind. Round-tripped so a state-export restart preserves the grace + // window and resync state instead of resetting them. + google.protobuf.Timestamp last_ack_time = 13 + [ (gogoproto.stdtime) = true ]; + uint64 highest_sent_vsc_id = 14; + uint64 highest_acked_vsc_id = 15; } // ValsetUpdateIdToHeight defines the genesis information for the mapping diff --git a/proto/vaas/provider/v1/provider.proto b/proto/vaas/provider/v1/provider.proto index 7c8cccc..38d9520 100644 --- a/proto/vaas/provider/v1/provider.proto +++ b/proto/vaas/provider/v1/provider.proto @@ -26,17 +26,23 @@ message Params { // TrustingPeriodFraction is used to compute the consumer and provider IBC // client's TrustingPeriod from the chain defined UnbondingPeriod string trusting_period_fraction = 1; + + // LivenessGraceFraction sets the consumer liveness grace period as a fraction + // of the provider unbonding period (grace = unbonding * fraction). A consumer + // with no successful VSC acknowledgement for longer than the grace is removed. + string liveness_grace_fraction = 2; + // Sent IBC packets will timeout after this duration - google.protobuf.Duration vaas_timeout_period = 2 + google.protobuf.Duration vaas_timeout_period = 3 [ (gogoproto.nullable) = false, (gogoproto.stdduration) = true ]; // The number of blocks that comprise an epoch. - int64 blocks_per_epoch = 3; + int64 blocks_per_epoch = 4; // The per-block fee amount charged for consumer chain operation. The denom // is not a parameter: it is fixed at module wiring (see Keeper.feeDenom) and // cannot be changed without a binary upgrade. - string fees_per_block_amount = 4 [ + string fees_per_block_amount = 5 [ (cosmos_proto.scalar) = "cosmos.Int", (gogoproto.customtype) = "cosmossdk.io/math.Int", (gogoproto.nullable) = false, @@ -47,7 +53,7 @@ message Params { // fees_per_block.Amount (the floor is fees_per_block.Amount * // min_deposit_blocks). Zero disables the check. The floor applies to every // funder including the gov authority. - uint64 min_deposit_blocks = 5; + uint64 min_deposit_blocks = 6; } // AddressList contains a list of consensus addresses @@ -170,6 +176,10 @@ message ConsumerInitializationParameters { // This param is a part of the cosmos sdk staking module. In the case of // a VAAS enabled consumer chain, the VAAS module acts as the staking module. int64 historical_entries = 7; + // How long the consumer may go without receiving a VSC packet before its + // tx admission gate enters safe mode (only ibc.core and gov messages pass). + google.protobuf.Duration safe_mode_threshold = 8 + [ (gogoproto.nullable) = false, (gogoproto.stdduration) = true ]; } // ConsumerIds contains consumer ids of chains diff --git a/proto/vaas/provider/v1/query.proto b/proto/vaas/provider/v1/query.proto index 63331ba..f22afba 100644 --- a/proto/vaas/provider/v1/query.proto +++ b/proto/vaas/provider/v1/query.proto @@ -6,6 +6,7 @@ option go_package = "github.com/allinbits/vaas/x/vaas/provider/types"; import "google/api/annotations.proto"; import "gogoproto/gogo.proto"; import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; import "vaas/provider/v1/genesis.proto"; import "vaas/provider/v1/provider.proto"; import "vaas/v1/shared_consumer.proto"; @@ -132,6 +133,13 @@ service Query { rpc ConsumerFeePoolClaims(QueryConsumerFeePoolClaimsRequest) returns (QueryConsumerFeePoolClaimsResponse) { option (google.api.http).get = "/vaas/provider/v1/consumer_fee_pool_claims/{consumer_id}"; } + + // QueryConsumerLiveness returns the liveness state of a launched consumer. + rpc QueryConsumerLiveness(QueryConsumerLivenessRequest) + returns (QueryConsumerLivenessResponse) { + option (google.api.http).get = + "/vaas/provider/consumer_liveness/{consumer_id}"; + } } message QueryConsumerGenesisRequest { @@ -358,3 +366,11 @@ message QueryConsumerFeePoolClaimsResponse { repeated DepositorClaim claims = 1 [(gogoproto.nullable) = false]; cosmos.base.query.v1beta1.PageResponse pagination = 2; } + +message QueryConsumerLivenessRequest { uint64 consumer_id = 1; } +message QueryConsumerLivenessResponse { + google.protobuf.Timestamp last_ack_time = 1 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; + google.protobuf.Duration grace_period = 2 [(gogoproto.stdduration) = true, (gogoproto.nullable) = false]; + google.protobuf.Timestamp removal_eta = 3 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; + bool degraded = 4; +} diff --git a/proto/vaas/v1/shared_consumer.proto b/proto/vaas/v1/shared_consumer.proto index c7c676e..e8fced7 100644 --- a/proto/vaas/v1/shared_consumer.proto +++ b/proto/vaas/v1/shared_consumer.proto @@ -39,6 +39,11 @@ message ConsumerParams { // which should be smaller than that of the provider in general. google.protobuf.Duration unbonding_period = 4 [ (gogoproto.nullable) = false, (gogoproto.stdduration) = true ]; + + // How long the consumer may go without receiving a VSC packet before its + // tx admission gate enters safe mode (only ibc.core and gov messages pass). + google.protobuf.Duration safe_mode_threshold = 5 + [ (gogoproto.nullable) = false, (gogoproto.stdduration) = true ]; } // ConsumerGenesisState defines shared genesis information between provider and diff --git a/proto/vaas/v1/wire.proto b/proto/vaas/v1/wire.proto index 946e99f..ae2080d 100644 --- a/proto/vaas/v1/wire.proto +++ b/proto/vaas/v1/wire.proto @@ -39,4 +39,11 @@ message ValidatorSetChangePacketData { // non-/ibc.core.* and non-/cosmos.gov.* messages until the provider // collects fees successfully and propagates false on the next VSC. bool consumer_in_debt = 3; + // is_snapshot indicates how the consumer must apply validator_updates. + // false: validator_updates is a diff against the consumer's previous set + // (the consumer accumulates). true: validator_updates is the provider's + // complete current set (the consumer replaces its set with it). The + // provider sends a snapshot while a consumer is behind on acks, so a + // consumer that missed packets converges to the exact set in one packet. + bool is_snapshot = 4; } diff --git a/tests/e2e/base_suite_test.go b/tests/e2e/base_suite_test.go new file mode 100644 index 0000000..d228449 --- /dev/null +++ b/tests/e2e/base_suite_test.go @@ -0,0 +1,549 @@ +package e2e + +// base_suite_test.go holds the shared Docker-based e2e scaffolding used by both +// IntegrationTestSuite (the main suite) and LivenessIntegrationTestSuite. The +// two suites differ only in a handful of constants (chain IDs, Docker network, +// host ports, container names) and in the genesis / config mutations they apply +// at bring-up. Those differences are captured in baseSuiteConfig; everything +// else -- container lifecycle, chain init, ts-relayer wiring, exec helpers -- +// lives here on baseTestSuite and is embedded by each concrete suite. + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "github.com/stretchr/testify/suite" +) + +// baseSuiteConfig captures everything that differs between the main and +// liveness suites. A concrete suite fills this in during its SetupSuite before +// invoking the shared bring-up helpers. +type baseSuiteConfig struct { + providerChainID string + consumerChainID string + dockerNetwork string + + // Init-container names and the MkdirTemp prefix keep the two suites' + // transient containers and host dirs from colliding on a shared host. + providerInitName string + consumerInitName string + tmpDirPrefix string + + // Host port bindings for the provider chain container. + providerRPCPort string + providerGRPCPort string + providerRESTPort string + providerP2PPort string + + // Host port bindings for the consumer chain container. + consumerRPCPort string + consumerGRPCPort string + consumerRESTPort string + consumerP2PPort string + + // create-consumer template file (under testdata) and the chain-id + // placeholder to substitute in it. + consumerTemplateFile string + consumerTemplatePlaceholder string + + // patchProviderGenesis mutates the provider's genesis app_state before the + // chain container starts. + patchProviderGenesis func(appState map[string]any) + + // patchProviderConfigToml / patchConsumerConfigToml, when non-nil, mutate + // the respective config.toml before the chain container starts (the + // liveness suite uses this for fast blocks). Leaving them nil skips the + // step. + patchProviderConfigToml func(path string) + patchConsumerConfigToml func(path string) +} + +// baseTestSuite carries the shared Docker state and helper methods. Concrete +// suites embed it and supply their own cfg and SetupSuite. +type baseTestSuite struct { + suite.Suite + + cfg baseSuiteConfig + + tmpDirs []string + provider *chain + consumer *chain + dkrPool *dockertest.Pool + dkrNet *dockertest.Network + providerValRes []*dockertest.Resource + consumerValRes []*dockertest.Resource + tsRelayerResource *dockertest.Resource +} + +// TearDownSuite cleans up all Docker resources and temp directories. +func (s *baseTestSuite) TearDownSuite() { + s.T().Log("tearing down e2e suite...") + + if os.Getenv("VAAS_E2E_SKIP_CLEANUP") == "true" { + s.T().Log("skipping cleanup (VAAS_E2E_SKIP_CLEANUP=true)") + return + } + + s.stopTSRelayer() + + // Purge consumer validators + for _, r := range s.consumerValRes { + if err := s.dkrPool.Purge(r); err != nil { + s.T().Logf("failed to purge consumer container: %v", err) + } + } + + // Purge provider validators + for _, r := range s.providerValRes { + if err := s.dkrPool.Purge(r); err != nil { + s.T().Logf("failed to purge provider container: %v", err) + } + } + + // Remove network + if s.dkrNet != nil { + if err := s.dkrPool.RemoveNetwork(s.dkrNet); err != nil { + s.T().Logf("failed to remove network: %v", err) + } + } + + // Remove temp dirs + for _, dir := range s.tmpDirs { + _ = os.RemoveAll(dir) + } +} + +// cleanupStaleContainers removes containers from previous failed test runs. +func (s *baseTestSuite) cleanupStaleContainers() { + staleNames := []string{ + s.cfg.providerInitName, + s.cfg.consumerInitName, + fmt.Sprintf("%s-val0", s.cfg.providerChainID), + fmt.Sprintf("%s-val0", s.cfg.consumerChainID), + fmt.Sprintf("%s-%s-ts-relayer", s.cfg.providerChainID, s.cfg.consumerChainID), + } + for _, name := range staleNames { + c, err := s.dkrPool.Client.InspectContainer(name) + if err != nil { + continue // container doesn't exist + } + s.T().Logf("removing stale container: %s", name) + _ = s.dkrPool.Client.RemoveContainer(docker.RemoveContainerOptions{ + ID: c.ID, + Force: true, + RemoveVolumes: true, + }) + } + // Also remove stale network + _ = s.dkrPool.Client.RemoveNetwork(s.cfg.dockerNetwork) +} + +// runInitContainer starts an init container with the given script mounted, +// waits for it to exit, checks the exit code, and purges it. +func (s *baseTestSuite) runInitContainer(name, scriptPath, containerScriptPath, dataDir, homePath string, env []string) { + initResource, err := s.dkrPool.RunWithOptions( + &dockertest.RunOptions{ + Name: name, + Repository: e2eChainImage, + NetworkID: s.dkrNet.Network.ID, + User: "nonroot", + Env: env, + Mounts: []string{ + fmt.Sprintf("%s:%s", dataDir, homePath), + fmt.Sprintf("%s:%s", scriptPath, containerScriptPath), + }, + Entrypoint: []string{"sh", containerScriptPath}, + }, + func(config *docker.HostConfig) { + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }, + ) + s.Require().NoError(err, "failed to start %s container", name) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + exitCode, err := s.dkrPool.Client.WaitContainerWithContext(initResource.Container.ID, ctx) + s.Require().NoError(err, "%s container wait failed", name) + + var logBuf bytes.Buffer + _ = s.dkrPool.Client.Logs(docker.LogsOptions{ + Container: initResource.Container.ID, + OutputStream: &logBuf, + ErrorStream: &logBuf, + Stdout: true, + Stderr: true, + }) + s.Require().Equal(0, exitCode, "%s container exited with code %d\noutput:\n%s", name, exitCode, logBuf.String()) + + s.Require().NoError(s.dkrPool.Purge(initResource), "failed to purge %s container", name) +} + +// initAndStartProvider initializes the provider chain using a temporary Docker +// container that runs provider-init.sh, then starts the actual chain container. +func (s *baseTestSuite) initAndStartProvider() { + // Create host directory for provider data + providerDir, err := os.MkdirTemp("", s.cfg.tmpDirPrefix+"provider-") + s.Require().NoError(err) + s.tmpDirs = append(s.tmpDirs, providerDir) + s.provider.dataDir = providerDir + + // Make writable + s.Require().NoError(os.Chmod(providerDir, 0o777)) + + // Run init script in a temporary container + scriptPath := filepath.Join(testDir(), "scripts", "provider-init.sh") + s.runInitContainer(s.cfg.providerInitName, scriptPath, "/scripts/provider-init.sh", providerDir, providerHomePath, []string{ + "BINARY=" + providerBinary, + "HOME_DIR=" + providerHomePath, + "CHAIN_ID=" + s.cfg.providerChainID, + "DENOM=" + bondDenom, + "MNEMONIC=" + relayerMnemonic, + }) + + // Apply the suite-specific genesis mutations. + genesisFile := filepath.Join(providerDir, "config", "genesis.json") + s.patchGenesisJSON(genesisFile, func(genesis map[string]any) { + appState := genesis["app_state"].(map[string]any) + s.cfg.patchProviderGenesis(appState) + }) + + // Apply the suite-specific config.toml mutations (e.g. fast blocks). + if s.cfg.patchProviderConfigToml != nil { + s.cfg.patchProviderConfigToml(filepath.Join(providerDir, "config", "config.toml")) + } + + // Now start the actual provider container + s.T().Log("starting provider chain container...") + + resource, err := s.dkrPool.RunWithOptions( + &dockertest.RunOptions{ + Name: fmt.Sprintf("%s-val0", s.cfg.providerChainID), + Repository: e2eChainImage, + NetworkID: s.dkrNet.Network.ID, + Mounts: []string{ + fmt.Sprintf("%s:%s", providerDir, providerHomePath), + }, + PortBindings: map[docker.Port][]docker.PortBinding{ + "26657/tcp": {{HostIP: "", HostPort: s.cfg.providerRPCPort}}, + "9090/tcp": {{HostIP: "", HostPort: s.cfg.providerGRPCPort}}, + "1317/tcp": {{HostIP: "", HostPort: s.cfg.providerRESTPort}}, + "26656/tcp": {{HostIP: "", HostPort: s.cfg.providerP2PPort}}, + }, + Cmd: []string{ + providerBinary, "start", + "--home", providerHomePath, + }, + }, + func(config *docker.HostConfig) { + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }, + ) + s.Require().NoError(err, "failed to start provider container") + + s.providerValRes = append(s.providerValRes, resource) + s.T().Logf("provider container started: %s", resource.Container.ID[:12]) + + // Wait for provider to produce blocks + waitCtx, waitCancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer waitCancel() + err = s.waitForChainHeight(waitCtx, "http://localhost:"+s.cfg.providerRPCPort, 3) + s.Require().NoError(err, "provider failed to produce blocks") + s.T().Log("provider chain is producing blocks") +} + +// registerConsumerOnProvider creates a consumer chain registration on the provider. +func (s *baseTestSuite) registerConsumerOnProvider() { + // Read consumer JSON template and substitute chain ID + templatePath := filepath.Join(testDir(), "testdata", s.cfg.consumerTemplateFile) + templateBytes, err := os.ReadFile(templatePath) + s.Require().NoError(err, "failed to read %s template", s.cfg.consumerTemplateFile) + + createConsumerJSON := strings.ReplaceAll(string(templateBytes), s.cfg.consumerTemplatePlaceholder, s.cfg.consumerChainID) + + // Write JSON to container and execute tx + s.dockerExecMust(s.providerValRes[0].Container.ID, []string{ + "sh", "-c", fmt.Sprintf("echo '%s' > /tmp/create_consumer.json", createConsumerJSON), + }) + + stdout, stderr, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "tx", "provider", "create-consumer", "/tmp/create_consumer.json", + "--from", "val", + "--home", providerHomePath, + "--keyring-backend", "test", + "--chain-id", s.cfg.providerChainID, + "--gas", "auto", + "--gas-adjustment", "1.5", + "--fees", "10000" + bondDenom, + "--broadcast-mode", "sync", + "-y", + "-o", "json", + }) + s.Require().NoErrorf(err, "failed to run create-consumer: stderr=%s", stderr.String()) + + // Assert the tx was accepted. Without this, a tx rejected at CheckTx (e.g. + // an init-parameter validation failure) passes silently here and only + // surfaces later as an empty consumer genesis. + var res struct { + Code int `json:"code"` + RawLog string `json:"raw_log"` + } + s.Require().NoErrorf(json.Unmarshal(stdout.Bytes(), &res), + "failed to decode create-consumer response:\nstdout=%q\nstderr=%q", stdout.String(), stderr.String()) + s.Require().Equalf(0, res.Code, "create-consumer tx failed: %s", res.RawLog) + + // Wait for the tx to be included + time.Sleep(10 * time.Second) +} + +// fetchConsumerGenesis queries the provider for the consumer genesis data. +func (s *baseTestSuite) fetchConsumerGenesis() []byte { + var output string + var lastErr error + + // Retry fetching consumer genesis (it may take a few blocks) + for range 30 { + stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "provider", "consumer-genesis", "0", + "--home", providerHomePath, + "--output", "json", + }) + if err == nil { + output = stdout.String() + if output != "" && !strings.Contains(output, "not found") && !strings.Contains(output, "Error") { + break + } + } + lastErr = err + time.Sleep(3 * time.Second) + } + s.Require().NotEmpty(output, "consumer genesis is empty, last error: %v", lastErr) + + s.T().Logf("fetched consumer genesis (%d bytes)", len(output)) + return []byte(output) +} + +// initAndStartConsumer initializes the consumer chain and starts it. +func (s *baseTestSuite) initAndStartConsumer(consumerGenesisJSON []byte) { + // Create host directory for consumer data + consumerDir, err := os.MkdirTemp("", s.cfg.tmpDirPrefix+"consumer-") + s.Require().NoError(err) + s.tmpDirs = append(s.tmpDirs, consumerDir) + s.consumer.dataDir = consumerDir + + s.Require().NoError(os.Chmod(consumerDir, 0o777)) + + // Run init script in a temporary container + scriptPath := filepath.Join(testDir(), "scripts", "consumer-init.sh") + s.runInitContainer(s.cfg.consumerInitName, scriptPath, "/scripts/consumer-init.sh", consumerDir, consumerHomePath, []string{ + "BINARY=" + consumerBinary, + "HOME_DIR=" + consumerHomePath, + "CHAIN_ID=" + s.cfg.consumerChainID, + "DENOM=" + bondDenom, + "MNEMONIC=" + relayerMnemonic, + }) + + // Patch consumer genesis with provider's consumer-genesis data on the host + genesisFile := filepath.Join(consumerDir, "config", "genesis.json") + err = patchConsumerGenesisWithProviderData(genesisFile, consumerGenesisJSON) + s.Require().NoError(err, "failed to patch consumer genesis") + + // Patch consumer slashing params for aggressive downtime detection + s.patchConsumerSlashingParams() + + // Apply the suite-specific config.toml mutations (e.g. fast blocks). + if s.cfg.patchConsumerConfigToml != nil { + s.cfg.patchConsumerConfigToml(filepath.Join(consumerDir, "config", "config.toml")) + } + + // Copy validator keys from provider to consumer + providerDir := s.provider.dataDir + err = copyFile( + filepath.Join(providerDir, "config", "priv_validator_key.json"), + filepath.Join(consumerDir, "config", "priv_validator_key.json"), + ) + s.Require().NoError(err, "failed to copy priv_validator_key.json") + + err = copyFile( + filepath.Join(providerDir, "config", "node_key.json"), + filepath.Join(consumerDir, "config", "node_key.json"), + ) + s.Require().NoError(err, "failed to copy node_key.json") + + // Start the actual consumer container + s.T().Log("starting consumer chain container...") + + resource, err := s.dkrPool.RunWithOptions( + &dockertest.RunOptions{ + Name: fmt.Sprintf("%s-val0", s.cfg.consumerChainID), + Repository: e2eChainImage, + NetworkID: s.dkrNet.Network.ID, + Mounts: []string{ + fmt.Sprintf("%s:%s", consumerDir, consumerHomePath), + }, + PortBindings: map[docker.Port][]docker.PortBinding{ + "26657/tcp": {{HostIP: "", HostPort: s.cfg.consumerRPCPort}}, + "9090/tcp": {{HostIP: "", HostPort: s.cfg.consumerGRPCPort}}, + "1317/tcp": {{HostIP: "", HostPort: s.cfg.consumerRESTPort}}, + "26656/tcp": {{HostIP: "", HostPort: s.cfg.consumerP2PPort}}, + }, + Cmd: []string{ + consumerBinary, "start", + "--home", consumerHomePath, + }, + }, + func(config *docker.HostConfig) { + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }, + ) + s.Require().NoError(err, "failed to start consumer container") + + s.consumerValRes = append(s.consumerValRes, resource) + s.T().Logf("consumer container started: %s", resource.Container.ID[:12]) + + // Wait for consumer to produce blocks + waitCtx, waitCancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer waitCancel() + err = s.waitForChainHeight(waitCtx, "http://localhost:"+s.cfg.consumerRPCPort, 3) + s.Require().NoError(err, "consumer failed to produce blocks") + s.T().Log("consumer chain is producing blocks") +} + +// setupTSRelayer starts the ts-relayer container, configures it with +// mnemonics and gas prices for both chains, and creates an IBC v2 path. +// The ts-relayer handles counterparty registration and packet relaying. +func (s *baseTestSuite) setupTSRelayer() { + s.startTSRelayer() + + s.tsRelayerAddMnemonic(s.cfg.providerChainID, relayerMnemonic) + s.tsRelayerAddMnemonic(s.cfg.consumerChainID, relayerMnemonic) + s.tsRelayerAddGasPrice(s.cfg.providerChainID, "0.025"+bondDenom) + s.tsRelayerAddGasPrice(s.cfg.consumerChainID, "0.025"+bondDenom) + + s.tsRelayerAddPath(IBCv2) + + s.tsRelayerDumpPaths() + s.startTSRelayerRelay() + s.T().Log("ts-relayer IBC v2 path configured") +} + +// waitForChainHeight polls a CometBFT RPC endpoint until the chain reaches +// the given block height. +func (s *baseTestSuite) waitForChainHeight(ctx context.Context, rpcEndpoint string, minHeight int64) error { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timeout waiting for chain at %s to reach height %d", rpcEndpoint, minHeight) + case <-ticker.C: + height, err := queryBlockHeight(rpcEndpoint) + if err != nil { + continue + } + if height >= minHeight { + return nil + } + } + } +} + +// waitForConsumerSync blocks until the provider has recorded a VSC ack from the +// consumer that is strictly newer than the launch seed, i.e. the relayer is +// actually delivering VSC packets and the consumer is acking them. +// +// This gate is what makes the timing-sensitive liveness tests deterministic: +// the provider seeds lastAck at launch and starts the grace clock then, but the +// relayer needs time (add-path, then the first relay cycle) before any VSC is +// delivered. Without this gate a test could assert against a consumer that has +// launched but never synced. The grace (~150s) is sized to exceed this +// first-sync latency, so the consumer is not swept while we wait here. +func (s *baseTestSuite) waitForConsumerSync(consumerID string) { + livenessURL := fmt.Sprintf("http://localhost:%s/vaas/provider/consumer_liveness/%s", + s.cfg.providerRESTPort, consumerID) + + var first string + s.Require().Eventuallyf(func() bool { + body, err := httpGet(livenessURL) + if err != nil { + return false + } + var res struct { + LastAckTime string `json:"last_ack_time"` + } + if json.Unmarshal(body, &res) != nil || res.LastAckTime == "" { + return false + } + // Require two distinct lastAck readings: the first records a baseline, + // the second proves lastAck is advancing (acks are flowing), not merely + // sitting at the launch seed. + if first == "" { + first = res.LastAckTime + s.T().Logf("consumer sync: baseline last_ack=%s", first) + return false + } + if res.LastAckTime != first { + s.T().Logf("consumer sync confirmed: last_ack advanced %s -> %s", first, res.LastAckTime) + return true + } + return false + }, 150*time.Second, 3*time.Second, + "consumer %s never synced a VSC ack (lastAck did not advance past the launch seed)", consumerID) +} + +// patchConfigToml lowers the CometBFT consensus timeouts so the chain produces +// blocks roughly every second instead of the ~5s default. Fast blocks matter +// for the liveness suite because every time-bounded step -- relayer add-path +// (which waits for client header heights), epoch/VSC cadence, and the VSC +// recv/ack round-trip -- is measured against the liveness grace. At the default +// block time these add up to minutes and the grace (which starts ticking at +// consumer launch) can expire before the consumer ever syncs. +func (s *baseTestSuite) patchConfigToml(path string) { + bz, err := os.ReadFile(path) + s.Require().NoError(err, "failed to read config.toml") + out := string(bz) + for from, to := range map[string]string{ + `timeout_propose = "3s"`: `timeout_propose = "500ms"`, + `timeout_commit = "5s"`: `timeout_commit = "1s"`, + `timeout_propose_delta = "500ms"`: `timeout_propose_delta = "200ms"`, + } { + s.Require().Containsf(out, from, "config.toml missing %q (default may have changed)", from) + out = strings.ReplaceAll(out, from, to) + } + s.Require().NoError(os.WriteFile(path, []byte(out), 0o600), "failed to write config.toml") +} + +// providerLogs returns the provider container's full stdout+stderr log. +func (s *baseTestSuite) providerLogs() string { + var buf bytes.Buffer + _ = s.dkrPool.Client.Logs(docker.LogsOptions{ + Container: s.providerValRes[0].Container.ID, + OutputStream: &buf, + ErrorStream: &buf, + Stdout: true, + Stderr: true, + }) + return buf.String() +} + +// consumerLogs returns the consumer container's full stdout+stderr log. +func (s *baseTestSuite) consumerLogs() string { + var buf bytes.Buffer + _ = s.dkrPool.Client.Logs(docker.LogsOptions{ + Container: s.consumerValRes[0].Container.ID, + OutputStream: &buf, + ErrorStream: &buf, + Stdout: true, + Stderr: true, + }) + return buf.String() +} diff --git a/tests/e2e/e2e_consumer_liveness_test.go b/tests/e2e/e2e_consumer_liveness_test.go new file mode 100644 index 0000000..59bdd2a --- /dev/null +++ b/tests/e2e/e2e_consumer_liveness_test.go @@ -0,0 +1,210 @@ +package e2e + +// e2e_consumer_liveness_test.go exercises the liveness scenarios introduced by +// issue #36 that require a full provider+consumer network: +// +// 1. testLivenessTransientOutage - brief outage + valset change; the consumer +// stays LAUNCHED and re-converges after recovery. +// 2. testLivenessRemoval - explicit governance removal transitions the +// consumer to STOPPED. +// +// Consumer-side safe mode (the MsgFilterDecorator restricting txs while the +// consumer is in debt or its VSC is stale) is not duplicated here: the ante +// gate's allow/block logic is covered deterministically by the ante unit tests +// (x/vaas/consumer/ante), the debt path end-to-end by testConsumerDebtFlow, and +// the real VSC-staleness safe mode end-to-end by the short-unbonding +// LivenessIntegrationTestSuite. +// +// HARNESS CONSTRAINTS +// +// These tests share the single IntegrationTestSuite spun up by TestVAAS in +// e2e_test.go. That consumer's unbonding_period is ~20 days (from +// testdata/create_consumer.json), so the derived liveness grace period +// (unbonding * 0.66) is ~13 days. Waiting for an automatic STOPPED transition +// in real-time is therefore impractical in CI. +// +// For testLivenessRemoval we issue an explicit remove-consumer via governance +// (gov authority is required since PR #53), then assert the provider transitions +// to STOPPED. The STOPPED -> DELETED leg relies on removal_time elapsing, which +// needs a short-unbonding consumer -- exercised by LivenessIntegrationTestSuite. +// +// For testLivenessTransientOutage we pause the consumer container (same +// mechanism as testDowntimeSlash) for a brief window while the provider's valset +// changes, then verify the consumer stays LAUNCHED and its VP re-converges after +// recovery. This is an end-to-end smoke of recovery-after-outage; it does not by +// itself prove *how* the consumer re-converged (the un-timed-out packet could be +// resent). That a timeout does not remove the consumer and that a behind consumer +// heals via a full snapshot are proven deterministically by unit tests +// (TestTimeoutDoesNotRemove, the TestSnapshot* set, TestSnapshotResyncEmitsEvent) +// and end-to-end by LivenessIntegrationTestSuite's forced-timeout snapshot test. +// +// Sequencing in TestVAAS: +// testLivenessTransientOutage runs after testValidatorSetSync (non-destructive VP change). +// testLivenessRemoval runs second-to-last, before testGenesisRoundTrip. + +import ( + "fmt" + "strings" + "time" +) + +// testLivenessTransientOutage is an end-to-end smoke that a brief consumer +// outage (shorter than the liveness grace period) does not stop the consumer, +// and that after recovery the consumer's consensus VP re-converges with the +// provider's updated VP. It does not assert the *mechanism* of re-convergence +// (the un-timed-out VSC packet could be resent rather than replaced by a +// snapshot); the snapshot path and the log-only timeout are covered by unit tests +// and by LivenessIntegrationTestSuite's forced-timeout snapshot test. +func (s *IntegrationTestSuite) testLivenessTransientOutage() { + s.Run("liveness transient outage: consumer stays LAUNCHED and re-converges", func() { + const consumerID = "0" + + // Record provider VP before the outage. + providerValsBefore, err := s.queryProviderNetValidators() + s.Require().NoError(err, "failed to query provider validators before outage") + s.Require().NotEmpty(providerValsBefore, "provider must have at least one validator") + _, vpsBefore := s.extractPubKeys(providerValsBefore) + s.Require().NotEmpty(vpsBefore) + initialProviderVP := vpsBefore[0] + + // Pause the consumer to simulate a transient network outage. + s.T().Log("pausing consumer container (transient outage starts)...") + err = s.dkrPool.Client.PauseContainer(s.consumerValRes[0].Container.ID) + s.Require().NoError(err, "failed to pause consumer container") + // Safety net: always unpause on exit so a failure before the explicit + // unpause below cannot leave the container paused and cascade into the + // next test (a paused container rejects docker exec). Errors ignored: + // the happy path already unpaused it, so this is a no-op then. + defer func() { _ = s.dkrPool.Client.UnpauseContainer(s.consumerValRes[0].Container.ID) }() + + // While the consumer is paused, delegate extra stake on the provider so + // it emits a VSC packet for the VP change. + s.T().Log("delegating extra stake on provider to generate a valset change...") + stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "keys", "show", "val", "--bech", "val", "-a", + "--home", providerHomePath, + "--keyring-backend", "test", + }) + s.Require().NoError(err, "failed to get validator operator address") + valoperAddr := strings.TrimSpace(stdout.String()) + + _, _, err = s.dockerExec(s.providerValRes[0].Container.ID, []string{ + // Delegate at least one unit of voting power (tokens / powerReduction, + // default 1e6) so the validator's integer power actually increases; a + // smaller delegation truncates to +0 power and the VSC never changes. + providerBinary, "tx", "staking", "delegate", valoperAddr, "100000000" + bondDenom, + "--from", "user", + "--home", providerHomePath, + "--keyring-backend", "test", + "--chain-id", providerChainID, + "--fees", "10000" + bondDenom, + "-y", + }) + s.Require().NoError(err, "failed to delegate extra stake on provider") + + // Wait for provider to reflect the new VP. + var newProviderVP uint64 + s.Require().Eventuallyf(func() bool { + vals, err := s.queryProviderNetValidators() + if err != nil || len(vals) == 0 { + return false + } + _, vps := s.extractPubKeys(vals) + if len(vps) == 0 { + return false + } + newProviderVP = vps[0] + return newProviderVP > initialProviderVP + }, 30*time.Second, 2*time.Second, + "provider VP did not increase after delegation (before=%d)", initialProviderVP) + s.T().Logf("provider VP changed: %d -> %d", initialProviderVP, newProviderVP) + + // Restore the consumer (outage ends well before grace expires). + s.T().Log("unpausing consumer container (outage ends)...") + err = s.dkrPool.Client.UnpauseContainer(s.consumerValRes[0].Container.ID) + s.Require().NoError(err, "failed to unpause consumer container") + + // The consumer must still be LAUNCHED after the transient outage. + phase := s.queryProviderConsumerPhase(consumerID) + s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, + "consumer %s must remain LAUNCHED after a transient outage", consumerID) + + // Consumer VP must re-converge to the updated provider VP after recovery. + s.T().Log("waiting for consumer valset to re-converge...") + s.Require().Eventuallyf(func() bool { + consumerVals, err := s.queryConsumerNetValidators() + if err != nil || len(consumerVals) == 0 { + return false + } + _, consumerVPs := s.extractPubKeys(consumerVals) + if len(consumerVPs) == 0 { + return false + } + s.T().Logf("consumer VP: %d (want %d)", consumerVPs[0], newProviderVP) + return consumerVPs[0] == newProviderVP + }, 3*time.Minute, 3*time.Second, + "consumer VP did not re-converge to provider VP %d after the transient outage", newProviderVP) + }) +} + +// testLivenessRemoval verifies the LAUNCHED -> STOPPED lifecycle when a +// consumer is explicitly removed via MsgRemoveConsumer. +// +// Explicit governance removal and the liveness sweep are the only ways to stop a +// consumer; a timed-out VSC packet does not. This test exercises the explicit +// removal path. +// +// Ordering: this test must run after all other sub-tests that depend on +// consumer "0" being LAUNCHED, and before testGenesisRoundTrip (which still +// succeeds because verifyExportedGenesis only requires consumer fields to be +// present, not a specific phase). +// +// Approximation note: the STOPPED -> DELETED transition requires +// removal_time to elapse (unbonding * 0.66). With the 20-day unbonding period +// used by the shared suite this takes ~13 days, so within the test window the +// consumer will remain STOPPED. A timing-faithful CI test would use a +// short-unbonding consumer (60s -> grace ~40s) launched in a dedicated suite. +func (s *IntegrationTestSuite) testLivenessRemoval() { + s.Run("liveness removal: explicit remove-consumer transitions to STOPPED", func() { + const consumerID = "0" + + // Precondition: consumer must still be LAUNCHED. + phase := s.queryProviderConsumerPhase(consumerID) + s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, + "consumer %s must be LAUNCHED before the liveness removal test", consumerID) + + // Issue an explicit remove-consumer via governance (gov authority is required + // since PR #53; a direct --from val tx would fail DeliverTx). + s.T().Log("submitting remove-consumer governance proposal...") + govAddr := s.queryGovAuthority() + removeJSON := fmt.Sprintf(`{ + "messages": [{ + "@type": "/vaas.provider.v1.MsgRemoveConsumer", + "authority": %q, + "consumer_id": %q + }], + "metadata": "ipfs://test", + "deposit": "10000000%s", + "title": "Remove consumer %s", + "summary": "e2e liveness removal test" +}`, govAddr, consumerID, bondDenom, consumerID) + s.submitAndPassProposal(removeJSON) + + // Wait for the provider to reflect the STOPPED phase. + s.T().Log("waiting for provider to move consumer to STOPPED or DELETED...") + s.Require().Eventuallyf(func() bool { + p := s.queryProviderConsumerPhase(consumerID) + s.T().Logf("consumer %s phase: %s", consumerID, p) + return p == "CONSUMER_PHASE_STOPPED" || p == "CONSUMER_PHASE_DELETED" + }, 2*time.Minute, 5*time.Second, + "provider did not transition consumer %s to STOPPED/DELETED after remove-consumer", + consumerID) + + finalPhase := s.queryProviderConsumerPhase(consumerID) + s.T().Logf("consumer %s terminal phase: %s", consumerID, finalPhase) + s.Require().True( + finalPhase == "CONSUMER_PHASE_STOPPED" || finalPhase == "CONSUMER_PHASE_DELETED", + "expected STOPPED or DELETED, got %q", finalPhase, + ) + }) +} diff --git a/tests/e2e/e2e_debt_test.go b/tests/e2e/e2e_debt_test.go index e1396b7..0c20a98 100644 --- a/tests/e2e/e2e_debt_test.go +++ b/tests/e2e/e2e_debt_test.go @@ -29,7 +29,7 @@ func (s *IntegrationTestSuite) queryConsumerFeePoolAddress(consumerID string) st // consumerUserBech32 returns the bech32 address of the consumer's "user" // account. It queries the key ring inside the consumer container. -func (s *IntegrationTestSuite) consumerUserBech32() string { +func (s *baseTestSuite) consumerUserBech32() string { stdout, _, err := s.dockerExec(s.consumerValRes[0].Container.ID, []string{ consumerBinary, "keys", "show", "user", "-a", "--home", consumerHomePath, @@ -43,13 +43,13 @@ func (s *IntegrationTestSuite) consumerUserBech32() string { // consumer in simulate mode. The simulation traverses the ante chain, so the // debt gate fires here just like it would for a real broadcast. Returns the // stderr output so callers can inspect rejection reasons. -func (s *IntegrationTestSuite) consumerBankSendDryRun() (string, error) { +func (s *baseTestSuite) consumerBankSendDryRun() (string, error) { user := s.consumerUserBech32() _, stderr, err := s.dockerExec(s.consumerValRes[0].Container.ID, []string{ consumerBinary, "tx", "bank", "send", user, user, "1" + bondDenom, "--home", consumerHomePath, "--keyring-backend", "test", - "--chain-id", consumerChainID, + "--chain-id", s.cfg.consumerChainID, "--fees", "1000" + bondDenom, "--dry-run", "-y", @@ -84,14 +84,14 @@ func (s *IntegrationTestSuite) providerFundAddress(addr, amount string) { // providerFundConsumerFeePool deposits `amount` into the named consumer's // fee pool via MsgFundConsumerFeePool, signed by val. -func (s *IntegrationTestSuite) providerFundConsumerFeePool(consumerID, amount string) { +func (s *baseTestSuite) providerFundConsumerFeePool(consumerID, amount string) { stdout, stderr, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ providerBinary, "tx", "provider", "fund-consumer-fee-pool", consumerID, amount, "--from", "val", "--home", providerHomePath, "--keyring-backend", "test", - "--chain-id", providerChainID, + "--chain-id", s.cfg.providerChainID, "--fees", "10000" + bondDenom, "-y", "-o", "json", @@ -107,7 +107,7 @@ func (s *IntegrationTestSuite) providerFundConsumerFeePool(consumerID, amount st // This surfaces handler-level rejections (e.g. the min-deposit floor) right at // the call site with the on-chain raw_log, instead of as a distant downstream // timeout. -func (s *IntegrationTestSuite) requireTxCommitted(broadcastOut []byte) { +func (s *baseTestSuite) requireTxCommitted(broadcastOut []byte) { var bres struct { TxHash string `json:"txhash"` Code int `json:"code"` diff --git a/tests/e2e/e2e_downtime_slash_test.go b/tests/e2e/e2e_downtime_slash_test.go index 4e251b2..69a5de1 100644 --- a/tests/e2e/e2e_downtime_slash_test.go +++ b/tests/e2e/e2e_downtime_slash_test.go @@ -51,7 +51,7 @@ func (s *IntegrationTestSuite) testDowntimeSlash() { // consumer chain and verify the provider slashes and jails that validator. } -func (s *IntegrationTestSuite) patchConsumerSlashingParams() { +func (s *baseTestSuite) patchConsumerSlashingParams() { s.patchGenesisJSON(s.consumer.dataDir+"/config/genesis.json", func(genesis map[string]any) { appState, ok := genesis["app_state"].(map[string]any) if !ok { diff --git a/tests/e2e/e2e_exec_test.go b/tests/e2e/e2e_exec_test.go index acf6f42..14db4a9 100644 --- a/tests/e2e/e2e_exec_test.go +++ b/tests/e2e/e2e_exec_test.go @@ -10,7 +10,7 @@ import ( ) // dockerExec runs a command in the specified Docker container and returns stdout/stderr. -func (s *IntegrationTestSuite) dockerExec(containerID string, cmd []string) (bytes.Buffer, bytes.Buffer, error) { +func (s *baseTestSuite) dockerExec(containerID string, cmd []string) (bytes.Buffer, bytes.Buffer, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -42,7 +42,7 @@ func (s *IntegrationTestSuite) dockerExec(containerID string, cmd []string) (byt } // dockerExecMust runs a command in a Docker container, failing the test on error. -func (s *IntegrationTestSuite) dockerExecMust(containerID string, cmd []string) { +func (s *baseTestSuite) dockerExecMust(containerID string, cmd []string) { stdout, stderr, err := s.dockerExec(containerID, cmd) if err != nil { s.T().Logf("cmd: %v", cmd) diff --git a/tests/e2e/e2e_liveness_suite_test.go b/tests/e2e/e2e_liveness_suite_test.go new file mode 100644 index 0000000..1551c46 --- /dev/null +++ b/tests/e2e/e2e_liveness_suite_test.go @@ -0,0 +1,552 @@ +package e2e + +// e2e_liveness_suite_test.go contains the LivenessIntegrationTestSuite, a +// Docker-based e2e suite that exercises liveness behaviour. The liveness grace +// period is unbonding * LivenessGraceFraction; this suite sets the fraction to +// 0.75 in provider genesis so the grace (~150s) is observable within a CI run, +// while keeping a long-enough provider/consumer unbonding (200s) that the +// relayer-derived IBC client trusting period (unbonding * 0.66 = ~132s) stays +// viable. Shrinking the grace via a short unbonding instead would collapse the +// trusting period and the relayer could never establish the clients. +// +// Two infrastructure measures keep the timing-sensitive assertions reliable: +// - Fast blocks (patchConfigToml lowers the CometBFT timeouts to ~1s blocks). +// The provider seeds lastAck at consumer launch and starts the grace clock +// then, so every setup step that precedes the first VSC sync -- relayer +// add-path, the first relay cycle, the recv/ack round-trip -- must finish +// inside the grace. At the default ~5s block time these add up to minutes +// and the consumer is swept before it ever syncs. +// - A first-sync gate (waitForConsumerSync) that blocks setup until the +// provider's lastAck has actually advanced past the launch seed, i.e. the +// relayer is delivering and the consumer is acking. Assertions then run from +// a known-synced state, independent of relayer startup jitter. +// +// The existing IntegrationTestSuite uses a ~21d provider unbonding, making the +// liveness sweep untestable in real time. This suite launches its own isolated +// set of containers (distinct chain IDs, Docker network, and host ports) and +// registers a single consumer via create_consumer_short_unbonding.json which +// carries a 200s consumer unbonding (200000000000 ns), 10m vaas_timeout +// (600000000000 ns), and 5s safe_mode_threshold (5000000000 ns). +// +// Test ordering within TestLivenessVAAS: +// 1. testRecoverBeforeGrace - brief pause < grace (~150s); consumer stays LAUNCHED. +// 2. testRealSafeMode - relayer paused > 5s; consumer enters restricted +// mode; bank send rejected; relayer unpaused; accepted. +// 3. testLivenessQuery - QueryConsumerLiveness via CLI; last_ack_time recent, +// non-zero grace, removal_eta present. +// 4. testForcedTimeoutSnapshotResync - relayer paused > the short (20s) provider +// vaas_timeout while the consumer keeps producing +// blocks, so a VSC packet genuinely times out; the +// consumer stays LAUNCHED (log-only OnTimeout) and +// heals via a snapshot resync (asserted via the +// provider timeout log and the consumer's +// snapshot-resync event). +// 5. testAutoSweepRemoval - relayer stopped indefinitely; poll until STOPPED. +// +// The STOPPED -> DELETED edge (deletion at stop-time + unbonding) is unit-tested +// (TestSweepRemovesStaleConsumer), not exercised here -- see the note above +// testAutoSweepRemoval's trailing comment. + +import ( + "encoding/json" + "fmt" + "strings" + "testing" + "time" + + "github.com/ory/dockertest/v3" + "github.com/stretchr/testify/suite" +) + +const ( + livenessProviderChainID = "provider-liveness" + livenessConsumerChainID = "consumer-liveness" + livenessDockerNetwork = "vaas-liveness-testnet" + + // Host port block: offset +10 from the existing suite (26657/9090/1317/26656) + // to avoid conflicts when both suites run in the same Docker host. + livenessProviderRPCPort = "26757" + livenessProviderGRPCPort = "9190" + livenessProviderRESTPort = "1417" + livenessProviderP2PPort = "26756" + + livenessConsumerRPCPort = "26767" + livenessConsumerGRPCPort = "9192" + livenessConsumerRESTPort = "1427" + livenessConsumerP2PPort = "26766" +) + +// LivenessIntegrationTestSuite mirrors IntegrationTestSuite with two key +// differences: +// - provider genesis is patched with unbonding_time "200s" and +// liveness_grace_fraction "0.75" (grace ~150s, trusting period ~132s) +// - consumer is registered via create_consumer_short_unbonding.json +// (200s unbonding, 10m vaas_timeout, 5s safe_mode_threshold) +type LivenessIntegrationTestSuite struct { + baseTestSuite +} + +// TestLivenessIntegrationTestSuite is the entry point for the short-unbonding +// liveness e2e suite. +func TestLivenessIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(LivenessIntegrationTestSuite)) +} + +// SetupSuite brings up provider + consumer + ts-relayer with short timers. +func (s *LivenessIntegrationTestSuite) SetupSuite() { + s.T().Log("setting up liveness e2e suite (short unbonding)...") + + s.cfg = baseSuiteConfig{ + providerChainID: livenessProviderChainID, + consumerChainID: livenessConsumerChainID, + dockerNetwork: livenessDockerNetwork, + providerInitName: "liveness-provider-init", + consumerInitName: "liveness-consumer-init", + tmpDirPrefix: "vaas-liveness-", + providerRPCPort: livenessProviderRPCPort, + providerGRPCPort: livenessProviderGRPCPort, + providerRESTPort: livenessProviderRESTPort, + providerP2PPort: livenessProviderP2PPort, + consumerRPCPort: livenessConsumerRPCPort, + consumerGRPCPort: livenessConsumerGRPCPort, + consumerRESTPort: livenessConsumerRESTPort, + consumerP2PPort: livenessConsumerP2PPort, + + consumerTemplateFile: "create_consumer_short_unbonding.json", + consumerTemplatePlaceholder: "CONSUMER_SHORT_CHAIN_ID", + + patchProviderGenesis: func(appState map[string]any) { + if gov, ok := appState["gov"].(map[string]any); ok { + if params, ok := gov["params"].(map[string]any); ok { + params["voting_period"] = "15s" + } + } + + if provider, ok := appState["provider"].(map[string]any); ok { + if params, ok := provider["params"].(map[string]any); ok { + // One block per epoch so a VSC packet (and its ack) flows every + // block. Combined with the fast block time (see patchConfigToml) + // this refreshes the provider's lastAck every few seconds, well + // inside the grace, so a healthy consumer is never swept between + // acks. + params["blocks_per_epoch"] = "1" + params["fees_per_block_amount"] = "1000" + // Short VSC packet timeout so testForcedTimeoutSnapshotResync can + // make a packet actually time out within a CI run (the relayer is + // paused while the consumer keeps producing blocks past this + // deadline). Timeouts are log-only, so this does not perturb the + // other liveness tests. + params["vaas_timeout_period"] = "20s" + // Shrink the liveness grace fraction so the grace period + // (unbonding * fraction = 200s * 0.75 = ~150s) is observable in + // a CI run, while keeping the unbonding itself long enough that + // the relayer-derived IBC client trusting period (unbonding * + // 0.66 = ~132s) survives the relayer add-path window. + // + // The grace must exceed the time from consumer launch to first + // VSC sync (the provider seeds lastAck at launch, so the clock + // starts before the relayer has delivered anything). With fast + // blocks that first sync is ~60-90s; 150s leaves margin. The + // suite also waits for that first sync explicitly before + // asserting (see waitForConsumerSync). + params["liveness_grace_fraction"] = "0.75" + } + } + + // Keep unbonding long enough for a viable IBC client trusting period + // (see liveness_grace_fraction note above); the short grace comes from + // the fraction, not from a short unbonding. + if staking, ok := appState["staking"].(map[string]any); ok { + if params, ok := staking["params"].(map[string]any); ok { + params["unbonding_time"] = "200s" + } + } + }, + + // Fast blocks so add-path, epochs, and VSC delivery all complete well + // inside the grace (see patchConfigToml). + patchProviderConfigToml: s.patchConfigToml, + patchConsumerConfigToml: s.patchConfigToml, + } + + var err error + + s.dkrPool, err = dockertest.NewPool("") + s.Require().NoError(err, "failed to create docker pool") + s.dkrPool.MaxWait = 5 * time.Minute + + s.cleanupStaleContainers() + + s.dkrNet, err = s.dkrPool.CreateNetwork(livenessDockerNetwork) + s.Require().NoError(err, "failed to create liveness docker network") + + s.T().Log("step 1: initializing liveness provider chain...") + s.provider = &chain{id: s.cfg.providerChainID} + s.initAndStartProvider() + + s.T().Log("step 2: registering consumer on liveness provider...") + s.registerConsumerOnProvider() + + s.T().Log("step 3: fetching consumer genesis from liveness provider...") + consumerGenesisJSON := s.fetchConsumerGenesis() + + s.T().Log("step 4: initializing liveness consumer chain...") + s.consumer = &chain{id: s.cfg.consumerChainID} + s.initAndStartConsumer(consumerGenesisJSON) + + s.T().Log("step 5: starting ts-relayer for liveness suite...") + s.setupTSRelayer() + + s.T().Log("step 6: waiting for the consumer to sync its first VSC...") + s.waitForConsumerSync("0") + + s.T().Log("liveness e2e suite setup complete!") +} + +// TestLivenessVAAS sequences the liveness tests in a safe order: non-destructive +// tests (recover, safe-mode, query) before the destructive sweep/delete tests. +func (s *LivenessIntegrationTestSuite) TestLivenessVAAS() { + s.testRecoverBeforeGrace() + s.testRealSafeMode() + s.testLivenessQuery() + s.testForcedTimeoutSnapshotResync() + s.testAutoSweepRemoval() +} + +// ---- test methods ---------------------------------------------------------- + +// testRecoverBeforeGrace pauses the consumer for ~10s (far less than the ~150s +// grace), unpauses it, and asserts the consumer remains LAUNCHED. This +// exercises the ack-refreshing code path: as long as an ack arrives before +// grace expires, the clock resets and the consumer is not swept. +func (s *LivenessIntegrationTestSuite) testRecoverBeforeGrace() { + s.Run("recover before grace: consumer stays LAUNCHED after brief outage", func() { + const consumerID = "0" + + // Non-fatal diagnostic: log the consumer's liveness state at the start of + // the run so the ack cadence (last_ack vs removal_eta) is visible even if + // a later assertion fails. The dedicated check is testLivenessQuery. + livenessURL := fmt.Sprintf("http://localhost:%s/vaas/provider/consumer_liveness/%s", + s.cfg.providerRESTPort, consumerID) + if body, err := httpGet(livenessURL); err == nil { + s.T().Logf("diagnostic: initial consumer liveness: %s", string(body)) + } + + phase := s.queryProviderConsumerPhase(consumerID) + s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, + "consumer %s must be LAUNCHED before recover-before-grace test", consumerID) + + s.T().Log("pausing consumer container for ~10s (far less than ~150s grace)...") + err := s.dkrPool.Client.PauseContainer(s.consumerValRes[0].Container.ID) + s.Require().NoError(err, "failed to pause consumer container") + + time.Sleep(10 * time.Second) + + s.T().Log("unpausing consumer container (outage ends before grace expiry)...") + err = s.dkrPool.Client.UnpauseContainer(s.consumerValRes[0].Container.ID) + s.Require().NoError(err, "failed to unpause consumer container") + + // Allow a few epochs for an ack to arrive and reset the liveness clock. + time.Sleep(10 * time.Second) + + // Diagnostic: capture the liveness state at the moment of assertion so a + // failure shows lastAck/removal_eta vs the sweep, not just the phase. + if body, err := httpGet(livenessURL); err == nil { + s.T().Logf("diagnostic: post-recovery consumer liveness: %s", string(body)) + } + + phase = s.queryProviderConsumerPhase(consumerID) + s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, + "consumer %s must remain LAUNCHED after a transient outage shorter than grace", consumerID) + s.T().Log("consumer remains LAUNCHED after recovery before grace") + }) +} + +// testRealSafeMode freezes VSC delivery so the consumer goes >5s without a VSC +// packet (the safe_mode_threshold). The consumer's MsgFilterDecorator then +// enters restricted mode and rejects bank sends. Once VSC delivery resumes, the +// consumer exits restricted mode. +// +// This exercises the VSC-staleness path directly (unlike the existing suite +// which relies on the fee-debt path as an approximation, because its +// safe_mode_threshold is 3h). +// +// Delivery is frozen by *pausing the relayer container* (docker pause), not by +// purging and rebuilding it. The distinction is essential: a rebuild runs +// add-path, which creates brand-new IBC clients. The provider, however, keeps +// using its already-discovered client as long as that client stays Active and +// has a counterparty (see discoverActiveConsumerClient) -- which it does for +// the ~132s trusting period. So a rebuild would leave the provider sending VSCs +// to the original, now-unrelayed client and the consumer would never recover +// within grace. Pausing the relayer leaves the original client untouched, so on +// unpause the relay loop resumes on the same client and delivery continues. It +// also keeps the outage short, decoupling this consumer-side test from the +// provider-side liveness grace. The consumer keeps producing blocks throughout +// (its staleness clock is wall-time on its own block height), which is why we +// pause the relayer rather than the consumer. +func (s *LivenessIntegrationTestSuite) testRealSafeMode() { + s.Run("real safe mode: VSC-stale consumer rejects bank sends, recovers when delivery resumes", func() { + const consumerID = "0" + + // Ensure the fee pool is funded so debt does not contaminate the test. + s.T().Log("funding consumer fee pool to avoid debt interference...") + s.providerFundConsumerFeePool(consumerID, "20000000"+feeDenom) + + // Verify normal mode before freezing delivery. + s.Require().Eventuallyf(func() bool { + out, err := s.consumerBankSendDryRun() + if err != nil { + return false + } + return !strings.Contains(out, "consumer chain is in debt") && + !strings.Contains(out, "stale validator set") + }, 2*time.Minute, 5*time.Second, + "consumer did not enter normal mode after fee pool funding") + + s.T().Log("pausing relayer container so VSC packets are no longer relayed...") + err := s.dkrPool.Client.PauseContainer(s.tsRelayerResource.Container.ID) + s.Require().NoError(err, "failed to pause relayer container") + + // Wait for the consumer to enter restricted mode (VSC stale after >5s). + s.T().Log("waiting for consumer to enter restricted mode (VSC stale after ~5s)...") + s.Require().Eventuallyf(func() bool { + out, err := s.consumerBankSendDryRun() + return err != nil || strings.Contains(out, "stale validator set") || + strings.Contains(out, "consumer chain is in debt") + }, 2*time.Minute, 3*time.Second, + "consumer did not enter restricted mode after VSC staleness") + s.T().Log("restricted mode confirmed; bank sends are rejected") + + // Resume VSC delivery by unpausing the relayer (same client, no rebuild). + s.T().Log("unpausing relayer container to resume VSC delivery...") + err = s.dkrPool.Client.UnpauseContainer(s.tsRelayerResource.Container.ID) + s.Require().NoError(err, "failed to unpause relayer container") + + // Wait for the consumer to exit restricted mode. + s.T().Log("waiting for consumer to exit restricted mode after VSC delivery resumes...") + s.Require().Eventuallyf(func() bool { + out, err := s.consumerBankSendDryRun() + if err != nil { + return false + } + return !strings.Contains(out, "stale validator set") && + !strings.Contains(out, "consumer chain is in debt") + }, 3*time.Minute, 5*time.Second, + "consumer did not exit restricted mode after VSC delivery resumed") + s.T().Log("consumer back in normal mode after VSC delivery") + }) +} + +// testLivenessQuery queries the provider's QueryConsumerLiveness via the CLI +// (there is no dedicated liveness CLI subcommand; we use the gRPC-gateway REST +// path which is exercised by the main suite's httpGet helper) and asserts that +// last_ack_time is recent (within the last 5 minutes), grace_period is non-zero, +// and removal_eta is present. +// +// The gRPC-gateway REST path is: +// +// GET /vaas/provider/consumer_liveness/{consumer_id} +// +// The response JSON uses protobuf JSON encoding: Timestamps are RFC3339 strings +// and Durations are strings like "19.8s". +func (s *LivenessIntegrationTestSuite) testLivenessQuery() { + s.Run("liveness query: last_ack_time recent, grace_period non-zero, removal_eta present", func() { + const consumerID = "0" + + // Confirm the consumer is still LAUNCHED before checking liveness. + phase := s.queryProviderConsumerPhase(consumerID) + s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, + "consumer must be LAUNCHED for liveness query test") + + // Diagnostic: log provider staking params and consumer chain init_params so + // the controller run can confirm the genesis patches actually took effect. + stakingOut, _, _ := s.dockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "staking", "params", + "--home", providerHomePath, "--output", "json", + }) + s.T().Logf("diagnostic: provider staking params: %s", stakingOut.String()) + + chainOut, _, _ := s.dockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "provider", "consumer-chain", consumerID, + "--home", providerHomePath, "--output", "json", + }) + s.T().Logf("diagnostic: consumer chain (init_params): %s", chainOut.String()) + + // Query liveness via the gRPC-gateway REST path. + // The response Timestamps are RFC3339 strings; Duration is a string like "19.8s". + livenessURL := fmt.Sprintf("http://localhost:%s/vaas/provider/consumer_liveness/%s", + s.cfg.providerRESTPort, consumerID) + + var livenessRes struct { + LastAckTime string `json:"last_ack_time"` + GracePeriod string `json:"grace_period"` + RemovalEta string `json:"removal_eta"` + } + + s.Require().Eventuallyf(func() bool { + body, err := httpGet(livenessURL) + if err != nil { + s.T().Logf("liveness REST query attempt failed: %v", err) + return false + } + s.T().Logf("liveness REST raw body: %s", string(body)) + if jsonErr := json.Unmarshal(body, &livenessRes); jsonErr != nil { + s.T().Logf("liveness REST decode failed: %v (body: %s)", jsonErr, string(body)) + return false + } + return livenessRes.LastAckTime != "" && livenessRes.GracePeriod != "" + }, 30*time.Second, 3*time.Second, + "liveness REST query did not return a valid response") + + s.T().Logf("diagnostic: consumer liveness: last_ack=%s grace=%s removal_eta=%s", + livenessRes.LastAckTime, livenessRes.GracePeriod, livenessRes.RemovalEta) + + // last_ack_time must be parseable and within 5 minutes of now. + lastAck, err := time.Parse(time.RFC3339Nano, livenessRes.LastAckTime) + if err != nil { + // gRPC-gateway may omit fractional seconds for round values. + lastAck, err = time.Parse(time.RFC3339, livenessRes.LastAckTime) + } + s.Require().NoError(err, "last_ack_time is not a valid RFC3339 timestamp: %s", livenessRes.LastAckTime) + s.Require().WithinDurationf(time.Now().UTC(), lastAck, 5*time.Minute, + "last_ack_time %s is not recent", livenessRes.LastAckTime) + + // grace_period must be non-empty and non-zero. + s.Require().NotEmpty(livenessRes.GracePeriod, "grace_period must be non-empty") + s.Require().NotEqualf("0s", livenessRes.GracePeriod, "grace_period must be non-zero") + + // removal_eta must be present and parseable. + s.Require().NotEmpty(livenessRes.RemovalEta, "removal_eta must be present") + retaAck, err := time.Parse(time.RFC3339Nano, livenessRes.RemovalEta) + if err != nil { + retaAck, err = time.Parse(time.RFC3339, livenessRes.RemovalEta) + } + s.Require().NoError(err, "removal_eta is not a valid RFC3339 timestamp: %s", livenessRes.RemovalEta) + s.Require().Truef(retaAck.After(time.Now().Add(-24*time.Hour)), + "removal_eta %s looks implausibly old", livenessRes.RemovalEta) + }) +} + +// testForcedTimeoutSnapshotResync proves the two behaviours the main suite's +// transient-outage smoke cannot: that a genuinely timed-out VSC packet does NOT +// remove the consumer (the log-only OnTimeout), and that a consumer that fell +// behind heals via a snapshot resync (not a resent diff). +// +// A real IBC timeout requires the packet to expire on the *consumer's* clock +// while still undelivered. Pausing the consumer would freeze that clock, so +// instead the relayer is paused while the consumer keeps producing blocks: its +// clock advances past the short vaas_timeout_period (20s) with packets +// undelivered. On unpause the relayer submits MsgTimeout for the expired packets +// (the provider's OnTimeout fires, log-only) and delivers a snapshot to the +// now-behind consumer. +func (s *LivenessIntegrationTestSuite) testForcedTimeoutSnapshotResync() { + s.Run("forced timeout: log-only OnTimeout keeps consumer LAUNCHED; behind consumer heals via snapshot", func() { + const consumerID = "0" + + s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", s.queryProviderConsumerPhase(consumerID), + "consumer %s must be LAUNCHED before the forced-timeout test", consumerID) + + // Pause the relayer; the consumer keeps producing blocks, so its clock + // advances past the 20s packet timeout while VSC packets go undelivered. + s.T().Log("pausing relayer for ~35s (> 20s vaas_timeout) to force VSC packet timeouts...") + s.Require().NoError(s.dkrPool.Client.PauseContainer(s.tsRelayerResource.Container.ID), + "failed to pause relayer container") + time.Sleep(35 * time.Second) + + s.T().Log("unpausing relayer; it submits MsgTimeout for expired packets and delivers a snapshot...") + s.Require().NoError(s.dkrPool.Client.UnpauseContainer(s.tsRelayerResource.Container.ID), + "failed to unpause relayer container") + + // (a) The log-only OnTimeout must not have removed the consumer. + s.Require().Eventuallyf(func() bool { + return s.queryProviderConsumerPhase(consumerID) == "CONSUMER_PHASE_LAUNCHED" + }, 30*time.Second, 3*time.Second, + "consumer %s must remain LAUNCHED despite VSC packet timeouts", consumerID) + s.T().Log("consumer remained LAUNCHED through the timeouts") + + // (b) Prove a real timeout actually fired -- otherwise (a) is vacuous. + // The provider logs the log-only OnTimeout handler. + s.Require().Eventuallyf(func() bool { + return strings.Contains(s.providerLogs(), "packet timeout, retrying next epoch") + }, 30*time.Second, 3*time.Second, + "provider never logged a VSC packet timeout; the timeout path was not exercised") + s.T().Log("provider processed a VSC timeout (OnTimeout is log-only)") + + // (c) Prove the behind consumer healed via a SNAPSHOT (not a resent diff): + // the consumer logs "applied snapshot resync" (and emits the matching + // event) only when it applies an is_snapshot packet. + s.Require().Eventuallyf(func() bool { + return strings.Contains(s.consumerLogs(), "applied snapshot resync") + }, 2*time.Minute, 5*time.Second, + "consumer never applied a snapshot resync after recovery") + s.T().Log("consumer applied a snapshot resync after recovery") + }) +} + +// testAutoSweepRemoval stops the relayer (and pauses the consumer) so no VSC +// acks return to the provider. After the liveness grace period (~150s) expires, +// the provider's SweepUnresponsiveConsumers moves the consumer to +// CONSUMER_PHASE_STOPPED. Polls with a ~2min window. +func (s *LivenessIntegrationTestSuite) testAutoSweepRemoval() { + s.Run("auto sweep removal: sustained outage causes LAUNCHED -> STOPPED", func() { + const consumerID = "0" + + phase := s.queryProviderConsumerPhase(consumerID) + s.Require().Equalf("CONSUMER_PHASE_LAUNCHED", phase, + "consumer %s must be LAUNCHED before auto-sweep test", consumerID) + + // Diagnostic: log provider staking params and consumer liveness state so + // the controller run can confirm the genesis patches took effect. + stakingOut, _, _ := s.dockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "staking", "params", + "--home", providerHomePath, "--output", "json", + }) + s.T().Logf("diagnostic: provider staking params: %s", stakingOut.String()) + + chainOut, _, _ := s.dockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "provider", "consumer-chain", consumerID, + "--home", providerHomePath, "--output", "json", + }) + s.T().Logf("diagnostic: consumer chain (init_params): %s", chainOut.String()) + + livenessURL := fmt.Sprintf("http://localhost:%s/vaas/provider/consumer_liveness/%s", + s.cfg.providerRESTPort, consumerID) + if body, err := httpGet(livenessURL); err == nil { + s.T().Logf("diagnostic: consumer liveness: %s", string(body)) + } + + s.T().Log("purging ts-relayer so no VSC acks are returned to provider...") + s.stopTSRelayer() + + s.T().Log("pausing consumer container to prevent acks...") + err := s.dkrPool.Client.PauseContainer(s.consumerValRes[0].Container.ID) + s.Require().NoError(err, "failed to pause consumer container for sweep test") + + // The grace period is provider_unbonding * liveness_grace_fraction = + // 200s * 0.75 = ~150s. Wait for the grace period to elapse before polling, + // so that the sweep has had time to fire by the first poll iteration. + const gracePlusBuffer = 160 * time.Second + s.T().Logf("outage started; waiting %s for grace period to elapse...", gracePlusBuffer) + time.Sleep(gracePlusBuffer) + + // Poll until the provider sweeps the consumer to STOPPED. + // Allow up to 2 minutes total (grace already elapsed above). + s.T().Log("polling for CONSUMER_PHASE_STOPPED (timeout 2min)...") + s.Require().Eventuallyf(func() bool { + p := s.queryProviderConsumerPhase(consumerID) + s.T().Logf("consumer %s phase: %s", consumerID, p) + return p == "CONSUMER_PHASE_STOPPED" + }, 2*time.Minute, 5*time.Second, + "provider did not sweep consumer %s to STOPPED within 2 minutes", consumerID) + + s.T().Logf("consumer %s successfully swept to STOPPED", consumerID) + }) +} + +// Note on STOPPED -> DELETED: the sweep schedules deletion at +// blockTime + providerUnbonding, the same path a governance removal takes. +// With a viable (relayer-survivable) ~200s unbonding, the real DELETED edge +// would only fire ~200s after STOPPED and so is deliberately not exercised +// here -- it is covered by TestSweepRemovesStaleConsumer, which asserts the +// removal_time scheduling directly. This e2e suite's contribution is proving +// the LAUNCHED -> STOPPED sweep fires end-to-end under real IBC silence. diff --git a/tests/e2e/e2e_setup_test.go b/tests/e2e/e2e_setup_test.go index c4c288b..d5270b4 100644 --- a/tests/e2e/e2e_setup_test.go +++ b/tests/e2e/e2e_setup_test.go @@ -1,19 +1,15 @@ package e2e import ( - "bytes" - "context" "fmt" "os" "os/exec" "path/filepath" "runtime" - "strings" "testing" "time" "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/suite" "github.com/cosmos/cosmos-sdk/codec" @@ -30,17 +26,9 @@ const ( // IntegrationTestSuite is the main e2e test suite that orchestrates // provider chain and consumer chain containers. type IntegrationTestSuite struct { - suite.Suite + baseTestSuite - cdc codec.Codec - tmpDirs []string - provider *chain - consumer *chain - dkrPool *dockertest.Pool - dkrNet *dockertest.Network - providerValRes []*dockertest.Resource - consumerValRes []*dockertest.Resource - tsRelayerResource *dockertest.Resource + cdc codec.Codec } // makeCodec creates a proto codec with the standard cosmos SDK interfaces registered. @@ -71,6 +59,45 @@ func testDir() string { func (s *IntegrationTestSuite) SetupSuite() { s.T().Log("setting up e2e integration test suite...") + s.cfg = baseSuiteConfig{ + providerChainID: providerChainID, + consumerChainID: consumerChainID, + dockerNetwork: dockerNetwork, + providerInitName: "provider-init", + consumerInitName: "consumer-init", + tmpDirPrefix: "vaas-e2e-", + providerRPCPort: "26657", + providerGRPCPort: "9090", + providerRESTPort: "1317", + providerP2PPort: "26656", + consumerRPCPort: "26667", + consumerGRPCPort: "9092", + consumerRESTPort: "1327", + consumerP2PPort: "26666", + + consumerTemplateFile: "create_consumer.json", + consumerTemplatePlaceholder: "CONSUMER_CHAIN_ID", + + patchProviderGenesis: func(appState map[string]any) { + // Set fast voting period + if gov, ok := appState["gov"].(map[string]any); ok { + if params, ok := gov["params"].(map[string]any); ok { + params["voting_period"] = "15s" + } + } + + // Set fast epoch for VSC and a small per-block fee amount. The fee + // denom is fixed to feeDenom at module wiring, only the amount is + // configurable here. val is funded with feeDenom in provider-init.sh + if provider, ok := appState["provider"].(map[string]any); ok { + if params, ok := provider["params"].(map[string]any); ok { + params["blocks_per_epoch"] = "5" + params["fees_per_block_amount"] = "1000" + } + } + }, + } + s.cdc = makeCodec() var err error @@ -89,7 +116,7 @@ func (s *IntegrationTestSuite) SetupSuite() { s.Require().NoError(err, "failed to create docker network") s.T().Log("step 1: initializing provider chain...") - s.provider = &chain{id: providerChainID} + s.provider = &chain{id: s.cfg.providerChainID} s.initAndStartProvider() s.T().Log("step 2: registering consumer chain on provider...") @@ -99,7 +126,7 @@ func (s *IntegrationTestSuite) SetupSuite() { consumerGenesisJSON := s.fetchConsumerGenesis() s.T().Log("step 4: initializing consumer chain...") - s.consumer = &chain{id: consumerChainID} + s.consumer = &chain{id: s.cfg.consumerChainID} s.initAndStartConsumer(consumerGenesisJSON) s.T().Log("step 5: starting ts-relayer and creating IBC v2 path...") @@ -111,374 +138,6 @@ func (s *IntegrationTestSuite) SetupSuite() { s.T().Log("e2e test suite setup complete!") } -// TearDownSuite cleans up all Docker resources and temp directories. -func (s *IntegrationTestSuite) TearDownSuite() { - s.T().Log("tearing down e2e integration test suite...") - - if os.Getenv("VAAS_E2E_SKIP_CLEANUP") == "true" { - s.T().Log("skipping cleanup (VAAS_E2E_SKIP_CLEANUP=true)") - return - } - - s.stopTSRelayer() - - // Purge consumer validators - for _, r := range s.consumerValRes { - if err := s.dkrPool.Purge(r); err != nil { - s.T().Logf("failed to purge consumer container: %v", err) - } - } - - // Purge provider validators - for _, r := range s.providerValRes { - if err := s.dkrPool.Purge(r); err != nil { - s.T().Logf("failed to purge provider container: %v", err) - } - } - - // Remove network - if s.dkrNet != nil { - if err := s.dkrPool.RemoveNetwork(s.dkrNet); err != nil { - s.T().Logf("failed to remove network: %v", err) - } - } - - // Remove temp dirs - for _, dir := range s.tmpDirs { - _ = os.RemoveAll(dir) - } -} - -// cleanupStaleContainers removes containers from previous failed test runs. -func (s *IntegrationTestSuite) cleanupStaleContainers() { - staleNames := []string{ - "provider-init", - "consumer-init", - fmt.Sprintf("%s-val0", providerChainID), - fmt.Sprintf("%s-val0", consumerChainID), - fmt.Sprintf("%s-%s-ts-relayer", providerChainID, consumerChainID), - } - for _, name := range staleNames { - c, err := s.dkrPool.Client.InspectContainer(name) - if err != nil { - continue // container doesn't exist - } - s.T().Logf("removing stale container: %s", name) - _ = s.dkrPool.Client.RemoveContainer(docker.RemoveContainerOptions{ - ID: c.ID, - Force: true, - RemoveVolumes: true, - }) - } - // Also remove stale network - _ = s.dkrPool.Client.RemoveNetwork(dockerNetwork) -} - -// runInitContainer starts an init container with the given script mounted, -// waits for it to exit, checks the exit code, and purges it. -func (s *IntegrationTestSuite) runInitContainer(name, scriptPath, containerScriptPath, dataDir, homePath string, env []string) { - initResource, err := s.dkrPool.RunWithOptions( - &dockertest.RunOptions{ - Name: name, - Repository: e2eChainImage, - NetworkID: s.dkrNet.Network.ID, - User: "nonroot", - Env: env, - Mounts: []string{ - fmt.Sprintf("%s:%s", dataDir, homePath), - fmt.Sprintf("%s:%s", scriptPath, containerScriptPath), - }, - Entrypoint: []string{"sh", containerScriptPath}, - }, - func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }, - ) - s.Require().NoError(err, "failed to start %s container", name) - - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancel() - - exitCode, err := s.dkrPool.Client.WaitContainerWithContext(initResource.Container.ID, ctx) - s.Require().NoError(err, "%s container wait failed", name) - - var logBuf bytes.Buffer - _ = s.dkrPool.Client.Logs(docker.LogsOptions{ - Container: initResource.Container.ID, - OutputStream: &logBuf, - ErrorStream: &logBuf, - Stdout: true, - Stderr: true, - }) - s.Require().Equal(0, exitCode, "%s container exited with code %d\noutput:\n%s", name, exitCode, logBuf.String()) - - s.Require().NoError(s.dkrPool.Purge(initResource), "failed to purge %s container", name) -} - -// initAndStartProvider initializes the provider chain using a temporary Docker -// container that runs provider-init.sh, then starts the actual chain container. -func (s *IntegrationTestSuite) initAndStartProvider() { - // Create host directory for provider data - providerDir, err := os.MkdirTemp("", "vaas-e2e-provider-") - s.Require().NoError(err) - s.tmpDirs = append(s.tmpDirs, providerDir) - s.provider.dataDir = providerDir - - // Make writable - s.Require().NoError(os.Chmod(providerDir, 0o777)) - - // Run init script in a temporary container - scriptPath := filepath.Join(testDir(), "scripts", "provider-init.sh") - s.runInitContainer("provider-init", scriptPath, "/scripts/provider-init.sh", providerDir, providerHomePath, []string{ - "BINARY=" + providerBinary, - "HOME_DIR=" + providerHomePath, - "CHAIN_ID=" + providerChainID, - "DENOM=" + bondDenom, - "MNEMONIC=" + relayerMnemonic, - }) - - // Modify genesis on the host: set fast voting period and small blocks_per_epoch - genesisFile := filepath.Join(providerDir, "config", "genesis.json") - s.patchGenesisJSON(genesisFile, func(genesis map[string]any) { - appState := genesis["app_state"].(map[string]any) - - // Set fast voting period - if gov, ok := appState["gov"].(map[string]any); ok { - if params, ok := gov["params"].(map[string]any); ok { - params["voting_period"] = "15s" - } - } - - // Set fast epoch for VSC and a small per-block fee amount. The fee - // denom is fixed to feeDenom at module wiring, only the amount is - // configurable here. val is funded with feeDenom in provider-init.sh - if provider, ok := appState["provider"].(map[string]any); ok { - if params, ok := provider["params"].(map[string]any); ok { - params["blocks_per_epoch"] = "5" - params["fees_per_block_amount"] = "1000" - } - } - }) - - // Now start the actual provider container - s.T().Log("starting provider chain container...") - - resource, err := s.dkrPool.RunWithOptions( - &dockertest.RunOptions{ - Name: fmt.Sprintf("%s-val0", providerChainID), - Repository: e2eChainImage, - NetworkID: s.dkrNet.Network.ID, - Mounts: []string{ - fmt.Sprintf("%s:%s", providerDir, providerHomePath), - }, - PortBindings: map[docker.Port][]docker.PortBinding{ - "26657/tcp": {{HostIP: "", HostPort: "26657"}}, - "9090/tcp": {{HostIP: "", HostPort: "9090"}}, - "1317/tcp": {{HostIP: "", HostPort: "1317"}}, - "26656/tcp": {{HostIP: "", HostPort: "26656"}}, - }, - Cmd: []string{ - providerBinary, "start", - "--home", providerHomePath, - }, - }, - func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }, - ) - s.Require().NoError(err, "failed to start provider container") - - s.providerValRes = append(s.providerValRes, resource) - s.T().Logf("provider container started: %s", resource.Container.ID[:12]) - - // Wait for provider to produce blocks - waitCtx, waitCancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer waitCancel() - err = s.waitForChainHeight(waitCtx, "http://localhost:26657", 3) - s.Require().NoError(err, "provider failed to produce blocks") - s.T().Log("provider chain is producing blocks") -} - -// registerConsumerOnProvider creates a consumer chain registration on the provider. -func (s *IntegrationTestSuite) registerConsumerOnProvider() { - // Read consumer JSON template and substitute chain ID - templatePath := filepath.Join(testDir(), "testdata", "create_consumer.json") - templateBytes, err := os.ReadFile(templatePath) - s.Require().NoError(err, "failed to read create_consumer.json template") - - createConsumerJSON := strings.ReplaceAll(string(templateBytes), "CONSUMER_CHAIN_ID", consumerChainID) - - // Write JSON to container and execute tx - s.dockerExecMust(s.providerValRes[0].Container.ID, []string{ - "sh", "-c", fmt.Sprintf("echo '%s' > /tmp/create_consumer.json", createConsumerJSON), - }) - - _, _, err = s.dockerExec(s.providerValRes[0].Container.ID, []string{ - providerBinary, "tx", "provider", "create-consumer", "/tmp/create_consumer.json", - "--from", "val", - "--home", providerHomePath, - "--keyring-backend", "test", - "--chain-id", providerChainID, - "--gas", "auto", - "--gas-adjustment", "1.5", - "--fees", "10000" + bondDenom, - "-y", - }) - - s.Require().NoError(err, "failed to create consumer on provider") - - // Wait for the tx to be included - time.Sleep(10 * time.Second) -} - -// fetchConsumerGenesis queries the provider for the consumer genesis data. -func (s *IntegrationTestSuite) fetchConsumerGenesis() []byte { - var output string - var lastErr error - - // Retry fetching consumer genesis (it may take a few blocks) - for range 30 { - stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ - providerBinary, "query", "provider", "consumer-genesis", "0", - "--home", providerHomePath, - "--output", "json", - }) - if err == nil { - output = stdout.String() - if output != "" && !strings.Contains(output, "not found") && !strings.Contains(output, "Error") { - break - } - } - lastErr = err - time.Sleep(3 * time.Second) - } - s.Require().NotEmpty(output, "consumer genesis is empty, last error: %v", lastErr) - - s.T().Logf("fetched consumer genesis (%d bytes)", len(output)) - return []byte(output) -} - -// initAndStartConsumer initializes the consumer chain and starts it. -func (s *IntegrationTestSuite) initAndStartConsumer(consumerGenesisJSON []byte) { - // Create host directory for consumer data - consumerDir, err := os.MkdirTemp("", "vaas-e2e-consumer-") - s.Require().NoError(err) - s.tmpDirs = append(s.tmpDirs, consumerDir) - s.consumer.dataDir = consumerDir - - s.Require().NoError(os.Chmod(consumerDir, 0o777)) - - // Run init script in a temporary container - scriptPath := filepath.Join(testDir(), "scripts", "consumer-init.sh") - s.runInitContainer("consumer-init", scriptPath, "/scripts/consumer-init.sh", consumerDir, consumerHomePath, []string{ - "BINARY=" + consumerBinary, - "HOME_DIR=" + consumerHomePath, - "CHAIN_ID=" + consumerChainID, - "DENOM=" + bondDenom, - "MNEMONIC=" + relayerMnemonic, - }) - - // Patch consumer genesis with provider's consumer-genesis data on the host - genesisFile := filepath.Join(consumerDir, "config", "genesis.json") - err = patchConsumerGenesisWithProviderData(genesisFile, consumerGenesisJSON) - s.Require().NoError(err, "failed to patch consumer genesis") - - // Patch consumer slashing params for aggressive downtime detection - s.patchConsumerSlashingParams() - - // Copy validator keys from provider to consumer - providerDir := s.provider.dataDir - err = copyFile( - filepath.Join(providerDir, "config", "priv_validator_key.json"), - filepath.Join(consumerDir, "config", "priv_validator_key.json"), - ) - s.Require().NoError(err, "failed to copy priv_validator_key.json") - - err = copyFile( - filepath.Join(providerDir, "config", "node_key.json"), - filepath.Join(consumerDir, "config", "node_key.json"), - ) - s.Require().NoError(err, "failed to copy node_key.json") - - // Start the actual consumer container - s.T().Log("starting consumer chain container...") - - resource, err := s.dkrPool.RunWithOptions( - &dockertest.RunOptions{ - Name: fmt.Sprintf("%s-val0", consumerChainID), - Repository: e2eChainImage, - NetworkID: s.dkrNet.Network.ID, - Mounts: []string{ - fmt.Sprintf("%s:%s", consumerDir, consumerHomePath), - }, - PortBindings: map[docker.Port][]docker.PortBinding{ - "26657/tcp": {{HostIP: "", HostPort: "26667"}}, - "9090/tcp": {{HostIP: "", HostPort: "9092"}}, - "1317/tcp": {{HostIP: "", HostPort: "1327"}}, - "26656/tcp": {{HostIP: "", HostPort: "26666"}}, - }, - Cmd: []string{ - consumerBinary, "start", - "--home", consumerHomePath, - }, - }, - func(config *docker.HostConfig) { - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }, - ) - s.Require().NoError(err, "failed to start consumer container") - - s.consumerValRes = append(s.consumerValRes, resource) - s.T().Logf("consumer container started: %s", resource.Container.ID[:12]) - - // Wait for consumer to produce blocks - waitCtx, waitCancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer waitCancel() - err = s.waitForChainHeight(waitCtx, "http://localhost:26667", 3) - s.Require().NoError(err, "consumer failed to produce blocks") - s.T().Log("consumer chain is producing blocks") -} - -// setupTSRelayer starts the ts-relayer container, configures it with -// mnemonics and gas prices for both chains, and creates an IBC v2 path. -// The ts-relayer handles counterparty registration and packet relaying. -func (s *IntegrationTestSuite) setupTSRelayer() { - s.startTSRelayer() - - s.tsRelayerAddMnemonic(providerChainID, relayerMnemonic) - s.tsRelayerAddMnemonic(consumerChainID, relayerMnemonic) - s.tsRelayerAddGasPrice(providerChainID, "0.025"+bondDenom) - s.tsRelayerAddGasPrice(consumerChainID, "0.025"+bondDenom) - - s.tsRelayerAddPath(IBCv2) - - s.tsRelayerDumpPaths() - s.startTSRelayerRelay() - s.T().Log("ts-relayer IBC v2 path configured") -} - -// waitForChainHeight polls a CometBFT RPC endpoint until the chain reaches -// the given block height. -func (s *IntegrationTestSuite) waitForChainHeight(ctx context.Context, rpcEndpoint string, minHeight int64) error { - ticker := time.NewTicker(2 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return fmt.Errorf("timeout waiting for chain at %s to reach height %d", rpcEndpoint, minHeight) - case <-ticker.C: - height, err := queryBlockHeight(rpcEndpoint) - if err != nil { - continue - } - if height >= minHeight { - return nil - } - } - } -} - // chmodRecursive changes permissions on a directory recursively. func chmodRecursive(path string, mode os.FileMode) error { cmd := exec.Command("chmod", "-R", fmt.Sprintf("%o", mode), path) diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index fe3f178..7d7ee29 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -6,11 +6,18 @@ func (s *IntegrationTestSuite) TestVAAS() { s.testConsumerOnProvider() s.testProviderOnConsumer() s.testValidatorSetSync() + // Pause consumer briefly while provider VP changes; verify consumer stays + // LAUNCHED and re-converges via snapshot resync after recovery. + s.testLivenessTransientOutage() s.testConsumerDebtFlow() s.testDowntimeSlash() s.testFeePoolSendRestriction() s.testFeePoolFundAndLockEnforcement() s.testFeePoolGovSubsidyClawback() + // Explicitly remove consumer "0"; verify STOPPED (DELETED if removal_time + // has elapsed). Must run after all tests that rely on consumer "0" being + // LAUNCHED and before testGenesisRoundTrip (which tolerates any phase). + s.testLivenessRemoval() // Run last: stops the provider container and replaces it with a fresh // one started from the exported genesis. s.testGenesisRoundTrip() diff --git a/tests/e2e/e2e_tsrelayer_test.go b/tests/e2e/e2e_tsrelayer_test.go index 87fb3d0..f782e15 100644 --- a/tests/e2e/e2e_tsrelayer_test.go +++ b/tests/e2e/e2e_tsrelayer_test.go @@ -34,12 +34,12 @@ func noRestart(config *docker.HostConfig) { config.RestartPolicy = docker.RestartPolicy{Name: "no"} } -func (s *IntegrationTestSuite) startTSRelayer() { +func (s *baseTestSuite) startTSRelayer() { s.T().Log("starting ts-relayer container") resource, err := s.dkrPool.RunWithOptions( &dockertest.RunOptions{ - Name: fmt.Sprintf("%s-%s-ts-relayer", providerChainID, consumerChainID), + Name: fmt.Sprintf("%s-%s-ts-relayer", s.cfg.providerChainID, s.cfg.consumerChainID), Repository: tsRelayerImage, Tag: tsRelayerImageTag, NetworkID: s.dkrNet.Network.ID, @@ -61,7 +61,7 @@ func (s *IntegrationTestSuite) startTSRelayer() { s.verifyTSRelayerConnectivity("consumer", consumerURL) } -func (s *IntegrationTestSuite) verifyTSRelayerConnectivity(chainName, rpcURL string) { +func (s *baseTestSuite) verifyTSRelayerConnectivity(chainName, rpcURL string) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -104,7 +104,7 @@ func (s *IntegrationTestSuite) verifyTSRelayerConnectivity(chainName, rpcURL str s.Require().Fail("ts-relayer cannot reach %s RPC at %s", chainName, rpcURL) } -func (s *IntegrationTestSuite) startTSRelayerRelay() { +func (s *baseTestSuite) startTSRelayerRelay() { s.T().Log("ts-relayer: starting relay process") ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -134,7 +134,7 @@ func (s *IntegrationTestSuite) startTSRelayerRelay() { s.T().Log("ts-relayer: relay process started") } -func (s *IntegrationTestSuite) stopTSRelayer() { +func (s *baseTestSuite) stopTSRelayer() { if s.tsRelayerResource != nil { s.T().Log("tearing down ts-relayer...") s.Require().NoError(s.dkrPool.Purge(s.tsRelayerResource)) @@ -142,7 +142,7 @@ func (s *IntegrationTestSuite) stopTSRelayer() { } } -func (s *IntegrationTestSuite) executeTSRelayerCommand(ctx context.Context, args []string) []byte { +func (s *baseTestSuite) executeTSRelayerCommand(ctx context.Context, args []string) []byte { tsRelayerBinary := []string{"/bin/with_keyring", "ibc-v2-ts-relayer"} cmd := append(tsRelayerBinary, args...) exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{ @@ -184,7 +184,7 @@ func (s *IntegrationTestSuite) executeTSRelayerCommand(ctx context.Context, args return out.Bytes() } -func (s *IntegrationTestSuite) tsRelayerAddMnemonic(chainID, mnemonic string) { +func (s *baseTestSuite) tsRelayerAddMnemonic(chainID, mnemonic string) { s.T().Logf("ts-relayer: adding mnemonic for chain %s", chainID) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -195,7 +195,7 @@ func (s *IntegrationTestSuite) tsRelayerAddMnemonic(chainID, mnemonic string) { }) } -func (s *IntegrationTestSuite) tsRelayerAddGasPrice(chainID, gasPrice string) { +func (s *baseTestSuite) tsRelayerAddGasPrice(chainID, gasPrice string) { s.T().Logf("ts-relayer: adding gas-price for chain %s", chainID) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -207,14 +207,14 @@ func (s *IntegrationTestSuite) tsRelayerAddGasPrice(chainID, gasPrice string) { }) } -func (s *IntegrationTestSuite) tsRelayerAddPath(ibcVersion string) { - s.T().Logf("ts-relayer: adding IBCv%s path between %s and %s", ibcVersion, providerChainID, consumerChainID) +func (s *baseTestSuite) tsRelayerAddPath(ibcVersion string) { + s.T().Logf("ts-relayer: adding IBCv%s path between %s and %s", ibcVersion, s.cfg.providerChainID, s.cfg.consumerChainID) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() s.executeTSRelayerCommand(ctx, []string{ "add-path", - "-s", providerChainID, - "-d", consumerChainID, + "-s", s.cfg.providerChainID, + "-d", s.cfg.consumerChainID, "--surl", "http://" + s.providerValRes[0].Container.Name[1:] + ":26657", "--durl", "http://" + s.consumerValRes[0].Container.Name[1:] + ":26657", "--st", "cosmos", @@ -225,7 +225,7 @@ func (s *IntegrationTestSuite) tsRelayerAddPath(ibcVersion string) { var ansiEscapeRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`) -func (s *IntegrationTestSuite) tsRelayerDumpPaths() []tsRelayerPath { +func (s *baseTestSuite) tsRelayerDumpPaths() []tsRelayerPath { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() out := s.executeTSRelayerCommand(ctx, []string{"dump-paths"}) @@ -241,7 +241,7 @@ func (s *IntegrationTestSuite) tsRelayerDumpPaths() []tsRelayerPath { return paths } -func (s *IntegrationTestSuite) collectChainDiagnostics() string { +func (s *baseTestSuite) collectChainDiagnostics() string { var sb strings.Builder for _, chain := range []struct { name string @@ -299,7 +299,7 @@ func (s *IntegrationTestSuite) collectIBCDiagnosticsLog() { } } -func (s *IntegrationTestSuite) collectIBCDiagnostics() string { +func (s *baseTestSuite) collectIBCDiagnostics() string { var sb strings.Builder chains := []struct { name string diff --git a/tests/e2e/genesis_test.go b/tests/e2e/genesis_test.go index 79641e1..ee2608f 100644 --- a/tests/e2e/genesis_test.go +++ b/tests/e2e/genesis_test.go @@ -43,7 +43,7 @@ func patchConsumerGenesisWithProviderData(genesisFilePath string, consumerGenesi // patchGenesisJSON reads a genesis.json file, applies a mutation function, // and writes it back. -func (s *IntegrationTestSuite) patchGenesisJSON(path string, mutate func(map[string]any)) { +func (s *baseTestSuite) patchGenesisJSON(path string, mutate func(map[string]any)) { bz, err := os.ReadFile(path) s.Require().NoError(err, "failed to read genesis file") diff --git a/tests/e2e/gov_proposal_helpers_test.go b/tests/e2e/gov_proposal_helpers_test.go index 15ed76d..be69c0e 100644 --- a/tests/e2e/gov_proposal_helpers_test.go +++ b/tests/e2e/gov_proposal_helpers_test.go @@ -13,7 +13,7 @@ import ( // queryModuleAccountAddress returns the bech32 address of the named module // account on the provider chain. -func (s *IntegrationTestSuite) queryModuleAccountAddress(name string) string { +func (s *baseTestSuite) queryModuleAccountAddress(name string) string { stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ providerBinary, "query", "auth", "module-account", name, "--home", providerHomePath, @@ -57,7 +57,7 @@ func (s *IntegrationTestSuite) queryModuleAccountAddress(name string) string { } // queryGovAuthority is a thin wrapper for queryModuleAccountAddress("gov"). -func (s *IntegrationTestSuite) queryGovAuthority() string { +func (s *baseTestSuite) queryGovAuthority() string { return s.queryModuleAccountAddress("gov") } @@ -97,7 +97,7 @@ func (s *IntegrationTestSuite) queryCommunityPoolBalance(denom string) int64 { // // proposalJSON must be a valid gov v1 proposal body. The submitter and voting // fees are paid in bondDenom by val. -func (s *IntegrationTestSuite) submitAndPassProposal(proposalJSON string) uint64 { +func (s *baseTestSuite) submitAndPassProposal(proposalJSON string) uint64 { containerID := s.providerValRes[0].Container.ID // 1. Write the proposal body to /tmp/proposal.json via base64 to avoid @@ -115,7 +115,7 @@ func (s *IntegrationTestSuite) submitAndPassProposal(proposalJSON string) uint64 "--from", "val", "--home", providerHomePath, "--keyring-backend", "test", - "--chain-id", providerChainID, + "--chain-id", s.cfg.providerChainID, "--fees", "10000" + bondDenom, "--yes", "-o", "json", @@ -158,7 +158,7 @@ func (s *IntegrationTestSuite) submitAndPassProposal(proposalJSON string) uint64 "--from", "val", "--home", providerHomePath, "--keyring-backend", "test", - "--chain-id", providerChainID, + "--chain-id", s.cfg.providerChainID, "--fees", "10000" + bondDenom, "--yes", }) @@ -188,7 +188,7 @@ func (s *IntegrationTestSuite) submitAndPassProposal(proposalJSON string) uint64 // dumpProposal returns the raw JSON of a gov proposal query, used to surface // the failed_reason / messages on terminal-bad status. -func (s *IntegrationTestSuite) dumpProposal(proposalID uint64) string { +func (s *baseTestSuite) dumpProposal(proposalID uint64) string { stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ providerBinary, "query", "gov", "proposal", fmt.Sprintf("%d", proposalID), "--home", providerHomePath, @@ -202,7 +202,7 @@ func (s *IntegrationTestSuite) dumpProposal(proposalID uint64) string { // queryProposalStatus returns the textual status of a gov proposal (e.g. // "PROPOSAL_STATUS_PASSED"). -func (s *IntegrationTestSuite) queryProposalStatus(proposalID uint64) string { +func (s *baseTestSuite) queryProposalStatus(proposalID uint64) string { stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ providerBinary, "query", "gov", "proposal", fmt.Sprintf("%d", proposalID), "--home", providerHomePath, diff --git a/tests/e2e/query_test.go b/tests/e2e/query_test.go index d7d3638..6df08e4 100644 --- a/tests/e2e/query_test.go +++ b/tests/e2e/query_test.go @@ -1,6 +1,7 @@ package e2e import ( + "encoding/json" "fmt" "strings" @@ -9,6 +10,28 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) +// queryProviderConsumerPhase returns the phase string for a given consumer ID +// from the provider chain (e.g. "CONSUMER_PHASE_LAUNCHED"). +func (s *baseTestSuite) queryProviderConsumerPhase(consumerID string) string { + stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{ + providerBinary, "query", "provider", "consumer-chain", consumerID, + "--home", providerHomePath, + "--output", "json", + }) + if err != nil { + s.T().Logf("queryProviderConsumerPhase(%s): exec error: %v", consumerID, err) + return "" + } + var res struct { + Phase string `json:"phase"` + } + if err := json.Unmarshal(stdout.Bytes(), &res); err != nil { + s.T().Logf("queryProviderConsumerPhase(%s): decode error: %v (raw: %s)", consumerID, err, stdout.String()) + return "" + } + return res.Phase +} + // providerRESTEndpoint returns the provider chain's REST HTTP endpoint using the Docker-assigned host port. func (s *IntegrationTestSuite) providerRESTEndpoint() string { return fmt.Sprintf("http://%s", s.providerValRes[0].GetHostPort("1317/tcp")) diff --git a/tests/e2e/testdata/create_consumer.json b/tests/e2e/testdata/create_consumer.json index 48aeb1c..0b7c2d6 100644 --- a/tests/e2e/testdata/create_consumer.json +++ b/tests/e2e/testdata/create_consumer.json @@ -14,7 +14,8 @@ "binary_hash": "", "spawn_time": "2024-01-01T00:00:00Z", "unbonding_period": 1728000000000000, - "vaas_timeout_period": 2419200000000000, + "vaas_timeout_period": 3600000000000, + "safe_mode_threshold": 10800000000000, "historical_entries": 10000 }, "infraction_parameters": { diff --git a/tests/e2e/testdata/create_consumer_short_unbonding.json b/tests/e2e/testdata/create_consumer_short_unbonding.json new file mode 100644 index 0000000..41301b1 --- /dev/null +++ b/tests/e2e/testdata/create_consumer_short_unbonding.json @@ -0,0 +1,33 @@ +{ + "chain_id": "CONSUMER_SHORT_CHAIN_ID", + "metadata": { + "name": "consumer-short", + "description": "e2e liveness test consumer with short unbonding period", + "metadata": "{}" + }, + "initialization_parameters": { + "initial_height": { + "revision_number": 0, + "revision_height": 1 + }, + "genesis_hash": "", + "binary_hash": "", + "spawn_time": "2024-01-01T00:00:00Z", + "unbonding_period": 200000000000, + "vaas_timeout_period": 600000000000, + "safe_mode_threshold": 5000000000, + "historical_entries": 10000 + }, + "infraction_parameters": { + "double_sign": { + "slash_fraction": "0.05", + "jail_duration": 9223372036854775807, + "tombstone": true + }, + "downtime": { + "slash_fraction": "0.0001", + "jail_duration": 600000000000, + "tombstone": false + } + } +} diff --git a/x/vaas/consumer/ante/msg_filter_ante.go b/x/vaas/consumer/ante/msg_filter_ante.go index a438daa..68e695d 100644 --- a/x/vaas/consumer/ante/msg_filter_ante.go +++ b/x/vaas/consumer/ante/msg_filter_ante.go @@ -18,19 +18,24 @@ type ( ConsumerKeeper interface { GetProviderClientID(ctx context.Context) (string, bool) IsConsumerInDebt(ctx context.Context) bool + IsVSCStale(ctx context.Context) bool } // MsgFilterDecorator is the consumer-side tx admission gate. It runs in - // three modes driven by consumer state: + // four modes driven by consumer state: // - // pre-CCV : provider IBC client not yet established → only /ibc.* msgs - // are accepted, so the chain can stand up its IBC stack. - // normal : provider client established, consumer is paying its fees → - // everything passes. - // in debt : provider client established, consumer has fallen behind on - // per-block fees → only /ibc.core.* and /cosmos.gov.* msgs - // pass, keeping CCV liveness and governance available so the - // chain can recover without off-chain coordination. + // pre-CCV : provider IBC client not yet established -> only /ibc.* msgs + // are accepted, so the chain can stand up its IBC stack. + // normal : provider client established, consumer is paying its fees + // and has a fresh validator set -> everything passes. + // in debt : provider client established, consumer has fallen behind on + // per-block fees -> restricted mode (see below). + // stale VSC : provider client established, no VSC packet received within + // the safe-mode threshold -> restricted mode (see below). + // + // Restricted mode: only /ibc.core.* and /cosmos.gov.* messages pass, + // keeping CCV liveness and governance available so the chain can recover + // without off-chain coordination. MsgFilterDecorator struct { ConsumerKeeper ConsumerKeeper } @@ -53,12 +58,9 @@ func (mfd MsgFilterDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bo return next(ctx, tx, simulate) } - // Hot path: post-CCV and not in debt → pass without walking the message list. - if !mfd.ConsumerKeeper.IsConsumerInDebt(ctx) { - return next(ctx, tx, simulate) - } - - // In debt: only let /ibc.core.* and /cosmos.gov.* messages through. + // Restricted mode: in debt, or no fresh VSC (validator set may be stale). + // + // Only /ibc.core.* and /cosmos.gov.* messages pass: // // /ibc.core.* preserves CCV liveness and keeps all IBC apps working. // The prefix covers both IBC v1 and v2 channel/connection/client @@ -69,16 +71,19 @@ func (mfd MsgFilterDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bo // their own keys; authz wrapping is not part of any realistic relayer // flow and is therefore treated as non-ibc.core and rejected here. // - // /cosmos.gov.* keeps governance alive during debt so the community can - // vote on recovery (e.g. emergency funding, param changes) without - // requiring off-chain coordination. - if isAllowedDuringDebtTx(tx.GetMsgs()) { - return next(ctx, tx, simulate) + // /cosmos.gov.* keeps governance alive so the community can vote on + // recovery (e.g. emergency funding, param changes) without requiring + // off-chain coordination. + if mfd.ConsumerKeeper.IsConsumerInDebt(ctx) || mfd.ConsumerKeeper.IsVSCStale(ctx) { + if isAllowedRestrictedTx(tx.GetMsgs()) { + return next(ctx, tx, simulate) + } + return ctx, errorsmod.Wrap( + consumertypes.ErrConsumerInDebt, + "consumer is in debt or has a stale validator set; only ibc.core and cosmos.gov messages are temporarily allowed", + ) } - return ctx, errorsmod.Wrap( - consumertypes.ErrConsumerInDebt, - "consumer chain is in debt; only ibc.core and cosmos.gov messages are temporarily allowed", - ) + return next(ctx, tx, simulate) } func hasOnlyPrefix(msgs []sdk.Msg, prefix string) bool { @@ -90,7 +95,7 @@ func hasOnlyPrefix(msgs []sdk.Msg, prefix string) bool { return true } -func isAllowedDuringDebtTx(msgs []sdk.Msg) bool { +func isAllowedRestrictedTx(msgs []sdk.Msg) bool { for _, msg := range msgs { url := sdk.MsgTypeURL(msg) if !strings.HasPrefix(url, "/ibc.core.") && !strings.HasPrefix(url, "/cosmos.gov.") { diff --git a/x/vaas/consumer/ante/msg_filter_ante_test.go b/x/vaas/consumer/ante/msg_filter_ante_test.go index 1d61778..3c9971f 100644 --- a/x/vaas/consumer/ante/msg_filter_ante_test.go +++ b/x/vaas/consumer/ante/msg_filter_ante_test.go @@ -20,6 +20,7 @@ import ( type mockConsumerKeeper struct { providerClientFound bool inDebt bool + vscStale bool } func (m mockConsumerKeeper) GetProviderClientID(context.Context) (string, bool) { @@ -30,6 +31,10 @@ func (m mockConsumerKeeper) IsConsumerInDebt(context.Context) bool { return m.inDebt } +func (m mockConsumerKeeper) IsVSCStale(context.Context) bool { + return m.vscStale +} + type mockTx struct { msgs []sdk.Msg } @@ -153,6 +158,56 @@ func TestMsgFilterDecoratorRejectsAuthzWrappedIBCCoreTxWhenInDebt(t *testing.T) require.False(t, nextCalled) } +// Safe mode (stale VSC): non-IBC, non-gov msgs are rejected with ErrConsumerInDebt. +func TestSafeModeRejectsAppMsgWhenStale(t *testing.T) { + // provider client established, not in debt, but VSC stale + nextCalled, err := runDecorator(t, + mockConsumerKeeper{providerClientFound: true, inDebt: false, vscStale: true}, + []sdk.Msg{bankSendMsg()}, + ) + require.Error(t, err) + require.True(t, errorsmod.IsOf(err, consumertypes.ErrConsumerInDebt)) + require.False(t, nextCalled) + + // /ibc.core.* message must pass (recovery path stays open) + nextCalled, err = runDecorator(t, + mockConsumerKeeper{providerClientFound: true, inDebt: false, vscStale: true}, + []sdk.Msg{&channeltypes.MsgRecvPacket{}}, + ) + require.NoError(t, err) + require.True(t, nextCalled) +} + func testAccAddress(seed byte) sdk.AccAddress { return sdk.AccAddress(bytes.Repeat([]byte{seed}, 20)) } + +// TestSafeModeAllowsGovWhenStale verifies that when the consumer has an +// established provider client, is not in debt, but has a stale validator set, +// governance messages still pass (restricted mode allows /cosmos.gov.*). +func TestSafeModeAllowsGovWhenStale(t *testing.T) { + msg := &govtypes.MsgVote{ + ProposalId: 1, + Voter: testAccAddress(1).String(), + Option: govtypes.OptionYes, + } + nextCalled, err := runDecorator(t, + mockConsumerKeeper{providerClientFound: true, inDebt: false, vscStale: true}, + []sdk.Msg{msg}, + ) + require.NoError(t, err) + require.True(t, nextCalled) +} + +// TestRestrictedWhenBothDebtAndStale verifies that when the consumer is both +// in debt and has a stale validator set, app messages (e.g. bank) are rejected. +// The combined condition still routes to restricted mode. +func TestRestrictedWhenBothDebtAndStale(t *testing.T) { + nextCalled, err := runDecorator(t, + mockConsumerKeeper{providerClientFound: true, inDebt: true, vscStale: true}, + []sdk.Msg{bankSendMsg()}, + ) + require.Error(t, err) + require.True(t, errorsmod.IsOf(err, consumertypes.ErrConsumerInDebt)) + require.False(t, nextCalled) +} diff --git a/x/vaas/consumer/keeper/genesis.go b/x/vaas/consumer/keeper/genesis.go index dd4567b..0c0d238 100644 --- a/x/vaas/consumer/keeper/genesis.go +++ b/x/vaas/consumer/keeper/genesis.go @@ -61,6 +61,12 @@ func (k Keeper) InitGenesis(ctx sdk.Context, state *types.GenesisState) []abci.V return []abci.ValidatorUpdate{} } + // Restore the VSC staleness clock on a restart (see ExportGenesis); absent + // (new chain / never received a VSC) leaves the never-stale default. + if state.LastVscRecvTime != nil { + k.SetLastVSCRecvTime(ctx, *state.LastVscRecvTime) + } + // populate cross chain validators states with initial valset k.ApplyCCValidatorChanges(ctx, state.Provider.InitialValSet) return state.Provider.InitialValSet @@ -88,5 +94,17 @@ func (k Keeper) ExportGenesis(ctx sdk.Context) (genesis *types.GenesisState) { params, ) + // Preserve the VSC staleness clock across a restart (see IsVSCStale): export + // the last-VSC-recv time only when actually recorded, so a consumer that has + // not received a VSC keeps the absent-default (never stale) on import. + has, err := k.LastVSCRecvTime.Has(ctx) + if err != nil { + panic(err) + } + if has { + t := k.GetLastVSCRecvTime(ctx) + genesis.LastVscRecvTime = &t + } + return genesis } diff --git a/x/vaas/consumer/keeper/genesis_test.go b/x/vaas/consumer/keeper/genesis_test.go index 66645ad..b1879fd 100644 --- a/x/vaas/consumer/keeper/genesis_test.go +++ b/x/vaas/consumer/keeper/genesis_test.go @@ -193,6 +193,44 @@ func TestExportGenesis(t *testing.T) { } } +// TestGenesisRoundTripLastVSCRecvTime verifies the consumer's VSC-staleness +// clock survives an export/import restart: ExportGenesis carries the recorded +// last-VSC-recv time, and InitGenesis restores it on a fresh keeper (rather than +// falling back to the current block time, which would reset the safe-mode clock). +func TestGenesisRoundTripLastVSCRecvTime(t *testing.T) { + provClientID := "tendermint-07" + params := vaastypes.DefaultConsumerParams() + params.Enabled = true + + pubKey := ed25519.GenPrivKey().PubKey() + tmPK, err := cryptocodec.ToCmtPubKeyInterface(pubKey) + require.NoError(t, err) + validator := tmtypes.NewValidator(tmPK, 1) + + lastRecv := time.Unix(1_850_000_000, 0).UTC() + + // Export half: a keeper with a recorded last-VSC-recv time exports it. + ck, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + ck.SetParams(ctx, params) + ck.SetProviderClientID(ctx, provClientID) + cVal, err := consumertypes.NewCCValidator(validator.Address.Bytes(), 1, pubKey) + require.NoError(t, err) + ck.SetCCValidator(ctx, cVal) + ck.SetHeightValsetUpdateID(ctx, 0, 0) + ck.SetLastVSCRecvTime(ctx, lastRecv) + + exported := ck.ExportGenesis(ctx) + require.NotNil(t, exported.LastVscRecvTime, "export must carry last_vsc_recv_time") + require.Equal(t, lastRecv, *exported.LastVscRecvTime) + + // Import half: a fresh keeper restores the exact time, not the block-time fallback. + ck2, ctx2, ctrl2, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl2.Finish() + ck2.InitGenesis(ctx2, exported) + require.Equal(t, lastRecv, ck2.GetLastVSCRecvTime(ctx2)) +} + func assertProviderClientID(t *testing.T, ctx sdk.Context, ck *consumerkeeper.Keeper, clientID string) { t.Helper() cid, ok := ck.GetProviderClientID(ctx) diff --git a/x/vaas/consumer/keeper/grpc_staking_query_test.go b/x/vaas/consumer/keeper/grpc_staking_query_test.go index 7f76f24..820a779 100644 --- a/x/vaas/consumer/keeper/grpc_staking_query_test.go +++ b/x/vaas/consumer/keeper/grpc_staking_query_test.go @@ -31,6 +31,7 @@ func TestStakingQueryServerParams(t *testing.T) { vaastypes.DefaultVAASTimeoutPeriod, 1234, 13*24*time.Hour, + vaastypes.DefaultSafeModeThreshold, )) server := consumerkeeper.NewStakingQueryServer(&consumerKeeper) diff --git a/x/vaas/consumer/keeper/keeper.go b/x/vaas/consumer/keeper/keeper.go index bb62e1e..6ca2d39 100644 --- a/x/vaas/consumer/keeper/keeper.go +++ b/x/vaas/consumer/keeper/keeper.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" "github.com/allinbits/vaas/x/vaas/consumer/types" vaastypes "github.com/allinbits/vaas/x/vaas/types" @@ -64,6 +65,7 @@ type Keeper struct { HistoricalInfos collections.Map[int64, stakingtypes.HistoricalInfo] HighestValsetUpdateID collections.Item[uint64] PendingEvidencePackets collections.Map[[]byte, []byte] + LastVSCRecvTime collections.Item[[]byte] } // NewKeeper creates a new Consumer Keeper instance @@ -110,6 +112,7 @@ func NewKeeper( HistoricalInfos: collections.NewMap(sb, types.HistoricalInfoPrefix, "historical_infos", collections.Int64Key, codec.CollValue[stakingtypes.HistoricalInfo](cdc)), HighestValsetUpdateID: collections.NewItem(sb, types.HighestValsetUpdateIDPrefix, "highest_valset_update_id", collections.Uint64Value), PendingEvidencePackets: collections.NewMap(sb, types.PendingEvidencePacketsPrefix, "pending_evidence_packets", collections.BytesKey, collections.BytesValue), + LastVSCRecvTime: collections.NewItem(sb, types.LastVSCRecvTimePrefix, "last_vsc_recv_time", collections.BytesValue), } schema, err := sb.Build() @@ -150,6 +153,7 @@ func NewNonZeroKeeper(cdc codec.BinaryCodec, storeService corestoretypes.KVStore HistoricalInfos: collections.NewMap(sb, types.HistoricalInfoPrefix, "historical_infos", collections.Int64Key, codec.CollValue[stakingtypes.HistoricalInfo](cdc)), HighestValsetUpdateID: collections.NewItem(sb, types.HighestValsetUpdateIDPrefix, "highest_valset_update_id", collections.Uint64Value), PendingEvidencePackets: collections.NewMap(sb, types.PendingEvidencePacketsPrefix, "pending_evidence_packets", collections.BytesKey, collections.BytesValue), + LastVSCRecvTime: collections.NewItem(sb, types.LastVSCRecvTimePrefix, "last_vsc_recv_time", collections.BytesValue), } schema, err := sb.Build() @@ -451,3 +455,36 @@ func (k Keeper) GetHighestValsetUpdateID(ctx context.Context) (uint64, bool, err return val, true, nil } + +// SetLastVSCRecvTime records the block time at which a VSC packet was last processed. +func (k Keeper) SetLastVSCRecvTime(ctx context.Context, t time.Time) { + buf, err := t.MarshalBinary() + if err != nil { + panic(err) + } + if err := k.LastVSCRecvTime.Set(ctx, buf); err != nil { + panic(err) + } +} + +// GetLastVSCRecvTime returns the block time of the last processed VSC packet. +// Falls back to the current block time when no packet has been received yet, +// so a freshly launched consumer is never considered stale. +func (k Keeper) GetLastVSCRecvTime(ctx context.Context) time.Time { + buf, err := k.LastVSCRecvTime.Get(ctx) + if err != nil { + return sdk.UnwrapSDKContext(ctx).BlockTime() + } + var t time.Time + if err := t.UnmarshalBinary(buf); err != nil { + return sdk.UnwrapSDKContext(ctx).BlockTime() + } + return t +} + +// IsVSCStale reports whether the consumer has not received a VSC packet within +// the safe-mode threshold. +func (k Keeper) IsVSCStale(ctx context.Context) bool { + sdkCtx := sdk.UnwrapSDKContext(ctx) + return sdkCtx.BlockTime().Sub(k.GetLastVSCRecvTime(ctx)) > k.GetConsumerParams(ctx).SafeModeThreshold +} diff --git a/x/vaas/consumer/keeper/params.go b/x/vaas/consumer/keeper/params.go index b41025a..5ee4910 100644 --- a/x/vaas/consumer/keeper/params.go +++ b/x/vaas/consumer/keeper/params.go @@ -2,6 +2,7 @@ package keeper import ( "context" + "fmt" "time" vaastypes "github.com/allinbits/vaas/x/vaas/types" @@ -11,10 +12,15 @@ import ( // GetConsumerParams returns the params for the consumer VAAS module // NOTE: it is different from the GetParams method which is required to implement StakingKeeper interface +// +// Params are set at InitGenesis and never removed, so a read error indicates +// state corruption. Panic rather than return a zero-value ConsumerParams: a +// zero SafeModeThreshold would make IsVSCStale trivially true and silently pin +// the consumer in safe mode, hiding the corruption. func (k Keeper) GetConsumerParams(ctx context.Context) vaastypes.ConsumerParams { params, err := k.Params.Get(ctx) if err != nil { - return vaastypes.ConsumerParams{} + panic(fmt.Sprintf("error getting consumer module parameters: %v", err)) } return params } diff --git a/x/vaas/consumer/keeper/params_test.go b/x/vaas/consumer/keeper/params_test.go index b9f7224..2c9e5d9 100644 --- a/x/vaas/consumer/keeper/params_test.go +++ b/x/vaas/consumer/keeper/params_test.go @@ -21,12 +21,13 @@ func TestParams(t *testing.T) { vaastypes.DefaultVAASTimeoutPeriod, vaastypes.DefaultHistoricalEntries, vaastypes.DefaultConsumerUnbondingPeriod, + vaastypes.DefaultSafeModeThreshold, ) // these are the default params, IBC suite independently sets enabled=true params := consumerKeeper.GetConsumerParams(ctx) require.Equal(t, expParams, params) - newParams := vaastypes.NewConsumerParams(false, 7*24*time.Hour, 500, 24*21*time.Hour) + newParams := vaastypes.NewConsumerParams(false, 7*24*time.Hour, 500, 24*21*time.Hour, vaastypes.DefaultSafeModeThreshold) consumerKeeper.SetParams(ctx, newParams) params = consumerKeeper.GetConsumerParams(ctx) require.Equal(t, newParams, params) diff --git a/x/vaas/consumer/keeper/relay.go b/x/vaas/consumer/keeper/relay.go index 5953cb6..9bdacd4 100644 --- a/x/vaas/consumer/keeper/relay.go +++ b/x/vaas/consumer/keeper/relay.go @@ -1,6 +1,8 @@ package keeper import ( + "strconv" + "github.com/allinbits/vaas/x/vaas/consumer/types" vaastypes "github.com/allinbits/vaas/x/vaas/types" @@ -30,6 +32,8 @@ func (k Keeper) OnRecvVSCPacketV2(ctx sdk.Context, sourceClientID string, newCha return nil } + k.SetLastVSCRecvTime(ctx, ctx.BlockTime()) + _, found = k.GetProviderClientID(ctx) if !found { k.SetProviderClientID(ctx, sourceClientID) @@ -46,13 +50,33 @@ func (k Keeper) OnRecvVSCPacketV2(ctx sdk.Context, sourceClientID string, newCha k.SetConsumerInDebt(ctx, newChanges.ConsumerInDebt) - // Set pending changes by accumulating changes from this packet with all prior changes - currentValUpdates := []abci.ValidatorUpdate{} - currentChanges, exists := k.GetPendingChanges(ctx) - if exists { - currentValUpdates = currentChanges.ValidatorUpdates + // Set pending changes: snapshot packets replace the set; diff packets accumulate. + var pendingChanges []abci.ValidatorUpdate + if newChanges.IsSnapshot { + pendingChanges = k.computeReplaceUpdates(ctx, newChanges.ValidatorUpdates) + // Surface snapshot resyncs (not ordinary diffs) so operators -- and the + // e2e -- can observe that a behind consumer was healed by a full-set + // replacement rather than an accumulated diff. Emitted both as an event + // (structured/queryable) and a log line (the e2e asserts on the log). + ctx.EventManager().EmitEvent( + sdk.NewEvent( + vaastypes.EventTypeSnapshotResync, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(vaastypes.AttributeValSetUpdateID, strconv.FormatUint(newChanges.ValsetUpdateId, 10)), + sdk.NewAttribute(vaastypes.AttributeNumValidators, strconv.Itoa(len(newChanges.ValidatorUpdates))), + ), + ) + k.Logger(ctx).Info("applied snapshot resync", + "vscID", newChanges.ValsetUpdateId, + "numValidators", len(newChanges.ValidatorUpdates), + ) + } else { + currentValUpdates := []abci.ValidatorUpdate{} + if currentChanges, exists := k.GetPendingChanges(ctx); exists { + currentValUpdates = currentChanges.ValidatorUpdates + } + pendingChanges = vaastypes.AccumulateChanges(currentValUpdates, newChanges.ValidatorUpdates) } - pendingChanges := vaastypes.AccumulateChanges(currentValUpdates, newChanges.ValidatorUpdates) k.SetPendingChanges(ctx, vaastypes.ValidatorSetChangePacketData{ ValidatorUpdates: pendingChanges, diff --git a/x/vaas/consumer/keeper/relay_test.go b/x/vaas/consumer/keeper/relay_test.go index a2061f0..acc3a00 100644 --- a/x/vaas/consumer/keeper/relay_test.go +++ b/x/vaas/consumer/keeper/relay_test.go @@ -3,6 +3,7 @@ package keeper_test import ( "sort" "testing" + "time" "github.com/stretchr/testify/require" @@ -244,3 +245,388 @@ func TestOnRecvVSCPacketV2DebtStatus(t *testing.T) { require.NoError(t, consumerKeeper.OnRecvVSCPacketV2(ctx, providerClientID, pd2)) require.False(t, consumerKeeper.IsConsumerInDebt(ctx)) } + +func TestConsumerVSCStaleness(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + // Use a non-default threshold so this proves IsVSCStale reads the param + // value (not a constant) and that the boundary tracks the param. + const threshold = 2 * time.Hour + k.SetParams(ctx, types.NewConsumerParams( + true, + types.DefaultVAASTimeoutPeriod, + types.DefaultHistoricalEntries, + types.DefaultConsumerUnbondingPeriod, + threshold, + )) + + require.False(t, k.IsVSCStale(ctx)) // absent -> BlockTime -> fresh + + k.SetLastVSCRecvTime(ctx, ctx.BlockTime()) + require.False(t, k.IsVSCStale(ctx)) + + // Exactly at the threshold is not stale (the check is strict >). + atBoundary := ctx.WithBlockTime(ctx.BlockTime().Add(threshold)) + require.False(t, k.IsVSCStale(atBoundary)) + + // Past the threshold is stale. + stale := ctx.WithBlockTime(ctx.BlockTime().Add(threshold + time.Minute)) + require.True(t, k.IsVSCStale(stale)) +} + +func TestSnapshotReplacesValidatorSet(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pkA := ed25519.GenPrivKey().PubKey() + pkB := ed25519.GenPrivKey().PubKey() + tmA, err := cryptocodec.ToCmtProtoPublicKey(pkA) + require.NoError(t, err) + tmB, err := cryptocodec.ToCmtProtoPublicKey(pkB) + require.NoError(t, err) + + // Seed current CC set: A=10, B=5. + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{{PubKey: tmA, Power: 10}, {PubKey: tmB, Power: 5}}) + require.NoError(t, k.SetHighestValsetUpdateID(ctx, 1)) + k.SetProviderClientID(ctx, "07-tendermint-0") + + snap := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: tmA, Power: 10}}, 2) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + + pending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + // snapshot must produce exactly 2 entries: A at 10 and B at 0 (explicit removal) + require.Len(t, pending.ValidatorUpdates, 2) + powers := map[string]int64{} + for _, u := range pending.ValidatorUpdates { + powers[u.PubKey.String()] = u.Power + } + require.Equal(t, int64(10), powers[tmA.String()]) + require.Equal(t, int64(0), powers[tmB.String()]) // B explicitly removed +} + +func TestOnRecvVSCRecordsRecvTime(t *testing.T) { + providerClientID := "07-tendermint-0" + + pk1, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + advancedTime := ctx.BlockTime().Add(10 * time.Minute) + ctx = ctx.WithBlockTime(advancedTime) + + changes := []abci.ValidatorUpdate{{PubKey: pk1, Power: 100}} + pd := types.NewValidatorSetChangePacketData(changes, 1) + err = consumerKeeper.OnRecvVSCPacketV2(ctx, providerClientID, pd) + require.NoError(t, err) + + got := consumerKeeper.GetLastVSCRecvTime(ctx) + require.Equal(t, advancedTime, got) +} + +// TestDedupDoesNotResetLastVSCRecvTime verifies that an out-of-order (duplicate) +// packet -- one whose ValsetUpdateId <= HighestValsetUpdateID -- returns early +// before recording block time, so a stale replay cannot silently reset the clock. +func TestDedupDoesNotResetLastVSCRecvTime(t *testing.T) { + providerClientID := "07-tendermint-0" + + pk1, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + // Deliver packet at blockTime T1 -- records lastVSCRecvTime = T1. + t1 := ctx.BlockTime().Add(5 * time.Minute) + ctx1 := ctx.WithBlockTime(t1) + pd1 := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pk1, Power: 10}}, 5) + require.NoError(t, k.OnRecvVSCPacketV2(ctx1, providerClientID, pd1)) + require.Equal(t, t1, k.GetLastVSCRecvTime(ctx1)) + + // Deliver an out-of-order packet (vscId 3 < highest 5) at a later blockTime T2. + // The dedup early-return fires before SetLastVSCRecvTime, so the clock must NOT advance. + t2 := t1.Add(10 * time.Minute) + ctx2 := ctx.WithBlockTime(t2) + pd2 := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pk1, Power: 99}}, 3) + require.NoError(t, k.OnRecvVSCPacketV2(ctx2, providerClientID, pd2)) + + got := k.GetLastVSCRecvTime(ctx2) + require.Equal(t, t1, got, "dedup replay must not reset lastVSCRecvTime") +} + +// TestRecvPacketAfterStalenessLiftsStale drives the consumer into safe mode by +// making the last VSC receipt time appear far in the past, then delivers a +// higher-id packet at a fresh block time and verifies IsVSCStale returns false. +func TestRecvPacketAfterStalenessLiftsStale(t *testing.T) { + providerClientID := "07-tendermint-0" + + pk1, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + const threshold = 2 * time.Hour + k.SetParams(ctx, types.NewConsumerParams( + true, + types.DefaultVAASTimeoutPeriod, + types.DefaultHistoricalEntries, + types.DefaultConsumerUnbondingPeriod, + threshold, + )) + + // Seed the consumer with an initial packet delivered at blockTime now. + baseTime := ctx.BlockTime() + ctxBase := ctx.WithBlockTime(baseTime) + pd1 := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pk1, Power: 10}}, 1) + require.NoError(t, k.OnRecvVSCPacketV2(ctxBase, providerClientID, pd1)) + + // Fast-forward block time past the threshold so the consumer is stale. + staleCtx := ctx.WithBlockTime(baseTime.Add(threshold + time.Minute)) + require.True(t, k.IsVSCStale(staleCtx), "consumer should be stale before resync") + + // Deliver a higher-id packet at the stale block time -- this constitutes resync. + pd2 := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pk1, Power: 20}}, 2) + require.NoError(t, k.OnRecvVSCPacketV2(staleCtx, providerClientID, pd2)) + + // After the resync, IsVSCStale should be false because lastVSCRecvTime was updated. + require.False(t, k.IsVSCStale(staleCtx), "resync must lift safe mode") +} + +// TestSnapshotResyncEmitsEvent verifies the consumer emits EventTypeSnapshotResync +// when (and only when) it applies a snapshot packet, so the resync is observable +// (used by the e2e to distinguish a snapshot from a resent diff). +func TestSnapshotResyncEmitsEvent(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pk := ed25519.GenPrivKey().PubKey() + tm, err := cryptocodec.ToCmtProtoPublicKey(pk) + require.NoError(t, err) + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{{PubKey: tm, Power: 10}}) + require.NoError(t, k.SetHighestValsetUpdateID(ctx, 1)) + k.SetProviderClientID(ctx, "07-tendermint-0") + + countSnapshotEvents := func() int { + n := 0 + for _, ev := range ctx.EventManager().Events() { + if ev.Type == types.EventTypeSnapshotResync { + n++ + } + } + return n + } + + // An ordinary diff packet must NOT emit the snapshot-resync event. + diff := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: tm, Power: 12}}, 2) + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", diff)) + require.Zero(t, countSnapshotEvents(), "a diff packet must not emit a snapshot-resync event") + + // A snapshot packet must emit exactly one. + snap := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: tm, Power: 15}}, 3) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + require.Equal(t, 1, countSnapshotEvents(), "a snapshot packet must emit exactly one snapshot-resync event") +} + +// TestSnapshotPowerChange seeds the CC set with A=10 and delivers a snapshot +// that changes A's power to 50. PendingChanges must contain A at power 50. +func TestSnapshotPowerChange(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pkA, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{{PubKey: pkA, Power: 10}}) + require.NoError(t, k.SetHighestValsetUpdateID(ctx, 1)) + k.SetProviderClientID(ctx, "07-tendermint-0") + + snap := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pkA, Power: 50}}, 2) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + + pending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + require.Len(t, pending.ValidatorUpdates, 1) + require.Equal(t, int64(50), pending.ValidatorUpdates[0].Power) +} + +// TestSnapshotAddsNewValidator seeds {A} and delivers a snapshot {A, B}. +// PendingChanges must contain both A and B at their snapshot powers, with no +// power-0 entry for A. +func TestSnapshotAddsNewValidator(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pkA, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + pkB, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{{PubKey: pkA, Power: 10}}) + require.NoError(t, k.SetHighestValsetUpdateID(ctx, 1)) + k.SetProviderClientID(ctx, "07-tendermint-0") + + snap := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{ + {PubKey: pkA, Power: 10}, + {PubKey: pkB, Power: 20}, + }, 2) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + + pending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + require.Len(t, pending.ValidatorUpdates, 2) + + powers := map[string]int64{} + for _, u := range pending.ValidatorUpdates { + powers[u.PubKey.String()] = u.Power + } + require.Equal(t, int64(10), powers[pkA.String()]) + require.Equal(t, int64(20), powers[pkB.String()]) +} + +// TestSnapshotMultipleRemovals seeds {A, B, C} and delivers a snapshot {A only}. +// PendingChanges must contain exactly 3 entries: A at its power, B=0, C=0. +func TestSnapshotMultipleRemovals(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pkA, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + pkB, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + pkC, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{ + {PubKey: pkA, Power: 30}, + {PubKey: pkB, Power: 20}, + {PubKey: pkC, Power: 10}, + }) + require.NoError(t, k.SetHighestValsetUpdateID(ctx, 1)) + k.SetProviderClientID(ctx, "07-tendermint-0") + + snap := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pkA, Power: 30}}, 2) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + + pending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + require.Len(t, pending.ValidatorUpdates, 3) + + powers := map[string]int64{} + for _, u := range pending.ValidatorUpdates { + powers[u.PubKey.String()] = u.Power + } + require.Equal(t, int64(30), powers[pkA.String()]) + require.Equal(t, int64(0), powers[pkB.String()]) + require.Equal(t, int64(0), powers[pkC.String()]) +} + +// TestSnapshotEmptyRemovesAll seeds {A, B} and delivers a snapshot with zero +// validator updates (IsSnapshot=true, empty list). PendingChanges must contain +// exactly two entries: A=0 and B=0. +func TestSnapshotEmptyRemovesAll(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pkA, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + pkB, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{ + {PubKey: pkA, Power: 10}, + {PubKey: pkB, Power: 5}, + }) + require.NoError(t, k.SetHighestValsetUpdateID(ctx, 1)) + k.SetProviderClientID(ctx, "07-tendermint-0") + + snap := types.NewValidatorSetChangePacketData(nil, 2) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + + pending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + require.Len(t, pending.ValidatorUpdates, 2) + + for _, u := range pending.ValidatorUpdates { + require.Equal(t, int64(0), u.Power, "all validators should be removed by empty snapshot") + } +} + +// TestSnapshotReplacesEarlierPendingChanges delivers a diff packet to populate +// PendingChanges, then immediately delivers a snapshot. The final PendingChanges +// must contain only snapshot-derived updates (snapshot replaces, does not merge). +func TestSnapshotReplacesEarlierPendingChanges(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pkA, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + pkB, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k.SetProviderClientID(ctx, "07-tendermint-0") + + // First: a diff packet introducing A and B. + diff := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{ + {PubKey: pkA, Power: 10}, + {PubKey: pkB, Power: 20}, + }, 1) + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", diff)) + + // Apply so the CC set reflects A and B. + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{ + {PubKey: pkA, Power: 10}, + {PubKey: pkB, Power: 20}, + }) + + // Second: a snapshot containing only A. B must be removed; the earlier diff + // entry for B must not survive in PendingChanges. + snap := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pkA, Power: 10}}, 2) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + + pending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + + powers := map[string]int64{} + for _, u := range pending.ValidatorUpdates { + powers[u.PubKey.String()] = u.Power + } + // Snapshot produced: A at 10, B at 0 (explicit removal). No other entries. + require.Len(t, pending.ValidatorUpdates, 2) + require.Equal(t, int64(10), powers[pkA.String()]) + require.Equal(t, int64(0), powers[pkB.String()]) +} + +// TestSnapshotNoDoubleEmitForUnchangedValidator seeds {A=10} and delivers a +// snapshot {A=10} (no power change). PendingChanges must contain exactly one +// entry for A -- the identity match must not duplicate the update. +func TestSnapshotNoDoubleEmitForUnchangedValidator(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pkA, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + k.ApplyCCValidatorChanges(ctx, []abci.ValidatorUpdate{{PubKey: pkA, Power: 10}}) + require.NoError(t, k.SetHighestValsetUpdateID(ctx, 1)) + k.SetProviderClientID(ctx, "07-tendermint-0") + + snap := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pkA, Power: 10}}, 2) + snap.IsSnapshot = true + require.NoError(t, k.OnRecvVSCPacketV2(ctx, "07-tendermint-0", snap)) + + pending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + require.Len(t, pending.ValidatorUpdates, 1, "unchanged validator must appear exactly once in snapshot pending changes") + require.Equal(t, int64(10), pending.ValidatorUpdates[0].Power) +} diff --git a/x/vaas/consumer/keeper/validators.go b/x/vaas/consumer/keeper/validators.go index 60f89d7..eab6c1e 100644 --- a/x/vaas/consumer/keeper/validators.go +++ b/x/vaas/consumer/keeper/validators.go @@ -308,3 +308,36 @@ func (k Keeper) GetAllValidators(ctx context.Context) ([]stakingtypes.Validator, func (k Keeper) GetBondedValidatorsByPower(goCtx context.Context) ([]stakingtypes.Validator, error) { return []stakingtypes.Validator{}, nil } + +// computeReplaceUpdates turns a complete target set into the updates needed to +// move the consumer's current cross-chain set to exactly that target: every +// target entry at its absolute power, plus a power-0 removal for each current +// validator absent from the target. +// +// Identity matching uses the cmt proto public key string, which is the same +// identity used by MustGetCurrentValidatorsAsABCIUpdates and ApplyCCValidatorChanges. +func (k Keeper) computeReplaceUpdates(ctx context.Context, target []abci.ValidatorUpdate) []abci.ValidatorUpdate { + inTarget := make(map[string]struct{}, len(target)) + for _, u := range target { + inTarget[u.PubKey.String()] = struct{}{} + } + + current := k.GetAllCCValidator(ctx) + updates := make([]abci.ValidatorUpdate, 0, len(target)+len(current)) + updates = append(updates, target...) + + for _, v := range current { + pk, err := v.ConsPubKey() + if err != nil { + continue + } + tmPk, err := cryptocodec.ToCmtProtoPublicKey(pk) + if err != nil { + continue + } + if _, ok := inTarget[tmPk.String()]; !ok { + updates = append(updates, abci.ValidatorUpdate{PubKey: tmPk, Power: 0}) + } + } + return updates +} diff --git a/x/vaas/consumer/types/genesis.pb.go b/x/vaas/consumer/types/genesis.pb.go index 07a5546..1144204 100644 --- a/x/vaas/consumer/types/genesis.pb.go +++ b/x/vaas/consumer/types/genesis.pb.go @@ -9,17 +9,20 @@ import ( _ "github.com/cometbft/cometbft/abci/types" _ "github.com/cosmos/gogoproto/gogoproto" proto "github.com/cosmos/gogoproto/proto" + github_com_cosmos_gogoproto_types "github.com/cosmos/gogoproto/types" _ "github.com/cosmos/ibc-go/v10/modules/light-clients/07-tendermint" _ "google.golang.org/protobuf/types/known/timestamppb" io "io" math "math" math_bits "math/bits" + time "time" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf +var _ = time.Kitchen // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. @@ -43,6 +46,11 @@ type GenesisState struct { // Flag indicating whether the consumer VAAS module starts in pre-VAAS state PreVAAS bool `protobuf:"varint,5,opt,name=preVAAS,proto3" json:"preVAAS,omitempty"` Provider types.ProviderInfo `protobuf:"bytes,6,opt,name=provider,proto3" json:"provider"` + // LastVSCRecvTime is the block time of the last VSC packet the consumer + // received; it drives IsVSCStale (safe mode). Round-tripped on restart so a + // state-export upgrade preserves the staleness clock. Absent for a new chain + // that has not received a VSC yet. + LastVscRecvTime *time.Time `protobuf:"bytes,7,opt,name=last_vsc_recv_time,json=lastVscRecvTime,proto3,stdtime" json:"last_vsc_recv_time,omitempty"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -120,6 +128,13 @@ func (m *GenesisState) GetProvider() types.ProviderInfo { return types.ProviderInfo{} } +func (m *GenesisState) GetLastVscRecvTime() *time.Time { + if m != nil { + return m.LastVscRecvTime + } + return nil +} + // HeightValsetUpdateID represents a mapping internal to the consumer VAAS module // which links a block height to each recv valset update id. type HeightToValsetUpdateID struct { @@ -182,36 +197,39 @@ func init() { func init() { proto.RegisterFile("vaas/consumer/v1/genesis.proto", fileDescriptor_cc650057d919c311) } var fileDescriptor_cc650057d919c311 = []byte{ - // 456 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0x41, 0x6f, 0xd3, 0x30, - 0x14, 0x6e, 0xba, 0x52, 0x3a, 0x0f, 0xa1, 0xca, 0x82, 0x12, 0x75, 0x22, 0xab, 0x76, 0xca, 0x01, - 0xc5, 0xea, 0x10, 0xe2, 0xbc, 0x15, 0x09, 0x7a, 0x9b, 0x3a, 0xd8, 0x61, 0x97, 0xc8, 0x49, 0xde, - 0x12, 0xa3, 0xc4, 0x8e, 0x62, 0x37, 0x85, 0x7f, 0xc1, 0xcf, 0xda, 0x8d, 0x1d, 0x39, 0x21, 0xd4, - 0xfe, 0x11, 0x94, 0x57, 0xa7, 0x30, 0xe8, 0xcd, 0x9f, 0xbf, 0xf7, 0xf9, 0x7d, 0xdf, 0x7b, 0x26, - 0x5e, 0xcd, 0xb9, 0x66, 0xb1, 0x92, 0x7a, 0x59, 0x40, 0xc5, 0xea, 0x29, 0x4b, 0x41, 0x82, 0x16, - 0x3a, 0x28, 0x2b, 0x65, 0x14, 0x1d, 0x36, 0x7c, 0xd0, 0xf2, 0x41, 0x3d, 0x1d, 0xbf, 0x44, 0x45, - 0x3d, 0x65, 0x3a, 0xe3, 0x15, 0x24, 0xe1, 0x8e, 0x43, 0xc1, 0x98, 0x89, 0x28, 0x66, 0xb9, 0x48, - 0x33, 0x13, 0xe7, 0x02, 0xa4, 0xd1, 0xcc, 0x80, 0x4c, 0xa0, 0x2a, 0x84, 0x34, 0x8d, 0xea, 0x0f, - 0xb2, 0x82, 0x67, 0xa9, 0x4a, 0x15, 0x1e, 0x59, 0x73, 0xb2, 0xb7, 0xb4, 0xed, 0xb2, 0x12, 0x15, - 0xd8, 0xbb, 0x93, 0x54, 0xa9, 0x34, 0x07, 0x86, 0x28, 0x5a, 0xde, 0x32, 0x23, 0x0a, 0xd0, 0x86, - 0x17, 0xa5, 0x2d, 0x38, 0xfe, 0xab, 0x15, 0x8f, 0x62, 0xc1, 0xcc, 0xd7, 0x12, 0x6c, 0x92, 0xd3, - 0xef, 0x5d, 0xf2, 0xe4, 0xfd, 0x36, 0xdb, 0x95, 0xe1, 0x06, 0xe8, 0x1b, 0xd2, 0x2f, 0x79, 0xc5, - 0x0b, 0xed, 0x3a, 0x13, 0xc7, 0x3f, 0x3a, 0x7b, 0x11, 0x60, 0xd6, 0x7a, 0x1a, 0xcc, 0x6c, 0xa4, - 0x4b, 0xa4, 0x2f, 0x7a, 0x77, 0x3f, 0x4f, 0x3a, 0x0b, 0x5b, 0x4c, 0x5f, 0x11, 0x5a, 0x56, 0xaa, - 0x16, 0x09, 0x54, 0xe1, 0x36, 0x62, 0x28, 0x12, 0xb7, 0x3b, 0x71, 0xfc, 0xc3, 0xc5, 0xb0, 0x65, - 0x66, 0x48, 0xcc, 0x13, 0x7a, 0x4c, 0x0e, 0x25, 0xac, 0xc2, 0x38, 0xe3, 0x42, 0xba, 0x07, 0x13, - 0xc7, 0x1f, 0x2c, 0x06, 0x12, 0x56, 0xb3, 0x06, 0xd3, 0xcf, 0x64, 0x9c, 0x41, 0x33, 0xaa, 0xd0, - 0xa8, 0xb0, 0xe6, 0xb9, 0x06, 0x13, 0x2e, 0xcb, 0x84, 0x1b, 0x68, 0x9e, 0xec, 0x4d, 0x0e, 0xfc, - 0xa3, 0x33, 0x3f, 0xf8, 0x77, 0x03, 0xc1, 0x07, 0xd4, 0x7c, 0x54, 0xd7, 0xa8, 0xf8, 0x84, 0x82, - 0xf9, 0x3b, 0x6b, 0x73, 0x94, 0xed, 0x63, 0x13, 0xea, 0x92, 0xc7, 0x65, 0x05, 0xd7, 0xe7, 0xe7, - 0x57, 0xee, 0x23, 0xb4, 0xd1, 0x42, 0xfa, 0x96, 0x0c, 0x5a, 0xdb, 0x6e, 0x1f, 0x27, 0xf1, 0x7c, - 0x37, 0x89, 0x4b, 0x4b, 0xcc, 0xe5, 0xad, 0xb2, 0x0d, 0x76, 0xc5, 0xa7, 0x37, 0x64, 0xb4, 0xdf, - 0x0a, 0x1d, 0x91, 0xfe, 0xd6, 0x06, 0x8e, 0xb6, 0xb7, 0xb0, 0x88, 0xfa, 0x64, 0xf8, 0x5f, 0xcc, - 0x2e, 0x56, 0x3c, 0xad, 0x1f, 0xd8, 0xbd, 0x98, 0xdf, 0xad, 0x3d, 0xe7, 0x7e, 0xed, 0x39, 0xbf, - 0xd6, 0x9e, 0xf3, 0x6d, 0xe3, 0x75, 0xee, 0x37, 0x5e, 0xe7, 0xc7, 0xc6, 0xeb, 0xdc, 0xb0, 0x54, - 0x98, 0x6c, 0x19, 0x05, 0xb1, 0x2a, 0x18, 0xcf, 0x73, 0x21, 0x23, 0x61, 0x34, 0xc3, 0xef, 0xf2, - 0x85, 0x3d, 0xfc, 0xcd, 0xb8, 0xfe, 0xa8, 0x8f, 0xfb, 0x7f, 0xfd, 0x3b, 0x00, 0x00, 0xff, 0xff, - 0x93, 0x27, 0x57, 0x0f, 0xeb, 0x02, 0x00, 0x00, + // 505 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x53, 0x41, 0x6f, 0xd3, 0x30, + 0x14, 0x6e, 0xb6, 0xd2, 0x6d, 0x1e, 0x82, 0xc9, 0x82, 0x12, 0x75, 0x22, 0xad, 0x76, 0xca, 0x01, + 0xd9, 0xea, 0x10, 0xe2, 0xbc, 0x16, 0x09, 0x7a, 0x40, 0x9a, 0xb2, 0xd1, 0xc3, 0x2e, 0x91, 0x93, + 0xbc, 0x25, 0x46, 0x49, 0x1c, 0xc5, 0xae, 0x0b, 0xff, 0x62, 0xbf, 0x80, 0xdf, 0xb3, 0xe3, 0x8e, + 0x9c, 0x00, 0xb5, 0x7f, 0x04, 0xc5, 0x75, 0x0a, 0x83, 0xdd, 0xf2, 0xf9, 0x7b, 0x9f, 0xdf, 0xfb, + 0xde, 0xe7, 0x20, 0x4f, 0x33, 0x26, 0x69, 0x2c, 0x4a, 0xb9, 0x28, 0xa0, 0xa6, 0x7a, 0x4c, 0x53, + 0x28, 0x41, 0x72, 0x49, 0xaa, 0x5a, 0x28, 0x81, 0x8f, 0x1a, 0x9e, 0xb4, 0x3c, 0xd1, 0xe3, 0xc1, + 0x4b, 0xa3, 0xd0, 0x63, 0x2a, 0x33, 0x56, 0x43, 0x12, 0x6e, 0x39, 0x23, 0x18, 0x50, 0x1e, 0xc5, + 0x34, 0xe7, 0x69, 0xa6, 0xe2, 0x9c, 0x43, 0xa9, 0x24, 0x55, 0x50, 0x26, 0x50, 0x17, 0xbc, 0x54, + 0x8d, 0xea, 0x0f, 0xb2, 0x82, 0x67, 0xa9, 0x48, 0x85, 0xf9, 0xa4, 0xcd, 0x97, 0x3d, 0xc5, 0x6d, + 0x97, 0x25, 0xaf, 0xc1, 0x9e, 0x0d, 0x53, 0x21, 0xd2, 0x1c, 0xa8, 0x41, 0xd1, 0xe2, 0x9a, 0x2a, + 0x5e, 0x80, 0x54, 0xac, 0xa8, 0x6c, 0xc1, 0xf1, 0x5f, 0xad, 0x58, 0x14, 0x73, 0xaa, 0xbe, 0x56, + 0x60, 0x9d, 0x9c, 0x7c, 0xdb, 0x45, 0x8f, 0xdf, 0x6f, 0xbc, 0x5d, 0x28, 0xa6, 0x00, 0xbf, 0x41, + 0xbd, 0x8a, 0xd5, 0xac, 0x90, 0xae, 0x33, 0x72, 0xfc, 0xc3, 0xd3, 0x17, 0xc4, 0x78, 0xd5, 0x63, + 0x32, 0xb5, 0x96, 0xce, 0x0d, 0x3d, 0xe9, 0xde, 0xfe, 0x18, 0x76, 0x02, 0x5b, 0x8c, 0x5f, 0x21, + 0x5c, 0xd5, 0x42, 0xf3, 0x04, 0xea, 0x70, 0x63, 0x31, 0xe4, 0x89, 0xbb, 0x33, 0x72, 0xfc, 0x83, + 0xe0, 0xa8, 0x65, 0xa6, 0x86, 0x98, 0x25, 0xf8, 0x18, 0x1d, 0x94, 0xb0, 0x0c, 0xe3, 0x8c, 0xf1, + 0xd2, 0xdd, 0x1d, 0x39, 0xfe, 0x7e, 0xb0, 0x5f, 0xc2, 0x72, 0xda, 0x60, 0xfc, 0x19, 0x0d, 0x32, + 0x68, 0x56, 0x15, 0x2a, 0x11, 0x6a, 0x96, 0x4b, 0x50, 0xe1, 0xa2, 0x4a, 0x98, 0x82, 0xe6, 0xca, + 0xee, 0x68, 0xd7, 0x3f, 0x3c, 0xf5, 0xc9, 0xbf, 0x09, 0x90, 0x0f, 0x46, 0x73, 0x29, 0xe6, 0x46, + 0xf1, 0xc9, 0x08, 0x66, 0xef, 0xec, 0x98, 0xfd, 0xec, 0x21, 0x36, 0xc1, 0x2e, 0xda, 0xab, 0x6a, + 0x98, 0x9f, 0x9d, 0x5d, 0xb8, 0x8f, 0xcc, 0x18, 0x2d, 0xc4, 0x6f, 0xd1, 0x7e, 0x3b, 0xb6, 0xdb, + 0x33, 0x9b, 0x78, 0xbe, 0xdd, 0xc4, 0xb9, 0x25, 0x66, 0xe5, 0xb5, 0xb0, 0x0d, 0xb6, 0xc5, 0xf8, + 0x23, 0xc2, 0x39, 0x93, 0x2a, 0xd4, 0x32, 0x0e, 0x6b, 0x88, 0x75, 0xd8, 0xe4, 0xe1, 0xee, 0x99, + 0x2b, 0x06, 0x64, 0x13, 0x16, 0x69, 0xc3, 0x22, 0x97, 0x6d, 0x58, 0x93, 0xee, 0xcd, 0xcf, 0xa1, + 0x13, 0x3c, 0x6d, 0xb4, 0x73, 0x19, 0x07, 0x10, 0xeb, 0x86, 0x3b, 0xb9, 0x42, 0xfd, 0x87, 0x9d, + 0xe1, 0x3e, 0xea, 0x6d, 0x5c, 0x99, 0xa4, 0xba, 0x81, 0x45, 0xd8, 0x47, 0x47, 0xff, 0x6d, 0x6d, + 0xc7, 0x54, 0x3c, 0xd1, 0xf7, 0xdc, 0x4f, 0x66, 0xb7, 0x2b, 0xcf, 0xb9, 0x5b, 0x79, 0xce, 0xaf, + 0x95, 0xe7, 0xdc, 0xac, 0xbd, 0xce, 0xdd, 0xda, 0xeb, 0x7c, 0x5f, 0x7b, 0x9d, 0x2b, 0x9a, 0x72, + 0x95, 0x2d, 0x22, 0x12, 0x8b, 0x82, 0xb2, 0x3c, 0xe7, 0x65, 0xc4, 0x95, 0xa4, 0xe6, 0xf5, 0x7d, + 0xa1, 0xf7, 0x7f, 0x0e, 0xf3, 0x9a, 0xa2, 0x9e, 0x71, 0xf4, 0xfa, 0x77, 0x00, 0x00, 0x00, 0xff, + 0xff, 0x76, 0x02, 0x7a, 0x56, 0x3a, 0x03, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -234,6 +252,16 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.LastVscRecvTime != nil { + n1, err1 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(*m.LastVscRecvTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.LastVscRecvTime):]) + if err1 != nil { + return 0, err1 + } + i -= n1 + i = encodeVarintGenesis(dAtA, i, uint64(n1)) + i-- + dAtA[i] = 0x3a + } { size, err := m.Provider.MarshalToSizedBuffer(dAtA[:i]) if err != nil { @@ -368,6 +396,10 @@ func (m *GenesisState) Size() (n int) { } l = m.Provider.Size() n += 1 + l + sovGenesis(uint64(l)) + if m.LastVscRecvTime != nil { + l = github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.LastVscRecvTime) + n += 1 + l + sovGenesis(uint64(l)) + } return n } @@ -593,6 +625,42 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LastVscRecvTime", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.LastVscRecvTime == nil { + m.LastVscRecvTime = new(time.Time) + } + if err := github_com_cosmos_gogoproto_types.StdTimeUnmarshal(m.LastVscRecvTime, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/vaas/consumer/types/genesis_test.go b/x/vaas/consumer/types/genesis_test.go index 7c84d23..168db28 100644 --- a/x/vaas/consumer/types/genesis_test.go +++ b/x/vaas/consumer/types/genesis_test.go @@ -126,6 +126,7 @@ func TestValidateInitialGenesisState(t *testing.T) { 0, vaastypes.DefaultHistoricalEntries, vaastypes.DefaultConsumerUnbondingPeriod, + vaastypes.DefaultSafeModeThreshold, )), true, }, @@ -216,6 +217,7 @@ func TestValidateRestartConsumerGenesisState(t *testing.T) { 0, vaastypes.DefaultHistoricalEntries, vaastypes.DefaultConsumerUnbondingPeriod, + vaastypes.DefaultSafeModeThreshold, )), true, }, diff --git a/x/vaas/consumer/types/keys.go b/x/vaas/consumer/types/keys.go index df51b4f..b3dc7cb 100644 --- a/x/vaas/consumer/types/keys.go +++ b/x/vaas/consumer/types/keys.go @@ -35,4 +35,5 @@ var ( HighestValsetUpdateIDPrefix = collections.NewPrefix(12) ConsumerDebtPrefix = collections.NewPrefix(13) PendingEvidencePacketsPrefix = collections.NewPrefix(14) + LastVSCRecvTimePrefix = collections.NewPrefix(15) ) diff --git a/x/vaas/consumer/types/params_test.go b/x/vaas/consumer/types/params_test.go index 8f3e587..1a00d64 100644 --- a/x/vaas/consumer/types/params_test.go +++ b/x/vaas/consumer/types/params_test.go @@ -19,19 +19,23 @@ func TestValidateParams(t *testing.T) { {"default params", vaastypes.DefaultConsumerParams(), true}, { "custom valid params", - vaastypes.NewConsumerParams(true, vaastypes.DefaultVAASTimeoutPeriod, 1000, 24*21*time.Hour), true, + vaastypes.NewConsumerParams(true, vaastypes.DefaultVAASTimeoutPeriod, 1000, 24*21*time.Hour, vaastypes.DefaultSafeModeThreshold), true, }, { "custom invalid params, VAAS timeout", - vaastypes.NewConsumerParams(true, 0, 1000, 24*21*time.Hour), false, + vaastypes.NewConsumerParams(true, 0, 1000, 24*21*time.Hour, vaastypes.DefaultSafeModeThreshold), false, }, { "custom invalid params, negative num historical entries", - vaastypes.NewConsumerParams(true, vaastypes.DefaultVAASTimeoutPeriod, -100, 24*21*time.Hour), false, + vaastypes.NewConsumerParams(true, vaastypes.DefaultVAASTimeoutPeriod, -100, 24*21*time.Hour, vaastypes.DefaultSafeModeThreshold), false, }, { "custom invalid params, negative unbonding period", - vaastypes.NewConsumerParams(true, vaastypes.DefaultVAASTimeoutPeriod, 1000, -24*21*time.Hour), false, + vaastypes.NewConsumerParams(true, vaastypes.DefaultVAASTimeoutPeriod, 1000, -24*21*time.Hour, vaastypes.DefaultSafeModeThreshold), false, + }, + { + "custom invalid params, zero safe mode threshold", + vaastypes.NewConsumerParams(true, vaastypes.DefaultVAASTimeoutPeriod, 1000, 24*21*time.Hour, 0), false, }, } diff --git a/x/vaas/provider/client/cli/tx.go b/x/vaas/provider/client/cli/tx.go index 7633401..611fb2a 100644 --- a/x/vaas/provider/client/cli/tx.go +++ b/x/vaas/provider/client/cli/tx.go @@ -36,7 +36,6 @@ func GetTxCmd() *cobra.Command { cmd.AddCommand(NewSubmitConsumerDoubleVotingCmd()) cmd.AddCommand(NewCreateConsumerCmd()) cmd.AddCommand(NewUpdateConsumerCmd()) - cmd.AddCommand(NewRemoveConsumerCmd()) cmd.AddCommand(NewFundConsumerFeePoolCmd()) cmd.AddCommand(NewWithdrawConsumerFeePoolCmd()) cmd.AddCommand(NewSweepConsumerFeePoolCmd()) @@ -388,53 +387,6 @@ If one of the fields is missing, it will be set to its zero value. return cmd } -func NewRemoveConsumerCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "remove-consumer [consumer-id]", - Short: "remove a consumer chain", - Long: strings.TrimSpace( - fmt.Sprintf(`Removes (and stops) a consumer chain. Note that only the owner of the chain can remove it. -Example: -%s tx provider remove-consumer [consumer-id] -`, version.AppName)), - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - clientCtx, err := client.GetClientTxContext(cmd) - if err != nil { - return err - } - - txf, err := tx.NewFactoryCLI(clientCtx, cmd.Flags()) - if err != nil { - return err - } - txf = txf.WithTxConfig(clientCtx.TxConfig).WithAccountRetriever(clientCtx.AccountRetriever) - - owner := clientCtx.GetFromAddress().String() - consumerId, err := parseConsumerIdArg(args[0]) - if err != nil { - return err - } - - msg, err := types.NewMsgRemoveConsumer(owner, consumerId) - if err != nil { - return err - } - if err := msg.ValidateBasic(); err != nil { - return err - } - - return tx.GenerateOrBroadcastTxWithFactory(clientCtx, txf, msg) - }, - } - - flags.AddTxFlagsToCmd(cmd) - - _ = cmd.MarkFlagRequired(flags.FlagFrom) - - return cmd -} - func NewFundConsumerFeePoolCmd() *cobra.Command { cmd := &cobra.Command{ Use: "fund-consumer-fee-pool [consumer-id] [amount]", diff --git a/x/vaas/provider/ibc_module.go b/x/vaas/provider/ibc_module.go index e869b25..8237f33 100644 --- a/x/vaas/provider/ibc_module.go +++ b/x/vaas/provider/ibc_module.go @@ -173,7 +173,13 @@ func (im IBCModule) OnAcknowledgementPacket( ackError = "error acknowledgement received" } - if err := im.keeper.OnAcknowledgementPacketV2(ctx, sourceClient, ackError); err != nil { + ackVscId := uint64(0) + var vsc vaastypes.ValidatorSetChangePacketData + if err := vaastypes.ModuleCdc.UnmarshalJSON(payload.Value, &vsc); err == nil { + ackVscId = vsc.ValsetUpdateId + } + + if err := im.keeper.OnAcknowledgementPacketV2(ctx, sourceClient, ackVscId, ackError); err != nil { return err } diff --git a/x/vaas/provider/ibc_module_test.go b/x/vaas/provider/ibc_module_test.go index d56971a..74ab3db 100644 --- a/x/vaas/provider/ibc_module_test.go +++ b/x/vaas/provider/ibc_module_test.go @@ -2,7 +2,6 @@ package provider_test import ( "testing" - "time" testkeeper "github.com/allinbits/vaas/testutil/keeper" "github.com/allinbits/vaas/x/vaas/provider" @@ -35,7 +34,7 @@ func TestIBCModuleOnSendPacketRequiresAuthority(t *testing.T) { } func TestIBCModuleOnAcknowledgementPacketHandlesErrorSentinel(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() consumerID := uint64(0) @@ -45,8 +44,6 @@ func TestIBCModuleOnAcknowledgementPacketHandlesErrorSentinel(t *testing.T) { providerKeeper.SetConsumerPhase(ctx, consumerID, providertypes.CONSUMER_PHASE_LAUNCHED) providerKeeper.SetConsumerChainId(ctx, consumerID, "consumer-chain") - mocks.MockStakingKeeper.EXPECT().UnbondingTime(ctx).Return(21*24*time.Hour, nil).Times(1) - module := provider.NewIBCModule(&providerKeeper) err := module.OnAcknowledgementPacket( ctx, @@ -58,12 +55,13 @@ func TestIBCModuleOnAcknowledgementPacketHandlesErrorSentinel(t *testing.T) { sdk.AccAddress{}, ) require.NoError(t, err) - require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, providerKeeper.GetConsumerPhase(ctx, consumerID)) + // Consumer stays launched; liveness sweep owns removal + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, providerKeeper.GetConsumerPhase(ctx, consumerID)) require.Equal(t, "65", findEventAttributeValue(ctx.EventManager().Events(), vaastypes.EventTypePacket, "sequence")) } func TestIBCModuleOnTimeoutPacketFormatsSequence(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() consumerID := uint64(0) @@ -73,8 +71,6 @@ func TestIBCModuleOnTimeoutPacketFormatsSequence(t *testing.T) { providerKeeper.SetConsumerPhase(ctx, consumerID, providertypes.CONSUMER_PHASE_LAUNCHED) providerKeeper.SetConsumerChainId(ctx, consumerID, "consumer-chain") - mocks.MockStakingKeeper.EXPECT().UnbondingTime(ctx).Return(21*24*time.Hour, nil).Times(1) - module := provider.NewIBCModule(&providerKeeper) err := module.OnTimeoutPacket( ctx, diff --git a/x/vaas/provider/keeper/consumer_lifecycle.go b/x/vaas/provider/keeper/consumer_lifecycle.go index 37fa258..72f9067 100644 --- a/x/vaas/provider/keeper/consumer_lifecycle.go +++ b/x/vaas/provider/keeper/consumer_lifecycle.go @@ -232,7 +232,7 @@ func (k Keeper) LaunchConsumer( } // compute consumer initial validator set (all validators validate all consumers) - initialValUpdates, err := k.ComputeConsumerNextValSet(ctx, bondedValidators, consumerId, []types.ConsensusValidator{}) + initialValUpdates, err := k.ComputeConsumerNextValSet(ctx, bondedValidators, consumerId, []types.ConsensusValidator{}, false) if err != nil { return fmt.Errorf("computing consumer next validator set, consumerId(%d): %w", consumerId, err) } @@ -254,6 +254,9 @@ func (k Keeper) LaunchConsumer( k.SetEquivocationEvidenceMinHeight(ctx, consumerId, initializationRecord.InitialHeight.RevisionHeight) k.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_LAUNCHED) + if err := k.SetConsumerLastAckTime(ctx, consumerId, ctx.BlockTime()); err != nil { + return err + } k.Logger(ctx).Info("consumer successfully launched", "consumerId", consumerId, @@ -281,6 +284,7 @@ func (k Keeper) MakeConsumerGenesis( initializationRecord.VaasTimeoutPeriod, initializationRecord.HistoricalEntries, initializationRecord.UnbondingPeriod, + initializationRecord.SafeModeThreshold, ) providerUnbondingPeriod, err := k.stakingKeeper.UnbondingTime(ctx) @@ -413,6 +417,9 @@ func (k Keeper) DeleteConsumerChain(ctx sdk.Context, consumerId uint64) (err err k.DeleteConsumerValSet(ctx, consumerId) k.DeleteConsumerRemovalTime(ctx, consumerId) + k.DeleteConsumerLastAckTime(ctx, consumerId) + k.DeleteConsumerHighestSentVscId(ctx, consumerId) + k.DeleteConsumerHighestAckedVscId(ctx, consumerId) k.DeleteConsumerDebt(ctx, consumerId) if err := k.ConsumerFeesPerBlockOverride.Remove(ctx, consumerId); err != nil { @@ -438,3 +445,33 @@ func (k Keeper) DeleteConsumerChain(ctx sdk.Context, consumerId uint64) (err err return nil } + +// SweepUnresponsiveConsumers stops any launched consumer that has produced no +// successful VSC ack within the liveness grace period. It is the sole authority +// for removing unresponsive consumers and runs on the provider block clock, so +// it does not depend on a live IBC client or an available relayer. +func (k Keeper) SweepUnresponsiveConsumers(ctx sdk.Context) error { + grace, err := k.LivenessGracePeriod(ctx) + if err != nil { + return err + } + for _, consumerId := range k.GetAllLaunchedConsumerIds(ctx) { + lastAck := k.GetConsumerLastAckTime(ctx, consumerId) + if ctx.BlockTime().Sub(lastAck) <= grace { + continue + } + k.Logger(ctx).Info("consumer unresponsive past liveness grace, stopping", + "consumerId", consumerId, "lastAck", lastAck, "grace", grace) + // Stop in a cached context so a partial failure does not commit the + // STOPPED phase without also scheduling removal (which would strand the + // consumer STOPPED-but-never-DELETED). On error nothing is written and + // the next sweep retries, mirroring BeginBlockRemoveConsumers. + cachedCtx, writeFn := ctx.CacheContext() + if err := k.StopAndPrepareForConsumerRemoval(cachedCtx, consumerId); err != nil { + k.Logger(ctx).Error("failed to stop unresponsive consumer", "consumerId", consumerId, "error", err.Error()) + continue + } + writeFn() + } + return nil +} diff --git a/x/vaas/provider/keeper/genesis.go b/x/vaas/provider/keeper/genesis.go index 93eba23..1e0b891 100644 --- a/x/vaas/provider/keeper/genesis.go +++ b/x/vaas/provider/keeper/genesis.go @@ -81,6 +81,19 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState) []abc panic(fmt.Errorf("init: set removal time for %d: %w", consumerId, err)) } } + // Restore the liveness clock (see ExportGenesis): only when present, so + // a consumer that never launched keeps the absent-defaults. + if cs.LastAckTime != nil { + if err := k.SetConsumerLastAckTime(ctx, consumerId, *cs.LastAckTime); err != nil { + panic(fmt.Errorf("init: set last ack time for %d: %w", consumerId, err)) + } + } + if cs.HighestSentVscId != 0 { + k.SetConsumerHighestSentVscId(ctx, consumerId, cs.HighestSentVscId) + } + if cs.HighestAckedVscId != 0 { + k.SetConsumerHighestAckedVscId(ctx, consumerId, cs.HighestAckedVscId) + } if len(cs.PendingValsetChanges) > 0 { k.AppendPendingVSCPackets(ctx, consumerId, cs.PendingValsetChanges...) } @@ -271,6 +284,11 @@ func (k Keeper) InitGenesisValUpdates(ctx sdk.Context) []abci.ValidatorUpdate { // Spawn-time queue, removal-time queue, equivocation-evidence-min-height, // and per-consumer debt are NOT exported because they are derivable from // the per-consumer fields above and / or other module state at InitGenesis. +// +// The liveness clock (last-ack time and the highest-sent / highest-acked VSC +// ids) IS exported per consumer and restored at InitGenesis, so a state-export +// restart preserves each consumer's grace window and resync counters rather +// than resetting them. func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { allConsumerIds := k.GetAllConsumerIds(ctx) @@ -322,6 +340,25 @@ func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { panic(fmt.Errorf("export: failed to read removal time for consumer %d: %w", consumerId, err)) } + // Liveness clock: export the last-ack time only when actually recorded + // (GetConsumerLastAckTime falls back to block time when absent, which we + // must not persist), and the resync counters only when non-zero (zero is + // their absent default). + hasAck, err := k.ConsumerLastAckTime.Has(ctx, consumerId) + if err != nil { + panic(fmt.Errorf("export: failed to check last ack time for consumer %d: %w", consumerId, err)) + } + if hasAck { + ackCopy := k.GetConsumerLastAckTime(ctx, consumerId) + cs.LastAckTime = &ackCopy + } + if v := k.GetConsumerHighestSentVscId(ctx, consumerId); v != 0 { + cs.HighestSentVscId = v + } + if v := k.GetConsumerHighestAckedVscId(ctx, consumerId); v != 0 { + cs.HighestAckedVscId = v + } + consumerStates = append(consumerStates, cs) } diff --git a/x/vaas/provider/keeper/genesis_test.go b/x/vaas/provider/keeper/genesis_test.go index 6faa9bb..6a32ae5 100644 --- a/x/vaas/provider/keeper/genesis_test.go +++ b/x/vaas/provider/keeper/genesis_test.go @@ -74,6 +74,7 @@ func TestExportGenesisIncludesNewFields(t *testing.T) { HistoricalEntries: 10, } removalTime := time.Unix(1_800_000_000, 0).UTC() + lastAckTime := time.Unix(1_850_000_000, 0).UTC() cg := *vaastypes.DefaultConsumerGenesisState() cg.NewChain = true @@ -105,6 +106,9 @@ func TestExportGenesisIncludesNewFields(t *testing.T) { require.NoError(t, pk.SetConsumerInitializationParameters(ctx, 2, initParams)) pk.SetConsumerClientId(ctx, 2, "07-tendermint-0") require.NoError(t, pk.SetConsumerGenesis(ctx, 2, cg)) + require.NoError(t, pk.SetConsumerLastAckTime(ctx, 2, lastAckTime)) + pk.SetConsumerHighestSentVscId(ctx, 2, 9) + pk.SetConsumerHighestAckedVscId(ctx, 2, 8) // id 3 STOPPED. pk.SetConsumerPhase(ctx, 3, providertypes.CONSUMER_PHASE_STOPPED) @@ -147,6 +151,12 @@ func TestExportGenesisIncludesNewFields(t *testing.T) { require.NotNil(t, byID["consumer-delta"].RemovalTime, "STOPPED must carry removal_time") require.Equal(t, removalTime, *byID["consumer-delta"].RemovalTime) require.Equal(t, providertypes.CONSUMER_PHASE_DELETED, byID["consumer-epsilon"].Phase) + + // LAUNCHED consumer carries the liveness clock (last-ack + resync counters). + require.NotNil(t, byID["consumer-gamma"].LastAckTime, "LAUNCHED must carry last_ack_time") + require.Equal(t, lastAckTime, *byID["consumer-gamma"].LastAckTime) + require.Equal(t, uint64(9), byID["consumer-gamma"].HighestSentVscId) + require.Equal(t, uint64(8), byID["consumer-gamma"].HighestAckedVscId) } func TestInitGenesisRestoresPerConsumerStateAndDerivedQueues(t *testing.T) { @@ -360,6 +370,12 @@ func TestGenesisRoundTrip(t *testing.T) { pkA.SetValidatorByConsumerAddr(ctxA, keyedConsumerID, assignedConsumerConsAddr, assignedProviderConsAddr) pkA.AppendConsumerAddrsToPrune(ctxA, keyedConsumerID, pruneTs, prunedAddr) + // Seed the liveness clock on the LAUNCHED consumer so the round-trip covers + // the last-ack time and the resync counters (highest sent / acked). + require.NoError(t, pkA.SetConsumerLastAckTime(ctxA, keyedConsumerID, time.Unix(1_850_000_000, 0).UTC())) + pkA.SetConsumerHighestSentVscId(ctxA, keyedConsumerID, 7) + pkA.SetConsumerHighestAckedVscId(ctxA, keyedConsumerID, 5) + pkA.SetParams(ctxA, providertypes.DefaultParams()) pkA.SetValidatorSetUpdateId(ctxA, 1) diff --git a/x/vaas/provider/keeper/grpc_query.go b/x/vaas/provider/keeper/grpc_query.go index 7eb9445..5d34bd9 100644 --- a/x/vaas/provider/keeper/grpc_query.go +++ b/x/vaas/provider/keeper/grpc_query.go @@ -593,6 +593,31 @@ func (k Keeper) QueryConsumerFeesPerBlock( }, nil } +func (k Keeper) QueryConsumerLiveness(goCtx context.Context, req *types.QueryConsumerLivenessRequest) (*types.QueryConsumerLivenessResponse, error) { + if req == nil { + return nil, status.Errorf(codes.InvalidArgument, "empty request") + } + ctx := sdk.UnwrapSDKContext(goCtx) + + if k.GetConsumerPhase(ctx, req.ConsumerId) == types.CONSUMER_PHASE_UNSPECIFIED { + return nil, status.Errorf(codes.InvalidArgument, "unknown consumer: %d", req.ConsumerId) + } + + grace, err := k.LivenessGracePeriod(ctx) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + lastAck := k.GetConsumerLastAckTime(ctx, req.ConsumerId) + removalEta := lastAck.Add(grace) + + return &types.QueryConsumerLivenessResponse{ + LastAckTime: lastAck, + GracePeriod: grace, + RemovalEta: removalEta, + Degraded: ctx.BlockTime().After(lastAck.Add(grace / 2)), + }, nil +} + // QueryAllConsumerFeesPerBlockOverrides returns the full list of overrides, // paginated, ordered by consumer_id ascending. func (k Keeper) QueryAllConsumerFeesPerBlockOverrides( diff --git a/x/vaas/provider/keeper/grpc_query_test.go b/x/vaas/provider/keeper/grpc_query_test.go index f3a7bf1..bc7fcbe 100644 --- a/x/vaas/provider/keeper/grpc_query_test.go +++ b/x/vaas/provider/keeper/grpc_query_test.go @@ -2,10 +2,12 @@ package keeper_test import ( "testing" + "time" "cosmossdk.io/collections" "cosmossdk.io/math" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -256,3 +258,59 @@ func TestQueryConsumerFeePoolClaims_UnknownConsumer(t *testing.T) { require.Error(t, err) require.Equal(t, codes.NotFound, status.Code(err)) } + +func TestQueryConsumerLiveness(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).AnyTimes() + + const cid = uint64(0) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime())) + + resp, err := k.QueryConsumerLiveness(ctx, &providertypes.QueryConsumerLivenessRequest{ConsumerId: cid}) + require.NoError(t, err) + require.False(t, resp.Degraded) + require.Equal(t, ctx.BlockTime().Add(resp.GracePeriod).UTC(), resp.RemovalEta.UTC()) +} + +// TestQueryConsumerLiveness_DegradedFlag verifies that Degraded is true when +// the last ack time is more than grace/2 ago. +// +// The Degraded condition is: blockTime.After(lastAck + grace/2). +// We set lastAck = blockTime - grace/2 - 1ns so elapsed > grace/2 by 1ns. +func TestQueryConsumerLiveness_DegradedFlag(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + unbonding := 21 * 24 * time.Hour + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).AnyTimes() + + const cid = uint64(0) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + + // Compute grace first so we can position lastAck precisely. + grace, err := k.LivenessGracePeriod(ctx) + require.NoError(t, err) + + // lastAck is grace/2 + 1ns before blockTime: blockTime.After(lastAck + grace/2) == true. + lastAck := ctx.BlockTime().Add(-(grace/2 + time.Nanosecond)) + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, lastAck)) + + resp, err := k.QueryConsumerLiveness(ctx, &providertypes.QueryConsumerLivenessRequest{ConsumerId: cid}) + require.NoError(t, err) + require.True(t, resp.Degraded, "expected Degraded=true when lastAck is past the half-grace mark") +} + +// TestQueryConsumerLiveness_UnknownConsumer verifies that a consumer whose phase +// is UNSPECIFIED (never registered) causes QueryConsumerLiveness to return a +// codes.InvalidArgument gRPC error. +func TestQueryConsumerLiveness_UnknownConsumer(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + // Consumer 999 was never registered; its phase is UNSPECIFIED. + _, err := k.QueryConsumerLiveness(ctx, &providertypes.QueryConsumerLivenessRequest{ConsumerId: 999}) + require.Error(t, err) + require.Equal(t, codes.InvalidArgument, status.Code(err)) +} diff --git a/x/vaas/provider/keeper/ibc_v2_integration_test.go b/x/vaas/provider/keeper/ibc_v2_integration_test.go index 11dff43..5e71403 100644 --- a/x/vaas/provider/keeper/ibc_v2_integration_test.go +++ b/x/vaas/provider/keeper/ibc_v2_integration_test.go @@ -2,7 +2,6 @@ package keeper_test import ( "testing" - "time" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -97,9 +96,9 @@ func TestIBCV2MultipleConsumers(t *testing.T) { } } -// TestIBCV2ConsumerRemovalOnTimeout tests that a timeout triggers consumer removal. -func TestIBCV2ConsumerRemovalOnTimeout(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) +// TestIBCV2TimeoutIsLogOnly tests that a timeout is log-only; liveness sweep owns removal. +func TestIBCV2TimeoutIsLogOnly(t *testing.T) { + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() consumerId := uint64(0) @@ -116,18 +115,17 @@ func TestIBCV2ConsumerRemovalOnTimeout(t *testing.T) { require.True(t, found) require.Equal(t, consumerId, gotConsumerId) - mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(time.Hour*24*21, nil).Times(1) - err := providerKeeper.OnTimeoutPacketV2(ctx, clientId) require.NoError(t, err) + // Consumer stays launched; liveness sweep will handle removal phase = providerKeeper.GetConsumerPhase(ctx, consumerId) - require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, phase) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) } -// TestIBCV2ConsumerRemovalOnErrorAck tests that an error acknowledgement triggers consumer removal. -func TestIBCV2ConsumerRemovalOnErrorAck(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) +// TestIBCV2ErrorAckIsLogOnly tests that an error acknowledgement is log-only; liveness sweep owns removal. +func TestIBCV2ErrorAckIsLogOnly(t *testing.T) { + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() consumerId := uint64(0) @@ -140,13 +138,12 @@ func TestIBCV2ConsumerRemovalOnErrorAck(t *testing.T) { phase := providerKeeper.GetConsumerPhase(ctx, consumerId) require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) - mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(time.Hour*24*21, nil).Times(1) - - err := providerKeeper.OnAcknowledgementPacketV2(ctx, clientId, "packet decode error") + err := providerKeeper.OnAcknowledgementPacketV2(ctx, clientId, 0, "packet decode error") require.NoError(t, err) + // Consumer stays launched; liveness sweep will handle removal phase = providerKeeper.GetConsumerPhase(ctx, consumerId) - require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, phase) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) } // TestIBCV2DualModeRouting tests that the provider correctly selects v2 routing diff --git a/x/vaas/provider/keeper/keeper.go b/x/vaas/provider/keeper/keeper.go index 1993ee6..003b35f 100644 --- a/x/vaas/provider/keeper/keeper.go +++ b/x/vaas/provider/keeper/keeper.go @@ -74,6 +74,9 @@ type Keeper struct { ConsumerFeesPerBlockOverride collections.Map[uint64, math.Int] EquivocationEvidenceMinHeight collections.Map[uint64, uint64] ConsumerRemovalTime collections.Map[uint64, []byte] + ConsumerLastAckTime collections.Map[uint64, []byte] + ConsumerHighestSentVscId collections.Map[uint64, uint64] + ConsumerHighestAckedVscId collections.Map[uint64, uint64] SpawnTimeToConsumerIds collections.Map[[]byte, types.ConsumerIds] RemovalTimeToConsumerIds collections.Map[[]byte, types.ConsumerIds] @@ -184,6 +187,9 @@ func NewKeeper( ConsumerFeesPerBlockOverride: collections.NewMap(sb, types.ConsumerIdToFeesPerBlockOverridePrefix, types.ConsumerIdToFeesPerBlockOverrideKeyName, collections.Uint64Key, sdk.IntValue), EquivocationEvidenceMinHeight: collections.NewMap(sb, types.EquivocationEvidenceMinHeightPrefix, "equivocation_evidence_min_height", collections.Uint64Key, collections.Uint64Value), ConsumerRemovalTime: collections.NewMap(sb, types.ConsumerIdToRemovalTimePrefix, "consumer_removal_time", collections.Uint64Key, collections.BytesValue), + ConsumerLastAckTime: collections.NewMap(sb, types.ConsumerIdToLastAckTimePrefix, "consumer_last_ack_time", collections.Uint64Key, collections.BytesValue), + ConsumerHighestSentVscId: collections.NewMap(sb, types.ConsumerIdToHighestSentVscIdPrefix, "consumer_highest_sent_vsc_id", collections.Uint64Key, collections.Uint64Value), + ConsumerHighestAckedVscId: collections.NewMap(sb, types.ConsumerIdToHighestAckedVscIdPrefix, "consumer_highest_acked_vsc_id", collections.Uint64Key, collections.Uint64Value), SpawnTimeToConsumerIds: collections.NewMap(sb, types.SpawnTimeToConsumerIdsPrefix, "spawn_time_to_consumer_ids", collections.BytesKey, codec.CollValue[types.ConsumerIds](cdc)), RemovalTimeToConsumerIds: collections.NewMap(sb, types.RemovalTimeToConsumerIdsPrefix, "removal_time_to_consumer_ids", collections.BytesKey, codec.CollValue[types.ConsumerIds](cdc)), @@ -571,6 +577,76 @@ func (k Keeper) DeleteConsumerRemovalTime(ctx context.Context, consumerId uint64 } } +// SetConsumerLastAckTime records the block time of the consumer's most recent successful VSC ack. +func (k Keeper) SetConsumerLastAckTime(ctx context.Context, consumerId uint64, t time.Time) error { + buf, err := t.MarshalBinary() + if err != nil { + return fmt.Errorf("failed to marshal last ack time (%+v) for consumer id (%d): %w", t, consumerId, err) + } + return k.ConsumerLastAckTime.Set(ctx, consumerId, buf) +} + +// GetConsumerLastAckTime returns the consumer's last successful VSC ack time. +// When absent it returns the current block time, so a consumer is never treated +// as stale before its first recorded ack. +func (k Keeper) GetConsumerLastAckTime(ctx context.Context, consumerId uint64) time.Time { + buf, err := k.ConsumerLastAckTime.Get(ctx, consumerId) + if err != nil { + return sdk.UnwrapSDKContext(ctx).BlockTime() + } + var t time.Time + if err := t.UnmarshalBinary(buf); err != nil { + return sdk.UnwrapSDKContext(ctx).BlockTime() + } + return t +} + +func (k Keeper) DeleteConsumerLastAckTime(ctx context.Context, consumerId uint64) { + if err := k.ConsumerLastAckTime.Remove(ctx, consumerId); err != nil { + panic(fmt.Errorf("failed to delete last ack time for consumer id (%d): %w", consumerId, err)) + } +} + +func (k Keeper) SetConsumerHighestSentVscId(ctx context.Context, consumerId, vscId uint64) { + if err := k.ConsumerHighestSentVscId.Set(ctx, consumerId, vscId); err != nil { + panic(fmt.Errorf("failed to set highest sent vscId for consumer id (%d): %w", consumerId, err)) + } +} + +func (k Keeper) GetConsumerHighestSentVscId(ctx context.Context, consumerId uint64) uint64 { + v, err := k.ConsumerHighestSentVscId.Get(ctx, consumerId) + if err != nil { + return 0 + } + return v +} + +func (k Keeper) DeleteConsumerHighestSentVscId(ctx context.Context, consumerId uint64) { + if err := k.ConsumerHighestSentVscId.Remove(ctx, consumerId); err != nil { + panic(fmt.Errorf("failed to delete highest sent vscId for consumer id (%d): %w", consumerId, err)) + } +} + +func (k Keeper) SetConsumerHighestAckedVscId(ctx context.Context, consumerId, vscId uint64) { + if err := k.ConsumerHighestAckedVscId.Set(ctx, consumerId, vscId); err != nil { + panic(fmt.Errorf("failed to set highest acked vscId for consumer id (%d): %w", consumerId, err)) + } +} + +func (k Keeper) GetConsumerHighestAckedVscId(ctx context.Context, consumerId uint64) uint64 { + v, err := k.ConsumerHighestAckedVscId.Get(ctx, consumerId) + if err != nil { + return 0 + } + return v +} + +func (k Keeper) DeleteConsumerHighestAckedVscId(ctx context.Context, consumerId uint64) { + if err := k.ConsumerHighestAckedVscId.Remove(ctx, consumerId); err != nil { + panic(fmt.Errorf("failed to delete highest acked vscId for consumer id (%d): %w", consumerId, err)) + } +} + // timeToBytes converts a time.Time to bytes for use as a key func timeToBytes(t time.Time) []byte { bz, _ := t.MarshalBinary() diff --git a/x/vaas/provider/keeper/liveness_test.go b/x/vaas/provider/keeper/liveness_test.go new file mode 100644 index 0000000..316461b --- /dev/null +++ b/x/vaas/provider/keeper/liveness_test.go @@ -0,0 +1,345 @@ +package keeper_test + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + sdk "github.com/cosmos/cosmos-sdk/types" + + testkeeper "github.com/allinbits/vaas/testutil/keeper" + providertypes "github.com/allinbits/vaas/x/vaas/provider/types" +) + +func TestConsumerLivenessState(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + const cid = uint64(0) + + // lastAck defaults to BlockTime when absent (defensive, not a migration crutch). + require.Equal(t, ctx.BlockTime(), k.GetConsumerLastAckTime(ctx, cid)) + + now := ctx.BlockTime().Add(time.Hour) + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, now)) + require.Equal(t, now.UTC(), k.GetConsumerLastAckTime(ctx, cid).UTC()) + + // counters default to 0. + require.Equal(t, uint64(0), k.GetConsumerHighestSentVscId(ctx, cid)) + require.Equal(t, uint64(0), k.GetConsumerHighestAckedVscId(ctx, cid)) + k.SetConsumerHighestSentVscId(ctx, cid, 5) + k.SetConsumerHighestAckedVscId(ctx, cid, 3) + require.Equal(t, uint64(5), k.GetConsumerHighestSentVscId(ctx, cid)) + require.Equal(t, uint64(3), k.GetConsumerHighestAckedVscId(ctx, cid)) + + k.DeleteConsumerLastAckTime(ctx, cid) + k.DeleteConsumerHighestSentVscId(ctx, cid) + k.DeleteConsumerHighestAckedVscId(ctx, cid) + require.Equal(t, ctx.BlockTime(), k.GetConsumerLastAckTime(ctx, cid)) + require.Equal(t, uint64(0), k.GetConsumerHighestSentVscId(ctx, cid)) +} + +func TestOnAckRecordsLiveness(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + const cid = uint64(0) + clientId := "07-tendermint-0" + k.SetConsumerClientId(ctx, cid, clientId) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + + future := ctx.BlockTime().Add(2 * time.Hour) + ctx = ctx.WithBlockTime(future) + + require.NoError(t, k.OnAcknowledgementPacketV2(ctx, clientId, 9, "")) + require.Equal(t, future.UTC(), k.GetConsumerLastAckTime(ctx, cid).UTC()) + require.Equal(t, uint64(9), k.GetConsumerHighestAckedVscId(ctx, cid)) +} + +func TestLivenessGracePeriod(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + unbonding := 21 * 24 * time.Hour + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).AnyTimes() + + // Default fraction (0.66) from the params seeded by the test helper. + grace, err := k.LivenessGracePeriod(ctx) + require.NoError(t, err) + require.Greater(t, grace, time.Duration(0)) + require.Less(t, grace, unbonding) // safety invariant + require.Equal(t, time.Duration(float64(unbonding)*0.66), grace) + + // The grace tracks the LivenessGraceFraction param: lowering it shortens + // the grace proportionally (this is what lets e2e/test chains sweep fast). + params := k.GetParams(ctx) + params.LivenessGraceFraction = "0.1" + k.SetParams(ctx, params) + + grace, err = k.LivenessGracePeriod(ctx) + require.NoError(t, err) + require.Equal(t, time.Duration(float64(unbonding)*0.1), grace) +} + +func TestSweepRemovesStaleConsumer(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + k.SetConsumerChainId(ctx, cid, "consumer-1") + + unbonding := 21 * 24 * time.Hour + // LivenessGracePeriod + StopAndPrepareForConsumerRemoval both read UnbondingTime. + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).AnyTimes() + + // Last ack far in the past -> beyond grace. + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime().Add(-30*24*time.Hour))) + + require.NoError(t, k.SweepUnresponsiveConsumers(ctx)) + require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, k.GetConsumerPhase(ctx, cid)) + + // The sweep does not just stop the consumer, it schedules its deletion at + // blockTime + unbonding (the same removal path a gov removal takes). The + // e2e suite asserts the LAUNCHED -> STOPPED edge end-to-end and relies on + // this assertion for the STOPPED -> DELETED scheduling. + removalTime, err := k.GetConsumerRemovalTime(ctx, cid) + require.NoError(t, err) + require.Equal(t, ctx.BlockTime().Add(unbonding), removalTime) +} + +func TestSweepSparesLiveConsumer(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).AnyTimes() + + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime())) // fresh + require.NoError(t, k.SweepUnresponsiveConsumers(ctx)) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cid)) +} + +func TestTimeoutDoesNotRemove(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + const cid = uint64(0) + clientId := "07-tendermint-0" + k.SetConsumerClientId(ctx, cid, clientId) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + + require.NoError(t, k.OnTimeoutPacketV2(ctx, clientId)) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cid)) +} + +func TestErrorAckDoesNotRemove(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + const cid = uint64(0) + clientId := "07-tendermint-0" + k.SetConsumerClientId(ctx, cid, clientId) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + + require.NoError(t, k.OnAcknowledgementPacketV2(ctx, clientId, 1, "some error")) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cid)) +} + +func TestDeleteConsumerChainClearsLivenessState(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).AnyTimes() + + const cid = uint64(0) + poolAddr := k.GetConsumerFeePoolAddress(cid) + k.SetConsumerClientId(ctx, cid, "07-tendermint-0") + require.NoError(t, k.FeePoolAddressToConsumerId.Set(ctx, poolAddr, cid)) + mocks.MockBankKeeper.EXPECT().GetAllBalances(ctx, poolAddr).Return(sdk.NewCoins()) + + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_STOPPED) + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime())) + k.SetConsumerHighestSentVscId(ctx, cid, 4) + k.SetConsumerHighestAckedVscId(ctx, cid, 4) + + // confirm keys are present before deletion + hasAck, err := k.ConsumerLastAckTime.Has(ctx, cid) + require.NoError(t, err) + require.True(t, hasAck) + hasSent, err := k.ConsumerHighestSentVscId.Has(ctx, cid) + require.NoError(t, err) + require.True(t, hasSent) + hasAcked, err := k.ConsumerHighestAckedVscId.Has(ctx, cid) + require.NoError(t, err) + require.True(t, hasAcked) + + require.NoError(t, k.DeleteConsumerChain(ctx, cid)) + + // assert underlying keys are gone, not just that the getter returns a default + hasAck, err = k.ConsumerLastAckTime.Has(ctx, cid) + require.NoError(t, err) + require.False(t, hasAck) + hasSent, err = k.ConsumerHighestSentVscId.Has(ctx, cid) + require.NoError(t, err) + require.False(t, hasSent) + hasAcked, err = k.ConsumerHighestAckedVscId.Has(ctx, cid) + require.NoError(t, err) + require.False(t, hasAcked) + + // getters return their absent-defaults after deletion + require.Equal(t, ctx.BlockTime(), k.GetConsumerLastAckTime(ctx, cid)) + require.Equal(t, uint64(0), k.GetConsumerHighestSentVscId(ctx, cid)) + require.Equal(t, uint64(0), k.GetConsumerHighestAckedVscId(ctx, cid)) +} + +// TestSweepBoundaryExact checks the boundary condition: lastAck exactly at the +// grace boundary is spared; one nanosecond earlier is stopped. +func TestSweepBoundaryExact(t *testing.T) { + unbonding := 21 * 24 * time.Hour + + t.Run("exactly at grace boundary - spared", func(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).AnyTimes() + grace, err := k.LivenessGracePeriod(ctx) + require.NoError(t, err) + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + + // lastAck == blockTime - grace: elapsed == grace, check is <= grace, so spared. + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime().Add(-grace))) + require.NoError(t, k.SweepUnresponsiveConsumers(ctx)) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cid)) + }) + + t.Run("one ns past grace - stopped", func(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).AnyTimes() + grace, err := k.LivenessGracePeriod(ctx) + require.NoError(t, err) + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + k.SetConsumerChainId(ctx, cid, "consumer-boundary") + + // lastAck == blockTime - grace - 1ns: elapsed > grace, should be stopped. + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime().Add(-grace-time.Nanosecond))) + require.NoError(t, k.SweepUnresponsiveConsumers(ctx)) + require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, k.GetConsumerPhase(ctx, cid)) + }) +} + +// TestSweepMultipleConsumers_PartialRemoval ensures only the stale consumer is +// stopped when three consumers are present: stale, fresh, and at-grace-boundary. +func TestSweepMultipleConsumers_PartialRemoval(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + unbonding := 21 * 24 * time.Hour + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).AnyTimes() + + grace, err := k.LivenessGracePeriod(ctx) + require.NoError(t, err) + + // stale: elapsed >> grace. + cidStale := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cidStale, providertypes.CONSUMER_PHASE_LAUNCHED) + k.SetConsumerChainId(ctx, cidStale, "consumer-stale") + require.NoError(t, k.SetConsumerLastAckTime(ctx, cidStale, ctx.BlockTime().Add(-30*24*time.Hour))) + + // fresh: lastAck == blockTime (elapsed = 0). + cidFresh := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cidFresh, providertypes.CONSUMER_PHASE_LAUNCHED) + require.NoError(t, k.SetConsumerLastAckTime(ctx, cidFresh, ctx.BlockTime())) + + // at boundary: elapsed == grace -> spared (check is <= grace). + cidBoundary := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cidBoundary, providertypes.CONSUMER_PHASE_LAUNCHED) + require.NoError(t, k.SetConsumerLastAckTime(ctx, cidBoundary, ctx.BlockTime().Add(-grace))) + + require.NoError(t, k.SweepUnresponsiveConsumers(ctx)) + + require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, k.GetConsumerPhase(ctx, cidStale)) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cidFresh)) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cidBoundary)) + + // The sweep is per-consumer: only the stale one is scheduled for removal; + // the spared consumers are left entirely untouched (no removal time). + staleRemoval, err := k.GetConsumerRemovalTime(ctx, cidStale) + require.NoError(t, err) + require.Equal(t, ctx.BlockTime().Add(unbonding), staleRemoval) + _, err = k.GetConsumerRemovalTime(ctx, cidFresh) + require.Error(t, err) + _, err = k.GetConsumerRemovalTime(ctx, cidBoundary) + require.Error(t, err) +} + +// TestSweepStrandOnStopErrorPrevented verifies that a per-consumer stop failure +// during the sweep does not strand the consumer STOPPED-with-no-removal. The +// sweep runs StopAndPrepareForConsumerRemoval in a cached context and only +// commits on success, so an error (here: UnbondingTime failing when the stop +// reads it, after the phase write in the cache) discards everything and leaves +// the consumer LAUNCHED for the next sweep to retry. +func TestSweepStrandOnStopErrorPrevented(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + // LivenessGracePeriod (top of the sweep) reads UnbondingTime once and must + // succeed; StopAndPrepareForConsumerRemoval reads it again and here fails. + gomock.InOrder( + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil), + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(time.Duration(0), errors.New("boom")), + ) + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + k.SetConsumerChainId(ctx, cid, "consumer-x") + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime().Add(-30*24*time.Hour))) + + // The sweep logs the per-consumer error and returns nil (it does not abort). + require.NoError(t, k.SweepUnresponsiveConsumers(ctx)) + + // Nothing was committed: the consumer is still LAUNCHED (not stranded) and + // has no scheduled removal. + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cid)) + _, err := k.GetConsumerRemovalTime(ctx, cid) + require.Error(t, err) +} + +// TestSweepRecoversAfterAck confirms a consumer that receives a fresh ack just +// inside the grace window survives a subsequent sweep once more time elapses. +func TestSweepRecoversAfterAck(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + unbonding := 21 * 24 * time.Hour + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(unbonding, nil).AnyTimes() + + grace, err := k.LivenessGracePeriod(ctx) + require.NoError(t, err) + + cid := k.FetchAndIncrementConsumerId(ctx) + clientId := "07-tendermint-recover" + k.SetConsumerClientId(ctx, cid, clientId) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + + // Set lastAck to just inside the grace window (1ns before expiry). + require.NoError(t, k.SetConsumerLastAckTime(ctx, cid, ctx.BlockTime().Add(-grace+time.Nanosecond))) + + // Simulate ack arriving at current block time, refreshing the liveness clock. + require.NoError(t, k.OnAcknowledgementPacketV2(ctx, clientId, 1, "")) + + // Advance block time by exactly grace; elapsed == grace which satisfies <= grace, so spared. + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(grace)) + + require.NoError(t, k.SweepUnresponsiveConsumers(ctx)) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, k.GetConsumerPhase(ctx, cid)) +} diff --git a/x/vaas/provider/keeper/msg_server.go b/x/vaas/provider/keeper/msg_server.go index e92f9ff..38c308b 100644 --- a/x/vaas/provider/keeper/msg_server.go +++ b/x/vaas/provider/keeper/msg_server.go @@ -218,6 +218,42 @@ func (k msgServer) SubmitConsumerDoubleVoting(goCtx context.Context, msg *types. return &types.MsgSubmitConsumerDoubleVotingResponse{}, nil } +// validateConsumerInitParams enforces the two bounds on a consumer's +// initialization parameters that can only be checked against provider state +// (positivity and format are already checked by ValidateInitializationParameters). +// Both are cross-chain settings the provider holds at create/update time, so it +// can validate them here against its own state -- it reads its unbonding once +// and derives the grace from it: +// +// - UnbondingPeriod must not exceed the provider's unbonding, so the consumer +// cannot outlive the provider's slashable window. No lower floor is imposed: +// an unbonding short enough to make the relayer-derived trusting period +// impractical is an operator concern (and useful for test/dev chains), not +// a protocol minimum. +// - SafeModeThreshold must be strictly shorter than the provider's liveness +// grace (unbonding * LivenessGraceFraction), so a lagging consumer enters +// safe mode -- and stops value-bearing work under a possibly-stale set -- +// before the provider's sweep removes it. +func (k Keeper) validateConsumerInitParams(ctx sdk.Context, p types.ConsumerInitializationParameters) error { + providerUnbonding, err := k.stakingKeeper.UnbondingTime(ctx) + if err != nil { + return err + } + if p.UnbondingPeriod > providerUnbonding { + return errorsmod.Wrapf(types.ErrInvalidConsumerInitializationParameters, + "unbonding period %s must not exceed provider unbonding %s", p.UnbondingPeriod, providerUnbonding) + } + grace, err := vaastypes.CalculateTrustPeriod(providerUnbonding, k.GetParams(ctx).LivenessGraceFraction) + if err != nil { + return err + } + if p.SafeModeThreshold >= grace { + return errorsmod.Wrapf(types.ErrInvalidConsumerInitializationParameters, + "safe mode threshold %s must be less than the provider liveness grace period %s", p.SafeModeThreshold, grace) + } + return nil +} + // CreateConsumer creates a consumer chain func (k msgServer) CreateConsumer(goCtx context.Context, msg *types.MsgCreateConsumer) (*types.MsgCreateConsumerResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) @@ -261,6 +297,9 @@ func (k msgServer) CreateConsumer(goCtx context.Context, msg *types.MsgCreateCon if msg.InitializationParameters != nil { initializationParameters = *msg.InitializationParameters } + if err := k.Keeper.validateConsumerInitParams(ctx, initializationParameters); err != nil { + return &resp, err + } if err := k.Keeper.SetConsumerInitializationParameters(ctx, consumerId, initializationParameters); err != nil { return &resp, errorsmod.Wrapf(types.ErrInvalidConsumerInitializationParameters, "cannot set consumer initialization parameters: %s", err.Error()) @@ -457,6 +496,9 @@ func (k msgServer) UpdateConsumer(goCtx context.Context, msg *types.MsgUpdateCon sdk.NewAttribute(types.AttributeConsumerGenesisHash, string(msg.InitializationParameters.GenesisHash))) } + if err = k.Keeper.validateConsumerInitParams(ctx, *msg.InitializationParameters); err != nil { + return &resp, err + } if err = k.Keeper.SetConsumerInitializationParameters(ctx, msg.ConsumerId, *msg.InitializationParameters); err != nil { return &resp, errorsmod.Wrapf(types.ErrInvalidConsumerInitializationParameters, "cannot set consumer initialization parameters: %s", err.Error()) diff --git a/x/vaas/provider/keeper/msg_server_test.go b/x/vaas/provider/keeper/msg_server_test.go index 82b3ec5..d40645c 100644 --- a/x/vaas/provider/keeper/msg_server_test.go +++ b/x/vaas/provider/keeper/msg_server_test.go @@ -29,6 +29,7 @@ import ( testkeeper "github.com/allinbits/vaas/testutil/keeper" providerkeeper "github.com/allinbits/vaas/x/vaas/provider/keeper" providertypes "github.com/allinbits/vaas/x/vaas/provider/types" + vaastypes "github.com/allinbits/vaas/x/vaas/types" ) var keeperBech32CfgOnce sync.Once @@ -49,9 +50,11 @@ func validSubmitter() string { } func TestCreateConsumer(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).AnyTimes() + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) consumerMetadata := providertypes.ConsumerMetadata{ @@ -61,7 +64,9 @@ func TestCreateConsumer(t *testing.T) { response, err := msgServer.CreateConsumer(ctx, &providertypes.MsgCreateConsumer{ Submitter: "submitter", ChainId: "chainId", Metadata: consumerMetadata, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.NoError(t, err) require.Equal(t, uint64(0), response.ConsumerId) @@ -82,7 +87,9 @@ func TestCreateConsumer(t *testing.T) { response, err = msgServer.CreateConsumer(ctx, &providertypes.MsgCreateConsumer{ Submitter: "submitter2", ChainId: "chainId2", Metadata: consumerMetadata, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.NoError(t, err) // assert that the consumer id is different from the previously registered chain @@ -98,9 +105,12 @@ func TestCreateConsumer(t *testing.T) { } func TestCreateConsumerDuplicateChainId(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + // only the first CreateConsumer reaches validateConsumerUnbonding; the duplicate is rejected earlier + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(1) + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) consumerMetadata := providertypes.ConsumerMetadata{ @@ -112,16 +122,20 @@ func TestCreateConsumerDuplicateChainId(t *testing.T) { response, err := msgServer.CreateConsumer(ctx, &providertypes.MsgCreateConsumer{ Submitter: "submitter1", ChainId: "duplicateChainId", Metadata: consumerMetadata, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.NoError(t, err) require.Equal(t, uint64(0), response.ConsumerId) - // Attempt to register another consumer with the same chainId + // Attempt to register another consumer with the same chainId — rejected before unbonding check _, err = msgServer.CreateConsumer(ctx, &providertypes.MsgCreateConsumer{ Submitter: "submitter2", ChainId: "duplicateChainId", Metadata: consumerMetadata, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.Error(t, err) require.ErrorIs(t, err, providertypes.ErrDuplicateChainId) @@ -131,6 +145,8 @@ func TestUpdateConsumer(t *testing.T) { providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(1) + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) // try to update a non-existing consumer @@ -200,9 +216,11 @@ func TestUpdateConsumer(t *testing.T) { } func TestUpdateConsumerDuplicateChainId(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(2) + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) // create a chain that register chainId-1 @@ -638,6 +656,9 @@ func TestRemoveConsumerGovAuth(t *testing.T) { msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) + // CreateConsumer calls validateConsumerUnbonding; RemoveConsumer also calls UnbondingTime + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(2) + // create a consumer chain and set it to LAUNCHED createResp, err := msgServer.CreateConsumer(ctx, &providertypes.MsgCreateConsumer{ @@ -646,15 +667,15 @@ func TestRemoveConsumerGovAuth(t *testing.T) { Name: "name", Description: "description", }, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.NoError(t, err) consumerId := createResp.ConsumerId providerKeeper.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) - mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(time.Hour*24*7*3, nil).Times(1) - // non-authority should be rejected _, err = msgServer.RemoveConsumer(ctx, &providertypes.MsgRemoveConsumer{ @@ -678,9 +699,11 @@ func TestRemoveConsumerGovAuth(t *testing.T) { } func TestRemoveConsumerNonLaunchedRejected(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(1) + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) // create a consumer chain (stays in REGISTERED phase) @@ -691,7 +714,9 @@ func TestRemoveConsumerNonLaunchedRejected(t *testing.T) { Name: "name", Description: "description", }, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.NoError(t, err) @@ -709,6 +734,8 @@ func TestUpdateConsumerLaunchedOnlyMetadata(t *testing.T) { providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(1) + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) // create a consumer chain @@ -719,7 +746,9 @@ func TestUpdateConsumerLaunchedOnlyMetadata(t *testing.T) { Name: "name", Description: "description", }, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.NoError(t, err) consumerId := createResp.ConsumerId @@ -786,6 +815,8 @@ func TestUpdateConsumerPreLaunchAllowsAll(t *testing.T) { providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(1) + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) // create a consumer chain (REGISTERED phase) @@ -828,9 +859,11 @@ func TestUpdateConsumerPreLaunchAllowsAll(t *testing.T) { } func TestCreateConsumerEventsIncludeInitParams(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(1) + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) binaryHash := []byte("deadbeef") @@ -846,9 +879,10 @@ func TestCreateConsumerEventsIncludeInitParams(t *testing.T) { Description: "description", }, InitializationParameters: &providertypes.ConsumerInitializationParameters{ - BinaryHash: binaryHash, - GenesisHash: genesisHash, - SpawnTime: spawnTime, + BinaryHash: binaryHash, + GenesisHash: genesisHash, + SpawnTime: spawnTime, + UnbondingPeriod: 21 * 24 * time.Hour, }, }) require.NoError(t, err) @@ -880,14 +914,19 @@ func TestCreateConsumerEventsIncludeInitParams(t *testing.T) { } func TestCreateConsumer_PopulatesReverseLookup(t *testing.T) { - k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).Times(1) + ms := providerkeeper.NewMsgServerImpl(&k) resp, err := ms.CreateConsumer(ctx, &providertypes.MsgCreateConsumer{ Submitter: "submitter", ChainId: "chainId", - Metadata: providertypes.ConsumerMetadata{Name: "n", Description: "d"}, - InitializationParameters: &providertypes.ConsumerInitializationParameters{}, + Metadata: providertypes.ConsumerMetadata{Name: "n", Description: "d"}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, }) require.NoError(t, err) @@ -1270,6 +1309,120 @@ func TestWithdrawConsumerFeePool(t *testing.T) { } } +func TestCreateConsumerUnbondingBounds(t *testing.T) { + providerUnbonding := 21 * 24 * time.Hour + + tests := []struct { + name string + unbondingPeriod time.Duration + wantErr bool + }{ + { + name: "too long: exceeds provider unbonding", + unbondingPeriod: 30 * 24 * time.Hour, + wantErr: true, + }, + { + name: "valid: equal to provider unbonding", + unbondingPeriod: 21 * 24 * time.Hour, + wantErr: false, + }, + { + name: "short value accepted (no lower floor)", + unbondingPeriod: 1 * 24 * time.Hour, + wantErr: false, + }, + { + name: "very short value accepted (no lower floor)", + unbondingPeriod: time.Hour, + wantErr: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) + + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(providerUnbonding, nil).Times(1) + + _, err := msgServer.CreateConsumer(ctx, + &providertypes.MsgCreateConsumer{ + Submitter: "submitter", + ChainId: "chainId", + Metadata: providertypes.ConsumerMetadata{ + Name: "name", + Description: "description", + }, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: tc.unbondingPeriod, + }, + }) + if tc.wantErr { + require.Error(t, err) + require.ErrorIs(t, err, providertypes.ErrInvalidConsumerInitializationParameters) + } else { + require.NoError(t, err) + } + }) + } +} + +// TestCreateConsumerSafeModeThresholdBounds checks that CreateConsumer rejects a +// safe-mode threshold that is not strictly below the provider's liveness grace +// (grace = providerUnbonding * LivenessGraceFraction), so a lagging consumer +// enters safe mode before the provider sweep removes it. +func TestCreateConsumerSafeModeThresholdBounds(t *testing.T) { + providerUnbonding := 21 * 24 * time.Hour + // Grace uses the default fraction seeded into params by the test helper. + grace, err := vaastypes.CalculateTrustPeriod(providerUnbonding, providertypes.DefaultLivenessGraceFraction) + require.NoError(t, err) + + tests := []struct { + name string + threshold time.Duration + wantErr bool + }{ + {name: "well below grace", threshold: time.Hour, wantErr: false}, + {name: "just below grace", threshold: grace - time.Second, wantErr: false}, + {name: "equal to grace (rejected)", threshold: grace, wantErr: true}, + {name: "above grace (rejected)", threshold: grace + time.Hour, wantErr: true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) + + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(providerUnbonding, nil).Times(1) + + _, err := msgServer.CreateConsumer(ctx, + &providertypes.MsgCreateConsumer{ + Submitter: "submitter", + ChainId: "chainId", + Metadata: providertypes.ConsumerMetadata{ + Name: "name", + Description: "description", + }, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: providerUnbonding, + SafeModeThreshold: tc.threshold, + }, + }) + if tc.wantErr { + require.Error(t, err) + require.ErrorIs(t, err, providertypes.ErrInvalidConsumerInitializationParameters) + } else { + require.NoError(t, err) + } + }) + } +} + func TestSweepConsumerFeePool(t *testing.T) { owner := sdk.AccAddress([]byte("owner___________")) alice := sdk.AccAddress([]byte("alice___________")) @@ -1372,3 +1525,78 @@ func TestSweepConsumerFeePool(t *testing.T) { }) } } + +// TestUpdateConsumerUnbondingBounds mirrors TestCreateConsumerUnbondingBounds +// but exercises the UpdateConsumer path for a pre-launch (REGISTERED) consumer. +func TestUpdateConsumerUnbondingBounds(t *testing.T) { + providerUnbonding := 21 * 24 * time.Hour + + tests := []struct { + name string + unbondingPeriod time.Duration + wantErr bool + }{ + { + name: "above provider unbonding: rejected", + unbondingPeriod: 30 * 24 * time.Hour, + wantErr: true, + }, + { + name: "short value accepted (no lower floor)", + unbondingPeriod: 1 * 24 * time.Hour, + wantErr: false, + }, + { + name: "very short value accepted (no lower floor)", + unbondingPeriod: time.Hour, + wantErr: false, + }, + { + name: "exactly provider unbonding: ok", + unbondingPeriod: providerUnbonding, + wantErr: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + // CreateConsumer calls validateConsumerUnbonding once; UpdateConsumer + // calls it once more when InitializationParameters is non-nil. + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(providerUnbonding, nil).Times(2) + mocks.MockAccountKeeper.EXPECT().AddressCodec().Return(address.NewBech32Codec("cosmos")).AnyTimes() + + msgServer := providerkeeper.NewMsgServerImpl(&providerKeeper) + + // Register a consumer with a valid unbonding period. + createResp, err := msgServer.CreateConsumer(ctx, &providertypes.MsgCreateConsumer{ + Submitter: "submitter", + ChainId: "chainId", + Metadata: providertypes.ConsumerMetadata{Name: "n", Description: "d"}, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: 21 * 24 * time.Hour, + }, + }) + require.NoError(t, err) + consumerId := createResp.ConsumerId + + // Consumer stays in REGISTERED (pre-launch), so InitializationParameters + // updates are allowed. Override only UnbondingPeriod. + _, err = msgServer.UpdateConsumer(ctx, &providertypes.MsgUpdateConsumer{ + Owner: "submitter", + ConsumerId: consumerId, + InitializationParameters: &providertypes.ConsumerInitializationParameters{ + UnbondingPeriod: tc.unbondingPeriod, + }, + }) + if tc.wantErr { + require.Error(t, err) + require.ErrorIs(t, err, providertypes.ErrInvalidConsumerInitializationParameters) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/x/vaas/provider/keeper/params.go b/x/vaas/provider/keeper/params.go index a5b7a9f..cd416ea 100644 --- a/x/vaas/provider/keeper/params.go +++ b/x/vaas/provider/keeper/params.go @@ -7,6 +7,7 @@ import ( "time" "github.com/allinbits/vaas/x/vaas/provider/types" + vaastypes "github.com/allinbits/vaas/x/vaas/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" commitmenttypes "github.com/cosmos/ibc-go/v10/modules/core/23-commitment/types" @@ -158,3 +159,15 @@ func (k Keeper) SetInfractionParams(ctx context.Context, params types.Infraction panic(fmt.Sprintf("error setting infraction parameters: %v", err)) } } + +// LivenessGracePeriod returns the maximum time a launched consumer may go +// without a successful VSC ack before it is removed, derived from the provider +// unbonding period and the LivenessGraceFraction param so it stays inside the +// slashable window. +func (k Keeper) LivenessGracePeriod(ctx context.Context) (time.Duration, error) { + unbonding, err := k.stakingKeeper.UnbondingTime(ctx) + if err != nil { + return 0, err + } + return vaastypes.CalculateTrustPeriod(unbonding, k.GetParams(ctx).LivenessGraceFraction) +} diff --git a/x/vaas/provider/keeper/params_test.go b/x/vaas/provider/keeper/params_test.go index f36974d..6feafd3 100644 --- a/x/vaas/provider/keeper/params_test.go +++ b/x/vaas/provider/keeper/params_test.go @@ -27,6 +27,7 @@ func TestParams(t *testing.T) { newParams := providertypes.NewParams( "0.25", + "0.5", 7*24*time.Hour, 600, math.NewInt(50), diff --git a/x/vaas/provider/keeper/relay.go b/x/vaas/provider/keeper/relay.go index b57b5b7..fbc117e 100644 --- a/x/vaas/provider/keeper/relay.go +++ b/x/vaas/provider/keeper/relay.go @@ -19,17 +19,23 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -func (k Keeper) OnAcknowledgementPacketV2(ctx sdk.Context, sourceClientID string, ackError string) error { +func (k Keeper) OnAcknowledgementPacketV2(ctx sdk.Context, sourceClientID string, ackVscId uint64, ackError string) error { + consumerId, found := k.GetClientIdToConsumerId(ctx, sourceClientID) + if !found { + return errorsmod.Wrapf(providertypes.ErrInvalidConsumerClient, "recv acknowledgement on unknown client %s", sourceClientID) + } + if ackError != "" { - k.Logger(ctx).Error( - "recv ErrorAcknowledgement", - "clientID", sourceClientID, - "error", ackError, - ) - if consumerId, found := k.GetClientIdToConsumerId(ctx, sourceClientID); found { - return k.StopAndPrepareForConsumerRemoval(ctx, consumerId) - } - return errorsmod.Wrapf(providertypes.ErrInvalidConsumerClient, "recv ErrorAcknowledgement on unknown client %s", sourceClientID) + k.Logger(ctx).Error("recv ErrorAcknowledgement, retrying next epoch; liveness sweep owns removal", + "clientID", sourceClientID, "consumerId", consumerId, "error", ackError) + return nil + } + + if err := k.SetConsumerLastAckTime(ctx, consumerId, ctx.BlockTime()); err != nil { + return err + } + if ackVscId > k.GetConsumerHighestAckedVscId(ctx, consumerId) { + k.SetConsumerHighestAckedVscId(ctx, consumerId, ackVscId) } return nil } @@ -38,13 +44,11 @@ func (k Keeper) OnTimeoutPacketV2(ctx sdk.Context, sourceClientID string) error consumerId, found := k.GetClientIdToConsumerId(ctx, sourceClientID) if !found { k.Logger(ctx).Error("packet timeout, unknown client:", "clientID", sourceClientID) - return errorsmod.Wrapf( - providertypes.ErrInvalidConsumerClient, - "timeout on unknown client %s", sourceClientID, - ) + return errorsmod.Wrapf(providertypes.ErrInvalidConsumerClient, "timeout on unknown client %s", sourceClientID) } - k.Logger(ctx).Info("packet timeout, deleting the consumer:", "consumerId", consumerId, "clientId", sourceClientID) - return k.StopAndPrepareForConsumerRemoval(ctx, consumerId) + k.Logger(ctx).Info("packet timeout, retrying next epoch; liveness sweep owns removal", + "consumerId", consumerId, "clientId", sourceClientID) + return nil } // EndBlockVSU contains the EndBlock logic needed for @@ -231,17 +235,8 @@ func (k Keeper) SendVSCPacketsToChain(ctx sdk.Context, consumerId uint64, client return nil } - k.Logger(ctx).Error("cannot send VSC, removing consumer:", - "consumerId", consumerId, - "clientId", clientId, - "vscid", data.ValsetUpdateId, - "err", err.Error(), - ) - - err := k.StopAndPrepareForConsumerRemoval(ctx, consumerId) - if err != nil { - k.Logger(ctx).Info("consumer chain failed to stop:", "consumerId", consumerId, "error", err.Error()) - } + k.Logger(ctx).Error("cannot send VSC, leaving packet data stored; liveness sweep owns removal", + "consumerId", consumerId, "clientId", clientId, "vscid", data.ValsetUpdateId, "err", err.Error()) return nil } @@ -251,6 +246,9 @@ func (k Keeper) SendVSCPacketsToChain(ctx sdk.Context, consumerId uint64, client "vscid", data.ValsetUpdateId, "sequence", resp.Sequence, ) + if data.ValsetUpdateId > k.GetConsumerHighestSentVscId(ctx, consumerId) { + k.SetConsumerHighestSentVscId(ctx, consumerId, data.ValsetUpdateId) + } } k.DeletePendingVSCPackets(ctx, consumerId) @@ -275,8 +273,11 @@ func (k Keeper) QueueVSCPackets(ctx sdk.Context) error { return fmt.Errorf("getting consumer current validator set, consumerId(%d): %w", consumerId, err) } - // compute consumer next validator set (all validators validate all consumers) - valUpdates, err := k.ComputeConsumerNextValSet(ctx, bondedValidators, consumerId, currentValSet) + // Send a full snapshot when the consumer is behind (i.e. it has + // unacknowledged packets), otherwise send a diff. + isSnapshot := k.GetConsumerHighestAckedVscId(ctx, consumerId) < k.GetConsumerHighestSentVscId(ctx, consumerId) + + valUpdates, err := k.ComputeConsumerNextValSet(ctx, bondedValidators, consumerId, currentValSet, isSnapshot) if err != nil { return fmt.Errorf("computing consumer next validator set, consumerId(%d): %w", consumerId, err) } @@ -290,6 +291,7 @@ func (k Keeper) QueueVSCPackets(ctx sdk.Context) error { // bounded: at most one packet per consumer per epoch. packet := vaastypes.NewValidatorSetChangePacketData(valUpdates, valUpdateID) packet.ConsumerInDebt = k.IsConsumerInDebt(ctx, consumerId) + packet.IsSnapshot = isSnapshot k.AppendPendingVSCPackets(ctx, consumerId, packet) k.Logger(ctx).Info("VSCPacket enqueued:", "consumerId", consumerId, diff --git a/x/vaas/provider/keeper/relay_test.go b/x/vaas/provider/keeper/relay_test.go index d76275a..41e112d 100644 --- a/x/vaas/provider/keeper/relay_test.go +++ b/x/vaas/provider/keeper/relay_test.go @@ -1,8 +1,8 @@ package keeper_test import ( + "fmt" "testing" - "time" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -10,6 +10,7 @@ import ( abci "github.com/cometbft/cometbft/abci/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" testkeeper "github.com/allinbits/vaas/testutil/keeper" providertypes "github.com/allinbits/vaas/x/vaas/provider/types" @@ -18,7 +19,7 @@ import ( // TestOnAcknowledgementPacketV2 tests the IBC v2 acknowledgement handler. func TestOnAcknowledgementPacketV2(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() consumerId := uint64(0) @@ -30,24 +31,20 @@ func TestOnAcknowledgementPacketV2(t *testing.T) { providerKeeper.SetConsumerChainId(ctx, consumerId, "consumer-chain") // Test 1: Success acknowledgement (empty error) - no action needed - err := providerKeeper.OnAcknowledgementPacketV2(ctx, clientId, "") + err := providerKeeper.OnAcknowledgementPacketV2(ctx, clientId, 1, "") require.NoError(t, err) // Consumer should still be launched phase := providerKeeper.GetConsumerPhase(ctx, consumerId) require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) - // Setup mock expectation for StopAndPrepareForConsumerRemoval - // which calls UnbondingTime - mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(time.Hour*24*21, nil).Times(1) - - // Test 2: Error acknowledgement - should trigger consumer removal - err = providerKeeper.OnAcknowledgementPacketV2(ctx, clientId, "packet data could not be decoded") + // Test 2: Error acknowledgement - liveness sweep owns removal, consumer stays launched + err = providerKeeper.OnAcknowledgementPacketV2(ctx, clientId, 1, "packet data could not be decoded") require.NoError(t, err) - // Consumer should now be stopped + // Consumer stays launched; liveness sweep will handle removal phase = providerKeeper.GetConsumerPhase(ctx, consumerId) - require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, phase) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) } // TestOnAcknowledgementPacketV2UnknownClient tests error handling for unknown clients. @@ -58,14 +55,14 @@ func TestOnAcknowledgementPacketV2UnknownClient(t *testing.T) { unknownClientId := "07-tendermint-999" // Error ack with unknown client should return error - err := providerKeeper.OnAcknowledgementPacketV2(ctx, unknownClientId, "some error") + err := providerKeeper.OnAcknowledgementPacketV2(ctx, unknownClientId, 0, "some error") require.Error(t, err) require.Contains(t, err.Error(), "unknown client") } // TestOnTimeoutPacketV2 tests the IBC v2 timeout handler. func TestOnTimeoutPacketV2(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() consumerId := uint64(0) @@ -80,16 +77,13 @@ func TestOnTimeoutPacketV2(t *testing.T) { phase := providerKeeper.GetConsumerPhase(ctx, consumerId) require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) - // Setup mock expectation for StopAndPrepareForConsumerRemoval - mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(time.Hour*24*21, nil).Times(1) - - // Timeout should trigger consumer removal + // Timeout is log-only; liveness sweep owns removal err := providerKeeper.OnTimeoutPacketV2(ctx, clientId) require.NoError(t, err) - // Consumer should now be stopped + // Consumer stays launched; liveness sweep will handle removal phase = providerKeeper.GetConsumerPhase(ctx, consumerId) - require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, phase) + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) } // TestOnTimeoutPacketV2UnknownClient tests error handling for unknown clients. @@ -170,3 +164,120 @@ func TestSendVSCPacketsToChainNoHandler(t *testing.T) { pending := providerKeeper.GetPendingVSCPackets(ctx, consumerId) require.Len(t, pending, 1) } + +// TestOnAckHighestAckedDoesNotRegress asserts that a lower vscId arriving after +// a higher one does not regress the highestAcked counter. +func TestOnAckHighestAckedDoesNotRegress(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + consumerId := k.FetchAndIncrementConsumerId(ctx) + clientId := "07-tendermint-0" + k.SetConsumerClientId(ctx, consumerId, clientId) + k.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) + + // First ack: vscId 9. + require.NoError(t, k.OnAcknowledgementPacketV2(ctx, clientId, 9, "")) + require.Equal(t, uint64(9), k.GetConsumerHighestAckedVscId(ctx, consumerId)) + + // Out-of-order ack: vscId 5 must not regress the counter. + require.NoError(t, k.OnAcknowledgementPacketV2(ctx, clientId, 5, "")) + require.Equal(t, uint64(9), k.GetConsumerHighestAckedVscId(ctx, consumerId)) +} + +// TestHighestSentAdvancesOnlyOnSuccess verifies that highestSent and pending +// packets are unchanged when SendPacket returns a non-ErrClientNotActive error, +// and that they are updated correctly on success. +func TestHighestSentAdvancesOnlyOnSuccess(t *testing.T) { + consumerId := uint64(0) + clientId := "07-tendermint-0" + + t.Run("send error leaves state unchanged", func(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + k.SetConsumerClientId(ctx, consumerId, clientId) + k.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) + + k.AppendPendingVSCPackets(ctx, consumerId, vaastypes.ValidatorSetChangePacketData{ + ValidatorUpdates: []abci.ValidatorUpdate{}, + ValsetUpdateId: 3, + }) + k.AppendPendingVSCPackets(ctx, consumerId, vaastypes.ValidatorSetChangePacketData{ + ValidatorUpdates: []abci.ValidatorUpdate{}, + ValsetUpdateId: 5, + }) + + // Non-ErrClientNotActive error: function logs and returns nil, leaving pending intact. + mocks.MockChannelV2Keeper.EXPECT(). + SendPacket(gomock.Any(), gomock.Any()). + Return(nil, fmt.Errorf("some transient send error")) + + require.NoError(t, k.SendVSCPacketsToChain(ctx, consumerId, clientId)) + + require.Equal(t, uint64(0), k.GetConsumerHighestSentVscId(ctx, consumerId)) + require.Len(t, k.GetPendingVSCPackets(ctx, consumerId), 2) + }) + + t.Run("send success advances highestSent and clears pending", func(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + k.SetConsumerClientId(ctx, consumerId, clientId) + k.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) + + k.AppendPendingVSCPackets(ctx, consumerId, vaastypes.ValidatorSetChangePacketData{ + ValidatorUpdates: []abci.ValidatorUpdate{}, + ValsetUpdateId: 3, + }) + k.AppendPendingVSCPackets(ctx, consumerId, vaastypes.ValidatorSetChangePacketData{ + ValidatorUpdates: []abci.ValidatorUpdate{}, + ValsetUpdateId: 5, + }) + + // Both packets sent successfully. + mocks.MockChannelV2Keeper.EXPECT(). + SendPacket(gomock.Any(), gomock.Any()). + Return(&channeltypesv2.MsgSendPacketResponse{Sequence: 1}, nil).Times(1) + mocks.MockChannelV2Keeper.EXPECT(). + SendPacket(gomock.Any(), gomock.Any()). + Return(&channeltypesv2.MsgSendPacketResponse{Sequence: 2}, nil).Times(1) + + require.NoError(t, k.SendVSCPacketsToChain(ctx, consumerId, clientId)) + + require.Equal(t, uint64(5), k.GetConsumerHighestSentVscId(ctx, consumerId)) + require.Empty(t, k.GetPendingVSCPackets(ctx, consumerId)) + }) + + t.Run("partial send: first succeeds, second fails, advances to the sent id and leaves pending", func(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + k.SetConsumerClientId(ctx, consumerId, clientId) + k.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) + + k.AppendPendingVSCPackets(ctx, consumerId, vaastypes.ValidatorSetChangePacketData{ + ValidatorUpdates: []abci.ValidatorUpdate{}, + ValsetUpdateId: 3, + }) + k.AppendPendingVSCPackets(ctx, consumerId, vaastypes.ValidatorSetChangePacketData{ + ValidatorUpdates: []abci.ValidatorUpdate{}, + ValsetUpdateId: 5, + }) + + // First packet (vscId 3) sends and advances highestSent; the second + // (vscId 5) fails, so SendVSCPacketsToChain returns nil early without + // reaching DeletePendingVSCPackets -- all pending packets stay queued. + mocks.MockChannelV2Keeper.EXPECT(). + SendPacket(gomock.Any(), gomock.Any()). + Return(&channeltypesv2.MsgSendPacketResponse{Sequence: 1}, nil).Times(1) + mocks.MockChannelV2Keeper.EXPECT(). + SendPacket(gomock.Any(), gomock.Any()). + Return(nil, fmt.Errorf("some transient send error")).Times(1) + + require.NoError(t, k.SendVSCPacketsToChain(ctx, consumerId, clientId)) + + require.Equal(t, uint64(3), k.GetConsumerHighestSentVscId(ctx, consumerId)) + require.Len(t, k.GetPendingVSCPackets(ctx, consumerId), 2) + }) +} diff --git a/x/vaas/provider/keeper/validator_set_update.go b/x/vaas/provider/keeper/validator_set_update.go index 51a3f0b..8da2fc6 100644 --- a/x/vaas/provider/keeper/validator_set_update.go +++ b/x/vaas/provider/keeper/validator_set_update.go @@ -122,14 +122,28 @@ func (k Keeper) GetLastBondedValidators(ctx sdk.Context) ([]stakingtypes.Validat return vaastypes.GetLastBondedValidatorsUtil(ctx, k.stakingKeeper, maxVals) } +// FullValSetUpdates renders a complete validator set as absolute-power updates. +// Used for snapshot VSC packets: the consumer replaces its set with these, +// deriving removals against its own current set. +func FullValSetUpdates(validators []types.ConsensusValidator) []abci.ValidatorUpdate { + updates := make([]abci.ValidatorUpdate, 0, len(validators)) + for _, v := range validators { + updates = append(updates, abci.ValidatorUpdate{PubKey: *v.PublicKey, Power: v.Power}) + } + return updates +} + // ComputeConsumerNextValSet computes the consumer next validator set and returns // the validator updates to be sent to the consumer chain. // Every active provider validator validates every consumer. +// When isSnapshot is true, it returns the full set as absolute-power updates; +// otherwise it returns the diff against currentConsumerValSet. func (k Keeper) ComputeConsumerNextValSet( ctx sdk.Context, bondedValidators []stakingtypes.Validator, consumerId uint64, currentConsumerValSet []types.ConsensusValidator, + isSnapshot bool, ) ([]abci.ValidatorUpdate, error) { nextValidators, err := k.CreateConsumerValidators(ctx, consumerId, bondedValidators) if err != nil { @@ -137,14 +151,13 @@ func (k Keeper) ComputeConsumerNextValSet( fmt.Errorf("computing next validators, consumerId(%d): %w", consumerId, err) } - err = k.SetConsumerValSet(ctx, consumerId, nextValidators) - if err != nil { + if err = k.SetConsumerValSet(ctx, consumerId, nextValidators); err != nil { return []abci.ValidatorUpdate{}, fmt.Errorf("setting consumer validator set, consumerId(%d): %w", consumerId, err) } - // get the initial updates with the latest set consumer public keys - valUpdates := DiffValidators(currentConsumerValSet, nextValidators) - - return valUpdates, nil + if isSnapshot { + return FullValSetUpdates(nextValidators), nil + } + return DiffValidators(currentConsumerValSet, nextValidators), nil } diff --git a/x/vaas/provider/keeper/validator_set_update_test.go b/x/vaas/provider/keeper/validator_set_update_test.go new file mode 100644 index 0000000..8a5713e --- /dev/null +++ b/x/vaas/provider/keeper/validator_set_update_test.go @@ -0,0 +1,186 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + testcrypto "github.com/allinbits/vaas/testutil/crypto" + testkeeper "github.com/allinbits/vaas/testutil/keeper" + keeper "github.com/allinbits/vaas/x/vaas/provider/keeper" + providertypes "github.com/allinbits/vaas/x/vaas/provider/types" +) + +// makeConsensusValidator is a test helper that builds a ConsensusValidator +// from an ed25519 private key and a power value. +func makeConsensusValidator(t *testing.T, power int64) providertypes.ConsensusValidator { + t.Helper() + pk := ed25519.GenPrivKey().PubKey() + tmPk, err := cryptocodec.ToCmtProtoPublicKey(pk) + require.NoError(t, err) + return providertypes.ConsensusValidator{PublicKey: &tmPk, Power: power} +} + +func TestFullValSetUpdatesReturnsCompleteSet(t *testing.T) { + pk1 := ed25519.GenPrivKey().PubKey() + tmPk1, err := cryptocodec.ToCmtProtoPublicKey(pk1) + require.NoError(t, err) + vals := []providertypes.ConsensusValidator{{PublicKey: &tmPk1, Power: 10}} + + updates := keeper.FullValSetUpdates(vals) + require.Len(t, updates, 1) + require.Equal(t, int64(10), updates[0].Power) +} + +func TestQueueVSCPacketsSnapshotWhenBehind(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + k.SetConsumerHighestSentVscId(ctx, cid, 5) + k.SetConsumerHighestAckedVscId(ctx, cid, 3) // behind: acked < sent: acked < sent + + mocks.MockStakingKeeper.EXPECT().MaxValidators(gomock.Any()).Return(uint32(100), nil).AnyTimes() + mocks.MockStakingKeeper.EXPECT().GetBondedValidatorsByPower(gomock.Any()).Return([]stakingtypes.Validator{}, nil).AnyTimes() + + require.NoError(t, k.QueueVSCPackets(ctx)) + pending := k.GetPendingVSCPackets(ctx, cid) + require.Len(t, pending, 1) + require.True(t, pending[0].IsSnapshot) +} + +func TestQueueVSCPacketsDiffWhenCaughtUp(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + // acked == sent == 0: caught up + k.SetConsumerHighestSentVscId(ctx, cid, 0) + k.SetConsumerHighestAckedVscId(ctx, cid, 0) + + mocks.MockStakingKeeper.EXPECT().MaxValidators(gomock.Any()).Return(uint32(100), nil).AnyTimes() + mocks.MockStakingKeeper.EXPECT().GetBondedValidatorsByPower(gomock.Any()).Return([]stakingtypes.Validator{}, nil).AnyTimes() + + require.NoError(t, k.QueueVSCPackets(ctx)) + pending := k.GetPendingVSCPackets(ctx, cid) + require.Len(t, pending, 1) + require.False(t, pending[0].IsSnapshot) +} + +// TestSnapshotAfterLostDiff is the core resync SAFETY property. +// +// When a consumer is BEHIND (acked < sent) the provider must send a full +// snapshot so the consumer can replace its entire set — no reliance on a +// diff that may never have arrived. This test verifies: +// - the queued packet is flagged as a snapshot (IsSnapshot == true), +// - the snapshot contains validator A with non-zero power (the survivor +// is present in the live set and kept in the snapshot), and +// - validator B is NOT present in the snapshot — proving the snapshot is +// computed from the live bonded set and not the stale consumer valset. +func TestSnapshotAfterLostDiff(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + // identityA is the surviving validator; identityB has departed. + identityA := testcrypto.NewCryptoIdentityFromIntSeed(0) + identityB := testcrypto.NewCryptoIdentityFromIntSeed(1) + + // Build the ConsensusValidator representations used to seed the consumer valset. + tmPkA := identityA.TMProtoCryptoPublicKey() + tmPkB := identityB.TMProtoCryptoPublicKey() + valAConsensus := providertypes.ConsensusValidator{PublicKey: &tmPkA, Power: 10} + valBConsensus := providertypes.ConsensusValidator{PublicKey: &tmPkB, Power: 5} + + cid := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerPhase(ctx, cid, providertypes.CONSUMER_PHASE_LAUNCHED) + + // The consumer last knew about {A, B}. + require.NoError(t, k.SetConsumerValSet(ctx, cid, []providertypes.ConsensusValidator{valAConsensus, valBConsensus})) + + // The consumer is behind: some sent packets have not been acknowledged. + k.SetConsumerHighestSentVscId(ctx, cid, 5) + k.SetConsumerHighestAckedVscId(ctx, cid, 3) + + // Build a real staking validator for A to return from the bonded set. + // B is absent — it has departed from the bonded set. + stakingValA := identityA.SDKStakingValidator() + const powerA = int64(10) + + // GetLastBondedValidators calls MaxValidators then GetBondedValidatorsByPower. + // CreateConsumerValidator calls GetLastValidatorPower for each bonded validator. + mocks.MockStakingKeeper.EXPECT().MaxValidators(gomock.Any()).Return(uint32(100), nil).AnyTimes() + mocks.MockStakingKeeper.EXPECT().GetBondedValidatorsByPower(gomock.Any()).Return([]stakingtypes.Validator{stakingValA}, nil).AnyTimes() + mocks.MockStakingKeeper.EXPECT().GetLastValidatorPower(gomock.Any(), identityA.SDKValOpAddress()).Return(powerA, nil).AnyTimes() + + require.NoError(t, k.QueueVSCPackets(ctx)) + + pending := k.GetPendingVSCPackets(ctx, cid) + require.Len(t, pending, 1) + + pkt := pending[0] + require.True(t, pkt.IsSnapshot, "expected IsSnapshot=true when consumer is behind") + + // Validator A must appear in the snapshot with non-zero power. + aKeyStr := tmPkA.String() + foundA := false + for _, u := range pkt.ValidatorUpdates { + if u.PubKey.String() == aKeyStr { + require.Greater(t, u.Power, int64(0), "validator A must have non-zero power in the snapshot") + foundA = true + } + } + require.True(t, foundA, "surviving validator A must be present in the snapshot") + + // Validator B was in the stale consumer valset but departed from the bonded + // set, so it must NOT appear in the snapshot. + bKeyStr := tmPkB.String() + for _, u := range pkt.ValidatorUpdates { + require.NotEqual(t, bKeyStr, u.PubKey.String(), + "departed validator B must not appear in the snapshot") + } +} + +// TestDiffValidators_RemoveDeparted verifies that a validator present in the +// current set but absent from the next set generates exactly one power-0 +// update for that validator. +func TestDiffValidators_RemoveDeparted(t *testing.T) { + valA := makeConsensusValidator(t, 10) + valB := makeConsensusValidator(t, 5) + + current := []providertypes.ConsensusValidator{valA, valB} + next := []providertypes.ConsensusValidator{valA} + + updates := keeper.DiffValidators(current, next) + require.Len(t, updates, 1, "expected exactly one update (removal of B)") + require.Equal(t, valB.PublicKey.String(), updates[0].PubKey.String()) + require.Equal(t, int64(0), updates[0].Power, "departed validator must have power=0") +} + +// TestDiffValidators_EmptyNextSet verifies that when the next set is empty +// all current validators are removed (power-0 updates), with no additions. +func TestDiffValidators_EmptyNextSet(t *testing.T) { + valA := makeConsensusValidator(t, 10) + valB := makeConsensusValidator(t, 5) + + current := []providertypes.ConsensusValidator{valA, valB} + updates := keeper.DiffValidators(current, nil) + + require.Len(t, updates, 2, "expected two removals") + for _, u := range updates { + require.Equal(t, int64(0), u.Power, "all removals must have power=0") + } +} + +// TestFullValSetUpdates_EmptyInput verifies that FullValSetUpdates on a nil +// or empty slice returns an empty (non-nil, len 0) result. +func TestFullValSetUpdates_EmptyInput(t *testing.T) { + require.Empty(t, keeper.FullValSetUpdates(nil)) + require.Empty(t, keeper.FullValSetUpdates([]providertypes.ConsensusValidator{})) +} diff --git a/x/vaas/provider/module.go b/x/vaas/provider/module.go index 2dddfbd..1794363 100644 --- a/x/vaas/provider/module.go +++ b/x/vaas/provider/module.go @@ -155,6 +155,11 @@ func (am AppModule) BeginBlock(ctx context.Context) error { return err } + // Stop consumers that have gone silent past the liveness grace period. + if err := am.keeper.SweepUnresponsiveConsumers(sdkCtx); err != nil { + return err + } + // Collect and distribute fees once per epoch, not every block. if am.keeper.BlocksUntilNextEpoch(sdkCtx) == 0 { if err := am.keeper.DistributeConsumerFees(sdkCtx); err != nil { diff --git a/x/vaas/provider/module_test.go b/x/vaas/provider/module_test.go index dce592f..dc36c4c 100644 --- a/x/vaas/provider/module_test.go +++ b/x/vaas/provider/module_test.go @@ -3,6 +3,7 @@ package provider import ( "bytes" "testing" + "time" "cosmossdk.io/math" testkeeper "github.com/allinbits/vaas/testutil/keeper" @@ -43,6 +44,7 @@ func TestBeginBlockCommitsDebtStateWhenDistributionFails(t *testing.T) { // One bonded validator. valAddrCodec := address.NewBech32Codec("cosmosvaloper") mocks.MockStakingKeeper.EXPECT().ValidatorAddressCodec().Return(valAddrCodec).AnyTimes() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).AnyTimes() opBytes := bytes.Repeat([]byte{0xfe}, 20) op, err := valAddrCodec.BytesToString(opBytes) require.NoError(t, err) @@ -57,7 +59,7 @@ func TestBeginBlockCommitsDebtStateWhenDistributionFails(t *testing.T) { GetBondedValidatorsByPower(gomock.Any()). Return([]stakingtypes.Validator{val}, nil) - // consumerInDebt: balance too low → in debt, no InputOutputCoins + // consumerInDebt: balance too low -> in debt, no InputOutputCoins mocks.MockBankKeeper.EXPECT(). GetBalance(gomock.Any(), consumerInDebtPool, "uphoton"). Return(sdk.NewCoin("uphoton", feesPerEpoch.Amount.QuoRaw(2))) @@ -84,7 +86,7 @@ func TestBeginBlockCommitsDebtStateWhenDistributionFails(t *testing.T) { // per-epoch fee distribution only runs at epoch boundaries. func TestBeginBlockSkipsFeeCollectionWhenNotAtEpochBoundary(t *testing.T) { params := testkeeper.NewInMemKeeperParams(t) - k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, params) + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, params) defer ctrl.Finish() appModule := NewAppModule(&k) @@ -100,7 +102,9 @@ func TestBeginBlockSkipsFeeCollectionWhenNotAtEpochBoundary(t *testing.T) { // Height 100 is not an epoch boundary (blocks_per_epoch=600) ctx = ctx.WithBlockHeight(100) - // No bank or staking mock expectations — nothing should be called. + // SweepUnresponsiveConsumers calls UnbondingTime on every block. + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).AnyTimes() + require.NoError(t, appModule.BeginBlock(sdk.WrapSDKContext(ctx))) } @@ -129,6 +133,7 @@ func TestBeginBlockCollectsFeesAtEpochBoundary(t *testing.T) { valAddrCodec := address.NewBech32Codec("cosmosvaloper") mocks.MockStakingKeeper.EXPECT().ValidatorAddressCodec().Return(valAddrCodec).AnyTimes() + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Return(21*24*time.Hour, nil).AnyTimes() opBytes := bytes.Repeat([]byte{0xfe}, 20) op, err := valAddrCodec.BytesToString(opBytes) require.NoError(t, err) diff --git a/x/vaas/provider/types/genesis.pb.go b/x/vaas/provider/types/genesis.pb.go index 1b2b7e3..0587c66 100644 --- a/x/vaas/provider/types/genesis.pb.go +++ b/x/vaas/provider/types/genesis.pb.go @@ -187,6 +187,15 @@ type ConsumerState struct { // scheduled to transition to DELETED. Set only when phase == STOPPED; // absent otherwise. Used to re-derive the keeper's removal-time queue. RemovalTime *time.Time `protobuf:"bytes,12,opt,name=removal_time,json=removalTime,proto3,stdtime" json:"removal_time,omitempty"` + // Liveness clock, present once a consumer has launched: LastAckTime is the + // block time of the consumer's most recent successful VSC acknowledgement + // (the provider's per-consumer grace clock), and HighestSentVscId / + // HighestAckedVscId are the resync counters that decide whether the consumer + // is behind. Round-tripped so a state-export restart preserves the grace + // window and resync state instead of resetting them. + LastAckTime *time.Time `protobuf:"bytes,13,opt,name=last_ack_time,json=lastAckTime,proto3,stdtime" json:"last_ack_time,omitempty"` + HighestSentVscId uint64 `protobuf:"varint,14,opt,name=highest_sent_vsc_id,json=highestSentVscId,proto3" json:"highest_sent_vsc_id,omitempty"` + HighestAckedVscId uint64 `protobuf:"varint,15,opt,name=highest_acked_vsc_id,json=highestAckedVscId,proto3" json:"highest_acked_vsc_id,omitempty"` } func (m *ConsumerState) Reset() { *m = ConsumerState{} } @@ -306,6 +315,27 @@ func (m *ConsumerState) GetRemovalTime() *time.Time { return nil } +func (m *ConsumerState) GetLastAckTime() *time.Time { + if m != nil { + return m.LastAckTime + } + return nil +} + +func (m *ConsumerState) GetHighestSentVscId() uint64 { + if m != nil { + return m.HighestSentVscId + } + return 0 +} + +func (m *ConsumerState) GetHighestAckedVscId() uint64 { + if m != nil { + return m.HighestAckedVscId + } + return 0 +} + // ValsetUpdateIdToHeight defines the genesis information for the mapping // of each valset update id to a block height type ValsetUpdateIdToHeight struct { @@ -490,70 +520,74 @@ func init() { func init() { proto.RegisterFile("vaas/provider/v1/genesis.proto", fileDescriptor_c9071b84cde652f9) } var fileDescriptor_c9071b84cde652f9 = []byte{ - // 1004 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xdd, 0x6e, 0xdc, 0x44, - 0x14, 0x8e, 0x9b, 0xcd, 0x36, 0x3b, 0x9b, 0x84, 0x68, 0x94, 0x06, 0x37, 0x25, 0xbb, 0x61, 0x51, - 0xab, 0x45, 0x50, 0x5b, 0x09, 0xa2, 0x17, 0x5c, 0x20, 0x65, 0x53, 0x15, 0x56, 0x08, 0x58, 0x39, - 0x81, 0x8b, 0xde, 0x58, 0xb3, 0x9e, 0x53, 0x7b, 0x58, 0xdb, 0x63, 0x79, 0x66, 0x1d, 0x96, 0x0b, - 0x9e, 0xa1, 0x4f, 0xc1, 0x13, 0xf4, 0x21, 0x2a, 0xae, 0xaa, 0x5e, 0x21, 0x2e, 0x02, 0x4a, 0x9e, - 0x00, 0x9e, 0x00, 0xcd, 0x78, 0xec, 0xe6, 0x67, 0x93, 0x88, 0xbb, 0xf5, 0xf9, 0xbe, 0xf9, 0xbe, - 0x33, 0x33, 0xe7, 0x9c, 0x59, 0xd4, 0x29, 0x08, 0x11, 0x6e, 0x96, 0xf3, 0x82, 0x51, 0xc8, 0xdd, - 0x62, 0xd7, 0x0d, 0x21, 0x05, 0xc1, 0x84, 0x93, 0xe5, 0x5c, 0x72, 0xbc, 0xae, 0x70, 0xa7, 0xc2, - 0x9d, 0x62, 0x77, 0xeb, 0x7e, 0xc0, 0x45, 0xc2, 0x85, 0xaf, 0x71, 0xb7, 0xfc, 0x28, 0xc9, 0x5b, - 0x1b, 0x21, 0x0f, 0x79, 0x19, 0x57, 0xbf, 0x4c, 0xb4, 0x1b, 0x72, 0x1e, 0xc6, 0xe0, 0xea, 0xaf, - 0xf1, 0xf4, 0x85, 0x2b, 0x59, 0x02, 0x42, 0x92, 0x24, 0x33, 0x84, 0x6d, 0x9d, 0x43, 0xb1, 0xeb, - 0x8a, 0x88, 0xe4, 0x40, 0xfd, 0x80, 0xa7, 0x62, 0x9a, 0x40, 0x6e, 0x60, 0x5c, 0xc1, 0xc7, 0x2c, - 0x87, 0x4a, 0xf3, 0x4a, 0xda, 0x75, 0x8a, 0x9a, 0xd0, 0xfb, 0xa7, 0x89, 0x56, 0xbe, 0x2a, 0x77, - 0x72, 0x28, 0x89, 0x04, 0xdc, 0x47, 0xeb, 0x05, 0x89, 0x05, 0x48, 0x7f, 0x9a, 0x51, 0x22, 0xc1, - 0x67, 0xd4, 0xb6, 0x76, 0xac, 0x7e, 0xc3, 0x5b, 0x2b, 0xe3, 0x3f, 0xe8, 0xf0, 0x90, 0xe2, 0x08, - 0xbd, 0x57, 0x65, 0xe0, 0x0b, 0xb5, 0x56, 0xd8, 0x77, 0x76, 0x16, 0xfb, 0xed, 0xbd, 0xae, 0x73, - 0xf9, 0x30, 0x9c, 0x03, 0x43, 0xd4, 0x1e, 0x83, 0xce, 0xeb, 0x93, 0xee, 0xc2, 0xbf, 0x27, 0xdd, - 0xcd, 0x19, 0x49, 0xe2, 0x2f, 0x7a, 0x97, 0x54, 0x7a, 0xde, 0x5a, 0x70, 0x9e, 0x2e, 0xf0, 0x4f, - 0x68, 0xeb, 0x72, 0x4e, 0xbe, 0xe4, 0x7e, 0x04, 0x2c, 0x8c, 0xa4, 0xbd, 0xa8, 0x4d, 0xfb, 0x57, - 0x4d, 0x7f, 0xbc, 0x90, 0xef, 0x11, 0xff, 0x5a, 0xf3, 0x07, 0x0d, 0xe5, 0xee, 0x6d, 0x16, 0x73, - 0x51, 0xfc, 0x04, 0x35, 0x33, 0x92, 0x93, 0x44, 0xd8, 0x8d, 0x1d, 0xab, 0xdf, 0xde, 0xb3, 0xaf, - 0xea, 0x8e, 0x34, 0x6e, 0x74, 0x0c, 0x1b, 0x27, 0x3a, 0x47, 0x46, 0x89, 0xe4, 0x79, 0x7d, 0x33, - 0x7e, 0x36, 0x1d, 0x4f, 0x60, 0x26, 0xec, 0x25, 0x9d, 0xe3, 0xc7, 0x73, 0x73, 0x2c, 0xd7, 0x54, - 0x27, 0x34, 0x9a, 0x8e, 0xbf, 0x81, 0x99, 0x11, 0xb7, 0x8b, 0x39, 0xb0, 0x12, 0xc4, 0x29, 0x7a, - 0x50, 0x63, 0xc2, 0x1f, 0xcf, 0xde, 0x59, 0x12, 0x4a, 0x73, 0xbb, 0x79, 0xab, 0xdf, 0x60, 0x56, - 0x49, 0xee, 0x53, 0x9a, 0x5f, 0xf1, 0x13, 0x17, 0x71, 0x1c, 0xa0, 0xf7, 0x2f, 0x38, 0x08, 0x75, - 0x01, 0x59, 0x3e, 0x4d, 0xc1, 0xbe, 0xab, 0xbd, 0x1e, 0x5d, 0x7f, 0xe9, 0x4a, 0x40, 0x1c, 0xf1, - 0x91, 0x62, 0x1b, 0xa3, 0x8d, 0x60, 0x0e, 0x86, 0x7f, 0x45, 0x1f, 0xd6, 0x26, 0x2f, 0x00, 0x84, - 0x9f, 0x41, 0xee, 0x8f, 0x63, 0x1e, 0x4c, 0x7c, 0x5e, 0x40, 0x9e, 0x33, 0x0a, 0xc2, 0x5e, 0xd6, - 0x76, 0xce, 0xf5, 0x76, 0xcf, 0x00, 0xc4, 0x08, 0xf2, 0x81, 0x5a, 0xf7, 0xbd, 0x59, 0x66, 0x6c, - 0xb7, 0x83, 0x1b, 0x38, 0x02, 0x03, 0xb2, 0xcf, 0xfb, 0xfb, 0x19, 0xe7, 0xb1, 0xaf, 0x9b, 0x4d, - 0xd8, 0xad, 0xdb, 0x76, 0xf9, 0x0c, 0x60, 0xc4, 0x79, 0x7c, 0xa8, 0xe8, 0xc6, 0xee, 0x5e, 0x30, - 0x07, 0x13, 0xbd, 0xdf, 0x96, 0xd0, 0xea, 0x85, 0x86, 0xc0, 0x5d, 0xd4, 0xae, 0x8d, 0xeb, 0x7e, - 0x43, 0x55, 0x68, 0x48, 0xf1, 0x7d, 0xb4, 0x1c, 0x44, 0x84, 0xa5, 0x0a, 0xbd, 0xb3, 0x63, 0xf5, - 0x5b, 0xde, 0x5d, 0xfd, 0x3d, 0xa4, 0xf8, 0x01, 0x6a, 0x05, 0x31, 0x83, 0x54, 0x2a, 0x6c, 0x51, - 0x63, 0xcb, 0x65, 0x60, 0x48, 0xf1, 0x43, 0xb4, 0xc6, 0x52, 0x26, 0x19, 0x89, 0xab, 0x6e, 0x69, - 0x68, 0xed, 0x55, 0x13, 0x35, 0x45, 0xff, 0x1d, 0x5a, 0xaf, 0xfd, 0xcd, 0x5c, 0xb3, 0x97, 0x74, - 0xf9, 0x6f, 0x97, 0x1b, 0x3e, 0xb7, 0xcf, 0xf3, 0xd3, 0xc2, 0xec, 0xb3, 0x9e, 0x03, 0x06, 0xc3, - 0x04, 0x6d, 0x66, 0x90, 0x52, 0x96, 0x86, 0xbe, 0x69, 0xdc, 0x20, 0x22, 0x69, 0x08, 0xc2, 0x14, - 0xe6, 0xc3, 0x5a, 0xb5, 0xae, 0xc7, 0x43, 0x90, 0x07, 0x9a, 0x33, 0x22, 0xc1, 0x04, 0xe4, 0x53, - 0x22, 0x49, 0x55, 0x2b, 0x46, 0xaa, 0x6c, 0xe7, 0x92, 0x24, 0xf0, 0xa7, 0x08, 0x8b, 0x98, 0x88, - 0xc8, 0xa7, 0xfc, 0x38, 0x55, 0x93, 0xd2, 0x27, 0xc1, 0x44, 0xd7, 0x62, 0xcb, 0x5b, 0xd7, 0xc8, - 0x53, 0x03, 0xec, 0x07, 0x13, 0xfc, 0x39, 0x5a, 0xca, 0x22, 0x22, 0xc0, 0x5e, 0xde, 0xb1, 0xfa, - 0x6b, 0x37, 0x4d, 0xa8, 0x91, 0xa2, 0x79, 0x25, 0x1b, 0x7f, 0x84, 0x56, 0xf9, 0x71, 0x6a, 0x4a, - 0x1e, 0x84, 0xaa, 0x02, 0x75, 0xbe, 0x2b, 0x3a, 0xb8, 0x5f, 0xc6, 0xf0, 0x97, 0x68, 0x39, 0x01, - 0x49, 0x28, 0x91, 0xc4, 0x46, 0xfa, 0xd0, 0x7a, 0xd7, 0xcb, 0x7f, 0x6b, 0x98, 0x5e, 0xbd, 0x06, - 0x1f, 0xa2, 0xb6, 0xba, 0x0d, 0xdf, 0x8c, 0x9d, 0xb6, 0x96, 0xd8, 0xbb, 0x5e, 0x62, 0x58, 0x5e, - 0x1d, 0xfb, 0x85, 0x48, 0xc6, 0x53, 0x3d, 0x8c, 0x40, 0x42, 0x2e, 0x3c, 0xa4, 0x64, 0xca, 0xe1, - 0x84, 0x0f, 0xd0, 0x4a, 0x0e, 0x09, 0x2f, 0x48, 0xec, 0xab, 0x33, 0xb0, 0x57, 0xb4, 0xea, 0x96, - 0x53, 0xbe, 0x31, 0x4e, 0xf5, 0xc6, 0x38, 0x47, 0xd5, 0x1b, 0x33, 0x68, 0xbc, 0xfc, 0xab, 0x6b, - 0x79, 0x6d, 0xb3, 0x4a, 0xc5, 0x7b, 0xcf, 0xd1, 0xe6, 0xfc, 0x19, 0xfa, 0x3f, 0x5e, 0x89, 0x4d, - 0xd4, 0x34, 0x95, 0x77, 0x47, 0xe3, 0xe6, 0xab, 0x17, 0xa2, 0x0f, 0x6e, 0x6a, 0xd8, 0xdb, 0x5b, - 0xe2, 0x11, 0x6a, 0x92, 0x84, 0x4f, 0xd3, 0x52, 0xb8, 0x35, 0x58, 0x7b, 0xfb, 0xea, 0x31, 0x32, - 0xcf, 0xec, 0x30, 0x95, 0x9e, 0x41, 0x7b, 0xbf, 0x5b, 0x68, 0x63, 0x5e, 0x8f, 0xde, 0xee, 0xf0, - 0x04, 0xb5, 0x28, 0x64, 0x5c, 0x30, 0xc9, 0x73, 0x63, 0x62, 0xbf, 0x7d, 0xf5, 0x78, 0xc3, 0x98, - 0x98, 0xfb, 0x3f, 0x94, 0x39, 0x4b, 0x43, 0xef, 0x1d, 0x15, 0x6f, 0xa0, 0x25, 0x0a, 0x29, 0x4f, - 0x4c, 0x37, 0x96, 0x1f, 0xf8, 0x00, 0x35, 0xcd, 0x28, 0x69, 0x68, 0xa9, 0x4f, 0x54, 0x71, 0xff, - 0x79, 0xd2, 0xbd, 0x57, 0xca, 0x09, 0x3a, 0x71, 0x18, 0x77, 0x13, 0x22, 0x23, 0x95, 0xfe, 0xe5, - 0xcd, 0x94, 0x4b, 0x07, 0xc3, 0xd7, 0xa7, 0x1d, 0xeb, 0xcd, 0x69, 0xc7, 0xfa, 0xfb, 0xb4, 0x63, - 0xbd, 0x3c, 0xeb, 0x2c, 0xbc, 0x39, 0xeb, 0x2c, 0xfc, 0x71, 0xd6, 0x59, 0x78, 0xee, 0x86, 0x4c, - 0x46, 0xd3, 0xb1, 0x13, 0xf0, 0xc4, 0x25, 0x71, 0xcc, 0xd2, 0x31, 0x93, 0xc2, 0xd5, 0xcf, 0xff, - 0xcf, 0xee, 0xc5, 0x7f, 0x01, 0x72, 0x96, 0x81, 0x18, 0x37, 0x75, 0x0d, 0x7c, 0xf6, 0x5f, 0x00, - 0x00, 0x00, 0xff, 0xff, 0xbf, 0x87, 0x0b, 0xc8, 0xda, 0x08, 0x00, 0x00, + // 1069 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xdd, 0x6e, 0x1b, 0x45, + 0x14, 0x8e, 0x13, 0xc7, 0x8d, 0xc7, 0x89, 0x1b, 0x06, 0x37, 0x6c, 0x53, 0x62, 0x07, 0xa3, 0x56, + 0x46, 0x10, 0xaf, 0x12, 0x44, 0x2f, 0xb8, 0x40, 0x8a, 0x13, 0x15, 0x2c, 0x04, 0x58, 0x76, 0xe8, + 0x45, 0x6f, 0x56, 0xe3, 0x9d, 0xd3, 0xdd, 0xc1, 0xbb, 0x3b, 0xab, 0x9d, 0xf1, 0x06, 0x73, 0xc1, + 0x33, 0xf4, 0x61, 0xfa, 0x10, 0x15, 0xe2, 0xa2, 0xea, 0x15, 0xe2, 0x22, 0xa0, 0xe4, 0x09, 0xe0, + 0x09, 0xd0, 0xcc, 0xce, 0x6e, 0xf3, 0xe3, 0x24, 0xf4, 0xce, 0x73, 0xbe, 0x6f, 0xbe, 0xef, 0xcc, + 0xcc, 0x39, 0x67, 0x8d, 0x9a, 0x29, 0x21, 0xc2, 0x8e, 0x13, 0x9e, 0x32, 0x0a, 0x89, 0x9d, 0xee, + 0xda, 0x1e, 0x44, 0x20, 0x98, 0xe8, 0xc6, 0x09, 0x97, 0x1c, 0xaf, 0x2b, 0xbc, 0x9b, 0xe3, 0xdd, + 0x74, 0x77, 0xf3, 0xbe, 0xcb, 0x45, 0xc8, 0x85, 0xa3, 0x71, 0x3b, 0x5b, 0x64, 0xe4, 0xcd, 0x86, + 0xc7, 0x3d, 0x9e, 0xc5, 0xd5, 0x2f, 0x13, 0x6d, 0x79, 0x9c, 0x7b, 0x01, 0xd8, 0x7a, 0x35, 0x9e, + 0x3e, 0xb7, 0x25, 0x0b, 0x41, 0x48, 0x12, 0xc6, 0x86, 0xb0, 0xa5, 0x73, 0x48, 0x77, 0x6d, 0xe1, + 0x93, 0x04, 0xa8, 0xe3, 0xf2, 0x48, 0x4c, 0x43, 0x48, 0x0c, 0x8c, 0x73, 0xf8, 0x98, 0x25, 0x90, + 0x6b, 0x5e, 0x49, 0xbb, 0x48, 0x51, 0x13, 0xda, 0xff, 0x54, 0xd0, 0xea, 0xd7, 0xd9, 0x49, 0x46, + 0x92, 0x48, 0xc0, 0x1d, 0xb4, 0x9e, 0x92, 0x40, 0x80, 0x74, 0xa6, 0x31, 0x25, 0x12, 0x1c, 0x46, + 0xad, 0xd2, 0x76, 0xa9, 0x53, 0x1e, 0xd6, 0xb3, 0xf8, 0x8f, 0x3a, 0xdc, 0xa7, 0xd8, 0x47, 0x77, + 0xf3, 0x0c, 0x1c, 0xa1, 0xf6, 0x0a, 0x6b, 0x71, 0x7b, 0xa9, 0x53, 0xdb, 0x6b, 0x75, 0x2f, 0x5f, + 0x46, 0xf7, 0xc0, 0x10, 0xb5, 0x47, 0xaf, 0xf9, 0xea, 0xa4, 0xb5, 0xf0, 0xef, 0x49, 0x6b, 0x63, + 0x46, 0xc2, 0xe0, 0xcb, 0xf6, 0x25, 0x95, 0xf6, 0xb0, 0xee, 0x9e, 0xa7, 0x0b, 0xfc, 0x13, 0xda, + 0xbc, 0x9c, 0x93, 0x23, 0xb9, 0xe3, 0x03, 0xf3, 0x7c, 0x69, 0x2d, 0x69, 0xd3, 0xce, 0x55, 0xd3, + 0xa7, 0x17, 0xf2, 0x3d, 0xe2, 0xdf, 0x68, 0x7e, 0xaf, 0xac, 0xdc, 0x87, 0x1b, 0xe9, 0x5c, 0x14, + 0x3f, 0x46, 0x95, 0x98, 0x24, 0x24, 0x14, 0x56, 0x79, 0xbb, 0xd4, 0xa9, 0xed, 0x59, 0x57, 0x75, + 0x07, 0x1a, 0x37, 0x3a, 0x86, 0x8d, 0x43, 0x9d, 0x23, 0xa3, 0x44, 0xf2, 0xa4, 0x78, 0x19, 0x27, + 0x9e, 0x8e, 0x27, 0x30, 0x13, 0xd6, 0xb2, 0xce, 0xf1, 0x93, 0xb9, 0x39, 0x66, 0x7b, 0xf2, 0x1b, + 0x1a, 0x4c, 0xc7, 0xdf, 0xc2, 0xcc, 0x88, 0x5b, 0xe9, 0x1c, 0x58, 0x09, 0xe2, 0x08, 0x3d, 0x28, + 0x30, 0xe1, 0x8c, 0x67, 0x6f, 0x2d, 0x09, 0xa5, 0x89, 0x55, 0xb9, 0xd5, 0xaf, 0x37, 0xcb, 0x25, + 0xf7, 0x29, 0x4d, 0xae, 0xf8, 0x89, 0x8b, 0x38, 0x76, 0xd1, 0x07, 0x17, 0x1c, 0x84, 0x7a, 0x80, + 0x38, 0x99, 0x46, 0x60, 0xdd, 0xd1, 0x5e, 0x8f, 0xae, 0x7f, 0x74, 0x25, 0x20, 0x8e, 0xf8, 0x40, + 0xb1, 0x8d, 0x51, 0xc3, 0x9d, 0x83, 0xe1, 0x5f, 0xd1, 0x47, 0x85, 0xc9, 0x73, 0x00, 0xe1, 0xc4, + 0x90, 0x38, 0xe3, 0x80, 0xbb, 0x13, 0x87, 0xa7, 0x90, 0x24, 0x8c, 0x82, 0xb0, 0x56, 0xb4, 0x5d, + 0xf7, 0x7a, 0xbb, 0x27, 0x00, 0x62, 0x00, 0x49, 0x4f, 0xed, 0xfb, 0xc1, 0x6c, 0x33, 0xb6, 0x5b, + 0xee, 0x0d, 0x1c, 0x81, 0x01, 0x59, 0xe7, 0xfd, 0x9d, 0x98, 0xf3, 0xc0, 0xd1, 0xcd, 0x26, 0xac, + 0xea, 0x6d, 0xa7, 0x7c, 0x02, 0x30, 0xe0, 0x3c, 0x18, 0x29, 0xba, 0xb1, 0xbb, 0xe7, 0xce, 0xc1, + 0x44, 0xfb, 0xf7, 0x0a, 0x5a, 0xbb, 0xd0, 0x10, 0xb8, 0x85, 0x6a, 0x85, 0x71, 0xd1, 0x6f, 0x28, + 0x0f, 0xf5, 0x29, 0xbe, 0x8f, 0x56, 0x5c, 0x9f, 0xb0, 0x48, 0xa1, 0x8b, 0xdb, 0xa5, 0x4e, 0x75, + 0x78, 0x47, 0xaf, 0xfb, 0x14, 0x3f, 0x40, 0x55, 0x37, 0x60, 0x10, 0x49, 0x85, 0x2d, 0x69, 0x6c, + 0x25, 0x0b, 0xf4, 0x29, 0x7e, 0x88, 0xea, 0x2c, 0x62, 0x92, 0x91, 0x20, 0xef, 0x96, 0xb2, 0xd6, + 0x5e, 0x33, 0x51, 0x53, 0xf4, 0xdf, 0xa3, 0xf5, 0xc2, 0xdf, 0xcc, 0x35, 0x6b, 0x59, 0x97, 0xff, + 0x56, 0x76, 0xe0, 0x73, 0xe7, 0x3c, 0x3f, 0x2d, 0xcc, 0x39, 0x8b, 0x39, 0x60, 0x30, 0x4c, 0xd0, + 0x46, 0x0c, 0x11, 0x65, 0x91, 0xe7, 0x98, 0xc6, 0x75, 0x7d, 0x12, 0x79, 0x20, 0x4c, 0x61, 0x3e, + 0x2c, 0x54, 0x8b, 0x7a, 0x1c, 0x81, 0x3c, 0xd0, 0x9c, 0x01, 0x71, 0x27, 0x20, 0x0f, 0x89, 0x24, + 0x79, 0xad, 0x18, 0xa9, 0xac, 0x9d, 0x33, 0x92, 0xc0, 0x9f, 0x21, 0x2c, 0x02, 0x22, 0x7c, 0x87, + 0xf2, 0xe3, 0x48, 0x4d, 0x4a, 0x87, 0xb8, 0x13, 0x5d, 0x8b, 0xd5, 0xe1, 0xba, 0x46, 0x0e, 0x0d, + 0xb0, 0xef, 0x4e, 0xf0, 0x17, 0x68, 0x39, 0xf6, 0x89, 0x00, 0x6b, 0x65, 0xbb, 0xd4, 0xa9, 0xdf, + 0x34, 0xa1, 0x06, 0x8a, 0x36, 0xcc, 0xd8, 0xf8, 0x63, 0xb4, 0xc6, 0x8f, 0x23, 0x53, 0xf2, 0x20, + 0x54, 0x15, 0xa8, 0xfb, 0x5d, 0xd5, 0xc1, 0xfd, 0x2c, 0x86, 0xbf, 0x42, 0x2b, 0x21, 0x48, 0x42, + 0x89, 0x24, 0x16, 0xd2, 0x97, 0xd6, 0xbe, 0x5e, 0xfe, 0x3b, 0xc3, 0x1c, 0x16, 0x7b, 0xf0, 0x08, + 0xd5, 0xd4, 0x6b, 0x38, 0x66, 0xec, 0xd4, 0xb4, 0xc4, 0xde, 0xf5, 0x12, 0xfd, 0xec, 0xe9, 0xd8, + 0x2f, 0x44, 0x32, 0x1e, 0xe9, 0x61, 0x04, 0x12, 0x12, 0x31, 0x44, 0x4a, 0x26, 0x1b, 0x4e, 0xf8, + 0x00, 0xad, 0x26, 0x10, 0xf2, 0x94, 0x04, 0x8e, 0xba, 0x03, 0x6b, 0x55, 0xab, 0x6e, 0x76, 0xb3, + 0x6f, 0x4c, 0x37, 0xff, 0xc6, 0x74, 0x8f, 0xf2, 0x6f, 0x4c, 0xaf, 0xfc, 0xe2, 0xaf, 0x56, 0x69, + 0x58, 0x33, 0xbb, 0x54, 0x1c, 0x1f, 0xa2, 0xb5, 0x80, 0x08, 0xa9, 0x6e, 0x36, 0x53, 0x59, 0xfb, + 0xbf, 0x2a, 0x6a, 0xdb, 0xbe, 0x3b, 0xd1, 0x2a, 0x3b, 0xe8, 0x7d, 0x9f, 0x79, 0x3e, 0x08, 0xe9, + 0x08, 0x55, 0xa6, 0xa9, 0x70, 0x55, 0xa9, 0xd6, 0x75, 0x21, 0xae, 0x1b, 0x68, 0x04, 0x91, 0x7c, + 0x2a, 0xdc, 0x3e, 0xc5, 0x36, 0x6a, 0xe4, 0x74, 0x55, 0x09, 0x34, 0xe7, 0xdf, 0xd5, 0xfc, 0xf7, + 0x0c, 0xb6, 0xaf, 0x20, 0xbd, 0xa1, 0xfd, 0x0c, 0x6d, 0xcc, 0x9f, 0xf4, 0xef, 0xf0, 0x2d, 0xdb, + 0x40, 0x15, 0xd3, 0x1f, 0x8b, 0x1a, 0x37, 0xab, 0xb6, 0x87, 0x3e, 0xbc, 0x69, 0xac, 0xdc, 0xde, + 0xb8, 0x8f, 0x50, 0x85, 0x84, 0x7c, 0x1a, 0x65, 0xc2, 0xd5, 0x5e, 0xfd, 0xcd, 0xcb, 0x1d, 0x64, + 0xfe, 0x0c, 0xf4, 0x23, 0x39, 0x34, 0x68, 0xfb, 0xb7, 0x12, 0x6a, 0xcc, 0x9b, 0x24, 0xb7, 0x3b, + 0x3c, 0x46, 0x55, 0x0a, 0x31, 0x17, 0x4c, 0xf2, 0xc4, 0x98, 0x58, 0x6f, 0x5e, 0xee, 0x34, 0x8c, + 0x89, 0xa9, 0xd2, 0x91, 0x4c, 0x58, 0xe4, 0x0d, 0xdf, 0x52, 0x71, 0x03, 0x2d, 0x53, 0x88, 0x78, + 0x68, 0x66, 0x46, 0xb6, 0xc0, 0x07, 0xa8, 0x62, 0x06, 0x5e, 0x59, 0x4b, 0x7d, 0xaa, 0x5a, 0xf0, + 0xcf, 0x93, 0xd6, 0xbd, 0x4c, 0x4e, 0xd0, 0x49, 0x97, 0x71, 0x3b, 0x24, 0xd2, 0x57, 0xe9, 0x5f, + 0x3e, 0x4c, 0xb6, 0xb5, 0xd7, 0x7f, 0x75, 0xda, 0x2c, 0xbd, 0x3e, 0x6d, 0x96, 0xfe, 0x3e, 0x6d, + 0x96, 0x5e, 0x9c, 0x35, 0x17, 0x5e, 0x9f, 0x35, 0x17, 0xfe, 0x38, 0x6b, 0x2e, 0x3c, 0xb3, 0x3d, + 0x26, 0xfd, 0xe9, 0xb8, 0xeb, 0xf2, 0xd0, 0x26, 0x41, 0xc0, 0xa2, 0x31, 0x93, 0xc2, 0xd6, 0x7f, + 0x52, 0x7e, 0xb6, 0x2f, 0xfe, 0x57, 0x91, 0xb3, 0x18, 0xc4, 0xb8, 0xa2, 0x6b, 0xec, 0xf3, 0xff, + 0x02, 0x00, 0x00, 0xff, 0xff, 0xc1, 0x70, 0xc0, 0x2f, 0x80, 0x09, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -712,14 +746,34 @@ func (m *ConsumerState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - if m.RemovalTime != nil { - n2, err2 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(*m.RemovalTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.RemovalTime):]) + if m.HighestAckedVscId != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.HighestAckedVscId)) + i-- + dAtA[i] = 0x78 + } + if m.HighestSentVscId != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.HighestSentVscId)) + i-- + dAtA[i] = 0x70 + } + if m.LastAckTime != nil { + n2, err2 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(*m.LastAckTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.LastAckTime):]) if err2 != nil { return 0, err2 } i -= n2 i = encodeVarintGenesis(dAtA, i, uint64(n2)) i-- + dAtA[i] = 0x6a + } + if m.RemovalTime != nil { + n3, err3 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(*m.RemovalTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.RemovalTime):]) + if err3 != nil { + return 0, err3 + } + i -= n3 + i = encodeVarintGenesis(dAtA, i, uint64(n3)) + i-- dAtA[i] = 0x62 } if m.InitParams != nil { @@ -1058,6 +1112,16 @@ func (m *ConsumerState) Size() (n int) { l = github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.RemovalTime) n += 1 + l + sovGenesis(uint64(l)) } + if m.LastAckTime != nil { + l = github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.LastAckTime) + n += 1 + l + sovGenesis(uint64(l)) + } + if m.HighestSentVscId != 0 { + n += 1 + sovGenesis(uint64(m.HighestSentVscId)) + } + if m.HighestAckedVscId != 0 { + n += 1 + sovGenesis(uint64(m.HighestAckedVscId)) + } return n } @@ -1849,6 +1913,80 @@ func (m *ConsumerState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 13: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LastAckTime", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.LastAckTime == nil { + m.LastAckTime = new(time.Time) + } + if err := github_com_cosmos_gogoproto_types.StdTimeUnmarshal(m.LastAckTime, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 14: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field HighestSentVscId", wireType) + } + m.HighestSentVscId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.HighestSentVscId |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 15: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field HighestAckedVscId", wireType) + } + m.HighestAckedVscId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.HighestAckedVscId |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/vaas/provider/types/genesis_test.go b/x/vaas/provider/types/genesis_test.go index f42cbe3..b1dc343 100644 --- a/x/vaas/provider/types/genesis_test.go +++ b/x/vaas/provider/types/genesis_test.go @@ -88,7 +88,7 @@ func TestValidateGenesisState(t *testing.T) { nil, []types.ConsumerState{launchedCS(0, "chainid-1", "client-id", false)}, types.NewParams( - types.DefaultTrustingPeriodFraction, time.Hour, 600, math.NewInt(42), types.DefaultMinDepositBlocks), + types.DefaultTrustingPeriodFraction, types.DefaultLivenessGraceFraction, time.Hour, 600, math.NewInt(42), types.DefaultMinDepositBlocks), nil, nil, nil, @@ -135,6 +135,7 @@ func TestValidateGenesisState(t *testing.T) { []types.ConsumerState{launchedCS(0, "chainid-1", "client-id", false)}, types.NewParams( "0.0", // 0 trusting period fraction here + types.DefaultLivenessGraceFraction, vaastypes.DefaultVAASTimeoutPeriod, 600, math.NewInt(42), types.DefaultMinDepositBlocks), nil, nil, @@ -152,6 +153,7 @@ func TestValidateGenesisState(t *testing.T) { []types.ConsumerState{launchedCS(0, "chainid-1", "client-id", false)}, types.NewParams( types.DefaultTrustingPeriodFraction, + types.DefaultLivenessGraceFraction, 0, // 0 ccv timeout here 600, math.NewInt(42), types.DefaultMinDepositBlocks), nil, diff --git a/x/vaas/provider/types/keys.go b/x/vaas/provider/types/keys.go index 01c4722..1350169 100644 --- a/x/vaas/provider/types/keys.go +++ b/x/vaas/provider/types/keys.go @@ -115,5 +115,8 @@ var ( ConsumerFeePoolTotalSharesPrefix = collections.NewPrefix(26) FeePoolAddressToConsumerIdPrefix = collections.NewPrefix(27) EpochDowntimePrefix = collections.NewPrefix(28) + ConsumerIdToLastAckTimePrefix = collections.NewPrefix(29) + ConsumerIdToHighestSentVscIdPrefix = collections.NewPrefix(30) + ConsumerIdToHighestAckedVscIdPrefix = collections.NewPrefix(31) ParametersPrefix = collections.NewPrefix(0xFF) ) diff --git a/x/vaas/provider/types/msg.go b/x/vaas/provider/types/msg.go index 5e53e98..f5b12da 100644 --- a/x/vaas/provider/types/msg.go +++ b/x/vaas/provider/types/msg.go @@ -12,6 +12,7 @@ import ( cmttypes "github.com/cometbft/cometbft/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" ibctmtypes "github.com/cosmos/ibc-go/v10/modules/light-clients/07-tendermint" errorsmod "cosmossdk.io/errors" @@ -428,7 +429,7 @@ func ValidateInitializationParameters(initializationParameters ConsumerInitializ return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "HistoricalEntries: %s", err.Error()) } - if err := vaastypes.ValidateDuration(initializationParameters.VaasTimeoutPeriod); err != nil { + if err := vaastypes.ValidateVAASTimeoutPeriod(initializationParameters.VaasTimeoutPeriod, channeltypesv2.MaxTimeoutDelta); err != nil { return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "VaasTimeoutPeriod: %s", err.Error()) } @@ -436,6 +437,10 @@ func ValidateInitializationParameters(initializationParameters ConsumerInitializ return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "UnbondingPeriod: %s", err.Error()) } + if err := vaastypes.ValidateDuration(initializationParameters.SafeModeThreshold); err != nil { + return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "SafeModeThreshold: %s", err.Error()) + } + return nil } diff --git a/x/vaas/provider/types/msg_test.go b/x/vaas/provider/types/msg_test.go index 468a790..a0b08b5 100644 --- a/x/vaas/provider/types/msg_test.go +++ b/x/vaas/provider/types/msg_test.go @@ -6,6 +6,7 @@ import ( "time" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" "github.com/stretchr/testify/require" "cosmossdk.io/math" @@ -16,6 +17,7 @@ import ( cryptoutil "github.com/allinbits/vaas/testutil/crypto" "github.com/allinbits/vaas/x/vaas/provider/types" + vaastypes "github.com/allinbits/vaas/x/vaas/types" ) func TestValidateStringField(t *testing.T) { @@ -160,78 +162,98 @@ func TestValidateInitializationParameters(t *testing.T) { { name: "valid", params: types.ConsumerInitializationParameters{ - InitialHeight: clienttypes.NewHeight(3, 4), - GenesisHash: []byte{0x01}, - BinaryHash: []byte{0x01}, - SpawnTime: now, - UnbondingPeriod: time.Duration(100000000000), - VaasTimeoutPeriod: time.Duration(100000000000), - HistoricalEntries: 10000, + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, + HistoricalEntries: 10000, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, }, valid: true, }, { name: "invalid - zero height", params: types.ConsumerInitializationParameters{ - InitialHeight: clienttypes.ZeroHeight(), - GenesisHash: []byte{0x01}, - BinaryHash: []byte{0x01}, - SpawnTime: now, - UnbondingPeriod: time.Duration(100000000000), - VaasTimeoutPeriod: time.Duration(100000000000), - HistoricalEntries: 10000, + InitialHeight: clienttypes.ZeroHeight(), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, + HistoricalEntries: 10000, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, }, valid: false, }, { name: "invalid - hash too long", params: types.ConsumerInitializationParameters{ - InitialHeight: clienttypes.NewHeight(3, 4), - GenesisHash: tooLongHash, - BinaryHash: []byte{0x01}, - SpawnTime: now, - UnbondingPeriod: time.Duration(100000000000), - VaasTimeoutPeriod: time.Duration(100000000000), - HistoricalEntries: 10000, + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: tooLongHash, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, + HistoricalEntries: 10000, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, }, valid: false, }, { name: "invalid - zero spawn time", params: types.ConsumerInitializationParameters{ - InitialHeight: clienttypes.NewHeight(3, 4), - GenesisHash: []byte{0x01}, - BinaryHash: []byte{0x01}, - SpawnTime: time.Time{}, - UnbondingPeriod: time.Duration(100000000000), - VaasTimeoutPeriod: time.Duration(100000000000), - HistoricalEntries: 10000, + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: time.Time{}, + UnbondingPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, + HistoricalEntries: 10000, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, }, valid: true, }, { name: "invalid - zero duration", params: types.ConsumerInitializationParameters{ - InitialHeight: clienttypes.NewHeight(3, 4), - GenesisHash: []byte{0x01}, - BinaryHash: []byte{0x01}, - SpawnTime: now, - UnbondingPeriod: 0, - VaasTimeoutPeriod: time.Duration(100000000000), - HistoricalEntries: 10000, + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: 0, + VaasTimeoutPeriod: 15 * time.Minute, + HistoricalEntries: 10000, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, }, valid: false, }, { name: "invalid - HistoricalEntries zero", params: types.ConsumerInitializationParameters{ - InitialHeight: clienttypes.NewHeight(3, 4), - GenesisHash: []byte{0x01}, - BinaryHash: []byte{0x01}, - SpawnTime: now, - UnbondingPeriod: time.Duration(100000000000), - VaasTimeoutPeriod: time.Duration(100000000000), - HistoricalEntries: 0, + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, + HistoricalEntries: 0, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, + }, + valid: false, + }, + { + name: "invalid - zero safe mode threshold", + params: types.ConsumerInitializationParameters{ + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + VaasTimeoutPeriod: 15 * time.Minute, + HistoricalEntries: 10000, + SafeModeThreshold: 0, }, valid: false, }, @@ -752,3 +774,65 @@ func TestMsgSetConsumerFeesPerBlock_ValidateBasic(t *testing.T) { }) } } + +// TestValidateInitParams_VaasTimeoutBounds table-drives ValidateInitializationParameters +// focusing on the VaasTimeoutPeriod bound (0, MaxTimeoutDelta]: positive with no +// lower floor, capped at the ibc-go v2 hard cap. All other fields are valid so +// only the timeout is under test. +func TestValidateInitParams_VaasTimeoutBounds(t *testing.T) { + base := types.ConsumerInitializationParameters{ + InitialHeight: clienttypes.NewHeight(1, 1), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: time.Now().UTC(), + UnbondingPeriod: 21 * 24 * time.Hour, + HistoricalEntries: 1000, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, + } + + tests := []struct { + name string + timeout time.Duration + wantErr bool + }{ + { + name: "small positive value: ok (no lower floor)", + timeout: time.Second, + wantErr: false, + }, + { + name: "zero: error", + timeout: 0, + wantErr: true, + }, + { + name: "negative: error", + timeout: -time.Second, + wantErr: true, + }, + { + name: "exactly MaxTimeoutDelta: ok", + timeout: channeltypesv2.MaxTimeoutDelta, + wantErr: false, + }, + { + name: "above MaxTimeoutDelta: error", + timeout: channeltypesv2.MaxTimeoutDelta + time.Hour, + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + params := base + params.VaasTimeoutPeriod = tc.timeout + err := types.ValidateInitializationParameters(params) + if tc.wantErr { + require.Error(t, err) + require.ErrorIs(t, err, types.ErrInvalidConsumerInitializationParameters) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/x/vaas/provider/types/params.go b/x/vaas/provider/types/params.go index 7392dd8..0c8a362 100644 --- a/x/vaas/provider/types/params.go +++ b/x/vaas/provider/types/params.go @@ -7,6 +7,7 @@ import ( vaastypes "github.com/allinbits/vaas/x/vaas/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" "cosmossdk.io/math" ) @@ -20,6 +21,14 @@ const ( // as UnbondingPeriod * TrustingPeriodFraction DefaultTrustingPeriodFraction = "0.66" + // DefaultLivenessGraceFraction is the default value of the LivenessGraceFraction + // param, which sets the consumer liveness grace period as a fraction of the + // provider unbonding period (grace = unbonding * fraction). A consumer that + // produces no successful VSC ack for longer than the grace is removed. 0.66 + // mirrors the trusting-period fraction: the grace ends around the client + // recovery horizon, leaving margin below the unbonding (slashable) window. + DefaultLivenessGraceFraction = "0.66" + // DefaultBlocksPerEpoch defines the default blocks that constitute an epoch. Assuming we need 6 seconds per block, // an epoch corresponds to 1 hour (6 * 600 = 3600 seconds). // forcing int64 as the Params KeyTable expects an int64 and not int. @@ -54,6 +63,7 @@ const ( // NewParams creates new provider parameters with provided arguments func NewParams( trustingPeriodFraction string, + livenessGraceFraction string, vaasTimeoutPeriod time.Duration, blocksPerEpoch int64, feesPerBlockAmount math.Int, @@ -61,6 +71,7 @@ func NewParams( ) Params { return Params{ TrustingPeriodFraction: trustingPeriodFraction, + LivenessGraceFraction: livenessGraceFraction, VaasTimeoutPeriod: vaasTimeoutPeriod, BlocksPerEpoch: blocksPerEpoch, FeesPerBlockAmount: feesPerBlockAmount, @@ -71,6 +82,7 @@ func NewParams( func DefaultParams() Params { return NewParams( DefaultTrustingPeriodFraction, + DefaultLivenessGraceFraction, vaastypes.DefaultVAASTimeoutPeriod, DefaultBlocksPerEpoch, math.NewInt(DefaultFeesPerBlockAmount), @@ -109,6 +121,7 @@ func DefaultConsumerInitializationParameters() ConsumerInitializationParameters UnbondingPeriod: vaastypes.DefaultConsumerUnbondingPeriod, VaasTimeoutPeriod: vaastypes.DefaultVAASTimeoutPeriod, HistoricalEntries: vaastypes.DefaultHistoricalEntries, + SafeModeThreshold: vaastypes.DefaultSafeModeThreshold, } } @@ -148,7 +161,7 @@ func (p Params) Validate() error { if err := vaastypes.ValidateStringFractionNonZero(p.TrustingPeriodFraction); err != nil { return fmt.Errorf("trusting period fraction is invalid: %s", err) } - if err := vaastypes.ValidateDuration(p.VaasTimeoutPeriod); err != nil { + if err := vaastypes.ValidateVAASTimeoutPeriod(p.VaasTimeoutPeriod, channeltypesv2.MaxTimeoutDelta); err != nil { return fmt.Errorf("VAAS timeout period is invalid: %s", err) } if err := vaastypes.ValidatePositiveInt64(p.BlocksPerEpoch); err != nil { @@ -157,6 +170,9 @@ func (p Params) Validate() error { if err := validateFeesPerBlockAmount(p.FeesPerBlockAmount); err != nil { return err } + if err := vaastypes.ValidateStringFractionNonZero(p.LivenessGraceFraction); err != nil { + return fmt.Errorf("liveness grace fraction is invalid: %s", err) + } return nil } diff --git a/x/vaas/provider/types/params_test.go b/x/vaas/provider/types/params_test.go index 693d375..4dc6433 100644 --- a/x/vaas/provider/types/params_test.go +++ b/x/vaas/provider/types/params_test.go @@ -4,10 +4,13 @@ import ( "testing" "time" - "github.com/allinbits/vaas/x/vaas/provider/types" "github.com/stretchr/testify/require" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" + "cosmossdk.io/math" + + "github.com/allinbits/vaas/x/vaas/provider/types" ) func TestValidateParams(t *testing.T) { @@ -17,12 +20,14 @@ func TestValidateParams(t *testing.T) { expPass bool }{ {"default params", types.DefaultParams(), true}, - {"custom valid params", types.NewParams("0.33", time.Hour, 1000, math.NewInt(42), types.DefaultMinDepositBlocks), true}, - {"zero fees per block", types.NewParams("0.33", time.Hour, 1000, math.NewInt(0), types.DefaultMinDepositBlocks), false}, + {"custom valid params", types.NewParams("0.33", "0.5", time.Hour, 1000, math.NewInt(42), types.DefaultMinDepositBlocks), true}, + {"zero fees per block", types.NewParams("0.33", "0.5", time.Hour, 1000, math.NewInt(0), types.DefaultMinDepositBlocks), false}, {"0 trusting period fraction", types.NewParams( - "0.00", time.Hour, 1000, math.NewInt(1), types.DefaultMinDepositBlocks), false}, + "0.00", "0.5", time.Hour, 1000, math.NewInt(1), types.DefaultMinDepositBlocks), false}, + {"0 liveness grace fraction", types.NewParams( + "0.33", "0.00", time.Hour, 1000, math.NewInt(1), types.DefaultMinDepositBlocks), false}, {"0 ccv timeout period", types.NewParams( - "0.33", 0, 1000, math.NewInt(1), types.DefaultMinDepositBlocks), false}, + "0.33", "0.5", 0, 1000, math.NewInt(1), types.DefaultMinDepositBlocks), false}, } for _, tc := range testCases { @@ -35,6 +40,18 @@ func TestValidateParams(t *testing.T) { } } +func TestParamsRejectsTimeoutAboveMaxDelta(t *testing.T) { + p := types.DefaultParams() + p.VaasTimeoutPeriod = channeltypesv2.MaxTimeoutDelta + time.Hour + require.Error(t, p.Validate()) +} + +func TestParamsAcceptsTimeoutAtCap(t *testing.T) { + p := types.DefaultParams() + p.VaasTimeoutPeriod = channeltypesv2.MaxTimeoutDelta + require.NoError(t, p.Validate()) +} + func TestValidateInfractionParameters(t *testing.T) { testCases := []struct { name string diff --git a/x/vaas/provider/types/provider.pb.go b/x/vaas/provider/types/provider.pb.go index 0a03ca1..7ba882d 100644 --- a/x/vaas/provider/types/provider.pb.go +++ b/x/vaas/provider/types/provider.pb.go @@ -90,19 +90,23 @@ type Params struct { // TrustingPeriodFraction is used to compute the consumer and provider IBC // client's TrustingPeriod from the chain defined UnbondingPeriod TrustingPeriodFraction string `protobuf:"bytes,1,opt,name=trusting_period_fraction,json=trustingPeriodFraction,proto3" json:"trusting_period_fraction,omitempty"` + // LivenessGraceFraction sets the consumer liveness grace period as a fraction + // of the provider unbonding period (grace = unbonding * fraction). A consumer + // with no successful VSC acknowledgement for longer than the grace is removed. + LivenessGraceFraction string `protobuf:"bytes,2,opt,name=liveness_grace_fraction,json=livenessGraceFraction,proto3" json:"liveness_grace_fraction,omitempty"` // Sent IBC packets will timeout after this duration - VaasTimeoutPeriod time.Duration `protobuf:"bytes,2,opt,name=vaas_timeout_period,json=vaasTimeoutPeriod,proto3,stdduration" json:"vaas_timeout_period"` + VaasTimeoutPeriod time.Duration `protobuf:"bytes,3,opt,name=vaas_timeout_period,json=vaasTimeoutPeriod,proto3,stdduration" json:"vaas_timeout_period"` // The number of blocks that comprise an epoch. - BlocksPerEpoch int64 `protobuf:"varint,3,opt,name=blocks_per_epoch,json=blocksPerEpoch,proto3" json:"blocks_per_epoch,omitempty"` + BlocksPerEpoch int64 `protobuf:"varint,4,opt,name=blocks_per_epoch,json=blocksPerEpoch,proto3" json:"blocks_per_epoch,omitempty"` // The per-block fee amount charged for consumer chain operation. The denom // is not a parameter: it is fixed at module wiring (see Keeper.feeDenom) and // cannot be changed without a binary upgrade. - FeesPerBlockAmount cosmossdk_io_math.Int `protobuf:"bytes,4,opt,name=fees_per_block_amount,json=feesPerBlockAmount,proto3,customtype=cosmossdk.io/math.Int" json:"fees_per_block_amount"` + FeesPerBlockAmount cosmossdk_io_math.Int `protobuf:"bytes,5,opt,name=fees_per_block_amount,json=feesPerBlockAmount,proto3,customtype=cosmossdk.io/math.Int" json:"fees_per_block_amount"` // Minimum deposit on MsgFundConsumerFeePool, expressed as a multiplier of // fees_per_block.Amount (the floor is fees_per_block.Amount * // min_deposit_blocks). Zero disables the check. The floor applies to every // funder including the gov authority. - MinDepositBlocks uint64 `protobuf:"varint,5,opt,name=min_deposit_blocks,json=minDepositBlocks,proto3" json:"min_deposit_blocks,omitempty"` + MinDepositBlocks uint64 `protobuf:"varint,6,opt,name=min_deposit_blocks,json=minDepositBlocks,proto3" json:"min_deposit_blocks,omitempty"` } func (m *Params) Reset() { *m = Params{} } @@ -145,6 +149,13 @@ func (m *Params) GetTrustingPeriodFraction() string { return "" } +func (m *Params) GetLivenessGraceFraction() string { + if m != nil { + return m.LivenessGraceFraction + } + return "" +} + func (m *Params) GetVaasTimeoutPeriod() time.Duration { if m != nil { return m.VaasTimeoutPeriod @@ -678,6 +689,9 @@ type ConsumerInitializationParameters struct { // This param is a part of the cosmos sdk staking module. In the case of // a VAAS enabled consumer chain, the VAAS module acts as the staking module. HistoricalEntries int64 `protobuf:"varint,7,opt,name=historical_entries,json=historicalEntries,proto3" json:"historical_entries,omitempty"` + // How long the consumer may go without receiving a VSC packet before its + // tx admission gate enters safe mode (only ibc.core and gov messages pass). + SafeModeThreshold time.Duration `protobuf:"bytes,8,opt,name=safe_mode_threshold,json=safeModeThreshold,proto3,stdduration" json:"safe_mode_threshold"` } func (m *ConsumerInitializationParameters) Reset() { *m = ConsumerInitializationParameters{} } @@ -762,6 +776,13 @@ func (m *ConsumerInitializationParameters) GetHistoricalEntries() int64 { return 0 } +func (m *ConsumerInitializationParameters) GetSafeModeThreshold() time.Duration { + if m != nil { + return m.SafeModeThreshold + } + return 0 +} + // ConsumerIds contains consumer ids of chains type ConsumerIds struct { Ids []uint64 `protobuf:"varint,1,rep,packed,name=ids,proto3" json:"ids,omitempty"` @@ -947,94 +968,98 @@ func init() { func init() { proto.RegisterFile("vaas/provider/v1/provider.proto", fileDescriptor_6404dd5d21545279) } var fileDescriptor_6404dd5d21545279 = []byte{ - // 1386 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x56, 0xcf, 0x6f, 0x1b, 0xc5, - 0x17, 0xcf, 0xc6, 0x6e, 0x9b, 0x8c, 0x93, 0x7c, 0xdd, 0x69, 0xda, 0x3a, 0x69, 0xeb, 0xf8, 0x6b, - 0x54, 0x14, 0x15, 0xba, 0x8b, 0x8b, 0x84, 0x10, 0x1c, 0x2a, 0xff, 0xd8, 0x36, 0x26, 0x69, 0x6a, - 0xad, 0x5d, 0x90, 0x2a, 0xd0, 0x6a, 0x76, 0x77, 0x6a, 0x4f, 0xb3, 0x3b, 0xb3, 0xda, 0x19, 0x3b, - 0x98, 0x33, 0x07, 0x8e, 0xe5, 0xc6, 0x11, 0x89, 0x0b, 0x47, 0x0e, 0x88, 0x23, 0xe7, 0x72, 0xab, - 0x10, 0x07, 0x84, 0x50, 0x41, 0xed, 0x81, 0xff, 0x81, 0x13, 0x9a, 0x99, 0xdd, 0x8d, 0x9b, 0xa4, - 0x22, 0x15, 0x97, 0xd5, 0xcc, 0xe7, 0xbd, 0xcf, 0x9b, 0x37, 0xef, 0xbd, 0x7d, 0x6f, 0xc0, 0xc6, - 0x04, 0x21, 0x6e, 0xc5, 0x09, 0x9b, 0x90, 0x00, 0x27, 0xd6, 0xa4, 0x91, 0xaf, 0xcd, 0x38, 0x61, - 0x82, 0xc1, 0xb2, 0x54, 0x30, 0x73, 0x70, 0xd2, 0x58, 0x3f, 0x8b, 0x22, 0x42, 0x99, 0xa5, 0xbe, - 0x5a, 0x69, 0x7d, 0xcd, 0x67, 0x3c, 0x62, 0xdc, 0x55, 0x3b, 0x4b, 0x6f, 0x52, 0xd1, 0xea, 0x90, - 0x0d, 0x99, 0xc6, 0xe5, 0x2a, 0x45, 0x5f, 0xd7, 0x3a, 0x16, 0x96, 0x56, 0xa9, 0x8f, 0xad, 0x49, - 0xc3, 0xc3, 0x02, 0x35, 0x72, 0x20, 0xd5, 0xab, 0x0e, 0x19, 0x1b, 0x86, 0xd8, 0x52, 0x3b, 0x6f, - 0xfc, 0xc0, 0x0a, 0xc6, 0x09, 0x12, 0x84, 0xd1, 0x54, 0xbe, 0x71, 0x58, 0x2e, 0x48, 0x84, 0xb9, - 0x40, 0x51, 0x9c, 0x29, 0x10, 0xcf, 0xb7, 0x7c, 0x96, 0x60, 0xcb, 0x0f, 0x09, 0xa6, 0x42, 0xde, - 0x50, 0xaf, 0x52, 0x05, 0x4b, 0x2a, 0x84, 0x64, 0x38, 0x12, 0x1a, 0xe6, 0x96, 0xc0, 0x34, 0xc0, - 0x49, 0x44, 0xb4, 0xf2, 0xc1, 0x2e, 0x25, 0x40, 0x15, 0xb1, 0x49, 0xc3, 0xda, 0x27, 0x49, 0xe6, - 0xe6, 0xe5, 0x19, 0x8e, 0x9f, 0x4c, 0x63, 0xc1, 0xac, 0x3d, 0x3c, 0x4d, 0x43, 0x50, 0xff, 0x65, - 0x1e, 0x9c, 0xee, 0xa1, 0x04, 0x45, 0x1c, 0xbe, 0x0b, 0x2a, 0x22, 0x19, 0x73, 0x41, 0xe8, 0xd0, - 0x8d, 0x71, 0x42, 0x58, 0xe0, 0x3e, 0x48, 0x90, 0x2f, 0x6f, 0x54, 0x31, 0x6a, 0xc6, 0xe6, 0xa2, - 0x73, 0x21, 0x93, 0xf7, 0x94, 0xf8, 0x56, 0x2a, 0x85, 0x7d, 0x70, 0x4e, 0x1e, 0xec, 0xca, 0x0b, - 0xb2, 0xb1, 0x48, 0xd9, 0x95, 0xf9, 0x9a, 0xb1, 0x59, 0xba, 0xb1, 0x66, 0xea, 0x38, 0x98, 0x59, - 0x1c, 0xcc, 0x4e, 0x1a, 0xa7, 0xd6, 0xc2, 0xe3, 0xa7, 0x1b, 0x73, 0x5f, 0xfd, 0xb1, 0x61, 0x38, - 0x67, 0x25, 0x7f, 0xa0, 0xe9, 0xda, 0x38, 0xdc, 0x04, 0x65, 0x2f, 0x64, 0xfe, 0x1e, 0x97, 0xe6, - 0x5c, 0x1c, 0x33, 0x7f, 0x54, 0x29, 0xd4, 0x8c, 0xcd, 0x82, 0xb3, 0xa2, 0xf1, 0x1e, 0x4e, 0x6c, - 0x89, 0x42, 0x1f, 0x9c, 0x7f, 0x80, 0xb1, 0xd6, 0x53, 0x22, 0x17, 0x45, 0x6c, 0x4c, 0x45, 0xa5, - 0x28, 0xbd, 0x6e, 0xbd, 0x25, 0x4f, 0xf9, 0xed, 0xe9, 0xc6, 0x79, 0x9d, 0x57, 0x1e, 0xec, 0x99, - 0x84, 0x59, 0x11, 0x12, 0x23, 0xb3, 0x4b, 0xc5, 0xcf, 0xdf, 0x5f, 0x07, 0x69, 0x51, 0x74, 0xa9, - 0xf8, 0xf6, 0xaf, 0xef, 0xae, 0x19, 0x0e, 0x94, 0xe6, 0x7a, 0x38, 0x69, 0x49, 0x63, 0x4d, 0x65, - 0x0b, 0xbe, 0x09, 0x60, 0x44, 0xa8, 0x1b, 0xe0, 0x98, 0x71, 0x22, 0xf4, 0x39, 0xbc, 0x72, 0xaa, - 0x66, 0x6c, 0x16, 0x9d, 0x72, 0x44, 0x68, 0x47, 0x0b, 0x14, 0x85, 0xd7, 0xdf, 0x00, 0xa5, 0x66, - 0x10, 0x24, 0x98, 0xf3, 0x1d, 0xc2, 0x05, 0xbc, 0x0c, 0x16, 0x91, 0xde, 0x62, 0x5e, 0x31, 0x6a, - 0x85, 0xcd, 0x25, 0xe7, 0x00, 0xa8, 0x7f, 0x0c, 0xd6, 0x3e, 0x44, 0x21, 0x09, 0x90, 0x60, 0x49, - 0x1f, 0x8b, 0xf6, 0x08, 0xd1, 0x21, 0xee, 0x21, 0x7f, 0x0f, 0x0b, 0x0e, 0x6f, 0x82, 0x62, 0x48, - 0xb8, 0x50, 0xac, 0xd2, 0x8d, 0xab, 0xa6, 0x2a, 0xf9, 0x49, 0xc3, 0x7c, 0x19, 0xa3, 0x83, 0x04, - 0x6a, 0x15, 0xe5, 0x95, 0x1d, 0x45, 0xac, 0x7f, 0x69, 0x80, 0xca, 0x36, 0x9e, 0x36, 0x39, 0x27, - 0x43, 0x1a, 0x61, 0x2a, 0x1c, 0x1c, 0x87, 0xc8, 0xc7, 0x72, 0x09, 0x5f, 0x03, 0xcb, 0xd9, 0xef, - 0xe3, 0x4a, 0x87, 0x54, 0xa2, 0x97, 0x9c, 0xa5, 0x0c, 0x94, 0x97, 0x80, 0xef, 0x01, 0x10, 0x27, - 0x78, 0xe2, 0xfa, 0xee, 0x1e, 0x9e, 0xa6, 0x59, 0xbd, 0x6c, 0xce, 0x14, 0x9f, 0x2e, 0x2b, 0xb3, - 0x37, 0xf6, 0x42, 0xe2, 0x6f, 0xe3, 0xa9, 0xb3, 0x20, 0xf5, 0xdb, 0xdb, 0x78, 0x0a, 0x57, 0xc1, - 0xa9, 0x98, 0xed, 0xe3, 0x24, 0x4d, 0x9d, 0xde, 0xd4, 0xbf, 0x36, 0xc0, 0xc5, 0xfc, 0x02, 0x6d, - 0x46, 0xf9, 0x38, 0xc2, 0x49, 0x6f, 0xec, 0x49, 0xc6, 0x06, 0x28, 0xf9, 0x29, 0xe2, 0x92, 0x40, - 0x39, 0x54, 0x74, 0x40, 0x06, 0x75, 0x83, 0xa3, 0x3e, 0xcf, 0x1f, 0xe3, 0xf3, 0x4d, 0xb0, 0x94, - 0x5b, 0x91, 0x5e, 0x17, 0x4e, 0xe0, 0x75, 0x7e, 0xee, 0x36, 0x9e, 0xd6, 0x3f, 0x9f, 0x75, 0xb1, - 0x35, 0xcd, 0x9c, 0x54, 0xc6, 0x4f, 0xe2, 0x62, 0xae, 0x30, 0xeb, 0xa2, 0x3f, 0x6b, 0xe5, 0xc8, - 0x3d, 0x0a, 0x47, 0xef, 0x51, 0xff, 0xd1, 0x00, 0xab, 0xb3, 0x67, 0xf3, 0x01, 0xeb, 0x25, 0x63, - 0x8a, 0xff, 0xdd, 0x87, 0x9b, 0x60, 0x21, 0x96, 0x9a, 0xae, 0xe0, 0x69, 0xce, 0xd6, 0x8f, 0xfc, - 0x89, 0x83, 0xac, 0x23, 0xe9, 0x5f, 0xf1, 0x91, 0xfc, 0x15, 0xcf, 0x28, 0xd6, 0x80, 0xc3, 0x0e, - 0x58, 0x79, 0xe1, 0x12, 0x3c, 0x0d, 0xe2, 0x15, 0xf3, 0x70, 0xdb, 0x35, 0x67, 0x6a, 0xdd, 0x59, - 0x9e, 0xbd, 0x24, 0xaf, 0xff, 0x60, 0x00, 0x28, 0x2f, 0x80, 0x29, 0x1f, 0xf3, 0x3c, 0xa0, 0xf2, - 0x77, 0xca, 0x2f, 0x2f, 0x09, 0xb3, 0xd5, 0x57, 0xce, 0x24, 0x92, 0xa7, 0x42, 0x95, 0x57, 0xd1, - 0xfc, 0x4c, 0x15, 0xc1, 0xf7, 0x01, 0x88, 0x55, 0xf2, 0x4e, 0x9c, 0xe1, 0xc5, 0x38, 0x5b, 0xca, - 0xf8, 0x3d, 0x64, 0x84, 0xba, 0x23, 0x2c, 0xfb, 0xab, 0x6a, 0x15, 0x05, 0x07, 0x48, 0x68, 0x4b, - 0x21, 0xf5, 0x00, 0x94, 0xb3, 0xc0, 0xdf, 0xc1, 0x02, 0x05, 0x48, 0x20, 0x08, 0x41, 0x91, 0xa2, - 0x08, 0xa7, 0xed, 0x50, 0xad, 0x61, 0x0d, 0x94, 0x02, 0xcc, 0xfd, 0x84, 0xc4, 0xaa, 0x53, 0xce, - 0x2b, 0xd1, 0x2c, 0x04, 0xd7, 0xc1, 0x42, 0x94, 0x5a, 0x50, 0x5e, 0x2e, 0x3a, 0xf9, 0xbe, 0xfe, - 0xb8, 0x00, 0x6a, 0xd9, 0x31, 0x5d, 0x4a, 0x04, 0x41, 0x21, 0xf9, 0x4c, 0x75, 0x47, 0xd5, 0x95, - 0xb1, 0xc0, 0x09, 0x87, 0xb7, 0xc1, 0x0a, 0xd1, 0xb2, 0xcc, 0x5d, 0x23, 0x4d, 0x28, 0xf1, 0x7c, - 0x53, 0x4e, 0x10, 0x33, 0x9d, 0x1b, 0x93, 0x86, 0xa9, 0xdd, 0x4f, 0x5b, 0xc0, 0x72, 0xca, 0xd3, - 0x20, 0xfc, 0x3f, 0x58, 0x1a, 0x62, 0x8a, 0x39, 0xe1, 0xee, 0x08, 0xf1, 0x51, 0x5a, 0x96, 0xa5, - 0x14, 0xdb, 0x42, 0x7c, 0x24, 0xe3, 0xe2, 0x11, 0x8a, 0x92, 0xa9, 0xd6, 0xd0, 0x35, 0x09, 0x34, - 0xa4, 0x14, 0xda, 0x00, 0xf0, 0x18, 0xed, 0x53, 0xd5, 0xed, 0x55, 0xdc, 0x4e, 0x5a, 0x59, 0x8b, - 0x8a, 0x27, 0x25, 0x70, 0x17, 0x94, 0xc7, 0xd4, 0x63, 0x34, 0x38, 0x18, 0x36, 0xaa, 0x97, 0x9e, - 0x70, 0x5c, 0xfc, 0x2f, 0x27, 0xa7, 0xc3, 0xe2, 0x25, 0x13, 0xe8, 0xf4, 0x7f, 0x9a, 0x40, 0xd7, - 0x01, 0x1c, 0x11, 0x2e, 0x58, 0x42, 0x7c, 0x14, 0xba, 0x98, 0x8a, 0x84, 0x60, 0x5e, 0x39, 0xa3, - 0x2a, 0xe5, 0xec, 0x81, 0xc4, 0xd6, 0x82, 0xfa, 0x06, 0x28, 0xe5, 0x99, 0x0c, 0x38, 0x2c, 0x83, - 0x02, 0x09, 0x74, 0xb7, 0x2f, 0x3a, 0x72, 0x59, 0xff, 0xdb, 0x00, 0xab, 0x5d, 0x9a, 0xcd, 0xd4, - 0x99, 0xfc, 0xde, 0x02, 0xa5, 0x80, 0x8d, 0xbd, 0x10, 0xbb, 0xb2, 0x47, 0xa7, 0xc9, 0xbd, 0x7a, - 0xf4, 0x37, 0xeb, 0x87, 0x88, 0x8f, 0x3e, 0x40, 0x24, 0x3c, 0xe0, 0x3a, 0x40, 0x33, 0xfb, 0x64, - 0x48, 0x61, 0x13, 0x2c, 0x04, 0x6c, 0x9f, 0xaa, 0xc4, 0xcc, 0xbf, 0x8a, 0x91, 0x9c, 0x06, 0x3f, - 0x02, 0xe7, 0xb3, 0xb5, 0x3b, 0x4c, 0x90, 0x8f, 0xb3, 0x50, 0x16, 0x4e, 0x1e, 0xca, 0x73, 0x99, - 0x85, 0xdb, 0xd2, 0x80, 0x0e, 0x66, 0xfd, 0x77, 0x03, 0x9c, 0x3b, 0xe6, 0x68, 0xf8, 0x09, 0x58, - 0xe1, 0x12, 0x7e, 0xf1, 0xad, 0xb1, 0xd4, 0x7a, 0x27, 0x9d, 0xda, 0x97, 0x8e, 0x4e, 0xed, 0x1d, - 0x3c, 0x44, 0xfe, 0xb4, 0x83, 0xfd, 0x99, 0xd9, 0xdd, 0xc1, 0xbe, 0x9e, 0xdd, 0xcb, 0xca, 0x5a, - 0xfe, 0x34, 0xd9, 0x02, 0xcb, 0x0f, 0x11, 0x09, 0xdd, 0xec, 0x6d, 0xf6, 0x2a, 0x8f, 0x92, 0x25, - 0xc9, 0xcc, 0x70, 0x39, 0xc3, 0x05, 0x8b, 0x3c, 0x2e, 0x18, 0xc5, 0x2a, 0x1a, 0x0b, 0xce, 0x01, - 0x70, 0xed, 0x27, 0x03, 0x2c, 0xe7, 0x83, 0x6c, 0x84, 0x38, 0x86, 0x55, 0xb0, 0xde, 0xbe, 0xbb, - 0xdb, 0xbf, 0x77, 0xc7, 0x76, 0xdc, 0xde, 0x56, 0xb3, 0x6f, 0xbb, 0xf7, 0x76, 0xfb, 0x3d, 0xbb, - 0xdd, 0xbd, 0xd5, 0xb5, 0x3b, 0xe5, 0x39, 0x78, 0x05, 0xac, 0x1d, 0x92, 0x3b, 0xf6, 0xed, 0x6e, - 0x7f, 0x60, 0x3b, 0x76, 0xa7, 0x6c, 0x1c, 0x43, 0xef, 0xee, 0x76, 0x07, 0xdd, 0xe6, 0x4e, 0xf7, - 0xbe, 0xdd, 0x29, 0xcf, 0xc3, 0x4b, 0xe0, 0xe2, 0x21, 0xf9, 0x4e, 0xf3, 0xde, 0x6e, 0x7b, 0xcb, - 0xee, 0x94, 0x0b, 0x70, 0x1d, 0x5c, 0x38, 0x24, 0xec, 0x0f, 0xee, 0xf6, 0x7a, 0x76, 0xa7, 0x5c, - 0x3c, 0x46, 0xd6, 0xb1, 0x77, 0xec, 0x81, 0xdd, 0x29, 0x9f, 0x5a, 0x2f, 0x7e, 0xf1, 0x4d, 0x75, - 0xae, 0xd5, 0x7d, 0xfc, 0xac, 0x6a, 0x3c, 0x79, 0x56, 0x35, 0xfe, 0x7c, 0x56, 0x35, 0x1e, 0x3d, - 0xaf, 0xce, 0x3d, 0x79, 0x5e, 0x9d, 0xfb, 0xf5, 0x79, 0x75, 0xee, 0xbe, 0x35, 0x24, 0x62, 0x34, - 0xf6, 0x4c, 0x9f, 0x45, 0x16, 0x0a, 0x43, 0x42, 0x3d, 0x22, 0xb8, 0xa5, 0x1e, 0x9d, 0x9f, 0x5a, - 0x2f, 0xbe, 0xd6, 0xc5, 0x34, 0xc6, 0xdc, 0x3b, 0xad, 0xe2, 0xfb, 0xf6, 0x3f, 0x01, 0x00, 0x00, - 0xff, 0xff, 0x52, 0x04, 0xde, 0xed, 0xcb, 0x0b, 0x00, 0x00, + // 1441 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x57, 0x31, 0x6f, 0x1b, 0x47, + 0x16, 0xd6, 0x8a, 0xb4, 0x2c, 0x0d, 0x25, 0x1d, 0x3d, 0x96, 0x6c, 0x4a, 0xb6, 0x29, 0x1d, 0x0f, + 0x3e, 0x08, 0xbe, 0xf3, 0xee, 0xc9, 0x07, 0x18, 0x87, 0x4b, 0x61, 0x48, 0x22, 0x6d, 0x31, 0x92, + 0x65, 0x62, 0x49, 0x27, 0x80, 0x91, 0x60, 0x31, 0xdc, 0x7d, 0x22, 0xc7, 0xda, 0x9d, 0x59, 0xec, + 0x0c, 0xa9, 0x30, 0x75, 0x8a, 0x14, 0x29, 0x9c, 0x2e, 0x4d, 0x80, 0x00, 0x69, 0x52, 0xa6, 0x08, + 0x52, 0xa6, 0x76, 0x3a, 0x23, 0x55, 0x10, 0x04, 0x4e, 0x60, 0x17, 0xf9, 0x0f, 0xa9, 0x82, 0x99, + 0xd9, 0x5d, 0xd1, 0x92, 0x8c, 0xc8, 0x48, 0x43, 0xcc, 0xbc, 0xf7, 0xbe, 0x37, 0xdf, 0x7b, 0xfb, + 0xe6, 0xcd, 0x23, 0x5a, 0x19, 0x12, 0x22, 0x9c, 0x38, 0xe1, 0x43, 0x1a, 0x40, 0xe2, 0x0c, 0xd7, + 0xf3, 0xb5, 0x1d, 0x27, 0x5c, 0x72, 0x5c, 0x56, 0x06, 0x76, 0x2e, 0x1c, 0xae, 0x2f, 0x5f, 0x20, + 0x11, 0x65, 0xdc, 0xd1, 0xbf, 0xc6, 0x68, 0x79, 0xc9, 0xe7, 0x22, 0xe2, 0xc2, 0xd3, 0x3b, 0xc7, + 0x6c, 0x52, 0xd5, 0x42, 0x8f, 0xf7, 0xb8, 0x91, 0xab, 0x55, 0x2a, 0xfd, 0xa7, 0xb1, 0x71, 0x40, + 0x79, 0x65, 0x3e, 0x38, 0xc3, 0xf5, 0x2e, 0x48, 0xb2, 0x9e, 0x0b, 0x52, 0xbb, 0x6a, 0x8f, 0xf3, + 0x5e, 0x08, 0x8e, 0xde, 0x75, 0x07, 0xfb, 0x4e, 0x30, 0x48, 0x88, 0xa4, 0x9c, 0xa5, 0xfa, 0x95, + 0xe3, 0x7a, 0x49, 0x23, 0x10, 0x92, 0x44, 0x71, 0x66, 0x40, 0xbb, 0xbe, 0xe3, 0xf3, 0x04, 0x1c, + 0x3f, 0xa4, 0xc0, 0xa4, 0x8a, 0xd0, 0xac, 0x52, 0x03, 0x47, 0x19, 0x84, 0xb4, 0xd7, 0x97, 0x46, + 0x2c, 0x1c, 0x09, 0x2c, 0x80, 0x24, 0xa2, 0xc6, 0xf8, 0x68, 0x97, 0x02, 0xb0, 0xce, 0xd8, 0x70, + 0xdd, 0x39, 0xa4, 0x49, 0x46, 0xf3, 0xea, 0x18, 0xc6, 0x4f, 0x46, 0xb1, 0xe4, 0xce, 0x01, 0x8c, + 0xd2, 0x14, 0xd4, 0x3e, 0x29, 0xa0, 0xa9, 0x16, 0x49, 0x48, 0x24, 0xf0, 0xff, 0x50, 0x45, 0x26, + 0x03, 0x21, 0x29, 0xeb, 0x79, 0x31, 0x24, 0x94, 0x07, 0xde, 0x7e, 0x42, 0x7c, 0x15, 0x51, 0xc5, + 0x5a, 0xb5, 0xd6, 0x66, 0xdc, 0x4b, 0x99, 0xbe, 0xa5, 0xd5, 0x77, 0x53, 0x2d, 0xbe, 0x8d, 0x2e, + 0x87, 0x74, 0x08, 0x0c, 0x84, 0xf0, 0x7a, 0x09, 0xf1, 0xe1, 0x08, 0x38, 0xa9, 0x81, 0x8b, 0x99, + 0xfa, 0x9e, 0xd2, 0xe6, 0xb8, 0x36, 0xba, 0xa8, 0x08, 0x7b, 0x2a, 0x31, 0x7c, 0x20, 0xd3, 0x53, + 0x2b, 0x85, 0x55, 0x6b, 0xad, 0x74, 0x6b, 0xc9, 0x36, 0xf9, 0xb3, 0xb3, 0xfc, 0xd9, 0xf5, 0x34, + 0xbf, 0x9b, 0xd3, 0x4f, 0x9f, 0xaf, 0x4c, 0x7c, 0xf6, 0xcb, 0x8a, 0xe5, 0x5e, 0x50, 0xf8, 0x8e, + 0x81, 0x1b, 0x52, 0x78, 0x0d, 0x95, 0xbb, 0x21, 0xf7, 0x0f, 0x84, 0x72, 0xe7, 0x41, 0xcc, 0xfd, + 0x7e, 0xa5, 0xb8, 0x6a, 0xad, 0x15, 0xdc, 0x79, 0x23, 0x6f, 0x41, 0xd2, 0x50, 0x52, 0xec, 0xa3, + 0xc5, 0x7d, 0x00, 0x63, 0xa7, 0x55, 0x1e, 0x89, 0xf8, 0x80, 0xc9, 0xca, 0x39, 0x45, 0x7a, 0xf3, + 0x3f, 0xea, 0x94, 0x9f, 0x9e, 0xaf, 0x2c, 0x9a, 0x7a, 0x10, 0xc1, 0x81, 0x4d, 0xb9, 0x13, 0x11, + 0xd9, 0xb7, 0x9b, 0x4c, 0xfe, 0xf0, 0xcd, 0x4d, 0x94, 0x16, 0x53, 0x93, 0xc9, 0xaf, 0x7e, 0xfb, + 0xfa, 0x86, 0xe5, 0x62, 0xe5, 0xae, 0x05, 0xc9, 0xa6, 0x72, 0xb6, 0xa1, 0x7d, 0xe1, 0x7f, 0x23, + 0x1c, 0x51, 0xe6, 0x05, 0x10, 0x73, 0x41, 0xa5, 0x39, 0x47, 0x54, 0xa6, 0x56, 0xad, 0xb5, 0xa2, + 0x5b, 0x8e, 0x28, 0xab, 0x1b, 0x85, 0x86, 0x88, 0xda, 0xbf, 0x50, 0x69, 0x23, 0x08, 0x12, 0x10, + 0x62, 0x97, 0x0a, 0x89, 0xaf, 0xa2, 0x19, 0x62, 0xb6, 0x20, 0x2a, 0xd6, 0x6a, 0x61, 0x6d, 0xd6, + 0x3d, 0x12, 0xd4, 0xde, 0x43, 0x4b, 0xef, 0x90, 0x90, 0x06, 0x44, 0xf2, 0xa4, 0x0d, 0x72, 0xab, + 0x4f, 0x58, 0x0f, 0x5a, 0xc4, 0x3f, 0x00, 0x29, 0xf0, 0x1d, 0x54, 0x0c, 0xa9, 0x90, 0x1a, 0x55, + 0xba, 0x75, 0xdd, 0xd6, 0x57, 0x65, 0xb8, 0x6e, 0xbf, 0x0e, 0x51, 0x27, 0x92, 0x6c, 0x16, 0x55, + 0xc8, 0xae, 0x06, 0xd6, 0x3e, 0xb5, 0x50, 0x65, 0x07, 0x46, 0x1b, 0x42, 0xd0, 0x1e, 0x8b, 0x80, + 0x49, 0x17, 0xe2, 0x90, 0xf8, 0xa0, 0x96, 0xf8, 0x1f, 0x68, 0x2e, 0xbb, 0x76, 0x9e, 0x22, 0xa4, + 0x0b, 0x64, 0xd6, 0x9d, 0xcd, 0x84, 0x2a, 0x08, 0xfc, 0x7f, 0x84, 0xe2, 0x04, 0x86, 0x9e, 0xef, + 0x1d, 0xc0, 0x48, 0x57, 0x42, 0xe9, 0xd6, 0x55, 0x7b, 0xac, 0x68, 0x4d, 0x39, 0xda, 0xad, 0x41, + 0x37, 0xa4, 0xfe, 0x0e, 0x8c, 0xdc, 0x69, 0x65, 0xbf, 0xb5, 0x03, 0x23, 0xbc, 0x80, 0xce, 0xc5, + 0xfc, 0x10, 0x12, 0x5d, 0x0c, 0x05, 0xd7, 0x6c, 0x6a, 0x5f, 0x58, 0xe8, 0x72, 0x1e, 0xc0, 0x16, + 0x67, 0x62, 0x10, 0x41, 0xd2, 0x1a, 0x74, 0x15, 0x62, 0x05, 0x95, 0xfc, 0x54, 0xe2, 0xd1, 0x40, + 0x13, 0x2a, 0xba, 0x28, 0x13, 0x35, 0x83, 0x93, 0x9c, 0x27, 0x4f, 0xe1, 0x7c, 0x07, 0xcd, 0xe6, + 0x5e, 0x14, 0xeb, 0xc2, 0x19, 0x58, 0xe7, 0xe7, 0xee, 0xc0, 0xa8, 0xf6, 0xd1, 0x38, 0xc5, 0xcd, + 0x51, 0x46, 0x52, 0x3b, 0x3f, 0x0b, 0xc5, 0xdc, 0x60, 0x9c, 0xa2, 0x3f, 0xee, 0xe5, 0x44, 0x1c, + 0x85, 0x93, 0x71, 0xd4, 0xbe, 0xb3, 0xd0, 0xc2, 0xf8, 0xd9, 0xa2, 0xc3, 0x5b, 0xc9, 0x80, 0xc1, + 0x9f, 0x73, 0xb8, 0x83, 0xa6, 0x63, 0x65, 0xe9, 0x49, 0x91, 0x7e, 0xb3, 0xe5, 0x13, 0x37, 0xb1, + 0x93, 0x75, 0x32, 0x73, 0x15, 0x9f, 0xa8, 0xab, 0x78, 0x5e, 0xa3, 0x3a, 0x02, 0xd7, 0xd1, 0xfc, + 0x2b, 0x41, 0x88, 0x34, 0x89, 0xd7, 0xec, 0xe3, 0xed, 0xda, 0x1e, 0xab, 0x75, 0x77, 0x6e, 0x3c, + 0x48, 0x51, 0xfb, 0xd6, 0x42, 0x58, 0x05, 0x00, 0x4c, 0x0c, 0x44, 0x9e, 0x50, 0x75, 0x9d, 0xf2, + 0xe0, 0x15, 0x60, 0xbc, 0xfa, 0xca, 0x99, 0x46, 0xe1, 0x74, 0xaa, 0xf2, 0x2a, 0x9a, 0x1c, 0xab, + 0x22, 0xfc, 0x16, 0x42, 0xb1, 0xfe, 0x78, 0x67, 0xfe, 0xc2, 0x33, 0x71, 0xb6, 0x54, 0xf9, 0x7b, + 0xcc, 0x29, 0xf3, 0xfa, 0xa0, 0xfa, 0x72, 0xda, 0x59, 0x90, 0x12, 0x6d, 0x6b, 0x49, 0x2d, 0x40, + 0xe5, 0x2c, 0xf1, 0xf7, 0x41, 0x92, 0x80, 0x48, 0x82, 0x31, 0x2a, 0x32, 0x12, 0x41, 0xda, 0x46, + 0xf5, 0x1a, 0xaf, 0xa2, 0x52, 0x00, 0xc2, 0x4f, 0x68, 0x3c, 0xd6, 0x28, 0xc7, 0x45, 0x78, 0x19, + 0x4d, 0x47, 0xa9, 0x07, 0xcd, 0x72, 0xc6, 0xcd, 0xf7, 0xb5, 0xcf, 0x8b, 0x68, 0x35, 0x3b, 0xa6, + 0xc9, 0xa8, 0xa4, 0x24, 0xa4, 0x1f, 0xea, 0xee, 0xa8, 0xbb, 0x39, 0x48, 0x48, 0x04, 0xbe, 0x87, + 0xe6, 0xa9, 0xd1, 0x65, 0x74, 0xad, 0xf4, 0x83, 0xd2, 0xae, 0x6f, 0xab, 0x97, 0xc7, 0x4e, 0xdf, + 0x9b, 0xe1, 0xba, 0x6d, 0xe8, 0xa7, 0x2d, 0x60, 0x2e, 0xc5, 0x19, 0x21, 0xfe, 0x3b, 0x9a, 0xed, + 0xa9, 0xfe, 0x4d, 0x85, 0xd7, 0x27, 0xa2, 0x9f, 0x96, 0x65, 0x29, 0x95, 0x6d, 0x13, 0xd1, 0x57, + 0x79, 0xe9, 0x52, 0x46, 0x92, 0x91, 0xb1, 0x30, 0x35, 0x89, 0x8c, 0x48, 0x1b, 0x6c, 0x21, 0x24, + 0x62, 0x72, 0xc8, 0x74, 0xb7, 0xd7, 0x79, 0x3b, 0x6b, 0x65, 0xcd, 0x68, 0x9c, 0xd2, 0xe0, 0x3d, + 0x54, 0x1e, 0xb0, 0x2e, 0x67, 0xc1, 0xd1, 0x23, 0xa5, 0xbb, 0xf5, 0x19, 0x9f, 0x8b, 0xbf, 0xe5, + 0xe0, 0xf4, 0xb1, 0x78, 0xcd, 0x0b, 0x34, 0xf5, 0x97, 0x5e, 0xa0, 0x9b, 0x08, 0xf7, 0xa9, 0x90, + 0x3c, 0xa1, 0x3e, 0x09, 0x3d, 0x60, 0x32, 0xa1, 0x20, 0x2a, 0xe7, 0x75, 0xa5, 0x5c, 0x38, 0xd2, + 0x34, 0x8c, 0x42, 0x71, 0x10, 0x64, 0x1f, 0xbc, 0x88, 0x07, 0xe0, 0xc9, 0x7e, 0x02, 0xa2, 0xcf, + 0xc3, 0xa0, 0x32, 0xfd, 0x06, 0x1c, 0x14, 0xfe, 0x3e, 0x0f, 0xa0, 0x93, 0xa1, 0x6b, 0x2b, 0xa8, + 0x94, 0x97, 0x47, 0x20, 0x70, 0x19, 0x15, 0x68, 0x60, 0x9e, 0x90, 0xa2, 0xab, 0x96, 0xb5, 0xdf, + 0x2d, 0xb4, 0xd0, 0x64, 0xd9, 0x3b, 0x3d, 0x56, 0x34, 0x77, 0x51, 0x29, 0xe0, 0x83, 0x6e, 0x08, + 0x9e, 0x6a, 0xfc, 0x69, 0xc5, 0x5c, 0x3f, 0x79, 0x77, 0xdb, 0x21, 0x11, 0xfd, 0xb7, 0x09, 0x0d, + 0x8f, 0xb0, 0x2e, 0x32, 0xc8, 0x36, 0xed, 0x31, 0xbc, 0x81, 0xa6, 0x03, 0x7e, 0xc8, 0xf4, 0xd7, + 0x9e, 0x7c, 0x13, 0x27, 0x39, 0x0c, 0xbf, 0x8b, 0x16, 0xb3, 0x75, 0x3a, 0x57, 0xbc, 0xf9, 0x84, + 0x70, 0x31, 0xf3, 0xa0, 0x47, 0x0f, 0xf3, 0x85, 0x6a, 0x3f, 0x5b, 0xe8, 0xe2, 0x29, 0x47, 0xe3, + 0xf7, 0xd1, 0xbc, 0x50, 0xe2, 0x57, 0x07, 0x9f, 0xd9, 0xcd, 0xdb, 0xe9, 0x28, 0x70, 0xe5, 0xe4, + 0x28, 0xb0, 0x0b, 0x3d, 0xe2, 0x8f, 0xea, 0xe0, 0x8f, 0x0d, 0x04, 0x75, 0xf0, 0xcd, 0x40, 0x30, + 0xa7, 0xbd, 0xe5, 0xf3, 0xce, 0x36, 0x9a, 0x7b, 0x4c, 0x68, 0xe8, 0x65, 0x83, 0x62, 0x9a, 0x97, + 0x33, 0xc5, 0x31, 0xab, 0x90, 0x99, 0x5c, 0x0d, 0x06, 0x92, 0x47, 0x5d, 0x21, 0x39, 0x03, 0x9d, + 0x8d, 0x69, 0xf7, 0x48, 0x70, 0xe3, 0x7b, 0x0b, 0xcd, 0xe5, 0xaf, 0x63, 0x9f, 0x08, 0xc0, 0x55, + 0xb4, 0xbc, 0xf5, 0x60, 0xaf, 0xfd, 0xf0, 0x7e, 0xc3, 0xf5, 0x5a, 0xdb, 0x1b, 0xed, 0x86, 0xf7, + 0x70, 0xaf, 0xdd, 0x6a, 0x6c, 0x35, 0xef, 0x36, 0x1b, 0xf5, 0xf2, 0x04, 0xbe, 0x86, 0x96, 0x8e, + 0xe9, 0xdd, 0xc6, 0xbd, 0x66, 0xbb, 0xd3, 0x70, 0x1b, 0xf5, 0xb2, 0x75, 0x0a, 0xbc, 0xb9, 0xd7, + 0xec, 0x34, 0x37, 0x76, 0x9b, 0x8f, 0x1a, 0xf5, 0xf2, 0x24, 0xbe, 0x82, 0x2e, 0x1f, 0xd3, 0xef, + 0x6e, 0x3c, 0xdc, 0xdb, 0xda, 0x6e, 0xd4, 0xcb, 0x05, 0xbc, 0x8c, 0x2e, 0x1d, 0x53, 0xb6, 0x3b, + 0x0f, 0x5a, 0xad, 0x46, 0xbd, 0x5c, 0x3c, 0x45, 0x57, 0x6f, 0xec, 0x36, 0x3a, 0x8d, 0x7a, 0xf9, + 0xdc, 0x72, 0xf1, 0xe3, 0x2f, 0xab, 0x13, 0x9b, 0xcd, 0xa7, 0x2f, 0xaa, 0xd6, 0xb3, 0x17, 0x55, + 0xeb, 0xd7, 0x17, 0x55, 0xeb, 0xc9, 0xcb, 0xea, 0xc4, 0xb3, 0x97, 0xd5, 0x89, 0x1f, 0x5f, 0x56, + 0x27, 0x1e, 0x39, 0x3d, 0x2a, 0xfb, 0x83, 0xae, 0xed, 0xf3, 0xc8, 0x21, 0x61, 0x48, 0x59, 0x97, + 0x4a, 0xe1, 0xe8, 0x09, 0xf8, 0x03, 0xe7, 0xd5, 0xbf, 0x0e, 0x72, 0x14, 0x83, 0xe8, 0x4e, 0xe9, + 0xfc, 0xfe, 0xf7, 0x8f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe9, 0x64, 0x6e, 0x32, 0x58, 0x0c, 0x00, + 0x00, } func (m *Params) Marshal() (dAtA []byte, err error) { @@ -1060,7 +1085,7 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { if m.MinDepositBlocks != 0 { i = encodeVarintProvider(dAtA, i, uint64(m.MinDepositBlocks)) i-- - dAtA[i] = 0x28 + dAtA[i] = 0x30 } { size := m.FeesPerBlockAmount.Size() @@ -1071,11 +1096,11 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { i = encodeVarintProvider(dAtA, i, uint64(size)) } i-- - dAtA[i] = 0x22 + dAtA[i] = 0x2a if m.BlocksPerEpoch != 0 { i = encodeVarintProvider(dAtA, i, uint64(m.BlocksPerEpoch)) i-- - dAtA[i] = 0x18 + dAtA[i] = 0x20 } n1, err1 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.VaasTimeoutPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.VaasTimeoutPeriod):]) if err1 != nil { @@ -1084,7 +1109,14 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { i -= n1 i = encodeVarintProvider(dAtA, i, uint64(n1)) i-- - dAtA[i] = 0x12 + dAtA[i] = 0x1a + if len(m.LivenessGraceFraction) > 0 { + i -= len(m.LivenessGraceFraction) + copy(dAtA[i:], m.LivenessGraceFraction) + i = encodeVarintProvider(dAtA, i, uint64(len(m.LivenessGraceFraction))) + i-- + dAtA[i] = 0x12 + } if len(m.TrustingPeriodFraction) > 0 { i -= len(m.TrustingPeriodFraction) copy(dAtA[i:], m.TrustingPeriodFraction) @@ -1464,34 +1496,42 @@ func (m *ConsumerInitializationParameters) MarshalToSizedBuffer(dAtA []byte) (in _ = i var l int _ = l - if m.HistoricalEntries != 0 { - i = encodeVarintProvider(dAtA, i, uint64(m.HistoricalEntries)) - i-- - dAtA[i] = 0x38 - } - n7, err7 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.VaasTimeoutPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.VaasTimeoutPeriod):]) + n7, err7 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.SafeModeThreshold, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.SafeModeThreshold):]) if err7 != nil { return 0, err7 } i -= n7 i = encodeVarintProvider(dAtA, i, uint64(n7)) i-- - dAtA[i] = 0x32 - n8, err8 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.UnbondingPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.UnbondingPeriod):]) + dAtA[i] = 0x42 + if m.HistoricalEntries != 0 { + i = encodeVarintProvider(dAtA, i, uint64(m.HistoricalEntries)) + i-- + dAtA[i] = 0x38 + } + n8, err8 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.VaasTimeoutPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.VaasTimeoutPeriod):]) if err8 != nil { return 0, err8 } i -= n8 i = encodeVarintProvider(dAtA, i, uint64(n8)) i-- - dAtA[i] = 0x2a - n9, err9 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(m.SpawnTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(m.SpawnTime):]) + dAtA[i] = 0x32 + n9, err9 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.UnbondingPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.UnbondingPeriod):]) if err9 != nil { return 0, err9 } i -= n9 i = encodeVarintProvider(dAtA, i, uint64(n9)) i-- + dAtA[i] = 0x2a + n10, err10 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(m.SpawnTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(m.SpawnTime):]) + if err10 != nil { + return 0, err10 + } + i -= n10 + i = encodeVarintProvider(dAtA, i, uint64(n10)) + i-- dAtA[i] = 0x22 if len(m.BinaryHash) > 0 { i -= len(m.BinaryHash) @@ -1541,20 +1581,20 @@ func (m *ConsumerIds) MarshalToSizedBuffer(dAtA []byte) (int, error) { var l int _ = l if len(m.Ids) > 0 { - dAtA12 := make([]byte, len(m.Ids)*10) - var j11 int + dAtA13 := make([]byte, len(m.Ids)*10) + var j12 int for _, num := range m.Ids { for num >= 1<<7 { - dAtA12[j11] = uint8(uint64(num)&0x7f | 0x80) + dAtA13[j12] = uint8(uint64(num)&0x7f | 0x80) num >>= 7 - j11++ + j12++ } - dAtA12[j11] = uint8(num) - j11++ + dAtA13[j12] = uint8(num) + j12++ } - i -= j11 - copy(dAtA[i:], dAtA12[:j11]) - i = encodeVarintProvider(dAtA, i, uint64(j11)) + i -= j12 + copy(dAtA[i:], dAtA13[:j12]) + i = encodeVarintProvider(dAtA, i, uint64(j12)) i-- dAtA[i] = 0xa } @@ -1581,12 +1621,12 @@ func (m *InfractionParameters) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - n13, err13 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.DowntimeGracePeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.DowntimeGracePeriod):]) - if err13 != nil { - return 0, err13 + n14, err14 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.DowntimeGracePeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.DowntimeGracePeriod):]) + if err14 != nil { + return 0, err14 } - i -= n13 - i = encodeVarintProvider(dAtA, i, uint64(n13)) + i -= n14 + i = encodeVarintProvider(dAtA, i, uint64(n14)) i-- dAtA[i] = 0x1a if m.Downtime != nil { @@ -1646,12 +1686,12 @@ func (m *SlashJailParameters) MarshalToSizedBuffer(dAtA []byte) (int, error) { i-- dAtA[i] = 0x18 } - n16, err16 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.JailDuration, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.JailDuration):]) - if err16 != nil { - return 0, err16 + n17, err17 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.JailDuration, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.JailDuration):]) + if err17 != nil { + return 0, err17 } - i -= n16 - i = encodeVarintProvider(dAtA, i, uint64(n16)) + i -= n17 + i = encodeVarintProvider(dAtA, i, uint64(n17)) i-- dAtA[i] = 0x12 { @@ -1688,6 +1728,10 @@ func (m *Params) Size() (n int) { if l > 0 { n += 1 + l + sovProvider(uint64(l)) } + l = len(m.LivenessGraceFraction) + if l > 0 { + n += 1 + l + sovProvider(uint64(l)) + } l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.VaasTimeoutPeriod) n += 1 + l + sovProvider(uint64(l)) if m.BlocksPerEpoch != 0 { @@ -1878,6 +1922,8 @@ func (m *ConsumerInitializationParameters) Size() (n int) { if m.HistoricalEntries != 0 { n += 1 + sovProvider(uint64(m.HistoricalEntries)) } + l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.SafeModeThreshold) + n += 1 + l + sovProvider(uint64(l)) return n } @@ -2000,6 +2046,38 @@ func (m *Params) Unmarshal(dAtA []byte) error { m.TrustingPeriodFraction = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LivenessGraceFraction", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProvider + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthProvider + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthProvider + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.LivenessGraceFraction = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field VaasTimeoutPeriod", wireType) } @@ -2032,7 +2110,7 @@ func (m *Params) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex - case 3: + case 4: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field BlocksPerEpoch", wireType) } @@ -2051,7 +2129,7 @@ func (m *Params) Unmarshal(dAtA []byte) error { break } } - case 4: + case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field FeesPerBlockAmount", wireType) } @@ -2085,7 +2163,7 @@ func (m *Params) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex - case 5: + case 6: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field MinDepositBlocks", wireType) } @@ -3396,6 +3474,39 @@ func (m *ConsumerInitializationParameters) Unmarshal(dAtA []byte) error { break } } + case 8: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SafeModeThreshold", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProvider + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthProvider + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthProvider + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_cosmos_gogoproto_types.StdDurationUnmarshal(&m.SafeModeThreshold, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipProvider(dAtA[iNdEx:]) diff --git a/x/vaas/provider/types/query.pb.go b/x/vaas/provider/types/query.pb.go index 406ee51..0de0f2e 100644 --- a/x/vaas/provider/types/query.pb.go +++ b/x/vaas/provider/types/query.pb.go @@ -22,6 +22,7 @@ import ( grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" + _ "google.golang.org/protobuf/types/known/durationpb" _ "google.golang.org/protobuf/types/known/timestamppb" io "io" math "math" @@ -1804,6 +1805,118 @@ func (m *QueryConsumerFeePoolClaimsResponse) GetPagination() *query.PageResponse return nil } +type QueryConsumerLivenessRequest struct { + ConsumerId uint64 `protobuf:"varint,1,opt,name=consumer_id,json=consumerId,proto3" json:"consumer_id,omitempty"` +} + +func (m *QueryConsumerLivenessRequest) Reset() { *m = QueryConsumerLivenessRequest{} } +func (m *QueryConsumerLivenessRequest) String() string { return proto.CompactTextString(m) } +func (*QueryConsumerLivenessRequest) ProtoMessage() {} +func (*QueryConsumerLivenessRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_71b3f55ed407aa51, []int{34} +} +func (m *QueryConsumerLivenessRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryConsumerLivenessRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryConsumerLivenessRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryConsumerLivenessRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryConsumerLivenessRequest.Merge(m, src) +} +func (m *QueryConsumerLivenessRequest) XXX_Size() int { + return m.Size() +} +func (m *QueryConsumerLivenessRequest) XXX_DiscardUnknown() { + xxx_messageInfo_QueryConsumerLivenessRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryConsumerLivenessRequest proto.InternalMessageInfo + +func (m *QueryConsumerLivenessRequest) GetConsumerId() uint64 { + if m != nil { + return m.ConsumerId + } + return 0 +} + +type QueryConsumerLivenessResponse struct { + LastAckTime time.Time `protobuf:"bytes,1,opt,name=last_ack_time,json=lastAckTime,proto3,stdtime" json:"last_ack_time"` + GracePeriod time.Duration `protobuf:"bytes,2,opt,name=grace_period,json=gracePeriod,proto3,stdduration" json:"grace_period"` + RemovalEta time.Time `protobuf:"bytes,3,opt,name=removal_eta,json=removalEta,proto3,stdtime" json:"removal_eta"` + Degraded bool `protobuf:"varint,4,opt,name=degraded,proto3" json:"degraded,omitempty"` +} + +func (m *QueryConsumerLivenessResponse) Reset() { *m = QueryConsumerLivenessResponse{} } +func (m *QueryConsumerLivenessResponse) String() string { return proto.CompactTextString(m) } +func (*QueryConsumerLivenessResponse) ProtoMessage() {} +func (*QueryConsumerLivenessResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_71b3f55ed407aa51, []int{35} +} +func (m *QueryConsumerLivenessResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryConsumerLivenessResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryConsumerLivenessResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryConsumerLivenessResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryConsumerLivenessResponse.Merge(m, src) +} +func (m *QueryConsumerLivenessResponse) XXX_Size() int { + return m.Size() +} +func (m *QueryConsumerLivenessResponse) XXX_DiscardUnknown() { + xxx_messageInfo_QueryConsumerLivenessResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryConsumerLivenessResponse proto.InternalMessageInfo + +func (m *QueryConsumerLivenessResponse) GetLastAckTime() time.Time { + if m != nil { + return m.LastAckTime + } + return time.Time{} +} + +func (m *QueryConsumerLivenessResponse) GetGracePeriod() time.Duration { + if m != nil { + return m.GracePeriod + } + return 0 +} + +func (m *QueryConsumerLivenessResponse) GetRemovalEta() time.Time { + if m != nil { + return m.RemovalEta + } + return time.Time{} +} + +func (m *QueryConsumerLivenessResponse) GetDegraded() bool { + if m != nil { + return m.Degraded + } + return false +} + func init() { proto.RegisterType((*QueryConsumerGenesisRequest)(nil), "vaas.provider.v1.QueryConsumerGenesisRequest") proto.RegisterType((*QueryConsumerGenesisResponse)(nil), "vaas.provider.v1.QueryConsumerGenesisResponse") @@ -1839,149 +1952,160 @@ func init() { proto.RegisterType((*DepositorClaim)(nil), "vaas.provider.v1.DepositorClaim") proto.RegisterType((*QueryConsumerFeePoolClaimsRequest)(nil), "vaas.provider.v1.QueryConsumerFeePoolClaimsRequest") proto.RegisterType((*QueryConsumerFeePoolClaimsResponse)(nil), "vaas.provider.v1.QueryConsumerFeePoolClaimsResponse") + proto.RegisterType((*QueryConsumerLivenessRequest)(nil), "vaas.provider.v1.QueryConsumerLivenessRequest") + proto.RegisterType((*QueryConsumerLivenessResponse)(nil), "vaas.provider.v1.QueryConsumerLivenessResponse") } func init() { proto.RegisterFile("vaas/provider/v1/query.proto", fileDescriptor_71b3f55ed407aa51) } var fileDescriptor_71b3f55ed407aa51 = []byte{ - // 2187 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x59, 0x4d, 0x6c, 0xdc, 0xc6, - 0xf5, 0x17, 0xd7, 0x92, 0xbc, 0x1a, 0xd9, 0x8a, 0xff, 0xf3, 0x97, 0xa3, 0xd5, 0x5a, 0xd6, 0xda, - 0x74, 0x9c, 0x28, 0xb6, 0x97, 0xb4, 0x56, 0xb6, 0x11, 0xa8, 0xae, 0x53, 0xef, 0x3a, 0x72, 0x05, - 0x37, 0xf5, 0x96, 0x76, 0x73, 0x08, 0xd0, 0x12, 0xdc, 0xe5, 0x78, 0x35, 0x15, 0x97, 0x64, 0x48, - 0xee, 0xda, 0x5b, 0x41, 0x97, 0x1e, 0x8a, 0x1e, 0x52, 0x20, 0x40, 0x0f, 0x3d, 0xf4, 0x12, 0x14, - 0x3d, 0xf5, 0xe3, 0x66, 0x14, 0x41, 0x81, 0x5e, 0x8a, 0x1e, 0x72, 0xc8, 0x21, 0x70, 0x2e, 0x41, - 0x0f, 0x4a, 0x61, 0x17, 0x45, 0xcf, 0x41, 0x7b, 0xe9, 0xa1, 0x28, 0x38, 0x1f, 0x5c, 0x72, 0x96, - 0xdc, 0xa5, 0x1c, 0xf7, 0xa4, 0xe5, 0xcc, 0x9b, 0x37, 0xbf, 0xf7, 0xe6, 0xcd, 0x9b, 0xf7, 0x7b, - 0x02, 0x2b, 0x7d, 0xc3, 0xf0, 0x55, 0xd7, 0x73, 0xfa, 0xd8, 0x44, 0x9e, 0xda, 0x5f, 0x57, 0xdf, - 0xeb, 0x21, 0x6f, 0xa0, 0xb8, 0x9e, 0x13, 0x38, 0xf0, 0x44, 0x38, 0xab, 0xf0, 0x59, 0xa5, 0xbf, - 0x5e, 0x5e, 0xe9, 0x38, 0x4e, 0xc7, 0x42, 0xaa, 0xe1, 0x62, 0xd5, 0xb0, 0x6d, 0x27, 0x30, 0x02, - 0xec, 0xd8, 0x3e, 0x95, 0x2f, 0x2f, 0x76, 0x9c, 0x8e, 0x43, 0x7e, 0xaa, 0xe1, 0x2f, 0x36, 0x5a, - 0x61, 0x6b, 0xc8, 0x57, 0xab, 0xf7, 0x40, 0x0d, 0x70, 0x17, 0xf9, 0x81, 0xd1, 0x75, 0x99, 0xc0, - 0xea, 0x08, 0x88, 0x0e, 0xb2, 0x91, 0x8f, 0xb9, 0xda, 0xca, 0xc8, 0x7c, 0x04, 0x89, 0x0a, 0x9c, - 0x26, 0x02, 0xfd, 0x75, 0xd5, 0xdf, 0x31, 0x3c, 0x64, 0xea, 0x6d, 0xc7, 0xf6, 0x7b, 0xdd, 0x68, - 0x1a, 0xf2, 0xe9, 0x87, 0xd8, 0x43, 0x6c, 0x6c, 0x25, 0x40, 0xb6, 0x89, 0xbc, 0x2e, 0xb6, 0x03, - 0xb5, 0xed, 0x0d, 0xdc, 0xc0, 0x51, 0x77, 0xd1, 0x80, 0xef, 0xb8, 0xdc, 0x76, 0xfc, 0xae, 0xe3, - 0xeb, 0xd4, 0x16, 0xfa, 0xc1, 0xc1, 0xd2, 0x2f, 0xb5, 0x65, 0xf8, 0x48, 0xed, 0xaf, 0xb7, 0x50, - 0x60, 0xac, 0xab, 0x6d, 0x07, 0xdb, 0x6c, 0xfe, 0x15, 0x36, 0xef, 0x07, 0xc6, 0x2e, 0xb6, 0x3b, - 0x91, 0x08, 0xfb, 0x66, 0x52, 0x17, 0xe2, 0x5a, 0x88, 0xcb, 0x23, 0x41, 0xd7, 0xe8, 0x60, 0x9b, - 0xb8, 0x95, 0xca, 0xca, 0x37, 0xc0, 0xa9, 0xef, 0x84, 0x12, 0x0d, 0x66, 0xd5, 0x6d, 0xea, 0x1c, - 0x0d, 0xbd, 0xd7, 0x43, 0x7e, 0x00, 0x2b, 0x60, 0x9e, 0xdb, 0xab, 0x63, 0xb3, 0x24, 0x9d, 0x91, - 0xd6, 0xa6, 0x35, 0xc0, 0x87, 0xb6, 0x4d, 0x79, 0x07, 0xac, 0xa4, 0xaf, 0xf7, 0x5d, 0xc7, 0xf6, - 0x11, 0xfc, 0x26, 0x38, 0xce, 0xfc, 0xad, 0xfb, 0x81, 0x11, 0x20, 0xa2, 0x62, 0xbe, 0x76, 0x5a, - 0x21, 0xa7, 0xdf, 0x5f, 0x57, 0x84, 0x85, 0xf7, 0x42, 0xa1, 0xfa, 0xf4, 0xc7, 0x07, 0x95, 0x29, - 0xed, 0x58, 0x27, 0x36, 0x26, 0xff, 0x42, 0x02, 0xe5, 0xc4, 0x56, 0x8d, 0x1d, 0x03, 0xdb, 0x11, - 0xd2, 0xab, 0x60, 0xc6, 0xdd, 0x31, 0x7c, 0xba, 0xc1, 0x42, 0xad, 0xa2, 0x88, 0xe1, 0x15, 0xed, - 0xd4, 0x0c, 0xc5, 0x34, 0x2a, 0x0d, 0xb7, 0x00, 0x18, 0xfa, 0xa4, 0x54, 0x20, 0xe0, 0x5e, 0x55, - 0xd8, 0xa1, 0x84, 0x0e, 0x54, 0x68, 0xcc, 0x32, 0x07, 0x2a, 0x4d, 0xa3, 0x83, 0xd8, 0x96, 0x5a, - 0x6c, 0xa5, 0xfc, 0x73, 0x49, 0x70, 0x24, 0x47, 0xc7, 0xfc, 0xa0, 0x82, 0xd9, 0x36, 0x19, 0x29, - 0x49, 0x67, 0x8e, 0xac, 0xcd, 0xd7, 0x96, 0x52, 0xf0, 0x85, 0xf3, 0x1a, 0x13, 0x83, 0xb7, 0x53, - 0x80, 0xbd, 0x36, 0x11, 0x18, 0xdd, 0x2d, 0x81, 0xec, 0xef, 0x12, 0x98, 0x21, 0xaa, 0xe1, 0x32, - 0x28, 0x12, 0xe5, 0xfc, 0x24, 0xe7, 0xb4, 0xa3, 0xe4, 0x7b, 0xdb, 0x84, 0xa7, 0xc0, 0x5c, 0xdb, - 0xc2, 0xc8, 0x0e, 0xc2, 0xb9, 0x02, 0x99, 0x2b, 0xd2, 0x81, 0x6d, 0x13, 0x2e, 0x72, 0xd7, 0x1e, - 0x21, 0x13, 0xcc, 0x73, 0xb7, 0x40, 0xb1, 0x8b, 0x02, 0xc3, 0x34, 0x02, 0xa3, 0x34, 0x4d, 0xe0, - 0xc9, 0xd9, 0x3e, 0x7f, 0x9b, 0x49, 0xb2, 0x93, 0x8d, 0x56, 0x8a, 0x01, 0x36, 0x23, 0x06, 0x18, - 0x5c, 0x03, 0x27, 0x1e, 0x20, 0xa4, 0xbb, 0x8e, 0x63, 0xe9, 0x86, 0x69, 0x7a, 0xc8, 0xf7, 0x4b, - 0xb3, 0x04, 0xc7, 0xc2, 0x03, 0x84, 0x9a, 0x8e, 0x63, 0xdd, 0xa4, 0xa3, 0xf2, 0x4f, 0x25, 0x70, - 0x96, 0x1c, 0xc1, 0x3b, 0x86, 0x85, 0x4d, 0x23, 0x70, 0x3c, 0xbe, 0x7b, 0x28, 0xc1, 0xe3, 0xe4, - 0xeb, 0xe0, 0x04, 0x07, 0x18, 0xe9, 0x23, 0xce, 0xa8, 0xc3, 0x2f, 0x0f, 0x2a, 0x0b, 0x03, 0xa3, - 0x6b, 0x6d, 0xca, 0x6c, 0x42, 0xd6, 0x5e, 0xe2, 0xb2, 0x6c, 0x13, 0x11, 0x6f, 0x41, 0xc4, 0xbb, - 0x59, 0xfc, 0xc9, 0x87, 0x95, 0xa9, 0x7f, 0x7c, 0x58, 0x99, 0x92, 0xef, 0x02, 0x79, 0x1c, 0x1c, - 0x16, 0x18, 0xaf, 0x83, 0x13, 0x91, 0xc2, 0x04, 0x1e, 0xed, 0xa5, 0x76, 0x4c, 0x3e, 0xdd, 0xc0, - 0x66, 0x0c, 0x5d, 0xcc, 0xc0, 0x74, 0x85, 0xe9, 0x06, 0x0a, 0x9b, 0x7c, 0x25, 0x03, 0x93, 0x70, - 0x86, 0x06, 0xa6, 0x3b, 0x7c, 0xc4, 0xb9, 0xf2, 0xb7, 0xc0, 0xeb, 0x44, 0xe1, 0x4d, 0xcb, 0x6a, - 0x1a, 0xd8, 0xf3, 0xdf, 0x31, 0xac, 0xd0, 0x67, 0xe1, 0x74, 0x3d, 0xba, 0x58, 0xb9, 0x53, 0xd3, - 0xfb, 0x12, 0xb8, 0x90, 0x47, 0x1d, 0xc3, 0xf9, 0x7d, 0xf0, 0x7f, 0xae, 0x81, 0x3d, 0xbd, 0x6f, - 0x58, 0x61, 0x8e, 0x27, 0x58, 0xd9, 0x65, 0xdd, 0x18, 0x0d, 0xec, 0x50, 0x21, 0xd5, 0x17, 0xaa, - 0x8b, 0x0c, 0xb7, 0xcd, 0x48, 0xef, 0x82, 0x9b, 0x10, 0x91, 0xff, 0x29, 0x81, 0xb3, 0x13, 0x57, - 0xc1, 0xad, 0xcc, 0xf0, 0x3c, 0xf5, 0xe5, 0x41, 0x65, 0x89, 0x9e, 0x9e, 0x28, 0x91, 0x12, 0xa7, - 0x5b, 0x29, 0x51, 0x50, 0x10, 0xf5, 0x88, 0x12, 0x29, 0xe1, 0xf0, 0x26, 0x38, 0x16, 0x49, 0xed, - 0xa2, 0x01, 0x49, 0x01, 0xf3, 0xb5, 0x15, 0x65, 0xf8, 0xc2, 0x29, 0xf4, 0x85, 0x53, 0x9a, 0xbd, - 0x96, 0x85, 0xdb, 0x77, 0xd0, 0x40, 0x8b, 0xce, 0xe5, 0x0e, 0x1a, 0xc8, 0x8b, 0x00, 0x92, 0x43, - 0x68, 0x1a, 0x9e, 0xd1, 0xe5, 0xd9, 0x5a, 0x7e, 0x1b, 0xfc, 0x7f, 0x62, 0x94, 0x9d, 0xc1, 0x35, - 0x30, 0xeb, 0x92, 0x11, 0xf6, 0x4c, 0x94, 0xd2, 0x1c, 0x1f, 0xce, 0xb3, 0x3c, 0xc2, 0xa4, 0xe5, - 0x9b, 0x60, 0x35, 0x91, 0x7c, 0xa3, 0x88, 0xcc, 0xff, 0x90, 0xfd, 0x67, 0x1a, 0x9c, 0xc9, 0xd0, - 0x11, 0xfd, 0xfa, 0xaa, 0xc9, 0x43, 0x74, 0x66, 0xe1, 0x90, 0xce, 0x84, 0xe7, 0xc1, 0x42, 0xa4, - 0xc0, 0x75, 0x1e, 0x22, 0x8f, 0x9c, 0xc7, 0x11, 0xed, 0x38, 0x1f, 0x6d, 0x86, 0x83, 0xf0, 0x0e, - 0x98, 0x37, 0x91, 0xdf, 0xf6, 0xb0, 0x4b, 0x1e, 0x0f, 0x9a, 0x9d, 0xcf, 0xf1, 0xc7, 0x83, 0x17, - 0x0b, 0xfc, 0xe5, 0xb8, 0x35, 0x14, 0x65, 0x6e, 0x8d, 0xaf, 0x86, 0xdf, 0x03, 0xcb, 0x91, 0xcd, - 0x8e, 0x8b, 0xbc, 0xd0, 0x11, 0x91, 0xf1, 0x33, 0xc4, 0xf8, 0xb3, 0x4f, 0x1e, 0x57, 0x4f, 0x33, - 0xed, 0x91, 0xb3, 0x98, 0xd1, 0xf7, 0x02, 0x0f, 0xdb, 0x1d, 0x6d, 0x89, 0xeb, 0xb8, 0xcb, 0x54, - 0x70, 0x9f, 0xbc, 0x0c, 0x66, 0x7f, 0x60, 0x60, 0x0b, 0x99, 0x24, 0xab, 0x17, 0x35, 0xf6, 0x05, - 0x37, 0xc1, 0x6c, 0x58, 0x30, 0xf4, 0xfc, 0xd2, 0x51, 0xf2, 0xa0, 0xcb, 0x59, 0xf0, 0xeb, 0x8e, - 0x6d, 0xde, 0x23, 0x92, 0x1a, 0x5b, 0x01, 0xef, 0x83, 0xc8, 0xf5, 0x7a, 0xe0, 0xec, 0x22, 0xdb, - 0x2f, 0x15, 0x09, 0xd0, 0x8b, 0xa1, 0x79, 0x7f, 0x39, 0xa8, 0x9c, 0xa4, 0xba, 0x7c, 0x73, 0x57, - 0xc1, 0x8e, 0xda, 0x35, 0x82, 0x1d, 0x65, 0xdb, 0x0e, 0x9e, 0x3c, 0xae, 0x02, 0xb6, 0xc9, 0xb6, - 0x1d, 0x68, 0x0b, 0x5c, 0xc7, 0x7d, 0xa2, 0x22, 0x74, 0x7e, 0xa4, 0x95, 0x3a, 0x7f, 0x8e, 0x3a, - 0x9f, 0x8f, 0x52, 0xe7, 0x5f, 0x03, 0x4b, 0x7d, 0xea, 0x03, 0xe4, 0xeb, 0xed, 0x9e, 0xe7, 0x85, - 0xaf, 0x2a, 0x72, 0x9d, 0xf6, 0x4e, 0x09, 0x10, 0x0b, 0x4f, 0x46, 0xd3, 0x0d, 0x3a, 0xfb, 0x56, - 0x38, 0x29, 0xf7, 0x40, 0x25, 0x33, 0x86, 0xd9, 0xf5, 0xd0, 0x00, 0xe8, 0x47, 0xa3, 0x2c, 0x37, - 0xd5, 0x46, 0xaf, 0xc8, 0xa4, 0x30, 0xd6, 0x62, 0x5a, 0x64, 0x99, 0x85, 0x7d, 0xdd, 0x72, 0xda, - 0xbb, 0xfe, 0x77, 0xed, 0x00, 0x5b, 0xdf, 0x46, 0x8f, 0x28, 0x26, 0x7e, 0x5b, 0xdf, 0x65, 0xef, - 0x4e, 0xba, 0x0c, 0x03, 0x77, 0x15, 0x2c, 0xb5, 0xc8, 0xbc, 0xde, 0x0b, 0x05, 0x74, 0x1b, 0x3d, - 0xe2, 0x76, 0xd3, 0xdb, 0xb6, 0xd8, 0x4a, 0x59, 0x2e, 0xdf, 0x64, 0x8f, 0x48, 0x23, 0xba, 0x8a, - 0x5b, 0x9e, 0xd3, 0x6d, 0xb0, 0xda, 0x83, 0x5f, 0xdf, 0x44, 0x7d, 0x22, 0x25, 0xeb, 0x13, 0x79, - 0x0b, 0x9c, 0x1b, 0xab, 0x82, 0x01, 0x9c, 0x98, 0x02, 0xae, 0x83, 0xe5, 0xd1, 0x12, 0x2e, 0x77, - 0x02, 0xf9, 0x57, 0x21, 0xad, 0x3e, 0xcd, 0xbd, 0x7b, 0xa2, 0x3a, 0x2b, 0x24, 0xab, 0xb3, 0x73, - 0xe0, 0xb8, 0xf3, 0xd0, 0x8e, 0xe5, 0x1c, 0x5a, 0x88, 0x1d, 0x23, 0x83, 0xfc, 0x22, 0x45, 0x55, - 0xda, 0x74, 0x56, 0x95, 0x36, 0xf3, 0xdc, 0x55, 0xda, 0x3d, 0x30, 0x8f, 0x6d, 0x1c, 0xe8, 0x2c, - 0x39, 0xcf, 0x12, 0x45, 0xb5, 0x6c, 0x45, 0xdb, 0x36, 0x0e, 0xb0, 0x61, 0xe1, 0x1f, 0x92, 0x12, - 0x94, 0xa4, 0x6c, 0x14, 0x20, 0xcf, 0xd7, 0x40, 0xa8, 0x86, 0xa6, 0xf0, 0xe4, 0x99, 0x1e, 0x15, - 0x6a, 0xce, 0xb4, 0xb2, 0xaf, 0x98, 0x5a, 0xf6, 0xd5, 0x85, 0x7b, 0xc3, 0x88, 0xc4, 0x7d, 0xdc, - 0x45, 0xb9, 0xcf, 0x6e, 0x57, 0xc8, 0xfd, 0x09, 0x1d, 0xec, 0x00, 0x6f, 0x03, 0xce, 0x47, 0xf4, - 0x90, 0x63, 0xb2, 0x17, 0xaa, 0xac, 0x50, 0x02, 0xaa, 0x70, 0x02, 0xaa, 0xdc, 0xe7, 0x04, 0xb4, - 0x5e, 0x0c, 0xbd, 0xf8, 0xc1, 0x17, 0x15, 0x49, 0x9b, 0xef, 0x0c, 0x15, 0xca, 0x0d, 0x61, 0xb3, - 0x2d, 0x84, 0xfc, 0x26, 0xf2, 0xc8, 0xe5, 0xca, 0x8d, 0xf8, 0xf7, 0xbc, 0x16, 0x4c, 0xd7, 0xc2, - 0x30, 0xbb, 0x20, 0xf4, 0x96, 0xaf, 0xbb, 0xc8, 0xd3, 0xc9, 0xed, 0x63, 0xa8, 0x97, 0x13, 0x44, - 0x82, 0x67, 0xd2, 0x86, 0x83, 0xed, 0xba, 0x1a, 0x82, 0xfe, 0xf7, 0x41, 0xe5, 0xb5, 0x0e, 0x0e, - 0x76, 0x7a, 0x2d, 0xa5, 0xed, 0x74, 0x19, 0x47, 0x65, 0x7f, 0xaa, 0xbe, 0xb9, 0xab, 0x06, 0x03, - 0x17, 0xf9, 0x64, 0x81, 0x76, 0xec, 0x41, 0x6c, 0xe7, 0x10, 0x38, 0xf6, 0x75, 0xa7, 0x8f, 0x3c, - 0x0f, 0x9b, 0x88, 0x04, 0x72, 0x51, 0x03, 0xd8, 0xbf, 0xcb, 0x46, 0xe4, 0x3e, 0xb8, 0xc4, 0x8b, - 0xb2, 0x34, 0xe8, 0x5c, 0x2e, 0x7a, 0xb8, 0x93, 0x04, 0x4d, 0x7a, 0x6e, 0x82, 0xf6, 0x89, 0x04, - 0xaa, 0x39, 0x37, 0x8e, 0xb2, 0xed, 0x1c, 0xb7, 0x83, 0x27, 0x5b, 0x25, 0x3b, 0xe4, 0xd3, 0x74, - 0xb1, 0x7b, 0x34, 0x54, 0xf3, 0xe2, 0x58, 0xdd, 0xde, 0x68, 0x10, 0x85, 0x97, 0xa2, 0x61, 0x19, - 0xb8, 0x9b, 0x37, 0x88, 0xe0, 0x35, 0x30, 0x67, 0x22, 0xd7, 0xf1, 0x71, 0xe0, 0x78, 0xac, 0x3a, - 0x2c, 0x3d, 0x79, 0x5c, 0x5d, 0x64, 0x78, 0x92, 0x2f, 0xf8, 0x50, 0x54, 0xfe, 0x71, 0x4a, 0xf0, - 0xc5, 0x76, 0x67, 0xfe, 0x33, 0xc0, 0x4c, 0x3b, 0x1c, 0x60, 0xbe, 0x1b, 0x13, 0x73, 0x97, 0x43, - 0x37, 0xfd, 0xfa, 0x8b, 0xca, 0x5a, 0xce, 0x98, 0xf3, 0x35, 0xaa, 0x59, 0xfe, 0x8d, 0x04, 0x16, - 0x6e, 0x71, 0x58, 0x64, 0xf7, 0xa4, 0x4d, 0x52, 0x6e, 0x9b, 0x86, 0x68, 0x0b, 0xff, 0x33, 0xb4, - 0xef, 0x8f, 0x73, 0x5b, 0xee, 0x4a, 0xf5, 0x85, 0xb5, 0x2c, 0x7e, 0x27, 0x09, 0x4f, 0xaf, 0x00, - 0x87, 0x1d, 0xe3, 0x0d, 0x30, 0x4b, 0xe0, 0xf3, 0x3b, 0x70, 0x66, 0xf4, 0x0e, 0x24, 0x8f, 0x80, - 0xd7, 0xe6, 0x74, 0xd5, 0x0b, 0x0b, 0xf9, 0xda, 0x47, 0x2f, 0x83, 0x19, 0x82, 0x17, 0xfe, 0x56, - 0x02, 0x8b, 0x69, 0xf9, 0x1a, 0x56, 0x27, 0x14, 0x43, 0xc9, 0xee, 0x56, 0x59, 0xc9, 0x2b, 0x4e, - 0xd1, 0xc8, 0x57, 0x7f, 0xf4, 0xd9, 0xdf, 0x7e, 0x56, 0x50, 0x61, 0x55, 0x4d, 0x36, 0x0d, 0xa3, - 0xf3, 0x62, 0x69, 0x5e, 0xdd, 0x8b, 0x9d, 0xe0, 0x3e, 0xfc, 0xa5, 0xc4, 0xd8, 0x4e, 0xb2, 0x37, - 0x04, 0x2f, 0x4d, 0xd8, 0x3e, 0xd1, 0xe0, 0x2a, 0x57, 0x73, 0x4a, 0x33, 0xac, 0x0a, 0xc1, 0xba, - 0x06, 0x5f, 0xcd, 0xc2, 0x4a, 0xfb, 0x4c, 0xea, 0x1e, 0xa9, 0x13, 0xf6, 0xe1, 0xe7, 0xbc, 0xbd, - 0x96, 0xda, 0xae, 0x80, 0x1b, 0x19, 0xbb, 0x8f, 0xeb, 0xb5, 0x94, 0xaf, 0x1c, 0x6e, 0x11, 0x43, - 0x7e, 0x97, 0x20, 0xdf, 0x86, 0xb7, 0x05, 0xe4, 0x51, 0xd1, 0xaa, 0x27, 0x78, 0x6b, 0xd2, 0xd9, - 0xea, 0x9e, 0x48, 0xd0, 0xd2, 0x4c, 0x8b, 0x37, 0x2a, 0x26, 0x9b, 0x96, 0xd2, 0x65, 0x99, 0x6c, - 0x5a, 0x5a, 0x2f, 0x24, 0x87, 0x69, 0x09, 0xf4, 0xa2, 0x69, 0x22, 0x5f, 0xdf, 0x87, 0x9f, 0xf1, - 0x3b, 0x3c, 0xb6, 0xc7, 0x01, 0xbf, 0x96, 0x81, 0x36, 0x4f, 0xa3, 0xa5, 0x7c, 0xfd, 0xf9, 0x16, - 0x33, 0x93, 0x6b, 0xc4, 0xe4, 0x4b, 0xf0, 0x82, 0x60, 0x32, 0x33, 0x41, 0x77, 0x43, 0x15, 0xc2, - 0x85, 0x19, 0x80, 0xf9, 0x58, 0x77, 0x00, 0xbe, 0x92, 0x01, 0x20, 0xd1, 0x52, 0x28, 0x9f, 0x9f, - 0x20, 0xc5, 0xf0, 0x9c, 0x26, 0x78, 0x96, 0xe0, 0x49, 0x01, 0x0f, 0x2d, 0x6d, 0xe1, 0x47, 0x12, - 0x58, 0xca, 0xe0, 0x4f, 0xf0, 0x72, 0x6e, 0xaa, 0xc5, 0x31, 0xad, 0x1f, 0x62, 0x05, 0xc3, 0xf7, - 0x06, 0xc1, 0x57, 0x83, 0x97, 0xb3, 0xee, 0xed, 0x90, 0xbb, 0x09, 0x5e, 0x7b, 0x2c, 0x31, 0xfe, - 0x92, 0x46, 0xd3, 0x60, 0x16, 0x4f, 0x1c, 0xc3, 0xfb, 0xca, 0x1b, 0x87, 0x5a, 0x33, 0x21, 0xf1, - 0x64, 0x90, 0x43, 0xf8, 0x07, 0xb1, 0x73, 0x9e, 0xa4, 0x6f, 0xf0, 0xca, 0x04, 0x1f, 0xa6, 0x12, - 0xc6, 0xf2, 0xd5, 0x43, 0xae, 0xca, 0x9b, 0x35, 0xb1, 0xa9, 0xee, 0x45, 0xac, 0x65, 0x1f, 0xfe, - 0x4a, 0x62, 0xed, 0xad, 0x44, 0x16, 0x86, 0x17, 0xf3, 0xe4, 0x6a, 0x0e, 0xf5, 0x52, 0x3e, 0x61, - 0x86, 0x70, 0x83, 0x20, 0xac, 0xc2, 0x8b, 0x63, 0xf3, 0xba, 0x10, 0x1a, 0x7f, 0x94, 0x40, 0x29, - 0x8b, 0xe0, 0xc0, 0xf5, 0x7c, 0xaf, 0x60, 0x8c, 0x50, 0x95, 0x6b, 0x87, 0x59, 0xc2, 0x80, 0x6f, - 0x12, 0xe0, 0x57, 0x60, 0x6d, 0xc2, 0xe3, 0x49, 0xd8, 0x95, 0x80, 0xff, 0x4f, 0x92, 0x40, 0xcd, - 0xe3, 0xd5, 0x36, 0x9c, 0x84, 0x26, 0x85, 0x60, 0x65, 0x86, 0xf6, 0x38, 0x3a, 0x25, 0x5f, 0x27, - 0x26, 0x5c, 0x83, 0x57, 0xb2, 0x4c, 0x48, 0x92, 0x2d, 0xc1, 0x88, 0xa7, 0x12, 0x38, 0x9f, 0x8b, - 0x81, 0xc0, 0x1b, 0xd9, 0x19, 0x37, 0x0f, 0x67, 0x2a, 0xbf, 0xf9, 0xdc, 0xeb, 0xf3, 0x26, 0xa1, - 0xa4, 0xa1, 0xfa, 0x90, 0xe0, 0x7c, 0x22, 0x81, 0xc5, 0xb4, 0x7a, 0x32, 0xcf, 0x21, 0x89, 0x04, - 0x26, 0xcf, 0x21, 0x8d, 0xd0, 0x0e, 0xf9, 0x0e, 0xc1, 0xfe, 0x16, 0x6c, 0xa8, 0x23, 0xff, 0xd9, - 0x8d, 0xc3, 0xa7, 0xad, 0x05, 0x52, 0xa3, 0x8a, 0xcf, 0x6c, 0x44, 0x0a, 0xf6, 0xe1, 0x9f, 0x25, - 0x70, 0x32, 0xb5, 0x3c, 0x86, 0x87, 0xc1, 0xe6, 0x4f, 0xaa, 0x1a, 0xc6, 0x56, 0xe0, 0xf2, 0x37, - 0x88, 0x45, 0x9b, 0xf0, 0x8d, 0xdc, 0x16, 0x09, 0x4f, 0x43, 0x7d, 0xfb, 0xe3, 0xa7, 0xab, 0xd2, - 0xa7, 0x4f, 0x57, 0xa5, 0xbf, 0x3e, 0x5d, 0x95, 0x3e, 0x78, 0xb6, 0x3a, 0xf5, 0xe9, 0xb3, 0xd5, - 0xa9, 0xcf, 0x9f, 0xad, 0x4e, 0xbd, 0xab, 0xc6, 0x48, 0x8c, 0x61, 0x59, 0xd8, 0x6e, 0xe1, 0xc0, - 0xa7, 0xfb, 0x3c, 0x12, 0xb6, 0x23, 0x8c, 0xa6, 0x35, 0x4b, 0x1a, 0x1d, 0x1b, 0xff, 0x0d, 0x00, - 0x00, 0xff, 0xff, 0xb1, 0xa1, 0x45, 0xf4, 0xdd, 0x1f, 0x00, 0x00, + // 2327 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x59, 0x4d, 0x6c, 0x1c, 0x49, + 0x15, 0x76, 0x4f, 0x6c, 0x67, 0x5c, 0x76, 0xbc, 0xa1, 0xb0, 0xf1, 0x78, 0x62, 0x7b, 0x92, 0xce, + 0x66, 0xd7, 0x9b, 0xc4, 0xdd, 0xf1, 0x38, 0xb1, 0x56, 0x26, 0x24, 0xd8, 0x4e, 0x1c, 0xac, 0xec, + 0x92, 0xa1, 0x13, 0xf6, 0xb0, 0x12, 0xb4, 0xca, 0xd3, 0x95, 0x71, 0x31, 0x3d, 0xdd, 0xbd, 0xdd, + 0x3d, 0x93, 0x18, 0xcb, 0x17, 0x0e, 0x88, 0xc3, 0x22, 0xad, 0x04, 0x12, 0x48, 0x5c, 0x56, 0x88, + 0x13, 0x3f, 0x12, 0x87, 0x08, 0x21, 0x24, 0x2e, 0x88, 0xc3, 0x1e, 0xf6, 0xb0, 0xca, 0x5e, 0x56, + 0x1c, 0xbc, 0x28, 0x41, 0x08, 0x89, 0xdb, 0x0a, 0x2e, 0x1c, 0x10, 0xaa, 0xea, 0xaa, 0x9e, 0xee, + 0x9e, 0xee, 0x99, 0xb6, 0x37, 0x9c, 0x3c, 0x5d, 0xf5, 0xea, 0xd5, 0xf7, 0x5e, 0xbd, 0x7a, 0xf5, + 0xbe, 0x67, 0x30, 0xd7, 0x41, 0xc8, 0x53, 0x1d, 0xd7, 0xee, 0x10, 0x03, 0xbb, 0x6a, 0x67, 0x59, + 0x7d, 0xa7, 0x8d, 0xdd, 0x3d, 0xc5, 0x71, 0x6d, 0xdf, 0x86, 0xa7, 0xe9, 0xac, 0x22, 0x66, 0x95, + 0xce, 0x72, 0x79, 0xae, 0x61, 0xdb, 0x0d, 0x13, 0xab, 0xc8, 0x21, 0x2a, 0xb2, 0x2c, 0xdb, 0x47, + 0x3e, 0xb1, 0x2d, 0x2f, 0x90, 0x2f, 0x4f, 0x35, 0xec, 0x86, 0xcd, 0x7e, 0xaa, 0xf4, 0x17, 0x1f, + 0xad, 0xf0, 0x35, 0xec, 0x6b, 0xa7, 0xfd, 0x50, 0xf5, 0x49, 0x0b, 0x7b, 0x3e, 0x6a, 0x39, 0x5c, + 0x60, 0x21, 0x29, 0x60, 0xb4, 0x5d, 0xa6, 0x57, 0xcc, 0xf7, 0x80, 0x6c, 0x60, 0x0b, 0x7b, 0x44, + 0x6c, 0x5b, 0xe9, 0x99, 0x0f, 0x21, 0x07, 0x02, 0xf3, 0x4c, 0xa0, 0xb3, 0xac, 0x7a, 0xbb, 0xc8, + 0xc5, 0x86, 0x5e, 0xb7, 0x2d, 0xaf, 0xdd, 0x0a, 0xa7, 0xa1, 0x98, 0x7e, 0x44, 0x5c, 0xcc, 0xc7, + 0xe6, 0x7c, 0x6c, 0x19, 0xd8, 0x6d, 0x11, 0xcb, 0x57, 0xeb, 0xee, 0x9e, 0xe3, 0xdb, 0x6a, 0x13, + 0xef, 0x89, 0x1d, 0x67, 0xeb, 0xb6, 0xd7, 0xb2, 0x3d, 0x3d, 0xb0, 0x35, 0xf8, 0x10, 0x60, 0x83, + 0x2f, 0x75, 0x07, 0x79, 0x58, 0xed, 0x2c, 0xef, 0x60, 0x1f, 0x2d, 0xab, 0x75, 0x9b, 0x08, 0x63, + 0x5e, 0xe6, 0xf3, 0x9e, 0x8f, 0x9a, 0xc4, 0x6a, 0x84, 0x22, 0xfc, 0x9b, 0x4b, 0x5d, 0x8c, 0x6a, + 0x61, 0x47, 0x12, 0x0a, 0x3a, 0xa8, 0x41, 0xac, 0x88, 0x7b, 0xe4, 0x1b, 0xe0, 0xcc, 0x37, 0xa8, + 0xc4, 0x26, 0xb7, 0xea, 0x4e, 0xe0, 0x1c, 0x0d, 0xbf, 0xd3, 0xc6, 0x9e, 0x0f, 0x2b, 0x60, 0x5c, + 0xd8, 0xab, 0x13, 0xa3, 0x24, 0x9d, 0x95, 0x16, 0x87, 0x35, 0x20, 0x86, 0xb6, 0x0d, 0x79, 0x17, + 0xcc, 0xa5, 0xaf, 0xf7, 0x1c, 0xdb, 0xf2, 0x30, 0xfc, 0x1a, 0x38, 0xc5, 0xfd, 0xad, 0x7b, 0x3e, + 0xf2, 0x31, 0x53, 0x31, 0x5e, 0x9d, 0x57, 0x58, 0x74, 0x74, 0x96, 0x95, 0xc4, 0xc2, 0xfb, 0x54, + 0x68, 0x63, 0xf8, 0x83, 0xc3, 0xca, 0x90, 0x36, 0xd1, 0x88, 0x8c, 0xc9, 0x3f, 0x93, 0x40, 0x39, + 0xb6, 0xd5, 0xe6, 0x2e, 0x22, 0x56, 0x88, 0xf4, 0x1a, 0x18, 0x71, 0x76, 0x91, 0x17, 0x6c, 0x30, + 0x59, 0xad, 0x28, 0xc9, 0xf0, 0x0b, 0x77, 0xaa, 0x51, 0x31, 0x2d, 0x90, 0x86, 0x5b, 0x00, 0x74, + 0x7d, 0x52, 0x2a, 0x30, 0x70, 0xaf, 0x28, 0xfc, 0x50, 0xa8, 0x03, 0x95, 0x20, 0xa6, 0xb9, 0x03, + 0x95, 0x1a, 0x6a, 0x60, 0xbe, 0xa5, 0x16, 0x59, 0x29, 0xff, 0x44, 0x4a, 0x38, 0x52, 0xa0, 0xe3, + 0x7e, 0x50, 0xc1, 0x68, 0x9d, 0x8d, 0x94, 0xa4, 0xb3, 0x27, 0x16, 0xc7, 0xab, 0x33, 0x29, 0xf8, + 0xe8, 0xbc, 0xc6, 0xc5, 0xe0, 0x9d, 0x14, 0x60, 0xaf, 0x0e, 0x04, 0x16, 0xec, 0x16, 0x43, 0xf6, + 0x77, 0x09, 0x8c, 0x30, 0xd5, 0x70, 0x16, 0x14, 0x99, 0x72, 0x71, 0x92, 0x63, 0xda, 0x49, 0xf6, + 0xbd, 0x6d, 0xc0, 0x33, 0x60, 0xac, 0x6e, 0x12, 0x6c, 0xf9, 0x74, 0xae, 0xc0, 0xe6, 0x8a, 0xc1, + 0xc0, 0xb6, 0x01, 0xa7, 0x84, 0x6b, 0x4f, 0xb0, 0x09, 0xee, 0xb9, 0x5b, 0xa0, 0xd8, 0xc2, 0x3e, + 0x32, 0x90, 0x8f, 0x4a, 0xc3, 0x0c, 0x9e, 0x9c, 0xed, 0xf3, 0x37, 0xb9, 0x24, 0x3f, 0xd9, 0x70, + 0x65, 0x32, 0xc0, 0x46, 0x92, 0x01, 0x06, 0x17, 0xc1, 0xe9, 0x87, 0x18, 0xeb, 0x8e, 0x6d, 0x9b, + 0x3a, 0x32, 0x0c, 0x17, 0x7b, 0x5e, 0x69, 0x94, 0xe1, 0x98, 0x7c, 0x88, 0x71, 0xcd, 0xb6, 0xcd, + 0xf5, 0x60, 0x54, 0xfe, 0xa1, 0x04, 0xce, 0xb1, 0x23, 0x78, 0x0b, 0x99, 0xc4, 0x40, 0xbe, 0xed, + 0x8a, 0xdd, 0xa9, 0x84, 0x88, 0x93, 0xaf, 0x80, 0xd3, 0x02, 0x60, 0xa8, 0x8f, 0x39, 0x63, 0x03, + 0x7e, 0x76, 0x58, 0x99, 0xdc, 0x43, 0x2d, 0x73, 0x4d, 0xe6, 0x13, 0xb2, 0xf6, 0x92, 0x90, 0xe5, + 0x9b, 0x24, 0xf1, 0x16, 0x92, 0x78, 0xd7, 0x8a, 0x3f, 0x78, 0xbf, 0x32, 0xf4, 0x8f, 0xf7, 0x2b, + 0x43, 0xf2, 0x3d, 0x20, 0xf7, 0x83, 0xc3, 0x03, 0xe3, 0x35, 0x70, 0x3a, 0x54, 0x18, 0xc3, 0xa3, + 0xbd, 0x54, 0x8f, 0xc8, 0xa7, 0x1b, 0x58, 0x8b, 0xa0, 0x8b, 0x18, 0x98, 0xae, 0x30, 0xdd, 0xc0, + 0xc4, 0x26, 0x9f, 0xcb, 0xc0, 0x38, 0x9c, 0xae, 0x81, 0xe9, 0x0e, 0xef, 0x71, 0xae, 0xfc, 0x06, + 0x78, 0x8d, 0x29, 0x5c, 0x37, 0xcd, 0x1a, 0x22, 0xae, 0xf7, 0x16, 0x32, 0xa9, 0xcf, 0xe8, 0xf4, + 0x46, 0x78, 0xb1, 0x72, 0xa7, 0xa6, 0x77, 0x25, 0x70, 0x31, 0x8f, 0x3a, 0x8e, 0xf3, 0xdb, 0xe0, + 0x0b, 0x0e, 0x22, 0xae, 0xde, 0x41, 0x26, 0xcd, 0xf1, 0x0c, 0x2b, 0xbf, 0xac, 0x2b, 0xbd, 0x81, + 0x4d, 0x15, 0x06, 0xfa, 0xa8, 0xba, 0xd0, 0x70, 0xcb, 0x08, 0xf5, 0x4e, 0x3a, 0x31, 0x11, 0xf9, + 0x5f, 0x12, 0x38, 0x37, 0x70, 0x15, 0xdc, 0xca, 0x0c, 0xcf, 0x33, 0x9f, 0x1d, 0x56, 0x66, 0x82, + 0xd3, 0x4b, 0x4a, 0xa4, 0xc4, 0xe9, 0x56, 0x4a, 0x14, 0x14, 0x92, 0x7a, 0x92, 0x12, 0x29, 0xe1, + 0x70, 0x13, 0x4c, 0x84, 0x52, 0x4d, 0xbc, 0xc7, 0x52, 0xc0, 0x78, 0x75, 0x4e, 0xe9, 0xbe, 0x70, + 0x4a, 0xf0, 0xc2, 0x29, 0xb5, 0xf6, 0x8e, 0x49, 0xea, 0x77, 0xf1, 0x9e, 0x16, 0x9e, 0xcb, 0x5d, + 0xbc, 0x27, 0x4f, 0x01, 0xc8, 0x0e, 0xa1, 0x86, 0x5c, 0xd4, 0x12, 0xd9, 0x5a, 0x7e, 0x13, 0x7c, + 0x31, 0x36, 0xca, 0xcf, 0x60, 0x15, 0x8c, 0x3a, 0x6c, 0x84, 0x3f, 0x13, 0xa5, 0x34, 0xc7, 0xd3, + 0x79, 0x9e, 0x47, 0xb8, 0xb4, 0xbc, 0x0e, 0x16, 0x62, 0xc9, 0x37, 0x8c, 0xc8, 0xfc, 0x0f, 0xd9, + 0x7f, 0x87, 0xc1, 0xd9, 0x0c, 0x1d, 0xe1, 0xaf, 0xcf, 0x9b, 0x3c, 0x92, 0xce, 0x2c, 0x1c, 0xd1, + 0x99, 0xf0, 0x02, 0x98, 0x0c, 0x15, 0x38, 0xf6, 0x23, 0xec, 0xb2, 0xf3, 0x38, 0xa1, 0x9d, 0x12, + 0xa3, 0x35, 0x3a, 0x08, 0xef, 0x82, 0x71, 0x03, 0x7b, 0x75, 0x97, 0x38, 0xec, 0xf1, 0x08, 0xb2, + 0xf3, 0x79, 0xf1, 0x78, 0x88, 0x62, 0x41, 0xbc, 0x1c, 0xb7, 0xba, 0xa2, 0xdc, 0xad, 0xd1, 0xd5, + 0xf0, 0x5b, 0x60, 0x36, 0xb4, 0xd9, 0x76, 0xb0, 0x4b, 0x1d, 0x11, 0x1a, 0x3f, 0xc2, 0x8c, 0x3f, + 0xf7, 0xf4, 0xc9, 0xd2, 0x3c, 0xd7, 0x1e, 0x3a, 0x8b, 0x1b, 0x7d, 0xdf, 0x77, 0x89, 0xd5, 0xd0, + 0x66, 0x84, 0x8e, 0x7b, 0x5c, 0x85, 0xf0, 0xc9, 0x97, 0xc0, 0xe8, 0x77, 0x10, 0x31, 0xb1, 0xc1, + 0xb2, 0x7a, 0x51, 0xe3, 0x5f, 0x70, 0x0d, 0x8c, 0xd2, 0x82, 0xa1, 0xed, 0x95, 0x4e, 0xb2, 0x07, + 0x5d, 0xce, 0x82, 0xbf, 0x61, 0x5b, 0xc6, 0x7d, 0x26, 0xa9, 0xf1, 0x15, 0xf0, 0x01, 0x08, 0x5d, + 0xaf, 0xfb, 0x76, 0x13, 0x5b, 0x5e, 0xa9, 0xc8, 0x80, 0x5e, 0xa2, 0xe6, 0xfd, 0xe5, 0xb0, 0x32, + 0x1d, 0xe8, 0xf2, 0x8c, 0xa6, 0x42, 0x6c, 0xb5, 0x85, 0xfc, 0x5d, 0x65, 0xdb, 0xf2, 0x9f, 0x3e, + 0x59, 0x02, 0x7c, 0x93, 0x6d, 0xcb, 0xd7, 0x26, 0x85, 0x8e, 0x07, 0x4c, 0x05, 0x75, 0x7e, 0xa8, + 0x35, 0x70, 0xfe, 0x58, 0xe0, 0x7c, 0x31, 0x1a, 0x38, 0x7f, 0x15, 0xcc, 0x74, 0x02, 0x1f, 0x60, + 0x4f, 0xaf, 0xb7, 0x5d, 0x97, 0xbe, 0xaa, 0xd8, 0xb1, 0xeb, 0xbb, 0x25, 0xc0, 0x2c, 0x9c, 0x0e, + 0xa7, 0x37, 0x83, 0xd9, 0xdb, 0x74, 0x52, 0x6e, 0x83, 0x4a, 0x66, 0x0c, 0xf3, 0xeb, 0xa1, 0x01, + 0xd0, 0x09, 0x47, 0x79, 0x6e, 0xaa, 0xf6, 0x5e, 0x91, 0x41, 0x61, 0xac, 0x45, 0xb4, 0xc8, 0x32, + 0x0f, 0xfb, 0x0d, 0xd3, 0xae, 0x37, 0xbd, 0x6f, 0x5a, 0x3e, 0x31, 0xbf, 0x8e, 0x1f, 0x07, 0x98, + 0xc4, 0x6d, 0x7d, 0x9b, 0xbf, 0x3b, 0xe9, 0x32, 0x1c, 0xdc, 0x35, 0x30, 0xb3, 0xc3, 0xe6, 0xf5, + 0x36, 0x15, 0xd0, 0x2d, 0xfc, 0x58, 0xd8, 0x1d, 0xdc, 0xb6, 0xa9, 0x9d, 0x94, 0xe5, 0xf2, 0x3a, + 0x7f, 0x44, 0x36, 0xc3, 0xab, 0xb8, 0xe5, 0xda, 0xad, 0x4d, 0x5e, 0x7b, 0x88, 0xeb, 0x1b, 0xab, + 0x4f, 0xa4, 0x78, 0x7d, 0x22, 0x6f, 0x81, 0xf3, 0x7d, 0x55, 0x70, 0x80, 0x03, 0x53, 0xc0, 0x75, + 0x30, 0xdb, 0x5b, 0xc2, 0xe5, 0x4e, 0x20, 0xff, 0x2e, 0xa4, 0xd5, 0xa7, 0xb9, 0x77, 0x8f, 0x55, + 0x67, 0x85, 0x78, 0x75, 0x76, 0x1e, 0x9c, 0xb2, 0x1f, 0x59, 0x91, 0x9c, 0x13, 0x14, 0x62, 0x13, + 0x6c, 0x50, 0x5c, 0xa4, 0xb0, 0x4a, 0x1b, 0xce, 0xaa, 0xd2, 0x46, 0x8e, 0x5d, 0xa5, 0xdd, 0x07, + 0xe3, 0xc4, 0x22, 0xbe, 0xce, 0x93, 0xf3, 0x28, 0x53, 0x54, 0xcd, 0x56, 0xb4, 0x6d, 0x11, 0x9f, + 0x20, 0x93, 0x7c, 0x97, 0x95, 0xa0, 0x2c, 0x65, 0x63, 0x1f, 0xbb, 0x9e, 0x06, 0xa8, 0x9a, 0x20, + 0x85, 0xc7, 0xcf, 0xf4, 0x64, 0xa2, 0xe6, 0x4c, 0x2b, 0xfb, 0x8a, 0xa9, 0x65, 0xdf, 0x46, 0xe2, + 0xde, 0x70, 0x22, 0xf1, 0x80, 0xb4, 0x70, 0xee, 0xb3, 0x6b, 0x26, 0x72, 0x7f, 0x4c, 0x07, 0x3f, + 0xc0, 0x3b, 0x40, 0xf0, 0x11, 0x9d, 0x72, 0x50, 0xfe, 0x42, 0x95, 0x95, 0x80, 0x7f, 0x2a, 0x82, + 0x7f, 0x2a, 0x0f, 0x04, 0x41, 0xdd, 0x28, 0x52, 0x2f, 0xbe, 0xf7, 0x69, 0x45, 0xd2, 0xc6, 0x1b, + 0x5d, 0x85, 0xf2, 0x66, 0x62, 0xb3, 0x2d, 0x8c, 0xbd, 0x1a, 0x76, 0xd9, 0xe5, 0xca, 0x8d, 0xf8, + 0x77, 0xa2, 0x16, 0x4c, 0xd7, 0xc2, 0x31, 0x3b, 0x80, 0x7a, 0xcb, 0xd3, 0x1d, 0xec, 0xea, 0xec, + 0xf6, 0x71, 0xd4, 0xb3, 0x31, 0x22, 0x21, 0x32, 0xe9, 0xa6, 0x4d, 0xac, 0x0d, 0x95, 0x82, 0xfe, + 0xcf, 0x61, 0xe5, 0xd5, 0x06, 0xf1, 0x77, 0xdb, 0x3b, 0x4a, 0xdd, 0x6e, 0x71, 0x8e, 0xca, 0xff, + 0x2c, 0x79, 0x46, 0x53, 0xf5, 0xf7, 0x1c, 0xec, 0xb1, 0x05, 0xda, 0xc4, 0xc3, 0xc8, 0xce, 0x14, + 0x38, 0xf1, 0x74, 0xbb, 0x83, 0x5d, 0x97, 0x18, 0x98, 0x05, 0x72, 0x51, 0x03, 0xc4, 0xbb, 0xc7, + 0x47, 0xe4, 0x0e, 0xb8, 0x2c, 0x8a, 0xb2, 0x34, 0xe8, 0x42, 0x2e, 0x7c, 0xb8, 0xe3, 0x04, 0x4d, + 0x3a, 0x36, 0x41, 0xfb, 0x50, 0x02, 0x4b, 0x39, 0x37, 0x0e, 0xb3, 0xed, 0x98, 0xb0, 0x43, 0x24, + 0x5b, 0x25, 0x3b, 0xe4, 0xd3, 0x74, 0xf1, 0x7b, 0xd4, 0x55, 0xf3, 0xe2, 0x58, 0xdd, 0x7e, 0x6f, + 0x10, 0xd1, 0x4b, 0xb1, 0x69, 0x22, 0xd2, 0xca, 0x1b, 0x44, 0x70, 0x15, 0x8c, 0x19, 0xd8, 0xb1, + 0x3d, 0xe2, 0xdb, 0x2e, 0xaf, 0x0e, 0x4b, 0x4f, 0x9f, 0x2c, 0x4d, 0x71, 0x3c, 0xf1, 0x17, 0xbc, + 0x2b, 0x2a, 0x7f, 0x3f, 0x25, 0xf8, 0x22, 0xbb, 0x73, 0xff, 0x21, 0x30, 0x52, 0xa7, 0x03, 0xdc, + 0x77, 0x7d, 0x62, 0xee, 0x0a, 0x75, 0xd3, 0x2f, 0x3f, 0xad, 0x2c, 0xe6, 0x8c, 0x39, 0x4f, 0x0b, + 0x34, 0xcb, 0xbf, 0x92, 0xc0, 0xe4, 0x2d, 0x01, 0x8b, 0xed, 0x1e, 0xb7, 0x49, 0xca, 0x6d, 0x53, + 0x17, 0x6d, 0xe1, 0xff, 0x86, 0xf6, 0xdd, 0x7e, 0x6e, 0xcb, 0x5d, 0xa9, 0xbe, 0xb0, 0x96, 0xc5, + 0x6f, 0xa4, 0xc4, 0xd3, 0x9b, 0x80, 0xc3, 0x8f, 0xf1, 0x06, 0x18, 0x65, 0xf0, 0xc5, 0x1d, 0x38, + 0xdb, 0x7b, 0x07, 0xe2, 0x47, 0x20, 0x6a, 0xf3, 0x60, 0xd5, 0x8b, 0x0b, 0xf9, 0x9b, 0x89, 0x56, + 0xd3, 0x1b, 0xa4, 0x43, 0xb3, 0x6a, 0xfe, 0x12, 0xff, 0xc7, 0x05, 0x30, 0x9f, 0xa1, 0xa1, 0xdb, + 0xad, 0x32, 0x91, 0xe7, 0xeb, 0xa8, 0xde, 0x3c, 0x46, 0x92, 0xa7, 0x4b, 0xd7, 0xeb, 0x4d, 0x3a, + 0x07, 0xb7, 0xc0, 0x44, 0xc3, 0x45, 0x75, 0x4c, 0x53, 0x2f, 0xb1, 0x0d, 0x6e, 0xf7, 0x6c, 0x8f, + 0xa2, 0x5b, 0xbc, 0x5b, 0x19, 0xe8, 0xf9, 0x69, 0xf0, 0x58, 0xd0, 0x85, 0x35, 0xb6, 0x0e, 0xde, + 0x06, 0xe3, 0x2e, 0x6e, 0xd9, 0x94, 0x94, 0x62, 0x1f, 0x71, 0xfa, 0x95, 0x0f, 0x0f, 0xe0, 0x0b, + 0x6f, 0xfb, 0x08, 0x96, 0x41, 0xd1, 0xc0, 0x0d, 0x17, 0x19, 0xd8, 0x60, 0xf5, 0x41, 0x51, 0x0b, + 0xbf, 0xab, 0xff, 0x9c, 0x01, 0x23, 0xcc, 0x2d, 0xf0, 0xd7, 0x12, 0x98, 0x4a, 0x7b, 0x07, 0xe1, + 0xd2, 0x80, 0x22, 0x33, 0xde, 0x35, 0x2c, 0x2b, 0x79, 0xc5, 0x03, 0xb7, 0xcb, 0xd7, 0xbe, 0xf7, + 0xf1, 0xdf, 0x7e, 0x54, 0x50, 0xe1, 0x92, 0x1a, 0x6f, 0xc6, 0x86, 0xc7, 0xc9, 0x9f, 0x4f, 0x75, + 0x3f, 0x72, 0xc0, 0x07, 0xf0, 0xe7, 0x12, 0x67, 0x91, 0xf1, 0x9e, 0x1b, 0xbc, 0x3c, 0x60, 0xfb, + 0x58, 0xe3, 0xb0, 0xbc, 0x94, 0x53, 0x9a, 0x63, 0x55, 0x18, 0xd6, 0x45, 0xf8, 0x4a, 0x16, 0xd6, + 0xa0, 0x7f, 0xa7, 0xee, 0xb3, 0xfa, 0xeb, 0x00, 0x7e, 0x22, 0xda, 0x96, 0xa9, 0x6d, 0x20, 0xb8, + 0x92, 0xb1, 0x7b, 0xbf, 0x1e, 0x56, 0xf9, 0xea, 0xd1, 0x16, 0x71, 0xe4, 0xf7, 0x18, 0xf2, 0x6d, + 0x78, 0x27, 0x81, 0x3c, 0x24, 0x03, 0x7a, 0xac, 0x1f, 0x10, 0x77, 0xb6, 0xba, 0x9f, 0x24, 0xbe, + 0x69, 0xa6, 0x45, 0x1b, 0x40, 0x83, 0x4d, 0x4b, 0xe9, 0x5e, 0x0d, 0x36, 0x2d, 0xad, 0xc7, 0x94, + 0xc3, 0xb4, 0x18, 0xfa, 0xa4, 0x69, 0xc9, 0x3e, 0xc8, 0x01, 0xfc, 0x58, 0xe4, 0xc6, 0xbe, 0xbd, + 0x23, 0xf8, 0xe5, 0x0c, 0xb4, 0x79, 0x1a, 0x58, 0xe5, 0xeb, 0xc7, 0x5b, 0xcc, 0x4d, 0xae, 0x32, + 0x93, 0x2f, 0xc3, 0x8b, 0x09, 0x93, 0xb9, 0x09, 0xba, 0x43, 0x55, 0x24, 0x2e, 0xcc, 0x1e, 0x18, + 0x8f, 0x74, 0x5d, 0xe0, 0xcb, 0x19, 0x00, 0x62, 0xad, 0x9a, 0xf2, 0x85, 0x01, 0x52, 0x1c, 0xcf, + 0x3c, 0xc3, 0x33, 0x03, 0xa7, 0x13, 0x78, 0x02, 0xca, 0x00, 0x7f, 0x2f, 0x81, 0x99, 0x0c, 0x5e, + 0x0a, 0xaf, 0xe4, 0xa6, 0xb0, 0x02, 0xd3, 0xf2, 0x11, 0x56, 0x70, 0x7c, 0xaf, 0x33, 0x7c, 0x55, + 0x78, 0x25, 0xeb, 0xde, 0x76, 0x39, 0x71, 0xc2, 0x6b, 0x4f, 0x24, 0xce, 0x0b, 0xd3, 0xe8, 0x2f, + 0xcc, 0xe2, 0xdf, 0x7d, 0xf8, 0x74, 0x79, 0xe5, 0x48, 0x6b, 0x06, 0x24, 0x9e, 0x0c, 0xd2, 0x0d, + 0xff, 0x90, 0xfc, 0x8f, 0x44, 0x9c, 0x16, 0xc3, 0xab, 0x03, 0x7c, 0x98, 0x4a, 0xc4, 0xcb, 0xd7, + 0x8e, 0xb8, 0x2a, 0x6f, 0xd6, 0x24, 0x86, 0xba, 0x1f, 0xb2, 0xc1, 0x03, 0xf8, 0x0b, 0x89, 0xb7, + 0x0d, 0x63, 0x59, 0x18, 0x5e, 0xca, 0x93, 0xab, 0x05, 0xd4, 0xcb, 0xf9, 0x84, 0x39, 0xc2, 0x15, + 0x86, 0x70, 0x09, 0x5e, 0xea, 0x9b, 0xd7, 0x13, 0xa1, 0xf1, 0x47, 0x09, 0x94, 0xb2, 0x88, 0x23, + 0x5c, 0xce, 0xf7, 0x0a, 0x46, 0x88, 0x6a, 0xb9, 0x7a, 0x94, 0x25, 0x1c, 0xf8, 0x1a, 0x03, 0x7e, + 0x15, 0x56, 0x07, 0x3c, 0x9e, 0xac, 0xa0, 0x49, 0xe0, 0xff, 0x93, 0x94, 0x68, 0x79, 0x44, 0x59, + 0x0c, 0x1c, 0x84, 0x26, 0x85, 0xb8, 0x66, 0x86, 0x76, 0x3f, 0x9a, 0x2a, 0x5f, 0x67, 0x26, 0xac, + 0xc2, 0xab, 0x59, 0x26, 0xc4, 0x49, 0x6c, 0xc2, 0x88, 0x67, 0x12, 0xb8, 0x90, 0x8b, 0xd9, 0xc1, + 0x1b, 0xd9, 0x19, 0x37, 0x0f, 0x17, 0x2d, 0xdf, 0x3c, 0xf6, 0xfa, 0xbc, 0x49, 0x28, 0x6e, 0xa8, + 0xde, 0x25, 0x8e, 0x1f, 0x4a, 0x60, 0x2a, 0xad, 0x4e, 0xcf, 0x73, 0x48, 0x49, 0x62, 0x98, 0xe7, + 0x90, 0x7a, 0xe8, 0x9c, 0x7c, 0x97, 0x61, 0xbf, 0x0d, 0x37, 0xd5, 0x9e, 0xff, 0x98, 0x47, 0xe1, + 0x07, 0x2d, 0x1b, 0x56, 0xfb, 0x27, 0x9f, 0xd9, 0x90, 0x6c, 0x1d, 0xc0, 0x3f, 0x4b, 0x60, 0x3a, + 0x95, 0x76, 0xc0, 0xa3, 0x60, 0xf3, 0x06, 0x55, 0x0d, 0x7d, 0x99, 0x8d, 0xfc, 0x55, 0x66, 0xd1, + 0x1a, 0x7c, 0x3d, 0xb7, 0x45, 0xc9, 0xa7, 0xe1, 0xb7, 0x12, 0x98, 0x4e, 0x65, 0x14, 0x70, 0x50, + 0x09, 0x9c, 0x20, 0x2f, 0x65, 0x35, 0xb7, 0x3c, 0x07, 0xbf, 0xca, 0xc0, 0x5f, 0x81, 0x4a, 0x56, + 0x28, 0x99, 0x7c, 0x45, 0x1c, 0xf2, 0xc6, 0xf6, 0x07, 0xcf, 0x16, 0xa4, 0x8f, 0x9e, 0x2d, 0x48, + 0x7f, 0x7d, 0xb6, 0x20, 0xbd, 0xf7, 0x7c, 0x61, 0xe8, 0xa3, 0xe7, 0x0b, 0x43, 0x9f, 0x3c, 0x5f, + 0x18, 0x7a, 0x5b, 0x8d, 0xf0, 0x59, 0x64, 0x9a, 0xc4, 0xda, 0x21, 0xbe, 0x17, 0x68, 0x7f, 0x9c, + 0xd8, 0x84, 0x91, 0xdb, 0x9d, 0x51, 0x46, 0x3f, 0x56, 0xfe, 0x17, 0x00, 0x00, 0xff, 0xff, 0xe0, + 0x53, 0x5d, 0x63, 0x08, 0x22, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -2038,6 +2162,8 @@ type QueryClient interface { QueryAllConsumerFeesPerBlockOverrides(ctx context.Context, in *QueryAllConsumerFeesPerBlockOverridesRequest, opts ...grpc.CallOption) (*QueryAllConsumerFeesPerBlockOverridesResponse, error) ConsumerFeePoolClaim(ctx context.Context, in *QueryConsumerFeePoolClaimRequest, opts ...grpc.CallOption) (*QueryConsumerFeePoolClaimResponse, error) ConsumerFeePoolClaims(ctx context.Context, in *QueryConsumerFeePoolClaimsRequest, opts ...grpc.CallOption) (*QueryConsumerFeePoolClaimsResponse, error) + // QueryConsumerLiveness returns the liveness state of a launched consumer. + QueryConsumerLiveness(ctx context.Context, in *QueryConsumerLivenessRequest, opts ...grpc.CallOption) (*QueryConsumerLivenessResponse, error) } type queryClient struct { @@ -2183,6 +2309,15 @@ func (c *queryClient) ConsumerFeePoolClaims(ctx context.Context, in *QueryConsum return out, nil } +func (c *queryClient) QueryConsumerLiveness(ctx context.Context, in *QueryConsumerLivenessRequest, opts ...grpc.CallOption) (*QueryConsumerLivenessResponse, error) { + out := new(QueryConsumerLivenessResponse) + err := c.cc.Invoke(ctx, "/vaas.provider.v1.Query/QueryConsumerLiveness", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // QueryServer is the server API for Query service. type QueryServer interface { // ConsumerGenesis queries the genesis state needed to start a consumer chain @@ -2227,6 +2362,8 @@ type QueryServer interface { QueryAllConsumerFeesPerBlockOverrides(context.Context, *QueryAllConsumerFeesPerBlockOverridesRequest) (*QueryAllConsumerFeesPerBlockOverridesResponse, error) ConsumerFeePoolClaim(context.Context, *QueryConsumerFeePoolClaimRequest) (*QueryConsumerFeePoolClaimResponse, error) ConsumerFeePoolClaims(context.Context, *QueryConsumerFeePoolClaimsRequest) (*QueryConsumerFeePoolClaimsResponse, error) + // QueryConsumerLiveness returns the liveness state of a launched consumer. + QueryConsumerLiveness(context.Context, *QueryConsumerLivenessRequest) (*QueryConsumerLivenessResponse, error) } // UnimplementedQueryServer can be embedded to have forward compatible implementations. @@ -2278,6 +2415,9 @@ func (*UnimplementedQueryServer) ConsumerFeePoolClaim(ctx context.Context, req * func (*UnimplementedQueryServer) ConsumerFeePoolClaims(ctx context.Context, req *QueryConsumerFeePoolClaimsRequest) (*QueryConsumerFeePoolClaimsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ConsumerFeePoolClaims not implemented") } +func (*UnimplementedQueryServer) QueryConsumerLiveness(ctx context.Context, req *QueryConsumerLivenessRequest) (*QueryConsumerLivenessResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method QueryConsumerLiveness not implemented") +} func RegisterQueryServer(s grpc1.Server, srv QueryServer) { s.RegisterService(&_Query_serviceDesc, srv) @@ -2553,6 +2693,24 @@ func _Query_ConsumerFeePoolClaims_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _Query_QueryConsumerLiveness_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryConsumerLivenessRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QueryServer).QueryConsumerLiveness(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/vaas.provider.v1.Query/QueryConsumerLiveness", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QueryServer).QueryConsumerLiveness(ctx, req.(*QueryConsumerLivenessRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _Query_serviceDesc = grpc.ServiceDesc{ ServiceName: "vaas.provider.v1.Query", HandlerType: (*QueryServer)(nil), @@ -2617,6 +2775,10 @@ var _Query_serviceDesc = grpc.ServiceDesc{ MethodName: "ConsumerFeePoolClaims", Handler: _Query_ConsumerFeePoolClaims_Handler, }, + { + MethodName: "QueryConsumerLiveness", + Handler: _Query_QueryConsumerLiveness_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "vaas/provider/v1/query.proto", @@ -3948,6 +4110,91 @@ func (m *QueryConsumerFeePoolClaimsResponse) MarshalToSizedBuffer(dAtA []byte) ( return len(dAtA) - i, nil } +func (m *QueryConsumerLivenessRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryConsumerLivenessRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryConsumerLivenessRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.ConsumerId != 0 { + i = encodeVarintQuery(dAtA, i, uint64(m.ConsumerId)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *QueryConsumerLivenessResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryConsumerLivenessResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryConsumerLivenessResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Degraded { + i-- + if m.Degraded { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x20 + } + n17, err17 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(m.RemovalEta, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(m.RemovalEta):]) + if err17 != nil { + return 0, err17 + } + i -= n17 + i = encodeVarintQuery(dAtA, i, uint64(n17)) + i-- + dAtA[i] = 0x1a + n18, err18 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.GracePeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.GracePeriod):]) + if err18 != nil { + return 0, err18 + } + i -= n18 + i = encodeVarintQuery(dAtA, i, uint64(n18)) + i-- + dAtA[i] = 0x12 + n19, err19 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(m.LastAckTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(m.LastAckTime):]) + if err19 != nil { + return 0, err19 + } + i -= n19 + i = encodeVarintQuery(dAtA, i, uint64(n19)) + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + func encodeVarintQuery(dAtA []byte, offset int, v uint64) int { offset -= sovQuery(v) base := offset @@ -4502,6 +4749,36 @@ func (m *QueryConsumerFeePoolClaimsResponse) Size() (n int) { return n } +func (m *QueryConsumerLivenessRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.ConsumerId != 0 { + n += 1 + sovQuery(uint64(m.ConsumerId)) + } + return n +} + +func (m *QueryConsumerLivenessResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = github_com_cosmos_gogoproto_types.SizeOfStdTime(m.LastAckTime) + n += 1 + l + sovQuery(uint64(l)) + l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.GracePeriod) + n += 1 + l + sovQuery(uint64(l)) + l = github_com_cosmos_gogoproto_types.SizeOfStdTime(m.RemovalEta) + n += 1 + l + sovQuery(uint64(l)) + if m.Degraded { + n += 2 + } + return n +} + func sovQuery(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -8077,6 +8354,244 @@ func (m *QueryConsumerFeePoolClaimsResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *QueryConsumerLivenessRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryConsumerLivenessRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryConsumerLivenessRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ConsumerId", wireType) + } + m.ConsumerId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ConsumerId |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *QueryConsumerLivenessResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryConsumerLivenessResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryConsumerLivenessResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LastAckTime", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_cosmos_gogoproto_types.StdTimeUnmarshal(&m.LastAckTime, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field GracePeriod", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_cosmos_gogoproto_types.StdDurationUnmarshal(&m.GracePeriod, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RemovalEta", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_cosmos_gogoproto_types.StdTimeUnmarshal(&m.RemovalEta, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Degraded", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Degraded = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipQuery(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/x/vaas/provider/types/query.pb.gw.go b/x/vaas/provider/types/query.pb.gw.go index 57c6058..51b18f7 100644 --- a/x/vaas/provider/types/query.pb.gw.go +++ b/x/vaas/provider/types/query.pb.gw.go @@ -861,6 +861,60 @@ func local_request_Query_ConsumerFeePoolClaims_0(ctx context.Context, marshaler } +func request_Query_QueryConsumerLiveness_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryConsumerLivenessRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["consumer_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "consumer_id") + } + + protoReq.ConsumerId, err = runtime.Uint64(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "consumer_id", err) + } + + msg, err := client.QueryConsumerLiveness(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Query_QueryConsumerLiveness_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryConsumerLivenessRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["consumer_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "consumer_id") + } + + protoReq.ConsumerId, err = runtime.Uint64(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "consumer_id", err) + } + + msg, err := server.QueryConsumerLiveness(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterQueryHandlerServer registers the http handlers for service Query to "mux". // UnaryRPC :call QueryServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -1212,6 +1266,29 @@ func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, serv }) + mux.Handle("GET", pattern_Query_QueryConsumerLiveness_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Query_QueryConsumerLiveness_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_QueryConsumerLiveness_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -1553,6 +1630,26 @@ func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie }) + mux.Handle("GET", pattern_Query_QueryConsumerLiveness_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Query_QueryConsumerLiveness_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_QueryConsumerLiveness_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -1586,6 +1683,8 @@ var ( pattern_Query_ConsumerFeePoolClaim_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4, 1, 0, 4, 1, 5, 5}, []string{"vaas", "provider", "v1", "consumer_fee_pool_claim", "consumer_id", "depositor"}, "", runtime.AssumeColonVerbOpt(false))) pattern_Query_ConsumerFeePoolClaims_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"vaas", "provider", "v1", "consumer_fee_pool_claims", "consumer_id"}, "", runtime.AssumeColonVerbOpt(false))) + + pattern_Query_QueryConsumerLiveness_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"vaas", "provider", "consumer_liveness", "consumer_id"}, "", runtime.AssumeColonVerbOpt(false))) ) var ( @@ -1618,4 +1717,6 @@ var ( forward_Query_ConsumerFeePoolClaim_0 = runtime.ForwardResponseMessage forward_Query_ConsumerFeePoolClaims_0 = runtime.ForwardResponseMessage + + forward_Query_QueryConsumerLiveness_0 = runtime.ForwardResponseMessage ) diff --git a/x/vaas/types/events.go b/x/vaas/types/events.go index 34ef5a6..1b7cc99 100644 --- a/x/vaas/types/events.go +++ b/x/vaas/types/events.go @@ -11,6 +11,11 @@ const ( EventTypeSubmitConsumerDoubleVoting = "submit_consumer_double_voting" EventTypeExecuteConsumerChainSlash = "execute_consumer_chain_slash" EventTypeConsumerEvidenceRequest = "consumer_evidence_request" + // EventTypeSnapshotResync is emitted by the consumer when it applies a + // snapshot VSC packet (is_snapshot=true), i.e. it replaces its cross-chain + // validator set rather than accumulating a diff. Emitted only on snapshots, + // not on ordinary diffs. + EventTypeSnapshotResync = "vaas_snapshot_resync" AttributeKeyAckSuccess = "success" AttributeKeyAck = "acknowledgement" @@ -34,4 +39,5 @@ const ( AttributeValidatorAddress = "validator_address" AttributeInfractionType = "infraction_type" AttributeValSetUpdateID = "valset_update_id" + AttributeNumValidators = "num_validators" ) diff --git a/x/vaas/types/params.go b/x/vaas/types/params.go index f3e9344..9747387 100644 --- a/x/vaas/types/params.go +++ b/x/vaas/types/params.go @@ -24,12 +24,14 @@ func NewConsumerParams(enabled bool, vaasTimeoutPeriod time.Duration, historicalEntries int64, consumerUnbondingPeriod time.Duration, + safeModeThreshold time.Duration, ) ConsumerParams { return ConsumerParams{ Enabled: enabled, VaasTimeoutPeriod: vaasTimeoutPeriod, HistoricalEntries: historicalEntries, UnbondingPeriod: consumerUnbondingPeriod, + SafeModeThreshold: safeModeThreshold, } } @@ -40,6 +42,7 @@ func DefaultConsumerParams() ConsumerParams { DefaultVAASTimeoutPeriod, DefaultHistoricalEntries, DefaultConsumerUnbondingPeriod, + DefaultSafeModeThreshold, ) } @@ -54,5 +57,8 @@ func (p ConsumerParams) Validate() error { if err := ValidateDuration(p.UnbondingPeriod); err != nil { return err } + if err := ValidateDuration(p.SafeModeThreshold); err != nil { + return err + } return nil } diff --git a/x/vaas/types/shared_consumer.pb.go b/x/vaas/types/shared_consumer.pb.go index 3a265cd..82ddfb9 100644 --- a/x/vaas/types/shared_consumer.pb.go +++ b/x/vaas/types/shared_consumer.pb.go @@ -47,6 +47,9 @@ type ConsumerParams struct { // Unbonding period for the consumer, // which should be smaller than that of the provider in general. UnbondingPeriod time.Duration `protobuf:"bytes,4,opt,name=unbonding_period,json=unbondingPeriod,proto3,stdduration" json:"unbonding_period"` + // How long the consumer may go without receiving a VSC packet before its + // tx admission gate enters safe mode (only ibc.core and gov messages pass). + SafeModeThreshold time.Duration `protobuf:"bytes,5,opt,name=safe_mode_threshold,json=safeModeThreshold,proto3,stdduration" json:"safe_mode_threshold"` } func (m *ConsumerParams) Reset() { *m = ConsumerParams{} } @@ -110,6 +113,13 @@ func (m *ConsumerParams) GetUnbondingPeriod() time.Duration { return 0 } +func (m *ConsumerParams) GetSafeModeThreshold() time.Duration { + if m != nil { + return m.SafeModeThreshold + } + return 0 +} + // ConsumerGenesisState defines shared genesis information between provider and // consumer type ConsumerGenesisState struct { @@ -258,42 +268,44 @@ func init() { func init() { proto.RegisterFile("vaas/v1/shared_consumer.proto", fileDescriptor_58a750255ca5daa3) } var fileDescriptor_58a750255ca5daa3 = []byte{ - // 560 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x53, 0x41, 0x6f, 0xd4, 0x3c, - 0x10, 0xdd, 0x74, 0xab, 0x36, 0x9f, 0xb7, 0x5f, 0x4b, 0x4d, 0x11, 0x4b, 0x2b, 0xd2, 0x55, 0x0f, - 0x68, 0x25, 0x84, 0xad, 0x16, 0x21, 0xae, 0xb4, 0x05, 0x21, 0x2e, 0x55, 0x95, 0x85, 0x22, 0x71, - 0x89, 0x9c, 0x64, 0x9a, 0xb5, 0x94, 0xb5, 0x23, 0xdb, 0x49, 0xe1, 0x17, 0x70, 0xe5, 0xc8, 0x9f, - 0xe1, 0xde, 0x63, 0x8f, 0x9c, 0x00, 0x75, 0xff, 0x07, 0x42, 0x71, 0x9c, 0x6d, 0x7a, 0x82, 0x53, - 0xfc, 0x3c, 0xf3, 0x26, 0x6f, 0x9e, 0x67, 0xd0, 0xc3, 0x8a, 0x31, 0x4d, 0xab, 0x7d, 0xaa, 0xa7, - 0x4c, 0x41, 0x1a, 0x25, 0x52, 0xe8, 0x72, 0x06, 0x8a, 0x14, 0x4a, 0x1a, 0x89, 0x57, 0xeb, 0x30, - 0xa9, 0xf6, 0xb7, 0x77, 0x0c, 0x88, 0x14, 0xd4, 0x8c, 0x0b, 0x43, 0x59, 0x9c, 0x70, 0x6a, 0x3e, - 0x15, 0xa0, 0x9b, 0xac, 0x6d, 0xca, 0xe3, 0x84, 0xe6, 0x3c, 0x9b, 0x9a, 0x24, 0xe7, 0x20, 0x8c, - 0xa6, 0x9d, 0xec, 0x6a, 0xbf, 0x83, 0x1c, 0x21, 0xc8, 0xa4, 0xcc, 0x72, 0xa0, 0x16, 0xc5, 0xe5, - 0x39, 0x4d, 0x4b, 0xc5, 0x0c, 0x97, 0xc2, 0xc5, 0xb7, 0x32, 0x99, 0x49, 0x7b, 0xa4, 0xf5, 0xa9, - 0xb9, 0xdd, 0xfb, 0xed, 0xa1, 0xf5, 0x63, 0xa7, 0xef, 0x94, 0x29, 0x36, 0xd3, 0x78, 0x88, 0x56, - 0x41, 0xb0, 0x38, 0x87, 0x74, 0xe8, 0x8d, 0xbc, 0xb1, 0x1f, 0xb6, 0x10, 0x4f, 0xd0, 0xdd, 0x5a, - 0x7b, 0x64, 0xf8, 0x0c, 0x64, 0x69, 0xa2, 0x02, 0x14, 0x97, 0xe9, 0x70, 0x69, 0xe4, 0x8d, 0x07, - 0x07, 0x0f, 0x48, 0x23, 0x80, 0xb4, 0x02, 0xc8, 0x4b, 0x27, 0xe0, 0xc8, 0xbf, 0xfc, 0xb1, 0xdb, - 0xfb, 0xfa, 0x73, 0xd7, 0x0b, 0x37, 0x6b, 0xfe, 0xdb, 0x86, 0x7e, 0x6a, 0xd9, 0xf8, 0x09, 0xc2, - 0x53, 0xae, 0x8d, 0x54, 0x3c, 0x61, 0x79, 0x04, 0xc2, 0x28, 0x0e, 0x7a, 0xd8, 0x1f, 0x79, 0xe3, - 0x7e, 0xb8, 0x79, 0x13, 0x79, 0xd5, 0x04, 0xf0, 0x09, 0xba, 0x53, 0x8a, 0x58, 0x8a, 0x94, 0x8b, - 0xac, 0x15, 0xb0, 0xfc, 0xef, 0x02, 0x36, 0x16, 0xe4, 0xe6, 0xf7, 0x7b, 0xdf, 0x3c, 0xb4, 0xd5, - 0x1a, 0xf0, 0x1a, 0x04, 0x68, 0xae, 0x27, 0x86, 0x19, 0xc0, 0xcf, 0xd0, 0x4a, 0x61, 0x0d, 0xb1, - 0x2e, 0x0c, 0x0e, 0xee, 0x13, 0xf7, 0x6e, 0xe4, 0xb6, 0x5f, 0x47, 0xcb, 0x75, 0xf1, 0xd0, 0x25, - 0xe3, 0xe7, 0xc8, 0x2f, 0x94, 0xac, 0x78, 0x0a, 0xca, 0x19, 0x73, 0x6f, 0x41, 0x3c, 0x75, 0x81, - 0x37, 0xe2, 0x5c, 0x3a, 0xda, 0x22, 0x19, 0xef, 0xa0, 0xff, 0x04, 0x5c, 0x44, 0xc9, 0x94, 0x71, - 0x61, 0xdb, 0xf7, 0x43, 0x5f, 0xc0, 0xc5, 0x71, 0x8d, 0xeb, 0x37, 0x29, 0x14, 0x9c, 0x1d, 0x1e, - 0x4e, 0x6c, 0xb3, 0x7e, 0xd8, 0xc2, 0xbd, 0xcf, 0x4b, 0x68, 0xad, 0x5b, 0x17, 0x9f, 0xa0, 0xb5, - 0x66, 0x62, 0x22, 0x5d, 0xf7, 0xe1, 0xd4, 0x3f, 0x26, 0x3c, 0x4e, 0x48, 0x77, 0x9e, 0x48, 0x67, - 0x82, 0xea, 0xa6, 0xec, 0xad, 0x6d, 0x3d, 0x1c, 0x24, 0x37, 0x00, 0xbf, 0x47, 0x1b, 0xf5, 0x00, - 0x83, 0xd0, 0xa5, 0x76, 0x25, 0x9b, 0xbe, 0xc8, 0x5f, 0x4b, 0xb6, 0xb4, 0xa6, 0xea, 0x7a, 0x72, - 0x0b, 0xe3, 0x13, 0xb4, 0xc1, 0x05, 0x37, 0x9c, 0xe5, 0x51, 0xc5, 0xf2, 0x48, 0x83, 0x19, 0xf6, - 0x47, 0xfd, 0xf1, 0xe0, 0x60, 0xd4, 0xad, 0x53, 0x2f, 0x06, 0x39, 0x63, 0x39, 0x4f, 0x99, 0x91, - 0xea, 0x5d, 0x91, 0x32, 0x03, 0xce, 0xbb, 0xff, 0x1d, 0xfd, 0x8c, 0xe5, 0x13, 0x30, 0x47, 0x2f, - 0x2e, 0xaf, 0x03, 0xef, 0xea, 0x3a, 0xf0, 0x7e, 0x5d, 0x07, 0xde, 0x97, 0x79, 0xd0, 0xbb, 0x9a, - 0x07, 0xbd, 0xef, 0xf3, 0xa0, 0xf7, 0xe1, 0x51, 0xc6, 0xcd, 0xb4, 0x8c, 0x49, 0x22, 0x67, 0x94, - 0xe5, 0x39, 0x17, 0x31, 0x37, 0x9a, 0xda, 0x2d, 0xfd, 0xd8, 0x7c, 0xec, 0xe6, 0xc5, 0x2b, 0x76, - 0x72, 0x9e, 0xfe, 0x09, 0x00, 0x00, 0xff, 0xff, 0xa2, 0x7b, 0x1a, 0x0c, 0xc1, 0x03, 0x00, 0x00, + // 588 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0x41, 0x4f, 0xd4, 0x40, + 0x14, 0xde, 0xb2, 0x08, 0xeb, 0x2c, 0x82, 0x14, 0x8c, 0x2b, 0xc4, 0xb2, 0xe1, 0x60, 0x36, 0x31, + 0x4e, 0x03, 0xc6, 0x78, 0x15, 0xd0, 0x18, 0x0f, 0x12, 0xd2, 0x45, 0x4c, 0xbc, 0x34, 0xd3, 0xf6, + 0xd1, 0x4e, 0x32, 0x3b, 0xd3, 0xcc, 0x4c, 0x17, 0xfd, 0x05, 0x5e, 0x3d, 0xfa, 0x67, 0xbc, 0x73, + 0x24, 0xf1, 0xe2, 0x49, 0x0d, 0xfc, 0x11, 0x33, 0xd3, 0xe9, 0x52, 0x4e, 0xea, 0xa9, 0xf3, 0xcd, + 0x7b, 0xdf, 0xeb, 0xf7, 0xbe, 0x79, 0x0f, 0x3d, 0x9c, 0x12, 0xa2, 0xc2, 0xe9, 0x4e, 0xa8, 0x0a, + 0x22, 0x21, 0x8b, 0x53, 0xc1, 0x55, 0x35, 0x01, 0x89, 0x4b, 0x29, 0xb4, 0xf0, 0x17, 0x4d, 0x18, + 0x4f, 0x77, 0x36, 0x36, 0x35, 0xf0, 0x0c, 0xe4, 0x84, 0x72, 0x1d, 0x92, 0x24, 0xa5, 0xa1, 0xfe, + 0x54, 0x82, 0xaa, 0xb3, 0x36, 0x42, 0x9a, 0xa4, 0x21, 0xa3, 0x79, 0xa1, 0x53, 0x46, 0x81, 0x6b, + 0x15, 0xb6, 0xb2, 0xa7, 0x3b, 0x2d, 0xe4, 0x08, 0x41, 0x2e, 0x44, 0xce, 0x20, 0xb4, 0x28, 0xa9, + 0x4e, 0xc3, 0xac, 0x92, 0x44, 0x53, 0xc1, 0x5d, 0x7c, 0x3d, 0x17, 0xb9, 0xb0, 0xc7, 0xd0, 0x9c, + 0xea, 0xdb, 0xed, 0xef, 0x73, 0x68, 0xf9, 0xc0, 0xe9, 0x3b, 0x22, 0x92, 0x4c, 0x94, 0x3f, 0x40, + 0x8b, 0xc0, 0x49, 0xc2, 0x20, 0x1b, 0x78, 0x43, 0x6f, 0xd4, 0x8b, 0x1a, 0xe8, 0x8f, 0xd1, 0x9a, + 0xd1, 0x1e, 0x6b, 0x3a, 0x01, 0x51, 0xe9, 0xb8, 0x04, 0x49, 0x45, 0x36, 0x98, 0x1b, 0x7a, 0xa3, + 0xfe, 0xee, 0x03, 0x5c, 0x0b, 0xc0, 0x8d, 0x00, 0xfc, 0xd2, 0x09, 0xd8, 0xef, 0x9d, 0xff, 0xdc, + 0xea, 0x7c, 0xfd, 0xb5, 0xe5, 0x45, 0xab, 0x86, 0x7f, 0x5c, 0xd3, 0x8f, 0x2c, 0xdb, 0x7f, 0x82, + 0xfc, 0x82, 0x2a, 0x2d, 0x24, 0x4d, 0x09, 0x8b, 0x81, 0x6b, 0x49, 0x41, 0x0d, 0xba, 0x43, 0x6f, + 0xd4, 0x8d, 0x56, 0xaf, 0x23, 0xaf, 0xea, 0x80, 0x7f, 0x88, 0xee, 0x56, 0x3c, 0x11, 0x3c, 0xa3, + 0x3c, 0x6f, 0x04, 0xcc, 0xff, 0xbb, 0x80, 0x95, 0x19, 0xd9, 0xfd, 0x7e, 0x8c, 0xd6, 0x14, 0x39, + 0x85, 0x78, 0x22, 0x32, 0x88, 0x75, 0x21, 0x41, 0x15, 0x82, 0x65, 0x83, 0x5b, 0xff, 0xd1, 0x93, + 0xe1, 0xbf, 0x15, 0x19, 0x1c, 0x37, 0xec, 0xed, 0x6f, 0x1e, 0x5a, 0x6f, 0x5c, 0x7d, 0x0d, 0x1c, + 0x14, 0x55, 0x63, 0x4d, 0x34, 0xf8, 0xcf, 0xd0, 0x42, 0x69, 0x5d, 0xb6, 0xd6, 0xf6, 0x77, 0xef, + 0x63, 0x37, 0x0c, 0xf8, 0xe6, 0x23, 0xec, 0xcf, 0x9b, 0xf2, 0x91, 0x4b, 0xf6, 0x9f, 0xa3, 0x5e, + 0x29, 0xc5, 0x94, 0x66, 0x20, 0x9d, 0xdb, 0xf7, 0x66, 0xc4, 0x23, 0x17, 0x78, 0xc3, 0x4f, 0x85, + 0xa3, 0xcd, 0x92, 0xfd, 0x4d, 0x74, 0x9b, 0xc3, 0x59, 0x9c, 0x16, 0x84, 0x72, 0xeb, 0x69, 0x2f, + 0xea, 0x71, 0x38, 0x3b, 0x30, 0xd8, 0x3c, 0x74, 0x29, 0xe1, 0x64, 0x6f, 0x6f, 0x6c, 0x1d, 0xec, + 0x45, 0x0d, 0xdc, 0xfe, 0x3c, 0x87, 0x96, 0xda, 0x75, 0xfd, 0x43, 0xb4, 0x54, 0x8f, 0x61, 0xac, + 0x4c, 0x1f, 0x4e, 0xfd, 0x63, 0x4c, 0x93, 0x14, 0xb7, 0x87, 0x14, 0xb7, 0xc6, 0xd2, 0x34, 0x65, + 0x6f, 0x6d, 0xeb, 0x51, 0x3f, 0xbd, 0x06, 0xfe, 0x7b, 0xb4, 0x62, 0xb6, 0x02, 0xb8, 0xaa, 0x94, + 0x2b, 0x59, 0xf7, 0x85, 0xff, 0x5a, 0xb2, 0xa1, 0xd5, 0x55, 0x97, 0xd3, 0x1b, 0xd8, 0x3f, 0x44, + 0x2b, 0x94, 0x53, 0x4d, 0x09, 0x8b, 0xa7, 0x84, 0xc5, 0x0a, 0xf4, 0xa0, 0x3b, 0xec, 0x8e, 0xfa, + 0xbb, 0xc3, 0x76, 0x1d, 0xb3, 0x6d, 0xf8, 0x84, 0x30, 0x9a, 0x11, 0x2d, 0xe4, 0xbb, 0x32, 0x23, + 0x1a, 0x9c, 0x77, 0x77, 0x1c, 0xfd, 0x84, 0xb0, 0x31, 0xe8, 0xfd, 0x17, 0xe7, 0x97, 0x81, 0x77, + 0x71, 0x19, 0x78, 0xbf, 0x2f, 0x03, 0xef, 0xcb, 0x55, 0xd0, 0xb9, 0xb8, 0x0a, 0x3a, 0x3f, 0xae, + 0x82, 0xce, 0x87, 0x47, 0x39, 0xd5, 0x45, 0x95, 0xe0, 0x54, 0x4c, 0x42, 0xc2, 0x18, 0xe5, 0x09, + 0xd5, 0x2a, 0xb4, 0xab, 0xff, 0xb1, 0xfe, 0xd8, 0x75, 0x4e, 0x16, 0xec, 0xec, 0x3c, 0xfd, 0x13, + 0x00, 0x00, 0xff, 0xff, 0x73, 0xbe, 0x93, 0xe2, 0x16, 0x04, 0x00, 0x00, } func (m *ConsumerParams) Marshal() (dAtA []byte, err error) { @@ -316,25 +328,33 @@ func (m *ConsumerParams) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - n1, err1 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.UnbondingPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.UnbondingPeriod):]) + n1, err1 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.SafeModeThreshold, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.SafeModeThreshold):]) if err1 != nil { return 0, err1 } i -= n1 i = encodeVarintSharedConsumer(dAtA, i, uint64(n1)) i-- + dAtA[i] = 0x2a + n2, err2 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.UnbondingPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.UnbondingPeriod):]) + if err2 != nil { + return 0, err2 + } + i -= n2 + i = encodeVarintSharedConsumer(dAtA, i, uint64(n2)) + i-- dAtA[i] = 0x22 if m.HistoricalEntries != 0 { i = encodeVarintSharedConsumer(dAtA, i, uint64(m.HistoricalEntries)) i-- dAtA[i] = 0x18 } - n2, err2 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.VaasTimeoutPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.VaasTimeoutPeriod):]) - if err2 != nil { - return 0, err2 + n3, err3 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.VaasTimeoutPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.VaasTimeoutPeriod):]) + if err3 != nil { + return 0, err3 } - i -= n2 - i = encodeVarintSharedConsumer(dAtA, i, uint64(n2)) + i -= n3 + i = encodeVarintSharedConsumer(dAtA, i, uint64(n3)) i-- dAtA[i] = 0x12 if m.Enabled { @@ -501,6 +521,8 @@ func (m *ConsumerParams) Size() (n int) { } l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.UnbondingPeriod) n += 1 + l + sovSharedConsumer(uint64(l)) + l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.SafeModeThreshold) + n += 1 + l + sovSharedConsumer(uint64(l)) return n } @@ -686,6 +708,39 @@ func (m *ConsumerParams) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SafeModeThreshold", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowSharedConsumer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthSharedConsumer + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthSharedConsumer + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_cosmos_gogoproto_types.StdDurationUnmarshal(&m.SafeModeThreshold, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipSharedConsumer(dAtA[iNdEx:]) diff --git a/x/vaas/types/shared_params.go b/x/vaas/types/shared_params.go index 933456b..3b3daa2 100644 --- a/x/vaas/types/shared_params.go +++ b/x/vaas/types/shared_params.go @@ -9,8 +9,17 @@ import ( ) const ( - // DefaultVAASTimeoutPeriod is 4 weeks to ensure channel doesn't close on timeout. - DefaultVAASTimeoutPeriod = 4 * 7 * 24 * time.Hour + // DefaultSafeModeThreshold is how long the consumer may go without + // receiving a VSC packet before its tx admission gate enters safe mode + // (only ibc.core and gov messages pass). Set well below the provider + // liveness grace period. + DefaultSafeModeThreshold = 3 * time.Hour + + // DefaultVAASTimeoutPeriod is the IBC packet timeout for VAAS packets. + // One epoch-scale value: undelivered packets are superseded the next + // epoch and late ones are dropped by the consumer, so a long timeout + // buys nothing. Must be <= MaxTimeoutDelta (24h, ibc-go v2 hard cap). + DefaultVAASTimeoutPeriod = time.Hour ) var KeyVAASTimeoutPeriod = []byte("VaasTimeoutPeriod") @@ -22,6 +31,23 @@ func ValidateDuration(d time.Duration) error { return nil } +// ValidateVAASTimeoutPeriod checks the VAAS packet timeout is within +// (0, maxTimeoutDelta]. maxTimeoutDelta is passed in to avoid importing ibc-go +// from this shared package. No lower floor is imposed: an undelivered packet is +// superseded by the next epoch's packet and a late one is dropped by the +// consumer's dedup, and a persistently unreachable consumer is handled by the +// liveness sweep -- so a short timeout is not dangerous, and forbidding one only +// blocks testing the timeout path. +func ValidateVAASTimeoutPeriod(d, maxTimeoutDelta time.Duration) error { + if d <= 0 { + return fmt.Errorf("VAAS timeout period must be positive, got %s", d) + } + if d > maxTimeoutDelta { + return fmt.Errorf("VAAS timeout period must be <= %s (MaxTimeoutDelta), got %s", maxTimeoutDelta, d) + } + return nil +} + func ValidatePositiveInt64(n int64) error { if n <= int64(0) { return errors.New("int must be positive") @@ -29,6 +55,12 @@ func ValidatePositiveInt64(n int64) error { return nil } +// ValidateStringFractionNonZero checks that str parses to a decimal strictly +// inside the open interval (0, 1). Its two users -- the IBC trusting-period +// fraction and the liveness grace fraction -- each scale the unbonding period +// into a duration that must stay strictly below it (the trusting period must be +// < unbonding for the light client, and the liveness grace must end inside the +// slashable window), so a fraction of exactly 1 is invalid, not just > 1. func ValidateStringFractionNonZero(str string) error { dec, err := math.LegacyNewDecFromStr(str) if err != nil { @@ -37,8 +69,8 @@ func ValidateStringFractionNonZero(str string) error { if dec.IsNegative() { return fmt.Errorf("param cannot be negative, got %s", str) } - if dec.Sub(math.LegacyNewDec(1)).IsPositive() { - return fmt.Errorf("param cannot be greater than 1, got %s", str) + if dec.GTE(math.LegacyOneDec()) { + return fmt.Errorf("param must be less than 1, got %s", str) } if dec.IsZero() { return fmt.Errorf("param cannot be zero, got %s", str) diff --git a/x/vaas/types/wire.pb.go b/x/vaas/types/wire.pb.go index 10eb5ee..c0324c1 100644 --- a/x/vaas/types/wire.pb.go +++ b/x/vaas/types/wire.pb.go @@ -46,6 +46,13 @@ type ValidatorSetChangePacketData struct { // non-/ibc.core.* and non-/cosmos.gov.* messages until the provider // collects fees successfully and propagates false on the next VSC. ConsumerInDebt bool `protobuf:"varint,3,opt,name=consumer_in_debt,json=consumerInDebt,proto3" json:"consumer_in_debt,omitempty"` + // is_snapshot indicates how the consumer must apply validator_updates. + // false: validator_updates is a diff against the consumer's previous set + // (the consumer accumulates). true: validator_updates is the provider's + // complete current set (the consumer replaces its set with it). The + // provider sends a snapshot while a consumer is behind on acks, so a + // consumer that missed packets converges to the exact set in one packet. + IsSnapshot bool `protobuf:"varint,4,opt,name=is_snapshot,json=isSnapshot,proto3" json:"is_snapshot,omitempty"` } func (m *ValidatorSetChangePacketData) Reset() { *m = ValidatorSetChangePacketData{} } @@ -102,6 +109,13 @@ func (m *ValidatorSetChangePacketData) GetConsumerInDebt() bool { return false } +func (m *ValidatorSetChangePacketData) GetIsSnapshot() bool { + if m != nil { + return m.IsSnapshot + } + return false +} + func init() { proto.RegisterType((*ValidatorSetChangePacketData)(nil), "vaas.v1.ValidatorSetChangePacketData") } @@ -109,28 +123,29 @@ func init() { func init() { proto.RegisterFile("vaas/v1/wire.proto", fileDescriptor_17a432f87dfb1130) } var fileDescriptor_17a432f87dfb1130 = []byte{ - // 333 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x91, 0xc1, 0x4a, 0xeb, 0x40, - 0x14, 0x86, 0x33, 0xb7, 0x97, 0x7b, 0x2f, 0xb9, 0x50, 0x34, 0xb8, 0x88, 0x55, 0xd2, 0x50, 0x44, - 0xb2, 0xca, 0x10, 0xdd, 0xb9, 0x92, 0xda, 0x4d, 0x77, 0x52, 0xd1, 0x85, 0x9b, 0x70, 0x26, 0x19, - 0xd2, 0xa1, 0xc9, 0x4c, 0xc9, 0x9c, 0x46, 0xfb, 0x16, 0x3e, 0x56, 0x97, 0x5d, 0xba, 0x2a, 0xd2, - 0xbc, 0x81, 0x4f, 0x20, 0xc9, 0xb4, 0x8a, 0xb8, 0x9a, 0x73, 0xfe, 0xff, 0x3b, 0xff, 0x81, 0x33, - 0xb6, 0x53, 0x01, 0x68, 0x5a, 0x45, 0xf4, 0x49, 0x94, 0x3c, 0x9c, 0x97, 0x0a, 0x95, 0xf3, 0xb7, - 0xd1, 0xc2, 0x2a, 0xea, 0x9d, 0x25, 0x4a, 0x17, 0x4a, 0x53, 0x8d, 0x30, 0x13, 0x32, 0xa3, 0x55, - 0xc4, 0x38, 0x42, 0xb4, 0xef, 0x0d, 0xde, 0x3b, 0xca, 0x54, 0xa6, 0xda, 0x92, 0x36, 0xd5, 0x4e, - 0x3d, 0x41, 0x2e, 0x53, 0x5e, 0x16, 0x42, 0x22, 0x05, 0x96, 0x08, 0x8a, 0xcb, 0x39, 0xd7, 0x3b, - 0xf3, 0xd8, 0x04, 0xc7, 0x66, 0xca, 0x34, 0xc6, 0x1a, 0xd4, 0xc4, 0x3e, 0x7d, 0x80, 0x5c, 0xa4, - 0x80, 0xaa, 0xbc, 0xe3, 0x78, 0x33, 0x05, 0x99, 0xf1, 0x5b, 0x48, 0x66, 0x1c, 0x47, 0x80, 0xe0, - 0x28, 0xfb, 0xb0, 0xda, 0xfb, 0xf1, 0x62, 0x9e, 0x02, 0x72, 0xed, 0x12, 0xbf, 0x13, 0xfc, 0xbf, - 0xf0, 0xc3, 0xaf, 0xa5, 0x61, 0xb3, 0x34, 0xfc, 0x4c, 0xba, 0x6f, 0xc1, 0xa1, 0xbf, 0xda, 0xf4, - 0xad, 0xf7, 0x4d, 0xdf, 0x5d, 0x42, 0x91, 0x5f, 0x0d, 0x7e, 0x04, 0x0d, 0x26, 0x07, 0xd5, 0xf7, - 0x11, 0xed, 0x04, 0x76, 0xa3, 0x69, 0x8e, 0x3b, 0x28, 0x16, 0xa9, 0xfb, 0xcb, 0x27, 0xc1, 0xef, - 0x49, 0xd7, 0xe8, 0x06, 0x1c, 0xa7, 0x0d, 0x99, 0x28, 0xa9, 0x17, 0x05, 0x2f, 0x63, 0x21, 0xe3, - 0x94, 0x33, 0x74, 0x3b, 0x3e, 0x09, 0xfe, 0x4d, 0xba, 0x7b, 0x7d, 0x2c, 0x47, 0x9c, 0xe1, 0xf0, - 0x7a, 0xb5, 0xf5, 0xc8, 0x7a, 0xeb, 0x91, 0xb7, 0xad, 0x47, 0x5e, 0x6a, 0xcf, 0x5a, 0xd7, 0x9e, - 0xf5, 0x5a, 0x7b, 0xd6, 0xe3, 0x79, 0x26, 0x70, 0xba, 0x60, 0x61, 0xa2, 0x0a, 0x0a, 0x79, 0x2e, - 0x24, 0x13, 0xa8, 0x69, 0xfb, 0x4b, 0xcf, 0xe6, 0x69, 0x0f, 0xc9, 0xfe, 0xb4, 0xe7, 0xba, 0xfc, - 0x08, 0x00, 0x00, 0xff, 0xff, 0x0e, 0x4a, 0x54, 0xd8, 0xc1, 0x01, 0x00, 0x00, + // 352 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x91, 0xc1, 0x4a, 0xf3, 0x40, + 0x10, 0x80, 0x93, 0xb6, 0xfc, 0xff, 0x4f, 0x0a, 0xe5, 0x37, 0x78, 0x88, 0x55, 0xd2, 0x50, 0x44, + 0x72, 0xca, 0x12, 0xbd, 0x79, 0x92, 0xda, 0x4b, 0x6f, 0xd2, 0xa2, 0x07, 0x2f, 0x61, 0x92, 0x2c, + 0xe9, 0xd2, 0x64, 0x37, 0x64, 0xa7, 0xd1, 0xde, 0x7d, 0x00, 0x1f, 0xab, 0xc7, 0x1e, 0x3d, 0x15, + 0x69, 0xdf, 0xc0, 0x27, 0x90, 0x64, 0x53, 0x45, 0x3c, 0xed, 0xcc, 0x37, 0xdf, 0xcc, 0xc0, 0xac, + 0x61, 0x96, 0x00, 0x92, 0x94, 0x3e, 0x79, 0x62, 0x05, 0xf5, 0xf2, 0x42, 0xa0, 0x30, 0xff, 0x56, + 0xcc, 0x2b, 0xfd, 0xfe, 0x79, 0x24, 0x64, 0x26, 0x24, 0x91, 0x08, 0x0b, 0xc6, 0x13, 0x52, 0xfa, + 0x21, 0x45, 0xf0, 0x0f, 0xb9, 0xd2, 0xfb, 0xc7, 0x89, 0x48, 0x44, 0x1d, 0x92, 0x2a, 0x6a, 0xe8, + 0x29, 0x52, 0x1e, 0xd3, 0x22, 0x63, 0x1c, 0x09, 0x84, 0x11, 0x23, 0xb8, 0xca, 0xa9, 0x6c, 0x8a, + 0x27, 0x6a, 0x70, 0xa0, 0xba, 0x54, 0xa2, 0x4a, 0xc3, 0x97, 0x96, 0x71, 0xf6, 0x00, 0x29, 0x8b, + 0x01, 0x45, 0x31, 0xa3, 0x78, 0x3b, 0x07, 0x9e, 0xd0, 0x3b, 0x88, 0x16, 0x14, 0xc7, 0x80, 0x60, + 0x0a, 0xe3, 0xa8, 0x3c, 0xd4, 0x83, 0x65, 0x1e, 0x03, 0x52, 0x69, 0xe9, 0x4e, 0xdb, 0xed, 0x5e, + 0x3a, 0xde, 0xf7, 0x52, 0xaf, 0x5a, 0xea, 0x7d, 0x4d, 0xba, 0xaf, 0xc5, 0x91, 0xb3, 0xde, 0x0e, + 0xb4, 0x8f, 0xed, 0xc0, 0x5a, 0x41, 0x96, 0x5e, 0x0f, 0x7f, 0x0d, 0x1a, 0x4e, 0xff, 0x97, 0x3f, + 0x5b, 0xa4, 0xe9, 0x1a, 0x15, 0x93, 0x14, 0x1b, 0x29, 0x60, 0xb1, 0xd5, 0x72, 0x74, 0xb7, 0x33, + 0xed, 0x29, 0xae, 0xc4, 0x49, 0x5c, 0x99, 0x91, 0xe0, 0x72, 0x99, 0xd1, 0x22, 0x60, 0x3c, 0x88, + 0x69, 0x88, 0x56, 0xdb, 0xd1, 0xdd, 0x7f, 0xd3, 0xde, 0x81, 0x4f, 0xf8, 0x98, 0x86, 0x68, 0x0e, + 0x8c, 0x2e, 0x93, 0x81, 0xe4, 0x90, 0xcb, 0xb9, 0x40, 0xab, 0x53, 0x4b, 0x06, 0x93, 0xb3, 0x86, + 0x8c, 0x6e, 0xd6, 0x3b, 0x5b, 0xdf, 0xec, 0x6c, 0xfd, 0x7d, 0x67, 0xeb, 0xaf, 0x7b, 0x5b, 0xdb, + 0xec, 0x6d, 0xed, 0x6d, 0x6f, 0x6b, 0x8f, 0x17, 0x09, 0xc3, 0xf9, 0x32, 0xf4, 0x22, 0x91, 0x11, + 0x48, 0x53, 0xc6, 0x43, 0x86, 0x92, 0xd4, 0xdf, 0xf8, 0xac, 0x9e, 0xfa, 0xd2, 0xe1, 0x9f, 0xfa, + 0x9e, 0x57, 0x9f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x95, 0xa1, 0xfa, 0xe0, 0xe2, 0x01, 0x00, 0x00, } func (m *ValidatorSetChangePacketData) Marshal() (dAtA []byte, err error) { @@ -153,6 +168,16 @@ func (m *ValidatorSetChangePacketData) MarshalToSizedBuffer(dAtA []byte) (int, e _ = i var l int _ = l + if m.IsSnapshot { + i-- + if m.IsSnapshot { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x20 + } if m.ConsumerInDebt { i-- if m.ConsumerInDebt { @@ -214,6 +239,9 @@ func (m *ValidatorSetChangePacketData) Size() (n int) { if m.ConsumerInDebt { n += 2 } + if m.IsSnapshot { + n += 2 + } return n } @@ -325,6 +353,26 @@ func (m *ValidatorSetChangePacketData) Unmarshal(dAtA []byte) error { } } m.ConsumerInDebt = bool(v != 0) + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field IsSnapshot", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowWire + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.IsSnapshot = bool(v != 0) default: iNdEx = preIndex skippy, err := skipWire(dAtA[iNdEx:]) diff --git a/x/vaas/types/wire_test.go b/x/vaas/types/wire_test.go new file mode 100644 index 0000000..617c1ea --- /dev/null +++ b/x/vaas/types/wire_test.go @@ -0,0 +1,20 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/allinbits/vaas/x/vaas/types" +) + +func TestValidatorSetChangePacketData_IsSnapshotRoundTrip(t *testing.T) { + pkt := types.NewValidatorSetChangePacketData(nil, 7) + pkt.IsSnapshot = true + + bz := pkt.GetBytes() + var got types.ValidatorSetChangePacketData + require.NoError(t, types.ModuleCdc.UnmarshalJSON(bz, &got)) + require.True(t, got.IsSnapshot) + require.Equal(t, uint64(7), got.ValsetUpdateId) +}