Skip to content

feat(ika_system): expose is_in_committees(System, ID): bool - #1708

Open
lmvdz wants to merge 1 commit into
dwallet-labs:mainfrom
inkwell-finance:feat/committee-membership-predicate
Open

feat(ika_system): expose is_in_committees(System, ID): bool#1708
lmvdz wants to merge 1 commit into
dwallet-labs:mainfrom
inkwell-finance:feat/committee-membership-predicate

Conversation

@lmvdz

@lmvdz lmvdz commented May 14, 2026

Copy link
Copy Markdown

Adds a public read predicate to ika_system::system::System that returns true when a validator is in the current active committee or the next epoch's active committee. Mirrors the internal is_current_committee || is_next_committee test already used by validator_set::request_withdraw_stake and
validator_set::withdraw_stake to branch between the two-epoch cooldown path and the inactive-direct-withdraw path.

Why

External integrations (liquid staking pools, custody integrations, anything wrapping StakedIka) need to know when a validator has rotated out of both committees, because request_withdraw_stake aborts with EWithdrawDirectly in that case. The intended flow per validator::withdraw_stake (validator.move:373-388) is to bypass request_withdraw_stake and call system::withdraw_stake directly on the still-Staked position — but the caller has no clean way to detect when to take that branch.

Without this predicate, integrators have three poor options:

  1. Speculatively call request_withdraw_stake and let it abort — Move has no try/catch, so this kills the whole transaction.
  2. Call the existing public active_committee() and next_epoch_active_committee() accessors and run .contains() externally. Works, but copies the full BlsCommittee value (BLS pubkeys + voting power per member) on every read just to compute one bool.
  3. Add an admin-gated rescue function with elevated trust to call system::withdraw_stake directly — expands the trust surface and breaks multisig/timelock-only governance designs.

The predicate is observability of state the system already tracks. No behaviour change.

Wiring

Threads through the same three layers as the existing active_committee() getter:

  • validator_set::is_in_committees(&ValidatorSet, ID): bool (public(package))
  • system_inner::is_in_committees(&SystemInner, ID): bool (public(package))
  • system::is_in_committees(&System, ID): bool (public)

Implementation is two reads on the existing active_committee and next_epoch_active_committee fields plus bls_committee::contains (already public). Zero new state, zero new dependencies.

Build status

Local sui move build on ika_system blocked by a pre-existing upstream stdlib mismatch:
ika_common/sources/bls_committee.move:149 calls
n_members.div_ceil(8) which isn't on std::u64 in sui 1.72.1. Same error reproduces on plain origin/main without this change — unrelated to this PR. The patch compiles cleanly against the sui version ika_common was authored on.

Context

Originating use case is liquid-staking pool recovery from validator rotation. Spec writeup including the validator.move line references and the alternatives we considered:
https://github.com/inkwell-finance/ika-liquid-staking-spec (drafted 2026-05-12; happy to share the markdown if helpful for review).

Happy to add a test once the div_ceil stdlib drift is resolved upstream, or to split that into a separate prep PR if you'd like.

Description

Describe the changes or additions included in this PR.

Test plan

How did you test the new or updated feature?


Release notes

Check each box that your changes affect. If none of the boxes relate to your changes, release notes aren't required.

For each box you select, include information after the relevant heading that describes the impact of your changes that a user might notice and any actions they must take to implement updates.

  • Protocol:
  • Nodes (Validators and Full nodes):
  • Indexer:
  • JSON-RPC:
  • GraphQL:
  • CLI:
  • Rust SDK:
  • REST API:

Adds a public read predicate to `ika_system::system::System` that
returns true when a validator is in the current active committee or
the next epoch's active committee. Mirrors the internal
`is_current_committee || is_next_committee` test already used by
`validator_set::request_withdraw_stake` and
`validator_set::withdraw_stake` to branch between the two-epoch
cooldown path and the inactive-direct-withdraw path.

Why

External integrations (liquid staking pools, custody integrations,
anything wrapping `StakedIka`) need to know when a validator has
rotated out of both committees, because `request_withdraw_stake`
aborts with `EWithdrawDirectly` in that case. The intended flow per
`validator::withdraw_stake` (validator.move:373-388) is to bypass
`request_withdraw_stake` and call `system::withdraw_stake` directly
on the still-Staked position — but the caller has no clean way to
detect when to take that branch.

Without this predicate, integrators have three poor options:
1. Speculatively call `request_withdraw_stake` and let it abort —
   Move has no try/catch, so this kills the whole transaction.
2. Call the existing public `active_committee()` and
   `next_epoch_active_committee()` accessors and run `.contains()`
   externally. Works, but copies the full `BlsCommittee` value (BLS
   pubkeys + voting power per member) on every read just to compute
   one bool.
3. Add an admin-gated rescue function with elevated trust to call
   `system::withdraw_stake` directly — expands the trust surface
   and breaks multisig/timelock-only governance designs.

The predicate is observability of state the system already tracks.
No behaviour change.

Wiring

Threads through the same three layers as the existing
`active_committee()` getter:
- `validator_set::is_in_committees(&ValidatorSet, ID): bool`
  (public(package))
- `system_inner::is_in_committees(&SystemInner, ID): bool`
  (public(package))
- `system::is_in_committees(&System, ID): bool`
  (public)

Implementation is two reads on the existing `active_committee` and
`next_epoch_active_committee` fields plus `bls_committee::contains`
(already public). Zero new state, zero new dependencies.

Build status

Local `sui move build` on ika_system blocked by a pre-existing
upstream stdlib mismatch:
`ika_common/sources/bls_committee.move:149` calls
`n_members.div_ceil(8)` which isn't on `std::u64` in sui 1.72.1.
Same error reproduces on plain `origin/main` without this change —
unrelated to this PR. The patch compiles cleanly against the sui
version `ika_common` was authored on.

Context

Originating use case is liquid-staking pool recovery from validator
rotation. Spec writeup including the validator.move line references
and the alternatives we considered:
https://github.com/inkwell-finance/ika-liquid-staking-spec (drafted
2026-05-12; happy to share the markdown if helpful for review).

Happy to add a test once the `div_ceil` stdlib drift is resolved
upstream, or to split that into a separate prep PR if you'd like.
@lmvdz

lmvdz commented Aug 29, 2026

Copy link
Copy Markdown
Author

Friendly ping on this one — open since May with no review. Still mergeable: clean against main, so nothing needed on our side to land it.

Recap of why it matters to us, with a concrete case: we run LIKA, a liquid-staking pool that spreads IKA across validators on an admin-set weight distribution. When a validator we hold positions on rotates out of both committees, our keeper's request_withdraw_stake lands on the abort EWithdrawDirectly branch (validator.move:334-340) and takes the whole transaction with it. The system already handles that case correctly in validator::withdraw_stake — the !in_current_committee && !in_next_committee && staked_ika.is_staked() path — we just have no supported way to detect from Move when to route there, and Move has no try/catch to probe with.

The diff is +35/-0, read-only, no behaviour change: it threads a predicate through the same three layers as the existing active_committee() getter.

Two things to make this easy to land:

  • If you'd prefer in_current_committee() / in_next_committee() as two separate predicates over the combined is_in_committees(), say the word and I'll push that shape instead — either one unblocks us.
  • Happy to add Move tests, or rebase if anything has shifted on your end.

Is there anything blocking beyond review bandwidth?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants