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

### Changes

- Onchain Programs
- Serviceability: 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
- Serviceability: integrate Index lifecycle into multicast group instructions (create, update, delete, close account) so Index accounts are managed atomically
- Serviceability: add standalone CreateIndex/DeleteIndex instructions (variants 104/105) for migration backfill of existing multicast groups
- SDK
- Rust: add O(1) multicast group lookup by code via Index PDA derivation, with fallback scan for pre-migration accounts
- Rust: add CreateIndex/DeleteIndex command wrappers for migration tooling
- E2E Tests
- Add geoprobe E2E test (`TestE2E_GeoprobeDiscovery`) that exercises the full geolocation flow: deploy geolocation program, create probe onchain, start geoprobe-agent container, and verify the telemetry-agent discovers and measures the probe via TWAMP
- Add geoprobe Docker image, geolocation program build/deploy support, and manager geolocation CLI configuration to the E2E devnet infrastructure
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 @@ -427,6 +428,12 @@ pub fn process_instruction(
DoubleZeroInstruction::DeleteReservedSubscribeUser(value) => {
process_delete_reserved_subscribe_user(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 @@ -221,6 +222,9 @@ pub enum DoubleZeroInstruction {

CreateReservedSubscribeUser(CreateReservedSubscribeUserArgs), // variant 102
DeleteReservedSubscribeUser(DeleteReservedSubscribeUserArgs), // variant 103

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

impl DoubleZeroInstruction {
Expand Down Expand Up @@ -358,6 +362,9 @@ impl DoubleZeroInstruction {
102 => Ok(Self::CreateReservedSubscribeUser(CreateReservedSubscribeUserArgs::try_from(rest).unwrap())),
103 => Ok(Self::DeleteReservedSubscribeUser(DeleteReservedSubscribeUserArgs::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 @@ -491,6 +498,9 @@ impl DoubleZeroInstruction {

Self::CreateReservedSubscribeUser(_) => "CreateReservedSubscribeUser".to_string(), // variant 102
Self::DeleteReservedSubscribeUser(_) => "DeleteReservedSubscribeUser".to_string(), // variant 103

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

Expand Down Expand Up @@ -617,6 +627,9 @@ impl DoubleZeroInstruction {

Self::CreateReservedSubscribeUser(args) => format!("{args:?}"), // variant 102
Self::DeleteReservedSubscribeUser(args) => format!("{args:?}"), // variant 103

Self::CreateIndex(args) => format!("{args:?}"), // variant 104
Self::DeleteIndex(args) => format!("{args:?}"), // variant 105
}
}
}
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_RESERVATION, SEED_SEGMENT_ROUTING_IDS, SEED_TENANT, SEED_TUNNEL_IDS, SEED_USER,
SEED_USER_TUNNEL_BLOCK, SEED_VRF_IDS,
Expand Down Expand Up @@ -119,6 +119,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,113 @@
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 program account
assert!(!entity_account.data_is_empty(), "Entity Account is empty");

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
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use crate::{
error::DoubleZeroError,
pda::get_resource_extension_pda,
pda::{get_index_pda, get_resource_extension_pda},
processors::resource::deallocate_ip,
resource::ResourceType,
seeds::SEED_MULTICAST_GROUP,
serializer::try_acc_close,
state::{globalstate::GlobalState, multicastgroup::*},
state::{globalstate::GlobalState, index::Index, multicastgroup::*},
};
use borsh::BorshSerialize;
use borsh_incremental::BorshDeserializeIncremental;
Expand Down Expand Up @@ -62,6 +63,9 @@ pub fn process_closeaccount_multicastgroup(
let payer_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;

// Optional: Index account to close alongside the multicast group
let index_account = next_account_info(accounts_iter).ok();

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

Expand Down Expand Up @@ -137,6 +141,24 @@ pub fn process_closeaccount_multicastgroup(

try_acc_close(multicastgroup_account, owner_account)?;

// Close the Index account if provided
if let Some(index_acc) = index_account {
assert_eq!(index_acc.owner, program_id, "Invalid Index Account Owner");
assert!(index_acc.is_writable, "Index Account is not writable");

let (expected_index_pda, _) =
get_index_pda(program_id, SEED_MULTICAST_GROUP, &multicastgroup.code);
assert_eq!(index_acc.key, &expected_index_pda, "Invalid Index Pubkey");

let index = Index::try_from(index_acc)?;
assert_eq!(
index.pk, *multicastgroup_account.key,
"Index does not point to this MulticastGroup"
);

try_acc_close(index_acc, payer_account)?;
}

#[cfg(test)]
msg!("Deactivated: MulticastGroup closed");

Expand Down
Loading