Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
1d19177
rfcs: add RFC-18 link classification flex-algo
ben-malbeclabs Mar 16, 2026
0b1b5a6
rfcs: rfc-18 major revision — per-tenant colors, controller config, m…
ben-malbeclabs Mar 20, 2026
4d0d865
rfc18: rename 'color' → 'topology' throughout; address packethog revi…
ben-malbeclabs Mar 30, 2026
6aafc4f
rfc18: rename eos_color_value → color; add UNICAST-DRAINED
ben-malbeclabs Mar 31, 2026
964c19f
rfc18: review pass — accuracy fixes, remove lab testing regurgitation…
ben-malbeclabs Mar 31, 2026
6cdcff8
rfc18: remove topology update command — all fields immutable after cr…
ben-malbeclabs Mar 31, 2026
c64dee2
smartcontract: add AdminGroupBits ResourceExtension with UNICAST-DRAI…
ben-malbeclabs Mar 31, 2026
3d1ceaf
smartcontract: add TopologyInfo, FlexAlgoNodeSegment state structs; I…
ben-malbeclabs Mar 31, 2026
393b89a
smartcontract: add TopologyCreate instruction with admin-group bit al…
ben-malbeclabs Mar 31, 2026
e737303
smartcontract: add TopologyDelete and TopologyClear instructions
ben-malbeclabs Apr 1, 2026
88b151e
smartcontract: add unicast_drained field to Link; contributor-writable
ben-malbeclabs Apr 1, 2026
3818674
smartcontract: add include_topologies to Tenant account; foundation-only
ben-malbeclabs Apr 1, 2026
6ab6b7d
smartcontract: fix include_topologies test assert and fixture generator
ben-malbeclabs Apr 1, 2026
396bbf0
smartcontract: auto-tag UNICAST-DEFAULT topology at link activation
ben-malbeclabs Apr 1, 2026
133cfff
smartcontract: fix activate: add unicast-default owner check; refacto…
ben-malbeclabs Apr 1, 2026
b481271
smartcontract: fix activate: fold owner check into InvalidArgument gu…
ben-malbeclabs Apr 1, 2026
98d968b
smartcontract: fix lint — missing include_topologies fields, unused i…
ben-malbeclabs Apr 1, 2026
19b435f
cli: add doublezero link topology create/delete/clear/list subcommands
ben-malbeclabs Apr 1, 2026
20eab2c
cli: topology delete guard referencing links; list shows link counts …
ben-malbeclabs Apr 1, 2026
c566386
cli: add --include-topologies to doublezero tenant update
ben-malbeclabs Apr 1, 2026
f49389b
cli: add --link-topology and --unicast-drained to doublezero link update
ben-malbeclabs Apr 1, 2026
034e886
cli: resolve topology names in link/tenant get and list display
ben-malbeclabs Apr 1, 2026
a7b4d43
cli: add doublezero-admin migrate command for RFC-18 link topology ba…
ben-malbeclabs Apr 1, 2026
baebb10
cli: fix migrate command — unicast-default seed case, dry-run counter…
ben-malbeclabs Apr 1, 2026
8ac6c2e
cli: add AdminGroupBits to resource CLI type enum
ben-malbeclabs Apr 1, 2026
27101d0
smartcontract: create AdminGroupBits in global-config set
ben-malbeclabs Apr 1, 2026
f4d2b41
cli: remove AdminGroupBits from resource operator commands
ben-malbeclabs Apr 1, 2026
1ad0433
smartcontract: fix topology/clear missing payer account in SDK comman…
ben-malbeclabs Apr 2, 2026
bed9396
smartcontract: add BackfillTopology instruction for post-creation Vpn…
ben-malbeclabs Apr 2, 2026
b5a049d
cli: validate topology name length before PDA derivation
ben-malbeclabs Apr 2, 2026
27583ee
e2e: add doublezero-admin to manager container
ben-malbeclabs Apr 2, 2026
3a9535b
sdk: regenerate fixtures with RFC-18 fields; enable flex_algo_node_se…
ben-malbeclabs Apr 2, 2026
88abcc1
sdk: fix InterfaceV3 deserialization in Go, Python, and TypeScript SDKs
ben-malbeclabs Apr 2, 2026
85d9271
sdk: fix duplicate payer in execute_transaction_inner when payer alre…
ben-malbeclabs Apr 2, 2026
cb4e3c5
sdk/serviceability: add FlexAlgoNodeSegment type to Python and TypeSc…
ben-malbeclabs Apr 2, 2026
106c5a2
smartcontract: rustfmt formatting fixes in instructions.rs
ben-malbeclabs Apr 2, 2026
673ab56
controlplane/controller: fix Interface zero-value comparison after sl…
ben-malbeclabs Apr 2, 2026
9950ade
merge: sync with origin/main
ben-malbeclabs Apr 2, 2026
d99304f
smartcontract: add link_flags, topology list/filter CLI, SDK updates …
ben-malbeclabs Apr 6, 2026
c376b63
smartcontract: link_flags bitmask, update validation, topology PDA ch…
ben-malbeclabs Apr 6, 2026
3aeb090
smartcontract: add topology account validation and cap enforcement to…
ben-malbeclabs Apr 6, 2026
ae55268
smartcontract: fix mtu values in link_wan_test fixtures; add topology…
ben-malbeclabs Apr 6, 2026
d224ac3
sdk/go: fix parse_valid_link test and controller findLink after LinkT…
ben-malbeclabs Apr 6, 2026
4e61546
smartcontract: restore start-test.sh to match main
ben-malbeclabs Apr 6, 2026
81b03d2
smartcontract: fix post-rebase
ben-malbeclabs Apr 7, 2026
25b32e7
smartcontract: add RFC-18 flex-algo CHANGELOG entries
ben-malbeclabs Apr 7, 2026
9d63cf6
smartcontract: fix incorrect MTU in test_update_cyoa_interface_with_i…
ben-malbeclabs Apr 7, 2026
ed33157
smartcontract: merge origin/main — add SetUserBGPStatus (variant 106)…
ben-malbeclabs Apr 7, 2026
43191cf
sdk/ts: fix deserializeInterface to read flex_algo_node_segments for …
ben-malbeclabs Apr 7, 2026
b9f2729
activator: automatically backfill flex-algo node segments on topology…
ben-malbeclabs Apr 6, 2026
267ba06
activator: add RFC-18 flex-algo CHANGELOG entry
ben-malbeclabs Apr 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@ All notable changes to this project will be documented in this file.
- Add `twamp-debug` diagnostic tool for testing kernel timestamping support on switches; sends real TWAMP probes to verify which SO_TIMESTAMPING modes (RX/TX software/hardware/sched) actually deliver timestamps, and reports RTT statistics comparing userspace vs kernel timestamp sources
- E2E Tests
- Switch backward compatibility test to install versioned CLI binaries from GitHub releases instead of Cloudsmith apt repos; version enumeration now uses the GitHub API directly from Go rather than querying apt-cache inside the container
- Smartcontract
- Add `TopologyInfo` onchain account for IS-IS flex-algo link classification: auto-assigned TE admin-group bit (1–62), derived flex-algo number (128 + bit), and constraint type (`include-any`/`include-all`); capped at 62 topologies via `AdminGroupBits` resource extension
- Add `link_topologies: Vec<Pubkey>` (capped at 8) and `link_flags: u8` (bit 0 = unicast-drained) to the `Link` account
- Add `include_topologies` to the `Tenant` account for topology-filtered routing opt-in
- Enforce UNICAST-DEFAULT topology existence as a precondition for link activation
- CLI
- Add `doublezero link topology` subcommands: `create`, `delete`, `clear`, `list`, `backfill`
- Add `--link-topology <name>` and `--unicast-drained <bool>` flags to `doublezero link update`
- Add `--topology <name>` filter to `doublezero link list` (`default` = untagged links)
- Add `--include-topologies <name>` flag to `doublezero tenant update`
- Add `doublezero-admin migrate flex-algo [--dry-run]` to tag existing links with UNICAST-DEFAULT and backfill node segments
- SDK
- Update Go, Python, and TypeScript SDKs with `TopologyInfo` deserialization and new `link_topologies`, `link_flags`, and `include_topologies` fields
- Activator
- Automatically backfill flex-algo node segment IDs for all activated devices when a new `TopologyInfo` account is created onchain
- Automatically backfill existing topologies' node segments when a Vpnv4 loopback interface is activated on a device
- Client
- Add `doublezero_connection_info` Prometheus metric exposing connection metadata (user_type, network, current_device, metro, tunnel_name, tunnel_src, tunnel_dst) ([#3201](https://github.com/malbeclabs/doublezero/pull/3201))
- Add `doublezero_connection_rtt_nanoseconds` and `doublezero_connection_loss_percentage` Prometheus metrics reporting RTT and packet loss to the current connected device
Expand Down
14 changes: 14 additions & 0 deletions activator/src/process/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,13 @@ mod tests {
)
.returning(|_, _| Ok(Signature::new_unique()));

// After activating a vpnv4 loopback, the interface manager queries existing topologies
// to auto-backfill flex-algo node segments.
client
.expect_gets()
.with(predicate::eq(AccountType::Topology))
.returning(|_| Ok(HashMap::new()));

// interfaces get checked on activated devices
device.status = DeviceStatus::Activated;

Expand Down Expand Up @@ -662,6 +669,13 @@ mod tests {
)
.returning(|_, _| Ok(Signature::new_unique()));

// After activating a vpnv4 loopback, the interface manager queries existing topologies
// to auto-backfill flex-algo node segments.
client
.expect_gets()
.with(predicate::eq(AccountType::Topology))
.returning(|_| Ok(HashMap::new()));

process_device_event(
&client,
&pubkey,
Expand Down
65 changes: 62 additions & 3 deletions activator/src/process/iface_mgr.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,67 @@
use crate::{idallocator::IDAllocator, ipblockallocator::IPBlockAllocator};
use doublezero_program_common::types::NetworkV4;
use doublezero_sdk::{
commands::device::interface::{
activate::ActivateDeviceInterfaceCommand, reject::RejectDeviceInterfaceCommand,
remove::RemoveDeviceInterfaceCommand, unlink::UnlinkDeviceInterfaceCommand,
commands::{
device::interface::{
activate::ActivateDeviceInterfaceCommand, reject::RejectDeviceInterfaceCommand,
remove::RemoveDeviceInterfaceCommand, unlink::UnlinkDeviceInterfaceCommand,
},
topology::{backfill::BackfillTopologyCommand, list::ListTopologyCommand},
},
CurrentInterfaceVersion, Device, DoubleZeroClient, InterfaceStatus, InterfaceType,
LoopbackType,
};
use log::{error, info};
use solana_sdk::pubkey::Pubkey;

/// After a vpnv4 loopback is activated, automatically backfill flex-algo node segments
/// on that device for every topology that currently exists on the ledger.
fn backfill_device_for_all_topologies(
client: &dyn DoubleZeroClient,
device_pubkey: &Pubkey,
device_code: &str,
) {
let topologies = match ListTopologyCommand.execute(client) {
Ok(t) => t,
Err(e) => {
error!(
"Failed to list topologies for backfill after loopback activation on {device_code}: {e}"
);
return;
}
};

if topologies.is_empty() {
return;
}

info!(
"Backfilling {} topology/topologies for device {device_code} after vpnv4 loopback activation",
topologies.len()
);

for topology in topologies.values() {
let cmd = BackfillTopologyCommand {
name: topology.name.clone(),
device_pubkeys: vec![*device_pubkey],
};
match cmd.execute(client) {
Ok(sig) => {
info!(
"Backfilled topology '{}' for device {device_code}: {sig}",
topology.name
);
}
Err(e) => {
error!(
"Failed to backfill topology '{}' for device {device_code}: {e}",
topology.name
);
}
}
}
}

/// Stateless interface manager for onchain allocation mode.
/// Does not use local allocators - all allocation is handled by the smart contract.
pub struct InterfaceMgrStateless<'a> {
Expand Down Expand Up @@ -74,6 +125,10 @@ impl<'a> InterfaceMgrStateless<'a> {
&NetworkV4::default(),
0,
);

if iface.loopback_type == LoopbackType::Vpnv4 {
backfill_device_for_all_topologies(self.client, device_pubkey, &device.code);
}
}

/// Handle interface deletion (stateless mode - no local deallocation)
Expand Down Expand Up @@ -254,6 +309,10 @@ impl<'a> InterfaceMgr<'a> {
&iface.ip_net,
iface.node_segment_idx,
);

if iface.loopback_type == LoopbackType::Vpnv4 {
backfill_device_for_all_topologies(self.client, device_pubkey, &device.code);
}
}

/// Handle interface deletion and resource cleanup
Expand Down
15 changes: 15 additions & 0 deletions activator/src/process/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ mod tests {
link_health: doublezero_serviceability::state::link::LinkHealth::Pending,
desired_status:
doublezero_serviceability::state::link::LinkDesiredStatus::Activated,

link_topologies: Vec::new(),
link_flags: 0,
};

let tunnel_cloned = tunnel.clone();
Expand Down Expand Up @@ -397,6 +400,9 @@ mod tests {
side_z_iface_name: "Ethernet1".to_string(),
link_health: doublezero_serviceability::state::link::LinkHealth::Pending,
desired_status: doublezero_serviceability::state::link::LinkDesiredStatus::Activated,

link_topologies: Vec::new(),
link_flags: 0,
};

let link_cloned = link.clone();
Expand Down Expand Up @@ -457,6 +463,9 @@ mod tests {
link_health: doublezero_serviceability::state::link::LinkHealth::Pending,
desired_status:
doublezero_serviceability::state::link::LinkDesiredStatus::Activated,

link_topologies: Vec::new(),
link_flags: 0,
};

let tunnel_clone = tunnel.clone();
Expand Down Expand Up @@ -544,6 +553,9 @@ mod tests {
link_health: doublezero_serviceability::state::link::LinkHealth::Pending,
desired_status:
doublezero_serviceability::state::link::LinkDesiredStatus::Activated,

link_topologies: Vec::new(),
link_flags: 0,
};

// SDK command fetches the link internally
Expand Down Expand Up @@ -623,6 +635,9 @@ mod tests {
link_health: doublezero_serviceability::state::link::LinkHealth::Pending,
desired_status:
doublezero_serviceability::state::link::LinkDesiredStatus::Activated,

link_topologies: Vec::new(),
link_flags: 0,
};

// SDK command fetches the link internally
Expand Down
53 changes: 52 additions & 1 deletion activator/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use doublezero_sdk::{
commands::{
device::list::ListDeviceCommand, exchange::list::ListExchangeCommand,
link::list::ListLinkCommand, location::list::ListLocationCommand,
user::list::ListUserCommand,
topology::backfill::BackfillTopologyCommand, user::list::ListUserCommand,
},
doublezeroclient::DoubleZeroClient,
AccountData, Device, DeviceStatus, Exchange, GetGlobalConfigCommand, InterfaceType, Link,
Expand Down Expand Up @@ -65,6 +65,35 @@ pub struct ProcessorStateless<T: DoubleZeroClient> {
multicastgroups: MulticastGroupMap,
}

/// Solana transaction account limit minus the 4 fixed accounts in BackfillTopologyCommand
/// (topology PDA, segment_routing_ids PDA, globalstate, payer).
const BACKFILL_BATCH_SIZE: usize = 28;

/// Backfill flex-algo node segments on a set of devices for a given topology, batching
/// device pubkeys to stay within Solana's per-transaction account limit.
fn backfill_topology_for_devices(
client: &dyn DoubleZeroClient,
topology_name: &str,
device_pubkeys: &[Pubkey],
) {
if device_pubkeys.is_empty() {
return;
}
info!(
"Backfilling topology '{topology_name}' for {} device(s)",
device_pubkeys.len()
);
for chunk in device_pubkeys.chunks(BACKFILL_BATCH_SIZE) {
let cmd = BackfillTopologyCommand {
name: topology_name.to_string(),
device_pubkeys: chunk.to_vec(),
};
if let Err(e) = cmd.execute(client) {
error!("Failed to backfill topology '{topology_name}' for device batch: {e}");
}
}
}

/// Reserve segment routing IDs and loopback IPs for devices that have active allocations.
/// Devices in Activated, Drained, DeviceProvisioning, or LinkProvisioning states all
/// hold allocated addresses that must not be handed out to new devices.
Expand Down Expand Up @@ -340,6 +369,14 @@ impl<T: DoubleZeroClient> Processor<T> {
error!("Error processing access pass event: {e}");
});
}
AccountData::Topology(topology) => {
let device_pubkeys: Vec<Pubkey> = self.devices.keys().copied().collect();
backfill_topology_for_devices(
self.client.as_ref(),
&topology.name,
&device_pubkeys,
);
}
_ => {}
};
metrics::counter!("doublezero_activator_event_handled").increment(1);
Expand Down Expand Up @@ -439,6 +476,14 @@ impl<T: DoubleZeroClient> ProcessorStateless<T> {
error!("Error processing access pass event: {e}");
});
}
AccountData::Topology(topology) => {
let device_pubkeys: Vec<Pubkey> = self.devices.keys().copied().collect();
backfill_topology_for_devices(
self.client.as_ref(),
&topology.name,
&device_pubkeys,
);
}
_ => {}
};
metrics::counter!("doublezero_activator_event_handled").increment(1);
Expand Down Expand Up @@ -761,6 +806,9 @@ mod tests {
side_z_iface_name: "Ethernet1".to_string(),
link_health: doublezero_serviceability::state::link::LinkHealth::Pending,
desired_status: doublezero_serviceability::state::link::LinkDesiredStatus::Activated,

link_topologies: Vec::new(),
link_flags: 0,
};

