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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file.
- CLI
- Allow incremental multicast group addition without disconnecting
- Reset SIGPIPE to SIG_DFL at the start of main() in all 3 CLI binaries (doublezero, doublezero-geolocation, doublezero-admin) so the process exits silently like standard CLI tools
- Smartcontract
- Add Index account for multicast group code uniqueness — PDA derived from entity type + lowercased code enforces unique codes onchain and enables O(1) code-to-pubkey lookup
- SDK
- Add Go SDK for shred subscription program with read-only account deserialization (epoch state, seat assignments, pricing, settlement, validator client rewards), PDA derivation helpers, RPC fetchers, compatibility tests, and a fetch example CLI
- Tools
Expand Down
11 changes: 10 additions & 1 deletion activator/src/process/multicastgroup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,13 +398,22 @@ mod tests {
// Insert it first so it can be removed
multicastgroups.insert(pubkey, multicastgroup.clone());

// Stateless mode: use_onchain_deallocation=true
// 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, close_index=true
client
.expect_execute_transaction()
.with(
predicate::eq(DoubleZeroInstruction::DeactivateMulticastGroup(
MulticastGroupDeactivateArgs {
use_onchain_deallocation: true,
close_index: true,
},
)),
predicate::always(),
Expand Down
1 change: 0 additions & 1 deletion e2e/internal/qa/provisioning.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,4 +600,3 @@ func formatBandwidth(bps uint64) string {
}
return fmt.Sprintf("%d bps", bps)
}

1 change: 1 addition & 0 deletions sdk/serviceability/go/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
ResourceExtensionType AccountType = 12
TenantType AccountType = 13
PermissionType AccountType = 15
IndexType AccountType = 16
)

