Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ 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
- Geolocation: add `doublezero-geolocation` program scaffolding and GeoProbe account type and related instructions as per rfcs/rfc16-geolocation-verification.md
- SDK
- SetFeatureFlagCommand added to manage on-chain feature flags for conditional behavior rollouts
- Dependencies
Expand Down
9 changes: 5 additions & 4 deletions rfcs/rfc16-geolocation-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ A geoProbe assigned to a specific DZD for periodic latency measurement, defined
Outbound Probing Flow
```
┌──────────┐ ┌───────────┐ ┌───────────┐
│ │<─────Reply───────│ │<─────Reply───────│ │
│ DZD │──────TWAMP──────>│ Probe │──────Probe──────>│ Target │
│ │──────TWAMP──────>│ │──────Probe──────>│ │
│ DZD │<─────Reply───────│ Probe │<─────Reply───────│ Target │
│ │──Signed Offset──>│ │──Signed Offset──>│ │
└──────────┘ └───────────┘ w/ references └───────────┘
^ │ ^ │ │
Expand All @@ -117,8 +117,8 @@ IP │ │ Offset Target IPs │ │ Measured Offset │
Inbound Probing Flow
```
┌──────────┐ ┌───────────┐ ┌───────────┐
│ │<─────Reply───────│ │<──Signed Probe───│ │
│ DZD │──────TWAMP──────>│ Probe │───Signed Reply──>│ Target │
│ │──────TWAMP──────>│ │<──Signed Probe───│ │
│ DZD │<─────Reply───────│ Probe │───Signed Reply──>│ Target │
│ │──Signed Offset──>│ │ │ │
└──────────┘ └───────────┘ └───────────┘
^ │ Measured ^ │ │
Expand Down Expand Up @@ -196,6 +196,7 @@ pub struct GeoProbe {
pub location_offset_port: u16, // UDP listen port (default 8923)
pub metrics_publisher_pk: Pubkey, // Signing key for telemetry
pub reference_count: u32, // GeolocationTargets referencing this probe
// Variable-length fields must be at the end for Borsh deserialization
pub code: String, // e.g., "ams-probe-01" (max 32 bytes)
pub parent_devices: Vec<Pubkey>, // DZDs that measure this probe
}
Expand Down
14 changes: 13 additions & 1 deletion smartcontract/programs/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

