Skip to content
Merged
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

- Smartcontract
- Add Index account for onchain key uniqueness enforcement and O(1) key-to-pubkey lookup, with standalone CreateIndex/DeleteIndex instructions for migration backfill
- 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
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
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], key: &str) -> (Pubkey, u8) {
let lowercase_key = key.to_ascii_lowercase();
Pubkey::find_program_address(
&[
SEED_PREFIX,
SEED_INDEX,
entity_seed,
lowercase_key.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,144 @@
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 key: String,
}

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

/// Core logic for validating and creating an Index account.
///
/// This is extracted so it can be called from standalone `process_create_index`
/// as well as from other processors (e.g., `process_create_multicastgroup`) that
/// want to atomically create an index alongside the entity.
pub fn create_index_account<'a>(
program_id: &Pubkey,
index_account: &AccountInfo<'a>,
entity_account: &AccountInfo<'a>,
payer_account: &AccountInfo<'a>,
system_program: &AccountInfo<'a>,
entity_seed: &[u8],
key: &str,
) -> ProgramResult {
// Validate and normalize key
let key = validate_account_code(key).map_err(|_| DoubleZeroError::InvalidAccountCode)?;
let lowercase_key = key.to_ascii_lowercase();

// Derive and verify the Index PDA
let (expected_pda, bump_seed) = get_index_pda(program_id, entity_seed, &key);
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,
entity_account_type: entity_type,
key: lowercase_key.clone(),
bump_seed,
};

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

Ok(())
}

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());
}

create_index_account(
program_id,
index_account,
entity_account,
payer_account,
system_program,
value.entity_seed.as_bytes(),
&value.key,
)
}
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
Expand Up @@ -21,3 +21,4 @@ pub const SEED_LINK_IDS: &[u8] = b"linkids";
pub const SEED_SEGMENT_ROUTING_IDS: &[u8] = b"segmentroutingids";
pub const SEED_VRF_IDS: &[u8] = b"vrfids";
pub const SEED_PERMISSION: &[u8] = b"permission";
pub const SEED_INDEX: &[u8] = b"index";
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use crate::{
error::DoubleZeroError,
state::{
accesspass::AccessPass, accounttype::AccountType, contributor::Contributor, device::Device,
exchange::Exchange, globalconfig::GlobalConfig, globalstate::GlobalState, link::Link,
location::Location, multicastgroup::MulticastGroup, permission::Permission,
exchange::Exchange, globalconfig::GlobalConfig, globalstate::GlobalState, index::Index,
link::Link, location::Location, multicastgroup::MulticastGroup, permission::Permission,
programconfig::ProgramConfig, resource_extension::ResourceExtensionOwned, tenant::Tenant,
user::User,
},
Expand All @@ -29,6 +29,7 @@ pub enum AccountData {
ResourceExtension(ResourceExtensionOwned),
Tenant(Tenant),
Permission(Permission),
Index(Index),
}

impl AccountData {
Expand All @@ -49,6 +50,7 @@ impl AccountData {
AccountData::ResourceExtension(_) => "ResourceExtension",
AccountData::Tenant(_) => "Tenant",
AccountData::Permission(_) => "Permission",
AccountData::Index(_) => "Index",
}
}

Expand All @@ -69,6 +71,7 @@ impl AccountData {
AccountData::ResourceExtension(resource_extension) => resource_extension.to_string(),
AccountData::Tenant(tenant) => tenant.to_string(),
AccountData::Permission(permission) => permission.to_string(),
AccountData::Index(index) => index.to_string(),
}
}

Expand Down Expand Up @@ -183,6 +186,14 @@ impl AccountData {
Err(DoubleZeroError::InvalidAccountType)
}
}

pub fn get_index(&self) -> Result<Index, DoubleZeroError> {
if let AccountData::Index(index) = self {
Ok(index.clone())
} else {
Err(DoubleZeroError::InvalidAccountType)
}
}
}

impl TryFrom<&[u8]> for AccountData {
Expand Down Expand Up @@ -224,6 +235,7 @@ impl TryFrom<&[u8]> for AccountData {
AccountType::Permission => Ok(AccountData::Permission(Permission::try_from(
bytes as &[u8],
)?)),
AccountType::Index => Ok(AccountData::Index(Index::try_from(bytes as &[u8])?)),
}
}
}
Loading
Loading