smartcontract: append new_interfaces vec to Device with custom serializer#3667
Merged
smartcontract: append new_interfaces vec to Device with custom serializer#3667
Conversation
…izer Implements #3665 — the second step of the forward-compatible Interface refactor. `Device` gains a trailing `new_interfaces: Vec<NewInterface>` field after `max_multicast_publishers`. A custom `BorshSerialize` projects the legacy `interfaces` slot from `new_interfaces` (always as `Interface::V2` per #3653) and writes the new vec at the end of the layout. Older readers continue to parse the legacy slot at its existing offset; newer readers read the full forward-compat data from the trailing vec. `Device::TryFrom<&[u8]>` reads the trailing vec with `unwrap_or_default()` and, on legacy accounts (no trailing bytes), rebuilds `new_interfaces` from the legacy enum vec via per-variant `TryFrom`. Mutations route through `Device::replace_interface` / `push_interface` / `remove_interface` so both vecs stay in sync — the alternative ("project legacy from new_interfaces, ignore self.interfaces") would silently drop processor mutations on save. `find_interface` now returns `(usize, &NewInterface)`; `find_interface_legacy` is a temporary helper for unrelated callers (CLI commands and tests) that get migrated in subsequent issues. `MigrateDeviceInterfaces`'s V2→V3 byte-format expectation is invalidated by the always-V2 legacy projection; the corresponding test is `#[ignore]`d with a note for follow-up.
7a7aa83 to
1728db6
Compare
ben-dz
reviewed
May 5, 2026
ben-dz
approved these changes
May 5, 2026
This was referenced May 5, 2026
elitegreg
added a commit
that referenced
this pull request
May 5, 2026
## Summary
- Migrate
`device/interface/{create,update,activate,delete,reject,remove,unlink}`,
`link/{accept,activate,closeaccount,create,delete,update}`, and
`topology/backfill` processors to read and mutate
`Device::new_interfaces` directly, dropping all `into_current_version()`
/ `into_v3()` / `find_interface_legacy()` calls from `processors/`.
- `Device::push_interface` now takes a `NewInterface` directly, avoiding
a fallible `TryFrom<&InterfaceV2>` at the call site.
- `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. The custom `Device::BorshSerialize` continues to
project `new_interfaces` → V2 for byte-compatibility with older readers.
- `Device::find_interface_legacy` is retained as scaffolding for CLI
callers; no `processors/` code calls it any more. CLI migration will
follow in a separate change.
Part of the forward-compatible Device interfaces refactor — depends on
#3666 and #3667. Closes #3658.
## Testing Verification
- New unit test
`test_flex_algo_segments_roundtrip_through_new_interfaces` (in
`state/device.rs`) seeds a Vpnv4 loopback with a populated
`flex_algo_node_segments`, round-trips through borsh, and asserts
segments survive in `new_interfaces` while the V2-projected legacy slot
drops them.
- Existing `test_topology_backfill_populates_vpnv4_loopbacks`
integration test continues to pass — it already asserted on
`device.new_interfaces[0].flex_algo_node_segments`, so removing the
legacy in-memory mirror is invisible to it.
- `make rust-fmt`, `make rust-lint`, and `make rust-test` all pass on
this branch.
- Sanity grep over `processors/` for legacy paths returns only the
out-of-scope `migrate_interfaces.rs`.
elitegreg
added a commit
that referenced
this pull request
May 5, 2026
## Summary - Migrate Rust read callers (CLI, sentinel, client, controlplane admin, Rust SDK topology helper) from `device.interfaces` to `device.new_interfaces`, and adopt the `Device::find_interface` signature that returns `&NewInterface`. - Drop `into_current_version()` / `into_v3()` calls at the migrated sites — `NewInterface` exposes the same field names directly. - Update `poll_for_device_interface_activated` to return `NewInterface` instead of `CurrentInterfaceVersion`. - Re-export `NewInterface` from the Rust SDK so CLI display structs can take `&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 because the smartcontract program processors (`processors/link/*.rs`, `processors/device/interface/*.rs`) still depend on it; those migrate in a later issue. Activator is deliberately excluded — it is deprecated. Design: https://gist.github.com/elitegreg/e1d27c97034656a980fa8d7628ae16c5 Depends on #3667 Closes #3659 ## Testing Verification - `make rust-fmt`, `make rust-lint`, and `make rust-test` all pass on the branch. - Verified per-crate with `cargo test -p doublezero_cli -p doublezero-sentinel -p doublezero-admin -p doublezero -p doublezero_sdk -p doublezero-serviceability`. - Confirmed no remaining `device.interfaces` / `into_current_version` / `into_v3` reads in the migrated areas via `grep`. The smartcontract program (`smartcontract/programs/...`) and activator are intentionally untouched. - Test fixtures that previously set `interfaces: vec![CurrentInterfaceVersion {...}.to_interface()]` and `new_interfaces: vec![]` were inverted to populate `new_interfaces` (via `(&v2).try_into().unwrap()`) so the migrated read paths see the data.
10 tasks
elitegreg
added a commit
that referenced
this pull request
May 5, 2026
…3675) ## Summary - Delete the `MigrateDeviceInterfaces` instruction (variant 111), its processor, and integration tests. `Device::TryFrom<&[u8]>` (landed in #3667 / #3665) auto-promotes legacy `interfaces` into `new_interfaces` when the trailing vec is missing, and the next account write persists the promoted vec — so a standalone migrate step is no longer needed. - Reword two stale `MigrateDeviceInterfaces` doc comments in the Go SDK (`serviceability/deserialize.go`, `serviceability/state.go`); the Go SDK has no caller of this instruction, only stale references. - Add a `Smartcontract` bullet to the `Unreleased` CHANGELOG. This is the cleanup step in the forward-compatible Device interfaces refactor (design: https://gist.github.com/elitegreg/e1d27c97034656a980fa8d7628ae16c5, Design #5 final bullet). Stack: #3666 → #3667 → this PR. Closes #3662. ## Testing Verification - `make rust-fmt`, `make rust-lint`, and `cargo test -p doublezero-serviceability` all pass; legacy-account compatibility paths (`test_state_compatibility_device`, `test_device_legacy_account_rebuilds_new_vec`, `test_device_serialize_keeps_vecs_in_sync`) still cover the auto-promotion behavior that replaces this instruction. - `go build ./...` and `go vet ./...` pass for `smartcontract/sdk/go` after the comment rewrites. - Repo-wide `grep` for `MigrateDeviceInterfaces` / `migrate_device_interfaces` / `migrate_interfaces` returns only the (intentional) historical and new CHANGELOG entries.
elitegreg
added a commit
that referenced
this pull request
May 6, 2026
## Summary - Go, Python, and TypeScript serviceability SDKs now parse the trailing `new_interfaces` vec on `Device` introduced by #3665, 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 a sticky `Device.DeserializeError` field so the existing void-returning `DeserializeDevice` signature is preserved. - Bumps `CURRENT_INTERFACE_VERSION` / `CurrentInterfaceVersion` to `4` across the three SDKs to match Rust's `CURRENT_INTERFACE_SCHEMA_VERSION`. Closes #3660. Builds on #3666 / #3667. ## Testing Verification - Hand-built byte-vector tests added in each SDK covering: populated trailing vec, legacy account rebuild, declared-length mismatch, and future-version skip. - `make sdk-test` — all SDK suites green (Go: serviceability + borsh-incremental + revdist; Python: 119 passed; TypeScript: 142 passed across 5 files). - Cross-language framing assertion: empty-name body length is 42 bytes, so on-disk `size` for an empty-name V4 element is 45 — verified in all three test files. - Fixture-driven coverage is deferred to the follow-up fixtures issue per #3660.
elitegreg
added a commit
that referenced
this pull request
May 6, 2026
…ts (#3676) ## Summary - Regenerates `sdk/serviceability/testdata/fixtures/device.{bin,json}` through Device's custom Borsh serializer (post-#3667 on-disk shape) with a populated `new_interfaces` vec: one Vpnv4 loopback carrying a `FlexAlgoNodeSegment`, one physical user-tunnel-endpoint. The legacy `interfaces` slot is now the V2 projection of `new_interfaces` (always V2 per #3653), so the fixture exercises the populated-trailing-vec read path. - Adds `device_legacy.{bin,json}` — pre-#3667 byte shape with only the legacy `interfaces` vec populated and no trailing bytes. SDKs detect the absent trailing vec and rebuild `new_interfaces` from the legacy enum vec, stamping each entry with `Version=CURRENT_INTERFACE_VERSION` and `Size=0`. - Adds `device_future_version.{bin,json}` — same shape as `device.bin`, but the **last** trailing-vec element is doctored to `version=5` with `size += 8` and 8 `0xAB` filler bytes appended at end-of-file. SDKs read the known body fields, then `seek(start + size)` over the junk. - Adds Go fixture-driven tests (`smartcontract/sdk/go/serviceability/fixture_test.go`) — first fixture-loader for the Go serviceability SDK, mirroring the pattern in `sdk/revdist/go/fixture_test.go`. Extends Python/TS fixture tests to cover all three Device fixtures. - Drive-by: `link.bin` (+3 bytes) and `sdk/telemetry/testdata/fixtures/generate-fixtures/Cargo.lock` are pre-existing fixture rot picked up by `make generate-fixtures`. Closes #3661. Depends on #3673 (size-prefixed Interface readers, already merged). ## Testing Verification - `cd sdk/serviceability/testdata/fixtures/generate-fixtures && cargo run` regenerates all fixtures deterministically. - `go test ./smartcontract/sdk/go/serviceability/...` — `TestFixtureDevice`, `TestFixtureDeviceLegacy`, `TestFixtureDeviceFutureVersion` all green. - `cd sdk/serviceability/python && uv run pytest serviceability/tests/test_fixtures.py` — 16 tests pass, including the three new Device test classes. - `cd sdk/serviceability/typescript && bun test serviceability/tests/fixtures.test.ts` — 16 tests pass, including the new "Device legacy" and "Device future-version" describe blocks. - Verified `xxd device_future_version.bin | tail` shows the doctored `0x05` version byte and 8x `0xAB` filler at end-of-file; trailing-element `size` is `original + 8` exactly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements #3665 — second step of the forward-compatible Interface refactor. Branched off
gm/issue-3657-new-interface-struct(#3666 / forward-compatNewInterfacestruct).new_interfaces: Vec<NewInterface>toDeviceaftermax_multicast_publishers.BorshSerializeforDeviceprojects the legacy on-diskinterfacesslot fromnew_interfaces(alwaysInterface::V2per smartcontract: revert default Interface to V2; keep V3 for migrate/backfill #3653) and writes the new vec at the end of the layout. Older readers continue to parse the legacy slot at its existing offset.Device::TryFrom<&[u8]>reads the trailing vec withunwrap_or_default(); legacy accounts (no trailing bytes) getnew_interfacesrebuilt from the legacy enum vec via per-variantTryFrom(V3 projected through V2;flex_algo_node_segmentsalready empty post-migrate).find_interfacereturns(usize, &NewInterface);find_interface_legacyis a temporary helper that returns the V2 projection for unrelated callers (CLI commands, tests) — those migrate tofind_interfacein subsequent issues.Device::replace_interface/push_interface/remove_interfacehelpers to keep both vecs in sync. Without this, the always-V2 legacy projection would silently drop processor-side mutations todevice.interfaceson save. Backfill additionally mirrorsflex_algo_node_segmentsintonew_interfaces(the V2-projected legacy slot doesn't carry segments).Deviations from the issue
new_interfacesmakes processor mutations todevice.interfacesinvisible on save. This PR therefore introduces minimalreplace_interface/push_interface/remove_interfacehelpers and routes existing processors (linkaccept/activate/closeaccount/create/delete/update, device interfaceactivate/create/delete/reject/remove/unlink/update, topologybackfill,migrate_interfaces) through them.MigrateDeviceInterfaces's V2→V3 byte-format expectation is invalidated by the always-V2 projection;test_migrate_device_interfaces_legacy_accountis#[ignore]d with a note. Migrate semantics need a follow-up to either bypass the projection on this admin path or to drop the V3 byte-format guarantee in favor of thenew_interfacestrailing slot.Tests
test_device_serialize_keeps_vecs_in_sync(round-trips a Device with N entries; legacy and new vecs match length and per-element name).test_device_legacy_account_rebuilds_new_vec(hand-serializes a Device omitting the trailing vec;Device::try_fromrebuildsnew_interfacesfrom the legacy vec).test_device_skips_future_interface_in_new_vec(forges aversion=5element with junk bytes inside its size envelope at the head of the trailing slot; the size-prefixed reader skips past unknown trailing bytes and surfaces both elements).test_state_device_serializationandtest_topology_backfill_*for the new source-of-truth innew_interfaces.test_state_compatibility_devicecontinues to pass, now exercising the legacy fallback rebuild path.Testing Verification
make rust-fmt && make rust-lint && make rust-testall pass.test_topology_backfill_populates_vpnv4_loopbacks) verified end-to-end after the V3→V2 projection change: backfill is now idempotent againstnew_interfacesrather than the legacy slot.Notes for the reviewer
find_interfacecall sites intests/and insmartcontract/cli/src/device/interface/{delete,update}.rscontinue to usefind_interface_legacy; per the issue these migrate in subsequent PRs.new_interfaces) requires every processor mutation to also touchnew_interfacesto avoid data loss. Splitting that into a separate PR would leave main in a broken state mid-stack.