From e7c61ac9a3e18262d542560b355348f72c212ff9 Mon Sep 17 00:00:00 2001 From: Martin Sander Date: Tue, 3 Mar 2026 00:29:17 -0600 Subject: [PATCH 1/3] smartcontract: avoid duplicate multicast group code at creation --- activator/src/process/multicastgroup.rs | 6 +- .../cli/src/multicastgroup/delete.rs | 2 +- smartcontract/cli/src/multicastgroup/get.rs | 6 +- .../cli/src/multicastgroup/update.rs | 2 +- .../doublezero-serviceability/src/pda.rs | 4 +- .../src/processors/multicastgroup/create.rs | 13 +- ...multicastgroup_allowlist_publisher_test.rs | 22 +-- ...multicastgroup_allowlist_subcriber_test.rs | 20 +-- .../tests/multicastgroup_subscribe_test.rs | 6 +- .../tests/multicastgroup_test.rs | 136 ++++++++++++----- .../tests/user_onchain_allocation_test.rs | 8 +- .../src/commands/multicastgroup/activate.rs | 4 +- .../multicastgroup/allowlist/publisher/add.rs | 22 +-- .../allowlist/publisher/remove.rs | 24 +-- .../allowlist/subscriber/add.rs | 24 +-- .../allowlist/subscriber/remove.rs | 24 +-- .../rs/src/commands/multicastgroup/create.rs | 7 +- .../rs/src/commands/multicastgroup/delete.rs | 2 +- .../sdk/rs/src/commands/multicastgroup/get.rs | 139 +++++++++++------- .../src/commands/multicastgroup/subscribe.rs | 2 +- .../sdk/rs/src/commands/user/delete.rs | 4 +- 21 files changed, 246 insertions(+), 231 deletions(-) diff --git a/activator/src/process/multicastgroup.rs b/activator/src/process/multicastgroup.rs index b00371b54..850d82273 100644 --- a/activator/src/process/multicastgroup.rs +++ b/activator/src/process/multicastgroup.rs @@ -225,7 +225,7 @@ mod tests { metrics::with_local_recorder(&recorder, || { let mut client = create_test_client(); - let (_, bump_seed) = get_multicastgroup_pda(&client.get_program_id(), 1); + let (_, bump_seed) = get_multicastgroup_pda(&client.get_program_id(), "test"); client .expect_execute_transaction() .with( @@ -308,7 +308,7 @@ mod tests { metrics::with_local_recorder(&recorder, || { let mut client = create_test_client(); - let (_, bump_seed) = get_multicastgroup_pda(&client.get_program_id(), 1); + let (_, bump_seed) = get_multicastgroup_pda(&client.get_program_id(), "test"); // Stateless mode: multicast_ip=UNSPECIFIED (onchain will allocate) client @@ -376,7 +376,7 @@ mod tests { metrics::with_local_recorder(&recorder, || { let mut client = create_test_client(); - let (_, bump_seed) = get_multicastgroup_pda(&client.get_program_id(), 1); + let (_, bump_seed) = get_multicastgroup_pda(&client.get_program_id(), "test"); let mut multicastgroups = HashMap::new(); let pubkey = Pubkey::new_unique(); diff --git a/smartcontract/cli/src/multicastgroup/delete.rs b/smartcontract/cli/src/multicastgroup/delete.rs index a670295b0..73f1615c1 100644 --- a/smartcontract/cli/src/multicastgroup/delete.rs +++ b/smartcontract/cli/src/multicastgroup/delete.rs @@ -216,7 +216,7 @@ mod tests { fn test_cli_multicastgroup_delete() { let mut client = create_test_client(); - let (mgroup_pubkey, _bump_seed) = get_multicastgroup_pda(&client.get_program_id(), 1); + let (mgroup_pubkey, _bump_seed) = get_multicastgroup_pda(&client.get_program_id(), "test"); let signature = Signature::from([ 120, 138, 162, 185, 59, 209, 241, 157, 71, 157, 74, 131, 4, 87, 54, 28, 38, 180, 222, 82, 64, 62, 61, 62, 22, 46, 17, 203, 187, 136, 62, 43, 11, 38, 235, 17, 239, 82, 240, diff --git a/smartcontract/cli/src/multicastgroup/get.rs b/smartcontract/cli/src/multicastgroup/get.rs index 21115a0c6..9ee29504d 100644 --- a/smartcontract/cli/src/multicastgroup/get.rs +++ b/smartcontract/cli/src/multicastgroup/get.rs @@ -259,7 +259,7 @@ mod tests { Ok(devices) }); - let (mgroup_pubkey, _bump_seed) = get_multicastgroup_pda(&client.get_program_id(), 1); + let (mgroup_pubkey, _bump_seed) = get_multicastgroup_pda(&client.get_program_id(), "test"); let user1_pk = Pubkey::from_str_const("11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo1"); let user1 = User { @@ -358,7 +358,7 @@ mod tests { .execute(&client, &mut output); assert!(res.is_ok(), "I should find a item by pubkey"); let output_str = String::from_utf8(output).unwrap(); - assert_eq!(output_str, "account: G4DjGHreV54t5yeNuSHi5iVcT5Qkykuj43pWWdSsP3dj\r\ncode: test\r\nmulticast_ip: 10.0.0.1\r\nmax_bandwidth: 1Gbps\r\nstatus: activated\r\nowner: G4DjGHreV54t5yeNuSHi5iVcT5Qkykuj43pWWdSsP3dj\n\r\nallowlist:\r\n account | mode | client_ip | user_payer \n G4DjGHreV54t5yeNuSHi5iVcT5Qkykuj43pWWdSsP3dj | P+S | 192.168.1.1 | 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo1 \n\r\nusers:\r\n account | multicast_mode | device | location | cyoa_type | client_ip | tunnel_id | tunnel_net | dz_ip | status | owner \n 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo1 | P | 11111111111111111111111111111111 | | GREOverDIA | 192.168.1.1 | 12345 | 10.0.0.0/32 | 10.0.0.2 | activated | 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo1 \n"); + assert_eq!(output_str, "account: 8BPMVhpGxZrAowXyBUtkATgAFB2dz7sKAq5yKgospWPp\r\ncode: test\r\nmulticast_ip: 10.0.0.1\r\nmax_bandwidth: 1Gbps\r\nstatus: activated\r\nowner: 8BPMVhpGxZrAowXyBUtkATgAFB2dz7sKAq5yKgospWPp\n\r\nallowlist:\r\n account | mode | client_ip | user_payer \n 8BPMVhpGxZrAowXyBUtkATgAFB2dz7sKAq5yKgospWPp | P+S | 192.168.1.1 | 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo1 \n\r\nusers:\r\n account | multicast_mode | device | location | cyoa_type | client_ip | tunnel_id | tunnel_net | dz_ip | status | owner \n 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo1 | P | 11111111111111111111111111111111 | | GREOverDIA | 192.168.1.1 | 12345 | 10.0.0.0/32 | 10.0.0.2 | activated | 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo1 \n"); // Expected success let mut output = Vec::new(); @@ -368,6 +368,6 @@ mod tests { .execute(&client, &mut output); assert!(res.is_ok(), "I should find a item by code"); let output_str = String::from_utf8(output).unwrap(); - assert_eq!(output_str, "account: G4DjGHreV54t5yeNuSHi5iVcT5Qkykuj43pWWdSsP3dj\r\ncode: test\r\nmulticast_ip: 10.0.0.1\r\nmax_bandwidth: 1Gbps\r\nstatus: activated\r\nowner: G4DjGHreV54t5yeNuSHi5iVcT5Qkykuj43pWWdSsP3dj\n\r\nallowlist:\r\n account | mode | client_ip | user_payer \n G4DjGHreV54t5yeNuSHi5iVcT5Qkykuj43pWWdSsP3dj | P+S | 192.168.1.1 | 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo1 \n\r\nusers:\r\n account | multicast_mode | device | location | cyoa_type | client_ip | tunnel_id | tunnel_net | dz_ip | status | owner \n 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo1 | P | 11111111111111111111111111111111 | | GREOverDIA | 192.168.1.1 | 12345 | 10.0.0.0/32 | 10.0.0.2 | activated | 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo1 \n"); + assert_eq!(output_str, "account: 8BPMVhpGxZrAowXyBUtkATgAFB2dz7sKAq5yKgospWPp\r\ncode: test\r\nmulticast_ip: 10.0.0.1\r\nmax_bandwidth: 1Gbps\r\nstatus: activated\r\nowner: 8BPMVhpGxZrAowXyBUtkATgAFB2dz7sKAq5yKgospWPp\n\r\nallowlist:\r\n account | mode | client_ip | user_payer \n 8BPMVhpGxZrAowXyBUtkATgAFB2dz7sKAq5yKgospWPp | P+S | 192.168.1.1 | 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo1 \n\r\nusers:\r\n account | multicast_mode | device | location | cyoa_type | client_ip | tunnel_id | tunnel_net | dz_ip | status | owner \n 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo1 | P | 11111111111111111111111111111111 | | GREOverDIA | 192.168.1.1 | 12345 | 10.0.0.0/32 | 10.0.0.2 | activated | 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo1 \n"); } } diff --git a/smartcontract/cli/src/multicastgroup/update.rs b/smartcontract/cli/src/multicastgroup/update.rs index bb8505dff..62cc5c096 100644 --- a/smartcontract/cli/src/multicastgroup/update.rs +++ b/smartcontract/cli/src/multicastgroup/update.rs @@ -165,7 +165,7 @@ mod tests { fn test_cli_multicastgroup_update() { let mut client = create_test_client(); - let (pda_pubkey, _bump_seed) = get_multicastgroup_pda(&client.get_program_id(), 1); + let (pda_pubkey, _bump_seed) = get_multicastgroup_pda(&client.get_program_id(), "test"); let signature = Signature::from([ 120, 138, 162, 185, 59, 209, 241, 157, 71, 157, 74, 131, 4, 87, 54, 28, 38, 180, 222, 82, 64, 62, 61, 62, 22, 46, 17, 203, 187, 136, 62, 43, 11, 38, 235, 17, 239, 82, 240, diff --git a/smartcontract/programs/doublezero-serviceability/src/pda.rs b/smartcontract/programs/doublezero-serviceability/src/pda.rs index 72b104806..3314e864c 100644 --- a/smartcontract/programs/doublezero-serviceability/src/pda.rs +++ b/smartcontract/programs/doublezero-serviceability/src/pda.rs @@ -62,9 +62,9 @@ pub fn get_user_pda(program_id: &Pubkey, ip: &Ipv4Addr, user_type: UserType) -> ) } -pub fn get_multicastgroup_pda(program_id: &Pubkey, index: u128) -> (Pubkey, u8) { +pub fn get_multicastgroup_pda(program_id: &Pubkey, code: &str) -> (Pubkey, u8) { Pubkey::find_program_address( - &[SEED_PREFIX, SEED_MULTICAST_GROUP, &index.to_le_bytes()], + &[SEED_PREFIX, SEED_MULTICAST_GROUP, code.as_bytes()], program_id, ) } diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs index 40f7c3899..127642818 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/multicastgroup/create.rs @@ -2,7 +2,7 @@ use crate::{ error::DoubleZeroError, pda::get_multicastgroup_pda, seeds::{SEED_MULTICAST_GROUP, SEED_PREFIX}, - serializer::{try_acc_create, try_acc_write}, + serializer::try_acc_create, state::{accounttype::AccountType, globalstate::GlobalState, multicastgroup::*}, }; use borsh::BorshSerialize; @@ -72,12 +72,10 @@ pub fn process_create_multicastgroup( assert!(mgroup_account.is_writable, "PDA Account is not writable"); // Parse the global state account & check if the payer is in the allowlist - let mut globalstate = GlobalState::try_from(globalstate_account)?; - globalstate.account_index += 1; + let globalstate = GlobalState::try_from(globalstate_account)?; // Get the PDA pubkey and bump seed for the account multicastgroup & check if it matches the account - let (expected_pda_account, bump_seed) = - get_multicastgroup_pda(program_id, globalstate.account_index); + let (expected_pda_account, bump_seed) = get_multicastgroup_pda(program_id, &code); assert_eq!( mgroup_account.key, &expected_pda_account, "Invalid MulticastGroup Pubkey" @@ -94,7 +92,7 @@ pub fn process_create_multicastgroup( let multicastgroup = MulticastGroup { account_type: AccountType::MulticastGroup, owner: value.owner, - index: globalstate.account_index, + index: 0, bump_seed, tenant_pk: Pubkey::default(), code, @@ -114,11 +112,10 @@ pub fn process_create_multicastgroup( &[ SEED_PREFIX, SEED_MULTICAST_GROUP, - &globalstate.account_index.to_le_bytes(), + multicastgroup.code.as_bytes(), &[bump_seed], ], )?; - try_acc_write(&globalstate, globalstate_account, payer_account, accounts)?; Ok(()) } 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 ed508a349..41046a094 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_publisher_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_publisher_test.rs @@ -54,14 +54,7 @@ async fn test_multicast_publisher_allowlist() { /*****************************************************************************************************************************************************/ println!("🟢 2. Create MulticastGroup..."); - let globalstate = get_account_data(&mut banks_client, globalstate_pubkey) - .await - .expect("Unable to get Account") - .get_global_state() - .unwrap(); - - let (multicastgroup_pubkey, _) = - get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "test"); execute_transaction( &mut banks_client, @@ -94,8 +87,6 @@ async fn test_multicast_publisher_allowlist() { /*****************************************************************************************************************************************************/ println!("🟢 3. Activate MulticastGroup..."); - let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, 1); - execute_transaction( &mut banks_client, recent_blockhash, @@ -181,7 +172,7 @@ async fn test_multicast_publisher_allowlist() { /*****************************************************************************************************************************************************/ println!("🟢 6. Remove Allowlist ..."); - let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, 1); + let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "test"); execute_transaction( &mut banks_client, @@ -264,14 +255,7 @@ async fn test_multicast_publisher_allowlist_sentinel_authority() { .await; // 3. Create and activate a multicast group (owned by payer, NOT sentinel) - let globalstate = get_account_data(&mut banks_client, globalstate_pubkey) - .await - .expect("Unable to get Account") - .get_global_state() - .unwrap(); - - let (multicastgroup_pubkey, _) = - get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "sentinel-test"); execute_transaction( &mut banks_client, 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 4484f96e5..e31f01b25 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_subcriber_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_allowlist_subcriber_test.rs @@ -54,14 +54,7 @@ async fn test_multicast_subscriber_allowlist() { /*****************************************************************************************************************************************************/ println!("🟢 2. Create MulticastGroup..."); - let globalstate = get_account_data(&mut banks_client, globalstate_pubkey) - .await - .expect("Unable to get Account") - .get_global_state() - .unwrap(); - - let (multicastgroup_pubkey, _) = - get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "test"); execute_transaction( &mut banks_client, @@ -94,8 +87,6 @@ async fn test_multicast_subscriber_allowlist() { /*****************************************************************************************************************************************************/ println!("🟢 3. Activate MulticastGroup..."); - let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, 1); - execute_transaction( &mut banks_client, recent_blockhash, @@ -264,14 +255,7 @@ async fn test_multicast_subscriber_allowlist_sentinel_authority() { .await; // 3. Create and activate a multicast group (owned by payer, NOT sentinel) - let globalstate = get_account_data(&mut banks_client, globalstate_pubkey) - .await - .expect("Unable to get Account") - .get_global_state() - .unwrap(); - - let (multicastgroup_pubkey, _) = - get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "sentinel-test"); execute_transaction( &mut banks_client, diff --git a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs index eab5f7b76..950ccd0cb 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_subscribe_test.rs @@ -251,8 +251,7 @@ async fn setup_fixture() -> TestFixture { .await; // 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); + let (mgroup1_pubkey, _) = get_multicastgroup_pda(&program_id, "group1"); execute_transaction( &mut banks_client, recent_blockhash, @@ -285,8 +284,7 @@ async fn setup_fixture() -> TestFixture { ) .await; - let gs = get_globalstate(&mut banks_client, globalstate_pubkey).await; - let (mgroup2_pubkey, _) = get_multicastgroup_pda(&program_id, gs.account_index + 1); + let (mgroup2_pubkey, _) = get_multicastgroup_pda(&program_id, "group2"); execute_transaction( &mut banks_client, recent_blockhash, diff --git a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs index fff19985f..8f208335d 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/multicastgroup_test.rs @@ -45,11 +45,8 @@ async fn test_multicastgroup() { let (globalstate_pubkey, _) = get_globalstate_pda(&program_id); println!("1. Testing MulticastGroup initialization..."); - let globalstate_account = get_globalstate(&mut banks_client, globalstate_pubkey).await; - assert_eq!(globalstate_account.account_index, 0); - let (multicastgroup_pubkey, _) = - get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); + let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "la"); execute_transaction( &mut banks_client, @@ -270,9 +267,7 @@ async fn test_multicastgroup_deactivate_fails_when_counts_nonzero() { ) .await; - let globalstate_account = get_globalstate(&mut banks_client, globalstate_pubkey).await; - let (multicastgroup_pubkey, _) = - get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); + let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "la"); execute_transaction( &mut banks_client, @@ -388,9 +383,7 @@ async fn test_multicastgroup_deactivate_fails_when_not_deleting() { ) .await; - let globalstate_account = get_globalstate(&mut banks_client, globalstate_pubkey).await; - let (multicastgroup_pubkey, _) = - get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); + let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "la"); execute_transaction( &mut banks_client, @@ -462,10 +455,10 @@ async fn test_multicastgroup_deactivate_fails_when_not_deleting() { } #[tokio::test] -async fn test_multicastgroup_create_with_wrong_index_fails() { +async fn test_multicastgroup_create_with_wrong_pda_fails() { let (mut banks_client, program_id, payer, recent_blockhash) = init_test().await; - println!("🟢 Start test_multicastgroup_create_with_wrong_index_fails"); + println!("🟢 Start test_multicastgroup_create_with_wrong_pda_fails"); let (program_config_pubkey, _) = get_program_config_pda(&program_id); let (globalstate_pubkey, _) = get_globalstate_pda(&program_id); @@ -484,18 +477,12 @@ async fn test_multicastgroup_create_with_wrong_index_fails() { ) .await; - println!("2. Testing MulticastGroup creation with wrong index..."); - let globalstate_account = get_globalstate(&mut banks_client, globalstate_pubkey).await; - assert_eq!(globalstate_account.account_index, 0); + println!("2. Testing MulticastGroup creation with mismatched PDA..."); - // Client passes wrong index (999 instead of 1) - let wrong_index = 999; - let correct_index = globalstate_account.account_index + 1; + // Derive PDA for a different code than what the instruction contains + let (wrong_multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "wrong_code"); - // Derive PDA with the WRONG index (what a malicious/buggy client might do) - let (wrong_multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, wrong_index); - - // Try to create with wrong index - should fail + // Try to create with mismatched PDA - should fail let result = try_execute_transaction( &mut banks_client, recent_blockhash, @@ -515,13 +502,13 @@ async fn test_multicastgroup_create_with_wrong_index_fails() { assert!( result.is_err(), - "Transaction should have failed with wrong index" + "Transaction should have failed with mismatched PDA" ); - println!("✅ Correctly rejected wrong index"); + println!("✅ Correctly rejected mismatched PDA"); - // Verify the correct index still works - println!("3. Testing MulticastGroup creation with correct index..."); - let (correct_multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, correct_index); + // Verify the correct PDA still works + println!("3. Testing MulticastGroup creation with correct PDA..."); + let (correct_multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "test"); execute_transaction( &mut banks_client, @@ -545,11 +532,10 @@ async fn test_multicastgroup_create_with_wrong_index_fails() { .expect("Unable to get Account") .get_multicastgroup() .unwrap(); - assert_eq!(multicastgroup.index, correct_index); assert_eq!(multicastgroup.code, "test".to_string()); - println!("✅ Correct index accepted and stored properly"); + println!("✅ Correct PDA accepted and stored properly"); - println!("🟢 End test_multicastgroup_create_with_wrong_index_fails"); + println!("🟢 End test_multicastgroup_create_with_wrong_pda_fails"); } #[tokio::test] @@ -576,9 +562,7 @@ async fn test_multicastgroup_reactivate_invalid_status_fails() { .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); + let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "reactivate-test"); execute_transaction( &mut banks_client, @@ -649,9 +633,7 @@ async fn test_suspend_multicastgroup_from_pending_fails() { .await; // Create a multicast group (starts in Pending status) - let globalstate_account = get_globalstate(&mut banks_client, globalstate_pubkey).await; - let (multicastgroup_pubkey, _) = - get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); + let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "test"); execute_transaction( &mut banks_client, @@ -726,9 +708,7 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( .await; println!("2. Create MulticastGroup..."); - let globalstate_account = get_globalstate(&mut banks_client, globalstate_pubkey).await; - let (multicastgroup_pubkey, _) = - get_multicastgroup_pda(&program_id, globalstate_account.account_index + 1); + let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "delete-test"); execute_transaction( &mut banks_client, @@ -900,3 +880,81 @@ async fn test_delete_multicastgroup_fails_with_active_publishers_or_subscribers( println!("🟢 End test_delete_multicastgroup_fails_with_active_publishers_or_subscribers"); } + +#[tokio::test] +async fn test_multicastgroup_create_duplicate_code_fails() { + let (mut banks_client, program_id, payer, recent_blockhash) = init_test().await; + + println!("🟢 Start test_multicastgroup_create_duplicate_code_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 first MulticastGroup with code 'unique-mg'..."); + let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "unique-mg"); + + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateMulticastGroup(MulticastGroupCreateArgs { + code: "unique-mg".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.code, "unique-mg"); + println!("✅ First MulticastGroup created successfully"); + + println!("3. Try to create second MulticastGroup with same code (should fail)..."); + let result = try_execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateMulticastGroup(MulticastGroupCreateArgs { + code: "unique-mg".to_string(), + max_bandwidth: 2000, + owner: Pubkey::new_unique(), + }), + vec![ + AccountMeta::new(multicastgroup_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + assert!( + result.is_err(), + "Creating a duplicate MulticastGroup with the same code should fail" + ); + println!("✅ Duplicate MulticastGroup correctly rejected"); + + println!("🟢 End test_multicastgroup_create_duplicate_code_fails"); +} 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 c245000da..d91b5b580 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/user_onchain_allocation_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/user_onchain_allocation_test.rs @@ -1085,9 +1085,7 @@ async fn test_multicast_subscribe_reactivation_preserves_allocations() { // ========================================================================= let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; - let globalstate = get_globalstate(&mut banks_client, globalstate_pubkey).await; - let (multicastgroup_pubkey, _) = - get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "test-mgroup"); // Create multicast group (4 accounts: mgroup, globalstate, payer, system_program) execute_transaction( @@ -1400,9 +1398,7 @@ async fn test_multicast_publisher_block_deallocation_and_reuse() { // ========================================================================= let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; - let globalstate = get_globalstate(&mut banks_client, globalstate_pubkey).await; - let (multicastgroup_pubkey, _) = - get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + let (multicastgroup_pubkey, _) = get_multicastgroup_pda(&program_id, "test-mgroup"); execute_transaction( &mut banks_client, diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/activate.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/activate.rs index 3f19fa85d..9865d7c47 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/activate.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/activate.rs @@ -67,7 +67,7 @@ mod tests { let mut client = create_test_client(); let (globalstate_pubkey, _) = get_globalstate_pda(&client.get_program_id()); - let (mgroup_pubkey, _) = get_multicastgroup_pda(&client.get_program_id(), 1); + let (mgroup_pubkey, _) = get_multicastgroup_pda(&client.get_program_id(), "test"); client .expect_execute_transaction() @@ -99,7 +99,7 @@ mod tests { let mut client = create_test_client(); let (globalstate_pubkey, _) = get_globalstate_pda(&client.get_program_id()); - let (mgroup_pubkey, _) = get_multicastgroup_pda(&client.get_program_id(), 1); + let (mgroup_pubkey, _) = get_multicastgroup_pda(&client.get_program_id(), "test"); let (resource_ext_pubkey, _, _) = get_resource_extension_pda(&client.get_program_id(), ResourceType::MulticastGroupBlock); 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 11b448170..3645293c5 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/publisher/add.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/publisher/add.rs @@ -46,10 +46,11 @@ impl AddMulticastGroupPubAllowlistCommand { mod tests { use crate::{ commands::multicastgroup::allowlist::publisher::add::AddMulticastGroupPubAllowlistCommand, - tests::utils::create_test_client, + tests::utils::create_test_client, DoubleZeroClient, }; use doublezero_serviceability::{ instructions::DoubleZeroInstruction, + pda::get_multicastgroup_pda, processors::multicastgroup::allowlist::publisher::add::AddMulticastGroupPubAllowlistArgs, state::{ accountdata::AccountData, @@ -64,10 +65,10 @@ mod tests { fn test_commands_multicastgroup_allowlist_publisher_add() { let mut client = create_test_client(); - let pubkey = Pubkey::new_unique(); + let (code_pda, _) = get_multicastgroup_pda(&client.get_program_id(), "test_code"); let mgroup = MulticastGroup { account_type: AccountType::MulticastGroup, - index: 1, + index: 0, bump_seed: 1, owner: Pubkey::new_unique(), tenant_pk: Pubkey::new_unique(), @@ -82,24 +83,15 @@ mod tests { let cloned_mgroup = mgroup.clone(); client .expect_get() - .with(predicate::eq(pubkey)) + .with(predicate::eq(code_pda)) .returning(move |_| Ok(AccountData::MulticastGroup(cloned_mgroup.clone()))); - let cloned_mgroup = mgroup.clone(); - client - .expect_gets() - .with(predicate::eq(AccountType::MulticastGroup)) - .returning(move |_| { - let mut map = std::collections::HashMap::new(); - map.insert(pubkey, AccountData::MulticastGroup(cloned_mgroup.clone())); - Ok(map) - }); client .expect_execute_transaction() .with( predicate::eq(DoubleZeroInstruction::AddMulticastGroupPubAllowlist( AddMulticastGroupPubAllowlistArgs { client_ip: [192, 168, 1, 1].into(), - user_payer: pubkey, + user_payer: code_pda, }, )), predicate::always(), @@ -109,7 +101,7 @@ mod tests { let res = AddMulticastGroupPubAllowlistCommand { pubkey_or_code: "test_code".to_string(), client_ip: [192, 168, 1, 1].into(), - user_payer: pubkey, + user_payer: code_pda, } .execute(&client); 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 b4adf443c..864f0042a 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/publisher/remove.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/publisher/remove.rs @@ -48,10 +48,11 @@ impl RemoveMulticastGroupPubAllowlistCommand { mod tests { use crate::{ commands::multicastgroup::allowlist::publisher::remove::RemoveMulticastGroupPubAllowlistCommand, - tests::utils::create_test_client, + tests::utils::create_test_client, DoubleZeroClient, }; use doublezero_serviceability::{ instructions::DoubleZeroInstruction, + pda::get_multicastgroup_pda, processors::multicastgroup::allowlist::publisher::remove::RemoveMulticastGroupPubAllowlistArgs, state::{ accountdata::AccountData, @@ -66,10 +67,10 @@ mod tests { fn test_commands_multicastgroup_allowlist_publisher_remove() { let mut client = create_test_client(); - let pubkey = Pubkey::new_unique(); + let (code_pda, _) = get_multicastgroup_pda(&client.get_program_id(), "test_code"); let mgroup = MulticastGroup { account_type: AccountType::MulticastGroup, - index: 1, + index: 0, bump_seed: 1, owner: Pubkey::new_unique(), tenant_pk: Pubkey::new_unique(), @@ -84,24 +85,15 @@ mod tests { let cloned_mgroup = mgroup.clone(); client .expect_get() - .with(predicate::eq(pubkey)) + .with(predicate::eq(code_pda)) .returning(move |_| Ok(AccountData::MulticastGroup(cloned_mgroup.clone()))); - let cloned_mgroup = mgroup.clone(); - client - .expect_gets() - .with(predicate::eq(AccountType::MulticastGroup)) - .returning(move |_| { - let mut map = std::collections::HashMap::new(); - map.insert(pubkey, AccountData::MulticastGroup(cloned_mgroup.clone())); - Ok(map) - }); client .expect_execute_transaction() .with( predicate::eq(DoubleZeroInstruction::RemoveMulticastGroupPubAllowlist( RemoveMulticastGroupPubAllowlistArgs { client_ip: [192, 168, 1, 1].into(), - user_payer: pubkey, + user_payer: code_pda, }, )), predicate::always(), @@ -112,7 +104,7 @@ mod tests { let res = RemoveMulticastGroupPubAllowlistCommand { pubkey_or_code: "test_code".to_string(), client_ip: [192, 168, 1, 1].into(), - user_payer: pubkey, + user_payer: code_pda, } .execute(&client); assert!(res.is_ok()); @@ -121,7 +113,7 @@ mod tests { let res = RemoveMulticastGroupPubAllowlistCommand { pubkey_or_code: "test^code".to_string(), client_ip: [192, 168, 1, 1].into(), - user_payer: pubkey, + user_payer: code_pda, } .execute(&client); assert!(res.is_err()); 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 60b79ea23..570319512 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/subscriber/add.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/subscriber/add.rs @@ -48,10 +48,11 @@ impl AddMulticastGroupSubAllowlistCommand { mod tests { use crate::{ commands::multicastgroup::allowlist::subscriber::add::AddMulticastGroupSubAllowlistCommand, - tests::utils::create_test_client, + tests::utils::create_test_client, DoubleZeroClient, }; use doublezero_serviceability::{ instructions::DoubleZeroInstruction, + pda::get_multicastgroup_pda, processors::multicastgroup::allowlist::subscriber::add::AddMulticastGroupSubAllowlistArgs, state::{ accountdata::AccountData, @@ -66,10 +67,10 @@ mod tests { fn test_commands_multicastgroup_allowlist_subscriber_add() { let mut client = create_test_client(); - let pubkey = Pubkey::new_unique(); + let (code_pda, _) = get_multicastgroup_pda(&client.get_program_id(), "test_code"); let mgroup = MulticastGroup { account_type: AccountType::MulticastGroup, - index: 1, + index: 0, bump_seed: 1, owner: Pubkey::new_unique(), tenant_pk: Pubkey::new_unique(), @@ -84,24 +85,15 @@ mod tests { let cloned_mgroup = mgroup.clone(); client .expect_get() - .with(predicate::eq(pubkey)) + .with(predicate::eq(code_pda)) .returning(move |_| Ok(AccountData::MulticastGroup(cloned_mgroup.clone()))); - let cloned_mgroup = mgroup.clone(); - client - .expect_gets() - .with(predicate::eq(AccountType::MulticastGroup)) - .returning(move |_| { - let mut map = std::collections::HashMap::new(); - map.insert(pubkey, AccountData::MulticastGroup(cloned_mgroup.clone())); - Ok(map) - }); client .expect_execute_transaction() .with( predicate::eq(DoubleZeroInstruction::AddMulticastGroupSubAllowlist( AddMulticastGroupSubAllowlistArgs { client_ip: [192, 168, 1, 1].into(), - user_payer: pubkey, + user_payer: code_pda, }, )), predicate::always(), @@ -112,7 +104,7 @@ mod tests { let res = AddMulticastGroupSubAllowlistCommand { pubkey_or_code: "test_code".to_string(), client_ip: [192, 168, 1, 1].into(), - user_payer: pubkey, + user_payer: code_pda, } .execute(&client); assert!(res.is_ok()); @@ -121,7 +113,7 @@ mod tests { let res = AddMulticastGroupSubAllowlistCommand { pubkey_or_code: "test code".to_string(), client_ip: [192, 168, 1, 1].into(), - user_payer: pubkey, + user_payer: code_pda, } .execute(&client); assert!(res.is_err()); 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 2a3431c71..4d6c9a3fb 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/subscriber/remove.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/allowlist/subscriber/remove.rs @@ -48,10 +48,11 @@ impl RemoveMulticastGroupSubAllowlistCommand { mod tests { use crate::{ commands::multicastgroup::allowlist::subscriber::remove::RemoveMulticastGroupSubAllowlistCommand, - tests::utils::create_test_client, + tests::utils::create_test_client, DoubleZeroClient, }; use doublezero_serviceability::{ instructions::DoubleZeroInstruction, + pda::get_multicastgroup_pda, processors::multicastgroup::allowlist::subscriber::remove::RemoveMulticastGroupSubAllowlistArgs, state::{ accountdata::AccountData, @@ -66,10 +67,10 @@ mod tests { fn test_commands_multicastgroup_allowlist_subscriber_remove() { let mut client = create_test_client(); - let pubkey = Pubkey::new_unique(); + let (code_pda, _) = get_multicastgroup_pda(&client.get_program_id(), "test_code"); let mgroup = MulticastGroup { account_type: AccountType::MulticastGroup, - index: 1, + index: 0, bump_seed: 1, owner: Pubkey::new_unique(), tenant_pk: Pubkey::new_unique(), @@ -84,24 +85,15 @@ mod tests { let cloned_mgroup = mgroup.clone(); client .expect_get() - .with(predicate::eq(pubkey)) + .with(predicate::eq(code_pda)) .returning(move |_| Ok(AccountData::MulticastGroup(cloned_mgroup.clone()))); - let cloned_mgroup = mgroup.clone(); - client - .expect_gets() - .with(predicate::eq(AccountType::MulticastGroup)) - .returning(move |_| { - let mut map = std::collections::HashMap::new(); - map.insert(pubkey, AccountData::MulticastGroup(cloned_mgroup.clone())); - Ok(map) - }); client .expect_execute_transaction() .with( predicate::eq(DoubleZeroInstruction::RemoveMulticastGroupSubAllowlist( RemoveMulticastGroupSubAllowlistArgs { client_ip: [192, 168, 1, 1].into(), - user_payer: pubkey, + user_payer: code_pda, }, )), predicate::always(), @@ -112,7 +104,7 @@ mod tests { let res = RemoveMulticastGroupSubAllowlistCommand { pubkey_or_code: "test_code".to_string(), client_ip: [192, 168, 1, 1].into(), - user_payer: pubkey, + user_payer: code_pda, } .execute(&client); assert!(res.is_ok()); @@ -121,7 +113,7 @@ mod tests { let res = RemoveMulticastGroupSubAllowlistCommand { pubkey_or_code: "test%code".to_string(), client_ip: [192, 168, 1, 1].into(), - user_payer: pubkey, + user_payer: code_pda, } .execute(&client); assert!(res.is_err()); diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/create.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/create.rs index e4443ef5a..2adf7aa18 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/create.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/create.rs @@ -19,12 +19,11 @@ impl CreateMulticastGroupCommand { let code = validate_account_code(&self.code).map_err(|err| eyre::eyre!("invalid code: {err}"))?; - let (globalstate_pubkey, globalstate) = GetGlobalStateCommand + let (globalstate_pubkey, _) = GetGlobalStateCommand .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; - let (pda_pubkey, _) = - get_multicastgroup_pda(&client.get_program_id(), globalstate.account_index + 1); + let (pda_pubkey, _) = get_multicastgroup_pda(&client.get_program_id(), &code); client .execute_transaction( DoubleZeroInstruction::CreateMulticastGroup(MulticastGroupCreateArgs { @@ -60,7 +59,7 @@ mod tests { let mut client = create_test_client(); let (globalstate_pubkey, _globalstate) = get_globalstate_pda(&client.get_program_id()); - let (pda_pubkey, _) = get_multicastgroup_pda(&client.get_program_id(), 1); + let (pda_pubkey, _) = get_multicastgroup_pda(&client.get_program_id(), "test_group"); client .expect_execute_transaction() diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/delete.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/delete.rs index aaa7708d6..acd146fb9 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/delete.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/delete.rs @@ -55,7 +55,7 @@ mod tests { let mut client = create_test_client(); let (globalstate_pubkey, _globalstate) = get_globalstate_pda(&client.get_program_id()); - let (pda_pubkey, bump_seed) = get_multicastgroup_pda(&client.get_program_id(), 1); + let (pda_pubkey, bump_seed) = get_multicastgroup_pda(&client.get_program_id(), "mg01"); let mgroup = MulticastGroup { account_type: AccountType::MulticastGroup, diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/get.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/get.rs index 0c0d1ad28..610cfa246 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_program_common::validate_account_code; +use doublezero_serviceability::{ + pda::get_multicastgroup_pda, + state::{accountdata::AccountData, accounttype::AccountType, multicastgroup::MulticastGroup}, }; use solana_sdk::pubkey::Pubkey; @@ -11,43 +13,47 @@ pub struct GetMulticastGroupCommand { impl GetMulticastGroupCommand { pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result<(Pubkey, MulticastGroup)> { - match parse_pubkey(&self.pubkey_or_code) { - Some(pk) => match client.get(pk)? { - AccountData::MulticastGroup(multicastgroup) => Ok((pk, multicastgroup)), + if let Some(pk) = parse_pubkey(&self.pubkey_or_code) { + return match client.get(pk)? { + AccountData::MulticastGroup(mg) => Ok((pk, mg)), _ => 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 - )) - }), + }; + } + + let code = validate_account_code(&self.pubkey_or_code) + .map_err(|_| eyre::eyre!("invalid code: {}", self.pubkey_or_code))?; + + // Try code-based PDA first (new derivation) + let (pda, _) = get_multicastgroup_pda(&client.get_program_id(), &code); + if let Ok(AccountData::MulticastGroup(mg)) = client.get(pda) { + return Ok((pda, mg)); + } + + // Fallback: scan all multicast groups for legacy index-based PDAs + let (pk, mg) = client + .gets(AccountType::MulticastGroup)? + .into_iter() + .find(|(_, v)| matches!(v, AccountData::MulticastGroup(mg) if mg.code == code)) + .ok_or_else(|| eyre::eyre!("multicast group not found: {}", code))?; + + match mg { + AccountData::MulticastGroup(mg) => Ok((pk, mg)), + _ => Err(eyre::eyre!("Invalid Account Type")), } } } #[cfg(test)] mod tests { - use std::collections::HashMap; - use crate::{ commands::multicastgroup::get::GetMulticastGroupCommand, tests::utils::create_test_client, + DoubleZeroClient, }; - use doublezero_serviceability::state::{ - accountdata::AccountData, accounttype::AccountType, multicastgroup::MulticastGroup, + use doublezero_serviceability::{ + pda::get_multicastgroup_pda, + state::{ + accountdata::AccountData, accounttype::AccountType, multicastgroup::MulticastGroup, + }, }; use mockall::predicate; use solana_sdk::pubkey::Pubkey; @@ -59,29 +65,27 @@ mod tests { let multicastgroup_pubkey = Pubkey::new_unique(); let multicastgroup = MulticastGroup { account_type: AccountType::MulticastGroup, - index: 1, + index: 0, bump_seed: 2, code: "multicastgroup_code".to_string(), owner: Pubkey::new_unique(), ..Default::default() }; + // Mock for pubkey-based lookup let multicastgroup2 = multicastgroup.clone(); client .expect_get() .with(predicate::eq(multicastgroup_pubkey)) .returning(move |_| Ok(AccountData::MulticastGroup(multicastgroup2.clone()))); + // Mock for code-based PDA lookup + let (code_pda, _) = get_multicastgroup_pda(&client.get_program_id(), "multicastgroup_code"); let multicastgroup2 = multicastgroup.clone(); client - .expect_gets() - .with(predicate::eq(AccountType::MulticastGroup)) - .returning(move |_| { - Ok(HashMap::from([( - multicastgroup_pubkey, - AccountData::MulticastGroup(multicastgroup2.clone()), - )])) - }); + .expect_get() + .with(predicate::eq(code_pda)) + .returning(move |_| Ok(AccountData::MulticastGroup(multicastgroup2.clone()))); // Search by pubkey let res = GetMulticastGroupCommand { @@ -94,7 +98,7 @@ mod tests { assert_eq!(res.1.code, "multicastgroup_code".to_string()); assert_eq!(res.1.owner, multicastgroup.owner); - // Search by code + // Search by code (derives PDA directly) let res = GetMulticastGroupCommand { pubkey_or_code: "multicastgroup_code".to_string(), } @@ -105,31 +109,58 @@ mod tests { assert_eq!(res.1.code, "multicastgroup_code".to_string()); assert_eq!(res.1.owner, multicastgroup.owner); - // Search by code UPPERCASE + // Search by invalid code let res = GetMulticastGroupCommand { - pubkey_or_code: "MULTICASTGROUP_CODE".to_string(), + pubkey_or_code: "s(%".to_string(), } .execute(&client); - assert!(res.is_ok()); - let res = res.unwrap(); - assert_eq!(res.1.code, "multicastgroup_code".to_string()); - assert_eq!(res.1.owner, multicastgroup.owner); + assert!(res.is_err()); + } - // Invalid search - let res = GetMulticastGroupCommand { - pubkey_or_code: "ssssssssssss".to_string(), - } - .execute(&client); + #[test] + fn test_commands_multicastgroup_get_legacy_index_pda_fallback() { + let mut client = create_test_client(); - assert!(res.is_err()); + // Simulate a legacy multicast group created with index-based PDA. + // Its pubkey won't match the code-derived PDA. + let legacy_pubkey = Pubkey::new_unique(); + let mgroup = MulticastGroup { + account_type: AccountType::MulticastGroup, + index: 42, + bump_seed: 1, + code: "legacy-mg".to_string(), + owner: Pubkey::new_unique(), + ..Default::default() + }; + + // Code-derived PDA lookup returns an error (account doesn't exist there) + let (code_pda, _) = get_multicastgroup_pda(&client.get_program_id(), "legacy-mg"); + client + .expect_get() + .with(predicate::eq(code_pda)) + .returning(|_| Err(eyre::eyre!("account not found"))); + + // Fallback: gets() returns the legacy account under its original pubkey + let mgroup2 = mgroup.clone(); + client + .expect_gets() + .with(predicate::eq(AccountType::MulticastGroup)) + .returning(move |_| { + let mut map = std::collections::HashMap::new(); + map.insert(legacy_pubkey, AccountData::MulticastGroup(mgroup2.clone())); + Ok(map) + }); - // Search by invalid code let res = GetMulticastGroupCommand { - pubkey_or_code: "s(%".to_string(), + pubkey_or_code: "legacy-mg".to_string(), } .execute(&client); - assert!(res.is_err()); + assert!(res.is_ok()); + let (pk, mg) = res.unwrap(); + assert_eq!(pk, legacy_pubkey); + assert_eq!(mg.code, "legacy-mg"); + assert_eq!(mg.index, 42); } } diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/subscribe.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/subscribe.rs index 282711b61..a66b1954c 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/subscribe.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/subscribe.rs @@ -109,7 +109,7 @@ mod tests { fn test_commands_multicastgroup_subscribe_command() { let mut client = create_test_client(); - let (mgroup_pubkey, _bump_seed) = get_multicastgroup_pda(&client.get_program_id(), 1); + let (mgroup_pubkey, _bump_seed) = get_multicastgroup_pda(&client.get_program_id(), "test"); let mgroup = MulticastGroup { account_type: AccountType::MulticastGroup, owner: client.get_payer(), diff --git a/smartcontract/sdk/rs/src/commands/user/delete.rs b/smartcontract/sdk/rs/src/commands/user/delete.rs index d0eb4522b..74c1abf6f 100644 --- a/smartcontract/sdk/rs/src/commands/user/delete.rs +++ b/smartcontract/sdk/rs/src/commands/user/delete.rs @@ -143,7 +143,7 @@ mod tests { let (globalstate_pubkey, _) = get_globalstate_pda(&client.get_program_id()); let user_pubkey = Pubkey::new_unique(); - let (mgroup_pubkey, _) = get_multicastgroup_pda(&client.get_program_id(), 1); + let (mgroup_pubkey, _) = get_multicastgroup_pda(&client.get_program_id(), "test"); let client_ip = Ipv4Addr::new(192, 168, 1, 10); // User with one subscriber - triggers the retry logic @@ -347,7 +347,7 @@ mod tests { let (globalstate_pubkey, _) = get_globalstate_pda(&client.get_program_id()); let user_pubkey = Pubkey::new_unique(); - let (mgroup_pubkey, _) = get_multicastgroup_pda(&client.get_program_id(), 1); + let (mgroup_pubkey, _) = get_multicastgroup_pda(&client.get_program_id(), "test"); let client_ip = Ipv4Addr::new(192, 168, 1, 10); // User is both publisher and subscriber of the same group From 702ea2e553ab03e8feb972707c71f9a343cee6b9 Mon Sep 17 00:00:00 2001 From: Martin Sander Date: Tue, 3 Mar 2026 00:30:48 -0600 Subject: [PATCH 2/3] smartcontract: add changelog for multicast group code-based PDA Resolves: #3115 --- CHANGELOG.md | 1 + e2e/compatibility_test.go | 30 +++++++++++++----------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af984feb3..5356a6f81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - Smartcontract - Serviceability: add `Reservation` account and `ReserveConnection`/`CloseReservation` instructions for pre-reserving connection seats on devices, with `reserved_seats` factored into capacity checks on both reservation and user creation - Allow sentinel authority to add/remove multicast publisher and subscriber allowlist entries + - Multicast group PDA derivation now uses `code` instead of `account_index`, preventing duplicate codes at the PDA level; code-based lookups fall back to `getProgramAccounts` scan for legacy index-based accounts - Telemetry - Fix global monitor crash when IBRL and multicast users share the same client IP but are on different devices, by preferring non-multicast users in client IP lookups to match status device selection - E2E tests diff --git a/e2e/compatibility_test.go b/e2e/compatibility_test.go index b655bcfc9..8371a099c 100644 --- a/e2e/compatibility_test.go +++ b/e2e/compatibility_test.go @@ -57,25 +57,21 @@ type knownIncompat struct { } 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": {minVersion: "0.8.1"}, + // multicast_group_create: v0.9.0 changed PDA derivation from account_index to code. + // Older CLIs compute the wrong PDA, causing an assertion failure in the onchain program. + "write/multicast_group_create": {minVersion: "0.9.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": {minVersion: "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": {minVersion: "0.8.9"}, - "write/multicast_group_pub_allowlist_add": {minVersion: "0.8.1"}, - "write/multicast_group_pub_allowlist_remove": {minVersion: "0.8.1"}, - "write/multicast_group_sub_allowlist_add": {minVersion: "0.8.1"}, - "write/user_subscribe": {minVersion: "0.8.1"}, - "write/multicast_group_sub_allowlist_remove": {minVersion: "0.8.1"}, - "write/multicast_group_get": {minVersion: "0.8.1"}, - "write/multicast_group_delete": {minVersion: "0.8.1"}, + // can't be created (< 0.9.0), these all fail with "MulticastGroup not found". + "write/multicast_group_wait_activated": {minVersion: "0.9.0"}, + "write/multicast_group_update": {minVersion: "0.9.0"}, + "write/multicast_group_pub_allowlist_add": {minVersion: "0.9.0"}, + "write/multicast_group_pub_allowlist_remove": {minVersion: "0.9.0"}, + "write/multicast_group_sub_allowlist_add": {minVersion: "0.9.0"}, + "write/user_subscribe": {minVersion: "0.9.0"}, + "write/multicast_group_sub_allowlist_remove": {minVersion: "0.9.0"}, + "write/multicast_group_get": {minVersion: "0.9.0"}, + "write/multicast_group_delete": {minVersion: "0.9.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. From bffa7a5479ef3f31918cfaf133425f0ce6a1051c Mon Sep 17 00:00:00 2001 From: Martin Sander Date: Tue, 3 Mar 2026 01:00:09 -0600 Subject: [PATCH 3/3] e2e: mark multicast PDA change as known incompatibility for testnet v0.9.0 testnet v0.9.0 was released before the code-based PDA change, so it computes the wrong PDA against the current program. --- e2e/compatibility_test.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/e2e/compatibility_test.go b/e2e/compatibility_test.go index 8371a099c..e3b05d7f1 100644 --- a/e2e/compatibility_test.go +++ b/e2e/compatibility_test.go @@ -57,21 +57,22 @@ type knownIncompat struct { } var knownIncompatibilities = map[string]knownIncompat{ - // multicast_group_create: v0.9.0 changed PDA derivation from account_index to code. + // multicast_group_create: v0.9.1 changed PDA derivation from account_index to code. // Older CLIs compute the wrong PDA, causing an assertion failure in the onchain program. - "write/multicast_group_create": {minVersion: "0.9.0"}, + // testnet v0.9.0 was released before this change, so it's incompatible there too. + "write/multicast_group_create": {minVersion: "0.9.0", envOverride: map[string]string{"testnet": "0.9.1"}}, // All multicast operations that depend on multicast_group_create. When the group - // can't be created (< 0.9.0), these all fail with "MulticastGroup not found". - "write/multicast_group_wait_activated": {minVersion: "0.9.0"}, - "write/multicast_group_update": {minVersion: "0.9.0"}, - "write/multicast_group_pub_allowlist_add": {minVersion: "0.9.0"}, - "write/multicast_group_pub_allowlist_remove": {minVersion: "0.9.0"}, - "write/multicast_group_sub_allowlist_add": {minVersion: "0.9.0"}, - "write/user_subscribe": {minVersion: "0.9.0"}, - "write/multicast_group_sub_allowlist_remove": {minVersion: "0.9.0"}, - "write/multicast_group_get": {minVersion: "0.9.0"}, - "write/multicast_group_delete": {minVersion: "0.9.0"}, + // can't be created, these all fail with "MulticastGroup not found". + "write/multicast_group_wait_activated": {minVersion: "0.9.0", envOverride: map[string]string{"testnet": "0.9.1"}}, + "write/multicast_group_update": {minVersion: "0.9.0", envOverride: map[string]string{"testnet": "0.9.1"}}, + "write/multicast_group_pub_allowlist_add": {minVersion: "0.9.0", envOverride: map[string]string{"testnet": "0.9.1"}}, + "write/multicast_group_pub_allowlist_remove": {minVersion: "0.9.0", envOverride: map[string]string{"testnet": "0.9.1"}}, + "write/multicast_group_sub_allowlist_add": {minVersion: "0.9.0", envOverride: map[string]string{"testnet": "0.9.1"}}, + "write/user_subscribe": {minVersion: "0.9.0", envOverride: map[string]string{"testnet": "0.9.1"}}, + "write/multicast_group_sub_allowlist_remove": {minVersion: "0.9.0", envOverride: map[string]string{"testnet": "0.9.1"}}, + "write/multicast_group_get": {minVersion: "0.9.0", envOverride: map[string]string{"testnet": "0.9.1"}}, + "write/multicast_group_delete": {minVersion: "0.9.0", envOverride: map[string]string{"testnet": "0.9.1"}}, // 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.