Status: Approved
This RFC adds separate per-device limits for unicast and multicast users, allowing operators to control each user type independently rather than relying on a combined max_users limit.
Multicast traffic can have significantly higher bandwidth utilization than unicast due to amplification--a single multicast stream is replicated to many subscribers. The existing max_users limit applies to all user types combined, giving operators no way to manage this difference. Separate limits allow operators to cap multicast users based on bandwidth capacity while allowing more unicast users on the same device.
-
Single combined limit only - Keep using
max_usersfor all user types. Simple but doesn't address the bandwidth disparity between unicast and multicast traffic. -
Percentage-based limits - Express multicast limit as a percentage of
max_users(e.g., "max 20% multicast"). More complex to implement and reason about; absolute numbers are clearer for operators. -
Bandwidth-based limits - Limit by estimated bandwidth rather than user count. Requires traffic estimation and is harder to enforce deterministically onchain.
-
Separate tunnel ID pools - Partition the tunnel ID range between user types. Adds complexity to the controller and reduces flexibility; a device with few multicast users couldn't reclaim those IDs for unicast.
The per-type count approach (option chosen) provides simple, deterministic limits while keeping the shared tunnel ID pool for maximum flexibility.
Four new fields on the Device account:
pub struct Device {
// ... existing fields ...
pub users_count: u16, // Total users (kept for backward compatibility)
pub max_users: u16, // Total max (kept for backward compatibility)
// Per-type user counts and limits
pub unicast_users_count: u16, // Count of non-multicast users
pub multicast_users_count: u16, // Count of multicast users
pub max_unicast_users: u16, // Per-device unicast limit
pub max_multicast_users: u16, // Per-device multicast limit
}When creating a user, the onchain program checks max_users first, then the per-type limit. The max_users check takes precedence—if it blocks, type-specific limits are not evaluated. Counters are incremented on user creation and decremented on user close.
New error codes:
MaxUnicastUsersExceeded(73)MaxMulticastUsersExceeded(74)
The CLI checks limits before submitting a transaction, providing immediate feedback. The onchain program remains the source of truth.
View limits (device get/list):
max_users: 255
users_count: 45
max_unicast_users: 80
unicast_users_count: 35
max_multicast_users: 48
multicast_users_count: 10
Set limits (device update):
doublezero device update --pubkey <device> --max-unicast-users 100 --max-multicast-users 28| Component | Changes |
|---|---|
| Onchain program | user/create.rs, user/create_subscribe.rs: limit checking + counter increment; user/closeaccount.rs: counter decrement; device/update.rs: set limits |
| Device state | New fields with Borsh deserialization defaults |
| CLI | Display new fields in device get/list; new flags for device update |
| SDKs | New Device fields in Go, Python, TypeScript SDKs |
| Component | Reason |
|---|---|
| Controller | Limits are enforced onchain; controller doesn't partition tunnel IDs |
| Activator | No changes needed for limit enforcement |
| doublezerod | Limits enforced onchain; old clients continue to work |
| Tunnel ID pool | All user types share the single pool (500-627) |
- Deploy code with 0 = unlimited semantics
- All existing devices default to 0 (unlimited) for new fields
- No operator action required
- Operators set limits per-device via
device update - Devices without explicit limits remain unlimited
- Change 0 semantics to mean "no users of this type allowed"
- Requires all devices to have explicit limits configured first
No explicit migration required:
- Borsh deserialization defaults - Existing Device accounts get 0 for all new fields when read
- First write persists defaults - Next update to a device serializes the new fields
- No ResourceExtension changes - All user types share the existing tunnel ID pool
- Existing users and tunnel IDs unaffected
- Existing devices default to 0 (unlimited during rollout)
max_usersandusers_countmaintained for total tracking- Old clients continue to work; onchain program handles enforcement
-
Phase 3 timing - When should we change 0 semantics from "unlimited" to "disallowed"? This requires coordination to ensure all devices have explicit limits configured first.
-
Global default limits - Should there be a global config for default per-type limits applied to new devices? Currently each device must be configured individually.
-
Limit change restrictions - Should we prevent lowering limits below current user counts? Currently allowed (existing users remain, but no new ones can join).