From 2e36c2502a2fa65d8d5c800363cd95d4b0274737 Mon Sep 17 00:00:00 2001 From: nikw9944 Date: Mon, 23 Feb 2026 22:10:46 +0000 Subject: [PATCH 1/4] geolocation: add program scaffolding and ProgramConfig instructions 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. --- Cargo.lock | 43 ++++- Cargo.toml | 5 + Makefile | 4 +- smartcontract/Makefile | 5 +- smartcontract/programs/CLAUDE.md | 33 ++++ .../doublezero-geolocation/Cargo.toml | 36 +++++ .../doublezero-geolocation/src/entrypoint.rs | 32 ++++ .../doublezero-geolocation/src/error.rs | 66 ++++++++ .../src/instructions.rs | 69 ++++++++ .../doublezero-geolocation/src/lib.rs | 12 ++ .../doublezero-geolocation/src/pda.rs | 32 ++++ .../src/processors/mod.rs | 56 +++++++ .../src/processors/program_config/init.rs | 102 ++++++++++++ .../src/processors/program_config/mod.rs | 21 +++ .../src/processors/program_config/update.rs | 117 ++++++++++++++ .../doublezero-geolocation/src/seeds.rs | 2 + .../doublezero-geolocation/src/serializer.rs | 75 +++++++++ .../src/state/accounttype.rs | 30 ++++ .../doublezero-geolocation/src/state/mod.rs | 2 + .../src/state/program_config.rs | 150 ++++++++++++++++++ 20 files changed, 883 insertions(+), 9 deletions(-) create mode 100644 smartcontract/programs/CLAUDE.md create mode 100644 smartcontract/programs/doublezero-geolocation/Cargo.toml create mode 100644 smartcontract/programs/doublezero-geolocation/src/entrypoint.rs create mode 100644 smartcontract/programs/doublezero-geolocation/src/error.rs create mode 100644 smartcontract/programs/doublezero-geolocation/src/instructions.rs create mode 100644 smartcontract/programs/doublezero-geolocation/src/lib.rs create mode 100644 smartcontract/programs/doublezero-geolocation/src/pda.rs create mode 100644 smartcontract/programs/doublezero-geolocation/src/processors/mod.rs create mode 100644 smartcontract/programs/doublezero-geolocation/src/processors/program_config/init.rs create mode 100644 smartcontract/programs/doublezero-geolocation/src/processors/program_config/mod.rs create mode 100644 smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs create mode 100644 smartcontract/programs/doublezero-geolocation/src/seeds.rs create mode 100644 smartcontract/programs/doublezero-geolocation/src/serializer.rs create mode 100644 smartcontract/programs/doublezero-geolocation/src/state/accounttype.rs create mode 100644 smartcontract/programs/doublezero-geolocation/src/state/mod.rs create mode 100644 smartcontract/programs/doublezero-geolocation/src/state/program_config.rs diff --git a/Cargo.lock b/Cargo.lock index 3fda7a07f..b75c0ff4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1632,6 +1632,23 @@ dependencies = [ "solana-sdk", ] +[[package]] +name = "doublezero-geolocation" +version = "0.8.10" +dependencies = [ + "borsh 1.5.7", + "doublezero-program-common", + "doublezero-serviceability", + "serde", + "serde_bytes", + "solana-bincode", + "solana-loader-v3-interface 3.0.0", + "solana-program", + "solana-program-test", + "solana-sdk", + "thiserror 2.0.17", +] + [[package]] name = "doublezero-program-common" version = "0.8.10" @@ -5139,7 +5156,7 @@ dependencies = [ "solana-hash", "solana-instruction", "solana-keccak-hasher", - "solana-loader-v3-interface", + "solana-loader-v3-interface 5.0.0", "solana-loader-v4-interface", "solana-log-collector", "solana-measure", @@ -5820,6 +5837,20 @@ dependencies = [ "solana-sdk-ids", ] +[[package]] +name = "solana-loader-v3-interface" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4be76cfa9afd84ca2f35ebc09f0da0f0092935ccdac0595d98447f259538c2" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", +] + [[package]] name = "solana-loader-v3-interface" version = "5.0.0" @@ -5862,7 +5893,7 @@ dependencies = [ "solana-bincode", "solana-bpf-loader-program", "solana-instruction", - "solana-loader-v3-interface", + "solana-loader-v3-interface 5.0.0", "solana-loader-v4-interface", "solana-log-collector", "solana-measure", @@ -6179,7 +6210,7 @@ dependencies = [ "solana-keccak-hasher", "solana-last-restart-slot", "solana-loader-v2-interface", - "solana-loader-v3-interface", + "solana-loader-v3-interface 5.0.0", "solana-loader-v4-interface", "solana-message", "solana-msg", @@ -6715,7 +6746,7 @@ dependencies = [ "solana-instruction", "solana-keypair", "solana-lattice-hash", - "solana-loader-v3-interface", + "solana-loader-v3-interface 5.0.0", "solana-loader-v4-interface", "solana-measure", "solana-message", @@ -7247,7 +7278,7 @@ dependencies = [ "solana-hash", "solana-instruction", "solana-instructions-sysvar", - "solana-loader-v3-interface", + "solana-loader-v3-interface 5.0.0", "solana-loader-v4-interface", "solana-loader-v4-program", "solana-log-collector", @@ -7643,7 +7674,7 @@ dependencies = [ "solana-hash", "solana-instruction", "solana-loader-v2-interface", - "solana-loader-v3-interface", + "solana-loader-v3-interface 5.0.0", "solana-message", "solana-program-option", "solana-pubkey", diff --git a/Cargo.toml b/Cargo.toml index ea5788d76..bd6c60b50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "smartcontract/programs/doublezero-record", "smartcontract/programs/doublezero-serviceability", "smartcontract/programs/doublezero-telemetry", + "smartcontract/programs/doublezero-geolocation", "smartcontract/programs/common", "e2e/docker/ledger/fork-accounts", ] @@ -109,3 +110,7 @@ features = ["no-entrypoint"] [workspace.dependencies.doublezero-telemetry] path = "smartcontract/programs/doublezero-telemetry" features = ["no-entrypoint"] + +[workspace.dependencies.doublezero-geolocation] +path = "smartcontract/programs/doublezero-geolocation" +features = ["no-entrypoint"] diff --git a/Makefile b/Makefile index ebad1e880..b4599a019 100644 --- a/Makefile +++ b/Makefile @@ -79,7 +79,7 @@ rust-build-programs: .PHONY: rust-lint rust-lint: rust-fmt-check @cargo install cargo-hack - cargo hack clippy --workspace --all-targets --exclude doublezero-telemetry --exclude doublezero-serviceability --exclude doublezero-program-common --exclude doublezero-record -- -Dclippy::all -Dwarnings + cargo hack clippy --workspace --all-targets --exclude doublezero-telemetry --exclude doublezero-serviceability --exclude doublezero-program-common --exclude doublezero-record --exclude doublezero-geolocation -- -Dclippy::all -Dwarnings cd smartcontract && $(MAKE) lint-programs .PHONY: rust-fmt @@ -94,7 +94,7 @@ rust-fmt-check: .PHONY: rust-test rust-test: - cargo test --workspace --exclude doublezero-telemetry --exclude doublezero-serviceability --exclude doublezero-program-common --exclude doublezero-record --all-features + cargo test --workspace --exclude doublezero-telemetry --exclude doublezero-serviceability --exclude doublezero-program-common --exclude doublezero-record --exclude doublezero-geolocation --all-features cd smartcontract && $(MAKE) test-programs $(MAKE) rust-program-accounts-compat diff --git a/smartcontract/Makefile b/smartcontract/Makefile index 8926d3011..71fb10393 100644 --- a/smartcontract/Makefile +++ b/smartcontract/Makefile @@ -26,7 +26,8 @@ test-programs: test-sbf -p doublezero-program-common \ -p doublezero-record \ -p doublezero-telemetry \ - -p doublezero-serviceability + -p doublezero-serviceability \ + -p doublezero-geolocation .PHONY: test-sbf test-sbf: @@ -44,6 +45,7 @@ lint-programs: -p doublezero-record \ -p doublezero-telemetry \ -p doublezero-serviceability \ + -p doublezero-geolocation \ $(CLIPPY_FLAGS) .PHONY: build @@ -56,3 +58,4 @@ build-programs: cd programs/doublezero-record && cargo build-sbf cd programs/doublezero-serviceability && cargo build-sbf cd programs/doublezero-telemetry && cargo build-sbf --features $(env) + cd programs/doublezero-geolocation && cargo build-sbf diff --git a/smartcontract/programs/CLAUDE.md b/smartcontract/programs/CLAUDE.md new file mode 100644 index 000000000..a48f151a4 --- /dev/null +++ b/smartcontract/programs/CLAUDE.md @@ -0,0 +1,33 @@ +## Smartcontract Development Best Practices + +### Security + +1. **PDA ownership verification**: Always verify the owner of PDA accounts (both internal PDAs and those from other programs like serviceability) to prevent being tricked into reading an account owned by another program. For serviceability accounts, verify the owner is the serviceability program ID. For your own PDAs, verify the owner is `program_id`. + +2. **System program validation**: Checks for the system program are unnecessary because the system interface builds instructions using the system program as the program ID. If the wrong program is provided, you'll get a revert automatically. + +3. **PDA validation**: When validating PDAs with expected seeds/bumps, you don't need to separately check the account derivation before the PDA validation - the PDA validation itself confirms the derivation is correct. + +### Error Handling + +1. **Simplify error enum conversions**: Use `#[repr(u32)]` on your error enum and implement `From for ProgramError` using `as u32`. This eliminates the need to manually maintain error code mappings when adding new variants. Remove `Custom(u32)` variants unless there's a specific use case. + +2. **Clear error messages**: Error messages should clearly state what condition is expected, not just what failed. For example, use "Cannot delete GeoProbe. reference_count of {n} > 0" instead of "ReferenceCountNotZero" so users understand what needs to be true. + +### Code Organization + +1. **Instruction struct placement**: Place instruction argument structs in the same file where the instruction is implemented, rather than collecting them all in a central `instructions.rs` file. This improves locality and makes it easier to understand what arguments an instruction uses. + +2. **Minimize stored data**: Don't store bump seeds unless the account needs to sign for something. Bump seeds are only needed for CPI signing, not for PDA validation. + +3. **Avoid redundant instruction arguments**: If you're passing an account, don't also pass that account's pubkey as an instruction argument and then check they match. Just use the account's key directly. + +### Serialization + +1. **Prefer standard derives**: Use `BorshDeserialize` when possible instead of implementing custom deserialization. Custom `unpack()` methods that manually match on instruction indices often duplicate what Borsh's derive already provides. + +2. **Use BorshDeserializeIncremental**: For instruction arguments that may gain new optional fields over time, use `BorshDeserializeIncremental` or derive `BorshDeserialize`. + +### Program Upgrades + +1. **Use standard interfaces**: Use `solana-loader-v3-interface` to parse `UpgradeableLoaderState` rather than implementing your own parser. The interface crate provides well-tested, maintained implementations. diff --git a/smartcontract/programs/doublezero-geolocation/Cargo.toml b/smartcontract/programs/doublezero-geolocation/Cargo.toml new file mode 100644 index 000000000..ce9767bca --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "doublezero-geolocation" + +# clippy ignores rust-version.toml +rust-version = "1.84.1" # cargo build-sbf --version + +# Workspace inherited keys +version.workspace = true +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +borsh.workspace = true +serde = { workspace = true, optional = true } +serde_bytes = { workspace = true, optional = true } +solana-bincode = "2.2.1" +solana-loader-v3-interface = { version = "3.0.0", features = ["serde"] } +solana-program.workspace = true +doublezero-program-common.workspace = true +doublezero-serviceability.workspace = true +thiserror.workspace = true + +[dev-dependencies] +solana-sdk.workspace = true +solana-program-test.workspace = true + +[features] +default = [] +no-entrypoint = [] +serde = ["dep:serde", "dep:serde_bytes"] diff --git a/smartcontract/programs/doublezero-geolocation/src/entrypoint.rs b/smartcontract/programs/doublezero-geolocation/src/entrypoint.rs new file mode 100644 index 000000000..3a9b91b50 --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/entrypoint.rs @@ -0,0 +1,32 @@ +use crate::{ + instructions::GeolocationInstruction, + processors::program_config::{ + init::process_init_program_config, update::process_update_program_config, + }, +}; + +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey}; + +#[cfg(not(feature = "no-entrypoint"))] +solana_program::entrypoint!(process_instruction); + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let instruction = GeolocationInstruction::unpack(data)?; + + msg!("Instruction: {:?}", instruction); + + match instruction { + GeolocationInstruction::InitProgramConfig(args) => { + process_init_program_config(program_id, accounts, &args)? + } + GeolocationInstruction::UpdateProgramConfig(args) => { + process_update_program_config(program_id, accounts, &args)? + } + }; + + Ok(()) +} diff --git a/smartcontract/programs/doublezero-geolocation/src/error.rs b/smartcontract/programs/doublezero-geolocation/src/error.rs new file mode 100644 index 000000000..6ed64bd59 --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/error.rs @@ -0,0 +1,66 @@ +use solana_program::program_error::ProgramError; +use thiserror::Error; + +#[derive(Debug, Error, PartialEq, Clone)] +#[repr(u32)] +pub enum GeolocationError { + #[error("Invalid account type")] + InvalidAccountType = 1, + #[error("Not allowed")] + NotAllowed = 2, + #[error("Invalid serviceability program ID")] + InvalidServiceabilityProgramId = 11, + #[error("Unauthorized: payer is not the upgrade authority")] + UnauthorizedInitializer = 17, + #[error("min_compatible_version cannot exceed version")] + InvalidMinCompatibleVersion = 18, +} + +impl From for ProgramError { + fn from(e: GeolocationError) -> Self { + ProgramError::Custom(e as u32) + } +} + +pub trait Validate { + fn validate(&self) -> Result<(), GeolocationError>; +} + +#[cfg(test)] +mod tests { + use super::*; + + fn all_variants() -> Vec<(GeolocationError, u32)> { + vec![ + (GeolocationError::InvalidAccountType, 1), + (GeolocationError::NotAllowed, 2), + (GeolocationError::InvalidServiceabilityProgramId, 11), + (GeolocationError::UnauthorizedInitializer, 17), + (GeolocationError::InvalidMinCompatibleVersion, 18), + ] + } + + #[test] + fn test_error_codes() { + for (variant, expected_code) in all_variants() { + let program_error: ProgramError = variant.clone().into(); + let ProgramError::Custom(code) = program_error else { + panic!("expected ProgramError::Custom for {:?}", variant); + }; + assert_eq!( + code, expected_code, + "variant {:?} should map to code {}", + variant, expected_code + ); + } + } + + #[test] + fn test_error_display_messages() { + assert_eq!( + GeolocationError::InvalidAccountType.to_string(), + "Invalid account type" + ); + assert_eq!(GeolocationError::NotAllowed.to_string(), "Not allowed"); + } +} diff --git a/smartcontract/programs/doublezero-geolocation/src/instructions.rs b/smartcontract/programs/doublezero-geolocation/src/instructions.rs new file mode 100644 index 000000000..ad9038655 --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/instructions.rs @@ -0,0 +1,69 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::program_error::ProgramError; + +pub use crate::processors::program_config::{ + init::InitProgramConfigArgs, update::UpdateProgramConfigArgs, +}; + +#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Clone)] +pub enum GeolocationInstruction { + InitProgramConfig(InitProgramConfigArgs), + UpdateProgramConfig(UpdateProgramConfigArgs), +} + +impl GeolocationInstruction { + pub fn pack(&self) -> Vec { + borsh::to_vec(&self).unwrap() + } + + pub fn unpack(data: &[u8]) -> Result { + borsh::from_slice(data).map_err(|_| ProgramError::InvalidInstructionData) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use solana_program::pubkey::Pubkey; + + fn test_instruction(instruction: GeolocationInstruction) { + let packed = instruction.pack(); + let unpacked = GeolocationInstruction::unpack(&packed).unwrap(); + assert_eq!(instruction, unpacked, "Instruction mismatch"); + } + + #[test] + fn test_pack_unpack_all_instructions() { + test_instruction(GeolocationInstruction::InitProgramConfig( + InitProgramConfigArgs { + serviceability_program_id: Pubkey::new_unique(), + }, + )); + test_instruction(GeolocationInstruction::UpdateProgramConfig( + UpdateProgramConfigArgs { + serviceability_program_id: Some(Pubkey::new_unique()), + version: Some(2), + min_compatible_version: Some(1), + }, + )); + test_instruction(GeolocationInstruction::UpdateProgramConfig( + UpdateProgramConfigArgs { + serviceability_program_id: None, + version: None, + min_compatible_version: None, + }, + )); + } + + #[test] + fn test_unpack_invalid() { + assert_eq!( + GeolocationInstruction::unpack(&[]).unwrap_err(), + ProgramError::InvalidInstructionData, + ); + assert_eq!( + GeolocationInstruction::unpack(&[255]).unwrap_err(), + ProgramError::InvalidInstructionData, + ); + } +} diff --git a/smartcontract/programs/doublezero-geolocation/src/lib.rs b/smartcontract/programs/doublezero-geolocation/src/lib.rs new file mode 100644 index 000000000..bc26ca0e3 --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/lib.rs @@ -0,0 +1,12 @@ +#![allow(unexpected_cfgs)] + +#[cfg(any(not(feature = "no-entrypoint"), test))] +pub mod entrypoint; + +pub mod error; +pub mod instructions; +pub mod pda; +pub mod processors; +pub mod seeds; +mod serializer; +pub mod state; diff --git a/smartcontract/programs/doublezero-geolocation/src/pda.rs b/smartcontract/programs/doublezero-geolocation/src/pda.rs new file mode 100644 index 000000000..b22cb0c87 --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/pda.rs @@ -0,0 +1,32 @@ +use solana_program::pubkey::Pubkey; + +use crate::seeds::{SEED_PREFIX, SEED_PROGRAM_CONFIG}; + +pub fn get_program_config_pda(program_id: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address(&[SEED_PREFIX, SEED_PROGRAM_CONFIG], program_id) +} + +#[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); + } +} diff --git a/smartcontract/programs/doublezero-geolocation/src/processors/mod.rs b/smartcontract/programs/doublezero-geolocation/src/processors/mod.rs new file mode 100644 index 000000000..f05a476b4 --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/processors/mod.rs @@ -0,0 +1,56 @@ +pub mod program_config; + +use crate::{ + error::GeolocationError, pda::get_program_config_pda, + state::program_config::GeolocationProgramConfig, +}; +use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; + +pub fn check_foundation_allowlist( + program_config_account: &AccountInfo, + serviceability_globalstate_account: &AccountInfo, + payer_account: &AccountInfo, + program_id: &Pubkey, +) -> Result { + if program_config_account.owner != program_id { + msg!("Invalid ProgramConfig Account Owner"); + return Err(ProgramError::IllegalOwner); + } + + // Verify ProgramConfig PDA address + 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); + } + + let program_config = GeolocationProgramConfig::try_from(program_config_account)?; + + if *serviceability_globalstate_account.owner != program_config.serviceability_program_id { + msg!( + "Expected serviceability program: {}, got: {}", + program_config.serviceability_program_id, + serviceability_globalstate_account.owner + ); + return Err(GeolocationError::InvalidServiceabilityProgramId.into()); + } + + // Verify serviceability GlobalState PDA address + let (expected_gs_pda, _) = doublezero_serviceability::pda::get_globalstate_pda( + &program_config.serviceability_program_id, + ); + if serviceability_globalstate_account.key != &expected_gs_pda { + msg!("Invalid Serviceability GlobalState PDA"); + return Err(ProgramError::InvalidSeeds); + } + + let globalstate = doublezero_serviceability::state::globalstate::GlobalState::try_from( + serviceability_globalstate_account, + )?; + + if !globalstate.foundation_allowlist.contains(payer_account.key) { + return Err(GeolocationError::NotAllowed.into()); + } + + Ok(program_config) +} diff --git a/smartcontract/programs/doublezero-geolocation/src/processors/program_config/init.rs b/smartcontract/programs/doublezero-geolocation/src/processors/program_config/init.rs new file mode 100644 index 000000000..73d5bce87 --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/processors/program_config/init.rs @@ -0,0 +1,102 @@ +use crate::{ + error::GeolocationError, + pda::get_program_config_pda, + seeds::{SEED_PREFIX, SEED_PROGRAM_CONFIG}, + serializer::try_acc_create, + state::{accounttype::AccountType, program_config::GeolocationProgramConfig}, +}; +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use super::parse_upgrade_authority; + +#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Clone)] +pub struct InitProgramConfigArgs { + pub serviceability_program_id: Pubkey, +} + +pub fn process_init_program_config( + program_id: &Pubkey, + accounts: &[AccountInfo], + args: &InitProgramConfigArgs, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let program_config_account = next_account_info(accounts_iter)?; + let program_data_account = next_account_info(accounts_iter)?; + let payer_account = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + + if !payer_account.is_signer { + msg!("Payer must be a signer"); + return Err(ProgramError::MissingRequiredSignature); + } + if !program_config_account.is_writable { + msg!("ProgramConfig must be writable"); + return Err(ProgramError::InvalidAccountData); + } + + // Verify the program data account derives from the program id + let expected_program_data = Pubkey::find_program_address( + &[program_id.as_ref()], + &solana_program::bpf_loader_upgradeable::id(), + ) + .0; + if program_data_account.key != &expected_program_data { + msg!("Invalid program data account"); + return Err(ProgramError::InvalidAccountData); + } + + // Verify payer is the program's upgrade authority + let program_data = program_data_account.try_borrow_data()?; + match parse_upgrade_authority(&program_data)? { + Some(authority) if authority == *payer_account.key => {} + Some(authority) => { + msg!( + "Payer {} is not the upgrade authority {}", + payer_account.key, + authority + ); + return Err(GeolocationError::UnauthorizedInitializer.into()); + } + None => { + msg!("Program has no upgrade authority (immutable)"); + return Err(ProgramError::InvalidAccountData); + } + } + + let (expected_pda, bump_seed) = get_program_config_pda(program_id); + if program_config_account.key != &expected_pda { + msg!("Invalid ProgramConfig Pubkey"); + return Err(ProgramError::InvalidSeeds); + } + + if !program_config_account.data_is_empty() { + return Err(ProgramError::AccountAlreadyInitialized); + } + + let program_config = GeolocationProgramConfig { + account_type: AccountType::ProgramConfig, + bump_seed, + version: 1, + min_compatible_version: 1, + serviceability_program_id: args.serviceability_program_id, + }; + + try_acc_create( + &program_config, + program_config_account, + payer_account, + system_program, + program_id, + &[SEED_PREFIX, SEED_PROGRAM_CONFIG, &[bump_seed]], + )?; + + Ok(()) +} diff --git a/smartcontract/programs/doublezero-geolocation/src/processors/program_config/mod.rs b/smartcontract/programs/doublezero-geolocation/src/processors/program_config/mod.rs new file mode 100644 index 000000000..7fcaecf68 --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/processors/program_config/mod.rs @@ -0,0 +1,21 @@ +pub mod init; +pub mod update; + +use solana_bincode::limited_deserialize; +use solana_loader_v3_interface::state::UpgradeableLoaderState; +use solana_program::{program_error::ProgramError, pubkey::Pubkey}; + +pub(crate) fn parse_upgrade_authority(data: &[u8]) -> Result, ProgramError> { + let state: UpgradeableLoaderState = limited_deserialize( + data, + UpgradeableLoaderState::size_of_programdata_metadata() as u64, + ) + .map_err(|_| ProgramError::InvalidAccountData)?; + match state { + UpgradeableLoaderState::ProgramData { + upgrade_authority_address, + .. + } => Ok(upgrade_authority_address), + _ => Err(ProgramError::InvalidAccountData), + } +} diff --git a/smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs b/smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs new file mode 100644 index 000000000..7a2cc23ee --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs @@ -0,0 +1,117 @@ +use crate::{ + error::GeolocationError, pda::get_program_config_pda, serializer::try_acc_write, + state::program_config::GeolocationProgramConfig, +}; +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use super::parse_upgrade_authority; + +#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Clone)] +pub struct UpdateProgramConfigArgs { + pub serviceability_program_id: Option, + pub version: Option, + pub min_compatible_version: Option, +} + +pub fn process_update_program_config( + program_id: &Pubkey, + accounts: &[AccountInfo], + args: &UpdateProgramConfigArgs, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let program_config_account = next_account_info(accounts_iter)?; + let program_data_account = next_account_info(accounts_iter)?; + let payer_account = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + + if !payer_account.is_signer { + msg!("Payer must be a signer"); + return Err(ProgramError::MissingRequiredSignature); + } + if system_program.key != &solana_program::system_program::id() { + msg!("Invalid System Program account"); + return Err(ProgramError::IncorrectProgramId); + } + if !program_config_account.is_writable { + msg!("ProgramConfig must be writable"); + return Err(ProgramError::InvalidAccountData); + } + if program_config_account.owner != program_id { + msg!("Invalid ProgramConfig Account Owner"); + return Err(ProgramError::IllegalOwner); + } + + // Verify the program data account is owned by the BPF Upgradeable Loader + if program_data_account.owner != &solana_program::bpf_loader_upgradeable::id() { + msg!("Program data account not owned by BPF Upgradeable Loader"); + return Err(ProgramError::IllegalOwner); + } + + // Verify the program data account derives from the program id + let expected_program_data = Pubkey::find_program_address( + &[program_id.as_ref()], + &solana_program::bpf_loader_upgradeable::id(), + ) + .0; + if program_data_account.key != &expected_program_data { + msg!("Invalid program data account"); + return Err(ProgramError::InvalidAccountData); + } + + // Verify payer is the program's upgrade authority + let program_data = program_data_account.try_borrow_data()?; + match parse_upgrade_authority(&program_data)? { + Some(authority) if authority == *payer_account.key => {} + Some(authority) => { + msg!( + "Payer {} is not the upgrade authority {}", + payer_account.key, + authority + ); + return Err(GeolocationError::UnauthorizedInitializer.into()); + } + None => { + msg!("Program has no upgrade authority (immutable)"); + return Err(ProgramError::InvalidAccountData); + } + } + drop(program_data); + + let (expected_pda, _) = get_program_config_pda(program_id); + if program_config_account.key != &expected_pda { + msg!("Invalid ProgramConfig Pubkey"); + return Err(ProgramError::InvalidSeeds); + } + + let mut program_config = GeolocationProgramConfig::try_from(program_config_account)?; + + if let Some(serviceability_program_id) = args.serviceability_program_id { + program_config.serviceability_program_id = serviceability_program_id; + } + if let Some(version) = args.version { + program_config.version = version; + } + if let Some(min_compatible_version) = args.min_compatible_version { + if min_compatible_version > program_config.version { + return Err(GeolocationError::InvalidMinCompatibleVersion.into()); + } + program_config.min_compatible_version = min_compatible_version; + } + + try_acc_write( + &program_config, + program_config_account, + payer_account, + accounts, + )?; + + Ok(()) +} diff --git a/smartcontract/programs/doublezero-geolocation/src/seeds.rs b/smartcontract/programs/doublezero-geolocation/src/seeds.rs new file mode 100644 index 000000000..4fc9f6f0f --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/seeds.rs @@ -0,0 +1,2 @@ +pub const SEED_PREFIX: &[u8] = b"doublezero"; +pub const SEED_PROGRAM_CONFIG: &[u8] = b"programconfig"; diff --git a/smartcontract/programs/doublezero-geolocation/src/serializer.rs b/smartcontract/programs/doublezero-geolocation/src/serializer.rs new file mode 100644 index 000000000..2f41902bd --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/serializer.rs @@ -0,0 +1,75 @@ +use crate::error::Validate; +use borsh::BorshSerialize; +use doublezero_program_common::{ + create_account::try_create_account, resize_account::resize_account_if_needed, +}; +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; + +#[cfg(test)] +use solana_program::msg; + +pub fn try_acc_create<'a, T>( + value: &T, + account: &AccountInfo<'a>, + payer_account: &AccountInfo<'a>, + system_program: &AccountInfo<'a>, + program_id: &Pubkey, + new_account_signer_seeds: &[&[u8]], +) -> ProgramResult +where + T: BorshSerialize + Validate + std::fmt::Debug, +{ + value.validate()?; + + let account_space = borsh::object_length(value)?; + + #[cfg(test)] + { + use solana_sdk::{rent::Rent, sysvar::Sysvar}; + + let rent = Rent::get().expect("Unable to get rent"); + let required_lamports = rent.minimum_balance(account_space); + msg!("Rent: {}", required_lamports); + } + + try_create_account( + payer_account.key, + account.key, + account.lamports(), + account_space, + program_id, + &[ + account.clone(), + payer_account.clone(), + system_program.clone(), + ], + new_account_signer_seeds, + )?; + + let mut account_data = &mut account.data.borrow_mut()[..]; + value.serialize(&mut account_data)?; + + #[cfg(test)] + msg!("Created: {:?}", value); + + Ok(()) +} + +pub fn try_acc_write( + value: &T, + account: &AccountInfo, + payer: &AccountInfo, + accounts: &[AccountInfo], +) -> ProgramResult +where + T: Validate + borsh::BorshSerialize, +{ + value.validate()?; + + resize_account_if_needed(account, payer, accounts, borsh::object_length(value)?)?; + + let mut data = &mut account.data.borrow_mut()[..]; + value.serialize(&mut data)?; + + Ok(()) +} diff --git a/smartcontract/programs/doublezero-geolocation/src/state/accounttype.rs b/smartcontract/programs/doublezero-geolocation/src/state/accounttype.rs new file mode 100644 index 000000000..8944cf77b --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/state/accounttype.rs @@ -0,0 +1,30 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use std::fmt; + +#[repr(u8)] +#[derive(BorshSerialize, BorshDeserialize, Debug, Default, Copy, Clone, PartialEq)] +#[borsh(use_discriminant = true)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum AccountType { + #[default] + None = 0, + ProgramConfig = 1, +} + +impl From for AccountType { + fn from(value: u8) -> Self { + match value { + 1 => AccountType::ProgramConfig, + _ => AccountType::None, + } + } +} + +impl fmt::Display for AccountType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AccountType::None => write!(f, "none"), + AccountType::ProgramConfig => write!(f, "programconfig"), + } + } +} diff --git a/smartcontract/programs/doublezero-geolocation/src/state/mod.rs b/smartcontract/programs/doublezero-geolocation/src/state/mod.rs new file mode 100644 index 000000000..58fc57bcb --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/state/mod.rs @@ -0,0 +1,2 @@ +pub mod accounttype; +pub mod program_config; diff --git a/smartcontract/programs/doublezero-geolocation/src/state/program_config.rs b/smartcontract/programs/doublezero-geolocation/src/state/program_config.rs new file mode 100644 index 000000000..aad70289a --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/state/program_config.rs @@ -0,0 +1,150 @@ +use crate::{ + error::{GeolocationError, Validate}, + state::accounttype::AccountType, +}; +use borsh::{BorshDeserialize, BorshSerialize}; +use core::fmt; +use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; + +#[derive(BorshSerialize, Debug, PartialEq, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct GeolocationProgramConfig { + pub account_type: AccountType, // 1 + pub bump_seed: u8, // 1 + pub version: u32, // 4 + pub min_compatible_version: u32, // 4 + #[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 serviceability_program_id: Pubkey, // 32 +} + +impl fmt::Display for GeolocationProgramConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "account_type: {}, bump_seed: {}, version: {}, min_compatible_version: {}, serviceability_program_id: {}", + self.account_type, self.bump_seed, self.version, self.min_compatible_version, self.serviceability_program_id, + ) + } +} + +impl TryFrom<&[u8]> for GeolocationProgramConfig { + type Error = ProgramError; + + fn try_from(mut data: &[u8]) -> Result { + 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(), + serviceability_program_id: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), + }; + + if out.account_type != AccountType::ProgramConfig { + return Err(ProgramError::InvalidAccountData); + } + + Ok(out) + } +} + +impl TryFrom<&AccountInfo<'_>> for GeolocationProgramConfig { + 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 GeolocationProgramConfig: {:?}", + res.as_ref().err() + ); + } + res + } +} + +impl Validate for GeolocationProgramConfig { + fn validate(&self) -> Result<(), GeolocationError> { + if self.account_type != AccountType::ProgramConfig { + msg!("Invalid account type: {}", self.account_type); + return Err(GeolocationError::InvalidAccountType); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_state_programconfig_try_from_defaults() { + let data = [AccountType::ProgramConfig as u8]; + let val = GeolocationProgramConfig::try_from(&data[..]).unwrap(); + + assert_eq!(val.version, 0); + assert_eq!(val.min_compatible_version, 0); + assert_eq!(val.serviceability_program_id, Pubkey::default()); + } + + #[test] + fn test_state_programconfig_serialization() { + let val = GeolocationProgramConfig { + account_type: AccountType::ProgramConfig, + bump_seed: 1, + version: 3, + min_compatible_version: 1, + serviceability_program_id: Pubkey::new_unique(), + }; + + let data = borsh::to_vec(&val).unwrap(); + let val2 = GeolocationProgramConfig::try_from(&data[..]).unwrap(); + + val.validate().unwrap(); + val2.validate().unwrap(); + + assert_eq!( + borsh::object_length(&val).unwrap(), + borsh::object_length(&val2).unwrap() + ); + assert_eq!(val.version, val2.version); + assert_eq!(val.min_compatible_version, val2.min_compatible_version); + assert_eq!( + val.serviceability_program_id, + val2.serviceability_program_id + ); + assert_eq!( + data.len(), + borsh::object_length(&val).unwrap(), + "Invalid Size" + ); + } + + #[test] + fn test_state_programconfig_validate_error_invalid_account_type() { + let val = GeolocationProgramConfig { + account_type: AccountType::None, + bump_seed: 1, + version: 3, + min_compatible_version: 1, + serviceability_program_id: Pubkey::new_unique(), + }; + let err = val.validate(); + assert!(err.is_err()); + assert_eq!(err.unwrap_err(), GeolocationError::InvalidAccountType); + } + + #[test] + 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()); + assert_eq!(result.unwrap_err(), ProgramError::InvalidAccountData); + } +} From 218a4934a45d472b188d4fe431b3f8eb181bbbff Mon Sep 17 00:00:00 2001 From: nikw9944 Date: Mon, 23 Feb 2026 23:19:01 +0000 Subject: [PATCH 2/4] geolocation: fix version downgrade bypassing min_compatible_version check 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. --- CHANGELOG.md | 2 + .../src/processors/program_config/update.rs | 6 +- .../tests/update_program_config_test.rs | 240 ++++++++++++++++++ 3 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 smartcontract/programs/doublezero-geolocation/tests/update_program_config_test.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index cd6f38778..1b3f6ab2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ All notable changes to this project will be documented in this file. - Serviceability: add feature flags support - Serviceability: expand `is_global` to reject all BGP martian address ranges (CGNAT 100.64/10, IETF 192.0.0/24, benchmarking 198.18/15, multicast 224/4, reserved 240/4, 0/8) - Serviceability: allow update and deletion of interfaces even when sibling interfaces have invalid CYOA configuration + - Geolocation: add `doublezero-geolocation` program scaffolding as per rfcs/rfc16-geolocation-verification.md +- SDK - SetFeatureFlagCommand added to manage on-chain feature flags for conditional behavior rollouts - Dependencies - Upgrade Solana SDK workspace dependencies from 2.2.7 to 2.3.x (`solana-sdk`, `solana-client`, `solana-program-test`, and others) diff --git a/smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs b/smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs index 7a2cc23ee..1cd61a6a7 100644 --- a/smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs +++ b/smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs @@ -100,11 +100,11 @@ pub fn process_update_program_config( program_config.version = version; } if let Some(min_compatible_version) = args.min_compatible_version { - if min_compatible_version > program_config.version { - return Err(GeolocationError::InvalidMinCompatibleVersion.into()); - } program_config.min_compatible_version = min_compatible_version; } + if program_config.min_compatible_version > program_config.version { + return Err(GeolocationError::InvalidMinCompatibleVersion.into()); + } try_acc_write( &program_config, diff --git a/smartcontract/programs/doublezero-geolocation/tests/update_program_config_test.rs b/smartcontract/programs/doublezero-geolocation/tests/update_program_config_test.rs new file mode 100644 index 000000000..61b6d5c91 --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/tests/update_program_config_test.rs @@ -0,0 +1,240 @@ +use doublezero_geolocation::{ + entrypoint::process_instruction, + instructions::GeolocationInstruction, + pda::get_program_config_pda, + processors::program_config::{init::InitProgramConfigArgs, update::UpdateProgramConfigArgs}, + state::program_config::GeolocationProgramConfig, +}; +#[allow(deprecated)] +use solana_program::bpf_loader_upgradeable; +use solana_program_test::*; +use solana_sdk::{ + account::AccountSharedData, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::Signer, + transaction::Transaction, +}; + +/// Builds a bincode-serialized UpgradeableLoaderState::ProgramData account +/// with the given upgrade authority. This is needed because ProgramTest with +/// processor!() deploys programs as builtins without creating the BPF +/// upgradeable loader program_data account that the geolocation program +/// requires for upgrade-authority verification. +fn build_program_data_account(upgrade_authority: &Pubkey) -> AccountSharedData { + // bincode layout of UpgradeableLoaderState::ProgramData: + // u32 discriminant = 3 (ProgramData variant) + // u64 slot = 0 + // u8 option tag = 1 (Some) + // [u8; 32] pubkey + 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()); + + let mut account = + AccountSharedData::new(1_000_000_000, data.len(), &bpf_loader_upgradeable::id()); + account.set_data_from_slice(&data); + account +} + +fn build_accounts(program_id: &Pubkey, payer: &Pubkey) -> Vec { + let (program_config_pda, _) = get_program_config_pda(program_id); + let (program_data_pda, _) = + Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader_upgradeable::id()); + + vec![ + AccountMeta::new(program_config_pda, false), + AccountMeta::new_readonly(program_data_pda, false), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + ] +} + +fn build_instruction( + program_id: &Pubkey, + instruction: &GeolocationInstruction, + accounts: Vec, +) -> Instruction { + Instruction::new_with_bytes(*program_id, &borsh::to_vec(instruction).unwrap(), accounts) +} + +async fn read_program_config( + banks_client: &mut BanksClient, + program_id: &Pubkey, +) -> GeolocationProgramConfig { + let (pda, _) = get_program_config_pda(program_id); + let account = banks_client + .get_account(pda) + .await + .unwrap() + .expect("ProgramConfig account must exist"); + GeolocationProgramConfig::try_from(&account.data[..]).unwrap() +} + +async fn setup() -> (BanksClient, solana_sdk::signature::Keypair, Pubkey) { + let program_id = Pubkey::new_unique(); + let program_test = ProgramTest::new( + "doublezero_geolocation", + program_id, + processor!(process_instruction), + ); + let mut context = program_test.start_with_context().await; + + // Inject the program_data account that the upgrade-authority check expects. + let (program_data_pda, _) = + Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader_upgradeable::id()); + let program_data_account = build_program_data_account(&context.payer.pubkey()); + context.set_account(&program_data_pda, &program_data_account); + + (context.banks_client, context.payer, program_id) +} + +async fn init_program_config( + banks_client: &mut BanksClient, + payer: &solana_sdk::signature::Keypair, + program_id: &Pubkey, +) { + let accounts = build_accounts(program_id, &payer.pubkey()); + let ix = build_instruction( + program_id, + &GeolocationInstruction::InitProgramConfig(InitProgramConfigArgs { + serviceability_program_id: Pubkey::new_unique(), + }), + accounts, + ); + let blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let mut tx = Transaction::new_with_payer(&[ix], Some(&payer.pubkey())); + tx.sign(&[payer], blockhash); + banks_client.process_transaction(tx).await.unwrap(); +} + +async fn send_update( + banks_client: &mut BanksClient, + payer: &solana_sdk::signature::Keypair, + program_id: &Pubkey, + args: UpdateProgramConfigArgs, +) -> Result<(), BanksClientError> { + let accounts = build_accounts(program_id, &payer.pubkey()); + let ix = build_instruction( + program_id, + &GeolocationInstruction::UpdateProgramConfig(args), + accounts, + ); + let blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let mut tx = Transaction::new_with_payer(&[ix], Some(&payer.pubkey())); + tx.sign(&[payer], blockhash); + banks_client.process_transaction(tx).await +} + +#[tokio::test] +async fn test_update_program_config_version_downgrade_below_min_compatible_version() { + let (mut banks_client, payer, program_id) = setup().await; + init_program_config(&mut banks_client, &payer, &program_id).await; + + // Bump to version=5, min_compatible_version=3 + send_update( + &mut banks_client, + &payer, + &program_id, + UpdateProgramConfigArgs { + serviceability_program_id: None, + version: Some(5), + min_compatible_version: Some(3), + }, + ) + .await + .unwrap(); + + let config = read_program_config(&mut banks_client, &program_id).await; + assert_eq!(config.version, 5); + assert_eq!(config.min_compatible_version, 3); + + // Attempt to downgrade version below the existing min_compatible_version + let result = send_update( + &mut banks_client, + &payer, + &program_id, + UpdateProgramConfigArgs { + serviceability_program_id: None, + version: Some(1), + min_compatible_version: None, + }, + ) + .await; + assert!( + result.is_err(), + "downgrading version below min_compatible_version must fail" + ); + + // State must be unchanged + let config = read_program_config(&mut banks_client, &program_id).await; + assert_eq!(config.version, 5); + assert_eq!(config.min_compatible_version, 3); +} + +#[tokio::test] +async fn test_update_program_config_min_compatible_version_exceeds_version() { + let (mut banks_client, payer, program_id) = setup().await; + init_program_config(&mut banks_client, &payer, &program_id).await; + + // min_compatible_version=5 exceeds current version=1 + let result = send_update( + &mut banks_client, + &payer, + &program_id, + UpdateProgramConfigArgs { + serviceability_program_id: None, + version: None, + min_compatible_version: Some(5), + }, + ) + .await; + assert!( + result.is_err(), + "min_compatible_version exceeding version must fail" + ); +} + +#[tokio::test] +async fn test_update_program_config_success() { + let (mut banks_client, payer, program_id) = setup().await; + init_program_config(&mut banks_client, &payer, &program_id).await; + + let config = read_program_config(&mut banks_client, &program_id).await; + assert_eq!(config.version, 1); + assert_eq!(config.min_compatible_version, 1); + + // Update version to 5 + send_update( + &mut banks_client, + &payer, + &program_id, + UpdateProgramConfigArgs { + serviceability_program_id: None, + version: Some(5), + min_compatible_version: None, + }, + ) + .await + .unwrap(); + + // Update min_compatible_version to 3 + send_update( + &mut banks_client, + &payer, + &program_id, + UpdateProgramConfigArgs { + serviceability_program_id: None, + version: None, + min_compatible_version: Some(3), + }, + ) + .await + .unwrap(); + + let config = read_program_config(&mut banks_client, &program_id).await; + assert_eq!(config.version, 5); + assert_eq!(config.min_compatible_version, 3); +} From 6f4761208e88a50dd57f6c82318768d3d2cb0fa2 Mon Sep 17 00:00:00 2001 From: nikw9944 Date: Tue, 24 Feb 2026 01:24:28 +0000 Subject: [PATCH 3/4] geolocation: move serviceability_program_id to build-time constant --- Cargo.lock | 1 + smartcontract/Makefile | 2 +- .../doublezero-geolocation/Cargo.toml | 7 +++ .../programs/doublezero-geolocation/build.rs | 47 +++++++++++++++++++ .../doublezero-geolocation/src/error.rs | 3 -- .../src/instructions.rs | 7 +-- .../doublezero-geolocation/src/lib.rs | 16 +++++++ .../src/processors/mod.rs | 12 ++--- .../src/processors/program_config/init.rs | 7 +-- .../src/processors/program_config/update.rs | 4 -- .../src/state/program_config.rs | 22 ++------- .../tests/update_program_config_test.rs | 9 +--- 12 files changed, 85 insertions(+), 52 deletions(-) create mode 100644 smartcontract/programs/doublezero-geolocation/build.rs diff --git a/Cargo.lock b/Cargo.lock index b75c0ff4e..73d6445de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1637,6 +1637,7 @@ name = "doublezero-geolocation" version = "0.8.10" dependencies = [ "borsh 1.5.7", + "doublezero-config", "doublezero-program-common", "doublezero-serviceability", "serde", diff --git a/smartcontract/Makefile b/smartcontract/Makefile index 71fb10393..70f6e3d07 100644 --- a/smartcontract/Makefile +++ b/smartcontract/Makefile @@ -58,4 +58,4 @@ build-programs: cd programs/doublezero-record && cargo build-sbf cd programs/doublezero-serviceability && cargo build-sbf cd programs/doublezero-telemetry && cargo build-sbf --features $(env) - cd programs/doublezero-geolocation && cargo build-sbf + cd programs/doublezero-geolocation && cargo build-sbf --features $(env) diff --git a/smartcontract/programs/doublezero-geolocation/Cargo.toml b/smartcontract/programs/doublezero-geolocation/Cargo.toml index ce9767bca..403f20ed3 100644 --- a/smartcontract/programs/doublezero-geolocation/Cargo.toml +++ b/smartcontract/programs/doublezero-geolocation/Cargo.toml @@ -30,7 +30,14 @@ thiserror.workspace = true solana-sdk.workspace = true solana-program-test.workspace = true +[build-dependencies] +doublezero-config.workspace = true + [features] default = [] +localnet = [] +testnet = [] +devnet = [] +mainnet-beta = [] no-entrypoint = [] serde = ["dep:serde", "dep:serde_bytes"] diff --git a/smartcontract/programs/doublezero-geolocation/build.rs b/smartcontract/programs/doublezero-geolocation/build.rs new file mode 100644 index 000000000..3d416ec13 --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/build.rs @@ -0,0 +1,47 @@ +use std::{env, fs, path::Path}; + +use doublezero_config::Environment; + +fn main() { + let out_dir = env::var("OUT_DIR").unwrap(); + let dest = Path::new(&out_dir).join("build_constants.rs"); + + // Determine the network environment config with serviceability program ID based on the + // features through cargo environment variables during build, so that we can validate and print + // the values being used at build-time. + // The CARGO_FEATURE_* env variables are set by cargo for build scripts based on the features + // defined in the Cargo.toml file. + let environment: Option = if std::env::var("CARGO_FEATURE_MAINNET_BETA").is_ok() { + Some(Environment::MainnetBeta) + } else if std::env::var("CARGO_FEATURE_TESTNET").is_ok() { + Some(Environment::Testnet) + } else if std::env::var("CARGO_FEATURE_DEVNET").is_ok() { + Some(Environment::Devnet) + } else { + None + }; + + let (env_code, serviceability_program_id) = match environment { + Some(environment) => ( + environment.to_string(), + environment + .config() + .unwrap() + .serviceability_program_id + .to_string(), + ), + None => ( + "localnet".to_string(), + "7CTniUa88iJKUHTrCkB4TjAoG6TD7AMivhQeuqN2LPtX".to_string(), + ), + }; + + println!("cargo:warning=Environment: {env_code}"); + println!("cargo:warning=Serviceability Program ID: {serviceability_program_id}"); + + fs::write( + dest, + format!(r#"pub const SERVICEABILITY_PROGRAM_ID: &str = "{serviceability_program_id}";"#), + ) + .unwrap(); +} diff --git a/smartcontract/programs/doublezero-geolocation/src/error.rs b/smartcontract/programs/doublezero-geolocation/src/error.rs index 6ed64bd59..fd9ebdcbb 100644 --- a/smartcontract/programs/doublezero-geolocation/src/error.rs +++ b/smartcontract/programs/doublezero-geolocation/src/error.rs @@ -8,8 +8,6 @@ pub enum GeolocationError { InvalidAccountType = 1, #[error("Not allowed")] NotAllowed = 2, - #[error("Invalid serviceability program ID")] - InvalidServiceabilityProgramId = 11, #[error("Unauthorized: payer is not the upgrade authority")] UnauthorizedInitializer = 17, #[error("min_compatible_version cannot exceed version")] @@ -34,7 +32,6 @@ mod tests { vec![ (GeolocationError::InvalidAccountType, 1), (GeolocationError::NotAllowed, 2), - (GeolocationError::InvalidServiceabilityProgramId, 11), (GeolocationError::UnauthorizedInitializer, 17), (GeolocationError::InvalidMinCompatibleVersion, 18), ] diff --git a/smartcontract/programs/doublezero-geolocation/src/instructions.rs b/smartcontract/programs/doublezero-geolocation/src/instructions.rs index ad9038655..7874974f9 100644 --- a/smartcontract/programs/doublezero-geolocation/src/instructions.rs +++ b/smartcontract/programs/doublezero-geolocation/src/instructions.rs @@ -24,7 +24,6 @@ impl GeolocationInstruction { #[cfg(test)] mod tests { use super::*; - use solana_program::pubkey::Pubkey; fn test_instruction(instruction: GeolocationInstruction) { let packed = instruction.pack(); @@ -35,20 +34,16 @@ mod tests { #[test] fn test_pack_unpack_all_instructions() { test_instruction(GeolocationInstruction::InitProgramConfig( - InitProgramConfigArgs { - serviceability_program_id: Pubkey::new_unique(), - }, + InitProgramConfigArgs {}, )); test_instruction(GeolocationInstruction::UpdateProgramConfig( UpdateProgramConfigArgs { - serviceability_program_id: Some(Pubkey::new_unique()), version: Some(2), min_compatible_version: Some(1), }, )); test_instruction(GeolocationInstruction::UpdateProgramConfig( UpdateProgramConfigArgs { - serviceability_program_id: None, version: None, min_compatible_version: None, }, diff --git a/smartcontract/programs/doublezero-geolocation/src/lib.rs b/smartcontract/programs/doublezero-geolocation/src/lib.rs index bc26ca0e3..3252a53bf 100644 --- a/smartcontract/programs/doublezero-geolocation/src/lib.rs +++ b/smartcontract/programs/doublezero-geolocation/src/lib.rs @@ -10,3 +10,19 @@ pub mod processors; pub mod seeds; mod serializer; pub mod state; + +use solana_program::pubkey::Pubkey; + +#[cfg(not(test))] +mod build_constants { + include!(concat!(env!("OUT_DIR"), "/build_constants.rs")); +} + +pub const fn serviceability_program_id() -> Pubkey { + Pubkey::from_str_const(crate::build_constants::SERVICEABILITY_PROGRAM_ID) +} + +#[cfg(test)] +mod build_constants { + pub const SERVICEABILITY_PROGRAM_ID: &str = ""; +} diff --git a/smartcontract/programs/doublezero-geolocation/src/processors/mod.rs b/smartcontract/programs/doublezero-geolocation/src/processors/mod.rs index f05a476b4..646daae86 100644 --- a/smartcontract/programs/doublezero-geolocation/src/processors/mod.rs +++ b/smartcontract/programs/doublezero-geolocation/src/processors/mod.rs @@ -26,19 +26,19 @@ pub fn check_foundation_allowlist( let program_config = GeolocationProgramConfig::try_from(program_config_account)?; - if *serviceability_globalstate_account.owner != program_config.serviceability_program_id { + let serviceability_program_id = &crate::serviceability_program_id(); + if serviceability_globalstate_account.owner != serviceability_program_id { msg!( "Expected serviceability program: {}, got: {}", - program_config.serviceability_program_id, + serviceability_program_id, serviceability_globalstate_account.owner ); - return Err(GeolocationError::InvalidServiceabilityProgramId.into()); + return Err(ProgramError::IncorrectProgramId); } // Verify serviceability GlobalState PDA address - let (expected_gs_pda, _) = doublezero_serviceability::pda::get_globalstate_pda( - &program_config.serviceability_program_id, - ); + 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); diff --git a/smartcontract/programs/doublezero-geolocation/src/processors/program_config/init.rs b/smartcontract/programs/doublezero-geolocation/src/processors/program_config/init.rs index 73d5bce87..bcb043699 100644 --- a/smartcontract/programs/doublezero-geolocation/src/processors/program_config/init.rs +++ b/smartcontract/programs/doublezero-geolocation/src/processors/program_config/init.rs @@ -17,14 +17,12 @@ use solana_program::{ use super::parse_upgrade_authority; #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Clone)] -pub struct InitProgramConfigArgs { - pub serviceability_program_id: Pubkey, -} +pub struct InitProgramConfigArgs {} pub fn process_init_program_config( program_id: &Pubkey, accounts: &[AccountInfo], - args: &InitProgramConfigArgs, + _args: &InitProgramConfigArgs, ) -> ProgramResult { let accounts_iter = &mut accounts.iter(); @@ -86,7 +84,6 @@ pub fn process_init_program_config( bump_seed, version: 1, min_compatible_version: 1, - serviceability_program_id: args.serviceability_program_id, }; try_acc_create( diff --git a/smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs b/smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs index 1cd61a6a7..e1eb22d8a 100644 --- a/smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs +++ b/smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs @@ -15,7 +15,6 @@ use super::parse_upgrade_authority; #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Clone)] pub struct UpdateProgramConfigArgs { - pub serviceability_program_id: Option, pub version: Option, pub min_compatible_version: Option, } @@ -93,9 +92,6 @@ pub fn process_update_program_config( let mut program_config = GeolocationProgramConfig::try_from(program_config_account)?; - if let Some(serviceability_program_id) = args.serviceability_program_id { - program_config.serviceability_program_id = serviceability_program_id; - } if let Some(version) = args.version { program_config.version = version; } diff --git a/smartcontract/programs/doublezero-geolocation/src/state/program_config.rs b/smartcontract/programs/doublezero-geolocation/src/state/program_config.rs index aad70289a..ae3d3af0b 100644 --- a/smartcontract/programs/doublezero-geolocation/src/state/program_config.rs +++ b/smartcontract/programs/doublezero-geolocation/src/state/program_config.rs @@ -4,7 +4,7 @@ use crate::{ }; use borsh::{BorshDeserialize, BorshSerialize}; use core::fmt; -use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; +use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError}; #[derive(BorshSerialize, Debug, PartialEq, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -13,22 +13,14 @@ pub struct GeolocationProgramConfig { pub bump_seed: u8, // 1 pub version: u32, // 4 pub min_compatible_version: u32, // 4 - #[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 serviceability_program_id: Pubkey, // 32 } impl fmt::Display for GeolocationProgramConfig { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "account_type: {}, bump_seed: {}, version: {}, min_compatible_version: {}, serviceability_program_id: {}", - self.account_type, self.bump_seed, self.version, self.min_compatible_version, self.serviceability_program_id, + "account_type: {}, bump_seed: {}, version: {}, min_compatible_version: {}", + self.account_type, self.bump_seed, self.version, self.min_compatible_version, ) } } @@ -42,7 +34,6 @@ impl TryFrom<&[u8]> for GeolocationProgramConfig { 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(), - serviceability_program_id: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), }; if out.account_type != AccountType::ProgramConfig { @@ -90,7 +81,6 @@ mod tests { assert_eq!(val.version, 0); assert_eq!(val.min_compatible_version, 0); - assert_eq!(val.serviceability_program_id, Pubkey::default()); } #[test] @@ -100,7 +90,6 @@ mod tests { bump_seed: 1, version: 3, min_compatible_version: 1, - serviceability_program_id: Pubkey::new_unique(), }; let data = borsh::to_vec(&val).unwrap(); @@ -115,10 +104,6 @@ mod tests { ); assert_eq!(val.version, val2.version); assert_eq!(val.min_compatible_version, val2.min_compatible_version); - assert_eq!( - val.serviceability_program_id, - val2.serviceability_program_id - ); assert_eq!( data.len(), borsh::object_length(&val).unwrap(), @@ -133,7 +118,6 @@ mod tests { bump_seed: 1, version: 3, min_compatible_version: 1, - serviceability_program_id: Pubkey::new_unique(), }; let err = val.validate(); assert!(err.is_err()); diff --git a/smartcontract/programs/doublezero-geolocation/tests/update_program_config_test.rs b/smartcontract/programs/doublezero-geolocation/tests/update_program_config_test.rs index 61b6d5c91..88f788821 100644 --- a/smartcontract/programs/doublezero-geolocation/tests/update_program_config_test.rs +++ b/smartcontract/programs/doublezero-geolocation/tests/update_program_config_test.rs @@ -99,9 +99,7 @@ async fn init_program_config( let accounts = build_accounts(program_id, &payer.pubkey()); let ix = build_instruction( program_id, - &GeolocationInstruction::InitProgramConfig(InitProgramConfigArgs { - serviceability_program_id: Pubkey::new_unique(), - }), + &GeolocationInstruction::InitProgramConfig(InitProgramConfigArgs {}), accounts, ); let blockhash = banks_client.get_latest_blockhash().await.unwrap(); @@ -139,7 +137,6 @@ async fn test_update_program_config_version_downgrade_below_min_compatible_versi &payer, &program_id, UpdateProgramConfigArgs { - serviceability_program_id: None, version: Some(5), min_compatible_version: Some(3), }, @@ -157,7 +154,6 @@ async fn test_update_program_config_version_downgrade_below_min_compatible_versi &payer, &program_id, UpdateProgramConfigArgs { - serviceability_program_id: None, version: Some(1), min_compatible_version: None, }, @@ -185,7 +181,6 @@ async fn test_update_program_config_min_compatible_version_exceeds_version() { &payer, &program_id, UpdateProgramConfigArgs { - serviceability_program_id: None, version: None, min_compatible_version: Some(5), }, @@ -212,7 +207,6 @@ async fn test_update_program_config_success() { &payer, &program_id, UpdateProgramConfigArgs { - serviceability_program_id: None, version: Some(5), min_compatible_version: None, }, @@ -226,7 +220,6 @@ async fn test_update_program_config_success() { &payer, &program_id, UpdateProgramConfigArgs { - serviceability_program_id: None, version: None, min_compatible_version: Some(3), }, From 07dd419db2073f12985f5bd6de8c6399ea0ff31c Mon Sep 17 00:00:00 2001 From: nikw9944 Date: Tue, 24 Feb 2026 16:28:23 +0000 Subject: [PATCH 4/4] address feedback --- Cargo.lock | 6 ++- Cargo.toml | 2 + rfcs/rfc16-geolocation-verification.md | 4 +- .../doublezero-geolocation/Cargo.toml | 6 ++- .../doublezero-geolocation/src/entrypoint.rs | 8 ++- .../src/instructions.rs | 54 ++++++------------- .../doublezero-geolocation/src/pda.rs | 25 --------- .../src/processors/mod.rs | 20 +------ .../src/processors/program_config/update.rs | 8 +-- .../src/state/program_config.rs | 54 +++++++------------ .../tests/update_program_config_test.rs | 47 +++++++++------- 11 files changed, 83 insertions(+), 151 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 73d6445de..b7c9355e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1636,7 +1636,9 @@ dependencies = [ name = "doublezero-geolocation" version = "0.8.10" dependencies = [ + "bincode 2.0.1", "borsh 1.5.7", + "borsh-incremental", "doublezero-config", "doublezero-program-common", "doublezero-serviceability", @@ -4855,7 +4857,7 @@ dependencies = [ "solana-epoch-schedule", "solana-fee-calculator", "solana-instruction", - "solana-loader-v3-interface", + "solana-loader-v3-interface 5.0.0", "solana-nonce", "solana-program-option", "solana-program-pack", @@ -6371,7 +6373,7 @@ dependencies = [ "solana-hash", "solana-instruction", "solana-keypair", - "solana-loader-v3-interface", + "solana-loader-v3-interface 5.0.0", "solana-log-collector", "solana-logger", "solana-message", diff --git a/Cargo.toml b/Cargo.toml index bd6c60b50..b0499e10e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,8 @@ serde_yaml = "0" serial_test = "0" solana-account-decoder = "2.3.1" solana-client = "2.3.1" +solana-bincode = "2.2.1" +solana-loader-v3-interface = { version = "3.0.0", features = ["serde"] } solana-program = "2.3.0" solana-program-test = "2.3.1" solana-pubsub-client = "2.3.1" diff --git a/rfcs/rfc16-geolocation-verification.md b/rfcs/rfc16-geolocation-verification.md index 85d6cfccd..a659e32d5 100644 --- a/rfcs/rfc16-geolocation-verification.md +++ b/rfcs/rfc16-geolocation-verification.md @@ -194,10 +194,10 @@ pub struct GeoProbe { pub exchange_pk: Pubkey, // Reference to Serviceability Exchange account pub public_ip: Ipv4Addr, // Where probe listens pub location_offset_port: u16, // UDP listen port (default 8923) - pub code: String, // e.g., "ams-probe-01" (max 32 bytes) - pub parent_devices: Vec, // DZDs that measure this probe pub metrics_publisher_pk: Pubkey, // Signing key for telemetry pub reference_count: u32, // GeolocationTargets referencing this probe + pub code: String, // e.g., "ams-probe-01" (max 32 bytes) + pub parent_devices: Vec, // DZDs that measure this probe } ``` **PDA Seeds:** `["doublezero", "probe", code.as_bytes()]` diff --git a/smartcontract/programs/doublezero-geolocation/Cargo.toml b/smartcontract/programs/doublezero-geolocation/Cargo.toml index 403f20ed3..06e9ca8c3 100644 --- a/smartcontract/programs/doublezero-geolocation/Cargo.toml +++ b/smartcontract/programs/doublezero-geolocation/Cargo.toml @@ -17,16 +17,18 @@ crate-type = ["cdylib", "lib"] [dependencies] borsh.workspace = true +borsh-incremental.workspace = true serde = { workspace = true, optional = true } serde_bytes = { workspace = true, optional = true } -solana-bincode = "2.2.1" -solana-loader-v3-interface = { version = "3.0.0", features = ["serde"] } +solana-bincode.workspace = true +solana-loader-v3-interface.workspace = true solana-program.workspace = true doublezero-program-common.workspace = true doublezero-serviceability.workspace = true thiserror.workspace = true [dev-dependencies] +bincode.workspace = true solana-sdk.workspace = true solana-program-test.workspace = true diff --git a/smartcontract/programs/doublezero-geolocation/src/entrypoint.rs b/smartcontract/programs/doublezero-geolocation/src/entrypoint.rs index 3a9b91b50..d974f1950 100644 --- a/smartcontract/programs/doublezero-geolocation/src/entrypoint.rs +++ b/smartcontract/programs/doublezero-geolocation/src/entrypoint.rs @@ -5,7 +5,10 @@ use crate::{ }, }; -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey}; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, + pubkey::Pubkey, +}; #[cfg(not(feature = "no-entrypoint"))] solana_program::entrypoint!(process_instruction); @@ -15,7 +18,8 @@ pub fn process_instruction( accounts: &[AccountInfo], data: &[u8], ) -> ProgramResult { - let instruction = GeolocationInstruction::unpack(data)?; + let instruction: GeolocationInstruction = + borsh::from_slice(data).map_err(|_| ProgramError::InvalidInstructionData)?; msg!("Instruction: {:?}", instruction); diff --git a/smartcontract/programs/doublezero-geolocation/src/instructions.rs b/smartcontract/programs/doublezero-geolocation/src/instructions.rs index 7874974f9..027cd0d63 100644 --- a/smartcontract/programs/doublezero-geolocation/src/instructions.rs +++ b/smartcontract/programs/doublezero-geolocation/src/instructions.rs @@ -1,5 +1,4 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use solana_program::program_error::ProgramError; pub use crate::processors::program_config::{ init::InitProgramConfigArgs, update::UpdateProgramConfigArgs, @@ -11,54 +10,33 @@ pub enum GeolocationInstruction { UpdateProgramConfig(UpdateProgramConfigArgs), } -impl GeolocationInstruction { - pub fn pack(&self) -> Vec { - borsh::to_vec(&self).unwrap() - } - - pub fn unpack(data: &[u8]) -> Result { - borsh::from_slice(data).map_err(|_| ProgramError::InvalidInstructionData) - } -} - #[cfg(test)] mod tests { use super::*; - fn test_instruction(instruction: GeolocationInstruction) { - let packed = instruction.pack(); - let unpacked = GeolocationInstruction::unpack(&packed).unwrap(); - assert_eq!(instruction, unpacked, "Instruction mismatch"); - } - #[test] - fn test_pack_unpack_all_instructions() { - test_instruction(GeolocationInstruction::InitProgramConfig( - InitProgramConfigArgs {}, - )); - test_instruction(GeolocationInstruction::UpdateProgramConfig( - UpdateProgramConfigArgs { + fn test_roundtrip_all_instructions() { + let cases = vec![ + GeolocationInstruction::InitProgramConfig(InitProgramConfigArgs {}), + GeolocationInstruction::UpdateProgramConfig(UpdateProgramConfigArgs { version: Some(2), min_compatible_version: Some(1), - }, - )); - test_instruction(GeolocationInstruction::UpdateProgramConfig( - UpdateProgramConfigArgs { + }), + GeolocationInstruction::UpdateProgramConfig(UpdateProgramConfigArgs { version: None, min_compatible_version: None, - }, - )); + }), + ]; + for instruction in cases { + let data = borsh::to_vec(&instruction).unwrap(); + let decoded: GeolocationInstruction = borsh::from_slice(&data).unwrap(); + assert_eq!(instruction, decoded); + } } #[test] - fn test_unpack_invalid() { - assert_eq!( - GeolocationInstruction::unpack(&[]).unwrap_err(), - ProgramError::InvalidInstructionData, - ); - assert_eq!( - GeolocationInstruction::unpack(&[255]).unwrap_err(), - ProgramError::InvalidInstructionData, - ); + fn test_deserialize_invalid() { + assert!(borsh::from_slice::(&[]).is_err()); + assert!(borsh::from_slice::(&[255]).is_err()); } } diff --git a/smartcontract/programs/doublezero-geolocation/src/pda.rs b/smartcontract/programs/doublezero-geolocation/src/pda.rs index b22cb0c87..17a90b45d 100644 --- a/smartcontract/programs/doublezero-geolocation/src/pda.rs +++ b/smartcontract/programs/doublezero-geolocation/src/pda.rs @@ -5,28 +5,3 @@ use crate::seeds::{SEED_PREFIX, SEED_PROGRAM_CONFIG}; pub fn get_program_config_pda(program_id: &Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address(&[SEED_PREFIX, SEED_PROGRAM_CONFIG], program_id) } - -#[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); - } -} diff --git a/smartcontract/programs/doublezero-geolocation/src/processors/mod.rs b/smartcontract/programs/doublezero-geolocation/src/processors/mod.rs index 646daae86..0e13e5d9f 100644 --- a/smartcontract/programs/doublezero-geolocation/src/processors/mod.rs +++ b/smartcontract/programs/doublezero-geolocation/src/processors/mod.rs @@ -1,9 +1,6 @@ pub mod program_config; -use crate::{ - error::GeolocationError, pda::get_program_config_pda, - state::program_config::GeolocationProgramConfig, -}; +use crate::{error::GeolocationError, state::program_config::GeolocationProgramConfig}; use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; pub fn check_foundation_allowlist( @@ -17,13 +14,6 @@ pub fn check_foundation_allowlist( return Err(ProgramError::IllegalOwner); } - // Verify ProgramConfig PDA address - 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); - } - let program_config = GeolocationProgramConfig::try_from(program_config_account)?; let serviceability_program_id = &crate::serviceability_program_id(); @@ -36,14 +26,6 @@ pub fn check_foundation_allowlist( return Err(ProgramError::IncorrectProgramId); } - // 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); - } - let globalstate = doublezero_serviceability::state::globalstate::GlobalState::try_from( serviceability_globalstate_account, )?; diff --git a/smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs b/smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs index e1eb22d8a..a3143a1a5 100644 --- a/smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs +++ b/smartcontract/programs/doublezero-geolocation/src/processors/program_config/update.rs @@ -1,5 +1,5 @@ use crate::{ - error::GeolocationError, pda::get_program_config_pda, serializer::try_acc_write, + error::GeolocationError, serializer::try_acc_write, state::program_config::GeolocationProgramConfig, }; use borsh::{BorshDeserialize, BorshSerialize}; @@ -84,12 +84,6 @@ pub fn process_update_program_config( } drop(program_data); - let (expected_pda, _) = get_program_config_pda(program_id); - if program_config_account.key != &expected_pda { - msg!("Invalid ProgramConfig Pubkey"); - return Err(ProgramError::InvalidSeeds); - } - let mut program_config = GeolocationProgramConfig::try_from(program_config_account)?; if let Some(version) = args.version { diff --git a/smartcontract/programs/doublezero-geolocation/src/state/program_config.rs b/smartcontract/programs/doublezero-geolocation/src/state/program_config.rs index ae3d3af0b..8cd3180ff 100644 --- a/smartcontract/programs/doublezero-geolocation/src/state/program_config.rs +++ b/smartcontract/programs/doublezero-geolocation/src/state/program_config.rs @@ -2,11 +2,12 @@ use crate::{ error::{GeolocationError, Validate}, state::accounttype::AccountType, }; -use borsh::{BorshDeserialize, BorshSerialize}; +use borsh::BorshSerialize; +use borsh_incremental::BorshDeserializeIncremental; use core::fmt; use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError}; -#[derive(BorshSerialize, Debug, PartialEq, Clone)] +#[derive(BorshSerialize, BorshDeserializeIncremental, Debug, PartialEq, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct GeolocationProgramConfig { pub account_type: AccountType, // 1 @@ -25,38 +26,20 @@ impl fmt::Display for GeolocationProgramConfig { } } -impl TryFrom<&[u8]> for GeolocationProgramConfig { - type Error = ProgramError; - - fn try_from(mut data: &[u8]) -> Result { - 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(), - }; - - if out.account_type != AccountType::ProgramConfig { - return Err(ProgramError::InvalidAccountData); - } - - Ok(out) - } -} - impl TryFrom<&AccountInfo<'_>> for GeolocationProgramConfig { 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 GeolocationProgramConfig: {:?}", - res.as_ref().err() - ); + let config = Self::try_from(&data[..]).map_err(|e| { + msg!("Failed to deserialize GeolocationProgramConfig: {}", e); + ProgramError::InvalidAccountData + })?; + if config.account_type != AccountType::ProgramConfig { + msg!("Invalid account type: {}", config.account_type); + return Err(ProgramError::InvalidAccountData); } - res + Ok(config) } } @@ -119,16 +102,19 @@ mod tests { version: 3, min_compatible_version: 1, }; - let err = val.validate(); - assert!(err.is_err()); - assert_eq!(err.unwrap_err(), GeolocationError::InvalidAccountType); + assert_eq!( + val.validate().unwrap_err(), + GeolocationError::InvalidAccountType + ); } #[test] 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()); - assert_eq!(result.unwrap_err(), ProgramError::InvalidAccountData); + let val = GeolocationProgramConfig::try_from(&data[..]).unwrap(); + assert_eq!( + val.validate().unwrap_err(), + GeolocationError::InvalidAccountType + ); } } diff --git a/smartcontract/programs/doublezero-geolocation/tests/update_program_config_test.rs b/smartcontract/programs/doublezero-geolocation/tests/update_program_config_test.rs index 88f788821..4cff9e66c 100644 --- a/smartcontract/programs/doublezero-geolocation/tests/update_program_config_test.rs +++ b/smartcontract/programs/doublezero-geolocation/tests/update_program_config_test.rs @@ -1,19 +1,21 @@ use doublezero_geolocation::{ entrypoint::process_instruction, + error::GeolocationError, instructions::GeolocationInstruction, pda::get_program_config_pda, processors::program_config::{init::InitProgramConfigArgs, update::UpdateProgramConfigArgs}, state::program_config::GeolocationProgramConfig, }; +use solana_loader_v3_interface::state::UpgradeableLoaderState; #[allow(deprecated)] use solana_program::bpf_loader_upgradeable; use solana_program_test::*; use solana_sdk::{ account::AccountSharedData, - instruction::{AccountMeta, Instruction}, + instruction::{AccountMeta, Instruction, InstructionError}, pubkey::Pubkey, signature::Signer, - transaction::Transaction, + transaction::{Transaction, TransactionError}, }; /// Builds a bincode-serialized UpgradeableLoaderState::ProgramData account @@ -22,16 +24,11 @@ use solana_sdk::{ /// upgradeable loader program_data account that the geolocation program /// requires for upgrade-authority verification. fn build_program_data_account(upgrade_authority: &Pubkey) -> AccountSharedData { - // bincode layout of UpgradeableLoaderState::ProgramData: - // u32 discriminant = 3 (ProgramData variant) - // u64 slot = 0 - // u8 option tag = 1 (Some) - // [u8; 32] pubkey - 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()); + let state = UpgradeableLoaderState::ProgramData { + slot: 0, + upgrade_authority_address: Some(*upgrade_authority), + }; + let data = bincode::serde::encode_to_vec(state, bincode::config::legacy()).unwrap(); let mut account = AccountSharedData::new(1_000_000_000, data.len(), &bpf_loader_upgradeable::id()); @@ -126,6 +123,22 @@ async fn send_update( banks_client.process_transaction(tx).await } +fn assert_geolocation_error(result: Result<(), BanksClientError>, expected: GeolocationError) { + let expected_code = expected.clone() as u32; + match result { + Ok(_) => panic!("Expected error {:?}, but got Ok", expected), + Err(BanksClientError::TransactionError(TransactionError::InstructionError( + _, + InstructionError::Custom(code), + ))) => assert_eq!( + code, expected_code, + "Expected {:?} ({}), got {}", + expected, expected_code, code + ), + Err(other) => panic!("Expected {:?}, got {:?}", expected, other), + } +} + #[tokio::test] async fn test_update_program_config_version_downgrade_below_min_compatible_version() { let (mut banks_client, payer, program_id) = setup().await; @@ -159,10 +172,7 @@ async fn test_update_program_config_version_downgrade_below_min_compatible_versi }, ) .await; - assert!( - result.is_err(), - "downgrading version below min_compatible_version must fail" - ); + assert_geolocation_error(result, GeolocationError::InvalidMinCompatibleVersion); // State must be unchanged let config = read_program_config(&mut banks_client, &program_id).await; @@ -186,10 +196,7 @@ async fn test_update_program_config_min_compatible_version_exceeds_version() { }, ) .await; - assert!( - result.is_err(), - "min_compatible_version exceeding version must fail" - ); + assert_geolocation_error(result, GeolocationError::InvalidMinCompatibleVersion); } #[tokio::test]