Skip to content

serviceability: add new_interfaces vec to Device with custom serializer #3665

@elitegreg

Description

@elitegreg

Part of the forward-compatible Device interfaces refactor.
Design: https://gist.github.com/elitegreg/e1d27c97034656a980fa8d7628ae16c5

Depends on #3657 (NewInterface struct).

Scope

Append new_interfaces: Vec<NewInterface> to Device after max_multicast_publishers, behind a custom BorshSerialize that keeps the existing interfaces: Vec<Interface> (legacy enum vec) length-locked with the new vec via per-write projection.

The legacy field stays at its current byte position so old readers don't change.

Tasks

state/device.rs

  • Drop #[derive(BorshSerialize)] on Device.
  • Append pub new_interfaces: Vec<NewInterface> as the last field.
  • Custom BorshSerialize for Device:
    • Rebuild legacy interfaces from new_interfaces via InterfaceV2::from(&NewInterface) wrapped as Interface::V2(...) before serializing (always V2 to match the post-smartcontract: revert default Interface to V2; keep V3 for migrate/backfill #3653 on-disk current).
    • Serialize all fields in declaration order. The legacy vec lands at its existing offset; new_interfaces lands at the end.
    • assert_eq!(legacy.len(), new_interfaces.len()).
  • Update TryFrom<&[u8]> for Device (lines 481-523):
    • Read legacy interfaces at the existing offset.
    • Read all post-interface fields up through max_multicast_publishers.
    • Try to read trailing Vec<NewInterface> with unwrap_or_default() so legacy accounts (no trailing bytes) yield Vec::new().
    • If trailing vec is empty, populate new_interfaces from legacy via the per-variant TryFrom<&InterfaceV1/V2> for NewInterface conversions. Otherwise use the trailing vec as-is.
    • If len(trailing) != len(legacy), panic — should not happen outside the empty-trailing case.
  • Update Default impl to initialize both vecs as Vec::new().
  • Device::find_interface returns (usize, &NewInterface) (was (usize, CurrentInterfaceVersion)). Note: ~30 call sites ripple — those move in subsequent issues, not this one. Where blockers arise, add a temporary helper that returns the legacy projection so this PR stays self-contained.

Tests

  • test_device_serialize_keeps_vecs_in_sync: round-trip a Device with N entries; assert len(legacy) == len(new_interfaces) == N and per-element name equality.
  • test_device_legacy_account_rebuilds_new_vec: hand-serialized device omitting the trailing vec — Device::try_from populates new_interfaces from legacy.
  • test_device_skips_future_interface_in_new_vec: forge version=5 on the first new_interfaces element with N trailing junk bytes; both interfaces remain accessible.
  • Existing test_state_compatibility_device continues to pass and now exercises the legacy fallback.

Notes

  • No processor changes in this issue. Keeping both vecs in sync is the responsibility of the device serializer, so write-path callers in subsequent issues can mutate new_interfaces and rely on the projection.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions