Scope-Bounded Trust: Structural Centralization Prevention
Status: Implemented (Wave 2)
Last Updated: 2026-02-01
Related Issues: #1008 (Wave 2)
Executive Summary
Trust centralization is a political-economy problem, not just a technical one.
Scope-bounded trust provides structural prevention of trust centralization by limiting trust computation to organizational boundaries. This is NOT a damping mechanism (decay, transitive damping) but a fundamental architectural constraint.
Key Insight: By partitioning trust into scopes (cooperatives, federations, domains, topic classes), no single entity can accumulate cross-boundary trust power. This prevents "super-nodes" that dominate multiple organizational contexts.
Design Principle
Scope must NOT enter kernel.
scope_idlives ONLY inicn-trust(app crate)- Kernel receives
ConstraintSetwith opaque constraint values - Kernel must NOT pattern-match on scope
- Scope selection is performed by apps/PolicyOracles → output is pure
ConstraintSet
This maintains the Meaning Firewall: the kernel enforces constraints without understanding their semantic origin.
Scope Types
Trust edges can be scoped to prevent centralization across organizational boundaries:
1. Cooperative Scope (coop:<id>)
Trust within a single cooperative.
Use Case: Member vouching, governance participation, work distribution within one coop.
Example: coop:food-coop-123
2. Federation Scope (federation:<id>)
Trust within a federation of cooperatives.
Use Case: Inter-cooperative trade credit, federated dispute resolution, resource sharing.
Example: federation:regional-food-network
3. Domain Scope (domain:<id>)
Trust within a governance domain.
Use Case: Specialized expertise (dispute mediators, auditors, technical stewards).
Example: domain:dispute-resolution
4. Topic Class Scope (topic:<class>)
Trust within a service class (compute, storage, etc.).
Use Case: Provider reputation scoped to service type. Storage providers don't automatically gain compute trust.
Example: topic:compute, topic:storage
5. Global Scope
Trust without scope boundaries (default for backward compatibility).
Use Case: Legacy edges, cross-scope foundational trust (rare).
Example: Foundational cooperative members, system architects.
Data Model
ScopeId Type
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ScopeId {
/// Trust within a single cooperative
Cooperative(String),
/// Trust within a federation of cooperatives
Federation(String),
/// Trust within a governance domain
Domain(String),
/// Trust within a topic class (e.g., compute, storage)
TopicClass(String),
/// Trust without scope boundaries (default)
Global,
}
TrustEdge Extension
pub struct TrustEdge {
pub source: Did,
pub target: Did,
pub labels: Vec<String>,
pub score: TrustScore,
pub evidence: Vec<TrustEvidence>,
pub expires_at: Option<u64>,
pub created_at: u64,
pub graph_type: TrustGraphType,
/// Optional scope identifier (Wave 2)
/// Defaults to None (global scope) for backward compatibility
#[serde(default)]
pub scope_id: Option<ScopeId>,
}
Backward Compatibility: Existing edges have scope_id: None, treated as global scope.
Scope-Aware Trust Computation
Method: compute_trust_score_in_scope
impl TrustGraph {
pub fn compute_trust_score_in_scope(
&self,
target: &Did,
scope: &ScopeId,
) -> Result<f64> {
// Filter edges to scope before computing trust
let scoped_edges: Vec<_> = self
.get_outgoing_edges(&self.own_did)?
.into_iter()
.filter(|e| match &e.scope_id {
Some(edge_scope) => edge_scope == scope,
None => scope.is_global(),
})
.collect();
// Compute trust using only scoped edges
// (direct + transitive within scope)
compute_score_from_edges(target, &scoped_edges)
}
}
Scope Isolation
Trust computed in one scope DOES NOT affect trust in another scope:
// Alice has high trust in coop scope
let coop_trust = graph.compute_trust_score_in_scope(
&alice,
&ScopeId::cooperative("food-coop-123")
)?;
// coop_trust = 0.85
// Alice has ZERO trust in federation scope
let fed_trust = graph.compute_trust_score_in_scope(
&alice,
&ScopeId::federation("regional-network")
)?;
// fed_trust = 0.0
// Scopes are isolated - no cross-contamination
Centralization Metrics (Observational Only)
Purpose
Metrics detect concentration of trust within a scope. They are observational only - they feed dashboards and governance, NOT automatic enforcement.
Metrics Flow
Metrics ─► Governance ─► PolicyOracle ─► ConstraintSet ─► Kernel
│ │
(human vote) (pure data)
Never: Metrics ─► Kernel (direct enforcement)
CentralizationMetrics
pub struct CentralizationMetrics {
/// Gini coefficient (0.0 = equal, 1.0 = one node has all trust)
pub gini_coefficient: f64,
/// Number of nodes in scope
pub node_count: usize,
/// Total inbound trust across all nodes
pub total_inbound_trust: f64,
/// Fraction of trust held by top 10% of nodes
pub top_10_percent_share: f64,
/// Betweenness centrality statistics
pub betweenness: BetweennessStats,
}
Gini Coefficient
Measures inequality in trust distribution:
- 0.0: Perfect equality (all nodes have equal trust)
- 1.0: Perfect inequality (one node has all trust)
Formula:
G = (2 * Σ(i * x_i)) / (n * Σ(x_i)) - (n + 1) / n
Where edges are sorted by inbound trust.
Top-N Concentration
Measures power concentration in top percentile:
let top_10 = metrics.top_10_percent_share;
// If top_10 > 0.5, top 10% of nodes hold >50% of trust
Betweenness Centrality
Measures "bridge" nodes that connect clusters:
- High betweenness: Node is a bottleneck/bridge between groups
- Low betweenness: Node is peripheral or in a dense cluster
pub struct BetweennessStats {
pub max: f64, // Most central node
pub mean: f64, // Average centrality
pub std_dev: f64, // Variance in centrality
}
Computing Metrics
use icn_trust::{TrustGraphAnalyzer, ScopeId};
let analyzer = TrustGraphAnalyzer::default();
let scope = ScopeId::cooperative("food-coop-123");
let metrics = analyzer.compute_centralization_metrics(&graph, &scope)?;
println!("Gini coefficient: {:.3}", metrics.gini_coefficient);
println!("Top 10% hold: {:.1}% of trust", metrics.top_10_percent_share * 100.0);
Bridge Nodes (Future Work)
Status: Not yet implemented. Included here for design reference.
Bridge nodes span multiple scopes and could accumulate cross-scope influence. When implemented, bridges will require explicit declaration and stricter constraints.
Planned Design
BridgeDeclarationstruct with DID, scopes, timestamp, and signature- Gossip topic
bridges:declarationsfor discovery - PolicyOracles apply stricter constraints (reduced rate limits, fewer topics)
- Governance policies can formalize "approved bridges" through voting
- Firewall compliance: Bridge status → PolicyOracle → ConstraintSet (kernel never sees bridge semantics)
See Merge Gate for tracking.
Migration Path
Existing Edges
All existing edges default to scope_id: None (global scope):
// Old edge (pre-Wave 2)
let edge = TrustEdge {
source: alice,
target: bob,
score: 0.8,
// scope_id is None (global)
..
};
// Serde deserializes to scope_id: None
// Global scope queries match these edges
Adding Scope to Edges
Apps can add scope when creating new edges:
// Coop-scoped trust
let edge = TrustEdge::new_scoped(
alice,
bob,
TrustScore::new(0.8)?,
TrustGraphType::Social,
ScopeId::cooperative("food-coop-123")
);
graph.add_edge(edge)?;
Gradual Migration
- Phase 1: All edges global (current state)
- Phase 2: New edges optionally scoped
- Phase 3: Governance policies enforce scope on new edges
- Phase 4: Legacy edges remain global or migrated by governance vote
Firewall Compliance Verification
Rule 1: Kernel Must Not Import icn-trust
✅ Verified: Kernel crates (icn-core, icn-kernel-api, icn-net, icn-gossip) do NOT depend on icn-trust.
CI enforcement: .github/scripts/firewall_denylist.py
Rule 2: Kernel Only Accepts Pure Data
✅ Verified: Kernel entrypoints accept only:
ConstraintSet(pure data)CapabilityToken(pure data)Did,Timestamp,Hash(primitives)
No ScopeId in kernel interfaces.
Rule 3: Semantic Lookup in PolicyOracle Only
✅ Verified: Trust score computation by scope happens in:
icn-trust::TrustGraph::compute_trust_score_in_scope(app layer)- PolicyOracle implementations (app layer)
Kernel NEVER calls scope-aware methods.
Rule 4: No Pattern-Matching on ConstraintSet.custom
✅ Verified: Kernel uses ONLY typed ConstraintSet fields:
rate_limit: Option<RateLimit>max_topics: Option<u32>- etc.
Kernel does NOT check custom keys for scope-related branching.
Rule 5: Mechanism vs Policy
✅ Verified:
| Component | Kernel Owns (Mechanism) | Apps Own (Value) |
|---|---|---|
| Rate Limiting | Token bucket algorithm | Messages/sec |
| Topic Admission | Max topics check | Max value (25 vs 100) |
| Connection Limits | Max connections check | Max value |
| Scope Trust | (none - not in kernel) | Scope filtering |
Example: PolicyOracle Integration
// In apps/trust/src/oracle.rs
impl PolicyOracle for ScopedTrustOracle {
fn evaluate(&self, request: &PolicyRequest) -> PolicyDecision {
// Extract scope from request context (app logic)
let scope = self.extract_scope_from_request(request)?;
// Compute scope-bounded trust (app logic)
let trust_score = self.graph
.compute_trust_score_in_scope(&request.actor, &scope)?;
// ═══════════════════════════════════════════════════════
// Semantic logic ENDS here. Below is pure data.
// ═══════════════════════════════════════════════════════
// Translate trust score to constraints (pure data)
let rate_limit = if trust_score >= 0.7 {
RateLimit::new(200, 50) // Federated
} else if trust_score >= 0.4 {
RateLimit::new(100, 25) // Partner
} else {
RateLimit::new(20, 5) // Known
};
PolicyDecision::allow_with(
ConstraintSet::new()
.with_rate_limit(rate_limit),
)
}
}
Kernel Enforcement (mechanism only):
// In icn-core (kernel)
fn apply_constraints(actor: &Did, constraints: &ConstraintSet) {
if let Some(limit) = constraints.rate_limit {
self.rate_limiter.apply(actor, limit);
// Kernel doesn't know WHY limit is 200/50 vs 20/5
}
}
Governance Integration
Metrics → Governance
Centralization metrics trigger governance discussions:
- Dashboard displays Gini coefficient per scope
- Alert if
gini > 0.7(high inequality) - Proposal created: "Limit top 10% trust share to 40%"
- Vote by cooperative members
- PolicyOracle updated to enforce new limits
PolicyOracle → Enforcement
// After governance vote, PolicyOracle enforces limit
impl PolicyOracle for GovernanceTrustOracle {
fn evaluate(&self, request: &PolicyRequest) -> PolicyDecision {
let scope = self.extract_scope(request)?;
let metrics = self.analyzer.compute_centralization_metrics(&self.graph, &scope)?;
// Governance policy: reject if Gini > 0.7
if metrics.gini_coefficient > 0.7 {
return PolicyDecision::deny(
"Trust centralization exceeds cooperative policy limit"
);
}
// Otherwise, standard constraints
PolicyDecision::allow()
}
}
Firewall Holds: Governance policy → PolicyOracle → ConstraintSet. Kernel sees only opaque allow/deny.
Research Directions (Phase 4)
Inbound Trust Saturation Cap
Prevent any single node from accumulating unbounded trust:
effective_trust = Σ edges[0..N] + Σ edges[N..] * decay^(i-N)
Where:
- First N edges count fully
- Subsequent edges decay exponentially
- Prevents "super-nodes" even with many incoming edges
Status: Prototype in Phase 4 (optional)
Merge Gate
Required for Wave 2 completion:
-
TrustEdge.scope_idfield exists - Trust tests pass with scope
- Gossip tests pass with scope-aware admission
- No scope logic in kernel crates (firewall holds)
- Metrics exposed but not auto-enforcing
- Bridge node declaration exists (observational only)
Current Status: 4/6 complete (documentation phase)
References
- Wave 1: #1007 - Firewall Contract
- Wave 2: #1008 - Scope-Bounded Trust (this)
- Firewall Architecture:
docs/architecture/KERNEL_APP_SEPARATION.md - Trust Graph Design:
icn/crates/icn-trust/src/lib.rs
Authors
- Implementation: GitHub Copilot (2026-02-01)
- Design Review: ICN Core Team