Skip to content
Draft
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 @@ -32,6 +32,7 @@ All notable changes to this project will be documented in this file.
- Serviceability: expand `is_global` to reject all BGP martian address ranges (CGNAT 100.64/10, IETF 192.0.0/24, benchmarking 198.18/15, multicast 224/4, reserved 240/4, 0/8)
- Serviceability: allow update and deletion of interfaces even when sibling interfaces have invalid CYOA configuration
- SetFeatureFlagCommand added to manage on-chain feature flags for conditional behavior rollouts
- Serviceability: DeleteUser instruction supports atomic deallocate+closeaccount when OnchainAllocation feature is enabled
- Dependencies
- Upgrade Solana SDK workspace dependencies from 2.2.7 to 2.3.x (`solana-sdk`, `solana-client`, `solana-program-test`, and others)

Expand Down
4 changes: 4 additions & 0 deletions smartcontract/programs/doublezero-serviceability/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ pub enum DoubleZeroError {
MaxMulticastUsersExceeded, // variant 82
#[error("Interface cannot have both a link and a CYOA or DIA assignment")]
InterfaceHasEdgeAssignment, // variant 83
#[error("Feature not enabled")]
FeatureNotEnabled, // variant 84
}

impl From<DoubleZeroError> for ProgramError {
Expand Down Expand Up @@ -261,6 +263,7 @@ impl From<DoubleZeroError> for ProgramError {
DoubleZeroError::MaxUnicastUsersExceeded => ProgramError::Custom(81),
DoubleZeroError::MaxMulticastUsersExceeded => ProgramError::Custom(82),
DoubleZeroError::InterfaceHasEdgeAssignment => ProgramError::Custom(83),
DoubleZeroError::FeatureNotEnabled => ProgramError::Custom(84),
}
}
}
Expand Down Expand Up @@ -351,6 +354,7 @@ impl From<u32> for DoubleZeroError {
81 => DoubleZeroError::MaxUnicastUsersExceeded,
82 => DoubleZeroError::MaxMulticastUsersExceeded,
83 => DoubleZeroError::InterfaceHasEdgeAssignment,
84 => DoubleZeroError::FeatureNotEnabled,
_ => DoubleZeroError::Custom(e),
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,10 @@ mod tests {
test_instruction(DoubleZeroInstruction::SuspendUser(), "SuspendUser");
test_instruction(DoubleZeroInstruction::ResumeUser(), "ResumeUser");
test_instruction(
DoubleZeroInstruction::DeleteUser(UserDeleteArgs {}),
DoubleZeroInstruction::DeleteUser(UserDeleteArgs {
dz_prefix_count: 0,
multicast_publisher_count: 0,
}),
"DeleteUser",
);
test_instruction(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,120 @@
use crate::{
error::DoubleZeroError,
pda::get_accesspass_pda,
serializer::try_acc_write,
serializer::{try_acc_close, try_acc_write},
state::{
accesspass::{AccessPass, AccessPassStatus},
device::Device,
globalstate::GlobalState,
tenant::Tenant,
user::*,
},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
use core::fmt;

use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
use std::net::Ipv4Addr;

use super::resource_onchain_helpers;

#[derive(BorshSerialize, BorshDeserializeIncremental, PartialEq, Clone, Default)]
pub struct UserDeleteArgs {}
pub struct UserDeleteArgs {
/// Number of DzPrefixBlock accounts passed for on-chain deallocation.
/// When 0, legacy behavior (Deleting status). When > 0, atomic delete+deallocate+close.
#[incremental(default = 0)]
pub dz_prefix_count: u8,
/// Whether MulticastPublisherBlock account is passed (1 = yes, 0 = no).
#[incremental(default = 0)]
pub multicast_publisher_count: u8,
}

impl fmt::Debug for UserDeleteArgs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "")
write!(
f,
"dz_prefix_count: {}, multicast_publisher_count: {}",
self.dz_prefix_count, self.multicast_publisher_count
)
}
}

pub fn process_delete_user(
program_id: &Pubkey,
accounts: &[AccountInfo],
_value: &UserDeleteArgs,
value: &UserDeleteArgs,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();

let user_account = next_account_info(accounts_iter)?;
let accesspass_account = next_account_info(accounts_iter)?;
let globalstate_account = next_account_info(accounts_iter)?;

// Optional: additional accounts for atomic deallocation (between globalstate and payer)
// Account layout WITH deallocation (dz_prefix_count > 0):
// [user, accesspass, globalstate, device, user_tunnel_block, multicast_publisher_block?, device_tunnel_ids, dz_prefix_0..N, optional_tenant, owner, payer, system]
// Account layout WITHOUT (legacy, dz_prefix_count == 0):
// [user, accesspass, globalstate, payer, system]
let deallocation_accounts = if value.dz_prefix_count > 0 {
let device_account = next_account_info(accounts_iter)?;
let user_tunnel_block_ext = next_account_info(accounts_iter)?;

let multicast_publisher_block_ext = if value.multicast_publisher_count > 0 {
Some(next_account_info(accounts_iter)?)
} else {
None
};

let device_tunnel_ids_ext = next_account_info(accounts_iter)?;

let mut dz_prefix_accounts = Vec::with_capacity(value.dz_prefix_count as usize);
for _ in 0..value.dz_prefix_count {
dz_prefix_accounts.push(next_account_info(accounts_iter)?);
}

Some((
device_account,
user_tunnel_block_ext,
multicast_publisher_block_ext,
device_tunnel_ids_ext,
dz_prefix_accounts,
))
} else {
None
};

// For atomic path: check if user has a tenant (we'll peek at user data)
// For legacy path: no tenant handling needed
let tenant_account = if value.dz_prefix_count > 0 {
// Peek at user to check tenant
let user_peek = User::try_from(user_account)?;
if user_peek.tenant_pk != Pubkey::default() {
Some(next_account_info(accounts_iter)?)
} else {
None
}
} else {
None
};

// For atomic path, owner account is needed for close
let owner_account = if value.dz_prefix_count > 0 {
Some(next_account_info(accounts_iter)?)
} else {
None
};

let payer_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;

#[cfg(test)]
msg!("process_delete_user({:?})", _value);
msg!("process_delete_user({:?})", value);

// Check if the payer is a signer
assert!(payer_account.is_signer, "Payer must be a signer");
Expand All @@ -69,7 +140,7 @@ pub fn process_delete_user(
// Check if the account is writable
assert!(user_account.is_writable, "PDA Account is not writable");

let mut user: User = User::try_from(user_account)?;
let user: User = User::try_from(user_account)?;

let globalstate = GlobalState::try_from(globalstate_account)?;
if !globalstate.foundation_allowlist.contains(payer_account.key)
Expand Down Expand Up @@ -124,21 +195,104 @@ pub fn process_delete_user(
try_acc_write(&accesspass, accesspass_account, payer_account, accounts)?;
}

if matches!(user.status, UserStatus::Deleting | UserStatus::Updating) {
return Err(DoubleZeroError::InvalidStatus.into());
// Status check differs between legacy and atomic paths
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block is not right. Need to think about it.

if value.dz_prefix_count > 0 {
// Atomic: reject Deleting and Updating
if matches!(user.status, UserStatus::Deleting | UserStatus::Updating) {
return Err(DoubleZeroError::InvalidStatus.into());
}
} else {
// Legacy: reject Deleting and Updating (same check)
if matches!(user.status, UserStatus::Deleting | UserStatus::Updating) {
return Err(DoubleZeroError::InvalidStatus.into());
}
}

if !user.publishers.is_empty() || !user.subscribers.is_empty() {
msg!("{:?}", user);
return Err(DoubleZeroError::ReferenceCountNotZero.into());
}

user.status = UserStatus::Deleting;
if let Some((
device_account,
user_tunnel_block_ext,
multicast_publisher_block_ext,
device_tunnel_ids_ext,
dz_prefix_accounts,
)) = deallocation_accounts
{
let owner_account = owner_account.unwrap();

try_acc_write(&user, user_account, payer_account, accounts)?;
// Validate additional accounts
assert_eq!(
device_account.owner, program_id,
"Invalid Device Account Owner"
);

#[cfg(test)]
msg!("Deleting: {:?}", user);
if user.device_pk != *device_account.key {
return Err(ProgramError::InvalidAccountData);
}
if user.owner != *owner_account.key {
return Err(ProgramError::InvalidAccountData);
}

// Deallocate resources via helper (checks feature flag, validates PDAs)
resource_onchain_helpers::validate_and_deallocate_user_resources(
program_id,
&user,
user_tunnel_block_ext,
multicast_publisher_block_ext.as_ref().map(|a| *a),
device_tunnel_ids_ext,
&dz_prefix_accounts,
&globalstate,
)?;

// Decrement tenant reference count if user has tenant assigned
if let Some(tenant_acc) = tenant_account {
assert_eq!(
tenant_acc.key, &user.tenant_pk,
"Tenant account doesn't match user's tenant"
);
assert_eq!(tenant_acc.owner, program_id, "Invalid Tenant Account Owner");
assert!(tenant_acc.is_writable, "Tenant Account is not writable");

let mut tenant = Tenant::try_from(tenant_acc)?;
tenant.reference_count = tenant
.reference_count
.checked_sub(1)
.ok_or(DoubleZeroError::InvalidIndex)?;

try_acc_write(&tenant, tenant_acc, payer_account, accounts)?;
}

// Decrement device counters
let mut device = Device::try_from(device_account)?;
device.reference_count = device.reference_count.saturating_sub(1);
device.users_count = device.users_count.saturating_sub(1);
match user.user_type {
UserType::Multicast => {
device.multicast_users_count = device.multicast_users_count.saturating_sub(1);
}
_ => {
device.unicast_users_count = device.unicast_users_count.saturating_sub(1);
}
}

try_acc_write(&device, device_account, payer_account, accounts)?;
try_acc_close(user_account, owner_account)?;

#[cfg(test)]
msg!("DeleteUser (atomic): User deallocated and closed");
} else {
// Legacy path: just mark as Deleting
let mut user: User = User::try_from(user_account)?;
user.status = UserStatus::Deleting;

try_acc_write(&user, user_account, payer_account, accounts)?;

#[cfg(test)]
msg!("Deleting: {:?}", user);
}

Ok(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ pub mod create_subscribe;
pub mod delete;
pub mod reject;
pub mod requestban;
pub mod resource_onchain_helpers;
pub mod update;
Loading
Loading