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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ pallet-subtensor = { path = "pallets/subtensor", default-features = false }
pallet-subtensor-swap = { path = "pallets/swap", default-features = false }
pallet-subtensor-swap-runtime-api = { path = "pallets/swap/runtime-api", default-features = false }
pallet-subtensor-swap-rpc = { path = "pallets/swap/rpc", default-features = false }
pallet-multi-collective = { path = "pallets/multi-collective", default-features = false }
procedural-fork = { path = "support/procedural-fork", default-features = false }
safe-bigmath = { package = "safe-bigmath", default-features = false, git = "https://github.com/sam0x17/safe-bigmath", rev = "013c49984910e1c9a23289e8c85e7a856e263a02" }
safe-math = { path = "primitives/safe-math", default-features = false }
Expand Down
1 change: 1 addition & 0 deletions common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ targets = ["x86_64-unknown-linux-gnu"]
codec = { workspace = true, features = ["derive"] }
environmental.workspace = true
frame-support.workspace = true
impl-trait-for-tuples.workspace = true
num-traits = { workspace = true, features = ["libm"] }
scale-info.workspace = true
serde.workspace = true
Expand Down
2 changes: 2 additions & 0 deletions common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ use subtensor_macros::freeze_struct;

pub use currency::*;
pub use evm_context::*;
pub use traits::*;
pub use transaction_error::*;

mod currency;
mod evm_context;
mod traits;
mod transaction_error;

/// Balance of an account.
Expand Down
34 changes: 34 additions & 0 deletions common/src/traits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use frame_support::pallet_prelude::*;

/// Handler for when the members of a collective have changed.
pub trait OnMembersChanged<CollectiveId, AccountId> {
/// A collective's members have changed, `incoming` members have joined and
/// `outgoing` members have left.
fn on_members_changed(
collective_id: CollectiveId,
incoming: &[AccountId],
outgoing: &[AccountId],
);
/// Worst-case upper bound on `on_members_changed`'s weight. The
/// implementation is responsible for bounding its own iteration over
/// `incoming`/`outgoing` against the relevant `MaxMembers` constant.
fn weight() -> Weight;
}

