Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions activator/src/process/multicastgroup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,14 @@ mod tests {
// Insert it first so it can be removed
multicastgroups.insert(pubkey, multicastgroup.clone());

// Mock get() for DeactivateMulticastGroupCommand which fetches the
// multicast group to derive the Index PDA
let mgroup_for_get = multicastgroup.clone();
client
.expect_get()
.with(predicate::eq(pubkey))
.returning(move |_| Ok(AccountData::MulticastGroup(mgroup_for_get.clone())));

// Stateless mode: use_onchain_deallocation=true
client
.expect_execute_transaction()
Expand Down
30 changes: 14 additions & 16 deletions e2e/compatibility_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,24 +91,22 @@ func before(v string) []versionRange {
}

var knownIncompatibilities = map[string]knownIncompat{
// multicast_group_create: The MulticastGroupCreateArgs Borsh struct changed in v0.8.1.
// The index and bump_seed fields were removed. Older CLIs send the old format which
// causes Borsh deserialization failure in the current program.
"write/multicast_group_create": {ranges: before("0.8.1")},
// multicast_group_create: The CreateMulticastGroup instruction now requires an Index
// account for unique code enforcement. All CLIs before v0.17.0 don't pass this account,
// causing "insufficient account keys for instruction".
"write/multicast_group_create": {ranges: before("0.17.0")},

// All multicast operations that depend on multicast_group_create. When the group
// can't be created (< 0.8.1), these all fail with "MulticastGroup not found".
"write/multicast_group_wait_activated": {ranges: before("0.8.1")},
// multicast_group_update: In addition to the dependency above, v0.8.1-v0.8.8 parsed
// --max-bandwidth as a plain integer. v0.8.9 added validate_parse_bandwidth (a855ca7a)
// which accepts unit strings like "200Mbps".
"write/multicast_group_update": {ranges: before("0.8.9")},
"write/multicast_group_pub_allowlist_add": {ranges: before("0.8.1")},
"write/multicast_group_pub_allowlist_remove": {ranges: before("0.8.1")},
"write/multicast_group_sub_allowlist_add": {ranges: before("0.8.1")},
"write/multicast_group_sub_allowlist_remove": {ranges: before("0.8.1")},
"write/multicast_group_get": {ranges: before("0.8.1")},
"write/multicast_group_delete": {ranges: before("0.8.1")},
// can't be created, these all fail with "MulticastGroup not found".
"write/multicast_group_wait_activated": {ranges: before("0.17.0")},
"write/multicast_group_update": {ranges: before("0.17.0")},
"write/multicast_group_pub_allowlist_add": {ranges: before("0.17.0")},
"write/multicast_group_pub_allowlist_remove": {ranges: before("0.17.0")},
"write/multicast_group_sub_allowlist_add": {ranges: before("0.17.0")},
"write/multicast_group_sub_allowlist_remove": {ranges: before("0.17.0")},
"write/multicast_group_get": {ranges: before("0.17.0")},
"write/multicast_group_delete": {ranges: before("0.17.0")},
"write/user_subscribe": {ranges: before("0.17.0")},

// set-health commands: The CLI subcommand was added in commit eb7ea308 (Jan 16).
// mainnet-beta v0.8.2 was built Jan 13 (before set-health) → doesn't have it.
Expand Down
1 change: 1 addition & 0 deletions sdk/serviceability/python/serviceability/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class AccountTypeEnum(IntEnum):
ACCESS_PASS = 11
TENANT = 13
PERMISSION = 15
INDEX = 16


# ---------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions sdk/serviceability/typescript/serviceability/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const ACCOUNT_TYPE_CONTRIBUTOR = 10;
export const ACCOUNT_TYPE_ACCESS_PASS = 11;
export const ACCOUNT_TYPE_TENANT = 13;
export const ACCOUNT_TYPE_PERMISSION = 15;
export const ACCOUNT_TYPE_INDEX = 16;

// ---------------------------------------------------------------------------
// Enum string mappings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,7 @@ mod tests {
publisher_count: None,
subscriber_count: None,
use_onchain_allocation: false,
rename_index: false,
}),
"UpdateMulticastGroup",
);
Expand Down
Comment thread
martinsander00 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use crate::{
error::DoubleZeroError,
pda::get_resource_extension_pda,
processors::resource::deallocate_ip,
pda::{get_index_pda, get_resource_extension_pda},
processors::{resource::deallocate_ip, validation::validate_program_account},
resource::ResourceType,
seeds::SEED_MULTICAST_GROUP,
serializer::try_acc_close,
state::{globalstate::GlobalState, multicastgroup::*},
state::{globalstate::GlobalState, index::Index, multicastgroup::*},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
Expand Down Expand Up @@ -47,18 +48,18 @@ pub fn process_closeaccount_multicastgroup(
let owner_account = next_account_info(accounts_iter)?;
let globalstate_account = next_account_info(accounts_iter)?;

// Optional: ResourceExtension account for on-chain deallocation (before payer)
// Account layout WITH ResourceExtension (use_onchain_deallocation = true):
// [multicastgroup, owner, globalstate, multicast_group_block, payer, system]
// Account layout WITHOUT (legacy, use_onchain_deallocation = false):
// [multicastgroup, owner, globalstate, payer, system]
// Account layout WITH deallocation:
// [multicastgroup, owner, globalstate, multicast_group_block, index, payer, system]
// Account layout WITHOUT deallocation:
// [multicastgroup, owner, globalstate, index, payer, system]
let resource_extension_account = if value.use_onchain_deallocation {
let multicast_group_block_ext = next_account_info(accounts_iter)?;
Some(multicast_group_block_ext)
} 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)?;

Expand All @@ -68,25 +69,24 @@ pub fn process_closeaccount_multicastgroup(
// Check if the payer is a signer
assert!(payer_account.is_signer, "Payer must be a signer");

// Check the owner of the accounts
assert_eq!(
multicastgroup_account.owner, program_id,
"Invalid PDA Account Owner"
// Validate accounts
validate_program_account!(
multicastgroup_account,
program_id,
writable = true,
"MulticastGroup"
);
assert_eq!(
globalstate_account.owner, program_id,
"Invalid GlobalState Account Owner"
validate_program_account!(
globalstate_account,
program_id,
writable = false,
"GlobalState"
);
assert_eq!(
*system_program.unsigned_key(),
solana_system_interface::program::ID,
"Invalid System Program Account Owner"
);
// Check if the account is writable
assert!(
multicastgroup_account.is_writable,
"PDA Account is not writable"
);

let globalstate = GlobalState::try_from(globalstate_account)?;
if globalstate.activator_authority_pk != *payer_account.key {
Expand All @@ -107,25 +107,14 @@ pub fn process_closeaccount_multicastgroup(
// Deallocate multicast_ip from ResourceExtension if account provided
// Deallocation is idempotent - safe to call even if resource wasn't allocated
if let Some(multicast_group_block_ext) = resource_extension_account {
// Validate multicast_group_block_ext (MulticastGroupBlock - global)
assert_eq!(
multicast_group_block_ext.owner, program_id,
"Invalid ResourceExtension Account Owner for MulticastGroupBlock"
);
assert!(
multicast_group_block_ext.is_writable,
"ResourceExtension Account for MulticastGroupBlock is not writable"
);
assert!(
!multicast_group_block_ext.data_is_empty(),
"ResourceExtension Account for MulticastGroupBlock is empty"
);

let (expected_multicast_group_pda, _, _) =
let (expected_pda, _, _) =
get_resource_extension_pda(program_id, ResourceType::MulticastGroupBlock);
assert_eq!(
multicast_group_block_ext.key, &expected_multicast_group_pda,
"Invalid ResourceExtension PDA for MulticastGroupBlock"
validate_program_account!(
multicast_group_block_ext,
program_id,
writable = true,
pda = &expected_pda,
"MulticastGroupBlock"
);

// Deallocate multicast_ip from global MulticastGroupBlock
Expand All @@ -137,6 +126,27 @@ pub fn process_closeaccount_multicastgroup(

try_acc_close(multicastgroup_account, owner_account)?;

// Close the Index account (skip for pre-migration accounts using Pubkey::default())
if *index_account.key != Pubkey::default() {
let (expected_index_pda, _) =
get_index_pda(program_id, SEED_MULTICAST_GROUP, &multicastgroup.code);
validate_program_account!(
index_account,
program_id,
writable = true,
pda = &expected_index_pda,
"Index"
);

let index = Index::try_from(index_account)?;
assert_eq!(
index.pk, *multicastgroup_account.key,
"Index does not point to this MulticastGroup"
);

try_acc_close(index_account, payer_account)?;
}

#[cfg(test)]
msg!("Deactivated: MulticastGroup closed");

Expand Down
Original file line number Diff line number Diff line change
@@ -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::*,
},
};
Expand Down Expand Up @@ -57,17 +58,18 @@ pub fn process_create_multicastgroup(
let mgroup_account = next_account_info(accounts_iter)?;
let globalstate_account = next_account_info(accounts_iter)?;

// Optional: ResourceExtension account for onchain allocation (before payer)
// Optional: ResourceExtension account for onchain allocation
// Account layout WITH ResourceExtension (use_onchain_allocation = true):
// [mgroup, globalstate, multicast_group_block, payer, system]
// [mgroup, globalstate, multicast_group_block, index, payer, system]
// Account layout WITHOUT (legacy, use_onchain_allocation = false):
// [mgroup, globalstate, payer, system]
// [mgroup, globalstate, index, payer, system]
let resource_extension_account = if value.use_onchain_allocation {
Some(next_account_info(accounts_iter)?)
} else {
None
};

let index_account = next_account_info(accounts_iter)?;
let payer_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;

Expand All @@ -80,18 +82,20 @@ pub fn process_create_multicastgroup(
// Validate and normalize code
let code =
validate_account_code(&value.code).map_err(|_| DoubleZeroError::InvalidAccountCode)?;
let lowercase_code = code.to_ascii_lowercase();

// Check the owner of the accounts
assert_eq!(
globalstate_account.owner, program_id,
"Invalid GlobalState Account Owner"
// Validate accounts
validate_program_account!(
globalstate_account,
program_id,
writable = true,
"GlobalState"
);
assert_eq!(
*system_program.unsigned_key(),
solana_system_interface::program::ID,
"Invalid System Program Account Owner"
);
// Check if the account is writable
assert!(mgroup_account.is_writable, "PDA Account is not writable");

// Parse the global state account & check if the payer is in the allowlist
Expand All @@ -114,6 +118,10 @@ pub fn process_create_multicastgroup(
return Err(ProgramError::AccountAlreadyInitialized);
}

// Validate Index PDA (before code is moved into multicastgroup)
let (expected_index_pda, index_bump_seed) =
get_index_pda(program_id, SEED_MULTICAST_GROUP, &code);

let mut multicastgroup = MulticastGroup {
account_type: AccountType::MulticastGroup,
owner: value.owner,
Expand Down Expand Up @@ -147,6 +155,16 @@ pub fn process_create_multicastgroup(
multicastgroup.multicast_ip = allocate_ip(multicast_group_block_ext, 1)?.ip();
multicastgroup.status = MulticastGroupStatus::Activated;
}
assert_eq!(
index_account.key, &expected_index_pda,
"Invalid Index Pubkey"
);
assert!(index_account.is_writable, "Index Account is not writable");

// Uniqueness: index account must not already exist
if !index_account.data_is_empty() {
return Err(ProgramError::AccountAlreadyInitialized);
}

try_acc_create(
&multicastgroup,
Expand All @@ -161,6 +179,31 @@ pub fn process_create_multicastgroup(
&[bump_seed],
],
)?;

// Create the Index account pointing to the multicast group
let index = Index {
account_type: AccountType::Index,
pk: *mgroup_account.key,
entity_account_type: AccountType::MulticastGroup,
key: multicastgroup.code.clone(),
bump_seed: index_bump_seed,
};

try_acc_create(
&index,
index_account,
payer_account,
system_program,
program_id,
&[
SEED_PREFIX,
SEED_INDEX,
SEED_MULTICAST_GROUP,
lowercase_code.as_bytes(),
&[index_bump_seed],
],
)?;

try_acc_write(&globalstate, globalstate_account, payer_account, accounts)?;

Ok(())
Expand Down
Loading
Loading