Skip to content

Comments

geolocation: add program scaffolding and ProgramConfig instructions#3083

Open
nikw9944 wants to merge 5 commits intomainfrom
bdz/doublezero-2952-part-1-program-config
Open

geolocation: add program scaffolding and ProgramConfig instructions#3083
nikw9944 wants to merge 5 commits intomainfrom
bdz/doublezero-2952-part-1-program-config

Conversation

@nikw9944
Copy link
Contributor

@nikw9944 nikw9944 commented Feb 23, 2026

Summary of Changes

  • Add the doublezero-geolocation onchain program with InitProgramConfig and UpdateProgramConfig instructions
  • Foundation allowlist authorization via CPI to the serviceability program's GlobalState
  • Upgrade authority verification for program config initialization and updates
  • Borsh-derived instruction enum with pack/unpack serialization
  • Workspace and Makefile integration for build, lint, and test

Diff Breakdown

Category Files Lines (+/-) Net
Core logic 12 +609 / -0 +609
Tests 4 +177 / -0 +177
Config/build 5 +93 / -9 +84
Docs 1 +33 / -0 +33

Part 1 of 3 for RFC16 geolocation verification — program scaffolding and ProgramConfig management only; GeoProbe CRUD and parent device instructions follow in subsequent PRs.

Key files (click to expand)
  • smartcontract/programs/doublezero-geolocation/src/state/program_config.rs — ProgramConfig account state with Borsh serialization, field-by-field deserialization, and validation
  • smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs — UpdateProgramConfig processor with upgrade authority verification and partial field updates
  • smartcontract/programs/doublezero-geolocation/src/processors/program_config/init.rs — InitProgramConfig processor with PDA creation and upgrade authority check
  • smartcontract/programs/doublezero-geolocation/src/serializer.rs — Generic account create/write helpers using doublezero-program-common utilities
  • smartcontract/programs/doublezero-geolocation/src/instructions.rs — Borsh-derived GeolocationInstruction enum with 2 variants
  • smartcontract/programs/doublezero-geolocation/src/error.rs — GeolocationError enum with thiserror derives and ProgramError conversion
  • smartcontract/programs/doublezero-geolocation/src/processors/mod.rscheck_foundation_allowlist shared validation via serviceability CPI

Testing Verification

  • 10 unit tests covering instruction pack/unpack roundtrips, PDA determinism, ProgramConfig serialization, error code mappings, and account type validation
  • Clippy clean with -Dclippy::all -Dwarnings
  • Formatting verified with nightly rustfmt

@nikw9944 nikw9944 linked an issue Feb 23, 2026 that may be closed by this pull request
@nikw9944 nikw9944 force-pushed the bdz/doublezero-2952-part-1-program-config branch from faa0ca6 to b909d19 Compare February 23, 2026 23:29
Comment on lines 87 to 91
let (expected_pda, _) = get_program_config_pda(program_id);
if program_config_account.key != &expected_pda {
msg!("Invalid ProgramConfig Pubkey");
return Err(ProgramError::InvalidSeeds);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually isn't needed when updating the program config since there is only one account that can be the program config (the one you initialized the program with). The discriminator defines this account, so the try_from below ensures that this account is the program config.

It doesn't hurt to have this check. But it doesn't add any safety, either

Comment on lines 21 to 25
let (expected_config_pda, _) = get_program_config_pda(program_id);
if program_config_account.key != &expected_config_pda {
msg!("Invalid ProgramConfig PDA");
return Err(ProgramError::InvalidSeeds);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Comment on lines 39 to 45
// Verify serviceability GlobalState PDA address
let (expected_gs_pda, _) =
doublezero_serviceability::pda::get_globalstate_pda(serviceability_program_id);
if serviceability_globalstate_account.key != &expected_gs_pda {
msg!("Invalid Serviceability GlobalState PDA");
return Err(ProgramError::InvalidSeeds);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as your program config, there is only one global state on the Serviceability program

use core::fmt;
use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError};

#[derive(BorshSerialize, Debug, PartialEq, Clone)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably derive BorshDeserialize or BorshDeserializeIncremental

Comment on lines 32 to 37
let out = Self {
account_type: BorshDeserialize::deserialize(&mut data).unwrap_or_default(),
bump_seed: BorshDeserialize::deserialize(&mut data).unwrap_or_default(),
version: BorshDeserialize::deserialize(&mut data).unwrap_or_default(),
min_compatible_version: BorshDeserialize::deserialize(&mut data).unwrap_or_default(),
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you derive either borsh deserialize traits, you wouldn't have to deserialize each element like this

min_compatible_version: 1,
};
let err = val.validate();
assert!(err.is_err());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unwrap_err below will panic if this line were false

fn test_state_programconfig_try_from_invalid_account_type() {
let data = [AccountType::None as u8];
let result = GeolocationProgramConfig::try_from(&data[..]);
assert!(result.is_err());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Comment on lines 14 to 22
impl GeolocationInstruction {
pub fn pack(&self) -> Vec<u8> {
borsh::to_vec(&self).unwrap()
}

pub fn unpack(data: &[u8]) -> Result<Self, ProgramError> {
borsh::from_slice(data).map_err(|_| ProgramError::InvalidInstructionData)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These don't seem necessary since they're light wrappers around the borsh methods

Comment on lines 9 to 32
#[cfg(test)]
mod tests {
use super::*;

fn test_program_id() -> Pubkey {
Pubkey::new_unique()
}

#[test]
fn test_program_config_pda_is_deterministic() {
let program_id = test_program_id();
let (pda1, bump1) = get_program_config_pda(&program_id);
let (pda2, bump2) = get_program_config_pda(&program_id);
assert_eq!(pda1, pda2);
assert_eq!(bump1, bump2);
}

#[test]
fn test_program_config_pda_differs_by_program_id() {
let (pda1, _) = get_program_config_pda(&Pubkey::new_unique());
let (pda2, _) = get_program_config_pda(&Pubkey::new_unique());
assert_ne!(pda1, pda2);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can probably do away with these tests since these seem to just exercise Pubkey::find_program_address

Comment on lines 30 to 34
let mut data = Vec::with_capacity(45);
data.extend_from_slice(&3u32.to_le_bytes());
data.extend_from_slice(&0u64.to_le_bytes());
data.push(1); // Some
data.extend_from_slice(upgrade_authority.as_ref());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like when you deserialize the upgradeable state, you should be able to serialize it with bincode, too

)
.await;
assert!(
result.is_err(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you expecting a specific error? Or are you okay with just checking that there is any error?

)
.await;
assert!(
result.is_err(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Comment on lines 22 to 23
solana-bincode = "2.2.1"
solana-loader-v3-interface = { version = "3.0.0", features = ["serde"] }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to add these to the workspace Cargo.toml?

Add the doublezero-geolocation onchain program with InitProgramConfig
and UpdateProgramConfig instructions, foundation allowlist integration
via serviceability CPI, and build/lint/test integration.

Part 1 of 3 for RFC16 geolocation verification.
…heck

Move the min_compatible_version > version invariant check to run after
both fields are applied, so a version-only downgrade cannot silently
create an inconsistent state where min_compatible_version > version.
@nikw9944 nikw9944 force-pushed the bdz/doublezero-2952-part-1-program-config branch from 6917536 to 7176113 Compare February 24, 2026 16:28
@nikw9944 nikw9944 modified the milestone: Geo Location Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

geolocation: program scaffolding

2 participants