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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,15 @@ All notable changes to this project will be documented in this file.
- Add onchain parent DZD discovery to geoprobe-agent: periodically queries the Geolocation program for this probe's parent devices and resolves their metrics publisher keys from Serviceability, replacing the need for static `--parent-dzd` CLI flags. Static parents from CLI are merged with onchain parents, with onchain taking precedence for duplicate keys.
- Optimize inbound probe-measured RTT accuracy: pre-sign both TWAMP probes before network I/O so probe 1 fires immediately after reply 0 with no signing delay, measure Tx-to-Rx interval (reply 0 Tx → probe 1 Rx) instead of Rx-to-Rx to exclude processing overhead on both sides, use kernel `SO_TIMESTAMPNS` receive timestamps on the reflector, and add a 15ms busy-poll window on the sender to avoid scheduler wakeup latency
- 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
- Split `execute_transaction` into `execute_transaction` (no auth) and `execute_authorized_transaction` (injects Permission PDA) to avoid breaking processors that use `accounts.len()` for optional-account detection
- 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

Expand Down
3 changes: 3 additions & 0 deletions sdk/serviceability/python/serviceability/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions sdk/serviceability/typescript/serviceability/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number, string> = {
0: "none",
Expand Down
15 changes: 15 additions & 0 deletions smartcontract/cli/src/permission/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ pub enum PermissionName {
AccessPassAdmin,
HealthOracle,
Qa,
TopologyAdmin,
ResourceAdmin,
IndexAdmin,
}

impl PermissionName {
Expand All @@ -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,
}
}

Expand All @@ -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",
}
}
}
Expand Down Expand Up @@ -87,6 +96,9 @@ impl ValueEnum for PermissionName {
Self::AccessPassAdmin,
Self::HealthOracle,
Self::Qa,
Self::TopologyAdmin,
Self::ResourceAdmin,
Self::IndexAdmin,
]
}

Expand Down Expand Up @@ -118,6 +130,9 @@ pub fn bitmask_to_names(mask: u128) -> Vec<String> {
(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)
Expand Down
28 changes: 17 additions & 11 deletions smartcontract/programs/doublezero-serviceability/PERMISSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:**
Expand Down
13 changes: 13 additions & 0 deletions smartcontract/programs/doublezero-serviceability/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

---

Expand Down
77 changes: 77 additions & 0 deletions smartcontract/programs/doublezero-serviceability/src/authorize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,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,
Expand Down Expand Up @@ -178,6 +181,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
}

Expand Down Expand Up @@ -606,6 +627,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]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use crate::{
authorize::authorize,
error::DoubleZeroError,
serializer::try_acc_close,
state::{accesspass::AccessPass, accounttype::AccountType, globalstate::GlobalState},
state::{
accesspass::AccessPass, accounttype::AccountType, globalstate::GlobalState,
permission::permission_flags,
},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
Expand Down Expand Up @@ -70,13 +74,15 @@ pub fn process_close_access_pass(
"Invalid System Program Account Owner"
);

// Parse the global state account & check if the payer is in the allowlist
// Parse the global state account & check authorization
let globalstate = GlobalState::try_from(globalstate_account)?;
if !globalstate.foundation_allowlist.contains(payer_account.key)
&& globalstate.feed_authority_pk != *payer_account.key
{
return Err(DoubleZeroError::NotAllowed.into());
}
authorize(
program_id,
accounts_iter,
payer_account.key,
&globalstate,
permission_flags::ACCESS_PASS_ADMIN,
)?;

if let Ok(data) = accesspass_account.try_borrow_data() {
let account_type: AccountType = data[0].into();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{
authorize::authorize,
error::DoubleZeroError,
pda::*,
processors::accesspass::airdrop_user_credits,
Expand All @@ -8,6 +9,7 @@ use crate::{
accesspass::{AccessPass, AccessPassStatus, AccessPassType, ALLOW_MULTIPLE_IP},
accounttype::AccountType,
globalstate::GlobalState,
permission::permission_flags,
tenant::Tenant,
},
};
Expand Down Expand Up @@ -111,17 +113,12 @@ pub fn process_set_access_pass(
"Invalid System Program Account Owner"
);

// Parse the global state account & resolve authorization. A caller is allowed if any of:
// - they are the sentinel authority,
// - they are the feed authority,
// - they are in the foundation allowlist, or
// Parse the global state account & resolve authorization. A caller is allowed if either:
// - they pass the ACCESS_PASS_ADMIN permission check (foundation allowlist, sentinel
// authority, feed authority, or a Permission account granting ACCESS_PASS_ADMIN), or
// - they are an administrator of the tenant being added (tenant_add_account).
let globalstate = GlobalState::try_from(globalstate_account)?;

let is_privileged = globalstate.sentinel_authority_pk == *payer_account.key
|| globalstate.feed_authority_pk == *payer_account.key
|| globalstate.foundation_allowlist.contains(payer_account.key);

// Pre-deserialize the tenant_add account when present so we can both authorize the caller
// and later increment its reference_count without double-reading it.
let tenant_add_pre = match tenant_add_account {
Expand All @@ -137,14 +134,20 @@ pub fn process_set_access_pass(
.map(|t| t.administrators.contains(payer_account.key))
.unwrap_or(false);

// A caller is "privileged" when they pass the ACCESS_PASS_ADMIN permission check
// (foundation allowlist, sentinel authority, feed authority, or a Permission account
// granting ACCESS_PASS_ADMIN). Privileged callers retain unrestricted authority for the
// tenant_remove path below; a tenant administrator is only authorized for their own tenant.
let is_privileged = authorize(
program_id,
accounts_iter,
payer_account.key,
&globalstate,
permission_flags::ACCESS_PASS_ADMIN,
)
.is_ok();

if !is_privileged && !is_tenant_admin {
msg!(
"sentinel_authority_pk: {} feed_authority_pk: {} payer: {} foundation_allowlist: {:?}",
globalstate.sentinel_authority_pk,
globalstate.feed_authority_pk,
payer_account.key,
globalstate.foundation_allowlist
);
return Err(DoubleZeroError::NotAllowed.into());
}

Expand Down
Loading
Loading