From 0c7036c72bf5e07970786334a3768600a8d9c765 Mon Sep 17 00:00:00 2001 From: Greg Mitchell Date: Wed, 6 May 2026 03:23:16 +0000 Subject: [PATCH] sdk: regenerate device fixtures with legacy and future-version variants Updates `device.{bin,json}` to the post-#3667 on-disk shape (custom Device serializer, populated `new_interfaces` with one Vpnv4 loopback carrying a FlexAlgoNodeSegment and one physical user-tunnel-endpoint). Adds `device_legacy.{bin,json}` (no trailing vec, exercises the SDK legacy-fallback path) and `device_future_version.{bin,json}` (last trailing element doctored to version=5 with 8 trailing junk bytes, exercises the SDK skip-to-end path). Adds Go fixture-driven tests; extends Python/TS fixture tests to cover all three Device fixtures. Refs #3661. --- CHANGELOG.md | 1 + .../serviceability/tests/test_fixtures.py | 96 +++- .../testdata/fixtures/device.bin | Bin 311 -> 478 bytes .../testdata/fixtures/device.json | 217 +++++++- .../fixtures/device_future_version.bin | Bin 0 -> 486 bytes .../fixtures/device_future_version.json | 481 ++++++++++++++++++ .../testdata/fixtures/device_legacy.bin | Bin 0 -> 311 bytes .../testdata/fixtures/device_legacy.json | 431 ++++++++++++++++ .../fixtures/generate-fixtures/src/main.rs | 350 ++++++++++++- sdk/serviceability/testdata/fixtures/link.bin | Bin 235 -> 238 bytes .../serviceability/tests/fixtures.test.ts | 103 +++- .../fixtures/generate-fixtures/Cargo.lock | 8 +- .../sdk/go/serviceability/fixture_test.go | 163 ++++++ 13 files changed, 1828 insertions(+), 22 deletions(-) create mode 100644 sdk/serviceability/testdata/fixtures/device_future_version.bin create mode 100644 sdk/serviceability/testdata/fixtures/device_future_version.json create mode 100644 sdk/serviceability/testdata/fixtures/device_legacy.bin create mode 100644 sdk/serviceability/testdata/fixtures/device_legacy.json create mode 100644 smartcontract/sdk/go/serviceability/fixture_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ffdbff036a..ec9c8afc1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/sdk/serviceability/python/serviceability/tests/test_fixtures.py b/sdk/serviceability/python/serviceability/tests/test_fixtures.py index 3ec3062e8b..a088b1981c 100644 --- a/sdk/serviceability/python/serviceability/tests/test_fixtures.py +++ b/sdk/serviceability/python/serviceability/tests/test_fixtures.py @@ -6,6 +6,7 @@ from solders.pubkey import Pubkey # type: ignore[import-untyped] from serviceability.state import ( + CURRENT_INTERFACE_VERSION, AccessPass, BGPStatus, Contributor, @@ -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 @@ -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") diff --git a/sdk/serviceability/testdata/fixtures/device.bin b/sdk/serviceability/testdata/fixtures/device.bin index 8554f47fb112076f8832b9d02e1856f69172d6a7..0bec98a9a99e831b471cdb74b0fcf81260c5924d 100644 GIT binary patch delta 128 zcmdnabdPz$6*Wd?P6h@BpZxrSq{QTG14af0AVLKi$_x{q23a!!MI#tkh*HnMzy&l& cAq8TZ>%@uT6Blr>>M)uyuuS}*&jK64#dkd&C5Z2+_bWDN??PzG{=_A)A@z|^~zWTY15rIr{nF#?r8 z;SK>A#qfz$3#3^?nK6Z7jQ|6q+zAFo9-sg#Lka^k&=*V$MnK36w1ErAW?+b5U?J6Z Zpe|P+#s_p5?HE|W<`VBpka?@2005MBCglJC literal 0 HcmV?d00001 diff --git a/sdk/serviceability/testdata/fixtures/device_future_version.json b/sdk/serviceability/testdata/fixtures/device_future_version.json new file mode 100644 index 0000000000..52d77513ed --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/device_future_version.json @@ -0,0 +1,481 @@ +{ + "name": "DeviceFutureVersion", + "account_type": 5, + "fields": [ + { + "name": "AccountType", + "value": "5", + "typ": "u8" + }, + { + "name": "Owner", + "value": "5Jq6NhSQCWgh9GhMZJ3aJJhdZdALBoX2NWjqDUrh8VK5", + "typ": "pubkey" + }, + { + "name": "Index", + "value": "7", + "typ": "u128" + }, + { + "name": "BumpSeed", + "value": "250", + "typ": "u8" + }, + { + "name": "LocationPk", + "value": "5NjW2CAV6MBQYxpL4oK2CESrpdj6tkcvxP3iigAgrHyR", + "typ": "pubkey" + }, + { + "name": "ExchangePk", + "value": "5SdufgtZzBg7xewJaJaU6AC65eHsbhiqYFMcDsUga6dm", + "typ": "pubkey" + }, + { + "name": "DeviceType", + "value": "2", + "typ": "u8" + }, + { + "name": "PublicIp", + "value": "203.0.113.1", + "typ": "ipv4" + }, + { + "name": "Status", + "value": "1", + "typ": "u8" + }, + { + "name": "Code", + "value": "dz1", + "typ": "string" + }, + { + "name": "DzPrefixesLen", + "value": "1", + "typ": "u32" + }, + { + "name": "DzPrefixes0", + "value": "10.10.0.0/24", + "typ": "networkv4" + }, + { + "name": "MetricsPublisherPk", + "value": "5WYKKBcet2AqNM4H5oquz5wKLereJepk87fVj4ngHuJ7", + "typ": "pubkey" + }, + { + "name": "ContributorPk", + "value": "5aSixgLjmrfYn3BFbK7Mt1gYbfRR1bvehyyPEG6g1hxT", + "typ": "pubkey" + }, + { + "name": "MgmtVrf", + "value": "mgmt", + "typ": "string" + }, + { + "name": "InterfacesLen", + "value": "2", + "typ": "u32" + }, + { + "name": "Interface0Version", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface0Status", + "value": "3", + "typ": "u8" + }, + { + "name": "Interface0Name", + "value": "Loopback0", + "typ": "string" + }, + { + "name": "Interface0InterfaceType", + "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", + "typ": "u16" + }, + { + "name": "Interface0IpNet", + "value": "10.0.0.1/32", + "typ": "networkv4" + }, + { + "name": "Interface0NodeSegmentIdx", + "value": "100", + "typ": "u16" + }, + { + "name": "Interface0UserTunnelEndpoint", + "value": "false", + "typ": "bool" + }, + { + "name": "Interface1Version", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface1Status", + "value": "3", + "typ": "u8" + }, + { + "name": "Interface1Name", + "value": "Ethernet1", + "typ": "string" + }, + { + "name": "Interface1InterfaceType", + "value": "2", + "typ": "u8" + }, + { + "name": "Interface1InterfaceCYOA", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface1InterfaceDIA", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface1LoopbackType", + "value": "0", + "typ": "u8" + }, + { + "name": "Interface1Bandwidth", + "value": "10000000000", + "typ": "u64" + }, + { + "name": "Interface1Cir", + "value": "5000000000", + "typ": "u64" + }, + { + "name": "Interface1Mtu", + "value": "9000", + "typ": "u16" + }, + { + "name": "Interface1RoutingMode", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface1VlanId", + "value": "100", + "typ": "u16" + }, + { + "name": "Interface1IpNet", + "value": "172.16.0.1/30", + "typ": "networkv4" + }, + { + "name": "Interface1NodeSegmentIdx", + "value": "200", + "typ": "u16" + }, + { + "name": "Interface1UserTunnelEndpoint", + "value": "true", + "typ": "bool" + }, + { + "name": "ReferenceCount", + "value": "12", + "typ": "u32" + }, + { + "name": "UsersCount", + "value": "5", + "typ": "u16" + }, + { + "name": "MaxUsers", + "value": "100", + "typ": "u16" + }, + { + "name": "DeviceHealth", + "value": "3", + "typ": "u8" + }, + { + "name": "DesiredStatus", + "value": "1", + "typ": "u8" + }, + { + "name": "UnicastUsersCount", + "value": "3", + "typ": "u16" + }, + { + "name": "MulticastSubscribersCount", + "value": "2", + "typ": "u16" + }, + { + "name": "MaxUnicastUsers", + "value": "50", + "typ": "u16" + }, + { + "name": "MaxMulticastSubscribers", + "value": "50", + "typ": "u16" + }, + { + "name": "ReservedSeats", + "value": "3", + "typ": "u16" + }, + { + "name": "MulticastPublishersCount", + "value": "1", + "typ": "u16" + }, + { + "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": "62", + "typ": "u16" + }, + { + "name": "NewInterface1Version", + "value": "5", + "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" + } + ] +} \ No newline at end of file diff --git a/sdk/serviceability/testdata/fixtures/device_legacy.bin b/sdk/serviceability/testdata/fixtures/device_legacy.bin new file mode 100644 index 0000000000000000000000000000000000000000..8554f47fb112076f8832b9d02e1856f69172d6a7 GIT binary patch literal 311 zcmZQ|V89R9(WHMlq6wgLozMl)_)Mo63K1 o3*;OPWyTbSH3AHbawixVd4K|}3@HrEKnF817y%(O10&Eh0J}dEuK)l5 literal 0 HcmV?d00001 diff --git a/sdk/serviceability/testdata/fixtures/device_legacy.json b/sdk/serviceability/testdata/fixtures/device_legacy.json new file mode 100644 index 0000000000..0bd2427cac --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/device_legacy.json @@ -0,0 +1,431 @@ +{ + "name": "DeviceLegacy", + "account_type": 5, + "fields": [ + { + "name": "AccountType", + "value": "5", + "typ": "u8" + }, + { + "name": "Owner", + "value": "5Jq6NhSQCWgh9GhMZJ3aJJhdZdALBoX2NWjqDUrh8VK5", + "typ": "pubkey" + }, + { + "name": "Index", + "value": "7", + "typ": "u128" + }, + { + "name": "BumpSeed", + "value": "250", + "typ": "u8" + }, + { + "name": "LocationPk", + "value": "5NjW2CAV6MBQYxpL4oK2CESrpdj6tkcvxP3iigAgrHyR", + "typ": "pubkey" + }, + { + "name": "ExchangePk", + "value": "5SdufgtZzBg7xewJaJaU6AC65eHsbhiqYFMcDsUga6dm", + "typ": "pubkey" + }, + { + "name": "DeviceType", + "value": "2", + "typ": "u8" + }, + { + "name": "PublicIp", + "value": "203.0.113.1", + "typ": "ipv4" + }, + { + "name": "Status", + "value": "1", + "typ": "u8" + }, + { + "name": "Code", + "value": "dz1", + "typ": "string" + }, + { + "name": "DzPrefixesLen", + "value": "1", + "typ": "u32" + }, + { + "name": "DzPrefixes0", + "value": "10.10.0.0/24", + "typ": "networkv4" + }, + { + "name": "MetricsPublisherPk", + "value": "5WYKKBcet2AqNM4H5oquz5wKLereJepk87fVj4ngHuJ7", + "typ": "pubkey" + }, + { + "name": "ContributorPk", + "value": "5aSixgLjmrfYn3BFbK7Mt1gYbfRR1bvehyyPEG6g1hxT", + "typ": "pubkey" + }, + { + "name": "MgmtVrf", + "value": "mgmt", + "typ": "string" + }, + { + "name": "InterfacesLen", + "value": "2", + "typ": "u32" + }, + { + "name": "Interface0Version", + "value": "0", + "typ": "u8" + }, + { + "name": "Interface0Status", + "value": "3", + "typ": "u8" + }, + { + "name": "Interface0Name", + "value": "Loopback0", + "typ": "string" + }, + { + "name": "Interface0InterfaceType", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface0LoopbackType", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface0VlanId", + "value": "0", + "typ": "u16" + }, + { + "name": "Interface0IpNet", + "value": "10.0.0.1/32", + "typ": "networkv4" + }, + { + "name": "Interface0NodeSegmentIdx", + "value": "100", + "typ": "u16" + }, + { + "name": "Interface0UserTunnelEndpoint", + "value": "false", + "typ": "bool" + }, + { + "name": "Interface1Version", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface1Status", + "value": "3", + "typ": "u8" + }, + { + "name": "Interface1Name", + "value": "Ethernet1", + "typ": "string" + }, + { + "name": "Interface1InterfaceType", + "value": "2", + "typ": "u8" + }, + { + "name": "Interface1InterfaceCYOA", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface1InterfaceDIA", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface1LoopbackType", + "value": "0", + "typ": "u8" + }, + { + "name": "Interface1Bandwidth", + "value": "10000000000", + "typ": "u64" + }, + { + "name": "Interface1Cir", + "value": "5000000000", + "typ": "u64" + }, + { + "name": "Interface1Mtu", + "value": "9000", + "typ": "u16" + }, + { + "name": "Interface1RoutingMode", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface1VlanId", + "value": "100", + "typ": "u16" + }, + { + "name": "Interface1IpNet", + "value": "172.16.0.1/30", + "typ": "networkv4" + }, + { + "name": "Interface1NodeSegmentIdx", + "value": "200", + "typ": "u16" + }, + { + "name": "Interface1UserTunnelEndpoint", + "value": "true", + "typ": "bool" + }, + { + "name": "ReferenceCount", + "value": "12", + "typ": "u32" + }, + { + "name": "UsersCount", + "value": "5", + "typ": "u16" + }, + { + "name": "MaxUsers", + "value": "100", + "typ": "u16" + }, + { + "name": "DeviceHealth", + "value": "3", + "typ": "u8" + }, + { + "name": "DesiredStatus", + "value": "1", + "typ": "u8" + }, + { + "name": "UnicastUsersCount", + "value": "3", + "typ": "u16" + }, + { + "name": "MulticastSubscribersCount", + "value": "2", + "typ": "u16" + }, + { + "name": "MaxUnicastUsers", + "value": "50", + "typ": "u16" + }, + { + "name": "MaxMulticastSubscribers", + "value": "50", + "typ": "u16" + }, + { + "name": "ReservedSeats", + "value": "3", + "typ": "u16" + }, + { + "name": "MulticastPublishersCount", + "value": "1", + "typ": "u16" + }, + { + "name": "MaxMulticastPublishers", + "value": "10", + "typ": "u16" + }, + { + "name": "NewInterfacesLen", + "value": "2", + "typ": "u32" + }, + { + "name": "NewInterface0Size", + "value": "0", + "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": "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": "0", + "typ": "u32" + }, + { + "name": "NewInterface1Size", + "value": "0", + "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" + } + ] +} \ No newline at end of file diff --git a/sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs b/sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs index 48c1ecc657..53d30da397 100644 --- a/sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs +++ b/sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs @@ -28,13 +28,14 @@ use doublezero_serviceability::state::{ globalstate::GlobalState, interface::{ Interface, InterfaceCYOA, InterfaceDIA, InterfaceStatus, InterfaceType, InterfaceV1, - InterfaceV2, LoopbackType, RoutingMode, + InterfaceV2, LoopbackType, NewInterface, RoutingMode, CURRENT_INTERFACE_SCHEMA_VERSION, }, link::{Link, LinkDesiredStatus, LinkHealth, LinkLinkType, LinkStatus}, location::{Location, LocationStatus}, multicastgroup::{MulticastGroup, MulticastGroupStatus}, programconfig::ProgramConfig, tenant::{Tenant, TenantBillingConfig, TenantPaymentStatus}, + topology::FlexAlgoNodeSegment, user::{BGPStatus, User, UserCYOA, UserStatus, UserType}, }; use serde::Serialize; @@ -81,6 +82,8 @@ fn main() { generate_location(&fixtures_dir); generate_exchange(&fixtures_dir); generate_device(&fixtures_dir); + generate_device_legacy(&fixtures_dir); + generate_device_future_version(&fixtures_dir); generate_link(&fixtures_dir); generate_user(&fixtures_dir); generate_multicast_group(&fixtures_dir); @@ -277,7 +280,259 @@ fn generate_exchange(dir: &Path) { write_fixture(dir, "exchange", &data, &meta); } +/// Build a canonical `Device` value used by `device.bin` and `device_future_version.bin`. +/// The trailing `new_interfaces` vec carries one Vpnv4 loopback with a `FlexAlgoNodeSegment` +/// and one physical user-tunnel-endpoint. `interfaces: vec![]` because the custom Device +/// serializer projects the legacy on-disk slot from `new_interfaces` (always V2 per #3653). +fn canonical_device() -> ( + Device, + solana_program::pubkey::Pubkey, // owner + solana_program::pubkey::Pubkey, // location_pk + solana_program::pubkey::Pubkey, // exchange_pk + solana_program::pubkey::Pubkey, // metrics_publisher_pk + solana_program::pubkey::Pubkey, // contributor_pk + solana_program::pubkey::Pubkey, // flex_algo topology pk +) { + let owner = pubkey_from_byte(0x40); + let location_pk = pubkey_from_byte(0x41); + let exchange_pk = pubkey_from_byte(0x42); + let metrics_publisher_pk = pubkey_from_byte(0x43); + let contributor_pk = pubkey_from_byte(0x44); + let topology_pk = pubkey_from_byte(0x45); + + // size/version on the in-memory NewInterface are recomputed by the BorshSerialize + // impl (interface.rs:687-688) — leave them at 0 here. + let new_interfaces = vec![ + NewInterface { + size: 0, + version: 0, + status: InterfaceStatus::Activated, + name: "Loopback0".into(), + interface_type: InterfaceType::Loopback, + interface_cyoa: InterfaceCYOA::None, + interface_dia: InterfaceDIA::None, + loopback_type: LoopbackType::Vpnv4, + bandwidth: 0, + cir: 0, + mtu: 9000, + routing_mode: RoutingMode::Static, + vlan_id: 0, + ip_net: "10.0.0.1/32".parse().unwrap(), + node_segment_idx: 100, + user_tunnel_endpoint: false, + flex_algo_node_segments: vec![FlexAlgoNodeSegment { + topology: topology_pk, + node_segment_idx: 300, + }], + }, + NewInterface { + size: 0, + version: 0, + status: InterfaceStatus::Activated, + name: "Ethernet1".into(), + interface_type: InterfaceType::Physical, + interface_cyoa: InterfaceCYOA::GREOverDIA, + interface_dia: InterfaceDIA::DIA, + loopback_type: LoopbackType::None, + bandwidth: 10_000_000_000, + cir: 5_000_000_000, + mtu: 9000, + routing_mode: RoutingMode::BGP, + vlan_id: 100, + ip_net: "172.16.0.1/30".parse().unwrap(), + node_segment_idx: 200, + user_tunnel_endpoint: true, + flex_algo_node_segments: vec![], + }, + ]; + + let val = Device { + account_type: AccountType::Device, + owner, + index: 7, + bump_seed: 250, + location_pk, + exchange_pk, + device_type: DeviceType::Edge, + public_ip: Ipv4Addr::new(203, 0, 113, 1), + status: DeviceStatus::Activated, + code: "dz1".into(), + dz_prefixes: vec!["10.10.0.0/24".parse().unwrap()].into(), + metrics_publisher_pk, + contributor_pk, + mgmt_vrf: "mgmt".into(), + interfaces: vec![], + new_interfaces, + reference_count: 12, + users_count: 5, + max_users: 100, + device_health: DeviceHealth::ReadyForUsers, + desired_status: DeviceDesiredStatus::Activated, + unicast_users_count: 3, + multicast_subscribers_count: 2, + max_unicast_users: 50, + max_multicast_subscribers: 50, + reserved_seats: 3, + multicast_publishers_count: 1, + max_multicast_publishers: 10, + }; + + (val, owner, location_pk, exchange_pk, metrics_publisher_pk, contributor_pk, topology_pk) +} + +/// Common `meta.fields` describing the canonical Device: legacy slot is the V2 projection +/// of `new_interfaces` (both elements have `Version = 1`, no FlexAlgoNodeSegments per +/// device.rs:527-534 / interface.rs:793-813); the trailing vec carries the full V4 +/// NewInterface bodies including `flex_algo_node_segments`. +#[allow(clippy::too_many_arguments)] +fn canonical_device_fields( + owner: &solana_program::pubkey::Pubkey, + location_pk: &solana_program::pubkey::Pubkey, + exchange_pk: &solana_program::pubkey::Pubkey, + metrics_publisher_pk: &solana_program::pubkey::Pubkey, + contributor_pk: &solana_program::pubkey::Pubkey, + topology_pk: &solana_program::pubkey::Pubkey, + new_interface0_size: u16, + new_interface0_version: u8, + new_interface1_size: u16, + new_interface1_version: u8, +) -> Vec { + assert_eq!(CURRENT_INTERFACE_SCHEMA_VERSION, 4, "fixture assumes CURRENT_INTERFACE_SCHEMA_VERSION = 4"); + vec![ + FieldValue { name: "AccountType".into(), value: "5".into(), typ: "u8".into() }, + FieldValue { name: "Owner".into(), value: pubkey_bs58(owner), typ: "pubkey".into() }, + FieldValue { name: "Index".into(), value: "7".into(), typ: "u128".into() }, + FieldValue { name: "BumpSeed".into(), value: "250".into(), typ: "u8".into() }, + FieldValue { name: "LocationPk".into(), value: pubkey_bs58(location_pk), typ: "pubkey".into() }, + FieldValue { name: "ExchangePk".into(), value: pubkey_bs58(exchange_pk), typ: "pubkey".into() }, + FieldValue { name: "DeviceType".into(), value: "2".into(), typ: "u8".into() }, + FieldValue { name: "PublicIp".into(), value: "203.0.113.1".into(), typ: "ipv4".into() }, + FieldValue { name: "Status".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Code".into(), value: "dz1".into(), typ: "string".into() }, + FieldValue { name: "DzPrefixesLen".into(), value: "1".into(), typ: "u32".into() }, + FieldValue { name: "DzPrefixes0".into(), value: "10.10.0.0/24".into(), typ: "networkv4".into() }, + FieldValue { name: "MetricsPublisherPk".into(), value: pubkey_bs58(metrics_publisher_pk), typ: "pubkey".into() }, + FieldValue { name: "ContributorPk".into(), value: pubkey_bs58(contributor_pk), typ: "pubkey".into() }, + FieldValue { name: "MgmtVrf".into(), value: "mgmt".into(), typ: "string".into() }, + FieldValue { name: "InterfacesLen".into(), value: "2".into(), typ: "u32".into() }, + // Interface 0 - V2 projection of NewInterface[0] (Loopback Vpnv4). + FieldValue { name: "Interface0Version".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Interface0Status".into(), value: "3".into(), typ: "u8".into() }, + FieldValue { name: "Interface0Name".into(), value: "Loopback0".into(), typ: "string".into() }, + FieldValue { name: "Interface0InterfaceType".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Interface0InterfaceCYOA".into(), value: "0".into(), typ: "u8".into() }, + FieldValue { name: "Interface0InterfaceDIA".into(), value: "0".into(), typ: "u8".into() }, + FieldValue { name: "Interface0LoopbackType".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Interface0Bandwidth".into(), value: "0".into(), typ: "u64".into() }, + FieldValue { name: "Interface0Cir".into(), value: "0".into(), typ: "u64".into() }, + FieldValue { name: "Interface0Mtu".into(), value: "9000".into(), typ: "u16".into() }, + FieldValue { name: "Interface0RoutingMode".into(), value: "0".into(), typ: "u8".into() }, + FieldValue { name: "Interface0VlanId".into(), value: "0".into(), typ: "u16".into() }, + FieldValue { name: "Interface0IpNet".into(), value: "10.0.0.1/32".into(), typ: "networkv4".into() }, + FieldValue { name: "Interface0NodeSegmentIdx".into(), value: "100".into(), typ: "u16".into() }, + FieldValue { name: "Interface0UserTunnelEndpoint".into(), value: "false".into(), typ: "bool".into() }, + // Interface 1 - V2 projection of NewInterface[1] (Physical user-tunnel-endpoint). + FieldValue { name: "Interface1Version".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Interface1Status".into(), value: "3".into(), typ: "u8".into() }, + FieldValue { name: "Interface1Name".into(), value: "Ethernet1".into(), typ: "string".into() }, + FieldValue { name: "Interface1InterfaceType".into(), value: "2".into(), typ: "u8".into() }, + FieldValue { name: "Interface1InterfaceCYOA".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Interface1InterfaceDIA".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Interface1LoopbackType".into(), value: "0".into(), typ: "u8".into() }, + FieldValue { name: "Interface1Bandwidth".into(), value: "10000000000".into(), typ: "u64".into() }, + FieldValue { name: "Interface1Cir".into(), value: "5000000000".into(), typ: "u64".into() }, + FieldValue { name: "Interface1Mtu".into(), value: "9000".into(), typ: "u16".into() }, + FieldValue { name: "Interface1RoutingMode".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Interface1VlanId".into(), value: "100".into(), typ: "u16".into() }, + FieldValue { name: "Interface1IpNet".into(), value: "172.16.0.1/30".into(), typ: "networkv4".into() }, + FieldValue { name: "Interface1NodeSegmentIdx".into(), value: "200".into(), typ: "u16".into() }, + FieldValue { name: "Interface1UserTunnelEndpoint".into(), value: "true".into(), typ: "bool".into() }, + FieldValue { name: "ReferenceCount".into(), value: "12".into(), typ: "u32".into() }, + FieldValue { name: "UsersCount".into(), value: "5".into(), typ: "u16".into() }, + FieldValue { name: "MaxUsers".into(), value: "100".into(), typ: "u16".into() }, + FieldValue { name: "DeviceHealth".into(), value: "3".into(), typ: "u8".into() }, + FieldValue { name: "DesiredStatus".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "UnicastUsersCount".into(), value: "3".into(), typ: "u16".into() }, + FieldValue { name: "MulticastSubscribersCount".into(), value: "2".into(), typ: "u16".into() }, + FieldValue { name: "MaxUnicastUsers".into(), value: "50".into(), typ: "u16".into() }, + FieldValue { name: "MaxMulticastSubscribers".into(), value: "50".into(), typ: "u16".into() }, + FieldValue { name: "ReservedSeats".into(), value: "3".into(), typ: "u16".into() }, + FieldValue { name: "MulticastPublishersCount".into(), value: "1".into(), typ: "u16".into() }, + FieldValue { name: "MaxMulticastPublishers".into(), value: "10".into(), typ: "u16".into() }, + // Trailing new_interfaces vec. + FieldValue { name: "NewInterfacesLen".into(), value: "2".into(), typ: "u32".into() }, + // NewInterface 0 - Loopback Vpnv4 with one FlexAlgoNodeSegment. + FieldValue { name: "NewInterface0Size".into(), value: new_interface0_size.to_string(), typ: "u16".into() }, + FieldValue { name: "NewInterface0Version".into(), value: new_interface0_version.to_string(), typ: "u8".into() }, + FieldValue { name: "NewInterface0Status".into(), value: "3".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface0Name".into(), value: "Loopback0".into(), typ: "string".into() }, + FieldValue { name: "NewInterface0InterfaceType".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface0InterfaceCYOA".into(), value: "0".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface0InterfaceDIA".into(), value: "0".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface0LoopbackType".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface0Bandwidth".into(), value: "0".into(), typ: "u64".into() }, + FieldValue { name: "NewInterface0Cir".into(), value: "0".into(), typ: "u64".into() }, + FieldValue { name: "NewInterface0Mtu".into(), value: "9000".into(), typ: "u16".into() }, + FieldValue { name: "NewInterface0RoutingMode".into(), value: "0".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface0VlanId".into(), value: "0".into(), typ: "u16".into() }, + FieldValue { name: "NewInterface0IpNet".into(), value: "10.0.0.1/32".into(), typ: "networkv4".into() }, + FieldValue { name: "NewInterface0NodeSegmentIdx".into(), value: "100".into(), typ: "u16".into() }, + FieldValue { name: "NewInterface0UserTunnelEndpoint".into(), value: "false".into(), typ: "bool".into() }, + FieldValue { name: "NewInterface0FlexAlgoNodeSegmentsLen".into(), value: "1".into(), typ: "u32".into() }, + FieldValue { name: "NewInterface0FlexAlgoNodeSegments0Topology".into(), value: pubkey_bs58(topology_pk), typ: "pubkey".into() }, + FieldValue { name: "NewInterface0FlexAlgoNodeSegments0NodeSegmentIdx".into(), value: "300".into(), typ: "u16".into() }, + // NewInterface 1 - Physical user-tunnel-endpoint, no flex segments. + FieldValue { name: "NewInterface1Size".into(), value: new_interface1_size.to_string(), typ: "u16".into() }, + FieldValue { name: "NewInterface1Version".into(), value: new_interface1_version.to_string(), typ: "u8".into() }, + FieldValue { name: "NewInterface1Status".into(), value: "3".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface1Name".into(), value: "Ethernet1".into(), typ: "string".into() }, + FieldValue { name: "NewInterface1InterfaceType".into(), value: "2".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface1InterfaceCYOA".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface1InterfaceDIA".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface1LoopbackType".into(), value: "0".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface1Bandwidth".into(), value: "10000000000".into(), typ: "u64".into() }, + FieldValue { name: "NewInterface1Cir".into(), value: "5000000000".into(), typ: "u64".into() }, + FieldValue { name: "NewInterface1Mtu".into(), value: "9000".into(), typ: "u16".into() }, + FieldValue { name: "NewInterface1RoutingMode".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface1VlanId".into(), value: "100".into(), typ: "u16".into() }, + FieldValue { name: "NewInterface1IpNet".into(), value: "172.16.0.1/30".into(), typ: "networkv4".into() }, + FieldValue { name: "NewInterface1NodeSegmentIdx".into(), value: "200".into(), typ: "u16".into() }, + FieldValue { name: "NewInterface1UserTunnelEndpoint".into(), value: "true".into(), typ: "bool".into() }, + FieldValue { name: "NewInterface1FlexAlgoNodeSegmentsLen".into(), value: "0".into(), typ: "u32".into() }, + ] +} + fn generate_device(dir: &Path) { + let (val, owner, location_pk, exchange_pk, metrics_publisher_pk, contributor_pk, topology_pk) = + canonical_device(); + + // Use Device's custom BorshSerialize: projects the on-disk legacy `interfaces` + // slot from `new_interfaces` (always V2 per #3653/#3667) and appends the + // size-prefixed `new_interfaces` vec. SDKs read both, with the trailing vec + // taking precedence over the rebuilt-from-legacy fallback. + let data = borsh::to_vec(&val).unwrap(); + + let size0 = val.new_interfaces[0].compute_on_disk_size().unwrap(); + let size1 = val.new_interfaces[1].compute_on_disk_size().unwrap(); + + let meta = FixtureMeta { + name: "Device".into(), + account_type: 5, + fields: canonical_device_fields( + &owner, &location_pk, &exchange_pk, &metrics_publisher_pk, &contributor_pk, &topology_pk, + size0, CURRENT_INTERFACE_SCHEMA_VERSION, + size1, CURRENT_INTERFACE_SCHEMA_VERSION, + ), + }; + + write_fixture(dir, "device", &data, &meta); +} + +/// Hand-serialized device with the legacy `interfaces` vec populated and **no** trailing +/// `new_interfaces` vec — the pre-#3667 on-disk format. SDKs detect the absent trailing +/// bytes and rebuild `new_interfaces` from the legacy enum vec, stamping each entry with +/// `Version = CURRENT_INTERFACE_VERSION` and `Size = 0`. +fn generate_device_legacy(dir: &Path) { let owner = pubkey_from_byte(0x40); let location_pk = pubkey_from_byte(0x41); let exchange_pk = pubkey_from_byte(0x42); @@ -342,11 +597,8 @@ fn generate_device(dir: &Path) { max_multicast_publishers: 10, }; - // Serialize each field manually to bypass `Device`'s custom `BorshSerialize`, - // which projects the legacy slot from `new_interfaces` (always V2) and - // appends a `new_interfaces` trailing vec. The fixture pins Interface0 to V1 - // and Interface1 to V2 in the pre-#3667 on-disk format, which the SDK - // exercises via the legacy fallback path in `Device::TryFrom`. + // Bypass Device::serialize so we don't write the trailing new_interfaces vec — + // this is exactly the pre-#3667 byte shape the SDK legacy-fallback path consumes. let mut data = Vec::new(); BorshSerialize::serialize(&val.account_type, &mut data).unwrap(); BorshSerialize::serialize(&val.owner, &mut data).unwrap(); @@ -376,8 +628,9 @@ fn generate_device(dir: &Path) { BorshSerialize::serialize(&val.multicast_publishers_count, &mut data).unwrap(); BorshSerialize::serialize(&val.max_multicast_publishers, &mut data).unwrap(); + let v = CURRENT_INTERFACE_SCHEMA_VERSION.to_string(); let meta = FixtureMeta { - name: "Device".into(), + name: "DeviceLegacy".into(), account_type: 5, fields: vec![ FieldValue { name: "AccountType".into(), value: "5".into(), typ: "u8".into() }, @@ -396,7 +649,7 @@ fn generate_device(dir: &Path) { FieldValue { name: "ContributorPk".into(), value: pubkey_bs58(&contributor_pk), typ: "pubkey".into() }, FieldValue { name: "MgmtVrf".into(), value: "mgmt".into(), typ: "string".into() }, FieldValue { name: "InterfacesLen".into(), value: "2".into(), typ: "u32".into() }, - // Interface 0 - V1 + // Interface 0 - V1 (legacy on-disk). FieldValue { name: "Interface0Version".into(), value: "0".into(), typ: "u8".into() }, FieldValue { name: "Interface0Status".into(), value: "3".into(), typ: "u8".into() }, FieldValue { name: "Interface0Name".into(), value: "Loopback0".into(), typ: "string".into() }, @@ -406,7 +659,7 @@ fn generate_device(dir: &Path) { FieldValue { name: "Interface0IpNet".into(), value: "10.0.0.1/32".into(), typ: "networkv4".into() }, FieldValue { name: "Interface0NodeSegmentIdx".into(), value: "100".into(), typ: "u16".into() }, FieldValue { name: "Interface0UserTunnelEndpoint".into(), value: "false".into(), typ: "bool".into() }, - // Interface 1 - V2 + // Interface 1 - V2 (legacy on-disk). FieldValue { name: "Interface1Version".into(), value: "1".into(), typ: "u8".into() }, FieldValue { name: "Interface1Status".into(), value: "3".into(), typ: "u8".into() }, FieldValue { name: "Interface1Name".into(), value: "Ethernet1".into(), typ: "string".into() }, @@ -434,10 +687,87 @@ fn generate_device(dir: &Path) { FieldValue { name: "ReservedSeats".into(), value: "3".into(), typ: "u16".into() }, FieldValue { name: "MulticastPublishersCount".into(), value: "1".into(), typ: "u16".into() }, FieldValue { name: "MaxMulticastPublishers".into(), value: "10".into(), typ: "u16".into() }, + // Rebuilt new_interfaces (size=0, version=current); both bodies mirror + // the V2 projection of the legacy entries — V1's missing fields default per + // `InterfaceV2::try_from(&InterfaceV1)` (interface.rs:353-374). + FieldValue { name: "NewInterfacesLen".into(), value: "2".into(), typ: "u32".into() }, + FieldValue { name: "NewInterface0Size".into(), value: "0".into(), typ: "u16".into() }, + FieldValue { name: "NewInterface0Version".into(), value: v.clone(), typ: "u8".into() }, + FieldValue { name: "NewInterface0Status".into(), value: "3".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface0Name".into(), value: "Loopback0".into(), typ: "string".into() }, + FieldValue { name: "NewInterface0InterfaceType".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface0InterfaceCYOA".into(), value: "0".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface0InterfaceDIA".into(), value: "0".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface0LoopbackType".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface0Mtu".into(), value: "9000".into(), typ: "u16".into() }, + FieldValue { name: "NewInterface0RoutingMode".into(), value: "0".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface0VlanId".into(), value: "0".into(), typ: "u16".into() }, + FieldValue { name: "NewInterface0IpNet".into(), value: "10.0.0.1/32".into(), typ: "networkv4".into() }, + FieldValue { name: "NewInterface0NodeSegmentIdx".into(), value: "100".into(), typ: "u16".into() }, + FieldValue { name: "NewInterface0UserTunnelEndpoint".into(), value: "false".into(), typ: "bool".into() }, + FieldValue { name: "NewInterface0FlexAlgoNodeSegmentsLen".into(), value: "0".into(), typ: "u32".into() }, + FieldValue { name: "NewInterface1Size".into(), value: "0".into(), typ: "u16".into() }, + FieldValue { name: "NewInterface1Version".into(), value: v, typ: "u8".into() }, + FieldValue { name: "NewInterface1Status".into(), value: "3".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface1Name".into(), value: "Ethernet1".into(), typ: "string".into() }, + FieldValue { name: "NewInterface1InterfaceType".into(), value: "2".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface1InterfaceCYOA".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface1InterfaceDIA".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface1LoopbackType".into(), value: "0".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface1Bandwidth".into(), value: "10000000000".into(), typ: "u64".into() }, + FieldValue { name: "NewInterface1Cir".into(), value: "5000000000".into(), typ: "u64".into() }, + FieldValue { name: "NewInterface1Mtu".into(), value: "9000".into(), typ: "u16".into() }, + FieldValue { name: "NewInterface1RoutingMode".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "NewInterface1VlanId".into(), value: "100".into(), typ: "u16".into() }, + FieldValue { name: "NewInterface1IpNet".into(), value: "172.16.0.1/30".into(), typ: "networkv4".into() }, + FieldValue { name: "NewInterface1NodeSegmentIdx".into(), value: "200".into(), typ: "u16".into() }, + FieldValue { name: "NewInterface1UserTunnelEndpoint".into(), value: "true".into(), typ: "bool".into() }, + FieldValue { name: "NewInterface1FlexAlgoNodeSegmentsLen".into(), value: "0".into(), typ: "u32".into() }, ], }; - write_fixture(dir, "device", &data, &meta); + write_fixture(dir, "device_legacy", &data, &meta); +} + +/// Same on-disk shape as `device.bin`, but the **last** trailing-vec element is doctored +/// with `version = 5` (a hypothetical future version) and `size += 8`, with 8 trailing +/// `0xAB` filler bytes appended at end-of-file. SDK readers consume the known body fields +/// then `seek(start + size)` over the junk — exercising the constant-time skip path. +const FUTURE_VERSION: u8 = 5; +const FUTURE_VERSION_JUNK: usize = 8; + +fn generate_device_future_version(dir: &Path) { + let (val, owner, location_pk, exchange_pk, metrics_publisher_pk, contributor_pk, topology_pk) = + canonical_device(); + + let mut data = borsh::to_vec(&val).unwrap(); + + // The trailing vec elements are written contiguously at end-of-buffer; the last + // element ends exactly at buf.len(). Locate its size+version header by subtracting + // the precomputed on-disk size. + let last = val.new_interfaces.last().expect("non-empty"); + let last_size = last.compute_on_disk_size().unwrap(); + let new_last_size = last_size + FUTURE_VERSION_JUNK as u16; + let last_start = data.len() - last_size as usize; + + // Bump size and version in place, then append junk bytes after the body. + data[last_start..last_start + 2].copy_from_slice(&new_last_size.to_le_bytes()); + data[last_start + 2] = FUTURE_VERSION; + data.extend(std::iter::repeat_n(0xAB, FUTURE_VERSION_JUNK)); + + let size0 = val.new_interfaces[0].compute_on_disk_size().unwrap(); + + let meta = FixtureMeta { + name: "DeviceFutureVersion".into(), + account_type: 5, + fields: canonical_device_fields( + &owner, &location_pk, &exchange_pk, &metrics_publisher_pk, &contributor_pk, &topology_pk, + size0, CURRENT_INTERFACE_SCHEMA_VERSION, + new_last_size, FUTURE_VERSION, + ), + }; + + write_fixture(dir, "device_future_version", &data, &meta); } fn generate_link(dir: &Path) { diff --git a/sdk/serviceability/testdata/fixtures/link.bin b/sdk/serviceability/testdata/fixtures/link.bin index 460511378d7ed4279f11fa3fa66249ce7189352b..f63962e6cf92f16b245aaf488adcef7289d75172 100644 GIT binary patch delta 10 RcmaFO_>OVHYi0%p1^^hl16lw8 delta 6 NcmaFI_?mITYXA#b16Ke5 diff --git a/sdk/serviceability/typescript/serviceability/tests/fixtures.test.ts b/sdk/serviceability/typescript/serviceability/tests/fixtures.test.ts index 5b9ddcad39..754a38b1c7 100644 --- a/sdk/serviceability/typescript/serviceability/tests/fixtures.test.ts +++ b/sdk/serviceability/typescript/serviceability/tests/fixtures.test.ts @@ -145,6 +145,23 @@ describe("Exchange fixture", () => { }); }); +// Matches Rust CURRENT_INTERFACE_SCHEMA_VERSION; private in state.ts so re-stated here. +const CURRENT_INTERFACE_VERSION = 4; + +// Recompute on-disk byte size for a NewInterface element so tests don't bake +// magic body byte counts. Layout matches Rust NewInterface::serialize_body +// (interface.rs:641-658): u16 size + u8 version (3-byte prefix) + +// u8 status + (u32+len) name + 4*u8 + u64*2 + u16 + u8 + u16 + 5-byte ip_net + +// u16 + u8 + (u32+34*N) flex_algo_node_segments. +function expectedNewInterfaceSize(ni: { + name: string; + flexAlgoNodeSegments?: Array; +}): number { + const flexLen = ni.flexAlgoNodeSegments?.length ?? 0; + const body = 1 + (4 + ni.name.length) + 1 + 1 + 1 + 1 + 8 + 8 + 2 + 1 + 2 + 5 + 2 + 1 + (4 + 34 * flexLen); + return 3 + body; +} + describe("Device fixture", () => { test("deserialize", () => { const [data, meta] = loadFixture("device"); @@ -190,22 +207,23 @@ describe("Device fixture", () => { expect(dev.dzPrefixes).toHaveLength(1); expect(formatNetworkV4(dev.dzPrefixes[0])).toBe("10.10.0.0/24"); - // interfaces + // Legacy slot is the V2 projection of new_interfaces (always V2 per #3653); + // both entries carry version 1 and no FlexAlgoNodeSegments. expect(dev.interfaces).toHaveLength(2); - - // Interface 0 (V1 format, version byte 0) const iface0 = dev.interfaces[0]; - expect(iface0.version).toBe(0); + expect(iface0.version).toBe(1); expect(iface0.status).toBe(3); expect(iface0.name).toBe("Loopback0"); expect(iface0.interfaceType).toBe(1); + expect(iface0.interfaceCyoa).toBe(0); + expect(iface0.interfaceDia).toBe(0); expect(iface0.loopbackType).toBe(1); - expect(iface0.vlanId).toBe(0); + expect(iface0.mtu).toBe(9000); + expect(iface0.flexAlgoNodeSegments).toEqual([]); expect(formatNetworkV4(iface0.ipNet)).toBe("10.0.0.1/32"); expect(iface0.nodeSegmentIdx).toBe(100); expect(iface0.userTunnelEndpoint).toBe(false); - // Interface 1 (V2 format, version byte 1) const iface1 = dev.interfaces[1]; expect(iface1.version).toBe(1); expect(iface1.status).toBe(3); @@ -222,6 +240,79 @@ describe("Device fixture", () => { expect(formatNetworkV4(iface1.ipNet)).toBe("172.16.0.1/30"); expect(iface1.nodeSegmentIdx).toBe(200); expect(iface1.userTunnelEndpoint).toBe(true); + + // Trailing new_interfaces vec carries the full V4 NewInterface bodies. + expect(dev.newInterfaces).toHaveLength(2); + const ni0 = dev.newInterfaces[0]; + expect(ni0.version).toBe(CURRENT_INTERFACE_VERSION); + expect(ni0.name).toBe("Loopback0"); + expect(ni0.loopbackType).toBe(1); // Vpnv4 + expect(ni0.flexAlgoNodeSegments).toHaveLength(1); + expect(ni0.flexAlgoNodeSegments![0].nodeSegmentIdx).toBe(300); + expect(ni0.size).toBe(expectedNewInterfaceSize(ni0)); + + const ni1 = dev.newInterfaces[1]; + expect(ni1.version).toBe(CURRENT_INTERFACE_VERSION); + expect(ni1.name).toBe("Ethernet1"); + expect(ni1.userTunnelEndpoint).toBe(true); + expect(ni1.flexAlgoNodeSegments).toEqual([]); + expect(ni1.size).toBe(expectedNewInterfaceSize(ni1)); + }); +}); + +// Pre-#3667 on-disk format: legacy `interfaces` vec only, no trailing +// `new_interfaces`. SDK rebuilds new_interfaces from the legacy vec, stamping +// each entry with version=CURRENT_INTERFACE_VERSION and size=0. +describe("Device legacy fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("device_legacy"); + const dev = deserializeDevice(data); + expect(meta.name).toBe("DeviceLegacy"); + + // Legacy slot mirrors the original V1+V2 hand-serialized shape. + expect(dev.interfaces).toHaveLength(2); + expect(dev.interfaces[0].version).toBe(0); // V1 + expect(dev.interfaces[0].name).toBe("Loopback0"); + expect(dev.interfaces[1].version).toBe(1); // V2 + expect(dev.interfaces[1].name).toBe("Ethernet1"); + + // Rebuilt new_interfaces: same field values as the legacy entries, but + // stamped with the current schema version and zero on-disk size. + expect(dev.newInterfaces).toHaveLength(2); + for (const ni of dev.newInterfaces) { + expect(ni.version).toBe(CURRENT_INTERFACE_VERSION); + expect(ni.size).toBe(0); + expect(ni.flexAlgoNodeSegments).toEqual([]); + } + expect(dev.newInterfaces[0].name).toBe("Loopback0"); + expect(dev.newInterfaces[0].loopbackType).toBe(1); // Vpnv4 + expect(dev.newInterfaces[1].name).toBe("Ethernet1"); + expect(dev.newInterfaces[1].userTunnelEndpoint).toBe(true); + }); +}); + +// Same on-disk shape as device.bin, but the last trailing-vec element is +// doctored with version=5 and 8 trailing junk bytes. SDK reads the known body +// fields then advances to start+size, skipping the junk. +describe("Device future-version fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("device_future_version"); + const dev = deserializeDevice(data); + expect(meta.name).toBe("DeviceFutureVersion"); + + expect(dev.newInterfaces).toHaveLength(2); + const ni0 = dev.newInterfaces[0]; + expect(ni0.version).toBe(CURRENT_INTERFACE_VERSION); + expect(ni0.name).toBe("Loopback0"); + expect(ni0.flexAlgoNodeSegments).toHaveLength(1); + + // Doctored element: future version stamp + 8 trailing junk bytes the reader + // skips via seek(start+size). + const ni1 = dev.newInterfaces[1]; + expect(ni1.version).toBe(5); + expect(ni1.size).toBe(expectedNewInterfaceSize(ni1) + 8); + expect(ni1.name).toBe("Ethernet1"); + expect(ni1.userTunnelEndpoint).toBe(true); }); }); diff --git a/sdk/telemetry/testdata/fixtures/generate-fixtures/Cargo.lock b/sdk/telemetry/testdata/fixtures/generate-fixtures/Cargo.lock index 2255b8f0b8..4e727d22a3 100644 --- a/sdk/telemetry/testdata/fixtures/generate-fixtures/Cargo.lock +++ b/sdk/telemetry/testdata/fixtures/generate-fixtures/Cargo.lock @@ -582,7 +582,7 @@ dependencies = [ [[package]] name = "doublezero-config" -version = "0.16.0" +version = "0.21.0" dependencies = [ "eyre", "serde", @@ -591,7 +591,7 @@ dependencies = [ [[package]] name = "doublezero-program-common" -version = "0.16.0" +version = "0.21.0" dependencies = [ "borsh 1.6.0", "byteorder", @@ -603,7 +603,7 @@ dependencies = [ [[package]] name = "doublezero-serviceability" -version = "0.16.0" +version = "0.21.0" dependencies = [ "bitflags", "borsh 1.6.0", @@ -618,7 +618,7 @@ dependencies = [ [[package]] name = "doublezero-telemetry" -version = "0.16.0" +version = "0.21.0" dependencies = [ "borsh 1.6.0", "borsh-incremental", diff --git a/smartcontract/sdk/go/serviceability/fixture_test.go b/smartcontract/sdk/go/serviceability/fixture_test.go new file mode 100644 index 0000000000..10169b857d --- /dev/null +++ b/smartcontract/sdk/go/serviceability/fixture_test.go @@ -0,0 +1,163 @@ +package serviceability_test + +import ( + "encoding/json" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/malbeclabs/doublezero/smartcontract/sdk/go/serviceability" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Fixture-driven tests over the binary fixtures in sdk/serviceability/testdata/fixtures. +// The .bin / .json pairs are produced by the Rust generator at +// sdk/serviceability/testdata/fixtures/generate-fixtures and shared with the +// Python and TypeScript SDKs so the on-disk shape stays in lockstep across +// languages. Regenerate with `make generate-fixtures`. + +type fixtureField struct { + Name string `json:"name"` + Value string `json:"value"` + Type string `json:"typ"` +} + +type fixtureMeta struct { + Name string `json:"name"` + AccountType uint8 `json:"account_type"` + Fields []fixtureField `json:"fields"` +} + +func fixturesDir() string { + _, filename, _, _ := runtime.Caller(0) + // .../smartcontract/sdk/go/serviceability/fixture_test.go → repo root + return filepath.Join(filepath.Dir(filename), "..", "..", "..", "..", "sdk", "serviceability", "testdata", "fixtures") +} + +func loadFixture(t *testing.T, name string) ([]byte, fixtureMeta) { + t.Helper() + dir := fixturesDir() + bin, err := os.ReadFile(filepath.Join(dir, name+".bin")) + require.NoErrorf(t, err, "reading %s.bin", name) + jsonBytes, err := os.ReadFile(filepath.Join(dir, name+".json")) + require.NoErrorf(t, err, "reading %s.json", name) + var meta fixtureMeta + require.NoErrorf(t, json.Unmarshal(jsonBytes, &meta), "parsing %s.json", name) + return bin, meta +} + +// expectedNewInterfaceSize recomputes the on-disk byte length of a NewInterface +// element so tests don't bake magic numbers. Layout matches Rust +// NewInterface::serialize_body (interface.rs:641-658): u16 size + u8 version +// (3-byte prefix) + u8 status + (u32+len) name + 4*u8 + u64*2 + u16 + u8 + u16 +// + 5-byte ip_net + u16 + u8 + (u32+34*N) flex_algo_node_segments. +func expectedNewInterfaceSize(ni serviceability.Interface) uint16 { + body := 1 + (4 + len(ni.Name)) + 1 + 1 + 1 + 1 + 8 + 8 + 2 + 1 + 2 + 5 + 2 + 1 + (4 + 34*len(ni.FlexAlgoNodeSegments)) + return uint16(3 + body) +} + +func TestFixtureDevice(t *testing.T) { + data, meta := loadFixture(t, "device") + require.Equal(t, "Device", meta.Name) + require.Equal(t, uint8(serviceability.DeviceType), meta.AccountType) + + var dev serviceability.Device + r := serviceability.NewByteReader(data) + serviceability.DeserializeDevice(r, &dev) + require.NoError(t, dev.DeserializeError) + + assert.Equal(t, serviceability.DeviceType, dev.AccountType) + assert.Equal(t, "dz1", dev.Code) + assert.Equal(t, "mgmt", dev.MgmtVrf) + assert.Equal(t, [4]byte{203, 0, 113, 1}, dev.PublicIp) + + // Legacy slot is the V2 projection of new_interfaces (always V2 per #3653); + // both entries carry version 1 and no FlexAlgoNodeSegments. + require.Len(t, dev.Interfaces, 2) + assert.Equal(t, uint8(1), dev.Interfaces[0].Version) + assert.Equal(t, "Loopback0", dev.Interfaces[0].Name) + assert.Equal(t, serviceability.LoopbackTypeVpnv4, dev.Interfaces[0].LoopbackType) + assert.Empty(t, dev.Interfaces[0].FlexAlgoNodeSegments) + assert.Equal(t, uint8(1), dev.Interfaces[1].Version) + assert.Equal(t, "Ethernet1", dev.Interfaces[1].Name) + assert.True(t, dev.Interfaces[1].UserTunnelEndpoint) + + // Trailing new_interfaces vec carries the full V4 NewInterface bodies. + require.Len(t, dev.NewInterfaces, 2) + ni0 := dev.NewInterfaces[0] + assert.Equal(t, uint8(serviceability.CurrentInterfaceVersion), ni0.Version) + assert.Equal(t, "Loopback0", ni0.Name) + assert.Equal(t, serviceability.LoopbackTypeVpnv4, ni0.LoopbackType) + require.Len(t, ni0.FlexAlgoNodeSegments, 1) + assert.Equal(t, uint16(300), ni0.FlexAlgoNodeSegments[0].NodeSegmentIdx) + assert.Equal(t, expectedNewInterfaceSize(ni0), ni0.Size) + + ni1 := dev.NewInterfaces[1] + assert.Equal(t, uint8(serviceability.CurrentInterfaceVersion), ni1.Version) + assert.Equal(t, "Ethernet1", ni1.Name) + assert.True(t, ni1.UserTunnelEndpoint) + assert.Empty(t, ni1.FlexAlgoNodeSegments) + assert.Equal(t, expectedNewInterfaceSize(ni1), ni1.Size) +} + +// Pre-#3667 on-disk format: legacy `interfaces` vec only, no trailing +// `new_interfaces`. SDK rebuilds new_interfaces from the legacy vec, stamping +// each entry with Version=CurrentInterfaceVersion and Size=0. +func TestFixtureDeviceLegacy(t *testing.T) { + data, meta := loadFixture(t, "device_legacy") + require.Equal(t, "DeviceLegacy", meta.Name) + + var dev serviceability.Device + r := serviceability.NewByteReader(data) + serviceability.DeserializeDevice(r, &dev) + require.NoError(t, dev.DeserializeError) + + // Legacy slot mirrors the original V1+V2 hand-serialized shape. + require.Len(t, dev.Interfaces, 2) + assert.Equal(t, uint8(0), dev.Interfaces[0].Version) // V1 + assert.Equal(t, "Loopback0", dev.Interfaces[0].Name) + assert.Equal(t, uint8(1), dev.Interfaces[1].Version) // V2 + assert.Equal(t, "Ethernet1", dev.Interfaces[1].Name) + + // Rebuilt new_interfaces: same field values as the legacy entries, but + // stamped with the current schema version and zero on-disk size. + require.Len(t, dev.NewInterfaces, 2) + for _, ni := range dev.NewInterfaces { + assert.Equal(t, uint8(serviceability.CurrentInterfaceVersion), ni.Version) + assert.Equal(t, uint16(0), ni.Size) + assert.Empty(t, ni.FlexAlgoNodeSegments) + } + assert.Equal(t, "Loopback0", dev.NewInterfaces[0].Name) + assert.Equal(t, serviceability.LoopbackTypeVpnv4, dev.NewInterfaces[0].LoopbackType) + assert.Equal(t, "Ethernet1", dev.NewInterfaces[1].Name) + assert.True(t, dev.NewInterfaces[1].UserTunnelEndpoint) +} + +// Same on-disk shape as device.bin, but the last trailing-vec element is +// doctored with Version=5 and 8 trailing junk bytes. SDK reads the known body +// fields then advances to start+size, skipping the junk. +func TestFixtureDeviceFutureVersion(t *testing.T) { + data, meta := loadFixture(t, "device_future_version") + require.Equal(t, "DeviceFutureVersion", meta.Name) + + var dev serviceability.Device + r := serviceability.NewByteReader(data) + serviceability.DeserializeDevice(r, &dev) + require.NoError(t, dev.DeserializeError) + + require.Len(t, dev.NewInterfaces, 2) + ni0 := dev.NewInterfaces[0] + assert.Equal(t, uint8(serviceability.CurrentInterfaceVersion), ni0.Version) + assert.Equal(t, "Loopback0", ni0.Name) + require.Len(t, ni0.FlexAlgoNodeSegments, 1) + + // Doctored element: future version stamp + 8 trailing junk bytes the reader + // skips via seek(start+size). + ni1 := dev.NewInterfaces[1] + assert.Equal(t, uint8(5), ni1.Version) + assert.Equal(t, expectedNewInterfaceSize(ni1)+8, ni1.Size) + assert.Equal(t, "Ethernet1", ni1.Name) + assert.True(t, ni1.UserTunnelEndpoint) +}