diff --git a/activator/src/process/multicastgroup.rs b/activator/src/process/multicastgroup.rs index b00371b54b..9e80be91a0 100644 --- a/activator/src/process/multicastgroup.rs +++ b/activator/src/process/multicastgroup.rs @@ -398,6 +398,14 @@ mod tests { // Insert it first so it can be removed multicastgroups.insert(pubkey, multicastgroup.clone()); + // 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 client .expect_execute_transaction() diff --git a/e2e/compatibility_test.go b/e2e/compatibility_test.go index 8fc0f192db..377d8336db 100644 --- a/e2e/compatibility_test.go +++ b/e2e/compatibility_test.go @@ -91,24 +91,22 @@ func before(v string) []versionRange { } var knownIncompatibilities = map[string]knownIncompat{ - // multicast_group_create: The MulticastGroupCreateArgs Borsh struct changed in v0.8.1. - // The index and bump_seed fields were removed. Older CLIs send the old format which - // causes Borsh deserialization failure in the current program. - "write/multicast_group_create": {ranges: before("0.8.1")}, + // multicast_group_create: The CreateMulticastGroup instruction now requires an Index + // account for unique code enforcement. All CLIs before v0.17.0 don't pass this account, + // causing "insufficient account keys for instruction". + "write/multicast_group_create": {ranges: before("0.17.0")}, // All multicast operations that depend on multicast_group_create. When the group - // can't be created (< 0.8.1), these all fail with "MulticastGroup not found". - "write/multicast_group_wait_activated": {ranges: before("0.8.1")}, - // multicast_group_update: In addition to the dependency above, v0.8.1-v0.8.8 parsed - // --max-bandwidth as a plain integer. v0.8.9 added validate_parse_bandwidth (a855ca7a) - // which accepts unit strings like "200Mbps". - "write/multicast_group_update": {ranges: before("0.8.9")}, - "write/multicast_group_pub_allowlist_add": {ranges: before("0.8.1")}, - "write/multicast_group_pub_allowlist_remove": {ranges: before("0.8.1")}, - "write/multicast_group_sub_allowlist_add": {ranges: before("0.8.1")}, - "write/multicast_group_sub_allowlist_remove": {ranges: before("0.8.1")}, - "write/multicast_group_get": {ranges: before("0.8.1")}, - "write/multicast_group_delete": {ranges: before("0.8.1")}, + // can't be created, these all fail with "MulticastGroup not found". + "write/multicast_group_wait_activated": {ranges: before("0.17.0")}, + "write/multicast_group_update": {ranges: before("0.17.0")}, + "write/multicast_group_pub_allowlist_add": {ranges: before("0.17.0")}, + "write/multicast_group_pub_allowlist_remove": {ranges: before("0.17.0")}, + "write/multicast_group_sub_allowlist_add": {ranges: before("0.17.0")}, + "write/multicast_group_sub_allowlist_remove": {ranges: before("0.17.0")}, + "write/multicast_group_get": {ranges: before("0.17.0")}, + "write/multicast_group_delete": {ranges: before("0.17.0")}, + "write/user_subscribe": {ranges: before("0.17.0")}, // set-health commands: The CLI subcommand was added in commit eb7ea308 (Jan 16). // mainnet-beta v0.8.2 was built Jan 13 (before set-health) → doesn't have it. diff --git a/sdk/serviceability/python/serviceability/state.py b/sdk/serviceability/python/serviceability/state.py index 1a61b961d9..575525bd75 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 d8ac7ffba1..fdfb8ae031 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/instructions.rs b/smartcontract/programs/doublezero-serviceability/src/instructions.rs index fbd60d4b1f..13686894ca 100644 --- a/smartcontract/programs/doublezero-serviceability/src/instructions.rs +++ b/smartcontract/programs/doublezero-serviceability/src/instructions.rs @@ -993,6 +993,7 @@ mod tests { publisher_count: None, subscriber_count: None, use_onchain_allocation: false, + rename_index: false, }), "UpdateMulticastGroup", ); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs index 0b2ffbcdd7..af672f7a96 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, - processors::resource::deallocate_ip, + 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, - state::{globalstate::GlobalState, multicastgroup::*}, + state::{globalstate::GlobalState, index::Index, multicastgroup::*}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -47,11 +48,10 @@ 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): - // [multicastgroup, owner, globalstate, payer, system] + // Account layout WITH deallocation: + // [multicastgroup, owner, globalstate, multicast_group_block, index, payer, system] + // Account layout WITHOUT deallocation: + // [multicastgroup, owner, globalstate, index, payer, system] let resource_extension_account = if value.use_onchain_deallocation { let multicast_group_block_ext = next_account_info(accounts_iter)?; Some(multicast_group_block_ext) @@ -59,6 +59,7 @@ pub fn process_closeaccount_multicastgroup( 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)?; @@ -68,25 +69,24 @@ pub fn process_closeaccount_multicastgroup( // Check if the payer is a signer assert!(payer_account.is_signer, "Payer must be a signer"); - // Check the owner of the accounts - assert_eq!( - multicastgroup_account.owner, program_id, - "Invalid PDA Account Owner" + // Validate accounts + validate_program_account!( + multicastgroup_account, + program_id, + writable = true, + "MulticastGroup" ); - assert_eq!( - globalstate_account.owner, program_id, - "Invalid GlobalState Account Owner" + validate_program_account!( + globalstate_account, + program_id, + writable = false, + "GlobalState" ); assert_eq!( *system_program.unsigned_key(), solana_system_interface::program::ID, "Invalid System Program Account Owner" ); - // Check if the account is writable - assert!( - multicastgroup_account.is_writable, - "PDA Account is not writable" - ); let globalstate = GlobalState::try_from(globalstate_account)?; if globalstate.activator_authority_pk != *payer_account.key { @@ -107,25 +107,14 @@ pub fn process_closeaccount_multicastgroup( // Deallocate multicast_ip from ResourceExtension if account provided // Deallocation is idempotent - safe to call even if resource wasn't allocated if let Some(multicast_group_block_ext) = resource_extension_account { - // Validate multicast_group_block_ext (MulticastGroupBlock - global) - assert_eq!( - multicast_group_block_ext.owner, program_id, - "Invalid ResourceExtension Account Owner for MulticastGroupBlock" - ); - assert!( - multicast_group_block_ext.is_writable, - "ResourceExtension Account for MulticastGroupBlock is not writable" - ); - assert!( - !multicast_group_block_ext.data_is_empty(), - "ResourceExtension Account for MulticastGroupBlock is empty" - ); - - let (expected_multicast_group_pda, _, _) = + let (expected_pda, _, _) = get_resource_extension_pda(program_id, ResourceType::MulticastGroupBlock); - assert_eq!( - multicast_group_block_ext.key, &expected_multicast_group_pda, - "Invalid ResourceExtension PDA for MulticastGroupBlock" + validate_program_account!( + multicast_group_block_ext, + program_id, + writable = true, + pda = &expected_pda, + "MulticastGroupBlock" ); // Deallocate multicast_ip from global MulticastGroupBlock @@ -137,6 +126,27 @@ pub fn process_closeaccount_multicastgroup( try_acc_close(multicastgroup_account, owner_account)?; + // Close the Index account (skip for pre-migration accounts using Pubkey::default()) + if *index_account.key != Pubkey::default() { + let (expected_index_pda, _) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &multicastgroup.code); + validate_program_account!( + index_account, + program_id, + writable = true, + pda = &expected_index_pda, + "Index" + ); + + let index = Index::try_from(index_account)?; + assert_eq!( + index.pk, *multicastgroup_account.key, + "Index does not point to this MulticastGroup" + ); + + try_acc_close(index_account, 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 88ceeb170c..33ea0c1dbc 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,18 +82,20 @@ 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!( - globalstate_account.owner, program_id, - "Invalid GlobalState Account Owner" + // Validate accounts + validate_program_account!( + globalstate_account, + program_id, + writable = true, + "GlobalState" ); assert_eq!( *system_program.unsigned_key(), solana_system_interface::program::ID, "Invalid System Program Account Owner" ); - // Check if the account is writable assert!(mgroup_account.is_writable, "PDA Account is not writable"); // Parse the global state account & check if the payer is in the allowlist @@ -114,6 +118,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 +155,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 +179,31 @@ 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, + entity_account_type: AccountType::MulticastGroup, + key: multicastgroup.code.clone(), + 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 53535570aa..1b166db561 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::*, }, }; @@ -50,11 +52,10 @@ 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): - // [mgroup, globalstate, payer, system] + // Account layout WITH deallocation: + // [mgroup, globalstate, multicast_group_block, owner, index, payer, system] + // Account layout WITHOUT deallocation: + // [mgroup, globalstate, index, payer, system] let deallocation_accounts = if value.use_onchain_deallocation { let multicast_group_block_ext = next_account_info(accounts_iter)?; let owner_account = next_account_info(accounts_iter)?; @@ -63,6 +64,7 @@ pub fn process_delete_multicastgroup( 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)?; @@ -72,24 +74,24 @@ pub fn process_delete_multicastgroup( // Check if the payer is a signer assert!(payer_account.is_signer, "Payer must be a signer"); - // Check the owner of the accounts - assert_eq!( - multicastgroup_account.owner, program_id, - "Invalid PDA Account Owner" + // Validate accounts + validate_program_account!( + multicastgroup_account, + program_id, + writable = true, + "MulticastGroup" ); - assert_eq!( - globalstate_account.owner, program_id, - "Invalid GlobalState Account Owner" + validate_program_account!( + globalstate_account, + program_id, + writable = false, + "GlobalState" ); assert_eq!( *system_program.unsigned_key(), solana_system_interface::program::ID, "Invalid System Program Account Owner" ); - assert!( - multicastgroup_account.is_writable, - "PDA Account is not writable" - ); // Parse the global state account & check if the payer is in the allowlist let globalstate = GlobalState::try_from(globalstate_account)?; @@ -98,6 +100,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 +161,27 @@ pub fn process_delete_multicastgroup( msg!("Deleted: {:?}", multicastgroup_account); } + // Close the Index account (skip for pre-migration accounts using Pubkey::default()) + if *index_account.key != Pubkey::default() { + let (expected_index_pda, _) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &multicastgroup_code); + validate_program_account!( + index_account, + program_id, + writable = true, + pda = &expected_index_pda, + "Index" + ); + + // Verify it's an Index account pointing to this multicast group + let index = Index::try_from(index_account)?; + assert_eq!( + index.pk, *multicastgroup_account.key, + "Index does not point to this MulticastGroup" + ); + + try_acc_close(index_account, 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 2630cd7f20..f78a292631 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)?; @@ -79,24 +96,24 @@ pub fn process_update_multicastgroup( // Check if the payer is a signer assert!(payer_account.is_signer, "Payer must be a signer"); - // Check the owner of the accounts - assert_eq!( - multicastgroup_account.owner, program_id, - "Invalid PDA Account Owner" + // Validate accounts + validate_program_account!( + multicastgroup_account, + program_id, + writable = true, + "MulticastGroup" ); - assert_eq!( - globalstate_account.owner, program_id, - "Invalid GlobalState Account Owner" + validate_program_account!( + globalstate_account, + program_id, + writable = false, + "GlobalState" ); assert_eq!( *system_program.unsigned_key(), solana_system_interface::program::ID, "Invalid System Program Account Owner" ); - assert!( - multicastgroup_account.is_writable, - "PDA Account is not writable" - ); // Parse the global state account & check if the payer is in the allowlist let globalstate = GlobalState::try_from(globalstate_account)?; if !globalstate.foundation_allowlist.contains(payer_account.key) { @@ -107,8 +124,78 @@ 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); + validate_program_account!( + old_index_account, + program_id, + writable = true, + pda = &expected_old_index_pda, + "Old Index" + ); + + // 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, + entity_account_type: AccountType::MulticastGroup, + key: new_code.clone(), + 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/tests/create_subscribe_user_test.rs b/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs index c18cab5ec4..4b8e4cc93d 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, @@ -296,7 +298,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, @@ -309,8 +313,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; @@ -881,7 +887,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, @@ -894,8 +902,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; @@ -1730,6 +1740,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, @@ -1743,6 +1754,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, ) @@ -2087,6 +2099,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, @@ -2100,6 +2113,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, ) @@ -2426,6 +2440,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, @@ -2439,6 +2454,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 index 40de6b9a43..5524a91e66 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/index_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/index_test.rs @@ -18,7 +18,7 @@ use solana_sdk::{ mod test_helpers; use test_helpers::*; -/// Helper: create a multicast group and return its pubkey. +/// 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, @@ -30,8 +30,9 @@ async fn create_multicast_group( 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( + execute_transaction_with_extra_accounts( banks_client, recent_blockhash, program_id, @@ -44,8 +45,10 @@ async fn create_multicast_group( vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], payer, + &[], ) .await; 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..75a275e1f2 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,7 +256,7 @@ 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, @@ -251,8 +268,10 @@ async fn test_delete_multicastgroup_atomic_with_deallocation() { 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 +313,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,13 +330,15 @@ 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, @@ -325,8 +348,10 @@ async fn test_delete_multicastgroup_atomic_backward_compat() { vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -369,8 +394,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 +411,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 +439,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 +488,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 +505,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 +545,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 +563,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 +578,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 +602,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 f686bb161f..18da129a30 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs @@ -22,6 +22,7 @@ use doublezero_serviceability::{ user::{activate::UserActivateArgs, create::UserCreateArgs}, }, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, state::{ accesspass::AccessPassType, device::DeviceType, @@ -262,7 +263,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, @@ -275,8 +278,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; @@ -297,7 +302,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, @@ -310,8 +317,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..bd73ef8d64 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,7 +214,9 @@ 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, @@ -212,8 +226,10 @@ async fn test_multicastgroup() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_lb, false), ], &payer, + &[], ) .await; @@ -223,13 +239,14 @@ 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)..."); - execute_transaction( + // Index account was already closed by DeleteMulticastGroup, pass Pubkey::default() to skip + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -240,8 +257,10 @@ async fn test_multicastgroup() { AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(multicastgroup.owner, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(Pubkey::default(), false), ], &payer, + &[], ) .await; @@ -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,7 +366,7 @@ 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, @@ -352,8 +376,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; @@ -400,7 +426,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 +441,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,7 +472,7 @@ 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, @@ -453,8 +483,10 @@ async fn test_multicastgroup_deactivate_fails_when_not_deleting() { 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 +536,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 +552,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 +569,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 +582,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 +629,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 +644,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 +707,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 +722,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 +789,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 +804,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 +846,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,7 +857,7 @@ 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, @@ -816,8 +867,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; @@ -842,6 +895,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,7 +906,7 @@ 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, @@ -862,8 +916,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; @@ -888,6 +944,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,7 +955,7 @@ 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, @@ -908,8 +965,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; 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 2bd2446220..79971bda7d 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; @@ -1418,7 +1423,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, @@ -1431,8 +1438,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; @@ -3643,6 +3652,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, @@ -3656,6 +3667,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, ) @@ -3896,6 +3908,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, @@ -3909,6 +3923,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, ) @@ -4168,6 +4183,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, @@ -4181,6 +4198,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, ) @@ -4468,6 +4486,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, @@ -4481,6 +4501,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, ) @@ -4805,6 +4826,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, @@ -4818,6 +4841,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, ) diff --git a/smartcontract/sdk/go/serviceability/state.go b/smartcontract/sdk/go/serviceability/state.go index 065614c95e..f18bc1eb3a 100644 --- a/smartcontract/sdk/go/serviceability/state.go +++ b/smartcontract/sdk/go/serviceability/state.go @@ -26,6 +26,7 @@ const ( TenantType // 13 // 14 is reserved PermissionType AccountType = 15 + IndexType AccountType = 16 ) type LocationStatus uint8 diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/publisher/add.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/publisher/add.rs index 11b4481702..1e8ea38d41 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/publisher/add.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/publisher/add.rs @@ -93,6 +93,11 @@ mod tests { map.insert(pubkey, AccountData::MulticastGroup(cloned_mgroup.clone())); Ok(map) }); + // Catch-all for Index PDA lookups + client + .expect_get() + .with(predicate::always()) + .returning(|_| Err(eyre::eyre!("Account not found"))); client .expect_execute_transaction() .with( diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/publisher/remove.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/publisher/remove.rs index b4adf443ce..a640fa11c7 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/publisher/remove.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/publisher/remove.rs @@ -95,6 +95,11 @@ mod tests { map.insert(pubkey, AccountData::MulticastGroup(cloned_mgroup.clone())); Ok(map) }); + // Catch-all for Index PDA lookups + client + .expect_get() + .with(predicate::always()) + .returning(|_| Err(eyre::eyre!("Account not found"))); client .expect_execute_transaction() .with( diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/subscriber/add.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/subscriber/add.rs index 60b79ea235..3d49425404 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/subscriber/add.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/subscriber/add.rs @@ -95,6 +95,11 @@ mod tests { map.insert(pubkey, AccountData::MulticastGroup(cloned_mgroup.clone())); Ok(map) }); + // Catch-all for Index PDA lookups + client + .expect_get() + .with(predicate::always()) + .returning(|_| Err(eyre::eyre!("Account not found"))); client .expect_execute_transaction() .with( diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/subscriber/remove.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/subscriber/remove.rs index 2a3431c710..548bae5322 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/subscriber/remove.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/subscriber/remove.rs @@ -95,6 +95,11 @@ mod tests { map.insert(pubkey, AccountData::MulticastGroup(cloned_mgroup.clone())); Ok(map) }); + // Catch-all for Index PDA lookups + client + .expect_get() + .with(predicate::always()) + .returning(|_| Err(eyre::eyre!("Account not found"))); client .expect_execute_transaction() .with( diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/create.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/create.rs index 01631b24f1..32a0788402 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/create.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/create.rs @@ -1,9 +1,10 @@ use doublezero_program_common::validate_account_code; use doublezero_serviceability::{ instructions::DoubleZeroInstruction, - pda::{get_multicastgroup_pda, get_resource_extension_pda}, + pda::{get_index_pda, get_multicastgroup_pda, get_resource_extension_pda}, processors::multicastgroup::create::MulticastGroupCreateArgs, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, state::feature_flags::{is_feature_enabled, FeatureFlag}, }; use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, signature::Signature}; @@ -45,6 +46,10 @@ impl CreateMulticastGroupCommand { accounts.push(AccountMeta::new(multicast_group_block_ext, false)); } + // Index account (payer and system_program are appended by the framework) + let (index_pda, _) = get_index_pda(&client.get_program_id(), SEED_MULTICAST_GROUP, &code); + accounts.push(AccountMeta::new(index_pda, false)); + client .execute_transaction( DoubleZeroInstruction::CreateMulticastGroup(MulticastGroupCreateArgs { @@ -67,9 +72,12 @@ mod tests { }; use doublezero_serviceability::{ instructions::DoubleZeroInstruction, - pda::{get_globalstate_pda, get_multicastgroup_pda, get_resource_extension_pda}, + pda::{ + get_globalstate_pda, get_index_pda, get_multicastgroup_pda, get_resource_extension_pda, + }, processors::multicastgroup::create::MulticastGroupCreateArgs, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, state::{ accountdata::AccountData, accounttype::AccountType, feature_flags::FeatureFlag, globalstate::GlobalState, @@ -84,6 +92,8 @@ mod tests { let (globalstate_pubkey, _globalstate) = get_globalstate_pda(&client.get_program_id()); let (pda_pubkey, _) = get_multicastgroup_pda(&client.get_program_id(), 1); + let (index_pda, _) = + get_index_pda(&client.get_program_id(), SEED_MULTICAST_GROUP, "test_group"); client .expect_execute_transaction() @@ -99,6 +109,7 @@ mod tests { predicate::eq(vec![ AccountMeta::new(pda_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ]), ) .returning(|_, _| Ok(Signature::new_unique())); @@ -155,6 +166,7 @@ mod tests { let (pda_pubkey, _) = get_multicastgroup_pda(&program_id, 1); let (multicast_group_block_ext, _, _) = get_resource_extension_pda(&program_id, ResourceType::MulticastGroupBlock); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "test_group"); let owner = Pubkey::new_unique(); client @@ -172,6 +184,7 @@ mod tests { AccountMeta::new(pda_pubkey, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_ext, false), + AccountMeta::new(index_pda, false), ]), ) .returning(|_, _| Ok(Signature::new_unique())); diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/deactivate.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/deactivate.rs index f9169d13dc..03c02f9654 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/deactivate.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/deactivate.rs @@ -1,7 +1,15 @@ -use crate::{commands::globalstate::get::GetGlobalStateCommand, DoubleZeroClient}; +use crate::{ + commands::{ + globalstate::get::GetGlobalStateCommand, multicastgroup::get::GetMulticastGroupCommand, + }, + DoubleZeroClient, +}; use doublezero_serviceability::{ - instructions::DoubleZeroInstruction, pda::get_resource_extension_pda, - processors::multicastgroup::closeaccount::MulticastGroupDeactivateArgs, resource::ResourceType, + instructions::DoubleZeroInstruction, + pda::{get_index_pda, get_resource_extension_pda}, + processors::multicastgroup::closeaccount::MulticastGroupDeactivateArgs, + resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, }; use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, signature::Signature}; @@ -20,6 +28,12 @@ impl DeactivateMulticastGroupCommand { .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; + let (_, mgroup) = GetMulticastGroupCommand { + pubkey_or_code: self.pubkey.to_string(), + } + .execute(client) + .map_err(|_err| eyre::eyre!("MulticastGroup not found"))?; + let mut accounts = vec![ AccountMeta::new(self.pubkey, false), AccountMeta::new(self.owner, false), @@ -27,7 +41,6 @@ impl DeactivateMulticastGroupCommand { ]; if self.use_onchain_deallocation { - // Global MulticastGroupBlock (for multicast_ip deallocation) let (multicast_group_block_ext, _, _) = get_resource_extension_pda( &client.get_program_id(), ResourceType::MulticastGroupBlock, @@ -35,6 +48,11 @@ impl DeactivateMulticastGroupCommand { accounts.push(AccountMeta::new(multicast_group_block_ext, false)); } + // Close the associated Index account + let (index_pda, _) = + get_index_pda(&client.get_program_id(), SEED_MULTICAST_GROUP, &mgroup.code); + accounts.push(AccountMeta::new(index_pda, false)); + client.execute_transaction( DoubleZeroInstruction::DeactivateMulticastGroup(MulticastGroupDeactivateArgs { use_onchain_deallocation: self.use_onchain_deallocation, @@ -47,25 +65,82 @@ impl DeactivateMulticastGroupCommand { #[cfg(test)] mod tests { use crate::{ - commands::multicastgroup::deactivate::DeactivateMulticastGroupCommand, - tests::utils::create_test_client, DoubleZeroClient, + commands::multicastgroup::deactivate::DeactivateMulticastGroupCommand, MockDoubleZeroClient, }; use doublezero_serviceability::{ instructions::DoubleZeroInstruction, - pda::{get_globalstate_pda, get_location_pda, get_resource_extension_pda}, + pda::{get_globalstate_pda, get_index_pda, get_location_pda, get_resource_extension_pda}, processors::multicastgroup::closeaccount::MulticastGroupDeactivateArgs, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, + state::{ + accountdata::AccountData, + accounttype::AccountType, + globalstate::GlobalState, + multicastgroup::{MulticastGroup, MulticastGroupStatus}, + }, }; use mockall::predicate; - use solana_sdk::{instruction::AccountMeta, signature::Signature}; + use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, signature::Signature}; + use std::net::Ipv4Addr; + + fn make_test_mgroup(owner: Pubkey) -> MulticastGroup { + MulticastGroup { + account_type: AccountType::MulticastGroup, + index: 2, + bump_seed: 0, + tenant_pk: Pubkey::default(), + code: "mg01".to_string(), + multicast_ip: Ipv4Addr::UNSPECIFIED, + max_bandwidth: 0, + status: MulticastGroupStatus::Activated, + owner, + publisher_count: 1, + subscriber_count: 0, + } + } #[test] fn test_commands_multicastgroup_deactivate_without_resource_extension() { - let mut client = create_test_client(); + let mut client = MockDoubleZeroClient::new(); + + let payer = Pubkey::new_unique(); + client.expect_get_payer().returning(move || payer); + let program_id = Pubkey::new_unique(); + client.expect_get_program_id().returning(move || program_id); + + let (globalstate_pubkey, bump_seed) = get_globalstate_pda(&program_id); + let globalstate = GlobalState { + account_type: AccountType::GlobalState, + bump_seed, + account_index: 0, + foundation_allowlist: vec![], + _device_allowlist: vec![], + _user_allowlist: vec![], + activator_authority_pk: Pubkey::new_unique(), + sentinel_authority_pk: Pubkey::new_unique(), + contributor_airdrop_lamports: 1_000_000_000, + user_airdrop_lamports: 40_000, + health_oracle_pk: Pubkey::new_unique(), + qa_allowlist: vec![], + feature_flags: 0, + feed_authority_pk: Pubkey::default(), + }; + client + .expect_get() + .with(predicate::eq(globalstate_pubkey)) + .returning(move |_| Ok(AccountData::GlobalState(globalstate.clone()))); - let (globalstate_pubkey, _globalstate) = get_globalstate_pda(&client.get_program_id()); - let (pda_pubkey, _) = get_location_pda(&client.get_program_id(), 1); - let payer = client.get_payer(); + let (pda_pubkey, _) = get_location_pda(&program_id, 1); + + let mgroup = make_test_mgroup(payer); + let mgroup_cloned = mgroup.clone(); + client + .expect_get() + .with(predicate::eq(pda_pubkey)) + .returning(move |_| Ok(AccountData::MulticastGroup(mgroup_cloned.clone()))); + + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg01"); client .expect_execute_transaction() @@ -79,6 +154,7 @@ mod tests { AccountMeta::new(pda_pubkey, false), AccountMeta::new(payer, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ]), ) .returning(|_, _| Ok(Signature::new_unique())); @@ -95,15 +171,49 @@ mod tests { #[test] fn test_commands_multicastgroup_deactivate_with_onchain_deallocation() { - let mut client = create_test_client(); + let mut client = MockDoubleZeroClient::new(); + + let payer = Pubkey::new_unique(); + client.expect_get_payer().returning(move || payer); + let program_id = Pubkey::new_unique(); + client.expect_get_program_id().returning(move || program_id); + + let (globalstate_pubkey, bump_seed) = get_globalstate_pda(&program_id); + let globalstate = GlobalState { + account_type: AccountType::GlobalState, + bump_seed, + account_index: 0, + foundation_allowlist: vec![], + _device_allowlist: vec![], + _user_allowlist: vec![], + activator_authority_pk: Pubkey::new_unique(), + sentinel_authority_pk: Pubkey::new_unique(), + contributor_airdrop_lamports: 1_000_000_000, + user_airdrop_lamports: 40_000, + health_oracle_pk: Pubkey::new_unique(), + qa_allowlist: vec![], + feature_flags: 0, + feed_authority_pk: Pubkey::default(), + }; + client + .expect_get() + .with(predicate::eq(globalstate_pubkey)) + .returning(move |_| Ok(AccountData::GlobalState(globalstate.clone()))); - let (globalstate_pubkey, _globalstate) = get_globalstate_pda(&client.get_program_id()); - let (pda_pubkey, _) = get_location_pda(&client.get_program_id(), 1); - let payer = client.get_payer(); + let (pda_pubkey, _) = get_location_pda(&program_id, 1); + + let mgroup = make_test_mgroup(payer); + let mgroup_cloned = mgroup.clone(); + client + .expect_get() + .with(predicate::eq(pda_pubkey)) + .returning(move |_| Ok(AccountData::MulticastGroup(mgroup_cloned.clone()))); // Compute ResourceExtension PDA let (multicast_group_block_ext, _, _) = - get_resource_extension_pda(&client.get_program_id(), ResourceType::MulticastGroupBlock); + get_resource_extension_pda(&program_id, ResourceType::MulticastGroupBlock); + + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg01"); client .expect_execute_transaction() @@ -118,6 +228,7 @@ mod tests { AccountMeta::new(payer, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_ext, false), + AccountMeta::new(index_pda, false), ]), ) .returning(|_, _| Ok(Signature::new_unique())); diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/delete.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/delete.rs index e8d2e27f55..60a6de7d41 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/delete.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/delete.rs @@ -6,9 +6,10 @@ use crate::{ }; use doublezero_serviceability::{ instructions::DoubleZeroInstruction, - pda::get_resource_extension_pda, + pda::{get_index_pda, get_resource_extension_pda}, processors::multicastgroup::delete::MulticastGroupDeleteArgs, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, state::feature_flags::{is_feature_enabled, FeatureFlag}, }; use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, signature::Signature}; @@ -29,18 +30,18 @@ impl DeleteMulticastGroupCommand { let mgroup_pubkey = self.pubkey; + let (_, mgroup) = GetMulticastGroupCommand { + pubkey_or_code: self.pubkey.to_string(), + } + .execute(client) + .map_err(|_err| eyre::eyre!("MulticastGroup not found"))?; + let mut accounts = vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), ]; if use_onchain_deallocation { - let (_, mgroup) = GetMulticastGroupCommand { - pubkey_or_code: self.pubkey.to_string(), - } - .execute(client) - .map_err(|_err| eyre::eyre!("MulticastGroup not found"))?; - let (multicast_group_block_ext, _, _) = get_resource_extension_pda( &client.get_program_id(), ResourceType::MulticastGroupBlock, @@ -49,6 +50,11 @@ impl DeleteMulticastGroupCommand { accounts.push(AccountMeta::new(mgroup.owner, false)); } + // Close the associated Index account + let (index_pda, _) = + get_index_pda(&client.get_program_id(), SEED_MULTICAST_GROUP, &mgroup.code); + accounts.push(AccountMeta::new(index_pda, false)); + client.execute_transaction( DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation, @@ -66,9 +72,12 @@ mod tests { }; use doublezero_serviceability::{ instructions::DoubleZeroInstruction, - pda::{get_globalstate_pda, get_multicastgroup_pda, get_resource_extension_pda}, + pda::{ + get_globalstate_pda, get_index_pda, get_multicastgroup_pda, get_resource_extension_pda, + }, processors::multicastgroup::delete::MulticastGroupDeleteArgs, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, state::{ accountdata::AccountData, accounttype::AccountType, @@ -101,8 +110,9 @@ mod tests { fn test_commands_multicastgroup_delete_legacy() { let mut client = create_test_client(); - let (globalstate_pubkey, _globalstate) = get_globalstate_pda(&client.get_program_id()); - let (pda_pubkey, bump_seed) = get_multicastgroup_pda(&client.get_program_id(), 1); + let program_id = client.get_program_id(); + let (globalstate_pubkey, _globalstate) = get_globalstate_pda(&program_id); + let (pda_pubkey, bump_seed) = get_multicastgroup_pda(&program_id, 1); let mgroup = make_test_mgroup(Pubkey::default(), bump_seed); @@ -112,6 +122,8 @@ mod tests { .with(predicate::eq(pda_pubkey)) .returning(move |_| Ok(AccountData::MulticastGroup(mgroup_cloned.clone()))); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg01"); + client .expect_execute_transaction() .with( @@ -123,6 +135,7 @@ mod tests { predicate::eq(vec![ AccountMeta::new(pda_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ]), ) .returning(|_, _| Ok(Signature::new_unique())); @@ -176,6 +189,8 @@ mod tests { let (multicast_group_block_ext, _, _) = get_resource_extension_pda(&program_id, ResourceType::MulticastGroupBlock); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg01"); + client .expect_execute_transaction() .with( @@ -189,6 +204,7 @@ mod tests { AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_ext, false), AccountMeta::new(owner, false), + AccountMeta::new(index_pda, false), ]), ) .returning(|_, _| Ok(Signature::new_unique())); diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/get.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/get.rs index 0c0d1ad28a..af5d74bc50 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/get.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/get.rs @@ -1,6 +1,8 @@ use crate::{utils::parse_pubkey, DoubleZeroClient}; -use doublezero_serviceability::state::{ - accountdata::AccountData, accounttype::AccountType, multicastgroup::MulticastGroup, +use doublezero_serviceability::{ + pda::get_index_pda, + seeds::SEED_MULTICAST_GROUP, + state::{accountdata::AccountData, accounttype::AccountType, multicastgroup::MulticastGroup}, }; use solana_sdk::pubkey::Pubkey; @@ -16,25 +18,43 @@ impl GetMulticastGroupCommand { AccountData::MulticastGroup(multicastgroup) => Ok((pk, multicastgroup)), _ => Err(eyre::eyre!("Invalid Account Type")), }, - None => client - .gets(AccountType::MulticastGroup)? - .into_iter() - .find(|(_, v)| match v { - AccountData::MulticastGroup(multicastgroup) => multicastgroup - .code - .eq_ignore_ascii_case(&self.pubkey_or_code), - _ => false, - }) - .map(|(pk, v)| match v { - AccountData::MulticastGroup(multicastgroup) => Ok((pk, multicastgroup)), - _ => Err(eyre::eyre!("Invalid Account Type")), - }) - .unwrap_or_else(|| { - Err(eyre::eyre!( - "MulticastGroup with code {} not found", - self.pubkey_or_code - )) - }), + None => { + // Try O(1) lookup via Index PDA first + let (index_pda, _) = get_index_pda( + &client.get_program_id(), + SEED_MULTICAST_GROUP, + &self.pubkey_or_code, + ); + if let Ok(AccountData::Index(index)) = client.get(index_pda) { + return match client.get(index.pk)? { + AccountData::MulticastGroup(multicastgroup) => { + Ok((index.pk, multicastgroup)) + } + _ => Err(eyre::eyre!("Invalid Account Type")), + }; + } + + // Fallback: scan all multicast groups (for pre-migration accounts) + client + .gets(AccountType::MulticastGroup)? + .into_iter() + .find(|(_, v)| match v { + AccountData::MulticastGroup(multicastgroup) => multicastgroup + .code + .eq_ignore_ascii_case(&self.pubkey_or_code), + _ => false, + }) + .map(|(pk, v)| match v { + AccountData::MulticastGroup(multicastgroup) => Ok((pk, multicastgroup)), + _ => Err(eyre::eyre!("Invalid Account Type")), + }) + .unwrap_or_else(|| { + Err(eyre::eyre!( + "MulticastGroup with code {} not found", + self.pubkey_or_code + )) + }) + } } } } @@ -83,6 +103,13 @@ mod tests { )])) }); + // Catch-all for Index PDA lookups (added last so it has highest LIFO priority, + // but uses predicate::always which only matches after specific predicates fail) + client + .expect_get() + .with(predicate::always()) + .returning(|_| Err(eyre::eyre!("Account not found"))); + // Search by pubkey let res = GetMulticastGroupCommand { pubkey_or_code: multicastgroup_pubkey.to_string(), diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/update.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/update.rs index 1f7554bab4..8de49c6fea 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/update.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/update.rs @@ -2,10 +2,14 @@ use crate::{DoubleZeroClient, GetGlobalStateCommand}; use doublezero_program_common::validate_account_code; use doublezero_serviceability::{ instructions::DoubleZeroInstruction, - pda::get_resource_extension_pda, + pda::{get_index_pda, get_resource_extension_pda}, processors::multicastgroup::update::MulticastGroupUpdateArgs, resource::ResourceType, - state::feature_flags::{is_feature_enabled, FeatureFlag}, + seeds::SEED_MULTICAST_GROUP, + state::{ + accountdata::AccountData, + feature_flags::{is_feature_enabled, FeatureFlag}, + }, }; use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, signature::Signature}; use std::net::Ipv4Addr; @@ -48,6 +52,27 @@ impl UpdateMulticastGroupCommand { accounts.push(AccountMeta::new(multicast_group_block_ext, false)); } + // If code is changing, check if the Index PDA actually changes (case-only renames + // produce the same PDA since seeds are lowercased) + let mut rename_index = false; + if let Some(ref new_code) = code { + let old_code = match client.get(self.pubkey)? { + AccountData::MulticastGroup(mgroup) => mgroup.code, + _ => return Err(eyre::eyre!("Invalid Account Type")), + }; + + let (old_index_pda, _) = + get_index_pda(&client.get_program_id(), SEED_MULTICAST_GROUP, &old_code); + let (new_index_pda, _) = + get_index_pda(&client.get_program_id(), SEED_MULTICAST_GROUP, new_code); + + if old_index_pda != new_index_pda { + accounts.push(AccountMeta::new(old_index_pda, false)); + accounts.push(AccountMeta::new(new_index_pda, false)); + rename_index = true; + } + } + client.execute_transaction( DoubleZeroInstruction::UpdateMulticastGroup(MulticastGroupUpdateArgs { code, @@ -56,6 +81,7 @@ impl UpdateMulticastGroupCommand { publisher_count: self.publisher_count, subscriber_count: self.subscriber_count, use_onchain_allocation, + rename_index, }), accounts, ) @@ -66,16 +92,17 @@ impl UpdateMulticastGroupCommand { mod tests { use crate::{ commands::multicastgroup::update::UpdateMulticastGroupCommand, - tests::utils::create_test_client, DoubleZeroClient, MockDoubleZeroClient, + tests::utils::create_test_client, MockDoubleZeroClient, }; use doublezero_serviceability::{ instructions::DoubleZeroInstruction, - pda::{get_globalstate_pda, get_location_pda, get_resource_extension_pda}, + pda::{get_globalstate_pda, get_index_pda, get_location_pda, get_resource_extension_pda}, processors::multicastgroup::update::MulticastGroupUpdateArgs, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, state::{ accountdata::AccountData, accounttype::AccountType, feature_flags::FeatureFlag, - globalstate::GlobalState, + globalstate::GlobalState, multicastgroup::MulticastGroup, }, }; use mockall::predicate; @@ -83,10 +110,51 @@ mod tests { #[test] fn test_commands_multicastgroup_update_command() { - let mut client = create_test_client(); + let mut client = MockDoubleZeroClient::new(); - let (globalstate_pubkey, _globalstate) = get_globalstate_pda(&client.get_program_id()); - let (pda_pubkey, _) = get_location_pda(&client.get_program_id(), 1); + let payer = Pubkey::new_unique(); + client.expect_get_payer().returning(move || payer); + let program_id = Pubkey::new_unique(); + client.expect_get_program_id().returning(move || program_id); + + let (globalstate_pubkey, bump_seed) = get_globalstate_pda(&program_id); + let globalstate = GlobalState { + account_type: AccountType::GlobalState, + bump_seed, + account_index: 0, + foundation_allowlist: vec![], + _device_allowlist: vec![], + _user_allowlist: vec![], + activator_authority_pk: Pubkey::new_unique(), + sentinel_authority_pk: Pubkey::new_unique(), + contributor_airdrop_lamports: 1_000_000_000, + user_airdrop_lamports: 40_000, + health_oracle_pk: Pubkey::new_unique(), + qa_allowlist: vec![], + feature_flags: 0, + feed_authority_pk: Pubkey::default(), + }; + + let (pda_pubkey, _) = get_location_pda(&program_id, 1); + + // Mock get for globalstate and multicast group + let globalstate_clone = globalstate.clone(); + client + .expect_get() + .with(predicate::eq(globalstate_pubkey)) + .returning(move |_| Ok(AccountData::GlobalState(globalstate_clone.clone()))); + client + .expect_get() + .with(predicate::eq(pda_pubkey)) + .returning(move |_| { + Ok(AccountData::MulticastGroup(MulticastGroup { + code: "old_code".to_string(), + ..Default::default() + })) + }); + + let (old_index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "old_code"); + let (new_index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "test_group"); client .expect_execute_transaction() @@ -99,11 +167,14 @@ mod tests { publisher_count: Some(10), subscriber_count: Some(100), use_onchain_allocation: false, + rename_index: true, }, )), predicate::eq(vec![ AccountMeta::new(pda_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(old_index_pda, false), + AccountMeta::new(new_index_pda, false), ]), ) .returning(|_, _| Ok(Signature::new_unique())); @@ -117,15 +188,24 @@ mod tests { subscriber_count: Some(100), }; - let update_invalid_command = UpdateMulticastGroupCommand { + let res = update_command.execute(&client); + assert!(res.is_ok()); + } + + #[test] + fn test_commands_multicastgroup_update_invalid_code() { + let client = create_test_client(); + + let update_command = UpdateMulticastGroupCommand { + pubkey: Pubkey::new_unique(), code: Some("test/group".to_string()), - ..update_command.clone() + multicast_ip: None, + max_bandwidth: None, + publisher_count: None, + subscriber_count: None, }; let res = update_command.execute(&client); - assert!(res.is_ok()); - - let res = update_invalid_command.execute(&client); assert!(res.is_err()); } @@ -175,6 +255,7 @@ mod tests { publisher_count: None, subscriber_count: None, use_onchain_allocation: true, + rename_index: false, }, )), predicate::eq(vec![