ICN Gap Remediation Sprint Plan
Date: 2025-12-17
Status: ACTIVE - Sprint 1 Starting
Goal: Close HIGH-priority gaps for production readiness
Timeline: 4 weeks (Sprint 1), then ongoing
Sprint 1: Quick Wins & Critical Path (Week 1-4)
Priority Order (Impact × Effort)
- Ledger Replay Attack Fix ⚡ HIGH IMPACT, LOW EFFORT (1-2 days)
- Upgrade Coordination Foundation 🎯 HIGH IMPACT, MEDIUM EFFORT (1 week)
- Trust Graph Gaming - Basic Detection 🎯 HIGH IMPACT, MEDIUM EFFORT (1 week)
- Gossip Amplification Protection ⚡ MEDIUM IMPACT, LOW EFFORT (2-3 days)
- Scalability Testing Setup 📊 HIGH IMPACT, MEDIUM EFFORT (3-4 days)
Why This Order?
- Replay Attack: Security vulnerability, easy fix, immediate value
- Upgrade Coordination: Foundational for all future protocol changes
- Trust Gaming: Prevents abuse, essential for pilot scaling
- Gossip Protection: Network stability, prevents DoS
- Scalability Testing: Parallel track, informs all other work
Task Breakdown
1. Ledger Replay Attack Fix ⚡
File: icn/crates/icn-net/src/envelope.rs, icn-ledger/src/ledger.rs
Duration: 1-2 days
Priority: CRITICAL
Current Vulnerability:
Time 0:00: Transaction submitted (nonce: 0xABC)
Time 0:01: Transaction processed
Time 0:04: Attacker replays (still within 5min window)
Result: Duplicate processing possible
Fix Strategy:
// Phase 1: Nonce finalization in ReplayGuard
pub struct NonceClaim {
nonce: [u8; 16],
claimed_at: u64,
finalized_at: Option<u64>,
purpose: NonceUsage,
}
pub enum NonceUsage {
LedgerTransaction { entry_hash: ContentHash },
NetworkMessage { msg_hash: ContentHash },
ContractExecution { task_hash: ContentHash },
}
impl ReplayGuard {
/// Claim nonce (returns false if already claimed)
pub fn claim_nonce(&mut self, nonce: &[u8; 16], purpose: NonceUsage) -> bool;
/// Finalize nonce (permanent, non-replayable)
pub fn finalize_nonce(&mut self, nonce: &[u8; 16]) -> Result<()>;
/// Check allows only unclaimed or unfinalizedنonces
pub fn check_nonce(&self, nonce: &[u8; 16]) -> bool {
match self.nonces.get(nonce) {
None => true, // New nonce
Some(claim) => claim.finalized_at.is_none(), // Not finalized yet
}
}
}
// Phase 2: Ledger integration
impl Ledger {
pub fn append_entry(&mut self, entry: JournalEntry) -> Result<()> {
// Validate entry...
// Finalize nonces from all transactions
for tx in &entry.transactions {
if let Some(nonce) = tx.extract_nonce() {
self.replay_guard.finalize_nonce(&nonce)?;
}
}
// Append to journal...
}
}
Tasks:
- Add
finalized_atfield toReplayGuard - Implement
finalize_nonce()method - Update ledger to finalize on append
- Add tests for replay within window
- Update documentation
Test Cases:
#[test]
fn test_replay_after_finalization() {
// Submit transaction at T=0
// Finalize nonce at T=1
// Attempt replay at T=2 (should fail even within 5min)
// Assert: Rejected
}
#[test]
fn test_replay_before_finalization() {
// Submit transaction at T=0
// Attempt replay at T=1 (before finalization)
// Assert: Allowed (for idempotency during processing)
}
2. Upgrade Coordination Foundation 🎯
Files: icn/crates/icn-governance/src/proposal.rs, icn-core/src/supervisor.rs
Duration: 1 week
Priority: HIGH
Goal: Enable governance-driven protocol upgrades
Implementation Plan:
Step 1: New Proposal Type (Day 1-2)
// icn-governance/src/proposal.rs
pub enum ProposalPayload {
// ... existing variants
/// Protocol upgrade proposal
ProtocolUpgrade {
/// New version (e.g., "1.2.0")
version: Version,
/// Breaking changes description
breaking_changes: Vec<String>,
/// Migration guide URL
migration_guide: Option<String>,
/// Upgrade deadline (Unix timestamp)
deadline: u64,
/// Minimum version required (nodes below this rejected)
min_required_version: Option<Version>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Version {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl Version {
pub fn parse(s: &str) -> Result<Self>;
pub fn is_compatible_with(&self, other: &Version) -> bool;
}
Step 2: Version Tracking (Day 2-3)
// icn-core/src/supervisor.rs
pub struct Supervisor {
// ... existing fields
/// Current protocol version
protocol_version: Version,
/// Pending upgrade (if governance approved)
pending_upgrade: Option<PendingUpgrade>,
/// Version adoption tracker
version_tracker: VersionTracker,
}
pub struct PendingUpgrade {
pub proposal_id: ProposalId,
pub target_version: Version,
pub approved_at: u64,
pub deadline: u64,
pub migration_applied: bool,
}
pub struct VersionTracker {
/// Peer versions (DID -> Version)
peer_versions: HashMap<Did, Version>,
/// Update timestamp
last_updated: HashMap<Did, u64>,
}
impl VersionTracker {
/// Record peer version from Hello message
pub fn record_peer_version(&mut self, peer: Did, version: Version);
/// Get adoption rate for a version
pub fn adoption_rate(&self, version: &Version) -> f64;
/// Get peers below minimum version
pub fn peers_below_version(&self, min: &Version) -> Vec<Did>;
}
Step 3: Hello Message Update (Day 3-4)
// icn-net/src/protocol.rs
pub struct NetworkMessage {
// ... existing variants
Hello {
did: Did,
version: u32, // <- Already exists, rename to protocol_version
capabilities: CapabilityFlags,
x25519_key: [u8; 32],
// NEW: Semantic version
protocol_semver: Version,
},
}
// Version negotiation
impl NetworkActor {
fn handle_hello(&mut self, peer: Did, hello: HelloMsg) -> Result<()> {
// Record peer version
self.version_tracker.record_peer_version(peer.clone(), hello.protocol_semver);
// Check compatibility
if !self.protocol_version.is_compatible_with(&hello.protocol_semver) {
warn!("Peer {} has incompatible version: {}", peer, hello.protocol_semver);
// If below minimum required, reject connection
if let Some(min) = &self.min_required_version {
if hello.protocol_semver < *min {
return Err(anyhow!("Version too old: {} < {}", hello.protocol_semver, min));
}
}
}
// Continue with connection...
}
}
Step 4: Metrics & Monitoring (Day 4-5)
// icn-obs/src/metrics.rs
pub fn init_upgrade_metrics() {
gauge!(
"icn_upgrade_adoption_rate",
"Percentage of peers on target version"
);
gauge!(
"icn_upgrade_deadline_remaining_seconds",
"Seconds until upgrade deadline"
);
counter!(
"icn_upgrade_rejected_connections_total",
"Connections rejected due to old version"
);
}
Step 5: Governance Integration (Day 5-7)
// icn-governance/src/execution.rs
impl GovernanceActor {
async fn execute_protocol_upgrade(&mut self, proposal: ProtocolUpgradeProposal) -> Result<()> {
info!("Executing protocol upgrade to version {}", proposal.version);
// Store pending upgrade
let pending = PendingUpgrade {
proposal_id: proposal.id.clone(),
target_version: proposal.version.clone(),
approved_at: now_ms(),
deadline: proposal.deadline,
migration_applied: false,
};
// Notify supervisor
self.supervisor_tx.send(SupervisorMsg::UpgradeApproved(pending)).await?;
// Announce via gossip
self.gossip.announce(
"governance:upgrade",
bincode::serialize(&pending)?,
).await?;
Ok(())
}
}
Tasks:
- Add
ProtocolUpgradeproposal type - Implement
Versionstruct with parsing - Add
VersionTrackerto supervisor - Update Hello message with semver
- Add version compatibility checks
- Add metrics (adoption rate, deadline)
- Integrate with governance execution
- Write tests (version negotiation, rejection)
- Document upgrade workflow
3. Trust Graph Gaming - Basic Detection 🎯
Files: icn/crates/icn-trust/src/, new file anomaly.rs
Duration: 1 week
Priority: HIGH
Goal: Detect and flag suspicious trust patterns
Step 1: Anomaly Detector Skeleton (Day 1)
// icn-trust/src/anomaly.rs
pub struct TrustGraphAnalyzer {
trust_graph: Arc<RwLock<TrustGraph>>,
config: AnomalyConfig,
}
pub struct AnomalyConfig {
/// Circular vouch cycle length threshold
pub max_cycle_length: usize, // 5
/// Minimum cycle strength to flag
pub min_cycle_strength: f64, // 0.8
/// Sybil cluster detection ratio
pub sybil_density_ratio: f64, // 5.0 (internal/external)
/// Rapid growth threshold (% per week)
pub rapid_growth_threshold: f64, // 0.5 (50%)
}
pub enum TrustAnomaly {
CircularVouching {
cycle: Vec<Did>,
cycle_strength: f64,
detected_at: u64,
},
SybilCluster {
cluster: Vec<Did>,
internal_density: f64,
external_density: f64,
detected_at: u64,
},
RapidTrustGrowth {
did: Did,
growth_rate: f64,
baseline: f64,
current: f64,
detected_at: u64,
},
}
Step 2: Circular Vouch Detection (Day 2-3)
impl TrustGraphAnalyzer {
/// Detect circular vouching using DFS cycle detection
pub fn detect_circular_vouching(&self) -> Vec<TrustAnomaly> {
let graph = self.trust_graph.read().unwrap();
let mut anomalies = vec![];
// Tarjan's SCC algorithm for cycle detection
let sccs = self.find_strongly_connected_components(&graph);
for scc in sccs {
if scc.len() > 1 && scc.len() <= self.config.max_cycle_length {
// Calculate cycle strength
let strength = self.calculate_cycle_strength(&scc, &graph);
if strength >= self.config.min_cycle_strength {
anomalies.push(TrustAnomaly::CircularVouching {
cycle: scc,
cycle_strength: strength,
detected_at: now_ms(),
});
}
}
}
anomalies
}
fn find_strongly_connected_components(&self, graph: &TrustGraph) -> Vec<Vec<Did>> {
// Tarjan's algorithm implementation
// Returns: Vec of SCCs (each SCC is a Vec of DIDs)
todo!("Implement Tarjan's SCC")
}
fn calculate_cycle_strength(&self, cycle: &[Did], graph: &TrustGraph) -> f64 {
let mut total_strength = 0.0;
let mut edge_count = 0;
for i in 0..cycle.len() {
let from = &cycle[i];
let to = &cycle[(i + 1) % cycle.len()];
if let Some(edge) = graph.get_edge(from, to) {
total_strength += edge.weight;
edge_count += 1;
}
}
if edge_count > 0 {
total_strength / edge_count as f64
} else {
0.0
}
}
}
Step 3: Sybil Cluster Detection (Day 3-4)
impl TrustGraphAnalyzer {
/// Detect Sybil clusters (high internal, low external trust)
pub fn detect_sybil_clusters(&self) -> Vec<TrustAnomaly> {
let graph = self.trust_graph.read().unwrap();
let mut anomalies = vec![];
// Use community detection (Louvain algorithm)
let communities = self.detect_communities(&graph);
for community in communities {
let internal = self.calculate_internal_density(&community, &graph);
let external = self.calculate_external_density(&community, &graph);
let ratio = if external > 0.0 {
internal / external
} else {
f64::INFINITY
};
if ratio >= self.config.sybil_density_ratio {
anomalies.push(TrustAnomaly::SybilCluster {
cluster: community,
internal_density: internal,
external_density: external,
detected_at: now_ms(),
});
}
}
anomalies
}
fn calculate_internal_density(&self, community: &[Did], graph: &TrustGraph) -> f64 {
let mut edge_count = 0;
let max_edges = community.len() * (community.len() - 1);
for from in community {
for to in community {
if from != to && graph.has_edge(from, to) {
edge_count += 1;
}
}
}
edge_count as f64 / max_edges as f64
}
fn calculate_external_density(&self, community: &[Did], graph: &TrustGraph) -> f64 {
let all_dids: Vec<Did> = graph.all_dids().collect();
let external: Vec<_> = all_dids.iter()
.filter(|did| !community.contains(did))
.collect();
if external.is_empty() {
return 0.0;
}
let mut edge_count = 0;
let max_edges = community.len() * external.len();
for from in community {
for to in &external {
if graph.has_edge(from, to) {
edge_count += 1;
}
}
}
edge_count as f64 / max_edges as f64
}
}
Step 4: Rapid Growth Detection (Day 4-5)
// icn-trust/src/history.rs (new file)
pub struct TrustScoreHistory {
/// (DID, timestamp) -> trust score
history: BTreeMap<(Did, u64), f64>,
}
impl TrustScoreHistory {
/// Record current trust score
pub fn record(&mut self, did: Did, score: f64) {
let timestamp = now_ms();
self.history.insert((did, timestamp), score);
// Prune old entries (>90 days)
self.prune_old_entries();
}
/// Get trust score N days ago
pub fn get_historical(&self, did: &Did, days_ago: u64) -> Option<f64> {
let target_ts = now_ms() - (days_ago * 24 * 60 * 60 * 1000);
// Find closest entry
self.history.range(..(did.clone(), target_ts))
.rev()
.find(|((d, _), _)| d == did)
.map(|(_, &score)| score)
}
/// Calculate growth rate over period
pub fn growth_rate(&self, did: &Did, days: u64) -> Option<f64> {
let current = self.get_historical(did, 0)?;
let baseline = self.get_historical(did, days)?;
if baseline > 0.0 {
Some((current - baseline) / baseline)
} else {
None
}
}
}
// icn-trust/src/anomaly.rs (continued)
impl TrustGraphAnalyzer {
/// Detect rapid trust growth
pub fn detect_rapid_growth(&self, history: &TrustScoreHistory) -> Vec<TrustAnomaly> {
let graph = self.trust_graph.read().unwrap();
let mut anomalies = vec![];
for did in graph.all_dids() {
if let Some(growth_rate) = history.growth_rate(&did, 7) { // 7-day window
if growth_rate >= self.config.rapid_growth_threshold {
let baseline = history.get_historical(&did, 7).unwrap_or(0.0);
let current = history.get_historical(&did, 0).unwrap_or(0.0);
anomalies.push(TrustAnomaly::RapidTrustGrowth {
did,
growth_rate,
baseline,
current,
detected_at: now_ms(),
});
}
}
}
anomalies
}
}
Step 5: Integration & Reporting (Day 5-7)
// icn-core/src/supervisor.rs
pub struct Supervisor {
// ... existing fields
/// Trust anomaly detector
trust_analyzer: Option<TrustGraphAnalyzer>,
/// Detected anomalies (for operator review)
trust_anomalies: Vec<TrustAnomaly>,
}
impl Supervisor {
/// Run anomaly detection (periodic task, every 24 hours)
async fn run_trust_anomaly_detection(&mut self) {
if let Some(analyzer) = &self.trust_analyzer {
let circular = analyzer.detect_circular_vouching();
let sybil = analyzer.detect_sybil_clusters();
let rapid = analyzer.detect_rapid_growth(&self.trust_history);
let mut new_anomalies = Vec::new();
new_anomalies.extend(circular);
new_anomalies.extend(sybil);
new_anomalies.extend(rapid);
if !new_anomalies.is_empty() {
warn!("Detected {} trust anomalies", new_anomalies.len());
// Store for operator review
self.trust_anomalies.extend(new_anomalies.clone());
// Publish to governance for community review
for anomaly in &new_anomalies {
self.publish_anomaly_report(anomaly).await;
}
// Update metrics
metrics::trust::anomalies_detected_inc(new_anomalies.len());
}
}
}
async fn publish_anomaly_report(&self, anomaly: &TrustAnomaly) {
// Create governance proposal for community review
// (Optional, can be manual for now)
}
}
Tasks:
- Create
anomaly.rsmodule - Implement Tarjan's SCC algorithm
- Implement circular vouch detection
- Implement Sybil cluster detection
- Create
TrustScoreHistoryfor tracking - Implement rapid growth detection
- Add periodic detection task (24h)
- Add metrics (anomalies detected, types)
- Write tests (each detection type)
- Document anomaly types & thresholds
4. Gossip Amplification Protection ⚡
Files: icn/crates/icn-gossip/src/actor.rs, icn-net/src/rate_limit.rs
Duration: 2-3 days
Priority: MEDIUM-HIGH
Goal: Prevent Sybil attack via gossip spam
Global Rate Limiter (Day 1-2)
// icn-gossip/src/rate_limit.rs (new file)
pub struct GlobalRateLimiter {
/// Time window for rate limiting
window: Duration, // 60 seconds
/// Maximum messages per window (across all peers)
max_messages_per_window: usize, // 1000
/// Current count (resets every window)
current_count: AtomicUsize,
/// Window start time
window_start: AtomicU64,
/// Trust-weighted allocation enabled
trust_weighted: bool,
}
impl GlobalRateLimiter {
/// Allocate message budget based on peer trust
pub fn allocate_budget(&self, peer_trust: f64, total_peers: usize) -> usize {
if !self.trust_weighted {
return self.max_messages_per_window / total_peers;
}
// Trust-weighted allocation
// Higher trust = more budget
let base = self.max_messages_per_window as f64 / total_peers as f64;
let multiplier = 1.0 + peer_trust; // 1.0-2.0x based on trust
(base * multiplier).round() as usize
}
/// Check if message should be accepted (global limit)
pub fn check_global_limit(&self) -> bool {
let now = now_ms();
let window_start = self.window_start.load(Ordering::Relaxed);
// Reset window if expired
if now - window_start > self.window.as_millis() as u64 {
self.current_count.store(0, Ordering::Relaxed);
self.window_start.store(now, Ordering::Relaxed);
}
// Check limit
let count = self.current_count.fetch_add(1, Ordering::Relaxed);
count < self.max_messages_per_window
}
}
// icn-gossip/src/actor.rs
impl GossipActor {
fn handle_incoming_message(&mut self, from: Did, msg: GossipMessage) -> Result<()> {
// Check per-peer rate limit (existing)
if !self.rate_limiter.check_and_consume(&from, 1.0)? {
warn!("Rate limit exceeded for peer {}", from);
return Err(anyhow!("Rate limit exceeded"));
}
// NEW: Check global rate limit
if !self.global_rate_limiter.check_global_limit() {
warn!("Global rate limit exceeded (total messages)");
metrics::gossip::global_rate_limit_exceeded_inc();
return Err(anyhow!("Global rate limit exceeded"));
}
// Process message...
}
}
Tasks:
- Create
GlobalRateLimiter - Add trust-weighted budget allocation
- Integrate with GossipActor
- Add metrics (global limit exceeded)
- Add config options (window, max)
- Write tests (Sybil spam scenario)
5. Scalability Testing Setup 📊
Files: New icn/loadtest/ directory
Duration: 3-4 days
Priority: HIGH (parallel track)
Goal: Automated load testing to find breaking points
Setup (Day 1)
# Create loadtest directory
mkdir -p icn/loadtest
cd icn/loadtest
# Setup Python virtual environment
python3 -m venv venv
source venv/bin/activate
# Install locust
pip install locust requests websocket-client
# Create locustfile.py
Locust Test Script (Day 1-2)
# icn/loadtest/locustfile.py
from locust import HttpUser, task, between
import random
import time
class ICNUser(HttpUser):
wait_time = between(1, 3)
def on_start(self):
"""Initialize user (create DID, connect)"""
# Generate test DID
self.did = f"did:icn:test{random.randint(1000, 9999)}"
# Connect to gateway
response = self.client.post("/api/v1/auth/connect", json={
"did": self.did
})
if response.status_code == 200:
self.token = response.json()["token"]
@task(5)
def create_trust_edge(self):
"""Create trust edge (common operation)"""
target = f"did:icn:test{random.randint(1000, 9999)}"
self.client.post("/api/v1/trust", json={
"target": target,
"weight": random.uniform(0.5, 1.0)
}, headers={"Authorization": f"Bearer {self.token}"})
@task(3)
def ledger_transaction(self):
"""Submit ledger transaction"""
recipient = f"did:icn:test{random.randint(1000, 9999)}"
self.client.post("/api/v1/ledger/transfer", json={
"to": recipient,
"amount": random.randint(1, 100),
"currency": "hours"
}, headers={"Authorization": f"Bearer {self.token}"})
@task(2)
def query_balance(self):
"""Query ledger balance"""
self.client.get(
f"/api/v1/ledger/balance/{self.did}",
headers={"Authorization": f"Bearer {self.token}"}
)
@task(1)
def submit_compute_task(self):
"""Submit compute task"""
self.client.post("/api/v1/compute/tasks", json={
"wasm_hash": "0x1234...",
"args": {"input": "test"}
}, headers={"Authorization": f"Bearer {self.token}"})
class StressTest(HttpUser):
"""High-load stress test"""
wait_time = between(0.1, 0.5) # Faster
@task
def rapid_transactions(self):
# Burst of transactions
for _ in range(10):
self.client.post("/api/v1/ledger/transfer", json={
"to": "did:icn:stress_target",
"amount": 1,
"currency": "hours"
})
Test Scenarios (Day 2-3)
# Scenario 1: Baseline (10 users, 5 minutes)
locust -f locustfile.py --host=http://localhost:8080 \
--users=10 --spawn-rate=2 --run-time=5m --headless
# Scenario 2: Medium load (100 users, 10 minutes)
locust -f locustfile.py --host=http://localhost:8080 \
--users=100 --spawn-rate=10 --run-time=10m --headless
# Scenario 3: High load (500 users, 15 minutes)
locust -f locustfile.py --host=http://localhost:8080 \
--users=500 --spawn-rate=50 --run-time=15m --headless
# Scenario 4: Stress test (1000 users, 20 minutes)
locust -f locustfile.py --host=http://localhost:8080 \
--users=1000 --spawn-rate=100 --run-time=20m --headless
# Scenario 5: Spike test (0→500→0 users)
# Run manually with web UI: http://localhost:8089
Analysis Script (Day 3-4)
# icn/loadtest/analyze.py
import pandas as pd
import matplotlib.pyplot as plt
def analyze_loadtest_results(csv_file):
"""Analyze locust CSV results"""
df = pd.read_csv(csv_file)
# Calculate metrics
print("=== Load Test Results ===")
print(f"Total Requests: {len(df)}")
print(f"Success Rate: {(df['success'] == True).mean() * 100:.2f}%")
print(f"Mean Response Time: {df['response_time'].mean():.2f}ms")
print(f"P50 Response Time: {df['response_time'].quantile(0.50):.2f}ms")
print(f"P95 Response Time: {df['response_time'].quantile(0.95):.2f}ms")
print(f"P99 Response Time: {df['response_time'].quantile(0.99):.2f}ms")
print(f"Requests/sec: {len(df) / df['timestamp'].max():.2f}")
# Plot response time distribution
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.hist(df['response_time'], bins=50)
plt.xlabel('Response Time (ms)')
plt.ylabel('Count')
plt.title('Response Time Distribution')
# Plot over time
plt.subplot(1, 2, 2)
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='s')
df.set_index('timestamp')['response_time'].plot()
plt.xlabel('Time')
plt.ylabel('Response Time (ms)')
plt.title('Response Time Over Time')
plt.tight_layout()
plt.savefig('loadtest_results.png')
print("\nPlot saved to loadtest_results.png")
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python analyze.py <results.csv>")
sys.exit(1)
analyze_loadtest_results(sys.argv[1])
Tasks:
- Create loadtest directory structure
- Write locustfile.py with scenarios
- Create analysis script
- Run baseline tests (10, 100, 500 users)
- Document breaking points
- Create loadtest README
Success Criteria
Week 1 Complete When:
- Replay attack fix merged & tested
- Upgrade coordination foundation implemented
- Trust anomaly detection running
- Global rate limiter deployed
- Load testing framework operational
Metrics to Track:
- Zero replay attacks in testing
- Upgrade proposals can be created
- Anomalies detected in test data
- Global rate limit triggers under load
- Performance baselines established
Next Sprint Preview (Week 5-8)
Sprint 2: Scalability Fixes
- Bottleneck fixes from load testing
- Vector clock optimization
- Trust graph caching
- Gossip fan-out optimization
Sprint 3: Operational Maturity
- Disk monitoring
- Split-brain detection
- Fork mediation workflow
- TURN relay deployment
Status: Ready to begin ✅
Start Date: 2025-12-17
Team: Open for contributors