Capability-Based Feature Gating
This document explains how to use ICN's capability-based feature gating system to build protocols that gracefully handle peers running different software versions.
Overview
ICN implements protocol version negotiation during the Hello handshake. Each node announces:
- Its supported protocol version range (min, current, max)
- A bitmap of feature capabilities it supports
- Its software version string
The network layer automatically negotiates the highest mutually-supported protocol version and calculates the intersection of capabilities. This information is then available for application-level feature gating.
Capability Flags
All capabilities are defined in `icn-net/src/version.rs`:
pub struct CapabilityFlags: u64 {
const E2E_ENCRYPTION = 0b00000001; // Phase 10: End-to-end encryption
const SIGNED_MESSAGES = 0b00000010; // Phase 9: Message signing
const GRACEFUL_RESTART = 0b00000100; // State persistence
const TOPOLOGY_AWARE = 0b00001000; // Topology-aware routing
const TRUST_RATE_LIMITING = 0b00010000; // Trust-based rate limits
const GOSSIP_PULL = 0b00100000; // Gossip pull protocol
const MULTI_DEVICE = 0b01000000; // Phase 11: Multi-device identity
const ECONOMIC_SAFETY = 0b10000000; // Phase 12: Economic safety rails
}
Checking Peer Capabilities
The NetworkHandle provides several methods for capability checking:
1. Check if a Peer Supports a Capability
if network_handle.peer_has_capability(&peer_did, CapabilityFlags::E2E_ENCRYPTION).await {
// Peer supports encrypted communication
network_handle.send_encrypted_message(/* ... */).await?;
} else {
// Fall back to signed-only communication
network_handle.send_message(peer_did, signed_msg).await?;
}
2. Get All Peers With a Capability
// Broadcast encrypted messages only to capable peers
let encrypted_peers = network_handle.get_peers_with_capability(
CapabilityFlags::E2E_ENCRYPTION
).await;
for peer in encrypted_peers {
network_handle.send_encrypted_message(&peer, /* ... */).await?;
}
3. Get Peer's Protocol Version
if let Some(version) = network_handle.get_peer_protocol_version(&peer_did).await {
if version >= 2 {
// Use protocol v2 features
} else {
// Fall back to v1 behavior
}
}
4. Get Full Connection Info
if let Some(info) = network_handle.get_peer_connection_info(&peer_did).await {
println!("Peer: {}", info.did);
println!("Version: {}", info.negotiated_version);
println!("Software: {}", info.peer_software);
println!("Capabilities: {:?}", info.peer_capabilities.describe());
}
Feature Gating Patterns
Pattern 1: Graceful Degradation
Use newer features when available, fall back to basic functionality otherwise:
async fn send_sensitive_data(
network: &NetworkHandle,
recipient: &Did,
data: &[u8],
) -> Result<()> {
if network.peer_has_capability(recipient, CapabilityFlags::E2E_ENCRYPTION).await {
// Best: End-to-end encrypted
network.send_encrypted_message(
recipient,
keypair,
x25519_secret,
sequence,
data
).await?;
} else if network.peer_has_capability(recipient, CapabilityFlags::SIGNED_MESSAGES).await {
// Good: Signed for authenticity
let signed = SignedEnvelope::new(/* ... */)?;
network.send_message(recipient.clone(), NetworkMessage::signed(/* ... */)).await?;
} else {
// Basic: No extra security (legacy peer)
warn!("Peer {} lacks modern security features", recipient);
network.send_message(recipient.clone(), NetworkMessage::new(/* ... */)).await?;
}
Ok(())
}
Pattern 2: Require Capability
Reject operations if the peer doesn't support required features:
async fn request_contract_execution(
network: &NetworkHandle,
executor: &Did,
contract: &Contract,
) -> Result<()> {
// Economic safety required for contract execution
if !network.peer_has_capability(executor, CapabilityFlags::ECONOMIC_SAFETY).await {
anyhow::bail!(
"Peer {} does not support ECONOMIC_SAFETY - cannot execute contracts",
executor
);
}
// Proceed with contract execution request...
Ok(())
}
Pattern 3: Broadcast to Capable Peers Only
Send feature-specific broadcasts only to peers that support them:
async fn broadcast_topology_update(
network: &NetworkHandle,
update: TopologyUpdate,
) -> Result<()> {
let topology_peers = network.get_peers_with_capability(
CapabilityFlags::TOPOLOGY_AWARE
).await;
if topology_peers.is_empty() {
info!("No topology-aware peers connected");
return Ok(());
}
for peer in topology_peers {
network.send_message(peer, update.clone().into()).await?;
}
Ok(())
}
Pattern 4: Version-Based Protocol Changes
Use protocol version for wire-format changes:
async fn sync_ledger_entries(
network: &NetworkHandle,
peer: &Did,
entries: &[Entry],
) -> Result<()> {
let version = network.get_peer_protocol_version(peer).await.unwrap_or(1);
let message = if version >= 2 {
// Protocol v2: Compressed entries
LedgerSyncMessage::V2 {
entries: compress_entries(entries)?,
}
} else {
// Protocol v1: Uncompressed
LedgerSyncMessage::V1 {
entries: entries.to_vec(),
}
};
network.send_message(peer.clone(), message.into()).await?;
Ok(())
}
Adding New Capabilities
When adding a new feature that requires capability detection:
1. Define the Capability Flag
Edit `icn-net/src/version.rs`:
pub struct CapabilityFlags: u64 {
// ... existing flags ...
/// Supports quantum-resistant signatures (Phase 15)
const QUANTUM_RESISTANT = 0b100000000;
}
2. Add to Current Capabilities
Update CapabilityFlags::current() if the feature is implemented:
pub fn current() -> Self {
Self::E2E_ENCRYPTION
| Self::SIGNED_MESSAGES
| Self::GRACEFUL_RESTART
| Self::TOPOLOGY_AWARE
| Self::TRUST_RATE_LIMITING
| Self::GOSSIP_PULL
| Self::MULTI_DEVICE
| Self::ECONOMIC_SAFETY
| Self::QUANTUM_RESISTANT // Add new capability
}
3. Add to Description Method
Update CapabilityFlags::describe() for human-readable output:
pub fn describe(&self) -> Vec<&'static str> {
let mut features = Vec::new();
// ... existing checks ...
if self.contains(Self::QUANTUM_RESISTANT) {
features.push("quantum_resistant");
}
features
}
4. Use in Your Code
if network.peer_has_capability(&peer, CapabilityFlags::QUANTUM_RESISTANT).await {
// Use quantum-resistant signatures
} else {
// Fall back to Ed25519
}
Backward Compatibility
Legacy Nodes
Nodes that don't send version_info in Hello messages are treated as:
- Protocol version: 1
- Capabilities: empty set
- Software: "legacy-node"
Your code should handle this gracefully:
let caps = network.get_peer_connection_info(&peer).await
.map(|info| info.peer_capabilities)
.unwrap_or(CapabilityFlags::empty());
if caps.is_empty() {
// Legacy peer - use basic protocol
} else {
// Modern peer - check specific capabilities
}
Capability Negotiation
The network layer automatically calculates the intersection of capabilities:
Local capabilities: E2E_ENCRYPTION | SIGNED_MESSAGES | GRACEFUL_RESTART
Remote capabilities: E2E_ENCRYPTION | SIGNED_MESSAGES | TOPOLOGY_AWARE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Common capabilities: E2E_ENCRYPTION | SIGNED_MESSAGES
Only use features that appear in the common set.
Testing Capability-Based Code
Unit Tests
#[tokio::test]
async fn test_capability_based_routing() {
let network = create_test_network().await;
// Mock peer with encryption support
let encrypted_peer = Did::from_str("did:icn:alice")?;
mock_peer_capabilities(
&network,
&encrypted_peer,
CapabilityFlags::E2E_ENCRYPTION
).await;
// Mock peer without encryption
let legacy_peer = Did::from_str("did:icn:bob")?;
mock_peer_capabilities(
&network,
&legacy_peer,
CapabilityFlags::empty()
).await;
// Should send encrypted to alice
assert!(network.peer_has_capability(&encrypted_peer, CapabilityFlags::E2E_ENCRYPTION).await);
// Should fall back to signed for bob
assert!(!network.peer_has_capability(&legacy_peer, CapabilityFlags::E2E_ENCRYPTION).await);
}
Integration Tests
Test mixed-version networks:
#[tokio::test]
async fn test_mixed_version_network() {
// Spawn v1 node
let node_v1 = spawn_node_with_version(1, CapabilityFlags::SIGNED_MESSAGES).await;
// Spawn v2 node
let node_v2 = spawn_node_with_version(2, CapabilityFlags::E2E_ENCRYPTION | CapabilityFlags::SIGNED_MESSAGES).await;
// Connect them
node_v2.dial(&node_v1.addr, &node_v1.did).await?;
// Wait for Hello exchange
tokio::time::sleep(Duration::from_millis(100)).await;
// v2 should detect v1's limited capabilities
let v1_caps = node_v2.network.get_peer_connection_info(&node_v1.did).await.unwrap();
assert_eq!(v1_caps.negotiated_version, 1);
assert_eq!(v1_caps.peer_capabilities, CapabilityFlags::SIGNED_MESSAGES);
}
Metrics
The system exports Prometheus metrics for monitoring version distribution:
icn_network_peer_versions{version="1"}- Number of peers at each protocol versionicn_network_peer_capabilities{capability="e2e_encryption"}- Number of peers with each capabilityicn_network_version_negotiation_success_total{negotiated_version="1"}- Successful negotiationsicn_network_version_negotiation_failures_total{reason="incompatible_version"}- Failed negotiations
Best Practices
Always check capabilities before using optional features
- Don't assume all peers support modern features
- Provide fallback behavior for legacy peers
Log capability mismatches
- Helps diagnose version rollout issues
- Use
info!()for expected behavior,warn!()for degraded mode
Test with mixed versions
- Write integration tests with old and new nodes
- Verify graceful degradation paths work
Document capability requirements
- Clearly state which capabilities a feature requires
- Include examples of fallback behavior
Use semantic versioning
- Protocol version for breaking wire-format changes
- Capabilities for optional features
- Software version for informational purposes
Monitor version distribution
- Track metrics to understand deployment progress
- Alert if too many peers lack critical capabilities
Example: Gossip Protocol
The gossip protocol demonstrates capability-based feature gating:
async fn propagate_entry(
network: &NetworkHandle,
entry: &Entry,
topic: &str,
) -> Result<()> {
// Get all peers subscribed to this topic
let subscribers = get_topic_subscribers(topic).await;
for peer in subscribers {
// Check if peer supports pull protocol
if network.peer_has_capability(&peer, CapabilityFlags::GOSSIP_PULL).await {
// Just send announcement hash (peer can pull if interested)
send_announcement(&network, &peer, entry.hash()).await?;
} else {
// Legacy: Send full entry
send_full_entry(&network, &peer, entry).await?;
}
}
Ok(())
}
Troubleshooting
Peer Capability Check Returns False
Possible causes:
- Peer hasn't completed Hello handshake yet
- Peer is a legacy node
- Peer's software doesn't support the capability
Solution:
if let Some(info) = network.get_peer_connection_info(&peer).await {
println!("Peer capabilities: {:?}", info.peer_capabilities.describe());
println!("Peer software: {}", info.peer_software);
} else {
println!("No connection info for peer {} - handshake not complete", peer);
}
Version Negotiation Fails
Error message: "No compatible version. Local: [1-2], Remote: [3-4]"
Cause: No overlap in supported version ranges
Solution:
- Check PROTOCOL_VERSION, MIN_SUPPORTED_VERSION, MAX_SUPPORTED_VERSION
- Ensure gradual version rollout (keep backward compatibility window)
- Don't drop old version support until all nodes upgraded
Capability Set Is Empty
Possible causes:
- Talking to a legacy node
- Peer capabilities genuinely empty (very old version)
Solution: Handle empty capabilities gracefully with fallback behavior
See Also
- Protocol Version Constants - Version numbers
- Version Negotiation Logic - Negotiation algorithm
- Network Actor - Hello handshake implementation
- Prometheus Metrics - Monitoring