diff --git a/CHANGELOG.md b/CHANGELOG.md index a5ba453fd..0622f6f35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -567,10 +567,13 @@ All notable changes to this project will be documented in this file. - Optimize outbound probe RTT accuracy: send a staggered warmup probe on a separate socket 2ms before the measurement probe to wake the reflector's thread, then take the min RTT of both - Onchain Programs - Serviceability: add `Permission` account with `CreatePermission`, `UpdatePermission`, `DeletePermission`, `SuspendPermission`, and `ResumePermission` instructions for managing per-keypair permission bitmasks onchain + - Serviceability: add `TOPOLOGY_ADMIN`, `RESOURCE_ADMIN`, and `INDEX_ADMIN` permission flags for delegating management of segment-routing topologies, ResourceExtension accounts, and internal Index accounts (legacy authorization maps each to the foundation allowlist) - SDK - Add `execute_authorized_transaction` (and its `_quiet` variant) alongside `execute_transaction`. The authorized variants append the payer's Permission PDA (read-only) as the trailing account when it exists on-chain, so `authorize()` can find it. All variants share the same builder, so the protocol-max compute-budget/heap-frame requests, preflight, and error-reporting behavior are identical to `execute_transaction`; the only difference is the optional trailing Permission account. The Permission PDA lookup is retried on transient RPC errors and memoized per client. + - Add `TOPOLOGY_ADMIN`/`RESOURCE_ADMIN`/`INDEX_ADMIN` permission-flag constants to the Go, TypeScript, and Python serviceability SDKs - CLI - Add `permission get`, `permission list`, and `permission set` commands with table and JSON output; `permission set` supports incremental `--add` / `--remove` flags and creates or updates the account as needed + - Add `topology-admin`, `resource-admin`, and `index-admin` to the named permissions accepted by `permission set --add` / `--remove` ## [v0.11.0](https://github.com/malbeclabs/doublezero/compare/client/v0.10.0...client/v0.11.0) - 2026-03-12 diff --git a/sdk/serviceability/python/serviceability/state.py b/sdk/serviceability/python/serviceability/state.py index 09a047634..fc1489875 100644 --- a/sdk/serviceability/python/serviceability/state.py +++ b/sdk/serviceability/python/serviceability/state.py @@ -1115,6 +1115,9 @@ def __str__(self) -> str: PERMISSION_FLAG_ACCESS_PASS_ADMIN = 1 << 10 PERMISSION_FLAG_HEALTH_ORACLE = 1 << 11 PERMISSION_FLAG_QA = 1 << 12 +PERMISSION_FLAG_TOPOLOGY_ADMIN = 1 << 15 +PERMISSION_FLAG_RESOURCE_ADMIN = 1 << 16 +PERMISSION_FLAG_INDEX_ADMIN = 1 << 17 @dataclass diff --git a/sdk/serviceability/typescript/serviceability/state.ts b/sdk/serviceability/typescript/serviceability/state.ts index c8e497e06..f769e0761 100644 --- a/sdk/serviceability/typescript/serviceability/state.ts +++ b/sdk/serviceability/typescript/serviceability/state.ts @@ -1143,6 +1143,9 @@ export const PERMISSION_FLAG_USER_ADMIN = 1n << 9n; export const PERMISSION_FLAG_ACCESS_PASS_ADMIN = 1n << 10n; export const PERMISSION_FLAG_HEALTH_ORACLE = 1n << 11n; export const PERMISSION_FLAG_QA = 1n << 12n; +export const PERMISSION_FLAG_TOPOLOGY_ADMIN = 1n << 15n; +export const PERMISSION_FLAG_RESOURCE_ADMIN = 1n << 16n; +export const PERMISSION_FLAG_INDEX_ADMIN = 1n << 17n; const PERMISSION_STATUS_NAMES: Record = { 0: "none", diff --git a/smartcontract/cli/src/permission/flags.rs b/smartcontract/cli/src/permission/flags.rs index 870c5d5e4..66c86b495 100644 --- a/smartcontract/cli/src/permission/flags.rs +++ b/smartcontract/cli/src/permission/flags.rs @@ -19,6 +19,9 @@ pub enum PermissionName { AccessPassAdmin, HealthOracle, Qa, + TopologyAdmin, + ResourceAdmin, + IndexAdmin, } impl PermissionName { @@ -39,6 +42,9 @@ impl PermissionName { Self::AccessPassAdmin => permission_flags::ACCESS_PASS_ADMIN, Self::HealthOracle => permission_flags::HEALTH_ORACLE, Self::Qa => permission_flags::QA, + Self::TopologyAdmin => permission_flags::TOPOLOGY_ADMIN, + Self::ResourceAdmin => permission_flags::RESOURCE_ADMIN, + Self::IndexAdmin => permission_flags::INDEX_ADMIN, } } @@ -59,6 +65,9 @@ impl PermissionName { Self::AccessPassAdmin => "access-pass-admin", Self::HealthOracle => "health-oracle", Self::Qa => "qa", + Self::TopologyAdmin => "topology-admin", + Self::ResourceAdmin => "resource-admin", + Self::IndexAdmin => "index-admin", } } } @@ -87,6 +96,9 @@ impl ValueEnum for PermissionName { Self::AccessPassAdmin, Self::HealthOracle, Self::Qa, + Self::TopologyAdmin, + Self::ResourceAdmin, + Self::IndexAdmin, ] } @@ -118,6 +130,9 @@ pub fn bitmask_to_names(mask: u128) -> Vec { (permission_flags::ACCESS_PASS_ADMIN, "access-pass-admin"), (permission_flags::HEALTH_ORACLE, "health-oracle"), (permission_flags::QA, "qa"), + (permission_flags::TOPOLOGY_ADMIN, "topology-admin"), + (permission_flags::RESOURCE_ADMIN, "resource-admin"), + (permission_flags::INDEX_ADMIN, "index-admin"), ]; all.iter() .filter(|(flag, _)| mask & flag != 0) diff --git a/smartcontract/programs/doublezero-serviceability/PERMISSION.md b/smartcontract/programs/doublezero-serviceability/PERMISSION.md index c0c699bde..09f44e04b 100644 --- a/smartcontract/programs/doublezero-serviceability/PERMISSION.md +++ b/smartcontract/programs/doublezero-serviceability/PERMISSION.md @@ -37,16 +37,19 @@ sufficient. | `PERMISSION_ADMIN` | `1<<1` | Manage Permission accounts (create/update/suspend/resume/delete) | | `GLOBALSTATE_ADMIN` | `1<<13` | Manage GlobalState: feature flags, allowlists, authority keys | | `CONTRIBUTOR_ADMIN` | `1<<14` | Manage Contributors: create, update, delete | +| `INDEX_ADMIN` | `1<<17` | Manage internal Index accounts: create, delete | ### Tier 2 — Infrastructure management -| Constant | Bit | Description | -|-------------------|--------|------------------------------------------------------------| -| `INFRA_ADMIN` | `1<<2` | Manage locations and exchanges | -| `NETWORK_ADMIN` | `1<<3` | Manage devices and links | -| `TENANT_ADMIN` | `1<<4` | Manage tenants | -| `MULTICAST_ADMIN` | `1<<5` | Manage multicast groups and their allowlists | -| `FEED_AUTHORITY` | `1<<6` | Manage access for feeds | +| Constant | Bit | Description | +|-------------------|---------|------------------------------------------------------------| +| `INFRA_ADMIN` | `1<<2` | Manage locations and exchanges | +| `NETWORK_ADMIN` | `1<<3` | Manage devices and links | +| `TENANT_ADMIN` | `1<<4` | Manage tenants | +| `MULTICAST_ADMIN` | `1<<5` | Manage multicast groups and their allowlists | +| `FEED_AUTHORITY` | `1<<6` | Manage access for feeds | +| `TOPOLOGY_ADMIN` | `1<<15` | Manage segment-routing topologies: create, delete, clear, assign node segments | +| `RESOURCE_ADMIN` | `1<<16` | Manage ResourceExtension accounts: create, allocate, deallocate, close | ### Tier 3 — Operational roles @@ -100,6 +103,9 @@ Falls back to `GlobalState` fields: | `INFRA_ADMIN` | `foundation_allowlist` | | `GLOBALSTATE_ADMIN` | `foundation_allowlist` | | `CONTRIBUTOR_ADMIN` | `foundation_allowlist` | +| `TOPOLOGY_ADMIN` | `foundation_allowlist` | +| `RESOURCE_ADMIN` | `foundation_allowlist` | +| `INDEX_ADMIN` | `foundation_allowlist` | ### Foundation bypass for `PERMISSION_ADMIN` @@ -172,7 +178,7 @@ Closes the account and refunds rent to the payer. ```rust /// Can manage Foo accounts: create, update, delete. - pub const FOO_ADMIN: u128 = 1 << 15; // next available bit + pub const FOO_ADMIN: u128 = 1 << 18; // next available bit ``` Place it in the appropriate tier with a doc comment describing what it gates. @@ -206,17 +212,17 @@ Closes the account and refunds rent to the payer. - **Go** (`smartcontract/sdk/go/serviceability/state.go`): ```go - PermissionFlagFooAdmin uint64 = 1 << 15 + PermissionFlagFooAdmin uint64 = 1 << 18 ``` - **TypeScript** (`sdk/serviceability/typescript/serviceability/state.ts`): ```ts - export const PERMISSION_FLAG_FOO_ADMIN = 1n << 15n; + export const PERMISSION_FLAG_FOO_ADMIN = 1n << 18n; ``` - **Python** (`sdk/serviceability/python/serviceability/state.py`): ```python - PERMISSION_FLAG_FOO_ADMIN = 1 << 15 + PERMISSION_FLAG_FOO_ADMIN = 1 << 18 ``` 5. **Add tests:** diff --git a/smartcontract/programs/doublezero-serviceability/README.md b/smartcontract/programs/doublezero-serviceability/README.md index de906950f..df4367796 100644 --- a/smartcontract/programs/doublezero-serviceability/README.md +++ b/smartcontract/programs/doublezero-serviceability/README.md @@ -21,6 +21,19 @@ The following Rust structures define the on-chain account types that the smart c - **MulticastGroup**: Structure and enums for multicast groups, including status. - **GlobalConfig**: Structure for global configuration parameters, such as ASNs and network blocks. - **GlobalState**: Structure for the global state, including allowlists and global indices. +- **Permission**: Structure granting named capabilities to a pubkey via a `u128` permission bitmask. See [PERMISSION.md](./PERMISSION.md). + +--- + +## Permissions + +Privileged instructions are authorized through the `Permission` system (`src/authorize.rs`), which +grants fine-grained, per-pubkey capabilities via a `u128` bitmask of `permission_flags::*` and falls +back to the legacy `GlobalState` allowlists during the transition. The full flag taxonomy, +authorization model, and instructions are documented in [PERMISSION.md](./PERMISSION.md). The current +flags include foundation/permission/globalstate/contributor/index admin (Tier 1), infra/network/ +tenant/multicast/feed/topology/resource admin (Tier 2), activator/sentinel/user/access-pass admin +(Tier 3), and health-oracle/QA (Tier 4). --- diff --git a/smartcontract/programs/doublezero-serviceability/src/authorize.rs b/smartcontract/programs/doublezero-serviceability/src/authorize.rs index 145277bf1..aaa855e30 100644 --- a/smartcontract/programs/doublezero-serviceability/src/authorize.rs +++ b/smartcontract/programs/doublezero-serviceability/src/authorize.rs @@ -42,6 +42,9 @@ use solana_program::{ /// INFRA_ADMIN → foundation_allowlist /// GLOBALSTATE_ADMIN → foundation_allowlist /// CONTRIBUTOR_ADMIN → foundation_allowlist +/// TOPOLOGY_ADMIN → foundation_allowlist +/// RESOURCE_ADMIN → foundation_allowlist +/// INDEX_ADMIN → foundation_allowlist pub fn authorize<'a, 'b: 'a, I>( program_id: &Pubkey, accounts_iter: &mut I, @@ -205,6 +208,24 @@ fn check_legacy_any(payer: &Pubkey, globalstate: &GlobalState, any_of: u128) -> { return true; } + // TOPOLOGY_ADMIN in legacy = foundation. + if any_of & permission_flags::TOPOLOGY_ADMIN != 0 + && globalstate.foundation_allowlist.contains(payer) + { + return true; + } + // RESOURCE_ADMIN in legacy = foundation. + if any_of & permission_flags::RESOURCE_ADMIN != 0 + && globalstate.foundation_allowlist.contains(payer) + { + return true; + } + // INDEX_ADMIN in legacy = foundation. + if any_of & permission_flags::INDEX_ADMIN != 0 + && globalstate.foundation_allowlist.contains(payer) + { + return true; + } false } @@ -636,6 +657,62 @@ mod tests { ); } + #[test] + fn test_legacy_topology_admin_via_foundation() { + let program_id = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let gs = gs_with_foundation(&payer); + assert!( + authorize_legacy(&program_id, &payer, &gs, permission_flags::TOPOLOGY_ADMIN).is_ok() + ); + } + + #[test] + fn test_legacy_topology_admin_unauthorized() { + let program_id = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let gs = gs_with_activator(&payer); // activator does NOT grant TOPOLOGY_ADMIN + assert!( + authorize_legacy(&program_id, &payer, &gs, permission_flags::TOPOLOGY_ADMIN).is_err() + ); + } + + #[test] + fn test_legacy_resource_admin_via_foundation() { + let program_id = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let gs = gs_with_foundation(&payer); + assert!( + authorize_legacy(&program_id, &payer, &gs, permission_flags::RESOURCE_ADMIN).is_ok() + ); + } + + #[test] + fn test_legacy_resource_admin_unauthorized() { + let program_id = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let gs = gs_with_sentinel(&payer); // sentinel does NOT grant RESOURCE_ADMIN + assert!( + authorize_legacy(&program_id, &payer, &gs, permission_flags::RESOURCE_ADMIN).is_err() + ); + } + + #[test] + fn test_legacy_index_admin_via_foundation() { + let program_id = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let gs = gs_with_foundation(&payer); + assert!(authorize_legacy(&program_id, &payer, &gs, permission_flags::INDEX_ADMIN).is_ok()); + } + + #[test] + fn test_legacy_index_admin_unauthorized() { + let program_id = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let gs = gs_with_qa(&payer); // QA does NOT grant INDEX_ADMIN + assert!(authorize_legacy(&program_id, &payer, &gs, permission_flags::INDEX_ADMIN).is_err()); + } + // ── RequirePermissionAccounts feature flag ──────────────────────────────── #[test] diff --git a/smartcontract/programs/doublezero-serviceability/src/state/permission.rs b/smartcontract/programs/doublezero-serviceability/src/state/permission.rs index a6c12376e..b923716f3 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/permission.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/permission.rs @@ -18,12 +18,18 @@ pub mod permission_flags { pub const GLOBALSTATE_ADMIN: u128 = 1 << 13; /// Can manage Contributors: create, update, delete. pub const CONTRIBUTOR_ADMIN: u128 = 1 << 14; + /// Can manage internal Index accounts: create, delete. + pub const INDEX_ADMIN: u128 = 1 << 17; // ── Tier 2: Infrastructure management ───────────────────────────────── /// Can manage infrastructure: locations and exchanges. pub const INFRA_ADMIN: u128 = 1 << 2; /// Can manage network devices and links: create, activate, reject, update, delete, sethealth. pub const NETWORK_ADMIN: u128 = 1 << 3; + /// Can manage segment-routing topologies: create, delete, clear, assign node segments. + pub const TOPOLOGY_ADMIN: u128 = 1 << 15; + /// Can manage ResourceExtension accounts: create, allocate, deallocate, close. + pub const RESOURCE_ADMIN: u128 = 1 << 16; /// Can manage tenants: create, update, delete, add/remove administrators, update payment status. pub const TENANT_ADMIN: u128 = 1 << 4; /// Can manage multicast groups: create, activate, reject, update, suspend, delete, allowlists. diff --git a/smartcontract/sdk/go/serviceability/state.go b/smartcontract/sdk/go/serviceability/state.go index a2f7911d5..e94ecbad7 100644 --- a/smartcontract/sdk/go/serviceability/state.go +++ b/smartcontract/sdk/go/serviceability/state.go @@ -1308,6 +1308,9 @@ const ( PermissionFlagQA uint64 = 1 << 12 PermissionFlagGlobalstateAdmin uint64 = 1 << 13 PermissionFlagContributorAdmin uint64 = 1 << 14 + PermissionFlagTopologyAdmin uint64 = 1 << 15 + PermissionFlagResourceAdmin uint64 = 1 << 16 + PermissionFlagIndexAdmin uint64 = 1 << 17 ) type Permission struct {