Skip to content
Closed
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 @@ -118,6 +118,7 @@ All notable changes to this project will be documented in this file.
- Restrict DeleteDeviceInterface to interfaces in Activated or Unlinked status; attempting to delete interfaces in other statuses now fails with InvalidStatus.
- Updated validation to allow public IP prefixes for CYOA/DIA, removing the restriction imposed by type-based checks.
- Transit devices can now be provisioned without a public IP, aligning the requirements with their actual networking model and avoiding unnecessary configuration constraints.
- Enforce that `CloseAccessPass` only closes AccessPass accounts when `connection_count == 0`, preventing closure while active connections are present.
- Enforce that ActivateDeviceInterface only activates interfaces in Pending or Unlinked status, returning InvalidStatus for all other interface states
- Introduce desired status to Link and Devices
- Internet Latency Telemetry
Expand Down
5 changes: 5 additions & 0 deletions smartcontract/programs/doublezero-serviceability/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ pub enum DoubleZeroError {
InvalidArgument, // variant 65
#[error("Invalid Foundation Allowlist: cannot be empty")]
InvalidFoundationAllowlist, // variant 66
#[error("Access Pass is in use (non-zero connection_count)")]
AccessPassInUse, // variant 70
#[error("Deprecated error")]
Deprecated, // variant 67
#[error("Immutable Field")]
Expand Down Expand Up @@ -221,6 +223,7 @@ impl From<DoubleZeroError> for ProgramError {
DoubleZeroError::ImmutableField => ProgramError::Custom(68),
DoubleZeroError::CyoaRequiresPhysical => ProgramError::Custom(69),
DoubleZeroError::DeviceHasInterfaces => ProgramError::Custom(70),
DoubleZeroError::AccessPassInUse => ProgramError::Custom(71),
}
}
}
Expand Down Expand Up @@ -298,6 +301,7 @@ impl From<u32> for DoubleZeroError {
68 => DoubleZeroError::ImmutableField,
69 => DoubleZeroError::CyoaRequiresPhysical,
70 => DoubleZeroError::DeviceHasInterfaces,
71 => DoubleZeroError::AccessPassInUse,
_ => DoubleZeroError::Custom(e),
}
}
Expand Down Expand Up @@ -391,6 +395,7 @@ mod tests {
SerializationFailure,
InvalidArgument,
InvalidFoundationAllowlist,
AccessPassInUse,
Deprecated,
ImmutableField,
CyoaRequiresPhysical,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
error::DoubleZeroError,
serializer::try_acc_close,
state::{accounttype::AccountType, globalstate::GlobalState},
state::{accesspass::AccessPass, accounttype::AccountType, globalstate::GlobalState},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
Expand Down Expand Up @@ -82,7 +82,18 @@ pub fn process_close_access_pass(
msg!("AccountType is not AccessPass, cannot close");
return Err(DoubleZeroError::InvalidAccountType.into());
}
msg!("AccountType is AccessPass, proceeding to close");

let accesspass = AccessPass::try_from(accesspass_account)?;

if accesspass.connection_count != 0 {
msg!(
"AccessPass has active connections (connection_count={}), cannot close",
accesspass.connection_count
);
return Err(DoubleZeroError::AccessPassInUse.into());
}

msg!("AccountType is AccessPass and connection_count == 0, proceeding to close");
} else {
msg!("Failed to borrow account data, cannot close");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ use doublezero_serviceability::{
processors::accesspass::{
check_status::CheckStatusAccessPassArgs, close::CloseAccessPassArgs, set::SetAccessPassArgs,
},
state::accesspass::AccessPassType,
state::{
accesspass::{AccessPass, AccessPassStatus, AccessPassType},
accounttype::AccountType,
},
};
use solana_program::rent::Rent;
use solana_program_test::*;
use solana_sdk::{
instruction::AccountMeta, pubkey::Pubkey, signature::Keypair, signer::Signer, system_program,
account::Account as SolanaAccount, instruction::AccountMeta, pubkey::Pubkey,
signature::Keypair, signer::Signer, system_program,
};
use std::net::Ipv4Addr;

Expand Down Expand Up @@ -265,6 +270,96 @@ async fn test_accesspass() {
println!("🟢 End test_accesspass");
}

#[tokio::test]
async fn test_close_accesspass_rejects_nonzero_connection_count() {
// Set up a dedicated ProgramTest so we can pre-seed an AccessPass account
let program_id = Pubkey::new_unique();

let (program_config_pubkey, _) = get_program_config_pda(&program_id);
let (globalstate_pubkey, _) = get_globalstate_pda(&program_id);

let client_ip = Ipv4Addr::new(101, 0, 0, 1);
let user_payer = Pubkey::new_unique();
let (accesspass_pubkey, bump_seed) = get_accesspass_pda(&program_id, &client_ip, &user_payer);

// Build an AccessPass with connection_count > 0
let seeded_accesspass = AccessPass {
account_type: AccountType::AccessPass,
owner: program_id,
bump_seed,
accesspass_type: AccessPassType::Prepaid,
client_ip,
user_payer,
last_access_epoch: 0,
connection_count: 1,
status: AccessPassStatus::Connected,
mgroup_pub_allowlist: vec![],
mgroup_sub_allowlist: vec![],
flags: 0,
};

let accesspass_data = borsh::to_vec(&seeded_accesspass).unwrap();
let rent = Rent::default();
let lamports = rent.minimum_balance(accesspass_data.len());

let mut program_test = ProgramTest::new(
"doublezero_serviceability",
program_id,
processor!(doublezero_serviceability::entrypoint::process_instruction),
);

// Pre-seed the AccessPass account owned by the program
program_test.add_account(
accesspass_pubkey,
SolanaAccount {
lamports,
data: accesspass_data,
owner: program_id,
executable: false,
rent_epoch: 0,
},
);

let (mut banks_client, payer, recent_blockhash) = program_test.start().await;

// Initialize global state so that payer is in the foundation_allowlist
execute_transaction(
&mut banks_client,
recent_blockhash,
program_id,
DoubleZeroInstruction::InitGlobalState(),
vec![
AccountMeta::new(program_config_pubkey, false),
AccountMeta::new(globalstate_pubkey, false),
],
&payer,
)
.await;

// Attempt to close the seeded AccessPass; this should fail because connection_count != 0
let res = try_execute_transaction(
&mut banks_client,
recent_blockhash,
program_id,
DoubleZeroInstruction::CloseAccessPass(CloseAccessPassArgs {}),
vec![
AccountMeta::new(accesspass_pubkey, false),
AccountMeta::new(globalstate_pubkey, false),
],
&payer,
)
.await;

assert!(
res.is_err(),
"CloseAccessPass should fail when connection_count > 0"
);

// The AccessPass account should still exist after the failed close attempt
let account_after = banks_client.get_account(accesspass_pubkey).await.unwrap();
assert!(account_after.is_some());
}

#[tokio::test]
async fn test_tx_lamports_to_pda_before_creation() {
let (mut banks_client, program_id, payer, recent_blockhash) = init_test().await;
Expand Down
Loading