type LocationStatus uint8
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 @@ -47,6 +47,7 @@ use crate::{
setauthority::process_set_authority, setfeatureflags::process_set_feature_flags,
setversion::process_set_version,
},
index::{create::process_create_index, delete::process_delete_index},
link::{
accept::process_accept_link, activate::process_activate_link,
closeaccount::process_closeaccount_link, create::process_create_link,
Expand Down Expand Up @@ -421,6 +422,12 @@ pub fn process_instruction(
DoubleZeroInstruction::DeletePermission(value) => {
process_delete_permission(program_id, accounts, &value)?
}
DoubleZeroInstruction::CreateIndex(value) => {
process_create_index(program_id, accounts, &value)?
}
DoubleZeroInstruction::DeleteIndex(value) => {
process_delete_index(program_id, accounts, &value)?
}
};
Ok(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use crate::processors::{
setairdrop::SetAirdropArgs, setauthority::SetAuthorityArgs,
setfeatureflags::SetFeatureFlagsArgs, setversion::SetVersionArgs,
},
index::{create::IndexCreateArgs, delete::IndexDeleteArgs},
link::{
accept::LinkAcceptArgs, activate::LinkActivateArgs, closeaccount::LinkCloseAccountArgs,
create::LinkCreateArgs, delete::LinkDeleteArgs, reject::LinkRejectArgs,
Expand Down Expand Up @@ -218,6 +219,9 @@ pub enum DoubleZeroInstruction {

Deprecated102(), // variant 102 (was CreateReservedSubscribeUser)
Deprecated103(), // variant 103 (was DeleteReservedSubscribeUser)

CreateIndex(IndexCreateArgs), // variant 104
DeleteIndex(IndexDeleteArgs), // variant 105
}

impl DoubleZeroInstruction {
Expand Down Expand Up @@ -350,6 +354,9 @@ impl DoubleZeroInstruction {
101 => Ok(Self::DeletePermission(PermissionDeleteArgs::try_from(rest).unwrap())),


104 => Ok(Self::CreateIndex(IndexCreateArgs::try_from(rest).unwrap())),
105 => Ok(Self::DeleteIndex(IndexDeleteArgs::try_from(rest).unwrap())),

_ => Err(ProgramError::InvalidInstructionData),
}
}
Expand Down Expand Up @@ -483,6 +490,9 @@ impl DoubleZeroInstruction {

Self::Deprecated102() => "Deprecated102".to_string(),
Self::Deprecated103() => "Deprecated103".to_string(),

Self::CreateIndex(_) => "CreateIndex".to_string(), // variant 104
Self::DeleteIndex(_) => "DeleteIndex".to_string(), // variant 105
}
}

Expand Down Expand Up @@ -609,6 +619,9 @@ impl DoubleZeroInstruction {

Self::Deprecated102() => String::new(),
Self::Deprecated103() => String::new(),

Self::CreateIndex(args) => format!("{args:?}"), // variant 104
Self::DeleteIndex(args) => format!("{args:?}"), // variant 105
}
}
}
Expand Down Expand Up @@ -975,6 +988,7 @@ mod tests {
publisher_count: None,
subscriber_count: None,
use_onchain_allocation: false,
rename_index: false,
}),
"UpdateMulticastGroup",
);
Expand All @@ -992,13 +1006,15 @@ mod tests {
test_instruction(
DoubleZeroInstruction::DeleteMulticastGroup(MulticastGroupDeleteArgs {
use_onchain_deallocation: false,
close_index: false,
}),
"DeleteMulticastGroup",
);

test_instruction(
DoubleZeroInstruction::DeactivateMulticastGroup(MulticastGroupDeactivateArgs {
use_onchain_deallocation: false,
close_index: false,
}),
"DeactivateMulticastGroup",
);
Expand Down
17 changes: 15 additions & 2 deletions smartcontract/programs/doublezero-serviceability/src/pda.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use solana_program::pubkey::Pubkey;
use crate::{
seeds::{
SEED_ACCESS_PASS, SEED_CONFIG, SEED_CONTRIBUTOR, SEED_DEVICE, SEED_DEVICE_TUNNEL_BLOCK,
SEED_DZ_PREFIX_BLOCK, SEED_EXCHANGE, SEED_GLOBALSTATE, SEED_LINK, SEED_LINK_IDS,
SEED_LOCATION, SEED_MULTICASTGROUP_BLOCK, SEED_MULTICAST_GROUP,
SEED_DZ_PREFIX_BLOCK, SEED_EXCHANGE, SEED_GLOBALSTATE, SEED_INDEX, SEED_LINK,
SEED_LINK_IDS, SEED_LOCATION, SEED_MULTICASTGROUP_BLOCK, SEED_MULTICAST_GROUP,
SEED_MULTICAST_PUBLISHER_BLOCK, SEED_PERMISSION, SEED_PREFIX, SEED_PROGRAM_CONFIG,
SEED_SEGMENT_ROUTING_IDS, SEED_TENANT, SEED_TUNNEL_IDS, SEED_USER, SEED_USER_TUNNEL_BLOCK,
SEED_VRF_IDS,
Expand Down Expand Up @@ -103,6 +103,19 @@ pub fn get_accesspass_pda(
)
}

pub fn get_index_pda(program_id: &Pubkey, entity_seed: &[u8], code: &str) -> (Pubkey, u8) {
let lowercase_code = code.to_ascii_lowercase();
Pubkey::find_program_address(
&[
SEED_PREFIX,
SEED_INDEX,
entity_seed,
lowercase_code.as_bytes(),
],
program_id,
)
}

pub fn get_resource_extension_pda(
program_id: &Pubkey,
resource_type: crate::resource::ResourceType,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use crate::{
error::DoubleZeroError,
pda::get_index_pda,
seeds::{SEED_INDEX, SEED_PREFIX},
serializer::try_acc_create,
state::{accounttype::AccountType, globalstate::GlobalState, index::Index},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
use doublezero_program_common::validate_account_code;
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program_error::ProgramError,
pubkey::Pubkey,
};
use std::fmt;

#[cfg(test)]
use solana_program::msg;

#[derive(BorshSerialize, BorshDeserializeIncremental, PartialEq, Clone, Default)]
pub struct IndexCreateArgs {
pub entity_seed: String,
pub code: String,
}

impl fmt::Debug for IndexCreateArgs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "entity_seed: {}, code: {}", self.entity_seed, self.code)
}
}

