Skip to content

Commit e22b5c0

Browse files
smartcontract: add Index account and standalone instructions (#3256)
Resolves: #3408 ## Summary - Introduce an Index account (PDA derived from entity type + lowercased code) for O(1) code-to-pubkey lookup and onchain uniqueness enforcement - Add standalone CreateIndex/DeleteIndex instructions (variants 104/105) for migration backfill of existing accounts - Add Index PDA derivation (`get_index_pda`) and Rust SDK command wrappers Split PR (1/2). Integration into multicast group lifecycle follows in #3415. New derivation workflow: #3256 (comment) ## Diff Breakdown | Category | Files | Lines (+/-) | Net | |------------------|-------|---------------|-------| | Onchain program | 12 | +373 / -4 | +369 | | Rust SDK | 4 | +75 / -0 | +75 | | Tests | 2 | +456 / -0 | +456 | | CHANGELOG | 1 | +2 / -0 | +2 | | **Total** | 18 | +906 / -4 | +902 | ~50% tests, ~41% onchain program, ~9% Rust SDK <details> <summary>Key files (click to expand)</summary> - `smartcontract/programs/doublezero-serviceability/src/state/index.rs` — new Index account struct (account_type + pubkey + bump_seed) with Borsh serialization - `smartcontract/programs/doublezero-serviceability/src/processors/index/create.rs` — standalone CreateIndex processor - `smartcontract/programs/doublezero-serviceability/src/processors/index/delete.rs` — standalone DeleteIndex processor - `smartcontract/programs/doublezero-serviceability/src/pda.rs` — `get_index_pda()` with case-insensitive seed derivation - `smartcontract/sdk/rs/src/commands/index/create.rs` — CreateIndex SDK command wrapper - `smartcontract/sdk/rs/src/commands/index/delete.rs` — DeleteIndex SDK command wrapper </details> ## Testing Verification - New `index_test.rs` with 5 tests: create, duplicate rejection, unauthorized create, delete, unauthorized delete - New test helpers for transactions with extra accounts - All existing tests continue to pass (no modifications to existing processors)
1 parent 44d16f2 commit e22b5c0

19 files changed

Lines changed: 946 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file.
88

99
### Changes
1010

11+
- Smartcontract
12+
- Add Index account for onchain key uniqueness enforcement and O(1) key-to-pubkey lookup, with standalone CreateIndex/DeleteIndex instructions for migration backfill
1113
- CLI
1214
- Allow incremental multicast group addition without disconnecting
1315
- 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/programs/doublezero-serviceability/src/entrypoint.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ use crate::{
4747
setauthority::process_set_authority, setfeatureflags::process_set_feature_flags,
4848
setversion::process_set_version,
4949
},
50+
index::{create::process_create_index, delete::process_delete_index},
5051
link::{
5152
accept::process_accept_link, activate::process_activate_link,
5253
closeaccount::process_closeaccount_link, create::process_create_link,
@@ -421,6 +422,12 @@ pub fn process_instruction(
421422
DoubleZeroInstruction::DeletePermission(value) => {
422423
process_delete_permission(program_id, accounts, &value)?
423424
}
425+
DoubleZeroInstruction::CreateIndex(value) => {
426+
process_create_index(program_id, accounts, &value)?
427+
}
428+
DoubleZeroInstruction::DeleteIndex(value) => {
429+
process_delete_index(program_id, accounts, &value)?
430+
}
424431
};
425432
Ok(())
426433
}

smartcontract/programs/doublezero-serviceability/src/instructions.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ use crate::processors::{
3535
setairdrop::SetAirdropArgs, setauthority::SetAuthorityArgs,
3636
setfeatureflags::SetFeatureFlagsArgs, setversion::SetVersionArgs,
3737
},
38+
index::{create::IndexCreateArgs, delete::IndexDeleteArgs},
3839
link::{
3940
accept::LinkAcceptArgs, activate::LinkActivateArgs, closeaccount::LinkCloseAccountArgs,
4041
create::LinkCreateArgs, delete::LinkDeleteArgs, reject::LinkRejectArgs,
@@ -218,6 +219,9 @@ pub enum DoubleZeroInstruction {
218219

219220
Deprecated102(), // variant 102 (was CreateReservedSubscribeUser)
220221
Deprecated103(), // variant 103 (was DeleteReservedSubscribeUser)
222+
223+
CreateIndex(IndexCreateArgs), // variant 104
224+
DeleteIndex(IndexDeleteArgs), // variant 105
221225
}
222226

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

352356

357+
104 => Ok(Self::CreateIndex(IndexCreateArgs::try_from(rest).unwrap())),
358+
105 => Ok(Self::DeleteIndex(IndexDeleteArgs::try_from(rest).unwrap())),
359+
353360
_ => Err(ProgramError::InvalidInstructionData),
354361
}
355362
}
@@ -483,6 +490,9 @@ impl DoubleZeroInstruction {
483490

484491
Self::Deprecated102() => "Deprecated102".to_string(),
485492
Self::Deprecated103() => "Deprecated103".to_string(),
493+
494+
Self::CreateIndex(_) => "CreateIndex".to_string(), // variant 104
495+
Self::DeleteIndex(_) => "DeleteIndex".to_string(), // variant 105
486496
}
487497
}
488498

