-
Notifications
You must be signed in to change notification settings - Fork 0
feat: liveness-based consumer removal, snapshot resync, and safe mode #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
giunatale
wants to merge
20
commits into
main
Choose a base branch
from
giunatale/consumer-removal
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
52a5c9a
liveness-based consumer removal: ack clock, block-clock sweep, and de…
giunatale ab45053
consumer safe mode on VSC staleness
giunatale 1876511
snapshot resync for behind consumers
giunatale 8eae442
bound vaas_timeout_period and consumer unbonding period
giunatale 6e0d7cd
consumer liveness query
giunatale b6aeaca
make liveness thresholds configurable params and relax the unbonding …
giunatale 0b39d9a
add liveness unit tests
giunatale c1cc5c6
add e2e coverage for consumer liveness
giunatale c799535
document consumer liveness, removal, and resync
giunatale 2d1ef38
harden liveness sweep, fraction validation, and consumer param reads
giunatale f437fea
fix the main e2e suite for the liveness changes
giunatale 3d746ce
enforce the consumer safe-mode threshold below the provider liveness …
giunatale 02ed6ce
round-trip the liveness clock through genesis export/import
giunatale 001d11b
remove the non-functional remove-consumer CLI command
giunatale 9a234fe
drop redundant sdk context unwrap in OnRecvVSCPacketV2
giunatale 3a6d426
drop the VaasTimeoutPeriod lower floor
giunatale eef8413
emit a snapshot-resync event; correct the transient-outage e2e's claims
giunatale bccc5a0
add a forced-timeout snapshot-resync e2e test
giunatale 8d817fc
extract a shared baseTestSuite for the two e2e suites
giunatale d52ebfa
describe the liveness design in the present tense
giunatale File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <consumer-id> | ||
| ``` | ||
|
|
||
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.