Skip to content

Commit a67cb86

Browse files
smartcontract: add Index account for multicast group code uniqueness
Introduce an Index account pattern that enforces unique multicast group codes onchain and enables O(1) code-to-pubkey lookup. The Index PDA is derived from the entity type seed and lowercased code, providing case-insensitive uniqueness. Integrated into existing instructions: - CreateMulticastGroup: creates Index alongside the group - UpdateMulticastGroup: atomically renames Index on code change - DeleteMulticastGroup/CloseAccount: closes Index if provided Standalone CreateIndex/DeleteIndex instructions (variants 104/105) added for migration backfill of existing accounts. SDK updated with Index PDA derivation in create/update/get commands and new CreateIndex/DeleteIndex command wrappers.
1 parent 7fd2980 commit a67cb86

37 files changed

Lines changed: 1056 additions & 96 deletions

CHANGELOG.md

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

99
### Changes
1010

11+
- Onchain Programs
12+
- 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
13+
- Serviceability: integrate Index lifecycle into multicast group instructions (create, update, delete, close account) so Index accounts are managed atomically
14+
- Serviceability: add standalone CreateIndex/DeleteIndex instructions (variants 104/105) for migration backfill of existing multicast groups
15+
- SDK
16+
- Rust: add O(1) multicast group lookup by code via Index PDA derivation, with fallback scan for pre-migration accounts
17+
- Rust: add CreateIndex/DeleteIndex command wrappers for migration tooling
1118
- E2E Tests
1219
- 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
1320
- Add geoprobe Docker image, geolocation program build/deploy support, and manager geolocation CLI configuration to the E2E devnet infrastructure

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,
@@ -427,6 +428,12 @@ pub fn process_instruction(
427428
DoubleZeroInstruction::DeleteReservedSubscribeUser(value) => {
428429
process_delete_reserved_subscribe_user(program_id, accounts, &value)?
429430
}
431+
DoubleZeroInstruction::CreateIndex(value) => {
432+
process_create_index(program_id, accounts, &value)?
433+
}
434+
DoubleZeroInstruction::DeleteIndex(value) => {
435+
process_delete_index(program_id, accounts, &value)?
436+
}
430437
};
431438
Ok(())
432439
}

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,
@@ -221,6 +222,9 @@ pub enum DoubleZeroInstruction {
221222

222223
CreateReservedSubscribeUser(CreateReservedSubscribeUserArgs), // variant 102
223224
DeleteReservedSubscribeUser(DeleteReservedSubscribeUserArgs), // variant 103
225+
226+
CreateIndex(IndexCreateArgs), // variant 104
227+
DeleteIndex(IndexDeleteArgs), // variant 105
224228
}
225229

226230
impl DoubleZeroInstruction {
@@ -358,6 +362,9 @@ impl DoubleZeroInstruction {
358362
102 => Ok(Self::CreateReservedSubscribeUser(CreateReservedSubscribeUserArgs::try_from(rest).unwrap())),
359363
103 => Ok(Self::DeleteReservedSubscribeUser(DeleteReservedSubscribeUserArgs::try_from(rest).unwrap())),
360364

365+
104 => Ok(Self::CreateIndex(IndexCreateArgs::try_from(rest).unwrap())),
366+
105 => Ok(Self::DeleteIndex(IndexDeleteArgs::try_from(rest).unwrap())),
367+
361368
_ => Err(ProgramError::InvalidInstructionData),
362369
}
363370
}
@@ -491,6 +498,9 @@ impl DoubleZeroInstruction {
491498

492499
Self::CreateReservedSubscribeUser(_) => "CreateReservedSubscribeUser".to_string(), // variant 102
493500
Self::DeleteReservedSubscribeUser(_) => "DeleteReservedSubscribeUser".to_string(), // variant 103
501+
502+
Self::CreateIndex(_) => "CreateIndex".to_string(), // variant 104
503+
Self::DeleteIndex(_) => "DeleteIndex".to_string(), // variant 105
494504
}
495505
}
496506

@@ -617,6 +627,9 @@ impl DoubleZeroInstruction {
617627

618628
Self::CreateReservedSubscribeUser(args) => format!("{args:?}"), // variant 102
619629
Self::DeleteReservedSubscribeUser(args) => format!("{args:?}"), // variant 103
630+
631+
Self::CreateIndex(args) => format!("{args:?}"), // variant 104
632+
Self::DeleteIndex(args) => format!("{args:?}"), // variant 105
620633
}
621634
}
622635
}

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_RESERVATION, SEED_SEGMENT_ROUTING_IDS, SEED_TENANT, SEED_TUNNEL_IDS, SEED_USER,
1212
SEED_USER_TUNNEL_BLOCK, SEED_VRF_IDS,
@@ -119,6 +119,19 @@ pub fn get_accesspass_pda(
119119
)
120120
}
121121

