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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ All notable changes to this project will be documented in this file.
- 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))
- SDK
- Go, Python, and TypeScript serviceability readers parse the trailing `new_interfaces` vec on `Device` with size-prefixed (u16 size + u8 version + body) forward-compat framing. Empty trailing falls back to rebuilding `new_interfaces` from the legacy enum vec, matching the Rust device reader. Length mismatch between the legacy and trailing vecs is surfaced as an error (Python/TS raise; Go sets `Device.DeserializeError`). Bumps `CURRENT_INTERFACE_VERSION` / `CurrentInterfaceVersion` to `4` across SDKs to match Rust's `CURRENT_INTERFACE_SCHEMA_VERSION` ([#3660](https://github.com/malbeclabs/doublezero/issues/3660))
- Regenerate `device.{bin,json}` through Device's custom serializer with a populated `new_interfaces` vec (one Vpnv4 loopback carrying a `FlexAlgoNodeSegment`, one physical user-tunnel-endpoint), and add `device_legacy.{bin,json}` (legacy `interfaces` vec only, no trailing bytes — exercises the SDK legacy-fallback path) and `device_future_version.{bin,json}` (last trailing-vec element doctored to `version=5` with 8 trailing junk bytes — exercises the SDK skip-to-end path). Adds fixture-driven Go SDK tests; extends the existing Python/TS fixture tests to cover all three Device fixtures ([#3661](https://github.com/malbeclabs/doublezero/issues/3661))

## [v0.21.0](https://github.com/malbeclabs/doublezero/compare/client/v0.20.0...client/v0.21.0) - 2026-05-01

Expand Down
96 changes: 95 additions & 1 deletion sdk/serviceability/python/serviceability/tests/test_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from solders.pubkey import Pubkey # type: ignore[import-untyped]

from serviceability.state import (
CURRENT_INTERFACE_VERSION,
AccessPass,
BGPStatus,
Contributor,
Expand Down Expand Up @@ -167,10 +168,29 @@ def test_deserialize(self):
"ContributorPk": dev.contributor_pub_key,
},
)
# Verify interfaces
# Legacy slot is the V2 projection of new_interfaces (always V2 per #3653);
# both entries carry version 1 and no FlexAlgoNodeSegments.
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]
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]
assert ni1.version == CURRENT_INTERFACE_VERSION
assert ni1.name == "Ethernet1"
assert ni1.user_tunnel_endpoint is True
assert ni1.flex_algo_node_segments == []
assert ni1.size == _expected_new_interface_size(ni1)
# Verify dz_prefixes
import ipaddress

Expand All @@ -185,6 +205,80 @@ def test_deserialize(self):
assert dev.index == 7


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