### 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`.
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`. Exception: for singleton PDAs (e.g., ProgramConfig, GlobalState) the account type discriminator check via `try_from` is sufficient — the ownership check is harmless but redundant since there is only one valid account.

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.

Expand All @@ -28,6 +28,18 @@

2. **Use BorshDeserializeIncremental**: For instruction arguments that may gain new optional fields over time, use `BorshDeserializeIncremental` or derive `BorshDeserialize`.

### Testing

1. **Assert specific errors**: Tests should assert specific error types (e.g., `ProgramError::Custom(17)`), not just `.is_err()`. This catches regressions where the instruction fails for the wrong reason.
Copy link
Contributor

Choose a reason for hiding this comment

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

And this


2. **Don't test framework functionality**: Avoid writing tests that only exercise SDK/framework behavior (e.g., testing that `Pubkey::find_program_address` is deterministic or produces different outputs for different inputs). Focus tests on your program's logic.
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess when the unit tests were generated in this PR, claude forgot about this


3. **Integration tests for all processors**: Every processor function (instruction handler) should have corresponding integration tests in the `tests/` directory. These tests should cover:
- Success cases with valid inputs
- All error cases (invalid inputs, unauthorized signers, wrong account states)
- Edge cases (boundary values, empty collections, maximum sizes)
- State transitions (account creation, updates, deletion)

### 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.
17 changes: 15 additions & 2 deletions smartcontract/programs/doublezero-geolocation/src/entrypoint.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
use crate::{
instructions::GeolocationInstruction,
processors::program_config::{
init::process_init_program_config, update::process_update_program_config,
processors::{
geo_probe::{
create::process_create_geo_probe, delete::process_delete_geo_probe,
update::process_update_geo_probe,
},
program_config::{
init::process_init_program_config, update::process_update_program_config,
},
},
};

Expand Down Expand Up @@ -30,6 +36,13 @@ pub fn process_instruction(
GeolocationInstruction::UpdateProgramConfig(args) => {
process_update_program_config(program_id, accounts, &args)?
}
GeolocationInstruction::CreateGeoProbe(args) => {
process_create_geo_probe(program_id, accounts, &args)?
}
GeolocationInstruction::UpdateGeoProbe(args) => {
process_update_geo_probe(program_id, accounts, &args)?
}
GeolocationInstruction::DeleteGeoProbe => process_delete_geo_probe(program_id, accounts)?,
};

Ok(())
Expand Down
22 changes: 22 additions & 0 deletions smartcontract/programs/doublezero-geolocation/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ pub enum GeolocationError {
InvalidAccountType = 1,
#[error("Not allowed")]
NotAllowed = 2,
#[error("Invalid code length (max 32 bytes)")]
InvalidCodeLength = 4,
#[error("Invalid IP address: not publicly routable")]
InvalidIpAddress = 5,
#[error("Maximum parent devices reached")]
MaxParentDevicesReached = 6,
#[error("Invalid serviceability program ID")]
InvalidServiceabilityProgramId = 11,
#[error("Invalid account code")]
InvalidAccountCode = 12,
#[error("Reference count is not zero")]
ReferenceCountNotZero = 15,
#[error("Unauthorized: payer is not the upgrade authority")]
UnauthorizedInitializer = 17,
#[error("min_compatible_version cannot exceed version")]
Expand All @@ -32,6 +44,12 @@ mod tests {
vec![
(GeolocationError::InvalidAccountType, 1),
(GeolocationError::NotAllowed, 2),
(GeolocationError::InvalidCodeLength, 4),
(GeolocationError::InvalidIpAddress, 5),
(GeolocationError::MaxParentDevicesReached, 6),
(GeolocationError::InvalidServiceabilityProgramId, 11),
(GeolocationError::InvalidAccountCode, 12),
(GeolocationError::ReferenceCountNotZero, 15),
(GeolocationError::UnauthorizedInitializer, 17),
(GeolocationError::InvalidMinCompatibleVersion, 18),
]
Expand Down Expand Up @@ -59,5 +77,9 @@ mod tests {
"Invalid account type"
);
assert_eq!(GeolocationError::NotAllowed.to_string(), "Not allowed");
assert_eq!(
GeolocationError::InvalidIpAddress.to_string(),
"Invalid IP address: not publicly routable"
);
}
}
51 changes: 37 additions & 14 deletions smartcontract/programs/doublezero-geolocation/src/instructions.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,60 @@
use borsh::{BorshDeserialize, BorshSerialize};

pub use crate::processors::program_config::{
init::InitProgramConfigArgs, update::UpdateProgramConfigArgs,
pub use crate::processors::{
geo_probe::{create::CreateGeoProbeArgs, update::UpdateGeoProbeArgs},
program_config::{init::InitProgramConfigArgs, update::UpdateProgramConfigArgs},
};

#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Clone)]
pub enum GeolocationInstruction {
InitProgramConfig(InitProgramConfigArgs),
UpdateProgramConfig(UpdateProgramConfigArgs),
CreateGeoProbe(CreateGeoProbeArgs),
UpdateGeoProbe(UpdateGeoProbeArgs),
DeleteGeoProbe,
}

#[cfg(test)]
mod tests {
use super::*;
use solana_program::pubkey::Pubkey;
use std::net::Ipv4Addr;

fn test_instruction(instruction: GeolocationInstruction) {
let data = borsh::to_vec(&instruction).unwrap();
let decoded: GeolocationInstruction = borsh::from_slice(&data).unwrap();
assert_eq!(instruction, decoded, "Instruction mismatch");
}

#[test]
fn test_roundtrip_all_instructions() {
let cases = vec![
GeolocationInstruction::InitProgramConfig(InitProgramConfigArgs {}),
GeolocationInstruction::UpdateProgramConfig(UpdateProgramConfigArgs {
test_instruction(GeolocationInstruction::InitProgramConfig(
InitProgramConfigArgs {},
));
test_instruction(GeolocationInstruction::UpdateProgramConfig(
UpdateProgramConfigArgs {
version: Some(2),
min_compatible_version: Some(1),
}),
GeolocationInstruction::UpdateProgramConfig(UpdateProgramConfigArgs {
},
));
test_instruction(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_instruction(GeolocationInstruction::CreateGeoProbe(CreateGeoProbeArgs {
code: "test-probe".to_string(),
public_ip: Ipv4Addr::new(8, 8, 8, 8),
location_offset_port: 8923,
metrics_publisher_pk: Pubkey::new_unique(),
}));
test_instruction(GeolocationInstruction::UpdateGeoProbe(UpdateGeoProbeArgs {
public_ip: Some(Ipv4Addr::new(1, 1, 1, 1)),
location_offset_port: Some(9999),
metrics_publisher_pk: None,
}));
test_instruction(GeolocationInstruction::DeleteGeoProbe);
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions smartcontract/programs/doublezero-geolocation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod processors;
pub mod seeds;
mod serializer;
pub mod state;
pub mod validation;

use solana_program::pubkey::Pubkey;

Expand Down
63 changes: 62 additions & 1 deletion smartcontract/programs/doublezero-geolocation/src/pda.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,68 @@
use solana_program::pubkey::Pubkey;

use crate::seeds::{SEED_PREFIX, SEED_PROGRAM_CONFIG};
use crate::seeds::{SEED_PREFIX, SEED_PROBE, 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)
}

pub fn get_geo_probe_pda(program_id: &Pubkey, code: &str) -> (Pubkey, u8) {
Pubkey::find_program_address(&[SEED_PREFIX, SEED_PROBE, code.as_bytes()], 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);
}

#[test]
fn test_geo_probe_pda_is_deterministic() {
let program_id = test_program_id();
let (pda1, bump1) = get_geo_probe_pda(&program_id, "probe-a");
let (pda2, bump2) = get_geo_probe_pda(&program_id, "probe-a");
assert_eq!(pda1, pda2);
assert_eq!(bump1, bump2);
}

#[test]
fn test_geo_probe_pda_differs_by_code() {
let program_id = test_program_id();
let (pda1, _) = get_geo_probe_pda(&program_id, "probe-a");
let (pda2, _) = get_geo_probe_pda(&program_id, "probe-b");
assert_ne!(pda1, pda2);
}

#[test]
fn test_geo_probe_pda_differs_by_program_id() {
let (pda1, _) = get_geo_probe_pda(&Pubkey::new_unique(), "probe-a");
let (pda2, _) = get_geo_probe_pda(&Pubkey::new_unique(), "probe-a");
assert_ne!(pda1, pda2);
}
Comment on lines +21 to +59
Copy link
Contributor

Choose a reason for hiding this comment

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

These tests don't seem necessary (and would argue the one below this comment isn't either, too). What do you think?


#[test]
fn test_different_pda_types_do_not_collide() {
let program_id = test_program_id();
let (config_pda, _) = get_program_config_pda(&program_id);
let (probe_pda, _) = get_geo_probe_pda(&program_id, "code");
assert_ne!(config_pda, probe_pda);
}
}
Loading
Loading