From 30d9c7acc1bdabadadafdacc80f0645a30568671 Mon Sep 17 00:00:00 2001 From: Martin Sander Date: Thu, 12 Mar 2026 16:45:30 -0500 Subject: [PATCH 1/2] smartcontract: add Index account for multicast group code uniqueness Introduce an Index account pattern that enforces unique multicast group codes onchain and enables O(1) code-to-pubkey lookup. The Index PDA is derived from the entity type seed and lowercased code, providing case-insensitive uniqueness. Integrated into existing instructions: - CreateMulticastGroup: creates Index alongside the group - UpdateMulticastGroup: atomically renames Index on code change - DeleteMulticastGroup/CloseAccount: closes Index if provided Standalone CreateIndex/DeleteIndex instructions (variants 104/105) added for migration backfill of existing accounts. SDK updated with Index PDA derivation in create/update/get commands and new CreateIndex/DeleteIndex command wrappers. --- CHANGELOG.md | 8 ++ .../src/entrypoint.rs | 7 + .../src/instructions.rs | 13 ++ .../doublezero-serviceability/src/pda.rs | 17 ++- .../src/processors/index/create.rs | 113 +++++++++++++++ .../src/processors/index/delete.rs | 69 ++++++++++ .../src/processors/index/mod.rs | 2 + .../src/processors/mod.rs | 1 + .../processors/multicastgroup/closeaccount.rs | 26 +++- .../src/processors/multicastgroup/create.rs | 44 +++++- .../src/processors/multicastgroup/delete.rs | 28 +++- .../src/processors/multicastgroup/update.rs | 94 ++++++++++++- .../doublezero-serviceability/src/seeds.rs | 1 + .../src/state/accountdata.rs | 16 ++- .../src/state/accounttype.rs | 3 + .../src/state/index.rs | 129 ++++++++++++++++++ .../src/state/mod.rs | 1 + .../tests/create_subscribe_user_test.rs | 15 +- ...multicastgroup_allowlist_publisher_test.rs | 11 +- ...multicastgroup_allowlist_subcriber_test.rs | 11 +- .../multicastgroup_onchain_allocation_test.rs | 62 +++++++-- .../tests/multicastgroup_subscribe_test.rs | 11 +- .../tests/multicastgroup_test.rs | 75 +++++++--- .../tests/reservation_test.rs | 6 +- .../tests/test_helpers.rs | 92 +++++++++++++ .../tests/user_onchain_allocation_test.rs | 17 ++- .../sdk/rs/src/commands/index/create.rs | 44 ++++++ .../sdk/rs/src/commands/index/delete.rs | 28 ++++ .../sdk/rs/src/commands/index/mod.rs | 2 + smartcontract/sdk/rs/src/commands/mod.rs | 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 ++- .../sdk/rs/src/commands/multicastgroup/get.rs | 69 +++++++--- .../rs/src/commands/multicastgroup/update.rs | 100 ++++++++++++-- 37 files changed, 1057 insertions(+), 96 deletions(-) create mode 100644 smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs create mode 100644 smartcontract/programs/doublezero-serviceability/src/processors/index/delete.rs create mode 100644 smartcontract/programs/doublezero-serviceability/src/processors/index/mod.rs create mode 100644 smartcontract/programs/doublezero-serviceability/src/state/index.rs create mode 100644 smartcontract/sdk/rs/src/commands/index/create.rs create mode 100644 smartcontract/sdk/rs/src/commands/index/delete.rs create mode 100644 smartcontract/sdk/rs/src/commands/index/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 06e8bcb9d0..be2cb2251a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ All notable changes to this project will be documented in this file. ### Breaking ### Changes + +- Onchain Programs + - Serviceability: add Index account for multicast group code uniqueness — PDA derived from entity type + lowercased code enforces unique codes onchain and enables O(1) code-to-pubkey lookup + - Serviceability: integrate Index lifecycle into multicast group instructions (create, update, delete, close account) so Index accounts are managed atomically + - Serviceability: add standalone CreateIndex/DeleteIndex instructions (variants 104/105) for migration backfill of existing multicast groups +- SDK + - Rust: add O(1) multicast group lookup by code via Index PDA derivation, with fallback scan for pre-migration accounts + - Rust: add CreateIndex/DeleteIndex command wrappers for migration tooling - E2E Tests - Add geoprobe E2E test (`TestE2E_GeoprobeDiscovery`) that exercises the full geolocation flow: deploy geolocation program, create probe onchain, start geoprobe-agent container, and verify the telemetry-agent discovers and measures the probe via TWAMP - Add geoprobe Docker image, geolocation program build/deploy support, and manager geolocation CLI configuration to the E2E devnet infrastructure diff --git a/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs b/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs index d029abc325..3f1798ce62 100644 --- a/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs +++ b/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs @@ -47,6 +47,7 @@ use crate::{ setauthority::process_set_authority, setfeatureflags::process_set_feature_flags, setversion::process_set_version, }, + index::{create::process_create_index, delete::process_delete_index}, link::{ accept::process_accept_link, activate::process_activate_link, closeaccount::process_closeaccount_link, create::process_create_link, @@ -427,6 +428,12 @@ pub fn process_instruction( DoubleZeroInstruction::DeleteReservedSubscribeUser(value) => { process_delete_reserved_subscribe_user(program_id, accounts, &value)? } + DoubleZeroInstruction::CreateIndex(value) => { + process_create_index(program_id, accounts, &value)? + } + DoubleZeroInstruction::DeleteIndex(value) => { + process_delete_index(program_id, accounts, &value)? + } }; Ok(()) } diff --git a/smartcontract/programs/doublezero-serviceability/src/instructions.rs b/smartcontract/programs/doublezero-serviceability/src/instructions.rs index 50b3864223..acd4070890 100644 --- a/smartcontract/programs/doublezero-serviceability/src/instructions.rs +++ b/smartcontract/programs/doublezero-serviceability/src/instructions.rs @@ -35,6 +35,7 @@ use crate::processors::{ setairdrop::SetAirdropArgs, setauthority::SetAuthorityArgs, setfeatureflags::SetFeatureFlagsArgs, setversion::SetVersionArgs, }, + index::{create::IndexCreateArgs, delete::IndexDeleteArgs}, link::{ accept::LinkAcceptArgs, activate::LinkActivateArgs, closeaccount::LinkCloseAccountArgs, create::LinkCreateArgs, delete::LinkDeleteArgs, reject::LinkRejectArgs, @@ -221,6 +222,9 @@ pub enum DoubleZeroInstruction { CreateReservedSubscribeUser(CreateReservedSubscribeUserArgs), // variant 102 DeleteReservedSubscribeUser(DeleteReservedSubscribeUserArgs), // variant 103 + + CreateIndex(IndexCreateArgs), // variant 104 + DeleteIndex(IndexDeleteArgs), // variant 105 } impl DoubleZeroInstruction { @@ -358,6 +362,9 @@ impl DoubleZeroInstruction { 102 => Ok(Self::CreateReservedSubscribeUser(CreateReservedSubscribeUserArgs::try_from(rest).unwrap())), 103 => Ok(Self::DeleteReservedSubscribeUser(DeleteReservedSubscribeUserArgs::try_from(rest).unwrap())), + 104 => Ok(Self::CreateIndex(IndexCreateArgs::try_from(rest).unwrap())), + 105 => Ok(Self::DeleteIndex(IndexDeleteArgs::try_from(rest).unwrap())), + _ => Err(ProgramError::InvalidInstructionData), } } @@ -491,6 +498,9 @@ impl DoubleZeroInstruction { Self::CreateReservedSubscribeUser(_) => "CreateReservedSubscribeUser".to_string(), // variant 102 Self::DeleteReservedSubscribeUser(_) => "DeleteReservedSubscribeUser".to_string(), // variant 103 + + Self::CreateIndex(_) => "CreateIndex".to_string(), // variant 104 + Self::DeleteIndex(_) => "DeleteIndex".to_string(), // variant 105 } } @@ -617,6 +627,9 @@ impl DoubleZeroInstruction { Self::CreateReservedSubscribeUser(args) => format!("{args:?}"), // variant 102 Self::DeleteReservedSubscribeUser(args) => format!("{args:?}"), // variant 103 + + Self::CreateIndex(args) => format!("{args:?}"), // variant 104 + Self::DeleteIndex(args) => format!("{args:?}"), // variant 105 } } } diff --git a/smartcontract/programs/doublezero-serviceability/src/pda.rs b/smartcontract/programs/doublezero-serviceability/src/pda.rs index 4e86219a36..80fb1ce1ef 100644 --- a/smartcontract/programs/doublezero-serviceability/src/pda.rs +++ b/smartcontract/programs/doublezero-serviceability/src/pda.rs @@ -5,8 +5,8 @@ use solana_program::pubkey::Pubkey; use crate::{ seeds::{ SEED_ACCESS_PASS, SEED_CONFIG, SEED_CONTRIBUTOR, SEED_DEVICE, SEED_DEVICE_TUNNEL_BLOCK, - SEED_DZ_PREFIX_BLOCK, SEED_EXCHANGE, SEED_GLOBALSTATE, SEED_LINK, SEED_LINK_IDS, - SEED_LOCATION, SEED_MULTICASTGROUP_BLOCK, SEED_MULTICAST_GROUP, + SEED_DZ_PREFIX_BLOCK, SEED_EXCHANGE, SEED_GLOBALSTATE, SEED_INDEX, SEED_LINK, + SEED_LINK_IDS, SEED_LOCATION, SEED_MULTICASTGROUP_BLOCK, SEED_MULTICAST_GROUP, SEED_MULTICAST_PUBLISHER_BLOCK, SEED_PERMISSION, SEED_PREFIX, SEED_PROGRAM_CONFIG, SEED_RESERVATION, SEED_SEGMENT_ROUTING_IDS, SEED_TENANT, SEED_TUNNEL_IDS, SEED_USER, SEED_USER_TUNNEL_BLOCK, SEED_VRF_IDS, @@ -119,6 +119,19 @@ pub fn get_accesspass_pda( ) } +pub fn get_index_pda(program_id: &Pubkey, entity_seed: &[u8], code: &str) -> (Pubkey, u8) { + let lowercase_code = code.to_ascii_lowercase(); + Pubkey::find_program_address( + &[ + SEED_PREFIX, + SEED_INDEX, + entity_seed, + lowercase_code.as_bytes(), + ], + program_id, + ) +} + pub fn get_resource_extension_pda( program_id: &Pubkey, resource_type: crate::resource::ResourceType, diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs new file mode 100644 index 0000000000..2e8f36808f --- /dev/null +++ b/smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs @@ -0,0 +1,113 @@ +use crate::{ + error::DoubleZeroError, + pda::get_index_pda, + seeds::{SEED_INDEX, SEED_PREFIX}, + serializer::try_acc_create, + state::{accounttype::AccountType, globalstate::GlobalState, index::Index}, +}; +use borsh::BorshSerialize; +use borsh_incremental::BorshDeserializeIncremental; +use doublezero_program_common::validate_account_code; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + program_error::ProgramError, + pubkey::Pubkey, +}; +use std::fmt; + +#[cfg(test)] +use solana_program::msg; + +#[derive(BorshSerialize, BorshDeserializeIncremental, PartialEq, Clone, Default)] +pub struct IndexCreateArgs { + pub entity_seed: String, + pub code: String, +} + +impl fmt::Debug for IndexCreateArgs { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "entity_seed: {}, code: {}", self.entity_seed, self.code) + } +} + +pub fn process_create_index( + program_id: &Pubkey, + accounts: &[AccountInfo], + value: &IndexCreateArgs, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let index_account = next_account_info(accounts_iter)?; + let entity_account = next_account_info(accounts_iter)?; + let globalstate_account = next_account_info(accounts_iter)?; + let payer_account = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + + #[cfg(test)] + msg!("process_create_index({:?})", value); + + assert!(payer_account.is_signer, "Payer must be a signer"); + + // Validate accounts + assert_eq!( + globalstate_account.owner, program_id, + "Invalid GlobalState Account Owner" + ); + assert_eq!( + entity_account.owner, program_id, + "Invalid Entity Account Owner" + ); + assert_eq!( + *system_program.unsigned_key(), + solana_system_interface::program::ID, + "Invalid System Program Account Owner" + ); + assert!(index_account.is_writable, "Index Account is not writable"); + + // Check foundation allowlist + let globalstate = GlobalState::try_from(globalstate_account)?; + if !globalstate.foundation_allowlist.contains(payer_account.key) { + return Err(DoubleZeroError::NotAllowed.into()); + } + + // Validate and normalize code + let code = + validate_account_code(&value.code).map_err(|_| DoubleZeroError::InvalidAccountCode)?; + let lowercase_code = code.to_ascii_lowercase(); + + // Derive and verify the Index PDA + let (expected_pda, bump_seed) = get_index_pda(program_id, value.entity_seed.as_bytes(), &code); + assert_eq!(index_account.key, &expected_pda, "Invalid Index Pubkey"); + + // Uniqueness: account must not already exist + if !index_account.data_is_empty() { + return Err(ProgramError::AccountAlreadyInitialized); + } + + // Verify the entity account is a valid program account + assert!(!entity_account.data_is_empty(), "Entity Account is empty"); + + let index = Index { + account_type: AccountType::Index, + pk: *entity_account.key, + bump_seed, + }; + + try_acc_create( + &index, + index_account, + payer_account, + system_program, + program_id, + &[ + SEED_PREFIX, + SEED_INDEX, + value.entity_seed.as_bytes(), + lowercase_code.as_bytes(), + &[bump_seed], + ], + )?; + + Ok(()) +} diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/index/delete.rs b/smartcontract/programs/doublezero-serviceability/src/processors/index/delete.rs new file mode 100644 index 0000000000..3fe0886736 --- /dev/null +++ b/smartcontract/programs/doublezero-serviceability/src/processors/index/delete.rs @@ -0,0 +1,69 @@ +use crate::{ + error::DoubleZeroError, + serializer::try_acc_close, + state::{globalstate::GlobalState, index::Index}, +}; +use borsh::BorshSerialize; +use borsh_incremental::BorshDeserializeIncremental; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + pubkey::Pubkey, +}; +use std::fmt; + +#[cfg(test)] +use solana_program::msg; + +#[derive(BorshSerialize, BorshDeserializeIncremental, PartialEq, Clone, Default)] +pub struct IndexDeleteArgs {} + +impl fmt::Debug for IndexDeleteArgs { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "IndexDeleteArgs") + } +} + +pub fn process_delete_index( + program_id: &Pubkey, + accounts: &[AccountInfo], + _value: &IndexDeleteArgs, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let index_account = next_account_info(accounts_iter)?; + let globalstate_account = next_account_info(accounts_iter)?; + let payer_account = next_account_info(accounts_iter)?; + + #[cfg(test)] + msg!("process_delete_index"); + + assert!(payer_account.is_signer, "Payer must be a signer"); + + // Validate accounts + assert_eq!( + index_account.owner, program_id, + "Invalid Index Account Owner" + ); + assert_eq!( + globalstate_account.owner, program_id, + "Invalid GlobalState Account Owner" + ); + assert!(index_account.is_writable, "Index Account is not writable"); + + // Check foundation allowlist + let globalstate = GlobalState::try_from(globalstate_account)?; + if !globalstate.foundation_allowlist.contains(payer_account.key) { + return Err(DoubleZeroError::NotAllowed.into()); + } + + // Verify it's actually an Index account + let _index = Index::try_from(index_account)?; + + try_acc_close(index_account, payer_account)?; + + #[cfg(test)] + msg!("Deleted Index account"); + + Ok(()) +} diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/index/mod.rs b/smartcontract/programs/doublezero-serviceability/src/processors/index/mod.rs new file mode 100644 index 0000000000..da1aa3ace2 --- /dev/null +++ b/smartcontract/programs/doublezero-serviceability/src/processors/index/mod.rs @@ -0,0 +1,2 @@ +pub mod create; +pub mod delete; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/mod.rs b/smartcontract/programs/doublezero-serviceability/src/processors/mod.rs index dc55b7141a..f4269abda1 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/mod.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/mod.rs @@ -5,6 +5,7 @@ pub mod device; pub mod exchange; pub mod globalconfig; pub mod globalstate; +pub mod index; pub mod link; pub mod location; pub mod migrate; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/closeaccount.rs index 0b2ffbcdd7..a553ca2b3e 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; @@ -62,6 +63,9 @@ pub fn process_closeaccount_multicastgroup( let payer_account = next_account_info(accounts_iter)?; let system_program = next_account_info(accounts_iter)?; + // Optional: Index account to close alongside the multicast group + let index_account = next_account_info(accounts_iter).ok(); + #[cfg(test)] msg!("process_deactivate_multicastgroup({:?})", value); @@ -137,6 +141,24 @@ pub fn process_closeaccount_multicastgroup( try_acc_close(multicastgroup_account, owner_account)?; + // Close the Index account if provided + if let Some(index_acc) = index_account { + assert_eq!(index_acc.owner, program_id, "Invalid Index Account Owner"); + assert!(index_acc.is_writable, "Index Account is not writable"); + + let (expected_index_pda, _) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &multicastgroup.code); + assert_eq!(index_acc.key, &expected_index_pda, "Invalid Index Pubkey"); + + let index = Index::try_from(index_acc)?; + assert_eq!( + index.pk, *multicastgroup_account.key, + "Index does not point to this MulticastGroup" + ); + + try_acc_close(index_acc, payer_account)?; + } + #[cfg(test)] msg!("Deactivated: MulticastGroup closed"); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs index 9b0e3a5606..cfd976f3d5 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::*, }, }; @@ -70,6 +71,7 @@ pub fn process_create_multicastgroup( let payer_account = next_account_info(accounts_iter)?; let system_program = next_account_info(accounts_iter)?; + let index_account = next_account_info(accounts_iter)?; #[cfg(test)] msg!("process_create_multicastgroup({:?})", value); @@ -80,6 +82,7 @@ pub fn process_create_multicastgroup( // Validate and normalize code let code = validate_account_code(&value.code).map_err(|_| DoubleZeroError::InvalidAccountCode)?; + let lowercase_code = code.to_ascii_lowercase(); // Check the owner of the accounts assert_eq!( @@ -114,6 +117,10 @@ pub fn process_create_multicastgroup( return Err(ProgramError::AccountAlreadyInitialized); } + // Validate Index PDA (before code is moved into multicastgroup) + let (expected_index_pda, index_bump_seed) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &code); + let mut multicastgroup = MulticastGroup { account_type: AccountType::MulticastGroup, owner: value.owner, @@ -147,6 +154,16 @@ pub fn process_create_multicastgroup( multicastgroup.multicast_ip = allocate_ip(multicast_group_block_ext, 1)?.ip(); multicastgroup.status = MulticastGroupStatus::Activated; } + assert_eq!( + index_account.key, &expected_index_pda, + "Invalid Index Pubkey" + ); + assert!(index_account.is_writable, "Index Account is not writable"); + + // Uniqueness: index account must not already exist + if !index_account.data_is_empty() { + return Err(ProgramError::AccountAlreadyInitialized); + } try_acc_create( &multicastgroup, @@ -161,6 +178,29 @@ pub fn process_create_multicastgroup( &[bump_seed], ], )?; + + // Create the Index account pointing to the multicast group + let index = Index { + account_type: AccountType::Index, + pk: *mgroup_account.key, + bump_seed: index_bump_seed, + }; + + try_acc_create( + &index, + index_account, + payer_account, + system_program, + program_id, + &[ + SEED_PREFIX, + SEED_INDEX, + SEED_MULTICAST_GROUP, + lowercase_code.as_bytes(), + &[index_bump_seed], + ], + )?; + try_acc_write(&globalstate, globalstate_account, payer_account, accounts)?; Ok(()) diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/delete.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/delete.rs index 46806dafdb..0731a91f06 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::*, }, }; @@ -66,6 +68,9 @@ pub fn process_delete_multicastgroup( let payer_account = next_account_info(accounts_iter)?; let system_program = next_account_info(accounts_iter)?; + // Optional: Index account to close alongside the multicast group + let index_account = next_account_info(accounts_iter).ok(); + #[cfg(test)] msg!("process_delete_multicastgroup({:?})", value); @@ -98,6 +103,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 +164,25 @@ pub fn process_delete_multicastgroup( msg!("Deleted: {:?}", multicastgroup_account); } + // Close the Index account if provided + if let Some(index_acc) = index_account { + assert_eq!(index_acc.owner, program_id, "Invalid Index Account Owner"); + assert!(index_acc.is_writable, "Index Account is not writable"); + + // Verify the Index PDA matches + let (expected_index_pda, _) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &multicastgroup_code); + assert_eq!(index_acc.key, &expected_index_pda, "Invalid Index Pubkey"); + + // Verify it's an Index account pointing to this multicast group + let index = Index::try_from(index_acc)?; + assert_eq!( + index.pk, *multicastgroup_account.key, + "Index does not point to this MulticastGroup" + ); + + try_acc_close(index_acc, payer_account)?; + } + Ok(()) } diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs index 3447898fa0..5195975b52 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; @@ -73,6 +77,19 @@ pub fn process_update_multicastgroup( let payer_account = next_account_info(accounts_iter)?; let system_program = next_account_info(accounts_iter)?; + // Optional: Index accounts for code rename + // Account layout when code changes: + // [..., payer, system, old_index_account, new_index_account] + // Account layout when code doesn't change: + // [..., payer, system] + let index_accounts = if value.code.is_some() { + 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 + }; + #[cfg(test)] msg!("process_update_multicastgroup({:?})", value); @@ -107,8 +124,79 @@ 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)?; + let new_lowercase_code = new_code.to_ascii_lowercase(); + + let (old_index_account, new_index_account) = + index_accounts.expect("Index accounts required for code change"); + + // Validate old Index PDA + let (expected_old_index_pda, _) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &multicastgroup.code); + assert_eq!( + old_index_account.key, &expected_old_index_pda, + "Invalid old Index Pubkey" + ); + assert_eq!( + old_index_account.owner, program_id, + "Invalid old Index Account Owner" + ); + assert!( + old_index_account.is_writable, + "Old Index Account is not writable" + ); + + // Validate new Index PDA + let (expected_new_index_pda, new_index_bump_seed) = + get_index_pda(program_id, SEED_MULTICAST_GROUP, &new_code); + assert_eq!( + new_index_account.key, &expected_new_index_pda, + "Invalid new Index Pubkey" + ); + assert!( + new_index_account.is_writable, + "New Index Account is not writable" + ); + + // New index must not already exist (uniqueness) + if !new_index_account.data_is_empty() { + return Err(ProgramError::AccountAlreadyInitialized); + } + + // Verify old index points to this multicast group + let old_index = Index::try_from(old_index_account)?; + assert_eq!( + old_index.pk, *multicastgroup_account.key, + "Old Index does not point to this MulticastGroup" + ); + + // Create new Index + let new_index = Index { + account_type: AccountType::Index, + pk: *multicastgroup_account.key, + bump_seed: new_index_bump_seed, + }; + + try_acc_create( + &new_index, + new_index_account, + payer_account, + system_program, + program_id, + &[ + SEED_PREFIX, + SEED_INDEX, + SEED_MULTICAST_GROUP, + new_lowercase_code.as_bytes(), + &[new_index_bump_seed], + ], + )?; + + // Close old Index + try_acc_close(old_index_account, payer_account)?; + + multicastgroup.code = new_code; } if let Some(ref multicast_ip) = value.multicast_ip { // Handle onchain allocation for IP changes diff --git a/smartcontract/programs/doublezero-serviceability/src/seeds.rs b/smartcontract/programs/doublezero-serviceability/src/seeds.rs index 97acf0ff2e..1f119fe828 100644 --- a/smartcontract/programs/doublezero-serviceability/src/seeds.rs +++ b/smartcontract/programs/doublezero-serviceability/src/seeds.rs @@ -22,3 +22,4 @@ pub const SEED_SEGMENT_ROUTING_IDS: &[u8] = b"segmentroutingids"; pub const SEED_VRF_IDS: &[u8] = b"vrfids"; pub const SEED_RESERVATION: &[u8] = b"reservation"; pub const SEED_PERMISSION: &[u8] = b"permission"; +pub const SEED_INDEX: &[u8] = b"index"; diff --git a/smartcontract/programs/doublezero-serviceability/src/state/accountdata.rs b/smartcontract/programs/doublezero-serviceability/src/state/accountdata.rs index d91307f65c..d3d44a3bd1 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/accountdata.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/accountdata.rs @@ -2,8 +2,8 @@ use crate::{ error::DoubleZeroError, state::{ accesspass::AccessPass, accounttype::AccountType, contributor::Contributor, device::Device, - exchange::Exchange, globalconfig::GlobalConfig, globalstate::GlobalState, link::Link, - location::Location, multicastgroup::MulticastGroup, permission::Permission, + exchange::Exchange, globalconfig::GlobalConfig, globalstate::GlobalState, index::Index, + link::Link, location::Location, multicastgroup::MulticastGroup, permission::Permission, programconfig::ProgramConfig, reservation::Reservation, resource_extension::ResourceExtensionOwned, tenant::Tenant, user::User, }, @@ -30,6 +30,7 @@ pub enum AccountData { Tenant(Tenant), Reservation(Reservation), Permission(Permission), + Index(Index), } impl AccountData { @@ -51,6 +52,7 @@ impl AccountData { AccountData::Tenant(_) => "Tenant", AccountData::Reservation(_) => "Reservation", AccountData::Permission(_) => "Permission", + AccountData::Index(_) => "Index", } } @@ -72,6 +74,7 @@ impl AccountData { AccountData::Tenant(tenant) => tenant.to_string(), AccountData::Reservation(reservation) => reservation.to_string(), AccountData::Permission(permission) => permission.to_string(), + AccountData::Index(index) => index.to_string(), } } @@ -186,6 +189,14 @@ impl AccountData { Err(DoubleZeroError::InvalidAccountType) } } + + pub fn get_index(&self) -> Result { + if let AccountData::Index(index) = self { + Ok(index.clone()) + } else { + Err(DoubleZeroError::InvalidAccountType) + } + } } impl TryFrom<&[u8]> for AccountData { @@ -230,6 +241,7 @@ impl TryFrom<&[u8]> for AccountData { AccountType::Permission => Ok(AccountData::Permission(Permission::try_from( bytes as &[u8], )?)), + AccountType::Index => Ok(AccountData::Index(Index::try_from(bytes as &[u8])?)), } } } diff --git a/smartcontract/programs/doublezero-serviceability/src/state/accounttype.rs b/smartcontract/programs/doublezero-serviceability/src/state/accounttype.rs index e880ed4b18..f054621050 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/accounttype.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/accounttype.rs @@ -24,6 +24,7 @@ pub enum AccountType { Tenant = 13, Reservation = 14, Permission = 15, + Index = 16, } pub trait AccountTypeInfo { @@ -52,6 +53,7 @@ impl From for AccountType { 13 => AccountType::Tenant, 14 => AccountType::Reservation, 15 => AccountType::Permission, + 16 => AccountType::Index, _ => AccountType::None, } } @@ -76,6 +78,7 @@ impl fmt::Display for AccountType { AccountType::Tenant => write!(f, "tenant"), AccountType::Reservation => write!(f, "reservation"), AccountType::Permission => write!(f, "permission"), + AccountType::Index => write!(f, "index"), } } } diff --git a/smartcontract/programs/doublezero-serviceability/src/state/index.rs b/smartcontract/programs/doublezero-serviceability/src/state/index.rs new file mode 100644 index 0000000000..71e46fba75 --- /dev/null +++ b/smartcontract/programs/doublezero-serviceability/src/state/index.rs @@ -0,0 +1,129 @@ +use crate::{ + error::{DoubleZeroError, Validate}, + state::accounttype::AccountType, +}; +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; +use std::fmt; + +#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Index { + pub account_type: AccountType, // 1 + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "doublezero_program_common::serializer::serialize_pubkey_as_string", + deserialize_with = "doublezero_program_common::serializer::deserialize_pubkey_from_string" + ) + )] + pub pk: Pubkey, // 32 + pub bump_seed: u8, // 1 +} + +impl fmt::Display for Index { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Index {{ account_type: {}, pk: {}, bump_seed: {} }}", + self.account_type, self.pk, self.bump_seed + ) + } +} + +impl Default for Index { + fn default() -> Self { + Self { + account_type: AccountType::Index, + pk: Pubkey::default(), + bump_seed: 0, + } + } +} + +impl TryFrom<&[u8]> for Index { + type Error = ProgramError; + + fn try_from(mut data: &[u8]) -> Result { + let out = Self { + account_type: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), + pk: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), + bump_seed: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), + }; + + if out.account_type != AccountType::Index { + return Err(ProgramError::InvalidAccountData); + } + + Ok(out) + } +} + +impl TryFrom<&AccountInfo<'_>> for Index { + type Error = ProgramError; + + fn try_from(account: &AccountInfo) -> Result { + let data = account.try_borrow_data()?; + let res = Self::try_from(&data[..]); + if res.is_err() { + msg!("Failed to deserialize Index: {:?}", res.as_ref().err()); + } + res + } +} + +impl Validate for Index { + fn validate(&self) -> Result<(), DoubleZeroError> { + if self.account_type != AccountType::Index { + msg!("Invalid account type: {}", self.account_type); + return Err(DoubleZeroError::InvalidAccountType); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_state_index_try_from_defaults() { + let data = [AccountType::Index as u8]; + let val = Index::try_from(&data[..]).unwrap(); + + assert_eq!(val.pk, Pubkey::default()); + assert_eq!(val.bump_seed, 0); + } + + #[test] + fn test_state_index_serialization() { + let val = Index { + account_type: AccountType::Index, + pk: Pubkey::new_unique(), + bump_seed: 254, + }; + + let data = borsh::to_vec(&val).unwrap(); + let val2 = Index::try_from(&data[..]).unwrap(); + + val.validate().unwrap(); + val2.validate().unwrap(); + + assert_eq!(val, val2); + assert_eq!(val.account_type as u8, data[0]); + assert_eq!(data.len(), borsh::object_length(&val).unwrap(),); + } + + #[test] + fn test_state_index_validate_error_invalid_account_type() { + let val = Index { + account_type: AccountType::Device, + pk: Pubkey::new_unique(), + bump_seed: 1, + }; + assert_eq!( + val.validate().unwrap_err(), + DoubleZeroError::InvalidAccountType + ); + } +} diff --git a/smartcontract/programs/doublezero-serviceability/src/state/mod.rs b/smartcontract/programs/doublezero-serviceability/src/state/mod.rs index 44a4ed965f..c917fac649 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/mod.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/mod.rs @@ -7,6 +7,7 @@ pub mod exchange; pub mod feature_flags; pub mod globalconfig; pub mod globalstate; +pub mod index; pub mod interface; pub mod link; pub mod location; diff --git a/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs b/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs index 20d8323f89..5804cefa27 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs @@ -13,8 +13,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, @@ -37,6 +37,7 @@ use doublezero_serviceability::{ user::create_subscribe::UserCreateSubscribeArgs, }, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, state::{ accesspass::AccessPassType, device::DeviceType, @@ -285,7 +286,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, @@ -300,6 +303,7 @@ async fn setup_create_subscribe_fixture(client_ip: [u8; 4]) -> CreateSubscribeFi AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_group1, false)], ) .await; @@ -864,7 +868,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, @@ -879,6 +885,7 @@ async fn test_create_subscribe_user_inactive_mgroup_fails() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_pending, false)], ) .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..c53f07ad25 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, @@ -78,6 +81,7 @@ async fn test_multicast_publisher_allowlist() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_test, false)], ) .await; @@ -274,7 +278,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, @@ -289,6 +295,7 @@ async fn test_multicast_publisher_allowlist_sentinel_authority() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .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 ec34c9e8d0..4c622fef1b 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, @@ -78,6 +81,7 @@ async fn test_multicast_subscriber_allowlist() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_test, false)], ) .await; @@ -274,7 +278,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, @@ -289,6 +295,7 @@ async fn test_multicast_subscriber_allowlist_sentinel_authority() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; 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..a1bfa79ed5 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, @@ -71,6 +74,7 @@ async fn test_create_multicastgroup_atomic_with_onchain_allocation() { AccountMeta::new(multicast_group_block_pda, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; @@ -103,7 +107,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, @@ -118,6 +124,7 @@ async fn test_create_multicastgroup_atomic_backward_compat() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; @@ -150,7 +157,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, @@ -166,6 +175,7 @@ async fn test_create_multicastgroup_atomic_feature_flag_disabled() { AccountMeta::new(multicast_group_block_pda, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; @@ -210,8 +220,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, @@ -227,6 +239,7 @@ async fn test_delete_multicastgroup_atomic_with_deallocation() { AccountMeta::new(multicast_group_block_pda, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; @@ -239,7 +252,7 @@ async fn test_delete_multicastgroup_atomic_with_deallocation() { assert_eq!(mgroup.status, MulticastGroupStatus::Activated); // Atomic delete+deallocate+close - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -253,6 +266,7 @@ async fn test_delete_multicastgroup_atomic_with_deallocation() { AccountMeta::new(owner, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; @@ -294,8 +308,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, @@ -311,11 +327,12 @@ async fn test_delete_multicastgroup_atomic_backward_compat() { AccountMeta::new(multicast_group_block_pda, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; // Legacy delete (use_onchain_deallocation=false, default) - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -327,6 +344,7 @@ async fn test_delete_multicastgroup_atomic_backward_compat() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; @@ -369,8 +387,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, @@ -386,6 +406,7 @@ async fn test_update_multicastgroup_with_onchain_reallocation() { AccountMeta::new(multicast_group_block_pda, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; @@ -458,8 +479,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, @@ -475,16 +498,20 @@ async fn test_update_multicastgroup_backward_compat() { AccountMeta::new(multicast_group_block_pda, false), ], &payer, + &[AccountMeta::new(index_pda_mg1, false)], ) .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, @@ -496,6 +523,10 @@ async fn test_update_multicastgroup_backward_compat() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[ + AccountMeta::new(old_index_pda, false), + AccountMeta::new(new_index_pda, false), + ], ) .await; @@ -504,7 +535,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 +553,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, @@ -537,6 +570,7 @@ async fn test_update_multicastgroup_feature_flag_disabled() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; diff --git a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs index 57f0a146b9..1e2eecced6 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs @@ -21,6 +21,7 @@ use doublezero_serviceability::{ user::{activate::UserActivateArgs, create::UserCreateArgs}, }, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, state::{ accesspass::AccessPassType, device::DeviceType, @@ -261,7 +262,9 @@ async fn setup_fixture() -> TestFixture { // 7. Create two multicast groups and activate them let gs = get_globalstate(&mut banks_client, globalstate_pubkey).await; let (mgroup1_pubkey, _) = get_multicastgroup_pda(&program_id, gs.account_index + 1); - execute_transaction( + let (index_pda_group1, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "group1"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -276,6 +279,7 @@ async fn setup_fixture() -> TestFixture { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_group1, false)], ) .await; @@ -296,7 +300,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, @@ -311,6 +317,7 @@ async fn setup_fixture() -> TestFixture { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_group2, false)], ) .await; diff --git a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs index b433230766..fcf6efd25b 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, @@ -66,6 +69,7 @@ async fn test_multicastgroup() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_la, false)], ) .await; @@ -164,12 +168,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 @@ -183,6 +190,10 @@ async fn test_multicastgroup() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[ + AccountMeta::new(old_index_pda_la, false), + AccountMeta::new(new_index_pda_lb, false), + ], ) .await; @@ -192,7 +203,7 @@ async fn test_multicastgroup() { .get_multicastgroup() .unwrap(); assert_eq!(multicastgroup_la.account_type, AccountType::MulticastGroup); - assert_eq!(multicastgroup_la.code, "la2".to_string()); + assert_eq!(multicastgroup_la.code, "lb".to_string()); assert_eq!(multicastgroup_la.multicast_ip, Ipv4Addr::new(239, 1, 1, 2)); assert_eq!(multicastgroup_la.max_bandwidth, 2000); assert_eq!(multicastgroup_la.publisher_count, 0); @@ -202,7 +213,9 @@ async fn test_multicastgroup() { println!("✅ MulticastGroup updated"); /*****************************************************************************************************************************************************/ println!("5. Testing MulticastGroup deletion..."); - execute_transaction( + let (index_pda_lb, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "lb"); + + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -214,6 +227,7 @@ async fn test_multicastgroup() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_lb, false)], ) .await; @@ -223,12 +237,13 @@ 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, @@ -278,7 +293,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, @@ -293,6 +310,7 @@ async fn test_multicastgroup_deactivate_fails_when_counts_nonzero() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_la, false)], ) .await; @@ -342,7 +360,7 @@ async fn test_multicastgroup_deactivate_fails_when_counts_nonzero() { assert_eq!(multicastgroup.subscriber_count, 1); // DeleteMulticastGroup should fail because counts are non-zero - let result = try_execute_transaction( + let result = try_execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -354,6 +372,7 @@ async fn test_multicastgroup_deactivate_fails_when_counts_nonzero() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_la, false)], ) .await; @@ -400,7 +419,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, @@ -415,6 +436,7 @@ async fn test_multicastgroup_deactivate_fails_when_not_deleting() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_la, false)], ) .await; @@ -442,7 +464,7 @@ async fn test_multicastgroup_deactivate_fails_when_not_deleting() { assert_eq!(multicastgroup.status, MulticastGroupStatus::Activated); // Try to deactivate without first deleting (status is Activated, not Deleting) - let result = try_execute_transaction( + let result = try_execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -455,6 +477,7 @@ async fn test_multicastgroup_deactivate_fails_when_not_deleting() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_la, false)], ) .await; @@ -504,8 +527,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, @@ -520,6 +545,7 @@ async fn test_multicastgroup_create_with_wrong_index_fails() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_test, false)], ) .await; @@ -533,7 +559,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, @@ -548,6 +574,7 @@ async fn test_multicastgroup_create_with_wrong_index_fails() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_test, false)], ) .await; @@ -591,7 +618,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, @@ -606,6 +635,7 @@ async fn test_multicastgroup_reactivate_invalid_status_fails() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; @@ -665,7 +695,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, @@ -680,6 +712,7 @@ async fn test_suspend_multicastgroup_from_pending_fails() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; @@ -743,7 +776,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, @@ -758,6 +793,7 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; @@ -806,7 +842,7 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( .await; println!("5. Try to delete with active publishers (should fail)..."); - let result = try_execute_transaction( + let result = try_execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -818,6 +854,7 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; @@ -852,7 +889,7 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( .await; println!("7. Try to delete with active subscribers (should fail)..."); - let result = try_execute_transaction( + let result = try_execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -864,6 +901,7 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; @@ -898,7 +936,7 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( .await; println!("9. Delete with zero counts (should succeed)..."); - execute_transaction( + execute_transaction_with_extra_accounts( &mut banks_client, recent_blockhash, program_id, @@ -910,6 +948,7 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda, false)], ) .await; diff --git a/smartcontract/programs/doublezero-serviceability/tests/reservation_test.rs b/smartcontract/programs/doublezero-serviceability/tests/reservation_test.rs index afd05e219b..8bcc8cbe6d 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/reservation_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/reservation_test.rs @@ -12,6 +12,7 @@ use doublezero_serviceability::{ *, }, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, state::{ accounttype::AccountType, device::*, @@ -845,8 +846,10 @@ async fn setup_multicast_group( let gs = get_globalstate(banks_client, globalstate_pubkey).await; let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, gs.account_index + 1); + let (index_pda_group1, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, "group1"); + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - execute_transaction( + execute_transaction_with_extra_accounts( banks_client, recent_blockhash, program_id, @@ -861,6 +864,7 @@ async fn setup_multicast_group( AccountMeta::new(globalstate_pubkey, false), ], payer, + &[AccountMeta::new(index_pda_group1, false)], ) .await; diff --git a/smartcontract/programs/doublezero-serviceability/tests/test_helpers.rs b/smartcontract/programs/doublezero-serviceability/tests/test_helpers.rs index 7ce9d55cd1..307179077c 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/test_helpers.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/test_helpers.rs @@ -278,6 +278,98 @@ pub async fn execute_transaction_expect_failure( result } +/// Execute a transaction with extra accounts and expect it to fail. Returns the error result. +#[allow(dead_code)] +pub async fn execute_transaction_expect_failure_with_extra_accounts( + banks_client: &mut BanksClient, + _recent_blockhash: solana_program::hash::Hash, + program_id: Pubkey, + instruction: DoubleZeroInstruction, + accounts: Vec, + payer: &Keypair, + extra_accounts: &[AccountMeta], +) -> Result<(), BanksClientError> { + print!("➡️ Transaction (expecting failure) {instruction:?} "); + + let recent_blockhash = banks_client + .get_latest_blockhash() + .await + .expect("Failed to get latest blockhash"); + let mut transaction = create_transaction_with_extra_accounts( + program_id, + &instruction, + &accounts, + payer, + extra_accounts, + ); + transaction.try_sign(&[&payer], recent_blockhash).unwrap(); + let result = banks_client.process_transaction(transaction).await; + + if result.is_err() { + println!("❌ (expected)"); + } else { + println!("✅ (unexpected success)"); + } + + result +} + +#[allow(dead_code)] +pub async fn execute_transaction_with_extra_accounts( + banks_client: &mut BanksClient, + _recent_blockhash: solana_program::hash::Hash, + program_id: Pubkey, + instruction: DoubleZeroInstruction, + accounts: Vec, + payer: &Keypair, + extra_accounts: &[AccountMeta], +) { + print!("➡️ Transaction {instruction:?} "); + + let recent_blockhash = banks_client + .get_latest_blockhash() + .await + .expect("Failed to get latest blockhash"); + let mut transaction = create_transaction_with_extra_accounts( + program_id, + &instruction, + &accounts, + payer, + extra_accounts, + ); + transaction.try_sign(&[&payer], recent_blockhash).unwrap(); + banks_client.process_transaction(transaction).await.unwrap(); + + println!("✅") +} + +#[allow(dead_code)] +pub async fn try_execute_transaction_with_extra_accounts( + banks_client: &mut BanksClient, + recent_blockhash: solana_program::hash::Hash, + program_id: Pubkey, + instruction: DoubleZeroInstruction, + accounts: Vec, + payer: &Keypair, + extra_accounts: &[AccountMeta], +) -> Result<(), BanksClientError> { + print!("➡️ Transaction {instruction:?} "); + + let mut transaction = create_transaction_with_extra_accounts( + program_id, + &instruction, + &accounts, + payer, + extra_accounts, + ); + transaction.try_sign(&[&payer], recent_blockhash).unwrap(); + banks_client.process_transaction(transaction).await?; + + println!("✅"); + + Ok(()) +} + pub fn create_transaction( program_id: Pubkey, instruction: &DoubleZeroInstruction, diff --git a/smartcontract/programs/doublezero-serviceability/tests/user_onchain_allocation_test.rs b/smartcontract/programs/doublezero-serviceability/tests/user_onchain_allocation_test.rs index 1c936d8ec1..ed93310965 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, @@ -38,6 +38,7 @@ use doublezero_serviceability::{ }, }, resource::ResourceType, + seeds::SEED_MULTICAST_GROUP, state::{ accesspass::AccessPassType, device::DeviceType, @@ -1096,8 +1097,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, @@ -1112,6 +1115,7 @@ async fn test_multicast_subscribe_reactivation_preserves_allocations() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_mgroup, false)], ) .await; @@ -1413,7 +1417,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, @@ -1428,6 +1434,7 @@ async fn test_multicast_publisher_block_deallocation_and_reuse() { AccountMeta::new(globalstate_pubkey, false), ], &payer, + &[AccountMeta::new(index_pda_mgroup, false)], ) .await; diff --git a/smartcontract/sdk/rs/src/commands/index/create.rs b/smartcontract/sdk/rs/src/commands/index/create.rs new file mode 100644 index 0000000000..aea9d26cff --- /dev/null +++ b/smartcontract/sdk/rs/src/commands/index/create.rs @@ -0,0 +1,44 @@ +use crate::{commands::globalstate::get::GetGlobalStateCommand, DoubleZeroClient}; +use doublezero_program_common::validate_account_code; +use doublezero_serviceability::{ + instructions::DoubleZeroInstruction, pda::get_index_pda, + processors::index::create::IndexCreateArgs, +}; +use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, signature::Signature}; + +#[derive(Debug, PartialEq, Clone)] +pub struct CreateIndexCommand { + pub entity_seed: String, + pub code: String, + pub entity_pubkey: Pubkey, +} + +impl CreateIndexCommand { + pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result<(Signature, Pubkey)> { + let code = + validate_account_code(&self.code).map_err(|err| eyre::eyre!("invalid code: {err}"))?; + + let (globalstate_pubkey, _) = GetGlobalStateCommand + .execute(client) + .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; + + let (index_pda, _) = + get_index_pda(&client.get_program_id(), self.entity_seed.as_bytes(), &code); + + let accounts = vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(self.entity_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ]; + + client + .execute_transaction( + DoubleZeroInstruction::CreateIndex(IndexCreateArgs { + entity_seed: self.entity_seed.clone(), + code, + }), + accounts, + ) + .map(|sig| (sig, index_pda)) + } +} diff --git a/smartcontract/sdk/rs/src/commands/index/delete.rs b/smartcontract/sdk/rs/src/commands/index/delete.rs new file mode 100644 index 0000000000..bea2160a0b --- /dev/null +++ b/smartcontract/sdk/rs/src/commands/index/delete.rs @@ -0,0 +1,28 @@ +use crate::{commands::globalstate::get::GetGlobalStateCommand, DoubleZeroClient}; +use doublezero_serviceability::{ + instructions::DoubleZeroInstruction, processors::index::delete::IndexDeleteArgs, +}; +use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, signature::Signature}; + +#[derive(Debug, PartialEq, Clone)] +pub struct DeleteIndexCommand { + pub index_pubkey: Pubkey, +} + +impl DeleteIndexCommand { + pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result { + let (globalstate_pubkey, _) = GetGlobalStateCommand + .execute(client) + .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; + + let accounts = vec![ + AccountMeta::new(self.index_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ]; + + client.execute_transaction( + DoubleZeroInstruction::DeleteIndex(IndexDeleteArgs {}), + accounts, + ) + } +} diff --git a/smartcontract/sdk/rs/src/commands/index/mod.rs b/smartcontract/sdk/rs/src/commands/index/mod.rs new file mode 100644 index 0000000000..da1aa3ace2 --- /dev/null +++ b/smartcontract/sdk/rs/src/commands/index/mod.rs @@ -0,0 +1,2 @@ +pub mod create; +pub mod delete; diff --git a/smartcontract/sdk/rs/src/commands/mod.rs b/smartcontract/sdk/rs/src/commands/mod.rs index ed36fffae3..b5f49ff28b 100644 --- a/smartcontract/sdk/rs/src/commands/mod.rs +++ b/smartcontract/sdk/rs/src/commands/mod.rs @@ -5,6 +5,7 @@ pub mod device; pub mod exchange; pub mod globalconfig; pub mod globalstate; +pub mod index; pub mod link; pub mod location; pub mod migrate; diff --git a/smartcontract/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 4fc972e1b9..1bec8c0d33 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)); } + // Add payer and system program (framework handles these), then index account + 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/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 482d575990..4d343cb1f1 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,23 @@ impl UpdateMulticastGroupCommand { accounts.push(AccountMeta::new(multicast_group_block_ext, false)); } + // If code is changing, add old and new index accounts + if let Some(ref new_code) = code { + // Fetch current multicast group to get old 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); + + accounts.push(AccountMeta::new(old_index_pda, false)); + accounts.push(AccountMeta::new(new_index_pda, false)); + } + client.execute_transaction( DoubleZeroInstruction::UpdateMulticastGroup(MulticastGroupUpdateArgs { code, @@ -66,16 +87,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 +105,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, + reservation_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() @@ -104,6 +167,8 @@ mod tests { 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 +182,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()); } From 41ae1611d488380dc5c343f673f244357317772e Mon Sep 17 00:00:00 2001 From: Martin Sander Date: Sun, 15 Mar 2026 12:58:20 -0500 Subject: [PATCH 2/2] fix e2e --- .../src/processors/multicastgroup/create.rs | 8 ++--- .../src/processors/multicastgroup/update.rs | 18 +++++------- .../tests/create_subscribe_user_test.rs | 6 ++-- ...multicastgroup_allowlist_publisher_test.rs | 6 ++-- ...multicastgroup_allowlist_subcriber_test.rs | 6 ++-- .../multicastgroup_onchain_allocation_test.rs | 29 ++++++++++++------- .../tests/multicastgroup_subscribe_test.rs | 6 ++-- .../tests/multicastgroup_test.rs | 29 ++++++++++++------- .../tests/reservation_test.rs | 3 +- .../tests/user_onchain_allocation_test.rs | 6 ++-- 10 files changed, 69 insertions(+), 48 deletions(-) diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs index cfd976f3d5..b88bfcbf2d 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs @@ -58,20 +58,20 @@ 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)?; - let index_account = next_account_info(accounts_iter)?; #[cfg(test)] msg!("process_create_multicastgroup({:?})", value); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs index 5195975b52..878c1558b5 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/update.rs @@ -63,25 +63,18 @@ 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 }; - let payer_account = next_account_info(accounts_iter)?; - let system_program = next_account_info(accounts_iter)?; - - // Optional: Index accounts for code rename - // Account layout when code changes: - // [..., payer, system, old_index_account, new_index_account] - // Account layout when code doesn't change: - // [..., payer, system] + // Optional: Index accounts for code rename (before payer/system) let index_accounts = if value.code.is_some() { let old_index_account = next_account_info(accounts_iter)?; let new_index_account = next_account_info(accounts_iter)?; @@ -90,6 +83,9 @@ pub fn process_update_multicastgroup( None }; + let payer_account = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + #[cfg(test)] msg!("process_update_multicastgroup({:?})", value); 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 5804cefa27..7474e90379 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs @@ -301,9 +301,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, - &[AccountMeta::new(index_pda_group1, false)], + &[], ) .await; @@ -883,9 +884,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, - &[AccountMeta::new(index_pda_pending, false)], + &[], ) .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 c53f07ad25..669ef3aeb8 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_publisher_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_publisher_test.rs @@ -79,9 +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, - &[AccountMeta::new(index_pda_test, false)], + &[], ) .await; @@ -293,9 +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, - &[AccountMeta::new(index_pda, false)], + &[], ) .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 4c622fef1b..66b5c04e74 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_subcriber_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_subcriber_test.rs @@ -79,9 +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, - &[AccountMeta::new(index_pda_test, false)], + &[], ) .await; @@ -293,9 +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, - &[AccountMeta::new(index_pda, false)], + &[], ) .await; 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 a1bfa79ed5..a039d1e54a 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_onchain_allocation_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_onchain_allocation_test.rs @@ -72,9 +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, - &[AccountMeta::new(index_pda, false)], + &[], ) .await; @@ -122,9 +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, - &[AccountMeta::new(index_pda, false)], + &[], ) .await; @@ -173,9 +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, - &[AccountMeta::new(index_pda, false)], + &[], ) .await; @@ -237,9 +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, - &[AccountMeta::new(index_pda, false)], + &[], ) .await; @@ -325,9 +329,10 @@ 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, - &[AccountMeta::new(index_pda, false)], + &[], ) .await; @@ -404,9 +409,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, - &[AccountMeta::new(index_pda, false)], + &[], ) .await; @@ -496,9 +502,10 @@ 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, - &[AccountMeta::new(index_pda_mg1, false)], + &[], ) .await; @@ -521,12 +528,11 @@ async fn test_update_multicastgroup_backward_compat() { vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), - ], - &payer, - &[ AccountMeta::new(old_index_pda, false), AccountMeta::new(new_index_pda, false), ], + &payer, + &[], ) .await; @@ -568,9 +574,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, - &[AccountMeta::new(index_pda, false)], + &[], ) .await; diff --git a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs index 1e2eecced6..f43350bab3 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs @@ -277,9 +277,10 @@ async fn setup_fixture() -> TestFixture { vec![ AccountMeta::new(mgroup1_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_group1, false), ], &payer, - &[AccountMeta::new(index_pda_group1, false)], + &[], ) .await; @@ -315,9 +316,10 @@ async fn setup_fixture() -> TestFixture { vec![ AccountMeta::new(mgroup2_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_group2, false), ], &payer, - &[AccountMeta::new(index_pda_group2, false)], + &[], ) .await; diff --git a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs index fcf6efd25b..203ed4fceb 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs @@ -67,9 +67,10 @@ async fn test_multicastgroup() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_la, false), ], &payer, - &[AccountMeta::new(index_pda_la, false)], + &[], ) .await; @@ -188,12 +189,11 @@ async fn test_multicastgroup() { vec![ AccountMeta::new(multicastgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), - ], - &payer, - &[ AccountMeta::new(old_index_pda_la, false), AccountMeta::new(new_index_pda_lb, false), ], + &payer, + &[], ) .await; @@ -308,9 +308,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, - &[AccountMeta::new(index_pda_la, false)], + &[], ) .await; @@ -434,9 +435,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, - &[AccountMeta::new(index_pda_la, false)], + &[], ) .await; @@ -543,9 +545,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, - &[AccountMeta::new(index_pda_test, false)], + &[], ) .await; @@ -572,9 +575,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, - &[AccountMeta::new(index_pda_test, false)], + &[], ) .await; @@ -633,9 +637,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, - &[AccountMeta::new(index_pda, false)], + &[], ) .await; @@ -710,9 +715,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, - &[AccountMeta::new(index_pda, false)], + &[], ) .await; @@ -791,9 +797,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, - &[AccountMeta::new(index_pda, false)], + &[], ) .await; diff --git a/smartcontract/programs/doublezero-serviceability/tests/reservation_test.rs b/smartcontract/programs/doublezero-serviceability/tests/reservation_test.rs index 8bcc8cbe6d..467cde819e 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/reservation_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/reservation_test.rs @@ -862,9 +862,10 @@ async fn setup_multicast_group( vec![ AccountMeta::new(mgroup_pubkey, false), AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(index_pda_group1, false), ], payer, - &[AccountMeta::new(index_pda_group1, false)], + &[], ) .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 ed93310965..2d05c6fbdd 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/user_onchain_allocation_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/user_onchain_allocation_test.rs @@ -1113,9 +1113,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, - &[AccountMeta::new(index_pda_mgroup, false)], + &[], ) .await; @@ -1432,9 +1433,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, - &[AccountMeta::new(index_pda_mgroup, false)], + &[], ) .await;