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..374517057 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,12 @@ pub fn process_create_device_interface( } } - device.push_interface(CurrentInterfaceVersion { + // 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, name, interface_type, @@ -239,7 +245,8 @@ pub fn process_create_device_interface( ip_net, node_segment_idx, user_tunnel_endpoint: value.user_tunnel_endpoint, - })?; + flex_algo_node_segments: vec![], + }); 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..cf709c763 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,42 @@ 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()); + + 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); + } }