#[impl_trait_for_tuples::impl_for_tuples(10)]
impl<CollectiveId: Clone, AccountId> OnMembersChanged<CollectiveId, AccountId> for Tuple {
fn on_members_changed(
collective_id: CollectiveId,
incoming: &[AccountId],
outgoing: &[AccountId],
) {
for_tuples!( #( Tuple::on_members_changed(collective_id.clone(), incoming, outgoing); )* );
}

fn weight() -> Weight {
#[allow(clippy::let_and_return)]
let mut weight = Weight::zero();
for_tuples!( #( weight.saturating_accrue(Tuple::weight()); )* );
weight
}
}
54 changes: 54 additions & 0 deletions pallets/multi-collective/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
[package]
name = "pallet-multi-collective"
version = "1.0.0"
authors = ["Bittensor Nucleus Team"]
edition.workspace = true
license = "Apache-2.0"
homepage = "https://bittensor.com"
description = "Membership for named collectives, with per-call origins and optional scheduled rotation."
readme = "README.md"

[lints]
workspace = true

[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

[dependencies]
codec = { workspace = true, features = ["max-encoded-len"] }
scale-info = { workspace = true, features = ["derive"] }
frame-benchmarking = { workspace = true, optional = true }
frame-system = { workspace = true }
frame-support = { workspace = true }
impl-trait-for-tuples = { workspace = true }
num-traits = { workspace = true }
subtensor-runtime-common = { workspace = true }

[dev-dependencies]
sp-io = { workspace = true, default-features = true }
sp-core = { workspace = true, default-features = true }
sp-runtime = { workspace = true, default-features = true }

[features]
default = ["std"]
std = [
"codec/std",
"scale-info/std",
"frame-benchmarking?/std",
"frame-system/std",
"frame-support/std",
"num-traits/std",
"subtensor-runtime-common/std",
]
runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"subtensor-runtime-common/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"sp-runtime/try-runtime"
]
99 changes: 99 additions & 0 deletions pallets/multi-collective/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# pallet-multi-collective

Membership storage for one or more named collectives, keyed by a
runtime-defined `CollectiveId`. Each collective is configured by a
`CollectivesInfo` impl: name, min/max members, optional term duration.

The pallet only stores membership. Voting, proposing, and tallying are
left to the consumer (e.g. `pallet-referenda` + `pallet-signed-voting`),
which read members through the `CollectiveInspect` trait.

## Concepts

| Type | Provided by | Purpose |
| ---- | ----------- | ------- |
| `CollectiveId` | runtime | Enum naming each collective. |
| `CollectivesInfo` | runtime | Returns the static config for each id (name, bounds, term). |
| `CollectiveInfo` | this crate | `{ name, min_members, max_members, term_duration }`. |
| `Members<_>` | this crate | `BoundedVec<AccountId, MaxMembers>` per id, sorted by `AccountId`. |

## Extrinsics

| Call | Origin | Effect |
| ---- | ------ | ------ |
| `add_member` | `T::AddOrigin` | Insert one member. Fails on `AlreadyMember`, `TooManyMembers`, `CollectiveNotFound`. |
| `remove_member` | `T::RemoveOrigin` | Remove one member. Fails on `NotMember`, `TooFewMembers`, `CollectiveNotFound`. |
| `swap_member` | `T::SwapOrigin` | Atomic remove + insert. Count is preserved, so the per-collective `min_members` / `max_members` bounds are not re-checked; works at either boundary. |
| `set_members` | `T::SetOrigin` | Replace the full list. Sorts the input and rejects `DuplicateAccounts` if any duplicates are present (the input is not silently deduplicated). |
| `force_rotate` | `T::RotateOrigin` | Trigger `OnNewTerm` for a rotating collective on demand. |

Every mutation fires `T::OnMembersChanged` with the incoming and
outgoing accounts so downstream pallets can react (e.g. clean up
votes). The Subtensor runtime currently wires this to `()`: active
polls snapshot the voter set at creation, so member changes cannot
retroactively invalidate votes, and no cleanup is needed.

## Rotation

A collective whose `CollectiveInfo::term_duration` is `Some(d)` rotates
every `d` blocks: `on_initialize` calls `T::OnNewTerm::on_new_term(id)`
when `block_number % d == 0`. The runtime-supplied handler typically
recomputes membership from on-chain data and writes it back through
`set_members`.

`force_rotate` runs the same hook on demand. Used to bootstrap the
first term (the natural cadence only fires after the first boundary,
which can be days or months in) and as a privileged override during
incidents. Calls against a collective with `term_duration: None` are
rejected with `CollectiveDoesNotRotate`.

Curated collectives (no term duration) are managed directly via the
membership extrinsics.

## Integrity check

`integrity_test` runs at runtime construction and panics on a
misconfigured `CollectivesInfo`:

- `min_members > T::MaxMembers` (collective can't reach its min)
- `max_members > T::MaxMembers` (storage can't hold the declared max)
- `min_members > max_members` (collective is unreachable)
- `term_duration: Some(0)` (silently disables rotation; use `None` to opt out)

## Migrations

Pinned at `StorageVersion::new(0)` to satisfy try-runtime CLI; the
project tracks migration runs through a per-pallet `HasMigrationRun`
storage map (see `pallet-crowdloan`), not via FRAME's `StorageVersion`
bump. Add a `migrations` module and an `on_runtime_upgrade` hook on
the next breaking change to `Members<_>` or any future persisted state.

## Configuration

```rust
parameter_types! {
pub const MaxMembers: u32 = 20;
}

impl pallet_multi_collective::Config for Runtime {
type CollectiveId = CollectiveId;
type Collectives = Collectives;
type AddOrigin = AsEnsureOriginWithArg<EnsureRoot<AccountId>>;
type RemoveOrigin = AsEnsureOriginWithArg<EnsureRoot<AccountId>>;
type SwapOrigin = AsEnsureOriginWithArg<EnsureRoot<AccountId>>;
type SetOrigin = AsEnsureOriginWithArg<EnsureRoot<AccountId>>;
type RotateOrigin = AsEnsureOriginWithArg<EnsureRoot<AccountId>>;
type OnMembersChanged = ();
type OnNewTerm = TermManagement;
type MaxMembers = MaxMembers;
type WeightInfo = pallet_multi_collective::weights::SubstrateWeight<Runtime>;
}
```

`T::MaxMembers` bounds storage; per-collective `max_members` from
`CollectivesInfo` may be smaller but never larger (enforced by
`integrity_test`).

## License

Apache-2.0.
Loading
Loading