From 67c5102fafd97c460f82bc0d6a705505aa926c4b Mon Sep 17 00:00:00 2001 From: Greg Mitchell Date: Tue, 5 May 2026 21:07:47 +0000 Subject: [PATCH 1/2] serviceability: delete migrate_interfaces processor and instruction Remove the `MigrateDeviceInterfaces` instruction (variant 111), its processor, and integration tests. `Device::TryFrom<&[u8]>` (#3665) auto-promotes legacy `interfaces` into `new_interfaces` when the trailing vec is missing, and the next account write persists the promoted vec, so a standalone migrate step is no longer needed. Also rewords two stale `MigrateDeviceInterfaces` doc comments in the Go SDK. Closes #3662 --- CHANGELOG.md | 1 + .../src/entrypoint.rs | 4 - .../src/instructions.rs | 6 - .../processors/device/migrate_interfaces.rs | 285 ------ .../src/processors/device/mod.rs | 1 - .../tests/migrate_interfaces_test.rs | 813 ------------------ .../sdk/go/serviceability/deserialize.go | 5 +- smartcontract/sdk/go/serviceability/state.go | 5 +- 8 files changed, 8 insertions(+), 1112 deletions(-) delete mode 100644 smartcontract/programs/doublezero-serviceability/src/processors/device/migrate_interfaces.rs delete mode 100644 smartcontract/programs/doublezero-serviceability/tests/migrate_interfaces_test.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 836d04ac64..031a44c68a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file. - Add forward-compatible `NewInterface` struct in `state/interface.rs` with a `size: u16` + `version: u8` on-disk prefix, V3-shaped body, and `flex_algo_node_segments`. Older readers can use the size prefix to skip past unknown future versions in constant time. Additive only — no callers, processors, or SDKs change in this PR ([#3666](https://github.com/malbeclabs/doublezero/pull/3666)) - Append `new_interfaces: Vec` to `Device` after `max_multicast_publishers`, behind a custom `BorshSerialize` that projects the on-disk legacy `interfaces` slot from `new_interfaces` (always `Interface::V2` per #3653) and writes `new_interfaces` at the end of the layout. Legacy accounts with no trailing bytes deserialize cleanly: `Device::try_from` rebuilds `new_interfaces` from the legacy enum vec via per-variant `TryFrom`. Older readers continue to parse the legacy slot at its existing offset; newer readers gain forward-compat via the trailing vec. Mutations now go through `Device::replace_interface` / `push_interface` / `remove_interface` so both vecs stay in sync; `find_interface` returns `&NewInterface` and `find_interface_legacy` is a temporary helper for unrelated callers ([#3665](https://github.com/malbeclabs/doublezero/pull/3665)) - Migrate serviceability processors (`device/interface/{create,update,activate,delete,reject,remove,unlink}`, `link/{accept,activate,closeaccount,create,delete,update}`, `topology/backfill`) to read and mutate `Device::new_interfaces` directly. `device.interfaces` is no longer touched in `processors/`, and `Device::push_interface` now takes a `NewInterface`. `BackfillTopology` no longer mirrors `flex_algo_node_segments` into the legacy in-memory `interfaces` vec — segments live only in `new_interfaces` and are intentionally dropped on the V2-projected on-disk legacy slot ([#3658](https://github.com/malbeclabs/doublezero/issues/3658)) + - Delete the `MigrateDeviceInterfaces` instruction (variant 111) and its processor from the serviceability program. `Device::TryFrom<&[u8]>` (#3665) now auto-promotes legacy `interfaces` into `new_interfaces` when the trailing vec is missing, and the next account write persists the promoted vec — no standalone migration step is needed ([#3662](https://github.com/malbeclabs/doublezero/issues/3662)) ## [v0.21.0](https://github.com/malbeclabs/doublezero/compare/client/v0.20.0...client/v0.21.0) - 2026-05-01 diff --git a/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs b/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs index df25307e3c..7baf49edb0 100644 --- a/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs +++ b/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs @@ -32,7 +32,6 @@ use crate::{ reject::process_reject_device_interface, remove::process_remove_device_interface, unlink::process_unlink_device_interface, update::process_update_device_interface, }, - migrate_interfaces::process_migrate_device_interfaces, reject::process_reject_device, sethealth::process_set_health_device, update::process_update_device, @@ -448,9 +447,6 @@ pub fn process_instruction( DoubleZeroInstruction::BackfillTopology(value) => { process_topology_backfill(program_id, accounts, &value)? } - DoubleZeroInstruction::MigrateDeviceInterfaces(value) => { - process_migrate_device_interfaces(program_id, accounts, &value)? - } }; Ok(()) } diff --git a/smartcontract/programs/doublezero-serviceability/src/instructions.rs b/smartcontract/programs/doublezero-serviceability/src/instructions.rs index 3eb8e9fc30..8522948111 100644 --- a/smartcontract/programs/doublezero-serviceability/src/instructions.rs +++ b/smartcontract/programs/doublezero-serviceability/src/instructions.rs @@ -22,7 +22,6 @@ use crate::processors::{ remove::DeviceInterfaceRemoveArgs, unlink::DeviceInterfaceUnlinkArgs, update::DeviceInterfaceUpdateArgs, }, - migrate_interfaces::MigrateDeviceInterfacesArgs, reject::DeviceRejectArgs, sethealth::DeviceSetHealthArgs, update::DeviceUpdateArgs, @@ -234,8 +233,6 @@ pub enum DoubleZeroInstruction { DeleteTopology(TopologyDeleteArgs), // variant 108 ClearTopology(TopologyClearArgs), // variant 109 BackfillTopology(TopologyBackfillArgs), // variant 110 - - MigrateDeviceInterfaces(MigrateDeviceInterfacesArgs), // variant 111 } impl DoubleZeroInstruction { @@ -376,7 +373,6 @@ impl DoubleZeroInstruction { 108 => Ok(Self::DeleteTopology(TopologyDeleteArgs::try_from(rest).unwrap())), 109 => Ok(Self::ClearTopology(TopologyClearArgs::try_from(rest).unwrap())), 110 => Ok(Self::BackfillTopology(TopologyBackfillArgs::try_from(rest).unwrap())), - 111 => Ok(Self::MigrateDeviceInterfaces(MigrateDeviceInterfacesArgs::try_from(rest).unwrap())), _ => Err(ProgramError::InvalidInstructionData), } @@ -520,7 +516,6 @@ impl DoubleZeroInstruction { Self::DeleteTopology(_) => "DeleteTopology".to_string(), // variant 108 Self::ClearTopology(_) => "ClearTopology".to_string(), // variant 109 Self::BackfillTopology(_) => "BackfillTopology".to_string(), // variant 110 - Self::MigrateDeviceInterfaces(_) => "MigrateDeviceInterfaces".to_string(), // variant 111 } } @@ -656,7 +651,6 @@ impl DoubleZeroInstruction { Self::DeleteTopology(args) => format!("{args:?}"), // variant 108 Self::ClearTopology(args) => format!("{args:?}"), // variant 109 Self::BackfillTopology(args) => format!("{args:?}"), // variant 110 - Self::MigrateDeviceInterfaces(args) => format!("{args:?}"), // variant 111 } } } diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/device/migrate_interfaces.rs b/smartcontract/programs/doublezero-serviceability/src/processors/device/migrate_interfaces.rs deleted file mode 100644 index 92dee1ae81..0000000000 --- a/smartcontract/programs/doublezero-serviceability/src/processors/device/migrate_interfaces.rs +++ /dev/null @@ -1,285 +0,0 @@ -use crate::{ - error::DoubleZeroError, - pda::{get_device_pda, get_globalstate_pda}, - processors::validation::validate_program_account, - serializer::try_acc_write, - state::{ - accounttype::AccountType, - device::Device, - globalstate::GlobalState, - interface::{Interface, InterfaceV2, NewInterface}, - }, -}; -use borsh::BorshSerialize; -use borsh_incremental::BorshDeserializeIncremental; -use core::fmt; -#[cfg(test)] -use solana_program::msg; -use solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - program_error::ProgramError, - pubkey::Pubkey, -}; - -#[derive(BorshSerialize, BorshDeserializeIncremental, PartialEq, Clone, Default)] -pub struct MigrateDeviceInterfacesArgs {} - -impl fmt::Debug for MigrateDeviceInterfacesArgs { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "MigrateDeviceInterfacesArgs") - } -} - -/// Deserializes a Device account using the legacy interface format (pre-RFC-18), -/// where Interface discriminant 1 does NOT have trailing flex_algo_node_segments bytes. -/// Also handles the current V3 format (discriminant 3) for idempotency detection. -fn deserialize_device_legacy(data: &[u8]) -> Result { - let mut reader = data; - - let account_type: AccountType = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let owner: solana_program::pubkey::Pubkey = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let index: u128 = borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let bump_seed: u8 = borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let location_pk: solana_program::pubkey::Pubkey = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let exchange_pk: solana_program::pubkey::Pubkey = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let device_type: crate::state::device::DeviceType = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let public_ip: std::net::Ipv4Addr = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or([0, 0, 0, 0].into()); - let status: crate::state::device::DeviceStatus = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let code: String = borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let dz_prefixes: doublezero_program_common::types::NetworkV4List = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let metrics_publisher_pk: solana_program::pubkey::Pubkey = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let contributor_pk: solana_program::pubkey::Pubkey = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let mgmt_vrf: String = borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - - // Read the interfaces vec using the legacy format. - // Borsh encodes Vec as: [u32 length][T...] — we read the count manually, - // then deserialize each interface using the legacy reader. - let iface_count: u32 = borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let mut interfaces: Vec = Vec::with_capacity(iface_count as usize); - for _ in 0..iface_count { - // Read the discriminant byte to determine interface version. - let discriminant: u8 = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let iface = match discriminant { - 0 => { - let v1: crate::state::interface::InterfaceV1 = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - Interface::V1(v1) - } - 1 | 2 => { - // V2 format — no flex_algo_node_segments bytes on disk. - let v2: crate::state::interface::InterfaceV2 = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - Interface::V2(v2) - } - 3 => { - // V3 format — includes flex_algo_node_segments. - let v3: crate::state::interface::InterfaceV3 = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - Interface::V3(v3) - } - _ => Interface::V3(crate::state::interface::InterfaceV3::default()), - }; - interfaces.push(iface); - } - - let reference_count: u32 = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let users_count: u16 = borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let max_users: u16 = borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let device_health: crate::state::device::DeviceHealth = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let desired_status: crate::state::device::DeviceDesiredStatus = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let unicast_users_count: u16 = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let multicast_subscribers_count: u16 = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let max_unicast_users: u16 = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let max_multicast_subscribers: u16 = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let reserved_seats: u16 = borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let multicast_publishers_count: u16 = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - let max_multicast_publishers: u16 = - borsh::BorshDeserialize::deserialize(&mut reader).unwrap_or_default(); - - if account_type != AccountType::Device { - return Err(ProgramError::InvalidAccountData); - } - - // Legacy on-disk format has no trailing `new_interfaces` vec; rebuild it - // from the legacy enum vec via per-variant TryFrom (V3 → V2 → NewInterface). - let new_interfaces = interfaces - .iter() - .map(|iface| -> Result { - match iface { - Interface::V1(v1) => v1.try_into(), - Interface::V2(v2) => v2.try_into(), - Interface::V3(v3) => { - let v2: InterfaceV2 = v3.try_into()?; - (&v2).try_into() - } - } - }) - .collect::, _>>()?; - - Ok(Device { - account_type, - owner, - index, - bump_seed, - location_pk, - exchange_pk, - device_type, - public_ip, - status, - code, - dz_prefixes, - metrics_publisher_pk, - contributor_pk, - mgmt_vrf, - interfaces, - reference_count, - users_count, - max_users, - device_health, - desired_status, - unicast_users_count, - multicast_subscribers_count, - max_unicast_users, - max_multicast_subscribers, - reserved_seats, - multicast_publishers_count, - max_multicast_publishers, - new_interfaces, - }) -} - -/// Migrates a Device account's interfaces from the pre-RFC-18 on-chain format -/// (Interface discriminant 1, no flex_algo_node_segments bytes) to the current -/// V3 format (Interface discriminant 3, with an empty flex_algo_node_segments vec). -/// -/// This instruction is idempotent: calling it on an already-migrated account is -/// a no-op. This is safe for the activator startup sweep, which calls it for all -/// devices without knowing which ones are already in the new format. -/// -/// Accounts expected: -/// 0. `device_account` — writable, owned by this program -/// 1. `globalstate_account` — readable, used for authorization -/// 2. `payer_account` — signer (foundation, device owner, or activator authority) -/// 3. `system_program` — for account resizing -pub fn process_migrate_device_interfaces( - program_id: &Pubkey, - accounts: &[AccountInfo], - _value: &MigrateDeviceInterfacesArgs, -) -> ProgramResult { - let accounts_iter = &mut accounts.iter(); - - let device_account = next_account_info(accounts_iter)?; - let globalstate_account = next_account_info(accounts_iter)?; - let payer_account = next_account_info(accounts_iter)?; - let _system_program = next_account_info(accounts_iter)?; - - #[cfg(test)] - msg!("process_migrate_device_interfaces"); - - if !payer_account.is_signer { - return Err(DoubleZeroError::NotAllowed.into()); - } - - // Owner + non-empty + writable checks before we trust the on-disk bytes. - validate_program_account!(device_account, program_id, writable = true, "Device"); - validate_program_account!( - globalstate_account, - program_id, - writable = false, - pda = &get_globalstate_pda(program_id).0, - "GlobalState" - ); - - let globalstate = GlobalState::try_from(globalstate_account)?; - - // Read the device using the legacy deserializer. The borrow and the data_slice - // derived from it are scoped to this block so the immutable borrow on - // device_account.data is released before try_acc_write takes a mutable borrow. - let (mut device, already_migrated) = { - let data_borrow = device_account.data.borrow(); - let device = deserialize_device_legacy(&data_borrow)?; - // If any interface is already V3, the account has been migrated. - let already_migrated = device - .interfaces - .iter() - .any(|i| matches!(i, Interface::V3(_))); - (device, already_migrated) - }; - - // Now that we have the device index, verify the PDA. - assert_eq!( - device_account.key, - &get_device_pda(program_id, device.index).0, - "Invalid Device PDA" - ); - - // Authorization: payer must be the foundation, the device owner, or the activator - // authority. The activator calls this during its startup sweep. - let is_foundation = globalstate.foundation_allowlist.contains(payer_account.key); - let is_owner = device.owner == *payer_account.key; - let is_activator = globalstate.activator_authority_pk == *payer_account.key; - if !is_foundation && !is_owner && !is_activator { - return Err(DoubleZeroError::NotAllowed.into()); - } - - // Idempotency check: the account already has V3 interfaces — skip migration - // so we don't zero any topology assignments in the flex_algo_node_segments vecs. - if already_migrated { - return Ok(()); - } - - // Convert all V1/V2 interfaces to V3 (adding empty flex_algo_node_segments vec). - let migrated: Vec = device - .interfaces - .iter() - .map(|iface| Interface::V3(iface.into_v3())) - .collect(); - // Mirror the migration into `new_interfaces` so the custom Device serializer - // (which projects the legacy on-disk slot from `new_interfaces`) doesn't drop - // the V3 conversion. V3's `flex_algo_node_segments` is empty after migration, - // so projecting through V2 is lossless here. - let migrated_new: Vec = migrated - .iter() - .map(|iface| -> Result { - match iface { - Interface::V1(v1) => v1.try_into(), - Interface::V2(v2) => v2.try_into(), - Interface::V3(v3) => { - let v2: InterfaceV2 = v3.try_into()?; - (&v2).try_into() - } - } - }) - .collect::, _>>()?; - device.interfaces = migrated; - device.new_interfaces = migrated_new; - - // Write back with the V3 format — each interface now includes the - // (empty) flex_algo_node_segments vec in its serialized form. - try_acc_write(&device, device_account, payer_account, accounts)?; - - #[cfg(test)] - msg!("Migrated device: {}", device.code); - - Ok(()) -} diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/device/mod.rs b/smartcontract/programs/doublezero-serviceability/src/processors/device/mod.rs index 5e7d7cd6b5..f540b5d6d9 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/device/mod.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/device/mod.rs @@ -3,7 +3,6 @@ pub mod closeaccount; pub mod create; pub mod delete; pub mod interface; -pub mod migrate_interfaces; pub mod reject; pub mod sethealth; pub mod update; diff --git a/smartcontract/programs/doublezero-serviceability/tests/migrate_interfaces_test.rs b/smartcontract/programs/doublezero-serviceability/tests/migrate_interfaces_test.rs deleted file mode 100644 index 7af1bdf29d..0000000000 --- a/smartcontract/programs/doublezero-serviceability/tests/migrate_interfaces_test.rs +++ /dev/null @@ -1,813 +0,0 @@ -use borsh::to_vec; -use doublezero_serviceability::{ - instructions::*, - pda::*, - processors::{ - contributor::create::ContributorCreateArgs, - device::{create::*, migrate_interfaces::MigrateDeviceInterfacesArgs}, - globalconfig::set::SetGlobalConfigArgs, - *, - }, - resource::ResourceType, - state::{ - accounttype::AccountType, - contributor::{Contributor, ContributorStatus}, - device::*, - globalstate::GlobalState, - interface::{ - Interface, InterfaceCYOA, InterfaceDIA, InterfaceStatus, InterfaceType, InterfaceV2, - InterfaceV3, LoopbackType, RoutingMode, - }, - }, -}; -use solana_program_test::*; -use solana_sdk::{ - account::Account, - instruction::{AccountMeta, Instruction, InstructionError}, - pubkey::Pubkey, - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, -}; - -mod test_helpers; -use test_helpers::*; - -// --------------------------------------------------------------------------- -// Shared setup: InitGlobalState + SetGlobalConfig + Location + Exchange + -// Contributor + Device. Returns the pubkeys needed for MigrateDeviceInterfaces. -// --------------------------------------------------------------------------- -async fn setup_with_device( - banks_client: &mut BanksClient, - program_id: Pubkey, - payer: &Keypair, -) -> (Pubkey, Pubkey) { - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - - let (program_config_pubkey, _) = get_program_config_pda(&program_id); - let (globalstate_pubkey, _) = get_globalstate_pda(&program_id); - let (config_pubkey, _) = get_globalconfig_pda(&program_id); - let (device_tunnel_block_pda, _, _) = - get_resource_extension_pda(&program_id, ResourceType::DeviceTunnelBlock); - let (user_tunnel_block_pda, _, _) = - get_resource_extension_pda(&program_id, ResourceType::UserTunnelBlock); - let (multicastgroup_block_pda, _, _) = - get_resource_extension_pda(&program_id, ResourceType::MulticastGroupBlock); - let (link_ids_pda, _, _) = get_resource_extension_pda(&program_id, ResourceType::LinkIds); - let (segment_routing_ids_pda, _, _) = - get_resource_extension_pda(&program_id, ResourceType::SegmentRoutingIds); - let (multicast_publisher_block_pda, _, _) = - get_resource_extension_pda(&program_id, ResourceType::MulticastPublisherBlock); - let (vrf_ids_pda, _, _) = get_resource_extension_pda(&program_id, ResourceType::VrfIds); - let (admin_group_bits_pda, _, _) = - get_resource_extension_pda(&program_id, ResourceType::AdminGroupBits); - - execute_transaction( - banks_client, - recent_blockhash, - program_id, - DoubleZeroInstruction::InitGlobalState(), - vec![ - AccountMeta::new(program_config_pubkey, false), - AccountMeta::new(globalstate_pubkey, false), - ], - payer, - ) - .await; - - execute_transaction( - banks_client, - recent_blockhash, - program_id, - DoubleZeroInstruction::SetGlobalConfig(SetGlobalConfigArgs { - local_asn: 65000, - remote_asn: 65001, - device_tunnel_block: "10.0.0.0/24".parse().unwrap(), - user_tunnel_block: "10.0.0.0/24".parse().unwrap(), - multicastgroup_block: "224.0.0.0/16".parse().unwrap(), - multicast_publisher_block: "148.51.120.0/21".parse().unwrap(), - next_bgp_community: None, - }), - vec![ - AccountMeta::new(config_pubkey, false), - AccountMeta::new(globalstate_pubkey, false), - AccountMeta::new(device_tunnel_block_pda, false), - AccountMeta::new(user_tunnel_block_pda, false), - AccountMeta::new(multicastgroup_block_pda, false), - AccountMeta::new(link_ids_pda, false), - AccountMeta::new(segment_routing_ids_pda, false), - AccountMeta::new(multicast_publisher_block_pda, false), - AccountMeta::new(vrf_ids_pda, false), - AccountMeta::new(admin_group_bits_pda, false), - ], - payer, - ) - .await; - - // Location - let gs = get_globalstate(banks_client, globalstate_pubkey).await; - let (location_pubkey, _) = get_location_pda(&program_id, gs.account_index + 1); - execute_transaction( - banks_client, - recent_blockhash, - program_id, - DoubleZeroInstruction::CreateLocation(location::create::LocationCreateArgs { - code: "test".to_string(), - name: "Test Location".to_string(), - country: "us".to_string(), - lat: 0.0, - lng: 0.0, - loc_id: 0, - }), - vec![ - AccountMeta::new(location_pubkey, false), - AccountMeta::new(globalstate_pubkey, false), - ], - payer, - ) - .await; - - // Exchange - let gs = get_globalstate(banks_client, globalstate_pubkey).await; - let (exchange_pubkey, _) = get_exchange_pda(&program_id, gs.account_index + 1); - execute_transaction( - banks_client, - recent_blockhash, - program_id, - DoubleZeroInstruction::CreateExchange(exchange::create::ExchangeCreateArgs { - code: "test".to_string(), - name: "Test Exchange".to_string(), - lat: 0.0, - lng: 0.0, - reserved: 0, - }), - vec![ - AccountMeta::new(exchange_pubkey, false), - AccountMeta::new(config_pubkey, false), - AccountMeta::new(globalstate_pubkey, false), - ], - payer, - ) - .await; - - // Contributor - let gs = get_globalstate(banks_client, globalstate_pubkey).await; - let (contributor_pubkey, _) = get_contributor_pda(&program_id, gs.account_index + 1); - execute_transaction( - banks_client, - recent_blockhash, - program_id, - DoubleZeroInstruction::CreateContributor(ContributorCreateArgs { - code: "testco".to_string(), - }), - vec![ - AccountMeta::new(contributor_pubkey, false), - AccountMeta::new(payer.pubkey(), false), - AccountMeta::new(globalstate_pubkey, false), - ], - payer, - ) - .await; - - // Device - let gs = get_globalstate(banks_client, globalstate_pubkey).await; - let (device_pubkey, _) = get_device_pda(&program_id, gs.account_index + 1); - execute_transaction( - banks_client, - recent_blockhash, - program_id, - DoubleZeroInstruction::CreateDevice(DeviceCreateArgs { - code: "testdev".to_string(), - device_type: DeviceType::Hybrid, - public_ip: [100, 0, 0, 1].into(), - dz_prefixes: "100.1.0.0/23".parse().unwrap(), - metrics_publisher_pk: Pubkey::default(), - mgmt_vrf: "mgmt".to_string(), - desired_status: Some(DeviceDesiredStatus::Activated), - resource_count: 0, - }), - vec![ - AccountMeta::new(device_pubkey, false), - AccountMeta::new(contributor_pubkey, false), - AccountMeta::new(location_pubkey, false), - AccountMeta::new(exchange_pubkey, false), - AccountMeta::new(globalstate_pubkey, false), - ], - payer, - ) - .await; - - (device_pubkey, globalstate_pubkey) -} - -// --------------------------------------------------------------------------- -// test_migrate_device_interfaces_idempotent -// -// Newly created devices have no interfaces. The migration finds no V3 interfaces -// so it runs the migration path, but since there are no interfaces to convert -// the result is identical. The second call also succeeds. -// --------------------------------------------------------------------------- -#[tokio::test] -async fn test_migrate_device_interfaces_idempotent() { - let program_id = Pubkey::new_unique(); - let mut program_test = ProgramTest::new( - "doublezero_serviceability", - program_id, - processor!(doublezero_serviceability::entrypoint::process_instruction), - ); - program_test.set_compute_max_units(1_000_000); - let (mut banks_client, funder, _recent_blockhash) = program_test.start().await; - - let payer = test_payer(); - transfer(&mut banks_client, &funder, &payer.pubkey(), 10_000_000_000).await; - - let (device_pubkey, globalstate_pubkey) = - setup_with_device(&mut banks_client, program_id, &payer).await; - - // Read raw bytes before any migration call. - let before = banks_client - .get_account(device_pubkey) - .await - .unwrap() - .unwrap() - .data - .clone(); - - // First call — should succeed (idempotency path: already new V2 format). - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - let result = try_execute_transaction( - &mut banks_client, - recent_blockhash, - program_id, - DoubleZeroInstruction::MigrateDeviceInterfaces(MigrateDeviceInterfacesArgs {}), - vec![ - AccountMeta::new(device_pubkey, false), - AccountMeta::new_readonly(globalstate_pubkey, false), - ], - &payer, - ) - .await; - assert!( - result.is_ok(), - "First MigrateDeviceInterfaces call should succeed: {result:?}" - ); - - let after_first = banks_client - .get_account(device_pubkey) - .await - .unwrap() - .unwrap() - .data - .clone(); - - // Account data must not change (no rewrite for already-migrated accounts). - assert_eq!( - before, after_first, - "Account bytes must be unchanged after idempotent first call" - ); - - // Second call — also succeeds (idempotency). - let recent_blockhash2 = banks_client.get_latest_blockhash().await.unwrap(); - let result2 = try_execute_transaction( - &mut banks_client, - recent_blockhash2, - program_id, - DoubleZeroInstruction::MigrateDeviceInterfaces(MigrateDeviceInterfacesArgs {}), - vec![ - AccountMeta::new(device_pubkey, false), - AccountMeta::new_readonly(globalstate_pubkey, false), - ], - &payer, - ) - .await; - assert!( - result2.is_ok(), - "Second MigrateDeviceInterfaces call should succeed: {result2:?}" - ); - - let after_second = banks_client - .get_account(device_pubkey) - .await - .unwrap() - .unwrap() - .data - .clone(); - - assert_eq!( - after_first, after_second, - "Account bytes must be identical after both idempotent calls" - ); -} - -// --------------------------------------------------------------------------- -// test_migrate_device_interfaces_unauthorized -// -// A keypair that is neither the foundation, the device owner, nor the activator -// authority must be rejected with NotAllowed (error code 8). -// --------------------------------------------------------------------------- -#[tokio::test] -async fn test_migrate_device_interfaces_unauthorized() { - let program_id = Pubkey::new_unique(); - let mut program_test = ProgramTest::new( - "doublezero_serviceability", - program_id, - processor!(doublezero_serviceability::entrypoint::process_instruction), - ); - program_test.set_compute_max_units(1_000_000); - let (mut banks_client, funder, _recent_blockhash) = program_test.start().await; - - let payer = test_payer(); - transfer(&mut banks_client, &funder, &payer.pubkey(), 10_000_000_000).await; - - let (device_pubkey, globalstate_pubkey) = - setup_with_device(&mut banks_client, program_id, &payer).await; - - // Fund an unauthorized keypair. - let unauthorized = Keypair::new(); - transfer( - &mut banks_client, - &funder, - &unauthorized.pubkey(), - 10_000_000, - ) - .await; - - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - let result = try_execute_transaction( - &mut banks_client, - recent_blockhash, - program_id, - DoubleZeroInstruction::MigrateDeviceInterfaces(MigrateDeviceInterfacesArgs {}), - vec![ - AccountMeta::new(device_pubkey, false), - AccountMeta::new_readonly(globalstate_pubkey, false), - ], - &unauthorized, - ) - .await; - - // NotAllowed is Custom(8). - match result { - Err(BanksClientError::TransactionError(TransactionError::InstructionError( - 0, - InstructionError::Custom(8), - ))) => {} - _ => panic!("Expected NotAllowed (Custom(8)), got: {result:?}"), - } -} - -// --------------------------------------------------------------------------- -// test_migrate_device_interfaces_activator_authority -// -// Inject a GlobalState with activator_authority_pk set to a known keypair, -// then call MigrateDeviceInterfaces signed by that keypair. Expect success. -// --------------------------------------------------------------------------- -#[tokio::test] -async fn test_migrate_device_interfaces_activator_authority() { - let activator = Keypair::new(); - let payer = test_payer(); - - let program_id = Pubkey::new_unique(); - let mut program_test = ProgramTest::new( - "doublezero_serviceability", - program_id, - processor!(doublezero_serviceability::entrypoint::process_instruction), - ); - program_test.set_compute_max_units(1_000_000); - - // Build accounts from scratch so we can inject activator_authority_pk - // without needing an onchain instruction to set it. - let (globalstate_pubkey, gs_bump) = get_globalstate_pda(&program_id); - let (contributor_pubkey, co_bump) = get_contributor_pda(&program_id, 1); - let (device_pubkey, dev_bump) = get_device_pda(&program_id, 2); - - let globalstate = GlobalState { - account_type: AccountType::GlobalState, - bump_seed: gs_bump, - account_index: 2, - foundation_allowlist: vec![payer.pubkey()], - activator_authority_pk: activator.pubkey(), - ..Default::default() - }; - let gs_data = borsh::to_vec(&globalstate).unwrap(); - program_test.add_account( - globalstate_pubkey, - Account { - lamports: 1_000_000_000, - data: gs_data, - owner: program_id, - ..Account::default() - }, - ); - - let contributor = Contributor { - account_type: AccountType::Contributor, - owner: payer.pubkey(), - index: 1, - bump_seed: co_bump, - status: ContributorStatus::Activated, - code: "testco".to_string(), - reference_count: 1, - ops_manager_pk: Pubkey::default(), - }; - let co_data = borsh::to_vec(&contributor).unwrap(); - program_test.add_account( - contributor_pubkey, - Account { - lamports: 1_000_000_000, - data: co_data, - owner: program_id, - ..Account::default() - }, - ); - - // A freshly serialized device with no interfaces — the migration will find no V3 - // interfaces and run the (no-op) migration path. The test only validates that the - // activator authority key is accepted. - let device = Device { - account_type: AccountType::Device, - owner: payer.pubkey(), - index: 2, - bump_seed: dev_bump, - location_pk: Pubkey::new_unique(), - exchange_pk: Pubkey::new_unique(), - device_type: DeviceType::Hybrid, - public_ip: [100, 0, 0, 1].into(), - status: DeviceStatus::Pending, - code: "testdev".to_string(), - dz_prefixes: vec!["100.1.0.0/23".parse().unwrap()].into(), - metrics_publisher_pk: Pubkey::default(), - contributor_pk: contributor_pubkey, - mgmt_vrf: "mgmt".to_string(), - interfaces: vec![], - new_interfaces: vec![], - reference_count: 0, - users_count: 0, - max_users: 128, - device_health: DeviceHealth::ReadyForUsers, - desired_status: DeviceDesiredStatus::Activated, - unicast_users_count: 0, - multicast_subscribers_count: 0, - max_unicast_users: 0, - max_multicast_subscribers: 0, - reserved_seats: 0, - multicast_publishers_count: 0, - max_multicast_publishers: 0, - }; - let dev_data = borsh::to_vec(&device).unwrap(); - program_test.add_account( - device_pubkey, - Account { - lamports: 1_000_000_000, - data: dev_data, - owner: program_id, - ..Account::default() - }, - ); - - let (mut banks_client, funder, _) = program_test.start().await; - transfer(&mut banks_client, &funder, &payer.pubkey(), 100_000_000).await; - transfer(&mut banks_client, &funder, &activator.pubkey(), 10_000_000).await; - - // Call MigrateDeviceInterfaces signed by the activator authority. - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - let result = try_execute_transaction( - &mut banks_client, - recent_blockhash, - program_id, - DoubleZeroInstruction::MigrateDeviceInterfaces(MigrateDeviceInterfacesArgs {}), - vec![ - AccountMeta::new(device_pubkey, false), - AccountMeta::new_readonly(globalstate_pubkey, false), - ], - &activator, - ) - .await; - - assert!( - result.is_ok(), - "Activator authority should be allowed to call MigrateDeviceInterfaces: {result:?}" - ); -} - -// --------------------------------------------------------------------------- -// test_migrate_device_interfaces_non_signer -// -// The payer account is included in the instruction accounts but is not marked -// as a signer (is_signer = false). The program checks `payer_account.is_signer` -// first and must return NotAllowed (Custom(8)). -// --------------------------------------------------------------------------- -#[tokio::test] -async fn test_migrate_device_interfaces_non_signer() { - let program_id = Pubkey::new_unique(); - let mut program_test = ProgramTest::new( - "doublezero_serviceability", - program_id, - processor!(doublezero_serviceability::entrypoint::process_instruction), - ); - program_test.set_compute_max_units(1_000_000); - let (mut banks_client, funder, _recent_blockhash) = program_test.start().await; - - let payer = test_payer(); - transfer(&mut banks_client, &funder, &payer.pubkey(), 10_000_000_000).await; - - let (device_pubkey, globalstate_pubkey) = - setup_with_device(&mut banks_client, program_id, &payer).await; - - // Build the instruction manually so the payer is listed as account[2] but - // is NOT marked is_signer = true. Only the transaction fee payer signs. - let non_signer_pubkey = Pubkey::new_unique(); - let instruction = - DoubleZeroInstruction::MigrateDeviceInterfaces(MigrateDeviceInterfacesArgs {}); - let ix = Instruction::new_with_bytes( - program_id, - &to_vec(&instruction).unwrap(), - vec![ - AccountMeta::new(device_pubkey, false), - AccountMeta::new_readonly(globalstate_pubkey, false), - AccountMeta::new(non_signer_pubkey, false), // payer slot — NOT a signer - AccountMeta::new(solana_system_interface::program::ID, false), - ], - ); - - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - let mut tx = Transaction::new_with_payer(&[ix], Some(&payer.pubkey())); - tx.sign(&[&payer], recent_blockhash); - let result = banks_client.process_transaction(tx).await; - - // NotAllowed is Custom(8). - match result { - Err(BanksClientError::TransactionError(TransactionError::InstructionError( - 0, - InstructionError::Custom(8), - ))) => {} - _ => panic!("Expected NotAllowed (Custom(8)), got: {result:?}"), - } -} - -// --------------------------------------------------------------------------- -// test_migrate_device_interfaces_legacy_account -// -// This test exercises the actual migration code path (V2 → V3 format). -// -// Approach: -// 1. Serialize a Device with V2 interfaces (discriminant 1, no flex_algo_node_segments). -// 2. Inject those bytes as the device account data (this is the legacy format). -// 3. Call MigrateDeviceInterfaces. -// 4. Verify the account data has grown by 4 bytes per interface (the -// flex_algo_node_segments vec length prefix) and the interface discriminant -// changed from 1 (V2) to 3 (V3). -// --------------------------------------------------------------------------- -// IGNORED for #3665: the new `Device` `BorshSerialize` impl always projects the -// legacy on-disk slot from `new_interfaces` as `Interface::V2`, so migrate's -// V2→V3 conversion of `device.interfaces` is no longer observable on disk — -// V3-shape data lives in the trailing `new_interfaces` vec. Migrate semantics -// and this test need to be reworked in a follow-up. -#[ignore = "migrate V2→V3 byte format invalidated by #3665 device serializer"] -#[tokio::test] -async fn test_migrate_device_interfaces_legacy_account() { - let payer = test_payer(); - - let program_id = Pubkey::new_unique(); - let mut program_test = ProgramTest::new( - "doublezero_serviceability", - program_id, - processor!(doublezero_serviceability::entrypoint::process_instruction), - ); - program_test.set_compute_max_units(1_000_000); - - let (globalstate_pubkey, gs_bump) = get_globalstate_pda(&program_id); - let (contributor_pubkey, co_bump) = get_contributor_pda(&program_id, 1); - let (device_pubkey, dev_bump) = get_device_pda(&program_id, 2); - - // GlobalState with payer as foundation member. - let globalstate = GlobalState { - account_type: AccountType::GlobalState, - bump_seed: gs_bump, - account_index: 2, - foundation_allowlist: vec![payer.pubkey()], - ..Default::default() - }; - let gs_data = borsh::to_vec(&globalstate).unwrap(); - program_test.add_account( - globalstate_pubkey, - Account { - lamports: 1_000_000_000, - data: gs_data, - owner: program_id, - ..Account::default() - }, - ); - - let contributor = Contributor { - account_type: AccountType::Contributor, - owner: payer.pubkey(), - index: 1, - bump_seed: co_bump, - status: ContributorStatus::Activated, - code: "testco".to_string(), - reference_count: 1, - ops_manager_pk: Pubkey::default(), - }; - let co_data = borsh::to_vec(&contributor).unwrap(); - program_test.add_account( - contributor_pubkey, - Account { - lamports: 1_000_000_000, - data: co_data, - owner: program_id, - ..Account::default() - }, - ); - - // Build a device with one V2 interface (no flex_algo_node_segments — this is the - // legacy on-disk format with discriminant 1). - let iface = InterfaceV2 { - status: InterfaceStatus::Pending, - name: "Ethernet1".to_string(), - interface_type: InterfaceType::Physical, - interface_cyoa: InterfaceCYOA::None, - interface_dia: InterfaceDIA::None, - loopback_type: LoopbackType::None, - bandwidth: 1000, - cir: 500, - mtu: 9000, - routing_mode: RoutingMode::Static, - vlan_id: 0, - ip_net: "192.168.1.0/24".parse().unwrap(), - node_segment_idx: 0, - user_tunnel_endpoint: false, - }; - let location_pk = Pubkey::new_unique(); - let exchange_pk = Pubkey::new_unique(); - let device = Device { - account_type: AccountType::Device, - owner: payer.pubkey(), - index: 2, - bump_seed: dev_bump, - location_pk, - exchange_pk, - device_type: DeviceType::Hybrid, - public_ip: [100, 0, 0, 1].into(), - status: DeviceStatus::Pending, - code: "testdev".to_string(), - dz_prefixes: vec!["100.1.0.0/23".parse().unwrap()].into(), - metrics_publisher_pk: Pubkey::default(), - contributor_pk: contributor_pubkey, - mgmt_vrf: "mgmt".to_string(), - interfaces: vec![iface.to_interface()], // Interface::V2, disc 1 - new_interfaces: vec![(&iface).try_into().unwrap()], - reference_count: 0, - users_count: 0, - max_users: 128, - device_health: DeviceHealth::ReadyForUsers, - desired_status: DeviceDesiredStatus::Activated, - unicast_users_count: 0, - multicast_subscribers_count: 0, - max_unicast_users: 0, - max_multicast_subscribers: 0, - reserved_seats: 0, - multicast_publishers_count: 0, - max_multicast_publishers: 0, - }; - - // The legacy bytes are just the V2 serialization (disc 1, no flex_algo). - let legacy_bytes = borsh::to_vec(&device).unwrap(); - - // Build the expected post-migration bytes: same device but with V3 interface. - let iface_v3: InterfaceV3 = iface.into(); - let device_v3 = Device { - interfaces: vec![iface_v3.to_interface()], // Interface::V3, disc 3 - ..device.clone() - }; - let expected_bytes = borsh::to_vec(&device_v3).unwrap(); - - // V3 should be 4 bytes larger (flex_algo_node_segments empty vec prefix). - assert_eq!( - expected_bytes.len(), - legacy_bytes.len() + 4, - "V3 format should be 4 bytes larger than V2 (empty flex_algo vec prefix)" - ); - - program_test.add_account( - device_pubkey, - Account { - lamports: 1_000_000_000, - data: legacy_bytes.clone(), - owner: program_id, - ..Account::default() - }, - ); - - let (mut banks_client, funder, _) = program_test.start().await; - transfer(&mut banks_client, &funder, &payer.pubkey(), 100_000_000).await; - - // Before migration: raw bytes are the V2 form. - let bytes_before = banks_client - .get_account(device_pubkey) - .await - .unwrap() - .unwrap() - .data - .clone(); - assert_eq!( - bytes_before.len(), - legacy_bytes.len(), - "Pre-migration byte length should match injected V2 data" - ); - - // Call MigrateDeviceInterfaces. - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - let result = try_execute_transaction( - &mut banks_client, - recent_blockhash, - program_id, - DoubleZeroInstruction::MigrateDeviceInterfaces(MigrateDeviceInterfacesArgs {}), - vec![ - AccountMeta::new(device_pubkey, false), - AccountMeta::new_readonly(globalstate_pubkey, false), - ], - &payer, - ) - .await; - assert!( - result.is_ok(), - "MigrateDeviceInterfaces on legacy account should succeed: {result:?}" - ); - - // After migration: the account must have grown by exactly 4 bytes (one interface × - // 4-byte empty-vec prefix). Solana may zero-pad accounts to alignment boundaries, - // so we allow bytes_after.len() >= expected_bytes.len(). - let bytes_after = banks_client - .get_account(device_pubkey) - .await - .unwrap() - .unwrap() - .data - .clone(); - - assert!( - bytes_after.len() >= expected_bytes.len(), - "Post-migration account ({} bytes) must be at least as large as canonical V3 format ({} bytes)", - bytes_after.len(), - expected_bytes.len() - ); - - // The first expected_bytes.len() bytes must match the canonical V3 serialization. - assert_eq!( - &bytes_after[..expected_bytes.len()], - &expected_bytes[..], - "Migrated account prefix must match the canonical V3 serialization" - ); - - // The account must be deserializable as a Device and the interface must be V3 - // with an empty flex_algo_node_segments vec. - let migrated_device = - Device::try_from(&bytes_after[..]).expect("Failed to deserialize migrated device"); - assert_eq!(migrated_device.code, device.code); - assert_eq!( - migrated_device.interfaces.len(), - 1, - "Interface count must be preserved after migration" - ); - assert!( - matches!(migrated_device.interfaces[0], Interface::V3(_)), - "Migrated interface must be V3" - ); - let migrated_iface = migrated_device.interfaces[0].into_v3(); - assert_eq!( - migrated_iface.flex_algo_node_segments.len(), - 0, - "Migrated interface must have an empty flex_algo_node_segments vec" - ); - - // Calling again must be idempotent: data unchanged. - let recent_blockhash2 = banks_client.get_latest_blockhash().await.unwrap(); - let result2 = try_execute_transaction( - &mut banks_client, - recent_blockhash2, - program_id, - DoubleZeroInstruction::MigrateDeviceInterfaces(MigrateDeviceInterfacesArgs {}), - vec![ - AccountMeta::new(device_pubkey, false), - AccountMeta::new_readonly(globalstate_pubkey, false), - ], - &payer, - ) - .await; - assert!( - result2.is_ok(), - "Second call after migration should also succeed" - ); - - let bytes_after2 = banks_client - .get_account(device_pubkey) - .await - .unwrap() - .unwrap() - .data - .clone(); - assert_eq!( - bytes_after, bytes_after2, - "Second call must not change account bytes" - ); -} diff --git a/smartcontract/sdk/go/serviceability/deserialize.go b/smartcontract/sdk/go/serviceability/deserialize.go index 995b0072c3..27b29604d6 100644 --- a/smartcontract/sdk/go/serviceability/deserialize.go +++ b/smartcontract/sdk/go/serviceability/deserialize.go @@ -89,7 +89,10 @@ func DeserializeContributor(reader *ByteReader, contributor *Contributor) { // 2 — reserved, never written // 3 — V3: V2 fields + flex_algo_node_segments (RFC-18) // -// MigrateDeviceInterfaces converts discriminant-1 accounts to discriminant-3. +// The on-chain Device serializer projects the legacy interfaces slot as V2 +// (per #3653); discriminant-3 entries seen here are residual from older +// accounts. Newer flex_algo_node_segments data lives in the trailing +// new_interfaces vec on Device, not in this slot. func DeserializeInterface(reader *ByteReader, iface *Interface) { iface.Version = reader.ReadU8() diff --git a/smartcontract/sdk/go/serviceability/state.go b/smartcontract/sdk/go/serviceability/state.go index 2de916c3c3..0b56ef3c87 100644 --- a/smartcontract/sdk/go/serviceability/state.go +++ b/smartcontract/sdk/go/serviceability/state.go @@ -412,8 +412,9 @@ type Interface struct { NodeSegmentIdx uint16 UserTunnelEndpoint bool // FlexAlgoNodeSegments holds flex-algo node segment assignments for this interface (RFC-18). - // Present in all V2 accounts after MigrateDeviceInterfaces has been run (empty vec for - // interfaces not yet assigned to any topology). Nil for V1 interfaces. + // Only populated when reading a discriminant-3 (V3) interface; the on-chain Device serializer + // now projects the legacy interfaces slot as V2 (per #3653) and surfaces segments through the + // trailing new_interfaces vec on Device, which this Go SDK does not yet read. Nil otherwise. FlexAlgoNodeSegments []FlexAlgoNodeSegment `json:",omitempty"` } From 27a4435beca552037dd5e2fb18d8493cdbc6c001 Mon Sep 17 00:00:00 2001 From: Greg Mitchell Date: Tue, 5 May 2026 21:53:56 +0000 Subject: [PATCH 2/2] serviceability: reserve variant 111 as Deprecated111 tombstone Per review: keep variant 111 in DoubleZeroInstruction so the slot is not reused for a future instruction. Variant 111 unpacks into `Deprecated111()` and dispatches to a no-op (auto-promotion on read is what actually performs the migration); name_string and get_args return "Deprecated111" / empty string respectively. --- CHANGELOG.md | 2 +- .../programs/doublezero-serviceability/src/entrypoint.rs | 1 + .../programs/doublezero-serviceability/src/instructions.rs | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 031a44c68a..e296657b16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ All notable changes to this project will be documented in this file. - Add forward-compatible `NewInterface` struct in `state/interface.rs` with a `size: u16` + `version: u8` on-disk prefix, V3-shaped body, and `flex_algo_node_segments`. Older readers can use the size prefix to skip past unknown future versions in constant time. Additive only — no callers, processors, or SDKs change in this PR ([#3666](https://github.com/malbeclabs/doublezero/pull/3666)) - Append `new_interfaces: Vec` to `Device` after `max_multicast_publishers`, behind a custom `BorshSerialize` that projects the on-disk legacy `interfaces` slot from `new_interfaces` (always `Interface::V2` per #3653) and writes `new_interfaces` at the end of the layout. Legacy accounts with no trailing bytes deserialize cleanly: `Device::try_from` rebuilds `new_interfaces` from the legacy enum vec via per-variant `TryFrom`. Older readers continue to parse the legacy slot at its existing offset; newer readers gain forward-compat via the trailing vec. Mutations now go through `Device::replace_interface` / `push_interface` / `remove_interface` so both vecs stay in sync; `find_interface` returns `&NewInterface` and `find_interface_legacy` is a temporary helper for unrelated callers ([#3665](https://github.com/malbeclabs/doublezero/pull/3665)) - Migrate serviceability processors (`device/interface/{create,update,activate,delete,reject,remove,unlink}`, `link/{accept,activate,closeaccount,create,delete,update}`, `topology/backfill`) to read and mutate `Device::new_interfaces` directly. `device.interfaces` is no longer touched in `processors/`, and `Device::push_interface` now takes a `NewInterface`. `BackfillTopology` no longer mirrors `flex_algo_node_segments` into the legacy in-memory `interfaces` vec — segments live only in `new_interfaces` and are intentionally dropped on the V2-projected on-disk legacy slot ([#3658](https://github.com/malbeclabs/doublezero/issues/3658)) - - Delete the `MigrateDeviceInterfaces` instruction (variant 111) and its processor from the serviceability program. `Device::TryFrom<&[u8]>` (#3665) now auto-promotes legacy `interfaces` into `new_interfaces` when the trailing vec is missing, and the next account write persists the promoted vec — no standalone migration step is needed ([#3662](https://github.com/malbeclabs/doublezero/issues/3662)) + - Delete the `MigrateDeviceInterfaces` processor and integration tests from the serviceability program. `Device::TryFrom<&[u8]>` (#3665) now auto-promotes legacy `interfaces` into `new_interfaces` when the trailing vec is missing, and the next account write persists the promoted vec — no standalone migration step is needed. Variant 111 is retained as a `Deprecated111()` tombstone (no-op dispatch, slot reserved so it isn't reused) for compatibility with older clients still emitting the old discriminator ([#3662](https://github.com/malbeclabs/doublezero/issues/3662)) ## [v0.21.0](https://github.com/malbeclabs/doublezero/compare/client/v0.20.0...client/v0.21.0) - 2026-05-01 diff --git a/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs b/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs index 7baf49edb0..c9ea78cadf 100644 --- a/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs +++ b/smartcontract/programs/doublezero-serviceability/src/entrypoint.rs @@ -447,6 +447,7 @@ pub fn process_instruction( DoubleZeroInstruction::BackfillTopology(value) => { process_topology_backfill(program_id, accounts, &value)? } + DoubleZeroInstruction::Deprecated111() => (), }; Ok(()) } diff --git a/smartcontract/programs/doublezero-serviceability/src/instructions.rs b/smartcontract/programs/doublezero-serviceability/src/instructions.rs index 8522948111..3a6fe893f7 100644 --- a/smartcontract/programs/doublezero-serviceability/src/instructions.rs +++ b/smartcontract/programs/doublezero-serviceability/src/instructions.rs @@ -233,6 +233,8 @@ pub enum DoubleZeroInstruction { DeleteTopology(TopologyDeleteArgs), // variant 108 ClearTopology(TopologyClearArgs), // variant 109 BackfillTopology(TopologyBackfillArgs), // variant 110 + + Deprecated111(), // variant 111, (was MigrateDeviceInterfaces) } impl DoubleZeroInstruction { @@ -373,6 +375,7 @@ impl DoubleZeroInstruction { 108 => Ok(Self::DeleteTopology(TopologyDeleteArgs::try_from(rest).unwrap())), 109 => Ok(Self::ClearTopology(TopologyClearArgs::try_from(rest).unwrap())), 110 => Ok(Self::BackfillTopology(TopologyBackfillArgs::try_from(rest).unwrap())), + 111 => Ok(Self::Deprecated111()), _ => Err(ProgramError::InvalidInstructionData), } @@ -516,6 +519,7 @@ impl DoubleZeroInstruction { Self::DeleteTopology(_) => "DeleteTopology".to_string(), // variant 108 Self::ClearTopology(_) => "ClearTopology".to_string(), // variant 109 Self::BackfillTopology(_) => "BackfillTopology".to_string(), // variant 110 + Self::Deprecated111() => "Deprecated111".to_string(), // variant 111 } } @@ -651,6 +655,7 @@ impl DoubleZeroInstruction { Self::DeleteTopology(args) => format!("{args:?}"), // variant 108 Self::ClearTopology(args) => format!("{args:?}"), // variant 109 Self::BackfillTopology(args) => format!("{args:?}"), // variant 110 + Self::Deprecated111() => String::new(), // variant 111 } } }