122+
pub fn get_index_pda(program_id: &Pubkey, entity_seed: &[u8], code: &str) -> (Pubkey, u8) {
123+
let lowercase_code = code.to_ascii_lowercase();
124+
Pubkey::find_program_address(
125+
&[
126+
SEED_PREFIX,
127+
SEED_INDEX,
128+
entity_seed,
129+
lowercase_code.as_bytes(),
130+
],
131+
program_id,
132+
)
133+
}
134+
122135
pub fn get_resource_extension_pda(
123136
program_id: &Pubkey,
124137
resource_type: crate::resource::ResourceType,
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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 code: String,
26+
}
27+
28+
impl fmt::Debug for IndexCreateArgs {
29+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30+
write!(f, "entity_seed: {}, code: {}", self.entity_seed, self.code)
31+
}
32+
}
33+
34+
pub fn process_create_index(
35+
program_id: &Pubkey,
36+
accounts: &[AccountInfo],
37+
value: &IndexCreateArgs,
38+
) -> ProgramResult {
39+
let accounts_iter = &mut accounts.iter();
40+
41+
let index_account = next_account_info(accounts_iter)?;
42+
let entity_account = next_account_info(accounts_iter)?;
43+
let globalstate_account = next_account_info(accounts_iter)?;
44+
let payer_account = next_account_info(accounts_iter)?;
45+
let system_program = next_account_info(accounts_iter)?;
46+
47+
#[cfg(test)]
48+
msg!("process_create_index({:?})", value);
49+
50+
assert!(payer_account.is_signer, "Payer must be a signer");
51+
52+
// Validate accounts
53+
assert_eq!(
54+
globalstate_account.owner, program_id,
55+
"Invalid GlobalState Account Owner"
56+
);
57+
assert_eq!(
58+
entity_account.owner, program_id,
59+
"Invalid Entity Account Owner"
60+
);
61+
assert_eq!(
62+
*system_program.unsigned_key(),
63+
solana_system_interface::program::ID,
64+
"Invalid System Program Account Owner"
65+
);
66+
assert!(index_account.is_writable, "Index Account is not writable");
67+
68+
// Check foundation allowlist
69+
let globalstate = GlobalState::try_from(globalstate_account)?;
70+
if !globalstate.foundation_allowlist.contains(payer_account.key) {
71+
return Err(DoubleZeroError::NotAllowed.into());
72+
}
73+
74+
// Validate and normalize code
75+
let code =
76+
validate_account_code(&value.code).map_err(|_| DoubleZeroError::InvalidAccountCode)?;
77+
let lowercase_code = code.to_ascii_lowercase();
78+
79+
// Derive and verify the Index PDA
80+
let (expected_pda, bump_seed) = get_index_pda(program_id, value.entity_seed.as_bytes(), &code);
81+
assert_eq!(index_account.key, &expected_pda, "Invalid Index Pubkey");
82+
83+
// Uniqueness: account must not already exist
84+
if !index_account.data_is_empty() {
85+
return Err(ProgramError::AccountAlreadyInitialized);
86+
}
87+
88+
// Verify the entity account is a valid program account
89+
assert!(!entity_account.data_is_empty(), "Entity Account is empty");
90+
91+
let index = Index {
92+
account_type: AccountType::Index,
93+
pk: *entity_account.key,
94+
bump_seed,
95+
};
96+
97+
try_acc_create(
98+
&index,
99+
index_account,
100+
payer_account,
101+
system_program,
102+
program_id,
103+
&[
104+
SEED_PREFIX,
105+
SEED_INDEX,
106+
value.entity_seed.as_bytes(),
107+
lowercase_code.as_bytes(),
108+
&[bump_seed],
109+
],
110+
)?;
111+
112+
Ok(())
113+
}
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/processors/multicastgroup/closeaccount.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use crate::{
22
error::DoubleZeroError,
3-
pda::get_resource_extension_pda,
3+
pda::{get_index_pda, get_resource_extension_pda},
44
processors::resource::deallocate_ip,
55
resource::ResourceType,
6+
seeds::SEED_MULTICAST_GROUP,
67
serializer::try_acc_close,
7-
state::{globalstate::GlobalState, multicastgroup::*},
8+
state::{globalstate::GlobalState, index::Index, multicastgroup::*},
89
};
910
use borsh::BorshSerialize;
1011
use borsh_incremental::BorshDeserializeIncremental;
@@ -62,6 +63,9 @@ pub fn process_closeaccount_multicastgroup(
6263
let payer_account = next_account_info(accounts_iter)?;
6364
let system_program = next_account_info(accounts_iter)?;
6465

66+
// Optional: Index account to close alongside the multicast group
67+
let index_account = next_account_info(accounts_iter).ok();
68+
6569
#[cfg(test)]
6670
msg!("process_deactivate_multicastgroup({:?})", value);
6771

@@ -137,6 +141,24 @@ pub fn process_closeaccount_multicastgroup(
137141

138142
try_acc_close(multicastgroup_account, owner_account)?;
139143

144+
// Close the Index account if provided
145+
if let Some(index_acc) = index_account {
146+
assert_eq!(index_acc.owner, program_id, "Invalid Index Account Owner");
147+
assert!(index_acc.is_writable, "Index Account is not writable");
148+
149+
let (expected_index_pda, _) =
150+
get_index_pda(program_id, SEED_MULTICAST_GROUP, &multicastgroup.code);
151+
assert_eq!(index_acc.key, &expected_index_pda, "Invalid Index Pubkey");
152+
153+
let index = Index::try_from(index_acc)?;
154+
assert_eq!(
155+
index.pk, *multicastgroup_account.key,
156+
"Index does not point to this MulticastGroup"
157+
);
158+
159+
try_acc_close(index_acc, payer_account)?;
160+
}
161+
140162
#[cfg(test)]
141163
msg!("Deactivated: MulticastGroup closed");
142164

0 commit comments

Comments
 (0)