From c310fb5089fcf63ff51a3969fadfc2099f137d11 Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Mon, 9 Mar 2026 17:11:30 +0000 Subject: [PATCH 1/8] smartcontract: scaffold Permission account state and instruction variants --- .../programs/doublezero-serviceability/src/state/permission.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/smartcontract/programs/doublezero-serviceability/src/state/permission.rs b/smartcontract/programs/doublezero-serviceability/src/state/permission.rs index f7f1c040ef..0d44e7a29c 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/permission.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/permission.rs @@ -57,6 +57,7 @@ pub enum PermissionStatus { None = 0, Activated = 1, Suspended = 2, + Deleting = 3, } impl From for PermissionStatus { @@ -65,6 +66,7 @@ impl From for PermissionStatus { 0 => PermissionStatus::None, 1 => PermissionStatus::Activated, 2 => PermissionStatus::Suspended, + 3 => PermissionStatus::Deleting, _ => PermissionStatus::None, } } @@ -76,6 +78,7 @@ impl fmt::Display for PermissionStatus { PermissionStatus::None => write!(f, "none"), PermissionStatus::Activated => write!(f, "activated"), PermissionStatus::Suspended => write!(f, "suspended"), + PermissionStatus::Deleting => write!(f, "deleting"), } } } From c4f15943e6877c6eca7f21c01749633b090e9db1 Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Mon, 9 Mar 2026 17:43:26 +0000 Subject: [PATCH 2/8] smartcontract: implement Permission account CRUD and authorize() mechanism --- .../programs/doublezero-serviceability/src/state/permission.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/smartcontract/programs/doublezero-serviceability/src/state/permission.rs b/smartcontract/programs/doublezero-serviceability/src/state/permission.rs index 0d44e7a29c..f7f1c040ef 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/permission.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/permission.rs @@ -57,7 +57,6 @@ pub enum PermissionStatus { None = 0, Activated = 1, Suspended = 2, - Deleting = 3, } impl From for PermissionStatus { @@ -66,7 +65,6 @@ impl From for PermissionStatus { 0 => PermissionStatus::None, 1 => PermissionStatus::Activated, 2 => PermissionStatus::Suspended, - 3 => PermissionStatus::Deleting, _ => PermissionStatus::None, } } @@ -78,7 +76,6 @@ impl fmt::Display for PermissionStatus { PermissionStatus::None => write!(f, "none"), PermissionStatus::Activated => write!(f, "activated"), PermissionStatus::Suspended => write!(f, "suspended"), - PermissionStatus::Deleting => write!(f, "deleting"), } } } From d98171383b8d16fc60a72dace440be5e97cc5398 Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Mon, 9 Mar 2026 17:28:55 +0000 Subject: [PATCH 3/8] smartcontract: enforce Permission-based authorization in existing instructions --- CHANGELOG.md | 6 + activator/src/process/user.rs | 8 +- .../src/instructions.rs | 1 - .../src/processors/accesspass/close.rs | 20 +- .../src/processors/accesspass/set.rs | 24 +- .../src/processors/user/ban.rs | 13 +- .../src/processors/user/closeaccount.rs | 20 +- .../src/processors/user/delete.rs | 15 +- .../src/processors/user/requestban.rs | 13 +- .../tests/create_subscribe_user_test.rs | 290 +--------------- smartcontract/sdk/rs/src/client.rs | 73 ++++- .../sdk/rs/src/commands/accesspass/close.rs | 4 +- .../sdk/rs/src/commands/accesspass/set.rs | 4 +- .../sdk/rs/src/commands/permission/create.rs | 4 +- .../sdk/rs/src/commands/permission/delete.rs | 4 +- .../sdk/rs/src/commands/permission/resume.rs | 4 +- .../sdk/rs/src/commands/permission/suspend.rs | 4 +- .../sdk/rs/src/commands/permission/update.rs | 4 +- .../sdk/rs/src/commands/tenant/delete.rs | 12 +- smartcontract/sdk/rs/src/commands/user/ban.rs | 24 +- .../sdk/rs/src/commands/user/closeaccount.rs | 31 +- .../rs/src/commands/user/create_subscribe.rs | 309 +----------------- .../sdk/rs/src/commands/user/delete.rs | 8 +- .../sdk/rs/src/commands/user/requestban.rs | 2 +- smartcontract/sdk/rs/src/doublezeroclient.rs | 9 + 25 files changed, 226 insertions(+), 680 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63c8d3569b..1d78ca8599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,12 @@ 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 +- 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 +- 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 ## [v0.11.0](https://github.com/malbeclabs/doublezero/compare/client/v0.10.0...client/v0.11.0) - 2026-03-12 diff --git a/activator/src/process/user.rs b/activator/src/process/user.rs index 782d0b6496..3dae97fbb7 100644 --- a/activator/src/process/user.rs +++ b/activator/src/process/user.rs @@ -1775,7 +1775,7 @@ mod tests { UserStatus::Deleting, |user_service, _, seq| { user_service - .expect_execute_transaction_quiet() + .expect_execute_authorized_transaction() .times(1) .in_sequence(seq) .with( @@ -1799,7 +1799,7 @@ mod tests { UserStatus::PendingBan, |user_service, _, seq| { user_service - .expect_execute_transaction_quiet() + .expect_execute_authorized_transaction() .times(1) .in_sequence(seq) .with( @@ -2853,7 +2853,7 @@ mod tests { // Stateless mode: use_onchain_deallocation=true client - .expect_execute_transaction_quiet() + .expect_execute_authorized_transaction() .times(1) .in_sequence(&mut seq) .with( @@ -2959,7 +2959,7 @@ mod tests { .returning(move |_| Ok(AccountData::User(user2.clone()))); client - .expect_execute_transaction_quiet() + .expect_execute_authorized_transaction() .times(1) .in_sequence(&mut seq) .with( diff --git a/smartcontract/programs/doublezero-serviceability/src/instructions.rs b/smartcontract/programs/doublezero-serviceability/src/instructions.rs index 50b3864223..34849a2777 100644 --- a/smartcontract/programs/doublezero-serviceability/src/instructions.rs +++ b/smartcontract/programs/doublezero-serviceability/src/instructions.rs @@ -1061,7 +1061,6 @@ mod tests { publisher: false, subscriber: true, tunnel_endpoint: Ipv4Addr::UNSPECIFIED, - dz_prefix_count: 0, }), "CreateSubscribeUser", ); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/accesspass/close.rs b/smartcontract/programs/doublezero-serviceability/src/processors/accesspass/close.rs index 0f43be89fe..99d3202bc8 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/accesspass/close.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/accesspass/close.rs @@ -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; @@ -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.reservation_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(); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/accesspass/set.rs b/smartcontract/programs/doublezero-serviceability/src/processors/accesspass/set.rs index 7c63e17d88..e04756167d 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/accesspass/set.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/accesspass/set.rs @@ -1,4 +1,5 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, pda::*, seeds::{SEED_ACCESS_PASS, SEED_PREFIX}, @@ -7,6 +8,7 @@ use crate::{ accesspass::{AccessPass, AccessPassStatus, AccessPassType, ALLOW_MULTIPLE_IP, IS_DYNAMIC}, accounttype::AccountType, globalstate::GlobalState, + permission::permission_flags, tenant::Tenant, }, }; @@ -107,21 +109,15 @@ pub fn process_set_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.sentinel_authority_pk != *payer_account.key - && globalstate.reservation_authority_pk != *payer_account.key - && !globalstate.foundation_allowlist.contains(payer_account.key) - { - msg!( - "sentinel_authority_pk: {} reservation_authority_pk: {} payer: {} foundation_allowlist: {:?}", - globalstate.sentinel_authority_pk, - globalstate.reservation_authority_pk, - payer_account.key, - globalstate.foundation_allowlist - ); - return Err(DoubleZeroError::NotAllowed.into()); - } + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::ACCESS_PASS_ADMIN, + )?; if let AccessPassType::SolanaValidator(node_id) = value.accesspass_type { if node_id == Pubkey::default() { diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/user/ban.rs b/smartcontract/programs/doublezero-serviceability/src/processors/user/ban.rs index fb8d8a89bf..f634dc1788 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/user/ban.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/user/ban.rs @@ -1,7 +1,8 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, serializer::try_acc_write, - state::{globalstate::GlobalState, user::*}, + state::{globalstate::GlobalState, permission::permission_flags, user::*}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -57,9 +58,13 @@ pub fn process_ban_user( assert!(user_account.is_writable, "PDA Account is not writable"); let globalstate = GlobalState::try_from(globalstate_account)?; - if globalstate.activator_authority_pk != *payer_account.key { - return Err(DoubleZeroError::NotAllowed.into()); - } + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::USER_ADMIN, + )?; let mut user: User = User::try_from(user_account)?; if user.status != UserStatus::PendingBan { diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/user/closeaccount.rs b/smartcontract/programs/doublezero-serviceability/src/processors/user/closeaccount.rs index cf9a2dec39..116e83c4d6 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/user/closeaccount.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/user/closeaccount.rs @@ -1,10 +1,13 @@ use crate::{ - error::DoubleZeroError, + authorize::authorize, pda::get_resource_extension_pda, processors::resource::{deallocate_id, deallocate_ip}, resource::ResourceType, serializer::{try_acc_close, try_acc_write}, - state::{device::Device, globalstate::GlobalState, tenant::Tenant, user::*}, + state::{ + device::Device, globalstate::GlobalState, permission::permission_flags, + resource_extension::ResourceExtensionBorrowed, tenant::Tenant, user::*, + }, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -130,12 +133,13 @@ pub fn process_closeaccount_user( let globalstate = GlobalState::try_from(globalstate_account)?; - // Authorization: allow activator_authority_pk OR foundation_allowlist (matching ActivateUser) - let is_activator = globalstate.activator_authority_pk == *payer_account.key; - let is_foundation = globalstate.foundation_allowlist.contains(payer_account.key); - if !is_activator && !is_foundation { - return Err(DoubleZeroError::NotAllowed.into()); - } + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::USER_ADMIN, + )?; let user = User::try_from(user_account)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/user/delete.rs b/smartcontract/programs/doublezero-serviceability/src/processors/user/delete.rs index 5407e1c737..15df6ba357 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/user/delete.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/user/delete.rs @@ -1,4 +1,5 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, pda::get_accesspass_pda, serializer::{try_acc_close, try_acc_write}, @@ -6,6 +7,7 @@ use crate::{ accesspass::{AccessPass, AccessPassStatus}, device::Device, globalstate::GlobalState, + permission::permission_flags, tenant::Tenant, user::*, }, @@ -143,10 +145,15 @@ pub fn process_delete_user( let user: User = User::try_from(user_account)?; let globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) - && user.owner != *payer_account.key - { - return Err(DoubleZeroError::NotAllowed.into()); + // The user owner can always delete their own account without a Permission account. + if user.owner != *payer_account.key { + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::USER_ADMIN, + )?; } let (accesspass_pda, _) = get_accesspass_pda(program_id, &user.client_ip, &user.owner); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/user/requestban.rs b/smartcontract/programs/doublezero-serviceability/src/processors/user/requestban.rs index 03404a54b6..a044c52426 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/user/requestban.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/user/requestban.rs @@ -1,7 +1,8 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, serializer::try_acc_write, - state::{globalstate::GlobalState, user::*}, + state::{globalstate::GlobalState, permission::permission_flags, user::*}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -104,9 +105,13 @@ pub fn process_request_ban_user( assert!(user_account.is_writable, "PDA Account is not writable"); 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::USER_ADMIN, + )?; let mut user: User = User::try_from(user_account)?; if !can_request_ban(user.status) { diff --git a/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs b/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs index 20d8323f89..b3fab71337 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs @@ -1,11 +1,8 @@ //! Integration tests for CreateSubscribeUser instruction. //! //! Tests cover: -//! - Legacy path (dz_prefix_count=0): user created in Pending status with subscription -//! - Atomic path (dz_prefix_count>0): user created + allocated + activated with subscription +//! - User created in Pending status with publisher/subscriber subscription //! - Publisher and subscriber count correctness -//! - Backward compatibility (old args without dz_prefix_count) -//! - Feature flag enforcement for atomic path //! - Invalid multicast group status rejection (graceful error, not panic) use doublezero_serviceability::{ @@ -24,7 +21,6 @@ use doublezero_serviceability::{ }, exchange::create::ExchangeCreateArgs, globalconfig::set::SetGlobalConfigArgs, - globalstate::setfeatureflags::SetFeatureFlagsArgs, location::create::LocationCreateArgs, multicastgroup::{ activate::MulticastGroupActivateArgs, @@ -40,7 +36,6 @@ use doublezero_serviceability::{ state::{ accesspass::AccessPassType, device::DeviceType, - feature_flags::FeatureFlag, user::{UserCYOA, UserStatus, UserType}, }, }; @@ -64,11 +59,6 @@ struct CreateSubscribeFixture { accesspass_pubkey: Pubkey, mgroup_pubkey: Pubkey, user_ip: Ipv4Addr, - // Resource extension PDAs - user_tunnel_block: Pubkey, - multicast_publisher_block: Pubkey, - tunnel_ids: Pubkey, - dz_prefix_block: Pubkey, } /// Setup a complete test environment for CreateSubscribeUser: @@ -384,20 +374,16 @@ async fn setup_create_subscribe_fixture(client_ip: [u8; 4]) -> CreateSubscribeFi accesspass_pubkey, mgroup_pubkey, user_ip, - user_tunnel_block, - multicast_publisher_block, - tunnel_ids, - dz_prefix_block, } } // ============================================================================ -// Legacy Path Tests (dz_prefix_count=0) +// CreateSubscribeUser Tests // ============================================================================ -/// Legacy CreateSubscribeUser: user created in Pending status with publisher subscription. +/// CreateSubscribeUser: user created in Pending status with publisher subscription. #[tokio::test] -async fn test_create_subscribe_user_legacy_publisher() { +async fn test_create_subscribe_user_publisher() { let client_ip = [100, 0, 0, 1]; let f = setup_create_subscribe_fixture(client_ip).await; let CreateSubscribeFixture { @@ -426,7 +412,6 @@ async fn test_create_subscribe_user_legacy_publisher() { publisher: true, subscriber: false, tunnel_endpoint: Ipv4Addr::UNSPECIFIED, - dz_prefix_count: 0, }), vec![ AccountMeta::new(user_pubkey, false), @@ -449,7 +434,7 @@ async fn test_create_subscribe_user_legacy_publisher() { assert!(user.subscribers.is_empty()); assert_eq!( user.tunnel_id, 0, - "Legacy path should not allocate tunnel_id" + "user should not have tunnel_id before activation" ); let mgroup = get_account_data(&mut banks_client, mgroup_pubkey) @@ -461,9 +446,9 @@ async fn test_create_subscribe_user_legacy_publisher() { assert_eq!(mgroup.subscriber_count, 0); } -/// Legacy CreateSubscribeUser: user created in Pending status with subscriber subscription. +/// CreateSubscribeUser: user created in Pending status with subscriber subscription. #[tokio::test] -async fn test_create_subscribe_user_legacy_subscriber() { +async fn test_create_subscribe_user_subscriber() { let client_ip = [100, 0, 0, 2]; let f = setup_create_subscribe_fixture(client_ip).await; let CreateSubscribeFixture { @@ -492,7 +477,6 @@ async fn test_create_subscribe_user_legacy_subscriber() { publisher: false, subscriber: true, tunnel_endpoint: Ipv4Addr::UNSPECIFIED, - dz_prefix_count: 0, }), vec![ AccountMeta::new(user_pubkey, false), @@ -523,9 +507,9 @@ async fn test_create_subscribe_user_legacy_subscriber() { assert_eq!(mgroup.subscriber_count, 1); } -/// Legacy CreateSubscribeUser: user created with both publisher and subscriber. +/// CreateSubscribeUser: user created with both publisher and subscriber. #[tokio::test] -async fn test_create_subscribe_user_legacy_publisher_and_subscriber() { +async fn test_create_subscribe_user_publisher_and_subscriber() { let client_ip = [100, 0, 0, 3]; let f = setup_create_subscribe_fixture(client_ip).await; let CreateSubscribeFixture { @@ -554,7 +538,6 @@ async fn test_create_subscribe_user_legacy_publisher_and_subscriber() { publisher: true, subscriber: true, tunnel_endpoint: Ipv4Addr::UNSPECIFIED, - dz_prefix_count: 0, }), vec![ AccountMeta::new(user_pubkey, false), @@ -585,264 +568,10 @@ async fn test_create_subscribe_user_legacy_publisher_and_subscriber() { assert_eq!(mgroup.subscriber_count, 1); } -// ============================================================================ -// Atomic Path Tests (dz_prefix_count > 0) -// ============================================================================ - -/// Atomic CreateSubscribeUser with publisher: user created + allocated + activated. -#[tokio::test] -async fn test_create_subscribe_user_atomic_publisher() { - let client_ip = [100, 0, 0, 4]; - let f = setup_create_subscribe_fixture(client_ip).await; - let CreateSubscribeFixture { - mut banks_client, - payer, - program_id, - globalstate_pubkey, - device_pubkey, - accesspass_pubkey, - mgroup_pubkey, - user_ip, - user_tunnel_block, - multicast_publisher_block, - tunnel_ids, - dz_prefix_block, - .. - } = f; - - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - - // Enable feature flag - execute_transaction( - &mut banks_client, - recent_blockhash, - program_id, - DoubleZeroInstruction::SetFeatureFlags(SetFeatureFlagsArgs { - feature_flags: FeatureFlag::OnChainAllocation.to_mask(), - }), - vec![AccountMeta::new(globalstate_pubkey, false)], - &payer, - ) - .await; - - let (user_pubkey, _) = get_user_pda(&program_id, &user_ip, UserType::Multicast); - - // Atomic CreateSubscribeUser with resource extensions - // Account layout: [user, device, mgroup, accesspass, globalstate, user_tunnel_block, multicast_publisher_block, tunnel_ids, dz_prefix_0, payer, system] - execute_transaction( - &mut banks_client, - recent_blockhash, - program_id, - DoubleZeroInstruction::CreateSubscribeUser(UserCreateSubscribeArgs { - user_type: UserType::Multicast, - cyoa_type: UserCYOA::GREOverDIA, - client_ip: user_ip, - publisher: true, - subscriber: false, - tunnel_endpoint: Ipv4Addr::UNSPECIFIED, - dz_prefix_count: 1, - }), - vec![ - AccountMeta::new(user_pubkey, false), - AccountMeta::new(device_pubkey, false), - AccountMeta::new(mgroup_pubkey, false), - AccountMeta::new(accesspass_pubkey, false), - AccountMeta::new(globalstate_pubkey, false), - AccountMeta::new(user_tunnel_block, false), - AccountMeta::new(multicast_publisher_block, false), - AccountMeta::new(tunnel_ids, false), - AccountMeta::new(dz_prefix_block, false), - ], - &payer, - ) - .await; - - let user = get_account_data(&mut banks_client, user_pubkey) - .await - .expect("User should exist") - .get_user() - .unwrap(); - assert_eq!(user.status, UserStatus::Activated); - assert_eq!(user.publishers, vec![mgroup_pubkey]); - assert!(user.subscribers.is_empty()); - assert_ne!(user.tunnel_id, 0, "tunnel_id should be allocated"); - assert_ne!( - user.tunnel_net, - doublezero_program_common::types::NetworkV4::default(), - "tunnel_net should be allocated" - ); - // Multicast publisher gets dz_ip from MulticastPublisherBlock - assert_ne!( - user.dz_ip, - Ipv4Addr::from(client_ip), - "Multicast publisher dz_ip should be allocated, not client_ip" - ); - - let mgroup = get_account_data(&mut banks_client, mgroup_pubkey) - .await - .expect("MulticastGroup should exist") - .get_multicastgroup() - .unwrap(); - assert_eq!(mgroup.publisher_count, 1); - assert_eq!(mgroup.subscriber_count, 0); -} - -/// Atomic CreateSubscribeUser with subscriber only: dz_ip = client_ip (no publisher allocation). -#[tokio::test] -async fn test_create_subscribe_user_atomic_subscriber() { - let client_ip = [100, 0, 0, 5]; - let f = setup_create_subscribe_fixture(client_ip).await; - let CreateSubscribeFixture { - mut banks_client, - payer, - program_id, - globalstate_pubkey, - device_pubkey, - accesspass_pubkey, - mgroup_pubkey, - user_ip, - user_tunnel_block, - multicast_publisher_block, - tunnel_ids, - dz_prefix_block, - .. - } = f; - - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - - // Enable feature flag - execute_transaction( - &mut banks_client, - recent_blockhash, - program_id, - DoubleZeroInstruction::SetFeatureFlags(SetFeatureFlagsArgs { - feature_flags: FeatureFlag::OnChainAllocation.to_mask(), - }), - vec![AccountMeta::new(globalstate_pubkey, false)], - &payer, - ) - .await; - - let (user_pubkey, _) = get_user_pda(&program_id, &user_ip, UserType::Multicast); - - execute_transaction( - &mut banks_client, - recent_blockhash, - program_id, - DoubleZeroInstruction::CreateSubscribeUser(UserCreateSubscribeArgs { - user_type: UserType::Multicast, - cyoa_type: UserCYOA::GREOverDIA, - client_ip: user_ip, - publisher: false, - subscriber: true, - tunnel_endpoint: Ipv4Addr::UNSPECIFIED, - dz_prefix_count: 1, - }), - vec![ - AccountMeta::new(user_pubkey, false), - AccountMeta::new(device_pubkey, false), - AccountMeta::new(mgroup_pubkey, false), - AccountMeta::new(accesspass_pubkey, false), - AccountMeta::new(globalstate_pubkey, false), - AccountMeta::new(user_tunnel_block, false), - AccountMeta::new(multicast_publisher_block, false), - AccountMeta::new(tunnel_ids, false), - AccountMeta::new(dz_prefix_block, false), - ], - &payer, - ) - .await; - - let user = get_account_data(&mut banks_client, user_pubkey) - .await - .expect("User should exist") - .get_user() - .unwrap(); - assert_eq!(user.status, UserStatus::Activated); - assert!(user.publishers.is_empty()); - assert_eq!(user.subscribers, vec![mgroup_pubkey]); - assert_ne!(user.tunnel_id, 0, "tunnel_id should be allocated"); - // Multicast subscriber (no publishers) gets dz_ip = client_ip - assert_eq!( - user.dz_ip, - Ipv4Addr::from(client_ip), - "Subscriber-only multicast user should get dz_ip = client_ip" - ); - - let mgroup = get_account_data(&mut banks_client, mgroup_pubkey) - .await - .expect("MulticastGroup should exist") - .get_multicastgroup() - .unwrap(); - assert_eq!(mgroup.publisher_count, 0); - assert_eq!(mgroup.subscriber_count, 1); -} - // ============================================================================ // Error Path Tests // ============================================================================ -/// Atomic CreateSubscribeUser fails when feature flag is disabled. -#[tokio::test] -async fn test_create_subscribe_user_atomic_feature_flag_disabled() { - let client_ip = [100, 0, 0, 6]; - let f = setup_create_subscribe_fixture(client_ip).await; - let CreateSubscribeFixture { - mut banks_client, - payer, - program_id, - globalstate_pubkey, - device_pubkey, - accesspass_pubkey, - mgroup_pubkey, - user_ip, - user_tunnel_block, - multicast_publisher_block, - tunnel_ids, - dz_prefix_block, - .. - } = f; - - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - - let (user_pubkey, _) = get_user_pda(&program_id, &user_ip, UserType::Multicast); - - // Feature flag NOT enabled — atomic create should fail - let result = execute_transaction_expect_failure( - &mut banks_client, - recent_blockhash, - program_id, - DoubleZeroInstruction::CreateSubscribeUser(UserCreateSubscribeArgs { - user_type: UserType::Multicast, - cyoa_type: UserCYOA::GREOverDIA, - client_ip: user_ip, - publisher: true, - subscriber: false, - tunnel_endpoint: Ipv4Addr::UNSPECIFIED, - dz_prefix_count: 1, - }), - vec![ - AccountMeta::new(user_pubkey, false), - AccountMeta::new(device_pubkey, false), - AccountMeta::new(mgroup_pubkey, false), - AccountMeta::new(accesspass_pubkey, false), - AccountMeta::new(globalstate_pubkey, false), - AccountMeta::new(user_tunnel_block, false), - AccountMeta::new(multicast_publisher_block, false), - AccountMeta::new(tunnel_ids, false), - AccountMeta::new(dz_prefix_block, false), - ], - &payer, - ) - .await; - - assert!(result.is_err(), "Should fail when feature flag is disabled"); - - // Verify user account was NOT created - let user_data = get_account_data(&mut banks_client, user_pubkey).await; - assert!(user_data.is_none(), "User account should not exist"); -} - /// CreateSubscribeUser fails when multicast group is not activated (graceful error, not panic). #[tokio::test] async fn test_create_subscribe_user_inactive_mgroup_fails() { @@ -914,7 +643,6 @@ async fn test_create_subscribe_user_inactive_mgroup_fails() { publisher: false, subscriber: true, tunnel_endpoint: Ipv4Addr::UNSPECIFIED, - dz_prefix_count: 0, }), vec![ AccountMeta::new(user_pubkey, false), diff --git a/smartcontract/sdk/rs/src/client.rs b/smartcontract/sdk/rs/src/client.rs index ff243aaa09..07532d3084 100644 --- a/smartcontract/sdk/rs/src/client.rs +++ b/smartcontract/sdk/rs/src/client.rs @@ -6,7 +6,8 @@ use std::time::Duration; use crate::config::default_program_id; use doublezero_serviceability::{ - error::DoubleZeroError, instructions::*, state::accounttype::AccountType, + error::DoubleZeroError, instructions::*, pda::get_permission_pda, + state::accounttype::AccountType, }; use eyre::{bail, eyre, OptionExt}; use log::debug; @@ -374,6 +375,66 @@ impl DZClient { Ok(errors) } + + fn build_and_send( + &self, + instruction: DoubleZeroInstruction, + accounts: Vec, + with_permission: bool, + ) -> eyre::Result { + let payer = self + .payer + .as_ref() + .ok_or_eyre("No default signer found, run \"doublezero keygen\" to create a new one")?; + let data = instruction.pack(); + + let mut trailing = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(program::id(), false), + ]; + if with_permission { + let (permission_pda, _) = get_permission_pda(&self.program_id, &payer.pubkey()); + if self.client.get_account(&permission_pda).is_ok() { + trailing.push(AccountMeta::new_readonly(permission_pda, false)); + } + } + + let mut transaction = Transaction::new_with_payer( + &[Instruction::new_with_bytes( + self.program_id, + &data, + [accounts, trailing].concat(), + )], + Some(&payer.pubkey()), + ); + + let blockhash = self.client.get_latest_blockhash().map_err(|e| eyre!(e))?; + transaction.sign(&[&payer], blockhash); + + debug!("Simulating transaction: {transaction:?}"); + + let result = self.client.simulate_transaction(&transaction)?; + if result.value.err.is_some() { + eprintln!("Program Logs:"); + if let Some(logs) = result.value.logs { + for log in logs { + eprintln!("{log}"); + } + } + } + + if let Some(TransactionError::InstructionError(_index, InstructionError::Custom(number))) = + result.value.err + { + return Err(eyre!(DoubleZeroError::from(number))); + } else if let Some(err) = result.value.err { + return Err(eyre!(err)); + } + + self.client + .send_and_confirm_transaction(&transaction) + .map_err(|e| eyre!(e)) + } } impl DoubleZeroClient for DZClient { @@ -450,7 +511,7 @@ impl DoubleZeroClient for DZClient { instruction: DoubleZeroInstruction, accounts: Vec, ) -> eyre::Result { - self.execute_transaction_inner(instruction, accounts, false) + self.build_and_send(instruction, accounts, false) } fn execute_transaction_quiet( @@ -461,6 +522,14 @@ impl DoubleZeroClient for DZClient { self.execute_transaction_inner(instruction, accounts, true) } + fn execute_authorized_transaction( + &self, + instruction: DoubleZeroInstruction, + accounts: Vec, + ) -> eyre::Result { + self.build_and_send(instruction, accounts, true) + } + fn gets(&self, account_type: AccountType) -> eyre::Result> { let account_type = account_type as u8; let filters = vec![RpcFilterType::Memcmp(Memcmp::new( diff --git a/smartcontract/sdk/rs/src/commands/accesspass/close.rs b/smartcontract/sdk/rs/src/commands/accesspass/close.rs index 364676a6c4..15cc77b179 100644 --- a/smartcontract/sdk/rs/src/commands/accesspass/close.rs +++ b/smartcontract/sdk/rs/src/commands/accesspass/close.rs @@ -15,7 +15,7 @@ impl CloseAccessPassCommand { .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::CloseAccessPass(CloseAccessPassArgs {}), vec![ AccountMeta::new(self.pubkey, false), @@ -50,7 +50,7 @@ mod tests { let (pda_pubkey, _) = get_accesspass_pda(&client.get_program_id(), &client_ip, &payer); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::CloseAccessPass( CloseAccessPassArgs {}, diff --git a/smartcontract/sdk/rs/src/commands/accesspass/set.rs b/smartcontract/sdk/rs/src/commands/accesspass/set.rs index 26c566fc0d..049c1f127d 100644 --- a/smartcontract/sdk/rs/src/commands/accesspass/set.rs +++ b/smartcontract/sdk/rs/src/commands/accesspass/set.rs @@ -65,7 +65,7 @@ impl SetAccessPassCommand { accounts.push(AccountMeta::new(self.tenant, false)); } - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::SetAccessPass(SetAccessPassArgs { accesspass_type: self.accesspass_type.clone(), client_ip: self.client_ip, @@ -127,7 +127,7 @@ mod tests { .returning(move |_| Ok(AccountData::AccessPass(accesspass.clone()))); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::SetAccessPass(SetAccessPassArgs { accesspass_type: AccessPassType::Prepaid, diff --git a/smartcontract/sdk/rs/src/commands/permission/create.rs b/smartcontract/sdk/rs/src/commands/permission/create.rs index 3c8b78d50e..af5887d448 100644 --- a/smartcontract/sdk/rs/src/commands/permission/create.rs +++ b/smartcontract/sdk/rs/src/commands/permission/create.rs @@ -20,7 +20,7 @@ impl CreatePermissionCommand { let (permission_pda, _) = get_permission_pda(&client.get_program_id(), &self.user_payer); client - .execute_transaction( + .execute_authorized_transaction( DoubleZeroInstruction::CreatePermission(PermissionCreateArgs { user_payer: self.user_payer, permissions: self.permissions, @@ -58,7 +58,7 @@ mod tests { let (permission_pda, _) = get_permission_pda(&client.get_program_id(), &user_payer); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::CreatePermission( PermissionCreateArgs { diff --git a/smartcontract/sdk/rs/src/commands/permission/delete.rs b/smartcontract/sdk/rs/src/commands/permission/delete.rs index 928481de0b..2aa709d5e2 100644 --- a/smartcontract/sdk/rs/src/commands/permission/delete.rs +++ b/smartcontract/sdk/rs/src/commands/permission/delete.rs @@ -14,7 +14,7 @@ impl DeletePermissionCommand { pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result { let (globalstate_pubkey, _) = get_globalstate_pda(&client.get_program_id()); - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::DeletePermission(PermissionDeleteArgs {}), vec![ AccountMeta::new(self.permission_pda, false), @@ -47,7 +47,7 @@ mod tests { let (permission_pda, _) = get_permission_pda(&client.get_program_id(), &user_payer); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::DeletePermission( PermissionDeleteArgs {}, diff --git a/smartcontract/sdk/rs/src/commands/permission/resume.rs b/smartcontract/sdk/rs/src/commands/permission/resume.rs index e855259455..38e3dd7fd4 100644 --- a/smartcontract/sdk/rs/src/commands/permission/resume.rs +++ b/smartcontract/sdk/rs/src/commands/permission/resume.rs @@ -14,7 +14,7 @@ impl ResumePermissionCommand { pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result { let (globalstate_pubkey, _) = get_globalstate_pda(&client.get_program_id()); - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::ResumePermission(PermissionResumeArgs {}), vec![ AccountMeta::new(self.permission_pda, false), @@ -47,7 +47,7 @@ mod tests { let (permission_pda, _) = get_permission_pda(&client.get_program_id(), &user_payer); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::ResumePermission( PermissionResumeArgs {}, diff --git a/smartcontract/sdk/rs/src/commands/permission/suspend.rs b/smartcontract/sdk/rs/src/commands/permission/suspend.rs index 814c8930c1..36d8ff8e11 100644 --- a/smartcontract/sdk/rs/src/commands/permission/suspend.rs +++ b/smartcontract/sdk/rs/src/commands/permission/suspend.rs @@ -14,7 +14,7 @@ impl SuspendPermissionCommand { pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result { let (globalstate_pubkey, _) = get_globalstate_pda(&client.get_program_id()); - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::SuspendPermission(PermissionSuspendArgs {}), vec![ AccountMeta::new(self.permission_pda, false), @@ -47,7 +47,7 @@ mod tests { let (permission_pda, _) = get_permission_pda(&client.get_program_id(), &user_payer); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::SuspendPermission( PermissionSuspendArgs {}, diff --git a/smartcontract/sdk/rs/src/commands/permission/update.rs b/smartcontract/sdk/rs/src/commands/permission/update.rs index 5f492c74a7..db1dbe8ca9 100644 --- a/smartcontract/sdk/rs/src/commands/permission/update.rs +++ b/smartcontract/sdk/rs/src/commands/permission/update.rs @@ -16,7 +16,7 @@ impl UpdatePermissionCommand { pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result { let (globalstate_pubkey, _) = get_globalstate_pda(&client.get_program_id()); - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::UpdatePermission(PermissionUpdateArgs { add: self.add, remove: self.remove, @@ -54,7 +54,7 @@ mod tests { let (permission_pda, _) = get_permission_pda(&client.get_program_id(), &user_payer); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::UpdatePermission( PermissionUpdateArgs { add, remove }, diff --git a/smartcontract/sdk/rs/src/commands/tenant/delete.rs b/smartcontract/sdk/rs/src/commands/tenant/delete.rs index df8c29254e..b6afa95f4b 100644 --- a/smartcontract/sdk/rs/src/commands/tenant/delete.rs +++ b/smartcontract/sdk/rs/src/commands/tenant/delete.rs @@ -286,9 +286,9 @@ mod tests { .in_sequence(&mut seq) .returning(|_| Ok(HashMap::new())); - // 4. DeleteUserCommand internally: execute_transaction(DeleteUser) + // 4. DeleteUserCommand internally: execute_authorized_transaction(DeleteUser) client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::DeleteUser(UserDeleteArgs::default())), predicate::always(), @@ -313,9 +313,9 @@ mod tests { Ok(map) }); - // 6. SetAccessPassCommand: execute_transaction(SetAccessPass) to reset tenant + // 6. SetAccessPassCommand: execute_authorized_transaction(SetAccessPass) to reset tenant client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::SetAccessPass(SetAccessPassArgs { accesspass_type: AccessPassType::Prepaid, @@ -415,9 +415,9 @@ mod tests { Ok(map) }); - // 2. SetAccessPassCommand: execute_transaction(SetAccessPass) to reset tenant + // 2. SetAccessPassCommand: execute_authorized_transaction(SetAccessPass) to reset tenant client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::SetAccessPass(SetAccessPassArgs { accesspass_type: AccessPassType::Prepaid, diff --git a/smartcontract/sdk/rs/src/commands/user/ban.rs b/smartcontract/sdk/rs/src/commands/user/ban.rs index 0533be6521..4418b85455 100644 --- a/smartcontract/sdk/rs/src/commands/user/ban.rs +++ b/smartcontract/sdk/rs/src/commands/user/ban.rs @@ -19,21 +19,21 @@ impl BanUserCommand { self.execute_inner(client, true) } - fn execute_inner(&self, client: &dyn DoubleZeroClient, quiet: bool) -> eyre::Result { + fn execute_inner( + &self, + client: &dyn DoubleZeroClient, + _quiet: bool, + ) -> eyre::Result { let (globalstate_pubkey, _globalstate) = GetGlobalStateCommand .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; - let instruction = DoubleZeroInstruction::BanUser(UserBanArgs {}); - let accounts = vec![ - AccountMeta::new(self.pubkey, false), - AccountMeta::new(globalstate_pubkey, false), - ]; - - if quiet { - client.execute_transaction_quiet(instruction, accounts) - } else { - client.execute_transaction(instruction, accounts) - } + client.execute_authorized_transaction( + DoubleZeroInstruction::BanUser(UserBanArgs {}), + vec![ + AccountMeta::new(self.pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + ], + ) } } diff --git a/smartcontract/sdk/rs/src/commands/user/closeaccount.rs b/smartcontract/sdk/rs/src/commands/user/closeaccount.rs index 998a7a20f0..757687ec7f 100644 --- a/smartcontract/sdk/rs/src/commands/user/closeaccount.rs +++ b/smartcontract/sdk/rs/src/commands/user/closeaccount.rs @@ -29,7 +29,11 @@ impl CloseAccountUserCommand { self.execute_inner(client, true) } - fn execute_inner(&self, client: &dyn DoubleZeroClient, quiet: bool) -> eyre::Result { + fn execute_inner( + &self, + client: &dyn DoubleZeroClient, + _quiet: bool, + ) -> eyre::Result { let (globalstate_pubkey, _globalstate) = GetGlobalStateCommand .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; @@ -113,16 +117,13 @@ impl CloseAccountUserCommand { accounts.push(AccountMeta::new(user.tenant_pk, false)); } - let instruction = DoubleZeroInstruction::CloseAccountUser(UserCloseAccountArgs { - dz_prefix_count, - multicast_publisher_count, - }); - - if quiet { - client.execute_transaction_quiet(instruction, accounts) - } else { - client.execute_transaction(instruction, accounts) - } + client.execute_authorized_transaction( + DoubleZeroInstruction::CloseAccountUser(UserCloseAccountArgs { + dz_prefix_count, + multicast_publisher_count, + }), + accounts, + ) } } @@ -185,7 +186,7 @@ mod tests { .returning(move |_| Ok(AccountData::User(user.clone()))); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::CloseAccountUser( UserCloseAccountArgs { @@ -289,7 +290,7 @@ mod tests { .returning(move |_| Ok(AccountData::GlobalConfig(globalconfig.clone()))); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::CloseAccountUser( UserCloseAccountArgs { @@ -354,7 +355,7 @@ mod tests { .returning(move |_| Ok(AccountData::User(user.clone()))); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::CloseAccountUser( UserCloseAccountArgs { @@ -457,7 +458,7 @@ mod tests { .returning(move |_| Ok(AccountData::GlobalConfig(globalconfig.clone()))); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::CloseAccountUser( UserCloseAccountArgs { diff --git a/smartcontract/sdk/rs/src/commands/user/create_subscribe.rs b/smartcontract/sdk/rs/src/commands/user/create_subscribe.rs index 438728f0d1..f879d4e951 100644 --- a/smartcontract/sdk/rs/src/commands/user/create_subscribe.rs +++ b/smartcontract/sdk/rs/src/commands/user/create_subscribe.rs @@ -1,10 +1,8 @@ use doublezero_serviceability::{ instructions::DoubleZeroInstruction, - pda::{get_resource_extension_pda, get_user_pda}, + pda::get_user_pda, processors::user::create_subscribe::UserCreateSubscribeArgs, - resource::ResourceType, state::{ - feature_flags::{is_feature_enabled, FeatureFlag}, multicastgroup::MulticastGroupStatus, user::{UserCYOA, UserType}, }, @@ -14,8 +12,8 @@ use std::net::Ipv4Addr; use crate::{ commands::{ - accesspass::get::GetAccessPassCommand, device::get::GetDeviceCommand, - globalstate::get::GetGlobalStateCommand, multicastgroup::get::GetMulticastGroupCommand, + accesspass::get::GetAccessPassCommand, globalstate::get::GetGlobalStateCommand, + multicastgroup::get::GetMulticastGroupCommand, }, DoubleZeroClient, }; @@ -34,13 +32,10 @@ pub struct CreateSubscribeUserCommand { impl CreateSubscribeUserCommand { pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result<(Signature, Pubkey)> { - let (globalstate_pubkey, globalstate) = GetGlobalStateCommand + let (globalstate_pubkey, _globalstate) = GetGlobalStateCommand .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; - let use_onchain_allocation = - is_feature_enabled(globalstate.feature_flags, FeatureFlag::OnChainAllocation); - let (_, mgroup) = GetMulticastGroupCommand { pubkey_or_code: self.mgroup_pk.to_string(), } @@ -71,51 +66,6 @@ impl CreateSubscribeUserCommand { let (pda_pubkey, _) = get_user_pda(&client.get_program_id(), &self.client_ip, self.user_type); - let mut accounts = vec![ - AccountMeta::new(pda_pubkey, false), - AccountMeta::new(self.device_pk, false), - AccountMeta::new(self.mgroup_pk, false), - AccountMeta::new(accesspass_pk, false), - AccountMeta::new(globalstate_pubkey, false), - ]; - - let dz_prefix_count: u8 = if use_onchain_allocation { - let (_, device) = GetDeviceCommand { - pubkey_or_code: self.device_pk.to_string(), - } - .execute(client) - .map_err(|_| eyre::eyre!("Device not found"))?; - - let count = device.dz_prefixes.len(); - - let (user_tunnel_block_ext, _, _) = - get_resource_extension_pda(&client.get_program_id(), ResourceType::UserTunnelBlock); - let (multicast_publisher_block_ext, _, _) = get_resource_extension_pda( - &client.get_program_id(), - ResourceType::MulticastPublisherBlock, - ); - let (device_tunnel_ids_ext, _, _) = get_resource_extension_pda( - &client.get_program_id(), - ResourceType::TunnelIds(self.device_pk, 0), - ); - - accounts.push(AccountMeta::new(user_tunnel_block_ext, false)); - accounts.push(AccountMeta::new(multicast_publisher_block_ext, false)); - accounts.push(AccountMeta::new(device_tunnel_ids_ext, false)); - - for idx in 0..count { - let (dz_prefix_ext, _, _) = get_resource_extension_pda( - &client.get_program_id(), - ResourceType::DzPrefixBlock(self.device_pk, idx), - ); - accounts.push(AccountMeta::new(dz_prefix_ext, false)); - } - - count as u8 - } else { - 0 - }; - client .execute_transaction( DoubleZeroInstruction::CreateSubscribeUser(UserCreateSubscribeArgs { @@ -125,254 +75,15 @@ impl CreateSubscribeUserCommand { publisher: self.publisher, subscriber: self.subscriber, tunnel_endpoint: self.tunnel_endpoint, - dz_prefix_count, }), - accounts, - ) - .map(|sig| (sig, pda_pubkey)) - } -} - -#[cfg(test)] -mod tests { - use crate::{ - commands::user::create_subscribe::CreateSubscribeUserCommand, - tests::utils::create_test_client, DoubleZeroClient, MockDoubleZeroClient, - }; - use doublezero_serviceability::{ - instructions::DoubleZeroInstruction, - pda::{get_accesspass_pda, get_globalstate_pda, get_resource_extension_pda, get_user_pda}, - processors::user::create_subscribe::UserCreateSubscribeArgs, - resource::ResourceType, - state::{ - accesspass::{AccessPass, AccessPassStatus, AccessPassType}, - accountdata::AccountData, - accounttype::AccountType, - device::Device, - feature_flags::FeatureFlag, - globalstate::GlobalState, - multicastgroup::{MulticastGroup, MulticastGroupStatus}, - user::{UserCYOA, UserType}, - }, - }; - use mockall::predicate; - use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, signature::Signature}; - use std::net::Ipv4Addr; - - #[test] - fn test_commands_user_create_subscribe_legacy() { - let mut client = create_test_client(); - - let program_id = client.get_program_id(); - let (globalstate_pubkey, _) = get_globalstate_pda(&program_id); - let device_pk = Pubkey::new_unique(); - let mgroup_pk = Pubkey::new_unique(); - let client_ip = Ipv4Addr::new(192, 168, 1, 10); - - let (pda_pubkey, _) = get_user_pda(&program_id, &client_ip, UserType::IBRLWithAllocatedIP); - - // Mock MulticastGroup - let mgroup = MulticastGroup { - status: MulticastGroupStatus::Activated, - ..Default::default() - }; - client - .expect_get() - .with(predicate::eq(mgroup_pk)) - .returning(move |_| Ok(AccountData::MulticastGroup(mgroup.clone()))); - - // Mock AccessPass - let (accesspass_pubkey, _) = - get_accesspass_pda(&program_id, &client_ip, &client.get_payer()); - let accesspass = AccessPass { - account_type: AccountType::AccessPass, - bump_seed: 0, - accesspass_type: AccessPassType::Prepaid, - client_ip, - user_payer: client.get_payer(), - last_access_epoch: 0, - connection_count: 0, - status: AccessPassStatus::Requested, - owner: client.get_payer(), - mgroup_pub_allowlist: vec![], - mgroup_sub_allowlist: vec![], - tenant_allowlist: vec![], - flags: 0, - }; - client - .expect_get() - .with(predicate::eq(accesspass_pubkey)) - .returning(move |_| Ok(AccountData::AccessPass(accesspass.clone()))); - - client - .expect_execute_transaction() - .with( - predicate::eq(DoubleZeroInstruction::CreateSubscribeUser( - UserCreateSubscribeArgs { - user_type: UserType::IBRLWithAllocatedIP, - cyoa_type: UserCYOA::GREOverDIA, - client_ip, - publisher: true, - subscriber: false, - tunnel_endpoint: Ipv4Addr::UNSPECIFIED, - dz_prefix_count: 0, - }, - )), - predicate::eq(vec![ + vec![ AccountMeta::new(pda_pubkey, false), - AccountMeta::new(device_pk, false), - AccountMeta::new(mgroup_pk, false), - AccountMeta::new(accesspass_pubkey, false), + AccountMeta::new(self.device_pk, false), + AccountMeta::new(self.mgroup_pk, false), + AccountMeta::new(accesspass_pk, false), AccountMeta::new(globalstate_pubkey, false), - ]), + ], ) - .returning(|_, _| Ok(Signature::new_unique())); - - let res = CreateSubscribeUserCommand { - user_type: UserType::IBRLWithAllocatedIP, - device_pk, - cyoa_type: UserCYOA::GREOverDIA, - client_ip, - mgroup_pk, - publisher: true, - subscriber: false, - tunnel_endpoint: Ipv4Addr::UNSPECIFIED, - } - .execute(&client); - - assert!(res.is_ok()); - } - - #[test] - fn test_commands_user_create_subscribe_with_onchain_allocation() { - let mut client = MockDoubleZeroClient::new(); - - let payer = Pubkey::new_unique(); - client.expect_get_payer().returning(move || payer); - let program_id = Pubkey::new_unique(); - client.expect_get_program_id().returning(move || program_id); - - let (globalstate_pubkey, bump_seed) = get_globalstate_pda(&program_id); - let globalstate = GlobalState { - account_type: AccountType::GlobalState, - bump_seed, - account_index: 0, - foundation_allowlist: vec![], - _device_allowlist: vec![], - _user_allowlist: vec![], - activator_authority_pk: Pubkey::new_unique(), - sentinel_authority_pk: Pubkey::new_unique(), - contributor_airdrop_lamports: 1_000_000_000, - user_airdrop_lamports: 40_000, - health_oracle_pk: Pubkey::new_unique(), - qa_allowlist: vec![], - feature_flags: FeatureFlag::OnChainAllocation.to_mask(), - reservation_authority_pk: Pubkey::default(), - }; - client - .expect_get() - .with(predicate::eq(globalstate_pubkey)) - .returning(move |_| Ok(AccountData::GlobalState(globalstate.clone()))); - - let device_pk = Pubkey::new_unique(); - let mgroup_pk = Pubkey::new_unique(); - let client_ip = Ipv4Addr::new(192, 168, 1, 10); - - let (pda_pubkey, _) = get_user_pda(&program_id, &client_ip, UserType::IBRLWithAllocatedIP); - - // Mock MulticastGroup - let mgroup = MulticastGroup { - status: MulticastGroupStatus::Activated, - ..Default::default() - }; - client - .expect_get() - .with(predicate::eq(mgroup_pk)) - .returning(move |_| Ok(AccountData::MulticastGroup(mgroup.clone()))); - - // Mock AccessPass - let (accesspass_pubkey, _) = get_accesspass_pda(&program_id, &client_ip, &payer); - let accesspass = AccessPass { - account_type: AccountType::AccessPass, - bump_seed: 0, - accesspass_type: AccessPassType::Prepaid, - client_ip, - user_payer: payer, - last_access_epoch: 0, - connection_count: 0, - status: AccessPassStatus::Requested, - owner: payer, - mgroup_pub_allowlist: vec![], - mgroup_sub_allowlist: vec![], - tenant_allowlist: vec![], - flags: 0, - }; - client - .expect_get() - .with(predicate::eq(accesspass_pubkey)) - .returning(move |_| Ok(AccountData::AccessPass(accesspass.clone()))); - - // Mock Device fetch (for dz_prefixes.len()) - let device = Device { - account_type: AccountType::Device, - dz_prefixes: "10.0.0.0/24".parse().unwrap(), - ..Default::default() - }; - client - .expect_get() - .with(predicate::eq(device_pk)) - .returning(move |_| Ok(AccountData::Device(device.clone()))); - - // Compute ResourceExtension PDAs - let (user_tunnel_block_ext, _, _) = - get_resource_extension_pda(&program_id, ResourceType::UserTunnelBlock); - let (multicast_publisher_block_ext, _, _) = - get_resource_extension_pda(&program_id, ResourceType::MulticastPublisherBlock); - let (device_tunnel_ids_ext, _, _) = - get_resource_extension_pda(&program_id, ResourceType::TunnelIds(device_pk, 0)); - let (dz_prefix_ext, _, _) = - get_resource_extension_pda(&program_id, ResourceType::DzPrefixBlock(device_pk, 0)); - - client - .expect_execute_transaction() - .with( - predicate::eq(DoubleZeroInstruction::CreateSubscribeUser( - UserCreateSubscribeArgs { - user_type: UserType::IBRLWithAllocatedIP, - cyoa_type: UserCYOA::GREOverDIA, - client_ip, - publisher: true, - subscriber: false, - tunnel_endpoint: Ipv4Addr::UNSPECIFIED, - dz_prefix_count: 1, - }, - )), - predicate::eq(vec![ - AccountMeta::new(pda_pubkey, false), - AccountMeta::new(device_pk, false), - AccountMeta::new(mgroup_pk, false), - AccountMeta::new(accesspass_pubkey, false), - AccountMeta::new(globalstate_pubkey, false), - AccountMeta::new(user_tunnel_block_ext, false), - AccountMeta::new(multicast_publisher_block_ext, false), - AccountMeta::new(device_tunnel_ids_ext, false), - AccountMeta::new(dz_prefix_ext, false), - ]), - ) - .returning(|_, _| Ok(Signature::new_unique())); - - let res = CreateSubscribeUserCommand { - user_type: UserType::IBRLWithAllocatedIP, - device_pk, - cyoa_type: UserCYOA::GREOverDIA, - client_ip, - mgroup_pk, - publisher: true, - subscriber: false, - tunnel_endpoint: Ipv4Addr::UNSPECIFIED, - } - .execute(&client); - - assert!(res.is_ok()); + .map(|sig| (sig, pda_pubkey)) } } diff --git a/smartcontract/sdk/rs/src/commands/user/delete.rs b/smartcontract/sdk/rs/src/commands/user/delete.rs index bbec473b1d..25eab0b892 100644 --- a/smartcontract/sdk/rs/src/commands/user/delete.rs +++ b/smartcontract/sdk/rs/src/commands/user/delete.rs @@ -168,7 +168,7 @@ impl DeleteUserCommand { (0u8, 0u8) }; - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::DeleteUser(UserDeleteArgs { dz_prefix_count, multicast_publisher_count, @@ -394,7 +394,7 @@ mod tests { // Execute transaction for DeleteUser client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::DeleteUser(UserDeleteArgs::default())), predicate::eq(vec![ @@ -595,7 +595,7 @@ mod tests { // DeleteUser transaction client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::DeleteUser(UserDeleteArgs::default())), predicate::eq(vec![ @@ -728,7 +728,7 @@ mod tests { get_resource_extension_pda(&program_id, ResourceType::DzPrefixBlock(device_pk, 0)); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::DeleteUser(UserDeleteArgs { dz_prefix_count: 1, diff --git a/smartcontract/sdk/rs/src/commands/user/requestban.rs b/smartcontract/sdk/rs/src/commands/user/requestban.rs index fa94559fd3..36b79dedd1 100644 --- a/smartcontract/sdk/rs/src/commands/user/requestban.rs +++ b/smartcontract/sdk/rs/src/commands/user/requestban.rs @@ -140,7 +140,7 @@ impl RequestBanUserCommand { (0u8, 0u8) }; - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::RequestBanUser(UserRequestBanArgs { dz_prefix_count, multicast_publisher_count, diff --git a/smartcontract/sdk/rs/src/doublezeroclient.rs b/smartcontract/sdk/rs/src/doublezeroclient.rs index 61e63007d1..f778e40ece 100644 --- a/smartcontract/sdk/rs/src/doublezeroclient.rs +++ b/smartcontract/sdk/rs/src/doublezeroclient.rs @@ -50,6 +50,15 @@ pub trait DoubleZeroClient { accounts: Vec, ) -> eyre::Result; + /// Like `execute_transaction` but appends the payer's Permission PDA + /// (read-only) when it exists on-chain, so `authorize()` can find it. + /// Use this for instructions whose processor calls `authorize()`. + fn execute_authorized_transaction( + &self, + instruction: DoubleZeroInstruction, + accounts: Vec, + ) -> eyre::Result; + fn get_transactions(&self, pubkey: Pubkey) -> eyre::Result>; } From 72212af44717924e63dce65d2517d7733021a3f4 Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Fri, 13 Mar 2026 13:35:05 +0000 Subject: [PATCH 4/8] smartcontract: fix lint errors after permission enforcement - remove unused ResourceExtensionBorrowed import in closeaccount - add missing dz_prefix_count field to UserCreateSubscribeArgs initializers in sdk, instructions test, and integration tests --- .../programs/doublezero-serviceability/src/instructions.rs | 1 + .../src/processors/user/closeaccount.rs | 4 ++-- .../tests/create_subscribe_user_test.rs | 4 ++++ smartcontract/sdk/rs/src/commands/user/create_subscribe.rs | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/smartcontract/programs/doublezero-serviceability/src/instructions.rs b/smartcontract/programs/doublezero-serviceability/src/instructions.rs index 34849a2777..50b3864223 100644 --- a/smartcontract/programs/doublezero-serviceability/src/instructions.rs +++ b/smartcontract/programs/doublezero-serviceability/src/instructions.rs @@ -1061,6 +1061,7 @@ mod tests { publisher: false, subscriber: true, tunnel_endpoint: Ipv4Addr::UNSPECIFIED, + dz_prefix_count: 0, }), "CreateSubscribeUser", ); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/user/closeaccount.rs b/smartcontract/programs/doublezero-serviceability/src/processors/user/closeaccount.rs index 116e83c4d6..7b685831cc 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/user/closeaccount.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/user/closeaccount.rs @@ -5,8 +5,8 @@ use crate::{ resource::ResourceType, serializer::{try_acc_close, try_acc_write}, state::{ - device::Device, globalstate::GlobalState, permission::permission_flags, - resource_extension::ResourceExtensionBorrowed, tenant::Tenant, user::*, + device::Device, globalstate::GlobalState, permission::permission_flags, tenant::Tenant, + user::*, }, }; use borsh::BorshSerialize; diff --git a/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs b/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs index b3fab71337..eb6f7969c8 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs @@ -412,6 +412,7 @@ async fn test_create_subscribe_user_publisher() { publisher: true, subscriber: false, tunnel_endpoint: Ipv4Addr::UNSPECIFIED, + dz_prefix_count: 0, }), vec![ AccountMeta::new(user_pubkey, false), @@ -477,6 +478,7 @@ async fn test_create_subscribe_user_subscriber() { publisher: false, subscriber: true, tunnel_endpoint: Ipv4Addr::UNSPECIFIED, + dz_prefix_count: 0, }), vec![ AccountMeta::new(user_pubkey, false), @@ -538,6 +540,7 @@ async fn test_create_subscribe_user_publisher_and_subscriber() { publisher: true, subscriber: true, tunnel_endpoint: Ipv4Addr::UNSPECIFIED, + dz_prefix_count: 0, }), vec![ AccountMeta::new(user_pubkey, false), @@ -643,6 +646,7 @@ async fn test_create_subscribe_user_inactive_mgroup_fails() { publisher: false, subscriber: true, tunnel_endpoint: Ipv4Addr::UNSPECIFIED, + dz_prefix_count: 0, }), vec![ AccountMeta::new(user_pubkey, false), diff --git a/smartcontract/sdk/rs/src/commands/user/create_subscribe.rs b/smartcontract/sdk/rs/src/commands/user/create_subscribe.rs index f879d4e951..32bb4deaff 100644 --- a/smartcontract/sdk/rs/src/commands/user/create_subscribe.rs +++ b/smartcontract/sdk/rs/src/commands/user/create_subscribe.rs @@ -75,6 +75,7 @@ impl CreateSubscribeUserCommand { publisher: self.publisher, subscriber: self.subscriber, tunnel_endpoint: self.tunnel_endpoint, + dz_prefix_count: 0, }), vec![ AccountMeta::new(pda_pubkey, false), From 6826a5c46732edae8674d5c50a957d11d6c3c4e7 Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Fri, 13 Mar 2026 13:49:30 +0000 Subject: [PATCH 5/8] smartcontract: fix requestban tests to use execute_authorized_transaction --- smartcontract/sdk/rs/src/commands/user/requestban.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartcontract/sdk/rs/src/commands/user/requestban.rs b/smartcontract/sdk/rs/src/commands/user/requestban.rs index 36b79dedd1..f9d23c9a9e 100644 --- a/smartcontract/sdk/rs/src/commands/user/requestban.rs +++ b/smartcontract/sdk/rs/src/commands/user/requestban.rs @@ -263,7 +263,7 @@ mod tests { get_resource_extension_pda(&program_id, ResourceType::DzPrefixBlock(device_pk, 0)); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::RequestBanUser(UserRequestBanArgs { dz_prefix_count: 1, @@ -324,7 +324,7 @@ mod tests { .returning(move |_| Ok(AccountData::User(user.clone()))); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::RequestBanUser( UserRequestBanArgs::default(), From b3d9f8d4f2872ecde56b1b6b595c1dee8e17d3d9 Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Fri, 13 Mar 2026 16:39:50 +0000 Subject: [PATCH 6/8] smartcontract: address review feedback on permission enforcement PR - Add execute_authorized_transaction_quiet to DoubleZeroClient trait and DZClient impl, restoring quiet mode for ban and closeaccount commands that was lost when switching to execute_authorized_transaction - Restore onchain allocation support in CreateSubscribeUserCommand SDK command (feature-flag-gated ResourceExtension PDA logic removed in permission enforcement refactor) - Restore atomic path tests and fixture resource extension PDA fields in create_subscribe_user_test.rs --- .../tests/create_subscribe_user_test.rs | 286 +++++++++++++++- smartcontract/sdk/rs/src/client.rs | 15 + smartcontract/sdk/rs/src/commands/user/ban.rs | 25 +- .../sdk/rs/src/commands/user/closeaccount.rs | 19 +- .../rs/src/commands/user/create_subscribe.rs | 312 +++++++++++++++++- smartcontract/sdk/rs/src/doublezeroclient.rs | 8 + 6 files changed, 628 insertions(+), 37 deletions(-) diff --git a/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs b/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs index eb6f7969c8..20d8323f89 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs @@ -1,8 +1,11 @@ //! Integration tests for CreateSubscribeUser instruction. //! //! Tests cover: -//! - User created in Pending status with publisher/subscriber subscription +//! - Legacy path (dz_prefix_count=0): user created in Pending status with subscription +//! - Atomic path (dz_prefix_count>0): user created + allocated + activated with subscription //! - Publisher and subscriber count correctness +//! - Backward compatibility (old args without dz_prefix_count) +//! - Feature flag enforcement for atomic path //! - Invalid multicast group status rejection (graceful error, not panic) use doublezero_serviceability::{ @@ -21,6 +24,7 @@ use doublezero_serviceability::{ }, exchange::create::ExchangeCreateArgs, globalconfig::set::SetGlobalConfigArgs, + globalstate::setfeatureflags::SetFeatureFlagsArgs, location::create::LocationCreateArgs, multicastgroup::{ activate::MulticastGroupActivateArgs, @@ -36,6 +40,7 @@ use doublezero_serviceability::{ state::{ accesspass::AccessPassType, device::DeviceType, + feature_flags::FeatureFlag, user::{UserCYOA, UserStatus, UserType}, }, }; @@ -59,6 +64,11 @@ struct CreateSubscribeFixture { accesspass_pubkey: Pubkey, mgroup_pubkey: Pubkey, user_ip: Ipv4Addr, + // Resource extension PDAs + user_tunnel_block: Pubkey, + multicast_publisher_block: Pubkey, + tunnel_ids: Pubkey, + dz_prefix_block: Pubkey, } /// Setup a complete test environment for CreateSubscribeUser: @@ -374,16 +384,20 @@ async fn setup_create_subscribe_fixture(client_ip: [u8; 4]) -> CreateSubscribeFi accesspass_pubkey, mgroup_pubkey, user_ip, + user_tunnel_block, + multicast_publisher_block, + tunnel_ids, + dz_prefix_block, } } // ============================================================================ -// CreateSubscribeUser Tests +// Legacy Path Tests (dz_prefix_count=0) // ============================================================================ -/// CreateSubscribeUser: user created in Pending status with publisher subscription. +/// Legacy CreateSubscribeUser: user created in Pending status with publisher subscription. #[tokio::test] -async fn test_create_subscribe_user_publisher() { +async fn test_create_subscribe_user_legacy_publisher() { let client_ip = [100, 0, 0, 1]; let f = setup_create_subscribe_fixture(client_ip).await; let CreateSubscribeFixture { @@ -435,7 +449,7 @@ async fn test_create_subscribe_user_publisher() { assert!(user.subscribers.is_empty()); assert_eq!( user.tunnel_id, 0, - "user should not have tunnel_id before activation" + "Legacy path should not allocate tunnel_id" ); let mgroup = get_account_data(&mut banks_client, mgroup_pubkey) @@ -447,9 +461,9 @@ async fn test_create_subscribe_user_publisher() { assert_eq!(mgroup.subscriber_count, 0); } -/// CreateSubscribeUser: user created in Pending status with subscriber subscription. +/// Legacy CreateSubscribeUser: user created in Pending status with subscriber subscription. #[tokio::test] -async fn test_create_subscribe_user_subscriber() { +async fn test_create_subscribe_user_legacy_subscriber() { let client_ip = [100, 0, 0, 2]; let f = setup_create_subscribe_fixture(client_ip).await; let CreateSubscribeFixture { @@ -509,9 +523,9 @@ async fn test_create_subscribe_user_subscriber() { assert_eq!(mgroup.subscriber_count, 1); } -/// CreateSubscribeUser: user created with both publisher and subscriber. +/// Legacy CreateSubscribeUser: user created with both publisher and subscriber. #[tokio::test] -async fn test_create_subscribe_user_publisher_and_subscriber() { +async fn test_create_subscribe_user_legacy_publisher_and_subscriber() { let client_ip = [100, 0, 0, 3]; let f = setup_create_subscribe_fixture(client_ip).await; let CreateSubscribeFixture { @@ -571,10 +585,264 @@ async fn test_create_subscribe_user_publisher_and_subscriber() { assert_eq!(mgroup.subscriber_count, 1); } +// ============================================================================ +// Atomic Path Tests (dz_prefix_count > 0) +// ============================================================================ + +/// Atomic CreateSubscribeUser with publisher: user created + allocated + activated. +#[tokio::test] +async fn test_create_subscribe_user_atomic_publisher() { + let client_ip = [100, 0, 0, 4]; + let f = setup_create_subscribe_fixture(client_ip).await; + let CreateSubscribeFixture { + mut banks_client, + payer, + program_id, + globalstate_pubkey, + device_pubkey, + accesspass_pubkey, + mgroup_pubkey, + user_ip, + user_tunnel_block, + multicast_publisher_block, + tunnel_ids, + dz_prefix_block, + .. + } = f; + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + + // Enable feature flag + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::SetFeatureFlags(SetFeatureFlagsArgs { + feature_flags: FeatureFlag::OnChainAllocation.to_mask(), + }), + vec![AccountMeta::new(globalstate_pubkey, false)], + &payer, + ) + .await; + + let (user_pubkey, _) = get_user_pda(&program_id, &user_ip, UserType::Multicast); + + // Atomic CreateSubscribeUser with resource extensions + // Account layout: [user, device, mgroup, accesspass, globalstate, user_tunnel_block, multicast_publisher_block, tunnel_ids, dz_prefix_0, payer, system] + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateSubscribeUser(UserCreateSubscribeArgs { + user_type: UserType::Multicast, + cyoa_type: UserCYOA::GREOverDIA, + client_ip: user_ip, + publisher: true, + subscriber: false, + tunnel_endpoint: Ipv4Addr::UNSPECIFIED, + dz_prefix_count: 1, + }), + vec![ + AccountMeta::new(user_pubkey, false), + AccountMeta::new(device_pubkey, false), + AccountMeta::new(mgroup_pubkey, false), + AccountMeta::new(accesspass_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(user_tunnel_block, false), + AccountMeta::new(multicast_publisher_block, false), + AccountMeta::new(tunnel_ids, false), + AccountMeta::new(dz_prefix_block, false), + ], + &payer, + ) + .await; + + let user = get_account_data(&mut banks_client, user_pubkey) + .await + .expect("User should exist") + .get_user() + .unwrap(); + assert_eq!(user.status, UserStatus::Activated); + assert_eq!(user.publishers, vec![mgroup_pubkey]); + assert!(user.subscribers.is_empty()); + assert_ne!(user.tunnel_id, 0, "tunnel_id should be allocated"); + assert_ne!( + user.tunnel_net, + doublezero_program_common::types::NetworkV4::default(), + "tunnel_net should be allocated" + ); + // Multicast publisher gets dz_ip from MulticastPublisherBlock + assert_ne!( + user.dz_ip, + Ipv4Addr::from(client_ip), + "Multicast publisher dz_ip should be allocated, not client_ip" + ); + + let mgroup = get_account_data(&mut banks_client, mgroup_pubkey) + .await + .expect("MulticastGroup should exist") + .get_multicastgroup() + .unwrap(); + assert_eq!(mgroup.publisher_count, 1); + assert_eq!(mgroup.subscriber_count, 0); +} + +/// Atomic CreateSubscribeUser with subscriber only: dz_ip = client_ip (no publisher allocation). +#[tokio::test] +async fn test_create_subscribe_user_atomic_subscriber() { + let client_ip = [100, 0, 0, 5]; + let f = setup_create_subscribe_fixture(client_ip).await; + let CreateSubscribeFixture { + mut banks_client, + payer, + program_id, + globalstate_pubkey, + device_pubkey, + accesspass_pubkey, + mgroup_pubkey, + user_ip, + user_tunnel_block, + multicast_publisher_block, + tunnel_ids, + dz_prefix_block, + .. + } = f; + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + + // Enable feature flag + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::SetFeatureFlags(SetFeatureFlagsArgs { + feature_flags: FeatureFlag::OnChainAllocation.to_mask(), + }), + vec![AccountMeta::new(globalstate_pubkey, false)], + &payer, + ) + .await; + + let (user_pubkey, _) = get_user_pda(&program_id, &user_ip, UserType::Multicast); + + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateSubscribeUser(UserCreateSubscribeArgs { + user_type: UserType::Multicast, + cyoa_type: UserCYOA::GREOverDIA, + client_ip: user_ip, + publisher: false, + subscriber: true, + tunnel_endpoint: Ipv4Addr::UNSPECIFIED, + dz_prefix_count: 1, + }), + vec![ + AccountMeta::new(user_pubkey, false), + AccountMeta::new(device_pubkey, false), + AccountMeta::new(mgroup_pubkey, false), + AccountMeta::new(accesspass_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(user_tunnel_block, false), + AccountMeta::new(multicast_publisher_block, false), + AccountMeta::new(tunnel_ids, false), + AccountMeta::new(dz_prefix_block, false), + ], + &payer, + ) + .await; + + let user = get_account_data(&mut banks_client, user_pubkey) + .await + .expect("User should exist") + .get_user() + .unwrap(); + assert_eq!(user.status, UserStatus::Activated); + assert!(user.publishers.is_empty()); + assert_eq!(user.subscribers, vec![mgroup_pubkey]); + assert_ne!(user.tunnel_id, 0, "tunnel_id should be allocated"); + // Multicast subscriber (no publishers) gets dz_ip = client_ip + assert_eq!( + user.dz_ip, + Ipv4Addr::from(client_ip), + "Subscriber-only multicast user should get dz_ip = client_ip" + ); + + let mgroup = get_account_data(&mut banks_client, mgroup_pubkey) + .await + .expect("MulticastGroup should exist") + .get_multicastgroup() + .unwrap(); + assert_eq!(mgroup.publisher_count, 0); + assert_eq!(mgroup.subscriber_count, 1); +} + // ============================================================================ // Error Path Tests // ============================================================================ +/// Atomic CreateSubscribeUser fails when feature flag is disabled. +#[tokio::test] +async fn test_create_subscribe_user_atomic_feature_flag_disabled() { + let client_ip = [100, 0, 0, 6]; + let f = setup_create_subscribe_fixture(client_ip).await; + let CreateSubscribeFixture { + mut banks_client, + payer, + program_id, + globalstate_pubkey, + device_pubkey, + accesspass_pubkey, + mgroup_pubkey, + user_ip, + user_tunnel_block, + multicast_publisher_block, + tunnel_ids, + dz_prefix_block, + .. + } = f; + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + + let (user_pubkey, _) = get_user_pda(&program_id, &user_ip, UserType::Multicast); + + // Feature flag NOT enabled — atomic create should fail + let result = execute_transaction_expect_failure( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateSubscribeUser(UserCreateSubscribeArgs { + user_type: UserType::Multicast, + cyoa_type: UserCYOA::GREOverDIA, + client_ip: user_ip, + publisher: true, + subscriber: false, + tunnel_endpoint: Ipv4Addr::UNSPECIFIED, + dz_prefix_count: 1, + }), + vec![ + AccountMeta::new(user_pubkey, false), + AccountMeta::new(device_pubkey, false), + AccountMeta::new(mgroup_pubkey, false), + AccountMeta::new(accesspass_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(user_tunnel_block, false), + AccountMeta::new(multicast_publisher_block, false), + AccountMeta::new(tunnel_ids, false), + AccountMeta::new(dz_prefix_block, false), + ], + &payer, + ) + .await; + + assert!(result.is_err(), "Should fail when feature flag is disabled"); + + // Verify user account was NOT created + let user_data = get_account_data(&mut banks_client, user_pubkey).await; + assert!(user_data.is_none(), "User account should not exist"); +} + /// CreateSubscribeUser fails when multicast group is not activated (graceful error, not panic). #[tokio::test] async fn test_create_subscribe_user_inactive_mgroup_fails() { diff --git a/smartcontract/sdk/rs/src/client.rs b/smartcontract/sdk/rs/src/client.rs index 07532d3084..10899abd47 100644 --- a/smartcontract/sdk/rs/src/client.rs +++ b/smartcontract/sdk/rs/src/client.rs @@ -530,6 +530,21 @@ impl DoubleZeroClient for DZClient { self.build_and_send(instruction, accounts, true) } + fn execute_authorized_transaction_quiet( + &self, + instruction: DoubleZeroInstruction, + accounts: Vec, + ) -> eyre::Result { + let mut accounts = accounts; + if let Some(payer) = self.payer.as_ref() { + let (permission_pda, _) = get_permission_pda(&self.program_id, &payer.pubkey()); + if self.client.get_account(&permission_pda).is_ok() { + accounts.push(AccountMeta::new_readonly(permission_pda, false)); + } + } + self.execute_transaction_inner(instruction, accounts, true) + } + fn gets(&self, account_type: AccountType) -> eyre::Result> { let account_type = account_type as u8; let filters = vec![RpcFilterType::Memcmp(Memcmp::new( diff --git a/smartcontract/sdk/rs/src/commands/user/ban.rs b/smartcontract/sdk/rs/src/commands/user/ban.rs index 4418b85455..1753662c97 100644 --- a/smartcontract/sdk/rs/src/commands/user/ban.rs +++ b/smartcontract/sdk/rs/src/commands/user/ban.rs @@ -22,18 +22,27 @@ impl BanUserCommand { fn execute_inner( &self, client: &dyn DoubleZeroClient, - _quiet: bool, + quiet: bool, ) -> eyre::Result { let (globalstate_pubkey, _globalstate) = GetGlobalStateCommand .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; - client.execute_authorized_transaction( - DoubleZeroInstruction::BanUser(UserBanArgs {}), - vec![ - AccountMeta::new(self.pubkey, false), - AccountMeta::new(globalstate_pubkey, false), - ], - ) + let accounts = vec![ + AccountMeta::new(self.pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + ]; + + if quiet { + client.execute_authorized_transaction_quiet( + DoubleZeroInstruction::BanUser(UserBanArgs {}), + accounts, + ) + } else { + client.execute_authorized_transaction( + DoubleZeroInstruction::BanUser(UserBanArgs {}), + accounts, + ) + } } } diff --git a/smartcontract/sdk/rs/src/commands/user/closeaccount.rs b/smartcontract/sdk/rs/src/commands/user/closeaccount.rs index 757687ec7f..25bb58f9f9 100644 --- a/smartcontract/sdk/rs/src/commands/user/closeaccount.rs +++ b/smartcontract/sdk/rs/src/commands/user/closeaccount.rs @@ -32,7 +32,7 @@ impl CloseAccountUserCommand { fn execute_inner( &self, client: &dyn DoubleZeroClient, - _quiet: bool, + quiet: bool, ) -> eyre::Result { let (globalstate_pubkey, _globalstate) = GetGlobalStateCommand .execute(client) @@ -117,13 +117,16 @@ impl CloseAccountUserCommand { accounts.push(AccountMeta::new(user.tenant_pk, false)); } - client.execute_authorized_transaction( - DoubleZeroInstruction::CloseAccountUser(UserCloseAccountArgs { - dz_prefix_count, - multicast_publisher_count, - }), - accounts, - ) + let instruction = DoubleZeroInstruction::CloseAccountUser(UserCloseAccountArgs { + dz_prefix_count, + multicast_publisher_count, + }); + + if quiet { + client.execute_authorized_transaction_quiet(instruction, accounts) + } else { + client.execute_authorized_transaction(instruction, accounts) + } } } diff --git a/smartcontract/sdk/rs/src/commands/user/create_subscribe.rs b/smartcontract/sdk/rs/src/commands/user/create_subscribe.rs index 32bb4deaff..4b6a373412 100644 --- a/smartcontract/sdk/rs/src/commands/user/create_subscribe.rs +++ b/smartcontract/sdk/rs/src/commands/user/create_subscribe.rs @@ -1,8 +1,10 @@ use doublezero_serviceability::{ instructions::DoubleZeroInstruction, - pda::get_user_pda, + pda::{get_resource_extension_pda, get_user_pda}, processors::user::create_subscribe::UserCreateSubscribeArgs, + resource::ResourceType, state::{ + feature_flags::{is_feature_enabled, FeatureFlag}, multicastgroup::MulticastGroupStatus, user::{UserCYOA, UserType}, }, @@ -12,8 +14,8 @@ use std::net::Ipv4Addr; use crate::{ commands::{ - accesspass::get::GetAccessPassCommand, globalstate::get::GetGlobalStateCommand, - multicastgroup::get::GetMulticastGroupCommand, + accesspass::get::GetAccessPassCommand, device::get::GetDeviceCommand, + globalstate::get::GetGlobalStateCommand, multicastgroup::get::GetMulticastGroupCommand, }, DoubleZeroClient, }; @@ -32,10 +34,13 @@ pub struct CreateSubscribeUserCommand { impl CreateSubscribeUserCommand { pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result<(Signature, Pubkey)> { - let (globalstate_pubkey, _globalstate) = GetGlobalStateCommand + let (globalstate_pubkey, globalstate) = GetGlobalStateCommand .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; + let use_onchain_allocation = + is_feature_enabled(globalstate.feature_flags, FeatureFlag::OnChainAllocation); + let (_, mgroup) = GetMulticastGroupCommand { pubkey_or_code: self.mgroup_pk.to_string(), } @@ -66,8 +71,53 @@ impl CreateSubscribeUserCommand { let (pda_pubkey, _) = get_user_pda(&client.get_program_id(), &self.client_ip, self.user_type); + let mut accounts = vec![ + AccountMeta::new(pda_pubkey, false), + AccountMeta::new(self.device_pk, false), + AccountMeta::new(self.mgroup_pk, false), + AccountMeta::new(accesspass_pk, false), + AccountMeta::new(globalstate_pubkey, false), + ]; + + let dz_prefix_count: u8 = if use_onchain_allocation { + let (_, device) = GetDeviceCommand { + pubkey_or_code: self.device_pk.to_string(), + } + .execute(client) + .map_err(|_| eyre::eyre!("Device not found"))?; + + let count = device.dz_prefixes.len(); + + let (user_tunnel_block_ext, _, _) = + get_resource_extension_pda(&client.get_program_id(), ResourceType::UserTunnelBlock); + let (multicast_publisher_block_ext, _, _) = get_resource_extension_pda( + &client.get_program_id(), + ResourceType::MulticastPublisherBlock, + ); + let (device_tunnel_ids_ext, _, _) = get_resource_extension_pda( + &client.get_program_id(), + ResourceType::TunnelIds(self.device_pk, 0), + ); + + accounts.push(AccountMeta::new(user_tunnel_block_ext, false)); + accounts.push(AccountMeta::new(multicast_publisher_block_ext, false)); + accounts.push(AccountMeta::new(device_tunnel_ids_ext, false)); + + for idx in 0..count { + let (dz_prefix_ext, _, _) = get_resource_extension_pda( + &client.get_program_id(), + ResourceType::DzPrefixBlock(self.device_pk, idx), + ); + accounts.push(AccountMeta::new(dz_prefix_ext, false)); + } + + count as u8 + } else { + 0 + }; + client - .execute_transaction( + .execute_authorized_transaction( DoubleZeroInstruction::CreateSubscribeUser(UserCreateSubscribeArgs { user_type: self.user_type, cyoa_type: self.cyoa_type, @@ -75,16 +125,254 @@ impl CreateSubscribeUserCommand { publisher: self.publisher, subscriber: self.subscriber, tunnel_endpoint: self.tunnel_endpoint, - dz_prefix_count: 0, + dz_prefix_count, }), - vec![ + accounts, + ) + .map(|sig| (sig, pda_pubkey)) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + commands::user::create_subscribe::CreateSubscribeUserCommand, + tests::utils::create_test_client, DoubleZeroClient, MockDoubleZeroClient, + }; + use doublezero_serviceability::{ + instructions::DoubleZeroInstruction, + pda::{get_accesspass_pda, get_globalstate_pda, get_resource_extension_pda, get_user_pda}, + processors::user::create_subscribe::UserCreateSubscribeArgs, + resource::ResourceType, + state::{ + accesspass::{AccessPass, AccessPassStatus, AccessPassType}, + accountdata::AccountData, + accounttype::AccountType, + device::Device, + feature_flags::FeatureFlag, + globalstate::GlobalState, + multicastgroup::{MulticastGroup, MulticastGroupStatus}, + user::{UserCYOA, UserType}, + }, + }; + use mockall::predicate; + use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, signature::Signature}; + use std::net::Ipv4Addr; + + #[test] + fn test_commands_user_create_subscribe_legacy() { + let mut client = create_test_client(); + + let program_id = client.get_program_id(); + let (globalstate_pubkey, _) = get_globalstate_pda(&program_id); + let device_pk = Pubkey::new_unique(); + let mgroup_pk = Pubkey::new_unique(); + let client_ip = Ipv4Addr::new(192, 168, 1, 10); + + let (pda_pubkey, _) = get_user_pda(&program_id, &client_ip, UserType::IBRLWithAllocatedIP); + + // Mock MulticastGroup + let mgroup = MulticastGroup { + status: MulticastGroupStatus::Activated, + ..Default::default() + }; + client + .expect_get() + .with(predicate::eq(mgroup_pk)) + .returning(move |_| Ok(AccountData::MulticastGroup(mgroup.clone()))); + + // Mock AccessPass + let (accesspass_pubkey, _) = + get_accesspass_pda(&program_id, &client_ip, &client.get_payer()); + let accesspass = AccessPass { + account_type: AccountType::AccessPass, + bump_seed: 0, + accesspass_type: AccessPassType::Prepaid, + client_ip, + user_payer: client.get_payer(), + last_access_epoch: 0, + connection_count: 0, + status: AccessPassStatus::Requested, + owner: client.get_payer(), + mgroup_pub_allowlist: vec![], + mgroup_sub_allowlist: vec![], + tenant_allowlist: vec![], + flags: 0, + }; + client + .expect_get() + .with(predicate::eq(accesspass_pubkey)) + .returning(move |_| Ok(AccountData::AccessPass(accesspass.clone()))); + + client + .expect_execute_authorized_transaction() + .with( + predicate::eq(DoubleZeroInstruction::CreateSubscribeUser( + UserCreateSubscribeArgs { + user_type: UserType::IBRLWithAllocatedIP, + cyoa_type: UserCYOA::GREOverDIA, + client_ip, + publisher: true, + subscriber: false, + tunnel_endpoint: Ipv4Addr::UNSPECIFIED, + dz_prefix_count: 0, + }, + )), + predicate::eq(vec![ AccountMeta::new(pda_pubkey, false), - AccountMeta::new(self.device_pk, false), - AccountMeta::new(self.mgroup_pk, false), - AccountMeta::new(accesspass_pk, false), + AccountMeta::new(device_pk, false), + AccountMeta::new(mgroup_pk, false), + AccountMeta::new(accesspass_pubkey, false), AccountMeta::new(globalstate_pubkey, false), - ], + ]), ) - .map(|sig| (sig, pda_pubkey)) + .returning(|_, _| Ok(Signature::new_unique())); + + let res = CreateSubscribeUserCommand { + user_type: UserType::IBRLWithAllocatedIP, + device_pk, + cyoa_type: UserCYOA::GREOverDIA, + client_ip, + mgroup_pk, + publisher: true, + subscriber: false, + tunnel_endpoint: Ipv4Addr::UNSPECIFIED, + } + .execute(&client); + + assert!(res.is_ok()); + } + + #[test] + fn test_commands_user_create_subscribe_with_onchain_allocation() { + let mut client = MockDoubleZeroClient::new(); + + let payer = Pubkey::new_unique(); + client.expect_get_payer().returning(move || payer); + let program_id = Pubkey::new_unique(); + client.expect_get_program_id().returning(move || program_id); + + let (globalstate_pubkey, bump_seed) = get_globalstate_pda(&program_id); + let globalstate = GlobalState { + account_type: AccountType::GlobalState, + bump_seed, + account_index: 0, + foundation_allowlist: vec![], + _device_allowlist: vec![], + _user_allowlist: vec![], + activator_authority_pk: Pubkey::new_unique(), + sentinel_authority_pk: Pubkey::new_unique(), + contributor_airdrop_lamports: 1_000_000_000, + user_airdrop_lamports: 40_000, + health_oracle_pk: Pubkey::new_unique(), + qa_allowlist: vec![], + feature_flags: FeatureFlag::OnChainAllocation.to_mask(), + reservation_authority_pk: Pubkey::default(), + }; + client + .expect_get() + .with(predicate::eq(globalstate_pubkey)) + .returning(move |_| Ok(AccountData::GlobalState(globalstate.clone()))); + + let device_pk = Pubkey::new_unique(); + let mgroup_pk = Pubkey::new_unique(); + let client_ip = Ipv4Addr::new(192, 168, 1, 10); + + let (pda_pubkey, _) = get_user_pda(&program_id, &client_ip, UserType::IBRLWithAllocatedIP); + + // Mock MulticastGroup + let mgroup = MulticastGroup { + status: MulticastGroupStatus::Activated, + ..Default::default() + }; + client + .expect_get() + .with(predicate::eq(mgroup_pk)) + .returning(move |_| Ok(AccountData::MulticastGroup(mgroup.clone()))); + + // Mock AccessPass + let (accesspass_pubkey, _) = get_accesspass_pda(&program_id, &client_ip, &payer); + let accesspass = AccessPass { + account_type: AccountType::AccessPass, + bump_seed: 0, + accesspass_type: AccessPassType::Prepaid, + client_ip, + user_payer: payer, + last_access_epoch: 0, + connection_count: 0, + status: AccessPassStatus::Requested, + owner: payer, + mgroup_pub_allowlist: vec![], + mgroup_sub_allowlist: vec![], + tenant_allowlist: vec![], + flags: 0, + }; + client + .expect_get() + .with(predicate::eq(accesspass_pubkey)) + .returning(move |_| Ok(AccountData::AccessPass(accesspass.clone()))); + + // Mock Device fetch (for dz_prefixes.len()) + let device = Device { + account_type: AccountType::Device, + dz_prefixes: "10.0.0.0/24".parse().unwrap(), + ..Default::default() + }; + client + .expect_get() + .with(predicate::eq(device_pk)) + .returning(move |_| Ok(AccountData::Device(device.clone()))); + + // Compute ResourceExtension PDAs + let (user_tunnel_block_ext, _, _) = + get_resource_extension_pda(&program_id, ResourceType::UserTunnelBlock); + let (multicast_publisher_block_ext, _, _) = + get_resource_extension_pda(&program_id, ResourceType::MulticastPublisherBlock); + let (device_tunnel_ids_ext, _, _) = + get_resource_extension_pda(&program_id, ResourceType::TunnelIds(device_pk, 0)); + let (dz_prefix_ext, _, _) = + get_resource_extension_pda(&program_id, ResourceType::DzPrefixBlock(device_pk, 0)); + + client + .expect_execute_authorized_transaction() + .with( + predicate::eq(DoubleZeroInstruction::CreateSubscribeUser( + UserCreateSubscribeArgs { + user_type: UserType::IBRLWithAllocatedIP, + cyoa_type: UserCYOA::GREOverDIA, + client_ip, + publisher: true, + subscriber: false, + tunnel_endpoint: Ipv4Addr::UNSPECIFIED, + dz_prefix_count: 1, + }, + )), + predicate::eq(vec![ + AccountMeta::new(pda_pubkey, false), + AccountMeta::new(device_pk, false), + AccountMeta::new(mgroup_pk, false), + AccountMeta::new(accesspass_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(user_tunnel_block_ext, false), + AccountMeta::new(multicast_publisher_block_ext, false), + AccountMeta::new(device_tunnel_ids_ext, false), + AccountMeta::new(dz_prefix_ext, false), + ]), + ) + .returning(|_, _| Ok(Signature::new_unique())); + + let res = CreateSubscribeUserCommand { + user_type: UserType::IBRLWithAllocatedIP, + device_pk, + cyoa_type: UserCYOA::GREOverDIA, + client_ip, + mgroup_pk, + publisher: true, + subscriber: false, + tunnel_endpoint: Ipv4Addr::UNSPECIFIED, + } + .execute(&client); + + assert!(res.is_ok()); } } diff --git a/smartcontract/sdk/rs/src/doublezeroclient.rs b/smartcontract/sdk/rs/src/doublezeroclient.rs index f778e40ece..8724e61428 100644 --- a/smartcontract/sdk/rs/src/doublezeroclient.rs +++ b/smartcontract/sdk/rs/src/doublezeroclient.rs @@ -59,6 +59,14 @@ pub trait DoubleZeroClient { accounts: Vec, ) -> eyre::Result; + /// Like `execute_authorized_transaction`, but suppresses program log output on simulation failure. + /// Use this for authorized transactions where simulation failures are expected (e.g., race conditions). + fn execute_authorized_transaction_quiet( + &self, + instruction: DoubleZeroInstruction, + accounts: Vec, + ) -> eyre::Result; + fn get_transactions(&self, pubkey: Pubkey) -> eyre::Result>; } From bd6e501a305ec5b178340888d3fc1f0d3f945ba1 Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Sat, 14 Mar 2026 17:10:46 +0000 Subject: [PATCH 7/8] activator: fix mock expectations to use execute_authorized_transaction_quiet --- activator/src/process/user.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/activator/src/process/user.rs b/activator/src/process/user.rs index 3dae97fbb7..53c678b3fc 100644 --- a/activator/src/process/user.rs +++ b/activator/src/process/user.rs @@ -1775,7 +1775,7 @@ mod tests { UserStatus::Deleting, |user_service, _, seq| { user_service - .expect_execute_authorized_transaction() + .expect_execute_authorized_transaction_quiet() .times(1) .in_sequence(seq) .with( @@ -1799,7 +1799,7 @@ mod tests { UserStatus::PendingBan, |user_service, _, seq| { user_service - .expect_execute_authorized_transaction() + .expect_execute_authorized_transaction_quiet() .times(1) .in_sequence(seq) .with( @@ -2853,7 +2853,7 @@ mod tests { // Stateless mode: use_onchain_deallocation=true client - .expect_execute_authorized_transaction() + .expect_execute_authorized_transaction_quiet() .times(1) .in_sequence(&mut seq) .with( @@ -2959,7 +2959,7 @@ mod tests { .returning(move |_| Ok(AccountData::User(user2.clone()))); client - .expect_execute_authorized_transaction() + .expect_execute_authorized_transaction_quiet() .times(1) .in_sequence(&mut seq) .with( From 5504a1f10f8d26fb0d8f21a7d98515322482c014 Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Sat, 14 Mar 2026 17:42:25 +0000 Subject: [PATCH 8/8] sdk: rustfmt --- smartcontract/sdk/rs/src/commands/user/ban.rs | 6 +----- smartcontract/sdk/rs/src/commands/user/closeaccount.rs | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/smartcontract/sdk/rs/src/commands/user/ban.rs b/smartcontract/sdk/rs/src/commands/user/ban.rs index 1753662c97..93cd5101ce 100644 --- a/smartcontract/sdk/rs/src/commands/user/ban.rs +++ b/smartcontract/sdk/rs/src/commands/user/ban.rs @@ -19,11 +19,7 @@ impl BanUserCommand { self.execute_inner(client, true) } - fn execute_inner( - &self, - client: &dyn DoubleZeroClient, - quiet: bool, - ) -> eyre::Result { + fn execute_inner(&self, client: &dyn DoubleZeroClient, quiet: bool) -> eyre::Result { let (globalstate_pubkey, _globalstate) = GetGlobalStateCommand .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; diff --git a/smartcontract/sdk/rs/src/commands/user/closeaccount.rs b/smartcontract/sdk/rs/src/commands/user/closeaccount.rs index 25bb58f9f9..21ab072a44 100644 --- a/smartcontract/sdk/rs/src/commands/user/closeaccount.rs +++ b/smartcontract/sdk/rs/src/commands/user/closeaccount.rs @@ -29,11 +29,7 @@ impl CloseAccountUserCommand { self.execute_inner(client, true) } - fn execute_inner( - &self, - client: &dyn DoubleZeroClient, - quiet: bool, - ) -> eyre::Result { + fn execute_inner(&self, client: &dyn DoubleZeroClient, quiet: bool) -> eyre::Result { let (globalstate_pubkey, _globalstate) = GetGlobalStateCommand .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?;