Layout (matches Rust NewInterface::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 +
u16 vlan_id + 5-byte ip_net + u16 node_segment_idx + u8 user_tunnel_endpoint +
(u32+34*N) flex_algo_node_segments
"""
body_len = (
1 + (4 + len(ni.name)) + 1 + 1 + 1 + 1 + 8 + 8 + 2 + 1 + 2 + 5 + 2 + 1
+ (4 + 34 * len(ni.flex_algo_node_segments))
)
return 3 + body_len


class TestFixtureDeviceLegacy:
"""Pre-#3667 on-disk format: legacy `interfaces` vec only, no trailing
`new_interfaces`. The SDK rebuilds new_interfaces from the legacy vec,
stamping each entry with version=CURRENT_INTERFACE_VERSION and size=0.
"""

def test_deserialize(self):
data, meta = _load_fixture("device_legacy")
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"
# 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 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


class TestFixtureDeviceFutureVersion:
"""Same on-disk shape as device.bin, but the last trailing-vec element is
doctored with version=5 (a hypothetical future schema) and 8 trailing junk
bytes after the known body. The SDK reads the known fields then advances
to start+size, skipping the junk.
"""

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
# First element parses normally at the current schema version.
ni0 = dev.new_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]
assert ni1.version == 5
assert ni1.size == _expected_new_interface_size(ni1) + 8
assert ni1.name == "Ethernet1"
assert ni1.user_tunnel_endpoint is True


class TestFixtureLink:
def test_deserialize(self):
data, meta = _load_fixture("link")
Expand Down
Binary file modified sdk/serviceability/testdata/fixtures/device.bin
Binary file not shown.
217 changes: 216 additions & 1 deletion sdk/serviceability/testdata/fixtures/device.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
},
{
"name": "Interface0Version",
"value": "0",
"value": "1",
"typ": "u8"
},
{
Expand All @@ -102,11 +102,41 @@
"value": "1",
"typ": "u8"
},
{
"name": "Interface0InterfaceCYOA",
"value": "0",
"typ": "u8"
},
{
"name": "Interface0InterfaceDIA",
"value": "0",
"typ": "u8"
},
{
"name": "Interface0LoopbackType",
"value": "1",
"typ": "u8"
},
{
"name": "Interface0Bandwidth",
"value": "0",
"typ": "u64"
},
{
"name": "Interface0Cir",
"value": "0",
"typ": "u64"
},
{
"name": "Interface0Mtu",
"value": "9000",
"typ": "u16"
},
{
"name": "Interface0RoutingMode",
"value": "0",
"typ": "u8"
},
{
"name": "Interface0VlanId",
"value": "0",
Expand Down Expand Up @@ -261,6 +291,191 @@
"name": "MaxMulticastPublishers",
"value": "10",
"typ": "u16"
},
{
"name": "NewInterfacesLen",
"value": "2",
"typ": "u32"
},
{
"name": "NewInterface0Size",
"value": "88",
"typ": "u16"
},
{
"name": "NewInterface0Version",
"value": "4",
"typ": "u8"
},
{
"name": "NewInterface0Status",
"value": "3",
"typ": "u8"
},
{
"name": "NewInterface0Name",
"value": "Loopback0",
"typ": "string"
},
{
"name": "NewInterface0InterfaceType",
"value": "1",
"typ": "u8"
},
{
"name": "NewInterface0InterfaceCYOA",
"value": "0",
"typ": "u8"
},
{
"name": "NewInterface0InterfaceDIA",
"value": "0",
"typ": "u8"
},
{
"name": "NewInterface0LoopbackType",
"value": "1",
"typ": "u8"
},
{
"name": "NewInterface0Bandwidth",
"value": "0",
"typ": "u64"
},
{
"name": "NewInterface0Cir",
"value": "0",
"typ": "u64"
},
{
"name": "NewInterface0Mtu",
"value": "9000",
"typ": "u16"
},
{
"name": "NewInterface0RoutingMode",
"value": "0",
"typ": "u8"
},
{
"name": "NewInterface0VlanId",
"value": "0",
"typ": "u16"
},
{
"name": "NewInterface0IpNet",
"value": "10.0.0.1/32",
"typ": "networkv4"
},
{
"name": "NewInterface0NodeSegmentIdx",
"value": "100",
"typ": "u16"
},
{
"name": "NewInterface0UserTunnelEndpoint",
"value": "false",
"typ": "bool"
},
{
"name": "NewInterface0FlexAlgoNodeSegmentsLen",
"value": "1",
"typ": "u32"
},
{
"name": "NewInterface0FlexAlgoNodeSegments0Topology",
"value": "5eM8cB4pfhAGBjJE6pNomwRmrfzBiZ2ZHrHGjTQfjWco",
"typ": "pubkey"
},
{
"name": "NewInterface0FlexAlgoNodeSegments0NodeSegmentIdx",
"value": "300",
"typ": "u16"
},
{
"name": "NewInterface1Size",
"value": "54",
"typ": "u16"
},
{
"name": "NewInterface1Version",
"value": "4",
"typ": "u8"
},
{
"name": "NewInterface1Status",
"value": "3",
"typ": "u8"
},
{
"name": "NewInterface1Name",
"value": "Ethernet1",
"typ": "string"
},
{
"name": "NewInterface1InterfaceType",
"value": "2",
"typ": "u8"
},
{
"name": "NewInterface1InterfaceCYOA",
"value": "1",
"typ": "u8"
},
{
"name": "NewInterface1InterfaceDIA",
"value": "1",
"typ": "u8"
},
{
"name": "NewInterface1LoopbackType",
"value": "0",
"typ": "u8"
},
{
"name": "NewInterface1Bandwidth",
"value": "10000000000",
"typ": "u64"
},
{
"name": "NewInterface1Cir",
"value": "5000000000",
"typ": "u64"
},
{
"name": "NewInterface1Mtu",
"value": "9000",
"typ": "u16"
},
{
"name": "NewInterface1RoutingMode",
"value": "1",
"typ": "u8"
},
{
"name": "NewInterface1VlanId",
"value": "100",
"typ": "u16"
},
{
"name": "NewInterface1IpNet",
"value": "172.16.0.1/30",
"typ": "networkv4"
},
{
"name": "NewInterface1NodeSegmentIdx",
"value": "200",
"typ": "u16"
},
{
"name": "NewInterface1UserTunnelEndpoint",
"value": "true",
"typ": "bool"
},
{
"name": "NewInterface1FlexAlgoNodeSegmentsLen",
"value": "0",
"typ": "u32"
}
]
}
Binary file not shown.
Loading
Loading