Skip to content

smartcontract: append new_interfaces vec to Device with custom serializer#3667

Merged
elitegreg merged 2 commits intomainfrom
gm/issue-3665-device-new-interfaces-vec
May 5, 2026
Merged

smartcontract: append new_interfaces vec to Device with custom serializer#3667
elitegreg merged 2 commits intomainfrom
gm/issue-3665-device-new-interfaces-vec

Conversation

@elitegreg
Copy link
Copy Markdown
Contributor

Summary

Implements #3665 — second step of the forward-compatible Interface refactor. Branched off gm/issue-3657-new-interface-struct (#3666 / forward-compat NewInterface struct).

  • Append new_interfaces: Vec<NewInterface> to Device after max_multicast_publishers.
  • Custom BorshSerialize for Device projects the legacy on-disk interfaces slot from new_interfaces (always Interface::V2 per 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 with unwrap_or_default(); legacy accounts (no trailing bytes) get new_interfaces rebuilt from the legacy enum vec via per-variant TryFrom (V3 projected through V2; flex_algo_node_segments already empty post-migrate).
  • find_interface returns (usize, &NewInterface); find_interface_legacy is a temporary helper that returns the V2 projection for unrelated callers (CLI commands, tests) — those migrate to find_interface in subsequent issues.
  • Mutations route through new Device::replace_interface / push_interface / remove_interface helpers to keep both vecs in sync. Without this, the always-V2 legacy projection would silently drop processor-side mutations to device.interfaces on save. Backfill additionally mirrors flex_algo_node_segments into new_interfaces (the V2-projected legacy slot doesn't carry segments).

Deviations from the issue

  • The issue says "no processor changes" but the always-V2 projection from new_interfaces makes processor mutations to device.interfaces invisible on save. This PR therefore introduces minimal replace_interface/push_interface/remove_interface helpers and routes existing processors (link accept/activate/closeaccount/create/delete/update, device interface activate/create/delete/reject/remove/unlink/update, topology backfill, migrate_interfaces) through them.
  • MigrateDeviceInterfaces's V2→V3 byte-format expectation is invalidated by the always-V2 projection; test_migrate_device_interfaces_legacy_account is #[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 the new_interfaces trailing slot.

Tests

  • Added test_device_serialize_keeps_vecs_in_sync (round-trips a Device with N entries; legacy and new vecs match length and per-element name).
  • Added test_device_legacy_account_rebuilds_new_vec (hand-serializes a Device omitting the trailing vec; Device::try_from rebuilds new_interfaces from the legacy vec).
  • Added test_device_skips_future_interface_in_new_vec (forges a version=5 element 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).
  • Updated test_state_device_serialization and test_topology_backfill_* for the new source-of-truth in new_interfaces.
  • test_state_compatibility_device continues to pass, now exercising the legacy fallback rebuild path.

Testing Verification

  • make rust-fmt && make rust-lint && make rust-test all pass.
  • Backfill idempotency (test_topology_backfill_populates_vpnv4_loopbacks) verified end-to-end after the V3→V2 projection change: backfill is now idempotent against new_interfaces rather than the legacy slot.

Notes for the reviewer

  • Roughly ~30 find_interface call sites in tests/ and in smartcontract/cli/src/device/interface/{delete,update}.rs continue to use find_interface_legacy; per the issue these migrate in subsequent PRs.
  • PR exceeds the 500-line guideline because the projection direction (legacy ← new_interfaces) requires every processor mutation to also touch new_interfaces to avoid data loss. Splitting that into a separate PR would leave main in a broken state mid-stack.

…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.
@elitegreg elitegreg force-pushed the gm/issue-3665-device-new-interfaces-vec branch from 7a7aa83 to 1728db6 Compare May 5, 2026 17:27
@elitegreg elitegreg marked this pull request as ready for review May 5, 2026 17:28
@elitegreg elitegreg changed the base branch from gm/issue-3657-new-interface-struct to main May 5, 2026 17:29
@elitegreg elitegreg enabled auto-merge (squash) May 5, 2026 17:51
Comment thread smartcontract/programs/doublezero-serviceability/src/state/device.rs Outdated
@elitegreg elitegreg merged commit c78c06c into main May 5, 2026
33 checks passed
@elitegreg elitegreg deleted the gm/issue-3665-device-new-interfaces-vec branch May 5, 2026 19:10
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.
@elitegreg elitegreg linked an issue May 5, 2026 that may be closed by this pull request
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

serviceability: add new_interfaces vec to Device with custom serializer

2 participants