From 30c1e4a2a92bd907f03f895a720e0503a09effe7 Mon Sep 17 00:00:00 2001 From: rhovian Date: Thu, 26 Mar 2026 18:15:43 -0600 Subject: [PATCH] smartcontract: add AdminGroupBits ResourceExtension (#3343) --- .../src/id_allocator.rs | 32 +++++++++++++ .../doublezero-serviceability/src/pda.rs | 11 +++-- .../src/processors/resource/mod.rs | 1 + .../doublezero-serviceability/src/resource.rs | 2 + .../doublezero-serviceability/src/seeds.rs | 1 + .../src/state/resource_extension.rs | 48 +++++++++++++++++++ 6 files changed, 92 insertions(+), 3 deletions(-) diff --git a/smartcontract/programs/doublezero-serviceability/src/id_allocator.rs b/smartcontract/programs/doublezero-serviceability/src/id_allocator.rs index c249244808..15d2ac3f25 100644 --- a/smartcontract/programs/doublezero-serviceability/src/id_allocator.rs +++ b/smartcontract/programs/doublezero-serviceability/src/id_allocator.rs @@ -230,6 +230,38 @@ mod tests { .is_err()); } + #[test] + fn test_admin_group_bits_exhaustion() { + let mut aligned_data = AlignedBitmap([0u8; 8]); + let mut allocator = IdAllocator::new((0, 32)).unwrap(); + + for expected in 0..32 { + let id = allocator.allocate(&mut aligned_data.0); + assert_eq!(id, Some(expected)); + } + assert!( + allocator.allocate(&mut aligned_data.0).is_none(), + "allocation must fail when all 32 bits are exhausted" + ); + } + + #[test] + fn test_admin_group_bits_lowest_available() { + let mut aligned_data = AlignedBitmap([0u8; 8]); + let mut allocator = IdAllocator::new((0, 32)).unwrap(); + + assert_eq!(allocator.allocate(&mut aligned_data.0), Some(0)); + assert_eq!(allocator.allocate(&mut aligned_data.0), Some(1)); + assert_eq!(allocator.allocate(&mut aligned_data.0), Some(2)); + + // Allocate a specific higher bit, then verify next auto-allocate + // still returns the lowest free bit + allocator + .allocate_specific(&mut aligned_data.0, 10) + .unwrap(); + assert_eq!(allocator.allocate(&mut aligned_data.0), Some(3)); + } + #[test] fn test_iter_allocated() { let mut aligned_data = AlignedBitmap([0u8; 8]); diff --git a/smartcontract/programs/doublezero-serviceability/src/pda.rs b/smartcontract/programs/doublezero-serviceability/src/pda.rs index d661aa6ccc..312097a498 100644 --- a/smartcontract/programs/doublezero-serviceability/src/pda.rs +++ b/smartcontract/programs/doublezero-serviceability/src/pda.rs @@ -4,9 +4,9 @@ use solana_program::pubkey::Pubkey; use crate::{ seeds::{ - SEED_ACCESS_PASS, SEED_CONFIG, SEED_CONTRIBUTOR, SEED_DEVICE, SEED_DEVICE_TUNNEL_BLOCK, - SEED_DZ_PREFIX_BLOCK, SEED_EXCHANGE, SEED_GLOBALSTATE, SEED_LINK, SEED_LINK_IDS, - SEED_LOCATION, SEED_MULTICASTGROUP_BLOCK, SEED_MULTICAST_GROUP, + SEED_ACCESS_PASS, SEED_ADMIN_GROUP_BITS, SEED_CONFIG, SEED_CONTRIBUTOR, SEED_DEVICE, + SEED_DEVICE_TUNNEL_BLOCK, SEED_DZ_PREFIX_BLOCK, SEED_EXCHANGE, SEED_GLOBALSTATE, SEED_LINK, + SEED_LINK_IDS, SEED_LOCATION, SEED_MULTICASTGROUP_BLOCK, SEED_MULTICAST_GROUP, SEED_MULTICAST_PUBLISHER_BLOCK, SEED_PERMISSION, SEED_PREFIX, SEED_PROGRAM_CONFIG, SEED_SEGMENT_ROUTING_IDS, SEED_TENANT, SEED_TUNNEL_IDS, SEED_USER, SEED_USER_TUNNEL_BLOCK, SEED_VRF_IDS, @@ -169,5 +169,10 @@ pub fn get_resource_extension_pda( Pubkey::find_program_address(&[SEED_PREFIX, SEED_VRF_IDS], program_id); (pda, bump_seed, SEED_VRF_IDS) } + crate::resource::ResourceType::AdminGroupBits => { + let (pda, bump_seed) = + Pubkey::find_program_address(&[SEED_PREFIX, SEED_ADMIN_GROUP_BITS], program_id); + (pda, bump_seed, SEED_ADMIN_GROUP_BITS) + } } } diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/resource/mod.rs b/smartcontract/programs/doublezero-serviceability/src/processors/resource/mod.rs index 6838ba8c47..77991a96b0 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/resource/mod.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/resource/mod.rs @@ -69,6 +69,7 @@ pub fn get_resource_extension_range( ResourceType::LinkIds => ResourceExtensionRange::IdRange(0, 65535), ResourceType::SegmentRoutingIds => ResourceExtensionRange::IdRange(1, 65535), ResourceType::VrfIds => ResourceExtensionRange::IdRange(1, 1024), + ResourceType::AdminGroupBits => ResourceExtensionRange::IdRange(0, 32), } } diff --git a/smartcontract/programs/doublezero-serviceability/src/resource.rs b/smartcontract/programs/doublezero-serviceability/src/resource.rs index 79de501b0b..9b2bdf95b7 100644 --- a/smartcontract/programs/doublezero-serviceability/src/resource.rs +++ b/smartcontract/programs/doublezero-serviceability/src/resource.rs @@ -15,6 +15,7 @@ pub enum ResourceType { LinkIds, SegmentRoutingIds, VrfIds, + AdminGroupBits, } impl fmt::Display for ResourceType { @@ -29,6 +30,7 @@ impl fmt::Display for ResourceType { ResourceType::LinkIds => write!(f, "LinkIds"), ResourceType::SegmentRoutingIds => write!(f, "SegmentRoutingIds"), ResourceType::VrfIds => write!(f, "VrfIds"), + ResourceType::AdminGroupBits => write!(f, "AdminGroupBits"), } } } diff --git a/smartcontract/programs/doublezero-serviceability/src/seeds.rs b/smartcontract/programs/doublezero-serviceability/src/seeds.rs index ed605fde57..9ed5cdce23 100644 --- a/smartcontract/programs/doublezero-serviceability/src/seeds.rs +++ b/smartcontract/programs/doublezero-serviceability/src/seeds.rs @@ -20,4 +20,5 @@ pub const SEED_TUNNEL_IDS: &[u8] = b"tunnelids"; pub const SEED_LINK_IDS: &[u8] = b"linkids"; pub const SEED_SEGMENT_ROUTING_IDS: &[u8] = b"segmentroutingids"; pub const SEED_VRF_IDS: &[u8] = b"vrfids"; +pub const SEED_ADMIN_GROUP_BITS: &[u8] = b"admingroupbits"; pub const SEED_PERMISSION: &[u8] = b"permission"; diff --git a/smartcontract/programs/doublezero-serviceability/src/state/resource_extension.rs b/smartcontract/programs/doublezero-serviceability/src/state/resource_extension.rs index e0c8a28500..35fed6f8fa 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/resource_extension.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/resource_extension.rs @@ -422,6 +422,54 @@ mod tests { assert_eq!(s, "ResourceExtensionOwned { account_type: ResourceExtension, owner: 11111111111111111111111111111111, bump_seed: 1, associated_with: 11111111111111111111111111111111, allocator: Id(IdAllocator { range: (0, 10), first_free_index: 0 }) }, allocated: []"); } + #[test] + fn test_admin_group_bits_resource_extension() { + let range = ResourceExtensionRange::IdRange(0, 32); + let mut buffer = vec![0u8; ResourceExtensionBorrowed::size(&range)]; + let account_pk = Pubkey::new_unique(); + let owner_pk = Pubkey::new_unique(); + ResourceExtensionBorrowed::construct_resource( + &AccountInfo::new( + &account_pk, + false, + true, + &mut 0, + &mut buffer, + &owner_pk, + false, + 0, + ), + &owner_pk, + 1, + &Pubkey::default(), + &range, + ) + .unwrap(); + + // Allocate first 3 bits + let mut resext = ResourceExtensionBorrowed::inplace_from(&mut buffer[..]).unwrap(); + assert_eq!(resext.allocate(1).unwrap(), IdOrIp::Id(0)); + assert_eq!(resext.allocate(1).unwrap(), IdOrIp::Id(1)); + assert_eq!(resext.allocate(1).unwrap(), IdOrIp::Id(2)); + + // Verify allocated state persists through re-parse + let resext_owned = ResourceExtensionOwned::try_from(&buffer[..]).unwrap(); + assert_eq!( + resext_owned.iter_allocated(), + vec![IdOrIp::Id(0), IdOrIp::Id(1), IdOrIp::Id(2)] + ); + + // Exhaust remaining bits + let mut resext = ResourceExtensionBorrowed::inplace_from(&mut buffer[..]).unwrap(); + for _ in 3..32 { + assert!(resext.allocate(1).is_ok()); + } + assert_eq!( + resext.allocate(1).unwrap_err(), + DoubleZeroError::AllocationFailed + ); + } + #[test] fn test_resource_extension_borrowed_display_trait() { let mut buffer =