Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ All notable changes to this project will be documented in this file.
- removes accesspass monitor task (that expires access passes)
- Onchain Programs
- Refactor user creation to validate all limits (max_users, max_multicast_users, max_unicast_users) before incrementing counters — improves efficiency by avoiding wasted work on validation failures and follows fail-fast best practice
- Serviceability: `UnlinkDeviceInterface` now only allows `Activated` or `Pending` interfaces; when an associated link account is provided for an `Activated` interface, the link must be in `Deleting` status
- SDK: `UnlinkDeviceInterfaceCommand` automatically discovers and passes associated link accounts
- E2E / QA Tests
- Fix QA unicast test flake caused by RPC 429 rate limiting during concurrent user deletion — treat transient RPC errors as non-fatal in the deletion polling loop

Expand Down
12 changes: 12 additions & 0 deletions activator/src/process/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,12 @@ mod tests {
assert!(devices.contains_key(&device_pubkey));
assert_eq!(devices.get(&device_pubkey).unwrap().device, device);

// UnlinkDeviceInterfaceCommand now looks up links to discover associated accounts
client
.expect_gets()
.with(predicate::eq(AccountType::Link))
.returning(|_| Ok(HashMap::new()));

client
.expect_execute_transaction()
.times(1)
Expand Down Expand Up @@ -481,6 +487,12 @@ mod tests {

let mut ip_block_allocator = IPBlockAllocator::new("1.1.1.0/24".parse().unwrap());

// UnlinkDeviceInterfaceCommand now looks up links to discover associated accounts
client
.expect_gets()
.with(predicate::eq(AccountType::Link))
.returning(|_| Ok(HashMap::new()));

client
.expect_execute_transaction()
.times(1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::{
device::*,
globalstate::GlobalState,
interface::{InterfaceStatus, InterfaceType},
link::*,
},
};
use borsh::BorshSerialize;
Expand Down Expand Up @@ -40,6 +41,15 @@ pub fn process_unlink_device_interface(

let device_account = next_account_info(accounts_iter)?;
let globalstate_account = next_account_info(accounts_iter)?;

// Optional link account: [device, globalstate, link, payer, system] = 5 accounts
// Without link: [device, globalstate, payer, system] = 4 accounts
let link_account = if accounts.len() > 4 {
Some(next_account_info(accounts_iter)?)
} else {
None
};

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

Expand Down Expand Up @@ -67,10 +77,34 @@ pub fn process_unlink_device_interface(
.find_interface(&value.name)
.map_err(|_| DoubleZeroError::InterfaceNotFound)?;

if iface.status == InterfaceStatus::Deleting {
if iface.status != InterfaceStatus::Activated && iface.status != InterfaceStatus::Pending {
return Err(DoubleZeroError::InvalidStatus.into());
}

// If interface is Activated and a link account is provided, verify the link
// is in Deleting status (i.e. the link must be deleted before unlinking).
if iface.status == InterfaceStatus::Activated {
if let Some(link_acc) = link_account {
if link_acc.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
let link: Link = Link::try_from(link_acc)?;

// Validate the link references this device and interface
let is_side_a =
link.side_a_pk == *device_account.key && link.side_a_iface_name == value.name;
let is_side_z =
link.side_z_pk == *device_account.key && link.side_z_iface_name == value.name;
if !is_side_a && !is_side_z {
return Err(ProgramError::InvalidAccountData);
}

if link.status != LinkStatus::Deleting {
return Err(DoubleZeroError::InvalidStatus.into());
}
}
}

iface.status = InterfaceStatus::Unlinked;
// Only reset ip_net for loopback interfaces (where IPs are auto-allocated from the pool).
// Physical interfaces keep their user-provided ip_net.
Expand Down
Loading
Loading