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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ All notable changes to this project will be documented in this file.
- 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)
- Serviceability: enforce `TOPOLOGY_ADMIN`/`RESOURCE_ADMIN`/`INDEX_ADMIN` via `authorize()` in the topology (create/delete/clear/assign-node-segments), resource (create/allocate/deallocate/close), and index (create/delete) instructions, which were previously gated by the foundation allowlist only
- 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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use crate::{
authorize::authorize,
error::DoubleZeroError,
pda::get_index_pda,
seeds::{SEED_INDEX, SEED_PREFIX},
serializer::try_acc_create,
state::{accounttype::AccountType, globalstate::GlobalState, index::Index},
state::{
accounttype::AccountType, globalstate::GlobalState, index::Index,
permission::permission_flags,
},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
Expand Down Expand Up @@ -126,11 +130,15 @@ pub fn process_create_index(
);
assert!(index_account.is_writable, "Index Account is not writable");

// Check foundation allowlist
// Authorization: INDEX_ADMIN (Permission account) or foundation (legacy).
let globalstate = GlobalState::try_from(globalstate_account)?;
if !globalstate.foundation_allowlist.contains(payer_account.key) {
return Err(DoubleZeroError::NotAllowed.into());
}
authorize(
program_id,
accounts_iter,
payer_account.key,
&globalstate,
permission_flags::INDEX_ADMIN,
)?;

create_index_account(
program_id,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::{
error::DoubleZeroError,
authorize::authorize,
processors::validation::validate_program_account,
serializer::try_acc_close,
state::{globalstate::GlobalState, index::Index},
state::{globalstate::GlobalState, index::Index, permission::permission_flags},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
Expand Down Expand Up @@ -35,6 +35,9 @@ pub fn process_delete_index(
let index_account = next_account_info(accounts_iter)?;
let globalstate_account = next_account_info(accounts_iter)?;
let payer_account = next_account_info(accounts_iter)?;
// system_program is appended by the transaction builder; consume it so the
// optional trailing Permission account is what authorize() reads next.
let _system_program = next_account_info(accounts_iter)?;

#[cfg(test)]
msg!("process_delete_index");
Expand All @@ -50,11 +53,15 @@ pub fn process_delete_index(
"GlobalState"
);

// Check foundation allowlist
// Authorization: INDEX_ADMIN (Permission account) or foundation (legacy).
let globalstate = GlobalState::try_from(globalstate_account)?;
if !globalstate.foundation_allowlist.contains(payer_account.key) {
return Err(DoubleZeroError::NotAllowed.into());
}
authorize(
program_id,
accounts_iter,
payer_account.key,
&globalstate,
permission_flags::INDEX_ADMIN,
)?;

// Verify it's actually an Index account
let _index = Index::try_from(index_account)?;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use crate::{
error::DoubleZeroError,
authorize::authorize,
pda::get_resource_extension_pda,
resource::IdOrIp,
state::{globalstate::GlobalState, resource_extension::ResourceExtensionBorrowed},
state::{
globalstate::GlobalState, permission::permission_flags,
resource_extension::ResourceExtensionBorrowed,
},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
Expand Down Expand Up @@ -67,10 +70,15 @@ pub fn process_allocate_resource(
// Check if the account is writable
assert!(resource_account.is_writable, "PDA Account is not writable");

// Authorization: RESOURCE_ADMIN (Permission account) or foundation (legacy).
let globalstate = GlobalState::try_from(globalstate_account)?;
if !globalstate.foundation_allowlist.contains(payer_account.key) {
return Err(DoubleZeroError::NotAllowed.into());
}
authorize(
program_id,
accounts_iter,
payer_account.key,
&globalstate,
permission_flags::RESOURCE_ADMIN,
)?;

match value.resource_type {
crate::resource::ResourceType::DzPrefixBlock(ref associated_pk, _)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use crate::{error::DoubleZeroError, serializer::try_acc_close, state::globalstate::GlobalState};
use crate::{
authorize::authorize,
serializer::try_acc_close,
state::{globalstate::GlobalState, permission::permission_flags},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
use core::fmt;
Expand Down Expand Up @@ -51,10 +55,15 @@ pub fn process_closeaccount_resource_extension(
assert!(resource_account.is_writable, "PDA Account is not writable");
assert!(owner_account.is_writable, "Owner Account is not writable");

// Authorization: RESOURCE_ADMIN (Permission account) or foundation (legacy).
let globalstate = GlobalState::try_from(globalstate_account)?;
if !globalstate.foundation_allowlist.contains(payer_account.key) {
return Err(DoubleZeroError::NotAllowed.into());
}
authorize(
program_id,
accounts_iter,
payer_account.key,
&globalstate,
permission_flags::RESOURCE_ADMIN,
)?;

try_acc_close(resource_account, owner_account)?;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::{error::DoubleZeroError, state::globalstate::GlobalState};
use crate::{
authorize::authorize,
state::{globalstate::GlobalState, permission::permission_flags},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
#[cfg(test)]
Expand Down Expand Up @@ -65,10 +68,15 @@ pub fn process_create_resource(
"Resource Account must be uninitialized"
);

// Authorization: RESOURCE_ADMIN (Permission account) or foundation (legacy).
let globalstate = GlobalState::try_from(globalstate_account)?;
if !globalstate.foundation_allowlist.contains(payer_account.key) {
return Err(DoubleZeroError::NotAllowed.into());
}
authorize(
program_id,
accounts_iter,
payer_account.key,
&globalstate,
permission_flags::RESOURCE_ADMIN,
)?;

super::create_resource(
program_id,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use crate::{
authorize::authorize,
error::DoubleZeroError,
pda::get_resource_extension_pda,
resource::{IdOrIp, ResourceType},
state::{globalstate::GlobalState, resource_extension::ResourceExtensionBorrowed},
state::{
globalstate::GlobalState, permission::permission_flags,
resource_extension::ResourceExtensionBorrowed,
},
};
use borsh::{BorshDeserialize, BorshSerialize};
#[cfg(test)]
Expand Down Expand Up @@ -75,10 +79,15 @@ pub fn process_deallocate_resource(
// Check if the account is writable
assert!(resource_account.is_writable, "PDA Account is not writable");

// Authorization: RESOURCE_ADMIN (Permission account) or foundation (legacy).
let globalstate = GlobalState::try_from(globalstate_account)?;
if !globalstate.foundation_allowlist.contains(payer_account.key) {
return Err(DoubleZeroError::NotAllowed.into());
}
authorize(
program_id,
accounts_iter,
payer_account.key,
&globalstate,
permission_flags::RESOURCE_ADMIN,
)?;

let (expected_resource_pda, _, _) = get_resource_extension_pda(program_id, value.resource_type);
assert_eq!(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use crate::{
authorize::authorize,
error::DoubleZeroError,
pda::{get_globalstate_pda, get_resource_extension_pda, get_topology_pda},
pda::{get_globalstate_pda, get_permission_pda, get_resource_extension_pda, get_topology_pda},
processors::{resource::allocate_id, validation::validate_program_account},
resource::ResourceType,
serializer::try_acc_write,
state::{
device::Device, globalstate::GlobalState, interface::LoopbackType,
topology::FlexAlgoNodeSegment,
permission::permission_flags, topology::FlexAlgoNodeSegment,
},
};
use borsh::BorshSerialize;
Expand Down Expand Up @@ -35,11 +36,13 @@ pub type TopologyBackfillArgs = AssignTopologyNodeSegmentsArgs;
/// [1] segment_routing_ids (writable, ResourceExtension)
/// [2] globalstate (readonly)
/// [3..n] Device accounts (writable)
/// [n+1] payer (writable, signer, must be in foundation_allowlist)
/// [n+1] payer (writable, signer, must hold TOPOLOGY_ADMIN)
/// [n+2] system_program
/// [n+3] permission (readonly, optional — payer's Permission PDA)
///
/// Note: payer and system_program are the last two accounts. The SDK client
/// always appends them after the variable-length device list.
/// Note: payer and system_program are the last two accounts (or the last two
/// before the optional Permission account). The SDK client always appends them
/// after the variable-length device list.
pub fn process_assign_topology_node_segments(
program_id: &Pubkey,
accounts: &[AccountInfo],
Expand All @@ -52,15 +55,37 @@ pub fn process_assign_topology_node_segments(
let globalstate_account = next_account_info(accounts_iter)?;

// Collect remaining accounts. The SDK client always appends payer and
// system_program at the end, after the variable-length device list.
// system_program at the end, after the variable-length device list, plus an
// optional Permission account when one exists for the payer.
let all_remaining: Vec<&AccountInfo> = accounts_iter.collect();
if all_remaining.len() < 2 {
msg!("AssignTopologyNodeSegments: expected at least payer and system_program accounts");
return Err(DoubleZeroError::InvalidArgument.into());
}
let payer_account = all_remaining[all_remaining.len() - 2];
let _system_program = all_remaining[all_remaining.len() - 1];
let device_accounts = &all_remaining[..all_remaining.len() - 2];
let n = all_remaining.len();
// Detect an optional trailing Permission account. With it present the layout
// is [devices.., payer, system, permission]; the payer would then be at n-3,
// so the last account is a Permission account iff it matches that payer's PDA.
let permission_account = if n >= 3 {
let candidate_payer = all_remaining[n - 3];
let (perm_pda, _) = get_permission_pda(program_id, candidate_payer.key);
(all_remaining[n - 1].key == &perm_pda).then_some(all_remaining[n - 1])
} else {
None
};
let (payer_account, _system_program, device_accounts) = if permission_account.is_some() {
(
all_remaining[n - 3],
all_remaining[n - 2],
&all_remaining[..n - 3],
)
} else {
(
all_remaining[n - 2],
all_remaining[n - 1],
&all_remaining[..n - 2],
)
};

#[cfg(test)]
msg!("process_assign_topology_node_segments(name={})", value.name);
Expand Down Expand Up @@ -114,11 +139,15 @@ pub fn process_assign_topology_node_segments(
"GlobalState"
);

// Authorization: TOPOLOGY_ADMIN (Permission account) or foundation (legacy).
let globalstate = GlobalState::try_from(globalstate_account)?;
if !globalstate.foundation_allowlist.contains(payer_account.key) {
msg!("AssignTopologyNodeSegments: unauthorized — foundation key required");
return Err(DoubleZeroError::Unauthorized.into());
}
authorize(
program_id,
&mut permission_account.into_iter(),
payer_account.key,
&globalstate,
permission_flags::TOPOLOGY_ADMIN,
)?;

let topology_key = topology_account.key;
let mut backfilled_count: usize = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use crate::{
authorize::authorize,
error::DoubleZeroError,
pda::{get_globalstate_pda, get_link_pda, get_topology_pda},
pda::{get_globalstate_pda, get_link_pda, get_permission_pda, get_topology_pda},
processors::validation::validate_program_account,
serializer::try_acc_write,
state::{globalstate::GlobalState, link::Link, topology::TopologyInfo},
state::{
globalstate::GlobalState, link::Link, permission::permission_flags, topology::TopologyInfo,
},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
Expand All @@ -23,9 +26,10 @@ pub struct TopologyClearArgs {
/// [0] topology PDA (writable when account still exists; readonly is accepted when
/// the topology has already been closed — clear is tolerant of that)
/// [1] globalstate (readonly)
/// [2] payer (writable, signer, must be in foundation_allowlist)
/// [2] payer (writable, signer, must hold TOPOLOGY_ADMIN)
/// [3] system_program
/// [4+] Link accounts (writable) — remove topology pubkey from link_topologies on each
/// [last] permission (readonly, optional — payer's Permission PDA, after the links)
pub fn process_topology_clear(
program_id: &Pubkey,
accounts: &[AccountInfo],
Expand Down Expand Up @@ -56,12 +60,31 @@ pub fn process_topology_clear(
"GlobalState"
);

// Authorization: foundation keys only
// The remaining accounts are the variable-length Link list, optionally
// followed by the payer's Permission account (appended last by the SDK).
// Peel it off so it is not mistaken for a Link in the loop below.
let remaining: Vec<&AccountInfo> = accounts_iter.collect();
let (permission_account, link_accounts) = match remaining.last() {
Some(last) => {
let (perm_pda, _) = get_permission_pda(program_id, payer_account.key);
if last.key == &perm_pda {
(Some(*last), &remaining[..remaining.len() - 1])
} else {
(None, &remaining[..])
}
}
None => (None, &remaining[..]),
};

// Authorization: TOPOLOGY_ADMIN (Permission account) or foundation (legacy).
let globalstate = GlobalState::try_from(globalstate_account)?;
if !globalstate.foundation_allowlist.contains(payer_account.key) {
msg!("TopologyClear: unauthorized — foundation key required");
return Err(DoubleZeroError::Unauthorized.into());
}
authorize(
program_id,
&mut permission_account.into_iter(),
payer_account.key,
&globalstate,
permission_flags::TOPOLOGY_ADMIN,
)?;

// Validate topology PDA. Clear is tolerant of an already-closed topology,
// so we cannot call validate_program_account! (it asserts non-empty). If
Expand All @@ -83,7 +106,7 @@ pub fn process_topology_clear(
let mut cleared_count: usize = 0;

// Process remaining Link accounts: remove topology key from link_topologies
for link_account in accounts_iter {
for link_account in link_accounts.iter().copied() {
validate_program_account!(link_account, program_id, writable = true, "Link");
let mut link = Link::try_from(link_account)?;
assert_eq!(
Expand Down
Loading
Loading