pub fn process_create_index(
program_id: &Pubkey,
accounts: &[AccountInfo],
value: &IndexCreateArgs,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();

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

#[cfg(test)]
msg!("process_create_index({:?})", value);

assert!(payer_account.is_signer, "Payer must be a signer");

// Validate accounts
assert_eq!(
globalstate_account.owner, program_id,
"Invalid GlobalState Account Owner"
);
assert_eq!(
entity_account.owner, program_id,
"Invalid Entity Account Owner"
);
assert_eq!(
*system_program.unsigned_key(),
solana_system_interface::program::ID,
"Invalid System Program Account Owner"
);
assert!(index_account.is_writable, "Index Account is not writable");

// Check foundation allowlist
let globalstate = GlobalState::try_from(globalstate_account)?;
if !globalstate.foundation_allowlist.contains(payer_account.key) {
return Err(DoubleZeroError::NotAllowed.into());
}

// Validate and normalize code
let code =
validate_account_code(&value.code).map_err(|_| DoubleZeroError::InvalidAccountCode)?;
let lowercase_code = code.to_ascii_lowercase();

// Derive and verify the Index PDA
let (expected_pda, bump_seed) = get_index_pda(program_id, value.entity_seed.as_bytes(), &code);
assert_eq!(index_account.key, &expected_pda, "Invalid Index Pubkey");

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

// Verify the entity account is a valid, non-Index program account
assert!(!entity_account.data_is_empty(), "Entity Account is empty");
let entity_type = AccountType::from(entity_account.try_borrow_data()?[0]);
assert!(
entity_type != AccountType::None && entity_type != AccountType::Index,
"Entity Account has invalid type for indexing: {entity_type}"
);

let index = Index {
account_type: AccountType::Index,
pk: *entity_account.key,
bump_seed,
};

try_acc_create(
&index,
index_account,
payer_account,
system_program,
program_id,
&[
SEED_PREFIX,
SEED_INDEX,
value.entity_seed.as_bytes(),
lowercase_code.as_bytes(),
&[bump_seed],
],
)?;

Ok(())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use crate::{
error::DoubleZeroError,
serializer::try_acc_close,
state::{globalstate::GlobalState, index::Index},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
};
use std::fmt;

#[cfg(test)]
use solana_program::msg;

#[derive(BorshSerialize, BorshDeserializeIncremental, PartialEq, Clone, Default)]
pub struct IndexDeleteArgs {}

impl fmt::Debug for IndexDeleteArgs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "IndexDeleteArgs")
}
}

pub fn process_delete_index(
program_id: &Pubkey,
accounts: &[AccountInfo],
_value: &IndexDeleteArgs,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();

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

#[cfg(test)]
msg!("process_delete_index");

assert!(payer_account.is_signer, "Payer must be a signer");

// Validate accounts
assert_eq!(
index_account.owner, program_id,
"Invalid Index Account Owner"
);
assert_eq!(
globalstate_account.owner, program_id,
"Invalid GlobalState Account Owner"
);
assert!(index_account.is_writable, "Index Account is not writable");

// Check foundation allowlist
let globalstate = GlobalState::try_from(globalstate_account)?;
if !globalstate.foundation_allowlist.contains(payer_account.key) {
return Err(DoubleZeroError::NotAllowed.into());
}

// Verify it's actually an Index account
let _index = Index::try_from(index_account)?;

try_acc_close(index_account, payer_account)?;

#[cfg(test)]
msg!("Deleted Index account");

Ok(())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod create;
pub mod delete;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod device;
pub mod exchange;
pub mod globalconfig;
pub mod globalstate;
pub mod index;
pub mod link;
pub mod location;
pub mod migrate;
Expand Down
Loading
Loading