From 5e62741d32e85b0fb8da81558e1470cdd48fa99c Mon Sep 17 00:00:00 2001 From: Greg Mitchell Date: Tue, 5 May 2026 19:54:13 +0000 Subject: [PATCH 1/2] serviceability: migrate processors to new_interfaces Move device/interface, link, and topology/backfill processors off into_current_version() / into_v3() / find_interface_legacy() and onto direct field access on &NewInterface via Device::new_interfaces. The Device serializer projects new_interfaces to the V2-on-disk legacy slot, so processors only mutate new_interfaces. - Device::push_interface now takes a NewInterface directly - BackfillTopology no longer mirrors flex_algo_node_segments into the in-memory legacy interfaces vec; segments live only in new_interfaces - Add a Device serialize/deserialize round-trip test asserting that flex_algo_node_segments survive through new_interfaces while the V2-projected legacy slot intentionally drops them device.interfaces is no longer touched in processors/. The find_interface_legacy helper is retained for the CLI's staged migration. Issue: #3658 --- CHANGELOG.md | 1 + .../processors/device/interface/activate.rs | 4 +- .../src/processors/device/interface/create.rs | 14 +++-- .../src/processors/device/interface/delete.rs | 6 +- .../src/processors/device/interface/reject.rs | 7 ++- .../src/processors/device/interface/unlink.rs | 7 ++- .../src/processors/device/interface/update.rs | 9 +-- .../src/processors/link/accept.rs | 28 +++++----- .../src/processors/link/activate.rs | 24 ++++---- .../src/processors/link/closeaccount.rs | 8 +-- .../src/processors/link/create.rs | 16 ++---- .../src/processors/link/delete.rs | 10 ++-- .../src/processors/link/update.rs | 8 +-- .../src/processors/topology/backfill.rs | 30 ++-------- .../src/state/device.rs | 56 ++++++++++++++++--- 15 files changed, 124 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ebeef449..4e4bc345b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. - Stop writing `InterfaceV3` from `CreateDeviceInterface` and `UpdateDeviceInterface`; `CurrentInterfaceVersion` is now `InterfaceV2`. `MigrateDeviceInterfaces` and `BackfillTopology` continue to write `InterfaceV3` since they are admin-controlled and need the `flex_algo_node_segments` field - 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)) ## [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/processors/device/interface/activate.rs b/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/activate.rs index fe19fdfa6..d07c01785 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/activate.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/activate.rs @@ -118,7 +118,7 @@ pub fn process_activate_device_interface( let mut device: Device = Device::try_from(device_account)?; let (idx, iface) = device - .find_interface_legacy(&value.name) + .find_interface(&value.name) .map_err(|_| DoubleZeroError::InterfaceNotFound)?; if iface.status != InterfaceStatus::Pending && iface.status != InterfaceStatus::Unlinked { @@ -148,7 +148,7 @@ pub fn process_activate_device_interface( updated_iface.node_segment_idx = value.node_segment_idx; } - device.replace_interface(idx, (&updated_iface).try_into()?); + device.replace_interface(idx, updated_iface); try_acc_write(&device, device_account, payer_account, accounts)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/create.rs index 353d88946..daf11d512 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/create.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/create.rs @@ -14,8 +14,9 @@ use crate::{ feature_flags::{is_feature_enabled, FeatureFlag}, globalstate::GlobalState, interface::{ - CurrentInterfaceVersion, InterfaceCYOA, InterfaceDIA, InterfaceStatus, InterfaceType, - LoopbackType, RoutingMode, CYOA_DIA_INTERFACE_MTU, INTERFACE_MTU, + InterfaceCYOA, InterfaceDIA, InterfaceStatus, InterfaceType, LoopbackType, + NewInterface, RoutingMode, CURRENT_INTERFACE_SCHEMA_VERSION, CYOA_DIA_INTERFACE_MTU, + INTERFACE_MTU, }, }, }; @@ -224,7 +225,9 @@ pub fn process_create_device_interface( } } - device.push_interface(CurrentInterfaceVersion { + let mut new_iface = NewInterface { + size: 0, + version: CURRENT_INTERFACE_SCHEMA_VERSION, status, name, interface_type, @@ -239,7 +242,10 @@ pub fn process_create_device_interface( ip_net, node_segment_idx, user_tunnel_endpoint: value.user_tunnel_endpoint, - })?; + flex_algo_node_segments: vec![], + }; + new_iface.size = new_iface.compute_on_disk_size()?; + device.push_interface(new_iface); try_acc_write(&device, device_account, payer_account, accounts)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/delete.rs b/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/delete.rs index a7035418c..68b6f4610 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/delete.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/delete.rs @@ -118,10 +118,10 @@ pub fn process_delete_device_interface( let mut device: Device = Device::try_from(device_account)?; - let (idx, _) = device + let (idx, iface) = device .find_interface(&value.name) .map_err(|_| DoubleZeroError::InterfaceNotFound)?; - let iface = device.interfaces[idx].into_current_version(); + let iface = iface.clone(); if iface.status != InterfaceStatus::Activated && iface.status != InterfaceStatus::Unlinked { return Err(DoubleZeroError::InvalidStatus.into()); @@ -178,7 +178,7 @@ pub fn process_delete_device_interface( // Legacy path: just mark as Deleting let mut iface = iface; iface.status = InterfaceStatus::Deleting; - device.replace_interface(idx, (&iface).try_into()?); + device.replace_interface(idx, iface); #[cfg(test)] msg!("Deleting interface: {} from {:?}", value.name, device); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/reject.rs b/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/reject.rs index 73c488232..2337cde28 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/reject.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/reject.rs @@ -58,16 +58,17 @@ pub fn process_reject_device_interface( let mut device: Device = Device::try_from(device_account)?; - let (idx, mut iface) = device - .find_interface_legacy(&value.name) + let (idx, iface) = device + .find_interface(&value.name) .map_err(|_| DoubleZeroError::InterfaceNotFound)?; if iface.status != InterfaceStatus::Pending { return Err(DoubleZeroError::InvalidStatus.into()); } + let mut iface = iface.clone(); iface.status = InterfaceStatus::Rejected; - device.replace_interface(idx, (&iface).try_into()?); + device.replace_interface(idx, iface); try_acc_write(&device, device_account, payer_account, accounts)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/unlink.rs b/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/unlink.rs index 76a9f4dd7..41a823ed9 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/unlink.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/unlink.rs @@ -73,9 +73,10 @@ pub fn process_unlink_device_interface( let mut device: Device = Device::try_from(device_account)?; - let (idx, mut iface) = device - .find_interface_legacy(&value.name) + let (idx, iface) = device + .find_interface(&value.name) .map_err(|_| DoubleZeroError::InterfaceNotFound)?; + let mut iface = iface.clone(); if iface.status != InterfaceStatus::Activated && iface.status != InterfaceStatus::Pending { return Err(DoubleZeroError::InvalidStatus.into()); @@ -111,7 +112,7 @@ pub fn process_unlink_device_interface( if iface.interface_type == InterfaceType::Loopback { iface.ip_net = NetworkV4::default(); } - device.replace_interface(idx, (&iface).try_into()?); + device.replace_interface(idx, iface); try_acc_write(&device, device_account, payer_account, accounts)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/update.rs b/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/update.rs index 215b1ccb2..c9be18ef8 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/update.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/update.rs @@ -137,7 +137,7 @@ pub fn process_update_device_interface( let (idx, _) = device .find_interface(&value.name) .map_err(|_| DoubleZeroError::InterfaceNotFound)?; - let mut iface = device.interfaces[idx].into_current_version(); + let mut iface = device.new_interfaces[idx].clone(); if let Some(loopback_type) = &value.loopback_type { if *loopback_type == LoopbackType::None { @@ -244,12 +244,9 @@ pub fn process_update_device_interface( return Err(DoubleZeroError::InvalidMtu.into()); } - // until we have release V2 version for interfaces, always convert to v1 - let updated_interface = iface.to_interface(); + iface.validate()?; - updated_interface.validate()?; - - device.replace_interface(idx, (&iface).try_into()?); + device.replace_interface(idx, iface); try_acc_write(&device, device_account, payer_account, accounts)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/link/accept.rs b/smartcontract/programs/doublezero-serviceability/src/processors/link/accept.rs index 490b4d4f1..ed2f503fd 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/link/accept.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/link/accept.rs @@ -114,9 +114,9 @@ pub fn process_accept_link( } if !side_z_dev - .interfaces + .new_interfaces .iter() - .any(|iface| iface.into_current_version().name == value.side_z_iface_name) + .any(|iface| iface.name == value.side_z_iface_name) { #[cfg(test)] msg!("{:?}", side_z_dev); @@ -152,23 +152,25 @@ pub fn process_accept_link( } let (idx_a, side_a_iface) = side_a_dev - .find_interface_legacy(&link.side_a_iface_name) + .find_interface(&link.side_a_iface_name) .map_err(|_| DoubleZeroError::InterfaceNotFound)?; + let mut updated_iface_a = side_a_iface.clone(); let (idx_z, side_z_iface) = side_z_dev - .find_interface_legacy(&link.side_z_iface_name) + .find_interface(&link.side_z_iface_name) .map_err(|_| DoubleZeroError::InterfaceNotFound)?; + let mut updated_iface_z = side_z_iface.clone(); - if side_a_iface.status != InterfaceStatus::Unlinked - || side_z_iface.status != InterfaceStatus::Unlinked + if updated_iface_a.status != InterfaceStatus::Unlinked + || updated_iface_z.status != InterfaceStatus::Unlinked { return Err(DoubleZeroError::InvalidStatus.into()); } - if side_a_iface.interface_cyoa != InterfaceCYOA::None - || side_a_iface.interface_dia != InterfaceDIA::None - || side_z_iface.interface_cyoa != InterfaceCYOA::None - || side_z_iface.interface_dia != InterfaceDIA::None + if updated_iface_a.interface_cyoa != InterfaceCYOA::None + || updated_iface_a.interface_dia != InterfaceDIA::None + || updated_iface_z.interface_cyoa != InterfaceCYOA::None + || updated_iface_z.interface_dia != InterfaceDIA::None { return Err(DoubleZeroError::InterfaceHasEdgeAssignment.into()); } @@ -182,21 +184,19 @@ pub fn process_accept_link( &globalstate, )?; - let mut updated_iface_a = side_a_iface.clone(); updated_iface_a.status = InterfaceStatus::Activated; if updated_iface_a.ip_net == NetworkV4::default() { updated_iface_a.ip_net = NetworkV4::new(link.tunnel_net.nth(0).unwrap(), link.tunnel_net.prefix()).unwrap(); } - side_a_dev.replace_interface(idx_a, (&updated_iface_a).try_into()?); + side_a_dev.replace_interface(idx_a, updated_iface_a); - let mut updated_iface_z = side_z_iface.clone(); updated_iface_z.status = InterfaceStatus::Activated; if updated_iface_z.ip_net == NetworkV4::default() { updated_iface_z.ip_net = NetworkV4::new(link.tunnel_net.nth(1).unwrap(), link.tunnel_net.prefix()).unwrap(); } - side_z_dev.replace_interface(idx_z, (&updated_iface_z).try_into()?); + side_z_dev.replace_interface(idx_z, updated_iface_z); link.status = LinkStatus::Activated; link.check_status_transition(); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/link/activate.rs b/smartcontract/programs/doublezero-serviceability/src/processors/link/activate.rs index 406c9c2cf..b84ca630d 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/link/activate.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/link/activate.rs @@ -125,23 +125,25 @@ pub fn process_activate_link( } let (idx_a, side_a_iface) = side_a_dev - .find_interface_legacy(&link.side_a_iface_name) + .find_interface(&link.side_a_iface_name) .map_err(|_| DoubleZeroError::InterfaceNotFound)?; + let mut updated_iface_a = side_a_iface.clone(); let (idx_z, side_z_iface) = side_z_dev - .find_interface_legacy(&link.side_z_iface_name) + .find_interface(&link.side_z_iface_name) .map_err(|_| DoubleZeroError::InterfaceNotFound)?; + let mut updated_iface_z = side_z_iface.clone(); - if side_a_iface.status != InterfaceStatus::Unlinked - || side_z_iface.status != InterfaceStatus::Unlinked + if updated_iface_a.status != InterfaceStatus::Unlinked + || updated_iface_z.status != InterfaceStatus::Unlinked { return Err(DoubleZeroError::InvalidStatus.into()); } - if side_a_iface.interface_cyoa != InterfaceCYOA::None - || side_a_iface.interface_dia != InterfaceDIA::None - || side_z_iface.interface_cyoa != InterfaceCYOA::None - || side_z_iface.interface_dia != InterfaceDIA::None + if updated_iface_a.interface_cyoa != InterfaceCYOA::None + || updated_iface_a.interface_dia != InterfaceDIA::None + || updated_iface_z.interface_cyoa != InterfaceCYOA::None + || updated_iface_z.interface_dia != InterfaceDIA::None { return Err(DoubleZeroError::InterfaceHasEdgeAssignment.into()); } @@ -183,7 +185,6 @@ pub fn process_activate_link( link.tunnel_net = value.tunnel_net; } - let mut updated_iface_a = side_a_iface.clone(); updated_iface_a.status = InterfaceStatus::Activated; // Only set ip_net from tunnel_net if the interface doesn't already have a user-provided ip_net // (e.g. CYOA/DIA physical interfaces). Interfaces without a user value get tunnel IPs. @@ -191,9 +192,8 @@ pub fn process_activate_link( updated_iface_a.ip_net = NetworkV4::new(link.tunnel_net.nth(0).unwrap(), link.tunnel_net.prefix()).unwrap(); } - side_a_dev.replace_interface(idx_a, (&updated_iface_a).try_into()?); + side_a_dev.replace_interface(idx_a, updated_iface_a); - let mut updated_iface_z = side_z_iface.clone(); updated_iface_z.status = InterfaceStatus::Activated; // Only set ip_net from tunnel_net if the interface doesn't already have a user-provided ip_net // (e.g. CYOA/DIA physical interfaces). Interfaces without a user value get tunnel IPs. @@ -201,7 +201,7 @@ pub fn process_activate_link( updated_iface_z.ip_net = NetworkV4::new(link.tunnel_net.nth(1).unwrap(), link.tunnel_net.prefix()).unwrap(); } - side_z_dev.replace_interface(idx_z, (&updated_iface_z).try_into()?); + side_z_dev.replace_interface(idx_z, updated_iface_z); //TODO: This should be changed once the Health Oracle is finalized. //link.status = LinkStatus::Provisioning; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/link/closeaccount.rs b/smartcontract/programs/doublezero-serviceability/src/processors/link/closeaccount.rs index bda0d401b..08d4bbe3c 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/link/closeaccount.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/link/closeaccount.rs @@ -176,7 +176,7 @@ pub fn process_closeaccount_link( deallocate_id(link_ids_ext, link.tunnel_id); } - if let Ok((idx_a, side_a_iface)) = side_a_dev.find_interface_legacy(&link.side_a_iface_name) { + if let Ok((idx_a, side_a_iface)) = side_a_dev.find_interface(&link.side_a_iface_name) { let mut updated_iface = side_a_iface.clone(); updated_iface.status = InterfaceStatus::Unlinked; // Preserve user-provided ip_net for CYOA/DIA physical interfaces. @@ -188,10 +188,10 @@ pub fn process_closeaccount_link( if !has_user_ip { updated_iface.ip_net = NetworkV4::default(); } - side_a_dev.replace_interface(idx_a, (&updated_iface).try_into()?); + side_a_dev.replace_interface(idx_a, updated_iface); } - if let Ok((idx_z, side_z_iface)) = side_z_dev.find_interface_legacy(&link.side_z_iface_name) { + if let Ok((idx_z, side_z_iface)) = side_z_dev.find_interface(&link.side_z_iface_name) { let mut updated_iface = side_z_iface.clone(); updated_iface.status = InterfaceStatus::Unlinked; // Preserve user-provided ip_net for CYOA/DIA physical interfaces. @@ -203,7 +203,7 @@ pub fn process_closeaccount_link( if !has_user_ip { updated_iface.ip_net = NetworkV4::default(); } - side_z_dev.replace_interface(idx_z, (&updated_iface).try_into()?); + side_z_dev.replace_interface(idx_z, updated_iface); } contributor.reference_count = contributor.reference_count.saturating_sub(1); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/link/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/link/create.rs index 7c1d288c7..4449b49bc 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/link/create.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/link/create.rs @@ -152,9 +152,8 @@ pub fn process_create_link( } let side_a_iface = side_a_dev - .interfaces + .new_interfaces .iter() - .map(|iface| iface.into_current_version()) .find(|iface| iface.name == value.side_a_iface_name) .ok_or_else(|| { #[cfg(test)] @@ -171,9 +170,8 @@ pub fn process_create_link( let side_z_iface_name = value.side_z_iface_name.clone().unwrap_or_default(); if let Some(ref z_name) = value.side_z_iface_name { let side_z_iface = side_z_dev - .interfaces + .new_interfaces .iter() - .map(|iface| iface.into_current_version()) .find(|iface| iface.name == *z_name) .ok_or_else(|| { #[cfg(test)] @@ -280,7 +278,7 @@ pub fn process_create_link( // Validate interfaces are Unlinked (required for activation) let (idx_a, side_a_iface) = side_a_dev - .find_interface_legacy(&link.side_a_iface_name) + .find_interface(&link.side_a_iface_name) .map_err(|_| DoubleZeroError::InterfaceNotFound)?; if side_a_iface.status != InterfaceStatus::Unlinked { return Err(DoubleZeroError::InvalidStatus.into()); @@ -294,12 +292,10 @@ pub fn process_create_link( NetworkV4::new(link.tunnel_net.nth(0).unwrap(), link.tunnel_net.prefix()) .unwrap(); } - side_a_dev.replace_interface(idx_a, (&updated_iface_a).try_into()?); + side_a_dev.replace_interface(idx_a, updated_iface_a); // Set side Z interface to Activated with IP from tunnel_net - if let Ok((idx_z, side_z_iface)) = - side_z_dev.find_interface_legacy(&link.side_z_iface_name) - { + if let Ok((idx_z, side_z_iface)) = side_z_dev.find_interface(&link.side_z_iface_name) { if side_z_iface.status != InterfaceStatus::Unlinked { return Err(DoubleZeroError::InvalidStatus.into()); } @@ -310,7 +306,7 @@ pub fn process_create_link( NetworkV4::new(link.tunnel_net.nth(1).unwrap(), link.tunnel_net.prefix()) .unwrap(); } - side_z_dev.replace_interface(idx_z, (&updated_iface_z).try_into()?); + side_z_dev.replace_interface(idx_z, updated_iface_z); } link.status = LinkStatus::Activated; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/link/delete.rs b/smartcontract/programs/doublezero-serviceability/src/processors/link/delete.rs index 0d7975e97..3f87b42b8 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/link/delete.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/link/delete.rs @@ -172,8 +172,7 @@ pub fn process_delete_link( let mut side_a_dev = Device::try_from(side_a_account)?; let mut side_z_dev = Device::try_from(side_z_account)?; - if let Ok((idx_a, side_a_iface)) = side_a_dev.find_interface_legacy(&link.side_a_iface_name) - { + if let Ok((idx_a, side_a_iface)) = side_a_dev.find_interface(&link.side_a_iface_name) { let mut updated_iface = side_a_iface.clone(); updated_iface.status = InterfaceStatus::Unlinked; // Preserve user-provided ip_net for CYOA/DIA physical interfaces. @@ -183,11 +182,10 @@ pub fn process_delete_link( if !has_user_ip { updated_iface.ip_net = NetworkV4::default(); } - side_a_dev.replace_interface(idx_a, (&updated_iface).try_into()?); + side_a_dev.replace_interface(idx_a, updated_iface); } - if let Ok((idx_z, side_z_iface)) = side_z_dev.find_interface_legacy(&link.side_z_iface_name) - { + if let Ok((idx_z, side_z_iface)) = side_z_dev.find_interface(&link.side_z_iface_name) { let mut updated_iface = side_z_iface.clone(); updated_iface.status = InterfaceStatus::Unlinked; let has_user_ip = updated_iface.interface_type == InterfaceType::Physical @@ -196,7 +194,7 @@ pub fn process_delete_link( if !has_user_ip { updated_iface.ip_net = NetworkV4::default(); } - side_z_dev.replace_interface(idx_z, (&updated_iface).try_into()?); + side_z_dev.replace_interface(idx_z, updated_iface); } // Decrement reference counts diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/link/update.rs b/smartcontract/programs/doublezero-serviceability/src/processors/link/update.rs index f655b7b0f..a0e7bacc6 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/link/update.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/link/update.rs @@ -358,20 +358,20 @@ pub fn process_update_link( let mut side_z_dev = Device::try_from(device_z_account)?; let (idx_a, side_a_iface) = side_a_dev - .find_interface_legacy(&link.side_a_iface_name) + .find_interface(&link.side_a_iface_name) .map_err(|_| DoubleZeroError::InterfaceNotFound)?; let mut updated_iface_a = side_a_iface.clone(); updated_iface_a.ip_net = NetworkV4::new(link.tunnel_net.nth(0).unwrap(), link.tunnel_net.prefix()).unwrap(); - side_a_dev.replace_interface(idx_a, (&updated_iface_a).try_into()?); + side_a_dev.replace_interface(idx_a, updated_iface_a); let (idx_z, side_z_iface) = side_z_dev - .find_interface_legacy(&link.side_z_iface_name) + .find_interface(&link.side_z_iface_name) .map_err(|_| DoubleZeroError::InterfaceNotFound)?; let mut updated_iface_z = side_z_iface.clone(); updated_iface_z.ip_net = NetworkV4::new(link.tunnel_net.nth(1).unwrap(), link.tunnel_net.prefix()).unwrap(); - side_z_dev.replace_interface(idx_z, (&updated_iface_z).try_into()?); + side_z_dev.replace_interface(idx_z, updated_iface_z); try_acc_write(&side_a_dev, device_a_account, payer_account, accounts)?; try_acc_write(&side_z_dev, device_z_account, payer_account, accounts)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/topology/backfill.rs b/smartcontract/programs/doublezero-serviceability/src/processors/topology/backfill.rs index 5f2cd8d7c..76f18171b 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/topology/backfill.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/topology/backfill.rs @@ -5,9 +5,7 @@ use crate::{ resource::ResourceType, serializer::try_acc_write, state::{ - device::Device, - globalstate::GlobalState, - interface::{Interface, LoopbackType}, + device::Device, globalstate::GlobalState, interface::LoopbackType, topology::FlexAlgoNodeSegment, }, }; @@ -125,17 +123,15 @@ pub fn process_topology_backfill( msg!("BackfillTopology: processing device {}", device_account.key); let mut device = Device::try_from(&device_account.data.borrow()[..])?; let mut modified = false; - // Iterate by index so we can simultaneously read from `new_interfaces` - // (the source of truth for `flex_algo_node_segments` post-#3665) and - // mirror the change into the legacy `interfaces` vec. + // `new_interfaces` is the source of truth for `flex_algo_node_segments`. + // The custom Device serializer projects `new_interfaces` to the legacy + // on-disk slot as V2, which intentionally drops segments — so we don't + // mirror the change into the legacy in-memory vec here. for idx in 0..device.new_interfaces.len() { let new_iface = &device.new_interfaces[idx]; if new_iface.loopback_type != LoopbackType::Vpnv4 { continue; } - // Idempotency check against `new_interfaces` — the legacy V2-projected - // slot does not carry segments, so checking it would mis-fire on the - // second call. if new_iface .flex_algo_node_segments .iter() @@ -152,23 +148,9 @@ pub fn process_topology_backfill( topology: *topology_key, node_segment_idx, }; - // Push to `new_interfaces` (forward-compat slot — survives the V2 - // legacy projection) and also to the in-memory legacy `interfaces` - // vec (upgraded to V3) so callers reading the in-memory device - // before save observe the change. device.new_interfaces[idx] .flex_algo_node_segments - .push(segment.clone()); - match &mut device.interfaces[idx] { - Interface::V3(v3) => { - v3.flex_algo_node_segments.push(segment); - } - other => { - let mut upgraded = other.into_v3(); - upgraded.flex_algo_node_segments.push(segment); - *other = Interface::V3(upgraded); - } - } + .push(segment); modified = true; backfilled_count += 1; } diff --git a/smartcontract/programs/doublezero-serviceability/src/state/device.rs b/smartcontract/programs/doublezero-serviceability/src/state/device.rs index b71556663..571dea875 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/device.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/device.rs @@ -334,10 +334,9 @@ impl Device { } /// Temporary helper that returns a `CurrentInterfaceVersion` projection of the - /// matched interface. Exists to keep call sites compiling while `find_interface` - /// switches to returning `&NewInterface`; the legacy projection is the V2 form - /// per #3653. Callers migrate to `find_interface` in subsequent issues, after - /// which this helper is removed. + /// matched interface. Retained for CLI callers during the staged migration; + /// once those callers move to `find_interface` (which returns `&NewInterface`), + /// this helper is removed. No `processors/` code calls this any more. pub fn find_interface_legacy( &self, name: &str, @@ -361,11 +360,10 @@ impl Device { /// Appends an interface to both `interfaces` and `new_interfaces`. Same /// rationale as `replace_interface`. - pub fn push_interface(&mut self, iface: CurrentInterfaceVersion) -> Result<(), ProgramError> { - let new_iface: NewInterface = (&iface).try_into()?; - self.interfaces.push(iface.to_interface()); - self.new_interfaces.push(new_iface); - Ok(()) + pub fn push_interface(&mut self, iface: NewInterface) { + self.interfaces + .push(InterfaceV2::from(&iface).to_interface()); + self.new_interfaces.push(iface); } /// Removes the interface at `idx` from both `interfaces` and `new_interfaces`. @@ -1864,4 +1862,44 @@ mod test_device_new_interfaces_vec { ); assert_eq!(decoded.new_interfaces[1].name, "Switch1/1/1"); } + + /// Mirrors what TopologyBackfill produces: a Vpnv4 loopback whose + /// `flex_algo_node_segments` is populated only on the new vec. After a full + /// borsh round-trip, segments must survive in `new_interfaces`, while the + /// V2-projected legacy vec carries no segments (V2 has no such field). + #[test] + fn test_flex_algo_segments_roundtrip_through_new_interfaces() { + use crate::state::topology::FlexAlgoNodeSegment; + + let mut device = sample_device_with_n_interfaces(1); + let topology = Pubkey::new_unique(); + let segment = FlexAlgoNodeSegment { + topology, + node_segment_idx: 42, + }; + device.new_interfaces[0].loopback_type = LoopbackType::Vpnv4; + device.new_interfaces[0] + .flex_algo_node_segments + .push(segment.clone()); + // Keep the on-disk size field consistent with the populated body. + device.new_interfaces[0].size = device.new_interfaces[0].compute_on_disk_size().unwrap(); + + let bytes = borsh::to_vec(&device).unwrap(); + let decoded = Device::try_from(&bytes[..]).unwrap(); + + // Source of truth: segments survive in new_interfaces. + assert_eq!(decoded.new_interfaces.len(), 1); + assert_eq!( + decoded.new_interfaces[0].flex_algo_node_segments, + vec![segment] + ); + assert_eq!(decoded.new_interfaces[0].loopback_type, LoopbackType::Vpnv4); + + // V2-projected legacy vec preserves the rest of the interface but cannot + // carry segments (V2 has no such field). + assert_eq!(decoded.interfaces.len(), 1); + let legacy_v2 = decoded.interfaces[0].into_current_version(); + assert_eq!(legacy_v2.name, decoded.new_interfaces[0].name); + assert_eq!(legacy_v2.loopback_type, LoopbackType::Vpnv4); + } } From 03cf4a9113ff34788eab6a24ada1854b9b2ccc3b Mon Sep 17 00:00:00 2001 From: Greg Mitchell Date: Tue, 5 May 2026 20:18:51 +0000 Subject: [PATCH 2/2] serviceability/create: drop redundant size pre-stamp on NewInterface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The NewInterface BorshSerialize derives the on-disk size from the actual body bytes and ignores the struct's `size` field — pre-computing it on the in-memory value before push_interface is a no-op. Drop the call here and in the new round-trip test. Per review feedback on #3670. --- .../src/processors/device/interface/create.rs | 9 +++++---- .../doublezero-serviceability/src/state/device.rs | 2 -- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/create.rs index daf11d512..374517057 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/create.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/device/interface/create.rs @@ -225,7 +225,10 @@ pub fn process_create_device_interface( } } - let mut new_iface = NewInterface { + // size is intentionally left at 0 — the NewInterface serializer derives the + // on-disk size fresh from the body bytes and ignores this field. It only + // gets populated on deserialize, from the wire prefix. + device.push_interface(NewInterface { size: 0, version: CURRENT_INTERFACE_SCHEMA_VERSION, status, @@ -243,9 +246,7 @@ pub fn process_create_device_interface( node_segment_idx, user_tunnel_endpoint: value.user_tunnel_endpoint, flex_algo_node_segments: vec![], - }; - new_iface.size = new_iface.compute_on_disk_size()?; - device.push_interface(new_iface); + }); try_acc_write(&device, device_account, payer_account, accounts)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/state/device.rs b/smartcontract/programs/doublezero-serviceability/src/state/device.rs index 571dea875..cf709c763 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/device.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/device.rs @@ -1881,8 +1881,6 @@ mod test_device_new_interfaces_vec { device.new_interfaces[0] .flex_algo_node_segments .push(segment.clone()); - // Keep the on-disk size field consistent with the populated body. - device.new_interfaces[0].size = device.new_interfaces[0].compute_on_disk_size().unwrap(); let bytes = borsh::to_vec(&device).unwrap(); let decoded = Device::try_from(&bytes[..]).unwrap();