From 4a51bc6a87d5451562905fed46e27707f624eaec Mon Sep 17 00:00:00 2001 From: Martin Sander Date: Mon, 30 Mar 2026 23:41:16 -0500 Subject: [PATCH 1/2] smartcontract: add Index account and standalone instructions Introduce an Index account (PDA derived from entity type + lowercased code) for O(1) code-to-pubkey lookup. Add standalone CreateIndex and DeleteIndex instructions (variants 104/105) for migration backfill. Add Index PDA derivation and Rust SDK command wrappers. --- CHANGELOG.md | 2 + .../src/entrypoint.rs | 7 + .../src/instructions.rs | 13 + .../doublezero-serviceability/src/pda.rs | 17 +- .../src/processors/index/create.rs | 118 ++++++ .../src/processors/index/delete.rs | 69 ++++ .../src/processors/index/mod.rs | 2 + .../src/processors/mod.rs | 1 + .../doublezero-serviceability/src/seeds.rs | 1 + .../src/state/accountdata.rs | 16 +- .../src/state/accounttype.rs | 3 + .../src/state/index.rs | 129 +++++++ .../src/state/mod.rs | 1 + .../tests/index_test.rs | 364 ++++++++++++++++++ .../tests/test_helpers.rs | 92 +++++ .../sdk/rs/src/commands/index/create.rs | 44 +++ .../sdk/rs/src/commands/index/delete.rs | 28 ++ .../sdk/rs/src/commands/index/mod.rs | 2 + smartcontract/sdk/rs/src/commands/mod.rs | 1 + 19 files changed, 906 insertions(+), 4 deletions(-) create mode 100644 smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs create mode 100644 smartcontract/programs/doublezero-serviceability/src/processors/index/delete.rs create mode 100644 smartcontract/programs/doublezero-serviceability/src/processors/index/mod.rs create mode 100644 smartcontract/programs/doublezero-serviceability/src/state/index.rs create mode 100644 smartcontract/programs/doublezero-serviceability/tests/index_test.rs create mode 100644 smartcontract/sdk/rs/src/commands/index/create.rs create mode 100644 smartcontract/sdk/rs/src/commands/index/delete.rs create mode 100644 smartcontract/sdk/rs/src/commands/index/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 999ff521f4..fed44429b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file. ### Changes +- Smartcontract + - Add Index account for onchain code uniqueness enforcement and O(1) code-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 diff --git a/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs b/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs index 02760eeacc..c7833b45ad 100644 --- a/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs +++ b/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs @@ -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, @@ -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(()) } diff --git a/smartcontract/programs/doublezero-serviceability/src/instructions.rs b/smartcontract/programs/doublezero-serviceability/src/instructions.rs index e29ff35c7e..9edc816145 100644 --- a/smartcontract/programs/doublezero-serviceability/src/instructions.rs +++ b/smartcontract/programs/doublezero-serviceability/src/instructions.rs @@ -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, @@ -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 { @@ -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), } } @@ -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 } } @@ -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 } } } diff --git a/smartcontract/programs/doublezero-serviceability/src/pda.rs b/smartcontract/programs/doublezero-serviceability/src/pda.rs index d661aa6ccc..40abc229a5 100644 --- a/smartcontract/programs/doublezero-serviceability/src/pda.rs +++ b/smartcontract/programs/doublezero-serviceability/src/pda.rs @@ -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, @@ -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, diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs new file mode 100644 index 0000000000..e18a8ac085 --- /dev/null +++ b/smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs @@ -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(()) +} diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/index/delete.rs b/smartcontract/programs/doublezero-serviceability/src/processors/index/delete.rs new file mode 100644 index 0000000000..3fe0886736 --- /dev/null +++ b/smartcontract/programs/doublezero-serviceability/src/processors/index/delete.rs @@ -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(()) +} diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/index/mod.rs b/smartcontract/programs/doublezero-serviceability/src/processors/index/mod.rs new file mode 100644 index 0000000000..da1aa3ace2 --- /dev/null +++ b/smartcontract/programs/doublezero-serviceability/src/processors/index/mod.rs @@ -0,0 +1,2 @@ +pub mod create; +pub mod delete; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/mod.rs b/smartcontract/programs/doublezero-serviceability/src/processors/mod.rs index a148f5c660..129390202c 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/mod.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/mod.rs @@ -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; diff --git a/smartcontract/programs/doublezero-serviceability/src/seeds.rs b/smartcontract/programs/doublezero-serviceability/src/seeds.rs index ed605fde57..4ef9626dd9 100644 --- a/smartcontract/programs/doublezero-serviceability/src/seeds.rs +++ b/smartcontract/programs/doublezero-serviceability/src/seeds.rs @@ -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"; diff --git a/smartcontract/programs/doublezero-serviceability/src/state/accountdata.rs b/smartcontract/programs/doublezero-serviceability/src/state/accountdata.rs index ac9c67a82c..89fb6ac3c8 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/accountdata.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/accountdata.rs @@ -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, }, @@ -29,6 +29,7 @@ pub enum AccountData { ResourceExtension(ResourceExtensionOwned), Tenant(Tenant), Permission(Permission), + Index(Index), } impl AccountData { @@ -49,6 +50,7 @@ impl AccountData { AccountData::ResourceExtension(_) => "ResourceExtension", AccountData::Tenant(_) => "Tenant", AccountData::Permission(_) => "Permission", + AccountData::Index(_) => "Index", } } @@ -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(), } } @@ -183,6 +186,14 @@ impl AccountData { Err(DoubleZeroError::InvalidAccountType) } } + + pub fn get_index(&self) -> Result { + if let AccountData::Index(index) = self { + Ok(index.clone()) + } else { + Err(DoubleZeroError::InvalidAccountType) + } + } } impl TryFrom<&[u8]> for AccountData { @@ -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])?)), } } } diff --git a/smartcontract/programs/doublezero-serviceability/src/state/accounttype.rs b/smartcontract/programs/doublezero-serviceability/src/state/accounttype.rs index 522bbebd57..24e8430bab 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/accounttype.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/accounttype.rs @@ -23,6 +23,7 @@ pub enum AccountType { ResourceExtension = 12, Tenant = 13, Permission = 15, + Index = 16, } pub trait AccountTypeInfo { @@ -50,6 +51,7 @@ impl From for AccountType { 12 => AccountType::ResourceExtension, 13 => AccountType::Tenant, 15 => AccountType::Permission, + 16 => AccountType::Index, _ => AccountType::None, } } @@ -73,6 +75,7 @@ impl fmt::Display for AccountType { AccountType::ResourceExtension => write!(f, "resourceextension"), AccountType::Tenant => write!(f, "tenant"), AccountType::Permission => write!(f, "permission"), + AccountType::Index => write!(f, "index"), } } } diff --git a/smartcontract/programs/doublezero-serviceability/src/state/index.rs b/smartcontract/programs/doublezero-serviceability/src/state/index.rs new file mode 100644 index 0000000000..71e46fba75 --- /dev/null +++ b/smartcontract/programs/doublezero-serviceability/src/state/index.rs @@ -0,0 +1,129 @@ +use crate::{ + error::{DoubleZeroError, Validate}, + state::accounttype::AccountType, +}; +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; +use std::fmt; + +#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Index { + pub account_type: AccountType, // 1 + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "doublezero_program_common::serializer::serialize_pubkey_as_string", + deserialize_with = "doublezero_program_common::serializer::deserialize_pubkey_from_string" + ) + )] + pub pk: Pubkey, // 32 + pub bump_seed: u8, // 1 +} + +impl fmt::Display for Index { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Index {{ account_type: {}, pk: {}, bump_seed: {} }}", + self.account_type, self.pk, self.bump_seed + ) + } +} + +impl Default for Index { + fn default() -> Self { + Self { + account_type: AccountType::Index, + pk: Pubkey::default(), + bump_seed: 0, + } + } +} + +impl TryFrom<&[u8]> for Index { + type Error = ProgramError; + + fn try_from(mut data: &[u8]) -> Result { + let out = Self { + account_type: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), + pk: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), + bump_seed: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), + }; + + if out.account_type != AccountType::Index { + return Err(ProgramError::InvalidAccountData); + } + + Ok(out) + } +} + +impl TryFrom<&AccountInfo<'_>> for Index { + type Error = ProgramError; + + fn try_from(account: &AccountInfo) -> Result { + let data = account.try_borrow_data()?; + let res = Self::try_from(&data[..]); + if res.is_err() { + msg!("Failed to deserialize Index: {:?}", res.as_ref().err()); + } + res + } +} + +impl Validate for Index { + fn validate(&self) -> Result<(), DoubleZeroError> { + if self.account_type != AccountType::Index { + msg!("Invalid account type: {}", self.account_type); + return Err(DoubleZeroError::InvalidAccountType); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_state_index_try_from_defaults() { + let data = [AccountType::Index as u8]; + let val = Index::try_from(&data[..]).unwrap(); + + assert_eq!(val.pk, Pubkey::default()); + assert_eq!(val.bump_seed, 0); + } + + #[test] + fn test_state_index_serialization() { + let val = Index { + account_type: AccountType::Index, + pk: Pubkey::new_unique(), + bump_seed: 254, + }; + + let data = borsh::to_vec(&val).unwrap(); + let val2 = Index::try_from(&data[..]).unwrap(); + + val.validate().unwrap(); + val2.validate().unwrap(); + + assert_eq!(val, val2); + assert_eq!(val.account_type as u8, data[0]); + assert_eq!(data.len(), borsh::object_length(&val).unwrap(),); + } + + #[test] + fn test_state_index_validate_error_invalid_account_type() { + let val = Index { + account_type: AccountType::Device, + pk: Pubkey::new_unique(), + bump_seed: 1, + }; + assert_eq!( + val.validate().unwrap_err(), + DoubleZeroError::InvalidAccountType + ); + } +} diff --git a/smartcontract/programs/doublezero-serviceability/src/state/mod.rs b/smartcontract/programs/doublezero-serviceability/src/state/mod.rs index 793c35e469..bbf02d13a1 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/mod.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/mod.rs @@ -7,6 +7,7 @@ pub mod exchange; pub mod feature_flags; pub mod globalconfig; pub mod globalstate; +pub mod index; pub mod interface; pub mod link; pub mod location; diff --git a/smartcontract/programs/doublezero-serviceability/tests/index_test.rs b/smartcontract/programs/doublezero-serviceability/tests/index_test.rs new file mode 100644 index 0000000000..4d7c2986cc --- /dev/null +++ b/smartcontract/programs/doublezero-serviceability/tests/index_test.rs @@ -0,0 +1,364 @@ +use doublezero_serviceability::{ + instructions::*, + pda::*, + processors::{ + index::{create::IndexCreateArgs, delete::IndexDeleteArgs}, + multicastgroup::create::MulticastGroupCreateArgs, + }, + seeds::SEED_MULTICAST_GROUP, + state::accounttype::AccountType, +}; +use solana_program_test::*; +use solana_sdk::{ + instruction::AccountMeta, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +mod test_helpers; +use test_helpers::*; + +/// Helper: create a multicast group and return its pubkey. +/// The multicast group is created without onchain allocation (Pending status). +async fn create_multicast_group( + banks_client: &mut BanksClient, + payer: &Keypair, + program_id: Pubkey, + globalstate_pubkey: Pubkey, + code: &str, +) -> Pubkey { + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let globalstate = get_globalstate(banks_client, globalstate_pubkey).await; + let (mgroup_pubkey, _) = get_multicastgroup_pda(&program_id, globalstate.account_index + 1); + + execute_transaction( + banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateMulticastGroup(MulticastGroupCreateArgs { + code: code.to_string(), + max_bandwidth: 1000, + owner: Pubkey::new_unique(), + use_onchain_allocation: false, + }), + vec![ + AccountMeta::new(mgroup_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + ], + payer, + ) + .await; + + mgroup_pubkey +} + +#[tokio::test] +async fn test_create_index() { + let (mut banks_client, payer, program_id, globalstate_pubkey, _) = + setup_program_with_globalconfig().await; + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + + // Create a multicast group to use as the entity account + let mgroup_pubkey = create_multicast_group( + &mut banks_client, + &payer, + program_id, + globalstate_pubkey, + "test-mg", + ) + .await; + + // Derive the Index PDA for a new code on the same entity seed + let code = "my-index"; + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, code); + + // Create the Index + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateIndex(IndexCreateArgs { + entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), + code: code.to_string(), + }), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(mgroup_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // Verify the Index account exists and contains the entity pubkey + let index_data = get_account_data(&mut banks_client, index_pda) + .await + .expect("Index account should exist"); + let index = index_data.get_index().unwrap(); + assert_eq!(index.account_type, AccountType::Index); + assert_eq!( + index.pk, mgroup_pubkey, + "Index should point to the multicast group" + ); +} + +#[tokio::test] +async fn test_create_index_duplicate_fails() { + let (mut banks_client, payer, program_id, globalstate_pubkey, _) = + setup_program_with_globalconfig().await; + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + + // Create a multicast group as the entity + let mgroup_pubkey = create_multicast_group( + &mut banks_client, + &payer, + program_id, + globalstate_pubkey, + "dup-mg", + ) + .await; + + let code = "dup-code"; + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, code); + + // First CreateIndex should succeed + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateIndex(IndexCreateArgs { + entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), + code: code.to_string(), + }), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(mgroup_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // Wait for a new blockhash to avoid transaction deduplication + let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; + + // Second CreateIndex with the same entity_seed+code should fail + let result = execute_transaction_expect_failure( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateIndex(IndexCreateArgs { + entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), + code: code.to_string(), + }), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(mgroup_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + let error_string = format!("{:?}", result.unwrap_err()); + assert!( + error_string.contains("AccountAlreadyInitialized") + || error_string.contains("already in use"), + "Expected AccountAlreadyInitialized error, got: {error_string}", + ); +} + +#[tokio::test] +async fn test_create_index_unauthorized_fails() { + let (mut banks_client, payer, program_id, globalstate_pubkey, _) = + setup_program_with_globalconfig().await; + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + + // Create a multicast group as the entity + let mgroup_pubkey = create_multicast_group( + &mut banks_client, + &payer, + program_id, + globalstate_pubkey, + "unauth-mg", + ) + .await; + + // Create an unauthorized keypair with some lamports + let unauthorized = Keypair::new(); + transfer( + &mut banks_client, + &payer, + &unauthorized.pubkey(), + 10_000_000, + ) + .await; + + let code = "unauth-code"; + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, code); + + // Attempt CreateIndex with the unauthorized payer + let result = try_execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateIndex(IndexCreateArgs { + entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), + code: code.to_string(), + }), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(mgroup_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &unauthorized, + ) + .await; + + let error_string = format!("{:?}", result.unwrap_err()); + assert!( + error_string.contains("Custom(8)"), + "Expected NotAllowed error (Custom(8)), got: {error_string}", + ); +} + +#[tokio::test] +async fn test_delete_index() { + let (mut banks_client, payer, program_id, globalstate_pubkey, _) = + setup_program_with_globalconfig().await; + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + + // Create a multicast group as the entity + let mgroup_pubkey = create_multicast_group( + &mut banks_client, + &payer, + program_id, + globalstate_pubkey, + "del-mg", + ) + .await; + + let code = "del-code"; + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, code); + + // Create the Index + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateIndex(IndexCreateArgs { + entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), + code: code.to_string(), + }), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(mgroup_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // Verify the Index exists + let index_data = get_account_data(&mut banks_client, index_pda).await; + assert!( + index_data.is_some(), + "Index account should exist before deletion" + ); + + // Delete the Index + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::DeleteIndex(IndexDeleteArgs {}), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // Verify the account is closed + let index_after = get_account_data(&mut banks_client, index_pda).await; + assert!( + index_after.is_none(), + "Index account should be closed after deletion" + ); +} + +#[tokio::test] +async fn test_delete_index_unauthorized_fails() { + let (mut banks_client, payer, program_id, globalstate_pubkey, _) = + setup_program_with_globalconfig().await; + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + + // Create a multicast group as the entity + let mgroup_pubkey = create_multicast_group( + &mut banks_client, + &payer, + program_id, + globalstate_pubkey, + "delauth-mg", + ) + .await; + + let code = "delauth-code"; + let (index_pda, _) = get_index_pda(&program_id, SEED_MULTICAST_GROUP, code); + + // Create the Index with the authorized payer + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateIndex(IndexCreateArgs { + entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), + code: code.to_string(), + }), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(mgroup_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // Create an unauthorized keypair with some lamports + let unauthorized = Keypair::new(); + transfer( + &mut banks_client, + &payer, + &unauthorized.pubkey(), + 10_000_000, + ) + .await; + + // Attempt DeleteIndex with the unauthorized payer + let result = try_execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::DeleteIndex(IndexDeleteArgs {}), + vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &unauthorized, + ) + .await; + + let error_string = format!("{:?}", result.unwrap_err()); + assert!( + error_string.contains("Custom(8)"), + "Expected NotAllowed error (Custom(8)), got: {error_string}", + ); + + // Verify the Index is still intact + let index_data = get_account_data(&mut banks_client, index_pda).await; + assert!( + index_data.is_some(), + "Index account should still exist after unauthorized delete" + ); +} diff --git a/smartcontract/programs/doublezero-serviceability/tests/test_helpers.rs b/smartcontract/programs/doublezero-serviceability/tests/test_helpers.rs index 7ce9d55cd1..307179077c 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/test_helpers.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/test_helpers.rs @@ -278,6 +278,98 @@ pub async fn execute_transaction_expect_failure( result } +/// Execute a transaction with extra accounts and expect it to fail. Returns the error result. +#[allow(dead_code)] +pub async fn execute_transaction_expect_failure_with_extra_accounts( + banks_client: &mut BanksClient, + _recent_blockhash: solana_program::hash::Hash, + program_id: Pubkey, + instruction: DoubleZeroInstruction, + accounts: Vec, + payer: &Keypair, + extra_accounts: &[AccountMeta], +) -> Result<(), BanksClientError> { + print!("➡️ Transaction (expecting failure) {instruction:?} "); + + let recent_blockhash = banks_client + .get_latest_blockhash() + .await + .expect("Failed to get latest blockhash"); + let mut transaction = create_transaction_with_extra_accounts( + program_id, + &instruction, + &accounts, + payer, + extra_accounts, + ); + transaction.try_sign(&[&payer], recent_blockhash).unwrap(); + let result = banks_client.process_transaction(transaction).await; + + if result.is_err() { + println!("❌ (expected)"); + } else { + println!("✅ (unexpected success)"); + } + + result +} + +#[allow(dead_code)] +pub async fn execute_transaction_with_extra_accounts( + banks_client: &mut BanksClient, + _recent_blockhash: solana_program::hash::Hash, + program_id: Pubkey, + instruction: DoubleZeroInstruction, + accounts: Vec, + payer: &Keypair, + extra_accounts: &[AccountMeta], +) { + print!("➡️ Transaction {instruction:?} "); + + let recent_blockhash = banks_client + .get_latest_blockhash() + .await + .expect("Failed to get latest blockhash"); + let mut transaction = create_transaction_with_extra_accounts( + program_id, + &instruction, + &accounts, + payer, + extra_accounts, + ); + transaction.try_sign(&[&payer], recent_blockhash).unwrap(); + banks_client.process_transaction(transaction).await.unwrap(); + + println!("✅") +} + +#[allow(dead_code)] +pub async fn try_execute_transaction_with_extra_accounts( + banks_client: &mut BanksClient, + recent_blockhash: solana_program::hash::Hash, + program_id: Pubkey, + instruction: DoubleZeroInstruction, + accounts: Vec, + payer: &Keypair, + extra_accounts: &[AccountMeta], +) -> Result<(), BanksClientError> { + print!("➡️ Transaction {instruction:?} "); + + let mut transaction = create_transaction_with_extra_accounts( + program_id, + &instruction, + &accounts, + payer, + extra_accounts, + ); + transaction.try_sign(&[&payer], recent_blockhash).unwrap(); + banks_client.process_transaction(transaction).await?; + + println!("✅"); + + Ok(()) +} + pub fn create_transaction( program_id: Pubkey, instruction: &DoubleZeroInstruction, diff --git a/smartcontract/sdk/rs/src/commands/index/create.rs b/smartcontract/sdk/rs/src/commands/index/create.rs new file mode 100644 index 0000000000..aea9d26cff --- /dev/null +++ b/smartcontract/sdk/rs/src/commands/index/create.rs @@ -0,0 +1,44 @@ +use crate::{commands::globalstate::get::GetGlobalStateCommand, DoubleZeroClient}; +use doublezero_program_common::validate_account_code; +use doublezero_serviceability::{ + instructions::DoubleZeroInstruction, pda::get_index_pda, + processors::index::create::IndexCreateArgs, +}; +use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, signature::Signature}; + +#[derive(Debug, PartialEq, Clone)] +pub struct CreateIndexCommand { + pub entity_seed: String, + pub code: String, + pub entity_pubkey: Pubkey, +} + +impl CreateIndexCommand { + pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result<(Signature, Pubkey)> { + let code = + validate_account_code(&self.code).map_err(|err| eyre::eyre!("invalid code: {err}"))?; + + let (globalstate_pubkey, _) = GetGlobalStateCommand + .execute(client) + .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; + + let (index_pda, _) = + get_index_pda(&client.get_program_id(), self.entity_seed.as_bytes(), &code); + + let accounts = vec![ + AccountMeta::new(index_pda, false), + AccountMeta::new_readonly(self.entity_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ]; + + client + .execute_transaction( + DoubleZeroInstruction::CreateIndex(IndexCreateArgs { + entity_seed: self.entity_seed.clone(), + code, + }), + accounts, + ) + .map(|sig| (sig, index_pda)) + } +} diff --git a/smartcontract/sdk/rs/src/commands/index/delete.rs b/smartcontract/sdk/rs/src/commands/index/delete.rs new file mode 100644 index 0000000000..bea2160a0b --- /dev/null +++ b/smartcontract/sdk/rs/src/commands/index/delete.rs @@ -0,0 +1,28 @@ +use crate::{commands::globalstate::get::GetGlobalStateCommand, DoubleZeroClient}; +use doublezero_serviceability::{ + instructions::DoubleZeroInstruction, processors::index::delete::IndexDeleteArgs, +}; +use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, signature::Signature}; + +#[derive(Debug, PartialEq, Clone)] +pub struct DeleteIndexCommand { + pub index_pubkey: Pubkey, +} + +impl DeleteIndexCommand { + pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result { + let (globalstate_pubkey, _) = GetGlobalStateCommand + .execute(client) + .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; + + let accounts = vec![ + AccountMeta::new(self.index_pubkey, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ]; + + client.execute_transaction( + DoubleZeroInstruction::DeleteIndex(IndexDeleteArgs {}), + accounts, + ) + } +} diff --git a/smartcontract/sdk/rs/src/commands/index/mod.rs b/smartcontract/sdk/rs/src/commands/index/mod.rs new file mode 100644 index 0000000000..da1aa3ace2 --- /dev/null +++ b/smartcontract/sdk/rs/src/commands/index/mod.rs @@ -0,0 +1,2 @@ +pub mod create; +pub mod delete; diff --git a/smartcontract/sdk/rs/src/commands/mod.rs b/smartcontract/sdk/rs/src/commands/mod.rs index ed36fffae3..b5f49ff28b 100644 --- a/smartcontract/sdk/rs/src/commands/mod.rs +++ b/smartcontract/sdk/rs/src/commands/mod.rs @@ -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; From 8113f221d3b4d4e563b59bec96ecdfe49b2a1daa Mon Sep 17 00:00:00 2001 From: Martin Sander Date: Tue, 31 Mar 2026 18:38:17 -0500 Subject: [PATCH 2/2] address comments --- CHANGELOG.md | 2 +- .../doublezero-serviceability/src/pda.rs | 6 +- .../src/processors/index/create.rs | 112 +++++++++++------- .../src/state/index.rs | 16 ++- .../tests/index_test.rs | 16 +-- .../sdk/rs/src/commands/index/create.rs | 10 +- 6 files changed, 101 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fed44429b8..ce9a751e94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ All notable changes to this project will be documented in this file. ### Changes - Smartcontract - - Add Index account for onchain code uniqueness enforcement and O(1) code-to-pubkey lookup, with standalone CreateIndex/DeleteIndex instructions for migration backfill + - 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 diff --git a/smartcontract/programs/doublezero-serviceability/src/pda.rs b/smartcontract/programs/doublezero-serviceability/src/pda.rs index 40abc229a5..7e8e0e1bcf 100644 --- a/smartcontract/programs/doublezero-serviceability/src/pda.rs +++ b/smartcontract/programs/doublezero-serviceability/src/pda.rs @@ -103,14 +103,14 @@ 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(); +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_code.as_bytes(), + lowercase_key.as_bytes(), ], program_id, ) diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs index e18a8ac085..2026ee3452 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs @@ -22,15 +22,76 @@ use solana_program::msg; #[derive(BorshSerialize, BorshDeserializeIncremental, PartialEq, Clone, Default)] pub struct IndexCreateArgs { pub entity_seed: String, - pub code: String, + pub key: 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) + 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], @@ -71,48 +132,13 @@ pub fn process_create_index( 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, + create_index_account( + program_id, index_account, + entity_account, payer_account, system_program, - program_id, - &[ - SEED_PREFIX, - SEED_INDEX, - value.entity_seed.as_bytes(), - lowercase_code.as_bytes(), - &[bump_seed], - ], - )?; - - Ok(()) + value.entity_seed.as_bytes(), + &value.key, + ) } diff --git a/smartcontract/programs/doublezero-serviceability/src/state/index.rs b/smartcontract/programs/doublezero-serviceability/src/state/index.rs index 71e46fba75..cdeac8abfc 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/index.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/index.rs @@ -18,6 +18,8 @@ pub struct Index { ) )] pub pk: Pubkey, // 32 + pub entity_account_type: AccountType, // 1 + pub key: String, // 4 + len pub bump_seed: u8, // 1 } @@ -25,8 +27,8 @@ impl fmt::Display for Index { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "Index {{ account_type: {}, pk: {}, bump_seed: {} }}", - self.account_type, self.pk, self.bump_seed + "Index {{ account_type: {}, pk: {}, entity_account_type: {}, key: {}, bump_seed: {} }}", + self.account_type, self.pk, self.entity_account_type, self.key, self.bump_seed ) } } @@ -36,6 +38,8 @@ impl Default for Index { Self { account_type: AccountType::Index, pk: Pubkey::default(), + entity_account_type: AccountType::None, + key: String::new(), bump_seed: 0, } } @@ -48,6 +52,8 @@ impl TryFrom<&[u8]> for Index { let out = Self { account_type: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), pk: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), + entity_account_type: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), + key: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), bump_seed: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), }; @@ -92,6 +98,8 @@ mod tests { let val = Index::try_from(&data[..]).unwrap(); assert_eq!(val.pk, Pubkey::default()); + assert_eq!(val.entity_account_type, AccountType::None); + assert_eq!(val.key, ""); assert_eq!(val.bump_seed, 0); } @@ -100,6 +108,8 @@ mod tests { let val = Index { account_type: AccountType::Index, pk: Pubkey::new_unique(), + entity_account_type: AccountType::MulticastGroup, + key: "my-key".to_string(), bump_seed: 254, }; @@ -119,6 +129,8 @@ mod tests { let val = Index { account_type: AccountType::Device, pk: Pubkey::new_unique(), + entity_account_type: AccountType::MulticastGroup, + key: "test".to_string(), bump_seed: 1, }; assert_eq!( diff --git a/smartcontract/programs/doublezero-serviceability/tests/index_test.rs b/smartcontract/programs/doublezero-serviceability/tests/index_test.rs index 4d7c2986cc..40de6b9a43 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/index_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/index_test.rs @@ -79,7 +79,7 @@ async fn test_create_index() { program_id, DoubleZeroInstruction::CreateIndex(IndexCreateArgs { entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), - code: code.to_string(), + key: code.to_string(), }), vec![ AccountMeta::new(index_pda, false), @@ -100,6 +100,8 @@ async fn test_create_index() { index.pk, mgroup_pubkey, "Index should point to the multicast group" ); + assert_eq!(index.entity_account_type, AccountType::MulticastGroup); + assert_eq!(index.key, code); } #[tokio::test] @@ -128,7 +130,7 @@ async fn test_create_index_duplicate_fails() { program_id, DoubleZeroInstruction::CreateIndex(IndexCreateArgs { entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), - code: code.to_string(), + key: code.to_string(), }), vec![ AccountMeta::new(index_pda, false), @@ -142,14 +144,14 @@ async fn test_create_index_duplicate_fails() { // Wait for a new blockhash to avoid transaction deduplication let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; - // Second CreateIndex with the same entity_seed+code should fail + // Second CreateIndex with the same entity_seed+key should fail let result = execute_transaction_expect_failure( &mut banks_client, recent_blockhash, program_id, DoubleZeroInstruction::CreateIndex(IndexCreateArgs { entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), - code: code.to_string(), + key: code.to_string(), }), vec![ AccountMeta::new(index_pda, false), @@ -204,7 +206,7 @@ async fn test_create_index_unauthorized_fails() { program_id, DoubleZeroInstruction::CreateIndex(IndexCreateArgs { entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), - code: code.to_string(), + key: code.to_string(), }), vec![ AccountMeta::new(index_pda, false), @@ -248,7 +250,7 @@ async fn test_delete_index() { program_id, DoubleZeroInstruction::CreateIndex(IndexCreateArgs { entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), - code: code.to_string(), + key: code.to_string(), }), vec![ AccountMeta::new(index_pda, false), @@ -314,7 +316,7 @@ async fn test_delete_index_unauthorized_fails() { program_id, DoubleZeroInstruction::CreateIndex(IndexCreateArgs { entity_seed: String::from_utf8(SEED_MULTICAST_GROUP.to_vec()).unwrap(), - code: code.to_string(), + key: code.to_string(), }), vec![ AccountMeta::new(index_pda, false), diff --git a/smartcontract/sdk/rs/src/commands/index/create.rs b/smartcontract/sdk/rs/src/commands/index/create.rs index aea9d26cff..4f71204d07 100644 --- a/smartcontract/sdk/rs/src/commands/index/create.rs +++ b/smartcontract/sdk/rs/src/commands/index/create.rs @@ -9,21 +9,21 @@ use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, signature::Signature} #[derive(Debug, PartialEq, Clone)] pub struct CreateIndexCommand { pub entity_seed: String, - pub code: String, + pub key: String, pub entity_pubkey: Pubkey, } impl CreateIndexCommand { pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result<(Signature, Pubkey)> { - let code = - validate_account_code(&self.code).map_err(|err| eyre::eyre!("invalid code: {err}"))?; + let key = + validate_account_code(&self.key).map_err(|err| eyre::eyre!("invalid key: {err}"))?; let (globalstate_pubkey, _) = GetGlobalStateCommand .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; let (index_pda, _) = - get_index_pda(&client.get_program_id(), self.entity_seed.as_bytes(), &code); + get_index_pda(&client.get_program_id(), self.entity_seed.as_bytes(), &key); let accounts = vec![ AccountMeta::new(index_pda, false), @@ -35,7 +35,7 @@ impl CreateIndexCommand { .execute_transaction( DoubleZeroInstruction::CreateIndex(IndexCreateArgs { entity_seed: self.entity_seed.clone(), - code, + key, }), accounts, )