let mut existing_links: HashMap<Pubkey, Link> = HashMap::new();
Expand Down Expand Up @@ -795,6 +843,9 @@ mod tests {
side_z_iface_name: "Ethernet3".to_string(),
link_health: doublezero_serviceability::state::link::LinkHealth::Pending,
desired_status: doublezero_serviceability::state::link::LinkDesiredStatus::Activated,

link_topologies: Vec::new(),
link_flags: 0,
};

let new_link_cloned = new_link.clone();
Expand Down
38 changes: 34 additions & 4 deletions client/doublezero/src/cli/link.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
use clap::{Args, Subcommand};
use doublezero_cli::link::{
accept::AcceptLinkCliCommand, delete::*, dzx_create::CreateDZXLinkCliCommand, get::*,
latency::LinkLatencyCliCommand, list::*, sethealth::SetLinkHealthCliCommand, update::*,
wan_create::*,
use doublezero_cli::{
link::{
accept::AcceptLinkCliCommand, delete::*, dzx_create::CreateDZXLinkCliCommand, get::*,
latency::LinkLatencyCliCommand, list::*, sethealth::SetLinkHealthCliCommand, update::*,
wan_create::*,
},
topology::{
backfill::BackfillTopologyCliCommand, clear::ClearTopologyCliCommand,
create::CreateTopologyCliCommand, delete::DeleteTopologyCliCommand,
list::ListTopologyCliCommand,
},
};

#[derive(Args, Debug)]
Expand Down Expand Up @@ -53,4 +60,27 @@ pub enum LinkCommands {
// Hidden because this is an internal/operational command not intended for general CLI users.
#[clap(hide = true)]
SetHealth(SetLinkHealthCliCommand),
/// Manage link topologies
#[clap()]
Topology(TopologyLinkCommand),
}

#[derive(Args, Debug)]
pub struct TopologyLinkCommand {
#[command(subcommand)]
pub command: TopologyCommands,
}

#[derive(Debug, Subcommand)]
pub enum TopologyCommands {
/// Create a new topology
Create(CreateTopologyCliCommand),
/// Delete a topology
Delete(DeleteTopologyCliCommand),
/// Clear a topology from links
Clear(ClearTopologyCliCommand),
/// Backfill FlexAlgoNodeSegment entries on existing Vpnv4 loopbacks
Backfill(BackfillTopologyCliCommand),
/// List all topologies
List(ListTopologyCliCommand),
}
2 changes: 2 additions & 0 deletions client/doublezero/src/command/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,7 @@ mod tests {
metro_routing: false,
route_liveness: false,
billing: TenantBillingConfig::default(),
include_topologies: vec![],
};

let mut tenants = HashMap::new();
Expand Down Expand Up @@ -1397,6 +1398,7 @@ mod tests {
metro_routing: false,
route_liveness: false,
billing: TenantBillingConfig::default(),
include_topologies: vec![],
};
tenants.insert(pk, tenant.clone());
(pk, tenant)
Expand Down
1 change: 1 addition & 0 deletions client/doublezero/src/dzd_latency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ mod tests {
ip_net: NetworkV4::new(ip, 32).unwrap(),
node_segment_idx: 0,
user_tunnel_endpoint: true,
flex_algo_node_segments: vec![],
})
})
.collect();
Expand Down
9 changes: 8 additions & 1 deletion client/doublezero/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::cli::{
AirdropCommands, AuthorityCommands, FeatureFlagsCommands, FoundationAllowlistCommands,
GlobalConfigCommands, QaAllowlistCommands,
},
link::LinkCommands,
link::{LinkCommands, TopologyCommands},
location::LocationCommands,
user::UserCommands,
};
Expand Down Expand Up @@ -232,6 +232,13 @@ async fn main() -> eyre::Result<()> {
LinkCommands::Latency(args) => args.execute(&client, &mut handle),
LinkCommands::Delete(args) => args.execute(&client, &mut handle),
LinkCommands::SetHealth(args) => args.execute(&client, &mut handle),
LinkCommands::Topology(args) => match args.command {
TopologyCommands::Create(args) => args.execute(&client, &mut handle),
TopologyCommands::Delete(args) => args.execute(&client, &mut handle),
TopologyCommands::Clear(args) => args.execute(&client, &mut handle),
TopologyCommands::Backfill(args) => args.execute(&client, &mut handle),
TopologyCommands::List(args) => args.execute(&client, &mut handle),
},
},
Command::AccessPass(command) => match command.command {
cli::accesspass::AccessPassCommands::Set(args) => args.execute(&client, &mut handle),
Expand Down
2 changes: 1 addition & 1 deletion controlplane/controller/internal/controller/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ type Interface struct {

// toInterface validates onchain data for a serviceability interface and converts it to a controller interface.
func toInterface(iface serviceability.Interface) (Interface, error) {
if iface == (serviceability.Interface{}) {
if iface.IpNet == ([5]byte{}) && iface.Name == "" {
return Interface{}, errors.New("serviceability interface cannot be nil")
}

Expand Down
Loading
Loading