Skip to content
Merged
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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ All notable changes to this project will be documented in this file.

- Client
- Increase default route liveness probe interval (TxMin/RxMin) from 300ms to 1s and raise MaxTxCeil from 1s to 3s to preserve backoff headroom
- Smartcontract
- Serviceability: fix `validate_account_code` forcing lowercase on all entity types — restrict lowercase normalization to device and link codes only, preserving original case for locations, exchanges, contributors, and other entities

## [v0.10.0](https://github.com/malbeclabs/doublezero/compare/client/v0.9.0...client/v0.10.0) - 2026-03-04

Expand Down
8 changes: 4 additions & 4 deletions smartcontract/programs/common/src/validate_account_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pub fn validate_account_code(val: &str) -> Result<String, &'static str> {
val.chars()
.try_fold(String::with_capacity(val.len()), |mut code, char| {
if char.is_alphanumeric() || char == ':' || char == '_' || char == '-' {
code.push(char.to_ascii_lowercase());
code.push(char);
} else {
return Err("name must be alphanumeric, `_`, `-`, or `:` only");
}
Expand All @@ -17,17 +17,17 @@ mod test {
use super::*;

#[test]
fn test_valid_code_lowercased() {
fn test_valid_code() {
let input = "my_device:-01".to_string();
let output = validate_account_code(&input).unwrap();
assert_eq!(output, input);
}

#[test]
fn test_valid_code_mixed_case_normalized() {
fn test_valid_code_preserves_case() {
let input = "My_Device:-01".to_string();
let output = validate_account_code(&input).unwrap();
assert_eq!(output, "my_device:-01".to_string());
assert_eq!(output, input);
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ pub fn process_create_device(
assert!(payer_account.is_signer, "Payer must be a signer");

// Validate and normalize code
let code =
let mut code =
validate_account_code(&value.code).map_err(|_| DoubleZeroError::InvalidAccountCode)?;
code.make_ascii_lowercase();

assert_eq!(
contributor_account.owner, program_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,10 @@ pub fn process_update_device(
}

if let Some(ref code) = value.code {
device.code =
let mut code =
validate_account_code(code).map_err(|_| DoubleZeroError::InvalidAccountCode)?;
code.make_ascii_lowercase();
device.code = code;
}
if let Some(device_type) = value.device_type {
device.device_type = device_type;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ pub fn process_create_link(
assert!(payer_account.is_signer, "Payer must be a signer");

// Validate and normalize code
let code =
let mut code =
validate_account_code(&value.code).map_err(|_| DoubleZeroError::InvalidAccountCode)?;
code.make_ascii_lowercase();

assert_eq!(
contributor_account.owner, program_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,10 @@ pub fn process_update_link(
// can be updated by contributor A
if link.contributor_pk == *contributor_account.key {
if let Some(ref code) = value.code {
link.code =
let mut code =
validate_account_code(code).map_err(|_| DoubleZeroError::InvalidAccountCode)?;
code.make_ascii_lowercase();
link.code = code;
}
if let Some(tunnel_type) = value.tunnel_type {
link.link_type = tunnel_type;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async fn test_contributor() {
recent_blockhash,
program_id,
DoubleZeroInstruction::CreateContributor(ContributorCreateArgs {
code: "la".to_string(),
code: "LA".to_string(),
}),
vec![
AccountMeta::new(contributor_pubkey, false),
Expand All @@ -78,7 +78,7 @@ async fn test_contributor() {
.get_contributor()
.unwrap();
assert_eq!(contributor_la.account_type, AccountType::Contributor);
assert_eq!(contributor_la.code, "la".to_string());
assert_eq!(contributor_la.code, "LA".to_string());
assert_eq!(contributor_la.status, ContributorStatus::Activated);
assert_eq!(contributor_la.ops_manager_pk, Pubkey::default());

Expand Down Expand Up @@ -161,7 +161,7 @@ async fn test_contributor() {
recent_blockhash,
program_id,
DoubleZeroInstruction::UpdateContributor(ContributorUpdateArgs {
code: Some("la2".to_string()),
code: Some("LA2".to_string()),
owner: Some(new_owner),
ops_manager_pk: Some(ops_manager_pk),
}),
Expand All @@ -179,7 +179,7 @@ async fn test_contributor() {
.get_contributor()
.unwrap();
assert_eq!(contributor_la.account_type, AccountType::Contributor);
assert_eq!(contributor_la.code, "la2".to_string());
assert_eq!(contributor_la.code, "LA2".to_string());
assert_eq!(contributor_la.status, ContributorStatus::Activated);
assert_eq!(contributor_la.ops_manager_pk, ops_manager_pk);
assert_eq!(contributor_la.owner, new_owner);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ async fn test_device() {
recent_blockhash,
program_id,
DoubleZeroInstruction::UpdateDevice(DeviceUpdateArgs {
code: Some("la2".to_string()),
code: Some("LA2".to_string()),
device_type: Some(DeviceType::Hybrid),
contributor_pk: None,
public_ip: Some([8, 8, 8, 8].into()), // Global public IP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ async fn test_exchange() {
recent_blockhash,
program_id,
DoubleZeroInstruction::CreateExchange(ExchangeCreateArgs {
code: "la".to_string(),
code: "LA".to_string(),
name: "Los Angeles".to_string(),
lat: 1.234,
lng: 4.567,
Expand All @@ -121,7 +121,7 @@ async fn test_exchange() {
.get_exchange()
.unwrap();
assert_eq!(exchange_la.account_type, AccountType::Exchange);
assert_eq!(exchange_la.code, "la".to_string());
assert_eq!(exchange_la.code, "LA".to_string());
assert_eq!(exchange_la.status, ExchangeStatus::Activated);

println!("✅ Exchange initialized successfully",);
Expand Down Expand Up @@ -201,7 +201,7 @@ async fn test_exchange() {
recent_blockhash,
program_id,
DoubleZeroInstruction::UpdateExchange(ExchangeUpdateArgs {
code: Some("la2".to_string()),
code: Some("LA2".to_string()),
name: Some("Los Angeles - Los Angeles".to_string()),
lat: Some(3.433),
lng: Some(23.223),
Expand All @@ -222,7 +222,7 @@ async fn test_exchange() {
.get_exchange()
.unwrap();
assert_eq!(exchange_la.account_type, AccountType::Exchange);
assert_eq!(exchange_la.code, "la2".to_string());
assert_eq!(exchange_la.code, "LA2".to_string());
assert_eq!(exchange_la.name, "Los Angeles - Los Angeles".to_string());
assert_eq!(exchange_la.status, ExchangeStatus::Activated);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ async fn test_wan_link() {
recent_blockhash,
program_id,
DoubleZeroInstruction::CreateLink(LinkCreateArgs {
code: "la".to_string(),
code: "LA".to_string(),
link_type: LinkLinkType::WAN,
bandwidth: 20000000000,
mtu: 9000,
Expand Down Expand Up @@ -589,7 +589,7 @@ async fn test_wan_link() {
recent_blockhash,
program_id,
DoubleZeroInstruction::UpdateLink(LinkUpdateArgs {
code: Some("la2".to_string()),
code: Some("LA2".to_string()),
contributor_pk: Some(contributor_pubkey),
tunnel_type: Some(LinkLinkType::WAN),
bandwidth: Some(20000000000),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async fn test_location() {
recent_blockhash,
program_id,
DoubleZeroInstruction::CreateLocation(LocationCreateArgs {
code: "la".to_string(),
code: "LA".to_string(),
name: "Los Angeles".to_string(),
country: "us".to_string(),
lat: 1.234,
Expand All @@ -71,7 +71,7 @@ async fn test_location() {
.get_location()
.unwrap();
assert_eq!(location_la.account_type, AccountType::Location);
assert_eq!(location_la.code, "la".to_string());
assert_eq!(location_la.code, "LA".to_string());
assert_eq!(location_la.status, LocationStatus::Activated);

println!("✅ Location initialized successfully",);
Expand Down Expand Up @@ -151,7 +151,7 @@ async fn test_location() {
recent_blockhash,
program_id,
DoubleZeroInstruction::UpdateLocation(LocationUpdateArgs {
code: Some("la2".to_string()),
code: Some("LA2".to_string()),
name: Some("Los Angeles - Los Angeles".to_string()),
country: Some("CA".to_string()),
lat: Some(3.433),
Expand All @@ -172,7 +172,7 @@ async fn test_location() {
.get_location()
.unwrap();
assert_eq!(location_la.account_type, AccountType::Location);
assert_eq!(location_la.code, "la2".to_string());
assert_eq!(location_la.code, "LA2".to_string());
assert_eq!(location_la.name, "Los Angeles - Los Angeles".to_string());
assert_eq!(location_la.status, LocationStatus::Activated);

Expand Down
7 changes: 5 additions & 2 deletions smartcontract/sdk/rs/src/commands/device/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ pub struct CreateDeviceCommand {

impl CreateDeviceCommand {
pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result<(Signature, Pubkey)> {
let code =
let mut code =
validate_account_code(&self.code).map_err(|err| eyre::eyre!("invalid code: {err}"))?;
code.make_ascii_lowercase();

let (globalstate_pubkey, globalstate) = GetGlobalStateCommand
.execute(client)
Expand Down Expand Up @@ -155,8 +156,10 @@ mod tests {
)
.returning(|_, _| Ok(Signature::new_unique()));

// Use mixed-case input to verify SDK lowercases device codes,
// preventing duplicates like "Test_Device" vs "test_device"
let command = CreateDeviceCommand {
code: "test_device".to_string(),
code: "Test_Device".to_string(),
contributor_pk: contributor_pubkey,
location_pk: location_pubkey,
exchange_pk: exchange_pubkey,
Expand Down
11 changes: 9 additions & 2 deletions smartcontract/sdk/rs/src/commands/device/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ impl UpdateDeviceCommand {
let code = self
.code
.as_ref()
.map(|code| validate_account_code(code))
.map(|code| {
validate_account_code(code).map(|mut c| {
c.make_ascii_lowercase();
c
})
})
.transpose()
.map_err(|err| eyre::eyre!("invalid code: {err}"))?;
let (globalstate_pubkey, _globalstate) = GetGlobalStateCommand
Expand Down Expand Up @@ -216,9 +221,11 @@ mod tests {
)
.returning(|_, _| Ok(Signature::new_unique()));

// Use mixed-case input to verify SDK lowercases device codes,
// preventing duplicates like "Test_Device" vs "test_device"
let update_command = UpdateDeviceCommand {
pubkey: device_pubkey,
code: Some("test_device".to_string()),
code: Some("Test_Device".to_string()),
contributor_pk: None,
device_type: Some(DeviceType::Hybrid),
public_ip: None,
Expand Down
3 changes: 2 additions & 1 deletion smartcontract/sdk/rs/src/commands/link/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ pub struct CreateLinkCommand {

impl CreateLinkCommand {
pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result<(Signature, Pubkey)> {
let code =
let mut code =
validate_account_code(&self.code).map_err(|err| eyre::eyre!("invalid code: {err}"))?;
code.make_ascii_lowercase();

let (globalstate_pubkey, globalstate) = GetGlobalStateCommand
.execute(client)
Expand Down
7 changes: 6 additions & 1 deletion smartcontract/sdk/rs/src/commands/link/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ impl UpdateLinkCommand {
let code = self
.code
.as_ref()
.map(|code| validate_account_code(code))
.map(|code| {
validate_account_code(code).map(|mut c| {
c.make_ascii_lowercase();
c
})
})
.transpose()
.map_err(|err| eyre::eyre!("invalid code: {err}"))?;

Expand Down
Loading