Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file.

### Changes

- Serviceability
- Gate Tenant instructions (create/update/delete/add_administrator/remove_administrator/update_payment_status) on `TENANT_ADMIN` or foundation/sentinel via `authorize()`. (#3983)
- Collector
- Harden ledger writes against a slow/degraded RPC endpoint: bound each RPC request (default 15s, `--ledger-rpc-timeout`), size the connection pool above the submitter concurrency (default 128, `--ledger-rpc-max-conns`), and deadline each submission attempt so it fails fast and retries with a fresh blockhash instead of sending an expired one and failing preflight with `BlockhashNotFound`. (#3973)
- E2E
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::{
authorize::authorize,
error::DoubleZeroError,
serializer::try_acc_write,
state::{globalstate::GlobalState, tenant::*},
state::{globalstate::GlobalState, permission::permission_flags, tenant::*},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
Expand Down Expand Up @@ -59,9 +60,14 @@ pub fn process_add_administrator_tenant(
let mut tenant = Tenant::try_from(tenant_account)?;

// Check authorization: only foundation allowlist members can add administrators
if !globalstate.foundation_allowlist.contains(payer_account.key) {
return Err(DoubleZeroError::NotAllowed.into());
}
// Authorization: TENANT_ADMIN (Permission account) or foundation/sentinel (legacy).
authorize(
program_id,
accounts_iter,
payer_account.key,
&globalstate,
permission_flags::TENANT_ADMIN,
)?;

// Check if administrator already exists
if tenant.administrators.contains(&value.administrator) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use crate::{
authorize::authorize,
error::DoubleZeroError,
pda::*,
processors::resource::allocate_id,
resource::ResourceType,
seeds::{SEED_PREFIX, SEED_TENANT},
serializer::try_acc_create,
state::{accounttype::AccountType, globalstate::GlobalState, tenant::*},
state::{
accounttype::AccountType, globalstate::GlobalState, permission::permission_flags, tenant::*,
},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
Expand Down Expand Up @@ -108,9 +111,14 @@ pub fn process_create_tenant(
// Parse the global state account & check if the payer is in the allowlist
let globalstate = GlobalState::try_from(globalstate_account)?;

if !globalstate.foundation_allowlist.contains(payer_account.key) {
return Err(DoubleZeroError::NotAllowed.into());
}
// Authorization: TENANT_ADMIN (Permission account) or foundation/sentinel (legacy).
authorize(
program_id,
accounts_iter,
payer_account.key,
&globalstate,
permission_flags::TENANT_ADMIN,
)?;
// get the PDA pubkey and bump seed for the account tenant & check if it matches the account
let (expected_pda_account, bump_seed) = get_tenant_pda(program_id, &code);
assert_eq!(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use crate::{
authorize::authorize,
error::DoubleZeroError,
pda::get_resource_extension_pda,
processors::resource::deallocate_id,
resource::ResourceType,
serializer::try_acc_close,
state::{globalstate::GlobalState, tenant::Tenant},
state::{globalstate::GlobalState, permission::permission_flags, tenant::Tenant},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
Expand Down Expand Up @@ -81,9 +82,14 @@ pub fn process_delete_tenant(
// Parse the global state account & check if the payer is in the allowlist
let globalstate = GlobalState::try_from(globalstate_account)?;

if !globalstate.foundation_allowlist.contains(payer_account.key) {
return Err(DoubleZeroError::NotAllowed.into());
}
// Authorization: TENANT_ADMIN (Permission account) or foundation/sentinel (legacy).
authorize(
program_id,
accounts_iter,
payer_account.key,
&globalstate,
permission_flags::TENANT_ADMIN,
)?;

// Parse the tenant account
let tenant = Tenant::try_from(tenant_account)?;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::{
authorize::authorize,
error::DoubleZeroError,
serializer::try_acc_write,
state::{globalstate::GlobalState, tenant::*},
state::{globalstate::GlobalState, permission::permission_flags, tenant::*},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
Expand Down Expand Up @@ -59,9 +60,14 @@ pub fn process_remove_administrator_tenant(
let mut tenant = Tenant::try_from(tenant_account)?;

// Check authorization: only foundation allowlist members can remove administrators
if !globalstate.foundation_allowlist.contains(payer_account.key) {
return Err(DoubleZeroError::NotAllowed.into());
}
// Authorization: TENANT_ADMIN (Permission account) or foundation/sentinel (legacy).
authorize(
program_id,
accounts_iter,
payer_account.key,
&globalstate,
permission_flags::TENANT_ADMIN,
)?;

// Find and remove the administrator
if let Some(pos) = tenant
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::{
error::DoubleZeroError,
authorize::authorize,
serializer::try_acc_write,
state::{
globalstate::GlobalState,
permission::permission_flags,
tenant::{Tenant, TenantBillingConfig},
},
};
Expand Down Expand Up @@ -72,9 +73,14 @@ pub fn process_update_tenant(
// Parse the global state account & check if the payer is in the allowlist
let globalstate = GlobalState::try_from(globalstate_account)?;

if !globalstate.foundation_allowlist.contains(payer_account.key) {
return Err(DoubleZeroError::NotAllowed.into());
}
// Authorization: TENANT_ADMIN (Permission account) or foundation/sentinel (legacy).
authorize(
program_id,
accounts_iter,
payer_account.key,
&globalstate,
permission_flags::TENANT_ADMIN,
)?;

// Parse the tenant account
let mut tenant = Tenant::try_from(tenant_account)?;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::{
authorize::authorize,
error::DoubleZeroError,
serializer::try_acc_write,
state::{
globalstate::GlobalState,
permission::permission_flags,
tenant::{Tenant, TenantBillingConfig},
},
};
Expand Down Expand Up @@ -51,17 +53,14 @@ pub fn process_update_payment_status(
// Parse the global state account & check if the payer is sentinel or foundation
let globalstate = GlobalState::try_from(globalstate_account)?;

if globalstate.sentinel_authority_pk != *payer_account.key
&& !globalstate.foundation_allowlist.contains(payer_account.key)
{
msg!(
"sentinel_authority_pk: {} payer: {} foundation_allowlist: {:?}",
globalstate.sentinel_authority_pk,
payer_account.key,
globalstate.foundation_allowlist
);
return Err(DoubleZeroError::NotAllowed.into());
}
// Authorization: TENANT_ADMIN (Permission account) or foundation/sentinel (legacy).
authorize(
program_id,
accounts_iter,
payer_account.key,
&globalstate,
permission_flags::TENANT_ADMIN,
)?;

// Validate payment status range
if value.payment_status > 1 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
use doublezero_serviceability::{
instructions::*,
pda::*,
processors::tenant::{
add_administrator::TenantAddAdministratorArgs, create::TenantCreateArgs,
delete::TenantDeleteArgs, remove_administrator::TenantRemoveAdministratorArgs,
update::TenantUpdateArgs, update_payment_status::UpdatePaymentStatusArgs,
processors::{
permission::create::PermissionCreateArgs,
tenant::{
add_administrator::TenantAddAdministratorArgs, create::TenantCreateArgs,
delete::TenantDeleteArgs, remove_administrator::TenantRemoveAdministratorArgs,
update::TenantUpdateArgs, update_payment_status::UpdatePaymentStatusArgs,
},
},
resource::ResourceType,
state::{accounttype::AccountType, tenant::*},
state::{accounttype::AccountType, permission::permission_flags, tenant::*},
};
use solana_program::instruction::InstructionError;
use solana_program_test::*;
Expand Down Expand Up @@ -844,3 +847,80 @@ async fn test_tenant_include_topologies_reset_to_empty() {

println!("✅ include_topologies can be reset to empty");
}

/// A non-foundation key holding a TENANT_ADMIN Permission account can create a
/// tenant — exercises the new Permission-account authorization path.
#[tokio::test]
async fn test_tenant_create_with_permission_account_allowed() {
let (mut banks_client, payer, program_id, globalstate_pubkey, _globalconfig_pubkey) =
setup_program_with_globalconfig().await;

let (vrf_ids_pda, _, _) = get_resource_extension_pda(&program_id, ResourceType::VrfIds);

// A key that is NOT in the foundation allowlist, granted TENANT_ADMIN.
let tenant_admin = Keypair::new();
transfer(
&mut banks_client,
&payer,
&tenant_admin.pubkey(),
2_000_000_000,
)
.await;

let (permission_pda, _) = get_permission_pda(&program_id, &tenant_admin.pubkey());
let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await;
execute_transaction(
&mut banks_client,
recent_blockhash,
program_id,
DoubleZeroInstruction::CreatePermission(PermissionCreateArgs {
user_payer: tenant_admin.pubkey(),
permissions: permission_flags::TENANT_ADMIN,
}),
vec![
AccountMeta::new(permission_pda, false),
AccountMeta::new_readonly(globalstate_pubkey, false),
],
&payer,
)
.await;

// tenant_admin creates a tenant, passing its Permission PDA as the optional
// trailing account that authorize() reads.
let tenant_code = "perm-tenant";
let (tenant_pubkey, _) = get_tenant_pda(&program_id, tenant_code);
let administrator = Pubkey::new_unique();
let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await;
let mut tx = create_transaction_with_extra_accounts(
program_id,
&DoubleZeroInstruction::CreateTenant(TenantCreateArgs {
code: tenant_code.to_string(),
administrator,
token_account: None,
metro_routing: true,
route_liveness: false,
}),
&vec![
AccountMeta::new(tenant_pubkey, false),
AccountMeta::new(globalstate_pubkey, false),
AccountMeta::new(vrf_ids_pda, false),
],
&tenant_admin,
&[AccountMeta::new_readonly(permission_pda, false)],
);
tx.try_sign(&[&tenant_admin], recent_blockhash).unwrap();
banks_client
.process_transaction(tx)
.await
.expect("TENANT_ADMIN permission holder should be able to create a tenant");

let tenant = get_account_data(&mut banks_client, tenant_pubkey)
.await
.expect("tenant")
.get_tenant()
.unwrap();
assert_eq!(tenant.account_type, AccountType::Tenant);
assert_eq!(tenant.code, tenant_code.to_string());

println!("✅ CreateTenant with TENANT_ADMIN permission succeeded");
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ impl AddAdministratorTenantCommand {
.execute(client)
.map_err(|_err| eyre::eyre!("Globalstate not initialized"))?;

client.execute_transaction(
client.execute_authorized_transaction(
DoubleZeroInstruction::TenantAddAdministrator(TenantAddAdministratorArgs {
administrator: self.administrator,
}),
Expand Down
4 changes: 2 additions & 2 deletions smartcontract/sdk/rs/src/commands/tenant/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ impl CreateTenantCommand {
let (vrf_ids_pda, _, _) =
get_resource_extension_pda(&client.get_program_id(), ResourceType::VrfIds);
client
.execute_transaction(
.execute_authorized_transaction(
DoubleZeroInstruction::CreateTenant(TenantCreateArgs {
code,
administrator: self.administrator,
Expand Down Expand Up @@ -64,7 +64,7 @@ mod tests {
let administrator = Pubkey::new_unique();

client
.expect_execute_transaction()
.expect_execute_authorized_transaction()
.with(
predicate::eq(DoubleZeroInstruction::CreateTenant(TenantCreateArgs {
code: "test".to_string(),
Expand Down
12 changes: 6 additions & 6 deletions smartcontract/sdk/rs/src/commands/tenant/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ impl DeleteTenantCommand {
let (vrf_ids_pda, _, _) =
get_resource_extension_pda(&client.get_program_id(), ResourceType::VrfIds);

client.execute_transaction(
client.execute_authorized_transaction(
DoubleZeroInstruction::DeleteTenant(TenantDeleteArgs {}),
vec![
AccountMeta::new(self.tenant_pubkey, false),
Expand Down Expand Up @@ -166,7 +166,7 @@ mod tests {
.returning(|_| Ok(HashMap::new()));

client
.expect_execute_transaction()
.expect_execute_authorized_transaction()
.with(
predicate::eq(DoubleZeroInstruction::DeleteTenant(TenantDeleteArgs {})),
predicate::eq(vec![
Expand Down Expand Up @@ -374,7 +374,7 @@ mod tests {

// 8. Final: execute_transaction(DeleteTenant)
client
.expect_execute_transaction()
.expect_execute_authorized_transaction()
.with(
predicate::eq(DoubleZeroInstruction::DeleteTenant(TenantDeleteArgs {})),
predicate::eq(vec![
Expand Down Expand Up @@ -485,7 +485,7 @@ mod tests {

// 3. execute_transaction(DeleteTenant)
client
.expect_execute_transaction()
.expect_execute_authorized_transaction()
.with(
predicate::eq(DoubleZeroInstruction::DeleteTenant(TenantDeleteArgs {})),
predicate::eq(vec![
Expand Down Expand Up @@ -528,7 +528,7 @@ mod tests {

// 2. execute_transaction(DeleteTenant) fails because users still connected
client
.expect_execute_transaction()
.expect_execute_authorized_transaction()
.with(
predicate::eq(DoubleZeroInstruction::DeleteTenant(TenantDeleteArgs {})),
predicate::eq(vec![
Expand Down Expand Up @@ -583,7 +583,7 @@ mod tests {

// 3. execute_transaction(DeleteTenant)
client
.expect_execute_transaction()
.expect_execute_authorized_transaction()
.with(
predicate::eq(DoubleZeroInstruction::DeleteTenant(TenantDeleteArgs {})),
predicate::eq(vec![
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ impl RemoveAdministratorTenantCommand {
.execute(client)
.map_err(|_err| eyre::eyre!("Globalstate not initialized"))?;

client.execute_transaction(
client.execute_authorized_transaction(
DoubleZeroInstruction::TenantRemoveAdministrator(TenantRemoveAdministratorArgs {
administrator: self.administrator,
}),
Expand Down
2 changes: 1 addition & 1 deletion smartcontract/sdk/rs/src/commands/tenant/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ impl UpdateTenantCommand {
pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result<Signature> {
let (globalstate_pubkey, _) = get_globalstate_pda(&client.get_program_id());

client.execute_transaction(
client.execute_authorized_transaction(
DoubleZeroInstruction::UpdateTenant(TenantUpdateArgs {
vrf_id: self.vrf_id,
token_account: self.token_account,
Expand Down
Loading
Loading