Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ pub fn process_remove_multicast_sub_allowlist(
// Check whether mgroup is authorized
let is_authorized = (mgroup.owner == *payer_account.key)
|| globalstate.sentinel_authority_pk == *payer_account.key
|| globalstate.feed_authority_pk == *payer_account.key
|| globalstate.foundation_allowlist.contains(payer_account.key);
if !is_authorized {
return Err(DoubleZeroError::NotAllowed.into());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,20 @@ pub fn process_subscribe_multicastgroup(

let accesspass = AccessPass::try_from(accesspass_account)?;

let (accesspass_pda, _) = get_accesspass_pda(program_id, &user.client_ip, &user.owner);
let (accesspass_dynamic_pda, _) =
// Accept the access pass from either the user's owner or the caller
// (payer). This allows a third party (e.g. an oracle) to subscribe an
// existing user to a new multicast group using its own access pass.
let (owner_pda, _) = get_accesspass_pda(program_id, &user.client_ip, &user.owner);
let (owner_dynamic_pda, _) =
get_accesspass_pda(program_id, &Ipv4Addr::UNSPECIFIED, &user.owner);
let (payer_pda, _) = get_accesspass_pda(program_id, &user.client_ip, payer_account.key);
let (payer_dynamic_pda, _) =
get_accesspass_pda(program_id, &Ipv4Addr::UNSPECIFIED, payer_account.key);
assert!(
accesspass_account.key == &accesspass_pda
|| accesspass_account.key == &accesspass_dynamic_pda,
accesspass_account.key == &owner_pda
|| accesspass_account.key == &owner_dynamic_pda
|| accesspass_account.key == &payer_pda
|| accesspass_account.key == &payer_dynamic_pda,
"Invalid AccessPass PDA",
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,33 +143,41 @@ pub fn process_delete_user(
let user: User = User::try_from(user_account)?;

let globalstate = GlobalState::try_from(globalstate_account)?;
if !globalstate.foundation_allowlist.contains(payer_account.key)
&& user.owner != *payer_account.key
{

// Allow delete if payer is: the user owner, the access pass user_payer,
// or on the foundation allowlist.
let accesspass_user_payer = if !accesspass_account.data_is_empty() {
AccessPass::try_from(accesspass_account)
.map(|ap| ap.user_payer)
.ok()
} else {
None
};
let is_authorized = globalstate.foundation_allowlist.contains(payer_account.key)
|| user.owner == *payer_account.key
|| accesspass_user_payer == Some(*payer_account.key);
if !is_authorized {
return Err(DoubleZeroError::NotAllowed.into());
}

let (accesspass_pda, _) = get_accesspass_pda(program_id, &user.client_ip, &user.owner);
let (accesspass_dynamic_pda, _) =
// Accept access pass from either user.owner or payer
let (owner_pda, _) = get_accesspass_pda(program_id, &user.client_ip, &user.owner);
let (owner_dynamic_pda, _) =
get_accesspass_pda(program_id, &Ipv4Addr::UNSPECIFIED, &user.owner);
// Access Pass must exist and match the client_ip or allow_multiple_ip must be enabled
let (payer_pda, _) = get_accesspass_pda(program_id, &user.client_ip, payer_account.key);
let (payer_dynamic_pda, _) =
get_accesspass_pda(program_id, &Ipv4Addr::UNSPECIFIED, payer_account.key);
assert!(
accesspass_account.key == &accesspass_pda
|| accesspass_account.key == &accesspass_dynamic_pda,
accesspass_account.key == &owner_pda
|| accesspass_account.key == &owner_dynamic_pda
|| accesspass_account.key == &payer_pda
|| accesspass_account.key == &payer_dynamic_pda,
"Invalid AccessPass PDA",
);

if !accesspass_account.data_is_empty() {
// Read Access Pass
let mut accesspass = AccessPass::try_from(accesspass_account)?;
if accesspass.user_payer != user.owner {
msg!(
"Invalid user_payer accesspass.user_payer: {} = user_payer: {} ",
accesspass.user_payer,
user.owner
);
return Err(DoubleZeroError::Unauthorized.into());
}
if accesspass.is_dynamic() && accesspass.client_ip == Ipv4Addr::UNSPECIFIED {
accesspass.client_ip = user.client_ip; // lock to the first used IP
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -761,3 +761,167 @@ async fn test_multicast_subscriber_allowlist_feed_authority_different_user_payer
"Non-feed authority should fail when user_payer doesn't match"
);
}

/// Feed authority can remove from subscriber allowlist.
#[tokio::test]
async fn test_multicast_subscriber_allowlist_feed_authority_remove() {
let (mut banks_client, program_id, payer, recent_blockhash) = init_test().await;

let client_ip = [100, 0, 0, 6].into();
let user_payer = payer.pubkey();

let (program_config_pubkey, _) = get_program_config_pda(&program_id);
let (globalstate_pubkey, _) = get_globalstate_pda(&program_id);

// 1. Initialize global state
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;

// 2. Create feed authority
let feed = Keypair::new();
transfer(&mut banks_client, &payer, &feed.pubkey(), 10_000_000_000).await;

execute_transaction(
&mut banks_client,
recent_blockhash,
program_id,
DoubleZeroInstruction::SetAuthority(SetAuthorityArgs {
feed_authority_pk: Some(feed.pubkey()),
..Default::default()
}),
vec![AccountMeta::new(globalstate_pubkey, false)],
&payer,
)
.await;

// 3. Create and activate multicast group
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);

execute_transaction(
&mut banks_client,
recent_blockhash,
program_id,
DoubleZeroInstruction::CreateMulticastGroup(MulticastGroupCreateArgs {
code: "feed-remove".to_string(),
max_bandwidth: 1_000_000_000,
owner: payer.pubkey(),
use_onchain_allocation: false,
}),
vec![
AccountMeta::new(multicastgroup_pubkey, false),
AccountMeta::new(globalstate_pubkey, false),
],
&payer,
)
.await;

execute_transaction(
&mut banks_client,
recent_blockhash,
program_id,
DoubleZeroInstruction::ActivateMulticastGroup(MulticastGroupActivateArgs {
multicast_ip: [224, 254, 0, 6].into(),
}),
vec![
AccountMeta::new(multicastgroup_pubkey, false),
AccountMeta::new(globalstate_pubkey, false),
],
&payer,
)
.await;

// 4. Payer creates access pass and adds allowlist entry
let (accesspass_pubkey, _) = get_accesspass_pda(&program_id, &client_ip, &user_payer);

execute_transaction(
&mut banks_client,
recent_blockhash,
program_id,
DoubleZeroInstruction::SetAccessPass(SetAccessPassArgs {
accesspass_type: AccessPassType::Prepaid,
client_ip,
last_access_epoch: 100,
allow_multiple_ip: false,
}),
vec![
AccountMeta::new(accesspass_pubkey, false),
AccountMeta::new(globalstate_pubkey, false),
AccountMeta::new(user_payer, false),
],
&payer,
)
.await;

execute_transaction(
&mut banks_client,
recent_blockhash,
program_id,
DoubleZeroInstruction::AddMulticastGroupSubAllowlist(AddMulticastGroupSubAllowlistArgs {
client_ip,
user_payer,
}),
vec![
AccountMeta::new(multicastgroup_pubkey, false),
AccountMeta::new(accesspass_pubkey, false),
AccountMeta::new(globalstate_pubkey, false),
],
&payer,
)
.await;

let accesspass = get_account_data(&mut banks_client, accesspass_pubkey)
.await
.expect("Unable to get Account")
.get_accesspass()
.unwrap();
assert_eq!(accesspass.mgroup_sub_allowlist.len(), 1);

// 5. Feed authority removes subscriber allowlist entry — should succeed
let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap();
let res = try_execute_transaction(
&mut banks_client,
recent_blockhash,
program_id,
DoubleZeroInstruction::RemoveMulticastGroupSubAllowlist(
RemoveMulticastGroupSubAllowlistArgs {
client_ip,
user_payer,
},
),
vec![
AccountMeta::new(multicastgroup_pubkey, false),
AccountMeta::new(accesspass_pubkey, false),
AccountMeta::new(globalstate_pubkey, false),
],
&feed,
)
.await;
assert!(
res.is_ok(),
"Feed authority should be able to remove from subscriber allowlist"
);

let accesspass = get_account_data(&mut banks_client, accesspass_pubkey)
.await
.expect("Unable to get Account")
.get_accesspass()
.unwrap();
assert_eq!(accesspass.mgroup_sub_allowlist.len(), 0);
}
Original file line number Diff line number Diff line change
Expand Up @@ -1658,3 +1658,71 @@ async fn test_user_delete_from_out_of_credits() {
.unwrap();
assert_eq!(user.status, UserStatus::Deleting);
}

/// Access pass user_payer can delete the user. Also verifies a stranger cannot.
#[tokio::test]
async fn test_user_delete_by_accesspass_user_payer() {
let (mut banks_client, payer, program_id, globalstate_pubkey, user_pubkey, accesspass_pubkey) =
setup_activated_user().await;

// Verify payer is the access pass user_payer
let accesspass = get_account_data(&mut banks_client, accesspass_pubkey)
.await
.unwrap()
.get_accesspass()
.unwrap();
assert_eq!(accesspass.user_payer, payer.pubkey());

// Stranger should NOT be able to delete
let stranger = Keypair::new();
transfer(&mut banks_client, &payer, &stranger.pubkey(), 10_000_000).await;

let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap();
let res = try_execute_transaction(
&mut banks_client,
recent_blockhash,
program_id,
DoubleZeroInstruction::DeleteUser(UserDeleteArgs {
dz_prefix_count: 0,
multicast_publisher_count: 0,
}),
vec![
AccountMeta::new(user_pubkey, false),
AccountMeta::new(accesspass_pubkey, false),
AccountMeta::new(globalstate_pubkey, false),
],
&stranger,
)
.await;
assert!(res.is_err(), "Stranger should not be able to delete user");

// Payer (access pass user_payer) can delete
let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap();
let res = try_execute_transaction(
&mut banks_client,
recent_blockhash,
program_id,
DoubleZeroInstruction::DeleteUser(UserDeleteArgs {
dz_prefix_count: 0,
multicast_publisher_count: 0,
}),
vec![
AccountMeta::new(user_pubkey, false),
AccountMeta::new(accesspass_pubkey, false),
AccountMeta::new(globalstate_pubkey, false),
],
&payer,
)
.await;
assert!(
res.is_ok(),
"Access pass user_payer should be able to delete user"
);

let user = get_account_data(&mut banks_client, user_pubkey)
.await
.unwrap()
.get_user()
.unwrap();
assert_eq!(user.status, UserStatus::Deleting);
}
Loading
Loading