From f95f3026677a63b66bd6561ebef48a2ceaa2c269 Mon Sep 17 00:00:00 2001 From: Martin Sander Date: Mon, 30 Mar 2026 23:26:48 -0500 Subject: [PATCH] smartcontract: add Index account for multicast group code uniqueness Introduce an Index account (PDA derived from entity type + lowercased code) that enforces unique multicast group codes onchain and enables O(1) code-to-pubkey lookup. Integrate Index lifecycle into multicast group create/update/delete/close instructions. Add standalone CreateIndex/DeleteIndex instructions (variants 104/105) for migration backfill. Update Go, Python, and TypeScript SDKs with Index account type. --- CHANGELOG.md | 2 + activator/src/process/multicastgroup.rs | 11 +- e2e/internal/qa/provisioning.go | 1 - sdk/serviceability/go/state.go | 1 + .../python/serviceability/state.py | 1 + .../typescript/serviceability/state.ts | 1 + .../src/entrypoint.rs | 7 + .../src/instructions.rs | 16 + .../doublezero-serviceability/src/pda.rs | 17 +- .../src/processors/index/create.rs | 118 ++++++ .../src/processors/index/delete.rs | 69 ++++ .../src/processors/index/mod.rs | 2 + .../src/processors/mod.rs | 1 + .../processors/multicastgroup/closeaccount.rs | 46 ++- .../src/processors/multicastgroup/create.rs | 50 ++- .../src/processors/multicastgroup/delete.rs | 48 ++- .../src/processors/multicastgroup/update.rs | 106 ++++- .../doublezero-serviceability/src/seeds.rs | 1 + .../src/state/accountdata.rs | 16 +- .../src/state/accounttype.rs | 3 + .../src/state/index.rs | 129 ++++++ .../src/state/mod.rs | 1 + .../tests/create_subscribe_user_test.rs | 24 +- .../tests/index_test.rs | 367 ++++++++++++++++++ ...multicastgroup_allowlist_publisher_test.rs | 13 +- ...multicastgroup_allowlist_subcriber_test.rs | 19 +- .../multicastgroup_onchain_allocation_test.rs | 76 +++- .../tests/multicastgroup_subscribe_test.rs | 13 +- .../tests/multicastgroup_test.rs | 100 ++++- .../tests/test_helpers.rs | 92 +++++ .../tests/user_onchain_allocation_test.rs | 34 +- 31 files changed, 1304 insertions(+), 81 deletions(-) create mode 100644 smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs create mode 100644 smartcontract/programs/doublezero-serviceability/src/processors/index/delete.rs create mode 100644 smartcontract/programs/doublezero-serviceability/src/processors/index/mod.rs create mode 100644 smartcontract/programs/doublezero-serviceability/src/state/index.rs create mode 100644 smartcontract/programs/doublezero-serviceability/tests/index_test.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 999ff521f4..15eb129b3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. - CLI - Allow incremental multicast group addition without disconnecting - Reset SIGPIPE to SIG_DFL at the start of main() in all 3 CLI binaries (doublezero, doublezero-geolocation, doublezero-admin) so the process exits silently like standard CLI tools +- Smartcontract + - Add Index account for multicast group code uniqueness — PDA derived from entity type + lowercased code enforces unique codes onchain and enables O(1) code-to-pubkey lookup - SDK - Add Go SDK for shred subscription program with read-only account deserialization (epoch state, seat assignments, pricing, settlement, validator client rewards), PDA derivation helpers, RPC fetchers, compatibility tests, and a fetch example CLI - Tools diff --git a/activator/src/process/multicastgroup.rs b/activator/src/process/multicastgroup.rs index b00371b54b..0633fd1275 100644 --- a/activator/src/process/multicastgroup.rs +++ b/activator/src/process/multicastgroup.rs @@ -398,13 +398,22 @@ mod tests { // Insert it first so it can be removed multicastgroups.insert(pubkey, multicastgroup.clone()); - // Stateless mode: use_onchain_deallocation=true + // Mock get() for DeactivateMulticastGroupCommand which fetches the + // multicast group to derive the Index PDA + let mgroup_for_get = multicastgroup.clone(); + client + .expect_get() + .with(predicate::eq(pubkey)) + .returning(move |_| Ok(AccountData::MulticastGroup(mgroup_for_get.clone()))); + + // Stateless mode: use_onchain_deallocation=true, close_index=true client .expect_execute_transaction() .with( predicate::eq(DoubleZeroInstruction::DeactivateMulticastGroup( MulticastGroupDeactivateArgs { use_onchain_deallocation: true, + close_index: true, }, )), predicate::always(), diff --git a/e2e/internal/qa/provisioning.go b/e2e/internal/qa/provisioning.go index b184030edd..db7bb31c2a 100644 --- a/e2e/internal/qa/provisioning.go +++ b/e2e/internal/qa/provisioning.go @@ -600,4 +600,3 @@ func formatBandwidth(bps uint64) string { } return fmt.Sprintf("%d bps", bps) } - diff --git a/sdk/serviceability/go/state.go b/sdk/serviceability/go/state.go index da14a688ff..3c58bbd058 100644 --- a/sdk/serviceability/go/state.go +++ b/sdk/serviceability/go/state.go @@ -25,6 +25,7 @@ const ( ResourceExtensionType AccountType = 12 TenantType AccountType = 13 PermissionType AccountType = 15 + IndexType AccountType = 16 ) type LocationStatus uint8 diff --git a/sdk/serviceability/python/serviceability/state.py b/sdk/serviceability/python/serviceability/state.py index 705799db5f..0a37f720fa 100644 --- a/sdk/serviceability/python/serviceability/state.py +++ b/sdk/serviceability/python/serviceability/state.py @@ -42,6 +42,7 @@ class AccountTypeEnum(IntEnum): ACCESS_PASS = 11 TENANT = 13 PERMISSION = 15 + INDEX = 16 # --------------------------------------------------------------------------- diff --git a/sdk/serviceability/typescript/serviceability/state.ts b/sdk/serviceability/typescript/serviceability/state.ts index d612b8dd56..d9ad47da24 100644 --- a/sdk/serviceability/typescript/serviceability/state.ts +++ b/sdk/serviceability/typescript/serviceability/state.ts @@ -32,6 +32,7 @@ export const ACCOUNT_TYPE_CONTRIBUTOR = 10; export const ACCOUNT_TYPE_ACCESS_PASS = 11; export const ACCOUNT_TYPE_TENANT = 13; export const ACCOUNT_TYPE_PERMISSION = 15; +export const ACCOUNT_TYPE_INDEX = 16; // --------------------------------------------------------------------------- // Enum string mappings diff --git a/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs b/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs index 02760eeacc..c7833b45ad 100644 --- a/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs +++ b/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs @@ -47,6 +47,7 @@ use crate::{ setauthority::process_set_authority, setfeatureflags::process_set_feature_flags, setversion::process_set_version, }, + index::{create::process_create_index, delete::process_delete_index}, link::{ accept::process_accept_link, activate::process_activate_link, closeaccount::process_closeaccount_link, create::process_create_link, @@ -421,6 +422,12 @@ pub fn process_instruction( DoubleZeroInstruction::DeletePermission(value) => { process_delete_permission(program_id, accounts, &value)? } + DoubleZeroInstruction::CreateIndex(value) => { + process_create_index(program_id, accounts, &value)? + } + DoubleZeroInstruction::DeleteIndex(value) => { + process_delete_index(program_id, accounts, &value)? + } }; Ok(()) } diff --git a/smartcontract/programs/doublezero-serviceability/src/instructions.rs b/smartcontract/programs/doublezero-serviceability/src/instructions.rs index e29ff35c7e..150f622bb6 100644 --- a/smartcontract/programs/doublezero-serviceability/src/instructions.rs +++ b/smartcontract/programs/doublezero-serviceability/src/instructions.rs @@ -35,6 +35,7 @@ use crate::processors::{ setairdrop::SetAirdropArgs, setauthority::SetAuthorityArgs, setfeatureflags::SetFeatureFlagsArgs, setversion::SetVersionArgs, }, + index::{create::IndexCreateArgs, delete::IndexDeleteArgs}, link::{ accept::LinkAcceptArgs, activate::LinkActivateArgs, closeaccount::LinkCloseAccountArgs, create::LinkCreateArgs, delete::LinkDeleteArgs, reject::LinkRejectArgs, @@ -218,6 +219,9 @@ pub enum DoubleZeroInstruction { Deprecated102(), // variant 102 (was CreateReservedSubscribeUser) Deprecated103(), // variant 103 (was DeleteReservedSubscribeUser) + + CreateIndex(IndexCreateArgs), // variant 104 + DeleteIndex(IndexDeleteArgs), // variant 105 } impl DoubleZeroInstruction { @@ -350,6 +354,9 @@ impl DoubleZeroInstruction { 101 => Ok(Self::DeletePermission(PermissionDeleteArgs::try_from(rest).unwrap())), + 104 => Ok(Self::CreateIndex(IndexCreateArgs::try_from(rest).unwrap())), + 105 => Ok(Self::DeleteIndex(IndexDeleteArgs::try_from(rest).unwrap())), + _ => Err(ProgramError::InvalidInstructionData), } } @@ -483,6 +490,9 @@ impl DoubleZeroInstruction { Self::Deprecated102() => "Deprecated102".to_string(), Self::Deprecated103() => "Deprecated103".to_string(), + + Self::CreateIndex(_) => "CreateIndex".to_string(), // variant 104 + Self::DeleteIndex(_) => "DeleteIndex".to_string(), // variant 105 } } @@ -609,6 +619,9 @@ impl DoubleZeroInstruction { Self::Deprecated102() => String::new(), Self::Deprecated103() => String::new(), + + Self::CreateIndex(args) => format!("{args:?}"), // variant 104 + Self::DeleteIndex(args) => format!("{args:?}"), // variant 105 } } } @@ -975,6 +988,7 @@ mod tests { publisher_count: None, subscriber_count: None, use_onchain_allocation: false, + rename_index: false, }), "UpdateMulticastGroup", ); @@ -992,6 +1006,7 @@ mod tests { test_instruction( DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, + close_index: false, }), "DeleteMulticastGroup", ); @@ -999,6 +1014,7 @@ mod tests { test_instruction( DoubleZeroInstruction::DeactivateMulticastGroup(MulticastGroupDeactivateArgs { use_onchain_deallocation: false, + close_index: false, }), "DeactivateMulticastGroup", ); diff --git a/smartcontract/programs/doublezero-serviceability/src/pda.rs b/smartcontract/programs/doublezero-serviceability/src/pda.rs index d661aa6ccc..40abc229a5 100644 --- a/smartcontract/programs/doublezero-serviceability/src/pda.rs +++ b/smartcontract/programs/doublezero-serviceability/src/pda.rs @@ -5,8 +5,8 @@ use solana_program::pubkey::Pubkey; use crate::{ seeds::{ SEED_ACCESS_PASS, SEED_CONFIG, SEED_CONTRIBUTOR, SEED_DEVICE, SEED_DEVICE_TUNNEL_BLOCK, - SEED_DZ_PREFIX_BLOCK, SEED_EXCHANGE, SEED_GLOBALSTATE, SEED_LINK, SEED_LINK_IDS, - SEED_LOCATION, SEED_MULTICASTGROUP_BLOCK, SEED_MULTICAST_GROUP, + SEED_DZ_PREFIX_BLOCK, SEED_EXCHANGE, SEED_GLOBALSTATE, SEED_INDEX, SEED_LINK, + SEED_LINK_IDS, SEED_LOCATION, SEED_MULTICASTGROUP_BLOCK, SEED_MULTICAST_GROUP, SEED_MULTICAST_PUBLISHER_BLOCK, SEED_PERMISSION, SEED_PREFIX, SEED_PROGRAM_CONFIG, SEED_SEGMENT_ROUTING_IDS, SEED_TENANT, SEED_TUNNEL_IDS, SEED_USER, SEED_USER_TUNNEL_BLOCK, SEED_VRF_IDS, @@ -103,6 +103,19 @@ pub fn get_accesspass_pda( ) } +pub fn get_index_pda(program_id: &Pubkey, entity_seed: &[u8], code: &str) -> (Pubkey, u8) { + let lowercase_code = code.to_ascii_lowercase(); + Pubkey::find_program_address( + &[ + SEED_PREFIX, + SEED_INDEX, + entity_seed, + lowercase_code.as_bytes(), + ], + program_id, + ) +} + pub fn get_resource_extension_pda( program_id: &Pubkey, resource_type: crate::resource::ResourceType, diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs new file mode 100644 index 0000000000..e18a8ac085 --- /dev/null +++ b/smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs @@ -0,0 +1,118 @@ +use crate::{ + error::DoubleZeroError, + pda::get_index_pda, + seeds::{SEED_INDEX, SEED_PREFIX}, + serializer::try_acc_create, + state::{accounttype::AccountType, globalstate::GlobalState, index::Index}, +}; +use borsh::BorshSerialize; +use borsh_incremental::BorshDeserializeIncremental; +use doublezero_program_common::validate_account_code; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + program_error::ProgramError, + pubkey::Pubkey, +}; +use std::fmt; + +#[cfg(test)] +use solana_program::msg; + +#[derive(BorshSerialize, BorshDeserializeIncremental, PartialEq, Clone, Default)] +pub struct IndexCreateArgs { + pub entity_seed: String, + pub code: String, +} + +impl fmt::Debug for IndexCreateArgs { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "entity_seed: {}, code: {}", self.entity_seed, self.code) + } +} + +pub fn process_create_index( + program_id: &Pubkey, + accounts: &[AccountInfo], + value: &IndexCreateArgs, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let index_account = next_account_info(accounts_iter)?; + let entity_account = next_account_info(accounts_iter)?; + let globalstate_account = next_account_info(accounts_iter)?; + let payer_account = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + + #[cfg(test)] + msg!("process_create_index({:?})", value); + + assert!(payer_account.is_signer, "Payer must be a signer"); + + // Validate accounts + assert_eq!( + globalstate_account.owner, program_id, + "Invalid GlobalState Account Owner" + ); + assert_eq!( + entity_account.owner, program_id, + "Invalid Entity Account Owner" + ); + assert_eq!( + *system_program.unsigned_key(), + solana_system_interface::program::ID, + "Invalid System Program Account Owner" + ); + assert!(index_account.is_writable, "Index Account is not writable"); + + // Check foundation allowlist + let globalstate = GlobalState::try_from(globalstate_account)?; + if !globalstate.foundation_allowlist.contains(payer_account.key) { + return Err(DoubleZeroError::NotAllowed.into()); + } + + // Validate and normalize code + let code = + validate_account_code(&value.code).map_err(|_| DoubleZeroError::InvalidAccountCode)?; + let lowercase_code = code.to_ascii_lowercase(); + + // Derive and verify the Index PDA + let (expected_pda, bump_seed) = get_index_pda(program_id, value.entity_seed.as_bytes(), &code); + assert_eq!(index_account.key, &expected_pda, "Invalid Index Pubkey"); + + // Uniqueness: account must not already exist + if !index_account.data_is_empty() { + return Err(ProgramError::AccountAlreadyInitialized); + } + + // Verify the entity account is a valid, non-Index program account + assert!(!entity_account.data_is_empty(), "Entity Account is empty"); + let entity_type = AccountType::from(entity_account.try_borrow_data()?[0]); + assert!( + entity_type != AccountType::None && entity_type != AccountType::Index, + "Entity Account has invalid type for indexing: {entity_type}" + ); + + let index = Index { + account_type: AccountType::Index, + pk: *entity_account.key, + bump_seed, + }; + + try_acc_create( + &index, + index_account, + payer_account, + system_program, + program_id, + &[ + SEED_PREFIX, + SEED_INDEX, + value.entity_seed.as_bytes(), + lowercase_code.as_bytes(), + &[bump_seed], + ], + )?; + + Ok(()) +} diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/index/delete.rs b/smartcontract/programs/doublezero-serviceability/src/processors/index/delete.rs new file mode 100644 index 0000000000..3fe0886736 --- /dev/null +++ b/smartcontract/programs/doublezero-serviceability/src/processors/index/delete.rs @@ -0,0 +1,69 @@ +use crate::{ + error::DoubleZeroError, + serializer::try_acc_close, + state::{globalstate::GlobalState, index::Index}, +}; +use borsh::BorshSerialize; +use borsh_incremental::BorshDeserializeIncremental; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + pubkey::Pubkey, +}; +use std::fmt; + +#[cfg(test)] +use solana_program::msg; + +#[derive(BorshSerialize, BorshDeserializeIncremental, PartialEq, Clone, Default)] +pub struct IndexDeleteArgs {} + +impl fmt::Debug for IndexDeleteArgs { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "IndexDeleteArgs") + } +} + +pub fn process_delete_index( + program_id: &Pubkey, + accounts: &[AccountInfo], + _value: &IndexDeleteArgs, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let index_account = next_account_info(accounts_iter)?; + let globalstate_account = next_account_info(accounts_iter)?; + let payer_account = next_account_info(accounts_iter)?; + + #[cfg(test)] + msg!("process_delete_index"); + + assert!(payer_account.is_signer, "Payer must be a signer"); + + // Validate accounts + assert_eq!( + index_account.owner, program_id, + "Invalid Index Account Owner" + ); + assert_eq!( + globalstate_account.owner, program_id, + "Invalid GlobalState Account Owner" + ); + assert!(index_account.is_writable, "Index Account is not writable"); + + // Check foundation allowlist + let globalstate = GlobalState::try_from(globalstate_account)?; + if !globalstate.foundation_allowlist.contains(payer_account.key) { + return Err(DoubleZeroError::NotAllowed.into()); + } + + // Verify it's actually an Index account + let _index = Index::try_from(index_account)?; + + try_acc_close(index_account, payer_account)?; + + #[cfg(test)] + msg!("Deleted Index account"); + + Ok(()) +} diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/index/mod.rs b/smartcontract/programs/doublezero-serviceability/src/processors/index/mod.rs new file mode 100644 index 0000000000..da1aa3ace2 --- /dev/null +++ b/smartcontract/programs/doublezero-serviceability/src/processors/index/mod.rs @@ -0,0 +1,2 @@ +pub mod create; +pub mod delete; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/mod.rs b/smartcontract/programs/doublezero-serviceability/src/processors/mod.rs index a148f5c660..129390202c 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/mod.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/mod.rs @@ -5,6 +5,7 @@ pub mod device; pub mod exchange; pub mod globalconfig; pub mod globalstate; +pub mod index; pub mod link; pub mod location; pub mod migrate; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs index 0b2ffbcdd7..dcfe59465b 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs @@ -1,10 +1,11 @@ use crate::{ error::DoubleZeroError, - pda::get_resource_extension_pda, + pda::{get_index_pda, get_resource_extension_pda}, processors::resource::deallocate_ip, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, serializer::try_acc_close, - state::{globalstate::GlobalState, multicastgroup::*}, + state::{globalstate::GlobalState, index::Index, multicastgroup::*}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -24,14 +25,17 @@ pub struct MulticastGroupDeactivateArgs { /// When false, legacy behavior is used (no deallocation). #[incremental(default = false)] pub use_onchain_deallocation: bool, + /// When true, close the associated Index account alongside the multicast group. + #[incremental(default = false)] + pub close_index: bool, } impl fmt::Debug for MulticastGroupDeactivateArgs { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "use_onchain_deallocation: {}", - self.use_onchain_deallocation + "use_onchain_deallocation: {}, close_index: {}", + self.use_onchain_deallocation, self.close_index ) } } @@ -47,10 +51,12 @@ pub fn process_closeaccount_multicastgroup( let owner_account = next_account_info(accounts_iter)?; let globalstate_account = next_account_info(accounts_iter)?; - // Optional: ResourceExtension account for on-chain deallocation (before payer) - // Account layout WITH ResourceExtension (use_onchain_deallocation = true): - // [multicastgroup, owner, globalstate, multicast_group_block, payer, system] - // Account layout WITHOUT (legacy, use_onchain_deallocation = false): + // Optional accounts (before payer/system): + // Account layout WITH deallocation + index: + // [multicastgroup, owner, globalstate, multicast_group_block, index, payer, system] + // Account layout WITHOUT deallocation, with index: + // [multicastgroup, owner, globalstate, index, payer, system] + // Legacy (no deallocation, no index): // [multicastgroup, owner, globalstate, payer, system] let resource_extension_account = if value.use_onchain_deallocation { let multicast_group_block_ext = next_account_info(accounts_iter)?; @@ -59,6 +65,12 @@ pub fn process_closeaccount_multicastgroup( None }; + let index_account = if value.close_index { + Some(next_account_info(accounts_iter)?) + } else { + None + }; + let payer_account = next_account_info(accounts_iter)?; let system_program = next_account_info(accounts_iter)?; @@ -137,6 +149,24 @@ pub fn process_closeaccount_multicastgroup( try_acc_close(multicastgroup_account, owner_account)?; + // Close the Index account if provided + if let Some(index_acc) = index_account { + assert_eq!(index_acc.owner, program_id, "Invalid Index Account Owner"); + assert!(index_acc.is_writable, "Index Account is not writable"); + + let (expected_index_pda, _) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &multicastgroup.code); + assert_eq!(index_acc.key, &expected_index_pda, "Invalid Index Pubkey"); + + let index = Index::try_from(index_acc)?; + assert_eq!( + index.pk, *multicastgroup_account.key, + "Index does not point to this MulticastGroup" + ); + + try_acc_close(index_acc, payer_account)?; + } + #[cfg(test)] msg!("Deactivated: MulticastGroup closed"); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs index 9b0e3a5606..b88bfcbf2d 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs @@ -1,14 +1,15 @@ use crate::{ error::DoubleZeroError, - pda::{get_multicastgroup_pda, get_resource_extension_pda}, + pda::{get_index_pda, get_multicastgroup_pda, get_resource_extension_pda}, processors::{resource::allocate_ip, validation::validate_program_account}, resource::ResourceType, - seeds::{SEED_MULTICAST_GROUP, SEED_PREFIX}, + seeds::{SEED_INDEX, SEED_MULTICAST_GROUP, SEED_PREFIX}, serializer::{try_acc_create, try_acc_write}, state::{ accounttype::AccountType, feature_flags::{is_feature_enabled, FeatureFlag}, globalstate::GlobalState, + index::Index, multicastgroup::*, }, }; @@ -57,17 +58,18 @@ pub fn process_create_multicastgroup( let mgroup_account = next_account_info(accounts_iter)?; let globalstate_account = next_account_info(accounts_iter)?; - // Optional: ResourceExtension account for onchain allocation (before payer) + // Optional: ResourceExtension account for onchain allocation // Account layout WITH ResourceExtension (use_onchain_allocation = true): - // [mgroup, globalstate, multicast_group_block, payer, system] + // [mgroup, globalstate, multicast_group_block, index, payer, system] // Account layout WITHOUT (legacy, use_onchain_allocation = false): - // [mgroup, globalstate, payer, system] + // [mgroup, globalstate, index, payer, system] let resource_extension_account = if value.use_onchain_allocation { Some(next_account_info(accounts_iter)?) } else { None }; + let index_account = next_account_info(accounts_iter)?; let payer_account = next_account_info(accounts_iter)?; let system_program = next_account_info(accounts_iter)?; @@ -80,6 +82,7 @@ pub fn process_create_multicastgroup( // Validate and normalize code let code = validate_account_code(&value.code).map_err(|_| DoubleZeroError::InvalidAccountCode)?; + let lowercase_code = code.to_ascii_lowercase(); // Check the owner of the accounts assert_eq!( @@ -114,6 +117,10 @@ pub fn process_create_multicastgroup( return Err(ProgramError::AccountAlreadyInitialized); } + // Validate Index PDA (before code is moved into multicastgroup) + let (expected_index_pda, index_bump_seed) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &code); + let mut multicastgroup = MulticastGroup { account_type: AccountType::MulticastGroup, owner: value.owner, @@ -147,6 +154,16 @@ pub fn process_create_multicastgroup( multicastgroup.multicast_ip = allocate_ip(multicast_group_block_ext, 1)?.ip(); multicastgroup.status = MulticastGroupStatus::Activated; } + assert_eq!( + index_account.key, &expected_index_pda, + "Invalid Index Pubkey" + ); + assert!(index_account.is_writable, "Index Account is not writable"); + + // Uniqueness: index account must not already exist + if !index_account.data_is_empty() { + return Err(ProgramError::AccountAlreadyInitialized); + } try_acc_create( &multicastgroup, @@ -161,6 +178,29 @@ pub fn process_create_multicastgroup( &[bump_seed], ], )?; + + // Create the Index account pointing to the multicast group + let index = Index { + account_type: AccountType::Index, + pk: *mgroup_account.key, + bump_seed: index_bump_seed, + }; + + try_acc_create( + &index, + index_account, + payer_account, + system_program, + program_id, + &[ + SEED_PREFIX, + SEED_INDEX, + SEED_MULTICAST_GROUP, + lowercase_code.as_bytes(), + &[index_bump_seed], + ], + )?; + try_acc_write(&globalstate, globalstate_account, payer_account, accounts)?; Ok(()) diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/delete.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/delete.rs index 46806dafdb..073d86a0e1 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/delete.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/delete.rs @@ -1,12 +1,14 @@ use crate::{ error::DoubleZeroError, - pda::get_resource_extension_pda, + pda::{get_index_pda, get_resource_extension_pda}, processors::{resource::deallocate_ip, validation::validate_program_account}, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, serializer::{try_acc_close, try_acc_write}, state::{ feature_flags::{is_feature_enabled, FeatureFlag}, globalstate::GlobalState, + index::Index, multicastgroup::*, }, }; @@ -28,14 +30,17 @@ pub struct MulticastGroupDeleteArgs { /// Requires ResourceExtension accounts and owner account. #[incremental(default = false)] pub use_onchain_deallocation: bool, + /// When true, close the associated Index account alongside the multicast group. + #[incremental(default = false)] + pub close_index: bool, } impl fmt::Debug for MulticastGroupDeleteArgs { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "use_onchain_deallocation: {}", - self.use_onchain_deallocation + "use_onchain_deallocation: {}, close_index: {}", + self.use_onchain_deallocation, self.close_index ) } } @@ -50,10 +55,12 @@ pub fn process_delete_multicastgroup( let multicastgroup_account = next_account_info(accounts_iter)?; let globalstate_account = next_account_info(accounts_iter)?; - // Optional: additional accounts for atomic deallocation (before payer) - // Account layout WITH deallocation (use_onchain_deallocation = true): - // [mgroup, globalstate, multicast_group_block, owner, payer, system] - // Account layout WITHOUT (legacy, use_onchain_deallocation = false): + // Optional: additional accounts for atomic deallocation + // Account layout WITH deallocation + index: + // [mgroup, globalstate, multicast_group_block, owner, index, payer, system] + // Account layout WITHOUT deallocation, with index: + // [mgroup, globalstate, index, payer, system] + // Legacy (no deallocation, no index): // [mgroup, globalstate, payer, system] let deallocation_accounts = if value.use_onchain_deallocation { let multicast_group_block_ext = next_account_info(accounts_iter)?; @@ -63,6 +70,12 @@ pub fn process_delete_multicastgroup( None }; + let index_account = if value.close_index { + Some(next_account_info(accounts_iter)?) + } else { + None + }; + let payer_account = next_account_info(accounts_iter)?; let system_program = next_account_info(accounts_iter)?; @@ -98,6 +111,7 @@ pub fn process_delete_multicastgroup( } let multicastgroup: MulticastGroup = MulticastGroup::try_from(multicastgroup_account)?; + let multicastgroup_code = multicastgroup.code.clone(); if matches!(multicastgroup.status, MulticastGroupStatus::Deleting) { return Err(DoubleZeroError::InvalidStatus.into()); @@ -158,5 +172,25 @@ pub fn process_delete_multicastgroup( msg!("Deleted: {:?}", multicastgroup_account); } + // Close the Index account if provided + if let Some(index_acc) = index_account { + assert_eq!(index_acc.owner, program_id, "Invalid Index Account Owner"); + assert!(index_acc.is_writable, "Index Account is not writable"); + + // Verify the Index PDA matches + let (expected_index_pda, _) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &multicastgroup_code); + assert_eq!(index_acc.key, &expected_index_pda, "Invalid Index Pubkey"); + + // Verify it's an Index account pointing to this multicast group + let index = Index::try_from(index_acc)?; + assert_eq!( + index.pk, *multicastgroup_account.key, + "Index does not point to this MulticastGroup" + ); + + try_acc_close(index_acc, payer_account)?; + } + Ok(()) } diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs index 3447898fa0..40923d0485 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs @@ -1,15 +1,18 @@ use crate::{ error::DoubleZeroError, - pda::get_resource_extension_pda, + pda::{get_index_pda, get_resource_extension_pda}, processors::{ resource::{allocate_specific_ip, deallocate_ip}, validation::validate_program_account, }, resource::ResourceType, - serializer::try_acc_write, + seeds::{SEED_INDEX, SEED_MULTICAST_GROUP, SEED_PREFIX}, + serializer::{try_acc_close, try_acc_create, try_acc_write}, state::{ + accounttype::AccountType, feature_flags::{is_feature_enabled, FeatureFlag}, globalstate::GlobalState, + index::Index, multicastgroup::*, }, }; @@ -19,6 +22,7 @@ use doublezero_program_common::{types::NetworkV4, validate_account_code}; use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, + program_error::ProgramError, pubkey::Pubkey, }; use std::fmt; @@ -37,14 +41,18 @@ pub struct MulticastGroupUpdateArgs { /// Requires ResourceExtension account (MulticastGroupBlock). #[incremental(default = false)] pub use_onchain_allocation: bool, + /// When true, old and new Index accounts are included for an Index rename. + /// Set to false when the code change doesn't affect the Index PDA (e.g. case-only rename). + #[incremental(default = false)] + pub rename_index: bool, } impl fmt::Debug for MulticastGroupUpdateArgs { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "code: {:?}, multicast_ip: {:?}, max_bandwidth: {:?}, publisher_count: {:?}, subscriber_count: {:?}, use_onchain_allocation: {}", - self.code, self.multicast_ip, self.max_bandwidth, self.publisher_count, self.subscriber_count, self.use_onchain_allocation + "code: {:?}, multicast_ip: {:?}, max_bandwidth: {:?}, publisher_count: {:?}, subscriber_count: {:?}, use_onchain_allocation: {}, rename_index: {}", + self.code, self.multicast_ip, self.max_bandwidth, self.publisher_count, self.subscriber_count, self.use_onchain_allocation, self.rename_index ) } } @@ -59,17 +67,26 @@ pub fn process_update_multicastgroup( let multicastgroup_account = next_account_info(accounts_iter)?; let globalstate_account = next_account_info(accounts_iter)?; - // Optional: ResourceExtension account for onchain allocation (before payer) + // Optional: ResourceExtension account for onchain allocation // Account layout WITH allocation (use_onchain_allocation = true): - // [mgroup, globalstate, multicast_group_block, payer, system] + // [mgroup, globalstate, multicast_group_block, (opt old_index, new_index), payer, system] // Account layout WITHOUT (legacy, use_onchain_allocation = false): - // [mgroup, globalstate, payer, system] + // [mgroup, globalstate, (opt old_index, new_index), payer, system] let resource_extension_account = if value.use_onchain_allocation { Some(next_account_info(accounts_iter)?) } else { None }; + // Optional: Index accounts for code rename (before payer/system) + let index_accounts = if value.rename_index { + let old_index_account = next_account_info(accounts_iter)?; + let new_index_account = next_account_info(accounts_iter)?; + Some((old_index_account, new_index_account)) + } else { + None + }; + let payer_account = next_account_info(accounts_iter)?; let system_program = next_account_info(accounts_iter)?; @@ -107,8 +124,81 @@ pub fn process_update_multicastgroup( let mut multicastgroup: MulticastGroup = MulticastGroup::try_from(multicastgroup_account)?; if let Some(ref code) = value.code { - multicastgroup.code = + let new_code = validate_account_code(code).map_err(|_| DoubleZeroError::InvalidAccountCode)?; + + // Rename the Index if accounts are provided (skip for case-only renames + // where the lowercased PDA is unchanged) + if let Some((old_index_account, new_index_account)) = index_accounts { + let new_lowercase_code = new_code.to_ascii_lowercase(); + + // Validate old Index PDA + let (expected_old_index_pda, _) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &multicastgroup.code); + assert_eq!( + old_index_account.key, &expected_old_index_pda, + "Invalid old Index Pubkey" + ); + assert_eq!( + old_index_account.owner, program_id, + "Invalid old Index Account Owner" + ); + assert!( + old_index_account.is_writable, + "Old Index Account is not writable" + ); + + // Validate new Index PDA + let (expected_new_index_pda, new_index_bump_seed) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &new_code); + assert_eq!( + new_index_account.key, &expected_new_index_pda, + "Invalid new Index Pubkey" + ); + assert!( + new_index_account.is_writable, + "New Index Account is not writable" + ); + + // New index must not already exist (uniqueness) + if !new_index_account.data_is_empty() { + return Err(ProgramError::AccountAlreadyInitialized); + } + + // Verify old index points to this multicast group + let old_index = Index::try_from(old_index_account)?; + assert_eq!( + old_index.pk, *multicastgroup_account.key, + "Old Index does not point to this MulticastGroup" + ); + + // Create new Index + let new_index = Index { + account_type: AccountType::Index, + pk: *multicastgroup_account.key, + bump_seed: new_index_bump_seed, + }; + + try_acc_create( + &new_index, + new_index_account, + payer_account, + system_program, + program_id, + &[ + SEED_PREFIX, + SEED_INDEX, + SEED_MULTICAST_GROUP, + new_lowercase_code.as_bytes(), + &[new_index_bump_seed], + ], + )?; + + // Close old Index + try_acc_close(old_index_account, payer_account)?; + } + + multicastgroup.code = new_code; } if let Some(ref multicast_ip) = value.multicast_ip { // Handle onchain allocation for IP changes diff --git a/smartcontract/programs/doublezero-serviceability/src/seeds.rs b/smartcontract/programs/doublezero-serviceability/src/seeds.rs index ed605fde57..4ef9626dd9 100644 --- a/smartcontract/programs/doublezero-serviceability/src/seeds.rs +++ b/smartcontract/programs/doublezero-serviceability/src/seeds.rs @@ -21,3 +21,4 @@ pub const SEED_LINK_IDS: &[u8] = b"linkids"; pub const SEED_SEGMENT_ROUTING_IDS: &[u8] = b"segmentroutingids"; pub const SEED_VRF_IDS: &[u8] = b"vrfids"; pub const SEED_PERMISSION: &[u8] = b"permission"; +pub const SEED_INDEX: &[u8] = b"index"; diff --git a/smartcontract/programs/doublezero-serviceability/src/state/accountdata.rs b/smartcontract/programs/doublezero-serviceability/src/state/accountdata.rs index ac9c67a82c..89fb6ac3c8 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/accountdata.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/accountdata.rs @@ -2,8 +2,8 @@ use crate::{ error::DoubleZeroError, state::{ accesspass::AccessPass, accounttype::AccountType, contributor::Contributor, device::Device, - exchange::Exchange, globalconfig::GlobalConfig, globalstate::GlobalState, link::Link, - location::Location, multicastgroup::MulticastGroup, permission::Permission, + exchange::Exchange, globalconfig::GlobalConfig, globalstate::GlobalState, index::Index, + link::Link, location::Location, multicastgroup::MulticastGroup, permission::Permission, programconfig::ProgramConfig, resource_extension::ResourceExtensionOwned, tenant::Tenant, user::User, }, @@ -29,6 +29,7 @@ pub enum AccountData { ResourceExtension(ResourceExtensionOwned), Tenant(Tenant), Permission(Permission), + Index(Index), } impl AccountData { @@ -49,6 +50,7 @@ impl AccountData { AccountData::ResourceExtension(_) => "ResourceExtension", AccountData::Tenant(_) => "Tenant", AccountData::Permission(_) => "Permission", + AccountData::Index(_) => "Index", } } @@ -69,6 +71,7 @@ impl AccountData { AccountData::ResourceExtension(resource_extension) => resource_extension.to_string(), AccountData::Tenant(tenant) => tenant.to_string(), AccountData::Permission(permission) => permission.to_string(), + AccountData::Index(index) => index.to_string(), } } @@ -183,6 +186,14 @@ impl AccountData { Err(DoubleZeroError::InvalidAccountType) } } + + pub fn get_index(&self) -> Result { + if let AccountData::Index(index) = self { + Ok(index.clone()) + } else { + Err(DoubleZeroError::InvalidAccountType) + } + } } impl TryFrom<&[u8]> for AccountData { @@ -224,6 +235,7 @@ impl TryFrom<&[u8]> for AccountData { AccountType::Permission => Ok(AccountData::Permission(Permission::try_from( bytes as &[u8], )?)), + AccountType::Index => Ok(AccountData::Index(Index::try_from(bytes as &[u8])?)), } } } diff --git a/smartcontract/programs/doublezero-serviceability/src/state/accounttype.rs b/smartcontract/programs/doublezero-serviceability/src/state/accounttype.rs index 522bbebd57..24e8430bab 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/accounttype.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/accounttype.rs @@ -23,6 +23,7 @@ pub enum AccountType { ResourceExtension = 12, Tenant = 13, Permission = 15, + Index = 16, } pub trait AccountTypeInfo { @@ -50,6 +51,7 @@ impl From for AccountType { 12 => AccountType::ResourceExtension, 13 => AccountType::Tenant, 15 => AccountType::Permission, + 16 => AccountType::Index, _ => AccountType::None, } } @@ -73,6 +75,7 @@ impl fmt::Display for AccountType { AccountType::ResourceExtension => write!(f, "resourceextension"), AccountType::Tenant => write!(f, "tenant"), AccountType::Permission => write!(f, "permission"), + AccountType::Index => write!(f, "index"), } } } diff --git a/smartcontract/programs/doublezero-serviceability/src/state/index.rs b/smartcontract/programs/doublezero-serviceability/src/state/index.rs new file mode 100644 index 0000000000..71e46fba75 --- /dev/null +++ b/smartcontract/programs/doublezero-serviceability/src/state/index.rs @@ -0,0 +1,129 @@ +use crate::{ + error::{DoubleZeroError, Validate}, + state::accounttype::AccountType, +}; +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; +use std::fmt; + +#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Index { + pub account_type: AccountType, // 1 + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "doublezero_program_common::serializer::serialize_pubkey_as_string", + deserialize_with = "doublezero_program_common::serializer::deserialize_pubkey_from_string" + ) + )] + pub pk: Pubkey, // 32 + pub bump_seed: u8, // 1 +} + +impl fmt::Display for Index { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Index {{ account_type: {}, pk: {}, bump_seed: {} }}", + self.account_type, self.pk, self.bump_seed + ) + } +} + +impl Default for Index { + fn default() -> Self { + Self { + account_type: AccountType::Index, + pk: Pubkey::default(), + bump_seed: 0, + } + } +} + +impl TryFrom<&[u8]> for Index { + type Error = ProgramError; + + fn try_from(mut data: &[u8]) -> Result { + let out = Self { + account_type: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), + pk: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), + bump_seed: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), + }; + + if out.account_type != AccountType::Index { + return Err(ProgramError::InvalidAccountData); + } + + Ok(out) + } +} + +impl TryFrom<&AccountInfo<'_>> for Index { + type Error = ProgramError; + + fn try_from(account: &AccountInfo) -> Result { + let data = account.try_borrow_data()?; + let res = Self::try_from(&data[..]); + if res.is_err() { + msg!("Failed to deserialize Index: {:?}", res.as_ref().err()); + } + res + } +} + +impl Validate for Index { + fn validate(&self) -> Result<(), DoubleZeroError> { + if self.account_type != AccountType::Index { + msg!("Invalid account type: {}", self.account_type); + return Err(DoubleZeroError::InvalidAccountType); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_state_index_try_from_defaults() { + let data = [AccountType::Index as u8]; + let val = Index::try_from(&data[..]).unwrap(); + + assert_eq!(val.pk, Pubkey::default()); + assert_eq!(val.bump_seed, 0); + } + + #[test] + fn test_state_index_serialization() { + let val = Index { + account_type: AccountType::Index, + pk: Pubkey::new_unique(), + bump_seed: 254, + }; + + let data = borsh::to_vec(&val).unwrap(); + let val2 = Index::try_from(&data[..]).unwrap(); + + val.validate().unwrap(); + val2.validate().unwrap(); + + assert_eq!(val, val2); + assert_eq!(val.account_type as u8, data[0]); + assert_eq!(data.len(), borsh::object_length(&val).unwrap(),); + } + + #[test] + fn test_state_index_validate_error_invalid_account_type() { + let val = Index { + account_type: AccountType::Device, + pk: Pubkey::new_unique(), + bump_seed: 1, + }; + assert_eq!( + val.validate().unwrap_err(), + DoubleZeroError::InvalidAccountType + ); + } +} diff --git a/smartcontract/programs/doublezero-serviceability/src/state/mod.rs b/smartcontract/programs/doublezero-serviceability/src/state/mod.rs index 793c35e469..bbf02d13a1 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/mod.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/mod.rs @@ -7,6 +7,7 @@ pub mod exchange; pub mod feature_flags; pub mod globalconfig; pub mod globalstate; +pub mod index; pub mod interface; pub mod link; pub mod location; 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 01ea8dae72..c16a34067b 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs @@ -14,8 +14,9 @@ use doublezero_serviceability::{ instructions::DoubleZeroInstruction, pda::{ get_accesspass_pda, get_contributor_pda, get_device_pda, get_exchange_pda, - get_globalconfig_pda, get_globalstate_pda, get_location_pda, get_multicastgroup_pda, - get_program_config_pda, get_resource_extension_pda, get_tenant_pda, get_user_pda, + get_globalconfig_pda, get_globalstate_pda, get_index_pda, get_location_pda, + get_multicastgroup_pda, get_program_config_pda, get_resource_extension_pda, get_tenant_pda, + get_user_pda, }, processors::{ accesspass::set::SetAccessPassArgs, @@ -43,6 +44,7 @@ use doublezero_serviceability::{ }, }, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, state::{ accesspass::AccessPassType, device::DeviceType, @@ -291,7 +293,9 @@ async fn setup_create_subscribe_fixture(client_ip: [u8; 4]) -> CreateSubscribeFi // Create and activate multicast group let gs = get_globalstate(&mut banks_client, globalstate_pubkey).await; let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, gs.account_index + 1); - execute_transaction( + let (index_pda_group1, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "group1"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -304,8 +308,10 @@ async fn setup_create_subscribe_fixture(client_ip: [u8; 4]) -> CreateSubscribeFi vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_group1, false), ], &payer, + &[], ) .await; @@ -876,7 +882,9 @@ async fn test_create_subscribe_user_inactive_mgroup_fails() { let (pending_mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, gs.account_index + 1); let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - execute_transaction( + let (index_pda_pending, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "pending"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -889,8 +897,10 @@ async fn test_create_subscribe_user_inactive_mgroup_fails() { vec![ AccountMeta::new(pending_mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_pending, false), ], &payer, + &[], ) .await; @@ -1723,6 +1733,7 @@ async fn test_create_subscribe_user_foundation_owner_override() { // Create and activate multicast group let gs = get_globalstate(&mut banks_client, globalstate_pubkey).await; let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, gs.account_index + 1); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "group1"); execute_transaction( &mut banks_client, recent_blockhash, @@ -1736,6 +1747,7 @@ async fn test_create_subscribe_user_foundation_owner_override() { vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, ) @@ -2080,6 +2092,7 @@ async fn test_create_subscribe_user_sentinel_owner_override() { // Create and activate multicast group let gs = get_globalstate(&mut banks_client, globalstate_pubkey).await; let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, gs.account_index + 1); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "group1"); execute_transaction( &mut banks_client, recent_blockhash, @@ -2093,6 +2106,7 @@ async fn test_create_subscribe_user_sentinel_owner_override() { vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, ) @@ -2419,6 +2433,7 @@ async fn test_create_subscribe_user_non_foundation_owner_override_rejected() { // Create and activate multicast group let gs = get_globalstate(&mut banks_client, globalstate_pubkey).await; let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, gs.account_index + 1); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "group1"); execute_transaction( &mut banks_client, recent_blockhash, @@ -2432,6 +2447,7 @@ async fn test_create_subscribe_user_non_foundation_owner_override_rejected() { vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, ) diff --git a/smartcontract/programs/doublezero-serviceability/tests/index_test.rs b/smartcontract/programs/doublezero-serviceability/tests/index_test.rs new file mode 100644 index 0000000000..405f50bc41 --- /dev/null +++ b/smartcontract/programs/doublezero-serviceability/tests/index_test.rs @@ -0,0 +1,367 @@ +use doublezero_serviceability::{ + instructions::*, + pda::*, + processors::{ + index::{create::IndexCreateArgs, delete::IndexDeleteArgs}, + multicastgroup::create::MulticastGroupCreateArgs, + }, + seeds::SEED_MULTICAST_GROUP, + state::accounttype::AccountType, +}; +use solana_program_test::*; +use solana_sdk::{ + instruction::AccountMeta, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +mod test_helpers; +use test_helpers::*; + +/// Helper: create a multicast group and return its pubkey + the new globalstate index. +/// The multicast group is created without onchain allocation (Pending status). +async fn create_multicast_group( + banks_client: &mut BanksClient, + payer: &Keypair, + program_id: Pubkey, + globalstate_pubkey: Pubkey, + code: &str, +) -> Pubkey { + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let globalstate = get_globalstate(banks_client, globalstate_pubkey).await; + let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, code); + + execute_transaction_with_extra_accounts( + banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateMulticastGroup(MulticastGroupCreateArgs { + code: code.to_string(), + max_bandwidth: 1000, + owner: Pubkey::new_unique(), + use_onchain_allocation: false, + }), + vec![ + AccountMeta::new(mgroup_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), + ], + payer, + &[], + ) + .await; + + mgroup_pubkey +} + +#[tokio::test] +async fn test_create_index() { + let (mut banks_client, payer, program_id, globalstate_pubkey, _) = + setup_program_with_globalconfig().await; + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + + // Create a multicast group to use as the entity account + let mgroup_pubkey = create_multicast_group( + &mut banks_client, + &payer, + program_id, + globalstate_pubkey, + "test-mg", + ) + .await; + + // Derive the Index PDA for a new code on the same entity seed + let code = "my-index"; + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, code); + + // Create the Index + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateIndex(IndexCreateArgs { + entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), + code: code.to_string(), + }), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(mgroup_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // Verify the Index account exists and contains the entity pubkey + let index_data = get_account_data(&mut banks_client, index_pda) + .await + .expect("Index account should exist"); + let index = index_data.get_index().unwrap(); + assert_eq!(index.account_type, AccountType::Index); + assert_eq!( + index.pk, mgroup_pubkey, + "Index should point to the multicast group" + ); +} + +#[tokio::test] +async fn test_create_index_duplicate_fails() { + let (mut banks_client, payer, program_id, globalstate_pubkey, _) = + setup_program_with_globalconfig().await; + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + + // Create a multicast group as the entity + let mgroup_pubkey = create_multicast_group( + &mut banks_client, + &payer, + program_id, + globalstate_pubkey, + "dup-mg", + ) + .await; + + let code = "dup-code"; + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, code); + + // First CreateIndex should succeed + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateIndex(IndexCreateArgs { + entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), + code: code.to_string(), + }), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(mgroup_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // Wait for a new blockhash to avoid transaction deduplication + let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; + + // Second CreateIndex with the same entity_seed+code should fail + let result = execute_transaction_expect_failure( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateIndex(IndexCreateArgs { + entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), + code: code.to_string(), + }), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(mgroup_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + let error_string = format!("{:?}", result.unwrap_err()); + assert!( + error_string.contains("AccountAlreadyInitialized") + || error_string.contains("already in use"), + "Expected AccountAlreadyInitialized error, got: {error_string}", + ); +} + +#[tokio::test] +async fn test_create_index_unauthorized_fails() { + let (mut banks_client, payer, program_id, globalstate_pubkey, _) = + setup_program_with_globalconfig().await; + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + + // Create a multicast group as the entity + let mgroup_pubkey = create_multicast_group( + &mut banks_client, + &payer, + program_id, + globalstate_pubkey, + "unauth-mg", + ) + .await; + + // Create an unauthorized keypair with some lamports + let unauthorized = Keypair::new(); + transfer( + &mut banks_client, + &payer, + &unauthorized.pubkey(), + 10_000_000, + ) + .await; + + let code = "unauth-code"; + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, code); + + // Attempt CreateIndex with the unauthorized payer + let result = try_execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateIndex(IndexCreateArgs { + entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), + code: code.to_string(), + }), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(mgroup_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &unauthorized, + ) + .await; + + let error_string = format!("{:?}", result.unwrap_err()); + assert!( + error_string.contains("Custom(8)"), + "Expected NotAllowed error (Custom(8)), got: {error_string}", + ); +} + +#[tokio::test] +async fn test_delete_index() { + let (mut banks_client, payer, program_id, globalstate_pubkey, _) = + setup_program_with_globalconfig().await; + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + + // Create a multicast group as the entity + let mgroup_pubkey = create_multicast_group( + &mut banks_client, + &payer, + program_id, + globalstate_pubkey, + "del-mg", + ) + .await; + + let code = "del-code"; + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, code); + + // Create the Index + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateIndex(IndexCreateArgs { + entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), + code: code.to_string(), + }), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(mgroup_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // Verify the Index exists + let index_data = get_account_data(&mut banks_client, index_pda).await; + assert!( + index_data.is_some(), + "Index account should exist before deletion" + ); + + // Delete the Index + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::DeleteIndex(IndexDeleteArgs {}), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // Verify the account is closed + let index_after = get_account_data(&mut banks_client, index_pda).await; + assert!( + index_after.is_none(), + "Index account should be closed after deletion" + ); +} + +#[tokio::test] +async fn test_delete_index_unauthorized_fails() { + let (mut banks_client, payer, program_id, globalstate_pubkey, _) = + setup_program_with_globalconfig().await; + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + + // Create a multicast group as the entity + let mgroup_pubkey = create_multicast_group( + &mut banks_client, + &payer, + program_id, + globalstate_pubkey, + "delauth-mg", + ) + .await; + + let code = "delauth-code"; + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, code); + + // Create the Index with the authorized payer + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateIndex(IndexCreateArgs { + entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), + code: code.to_string(), + }), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(mgroup_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // Create an unauthorized keypair with some lamports + let unauthorized = Keypair::new(); + transfer( + &mut banks_client, + &payer, + &unauthorized.pubkey(), + 10_000_000, + ) + .await; + + // Attempt DeleteIndex with the unauthorized payer + let result = try_execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::DeleteIndex(IndexDeleteArgs {}), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &unauthorized, + ) + .await; + + let error_string = format!("{:?}", result.unwrap_err()); + assert!( + error_string.contains("Custom(8)"), + "Expected NotAllowed error (Custom(8)), got: {error_string}", + ); + + // Verify the Index is still intact + let index_data = get_account_data(&mut banks_client, index_pda).await; + assert!( + index_data.is_some(), + "Index account should still exist after unauthorized delete" + ); +} diff --git a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_publisher_test.rs b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_publisher_test.rs index 2449814b65..669ef3aeb8 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_publisher_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_publisher_test.rs @@ -13,6 +13,7 @@ use doublezero_serviceability::{ create::MulticastGroupCreateArgs, }, }, + seeds::SEED_MULTICAST_GROUP, state::{ accesspass::AccessPassType, accounttype::AccountType, multicastgroup::MulticastGroupStatus, }, @@ -63,7 +64,9 @@ async fn test_multicast_publisher_allowlist() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); - execute_transaction( + let (index_pda_test, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "test"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -76,8 +79,10 @@ async fn test_multicast_publisher_allowlist() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_test, false), ], &payer, + &[], ) .await; @@ -274,7 +279,9 @@ async fn test_multicast_publisher_allowlist_sentinel_authority() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); - execute_transaction( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "sentinel-test"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -287,8 +294,10 @@ async fn test_multicast_publisher_allowlist_sentinel_authority() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; diff --git a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_subcriber_test.rs b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_subcriber_test.rs index 92e4b8d117..ea4404931f 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_subcriber_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_subcriber_test.rs @@ -13,6 +13,7 @@ use doublezero_serviceability::{ create::MulticastGroupCreateArgs, }, }, + seeds::SEED_MULTICAST_GROUP, state::{ accesspass::AccessPassType, accounttype::AccountType, multicastgroup::MulticastGroupStatus, }, @@ -63,7 +64,9 @@ async fn test_multicast_subscriber_allowlist() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); - execute_transaction( + let (index_pda_test, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "test"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -76,8 +79,10 @@ async fn test_multicast_subscriber_allowlist() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_test, false), ], &payer, + &[], ) .await; @@ -274,7 +279,9 @@ async fn test_multicast_subscriber_allowlist_sentinel_authority() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); - execute_transaction( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "sentinel-test"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -287,8 +294,10 @@ async fn test_multicast_subscriber_allowlist_sentinel_authority() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -476,6 +485,7 @@ async fn test_multicast_subscriber_allowlist_feed_authority() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "feed-test"); execute_transaction( &mut banks_client, @@ -490,6 +500,7 @@ async fn test_multicast_subscriber_allowlist_feed_authority() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, ) @@ -620,6 +631,7 @@ async fn test_multicast_subscriber_allowlist_feed_authority_different_user_payer let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "feed-diff-payer"); execute_transaction( &mut banks_client, @@ -634,6 +646,7 @@ async fn test_multicast_subscriber_allowlist_feed_authority_different_user_payer vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, ) @@ -813,6 +826,7 @@ async fn test_multicast_subscriber_allowlist_feed_authority_remove() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "feed-remove"); execute_transaction( &mut banks_client, @@ -827,6 +841,7 @@ async fn test_multicast_subscriber_allowlist_feed_authority_remove() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, ) diff --git a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_onchain_allocation_test.rs b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_onchain_allocation_test.rs index 1bf2f422fc..1f704bd713 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_onchain_allocation_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_onchain_allocation_test.rs @@ -18,6 +18,7 @@ use doublezero_serviceability::{ }, }, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, state::{feature_flags::FeatureFlag, multicastgroup::*}, }; use solana_program::instruction::InstructionError; @@ -55,7 +56,9 @@ async fn test_create_multicastgroup_atomic_with_onchain_allocation() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -69,8 +72,10 @@ async fn test_create_multicastgroup_atomic_with_onchain_allocation() { AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_pda, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -103,7 +108,9 @@ async fn test_create_multicastgroup_atomic_backward_compat() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -116,8 +123,10 @@ async fn test_create_multicastgroup_atomic_backward_compat() { vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -150,7 +159,9 @@ async fn test_create_multicastgroup_atomic_feature_flag_disabled() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - let result = execute_transaction_expect_failure( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + + let result = execute_transaction_expect_failure_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -164,8 +175,10 @@ async fn test_create_multicastgroup_atomic_feature_flag_disabled() { AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_pda, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -210,8 +223,10 @@ async fn test_delete_multicastgroup_atomic_with_deallocation() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + // Create with atomic onchain allocation - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -225,8 +240,10 @@ async fn test_delete_multicastgroup_atomic_with_deallocation() { AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_pda, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -239,20 +256,23 @@ async fn test_delete_multicastgroup_atomic_with_deallocation() { assert_eq!(mgroup.status, MulticastGroupStatus::Activated); // Atomic delete+deallocate+close - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: true, + close_index: true, }), vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_pda, false), AccountMeta::new(owner, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -294,8 +314,10 @@ async fn test_delete_multicastgroup_atomic_backward_compat() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + // Create with atomic onchain allocation - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -309,24 +331,29 @@ async fn test_delete_multicastgroup_atomic_backward_compat() { AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_pda, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; // Legacy delete (use_onchain_deallocation=false, default) - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, + close_index: true, }), vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -369,8 +396,10 @@ async fn test_update_multicastgroup_with_onchain_reallocation() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + // Create with atomic onchain allocation - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -384,8 +413,10 @@ async fn test_update_multicastgroup_with_onchain_reallocation() { AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_pda, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -410,6 +441,7 @@ async fn test_update_multicastgroup_with_onchain_reallocation() { publisher_count: None, subscriber_count: None, use_onchain_allocation: true, + rename_index: false, }), vec![ AccountMeta::new(mgroup_pubkey, false), @@ -458,8 +490,10 @@ async fn test_update_multicastgroup_backward_compat() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); + let (index_pda_mg1, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + // Create with atomic onchain allocation - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -473,29 +507,38 @@ async fn test_update_multicastgroup_backward_compat() { AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_pda, false), + AccountMeta::new(index_pda_mg1, false), ], &payer, + &[], ) .await; - // Legacy update without onchain allocation - execute_transaction( + // Legacy update without onchain allocation (code changes, so needs old+new index accounts) + let (old_index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + let (new_index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg2"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::UpdateMulticastGroup(MulticastGroupUpdateArgs { - code: Some("mg1_updated".to_string()), + code: Some("mg2".to_string()), multicast_ip: None, max_bandwidth: Some(2000), publisher_count: None, subscriber_count: None, use_onchain_allocation: false, + rename_index: true, }), vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(old_index_pda, false), + AccountMeta::new(new_index_pda, false), ], &payer, + &[], ) .await; @@ -504,7 +547,7 @@ async fn test_update_multicastgroup_backward_compat() { .expect("MulticastGroup not found") .get_multicastgroup() .unwrap(); - assert_eq!(mgroup.code, "mg1_updated"); + assert_eq!(mgroup.code, "mg2"); assert_eq!(mgroup.max_bandwidth, 2000); println!("test_update_multicastgroup_backward_compat PASSED"); @@ -522,7 +565,9 @@ async fn test_update_multicastgroup_feature_flag_disabled() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -535,8 +580,10 @@ async fn test_update_multicastgroup_feature_flag_disabled() { vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -557,6 +604,7 @@ async fn test_update_multicastgroup_feature_flag_disabled() { publisher_count: None, subscriber_count: None, use_onchain_allocation: true, + rename_index: false, }), vec![ AccountMeta::new(mgroup_pubkey, false), diff --git a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs index b8f54c962e..0e611a8c09 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs @@ -21,6 +21,7 @@ use doublezero_serviceability::{ user::{activate::UserActivateArgs, create::UserCreateArgs}, }, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, state::{ accesspass::AccessPassType, device::DeviceType, @@ -261,7 +262,9 @@ async fn setup_fixture() -> TestFixture { // 7. Create two multicast groups and activate them let gs = get_globalstate(&mut banks_client, globalstate_pubkey).await; let (mgroup1_pubkey, _) = get_multicastgroup_pda(&program_id, gs.account_index + 1); - execute_transaction( + let (index_pda_group1, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "group1"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -274,8 +277,10 @@ async fn setup_fixture() -> TestFixture { vec![ AccountMeta::new(mgroup1_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_group1, false), ], &payer, + &[], ) .await; @@ -296,7 +301,9 @@ async fn setup_fixture() -> TestFixture { let gs = get_globalstate(&mut banks_client, globalstate_pubkey).await; let (mgroup2_pubkey, _) = get_multicastgroup_pda(&program_id, gs.account_index + 1); - execute_transaction( + let (index_pda_group2, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "group2"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -309,8 +316,10 @@ async fn setup_fixture() -> TestFixture { vec![ AccountMeta::new(mgroup2_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_group2, false), ], &payer, + &[], ) .await; diff --git a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs index b433230766..7b5b85931a 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs @@ -7,6 +7,7 @@ use doublezero_serviceability::{ activate::MulticastGroupActivateArgs, closeaccount::MulticastGroupDeactivateArgs, create::*, delete::*, reactivate::*, suspend::*, update::*, }, + seeds::SEED_MULTICAST_GROUP, state::{accounttype::AccountType, multicastgroup::*}, }; use solana_program_test::*; @@ -51,7 +52,9 @@ async fn test_multicastgroup() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda_la, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "la"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -64,8 +67,10 @@ async fn test_multicastgroup() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_la, false), ], &payer, + &[], ) .await; @@ -164,12 +169,15 @@ async fn test_multicastgroup() { println!("✅ MulticastGroup reactivated"); /*****************************************************************************************************************************************************/ println!("4. Testing MulticastGroup update..."); - execute_transaction( + let (old_index_pda_la, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "la"); + let (new_index_pda_lb, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "lb"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::UpdateMulticastGroup(MulticastGroupUpdateArgs { - code: Some("la2".to_string()), + code: Some("lb".to_string()), multicast_ip: Some([239, 1, 1, 2].into()), max_bandwidth: Some(2000), // Keep publisher/subscriber counts at zero so that DeactivateMulticastGroup @@ -177,12 +185,16 @@ async fn test_multicastgroup() { publisher_count: None, subscriber_count: None, use_onchain_allocation: false, + rename_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(old_index_pda_la, false), + AccountMeta::new(new_index_pda_lb, false), ], &payer, + &[], ) .await; @@ -192,7 +204,7 @@ async fn test_multicastgroup() { .get_multicastgroup() .unwrap(); assert_eq!(multicastgroup_la.account_type, AccountType::MulticastGroup); - assert_eq!(multicastgroup_la.code, "la2".to_string()); + assert_eq!(multicastgroup_la.code, "lb".to_string()); assert_eq!(multicastgroup_la.multicast_ip, Ipv4Addr::new(239, 1, 1, 2)); assert_eq!(multicastgroup_la.max_bandwidth, 2000); assert_eq!(multicastgroup_la.publisher_count, 0); @@ -202,18 +214,23 @@ async fn test_multicastgroup() { println!("✅ MulticastGroup updated"); /*****************************************************************************************************************************************************/ println!("5. Testing MulticastGroup deletion..."); - execute_transaction( + let (index_pda_lb, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "lb"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, + close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_lb, false), ], &payer, + &[], ) .await; @@ -223,18 +240,20 @@ async fn test_multicastgroup() { .get_multicastgroup() .unwrap(); assert_eq!(multicastgroup_la.account_type, AccountType::MulticastGroup); - assert_eq!(multicastgroup_la.code, "la2".to_string()); + assert_eq!(multicastgroup_la.code, "lb".to_string()); assert_eq!(multicastgroup_la.status, MulticastGroupStatus::Deleting); println!("✅ MulticastGroup deleted"); /*****************************************************************************************************************************************************/ println!("6. Testing MulticastGroup deactivation (final delete)..."); + // Index account was already closed by DeleteMulticastGroup, so don't pass it here execute_transaction( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeactivateMulticastGroup(MulticastGroupDeactivateArgs { use_onchain_deallocation: false, + close_index: false, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), @@ -278,7 +297,9 @@ async fn test_multicastgroup_deactivate_fails_when_counts_nonzero() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda_la, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "la"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -291,8 +312,10 @@ async fn test_multicastgroup_deactivate_fails_when_counts_nonzero() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_la, false), ], &payer, + &[], ) .await; @@ -324,6 +347,7 @@ async fn test_multicastgroup_deactivate_fails_when_counts_nonzero() { publisher_count: Some(1), subscriber_count: Some(1), use_onchain_allocation: false, + rename_index: false, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), @@ -342,18 +366,21 @@ async fn test_multicastgroup_deactivate_fails_when_counts_nonzero() { assert_eq!(multicastgroup.subscriber_count, 1); // DeleteMulticastGroup should fail because counts are non-zero - let result = try_execute_transaction( + let result = try_execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, + close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_la, false), ], &payer, + &[], ) .await; @@ -400,7 +427,9 @@ async fn test_multicastgroup_deactivate_fails_when_not_deleting() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda_la, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "la"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -413,8 +442,10 @@ async fn test_multicastgroup_deactivate_fails_when_not_deleting() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_la, false), ], &payer, + &[], ) .await; @@ -442,19 +473,22 @@ async fn test_multicastgroup_deactivate_fails_when_not_deleting() { assert_eq!(multicastgroup.status, MulticastGroupStatus::Activated); // Try to deactivate without first deleting (status is Activated, not Deleting) - let result = try_execute_transaction( + let result = try_execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeactivateMulticastGroup(MulticastGroupDeactivateArgs { use_onchain_deallocation: false, + close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(multicastgroup.owner, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_la, false), ], &payer, + &[], ) .await; @@ -504,8 +538,10 @@ async fn test_multicastgroup_create_with_wrong_index_fails() { // Derive PDA with the WRONG index (what a malicious/buggy client might do) let (wrong_multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, wrong_index); + let (index_pda_test, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "test"); + // Try to create with wrong index - should fail - let result = try_execute_transaction( + let result = try_execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -518,8 +554,10 @@ async fn test_multicastgroup_create_with_wrong_index_fails() { vec![ AccountMeta::new(wrong_multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_test, false), ], &payer, + &[], ) .await; @@ -533,7 +571,7 @@ async fn test_multicastgroup_create_with_wrong_index_fails() { println!("3. Testing MulticastGroup creation with correct index..."); let (correct_multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, correct_index); - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -546,8 +584,10 @@ async fn test_multicastgroup_create_with_wrong_index_fails() { vec![ AccountMeta::new(correct_multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_test, false), ], &payer, + &[], ) .await; @@ -591,7 +631,9 @@ async fn test_multicastgroup_reactivate_invalid_status_fails() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "reactivate-test"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -604,8 +646,10 @@ async fn test_multicastgroup_reactivate_invalid_status_fails() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -665,7 +709,9 @@ async fn test_suspend_multicastgroup_from_pending_fails() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "test"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -678,8 +724,10 @@ async fn test_suspend_multicastgroup_from_pending_fails() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -743,7 +791,9 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "delete-test"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -756,8 +806,10 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -796,6 +848,7 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( publisher_count: Some(1), subscriber_count: None, use_onchain_allocation: false, + rename_index: false, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), @@ -806,18 +859,21 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( .await; println!("5. Try to delete with active publishers (should fail)..."); - let result = try_execute_transaction( + let result = try_execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, + close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -842,6 +898,7 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( publisher_count: Some(0), subscriber_count: Some(1), use_onchain_allocation: false, + rename_index: false, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), @@ -852,18 +909,21 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( .await; println!("7. Try to delete with active subscribers (should fail)..."); - let result = try_execute_transaction( + let result = try_execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, + close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -888,6 +948,7 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( publisher_count: Some(0), subscriber_count: Some(0), use_onchain_allocation: false, + rename_index: false, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), @@ -898,18 +959,21 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( .await; println!("9. Delete with zero counts (should succeed)..."); - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, + close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; diff --git a/smartcontract/programs/doublezero-serviceability/tests/test_helpers.rs b/smartcontract/programs/doublezero-serviceability/tests/test_helpers.rs index 7ce9d55cd1..307179077c 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/test_helpers.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/test_helpers.rs @@ -278,6 +278,98 @@ pub async fn execute_transaction_expect_failure( result } +/// Execute a transaction with extra accounts and expect it to fail. Returns the error result. +#[allow(dead_code)] +pub async fn execute_transaction_expect_failure_with_extra_accounts( + banks_client: &mut BanksClient, + _recent_blockhash: solana_program::hash::Hash, + program_id: Pubkey, + instruction: DoubleZeroInstruction, + accounts: Vec, + payer: &Keypair, + extra_accounts: &[AccountMeta], +) -> Result<(), BanksClientError> { + print!("➡️ Transaction (expecting failure) {instruction:?} "); + + let recent_blockhash = banks_client + .get_latest_blockhash() + .await + .expect("Failed to get latest blockhash"); + let mut transaction = create_transaction_with_extra_accounts( + program_id, + &instruction, + &accounts, + payer, + extra_accounts, + ); + transaction.try_sign(&[&payer], recent_blockhash).unwrap(); + let result = banks_client.process_transaction(transaction).await; + + if result.is_err() { + println!("❌ (expected)"); + } else { + println!("✅ (unexpected success)"); + } + + result +} + +#[allow(dead_code)] +pub async fn execute_transaction_with_extra_accounts( + banks_client: &mut BanksClient, + _recent_blockhash: solana_program::hash::Hash, + program_id: Pubkey, + instruction: DoubleZeroInstruction, + accounts: Vec, + payer: &Keypair, + extra_accounts: &[AccountMeta], +) { + print!("➡️ Transaction {instruction:?} "); + + let recent_blockhash = banks_client + .get_latest_blockhash() + .await + .expect("Failed to get latest blockhash"); + let mut transaction = create_transaction_with_extra_accounts( + program_id, + &instruction, + &accounts, + payer, + extra_accounts, + ); + transaction.try_sign(&[&payer], recent_blockhash).unwrap(); + banks_client.process_transaction(transaction).await.unwrap(); + + println!("✅") +} + +#[allow(dead_code)] +pub async fn try_execute_transaction_with_extra_accounts( + banks_client: &mut BanksClient, + recent_blockhash: solana_program::hash::Hash, + program_id: Pubkey, + instruction: DoubleZeroInstruction, + accounts: Vec, + payer: &Keypair, + extra_accounts: &[AccountMeta], +) -> Result<(), BanksClientError> { + print!("➡️ Transaction {instruction:?} "); + + let mut transaction = create_transaction_with_extra_accounts( + program_id, + &instruction, + &accounts, + payer, + extra_accounts, + ); + transaction.try_sign(&[&payer], recent_blockhash).unwrap(); + banks_client.process_transaction(transaction).await?; + + println!("✅"); + + Ok(()) +} + pub fn create_transaction( program_id: Pubkey, instruction: &DoubleZeroInstruction, diff --git a/smartcontract/programs/doublezero-serviceability/tests/user_onchain_allocation_test.rs b/smartcontract/programs/doublezero-serviceability/tests/user_onchain_allocation_test.rs index 1fd5eff0c1..b430049a44 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/user_onchain_allocation_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/user_onchain_allocation_test.rs @@ -14,8 +14,8 @@ use doublezero_serviceability::{ instructions::DoubleZeroInstruction, pda::{ get_accesspass_pda, get_contributor_pda, get_device_pda, get_exchange_pda, - get_globalconfig_pda, get_globalstate_pda, get_location_pda, get_multicastgroup_pda, - get_program_config_pda, get_resource_extension_pda, get_user_pda, + get_globalconfig_pda, get_globalstate_pda, get_index_pda, get_location_pda, + get_multicastgroup_pda, get_program_config_pda, get_resource_extension_pda, get_user_pda, }, processors::{ accesspass::set::SetAccessPassArgs, @@ -42,6 +42,7 @@ use doublezero_serviceability::{ }, }, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, state::{ accesspass::AccessPassType, device::DeviceType, @@ -1100,8 +1101,10 @@ async fn test_multicast_subscribe_reactivation_preserves_allocations() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); - // Create multicast group (4 accounts: mgroup, globalstate, payer, system_program) - execute_transaction( + let (index_pda_mgroup, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "test-mgroup"); + + // Create multicast group (4 accounts: mgroup, globalstate, payer, system_program, index) + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -1114,8 +1117,10 @@ async fn test_multicast_subscribe_reactivation_preserves_allocations() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_mgroup, false), ], &payer, + &[], ) .await; @@ -1417,7 +1422,9 @@ async fn test_multicast_publisher_block_deallocation_and_reuse() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); - execute_transaction( + let (index_pda_mgroup, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "test-mgroup"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -1430,8 +1437,10 @@ async fn test_multicast_publisher_block_deallocation_and_reuse() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_mgroup, false), ], &payer, + &[], ) .await; @@ -3639,6 +3648,8 @@ async fn test_activate_updating_does_not_set_multicast_publisher_for_non_publish let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + let (index_pda_mgroup, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "pub-test-mgroup"); + execute_transaction( &mut banks_client, recent_blockhash, @@ -3652,6 +3663,7 @@ async fn test_activate_updating_does_not_set_multicast_publisher_for_non_publish vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_mgroup, false), ], &payer, ) @@ -3891,6 +3903,8 @@ async fn test_delete_user_atomic_decrements_subscribers_count_for_non_publisher( let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + let (index_pda_mgroup, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "pub-del-mgroup"); + execute_transaction( &mut banks_client, recent_blockhash, @@ -3904,6 +3918,7 @@ async fn test_delete_user_atomic_decrements_subscribers_count_for_non_publisher( vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_mgroup, false), ], &payer, ) @@ -4161,6 +4176,8 @@ async fn test_delete_user_atomic_decrements_multicast_subscribers_count() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + let (index_pda_mgroup, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "sub-del-mgroup"); + execute_transaction( &mut banks_client, recent_blockhash, @@ -4174,6 +4191,7 @@ async fn test_delete_user_atomic_decrements_multicast_subscribers_count() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_mgroup, false), ], &payer, ) @@ -4459,6 +4477,8 @@ async fn test_closeaccount_user_legacy_after_publisher_unsubscribed_decrements_s let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + let (index_pda_mgroup, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "ca-pub-mgroup"); + execute_transaction( &mut banks_client, recent_blockhash, @@ -4472,6 +4492,7 @@ async fn test_closeaccount_user_legacy_after_publisher_unsubscribed_decrements_s vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_mgroup, false), ], &payer, ) @@ -4794,6 +4815,8 @@ async fn test_closeaccount_user_legacy_decrements_subscribers_count_for_non_publ let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + let (index_pda_mgroup, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "pub-mgroup"); + execute_transaction( &mut banks_client, recent_blockhash, @@ -4807,6 +4830,7 @@ async fn test_closeaccount_user_legacy_decrements_subscribers_count_for_non_publ vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_mgroup, false), ], &payer, )