Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.

### Changes

- Smartcontract
- Canonicalize the forward-compatible Device interface names: rename the new struct from `NewInterface` to `Interface`, the legacy enum from `Interface` to `InterfaceDeprecated`, the `Device::new_interfaces` field to `Device::interfaces`, and the legacy `Device::interfaces` field to `Device::deprecated_interfaces`. On-disk format and behavior are unchanged — Borsh is positional, so the regenerated device fixture is byte-identical. Mechanical rename across the serviceability program, processors, CLI, sentinel, client, controlplane admin, and the Rust SDK. The activator/ crate was already deleted in an earlier change so the rename does not touch it ([#3663](https://github.com/malbeclabs/doublezero/issues/3663))
- Stop reading `Device::deprecated_interfaces` outside of backward-compat tests. The `delete_device` processor and `Device::validate` now check the canonical `Device::interfaces` vec; the legacy slot is treated as wire-format-only state populated by the deserializer and projected by the serializer. CLI, processor, and Rust SDK construction sites use `..Default::default()` to avoid initializing the legacy field. The unused `UpdateDeviceCommand::deprecated_interfaces` SDK command field is removed. `Device::deprecated_interfaces` itself is retained for backward-compat fixture tests that verify legacy-slot decoding ([#3663](https://github.com/malbeclabs/doublezero/issues/3663))
- Remove the `CurrentInterfaceVersion` type alias and the unused `Device::find_interface_legacy` helper. Tests that used to construct `CurrentInterfaceVersion {...}` (an `InterfaceV2` literal) and convert into either the canonical `Interface` or the legacy `InterfaceDeprecated` enum now build `Interface {...}` directly. The legacy enum's `into_current_version()` method is replaced by `to_v2()` for the few backward-compat sites that still need an `InterfaceV2` projection ([#3663](https://github.com/malbeclabs/doublezero/issues/3663))
- SDK
- Apply the same rename in the Go, Python, and TypeScript serviceability readers: Go gets `Device.DeprecatedInterfaces` ↔ `Device.Interfaces` (with the new `Interfaces` taking the trailing-vec slot previously held by `NewInterfaces`); Python gets `Device.deprecated_interfaces` ↔ `Device.interfaces`; TypeScript gets `Device.deprecatedInterfaces` ↔ `Device.interfaces`. The `Interface` / `DeviceInterface` element type in each SDK already represents the canonical (new) format and needs no rename. Length-mismatch error messages now reference the canonical field names ([#3663](https://github.com/malbeclabs/doublezero/issues/3663))
- Smartcontract
- Migrate read callers in the CLI, sentinel, client, controlplane admin, and Rust SDK topology helper to read interfaces from `Device::new_interfaces` instead of the legacy `interfaces` enum vec, and adopt the `Device::find_interface` signature that returns `&NewInterface`. The legacy `interfaces` slot is still written on-disk via the per-write V2 projection from #3667; this PR only migrates reads. The temporary `Device::find_interface_legacy` helper is retained for the smartcontract program processors, which migrate in a later issue. Activator is intentionally excluded — it is deprecated ([#3659](https://github.com/malbeclabs/doublezero/issues/3659))
- Activator
Expand Down
2 changes: 1 addition & 1 deletion client/doublezero/src/command/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1337,7 +1337,6 @@ mod tests {
dz_prefixes: format!("10.{}.0.0/24", device_number).parse().unwrap(),
mgmt_vrf: "default".to_string(),
interfaces: vec![],
new_interfaces: vec![],
max_users: 255,
users_count: 0,
device_health:
Expand All @@ -1351,6 +1350,7 @@ mod tests {
reserved_seats: 0,
multicast_publishers_count: 0,
max_multicast_publishers: 0,
..Default::default()
};
devices.insert(pk, device.clone());
(pk, device)
Expand Down
16 changes: 7 additions & 9 deletions client/doublezero/src/dzd_latency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub(crate) fn get_device_tunnel_endpoints(device: &Device) -> Vec<Ipv4Addr> {
let mut endpoints = vec![device.public_ip];

// Add all UserTunnelEndpoint interfaces
for iface in &device.new_interfaces {
for iface in &device.interfaces {
if iface.user_tunnel_endpoint && iface.ip_net != Default::default() {
endpoints.push(iface.ip_net.ip());
}
Expand Down Expand Up @@ -236,8 +236,8 @@ mod tests {
use crate::servicecontroller::{LatencyRecord, LatencyResponse, MockServiceController};
use doublezero_program_common::types::{NetworkV4, NetworkV4List};
use doublezero_sdk::{
AccountType, CurrentInterfaceVersion, Device, DeviceStatus, DeviceType, Interface,
InterfaceStatus, InterfaceType, LoopbackType,
AccountType, Device, DeviceStatus, DeviceType, Interface, InterfaceStatus, InterfaceType,
LoopbackType,
};
use doublezero_serviceability::state::interface::{InterfaceCYOA, InterfaceDIA, RoutingMode};
use solana_sdk::pubkey::Pubkey;
Expand All @@ -254,10 +254,10 @@ mod tests {
tunnel_endpoint_ips: Vec<Ipv4Addr>,
) -> (Pubkey, Device) {
let pubkey = Pubkey::new_unique();
let v2_ifaces: Vec<CurrentInterfaceVersion> = tunnel_endpoint_ips
let interfaces: Vec<Interface> = tunnel_endpoint_ips
.into_iter()
.enumerate()
.map(|(i, ip)| CurrentInterfaceVersion {
.map(|(i, ip)| Interface {
status: InterfaceStatus::Activated,
name: format!("Loopback{}", i),
interface_type: InterfaceType::Loopback,
Expand All @@ -272,11 +272,9 @@ mod tests {
ip_net: NetworkV4::new(ip, 32).unwrap(),
node_segment_idx: 0,
user_tunnel_endpoint: true,
..Default::default()
})
.collect();
let interfaces: Vec<Interface> =
v2_ifaces.iter().map(|v| Interface::V2(v.clone())).collect();
let new_interfaces = v2_ifaces.iter().map(|v| v.try_into().unwrap()).collect();
(
pubkey,
Device {
Expand All @@ -295,7 +293,6 @@ mod tests {
contributor_pk: Pubkey::default(),
mgmt_vrf: "default".to_string(),
interfaces,
new_interfaces,
reference_count: 0,
users_count,
max_users: 1,
Expand All @@ -310,6 +307,7 @@ mod tests {
reserved_seats: 0,
multicast_publishers_count: 0,
max_multicast_publishers: 0,
..Default::default()
},
)
}
Expand Down
4 changes: 1 addition & 3 deletions controlplane/doublezero-admin/src/cli/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ impl MigrateMulticastCountsCommand {
contributor_pk: None,
location_pk: None,
mgmt_vrf: None,
interfaces: None,
max_users: None,
users_count: None,
status: None,
Expand Down Expand Up @@ -257,7 +256,6 @@ impl MigrateUnicastCountsCommand {
contributor_pk: None,
location_pk: None,
mgmt_vrf: None,
interfaces: None,
max_users: None,
users_count: None,
status: None,
Expand Down Expand Up @@ -312,7 +310,6 @@ mod tests {
dz_prefixes: "10.0.0.1/32".parse().unwrap(),
mgmt_vrf: "default".to_string(),
interfaces: vec![],
new_interfaces: vec![],
max_users: 255,
users_count: 0,
device_health: DeviceHealth::ReadyForUsers,
Expand All @@ -324,6 +321,7 @@ mod tests {
reserved_seats: 0,
multicast_publishers_count: pub_count,
max_multicast_publishers: 0,
..Default::default()
}
}

Expand Down
2 changes: 1 addition & 1 deletion controlplane/doublezero-admin/src/cli/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ impl FlexAlgoMigrateCliCommand {
let mut devices_needing_backfill: Vec<Pubkey> = Vec::new();

for (device_pubkey, device) in &device_entries {
let needs_backfill = device.new_interfaces.iter().any(|iface| {
let needs_backfill = device.interfaces.iter().any(|iface| {
iface.loopback_type == LoopbackType::Vpnv4
&& !iface
.flex_algo_node_segments
Expand Down
2 changes: 1 addition & 1 deletion crates/sentinel/src/dz_ledger_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ pub fn fetch_device_infos(
.copied()
.unwrap_or((0.0, 0.0));
let user_tunnel_endpoints = device
.new_interfaces
.interfaces
.iter()
.filter_map(|iface| {
if iface.user_tunnel_endpoint && iface.ip_net != Default::default() {
Expand Down
2 changes: 1 addition & 1 deletion crates/sentinel/src/multicast_publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ impl MulticastDzLedgerClient for RpcMulticastDzLedgerClient {
continue;
}
let user_tunnel_endpoints = device
.new_interfaces
.interfaces
.iter()
.filter_map(|iface| {
if iface.user_tunnel_endpoint && iface.ip_net != Default::default() {
Expand Down
40 changes: 20 additions & 20 deletions sdk/serviceability/python/serviceability/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ def __str__(self) -> str:
# Account dataclasses
# ---------------------------------------------------------------------------

# On-wire schema version for the size-prefixed NewInterface format
# On-wire schema version for the size-prefixed Interface format
# (matches Rust's CURRENT_INTERFACE_SCHEMA_VERSION). Note: prior to issue #3660
# this constant gated the legacy enum reader at value 2 (max known disc=1); it
# is now bumped to 4 to match the size-prefixed schema. Legacy enum reads still
Expand Down Expand Up @@ -482,7 +482,7 @@ def from_reader(cls, r: DefensiveReader) -> Interface:

@classmethod
def from_reader_sized(cls, r: DefensiveReader) -> Interface:
"""Read a single size-prefixed NewInterface element.
"""Read a single size-prefixed Interface element.

Wire format: u16 size (incl. 3-byte prefix) + u8 version + body. After
reading the known body fields, the reader is advanced to start+size so
Expand Down Expand Up @@ -681,7 +681,7 @@ class Device:
metrics_publisher_pub_key: Pubkey = Pubkey.default()
contributor_pub_key: Pubkey = Pubkey.default()
mgmt_vrf: str = ""
interfaces: list[Interface] = field(default_factory=list)
deprecated_interfaces: list[Interface] = field(default_factory=list)
reference_count: int = 0
users_count: int = 0
max_users: int = 0
Expand All @@ -694,11 +694,11 @@ class Device:
reserved_seats: int = 0
multicast_publishers_count: int = 0
max_multicast_publishers: int = 0
# new_interfaces is the trailing size-prefixed vec parallel to interfaces. For
# legacy accounts (no trailing bytes), this is rebuilt from interfaces by
# from_bytes. When populated from the wire, len(new_interfaces) ==
# len(interfaces) is enforced.
new_interfaces: list[Interface] = field(default_factory=list)
# interfaces is the trailing size-prefixed vec parallel to deprecated_interfaces.
# For legacy accounts (no trailing bytes), this is rebuilt from deprecated_interfaces
# by from_bytes. When populated from the wire, len(interfaces) ==
# len(deprecated_interfaces) is enforced.
interfaces: list[Interface] = field(default_factory=list)

@classmethod
def from_bytes(cls, data: bytes) -> Device:
Expand All @@ -719,7 +719,7 @@ def from_bytes(cls, data: bytes) -> Device:
dev.contributor_pub_key = _read_pubkey(r)
dev.mgmt_vrf = r.read_string()
iface_len = r.read_u32()
dev.interfaces = [Interface.from_reader(r) for _ in range(iface_len)]
dev.deprecated_interfaces = [Interface.from_reader(r) for _ in range(iface_len)]
dev.reference_count = r.read_u32()
dev.users_count = r.read_u16()
dev.max_users = r.read_u16()
Expand All @@ -733,13 +733,13 @@ def from_bytes(cls, data: bytes) -> Device:
dev.multicast_publishers_count = r.read_u16()
dev.max_multicast_publishers = r.read_u16()

# Trailing new_interfaces vec (size-prefixed). Empty trailing => rebuild
# from legacy. Non-empty trailing whose declared length differs from the
# legacy interfaces length is a corrupt-account condition; raise per
# Rust device-reader semantics (length mismatch is fatal).
# Trailing interfaces vec (size-prefixed). Empty trailing => rebuild
# from deprecated_interfaces. Non-empty trailing whose declared length
# differs from the deprecated_interfaces length is a corrupt-account
# condition; raise per Rust device-reader semantics (length mismatch is fatal).
if r.remaining == 0:
dev.new_interfaces = []
for legacy in dev.interfaces:
dev.interfaces = []
for legacy in dev.deprecated_interfaces:
rebuilt = Interface(
size=0,
version=CURRENT_INTERFACE_VERSION,
Expand All @@ -759,15 +759,15 @@ def from_bytes(cls, data: bytes) -> Device:
user_tunnel_endpoint=legacy.user_tunnel_endpoint,
flex_algo_node_segments=list(legacy.flex_algo_node_segments),
)
dev.new_interfaces.append(rebuilt)
dev.interfaces.append(rebuilt)
else:
new_len = r.read_u32()
if new_len != len(dev.interfaces):
if new_len != len(dev.deprecated_interfaces):
raise ValueError(
f"Device new_interfaces length {new_len} != "
f"interfaces length {len(dev.interfaces)}"
f"Device interfaces length {new_len} != "
f"deprecated_interfaces length {len(dev.deprecated_interfaces)}"
)
dev.new_interfaces = [
dev.interfaces = [
Interface.from_reader_sized(r) for _ in range(new_len)
]

Expand Down
50 changes: 25 additions & 25 deletions sdk/serviceability/python/serviceability/tests/test_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,22 +170,22 @@ def test_deserialize(self):
)
# Legacy slot is the V2 projection of new_interfaces (always V2 per #3653);
# both entries carry version 1 and no FlexAlgoNodeSegments.
assert len(dev.deprecated_interfaces) == 2
assert dev.deprecated_interfaces[0].version == 1
assert dev.deprecated_interfaces[0].name == "Loopback0"
assert dev.deprecated_interfaces[0].flex_algo_node_segments == []
assert dev.deprecated_interfaces[1].version == 1
assert dev.deprecated_interfaces[1].name == "Ethernet1"
# Trailing new_interfaces vec carries the full V4 Interface bodies.
assert len(dev.interfaces) == 2
assert dev.interfaces[0].version == 1
assert dev.interfaces[0].name == "Loopback0"
assert dev.interfaces[0].flex_algo_node_segments == []
assert dev.interfaces[1].version == 1
assert dev.interfaces[1].name == "Ethernet1"
# Trailing new_interfaces vec carries the full V4 NewInterface bodies.
assert len(dev.new_interfaces) == 2
ni0 = dev.new_interfaces[0]
ni0 = dev.interfaces[0]
assert ni0.version == CURRENT_INTERFACE_VERSION
assert ni0.name == "Loopback0"
assert ni0.loopback_type.value == 1 # Vpnv4
assert len(ni0.flex_algo_node_segments) == 1
assert ni0.flex_algo_node_segments[0].node_segment_idx == 300
assert ni0.size == _expected_new_interface_size(ni0)
ni1 = dev.new_interfaces[1]
ni1 = dev.interfaces[1]
assert ni1.version == CURRENT_INTERFACE_VERSION
assert ni1.name == "Ethernet1"
assert ni1.user_tunnel_endpoint is True
Expand All @@ -206,10 +206,10 @@ def test_deserialize(self):


def _expected_new_interface_size(ni) -> int:
"""Recompute the on-disk size for a NewInterface element so tests don't bake
"""Recompute the on-disk size for a Interface element so tests don't bake
body byte counts as magic numbers.

Layout (matches Rust NewInterface::serialize_body, interface.rs:641-658):
Layout (matches Rust Interface::serialize_body, interface.rs:641-658):
u16 size + u8 version (3 bytes prefix) +
u8 status + (u32+len) name + u8 interface_type + u8 cyoa + u8 dia +
u8 loopback_type + u64 bandwidth + u64 cir + u16 mtu + u8 routing_mode +
Expand All @@ -234,22 +234,22 @@ def test_deserialize(self):
dev = Device.from_bytes(data)
assert meta["name"] == "DeviceLegacy"
# Legacy slot mirrors the original V1+V2 hand-serialized shape.
assert len(dev.interfaces) == 2
assert dev.interfaces[0].version == 0 # V1
assert dev.interfaces[0].name == "Loopback0"
assert dev.interfaces[1].version == 1 # V2
assert dev.interfaces[1].name == "Ethernet1"
assert len(dev.deprecated_interfaces) == 2
assert dev.deprecated_interfaces[0].version == 0 # V1
assert dev.deprecated_interfaces[0].name == "Loopback0"
assert dev.deprecated_interfaces[1].version == 1 # V2
assert dev.deprecated_interfaces[1].name == "Ethernet1"
# Rebuilt new_interfaces: same field values as the legacy entries, but
# stamped with the current schema version and zero on-disk size.
assert len(dev.new_interfaces) == 2
for ni in dev.new_interfaces:
assert len(dev.interfaces) == 2
for ni in dev.interfaces:
assert ni.version == CURRENT_INTERFACE_VERSION
assert ni.size == 0
assert ni.flex_algo_node_segments == []
assert dev.new_interfaces[0].name == "Loopback0"
assert dev.new_interfaces[0].loopback_type.value == 1 # Vpnv4
assert dev.new_interfaces[1].name == "Ethernet1"
assert dev.new_interfaces[1].user_tunnel_endpoint is True
assert dev.interfaces[0].name == "Loopback0"
assert dev.interfaces[0].loopback_type.value == 1 # Vpnv4
assert dev.interfaces[1].name == "Ethernet1"
assert dev.interfaces[1].user_tunnel_endpoint is True


class TestFixtureDeviceFutureVersion:
Expand All @@ -263,16 +263,16 @@ def test_deserialize(self):
data, meta = _load_fixture("device_future_version")
dev = Device.from_bytes(data)
assert meta["name"] == "DeviceFutureVersion"
assert len(dev.new_interfaces) == 2
assert len(dev.interfaces) == 2
# First element parses normally at the current schema version.
ni0 = dev.new_interfaces[0]
ni0 = dev.interfaces[0]
assert ni0.version == CURRENT_INTERFACE_VERSION
assert ni0.name == "Loopback0"
assert len(ni0.flex_algo_node_segments) == 1
# Second element has the future-version stamp + extra trailing bytes;
# known body fields still parse correctly because the reader advances
# to start+size.
ni1 = dev.new_interfaces[1]
ni1 = dev.interfaces[1]
assert ni1.version == 5
assert ni1.size == _expected_new_interface_size(ni1) + 8
assert ni1.name == "Ethernet1"
Expand Down
Loading
Loading