diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2b706a3..dc1994bf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,6 +128,9 @@ All notable changes to this project will be documented in this file. - The QA alldevices test now considers device location and connects hosts to nearby devices - QA agent and tests now support doublezero connect ibrl's --allocate-addr flag - The QA alldevices test now publishes success/failure metrics to InfluxDB in support of rfc12 +- Onchain programs + - Fix CreateMulticastGroup to use incremented globalstate.account_index for PDA derivation instead of client-provided index, to ensure the contract is the authoritative source for account indices + - ReactivateMulticastGroup now enforces that the multicast group status must be Suspended before reactivation, returning InvalidStatus otherwise; negative-path regression tests were added. ## [v0.8.0](https://github.com/malbeclabs/doublezero/compare/client/v0.7.1...client/v0.8.0) – 2025-12-02 diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/reactivate.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/reactivate.rs index 980e86f63..e8d4010d9 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/reactivate.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/reactivate.rs @@ -66,6 +66,11 @@ pub fn process_reactivate_multicastgroup( } let mut multicastgroup: MulticastGroup = MulticastGroup::try_from(multicastgroup_account)?; + + if multicastgroup.status != MulticastGroupStatus::Suspended { + return Err(DoubleZeroError::InvalidStatus.into()); + } + multicastgroup.status = MulticastGroupStatus::Activated; try_acc_write( diff --git a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs index 57d0b08f4..1051efc74 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs @@ -334,3 +334,78 @@ async fn test_multicastgroup_create_with_wrong_index_fails() { println!("🟢 End test_multicastgroup_create_with_wrong_index_fails"); } + +#[tokio::test] +async fn test_multicastgroup_reactivate_invalid_status_fails() { + let (mut banks_client, program_id, payer, recent_blockhash) = init_test().await; + + println!("🟢 Start test_multicastgroup_reactivate_invalid_status_fails"); + + let (program_config_pubkey, _) = get_program_config_pda(&program_id); + let (globalstate_pubkey, _) = get_globalstate_pda(&program_id); + + println!("1. Global Initialization..."); + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::InitGlobalState(), + vec![ + AccountMeta::new(program_config_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + println!("2. Create MulticastGroup (status Pending)..."); + let globalstate_account = get_globalstate(&mut banks_client, globalstate_pubkey).await; + let (multicastgroup_pubkey, _) = + get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); + + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateMulticastGroup(MulticastGroupCreateArgs { + code: "reactivate-test".to_string(), + max_bandwidth: 1000, + owner: Pubkey::new_unique(), + }), + vec![ + AccountMeta::new(multicastgroup_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + let multicastgroup = get_account_data(&mut banks_client, multicastgroup_pubkey) + .await + .expect("Unable to get Account") + .get_multicastgroup() + .unwrap(); + assert_eq!(multicastgroup.status, MulticastGroupStatus::Pending); + + println!("3. Attempt to reactivate while not Suspended (should fail)..."); + let result = try_execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::ReactivateMulticastGroup(MulticastGroupReactivateArgs {}), + vec![ + AccountMeta::new(multicastgroup_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + assert!( + result.is_err(), + "Reactivate should fail when status is not Suspended" + ); + + println!("✅ Correctly rejected ReactivateMulticastGroup for non-Suspended status"); + println!("🟢 End test_multicastgroup_reactivate_invalid_status_fails"); +}