From 700a975f34d856544763b41be7367a1e9017ca08 Mon Sep 17 00:00:00 2001 From: Martin Sander Date: Mon, 30 Mar 2026 23:42:19 -0500 Subject: [PATCH 1/3] smartcontract: integrate Index into multicast group lifecycle Wire Index account management into multicast group create, update, delete, and close instructions. Update Rust SDK commands to derive and pass Index PDAs. Add close_index/rename_index args to control Index lifecycle during deactivation and updates. Update activator, Go/Python/TypeScript SDKs, and all integration tests. --- activator/src/process/multicastgroup.rs | 11 +- .../python/serviceability/state.py | 1 + .../typescript/serviceability/state.ts | 1 + .../src/instructions.rs | 3 + .../processors/multicastgroup/closeaccount.rs | 46 +++++- .../src/processors/multicastgroup/create.rs | 50 +++++- .../src/processors/multicastgroup/delete.rs | 48 +++++- .../src/processors/multicastgroup/update.rs | 106 ++++++++++++- .../tests/create_subscribe_user_test.rs | 24 ++- .../tests/index_test.rs | 7 +- ...multicastgroup_allowlist_publisher_test.rs | 13 +- ...multicastgroup_allowlist_subcriber_test.rs | 19 ++- .../multicastgroup_onchain_allocation_test.rs | 76 +++++++-- .../tests/multicastgroup_subscribe_test.rs | 13 +- .../tests/multicastgroup_test.rs | 100 +++++++++--- .../tests/user_onchain_allocation_test.rs | 34 +++- smartcontract/sdk/go/serviceability/state.go | 1 + .../multicastgroup/allowlist/publisher/add.rs | 5 + .../allowlist/publisher/remove.rs | 5 + .../allowlist/subscriber/add.rs | 5 + .../allowlist/subscriber/remove.rs | 5 + .../rs/src/commands/multicastgroup/create.rs | 17 +- .../src/commands/multicastgroup/deactivate.rs | 148 ++++++++++++++++-- .../rs/src/commands/multicastgroup/delete.rs | 39 +++-- .../sdk/rs/src/commands/multicastgroup/get.rs | 69 +++++--- .../rs/src/commands/multicastgroup/update.rs | 107 +++++++++++-- 26 files changed, 812 insertions(+), 141 deletions(-) diff --git a/activator/src/process/multicastgroup.rs b/activator/src/process/multicastgroup.rs index b00371b54b..0633fd1275 100644 --- a/activator/src/process/multicastgroup.rs +++ b/activator/src/process/multicastgroup.rs @@ -398,13 +398,22 @@ mod tests { // Insert it first so it can be removed multicastgroups.insert(pubkey, multicastgroup.clone()); - // Stateless mode: use_onchain_deallocation=true + // Mock get() for DeactivateMulticastGroupCommand which fetches the + // multicast group to derive the Index PDA + let mgroup_for_get = multicastgroup.clone(); + client + .expect_get() + .with(predicate::eq(pubkey)) + .returning(move |_| Ok(AccountData::MulticastGroup(mgroup_for_get.clone()))); + + // Stateless mode: use_onchain_deallocation=true, close_index=true client .expect_execute_transaction() .with( predicate::eq(DoubleZeroInstruction::DeactivateMulticastGroup( MulticastGroupDeactivateArgs { use_onchain_deallocation: true, + close_index: true, }, )), predicate::always(), diff --git a/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..27a01fcbfc 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", ); @@ -1010,6 +1011,7 @@ mod tests { test_instruction( DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, + close_index: false, }), "DeleteMulticastGroup", ); @@ -1017,6 +1019,7 @@ mod tests { test_instruction( DoubleZeroInstruction::DeactivateMulticastGroup(MulticastGroupDeactivateArgs { use_onchain_deallocation: false, + close_index: false, }), "DeactivateMulticastGroup", ); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs index 0b2ffbcdd7..dcfe59465b 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs @@ -1,10 +1,11 @@ use crate::{ error::DoubleZeroError, - pda::get_resource_extension_pda, + pda::{get_index_pda, get_resource_extension_pda}, processors::resource::deallocate_ip, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, serializer::try_acc_close, - state::{globalstate::GlobalState, multicastgroup::*}, + state::{globalstate::GlobalState, index::Index, multicastgroup::*}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -24,14 +25,17 @@ pub struct MulticastGroupDeactivateArgs { /// When false, legacy behavior is used (no deallocation). #[incremental(default = false)] pub use_onchain_deallocation: bool, + /// When true, close the associated Index account alongside the multicast group. + #[incremental(default = false)] + pub close_index: bool, } impl fmt::Debug for MulticastGroupDeactivateArgs { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "use_onchain_deallocation: {}", - self.use_onchain_deallocation + "use_onchain_deallocation: {}, close_index: {}", + self.use_onchain_deallocation, self.close_index ) } } @@ -47,10 +51,12 @@ pub fn process_closeaccount_multicastgroup( let owner_account = next_account_info(accounts_iter)?; let globalstate_account = next_account_info(accounts_iter)?; - // Optional: ResourceExtension account for on-chain deallocation (before payer) - // Account layout WITH ResourceExtension (use_onchain_deallocation = true): - // [multicastgroup, owner, globalstate, multicast_group_block, payer, system] - // Account layout WITHOUT (legacy, use_onchain_deallocation = false): + // Optional accounts (before payer/system): + // Account layout WITH deallocation + index: + // [multicastgroup, owner, globalstate, multicast_group_block, index, payer, system] + // Account layout WITHOUT deallocation, with index: + // [multicastgroup, owner, globalstate, index, payer, system] + // Legacy (no deallocation, no index): // [multicastgroup, owner, globalstate, payer, system] let resource_extension_account = if value.use_onchain_deallocation { let multicast_group_block_ext = next_account_info(accounts_iter)?; @@ -59,6 +65,12 @@ pub fn process_closeaccount_multicastgroup( None }; + let index_account = if value.close_index { + Some(next_account_info(accounts_iter)?) + } else { + None + }; + let payer_account = next_account_info(accounts_iter)?; let system_program = next_account_info(accounts_iter)?; @@ -137,6 +149,24 @@ pub fn process_closeaccount_multicastgroup( try_acc_close(multicastgroup_account, owner_account)?; + // Close the Index account if provided + if let Some(index_acc) = index_account { + assert_eq!(index_acc.owner, program_id, "Invalid Index Account Owner"); + assert!(index_acc.is_writable, "Index Account is not writable"); + + let (expected_index_pda, _) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &multicastgroup.code); + assert_eq!(index_acc.key, &expected_index_pda, "Invalid Index Pubkey"); + + let index = Index::try_from(index_acc)?; + assert_eq!( + index.pk, *multicastgroup_account.key, + "Index does not point to this MulticastGroup" + ); + + try_acc_close(index_acc, payer_account)?; + } + #[cfg(test)] msg!("Deactivated: MulticastGroup closed"); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs index 88ceeb170c..ffdb4cb852 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs @@ -1,14 +1,15 @@ use crate::{ error::DoubleZeroError, - pda::{get_multicastgroup_pda, get_resource_extension_pda}, + pda::{get_index_pda, get_multicastgroup_pda, get_resource_extension_pda}, processors::{resource::allocate_ip, validation::validate_program_account}, resource::ResourceType, - seeds::{SEED_MULTICAST_GROUP, SEED_PREFIX}, + seeds::{SEED_INDEX, SEED_MULTICAST_GROUP, SEED_PREFIX}, serializer::{try_acc_create, try_acc_write}, state::{ accounttype::AccountType, feature_flags::{is_feature_enabled, FeatureFlag}, globalstate::GlobalState, + index::Index, multicastgroup::*, }, }; @@ -57,17 +58,18 @@ pub fn process_create_multicastgroup( let mgroup_account = next_account_info(accounts_iter)?; let globalstate_account = next_account_info(accounts_iter)?; - // Optional: ResourceExtension account for onchain allocation (before payer) + // Optional: ResourceExtension account for onchain allocation // Account layout WITH ResourceExtension (use_onchain_allocation = true): - // [mgroup, globalstate, multicast_group_block, payer, system] + // [mgroup, globalstate, multicast_group_block, index, payer, system] // Account layout WITHOUT (legacy, use_onchain_allocation = false): - // [mgroup, globalstate, payer, system] + // [mgroup, globalstate, index, payer, system] let resource_extension_account = if value.use_onchain_allocation { Some(next_account_info(accounts_iter)?) } else { None }; + let index_account = next_account_info(accounts_iter)?; let payer_account = next_account_info(accounts_iter)?; let system_program = next_account_info(accounts_iter)?; @@ -80,6 +82,7 @@ pub fn process_create_multicastgroup( // Validate and normalize code let code = validate_account_code(&value.code).map_err(|_| DoubleZeroError::InvalidAccountCode)?; + let lowercase_code = code.to_ascii_lowercase(); // Check the owner of the accounts assert_eq!( @@ -114,6 +117,10 @@ pub fn process_create_multicastgroup( return Err(ProgramError::AccountAlreadyInitialized); } + // Validate Index PDA (before code is moved into multicastgroup) + let (expected_index_pda, index_bump_seed) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &code); + let mut multicastgroup = MulticastGroup { account_type: AccountType::MulticastGroup, owner: value.owner, @@ -147,6 +154,16 @@ pub fn process_create_multicastgroup( multicastgroup.multicast_ip = allocate_ip(multicast_group_block_ext, 1)?.ip(); multicastgroup.status = MulticastGroupStatus::Activated; } + assert_eq!( + index_account.key, &expected_index_pda, + "Invalid Index Pubkey" + ); + assert!(index_account.is_writable, "Index Account is not writable"); + + // Uniqueness: index account must not already exist + if !index_account.data_is_empty() { + return Err(ProgramError::AccountAlreadyInitialized); + } try_acc_create( &multicastgroup, @@ -161,6 +178,29 @@ pub fn process_create_multicastgroup( &[bump_seed], ], )?; + + // Create the Index account pointing to the multicast group + let index = Index { + account_type: AccountType::Index, + pk: *mgroup_account.key, + bump_seed: index_bump_seed, + }; + + try_acc_create( + &index, + index_account, + payer_account, + system_program, + program_id, + &[ + SEED_PREFIX, + SEED_INDEX, + SEED_MULTICAST_GROUP, + lowercase_code.as_bytes(), + &[index_bump_seed], + ], + )?; + try_acc_write(&globalstate, globalstate_account, payer_account, accounts)?; Ok(()) diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/delete.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/delete.rs index 53535570aa..db7b6d2814 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/delete.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/delete.rs @@ -1,12 +1,14 @@ use crate::{ error::DoubleZeroError, - pda::get_resource_extension_pda, + pda::{get_index_pda, get_resource_extension_pda}, processors::{resource::deallocate_ip, validation::validate_program_account}, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, serializer::{try_acc_close, try_acc_write}, state::{ feature_flags::{is_feature_enabled, FeatureFlag}, globalstate::GlobalState, + index::Index, multicastgroup::*, }, }; @@ -28,14 +30,17 @@ pub struct MulticastGroupDeleteArgs { /// Requires ResourceExtension accounts and owner account. #[incremental(default = false)] pub use_onchain_deallocation: bool, + /// When true, close the associated Index account alongside the multicast group. + #[incremental(default = false)] + pub close_index: bool, } impl fmt::Debug for MulticastGroupDeleteArgs { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "use_onchain_deallocation: {}", - self.use_onchain_deallocation + "use_onchain_deallocation: {}, close_index: {}", + self.use_onchain_deallocation, self.close_index ) } } @@ -50,10 +55,12 @@ pub fn process_delete_multicastgroup( let multicastgroup_account = next_account_info(accounts_iter)?; let globalstate_account = next_account_info(accounts_iter)?; - // Optional: additional accounts for atomic deallocation (before payer) - // Account layout WITH deallocation (use_onchain_deallocation = true): - // [mgroup, globalstate, multicast_group_block, owner, payer, system] - // Account layout WITHOUT (legacy, use_onchain_deallocation = false): + // Optional: additional accounts for atomic deallocation + // Account layout WITH deallocation + index: + // [mgroup, globalstate, multicast_group_block, owner, index, payer, system] + // Account layout WITHOUT deallocation, with index: + // [mgroup, globalstate, index, payer, system] + // Legacy (no deallocation, no index): // [mgroup, globalstate, payer, system] let deallocation_accounts = if value.use_onchain_deallocation { let multicast_group_block_ext = next_account_info(accounts_iter)?; @@ -63,6 +70,12 @@ pub fn process_delete_multicastgroup( None }; + let index_account = if value.close_index { + Some(next_account_info(accounts_iter)?) + } else { + None + }; + let payer_account = next_account_info(accounts_iter)?; let system_program = next_account_info(accounts_iter)?; @@ -98,6 +111,7 @@ pub fn process_delete_multicastgroup( } let multicastgroup: MulticastGroup = MulticastGroup::try_from(multicastgroup_account)?; + let multicastgroup_code = multicastgroup.code.clone(); if matches!(multicastgroup.status, MulticastGroupStatus::Deleting) { return Err(DoubleZeroError::InvalidStatus.into()); @@ -158,5 +172,25 @@ pub fn process_delete_multicastgroup( msg!("Deleted: {:?}", multicastgroup_account); } + // Close the Index account if provided + if let Some(index_acc) = index_account { + assert_eq!(index_acc.owner, program_id, "Invalid Index Account Owner"); + assert!(index_acc.is_writable, "Index Account is not writable"); + + // Verify the Index PDA matches + let (expected_index_pda, _) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &multicastgroup_code); + assert_eq!(index_acc.key, &expected_index_pda, "Invalid Index Pubkey"); + + // Verify it's an Index account pointing to this multicast group + let index = Index::try_from(index_acc)?; + assert_eq!( + index.pk, *multicastgroup_account.key, + "Index does not point to this MulticastGroup" + ); + + try_acc_close(index_acc, payer_account)?; + } + Ok(()) } diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs index 2630cd7f20..ca159aca91 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs @@ -1,15 +1,18 @@ use crate::{ error::DoubleZeroError, - pda::get_resource_extension_pda, + pda::{get_index_pda, get_resource_extension_pda}, processors::{ resource::{allocate_specific_ip, deallocate_ip}, validation::validate_program_account, }, resource::ResourceType, - serializer::try_acc_write, + seeds::{SEED_INDEX, SEED_MULTICAST_GROUP, SEED_PREFIX}, + serializer::{try_acc_close, try_acc_create, try_acc_write}, state::{ + accounttype::AccountType, feature_flags::{is_feature_enabled, FeatureFlag}, globalstate::GlobalState, + index::Index, multicastgroup::*, }, }; @@ -19,6 +22,7 @@ use doublezero_program_common::{types::NetworkV4, validate_account_code}; use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, + program_error::ProgramError, pubkey::Pubkey, }; use std::fmt; @@ -37,14 +41,18 @@ pub struct MulticastGroupUpdateArgs { /// Requires ResourceExtension account (MulticastGroupBlock). #[incremental(default = false)] pub use_onchain_allocation: bool, + /// When true, old and new Index accounts are included for an Index rename. + /// Set to false when the code change doesn't affect the Index PDA (e.g. case-only rename). + #[incremental(default = false)] + pub rename_index: bool, } impl fmt::Debug for MulticastGroupUpdateArgs { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "code: {:?}, multicast_ip: {:?}, max_bandwidth: {:?}, publisher_count: {:?}, subscriber_count: {:?}, use_onchain_allocation: {}", - self.code, self.multicast_ip, self.max_bandwidth, self.publisher_count, self.subscriber_count, self.use_onchain_allocation + "code: {:?}, multicast_ip: {:?}, max_bandwidth: {:?}, publisher_count: {:?}, subscriber_count: {:?}, use_onchain_allocation: {}, rename_index: {}", + self.code, self.multicast_ip, self.max_bandwidth, self.publisher_count, self.subscriber_count, self.use_onchain_allocation, self.rename_index ) } } @@ -59,17 +67,26 @@ pub fn process_update_multicastgroup( let multicastgroup_account = next_account_info(accounts_iter)?; let globalstate_account = next_account_info(accounts_iter)?; - // Optional: ResourceExtension account for onchain allocation (before payer) + // Optional: ResourceExtension account for onchain allocation // Account layout WITH allocation (use_onchain_allocation = true): - // [mgroup, globalstate, multicast_group_block, payer, system] + // [mgroup, globalstate, multicast_group_block, (opt old_index, new_index), payer, system] // Account layout WITHOUT (legacy, use_onchain_allocation = false): - // [mgroup, globalstate, payer, system] + // [mgroup, globalstate, (opt old_index, new_index), payer, system] let resource_extension_account = if value.use_onchain_allocation { Some(next_account_info(accounts_iter)?) } else { None }; + // Optional: Index accounts for code rename (before payer/system) + let index_accounts = if value.rename_index { + let old_index_account = next_account_info(accounts_iter)?; + let new_index_account = next_account_info(accounts_iter)?; + Some((old_index_account, new_index_account)) + } else { + None + }; + let payer_account = next_account_info(accounts_iter)?; let system_program = next_account_info(accounts_iter)?; @@ -107,8 +124,81 @@ pub fn process_update_multicastgroup( let mut multicastgroup: MulticastGroup = MulticastGroup::try_from(multicastgroup_account)?; if let Some(ref code) = value.code { - multicastgroup.code = + let new_code = validate_account_code(code).map_err(|_| DoubleZeroError::InvalidAccountCode)?; + + // Rename the Index if accounts are provided (skip for case-only renames + // where the lowercased PDA is unchanged) + if let Some((old_index_account, new_index_account)) = index_accounts { + let new_lowercase_code = new_code.to_ascii_lowercase(); + + // Validate old Index PDA + let (expected_old_index_pda, _) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &multicastgroup.code); + assert_eq!( + old_index_account.key, &expected_old_index_pda, + "Invalid old Index Pubkey" + ); + assert_eq!( + old_index_account.owner, program_id, + "Invalid old Index Account Owner" + ); + assert!( + old_index_account.is_writable, + "Old Index Account is not writable" + ); + + // Validate new Index PDA + let (expected_new_index_pda, new_index_bump_seed) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &new_code); + assert_eq!( + new_index_account.key, &expected_new_index_pda, + "Invalid new Index Pubkey" + ); + assert!( + new_index_account.is_writable, + "New Index Account is not writable" + ); + + // New index must not already exist (uniqueness) + if !new_index_account.data_is_empty() { + return Err(ProgramError::AccountAlreadyInitialized); + } + + // Verify old index points to this multicast group + let old_index = Index::try_from(old_index_account)?; + assert_eq!( + old_index.pk, *multicastgroup_account.key, + "Old Index does not point to this MulticastGroup" + ); + + // Create new Index + let new_index = Index { + account_type: AccountType::Index, + pk: *multicastgroup_account.key, + bump_seed: new_index_bump_seed, + }; + + try_acc_create( + &new_index, + new_index_account, + payer_account, + system_program, + program_id, + &[ + SEED_PREFIX, + SEED_INDEX, + SEED_MULTICAST_GROUP, + new_lowercase_code.as_bytes(), + &[new_index_bump_seed], + ], + )?; + + // Close old Index + try_acc_close(old_index_account, payer_account)?; + } + + multicastgroup.code = new_code; } if let Some(ref multicast_ip) = value.multicast_ip { // Handle onchain allocation for IP changes diff --git a/smartcontract/programs/doublezero-serviceability/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..1f704bd713 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_onchain_allocation_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_onchain_allocation_test.rs @@ -18,6 +18,7 @@ use doublezero_serviceability::{ }, }, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, state::{feature_flags::FeatureFlag, multicastgroup::*}, }; use solana_program::instruction::InstructionError; @@ -55,7 +56,9 @@ async fn test_create_multicastgroup_atomic_with_onchain_allocation() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -69,8 +72,10 @@ async fn test_create_multicastgroup_atomic_with_onchain_allocation() { AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_pda, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -103,7 +108,9 @@ async fn test_create_multicastgroup_atomic_backward_compat() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -116,8 +123,10 @@ async fn test_create_multicastgroup_atomic_backward_compat() { vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -150,7 +159,9 @@ async fn test_create_multicastgroup_atomic_feature_flag_disabled() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - let result = execute_transaction_expect_failure( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + + let result = execute_transaction_expect_failure_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -164,8 +175,10 @@ async fn test_create_multicastgroup_atomic_feature_flag_disabled() { AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_pda, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -210,8 +223,10 @@ async fn test_delete_multicastgroup_atomic_with_deallocation() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + // Create with atomic onchain allocation - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -225,8 +240,10 @@ async fn test_delete_multicastgroup_atomic_with_deallocation() { AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_pda, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -239,20 +256,23 @@ async fn test_delete_multicastgroup_atomic_with_deallocation() { assert_eq!(mgroup.status, MulticastGroupStatus::Activated); // Atomic delete+deallocate+close - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: true, + close_index: true, }), vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_pda, false), AccountMeta::new(owner, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -294,8 +314,10 @@ async fn test_delete_multicastgroup_atomic_backward_compat() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + // Create with atomic onchain allocation - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -309,24 +331,29 @@ async fn test_delete_multicastgroup_atomic_backward_compat() { AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_pda, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; // Legacy delete (use_onchain_deallocation=false, default) - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, + close_index: true, }), vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -369,8 +396,10 @@ async fn test_update_multicastgroup_with_onchain_reallocation() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + // Create with atomic onchain allocation - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -384,8 +413,10 @@ async fn test_update_multicastgroup_with_onchain_reallocation() { AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_pda, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -410,6 +441,7 @@ async fn test_update_multicastgroup_with_onchain_reallocation() { publisher_count: None, subscriber_count: None, use_onchain_allocation: true, + rename_index: false, }), vec![ AccountMeta::new(mgroup_pubkey, false), @@ -458,8 +490,10 @@ async fn test_update_multicastgroup_backward_compat() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); + let (index_pda_mg1, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + // Create with atomic onchain allocation - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -473,29 +507,38 @@ async fn test_update_multicastgroup_backward_compat() { AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), AccountMeta::new(multicast_group_block_pda, false), + AccountMeta::new(index_pda_mg1, false), ], &payer, + &[], ) .await; - // Legacy update without onchain allocation - execute_transaction( + // Legacy update without onchain allocation (code changes, so needs old+new index accounts) + let (old_index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + let (new_index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg2"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::UpdateMulticastGroup(MulticastGroupUpdateArgs { - code: Some("mg1_updated".to_string()), + code: Some("mg2".to_string()), multicast_ip: None, max_bandwidth: Some(2000), publisher_count: None, subscriber_count: None, use_onchain_allocation: false, + rename_index: true, }), vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(old_index_pda, false), + AccountMeta::new(new_index_pda, false), ], &payer, + &[], ) .await; @@ -504,7 +547,7 @@ async fn test_update_multicastgroup_backward_compat() { .expect("MulticastGroup not found") .get_multicastgroup() .unwrap(); - assert_eq!(mgroup.code, "mg1_updated"); + assert_eq!(mgroup.code, "mg2"); assert_eq!(mgroup.max_bandwidth, 2000); println!("test_update_multicastgroup_backward_compat PASSED"); @@ -522,7 +565,9 @@ async fn test_update_multicastgroup_feature_flag_disabled() { let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "mg1"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -535,8 +580,10 @@ async fn test_update_multicastgroup_feature_flag_disabled() { vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -557,6 +604,7 @@ async fn test_update_multicastgroup_feature_flag_disabled() { publisher_count: None, subscriber_count: None, use_onchain_allocation: true, + rename_index: false, }), vec![ AccountMeta::new(mgroup_pubkey, false), diff --git a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs index 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..7b5b85931a 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs @@ -7,6 +7,7 @@ use doublezero_serviceability::{ activate::MulticastGroupActivateArgs, closeaccount::MulticastGroupDeactivateArgs, create::*, delete::*, reactivate::*, suspend::*, update::*, }, + seeds::SEED_MULTICAST_GROUP, state::{accounttype::AccountType, multicastgroup::*}, }; use solana_program_test::*; @@ -51,7 +52,9 @@ async fn test_multicastgroup() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda_la, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "la"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -64,8 +67,10 @@ async fn test_multicastgroup() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_la, false), ], &payer, + &[], ) .await; @@ -164,12 +169,15 @@ async fn test_multicastgroup() { println!("✅ MulticastGroup reactivated"); /*****************************************************************************************************************************************************/ println!("4. Testing MulticastGroup update..."); - execute_transaction( + let (old_index_pda_la, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "la"); + let (new_index_pda_lb, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "lb"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::UpdateMulticastGroup(MulticastGroupUpdateArgs { - code: Some("la2".to_string()), + code: Some("lb".to_string()), multicast_ip: Some([239, 1, 1, 2].into()), max_bandwidth: Some(2000), // Keep publisher/subscriber counts at zero so that DeactivateMulticastGroup @@ -177,12 +185,16 @@ async fn test_multicastgroup() { publisher_count: None, subscriber_count: None, use_onchain_allocation: false, + rename_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(old_index_pda_la, false), + AccountMeta::new(new_index_pda_lb, false), ], &payer, + &[], ) .await; @@ -192,7 +204,7 @@ async fn test_multicastgroup() { .get_multicastgroup() .unwrap(); assert_eq!(multicastgroup_la.account_type, AccountType::MulticastGroup); - assert_eq!(multicastgroup_la.code, "la2".to_string()); + assert_eq!(multicastgroup_la.code, "lb".to_string()); assert_eq!(multicastgroup_la.multicast_ip, Ipv4Addr::new(239, 1, 1, 2)); assert_eq!(multicastgroup_la.max_bandwidth, 2000); assert_eq!(multicastgroup_la.publisher_count, 0); @@ -202,18 +214,23 @@ async fn test_multicastgroup() { println!("✅ MulticastGroup updated"); /*****************************************************************************************************************************************************/ println!("5. Testing MulticastGroup deletion..."); - execute_transaction( + let (index_pda_lb, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "lb"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, + close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_lb, false), ], &payer, + &[], ) .await; @@ -223,18 +240,20 @@ async fn test_multicastgroup() { .get_multicastgroup() .unwrap(); assert_eq!(multicastgroup_la.account_type, AccountType::MulticastGroup); - assert_eq!(multicastgroup_la.code, "la2".to_string()); + assert_eq!(multicastgroup_la.code, "lb".to_string()); assert_eq!(multicastgroup_la.status, MulticastGroupStatus::Deleting); println!("✅ MulticastGroup deleted"); /*****************************************************************************************************************************************************/ println!("6. Testing MulticastGroup deactivation (final delete)..."); + // Index account was already closed by DeleteMulticastGroup, so don't pass it here execute_transaction( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeactivateMulticastGroup(MulticastGroupDeactivateArgs { use_onchain_deallocation: false, + close_index: false, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), @@ -278,7 +297,9 @@ async fn test_multicastgroup_deactivate_fails_when_counts_nonzero() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda_la, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "la"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -291,8 +312,10 @@ async fn test_multicastgroup_deactivate_fails_when_counts_nonzero() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_la, false), ], &payer, + &[], ) .await; @@ -324,6 +347,7 @@ async fn test_multicastgroup_deactivate_fails_when_counts_nonzero() { publisher_count: Some(1), subscriber_count: Some(1), use_onchain_allocation: false, + rename_index: false, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), @@ -342,18 +366,21 @@ async fn test_multicastgroup_deactivate_fails_when_counts_nonzero() { assert_eq!(multicastgroup.subscriber_count, 1); // DeleteMulticastGroup should fail because counts are non-zero - let result = try_execute_transaction( + let result = try_execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, + close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_la, false), ], &payer, + &[], ) .await; @@ -400,7 +427,9 @@ async fn test_multicastgroup_deactivate_fails_when_not_deleting() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda_la, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "la"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -413,8 +442,10 @@ async fn test_multicastgroup_deactivate_fails_when_not_deleting() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_la, false), ], &payer, + &[], ) .await; @@ -442,19 +473,22 @@ async fn test_multicastgroup_deactivate_fails_when_not_deleting() { assert_eq!(multicastgroup.status, MulticastGroupStatus::Activated); // Try to deactivate without first deleting (status is Activated, not Deleting) - let result = try_execute_transaction( + let result = try_execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeactivateMulticastGroup(MulticastGroupDeactivateArgs { use_onchain_deallocation: false, + close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(multicastgroup.owner, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_la, false), ], &payer, + &[], ) .await; @@ -504,8 +538,10 @@ async fn test_multicastgroup_create_with_wrong_index_fails() { // Derive PDA with the WRONG index (what a malicious/buggy client might do) let (wrong_multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, wrong_index); + let (index_pda_test, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "test"); + // Try to create with wrong index - should fail - let result = try_execute_transaction( + let result = try_execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -518,8 +554,10 @@ async fn test_multicastgroup_create_with_wrong_index_fails() { vec![ AccountMeta::new(wrong_multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_test, false), ], &payer, + &[], ) .await; @@ -533,7 +571,7 @@ async fn test_multicastgroup_create_with_wrong_index_fails() { println!("3. Testing MulticastGroup creation with correct index..."); let (correct_multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, correct_index); - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -546,8 +584,10 @@ async fn test_multicastgroup_create_with_wrong_index_fails() { vec![ AccountMeta::new(correct_multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_test, false), ], &payer, + &[], ) .await; @@ -591,7 +631,9 @@ async fn test_multicastgroup_reactivate_invalid_status_fails() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "reactivate-test"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -604,8 +646,10 @@ async fn test_multicastgroup_reactivate_invalid_status_fails() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -665,7 +709,9 @@ async fn test_suspend_multicastgroup_from_pending_fails() { let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "test"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -678,8 +724,10 @@ async fn test_suspend_multicastgroup_from_pending_fails() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -743,7 +791,9 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); - execute_transaction( + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "delete-test"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -756,8 +806,10 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -796,6 +848,7 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( publisher_count: Some(1), subscriber_count: None, use_onchain_allocation: false, + rename_index: false, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), @@ -806,18 +859,21 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( .await; println!("5. Try to delete with active publishers (should fail)..."); - let result = try_execute_transaction( + let result = try_execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, + close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -842,6 +898,7 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( publisher_count: Some(0), subscriber_count: Some(1), use_onchain_allocation: false, + rename_index: false, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), @@ -852,18 +909,21 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( .await; println!("7. Try to delete with active subscribers (should fail)..."); - let result = try_execute_transaction( + let result = try_execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, + close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; @@ -888,6 +948,7 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( publisher_count: Some(0), subscriber_count: Some(0), use_onchain_allocation: false, + rename_index: false, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), @@ -898,18 +959,21 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( .await; println!("9. Delete with zero counts (should succeed)..."); - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, + close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ], &payer, + &[], ) .await; diff --git a/smartcontract/programs/doublezero-serviceability/tests/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..f8990fd7cc 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,9 +48,15 @@ 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, + close_index: true, }), accounts, ) @@ -47,25 +66,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() @@ -73,12 +149,14 @@ mod tests { predicate::eq(DoubleZeroInstruction::DeactivateMulticastGroup( MulticastGroupDeactivateArgs { use_onchain_deallocation: false, + close_index: true, }, )), predicate::eq(vec![ 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 +173,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() @@ -111,6 +223,7 @@ mod tests { predicate::eq(DoubleZeroInstruction::DeactivateMulticastGroup( MulticastGroupDeactivateArgs { use_onchain_deallocation: true, + close_index: true, }, )), predicate::eq(vec![ @@ -118,6 +231,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..214eee5edc 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,9 +50,15 @@ 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, + close_index: true, }), accounts, ) @@ -66,9 +73,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 +111,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,17 +123,21 @@ 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( predicate::eq(DoubleZeroInstruction::DeleteMulticastGroup( MulticastGroupDeleteArgs { use_onchain_deallocation: false, + close_index: true, }, )), predicate::eq(vec![ AccountMeta::new(pda_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda, false), ]), ) .returning(|_, _| Ok(Signature::new_unique())); @@ -176,12 +191,15 @@ 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( predicate::eq(DoubleZeroInstruction::DeleteMulticastGroup( MulticastGroupDeleteArgs { use_onchain_deallocation: true, + close_index: true, }, )), predicate::eq(vec![ @@ -189,6 +207,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![ From f78596ee53b545647ba2331effd861000d99887d Mon Sep 17 00:00:00 2001 From: Martin Sander Date: Wed, 8 Apr 2026 17:17:26 -0500 Subject: [PATCH 2/3] address comments --- activator/src/process/multicastgroup.rs | 3 +- .../src/instructions.rs | 2 - .../processors/multicastgroup/closeaccount.rs | 90 ++++++++----------- .../src/processors/multicastgroup/create.rs | 13 +-- .../src/processors/multicastgroup/delete.rs | 63 ++++++------- .../src/processors/multicastgroup/update.rs | 41 ++++----- .../multicastgroup_onchain_allocation_test.rs | 2 - .../tests/multicastgroup_test.rs | 13 +-- .../src/commands/multicastgroup/deactivate.rs | 3 - .../rs/src/commands/multicastgroup/delete.rs | 3 - 10 files changed, 94 insertions(+), 139 deletions(-) diff --git a/activator/src/process/multicastgroup.rs b/activator/src/process/multicastgroup.rs index 0633fd1275..9e80be91a0 100644 --- a/activator/src/process/multicastgroup.rs +++ b/activator/src/process/multicastgroup.rs @@ -406,14 +406,13 @@ mod tests { .with(predicate::eq(pubkey)) .returning(move |_| Ok(AccountData::MulticastGroup(mgroup_for_get.clone()))); - // Stateless mode: use_onchain_deallocation=true, close_index=true + // Stateless mode: use_onchain_deallocation=true client .expect_execute_transaction() .with( predicate::eq(DoubleZeroInstruction::DeactivateMulticastGroup( MulticastGroupDeactivateArgs { use_onchain_deallocation: true, - close_index: true, }, )), predicate::always(), diff --git a/smartcontract/programs/doublezero-serviceability/src/instructions.rs b/smartcontract/programs/doublezero-serviceability/src/instructions.rs index 27a01fcbfc..13686894ca 100644 --- a/smartcontract/programs/doublezero-serviceability/src/instructions.rs +++ b/smartcontract/programs/doublezero-serviceability/src/instructions.rs @@ -1011,7 +1011,6 @@ mod tests { test_instruction( DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, - close_index: false, }), "DeleteMulticastGroup", ); @@ -1019,7 +1018,6 @@ mod tests { test_instruction( DoubleZeroInstruction::DeactivateMulticastGroup(MulticastGroupDeactivateArgs { use_onchain_deallocation: false, - close_index: false, }), "DeactivateMulticastGroup", ); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs index dcfe59465b..af672f7a96 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs @@ -1,7 +1,7 @@ use crate::{ error::DoubleZeroError, pda::{get_index_pda, get_resource_extension_pda}, - processors::resource::deallocate_ip, + processors::{resource::deallocate_ip, validation::validate_program_account}, resource::ResourceType, seeds::SEED_MULTICAST_GROUP, serializer::try_acc_close, @@ -25,17 +25,14 @@ pub struct MulticastGroupDeactivateArgs { /// When false, legacy behavior is used (no deallocation). #[incremental(default = false)] pub use_onchain_deallocation: bool, - /// When true, close the associated Index account alongside the multicast group. - #[incremental(default = false)] - pub close_index: bool, } impl fmt::Debug for MulticastGroupDeactivateArgs { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "use_onchain_deallocation: {}, close_index: {}", - self.use_onchain_deallocation, self.close_index + "use_onchain_deallocation: {}", + self.use_onchain_deallocation ) } } @@ -51,13 +48,10 @@ pub fn process_closeaccount_multicastgroup( let owner_account = next_account_info(accounts_iter)?; let globalstate_account = next_account_info(accounts_iter)?; - // Optional accounts (before payer/system): - // Account layout WITH deallocation + index: + // Account layout WITH deallocation: // [multicastgroup, owner, globalstate, multicast_group_block, index, payer, system] - // Account layout WITHOUT deallocation, with index: + // Account layout WITHOUT deallocation: // [multicastgroup, owner, globalstate, index, payer, system] - // Legacy (no deallocation, no index): - // [multicastgroup, owner, globalstate, payer, system] let resource_extension_account = if value.use_onchain_deallocation { let multicast_group_block_ext = next_account_info(accounts_iter)?; Some(multicast_group_block_ext) @@ -65,12 +59,7 @@ pub fn process_closeaccount_multicastgroup( None }; - let index_account = if value.close_index { - 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,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 { @@ -119,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 @@ -149,22 +126,25 @@ pub fn process_closeaccount_multicastgroup( try_acc_close(multicastgroup_account, owner_account)?; - // Close the Index account if provided - if let Some(index_acc) = index_account { - assert_eq!(index_acc.owner, program_id, "Invalid Index Account Owner"); - assert!(index_acc.is_writable, "Index Account is not writable"); - + // 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); - assert_eq!(index_acc.key, &expected_index_pda, "Invalid Index Pubkey"); + validate_program_account!( + index_account, + program_id, + writable = true, + pda = &expected_index_pda, + "Index" + ); - let index = Index::try_from(index_acc)?; + 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_acc, payer_account)?; + try_acc_close(index_account, payer_account)?; } #[cfg(test)] diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs index ffdb4cb852..33ea0c1dbc 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs @@ -84,17 +84,18 @@ pub fn process_create_multicastgroup( 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 @@ -183,6 +184,8 @@ pub fn process_create_multicastgroup( 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, }; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/delete.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/delete.rs index db7b6d2814..1b166db561 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/delete.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/delete.rs @@ -30,17 +30,14 @@ pub struct MulticastGroupDeleteArgs { /// Requires ResourceExtension accounts and owner account. #[incremental(default = false)] pub use_onchain_deallocation: bool, - /// When true, close the associated Index account alongside the multicast group. - #[incremental(default = false)] - pub close_index: bool, } impl fmt::Debug for MulticastGroupDeleteArgs { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "use_onchain_deallocation: {}, close_index: {}", - self.use_onchain_deallocation, self.close_index + "use_onchain_deallocation: {}", + self.use_onchain_deallocation ) } } @@ -55,13 +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 - // Account layout WITH deallocation + index: + // Account layout WITH deallocation: // [mgroup, globalstate, multicast_group_block, owner, index, payer, system] - // Account layout WITHOUT deallocation, with index: + // Account layout WITHOUT deallocation: // [mgroup, globalstate, index, payer, system] - // Legacy (no deallocation, no index): - // [mgroup, globalstate, payer, system] let deallocation_accounts = if value.use_onchain_deallocation { let multicast_group_block_ext = next_account_info(accounts_iter)?; let owner_account = next_account_info(accounts_iter)?; @@ -70,12 +64,7 @@ pub fn process_delete_multicastgroup( None }; - let index_account = if value.close_index { - 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)?; @@ -85,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)?; @@ -172,24 +161,26 @@ pub fn process_delete_multicastgroup( msg!("Deleted: {:?}", multicastgroup_account); } - // Close the Index account if provided - if let Some(index_acc) = index_account { - assert_eq!(index_acc.owner, program_id, "Invalid Index Account Owner"); - assert!(index_acc.is_writable, "Index Account is not writable"); - - // Verify the Index PDA matches + // 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); - assert_eq!(index_acc.key, &expected_index_pda, "Invalid Index Pubkey"); + 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_acc)?; + 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_acc, payer_account)?; + 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 ca159aca91..f78a292631 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs @@ -96,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) { @@ -135,17 +135,12 @@ pub fn process_update_multicastgroup( // Validate old Index PDA let (expected_old_index_pda, _) = get_index_pda(program_id, SEED_MULTICAST_GROUP, &multicastgroup.code); - assert_eq!( - old_index_account.key, &expected_old_index_pda, - "Invalid old Index Pubkey" - ); - assert_eq!( - old_index_account.owner, program_id, - "Invalid old Index Account Owner" - ); - assert!( - old_index_account.is_writable, - "Old Index Account is not writable" + validate_program_account!( + old_index_account, + program_id, + writable = true, + pda = &expected_old_index_pda, + "Old Index" ); // Validate new Index PDA @@ -176,6 +171,8 @@ pub fn process_update_multicastgroup( 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, }; 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 1f704bd713..75a275e1f2 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_onchain_allocation_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_onchain_allocation_test.rs @@ -262,7 +262,6 @@ async fn test_delete_multicastgroup_atomic_with_deallocation() { program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: true, - close_index: true, }), vec![ AccountMeta::new(mgroup_pubkey, false), @@ -345,7 +344,6 @@ async fn test_delete_multicastgroup_atomic_backward_compat() { program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, - close_index: true, }), vec![ AccountMeta::new(mgroup_pubkey, false), diff --git a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs index 7b5b85931a..bd73ef8d64 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs @@ -222,7 +222,6 @@ async fn test_multicastgroup() { program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, - close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), @@ -246,21 +245,22 @@ async fn test_multicastgroup() { println!("✅ MulticastGroup deleted"); /*****************************************************************************************************************************************************/ println!("6. Testing MulticastGroup deactivation (final delete)..."); - // Index account was already closed by DeleteMulticastGroup, so don't pass it here - execute_transaction( + // Index account was already closed by DeleteMulticastGroup, pass Pubkey::default() to skip + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::DeactivateMulticastGroup(MulticastGroupDeactivateArgs { use_onchain_deallocation: false, - close_index: false, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(multicastgroup.owner, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(Pubkey::default(), false), ], &payer, + &[], ) .await; @@ -372,7 +372,6 @@ async fn test_multicastgroup_deactivate_fails_when_counts_nonzero() { program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, - close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), @@ -479,7 +478,6 @@ async fn test_multicastgroup_deactivate_fails_when_not_deleting() { program_id, DoubleZeroInstruction::DeactivateMulticastGroup(MulticastGroupDeactivateArgs { use_onchain_deallocation: false, - close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), @@ -865,7 +863,6 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, - close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), @@ -915,7 +912,6 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, - close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), @@ -965,7 +961,6 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( program_id, DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation: false, - close_index: true, }), vec![ AccountMeta::new(multicastgroup_pubkey, false), diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/deactivate.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/deactivate.rs index f8990fd7cc..03c02f9654 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/deactivate.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/deactivate.rs @@ -56,7 +56,6 @@ impl DeactivateMulticastGroupCommand { client.execute_transaction( DoubleZeroInstruction::DeactivateMulticastGroup(MulticastGroupDeactivateArgs { use_onchain_deallocation: self.use_onchain_deallocation, - close_index: true, }), accounts, ) @@ -149,7 +148,6 @@ mod tests { predicate::eq(DoubleZeroInstruction::DeactivateMulticastGroup( MulticastGroupDeactivateArgs { use_onchain_deallocation: false, - close_index: true, }, )), predicate::eq(vec![ @@ -223,7 +221,6 @@ mod tests { predicate::eq(DoubleZeroInstruction::DeactivateMulticastGroup( MulticastGroupDeactivateArgs { use_onchain_deallocation: true, - close_index: true, }, )), predicate::eq(vec![ diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/delete.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/delete.rs index 214eee5edc..60a6de7d41 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/delete.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/delete.rs @@ -58,7 +58,6 @@ impl DeleteMulticastGroupCommand { client.execute_transaction( DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs { use_onchain_deallocation, - close_index: true, }), accounts, ) @@ -131,7 +130,6 @@ mod tests { predicate::eq(DoubleZeroInstruction::DeleteMulticastGroup( MulticastGroupDeleteArgs { use_onchain_deallocation: false, - close_index: true, }, )), predicate::eq(vec![ @@ -199,7 +197,6 @@ mod tests { predicate::eq(DoubleZeroInstruction::DeleteMulticastGroup( MulticastGroupDeleteArgs { use_onchain_deallocation: true, - close_index: true, }, )), predicate::eq(vec![ From 63fffef7d65bc13dc811ee2f9f9f3ec4a92159ec Mon Sep 17 00:00:00 2001 From: Martin Sander Date: Thu, 9 Apr 2026 12:33:20 -0500 Subject: [PATCH 3/3] e2e: update backward compat for multicast group Index requirement --- e2e/compatibility_test.go | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) 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.