@@ -609,6 +619,9 @@ impl DoubleZeroInstruction {
609619

610620
Self::Deprecated102() => String::new(),
611621
Self::Deprecated103() => String::new(),
622+
623+
Self::CreateIndex(args) => format!("{args:?}"), // variant 104
624+
Self::DeleteIndex(args) => format!("{args:?}"), // variant 105
612625
}
613626
}
614627
}

smartcontract/programs/doublezero-serviceability/src/pda.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use solana_program::pubkey::Pubkey;
55
use crate::{
66
seeds::{
77
SEED_ACCESS_PASS, SEED_CONFIG, SEED_CONTRIBUTOR, SEED_DEVICE, SEED_DEVICE_TUNNEL_BLOCK,
8-
SEED_DZ_PREFIX_BLOCK, SEED_EXCHANGE, SEED_GLOBALSTATE, SEED_LINK, SEED_LINK_IDS,
9-
SEED_LOCATION, SEED_MULTICASTGROUP_BLOCK, SEED_MULTICAST_GROUP,
8+
SEED_DZ_PREFIX_BLOCK, SEED_EXCHANGE, SEED_GLOBALSTATE, SEED_INDEX, SEED_LINK,
9+
SEED_LINK_IDS, SEED_LOCATION, SEED_MULTICASTGROUP_BLOCK, SEED_MULTICAST_GROUP,
1010
SEED_MULTICAST_PUBLISHER_BLOCK, SEED_PERMISSION, SEED_PREFIX, SEED_PROGRAM_CONFIG,
1111
SEED_SEGMENT_ROUTING_IDS, SEED_TENANT, SEED_TUNNEL_IDS, SEED_USER, SEED_USER_TUNNEL_BLOCK,
1212
SEED_VRF_IDS,
@@ -103,6 +103,19 @@ pub fn get_accesspass_pda(
103103
)
104104
}
105105

106+
pub fn get_index_pda(program_id: &Pubkey, entity_seed: &[u8], key: &str) -> (Pubkey, u8) {
107+
let lowercase_key = key.to_ascii_lowercase();
108+
Pubkey::find_program_address(
109+
&[
110+
SEED_PREFIX,
111+
SEED_INDEX,
112+
entity_seed,
113+
lowercase_key.as_bytes(),
114+
],
115+
program_id,
116+
)
117+
}
118+
106119
pub fn get_resource_extension_pda(
107120
program_id: &Pubkey,
108121
resource_type: crate::resource::ResourceType,
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
use crate::{
2+
error::DoubleZeroError,
3+
pda::get_index_pda,
4+
seeds::{SEED_INDEX, SEED_PREFIX},
5+
serializer::try_acc_create,
6+
state::{accounttype::AccountType, globalstate::GlobalState, index::Index},
7+
};
8+
use borsh::BorshSerialize;
9+
use borsh_incremental::BorshDeserializeIncremental;
10+
use doublezero_program_common::validate_account_code;
11+
use solana_program::{
12+
account_info::{next_account_info, AccountInfo},
13+
entrypoint::ProgramResult,
14+
program_error::ProgramError,
15+
pubkey::Pubkey,
16+
};
17+
use std::fmt;
18+
19+
#[cfg(test)]
20+
use solana_program::msg;
21+
22+
#[derive(BorshSerialize, BorshDeserializeIncremental, PartialEq, Clone, Default)]
23+
pub struct IndexCreateArgs {
24+
pub entity_seed: String,
25+
pub key: String,
26+
}
27+
28+
impl fmt::Debug for IndexCreateArgs {
29+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30+
write!(f, "entity_seed: {}, key: {}", self.entity_seed, self.key)
31+
}
32+
}
33+
34+
/// Core logic for validating and creating an Index account.
35+
///
36+
/// This is extracted so it can be called from standalone `process_create_index`
37+
/// as well as from other processors (e.g., `process_create_multicastgroup`) that
38+
/// want to atomically create an index alongside the entity.
39+
pub fn create_index_account<'a>(
40+
program_id: &Pubkey,
41+
index_account: &AccountInfo<'a>,
42+
entity_account: &AccountInfo<'a>,
43+
payer_account: &AccountInfo<'a>,
44+
system_program: &AccountInfo<'a>,
45+
entity_seed: &[u8],
46+
key: &str,
47+
) -> ProgramResult {
48+
// Validate and normalize key
49+
let key = validate_account_code(key).map_err(|_| DoubleZeroError::InvalidAccountCode)?;
50+
let lowercase_key = key.to_ascii_lowercase();
51+
52+
// Derive and verify the Index PDA
53+
let (expected_pda, bump_seed) = get_index_pda(program_id, entity_seed, &key);
54+
assert_eq!(index_account.key, &expected_pda, "Invalid Index Pubkey");
55+
56+
// Uniqueness: account must not already exist
57+
if !index_account.data_is_empty() {
58+
return Err(ProgramError::AccountAlreadyInitialized);
59+
}
60+
61+
// Verify the entity account is a valid, non-Index program account
62+
assert!(!entity_account.data_is_empty(), "Entity Account is empty");
63+
let entity_type = AccountType::from(entity_account.try_borrow_data()?[0]);
64+
assert!(
65+
entity_type != AccountType::None && entity_type != AccountType::Index,
66+
"Entity Account has invalid type for indexing: {entity_type}"
67+
);
68+
69+
let index = Index {
70+
account_type: AccountType::Index,
71+
pk: *entity_account.key,
72+
entity_account_type: entity_type,
73+
key: lowercase_key.clone(),
74+
bump_seed,
75+
};
76+
77+
try_acc_create(
78+
&index,
79+
index_account,
80+
payer_account,
81+
system_program,
82+
program_id,
83+
&[
84+
SEED_PREFIX,
85+
SEED_INDEX,
86+
entity_seed,
87+
lowercase_key.as_bytes(),
88+
&[bump_seed],
89+
],
90+
)?;
91+
92+
Ok(())
93+
}
94+
95+
pub fn process_create_index(
96+
program_id: &Pubkey,
97+
accounts: &[AccountInfo],
98+
value: &IndexCreateArgs,
99+
) -> ProgramResult {
100+
let accounts_iter = &mut accounts.iter();
101+
102+
let index_account = next_account_info(accounts_iter)?;
103+
let entity_account = next_account_info(accounts_iter)?;
104+
let globalstate_account = next_account_info(accounts_iter)?;
105+
let payer_account = next_account_info(accounts_iter)?;
106+
let system_program = next_account_info(accounts_iter)?;
107+
108+
#[cfg(test)]
109+
msg!("process_create_index({:?})", value);
110+
111+
assert!(payer_account.is_signer, "Payer must be a signer");
112+
113+
// Validate accounts
114+
assert_eq!(
115+
globalstate_account.owner, program_id,
116+
"Invalid GlobalState Account Owner"
117+
);
118+
assert_eq!(
119+
entity_account.owner, program_id,
120+
"Invalid Entity Account Owner"
121+
);
122+
assert_eq!(
123+
*system_program.unsigned_key(),
124+
solana_system_interface::program::ID,
125+
"Invalid System Program Account Owner"
126+
);
127+
assert!(index_account.is_writable, "Index Account is not writable");
128+
129+
// Check foundation allowlist
130+
let globalstate = GlobalState::try_from(globalstate_account)?;
131+
if !globalstate.foundation_allowlist.contains(payer_account.key) {
132+
return Err(DoubleZeroError::NotAllowed.into());
133+
}
134+
135+
create_index_account(
136+
program_id,
137+
index_account,
138+
entity_account,
139+
payer_account,
140+
system_program,
141+
value.entity_seed.as_bytes(),
142+
&value.key,
143+
)
144+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use crate::{
2+
error::DoubleZeroError,
3+
serializer::try_acc_close,
4+
state::{globalstate::GlobalState, index::Index},
5+
};
6+
use borsh::BorshSerialize;
7+
use borsh_incremental::BorshDeserializeIncremental;
8+
use solana_program::{
9+
account_info::{next_account_info, AccountInfo},
10+
entrypoint::ProgramResult,
11+
pubkey::Pubkey,
12+
};
13+
use std::fmt;
14+
15+
#[cfg(test)]
16+
use solana_program::msg;
17+
18+
#[derive(BorshSerialize, BorshDeserializeIncremental, PartialEq, Clone, Default)]
19+
pub struct IndexDeleteArgs {}
20+
21+
impl fmt::Debug for IndexDeleteArgs {
22+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23+
write!(f, "IndexDeleteArgs")
24+
}
25+
}
26+
27+
pub fn process_delete_index(
28+
program_id: &Pubkey,
29+
accounts: &[AccountInfo],
30+
_value: &IndexDeleteArgs,
31+
) -> ProgramResult {
32+
let accounts_iter = &mut accounts.iter();
33+
34+
let index_account = next_account_info(accounts_iter)?;
35+
let globalstate_account = next_account_info(accounts_iter)?;
36+
let payer_account = next_account_info(accounts_iter)?;
37+
38+
#[cfg(test)]
39+
msg!("process_delete_index");
40+
41+
assert!(payer_account.is_signer, "Payer must be a signer");
42+
43+
// Validate accounts
44+
assert_eq!(
45+
index_account.owner, program_id,
46+
"Invalid Index Account Owner"
47+
);
48+
assert_eq!(
49+
globalstate_account.owner, program_id,
50+
"Invalid GlobalState Account Owner"
51+
);
52+
assert!(index_account.is_writable, "Index Account is not writable");
53+
54+
// Check foundation allowlist
55+
let globalstate = GlobalState::try_from(globalstate_account)?;
56+
if !globalstate.foundation_allowlist.contains(payer_account.key) {
57+
return Err(DoubleZeroError::NotAllowed.into());
58+
}
59+
60+
// Verify it's actually an Index account
61+
let _index = Index::try_from(index_account)?;
62+
63+
try_acc_close(index_account, payer_account)?;
64+
65+
#[cfg(test)]
66+
msg!("Deleted Index account");
67+
68+
Ok(())
69+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod create;
2+
pub mod delete;

smartcontract/programs/doublezero-serviceability/src/processors/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod device;
55
pub mod exchange;
66
pub mod globalconfig;
77
pub mod globalstate;
8+
pub mod index;
89
pub mod link;
910
pub mod location;
1011
pub mod migrate;

smartcontract/programs/doublezero-serviceability/src/seeds.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ pub const SEED_LINK_IDS: &[u8] = b"linkids";
2121
pub const SEED_SEGMENT_ROUTING_IDS: &[u8] = b"segmentroutingids";
2222
pub const SEED_VRF_IDS: &[u8] = b"vrfids";
2323
pub const SEED_PERMISSION: &[u8] = b"permission";
24+
pub const SEED_INDEX: &[u8] = b"index";

smartcontract/programs/doublezero-serviceability/src/state/accountdata.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use crate::{
22
error::DoubleZeroError,
33
state::{
44
accesspass::AccessPass, accounttype::AccountType, contributor::Contributor, device::Device,
5-
exchange::Exchange, globalconfig::GlobalConfig, globalstate::GlobalState, link::Link,
6-
location::Location, multicastgroup::MulticastGroup, permission::Permission,
5+
exchange::Exchange, globalconfig::GlobalConfig, globalstate::GlobalState, index::Index,
6+
link::Link, location::Location, multicastgroup::MulticastGroup, permission::Permission,
77
programconfig::ProgramConfig, resource_extension::ResourceExtensionOwned, tenant::Tenant,
88
user::User,
99
},
@@ -29,6 +29,7 @@ pub enum AccountData {
2929
ResourceExtension(ResourceExtensionOwned),
3030
Tenant(Tenant),
3131
Permission(Permission),
32+
Index(Index),
3233
}
3334

3435
impl AccountData {
@@ -49,6 +50,7 @@ impl AccountData {
4950
AccountData::ResourceExtension(_) => "ResourceExtension",
5051
AccountData::Tenant(_) => "Tenant",
5152
AccountData::Permission(_) => "Permission",
53+
AccountData::Index(_) => "Index",
5254
}
5355
}
5456

@@ -69,6 +71,7 @@ impl AccountData {
6971
AccountData::ResourceExtension(resource_extension) => resource_extension.to_string(),
7072
AccountData::Tenant(tenant) => tenant.to_string(),
7173
AccountData::Permission(permission) => permission.to_string(),
74+
AccountData::Index(index) => index.to_string(),
7275
}
7376
}
7477

@@ -183,6 +186,14 @@ impl AccountData {
183186
Err(DoubleZeroError::InvalidAccountType)
184187
}
185188
}
189+
190+
pub fn get_index(&self) -> Result<Index, DoubleZeroError> {
191+
if let AccountData::Index(index) = self {
192+
Ok(index.clone())
193+
} else {
194+
Err(DoubleZeroError::InvalidAccountType)
195+
}
196+
}
186197
}
187198

188199
impl TryFrom<&[u8]> for AccountData {
@@ -224,6 +235,7 @@ impl TryFrom<&[u8]> for AccountData {
224235
AccountType::Permission => Ok(AccountData::Permission(Permission::try_from(
225236
bytes as &[u8],
226237
)?)),
238+
AccountType::Index => Ok(AccountData::Index(Index::try_from(bytes as &[u8])?)),
227239
}
228240
}
229241
}

0 commit comments

Comments
 (0)