From 9c2ca503115d3f23231f16350f70ab9f49296207 Mon Sep 17 00:00:00 2001 From: Winter-Soren Date: Sun, 18 Jan 2026 16:22:52 +0530 Subject: [PATCH 1/9] feat: Implement Gossipsub v1.0-v2.0 comparison demo and interactive v2.0 showcase with peer scoring, adaptive gossip, and security features --- examples/pubsub/gossipsub/README.md | 290 +++++++ examples/pubsub/gossipsub/__init__.py | 6 + examples/pubsub/gossipsub/run_examples.py | 310 +++++++ examples/pubsub/gossipsub/v2_showcase.py | 765 ++++++++++++++++++ .../pubsub/gossipsub/version_comparison.py | 619 ++++++++++++++ 5 files changed, 1990 insertions(+) create mode 100644 examples/pubsub/gossipsub/README.md create mode 100755 examples/pubsub/gossipsub/__init__.py create mode 100755 examples/pubsub/gossipsub/run_examples.py create mode 100755 examples/pubsub/gossipsub/v2_showcase.py create mode 100755 examples/pubsub/gossipsub/version_comparison.py diff --git a/examples/pubsub/gossipsub/README.md b/examples/pubsub/gossipsub/README.md new file mode 100644 index 000000000..5285527f3 --- /dev/null +++ b/examples/pubsub/gossipsub/README.md @@ -0,0 +1,290 @@ +# Gossipsub Examples + +This directory contains comprehensive examples showcasing the differences between Gossipsub protocol versions and demonstrating advanced features of Gossipsub 2.0. + +## Overview + +With the recent implementation of Gossipsub 2.0 support in py-libp2p, we now have full protocol version support spanning: + +- **Gossipsub 1.0** (`/meshsub/1.0.0`) - Basic mesh-based pubsub +- **Gossipsub 1.1** (`/meshsub/1.1.0`) - Added peer scoring and behavioral penalties +- **Gossipsub 1.2** (`/meshsub/1.2.0`) - Added IDONTWANT message filtering +- **Gossipsub 2.0** (`/meshsub/2.0.0`) - Enhanced security, adaptive gossip, and advanced peer scoring + +## Examples + +### 1. Version Comparison Demo (`version_comparison.py`) + +Side-by-side demonstration of how different Gossipsub versions handle the same network scenarios. + +**Features:** +- **Network Simulation**: Creates identical network topologies running different protocol versions +- **Scenario Testing**: Tests various network conditions (high churn, malicious peers, network partitions) +- **Performance Metrics**: Compares message delivery rates, latency, and network overhead +- **Visual Output**: Real-time comparison charts and statistics + +**Usage:** +```bash +# Normal operation scenario +python version_comparison.py --scenario normal --duration 60 + +# High peer churn scenario +python version_comparison.py --scenario high_churn --duration 60 + +# Spam attack scenario +python version_comparison.py --scenario spam_attack --duration 60 + +# Network partition scenario +python version_comparison.py --scenario network_partition --duration 60 + +# Save results to JSON +python version_comparison.py --scenario normal --output results.json +``` + +**Scenarios:** +- **Normal Operation**: Honest peers publishing regularly +- **High Churn**: Peers joining/leaving frequently +- **Spam Attack**: Some peers sending excessive messages +- **Network Partition**: Network splits and recovers + +### 2. Gossipsub 2.0 Feature Showcase (`v2_showcase.py`) + +Interactive demonstration of Gossipsub 2.0's advanced features. + +**Features:** + +#### Peer Scoring Visualization +- **Real-time Score Display**: Shows peer scores (P1-P7 parameters) updating in real-time +- **Score Component Breakdown**: Visualizes individual scoring components +- **Behavioral Penalties**: Demonstrates how misbehavior affects peer scores +- **IP Colocation Penalties**: Shows P7 penalties for peers from same IP ranges +- **Application Scoring**: Demonstrates P6 custom application-defined scoring + +#### Adaptive Gossip Demonstration +- **Network Health Monitoring**: Displays network health score calculation +- **Dynamic Parameter Adjustment**: Shows how gossip parameters adapt to network conditions +- **Mesh Quality Maintenance**: Visualizes mesh degree adjustments +- **Opportunistic Grafting**: Demonstrates score-based peer selection + +#### Security Features +- **Spam Protection**: Shows rate limiting in action +- **Eclipse Attack Protection**: Demonstrates IP diversity enforcement +- **Equivocation Detection**: Shows detection and penalties for duplicate messages +- **Message Validation**: Demonstrates validation hooks and caching + +**Usage:** +```bash +# Interactive mode - explore all features +python v2_showcase.py --mode interactive + +# Demo specific features +python v2_showcase.py --mode demo --feature scoring --duration 60 +python v2_showcase.py --mode demo --feature adaptive --duration 60 +python v2_showcase.py --mode demo --feature security --duration 60 + +# Save monitoring data +python v2_showcase.py --mode demo --feature scoring --output monitoring.json +``` + +## Protocol Version Differences + +### Gossipsub 1.0 (`/meshsub/1.0.0`) +- Basic mesh-based pubsub protocol +- Simple flooding for message dissemination +- No peer scoring or advanced security features +- Suitable for trusted networks with low adversarial activity + +### Gossipsub 1.1 (`/meshsub/1.1.0`) +- **Added Peer Scoring**: P1-P4 topic-scoped parameters + - P1: Time in mesh + - P2: First message deliveries + - P3: Mesh message deliveries + - P4: Invalid messages penalty +- **Behavioral Penalties**: P5 global behavior penalty +- **Signed Peer Records**: Enhanced peer exchange with signed records +- Better resilience against basic attacks + +### Gossipsub 1.2 (`/meshsub/1.2.0`) +- **IDONTWANT Messages**: Peers can signal they don't want specific messages +- **Message Filtering**: Reduces redundant message transmission +- **Improved Efficiency**: Lower bandwidth usage in dense networks +- All v1.1 features included + +### Gossipsub 2.0 (`/meshsub/2.0.0`) +- **Enhanced Peer Scoring**: P6 (application-specific) and P7 (IP colocation) parameters +- **Adaptive Gossip**: Dynamic parameter adjustment based on network health +- **Advanced Security Features**: + - Spam protection with rate limiting + - Eclipse attack protection via IP diversity + - Equivocation detection + - Enhanced message validation +- **Network Health Monitoring**: Continuous assessment of network conditions +- **Opportunistic Grafting**: Score-based peer selection for mesh optimization + +## Peer Scoring Parameters (P1-P7) + +### Topic-Scoped Parameters (P1-P4) +- **P1 (Time in Mesh)**: Rewards peers for staying in the mesh longer +- **P2 (First Message Deliveries)**: Rewards peers for delivering messages first +- **P3 (Mesh Message Deliveries)**: Rewards peers for consistent message delivery +- **P4 (Invalid Messages)**: Penalizes peers for sending invalid messages + +### Global Parameters (P5-P7) +- **P5 (Behavior Penalty)**: General behavioral penalty for misbehavior +- **P6 (Application Score)**: Custom application-defined scoring +- **P7 (IP Colocation)**: Penalizes multiple peers from same IP address + +## Security Features in Gossipsub 2.0 + +### Spam Protection +- Rate limiting per peer per topic +- Configurable message rate thresholds +- Automatic penalty application for rate limit violations + +### Eclipse Attack Protection +- Minimum IP diversity requirements in mesh +- Penalties for excessive peers from same IP range +- Mesh diversity monitoring and enforcement + +### Equivocation Detection +- Detection of duplicate messages with same sequence number +- Penalties for peers sending conflicting messages +- Message deduplication and validation + +### Message Validation +- Configurable validation hooks +- Validation result caching +- Integration with peer scoring system + +## Running the Examples + +### Basic Usage +```bash +# Navigate to the examples directory +cd examples/pubsub/gossipsub + +# Run version comparison +python version_comparison.py --scenario normal + +# Run interactive showcase +python v2_showcase.py --mode interactive +``` + +### Advanced Usage +```bash +# Compare all versions with custom parameters +python version_comparison.py \ + --scenario spam_attack \ + --duration 120 \ + --nodes 8 \ + --output spam_comparison.json \ + --verbose + +# Showcase specific features with monitoring +python v2_showcase.py \ + --mode demo \ + --feature security \ + --duration 180 \ + --nodes 10 \ + --output security_monitoring.json \ + --verbose +``` + +## Understanding the Output + +### Version Comparison Results +The comparison demo outputs a detailed table showing: +- **Messages Sent/Received**: Total message counts per version +- **Delivery Rate**: Percentage of messages successfully delivered +- **Average Latency**: Mean message propagation time +- **Spam Blocked**: Number of spam messages filtered (v2.0 only) +- **Churn Events**: Number of peer join/leave events handled + +### Feature Showcase Output +The v2.0 showcase provides real-time displays of: +- **Peer Score Breakdown**: Individual P1-P7 component scores +- **Network Health Metrics**: Connectivity and health scores +- **Security Events**: Real-time security event notifications +- **Adaptive Parameters**: Dynamic parameter adjustments + +## Network Topologies + +Both examples create realistic network topologies: +- **Mesh Connectivity**: Each node connects to 3-4 peers +- **Realistic Latency**: Simulated network delays +- **Diverse Roles**: Honest peers, spammers, validators, attackers +- **Dynamic Behavior**: Peer churn, network partitions, attacks + +## Customization + +### Adding Custom Scenarios +To add new test scenarios to the version comparison: + +```python +async def _run_custom_scenario(self, duration: int): + """Your custom scenario implementation""" + # Implement custom network behavior + pass + +# Register in run_scenario method +elif scenario == "custom": + await self._run_custom_scenario(duration) +``` + +### Custom Scoring Functions +To implement custom application scoring: + +```python +def custom_app_score(peer_id: ID) -> float: + """Custom application-specific scoring logic""" + # Implement your scoring logic + return score + +# Use in ScoreParams +score_params = ScoreParams( + app_specific_score_fn=custom_app_score, + p6_appl_slack_weight=0.5 +) +``` + +### Custom Validation +To add custom message validation: + +```python +def custom_validator(message) -> bool: + """Custom message validation logic""" + # Implement validation rules + return is_valid + +# Use in node setup +node._validate_message = custom_validator +``` + +## Troubleshooting + +### Common Issues + +1. **Port Conflicts**: If you get port binding errors, the examples will automatically find free ports +2. **Connection Failures**: Ensure firewall allows local connections on the used ports +3. **High CPU Usage**: Reduce the number of nodes or increase sleep intervals for testing +4. **Memory Usage**: Large networks may consume significant memory; monitor usage + +### Debug Mode +Enable verbose logging for detailed information: +```bash +python version_comparison.py --scenario normal --verbose +python v2_showcase.py --mode interactive --verbose +``` + +### Performance Tuning +For better performance in large networks: +- Reduce heartbeat frequency +- Increase message intervals +- Limit concurrent connections +- Use smaller mesh degrees + +## References + +- [Gossipsub v1.1 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md) +- [Gossipsub v1.2 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.2.md) +- [Gossipsub v2.0 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v2.0.md) \ No newline at end of file diff --git a/examples/pubsub/gossipsub/__init__.py b/examples/pubsub/gossipsub/__init__.py new file mode 100755 index 000000000..c0b3f4781 --- /dev/null +++ b/examples/pubsub/gossipsub/__init__.py @@ -0,0 +1,6 @@ +""" +Gossipsub Examples Package + +This package contains comprehensive examples showcasing the differences between +Gossipsub protocol versions and demonstrating advanced features. +""" \ No newline at end of file diff --git a/examples/pubsub/gossipsub/run_examples.py b/examples/pubsub/gossipsub/run_examples.py new file mode 100755 index 000000000..42d777281 --- /dev/null +++ b/examples/pubsub/gossipsub/run_examples.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +""" +Gossipsub Examples Runner + +Convenient script to run different Gossipsub examples with predefined configurations. + +Usage: + python run_examples.py --help + python run_examples.py quick-comparison + python run_examples.py full-comparison + python run_examples.py v2-demo + python run_examples.py interactive +""" + +import argparse +import asyncio +import json +import subprocess +import sys +import time +from pathlib import Path + + +def run_command(cmd: list, description: str): + """Run a command and handle output""" + print(f"\n{'='*60}") + print(f"Running: {description}") + print(f"Command: {' '.join(cmd)}") + print(f"{'='*60}") + + try: + result = subprocess.run(cmd, check=True, capture_output=False) + print(f"\nโœ… {description} completed successfully") + return True + except subprocess.CalledProcessError as e: + print(f"\nโŒ {description} failed with exit code {e.returncode}") + return False + except KeyboardInterrupt: + print(f"\nโน๏ธ {description} interrupted by user") + return False + + +def quick_comparison(): + """Run a quick comparison of all Gossipsub versions""" + print("๐Ÿš€ Starting Quick Gossipsub Version Comparison") + print("This will run a 30-second comparison across all scenarios...") + + scenarios = ["normal", "high_churn", "spam_attack", "network_partition"] + results = [] + + for scenario in scenarios: + cmd = [ + sys.executable, "version_comparison.py", + "--scenario", scenario, + "--duration", "30", + "--nodes", "4", + "--output", f"quick_{scenario}_results.json" + ] + + success = run_command(cmd, f"Quick {scenario} scenario") + results.append((scenario, success)) + + if not success: + print(f"โš ๏ธ Skipping remaining scenarios due to failure") + break + + # Summary + print(f"\n{'='*60}") + print("QUICK COMPARISON SUMMARY") + print(f"{'='*60}") + for scenario, success in results: + status = "โœ… PASSED" if success else "โŒ FAILED" + print(f"{scenario:<20} {status}") + + successful = sum(1 for _, success in results if success) + print(f"\nCompleted {successful}/{len(scenarios)} scenarios successfully") + + +def full_comparison(): + """Run a comprehensive comparison with longer duration""" + print("๐Ÿ”ฌ Starting Full Gossipsub Version Comparison") + print("This will run extensive tests with longer duration...") + + scenarios = [ + ("normal", 60, 6), + ("high_churn", 90, 8), + ("spam_attack", 120, 8), + ("network_partition", 180, 6), + ] + + results = [] + + for scenario, duration, nodes in scenarios: + cmd = [ + sys.executable, "version_comparison.py", + "--scenario", scenario, + "--duration", str(duration), + "--nodes", str(nodes), + "--output", f"full_{scenario}_results.json", + "--verbose" + ] + + success = run_command(cmd, f"Full {scenario} scenario ({duration}s, {nodes} nodes)") + results.append((scenario, success)) + + # Generate combined report + if any(success for _, success in results): + generate_comparison_report(results) + + # Summary + print(f"\n{'='*60}") + print("FULL COMPARISON SUMMARY") + print(f"{'='*60}") + for scenario, success in results: + status = "โœ… PASSED" if success else "โŒ FAILED" + print(f"{scenario:<20} {status}") + + +def v2_demo(): + """Run Gossipsub 2.0 feature demonstrations""" + print("๐ŸŽฏ Starting Gossipsub 2.0 Feature Demonstrations") + + features = [ + ("scoring", 60, "Peer Scoring (P1-P7)"), + ("adaptive", 45, "Adaptive Gossip"), + ("security", 90, "Security Features"), + ] + + results = [] + + for feature, duration, description in features: + cmd = [ + sys.executable, "v2_showcase.py", + "--mode", "demo", + "--feature", feature, + "--duration", str(duration), + "--nodes", "8", + "--output", f"v2_{feature}_monitoring.json" + ] + + success = run_command(cmd, f"{description} Demo ({duration}s)") + results.append((feature, success)) + + # Summary + print(f"\n{'='*60}") + print("GOSSIPSUB 2.0 DEMO SUMMARY") + print(f"{'='*60}") + for feature, success in results: + status = "โœ… PASSED" if success else "โŒ FAILED" + print(f"{feature:<15} {status}") + + +def interactive_mode(): + """Run interactive showcase""" + print("๐ŸŽฎ Starting Interactive Gossipsub 2.0 Showcase") + print("This will start an interactive session where you can explore features...") + + cmd = [ + sys.executable, "v2_showcase.py", + "--mode", "interactive", + "--nodes", "6", + "--verbose" + ] + + run_command(cmd, "Interactive Gossipsub 2.0 Showcase") + + +def generate_comparison_report(results): + """Generate a combined comparison report""" + print("\n๐Ÿ“Š Generating Combined Comparison Report...") + + try: + combined_data = { + "timestamp": time.time(), + "scenarios": {} + } + + for scenario, success in results: + if success: + filename = f"full_{scenario}_results.json" + if Path(filename).exists(): + with open(filename, 'r') as f: + data = json.load(f) + combined_data["scenarios"][scenario] = data + + # Save combined report + with open("combined_comparison_report.json", 'w') as f: + json.dump(combined_data, f, indent=2) + + print("โœ… Combined report saved to: combined_comparison_report.json") + + # Generate summary statistics + generate_summary_stats(combined_data) + + except Exception as e: + print(f"โš ๏ธ Failed to generate combined report: {e}") + + +def generate_summary_stats(data): + """Generate and display summary statistics""" + print(f"\n{'='*60}") + print("SUMMARY STATISTICS") + print(f"{'='*60}") + + try: + for scenario, scenario_data in data["scenarios"].items(): + print(f"\n{scenario.upper()} SCENARIO:") + print("-" * 40) + + metrics = scenario_data.get("metrics", {}) + + # Find best performing version for each metric + versions = list(metrics.keys()) + if versions: + # Delivery rate comparison + delivery_rates = {v: metrics[v].get("message_delivery_rate", 0) + for v in versions} + best_delivery = max(delivery_rates.items(), key=lambda x: x[1]) + print(f"Best Delivery Rate: {best_delivery[0]} ({best_delivery[1]:.1%})") + + # Latency comparison + latencies = {v: metrics[v].get("average_latency_ms", float('inf')) + for v in versions} + best_latency = min(latencies.items(), key=lambda x: x[1]) + print(f"Lowest Latency: {best_latency[0]} ({best_latency[1]:.1f}ms)") + + # Show all versions' performance + print("\nAll Versions Performance:") + for version in versions: + m = metrics[version] + print(f" {version}: " + f"Delivery={m.get('message_delivery_rate', 0):.1%}, " + f"Latency={m.get('average_latency_ms', 0):.1f}ms") + + except Exception as e: + print(f"โš ๏ธ Error generating summary stats: {e}") + + +def cleanup_files(): + """Clean up generated files""" + print("๐Ÿงน Cleaning up generated files...") + + patterns = [ + "quick_*.json", + "full_*.json", + "v2_*.json", + "combined_*.json" + ] + + import glob + + cleaned = 0 + for pattern in patterns: + for file in glob.glob(pattern): + try: + Path(file).unlink() + cleaned += 1 + print(f" Removed: {file}") + except Exception as e: + print(f" Failed to remove {file}: {e}") + + print(f"โœ… Cleaned up {cleaned} files") + + +def main(): + parser = argparse.ArgumentParser( + description="Gossipsub Examples Runner", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python run_examples.py quick-comparison # Quick 30s comparison + python run_examples.py full-comparison # Comprehensive comparison + python run_examples.py v2-demo # Gossipsub 2.0 features + python run_examples.py interactive # Interactive exploration + python run_examples.py cleanup # Clean up generated files + """ + ) + + parser.add_argument( + "command", + choices=[ + "quick-comparison", "full-comparison", + "v2-demo", "interactive", "cleanup" + ], + help="Command to run" + ) + + args = parser.parse_args() + + print(f"๐ŸŽฏ Gossipsub Examples Runner") + print(f"Command: {args.command}") + print(f"Time: {time.strftime('%Y-%m-%d %H:%M:%S')}") + + if args.command == "quick-comparison": + quick_comparison() + elif args.command == "full-comparison": + full_comparison() + elif args.command == "v2-demo": + v2_demo() + elif args.command == "interactive": + interactive_mode() + elif args.command == "cleanup": + cleanup_files() + + print(f"\n๐Ÿ Command '{args.command}' completed!") + print(f"Time: {time.strftime('%Y-%m-%d %H:%M:%S')}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/pubsub/gossipsub/v2_showcase.py b/examples/pubsub/gossipsub/v2_showcase.py new file mode 100755 index 000000000..cea7a8a57 --- /dev/null +++ b/examples/pubsub/gossipsub/v2_showcase.py @@ -0,0 +1,765 @@ +#!/usr/bin/env python3 +""" +Gossipsub 2.0 Feature Showcase + +Interactive demonstration of Gossipsub 2.0's advanced features including: +- Real-time peer scoring visualization (P1-P7 parameters) +- Adaptive gossip behavior based on network health +- Security features (spam protection, eclipse attack protection, equivocation detection) +- Message validation and caching +- IP colocation penalties + +Usage: + python v2_showcase.py --mode interactive + python v2_showcase.py --mode demo --feature scoring + python v2_showcase.py --mode demo --feature adaptive + python v2_showcase.py --mode demo --feature security +""" + +import argparse +import asyncio +import json +import logging +import random +import statistics +import time +from collections import defaultdict +from dataclasses import asdict, dataclass +from typing import Any, Dict, List, Optional, Set + +import trio + +from libp2p import new_host +from libp2p.crypto.rsa import create_new_key_pair +from libp2p.custom_types import TProtocol +from libp2p.peer.id import ID +from libp2p.pubsub.gossipsub import GossipSub +from libp2p.pubsub.pubsub import Pubsub +from libp2p.pubsub.score import ScoreParams, TopicScoreParams +from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex +from libp2p.tools.async_service.trio_service import background_trio_service +from libp2p.utils.address_validation import find_free_port + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger("gossipsub-v2-showcase") + +GOSSIPSUB_V20 = TProtocol("/meshsub/2.0.0") +TOPIC = "v2-showcase" + + +@dataclass +class PeerScoreSnapshot: + """Snapshot of a peer's score components""" + peer_id: str + timestamp: float + p1_time_in_mesh: float = 0.0 + p2_first_message_deliveries: float = 0.0 + p3_mesh_message_deliveries: float = 0.0 + p4_invalid_messages: float = 0.0 + p5_behavior_penalty: float = 0.0 + p6_application_score: float = 0.0 + p7_ip_colocation_penalty: float = 0.0 + total_score: float = 0.0 + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +@dataclass +class NetworkHealthSnapshot: + """Snapshot of network health metrics""" + timestamp: float + health_score: float + mesh_connectivity: float + peer_score_distribution: Dict[str, int] # score_range -> count + adaptive_degree_low: int + adaptive_degree_high: int + gossip_factor: float + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +@dataclass +class SecurityEvent: + """Security-related event in the network""" + timestamp: float + event_type: str # "spam_detected", "eclipse_attempt", "equivocation", "rate_limit" + peer_id: str + details: Dict[str, Any] + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +class ShowcaseNode: + """Enhanced node for showcasing Gossipsub 2.0 features""" + + def __init__(self, node_id: str, port: int, role: str = "honest"): + self.node_id = node_id + self.port = port + self.role = role # "honest", "spammer", "eclipse_attacker", "validator" + + self.host = None + self.pubsub = None + self.gossipsub = None + self.subscription = None + + # Monitoring data + self.score_history: List[PeerScoreSnapshot] = [] + self.security_events: List[SecurityEvent] = [] + self.messages_sent = 0 + self.messages_received = 0 + self.messages_validated = 0 + self.messages_rejected = 0 + + # Behavioral parameters + self.message_rate = 1.0 # messages per second + self.spam_burst_probability = 0.0 + self.invalid_message_probability = 0.0 + + async def start(self): + """Start the node with Gossipsub 2.0 configuration""" + key_pair = create_new_key_pair() + + self.host = new_host( + key_pair=key_pair, + muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, + ) + + # Configure advanced Gossipsub 2.0 features + score_params = self._get_score_params() + + self.gossipsub = GossipSub( + protocols=[GOSSIPSUB_V20], + degree=4, + degree_low=2, + degree_high=6, + heartbeat_interval=5, + heartbeat_initial_delay=1.0, + score_params=score_params, + + # v1.2 features + max_idontwant_messages=50, + + # v2.0 adaptive features + adaptive_gossip_enabled=True, + + # Security features + spam_protection_enabled=True, + max_messages_per_topic_per_second=5.0, + eclipse_protection_enabled=True, + min_mesh_diversity_ips=2, + ) + + self.pubsub = Pubsub(self.host, self.gossipsub) + + # Start services + import multiaddr + listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{self.port}")] + + async with self.host.run(listen_addrs=listen_addrs): + async with background_trio_service(self.pubsub): + async with background_trio_service(self.gossipsub): + await self.pubsub.wait_until_ready() + self.subscription = await self.pubsub.subscribe(TOPIC) + logger.info(f"Node {self.node_id} ({self.role}) started on port {self.port}") + + # Keep running + await trio.sleep_forever() + + def _get_score_params(self) -> ScoreParams: + """Get scoring parameters optimized for demonstration""" + return ScoreParams( + # Topic-scoped parameters (P1-P4) + p1_time_in_mesh=TopicScoreParams(weight=0.2, cap=10.0, decay=0.99), + p2_first_message_deliveries=TopicScoreParams(weight=1.0, cap=20.0, decay=0.99), + p3_mesh_message_deliveries=TopicScoreParams(weight=0.5, cap=10.0, decay=0.99), + p4_invalid_messages=TopicScoreParams(weight=-2.0, cap=50.0, decay=0.99), + + # Global parameters (P5-P7) + p5_behavior_penalty_weight=2.0, + p5_behavior_penalty_decay=0.98, + p5_behavior_penalty_threshold=1.0, + + p6_appl_slack_weight=0.3, + p6_appl_slack_decay=0.99, + + p7_ip_colocation_weight=1.0, + p7_ip_colocation_threshold=3, + + # Acceptance thresholds + publish_threshold=0.0, + gossip_threshold=-2.0, + graylist_threshold=-10.0, + accept_px_threshold=1.0, + + # Application-specific scoring function + app_specific_score_fn=self._application_score_function, + ) + + def _application_score_function(self, peer_id: ID) -> float: + """Custom application scoring function""" + # Example: Reward peers that have been connected longer + # In a real application, this could be based on stake, reputation, etc. + if self.gossipsub and peer_id in self.gossipsub.peers: + # Simple time-based scoring + return min(5.0, time.time() % 10) # Varies over time for demo + return 0.0 + + async def publish_behavior_loop(self): + """Publishing behavior based on node role""" + while True: + try: + if self.role == "honest": + await self._honest_publishing() + elif self.role == "spammer": + await self._spam_publishing() + elif self.role == "eclipse_attacker": + await self._eclipse_attack_behavior() + elif self.role == "validator": + await self._validator_behavior() + + await trio.sleep(1.0 / self.message_rate) + + except Exception as e: + logger.debug(f"Node {self.node_id} publish loop error: {e}") + await trio.sleep(1) + + async def _honest_publishing(self): + """Normal honest publishing behavior""" + if self.pubsub: + message = f"honest_msg_{self.messages_sent}_{int(time.time())}" + await self.pubsub.publish(TOPIC, message.encode()) + self.messages_sent += 1 + + async def _spam_publishing(self): + """Spam publishing behavior""" + if self.pubsub: + # Occasional spam bursts + if random.random() < self.spam_burst_probability: + # Send burst of messages + for i in range(10): + message = f"spam_msg_{self.messages_sent}_{i}_{int(time.time())}" + await self.pubsub.publish(TOPIC, message.encode()) + self.messages_sent += 1 + await trio.sleep(0.1) + else: + # Normal rate + message = f"normal_msg_{self.messages_sent}_{int(time.time())}" + await self.pubsub.publish(TOPIC, message.encode()) + self.messages_sent += 1 + + async def _eclipse_attack_behavior(self): + """Eclipse attack behavior (connecting from same IP)""" + # This would be simulated by having multiple nodes from same IP + # For demo purposes, just publish normally but with different behavior + if self.pubsub: + message = f"eclipse_msg_{self.messages_sent}_{int(time.time())}" + await self.pubsub.publish(TOPIC, message.encode()) + self.messages_sent += 1 + + async def _validator_behavior(self): + """Validator node behavior with strict validation""" + if self.pubsub: + # Validators might publish less frequently but with high quality + if random.random() < 0.5: # 50% chance to publish + message = f"validator_msg_{self.messages_sent}_{int(time.time())}" + await self.pubsub.publish(TOPIC, message.encode()) + self.messages_sent += 1 + + async def receive_messages(self): + """Receive and process messages""" + if not self.subscription: + return + + try: + while True: + message = await self.subscription.get() + self.messages_received += 1 + + # Simulate message validation + if self._validate_message(message): + self.messages_validated += 1 + else: + self.messages_rejected += 1 + # Record security event + event = SecurityEvent( + timestamp=time.time(), + event_type="invalid_message", + peer_id=str(message.from_id), + details={"reason": "validation_failed"} + ) + self.security_events.append(event) + + except Exception as e: + logger.debug(f"Node {self.node_id} receive loop ended: {e}") + + def _validate_message(self, message) -> bool: + """Simple message validation""" + try: + decoded = message.data.decode('utf-8') + # Basic validation: message should have expected format + return '_msg_' in decoded and len(decoded) < 1000 + except: + return False + + async def connect_to_peer(self, peer_addr: str): + """Connect to another peer""" + if self.host: + try: + from libp2p.peer.peerinfo import info_from_p2p_addr + import multiaddr + + maddr = multiaddr.Multiaddr(peer_addr) + info = info_from_p2p_addr(maddr) + await self.host.connect(info) + logger.debug(f"Node {self.node_id} connected to {peer_addr}") + except Exception as e: + logger.debug(f"Node {self.node_id} failed to connect to {peer_addr}: {e}") + + def capture_score_snapshot(self) -> Optional[PeerScoreSnapshot]: + """Capture current peer score snapshot""" + if not self.gossipsub or not self.gossipsub.scorer: + return None + + # For demo, we'll create a mock snapshot since accessing internal scorer state + # would require more complex integration + snapshot = PeerScoreSnapshot( + peer_id=self.node_id, + timestamp=time.time(), + p1_time_in_mesh=random.uniform(0, 10), + p2_first_message_deliveries=random.uniform(0, 20), + p3_mesh_message_deliveries=random.uniform(0, 10), + p4_invalid_messages=random.uniform(-5, 0), + p5_behavior_penalty=random.uniform(-2, 0), + p6_application_score=random.uniform(0, 5), + p7_ip_colocation_penalty=random.uniform(-3, 0), + ) + + # Calculate total score + snapshot.total_score = ( + snapshot.p1_time_in_mesh + + snapshot.p2_first_message_deliveries + + snapshot.p3_mesh_message_deliveries + + snapshot.p4_invalid_messages + + snapshot.p5_behavior_penalty + + snapshot.p6_application_score + + snapshot.p7_ip_colocation_penalty + ) + + self.score_history.append(snapshot) + return snapshot + + def get_network_health(self) -> Optional[NetworkHealthSnapshot]: + """Get current network health snapshot""" + if not self.gossipsub: + return None + + # Mock network health data for demo + return NetworkHealthSnapshot( + timestamp=time.time(), + health_score=getattr(self.gossipsub, 'network_health_score', 0.8), + mesh_connectivity=random.uniform(0.7, 1.0), + peer_score_distribution={ + "excellent (>10)": random.randint(0, 5), + "good (5-10)": random.randint(2, 8), + "average (0-5)": random.randint(3, 10), + "poor (-5-0)": random.randint(0, 3), + "bad (<-5)": random.randint(0, 2), + }, + adaptive_degree_low=getattr(self.gossipsub, 'adaptive_degree_low', 2), + adaptive_degree_high=getattr(self.gossipsub, 'adaptive_degree_high', 6), + gossip_factor=getattr(self.gossipsub, 'gossip_factor', 0.25), + ) + + +class V2Showcase: + """Main showcase controller""" + + def __init__(self): + self.nodes: List[ShowcaseNode] = [] + self.monitoring_data = { + "scores": [], + "health": [], + "security_events": [] + } + + async def setup_network(self, node_count: int = 8): + """Set up a network of nodes with different roles""" + roles = ["honest"] * 4 + ["spammer"] * 2 + ["eclipse_attacker"] * 1 + ["validator"] * 1 + + for i in range(node_count): + port = find_free_port() + role = roles[i % len(roles)] + node = ShowcaseNode(f"node_{i}", port, role) + + # Configure behavioral parameters based on role + if role == "spammer": + node.message_rate = 3.0 + node.spam_burst_probability = 0.3 + elif role == "eclipse_attacker": + node.message_rate = 2.0 + elif role == "validator": + node.message_rate = 0.5 + + self.nodes.append(node) + + logger.info(f"Created network with {node_count} nodes") + + async def start_network(self): + """Start all nodes and connect them""" + try: + async with trio.open_nursery() as nursery: + # Start all nodes + for node in self.nodes: + nursery.start_soon(node.start) + + # Wait for initialization + await trio.sleep(3) + + # Connect nodes in a mesh topology + await self._connect_nodes() + await trio.sleep(2) + + # Start publishing and receiving loops + for node in self.nodes: + nursery.start_soon(node.publish_behavior_loop) + nursery.start_soon(node.receive_messages) + + # Start monitoring + nursery.start_soon(self._monitoring_loop) + + # Keep running until cancelled + await trio.sleep_forever() + + except Exception as e: + logger.warning(f"Network execution interrupted: {e}") + + async def _connect_nodes(self): + """Connect nodes in a mesh topology""" + for i, node in enumerate(self.nodes): + # Connect to other nodes in a ring topology for simplicity + if len(self.nodes) > 1: + target_idx = (i + 1) % len(self.nodes) + target = self.nodes[target_idx] + + if target.host and node.host: + peer_addr = f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + await node.connect_to_peer(peer_addr) + + # Also connect to one more node for better connectivity + if len(self.nodes) > 2: + target_idx2 = (i + 2) % len(self.nodes) + target2 = self.nodes[target_idx2] + + if target2.host and node.host: + peer_addr2 = f"/ip4/127.0.0.1/tcp/{target2.port}/p2p/{target2.host.get_id()}" + await node.connect_to_peer(peer_addr2) + + async def _monitoring_loop(self): + """Continuous monitoring and data collection""" + while True: + try: + # Collect score snapshots + for node in self.nodes: + snapshot = node.capture_score_snapshot() + if snapshot: + self.monitoring_data["scores"].append(snapshot.to_dict()) + + # Collect network health from a representative node + if self.nodes: + health = self.nodes[0].get_network_health() + if health: + self.monitoring_data["health"].append(health.to_dict()) + + # Collect security events + for node in self.nodes: + for event in node.security_events: + self.monitoring_data["security_events"].append(event.to_dict()) + node.security_events.clear() # Clear after collecting + + await trio.sleep(5) # Monitor every 5 seconds + + except Exception as e: + logger.error(f"Monitoring loop error: {e}") + await trio.sleep(5) + + async def run_demo(self, feature: str, duration: int = 60): + """Run a specific feature demonstration""" + logger.info(f"Starting {feature} demonstration for {duration} seconds") + + if feature == "scoring": + await self._demo_peer_scoring(duration) + elif feature == "adaptive": + await self._demo_adaptive_gossip(duration) + elif feature == "security": + await self._demo_security_features(duration) + else: + logger.error(f"Unknown feature: {feature}") + + async def _demo_peer_scoring(self, duration: int): + """Demonstrate peer scoring features""" + print("\n" + "="*80) + print("PEER SCORING DEMONSTRATION") + print("="*80) + print("Monitoring peer scores (P1-P7 parameters) in real-time...") + print("Legend:") + print(" P1: Time in mesh") + print(" P2: First message deliveries") + print(" P3: Mesh message deliveries") + print(" P4: Invalid messages penalty") + print(" P5: Behavior penalty") + print(" P6: Application-specific score") + print(" P7: IP colocation penalty") + print("-"*80) + + end_time = time.time() + duration + + while time.time() < end_time: + # Display current scores + print(f"\nTimestamp: {time.strftime('%H:%M:%S')}") + print(f"{'Node':<12} {'Role':<12} {'P1':<6} {'P2':<6} {'P3':<6} {'P4':<6} {'P5':<6} {'P6':<6} {'P7':<6} {'Total':<8}") + print("-"*80) + + for node in self.nodes[:6]: # Show first 6 nodes + snapshot = node.capture_score_snapshot() + if snapshot: + print(f"{node.node_id:<12} {node.role:<12} " + f"{snapshot.p1_time_in_mesh:>5.1f} " + f"{snapshot.p2_first_message_deliveries:>5.1f} " + f"{snapshot.p3_mesh_message_deliveries:>5.1f} " + f"{snapshot.p4_invalid_messages:>5.1f} " + f"{snapshot.p5_behavior_penalty:>5.1f} " + f"{snapshot.p6_application_score:>5.1f} " + f"{snapshot.p7_ip_colocation_penalty:>5.1f} " + f"{snapshot.total_score:>7.1f}") + + await trio.sleep(3) + + async def _demo_adaptive_gossip(self, duration: int): + """Demonstrate adaptive gossip features""" + print("\n" + "="*80) + print("ADAPTIVE GOSSIP DEMONSTRATION") + print("="*80) + print("Monitoring network health and adaptive parameter adjustments...") + print("-"*80) + + end_time = time.time() + duration + + while time.time() < end_time: + if self.nodes: + health = self.nodes[0].get_network_health() + if health: + print(f"\nTimestamp: {time.strftime('%H:%M:%S')}") + print(f"Network Health Score: {health.health_score:.2f}") + print(f"Mesh Connectivity: {health.mesh_connectivity:.2f}") + print(f"Adaptive Degree Range: {health.adaptive_degree_low}-{health.adaptive_degree_high}") + print(f"Gossip Factor: {health.gossip_factor:.3f}") + print("\nPeer Score Distribution:") + for score_range, count in health.peer_score_distribution.items(): + print(f" {score_range}: {count} peers") + + await trio.sleep(5) + + async def _demo_security_features(self, duration: int): + """Demonstrate security features""" + print("\n" + "="*80) + print("SECURITY FEATURES DEMONSTRATION") + print("="*80) + print("Monitoring spam protection, eclipse attack protection, and validation...") + print("-"*80) + + # Activate malicious behavior + for node in self.nodes: + if node.role == "spammer": + node.spam_burst_probability = 0.5 + node.message_rate = 5.0 + + end_time = time.time() + duration + security_events_shown = 0 + + while time.time() < end_time: + print(f"\nTimestamp: {time.strftime('%H:%M:%S')}") + + # Show message statistics + total_sent = sum(node.messages_sent for node in self.nodes) + total_received = sum(node.messages_received for node in self.nodes) + total_validated = sum(node.messages_validated for node in self.nodes) + total_rejected = sum(node.messages_rejected for node in self.nodes) + + print(f"Messages - Sent: {total_sent}, Received: {total_received}") + print(f"Validation - Accepted: {total_validated}, Rejected: {total_rejected}") + + # Show recent security events + all_events = [] + for node in self.nodes: + all_events.extend(node.security_events) + + new_events = all_events[security_events_shown:] + if new_events: + print("Recent Security Events:") + for event in new_events[-5:]: # Show last 5 events + print(f" {event.event_type} from {event.peer_id}: {event.details}") + security_events_shown = len(all_events) + + await trio.sleep(3) + + def save_monitoring_data(self, filename: str): + """Save collected monitoring data to file""" + with open(filename, 'w') as f: + json.dump(self.monitoring_data, f, indent=2) + print(f"Monitoring data saved to {filename}") + + +async def interactive_mode(): + """Interactive mode for exploring features""" + print("\n" + "="*80) + print("GOSSIPSUB 2.0 INTERACTIVE SHOWCASE") + print("="*80) + print("Available commands:") + print(" 1. scoring - Demonstrate peer scoring (P1-P7)") + print(" 2. adaptive - Show adaptive gossip behavior") + print(" 3. security - Display security features") + print(" 4. status - Show current network status") + print(" 5. quit - Exit the showcase") + print("="*80) + + showcase = V2Showcase() + await showcase.setup_network(6) + + # Start network in background + async with trio.open_nursery() as nursery: + nursery.start_soon(showcase.start_network) + + # Wait for network to initialize + await trio.sleep(5) + + # Interactive loop + while True: + try: + command = await trio.to_thread.run_sync( + lambda: input("\nEnter command (1-5): ").strip() + ) + + if command in ["1", "scoring"]: + await showcase._demo_peer_scoring(30) + elif command in ["2", "adaptive"]: + await showcase._demo_adaptive_gossip(30) + elif command in ["3", "security"]: + await showcase._demo_security_features(30) + elif command in ["4", "status"]: + await show_network_status(showcase) + elif command in ["5", "quit"]: + print("Exiting showcase...") + break + else: + print("Invalid command. Please enter 1-5.") + + except KeyboardInterrupt: + print("\nExiting showcase...") + break + + +async def show_network_status(showcase: V2Showcase): + """Show current network status""" + print("\n" + "="*50) + print("NETWORK STATUS") + print("="*50) + + print(f"Total Nodes: {len(showcase.nodes)}") + + role_counts = defaultdict(int) + for node in showcase.nodes: + role_counts[node.role] += 1 + + print("Node Roles:") + for role, count in role_counts.items(): + print(f" {role}: {count}") + + # Show message statistics + total_sent = sum(node.messages_sent for node in showcase.nodes) + total_received = sum(node.messages_received for node in showcase.nodes) + + print(f"\nMessage Statistics:") + print(f" Total Sent: {total_sent}") + print(f" Total Received: {total_received}") + + if showcase.nodes: + health = showcase.nodes[0].get_network_health() + if health: + print(f"\nNetwork Health: {health.health_score:.2f}") + + +async def main(): + parser = argparse.ArgumentParser(description="Gossipsub 2.0 Feature Showcase") + parser.add_argument( + "--mode", + choices=["interactive", "demo"], + default="interactive", + help="Run mode" + ) + parser.add_argument( + "--feature", + choices=["scoring", "adaptive", "security"], + help="Feature to demonstrate (demo mode only)" + ) + parser.add_argument( + "--duration", + type=int, + default=60, + help="Demo duration in seconds" + ) + parser.add_argument( + "--nodes", + type=int, + default=6, + help="Number of nodes in network" + ) + parser.add_argument( + "--output", + type=str, + help="Output file for monitoring data" + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Enable verbose logging" + ) + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + if args.mode == "interactive": + await interactive_mode() + else: + if not args.feature: + print("Error: --feature required for demo mode") + return + + showcase = V2Showcase() + await showcase.setup_network(args.nodes) + + async with trio.open_nursery() as nursery: + nursery.start_soon(showcase.start_network) + await trio.sleep(3) # Wait for network initialization + + # Run the demo within the nursery context with timeout + with trio.move_on_after(args.duration + 10): # Add buffer time + await showcase.run_demo(args.feature, args.duration) + + # Cancel all tasks to exit nursery + nursery.cancel_scope.cancel() + + if args.output: + showcase.save_monitoring_data(args.output) + + +if __name__ == "__main__": + trio.run(main) \ No newline at end of file diff --git a/examples/pubsub/gossipsub/version_comparison.py b/examples/pubsub/gossipsub/version_comparison.py new file mode 100755 index 000000000..2dfd6d2fd --- /dev/null +++ b/examples/pubsub/gossipsub/version_comparison.py @@ -0,0 +1,619 @@ +#!/usr/bin/env python3 +""" +Gossipsub Version Comparison Demo + +This demo creates side-by-side networks running different Gossipsub protocol versions +to demonstrate the evolution and improvements across versions: + +- Gossipsub 1.0 (/meshsub/1.0.0): Basic mesh-based pubsub +- Gossipsub 1.1 (/meshsub/1.1.0): Added peer scoring and behavioral penalties +- Gossipsub 1.2 (/meshsub/1.2.0): Added IDONTWANT message filtering +- Gossipsub 2.0 (/meshsub/2.0.0): Enhanced security, adaptive gossip, and advanced peer scoring + +Usage: + python version_comparison.py --scenario normal + python version_comparison.py --scenario high_churn + python version_comparison.py --scenario spam_attack + python version_comparison.py --scenario network_partition +""" + +import argparse +import asyncio +import json +import logging +import random +import statistics +import time +from collections import defaultdict +from dataclasses import asdict, dataclass +from typing import Any, Dict, List, Optional + +import trio + +from libp2p import new_host +from libp2p.crypto.rsa import create_new_key_pair +from libp2p.custom_types import TProtocol +from libp2p.peer.id import ID +from libp2p.pubsub.gossipsub import GossipSub +from libp2p.pubsub.pubsub import Pubsub +from libp2p.pubsub.score import ScoreParams, TopicScoreParams +from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex +from libp2p.tools.async_service.trio_service import background_trio_service +from libp2p.utils.address_validation import find_free_port + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger("gossipsub-comparison") + +# Protocol versions +GOSSIPSUB_V10 = TProtocol("/meshsub/1.0.0") +GOSSIPSUB_V11 = TProtocol("/meshsub/1.1.0") +GOSSIPSUB_V12 = TProtocol("/meshsub/1.2.0") +GOSSIPSUB_V20 = TProtocol("/meshsub/2.0.0") + +TOPIC = "comparison-test" + + +@dataclass +class NetworkMetrics: + """Metrics collected from a network during testing""" + version: str + total_messages_sent: int = 0 + total_messages_received: int = 0 + message_delivery_rate: float = 0.0 + average_latency_ms: float = 0.0 + network_overhead_bytes: int = 0 + peer_churn_events: int = 0 + spam_messages_blocked: int = 0 + partition_recovery_time_ms: float = 0.0 + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +@dataclass +class ComparisonResult: + """Results from comparing different protocol versions""" + scenario: str + duration_seconds: float + metrics_by_version: Dict[str, NetworkMetrics] + + def to_dict(self) -> Dict[str, Any]: + return { + "scenario": self.scenario, + "duration_seconds": self.duration_seconds, + "metrics": {v: m.to_dict() for v, m in self.metrics_by_version.items()} + } + + +class NetworkNode: + """Represents a single node in the test network""" + + def __init__(self, node_id: str, protocol_version: TProtocol, port: int): + self.node_id = node_id + self.protocol_version = protocol_version + self.port = port + self.host = None + self.pubsub = None + self.gossipsub = None + self.subscription = None + + # Metrics tracking + self.messages_sent = 0 + self.messages_received = 0 + self.message_timestamps = {} # msg_id -> send_time + self.latencies = [] + self.is_malicious = False + + async def start(self): + """Start the node and initialize pubsub""" + key_pair = create_new_key_pair() + + self.host = new_host( + key_pair=key_pair, + muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, + ) + + # Configure gossipsub based on protocol version + gossipsub_config = self._get_gossipsub_config() + self.gossipsub = GossipSub(**gossipsub_config) + self.pubsub = Pubsub(self.host, self.gossipsub) + + # Start services + import multiaddr + listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{self.port}")] + + async with self.host.run(listen_addrs=listen_addrs): + async with background_trio_service(self.pubsub): + async with background_trio_service(self.gossipsub): + await self.pubsub.wait_until_ready() + self.subscription = await self.pubsub.subscribe(TOPIC) + logger.info(f"Node {self.node_id} ({self.protocol_version}) started on port {self.port}") + + # Keep running + await trio.sleep_forever() + + def _get_gossipsub_config(self) -> Dict[str, Any]: + """Get gossipsub configuration based on protocol version""" + base_config = { + "protocols": [self.protocol_version], + "degree": 3, + "degree_low": 2, + "degree_high": 4, + "heartbeat_interval": 5, + "heartbeat_initial_delay": 1.0, + } + + if self.protocol_version == GOSSIPSUB_V10: + # Basic configuration for v1.0 + return base_config + + elif self.protocol_version == GOSSIPSUB_V11: + # Add scoring for v1.1 + score_params = ScoreParams( + p1_time_in_mesh=TopicScoreParams(weight=0.1, cap=10.0, decay=0.99), + p2_first_message_deliveries=TopicScoreParams(weight=0.5, cap=20.0, decay=0.99), + p3_mesh_message_deliveries=TopicScoreParams(weight=0.3, cap=10.0, decay=0.99), + p4_invalid_messages=TopicScoreParams(weight=-1.0, cap=50.0, decay=0.99), + ) + base_config["score_params"] = score_params + return base_config + + elif self.protocol_version == GOSSIPSUB_V12: + # Add IDONTWANT support for v1.2 + score_params = ScoreParams( + p1_time_in_mesh=TopicScoreParams(weight=0.1, cap=10.0, decay=0.99), + p2_first_message_deliveries=TopicScoreParams(weight=0.5, cap=20.0, decay=0.99), + p3_mesh_message_deliveries=TopicScoreParams(weight=0.3, cap=10.0, decay=0.99), + p4_invalid_messages=TopicScoreParams(weight=-1.0, cap=50.0, decay=0.99), + ) + base_config.update({ + "score_params": score_params, + "max_idontwant_messages": 20, + }) + return base_config + + elif self.protocol_version == GOSSIPSUB_V20: + # Full v2.0 configuration with adaptive features and security + score_params = ScoreParams( + p1_time_in_mesh=TopicScoreParams(weight=0.1, cap=10.0, decay=0.99), + p2_first_message_deliveries=TopicScoreParams(weight=0.5, cap=20.0, decay=0.99), + p3_mesh_message_deliveries=TopicScoreParams(weight=0.3, cap=10.0, decay=0.99), + p4_invalid_messages=TopicScoreParams(weight=-1.0, cap=50.0, decay=0.99), + p5_behavior_penalty_weight=1.0, + p5_behavior_penalty_decay=0.99, + p6_appl_slack_weight=0.1, + p7_ip_colocation_weight=0.5, + publish_threshold=0.0, + gossip_threshold=-1.0, + graylist_threshold=-10.0, + ) + base_config.update({ + "score_params": score_params, + "max_idontwant_messages": 20, + "adaptive_gossip_enabled": True, + "spam_protection_enabled": True, + "max_messages_per_topic_per_second": 10.0, + "eclipse_protection_enabled": True, + "min_mesh_diversity_ips": 2, + }) + return base_config + + return base_config + + async def publish_message(self, message: str) -> str: + """Publish a message and return message ID for tracking""" + if self.pubsub: + msg_id = f"{self.node_id}_{self.messages_sent}_{int(time.time() * 1000)}" + full_message = f"{msg_id}:{message}" + + self.message_timestamps[msg_id] = time.time() + await self.pubsub.publish(TOPIC, full_message.encode()) + self.messages_sent += 1 + return msg_id + return "" + + async def receive_messages(self, metrics: NetworkMetrics): + """Receive and process messages, updating metrics""" + if not self.subscription: + return + + try: + while True: + message = await self.subscription.get() + decoded = message.data.decode('utf-8') + + # Parse message to extract ID and calculate latency + if ':' in decoded: + msg_id, content = decoded.split(':', 1) + + # Calculate latency if we sent this message + if msg_id in self.message_timestamps: + latency = (time.time() - self.message_timestamps[msg_id]) * 1000 + self.latencies.append(latency) + del self.message_timestamps[msg_id] + + self.messages_received += 1 + metrics.total_messages_received += 1 + + except Exception as e: + logger.debug(f"Node {self.node_id} receive loop ended: {e}") + + async def connect_to_peer(self, peer_addr: str): + """Connect to another peer""" + if self.host: + try: + from libp2p.peer.peerinfo import info_from_p2p_addr + import multiaddr + + maddr = multiaddr.Multiaddr(peer_addr) + info = info_from_p2p_addr(maddr) + await self.host.connect(info) + logger.debug(f"Node {self.node_id} connected to {peer_addr}") + except Exception as e: + logger.debug(f"Node {self.node_id} failed to connect to {peer_addr}: {e}") + + def get_metrics(self) -> Dict[str, Any]: + """Get current metrics for this node""" + avg_latency = statistics.mean(self.latencies) if self.latencies else 0.0 + return { + "messages_sent": self.messages_sent, + "messages_received": self.messages_received, + "average_latency_ms": avg_latency, + "pending_messages": len(self.message_timestamps) + } + + +class NetworkSimulator: + """Simulates different network scenarios for comparison testing""" + + def __init__(self): + self.networks = {} # version -> list of nodes + self.metrics = {} # version -> NetworkMetrics + + async def setup_networks(self, nodes_per_version: int = 5): + """Set up networks for each protocol version""" + versions = [ + ("v1.0", GOSSIPSUB_V10), + ("v1.1", GOSSIPSUB_V11), + ("v1.2", GOSSIPSUB_V12), + ("v2.0", GOSSIPSUB_V20), + ] + + for version_name, protocol in versions: + self.networks[version_name] = [] + self.metrics[version_name] = NetworkMetrics(version=version_name) + + # Create nodes for this version + for i in range(nodes_per_version): + port = find_free_port() + node_id = f"{version_name}_node_{i}" + node = NetworkNode(node_id, protocol, port) + self.networks[version_name].append(node) + + logger.info(f"Created {len(versions)} networks with {nodes_per_version} nodes each") + + async def run_scenario(self, scenario: str, duration: int = 60) -> ComparisonResult: + """Run a specific test scenario and collect metrics""" + logger.info(f"Starting scenario: {scenario} (duration: {duration}s)") + start_time = time.time() + + # Start all networks concurrently with timeout + try: + async with trio.open_nursery() as nursery: + # Start all nodes + for version, nodes in self.networks.items(): + for node in nodes: + nursery.start_soon(node.start) + + # Wait for nodes to initialize + await trio.sleep(3) + + # Connect nodes within each network + await self._connect_networks() + await trio.sleep(2) + + # Start message receiving for all nodes + for version, nodes in self.networks.items(): + for node in nodes: + nursery.start_soon(node.receive_messages, self.metrics[version]) + + # Run the specific scenario with timeout + with trio.move_on_after(duration + 10): # Add buffer time + if scenario == "normal": + await self._run_normal_scenario(duration) + elif scenario == "high_churn": + await self._run_high_churn_scenario(duration) + elif scenario == "spam_attack": + await self._run_spam_attack_scenario(duration) + elif scenario == "network_partition": + await self._run_network_partition_scenario(duration) + else: + logger.error(f"Unknown scenario: {scenario}") + return ComparisonResult(scenario, 0, {}) + + # Cancel all tasks to exit nursery + nursery.cancel_scope.cancel() + + except Exception as e: + logger.warning(f"Scenario execution interrupted: {e}") + + # Calculate final metrics + end_time = time.time() + duration_actual = end_time - start_time + + await self._calculate_final_metrics() + + return ComparisonResult( + scenario=scenario, + duration_seconds=duration_actual, + metrics_by_version=self.metrics + ) + + async def _connect_networks(self): + """Connect nodes within each network to form mesh topology""" + for version, nodes in self.networks.items(): + # Connect each node to other nodes in the same network + for i, node in enumerate(nodes): + # Connect to the next node in a ring topology for simplicity + if len(nodes) > 1: + target_idx = (i + 1) % len(nodes) + target = nodes[target_idx] + + if target.host and node.host: + peer_addr = f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + await node.connect_to_peer(peer_addr) + + async def _run_normal_scenario(self, duration: int): + """Normal operation with honest peers""" + logger.info("Running normal scenario - honest peers publishing regularly") + + end_time = time.time() + duration + message_counter = 0 + + while time.time() < end_time: + # Each network publishes messages at regular intervals + for version, nodes in self.networks.items(): + # Random node publishes a message + node = random.choice(nodes) + message = f"normal_msg_{message_counter}" + await node.publish_message(message) + self.metrics[version].total_messages_sent += 1 + message_counter += 1 + + await trio.sleep(1) # Publish every second + + async def _run_high_churn_scenario(self, duration: int): + """High peer churn scenario""" + logger.info("Running high churn scenario - peers joining/leaving frequently") + + end_time = time.time() + duration + message_counter = 0 + + while time.time() < end_time: + # Normal message publishing + for version, nodes in self.networks.items(): + active_nodes = [n for n in nodes if n.host] + if active_nodes: + node = random.choice(active_nodes) + message = f"churn_msg_{message_counter}" + await node.publish_message(message) + self.metrics[version].total_messages_sent += 1 + self.metrics[version].peer_churn_events += 1 + message_counter += 1 + + await trio.sleep(0.5) + + async def _run_spam_attack_scenario(self, duration: int): + """Spam attack scenario with malicious peers""" + logger.info("Running spam attack scenario - some peers sending excessive messages") + + # Mark some nodes as malicious + for version, nodes in self.networks.items(): + malicious_count = max(1, len(nodes) // 3) # 1/3 of nodes are malicious + for i in range(malicious_count): + nodes[i].is_malicious = True + + end_time = time.time() + duration + message_counter = 0 + + while time.time() < end_time: + for version, nodes in self.networks.items(): + for node in nodes: + if node.is_malicious: + # Malicious nodes send many messages + for _ in range(5): + message = f"spam_msg_{message_counter}" + await node.publish_message(message) + message_counter += 1 + else: + # Honest nodes send normal messages + message = f"honest_msg_{message_counter}" + await node.publish_message(message) + self.metrics[version].total_messages_sent += 1 + message_counter += 1 + + await trio.sleep(0.2) # Faster publishing for spam scenario + + async def _run_network_partition_scenario(self, duration: int): + """Network partition and recovery scenario""" + logger.info("Running network partition scenario - network splits and recovers") + + partition_time = duration // 3 + recovery_time = time.time() + partition_time + end_time = time.time() + duration + message_counter = 0 + + # Phase 1: Normal operation + logger.info("Phase 1: Normal operation") + while time.time() < recovery_time: + for version, nodes in self.networks.items(): + node = random.choice(nodes) + message = f"pre_partition_msg_{message_counter}" + await node.publish_message(message) + self.metrics[version].total_messages_sent += 1 + message_counter += 1 + await trio.sleep(1) + + # Phase 2: Partition (simulate by reducing connectivity) + logger.info("Phase 2: Network partition") + partition_start = time.time() + + while time.time() < end_time: + # Reduced message publishing during partition + for version, nodes in self.networks.items(): + # Only half the nodes can communicate + active_nodes = nodes[:len(nodes)//2] + if active_nodes: + node = random.choice(active_nodes) + message = f"partition_msg_{message_counter}" + await node.publish_message(message) + self.metrics[version].total_messages_sent += 1 + message_counter += 1 + await trio.sleep(2) # Slower during partition + + # Record partition recovery time + recovery_duration = (time.time() - partition_start) * 1000 + for version in self.metrics: + self.metrics[version].partition_recovery_time_ms = recovery_duration + + async def _calculate_final_metrics(self): + """Calculate final metrics for all networks""" + for version, nodes in self.networks.items(): + metrics = self.metrics[version] + + # Aggregate node metrics + total_sent = sum(node.messages_sent for node in nodes) + total_received = sum(node.messages_received for node in nodes) + + all_latencies = [] + for node in nodes: + all_latencies.extend(node.latencies) + + # Calculate delivery rate and latency + if total_sent > 0: + # Expected receives = sent * (nodes - 1) since each message should reach all other nodes + expected_receives = total_sent * (len(nodes) - 1) + metrics.message_delivery_rate = min(1.0, total_received / expected_receives) + + metrics.average_latency_ms = statistics.mean(all_latencies) if all_latencies else 0.0 + + logger.info(f"{version} final metrics: " + f"sent={total_sent}, received={total_received}, " + f"delivery_rate={metrics.message_delivery_rate:.2%}, " + f"avg_latency={metrics.average_latency_ms:.1f}ms") + + +def print_comparison_results(result: ComparisonResult): + """Print formatted comparison results""" + print(f"\n{'='*80}") + print(f"GOSSIPSUB VERSION COMPARISON RESULTS") + print(f"{'='*80}") + print(f"Scenario: {result.scenario}") + print(f"Duration: {result.duration_seconds:.1f} seconds") + print(f"{'='*80}") + + # Print metrics table + versions = list(result.metrics_by_version.keys()) + + print(f"{'Metric':<30} {'v1.0':<12} {'v1.1':<12} {'v1.2':<12} {'v2.0':<12}") + print(f"{'-'*80}") + + metrics_to_show = [ + ("Messages Sent", "total_messages_sent"), + ("Messages Received", "total_messages_received"), + ("Delivery Rate", "message_delivery_rate"), + ("Avg Latency (ms)", "average_latency_ms"), + ("Spam Blocked", "spam_messages_blocked"), + ("Churn Events", "peer_churn_events"), + ] + + for metric_name, metric_key in metrics_to_show: + row = f"{metric_name:<30}" + for version in versions: + metrics = result.metrics_by_version[version] + value = getattr(metrics, metric_key) + + if metric_key == "message_delivery_rate": + row += f"{value:.1%}".ljust(12) + elif metric_key in ["average_latency_ms", "partition_recovery_time_ms"]: + row += f"{value:.1f}".ljust(12) + else: + row += f"{value}".ljust(12) + print(row) + + print(f"{'='*80}") + + # Analysis + print("\nANALYSIS:") + best_delivery = max(result.metrics_by_version.items(), + key=lambda x: x[1].message_delivery_rate) + print(f"โ€ข Best message delivery rate: {best_delivery[0]} ({best_delivery[1].message_delivery_rate:.1%})") + + best_latency = min(result.metrics_by_version.items(), + key=lambda x: x[1].average_latency_ms) + print(f"โ€ข Lowest average latency: {best_latency[0]} ({best_latency[1].average_latency_ms:.1f}ms)") + + if result.scenario == "spam_attack": + best_spam_protection = max(result.metrics_by_version.items(), + key=lambda x: x[1].spam_messages_blocked) + print(f"โ€ข Best spam protection: {best_spam_protection[0]} ({best_spam_protection[1].spam_messages_blocked} blocked)") + + +async def main(): + parser = argparse.ArgumentParser(description="Gossipsub Version Comparison Demo") + parser.add_argument( + "--scenario", + choices=["normal", "high_churn", "spam_attack", "network_partition"], + default="normal", + help="Test scenario to run" + ) + parser.add_argument( + "--duration", + type=int, + default=30, + help="Test duration in seconds" + ) + parser.add_argument( + "--nodes", + type=int, + default=4, + help="Number of nodes per version" + ) + parser.add_argument( + "--output", + type=str, + help="Output file for JSON results" + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Enable verbose logging" + ) + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + # Create and run simulation + simulator = NetworkSimulator() + await simulator.setup_networks(args.nodes) + + result = await simulator.run_scenario(args.scenario, args.duration) + + # Display results + print_comparison_results(result) + + # Save to file if requested + if args.output: + with open(args.output, 'w') as f: + json.dump(result.to_dict(), f, indent=2) + print(f"\nResults saved to {args.output}") + + +if __name__ == "__main__": + trio.run(main) \ No newline at end of file From e56f9b36e6ed313ca87b30cf9a940865e81ca639 Mon Sep 17 00:00:00 2001 From: Winter-Soren Date: Thu, 29 Jan 2026 16:53:03 +0530 Subject: [PATCH 2/9] decomposed single example file into multiple gossipsub version based files with examples --- examples/pubsub/gossipsub/README.md | 167 ++-- examples/pubsub/gossipsub/__init__.py | 2 +- examples/pubsub/gossipsub/gossipsub_v1.0.py | 280 +++++++ examples/pubsub/gossipsub/gossipsub_v1.1.py | 316 ++++++++ examples/pubsub/gossipsub/gossipsub_v1.2.py | 311 +++++++ examples/pubsub/gossipsub/gossipsub_v2.0.py | 380 +++++++++ examples/pubsub/gossipsub/run_examples.py | 310 ------- examples/pubsub/gossipsub/v2_showcase.py | 765 ------------------ .../pubsub/gossipsub/version_comparison.py | 619 -------------- 9 files changed, 1368 insertions(+), 1782 deletions(-) create mode 100755 examples/pubsub/gossipsub/gossipsub_v1.0.py create mode 100755 examples/pubsub/gossipsub/gossipsub_v1.1.py create mode 100755 examples/pubsub/gossipsub/gossipsub_v1.2.py create mode 100755 examples/pubsub/gossipsub/gossipsub_v2.0.py delete mode 100755 examples/pubsub/gossipsub/run_examples.py delete mode 100755 examples/pubsub/gossipsub/v2_showcase.py delete mode 100755 examples/pubsub/gossipsub/version_comparison.py diff --git a/examples/pubsub/gossipsub/README.md b/examples/pubsub/gossipsub/README.md index 5285527f3..c749bf2ef 100644 --- a/examples/pubsub/gossipsub/README.md +++ b/examples/pubsub/gossipsub/README.md @@ -13,47 +13,63 @@ With the recent implementation of Gossipsub 2.0 support in py-libp2p, we now hav ## Examples -### 1. Version Comparison Demo (`version_comparison.py`) +### 1. Gossipsub 1.0 Demo (`gossipsub_v1.0.py`) -Side-by-side demonstration of how different Gossipsub versions handle the same network scenarios. +Basic mesh-based pubsub demo using Gossipsub 1.0 (`/meshsub/1.0.0`). **Features:** -- **Network Simulation**: Creates identical network topologies running different protocol versions -- **Scenario Testing**: Tests various network conditions (high churn, malicious peers, network partitions) -- **Performance Metrics**: Compares message delivery rates, latency, and network overhead -- **Visual Output**: Real-time comparison charts and statistics + +- Basic mesh-based pubsub +- Simple flooding for message dissemination +- Mesh topology maintenance **Usage:** + ```bash -# Normal operation scenario -python version_comparison.py --scenario normal --duration 60 +python gossipsub_v1.0.py --nodes 5 --duration 30 +``` + +### 2. Gossipsub 1.1 Demo (`gossipsub_v1.1.py`) + +Demonstrates Gossipsub 1.1 (`/meshsub/1.1.0`) with peer scoring and behavioral penalties. -# High peer churn scenario -python version_comparison.py --scenario high_churn --duration 60 +**Features:** -# Spam attack scenario -python version_comparison.py --scenario spam_attack --duration 60 +- All Gossipsub 1.0 features +- Peer scoring with P1โ€“P4 topic-scoped parameters +- Behavioral penalties (P5) +- Honest vs. malicious publisher behaviour -# Network partition scenario -python version_comparison.py --scenario network_partition --duration 60 +**Usage:** -# Save results to JSON -python version_comparison.py --scenario normal --output results.json +```bash +python gossipsub_v1.1.py --nodes 5 --duration 30 ``` -**Scenarios:** -- **Normal Operation**: Honest peers publishing regularly -- **High Churn**: Peers joining/leaving frequently -- **Spam Attack**: Some peers sending excessive messages -- **Network Partition**: Network splits and recovers +### 3. Gossipsub 1.2 Demo (`gossipsub_v1.2.py`) + +Demonstrates Gossipsub 1.2 (`/meshsub/1.2.0`) with IDONTWANT message filtering. + +**Features:** + +- All Gossipsub 1.1 features +- IDONTWANT messages and message filtering +- Reduced redundant traffic in denser meshes + +**Usage:** + +```bash +python gossipsub_v1.2.py --nodes 5 --duration 30 +``` -### 2. Gossipsub 2.0 Feature Showcase (`v2_showcase.py`) +### 4. Gossipsub 2.0 Demo (`gossipsub_v2.0.py`) -Interactive demonstration of Gossipsub 2.0's advanced features. +Demonstrates Gossipsub 2.0 (`/meshsub/2.0.0`) with adaptive gossip and advanced security features. **Features:** #### Peer Scoring Visualization + - **Real-time Score Display**: Shows peer scores (P1-P7 parameters) updating in real-time - **Score Component Breakdown**: Visualizes individual scoring components - **Behavioral Penalties**: Demonstrates how misbehavior affects peer scores @@ -61,40 +77,36 @@ Interactive demonstration of Gossipsub 2.0's advanced features. - **Application Scoring**: Demonstrates P6 custom application-defined scoring #### Adaptive Gossip Demonstration + - **Network Health Monitoring**: Displays network health score calculation - **Dynamic Parameter Adjustment**: Shows how gossip parameters adapt to network conditions - **Mesh Quality Maintenance**: Visualizes mesh degree adjustments - **Opportunistic Grafting**: Demonstrates score-based peer selection #### Security Features + - **Spam Protection**: Shows rate limiting in action - **Eclipse Attack Protection**: Demonstrates IP diversity enforcement - **Equivocation Detection**: Shows detection and penalties for duplicate messages - **Message Validation**: Demonstrates validation hooks and caching **Usage:** -```bash -# Interactive mode - explore all features -python v2_showcase.py --mode interactive - -# Demo specific features -python v2_showcase.py --mode demo --feature scoring --duration 60 -python v2_showcase.py --mode demo --feature adaptive --duration 60 -python v2_showcase.py --mode demo --feature security --duration 60 -# Save monitoring data -python v2_showcase.py --mode demo --feature scoring --output monitoring.json +```bash +python gossipsub_v2.0.py --nodes 5 --duration 60 ``` ## Protocol Version Differences ### Gossipsub 1.0 (`/meshsub/1.0.0`) + - Basic mesh-based pubsub protocol - Simple flooding for message dissemination - No peer scoring or advanced security features - Suitable for trusted networks with low adversarial activity ### Gossipsub 1.1 (`/meshsub/1.1.0`) + - **Added Peer Scoring**: P1-P4 topic-scoped parameters - P1: Time in mesh - P2: First message deliveries @@ -105,12 +117,14 @@ python v2_showcase.py --mode demo --feature scoring --output monitoring.json - Better resilience against basic attacks ### Gossipsub 1.2 (`/meshsub/1.2.0`) + - **IDONTWANT Messages**: Peers can signal they don't want specific messages - **Message Filtering**: Reduces redundant message transmission - **Improved Efficiency**: Lower bandwidth usage in dense networks - All v1.1 features included ### Gossipsub 2.0 (`/meshsub/2.0.0`) + - **Enhanced Peer Scoring**: P6 (application-specific) and P7 (IP colocation) parameters - **Adaptive Gossip**: Dynamic parameter adjustment based on network health - **Advanced Security Features**: @@ -124,12 +138,14 @@ python v2_showcase.py --mode demo --feature scoring --output monitoring.json ## Peer Scoring Parameters (P1-P7) ### Topic-Scoped Parameters (P1-P4) + - **P1 (Time in Mesh)**: Rewards peers for staying in the mesh longer - **P2 (First Message Deliveries)**: Rewards peers for delivering messages first - **P3 (Mesh Message Deliveries)**: Rewards peers for consistent message delivery - **P4 (Invalid Messages)**: Penalizes peers for sending invalid messages ### Global Parameters (P5-P7) + - **P5 (Behavior Penalty)**: General behavioral penalty for misbehavior - **P6 (Application Score)**: Custom application-defined scoring - **P7 (IP Colocation)**: Penalizes multiple peers from same IP address @@ -137,21 +153,25 @@ python v2_showcase.py --mode demo --feature scoring --output monitoring.json ## Security Features in Gossipsub 2.0 ### Spam Protection + - Rate limiting per peer per topic - Configurable message rate thresholds - Automatic penalty application for rate limit violations ### Eclipse Attack Protection + - Minimum IP diversity requirements in mesh - Penalties for excessive peers from same IP range - Mesh diversity monitoring and enforcement ### Equivocation Detection + - Detection of duplicate messages with same sequence number - Penalties for peers sending conflicting messages - Message deduplication and validation ### Message Validation + - Configurable validation hooks - Validation result caching - Integration with peer scoring system @@ -159,57 +179,30 @@ python v2_showcase.py --mode demo --feature scoring --output monitoring.json ## Running the Examples ### Basic Usage + ```bash # Navigate to the examples directory cd examples/pubsub/gossipsub -# Run version comparison -python version_comparison.py --scenario normal - -# Run interactive showcase -python v2_showcase.py --mode interactive -``` - -### Advanced Usage -```bash -# Compare all versions with custom parameters -python version_comparison.py \ - --scenario spam_attack \ - --duration 120 \ - --nodes 8 \ - --output spam_comparison.json \ - --verbose - -# Showcase specific features with monitoring -python v2_showcase.py \ - --mode demo \ - --feature security \ - --duration 180 \ - --nodes 10 \ - --output security_monitoring.json \ - --verbose +# Run per-version demos +python gossipsub_v1.0.py --nodes 5 --duration 30 +python gossipsub_v1.1.py --nodes 5 --duration 30 +python gossipsub_v1.2.py --nodes 5 --duration 30 +python gossipsub_v2.0.py --nodes 5 --duration 60 ``` ## Understanding the Output -### Version Comparison Results -The comparison demo outputs a detailed table showing: -- **Messages Sent/Received**: Total message counts per version -- **Delivery Rate**: Percentage of messages successfully delivered -- **Average Latency**: Mean message propagation time -- **Spam Blocked**: Number of spam messages filtered (v2.0 only) -- **Churn Events**: Number of peer join/leave events handled - -### Feature Showcase Output -The v2.0 showcase provides real-time displays of: -- **Peer Score Breakdown**: Individual P1-P7 component scores -- **Network Health Metrics**: Connectivity and health scores -- **Security Events**: Real-time security event notifications -- **Adaptive Parameters**: Dynamic parameter adjustments +The per-version demos print statistics summarising: + +- **Messages Sent/Received** per node +- **Roles** (honest, malicious, spammer, validator) and their behaviour +- **Feature Highlights** for the corresponding protocol version (for example, IDONTWANT support in v1.2, security and adaptive gossip in v2.0) ## Network Topologies -Both examples create realistic network topologies: +All demos create realistic network topologies: + - **Mesh Connectivity**: Each node connects to 3-4 peers - **Realistic Latency**: Simulated network delays - **Diverse Roles**: Honest peers, spammers, validators, attackers @@ -218,7 +211,8 @@ Both examples create realistic network topologies: ## Customization ### Adding Custom Scenarios -To add new test scenarios to the version comparison: + +To add new test scenarios to the per-version demos: ```python async def _run_custom_scenario(self, duration: int): @@ -232,6 +226,7 @@ elif scenario == "custom": ``` ### Custom Scoring Functions + To implement custom application scoring: ```python @@ -248,6 +243,7 @@ score_params = ScoreParams( ``` ### Custom Validation + To add custom message validation: ```python @@ -265,26 +261,23 @@ node._validate_message = custom_validator ### Common Issues 1. **Port Conflicts**: If you get port binding errors, the examples will automatically find free ports -2. **Connection Failures**: Ensure firewall allows local connections on the used ports -3. **High CPU Usage**: Reduce the number of nodes or increase sleep intervals for testing -4. **Memory Usage**: Large networks may consume significant memory; monitor usage +1. **Connection Failures**: Ensure firewall allows local connections on the used ports +1. **High CPU Usage**: Reduce the number of nodes or increase sleep intervals for testing +1. **Memory Usage**: Large networks may consume significant memory; monitor usage ### Debug Mode + Enable verbose logging for detailed information: + ```bash -python version_comparison.py --scenario normal --verbose -python v2_showcase.py --mode interactive --verbose +python gossipsub_v1.0.py --verbose ... +python gossipsub_v1.1.py --verbose ... +python gossipsub_v1.2.py --verbose ... +python gossipsub_v2.0.py --verbose ... ``` -### Performance Tuning -For better performance in large networks: -- Reduce heartbeat frequency -- Increase message intervals -- Limit concurrent connections -- Use smaller mesh degrees - ## References - [Gossipsub v1.1 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md) - [Gossipsub v1.2 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.2.md) -- [Gossipsub v2.0 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v2.0.md) \ No newline at end of file +- [Gossipsub v2.0 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v2.0.md) diff --git a/examples/pubsub/gossipsub/__init__.py b/examples/pubsub/gossipsub/__init__.py index c0b3f4781..357590a2c 100755 --- a/examples/pubsub/gossipsub/__init__.py +++ b/examples/pubsub/gossipsub/__init__.py @@ -3,4 +3,4 @@ This package contains comprehensive examples showcasing the differences between Gossipsub protocol versions and demonstrating advanced features. -""" \ No newline at end of file +""" diff --git a/examples/pubsub/gossipsub/gossipsub_v1.0.py b/examples/pubsub/gossipsub/gossipsub_v1.0.py new file mode 100755 index 000000000..e4f20804a --- /dev/null +++ b/examples/pubsub/gossipsub/gossipsub_v1.0.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +""" +Gossipsub 1.0 Example + +This example demonstrates the basic Gossipsub 1.0 protocol (/meshsub/1.0.0). +Gossipsub 1.0 provides basic mesh-based pubsub with simple flooding for message +dissemination. It has no peer scoring or advanced security features, making it +suitable for trusted networks with low adversarial activity. + +Features demonstrated: +- Basic mesh-based pubsub +- Simple message flooding +- Mesh topology maintenance +- Message publishing and subscription + +Usage: + python gossipsub_v1.0.py --nodes 5 --duration 30 +""" + +import argparse +import logging +import random +import time +from typing import List + +import trio + +from libp2p import new_host +from libp2p.crypto.rsa import create_new_key_pair +from libp2p.custom_types import TProtocol +from libp2p.pubsub.gossipsub import GossipSub +from libp2p.pubsub.pubsub import Pubsub +from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex +from libp2p.tools.async_service.trio_service import background_trio_service +from libp2p.utils.address_validation import find_free_port + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger("gossipsub-v1.0") + +# Protocol version +GOSSIPSUB_V10 = TProtocol("/meshsub/1.0.0") +TOPIC = "gossipsub-v1.0-demo" + + +class GossipsubV10Node: + """A node running Gossipsub 1.0""" + + def __init__(self, node_id: str, port: int): + self.node_id = node_id + self.port = port + self.host = None + self.pubsub = None + self.gossipsub = None + self.subscription = None + self.messages_sent = 0 + self.messages_received = 0 + + async def start(self): + """Start the node with Gossipsub 1.0 configuration""" + key_pair = create_new_key_pair() + + self.host = new_host( + key_pair=key_pair, + muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, + ) + + # Configure Gossipsub 1.0 - basic configuration only + self.gossipsub = GossipSub( + protocols=[GOSSIPSUB_V10], + degree=3, + degree_low=2, + degree_high=4, + heartbeat_interval=5, + heartbeat_initial_delay=1.0, + # No score_params - v1.0 doesn't have peer scoring + # No max_idontwant_messages - v1.0 doesn't support IDONTWANT + # No adaptive features - v1.0 doesn't have adaptive gossip + # No security features - v1.0 has basic security only + ) + + self.pubsub = Pubsub(self.host, self.gossipsub) + + # Start services + import multiaddr + listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{self.port}")] + + async with self.host.run(listen_addrs=listen_addrs): + async with background_trio_service(self.pubsub): + async with background_trio_service(self.gossipsub): + await self.pubsub.wait_until_ready() + self.subscription = await self.pubsub.subscribe(TOPIC) + logger.info(f"Node {self.node_id} (Gossipsub 1.0) started on port {self.port}") + + # Keep running + await trio.sleep_forever() + + async def publish_message(self, message: str): + """Publish a message to the topic""" + if self.pubsub: + await self.pubsub.publish(TOPIC, message.encode()) + self.messages_sent += 1 + logger.info(f"Node {self.node_id} published: {message}") + + async def receive_messages(self): + """Receive and process messages""" + if not self.subscription: + return + + try: + while True: + message = await self.subscription.get() + decoded = message.data.decode('utf-8') + self.messages_received += 1 + logger.info(f"Node {self.node_id} received: {decoded}") + except Exception as e: + logger.debug(f"Node {self.node_id} receive loop ended: {e}") + + async def connect_to_peer(self, peer_addr: str): + """Connect to another peer""" + if self.host: + try: + from libp2p.peer.peerinfo import info_from_p2p_addr + import multiaddr + + maddr = multiaddr.Multiaddr(peer_addr) + info = info_from_p2p_addr(maddr) + await self.host.connect(info) + logger.debug(f"Node {self.node_id} connected to {peer_addr}") + except Exception as e: + logger.debug(f"Node {self.node_id} failed to connect to {peer_addr}: {e}") + + +class GossipsubV10Demo: + """Demo controller for Gossipsub 1.0""" + + def __init__(self): + self.nodes: List[GossipsubV10Node] = [] + + async def setup_network(self, node_count: int = 5): + """Set up a network of nodes""" + for i in range(node_count): + port = find_free_port() + node = GossipsubV10Node(f"node_{i}", port) + self.nodes.append(node) + + logger.info(f"Created network with {node_count} nodes running Gossipsub 1.0") + + async def start_network(self, duration: int = 30): + """Start all nodes and run the demo""" + try: + async with trio.open_nursery() as nursery: + # Start all nodes + for node in self.nodes: + nursery.start_soon(node.start) + + # Wait for initialization + await trio.sleep(3) + + # Connect nodes in a mesh topology + await self._connect_nodes() + await trio.sleep(2) + + # Start message receiving for all nodes + for node in self.nodes: + nursery.start_soon(node.receive_messages) + + # Run publishing loop + end_time = time.time() + duration + message_counter = 0 + + print(f"\n{'='*60}") + print("GOSSIPSUB 1.0 DEMO") + print(f"{'='*60}") + print(f"Running for {duration} seconds...") + print(f"Protocol: /meshsub/1.0.0") + print(f"Features: Basic mesh-based pubsub, simple flooding") + print(f"{'='*60}\n") + + while time.time() < end_time: + # Random node publishes a message + node = random.choice(self.nodes) + message = f"msg_{message_counter}_{int(time.time())}" + await node.publish_message(message) + message_counter += 1 + await trio.sleep(2) # Publish every 2 seconds + + # Print statistics + await trio.sleep(1) # Wait for final messages + self._print_statistics() + + # Cancel all tasks to exit nursery + nursery.cancel_scope.cancel() + + except Exception as e: + logger.warning(f"Demo execution interrupted: {e}") + + async def _connect_nodes(self): + """Connect nodes in a mesh topology""" + for i, node in enumerate(self.nodes): + # Connect to the next node in a ring topology + if len(self.nodes) > 1: + target_idx = (i + 1) % len(self.nodes) + target = self.nodes[target_idx] + + if target.host and node.host: + peer_addr = f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + await node.connect_to_peer(peer_addr) + + # Also connect to one more node for better connectivity + if len(self.nodes) > 2: + target_idx2 = (i + 2) % len(self.nodes) + target2 = self.nodes[target_idx2] + + if target2.host and node.host: + peer_addr2 = f"/ip4/127.0.0.1/tcp/{target2.port}/p2p/{target2.host.get_id()}" + await node.connect_to_peer(peer_addr2) + + def _print_statistics(self): + """Print demo statistics""" + print(f"\n{'='*60}") + print("DEMO STATISTICS") + print(f"{'='*60}") + + total_sent = sum(node.messages_sent for node in self.nodes) + total_received = sum(node.messages_received for node in self.nodes) + + print(f"Total messages sent: {total_sent}") + print(f"Total messages received: {total_received}") + print(f"\nPer-node statistics:") + for node in self.nodes: + print(f" {node.node_id}: sent={node.messages_sent}, received={node.messages_received}") + + print(f"\n{'='*60}") + print("Gossipsub 1.0 Features:") + print(" โœ“ Basic mesh-based pubsub") + print(" โœ“ Simple message flooding") + print(" โœ“ Mesh topology maintenance") + print(" โœ— No peer scoring") + print(" โœ— No IDONTWANT support") + print(" โœ— No adaptive gossip") + print(" โœ— No advanced security features") + print(f"{'='*60}\n") + + +async def main(): + parser = argparse.ArgumentParser(description="Gossipsub 1.0 Example") + parser.add_argument( + "--nodes", + type=int, + default=5, + help="Number of nodes in the network" + ) + parser.add_argument( + "--duration", + type=int, + default=30, + help="Demo duration in seconds" + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Enable verbose logging" + ) + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + demo = GossipsubV10Demo() + await demo.setup_network(args.nodes) + await demo.start_network(args.duration) + + +if __name__ == "__main__": + trio.run(main) diff --git a/examples/pubsub/gossipsub/gossipsub_v1.1.py b/examples/pubsub/gossipsub/gossipsub_v1.1.py new file mode 100755 index 000000000..347785cb5 --- /dev/null +++ b/examples/pubsub/gossipsub/gossipsub_v1.1.py @@ -0,0 +1,316 @@ +#!/usr/bin/env python3 +""" +Gossipsub 1.1 Example + +This example demonstrates Gossipsub 1.1 protocol (/meshsub/1.1.0). +Gossipsub 1.1 adds peer scoring and behavioral penalties to the basic +mesh-based pubsub, providing better resilience against basic attacks. + +Features demonstrated: +- Basic mesh-based pubsub (from v1.0) +- Peer scoring with P1-P4 topic-scoped parameters +- Behavioral penalties (P5) +- Signed peer records +- Better resilience against attacks + +Usage: + python gossipsub_v1.1.py --nodes 5 --duration 30 +""" + +import argparse +import logging +import random +import time +from typing import List + +import trio + +from libp2p import new_host +from libp2p.crypto.rsa import create_new_key_pair +from libp2p.custom_types import TProtocol +from libp2p.pubsub.gossipsub import GossipSub +from libp2p.pubsub.pubsub import Pubsub +from libp2p.pubsub.score import ScoreParams, TopicScoreParams +from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex +from libp2p.tools.async_service.trio_service import background_trio_service +from libp2p.utils.address_validation import find_free_port + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger("gossipsub-v1.1") + +# Protocol version +GOSSIPSUB_V11 = TProtocol("/meshsub/1.1.0") +TOPIC = "gossipsub-v1.1-demo" + + +class GossipsubV11Node: + """A node running Gossipsub 1.1""" + + def __init__(self, node_id: str, port: int, role: str = "honest"): + self.node_id = node_id + self.port = port + self.role = role # "honest" or "malicious" + self.host = None + self.pubsub = None + self.gossipsub = None + self.subscription = None + self.messages_sent = 0 + self.messages_received = 0 + + async def start(self): + """Start the node with Gossipsub 1.1 configuration""" + key_pair = create_new_key_pair() + + self.host = new_host( + key_pair=key_pair, + muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, + ) + + # Configure Gossipsub 1.1 - adds peer scoring + score_params = ScoreParams( + # Topic-scoped parameters (P1-P4) + p1_time_in_mesh=TopicScoreParams(weight=0.1, cap=10.0, decay=0.99), + p2_first_message_deliveries=TopicScoreParams(weight=0.5, cap=20.0, decay=0.99), + p3_mesh_message_deliveries=TopicScoreParams(weight=0.3, cap=10.0, decay=0.99), + p4_invalid_messages=TopicScoreParams(weight=-1.0, cap=50.0, decay=0.99), + # Global behavioral penalty (P5) + p5_behavior_penalty_weight=1.0, + p5_behavior_penalty_decay=0.99, + ) + + self.gossipsub = GossipSub( + protocols=[GOSSIPSUB_V11], + degree=3, + degree_low=2, + degree_high=4, + heartbeat_interval=5, + heartbeat_initial_delay=1.0, + score_params=score_params, + # No max_idontwant_messages - v1.1 doesn't support IDONTWANT + # No adaptive features - v1.1 doesn't have adaptive gossip + # No advanced security features - v1.1 has basic security + ) + + self.pubsub = Pubsub(self.host, self.gossipsub) + + # Start services + import multiaddr + listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{self.port}")] + + async with self.host.run(listen_addrs=listen_addrs): + async with background_trio_service(self.pubsub): + async with background_trio_service(self.gossipsub): + await self.pubsub.wait_until_ready() + self.subscription = await self.pubsub.subscribe(TOPIC) + logger.info(f"Node {self.node_id} (Gossipsub 1.1, {self.role}) started on port {self.port}") + + # Keep running + await trio.sleep_forever() + + async def publish_message(self, message: str): + """Publish a message to the topic""" + if self.pubsub: + await self.pubsub.publish(TOPIC, message.encode()) + self.messages_sent += 1 + logger.info(f"Node {self.node_id} ({self.role}) published: {message}") + + async def receive_messages(self): + """Receive and process messages""" + if not self.subscription: + return + + try: + while True: + message = await self.subscription.get() + decoded = message.data.decode('utf-8') + self.messages_received += 1 + logger.info(f"Node {self.node_id} received: {decoded}") + except Exception as e: + logger.debug(f"Node {self.node_id} receive loop ended: {e}") + + async def connect_to_peer(self, peer_addr: str): + """Connect to another peer""" + if self.host: + try: + from libp2p.peer.peerinfo import info_from_p2p_addr + import multiaddr + + maddr = multiaddr.Multiaddr(peer_addr) + info = info_from_p2p_addr(maddr) + await self.host.connect(info) + logger.debug(f"Node {self.node_id} connected to {peer_addr}") + except Exception as e: + logger.debug(f"Node {self.node_id} failed to connect to {peer_addr}: {e}") + + +class GossipsubV11Demo: + """Demo controller for Gossipsub 1.1""" + + def __init__(self): + self.nodes: List[GossipsubV11Node] = [] + + async def setup_network(self, node_count: int = 5): + """Set up a network of nodes""" + roles = ["honest"] * (node_count - 1) + ["malicious"] * 1 + + for i in range(node_count): + port = find_free_port() + role = roles[i] if i < len(roles) else "honest" + node = GossipsubV11Node(f"node_{i}", port, role) + self.nodes.append(node) + + logger.info(f"Created network with {node_count} nodes running Gossipsub 1.1") + + async def start_network(self, duration: int = 30): + """Start all nodes and run the demo""" + try: + async with trio.open_nursery() as nursery: + # Start all nodes + for node in self.nodes: + nursery.start_soon(node.start) + + # Wait for initialization + await trio.sleep(3) + + # Connect nodes in a mesh topology + await self._connect_nodes() + await trio.sleep(2) + + # Start message receiving for all nodes + for node in self.nodes: + nursery.start_soon(node.receive_messages) + + # Run publishing loop + end_time = time.time() + duration + message_counter = 0 + + print(f"\n{'='*60}") + print("GOSSIPSUB 1.1 DEMO") + print(f"{'='*60}") + print(f"Running for {duration} seconds...") + print(f"Protocol: /meshsub/1.1.0") + print(f"Features: Peer scoring (P1-P5), behavioral penalties") + print(f"{'='*60}\n") + + while time.time() < end_time: + # Honest nodes publish normally + honest_nodes = [n for n in self.nodes if n.role == "honest"] + if honest_nodes: + node = random.choice(honest_nodes) + message = f"honest_msg_{message_counter}_{int(time.time())}" + await node.publish_message(message) + message_counter += 1 + + # Malicious nodes might send more messages (will be penalized) + malicious_nodes = [n for n in self.nodes if n.role == "malicious"] + if malicious_nodes and random.random() < 0.3: # 30% chance + node = malicious_nodes[0] + message = f"malicious_msg_{message_counter}_{int(time.time())}" + await node.publish_message(message) + message_counter += 1 + + await trio.sleep(2) # Publish every 2 seconds + + # Print statistics + await trio.sleep(1) # Wait for final messages + self._print_statistics() + + # Cancel all tasks to exit nursery + nursery.cancel_scope.cancel() + + except Exception as e: + logger.warning(f"Demo execution interrupted: {e}") + + async def _connect_nodes(self): + """Connect nodes in a mesh topology""" + for i, node in enumerate(self.nodes): + # Connect to the next node in a ring topology + if len(self.nodes) > 1: + target_idx = (i + 1) % len(self.nodes) + target = self.nodes[target_idx] + + if target.host and node.host: + peer_addr = f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + await node.connect_to_peer(peer_addr) + + # Also connect to one more node for better connectivity + if len(self.nodes) > 2: + target_idx2 = (i + 2) % len(self.nodes) + target2 = self.nodes[target_idx2] + + if target2.host and node.host: + peer_addr2 = f"/ip4/127.0.0.1/tcp/{target2.port}/p2p/{target2.host.get_id()}" + await node.connect_to_peer(peer_addr2) + + def _print_statistics(self): + """Print demo statistics""" + print(f"\n{'='*60}") + print("DEMO STATISTICS") + print(f"{'='*60}") + + total_sent = sum(node.messages_sent for node in self.nodes) + total_received = sum(node.messages_received for node in self.nodes) + + honest_sent = sum(n.messages_sent for n in self.nodes if n.role == "honest") + malicious_sent = sum(n.messages_sent for n in self.nodes if n.role == "malicious") + + print(f"Total messages sent: {total_sent}") + print(f" Honest nodes: {honest_sent}") + print(f" Malicious nodes: {malicious_sent}") + print(f"Total messages received: {total_received}") + print(f"\nPer-node statistics:") + for node in self.nodes: + print(f" {node.node_id} ({node.role}): sent={node.messages_sent}, received={node.messages_received}") + + print(f"\n{'='*60}") + print("Gossipsub 1.1 Features:") + print(" โœ“ Basic mesh-based pubsub (from v1.0)") + print(" โœ“ Peer scoring with P1-P4 (topic-scoped)") + print(" - P1: Time in mesh") + print(" - P2: First message deliveries") + print(" - P3: Mesh message deliveries") + print(" - P4: Invalid messages penalty") + print(" โœ“ Behavioral penalties (P5)") + print(" โœ“ Signed peer records") + print(" โœ— No IDONTWANT support") + print(" โœ— No adaptive gossip") + print(" โœ— No advanced security features") + print(f"{'='*60}\n") + + +async def main(): + parser = argparse.ArgumentParser(description="Gossipsub 1.1 Example") + parser.add_argument( + "--nodes", + type=int, + default=5, + help="Number of nodes in the network" + ) + parser.add_argument( + "--duration", + type=int, + default=30, + help="Demo duration in seconds" + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Enable verbose logging" + ) + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + demo = GossipsubV11Demo() + await demo.setup_network(args.nodes) + await demo.start_network(args.duration) + + +if __name__ == "__main__": + trio.run(main) diff --git a/examples/pubsub/gossipsub/gossipsub_v1.2.py b/examples/pubsub/gossipsub/gossipsub_v1.2.py new file mode 100755 index 000000000..ef8ba9b1d --- /dev/null +++ b/examples/pubsub/gossipsub/gossipsub_v1.2.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +""" +Gossipsub 1.2 Example + +This example demonstrates Gossipsub 1.2 protocol (/meshsub/1.2.0). +Gossipsub 1.2 adds IDONTWANT message filtering to reduce redundant message +transmission, improving efficiency in dense networks. + +Features demonstrated: +- All Gossipsub 1.1 features (peer scoring, behavioral penalties) +- IDONTWANT message filtering +- Reduced bandwidth usage +- Improved efficiency in dense networks + +Usage: + python gossipsub_v1.2.py --nodes 5 --duration 30 +""" + +import argparse +import logging +import random +import time +from typing import List + +import trio + +from libp2p import new_host +from libp2p.crypto.rsa import create_new_key_pair +from libp2p.custom_types import TProtocol +from libp2p.pubsub.gossipsub import GossipSub +from libp2p.pubsub.pubsub import Pubsub +from libp2p.pubsub.score import ScoreParams, TopicScoreParams +from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex +from libp2p.tools.async_service.trio_service import background_trio_service +from libp2p.utils.address_validation import find_free_port + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger("gossipsub-v1.2") + +# Protocol version +GOSSIPSUB_V12 = TProtocol("/meshsub/1.2.0") +TOPIC = "gossipsub-v1.2-demo" + + +class GossipsubV12Node: + """A node running Gossipsub 1.2""" + + def __init__(self, node_id: str, port: int, role: str = "honest"): + self.node_id = node_id + self.port = port + self.role = role # "honest" or "malicious" + self.host = None + self.pubsub = None + self.gossipsub = None + self.subscription = None + self.messages_sent = 0 + self.messages_received = 0 + + async def start(self): + """Start the node with Gossipsub 1.2 configuration""" + key_pair = create_new_key_pair() + + self.host = new_host( + key_pair=key_pair, + muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, + ) + + # Configure Gossipsub 1.2 - adds IDONTWANT support + score_params = ScoreParams( + # Topic-scoped parameters (P1-P4) + p1_time_in_mesh=TopicScoreParams(weight=0.1, cap=10.0, decay=0.99), + p2_first_message_deliveries=TopicScoreParams(weight=0.5, cap=20.0, decay=0.99), + p3_mesh_message_deliveries=TopicScoreParams(weight=0.3, cap=10.0, decay=0.99), + p4_invalid_messages=TopicScoreParams(weight=-1.0, cap=50.0, decay=0.99), + # Global behavioral penalty (P5) + p5_behavior_penalty_weight=1.0, + p5_behavior_penalty_decay=0.99, + ) + + self.gossipsub = GossipSub( + protocols=[GOSSIPSUB_V12], + degree=3, + degree_low=2, + degree_high=4, + heartbeat_interval=5, + heartbeat_initial_delay=1.0, + score_params=score_params, + # v1.2 feature: IDONTWANT support + max_idontwant_messages=20, + # No adaptive features - v1.2 doesn't have adaptive gossip + # No advanced security features - v1.2 has basic security + ) + + self.pubsub = Pubsub(self.host, self.gossipsub) + + # Start services + import multiaddr + listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{self.port}")] + + async with self.host.run(listen_addrs=listen_addrs): + async with background_trio_service(self.pubsub): + async with background_trio_service(self.gossipsub): + await self.pubsub.wait_until_ready() + self.subscription = await self.pubsub.subscribe(TOPIC) + logger.info(f"Node {self.node_id} (Gossipsub 1.2, {self.role}) started on port {self.port}") + + # Keep running + await trio.sleep_forever() + + async def publish_message(self, message: str): + """Publish a message to the topic""" + if self.pubsub: + await self.pubsub.publish(TOPIC, message.encode()) + self.messages_sent += 1 + logger.info(f"Node {self.node_id} ({self.role}) published: {message}") + + async def receive_messages(self): + """Receive and process messages""" + if not self.subscription: + return + + try: + while True: + message = await self.subscription.get() + decoded = message.data.decode('utf-8') + self.messages_received += 1 + logger.info(f"Node {self.node_id} received: {decoded}") + except Exception as e: + logger.debug(f"Node {self.node_id} receive loop ended: {e}") + + async def connect_to_peer(self, peer_addr: str): + """Connect to another peer""" + if self.host: + try: + from libp2p.peer.peerinfo import info_from_p2p_addr + import multiaddr + + maddr = multiaddr.Multiaddr(peer_addr) + info = info_from_p2p_addr(maddr) + await self.host.connect(info) + logger.debug(f"Node {self.node_id} connected to {peer_addr}") + except Exception as e: + logger.debug(f"Node {self.node_id} failed to connect to {peer_addr}: {e}") + + +class GossipsubV12Demo: + """Demo controller for Gossipsub 1.2""" + + def __init__(self): + self.nodes: List[GossipsubV12Node] = [] + + async def setup_network(self, node_count: int = 5): + """Set up a network of nodes""" + roles = ["honest"] * (node_count - 1) + ["malicious"] * 1 + + for i in range(node_count): + port = find_free_port() + role = roles[i] if i < len(roles) else "honest" + node = GossipsubV12Node(f"node_{i}", port, role) + self.nodes.append(node) + + logger.info(f"Created network with {node_count} nodes running Gossipsub 1.2") + + async def start_network(self, duration: int = 30): + """Start all nodes and run the demo""" + try: + async with trio.open_nursery() as nursery: + # Start all nodes + for node in self.nodes: + nursery.start_soon(node.start) + + # Wait for initialization + await trio.sleep(3) + + # Connect nodes in a mesh topology + await self._connect_nodes() + await trio.sleep(2) + + # Start message receiving for all nodes + for node in self.nodes: + nursery.start_soon(node.receive_messages) + + # Run publishing loop + end_time = time.time() + duration + message_counter = 0 + + print(f"\n{'='*60}") + print("GOSSIPSUB 1.2 DEMO") + print(f"{'='*60}") + print(f"Running for {duration} seconds...") + print(f"Protocol: /meshsub/1.2.0") + print(f"Features: IDONTWANT filtering, reduced bandwidth usage") + print(f"{'='*60}\n") + + while time.time() < end_time: + # Honest nodes publish normally + honest_nodes = [n for n in self.nodes if n.role == "honest"] + if honest_nodes: + node = random.choice(honest_nodes) + message = f"honest_msg_{message_counter}_{int(time.time())}" + await node.publish_message(message) + message_counter += 1 + + # Malicious nodes might send more messages (will be penalized) + malicious_nodes = [n for n in self.nodes if n.role == "malicious"] + if malicious_nodes and random.random() < 0.3: # 30% chance + node = malicious_nodes[0] + message = f"malicious_msg_{message_counter}_{int(time.time())}" + await node.publish_message(message) + message_counter += 1 + + await trio.sleep(2) # Publish every 2 seconds + + # Print statistics + await trio.sleep(1) # Wait for final messages + self._print_statistics() + + # Cancel all tasks to exit nursery + nursery.cancel_scope.cancel() + + except Exception as e: + logger.warning(f"Demo execution interrupted: {e}") + + async def _connect_nodes(self): + """Connect nodes in a mesh topology""" + for i, node in enumerate(self.nodes): + # Connect to the next node in a ring topology + if len(self.nodes) > 1: + target_idx = (i + 1) % len(self.nodes) + target = self.nodes[target_idx] + + if target.host and node.host: + peer_addr = f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + await node.connect_to_peer(peer_addr) + + # Also connect to one more node for better connectivity + if len(self.nodes) > 2: + target_idx2 = (i + 2) % len(self.nodes) + target2 = self.nodes[target_idx2] + + if target2.host and node.host: + peer_addr2 = f"/ip4/127.0.0.1/tcp/{target2.port}/p2p/{target2.host.get_id()}" + await node.connect_to_peer(peer_addr2) + + def _print_statistics(self): + """Print demo statistics""" + print(f"\n{'='*60}") + print("DEMO STATISTICS") + print(f"{'='*60}") + + total_sent = sum(node.messages_sent for node in self.nodes) + total_received = sum(node.messages_received for node in self.nodes) + + honest_sent = sum(n.messages_sent for n in self.nodes if n.role == "honest") + malicious_sent = sum(n.messages_sent for n in self.nodes if n.role == "malicious") + + print(f"Total messages sent: {total_sent}") + print(f" Honest nodes: {honest_sent}") + print(f" Malicious nodes: {malicious_sent}") + print(f"Total messages received: {total_received}") + print(f"\nPer-node statistics:") + for node in self.nodes: + print(f" {node.node_id} ({node.role}): sent={node.messages_sent}, received={node.messages_received}") + + print(f"\n{'='*60}") + print("Gossipsub 1.2 Features:") + print(" โœ“ All Gossipsub 1.1 features") + print(" โœ“ IDONTWANT message filtering") + print(" โœ“ Reduced bandwidth usage") + print(" โœ“ Improved efficiency in dense networks") + print(" โœ— No adaptive gossip") + print(" โœ— No advanced security features (P6, P7)") + print(f"{'='*60}\n") + + +async def main(): + parser = argparse.ArgumentParser(description="Gossipsub 1.2 Example") + parser.add_argument( + "--nodes", + type=int, + default=5, + help="Number of nodes in the network" + ) + parser.add_argument( + "--duration", + type=int, + default=30, + help="Demo duration in seconds" + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Enable verbose logging" + ) + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + demo = GossipsubV12Demo() + await demo.setup_network(args.nodes) + await demo.start_network(args.duration) + + +if __name__ == "__main__": + trio.run(main) diff --git a/examples/pubsub/gossipsub/gossipsub_v2.0.py b/examples/pubsub/gossipsub/gossipsub_v2.0.py new file mode 100755 index 000000000..116f74f6c --- /dev/null +++ b/examples/pubsub/gossipsub/gossipsub_v2.0.py @@ -0,0 +1,380 @@ +#!/usr/bin/env python3 +""" +Gossipsub 2.0 Example + +This example demonstrates Gossipsub 2.0 protocol (/meshsub/2.0.0). +Gossipsub 2.0 adds enhanced security, adaptive gossip, and advanced peer scoring +to provide the most robust and efficient pubsub protocol. + +Features demonstrated: +- All Gossipsub 1.2 features (peer scoring, IDONTWANT) +- Enhanced peer scoring with P6 (application-specific) and P7 (IP colocation) +- Adaptive gossip behavior based on network health +- Advanced security features: + - Spam protection with rate limiting + - Eclipse attack protection via IP diversity + - Equivocation detection + - Enhanced message validation + +Usage: + python gossipsub_v2.0.py --nodes 5 --duration 30 +""" + +import argparse +import logging +import random +import time +from typing import List + +import trio + +from libp2p import new_host +from libp2p.crypto.rsa import create_new_key_pair +from libp2p.custom_types import TProtocol +from libp2p.peer.id import ID +from libp2p.pubsub.gossipsub import GossipSub +from libp2p.pubsub.pubsub import Pubsub +from libp2p.pubsub.score import ScoreParams, TopicScoreParams +from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex +from libp2p.tools.async_service.trio_service import background_trio_service +from libp2p.utils.address_validation import find_free_port + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger("gossipsub-v2.0") + +# Protocol version +GOSSIPSUB_V20 = TProtocol("/meshsub/2.0.0") +TOPIC = "gossipsub-v2.0-demo" + + +class GossipsubV20Node: + """A node running Gossipsub 2.0""" + + def __init__(self, node_id: str, port: int, role: str = "honest"): + self.node_id = node_id + self.port = port + self.role = role # "honest", "spammer", "validator" + self.host = None + self.pubsub = None + self.gossipsub = None + self.subscription = None + self.messages_sent = 0 + self.messages_received = 0 + self.messages_validated = 0 + self.messages_rejected = 0 + + async def start(self): + """Start the node with Gossipsub 2.0 configuration""" + key_pair = create_new_key_pair() + + self.host = new_host( + key_pair=key_pair, + muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, + ) + + # Configure Gossipsub 2.0 - full feature set + score_params = ScoreParams( + # Topic-scoped parameters (P1-P4) + p1_time_in_mesh=TopicScoreParams(weight=0.1, cap=10.0, decay=0.99), + p2_first_message_deliveries=TopicScoreParams(weight=0.5, cap=20.0, decay=0.99), + p3_mesh_message_deliveries=TopicScoreParams(weight=0.3, cap=10.0, decay=0.99), + p4_invalid_messages=TopicScoreParams(weight=-1.0, cap=50.0, decay=0.99), + # Global behavioral penalty (P5) + p5_behavior_penalty_weight=1.0, + p5_behavior_penalty_decay=0.99, + # Application-specific score (P6) + p6_appl_slack_weight=0.1, + p6_appl_slack_decay=0.99, + # IP colocation penalty (P7) + p7_ip_colocation_weight=0.5, + p7_ip_colocation_threshold=3, + # Application-specific scoring function + app_specific_score_fn=self._application_score_function, + ) + + self.gossipsub = GossipSub( + protocols=[GOSSIPSUB_V20], + degree=4, + degree_low=2, + degree_high=6, + heartbeat_interval=5, + heartbeat_initial_delay=1.0, + score_params=score_params, + # v1.2 feature: IDONTWANT support + max_idontwant_messages=20, + # v2.0 adaptive features + adaptive_gossip_enabled=True, + # v2.0 security features + spam_protection_enabled=True, + max_messages_per_topic_per_second=5.0, + eclipse_protection_enabled=True, + min_mesh_diversity_ips=2, + ) + + self.pubsub = Pubsub(self.host, self.gossipsub) + + # Start services + import multiaddr + listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{self.port}")] + + async with self.host.run(listen_addrs=listen_addrs): + async with background_trio_service(self.pubsub): + async with background_trio_service(self.gossipsub): + await self.pubsub.wait_until_ready() + self.subscription = await self.pubsub.subscribe(TOPIC) + logger.info(f"Node {self.node_id} (Gossipsub 2.0, {self.role}) started on port {self.port}") + + # Keep running + await trio.sleep_forever() + + def _application_score_function(self, peer_id: ID) -> float: + """Custom application scoring function (P6)""" + # Example: Reward peers that have been connected longer + # In a real application, this could be based on stake, reputation, etc. + if self.gossipsub and peer_id in getattr(self.gossipsub, 'peers', {}): + # Simple time-based scoring for demo + return min(5.0, time.time() % 10) # Varies over time for demo + return 0.0 + + async def publish_message(self, message: str): + """Publish a message to the topic""" + if self.pubsub: + await self.pubsub.publish(TOPIC, message.encode()) + self.messages_sent += 1 + logger.info(f"Node {self.node_id} ({self.role}) published: {message}") + + async def receive_messages(self): + """Receive and process messages""" + if not self.subscription: + return + + try: + while True: + message = await self.subscription.get() + decoded = message.data.decode('utf-8') + self.messages_received += 1 + + # Simulate message validation + if self._validate_message(message): + self.messages_validated += 1 + logger.info(f"Node {self.node_id} received (valid): {decoded}") + else: + self.messages_rejected += 1 + logger.warning(f"Node {self.node_id} received (invalid): {decoded}") + except Exception as e: + logger.debug(f"Node {self.node_id} receive loop ended: {e}") + + def _validate_message(self, message) -> bool: + """Simple message validation""" + try: + decoded = message.data.decode('utf-8') + # Basic validation: message should have expected format + return '_msg_' in decoded and len(decoded) < 1000 + except: + return False + + async def connect_to_peer(self, peer_addr: str): + """Connect to another peer""" + if self.host: + try: + from libp2p.peer.peerinfo import info_from_p2p_addr + import multiaddr + + maddr = multiaddr.Multiaddr(peer_addr) + info = info_from_p2p_addr(maddr) + await self.host.connect(info) + logger.debug(f"Node {self.node_id} connected to {peer_addr}") + except Exception as e: + logger.debug(f"Node {self.node_id} failed to connect to {peer_addr}: {e}") + + +class GossipsubV20Demo: + """Demo controller for Gossipsub 2.0""" + + def __init__(self): + self.nodes: List[GossipsubV20Node] = [] + + async def setup_network(self, node_count: int = 5): + """Set up a network of nodes with different roles""" + # Mix of honest, spammer, and validator nodes + roles = ["honest"] * (node_count - 2) + ["spammer"] * 1 + ["validator"] * 1 + + for i in range(node_count): + port = find_free_port() + role = roles[i] if i < len(roles) else "honest" + node = GossipsubV20Node(f"node_{i}", port, role) + self.nodes.append(node) + + logger.info(f"Created network with {node_count} nodes running Gossipsub 2.0") + + async def start_network(self, duration: int = 30): + """Start all nodes and run the demo""" + try: + async with trio.open_nursery() as nursery: + # Start all nodes + for node in self.nodes: + nursery.start_soon(node.start) + + # Wait for initialization + await trio.sleep(3) + + # Connect nodes in a mesh topology + await self._connect_nodes() + await trio.sleep(2) + + # Start message receiving for all nodes + for node in self.nodes: + nursery.start_soon(node.receive_messages) + + # Run publishing loop + end_time = time.time() + duration + message_counter = 0 + + print(f"\n{'='*60}") + print("GOSSIPSUB 2.0 DEMO") + print(f"{'='*60}") + print(f"Running for {duration} seconds...") + print(f"Protocol: /meshsub/2.0.0") + print(f"Features: Adaptive gossip, advanced security, P6/P7 scoring") + print(f"{'='*60}\n") + + while time.time() < end_time: + # Honest nodes publish normally + honest_nodes = [n for n in self.nodes if n.role == "honest"] + if honest_nodes: + node = random.choice(honest_nodes) + message = f"honest_msg_{message_counter}_{int(time.time())}" + await node.publish_message(message) + message_counter += 1 + + # Validator nodes publish less frequently but with high quality + validator_nodes = [n for n in self.nodes if n.role == "validator"] + if validator_nodes and random.random() < 0.3: # 30% chance + node = validator_nodes[0] + message = f"validator_msg_{message_counter}_{int(time.time())}" + await node.publish_message(message) + message_counter += 1 + + # Spammer nodes try to send many messages (will be rate-limited) + spammer_nodes = [n for n in self.nodes if n.role == "spammer"] + if spammer_nodes and random.random() < 0.5: # 50% chance + node = spammer_nodes[0] + # Try to send multiple messages quickly + for _ in range(3): + message = f"spam_msg_{message_counter}_{int(time.time())}" + await node.publish_message(message) + message_counter += 1 + await trio.sleep(0.1) # Small delay between spam messages + + await trio.sleep(2) # Publish every 2 seconds + + # Print statistics + await trio.sleep(1) # Wait for final messages + self._print_statistics() + + # Cancel all tasks to exit nursery + nursery.cancel_scope.cancel() + + except Exception as e: + logger.warning(f"Demo execution interrupted: {e}") + + async def _connect_nodes(self): + """Connect nodes in a mesh topology""" + for i, node in enumerate(self.nodes): + # Connect to the next node in a ring topology + if len(self.nodes) > 1: + target_idx = (i + 1) % len(self.nodes) + target = self.nodes[target_idx] + + if target.host and node.host: + peer_addr = f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + await node.connect_to_peer(peer_addr) + + # Also connect to one more node for better connectivity + if len(self.nodes) > 2: + target_idx2 = (i + 2) % len(self.nodes) + target2 = self.nodes[target_idx2] + + if target2.host and node.host: + peer_addr2 = f"/ip4/127.0.0.1/tcp/{target2.port}/p2p/{target2.host.get_id()}" + await node.connect_to_peer(peer_addr2) + + def _print_statistics(self): + """Print demo statistics""" + print(f"\n{'='*60}") + print("DEMO STATISTICS") + print(f"{'='*60}") + + total_sent = sum(node.messages_sent for node in self.nodes) + total_received = sum(node.messages_received for node in self.nodes) + total_validated = sum(node.messages_validated for node in self.nodes) + total_rejected = sum(node.messages_rejected for node in self.nodes) + + honest_sent = sum(n.messages_sent for n in self.nodes if n.role == "honest") + spammer_sent = sum(n.messages_sent for n in self.nodes if n.role == "spammer") + validator_sent = sum(n.messages_sent for n in self.nodes if n.role == "validator") + + print(f"Total messages sent: {total_sent}") + print(f" Honest nodes: {honest_sent}") + print(f" Spammer nodes: {spammer_sent}") + print(f" Validator nodes: {validator_sent}") + print(f"Total messages received: {total_received}") + print(f"Messages validated: {total_validated}") + print(f"Messages rejected: {total_rejected}") + print(f"\nPer-node statistics:") + for node in self.nodes: + print(f" {node.node_id} ({node.role}): " + f"sent={node.messages_sent}, received={node.messages_received}, " + f"validated={node.messages_validated}, rejected={node.messages_rejected}") + + print(f"\n{'='*60}") + print("Gossipsub 2.0 Features:") + print(" โœ“ All Gossipsub 1.2 features") + print(" โœ“ Enhanced peer scoring:") + print(" - P6: Application-specific score") + print(" - P7: IP colocation penalty") + print(" โœ“ Adaptive gossip behavior") + print(" โœ“ Advanced security features:") + print(" - Spam protection with rate limiting") + print(" - Eclipse attack protection") + print(" - Equivocation detection") + print(" - Enhanced message validation") + print(f"{'='*60}\n") + + +async def main(): + parser = argparse.ArgumentParser(description="Gossipsub 2.0 Example") + parser.add_argument( + "--nodes", + type=int, + default=5, + help="Number of nodes in the network" + ) + parser.add_argument( + "--duration", + type=int, + default=30, + help="Demo duration in seconds" + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Enable verbose logging" + ) + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + demo = GossipsubV20Demo() + await demo.setup_network(args.nodes) + await demo.start_network(args.duration) + + +if __name__ == "__main__": + trio.run(main) diff --git a/examples/pubsub/gossipsub/run_examples.py b/examples/pubsub/gossipsub/run_examples.py deleted file mode 100755 index 42d777281..000000000 --- a/examples/pubsub/gossipsub/run_examples.py +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/env python3 -""" -Gossipsub Examples Runner - -Convenient script to run different Gossipsub examples with predefined configurations. - -Usage: - python run_examples.py --help - python run_examples.py quick-comparison - python run_examples.py full-comparison - python run_examples.py v2-demo - python run_examples.py interactive -""" - -import argparse -import asyncio -import json -import subprocess -import sys -import time -from pathlib import Path - - -def run_command(cmd: list, description: str): - """Run a command and handle output""" - print(f"\n{'='*60}") - print(f"Running: {description}") - print(f"Command: {' '.join(cmd)}") - print(f"{'='*60}") - - try: - result = subprocess.run(cmd, check=True, capture_output=False) - print(f"\nโœ… {description} completed successfully") - return True - except subprocess.CalledProcessError as e: - print(f"\nโŒ {description} failed with exit code {e.returncode}") - return False - except KeyboardInterrupt: - print(f"\nโน๏ธ {description} interrupted by user") - return False - - -def quick_comparison(): - """Run a quick comparison of all Gossipsub versions""" - print("๐Ÿš€ Starting Quick Gossipsub Version Comparison") - print("This will run a 30-second comparison across all scenarios...") - - scenarios = ["normal", "high_churn", "spam_attack", "network_partition"] - results = [] - - for scenario in scenarios: - cmd = [ - sys.executable, "version_comparison.py", - "--scenario", scenario, - "--duration", "30", - "--nodes", "4", - "--output", f"quick_{scenario}_results.json" - ] - - success = run_command(cmd, f"Quick {scenario} scenario") - results.append((scenario, success)) - - if not success: - print(f"โš ๏ธ Skipping remaining scenarios due to failure") - break - - # Summary - print(f"\n{'='*60}") - print("QUICK COMPARISON SUMMARY") - print(f"{'='*60}") - for scenario, success in results: - status = "โœ… PASSED" if success else "โŒ FAILED" - print(f"{scenario:<20} {status}") - - successful = sum(1 for _, success in results if success) - print(f"\nCompleted {successful}/{len(scenarios)} scenarios successfully") - - -def full_comparison(): - """Run a comprehensive comparison with longer duration""" - print("๐Ÿ”ฌ Starting Full Gossipsub Version Comparison") - print("This will run extensive tests with longer duration...") - - scenarios = [ - ("normal", 60, 6), - ("high_churn", 90, 8), - ("spam_attack", 120, 8), - ("network_partition", 180, 6), - ] - - results = [] - - for scenario, duration, nodes in scenarios: - cmd = [ - sys.executable, "version_comparison.py", - "--scenario", scenario, - "--duration", str(duration), - "--nodes", str(nodes), - "--output", f"full_{scenario}_results.json", - "--verbose" - ] - - success = run_command(cmd, f"Full {scenario} scenario ({duration}s, {nodes} nodes)") - results.append((scenario, success)) - - # Generate combined report - if any(success for _, success in results): - generate_comparison_report(results) - - # Summary - print(f"\n{'='*60}") - print("FULL COMPARISON SUMMARY") - print(f"{'='*60}") - for scenario, success in results: - status = "โœ… PASSED" if success else "โŒ FAILED" - print(f"{scenario:<20} {status}") - - -def v2_demo(): - """Run Gossipsub 2.0 feature demonstrations""" - print("๐ŸŽฏ Starting Gossipsub 2.0 Feature Demonstrations") - - features = [ - ("scoring", 60, "Peer Scoring (P1-P7)"), - ("adaptive", 45, "Adaptive Gossip"), - ("security", 90, "Security Features"), - ] - - results = [] - - for feature, duration, description in features: - cmd = [ - sys.executable, "v2_showcase.py", - "--mode", "demo", - "--feature", feature, - "--duration", str(duration), - "--nodes", "8", - "--output", f"v2_{feature}_monitoring.json" - ] - - success = run_command(cmd, f"{description} Demo ({duration}s)") - results.append((feature, success)) - - # Summary - print(f"\n{'='*60}") - print("GOSSIPSUB 2.0 DEMO SUMMARY") - print(f"{'='*60}") - for feature, success in results: - status = "โœ… PASSED" if success else "โŒ FAILED" - print(f"{feature:<15} {status}") - - -def interactive_mode(): - """Run interactive showcase""" - print("๐ŸŽฎ Starting Interactive Gossipsub 2.0 Showcase") - print("This will start an interactive session where you can explore features...") - - cmd = [ - sys.executable, "v2_showcase.py", - "--mode", "interactive", - "--nodes", "6", - "--verbose" - ] - - run_command(cmd, "Interactive Gossipsub 2.0 Showcase") - - -def generate_comparison_report(results): - """Generate a combined comparison report""" - print("\n๐Ÿ“Š Generating Combined Comparison Report...") - - try: - combined_data = { - "timestamp": time.time(), - "scenarios": {} - } - - for scenario, success in results: - if success: - filename = f"full_{scenario}_results.json" - if Path(filename).exists(): - with open(filename, 'r') as f: - data = json.load(f) - combined_data["scenarios"][scenario] = data - - # Save combined report - with open("combined_comparison_report.json", 'w') as f: - json.dump(combined_data, f, indent=2) - - print("โœ… Combined report saved to: combined_comparison_report.json") - - # Generate summary statistics - generate_summary_stats(combined_data) - - except Exception as e: - print(f"โš ๏ธ Failed to generate combined report: {e}") - - -def generate_summary_stats(data): - """Generate and display summary statistics""" - print(f"\n{'='*60}") - print("SUMMARY STATISTICS") - print(f"{'='*60}") - - try: - for scenario, scenario_data in data["scenarios"].items(): - print(f"\n{scenario.upper()} SCENARIO:") - print("-" * 40) - - metrics = scenario_data.get("metrics", {}) - - # Find best performing version for each metric - versions = list(metrics.keys()) - if versions: - # Delivery rate comparison - delivery_rates = {v: metrics[v].get("message_delivery_rate", 0) - for v in versions} - best_delivery = max(delivery_rates.items(), key=lambda x: x[1]) - print(f"Best Delivery Rate: {best_delivery[0]} ({best_delivery[1]:.1%})") - - # Latency comparison - latencies = {v: metrics[v].get("average_latency_ms", float('inf')) - for v in versions} - best_latency = min(latencies.items(), key=lambda x: x[1]) - print(f"Lowest Latency: {best_latency[0]} ({best_latency[1]:.1f}ms)") - - # Show all versions' performance - print("\nAll Versions Performance:") - for version in versions: - m = metrics[version] - print(f" {version}: " - f"Delivery={m.get('message_delivery_rate', 0):.1%}, " - f"Latency={m.get('average_latency_ms', 0):.1f}ms") - - except Exception as e: - print(f"โš ๏ธ Error generating summary stats: {e}") - - -def cleanup_files(): - """Clean up generated files""" - print("๐Ÿงน Cleaning up generated files...") - - patterns = [ - "quick_*.json", - "full_*.json", - "v2_*.json", - "combined_*.json" - ] - - import glob - - cleaned = 0 - for pattern in patterns: - for file in glob.glob(pattern): - try: - Path(file).unlink() - cleaned += 1 - print(f" Removed: {file}") - except Exception as e: - print(f" Failed to remove {file}: {e}") - - print(f"โœ… Cleaned up {cleaned} files") - - -def main(): - parser = argparse.ArgumentParser( - description="Gossipsub Examples Runner", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - python run_examples.py quick-comparison # Quick 30s comparison - python run_examples.py full-comparison # Comprehensive comparison - python run_examples.py v2-demo # Gossipsub 2.0 features - python run_examples.py interactive # Interactive exploration - python run_examples.py cleanup # Clean up generated files - """ - ) - - parser.add_argument( - "command", - choices=[ - "quick-comparison", "full-comparison", - "v2-demo", "interactive", "cleanup" - ], - help="Command to run" - ) - - args = parser.parse_args() - - print(f"๐ŸŽฏ Gossipsub Examples Runner") - print(f"Command: {args.command}") - print(f"Time: {time.strftime('%Y-%m-%d %H:%M:%S')}") - - if args.command == "quick-comparison": - quick_comparison() - elif args.command == "full-comparison": - full_comparison() - elif args.command == "v2-demo": - v2_demo() - elif args.command == "interactive": - interactive_mode() - elif args.command == "cleanup": - cleanup_files() - - print(f"\n๐Ÿ Command '{args.command}' completed!") - print(f"Time: {time.strftime('%Y-%m-%d %H:%M:%S')}") - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/examples/pubsub/gossipsub/v2_showcase.py b/examples/pubsub/gossipsub/v2_showcase.py deleted file mode 100755 index cea7a8a57..000000000 --- a/examples/pubsub/gossipsub/v2_showcase.py +++ /dev/null @@ -1,765 +0,0 @@ -#!/usr/bin/env python3 -""" -Gossipsub 2.0 Feature Showcase - -Interactive demonstration of Gossipsub 2.0's advanced features including: -- Real-time peer scoring visualization (P1-P7 parameters) -- Adaptive gossip behavior based on network health -- Security features (spam protection, eclipse attack protection, equivocation detection) -- Message validation and caching -- IP colocation penalties - -Usage: - python v2_showcase.py --mode interactive - python v2_showcase.py --mode demo --feature scoring - python v2_showcase.py --mode demo --feature adaptive - python v2_showcase.py --mode demo --feature security -""" - -import argparse -import asyncio -import json -import logging -import random -import statistics -import time -from collections import defaultdict -from dataclasses import asdict, dataclass -from typing import Any, Dict, List, Optional, Set - -import trio - -from libp2p import new_host -from libp2p.crypto.rsa import create_new_key_pair -from libp2p.custom_types import TProtocol -from libp2p.peer.id import ID -from libp2p.pubsub.gossipsub import GossipSub -from libp2p.pubsub.pubsub import Pubsub -from libp2p.pubsub.score import ScoreParams, TopicScoreParams -from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex -from libp2p.tools.async_service.trio_service import background_trio_service -from libp2p.utils.address_validation import find_free_port - -# Configure logging -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -) -logger = logging.getLogger("gossipsub-v2-showcase") - -GOSSIPSUB_V20 = TProtocol("/meshsub/2.0.0") -TOPIC = "v2-showcase" - - -@dataclass -class PeerScoreSnapshot: - """Snapshot of a peer's score components""" - peer_id: str - timestamp: float - p1_time_in_mesh: float = 0.0 - p2_first_message_deliveries: float = 0.0 - p3_mesh_message_deliveries: float = 0.0 - p4_invalid_messages: float = 0.0 - p5_behavior_penalty: float = 0.0 - p6_application_score: float = 0.0 - p7_ip_colocation_penalty: float = 0.0 - total_score: float = 0.0 - - def to_dict(self) -> Dict[str, Any]: - return asdict(self) - - -@dataclass -class NetworkHealthSnapshot: - """Snapshot of network health metrics""" - timestamp: float - health_score: float - mesh_connectivity: float - peer_score_distribution: Dict[str, int] # score_range -> count - adaptive_degree_low: int - adaptive_degree_high: int - gossip_factor: float - - def to_dict(self) -> Dict[str, Any]: - return asdict(self) - - -@dataclass -class SecurityEvent: - """Security-related event in the network""" - timestamp: float - event_type: str # "spam_detected", "eclipse_attempt", "equivocation", "rate_limit" - peer_id: str - details: Dict[str, Any] - - def to_dict(self) -> Dict[str, Any]: - return asdict(self) - - -class ShowcaseNode: - """Enhanced node for showcasing Gossipsub 2.0 features""" - - def __init__(self, node_id: str, port: int, role: str = "honest"): - self.node_id = node_id - self.port = port - self.role = role # "honest", "spammer", "eclipse_attacker", "validator" - - self.host = None - self.pubsub = None - self.gossipsub = None - self.subscription = None - - # Monitoring data - self.score_history: List[PeerScoreSnapshot] = [] - self.security_events: List[SecurityEvent] = [] - self.messages_sent = 0 - self.messages_received = 0 - self.messages_validated = 0 - self.messages_rejected = 0 - - # Behavioral parameters - self.message_rate = 1.0 # messages per second - self.spam_burst_probability = 0.0 - self.invalid_message_probability = 0.0 - - async def start(self): - """Start the node with Gossipsub 2.0 configuration""" - key_pair = create_new_key_pair() - - self.host = new_host( - key_pair=key_pair, - muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, - ) - - # Configure advanced Gossipsub 2.0 features - score_params = self._get_score_params() - - self.gossipsub = GossipSub( - protocols=[GOSSIPSUB_V20], - degree=4, - degree_low=2, - degree_high=6, - heartbeat_interval=5, - heartbeat_initial_delay=1.0, - score_params=score_params, - - # v1.2 features - max_idontwant_messages=50, - - # v2.0 adaptive features - adaptive_gossip_enabled=True, - - # Security features - spam_protection_enabled=True, - max_messages_per_topic_per_second=5.0, - eclipse_protection_enabled=True, - min_mesh_diversity_ips=2, - ) - - self.pubsub = Pubsub(self.host, self.gossipsub) - - # Start services - import multiaddr - listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{self.port}")] - - async with self.host.run(listen_addrs=listen_addrs): - async with background_trio_service(self.pubsub): - async with background_trio_service(self.gossipsub): - await self.pubsub.wait_until_ready() - self.subscription = await self.pubsub.subscribe(TOPIC) - logger.info(f"Node {self.node_id} ({self.role}) started on port {self.port}") - - # Keep running - await trio.sleep_forever() - - def _get_score_params(self) -> ScoreParams: - """Get scoring parameters optimized for demonstration""" - return ScoreParams( - # Topic-scoped parameters (P1-P4) - p1_time_in_mesh=TopicScoreParams(weight=0.2, cap=10.0, decay=0.99), - p2_first_message_deliveries=TopicScoreParams(weight=1.0, cap=20.0, decay=0.99), - p3_mesh_message_deliveries=TopicScoreParams(weight=0.5, cap=10.0, decay=0.99), - p4_invalid_messages=TopicScoreParams(weight=-2.0, cap=50.0, decay=0.99), - - # Global parameters (P5-P7) - p5_behavior_penalty_weight=2.0, - p5_behavior_penalty_decay=0.98, - p5_behavior_penalty_threshold=1.0, - - p6_appl_slack_weight=0.3, - p6_appl_slack_decay=0.99, - - p7_ip_colocation_weight=1.0, - p7_ip_colocation_threshold=3, - - # Acceptance thresholds - publish_threshold=0.0, - gossip_threshold=-2.0, - graylist_threshold=-10.0, - accept_px_threshold=1.0, - - # Application-specific scoring function - app_specific_score_fn=self._application_score_function, - ) - - def _application_score_function(self, peer_id: ID) -> float: - """Custom application scoring function""" - # Example: Reward peers that have been connected longer - # In a real application, this could be based on stake, reputation, etc. - if self.gossipsub and peer_id in self.gossipsub.peers: - # Simple time-based scoring - return min(5.0, time.time() % 10) # Varies over time for demo - return 0.0 - - async def publish_behavior_loop(self): - """Publishing behavior based on node role""" - while True: - try: - if self.role == "honest": - await self._honest_publishing() - elif self.role == "spammer": - await self._spam_publishing() - elif self.role == "eclipse_attacker": - await self._eclipse_attack_behavior() - elif self.role == "validator": - await self._validator_behavior() - - await trio.sleep(1.0 / self.message_rate) - - except Exception as e: - logger.debug(f"Node {self.node_id} publish loop error: {e}") - await trio.sleep(1) - - async def _honest_publishing(self): - """Normal honest publishing behavior""" - if self.pubsub: - message = f"honest_msg_{self.messages_sent}_{int(time.time())}" - await self.pubsub.publish(TOPIC, message.encode()) - self.messages_sent += 1 - - async def _spam_publishing(self): - """Spam publishing behavior""" - if self.pubsub: - # Occasional spam bursts - if random.random() < self.spam_burst_probability: - # Send burst of messages - for i in range(10): - message = f"spam_msg_{self.messages_sent}_{i}_{int(time.time())}" - await self.pubsub.publish(TOPIC, message.encode()) - self.messages_sent += 1 - await trio.sleep(0.1) - else: - # Normal rate - message = f"normal_msg_{self.messages_sent}_{int(time.time())}" - await self.pubsub.publish(TOPIC, message.encode()) - self.messages_sent += 1 - - async def _eclipse_attack_behavior(self): - """Eclipse attack behavior (connecting from same IP)""" - # This would be simulated by having multiple nodes from same IP - # For demo purposes, just publish normally but with different behavior - if self.pubsub: - message = f"eclipse_msg_{self.messages_sent}_{int(time.time())}" - await self.pubsub.publish(TOPIC, message.encode()) - self.messages_sent += 1 - - async def _validator_behavior(self): - """Validator node behavior with strict validation""" - if self.pubsub: - # Validators might publish less frequently but with high quality - if random.random() < 0.5: # 50% chance to publish - message = f"validator_msg_{self.messages_sent}_{int(time.time())}" - await self.pubsub.publish(TOPIC, message.encode()) - self.messages_sent += 1 - - async def receive_messages(self): - """Receive and process messages""" - if not self.subscription: - return - - try: - while True: - message = await self.subscription.get() - self.messages_received += 1 - - # Simulate message validation - if self._validate_message(message): - self.messages_validated += 1 - else: - self.messages_rejected += 1 - # Record security event - event = SecurityEvent( - timestamp=time.time(), - event_type="invalid_message", - peer_id=str(message.from_id), - details={"reason": "validation_failed"} - ) - self.security_events.append(event) - - except Exception as e: - logger.debug(f"Node {self.node_id} receive loop ended: {e}") - - def _validate_message(self, message) -> bool: - """Simple message validation""" - try: - decoded = message.data.decode('utf-8') - # Basic validation: message should have expected format - return '_msg_' in decoded and len(decoded) < 1000 - except: - return False - - async def connect_to_peer(self, peer_addr: str): - """Connect to another peer""" - if self.host: - try: - from libp2p.peer.peerinfo import info_from_p2p_addr - import multiaddr - - maddr = multiaddr.Multiaddr(peer_addr) - info = info_from_p2p_addr(maddr) - await self.host.connect(info) - logger.debug(f"Node {self.node_id} connected to {peer_addr}") - except Exception as e: - logger.debug(f"Node {self.node_id} failed to connect to {peer_addr}: {e}") - - def capture_score_snapshot(self) -> Optional[PeerScoreSnapshot]: - """Capture current peer score snapshot""" - if not self.gossipsub or not self.gossipsub.scorer: - return None - - # For demo, we'll create a mock snapshot since accessing internal scorer state - # would require more complex integration - snapshot = PeerScoreSnapshot( - peer_id=self.node_id, - timestamp=time.time(), - p1_time_in_mesh=random.uniform(0, 10), - p2_first_message_deliveries=random.uniform(0, 20), - p3_mesh_message_deliveries=random.uniform(0, 10), - p4_invalid_messages=random.uniform(-5, 0), - p5_behavior_penalty=random.uniform(-2, 0), - p6_application_score=random.uniform(0, 5), - p7_ip_colocation_penalty=random.uniform(-3, 0), - ) - - # Calculate total score - snapshot.total_score = ( - snapshot.p1_time_in_mesh + - snapshot.p2_first_message_deliveries + - snapshot.p3_mesh_message_deliveries + - snapshot.p4_invalid_messages + - snapshot.p5_behavior_penalty + - snapshot.p6_application_score + - snapshot.p7_ip_colocation_penalty - ) - - self.score_history.append(snapshot) - return snapshot - - def get_network_health(self) -> Optional[NetworkHealthSnapshot]: - """Get current network health snapshot""" - if not self.gossipsub: - return None - - # Mock network health data for demo - return NetworkHealthSnapshot( - timestamp=time.time(), - health_score=getattr(self.gossipsub, 'network_health_score', 0.8), - mesh_connectivity=random.uniform(0.7, 1.0), - peer_score_distribution={ - "excellent (>10)": random.randint(0, 5), - "good (5-10)": random.randint(2, 8), - "average (0-5)": random.randint(3, 10), - "poor (-5-0)": random.randint(0, 3), - "bad (<-5)": random.randint(0, 2), - }, - adaptive_degree_low=getattr(self.gossipsub, 'adaptive_degree_low', 2), - adaptive_degree_high=getattr(self.gossipsub, 'adaptive_degree_high', 6), - gossip_factor=getattr(self.gossipsub, 'gossip_factor', 0.25), - ) - - -class V2Showcase: - """Main showcase controller""" - - def __init__(self): - self.nodes: List[ShowcaseNode] = [] - self.monitoring_data = { - "scores": [], - "health": [], - "security_events": [] - } - - async def setup_network(self, node_count: int = 8): - """Set up a network of nodes with different roles""" - roles = ["honest"] * 4 + ["spammer"] * 2 + ["eclipse_attacker"] * 1 + ["validator"] * 1 - - for i in range(node_count): - port = find_free_port() - role = roles[i % len(roles)] - node = ShowcaseNode(f"node_{i}", port, role) - - # Configure behavioral parameters based on role - if role == "spammer": - node.message_rate = 3.0 - node.spam_burst_probability = 0.3 - elif role == "eclipse_attacker": - node.message_rate = 2.0 - elif role == "validator": - node.message_rate = 0.5 - - self.nodes.append(node) - - logger.info(f"Created network with {node_count} nodes") - - async def start_network(self): - """Start all nodes and connect them""" - try: - async with trio.open_nursery() as nursery: - # Start all nodes - for node in self.nodes: - nursery.start_soon(node.start) - - # Wait for initialization - await trio.sleep(3) - - # Connect nodes in a mesh topology - await self._connect_nodes() - await trio.sleep(2) - - # Start publishing and receiving loops - for node in self.nodes: - nursery.start_soon(node.publish_behavior_loop) - nursery.start_soon(node.receive_messages) - - # Start monitoring - nursery.start_soon(self._monitoring_loop) - - # Keep running until cancelled - await trio.sleep_forever() - - except Exception as e: - logger.warning(f"Network execution interrupted: {e}") - - async def _connect_nodes(self): - """Connect nodes in a mesh topology""" - for i, node in enumerate(self.nodes): - # Connect to other nodes in a ring topology for simplicity - if len(self.nodes) > 1: - target_idx = (i + 1) % len(self.nodes) - target = self.nodes[target_idx] - - if target.host and node.host: - peer_addr = f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" - await node.connect_to_peer(peer_addr) - - # Also connect to one more node for better connectivity - if len(self.nodes) > 2: - target_idx2 = (i + 2) % len(self.nodes) - target2 = self.nodes[target_idx2] - - if target2.host and node.host: - peer_addr2 = f"/ip4/127.0.0.1/tcp/{target2.port}/p2p/{target2.host.get_id()}" - await node.connect_to_peer(peer_addr2) - - async def _monitoring_loop(self): - """Continuous monitoring and data collection""" - while True: - try: - # Collect score snapshots - for node in self.nodes: - snapshot = node.capture_score_snapshot() - if snapshot: - self.monitoring_data["scores"].append(snapshot.to_dict()) - - # Collect network health from a representative node - if self.nodes: - health = self.nodes[0].get_network_health() - if health: - self.monitoring_data["health"].append(health.to_dict()) - - # Collect security events - for node in self.nodes: - for event in node.security_events: - self.monitoring_data["security_events"].append(event.to_dict()) - node.security_events.clear() # Clear after collecting - - await trio.sleep(5) # Monitor every 5 seconds - - except Exception as e: - logger.error(f"Monitoring loop error: {e}") - await trio.sleep(5) - - async def run_demo(self, feature: str, duration: int = 60): - """Run a specific feature demonstration""" - logger.info(f"Starting {feature} demonstration for {duration} seconds") - - if feature == "scoring": - await self._demo_peer_scoring(duration) - elif feature == "adaptive": - await self._demo_adaptive_gossip(duration) - elif feature == "security": - await self._demo_security_features(duration) - else: - logger.error(f"Unknown feature: {feature}") - - async def _demo_peer_scoring(self, duration: int): - """Demonstrate peer scoring features""" - print("\n" + "="*80) - print("PEER SCORING DEMONSTRATION") - print("="*80) - print("Monitoring peer scores (P1-P7 parameters) in real-time...") - print("Legend:") - print(" P1: Time in mesh") - print(" P2: First message deliveries") - print(" P3: Mesh message deliveries") - print(" P4: Invalid messages penalty") - print(" P5: Behavior penalty") - print(" P6: Application-specific score") - print(" P7: IP colocation penalty") - print("-"*80) - - end_time = time.time() + duration - - while time.time() < end_time: - # Display current scores - print(f"\nTimestamp: {time.strftime('%H:%M:%S')}") - print(f"{'Node':<12} {'Role':<12} {'P1':<6} {'P2':<6} {'P3':<6} {'P4':<6} {'P5':<6} {'P6':<6} {'P7':<6} {'Total':<8}") - print("-"*80) - - for node in self.nodes[:6]: # Show first 6 nodes - snapshot = node.capture_score_snapshot() - if snapshot: - print(f"{node.node_id:<12} {node.role:<12} " - f"{snapshot.p1_time_in_mesh:>5.1f} " - f"{snapshot.p2_first_message_deliveries:>5.1f} " - f"{snapshot.p3_mesh_message_deliveries:>5.1f} " - f"{snapshot.p4_invalid_messages:>5.1f} " - f"{snapshot.p5_behavior_penalty:>5.1f} " - f"{snapshot.p6_application_score:>5.1f} " - f"{snapshot.p7_ip_colocation_penalty:>5.1f} " - f"{snapshot.total_score:>7.1f}") - - await trio.sleep(3) - - async def _demo_adaptive_gossip(self, duration: int): - """Demonstrate adaptive gossip features""" - print("\n" + "="*80) - print("ADAPTIVE GOSSIP DEMONSTRATION") - print("="*80) - print("Monitoring network health and adaptive parameter adjustments...") - print("-"*80) - - end_time = time.time() + duration - - while time.time() < end_time: - if self.nodes: - health = self.nodes[0].get_network_health() - if health: - print(f"\nTimestamp: {time.strftime('%H:%M:%S')}") - print(f"Network Health Score: {health.health_score:.2f}") - print(f"Mesh Connectivity: {health.mesh_connectivity:.2f}") - print(f"Adaptive Degree Range: {health.adaptive_degree_low}-{health.adaptive_degree_high}") - print(f"Gossip Factor: {health.gossip_factor:.3f}") - print("\nPeer Score Distribution:") - for score_range, count in health.peer_score_distribution.items(): - print(f" {score_range}: {count} peers") - - await trio.sleep(5) - - async def _demo_security_features(self, duration: int): - """Demonstrate security features""" - print("\n" + "="*80) - print("SECURITY FEATURES DEMONSTRATION") - print("="*80) - print("Monitoring spam protection, eclipse attack protection, and validation...") - print("-"*80) - - # Activate malicious behavior - for node in self.nodes: - if node.role == "spammer": - node.spam_burst_probability = 0.5 - node.message_rate = 5.0 - - end_time = time.time() + duration - security_events_shown = 0 - - while time.time() < end_time: - print(f"\nTimestamp: {time.strftime('%H:%M:%S')}") - - # Show message statistics - total_sent = sum(node.messages_sent for node in self.nodes) - total_received = sum(node.messages_received for node in self.nodes) - total_validated = sum(node.messages_validated for node in self.nodes) - total_rejected = sum(node.messages_rejected for node in self.nodes) - - print(f"Messages - Sent: {total_sent}, Received: {total_received}") - print(f"Validation - Accepted: {total_validated}, Rejected: {total_rejected}") - - # Show recent security events - all_events = [] - for node in self.nodes: - all_events.extend(node.security_events) - - new_events = all_events[security_events_shown:] - if new_events: - print("Recent Security Events:") - for event in new_events[-5:]: # Show last 5 events - print(f" {event.event_type} from {event.peer_id}: {event.details}") - security_events_shown = len(all_events) - - await trio.sleep(3) - - def save_monitoring_data(self, filename: str): - """Save collected monitoring data to file""" - with open(filename, 'w') as f: - json.dump(self.monitoring_data, f, indent=2) - print(f"Monitoring data saved to {filename}") - - -async def interactive_mode(): - """Interactive mode for exploring features""" - print("\n" + "="*80) - print("GOSSIPSUB 2.0 INTERACTIVE SHOWCASE") - print("="*80) - print("Available commands:") - print(" 1. scoring - Demonstrate peer scoring (P1-P7)") - print(" 2. adaptive - Show adaptive gossip behavior") - print(" 3. security - Display security features") - print(" 4. status - Show current network status") - print(" 5. quit - Exit the showcase") - print("="*80) - - showcase = V2Showcase() - await showcase.setup_network(6) - - # Start network in background - async with trio.open_nursery() as nursery: - nursery.start_soon(showcase.start_network) - - # Wait for network to initialize - await trio.sleep(5) - - # Interactive loop - while True: - try: - command = await trio.to_thread.run_sync( - lambda: input("\nEnter command (1-5): ").strip() - ) - - if command in ["1", "scoring"]: - await showcase._demo_peer_scoring(30) - elif command in ["2", "adaptive"]: - await showcase._demo_adaptive_gossip(30) - elif command in ["3", "security"]: - await showcase._demo_security_features(30) - elif command in ["4", "status"]: - await show_network_status(showcase) - elif command in ["5", "quit"]: - print("Exiting showcase...") - break - else: - print("Invalid command. Please enter 1-5.") - - except KeyboardInterrupt: - print("\nExiting showcase...") - break - - -async def show_network_status(showcase: V2Showcase): - """Show current network status""" - print("\n" + "="*50) - print("NETWORK STATUS") - print("="*50) - - print(f"Total Nodes: {len(showcase.nodes)}") - - role_counts = defaultdict(int) - for node in showcase.nodes: - role_counts[node.role] += 1 - - print("Node Roles:") - for role, count in role_counts.items(): - print(f" {role}: {count}") - - # Show message statistics - total_sent = sum(node.messages_sent for node in showcase.nodes) - total_received = sum(node.messages_received for node in showcase.nodes) - - print(f"\nMessage Statistics:") - print(f" Total Sent: {total_sent}") - print(f" Total Received: {total_received}") - - if showcase.nodes: - health = showcase.nodes[0].get_network_health() - if health: - print(f"\nNetwork Health: {health.health_score:.2f}") - - -async def main(): - parser = argparse.ArgumentParser(description="Gossipsub 2.0 Feature Showcase") - parser.add_argument( - "--mode", - choices=["interactive", "demo"], - default="interactive", - help="Run mode" - ) - parser.add_argument( - "--feature", - choices=["scoring", "adaptive", "security"], - help="Feature to demonstrate (demo mode only)" - ) - parser.add_argument( - "--duration", - type=int, - default=60, - help="Demo duration in seconds" - ) - parser.add_argument( - "--nodes", - type=int, - default=6, - help="Number of nodes in network" - ) - parser.add_argument( - "--output", - type=str, - help="Output file for monitoring data" - ) - parser.add_argument( - "--verbose", - action="store_true", - help="Enable verbose logging" - ) - - args = parser.parse_args() - - if args.verbose: - logging.getLogger().setLevel(logging.DEBUG) - - if args.mode == "interactive": - await interactive_mode() - else: - if not args.feature: - print("Error: --feature required for demo mode") - return - - showcase = V2Showcase() - await showcase.setup_network(args.nodes) - - async with trio.open_nursery() as nursery: - nursery.start_soon(showcase.start_network) - await trio.sleep(3) # Wait for network initialization - - # Run the demo within the nursery context with timeout - with trio.move_on_after(args.duration + 10): # Add buffer time - await showcase.run_demo(args.feature, args.duration) - - # Cancel all tasks to exit nursery - nursery.cancel_scope.cancel() - - if args.output: - showcase.save_monitoring_data(args.output) - - -if __name__ == "__main__": - trio.run(main) \ No newline at end of file diff --git a/examples/pubsub/gossipsub/version_comparison.py b/examples/pubsub/gossipsub/version_comparison.py deleted file mode 100755 index 2dfd6d2fd..000000000 --- a/examples/pubsub/gossipsub/version_comparison.py +++ /dev/null @@ -1,619 +0,0 @@ -#!/usr/bin/env python3 -""" -Gossipsub Version Comparison Demo - -This demo creates side-by-side networks running different Gossipsub protocol versions -to demonstrate the evolution and improvements across versions: - -- Gossipsub 1.0 (/meshsub/1.0.0): Basic mesh-based pubsub -- Gossipsub 1.1 (/meshsub/1.1.0): Added peer scoring and behavioral penalties -- Gossipsub 1.2 (/meshsub/1.2.0): Added IDONTWANT message filtering -- Gossipsub 2.0 (/meshsub/2.0.0): Enhanced security, adaptive gossip, and advanced peer scoring - -Usage: - python version_comparison.py --scenario normal - python version_comparison.py --scenario high_churn - python version_comparison.py --scenario spam_attack - python version_comparison.py --scenario network_partition -""" - -import argparse -import asyncio -import json -import logging -import random -import statistics -import time -from collections import defaultdict -from dataclasses import asdict, dataclass -from typing import Any, Dict, List, Optional - -import trio - -from libp2p import new_host -from libp2p.crypto.rsa import create_new_key_pair -from libp2p.custom_types import TProtocol -from libp2p.peer.id import ID -from libp2p.pubsub.gossipsub import GossipSub -from libp2p.pubsub.pubsub import Pubsub -from libp2p.pubsub.score import ScoreParams, TopicScoreParams -from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex -from libp2p.tools.async_service.trio_service import background_trio_service -from libp2p.utils.address_validation import find_free_port - -# Configure logging -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -) -logger = logging.getLogger("gossipsub-comparison") - -# Protocol versions -GOSSIPSUB_V10 = TProtocol("/meshsub/1.0.0") -GOSSIPSUB_V11 = TProtocol("/meshsub/1.1.0") -GOSSIPSUB_V12 = TProtocol("/meshsub/1.2.0") -GOSSIPSUB_V20 = TProtocol("/meshsub/2.0.0") - -TOPIC = "comparison-test" - - -@dataclass -class NetworkMetrics: - """Metrics collected from a network during testing""" - version: str - total_messages_sent: int = 0 - total_messages_received: int = 0 - message_delivery_rate: float = 0.0 - average_latency_ms: float = 0.0 - network_overhead_bytes: int = 0 - peer_churn_events: int = 0 - spam_messages_blocked: int = 0 - partition_recovery_time_ms: float = 0.0 - - def to_dict(self) -> Dict[str, Any]: - return asdict(self) - - -@dataclass -class ComparisonResult: - """Results from comparing different protocol versions""" - scenario: str - duration_seconds: float - metrics_by_version: Dict[str, NetworkMetrics] - - def to_dict(self) -> Dict[str, Any]: - return { - "scenario": self.scenario, - "duration_seconds": self.duration_seconds, - "metrics": {v: m.to_dict() for v, m in self.metrics_by_version.items()} - } - - -class NetworkNode: - """Represents a single node in the test network""" - - def __init__(self, node_id: str, protocol_version: TProtocol, port: int): - self.node_id = node_id - self.protocol_version = protocol_version - self.port = port - self.host = None - self.pubsub = None - self.gossipsub = None - self.subscription = None - - # Metrics tracking - self.messages_sent = 0 - self.messages_received = 0 - self.message_timestamps = {} # msg_id -> send_time - self.latencies = [] - self.is_malicious = False - - async def start(self): - """Start the node and initialize pubsub""" - key_pair = create_new_key_pair() - - self.host = new_host( - key_pair=key_pair, - muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, - ) - - # Configure gossipsub based on protocol version - gossipsub_config = self._get_gossipsub_config() - self.gossipsub = GossipSub(**gossipsub_config) - self.pubsub = Pubsub(self.host, self.gossipsub) - - # Start services - import multiaddr - listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{self.port}")] - - async with self.host.run(listen_addrs=listen_addrs): - async with background_trio_service(self.pubsub): - async with background_trio_service(self.gossipsub): - await self.pubsub.wait_until_ready() - self.subscription = await self.pubsub.subscribe(TOPIC) - logger.info(f"Node {self.node_id} ({self.protocol_version}) started on port {self.port}") - - # Keep running - await trio.sleep_forever() - - def _get_gossipsub_config(self) -> Dict[str, Any]: - """Get gossipsub configuration based on protocol version""" - base_config = { - "protocols": [self.protocol_version], - "degree": 3, - "degree_low": 2, - "degree_high": 4, - "heartbeat_interval": 5, - "heartbeat_initial_delay": 1.0, - } - - if self.protocol_version == GOSSIPSUB_V10: - # Basic configuration for v1.0 - return base_config - - elif self.protocol_version == GOSSIPSUB_V11: - # Add scoring for v1.1 - score_params = ScoreParams( - p1_time_in_mesh=TopicScoreParams(weight=0.1, cap=10.0, decay=0.99), - p2_first_message_deliveries=TopicScoreParams(weight=0.5, cap=20.0, decay=0.99), - p3_mesh_message_deliveries=TopicScoreParams(weight=0.3, cap=10.0, decay=0.99), - p4_invalid_messages=TopicScoreParams(weight=-1.0, cap=50.0, decay=0.99), - ) - base_config["score_params"] = score_params - return base_config - - elif self.protocol_version == GOSSIPSUB_V12: - # Add IDONTWANT support for v1.2 - score_params = ScoreParams( - p1_time_in_mesh=TopicScoreParams(weight=0.1, cap=10.0, decay=0.99), - p2_first_message_deliveries=TopicScoreParams(weight=0.5, cap=20.0, decay=0.99), - p3_mesh_message_deliveries=TopicScoreParams(weight=0.3, cap=10.0, decay=0.99), - p4_invalid_messages=TopicScoreParams(weight=-1.0, cap=50.0, decay=0.99), - ) - base_config.update({ - "score_params": score_params, - "max_idontwant_messages": 20, - }) - return base_config - - elif self.protocol_version == GOSSIPSUB_V20: - # Full v2.0 configuration with adaptive features and security - score_params = ScoreParams( - p1_time_in_mesh=TopicScoreParams(weight=0.1, cap=10.0, decay=0.99), - p2_first_message_deliveries=TopicScoreParams(weight=0.5, cap=20.0, decay=0.99), - p3_mesh_message_deliveries=TopicScoreParams(weight=0.3, cap=10.0, decay=0.99), - p4_invalid_messages=TopicScoreParams(weight=-1.0, cap=50.0, decay=0.99), - p5_behavior_penalty_weight=1.0, - p5_behavior_penalty_decay=0.99, - p6_appl_slack_weight=0.1, - p7_ip_colocation_weight=0.5, - publish_threshold=0.0, - gossip_threshold=-1.0, - graylist_threshold=-10.0, - ) - base_config.update({ - "score_params": score_params, - "max_idontwant_messages": 20, - "adaptive_gossip_enabled": True, - "spam_protection_enabled": True, - "max_messages_per_topic_per_second": 10.0, - "eclipse_protection_enabled": True, - "min_mesh_diversity_ips": 2, - }) - return base_config - - return base_config - - async def publish_message(self, message: str) -> str: - """Publish a message and return message ID for tracking""" - if self.pubsub: - msg_id = f"{self.node_id}_{self.messages_sent}_{int(time.time() * 1000)}" - full_message = f"{msg_id}:{message}" - - self.message_timestamps[msg_id] = time.time() - await self.pubsub.publish(TOPIC, full_message.encode()) - self.messages_sent += 1 - return msg_id - return "" - - async def receive_messages(self, metrics: NetworkMetrics): - """Receive and process messages, updating metrics""" - if not self.subscription: - return - - try: - while True: - message = await self.subscription.get() - decoded = message.data.decode('utf-8') - - # Parse message to extract ID and calculate latency - if ':' in decoded: - msg_id, content = decoded.split(':', 1) - - # Calculate latency if we sent this message - if msg_id in self.message_timestamps: - latency = (time.time() - self.message_timestamps[msg_id]) * 1000 - self.latencies.append(latency) - del self.message_timestamps[msg_id] - - self.messages_received += 1 - metrics.total_messages_received += 1 - - except Exception as e: - logger.debug(f"Node {self.node_id} receive loop ended: {e}") - - async def connect_to_peer(self, peer_addr: str): - """Connect to another peer""" - if self.host: - try: - from libp2p.peer.peerinfo import info_from_p2p_addr - import multiaddr - - maddr = multiaddr.Multiaddr(peer_addr) - info = info_from_p2p_addr(maddr) - await self.host.connect(info) - logger.debug(f"Node {self.node_id} connected to {peer_addr}") - except Exception as e: - logger.debug(f"Node {self.node_id} failed to connect to {peer_addr}: {e}") - - def get_metrics(self) -> Dict[str, Any]: - """Get current metrics for this node""" - avg_latency = statistics.mean(self.latencies) if self.latencies else 0.0 - return { - "messages_sent": self.messages_sent, - "messages_received": self.messages_received, - "average_latency_ms": avg_latency, - "pending_messages": len(self.message_timestamps) - } - - -class NetworkSimulator: - """Simulates different network scenarios for comparison testing""" - - def __init__(self): - self.networks = {} # version -> list of nodes - self.metrics = {} # version -> NetworkMetrics - - async def setup_networks(self, nodes_per_version: int = 5): - """Set up networks for each protocol version""" - versions = [ - ("v1.0", GOSSIPSUB_V10), - ("v1.1", GOSSIPSUB_V11), - ("v1.2", GOSSIPSUB_V12), - ("v2.0", GOSSIPSUB_V20), - ] - - for version_name, protocol in versions: - self.networks[version_name] = [] - self.metrics[version_name] = NetworkMetrics(version=version_name) - - # Create nodes for this version - for i in range(nodes_per_version): - port = find_free_port() - node_id = f"{version_name}_node_{i}" - node = NetworkNode(node_id, protocol, port) - self.networks[version_name].append(node) - - logger.info(f"Created {len(versions)} networks with {nodes_per_version} nodes each") - - async def run_scenario(self, scenario: str, duration: int = 60) -> ComparisonResult: - """Run a specific test scenario and collect metrics""" - logger.info(f"Starting scenario: {scenario} (duration: {duration}s)") - start_time = time.time() - - # Start all networks concurrently with timeout - try: - async with trio.open_nursery() as nursery: - # Start all nodes - for version, nodes in self.networks.items(): - for node in nodes: - nursery.start_soon(node.start) - - # Wait for nodes to initialize - await trio.sleep(3) - - # Connect nodes within each network - await self._connect_networks() - await trio.sleep(2) - - # Start message receiving for all nodes - for version, nodes in self.networks.items(): - for node in nodes: - nursery.start_soon(node.receive_messages, self.metrics[version]) - - # Run the specific scenario with timeout - with trio.move_on_after(duration + 10): # Add buffer time - if scenario == "normal": - await self._run_normal_scenario(duration) - elif scenario == "high_churn": - await self._run_high_churn_scenario(duration) - elif scenario == "spam_attack": - await self._run_spam_attack_scenario(duration) - elif scenario == "network_partition": - await self._run_network_partition_scenario(duration) - else: - logger.error(f"Unknown scenario: {scenario}") - return ComparisonResult(scenario, 0, {}) - - # Cancel all tasks to exit nursery - nursery.cancel_scope.cancel() - - except Exception as e: - logger.warning(f"Scenario execution interrupted: {e}") - - # Calculate final metrics - end_time = time.time() - duration_actual = end_time - start_time - - await self._calculate_final_metrics() - - return ComparisonResult( - scenario=scenario, - duration_seconds=duration_actual, - metrics_by_version=self.metrics - ) - - async def _connect_networks(self): - """Connect nodes within each network to form mesh topology""" - for version, nodes in self.networks.items(): - # Connect each node to other nodes in the same network - for i, node in enumerate(nodes): - # Connect to the next node in a ring topology for simplicity - if len(nodes) > 1: - target_idx = (i + 1) % len(nodes) - target = nodes[target_idx] - - if target.host and node.host: - peer_addr = f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" - await node.connect_to_peer(peer_addr) - - async def _run_normal_scenario(self, duration: int): - """Normal operation with honest peers""" - logger.info("Running normal scenario - honest peers publishing regularly") - - end_time = time.time() + duration - message_counter = 0 - - while time.time() < end_time: - # Each network publishes messages at regular intervals - for version, nodes in self.networks.items(): - # Random node publishes a message - node = random.choice(nodes) - message = f"normal_msg_{message_counter}" - await node.publish_message(message) - self.metrics[version].total_messages_sent += 1 - message_counter += 1 - - await trio.sleep(1) # Publish every second - - async def _run_high_churn_scenario(self, duration: int): - """High peer churn scenario""" - logger.info("Running high churn scenario - peers joining/leaving frequently") - - end_time = time.time() + duration - message_counter = 0 - - while time.time() < end_time: - # Normal message publishing - for version, nodes in self.networks.items(): - active_nodes = [n for n in nodes if n.host] - if active_nodes: - node = random.choice(active_nodes) - message = f"churn_msg_{message_counter}" - await node.publish_message(message) - self.metrics[version].total_messages_sent += 1 - self.metrics[version].peer_churn_events += 1 - message_counter += 1 - - await trio.sleep(0.5) - - async def _run_spam_attack_scenario(self, duration: int): - """Spam attack scenario with malicious peers""" - logger.info("Running spam attack scenario - some peers sending excessive messages") - - # Mark some nodes as malicious - for version, nodes in self.networks.items(): - malicious_count = max(1, len(nodes) // 3) # 1/3 of nodes are malicious - for i in range(malicious_count): - nodes[i].is_malicious = True - - end_time = time.time() + duration - message_counter = 0 - - while time.time() < end_time: - for version, nodes in self.networks.items(): - for node in nodes: - if node.is_malicious: - # Malicious nodes send many messages - for _ in range(5): - message = f"spam_msg_{message_counter}" - await node.publish_message(message) - message_counter += 1 - else: - # Honest nodes send normal messages - message = f"honest_msg_{message_counter}" - await node.publish_message(message) - self.metrics[version].total_messages_sent += 1 - message_counter += 1 - - await trio.sleep(0.2) # Faster publishing for spam scenario - - async def _run_network_partition_scenario(self, duration: int): - """Network partition and recovery scenario""" - logger.info("Running network partition scenario - network splits and recovers") - - partition_time = duration // 3 - recovery_time = time.time() + partition_time - end_time = time.time() + duration - message_counter = 0 - - # Phase 1: Normal operation - logger.info("Phase 1: Normal operation") - while time.time() < recovery_time: - for version, nodes in self.networks.items(): - node = random.choice(nodes) - message = f"pre_partition_msg_{message_counter}" - await node.publish_message(message) - self.metrics[version].total_messages_sent += 1 - message_counter += 1 - await trio.sleep(1) - - # Phase 2: Partition (simulate by reducing connectivity) - logger.info("Phase 2: Network partition") - partition_start = time.time() - - while time.time() < end_time: - # Reduced message publishing during partition - for version, nodes in self.networks.items(): - # Only half the nodes can communicate - active_nodes = nodes[:len(nodes)//2] - if active_nodes: - node = random.choice(active_nodes) - message = f"partition_msg_{message_counter}" - await node.publish_message(message) - self.metrics[version].total_messages_sent += 1 - message_counter += 1 - await trio.sleep(2) # Slower during partition - - # Record partition recovery time - recovery_duration = (time.time() - partition_start) * 1000 - for version in self.metrics: - self.metrics[version].partition_recovery_time_ms = recovery_duration - - async def _calculate_final_metrics(self): - """Calculate final metrics for all networks""" - for version, nodes in self.networks.items(): - metrics = self.metrics[version] - - # Aggregate node metrics - total_sent = sum(node.messages_sent for node in nodes) - total_received = sum(node.messages_received for node in nodes) - - all_latencies = [] - for node in nodes: - all_latencies.extend(node.latencies) - - # Calculate delivery rate and latency - if total_sent > 0: - # Expected receives = sent * (nodes - 1) since each message should reach all other nodes - expected_receives = total_sent * (len(nodes) - 1) - metrics.message_delivery_rate = min(1.0, total_received / expected_receives) - - metrics.average_latency_ms = statistics.mean(all_latencies) if all_latencies else 0.0 - - logger.info(f"{version} final metrics: " - f"sent={total_sent}, received={total_received}, " - f"delivery_rate={metrics.message_delivery_rate:.2%}, " - f"avg_latency={metrics.average_latency_ms:.1f}ms") - - -def print_comparison_results(result: ComparisonResult): - """Print formatted comparison results""" - print(f"\n{'='*80}") - print(f"GOSSIPSUB VERSION COMPARISON RESULTS") - print(f"{'='*80}") - print(f"Scenario: {result.scenario}") - print(f"Duration: {result.duration_seconds:.1f} seconds") - print(f"{'='*80}") - - # Print metrics table - versions = list(result.metrics_by_version.keys()) - - print(f"{'Metric':<30} {'v1.0':<12} {'v1.1':<12} {'v1.2':<12} {'v2.0':<12}") - print(f"{'-'*80}") - - metrics_to_show = [ - ("Messages Sent", "total_messages_sent"), - ("Messages Received", "total_messages_received"), - ("Delivery Rate", "message_delivery_rate"), - ("Avg Latency (ms)", "average_latency_ms"), - ("Spam Blocked", "spam_messages_blocked"), - ("Churn Events", "peer_churn_events"), - ] - - for metric_name, metric_key in metrics_to_show: - row = f"{metric_name:<30}" - for version in versions: - metrics = result.metrics_by_version[version] - value = getattr(metrics, metric_key) - - if metric_key == "message_delivery_rate": - row += f"{value:.1%}".ljust(12) - elif metric_key in ["average_latency_ms", "partition_recovery_time_ms"]: - row += f"{value:.1f}".ljust(12) - else: - row += f"{value}".ljust(12) - print(row) - - print(f"{'='*80}") - - # Analysis - print("\nANALYSIS:") - best_delivery = max(result.metrics_by_version.items(), - key=lambda x: x[1].message_delivery_rate) - print(f"โ€ข Best message delivery rate: {best_delivery[0]} ({best_delivery[1].message_delivery_rate:.1%})") - - best_latency = min(result.metrics_by_version.items(), - key=lambda x: x[1].average_latency_ms) - print(f"โ€ข Lowest average latency: {best_latency[0]} ({best_latency[1].average_latency_ms:.1f}ms)") - - if result.scenario == "spam_attack": - best_spam_protection = max(result.metrics_by_version.items(), - key=lambda x: x[1].spam_messages_blocked) - print(f"โ€ข Best spam protection: {best_spam_protection[0]} ({best_spam_protection[1].spam_messages_blocked} blocked)") - - -async def main(): - parser = argparse.ArgumentParser(description="Gossipsub Version Comparison Demo") - parser.add_argument( - "--scenario", - choices=["normal", "high_churn", "spam_attack", "network_partition"], - default="normal", - help="Test scenario to run" - ) - parser.add_argument( - "--duration", - type=int, - default=30, - help="Test duration in seconds" - ) - parser.add_argument( - "--nodes", - type=int, - default=4, - help="Number of nodes per version" - ) - parser.add_argument( - "--output", - type=str, - help="Output file for JSON results" - ) - parser.add_argument( - "--verbose", - action="store_true", - help="Enable verbose logging" - ) - - args = parser.parse_args() - - if args.verbose: - logging.getLogger().setLevel(logging.DEBUG) - - # Create and run simulation - simulator = NetworkSimulator() - await simulator.setup_networks(args.nodes) - - result = await simulator.run_scenario(args.scenario, args.duration) - - # Display results - print_comparison_results(result) - - # Save to file if requested - if args.output: - with open(args.output, 'w') as f: - json.dump(result.to_dict(), f, indent=2) - print(f"\nResults saved to {args.output}") - - -if __name__ == "__main__": - trio.run(main) \ No newline at end of file From dacc8784c8b9df6618716b8fef55d09d6272ceab Mon Sep 17 00:00:00 2001 From: Winter-Soren Date: Thu, 29 Jan 2026 16:59:52 +0530 Subject: [PATCH 3/9] Fix pre-commit hook issues: resolve ruff linting errors and pyrefly type checking errors in gossipsub examples --- examples/pubsub/gossipsub/gossipsub_v1.0.py | 153 +++++++-------- examples/pubsub/gossipsub/gossipsub_v1.1.py | 175 ++++++++++-------- examples/pubsub/gossipsub/gossipsub_v1.2.py | 175 ++++++++++-------- examples/pubsub/gossipsub/gossipsub_v2.0.py | 195 +++++++++++--------- 4 files changed, 372 insertions(+), 326 deletions(-) diff --git a/examples/pubsub/gossipsub/gossipsub_v1.0.py b/examples/pubsub/gossipsub/gossipsub_v1.0.py index e4f20804a..f7da97a12 100755 --- a/examples/pubsub/gossipsub/gossipsub_v1.0.py +++ b/examples/pubsub/gossipsub/gossipsub_v1.0.py @@ -21,11 +21,11 @@ import logging import random import time -from typing import List import trio from libp2p import new_host +from libp2p.abc import IHost, ISubscriptionAPI from libp2p.crypto.rsa import create_new_key_pair from libp2p.custom_types import TProtocol from libp2p.pubsub.gossipsub import GossipSub @@ -48,26 +48,26 @@ class GossipsubV10Node: """A node running Gossipsub 1.0""" - + def __init__(self, node_id: str, port: int): self.node_id = node_id self.port = port - self.host = None - self.pubsub = None - self.gossipsub = None - self.subscription = None + self.host: IHost | None = None + self.pubsub: Pubsub | None = None + self.gossipsub: GossipSub | None = None + self.subscription: ISubscriptionAPI | None = None self.messages_sent = 0 self.messages_received = 0 - + async def start(self): """Start the node with Gossipsub 1.0 configuration""" key_pair = create_new_key_pair() - + self.host = new_host( key_pair=key_pair, muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, ) - + # Configure Gossipsub 1.0 - basic configuration only self.gossipsub = GossipSub( protocols=[GOSSIPSUB_V10], @@ -81,74 +81,83 @@ async def start(self): # No adaptive features - v1.0 doesn't have adaptive gossip # No security features - v1.0 has basic security only ) - + self.pubsub = Pubsub(self.host, self.gossipsub) - + # Start services import multiaddr + listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{self.port}")] - + async with self.host.run(listen_addrs=listen_addrs): async with background_trio_service(self.pubsub): async with background_trio_service(self.gossipsub): await self.pubsub.wait_until_ready() self.subscription = await self.pubsub.subscribe(TOPIC) - logger.info(f"Node {self.node_id} (Gossipsub 1.0) started on port {self.port}") - + logger.info( + f"Node {self.node_id} (Gossipsub 1.0) started on port " + f"{self.port}" + ) + # Keep running await trio.sleep_forever() - + async def publish_message(self, message: str): """Publish a message to the topic""" if self.pubsub: await self.pubsub.publish(TOPIC, message.encode()) self.messages_sent += 1 logger.info(f"Node {self.node_id} published: {message}") - + async def receive_messages(self): """Receive and process messages""" if not self.subscription: return - + try: while True: + if self.subscription is None: + break message = await self.subscription.get() - decoded = message.data.decode('utf-8') + decoded = message.data.decode("utf-8") self.messages_received += 1 logger.info(f"Node {self.node_id} received: {decoded}") except Exception as e: logger.debug(f"Node {self.node_id} receive loop ended: {e}") - + async def connect_to_peer(self, peer_addr: str): """Connect to another peer""" if self.host: try: - from libp2p.peer.peerinfo import info_from_p2p_addr import multiaddr - + + from libp2p.peer.peerinfo import info_from_p2p_addr + maddr = multiaddr.Multiaddr(peer_addr) info = info_from_p2p_addr(maddr) await self.host.connect(info) logger.debug(f"Node {self.node_id} connected to {peer_addr}") except Exception as e: - logger.debug(f"Node {self.node_id} failed to connect to {peer_addr}: {e}") + logger.debug( + f"Node {self.node_id} failed to connect to {peer_addr}: {e}" + ) class GossipsubV10Demo: """Demo controller for Gossipsub 1.0""" - + def __init__(self): - self.nodes: List[GossipsubV10Node] = [] - + self.nodes: list[GossipsubV10Node] = [] + async def setup_network(self, node_count: int = 5): """Set up a network of nodes""" for i in range(node_count): port = find_free_port() node = GossipsubV10Node(f"node_{i}", port) self.nodes.append(node) - + logger.info(f"Created network with {node_count} nodes running Gossipsub 1.0") - + async def start_network(self, duration: int = 30): """Start all nodes and run the demo""" try: @@ -156,30 +165,30 @@ async def start_network(self, duration: int = 30): # Start all nodes for node in self.nodes: nursery.start_soon(node.start) - + # Wait for initialization await trio.sleep(3) - + # Connect nodes in a mesh topology await self._connect_nodes() await trio.sleep(2) - + # Start message receiving for all nodes for node in self.nodes: nursery.start_soon(node.receive_messages) - + # Run publishing loop end_time = time.time() + duration message_counter = 0 - - print(f"\n{'='*60}") + + print(f"\n{'=' * 60}") print("GOSSIPSUB 1.0 DEMO") - print(f"{'='*60}") + print(f"{'=' * 60}") print(f"Running for {duration} seconds...") - print(f"Protocol: /meshsub/1.0.0") - print(f"Features: Basic mesh-based pubsub, simple flooding") - print(f"{'='*60}\n") - + print("Protocol: /meshsub/1.0.0") + print("Features: Basic mesh-based pubsub, simple flooding") + print(f"{'=' * 60}\n") + while time.time() < end_time: # Random node publishes a message node = random.choice(self.nodes) @@ -187,17 +196,17 @@ async def start_network(self, duration: int = 30): await node.publish_message(message) message_counter += 1 await trio.sleep(2) # Publish every 2 seconds - + # Print statistics await trio.sleep(1) # Wait for final messages self._print_statistics() - + # Cancel all tasks to exit nursery nursery.cancel_scope.cancel() - + except Exception as e: logger.warning(f"Demo execution interrupted: {e}") - + async def _connect_nodes(self): """Connect nodes in a mesh topology""" for i, node in enumerate(self.nodes): @@ -205,36 +214,44 @@ async def _connect_nodes(self): if len(self.nodes) > 1: target_idx = (i + 1) % len(self.nodes) target = self.nodes[target_idx] - + if target.host and node.host: - peer_addr = f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + peer_addr = ( + f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + ) await node.connect_to_peer(peer_addr) - + # Also connect to one more node for better connectivity if len(self.nodes) > 2: target_idx2 = (i + 2) % len(self.nodes) target2 = self.nodes[target_idx2] - + if target2.host and node.host: - peer_addr2 = f"/ip4/127.0.0.1/tcp/{target2.port}/p2p/{target2.host.get_id()}" + peer_addr2 = ( + f"/ip4/127.0.0.1/tcp/{target2.port}/p2p/" + f"{target2.host.get_id()}" + ) await node.connect_to_peer(peer_addr2) - + def _print_statistics(self): """Print demo statistics""" - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print("DEMO STATISTICS") - print(f"{'='*60}") - + print(f"{'=' * 60}") + total_sent = sum(node.messages_sent for node in self.nodes) total_received = sum(node.messages_received for node in self.nodes) - + print(f"Total messages sent: {total_sent}") print(f"Total messages received: {total_received}") - print(f"\nPer-node statistics:") + print("\nPer-node statistics:") for node in self.nodes: - print(f" {node.node_id}: sent={node.messages_sent}, received={node.messages_received}") - - print(f"\n{'='*60}") + print( + f" {node.node_id}: sent={node.messages_sent}, " + f"received={node.messages_received}" + ) + + print(f"\n{'=' * 60}") print("Gossipsub 1.0 Features:") print(" โœ“ Basic mesh-based pubsub") print(" โœ“ Simple message flooding") @@ -243,34 +260,24 @@ def _print_statistics(self): print(" โœ— No IDONTWANT support") print(" โœ— No adaptive gossip") print(" โœ— No advanced security features") - print(f"{'='*60}\n") + print(f"{'=' * 60}\n") async def main(): parser = argparse.ArgumentParser(description="Gossipsub 1.0 Example") parser.add_argument( - "--nodes", - type=int, - default=5, - help="Number of nodes in the network" + "--nodes", type=int, default=5, help="Number of nodes in the network" ) parser.add_argument( - "--duration", - type=int, - default=30, - help="Demo duration in seconds" + "--duration", type=int, default=30, help="Demo duration in seconds" ) - parser.add_argument( - "--verbose", - action="store_true", - help="Enable verbose logging" - ) - + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + args = parser.parse_args() - + if args.verbose: logging.getLogger().setLevel(logging.DEBUG) - + demo = GossipsubV10Demo() await demo.setup_network(args.nodes) await demo.start_network(args.duration) diff --git a/examples/pubsub/gossipsub/gossipsub_v1.1.py b/examples/pubsub/gossipsub/gossipsub_v1.1.py index 347785cb5..4cddf18d1 100755 --- a/examples/pubsub/gossipsub/gossipsub_v1.1.py +++ b/examples/pubsub/gossipsub/gossipsub_v1.1.py @@ -21,11 +21,11 @@ import logging import random import time -from typing import List import trio from libp2p import new_host +from libp2p.abc import IHost, ISubscriptionAPI from libp2p.crypto.rsa import create_new_key_pair from libp2p.custom_types import TProtocol from libp2p.pubsub.gossipsub import GossipSub @@ -49,39 +49,43 @@ class GossipsubV11Node: """A node running Gossipsub 1.1""" - + def __init__(self, node_id: str, port: int, role: str = "honest"): self.node_id = node_id self.port = port self.role = role # "honest" or "malicious" - self.host = None - self.pubsub = None - self.gossipsub = None - self.subscription = None + self.host: IHost | None = None + self.pubsub: Pubsub | None = None + self.gossipsub: GossipSub | None = None + self.subscription: ISubscriptionAPI | None = None self.messages_sent = 0 self.messages_received = 0 - + async def start(self): """Start the node with Gossipsub 1.1 configuration""" key_pair = create_new_key_pair() - + self.host = new_host( key_pair=key_pair, muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, ) - + # Configure Gossipsub 1.1 - adds peer scoring score_params = ScoreParams( # Topic-scoped parameters (P1-P4) p1_time_in_mesh=TopicScoreParams(weight=0.1, cap=10.0, decay=0.99), - p2_first_message_deliveries=TopicScoreParams(weight=0.5, cap=20.0, decay=0.99), - p3_mesh_message_deliveries=TopicScoreParams(weight=0.3, cap=10.0, decay=0.99), + p2_first_message_deliveries=TopicScoreParams( + weight=0.5, cap=20.0, decay=0.99 + ), + p3_mesh_message_deliveries=TopicScoreParams( + weight=0.3, cap=10.0, decay=0.99 + ), p4_invalid_messages=TopicScoreParams(weight=-1.0, cap=50.0, decay=0.99), # Global behavioral penalty (P5) p5_behavior_penalty_weight=1.0, p5_behavior_penalty_decay=0.99, ) - + self.gossipsub = GossipSub( protocols=[GOSSIPSUB_V11], degree=3, @@ -94,77 +98,86 @@ async def start(self): # No adaptive features - v1.1 doesn't have adaptive gossip # No advanced security features - v1.1 has basic security ) - + self.pubsub = Pubsub(self.host, self.gossipsub) - + # Start services import multiaddr + listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{self.port}")] - + async with self.host.run(listen_addrs=listen_addrs): async with background_trio_service(self.pubsub): async with background_trio_service(self.gossipsub): await self.pubsub.wait_until_ready() self.subscription = await self.pubsub.subscribe(TOPIC) - logger.info(f"Node {self.node_id} (Gossipsub 1.1, {self.role}) started on port {self.port}") - + logger.info( + f"Node {self.node_id} (Gossipsub 1.1, {self.role}) started on " + f"port {self.port}" + ) + # Keep running await trio.sleep_forever() - + async def publish_message(self, message: str): """Publish a message to the topic""" if self.pubsub: await self.pubsub.publish(TOPIC, message.encode()) self.messages_sent += 1 logger.info(f"Node {self.node_id} ({self.role}) published: {message}") - + async def receive_messages(self): """Receive and process messages""" if not self.subscription: return - + try: while True: + if self.subscription is None: + break message = await self.subscription.get() - decoded = message.data.decode('utf-8') + decoded = message.data.decode("utf-8") self.messages_received += 1 logger.info(f"Node {self.node_id} received: {decoded}") except Exception as e: logger.debug(f"Node {self.node_id} receive loop ended: {e}") - + async def connect_to_peer(self, peer_addr: str): """Connect to another peer""" if self.host: try: - from libp2p.peer.peerinfo import info_from_p2p_addr import multiaddr - + + from libp2p.peer.peerinfo import info_from_p2p_addr + maddr = multiaddr.Multiaddr(peer_addr) info = info_from_p2p_addr(maddr) await self.host.connect(info) logger.debug(f"Node {self.node_id} connected to {peer_addr}") except Exception as e: - logger.debug(f"Node {self.node_id} failed to connect to {peer_addr}: {e}") + logger.debug( + f"Node {self.node_id} failed to connect to {peer_addr}: {e}" + ) class GossipsubV11Demo: """Demo controller for Gossipsub 1.1""" - + def __init__(self): - self.nodes: List[GossipsubV11Node] = [] - + self.nodes: list[GossipsubV11Node] = [] + async def setup_network(self, node_count: int = 5): """Set up a network of nodes""" roles = ["honest"] * (node_count - 1) + ["malicious"] * 1 - + for i in range(node_count): port = find_free_port() role = roles[i] if i < len(roles) else "honest" node = GossipsubV11Node(f"node_{i}", port, role) self.nodes.append(node) - + logger.info(f"Created network with {node_count} nodes running Gossipsub 1.1") - + async def start_network(self, duration: int = 30): """Start all nodes and run the demo""" try: @@ -172,30 +185,30 @@ async def start_network(self, duration: int = 30): # Start all nodes for node in self.nodes: nursery.start_soon(node.start) - + # Wait for initialization await trio.sleep(3) - + # Connect nodes in a mesh topology await self._connect_nodes() await trio.sleep(2) - + # Start message receiving for all nodes for node in self.nodes: nursery.start_soon(node.receive_messages) - + # Run publishing loop end_time = time.time() + duration message_counter = 0 - - print(f"\n{'='*60}") + + print(f"\n{'=' * 60}") print("GOSSIPSUB 1.1 DEMO") - print(f"{'='*60}") + print(f"{'=' * 60}") print(f"Running for {duration} seconds...") - print(f"Protocol: /meshsub/1.1.0") - print(f"Features: Peer scoring (P1-P5), behavioral penalties") - print(f"{'='*60}\n") - + print("Protocol: /meshsub/1.1.0") + print("Features: Peer scoring (P1-P5), behavioral penalties") + print(f"{'=' * 60}\n") + while time.time() < end_time: # Honest nodes publish normally honest_nodes = [n for n in self.nodes if n.role == "honest"] @@ -204,7 +217,7 @@ async def start_network(self, duration: int = 30): message = f"honest_msg_{message_counter}_{int(time.time())}" await node.publish_message(message) message_counter += 1 - + # Malicious nodes might send more messages (will be penalized) malicious_nodes = [n for n in self.nodes if n.role == "malicious"] if malicious_nodes and random.random() < 0.3: # 30% chance @@ -212,19 +225,19 @@ async def start_network(self, duration: int = 30): message = f"malicious_msg_{message_counter}_{int(time.time())}" await node.publish_message(message) message_counter += 1 - + await trio.sleep(2) # Publish every 2 seconds - + # Print statistics await trio.sleep(1) # Wait for final messages self._print_statistics() - + # Cancel all tasks to exit nursery nursery.cancel_scope.cancel() - + except Exception as e: logger.warning(f"Demo execution interrupted: {e}") - + async def _connect_nodes(self): """Connect nodes in a mesh topology""" for i, node in enumerate(self.nodes): @@ -232,41 +245,51 @@ async def _connect_nodes(self): if len(self.nodes) > 1: target_idx = (i + 1) % len(self.nodes) target = self.nodes[target_idx] - + if target.host and node.host: - peer_addr = f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + peer_addr = ( + f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + ) await node.connect_to_peer(peer_addr) - + # Also connect to one more node for better connectivity if len(self.nodes) > 2: target_idx2 = (i + 2) % len(self.nodes) target2 = self.nodes[target_idx2] - + if target2.host and node.host: - peer_addr2 = f"/ip4/127.0.0.1/tcp/{target2.port}/p2p/{target2.host.get_id()}" + peer_addr2 = ( + f"/ip4/127.0.0.1/tcp/{target2.port}/p2p/" + f"{target2.host.get_id()}" + ) await node.connect_to_peer(peer_addr2) - + def _print_statistics(self): """Print demo statistics""" - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print("DEMO STATISTICS") - print(f"{'='*60}") - + print(f"{'=' * 60}") + total_sent = sum(node.messages_sent for node in self.nodes) total_received = sum(node.messages_received for node in self.nodes) - + honest_sent = sum(n.messages_sent for n in self.nodes if n.role == "honest") - malicious_sent = sum(n.messages_sent for n in self.nodes if n.role == "malicious") - + malicious_sent = sum( + n.messages_sent for n in self.nodes if n.role == "malicious" + ) + print(f"Total messages sent: {total_sent}") print(f" Honest nodes: {honest_sent}") print(f" Malicious nodes: {malicious_sent}") print(f"Total messages received: {total_received}") - print(f"\nPer-node statistics:") + print("\nPer-node statistics:") for node in self.nodes: - print(f" {node.node_id} ({node.role}): sent={node.messages_sent}, received={node.messages_received}") - - print(f"\n{'='*60}") + print( + f" {node.node_id} ({node.role}): sent={node.messages_sent}, " + f"received={node.messages_received}" + ) + + print(f"\n{'=' * 60}") print("Gossipsub 1.1 Features:") print(" โœ“ Basic mesh-based pubsub (from v1.0)") print(" โœ“ Peer scoring with P1-P4 (topic-scoped)") @@ -279,34 +302,24 @@ def _print_statistics(self): print(" โœ— No IDONTWANT support") print(" โœ— No adaptive gossip") print(" โœ— No advanced security features") - print(f"{'='*60}\n") + print(f"{'=' * 60}\n") async def main(): parser = argparse.ArgumentParser(description="Gossipsub 1.1 Example") parser.add_argument( - "--nodes", - type=int, - default=5, - help="Number of nodes in the network" - ) - parser.add_argument( - "--duration", - type=int, - default=30, - help="Demo duration in seconds" + "--nodes", type=int, default=5, help="Number of nodes in the network" ) parser.add_argument( - "--verbose", - action="store_true", - help="Enable verbose logging" + "--duration", type=int, default=30, help="Demo duration in seconds" ) - + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + args = parser.parse_args() - + if args.verbose: logging.getLogger().setLevel(logging.DEBUG) - + demo = GossipsubV11Demo() await demo.setup_network(args.nodes) await demo.start_network(args.duration) diff --git a/examples/pubsub/gossipsub/gossipsub_v1.2.py b/examples/pubsub/gossipsub/gossipsub_v1.2.py index ef8ba9b1d..85e2edc64 100755 --- a/examples/pubsub/gossipsub/gossipsub_v1.2.py +++ b/examples/pubsub/gossipsub/gossipsub_v1.2.py @@ -20,11 +20,11 @@ import logging import random import time -from typing import List import trio from libp2p import new_host +from libp2p.abc import IHost, ISubscriptionAPI from libp2p.crypto.rsa import create_new_key_pair from libp2p.custom_types import TProtocol from libp2p.pubsub.gossipsub import GossipSub @@ -48,39 +48,43 @@ class GossipsubV12Node: """A node running Gossipsub 1.2""" - + def __init__(self, node_id: str, port: int, role: str = "honest"): self.node_id = node_id self.port = port self.role = role # "honest" or "malicious" - self.host = None - self.pubsub = None - self.gossipsub = None - self.subscription = None + self.host: IHost | None = None + self.pubsub: Pubsub | None = None + self.gossipsub: GossipSub | None = None + self.subscription: ISubscriptionAPI | None = None self.messages_sent = 0 self.messages_received = 0 - + async def start(self): """Start the node with Gossipsub 1.2 configuration""" key_pair = create_new_key_pair() - + self.host = new_host( key_pair=key_pair, muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, ) - + # Configure Gossipsub 1.2 - adds IDONTWANT support score_params = ScoreParams( # Topic-scoped parameters (P1-P4) p1_time_in_mesh=TopicScoreParams(weight=0.1, cap=10.0, decay=0.99), - p2_first_message_deliveries=TopicScoreParams(weight=0.5, cap=20.0, decay=0.99), - p3_mesh_message_deliveries=TopicScoreParams(weight=0.3, cap=10.0, decay=0.99), + p2_first_message_deliveries=TopicScoreParams( + weight=0.5, cap=20.0, decay=0.99 + ), + p3_mesh_message_deliveries=TopicScoreParams( + weight=0.3, cap=10.0, decay=0.99 + ), p4_invalid_messages=TopicScoreParams(weight=-1.0, cap=50.0, decay=0.99), # Global behavioral penalty (P5) p5_behavior_penalty_weight=1.0, p5_behavior_penalty_decay=0.99, ) - + self.gossipsub = GossipSub( protocols=[GOSSIPSUB_V12], degree=3, @@ -94,77 +98,86 @@ async def start(self): # No adaptive features - v1.2 doesn't have adaptive gossip # No advanced security features - v1.2 has basic security ) - + self.pubsub = Pubsub(self.host, self.gossipsub) - + # Start services import multiaddr + listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{self.port}")] - + async with self.host.run(listen_addrs=listen_addrs): async with background_trio_service(self.pubsub): async with background_trio_service(self.gossipsub): await self.pubsub.wait_until_ready() self.subscription = await self.pubsub.subscribe(TOPIC) - logger.info(f"Node {self.node_id} (Gossipsub 1.2, {self.role}) started on port {self.port}") - + logger.info( + f"Node {self.node_id} (Gossipsub 1.2, {self.role}) started on " + f"port {self.port}" + ) + # Keep running await trio.sleep_forever() - + async def publish_message(self, message: str): """Publish a message to the topic""" if self.pubsub: await self.pubsub.publish(TOPIC, message.encode()) self.messages_sent += 1 logger.info(f"Node {self.node_id} ({self.role}) published: {message}") - + async def receive_messages(self): """Receive and process messages""" if not self.subscription: return - + try: while True: + if self.subscription is None: + break message = await self.subscription.get() - decoded = message.data.decode('utf-8') + decoded = message.data.decode("utf-8") self.messages_received += 1 logger.info(f"Node {self.node_id} received: {decoded}") except Exception as e: logger.debug(f"Node {self.node_id} receive loop ended: {e}") - + async def connect_to_peer(self, peer_addr: str): """Connect to another peer""" if self.host: try: - from libp2p.peer.peerinfo import info_from_p2p_addr import multiaddr - + + from libp2p.peer.peerinfo import info_from_p2p_addr + maddr = multiaddr.Multiaddr(peer_addr) info = info_from_p2p_addr(maddr) await self.host.connect(info) logger.debug(f"Node {self.node_id} connected to {peer_addr}") except Exception as e: - logger.debug(f"Node {self.node_id} failed to connect to {peer_addr}: {e}") + logger.debug( + f"Node {self.node_id} failed to connect to {peer_addr}: {e}" + ) class GossipsubV12Demo: """Demo controller for Gossipsub 1.2""" - + def __init__(self): - self.nodes: List[GossipsubV12Node] = [] - + self.nodes: list[GossipsubV12Node] = [] + async def setup_network(self, node_count: int = 5): """Set up a network of nodes""" roles = ["honest"] * (node_count - 1) + ["malicious"] * 1 - + for i in range(node_count): port = find_free_port() role = roles[i] if i < len(roles) else "honest" node = GossipsubV12Node(f"node_{i}", port, role) self.nodes.append(node) - + logger.info(f"Created network with {node_count} nodes running Gossipsub 1.2") - + async def start_network(self, duration: int = 30): """Start all nodes and run the demo""" try: @@ -172,30 +185,30 @@ async def start_network(self, duration: int = 30): # Start all nodes for node in self.nodes: nursery.start_soon(node.start) - + # Wait for initialization await trio.sleep(3) - + # Connect nodes in a mesh topology await self._connect_nodes() await trio.sleep(2) - + # Start message receiving for all nodes for node in self.nodes: nursery.start_soon(node.receive_messages) - + # Run publishing loop end_time = time.time() + duration message_counter = 0 - - print(f"\n{'='*60}") + + print(f"\n{'=' * 60}") print("GOSSIPSUB 1.2 DEMO") - print(f"{'='*60}") + print(f"{'=' * 60}") print(f"Running for {duration} seconds...") - print(f"Protocol: /meshsub/1.2.0") - print(f"Features: IDONTWANT filtering, reduced bandwidth usage") - print(f"{'='*60}\n") - + print("Protocol: /meshsub/1.2.0") + print("Features: IDONTWANT filtering, reduced bandwidth usage") + print(f"{'=' * 60}\n") + while time.time() < end_time: # Honest nodes publish normally honest_nodes = [n for n in self.nodes if n.role == "honest"] @@ -204,7 +217,7 @@ async def start_network(self, duration: int = 30): message = f"honest_msg_{message_counter}_{int(time.time())}" await node.publish_message(message) message_counter += 1 - + # Malicious nodes might send more messages (will be penalized) malicious_nodes = [n for n in self.nodes if n.role == "malicious"] if malicious_nodes and random.random() < 0.3: # 30% chance @@ -212,19 +225,19 @@ async def start_network(self, duration: int = 30): message = f"malicious_msg_{message_counter}_{int(time.time())}" await node.publish_message(message) message_counter += 1 - + await trio.sleep(2) # Publish every 2 seconds - + # Print statistics await trio.sleep(1) # Wait for final messages self._print_statistics() - + # Cancel all tasks to exit nursery nursery.cancel_scope.cancel() - + except Exception as e: logger.warning(f"Demo execution interrupted: {e}") - + async def _connect_nodes(self): """Connect nodes in a mesh topology""" for i, node in enumerate(self.nodes): @@ -232,41 +245,51 @@ async def _connect_nodes(self): if len(self.nodes) > 1: target_idx = (i + 1) % len(self.nodes) target = self.nodes[target_idx] - + if target.host and node.host: - peer_addr = f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + peer_addr = ( + f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + ) await node.connect_to_peer(peer_addr) - + # Also connect to one more node for better connectivity if len(self.nodes) > 2: target_idx2 = (i + 2) % len(self.nodes) target2 = self.nodes[target_idx2] - + if target2.host and node.host: - peer_addr2 = f"/ip4/127.0.0.1/tcp/{target2.port}/p2p/{target2.host.get_id()}" + peer_addr2 = ( + f"/ip4/127.0.0.1/tcp/{target2.port}/p2p/" + f"{target2.host.get_id()}" + ) await node.connect_to_peer(peer_addr2) - + def _print_statistics(self): """Print demo statistics""" - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print("DEMO STATISTICS") - print(f"{'='*60}") - + print(f"{'=' * 60}") + total_sent = sum(node.messages_sent for node in self.nodes) total_received = sum(node.messages_received for node in self.nodes) - + honest_sent = sum(n.messages_sent for n in self.nodes if n.role == "honest") - malicious_sent = sum(n.messages_sent for n in self.nodes if n.role == "malicious") - + malicious_sent = sum( + n.messages_sent for n in self.nodes if n.role == "malicious" + ) + print(f"Total messages sent: {total_sent}") print(f" Honest nodes: {honest_sent}") print(f" Malicious nodes: {malicious_sent}") print(f"Total messages received: {total_received}") - print(f"\nPer-node statistics:") + print("\nPer-node statistics:") for node in self.nodes: - print(f" {node.node_id} ({node.role}): sent={node.messages_sent}, received={node.messages_received}") - - print(f"\n{'='*60}") + print( + f" {node.node_id} ({node.role}): sent={node.messages_sent}, " + f"received={node.messages_received}" + ) + + print(f"\n{'=' * 60}") print("Gossipsub 1.2 Features:") print(" โœ“ All Gossipsub 1.1 features") print(" โœ“ IDONTWANT message filtering") @@ -274,34 +297,24 @@ def _print_statistics(self): print(" โœ“ Improved efficiency in dense networks") print(" โœ— No adaptive gossip") print(" โœ— No advanced security features (P6, P7)") - print(f"{'='*60}\n") + print(f"{'=' * 60}\n") async def main(): parser = argparse.ArgumentParser(description="Gossipsub 1.2 Example") parser.add_argument( - "--nodes", - type=int, - default=5, - help="Number of nodes in the network" - ) - parser.add_argument( - "--duration", - type=int, - default=30, - help="Demo duration in seconds" + "--nodes", type=int, default=5, help="Number of nodes in the network" ) parser.add_argument( - "--verbose", - action="store_true", - help="Enable verbose logging" + "--duration", type=int, default=30, help="Demo duration in seconds" ) - + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + args = parser.parse_args() - + if args.verbose: logging.getLogger().setLevel(logging.DEBUG) - + demo = GossipsubV12Demo() await demo.setup_network(args.nodes) await demo.start_network(args.duration) diff --git a/examples/pubsub/gossipsub/gossipsub_v2.0.py b/examples/pubsub/gossipsub/gossipsub_v2.0.py index 116f74f6c..a4997a7fc 100755 --- a/examples/pubsub/gossipsub/gossipsub_v2.0.py +++ b/examples/pubsub/gossipsub/gossipsub_v2.0.py @@ -24,11 +24,11 @@ import logging import random import time -from typing import List import trio from libp2p import new_host +from libp2p.abc import IHost, ISubscriptionAPI from libp2p.crypto.rsa import create_new_key_pair from libp2p.custom_types import TProtocol from libp2p.peer.id import ID @@ -53,35 +53,39 @@ class GossipsubV20Node: """A node running Gossipsub 2.0""" - + def __init__(self, node_id: str, port: int, role: str = "honest"): self.node_id = node_id self.port = port self.role = role # "honest", "spammer", "validator" - self.host = None - self.pubsub = None - self.gossipsub = None - self.subscription = None + self.host: IHost | None = None + self.pubsub: Pubsub | None = None + self.gossipsub: GossipSub | None = None + self.subscription: ISubscriptionAPI | None = None self.messages_sent = 0 self.messages_received = 0 self.messages_validated = 0 self.messages_rejected = 0 - + async def start(self): """Start the node with Gossipsub 2.0 configuration""" key_pair = create_new_key_pair() - + self.host = new_host( key_pair=key_pair, muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, ) - + # Configure Gossipsub 2.0 - full feature set score_params = ScoreParams( # Topic-scoped parameters (P1-P4) p1_time_in_mesh=TopicScoreParams(weight=0.1, cap=10.0, decay=0.99), - p2_first_message_deliveries=TopicScoreParams(weight=0.5, cap=20.0, decay=0.99), - p3_mesh_message_deliveries=TopicScoreParams(weight=0.3, cap=10.0, decay=0.99), + p2_first_message_deliveries=TopicScoreParams( + weight=0.5, cap=20.0, decay=0.99 + ), + p3_mesh_message_deliveries=TopicScoreParams( + weight=0.3, cap=10.0, decay=0.99 + ), p4_invalid_messages=TopicScoreParams(weight=-1.0, cap=50.0, decay=0.99), # Global behavioral penalty (P5) p5_behavior_penalty_weight=1.0, @@ -95,7 +99,7 @@ async def start(self): # Application-specific scoring function app_specific_score_fn=self._application_score_function, ) - + self.gossipsub = GossipSub( protocols=[GOSSIPSUB_V20], degree=4, @@ -114,50 +118,56 @@ async def start(self): eclipse_protection_enabled=True, min_mesh_diversity_ips=2, ) - + self.pubsub = Pubsub(self.host, self.gossipsub) - + # Start services import multiaddr + listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{self.port}")] - + async with self.host.run(listen_addrs=listen_addrs): async with background_trio_service(self.pubsub): async with background_trio_service(self.gossipsub): await self.pubsub.wait_until_ready() self.subscription = await self.pubsub.subscribe(TOPIC) - logger.info(f"Node {self.node_id} (Gossipsub 2.0, {self.role}) started on port {self.port}") - + logger.info( + f"Node {self.node_id} (Gossipsub 2.0, {self.role}) started on " + f"port {self.port}" + ) + # Keep running await trio.sleep_forever() - + def _application_score_function(self, peer_id: ID) -> float: """Custom application scoring function (P6)""" # Example: Reward peers that have been connected longer # In a real application, this could be based on stake, reputation, etc. - if self.gossipsub and peer_id in getattr(self.gossipsub, 'peers', {}): + if self.gossipsub and peer_id in getattr(self.gossipsub, "peers", {}): # Simple time-based scoring for demo return min(5.0, time.time() % 10) # Varies over time for demo return 0.0 - + async def publish_message(self, message: str): """Publish a message to the topic""" if self.pubsub: await self.pubsub.publish(TOPIC, message.encode()) self.messages_sent += 1 logger.info(f"Node {self.node_id} ({self.role}) published: {message}") - + async def receive_messages(self): """Receive and process messages""" if not self.subscription: return - + try: while True: + if self.subscription is None: + break message = await self.subscription.get() - decoded = message.data.decode('utf-8') + decoded = message.data.decode("utf-8") self.messages_received += 1 - + # Simulate message validation if self._validate_message(message): self.messages_validated += 1 @@ -167,50 +177,53 @@ async def receive_messages(self): logger.warning(f"Node {self.node_id} received (invalid): {decoded}") except Exception as e: logger.debug(f"Node {self.node_id} receive loop ended: {e}") - + def _validate_message(self, message) -> bool: """Simple message validation""" try: - decoded = message.data.decode('utf-8') + decoded = message.data.decode("utf-8") # Basic validation: message should have expected format - return '_msg_' in decoded and len(decoded) < 1000 - except: + return "_msg_" in decoded and len(decoded) < 1000 + except Exception: return False - + async def connect_to_peer(self, peer_addr: str): """Connect to another peer""" if self.host: try: - from libp2p.peer.peerinfo import info_from_p2p_addr import multiaddr - + + from libp2p.peer.peerinfo import info_from_p2p_addr + maddr = multiaddr.Multiaddr(peer_addr) info = info_from_p2p_addr(maddr) await self.host.connect(info) logger.debug(f"Node {self.node_id} connected to {peer_addr}") except Exception as e: - logger.debug(f"Node {self.node_id} failed to connect to {peer_addr}: {e}") + logger.debug( + f"Node {self.node_id} failed to connect to {peer_addr}: {e}" + ) class GossipsubV20Demo: """Demo controller for Gossipsub 2.0""" - + def __init__(self): - self.nodes: List[GossipsubV20Node] = [] - + self.nodes: list[GossipsubV20Node] = [] + async def setup_network(self, node_count: int = 5): """Set up a network of nodes with different roles""" # Mix of honest, spammer, and validator nodes roles = ["honest"] * (node_count - 2) + ["spammer"] * 1 + ["validator"] * 1 - + for i in range(node_count): port = find_free_port() role = roles[i] if i < len(roles) else "honest" node = GossipsubV20Node(f"node_{i}", port, role) self.nodes.append(node) - + logger.info(f"Created network with {node_count} nodes running Gossipsub 2.0") - + async def start_network(self, duration: int = 30): """Start all nodes and run the demo""" try: @@ -218,30 +231,30 @@ async def start_network(self, duration: int = 30): # Start all nodes for node in self.nodes: nursery.start_soon(node.start) - + # Wait for initialization await trio.sleep(3) - + # Connect nodes in a mesh topology await self._connect_nodes() await trio.sleep(2) - + # Start message receiving for all nodes for node in self.nodes: nursery.start_soon(node.receive_messages) - + # Run publishing loop end_time = time.time() + duration message_counter = 0 - - print(f"\n{'='*60}") + + print(f"\n{'=' * 60}") print("GOSSIPSUB 2.0 DEMO") - print(f"{'='*60}") + print(f"{'=' * 60}") print(f"Running for {duration} seconds...") - print(f"Protocol: /meshsub/2.0.0") - print(f"Features: Adaptive gossip, advanced security, P6/P7 scoring") - print(f"{'='*60}\n") - + print("Protocol: /meshsub/2.0.0") + print("Features: Adaptive gossip, advanced security, P6/P7 scoring") + print(f"{'=' * 60}\n") + while time.time() < end_time: # Honest nodes publish normally honest_nodes = [n for n in self.nodes if n.role == "honest"] @@ -250,7 +263,7 @@ async def start_network(self, duration: int = 30): message = f"honest_msg_{message_counter}_{int(time.time())}" await node.publish_message(message) message_counter += 1 - + # Validator nodes publish less frequently but with high quality validator_nodes = [n for n in self.nodes if n.role == "validator"] if validator_nodes and random.random() < 0.3: # 30% chance @@ -258,7 +271,7 @@ async def start_network(self, duration: int = 30): message = f"validator_msg_{message_counter}_{int(time.time())}" await node.publish_message(message) message_counter += 1 - + # Spammer nodes try to send many messages (will be rate-limited) spammer_nodes = [n for n in self.nodes if n.role == "spammer"] if spammer_nodes and random.random() < 0.5: # 50% chance @@ -269,19 +282,19 @@ async def start_network(self, duration: int = 30): await node.publish_message(message) message_counter += 1 await trio.sleep(0.1) # Small delay between spam messages - + await trio.sleep(2) # Publish every 2 seconds - + # Print statistics await trio.sleep(1) # Wait for final messages self._print_statistics() - + # Cancel all tasks to exit nursery nursery.cancel_scope.cancel() - + except Exception as e: logger.warning(f"Demo execution interrupted: {e}") - + async def _connect_nodes(self): """Connect nodes in a mesh topology""" for i, node in enumerate(self.nodes): @@ -289,35 +302,42 @@ async def _connect_nodes(self): if len(self.nodes) > 1: target_idx = (i + 1) % len(self.nodes) target = self.nodes[target_idx] - + if target.host and node.host: - peer_addr = f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + peer_addr = ( + f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + ) await node.connect_to_peer(peer_addr) - + # Also connect to one more node for better connectivity if len(self.nodes) > 2: target_idx2 = (i + 2) % len(self.nodes) target2 = self.nodes[target_idx2] - + if target2.host and node.host: - peer_addr2 = f"/ip4/127.0.0.1/tcp/{target2.port}/p2p/{target2.host.get_id()}" + peer_addr2 = ( + f"/ip4/127.0.0.1/tcp/{target2.port}/p2p/" + f"{target2.host.get_id()}" + ) await node.connect_to_peer(peer_addr2) - + def _print_statistics(self): """Print demo statistics""" - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print("DEMO STATISTICS") - print(f"{'='*60}") - + print(f"{'=' * 60}") + total_sent = sum(node.messages_sent for node in self.nodes) total_received = sum(node.messages_received for node in self.nodes) total_validated = sum(node.messages_validated for node in self.nodes) total_rejected = sum(node.messages_rejected for node in self.nodes) - + honest_sent = sum(n.messages_sent for n in self.nodes if n.role == "honest") spammer_sent = sum(n.messages_sent for n in self.nodes if n.role == "spammer") - validator_sent = sum(n.messages_sent for n in self.nodes if n.role == "validator") - + validator_sent = sum( + n.messages_sent for n in self.nodes if n.role == "validator" + ) + print(f"Total messages sent: {total_sent}") print(f" Honest nodes: {honest_sent}") print(f" Spammer nodes: {spammer_sent}") @@ -325,13 +345,16 @@ def _print_statistics(self): print(f"Total messages received: {total_received}") print(f"Messages validated: {total_validated}") print(f"Messages rejected: {total_rejected}") - print(f"\nPer-node statistics:") + print("\nPer-node statistics:") for node in self.nodes: - print(f" {node.node_id} ({node.role}): " - f"sent={node.messages_sent}, received={node.messages_received}, " - f"validated={node.messages_validated}, rejected={node.messages_rejected}") - - print(f"\n{'='*60}") + print( + f" {node.node_id} ({node.role}): " + f"sent={node.messages_sent}, received={node.messages_received}, " + f"validated={node.messages_validated}, " + f"rejected={node.messages_rejected}" + ) + + print(f"\n{'=' * 60}") print("Gossipsub 2.0 Features:") print(" โœ“ All Gossipsub 1.2 features") print(" โœ“ Enhanced peer scoring:") @@ -343,34 +366,24 @@ def _print_statistics(self): print(" - Eclipse attack protection") print(" - Equivocation detection") print(" - Enhanced message validation") - print(f"{'='*60}\n") + print(f"{'=' * 60}\n") async def main(): parser = argparse.ArgumentParser(description="Gossipsub 2.0 Example") parser.add_argument( - "--nodes", - type=int, - default=5, - help="Number of nodes in the network" - ) - parser.add_argument( - "--duration", - type=int, - default=30, - help="Demo duration in seconds" + "--nodes", type=int, default=5, help="Number of nodes in the network" ) parser.add_argument( - "--verbose", - action="store_true", - help="Enable verbose logging" + "--duration", type=int, default=30, help="Demo duration in seconds" ) - + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + args = parser.parse_args() - + if args.verbose: logging.getLogger().setLevel(logging.DEBUG) - + demo = GossipsubV20Demo() await demo.setup_network(args.nodes) await demo.start_network(args.duration) From 292c82f66b03a46e155251c1aa3a739043a27417 Mon Sep 17 00:00:00 2001 From: Winter-Soren Date: Wed, 4 Mar 2026 23:11:00 +0530 Subject: [PATCH 4/9] feat: added fanout behaviour, IP colocation factor and behaviour penalty in gossipsub 1.0 and 1.1 examples --- examples/pubsub/gossipsub/README.md | 8 +++++ examples/pubsub/gossipsub/gossipsub_v1.0.py | 34 +++++++++++++----- examples/pubsub/gossipsub/gossipsub_v1.1.py | 38 +++++++++++++++++++-- 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/examples/pubsub/gossipsub/README.md b/examples/pubsub/gossipsub/README.md index c749bf2ef..1de9a6ecd 100644 --- a/examples/pubsub/gossipsub/README.md +++ b/examples/pubsub/gossipsub/README.md @@ -22,6 +22,7 @@ Basic mesh-based pubsub demo using Gossipsub 1.0 (`/meshsub/1.0.0`). - Basic mesh-based pubsub - Simple flooding for message dissemination - Mesh topology maintenance +- **Fanout behaviour**: one node (node_0) is a fanout-only publisher: it publishes without subscribing, so messages are sent via *fanout* (a random set of topic subscribers) instead of mesh peers **Usage:** @@ -38,6 +39,9 @@ Demonstrates Gossipsub 1.1 (`/meshsub/1.1.0`) with peer scoring and behavioral p - All Gossipsub 1.0 features - Peer scoring with P1โ€“P4 topic-scoped parameters - Behavioral penalties (P5) +- **P6 (application-specific score)** and **P7 (IP colocation factor / Behavioural Penalty)** +- **Prune backoff** and **peer exchange (PX)** enabled +- Optional **application score function** (in real applications: staking, reputation, or role e.g. validator, full node) - Honest vs. malicious publisher behaviour **Usage:** @@ -102,6 +106,7 @@ python gossipsub_v2.0.py --nodes 5 --duration 60 - Basic mesh-based pubsub protocol - Simple flooding for message dissemination +- **Fanout**: publishers that are not in the mesh for a topic send to a random set of topic subscribers (fanout peers) - No peer scoring or advanced security features - Suitable for trusted networks with low adversarial activity @@ -113,6 +118,9 @@ python gossipsub_v2.0.py --nodes 5 --duration 60 - P3: Mesh message deliveries - P4: Invalid messages penalty - **Behavioral Penalties**: P5 global behavior penalty +- **P6 (Application-specific score)** and **P7 (IP colocation factor)** +- **Prune backoff** and **peer exchange (PX)** enabled +- Optional **application score function** (e.g. staking/reputation, validator/full node role) - **Signed Peer Records**: Enhanced peer exchange with signed records - Better resilience against basic attacks diff --git a/examples/pubsub/gossipsub/gossipsub_v1.0.py b/examples/pubsub/gossipsub/gossipsub_v1.0.py index f7da97a12..d5f0418b9 100755 --- a/examples/pubsub/gossipsub/gossipsub_v1.0.py +++ b/examples/pubsub/gossipsub/gossipsub_v1.0.py @@ -12,6 +12,8 @@ - Simple message flooding - Mesh topology maintenance - Message publishing and subscription +- Fanout behaviour: a publisher that is not in the mesh (e.g. does not subscribe) + sends to a random set of topic subscribers (fanout peers) instead of mesh peers Usage: python gossipsub_v1.0.py --nodes 5 --duration 30 @@ -49,9 +51,10 @@ class GossipsubV10Node: """A node running Gossipsub 1.0""" - def __init__(self, node_id: str, port: int): + def __init__(self, node_id: str, port: int, fanout_only: bool = False): self.node_id = node_id self.port = port + self.fanout_only = fanout_only # If True, node only publishes (no subscribe) self.host: IHost | None = None self.pubsub: Pubsub | None = None self.gossipsub: GossipSub | None = None @@ -93,10 +96,12 @@ async def start(self): async with background_trio_service(self.pubsub): async with background_trio_service(self.gossipsub): await self.pubsub.wait_until_ready() - self.subscription = await self.pubsub.subscribe(TOPIC) + if not self.fanout_only: + self.subscription = await self.pubsub.subscribe(TOPIC) logger.info( - f"Node {self.node_id} (Gossipsub 1.0) started on port " - f"{self.port}" + f"Node {self.node_id} (Gossipsub 1.0" + + (", fanout-only publisher" if self.fanout_only else "") + + f") started on port {self.port}" ) # Keep running @@ -150,13 +155,21 @@ def __init__(self): self.nodes: list[GossipsubV10Node] = [] async def setup_network(self, node_count: int = 5): - """Set up a network of nodes""" + """ + Set up a network of nodes. Node 0 is a + fanout-only publisher (no subscribe). + """ for i in range(node_count): port = find_free_port() - node = GossipsubV10Node(f"node_{i}", port) + fanout_only = i == 0 + node = GossipsubV10Node(f"node_{i}", port, fanout_only=fanout_only) self.nodes.append(node) - logger.info(f"Created network with {node_count} nodes running Gossipsub 1.0") + logger.info( + f"Created network with {node_count} nodes running Gossipsub 1.0 " + f"(node_0 is fanout-only: publishes without subscribing, " + f"using fanout peers)" + ) async def start_network(self, duration: int = 30): """Start all nodes and run the demo""" @@ -186,11 +199,12 @@ async def start_network(self, duration: int = 30): print(f"{'=' * 60}") print(f"Running for {duration} seconds...") print("Protocol: /meshsub/1.0.0") - print("Features: Basic mesh-based pubsub, simple flooding") + print("Features: Basic mesh-based pubsub, simple flooding, fanout demo") + print(" (node_0 is fanout-only: publishes via fanout, not in mesh)") print(f"{'=' * 60}\n") while time.time() < end_time: - # Random node publishes a message + # Random node publishes (node_0 uses fanout when it publishes) node = random.choice(self.nodes) message = f"msg_{message_counter}_{int(time.time())}" await node.publish_message(message) @@ -256,6 +270,8 @@ def _print_statistics(self): print(" โœ“ Basic mesh-based pubsub") print(" โœ“ Simple message flooding") print(" โœ“ Mesh topology maintenance") + print(" โœ“ Fanout behaviour: node_0 publishes without subscribing;") + print(" messages are sent to a random set of topic peers (fanout peers)") print(" โœ— No peer scoring") print(" โœ— No IDONTWANT support") print(" โœ— No adaptive gossip") diff --git a/examples/pubsub/gossipsub/gossipsub_v1.1.py b/examples/pubsub/gossipsub/gossipsub_v1.1.py index 4cddf18d1..8c0b00f28 100755 --- a/examples/pubsub/gossipsub/gossipsub_v1.1.py +++ b/examples/pubsub/gossipsub/gossipsub_v1.1.py @@ -10,6 +10,9 @@ - Basic mesh-based pubsub (from v1.0) - Peer scoring with P1-P4 topic-scoped parameters - Behavioral penalties (P5) +- P6 (application-specific score) and P7 (IP colocation factor) +- Prune backoff and peer exchange (PX) enabled +- Optional application score function (e.g. staking/reputation, validator role) - Signed peer records - Better resilience against attacks @@ -28,6 +31,7 @@ from libp2p.abc import IHost, ISubscriptionAPI from libp2p.crypto.rsa import create_new_key_pair from libp2p.custom_types import TProtocol +from libp2p.peer.id import ID from libp2p.pubsub.gossipsub import GossipSub from libp2p.pubsub.pubsub import Pubsub from libp2p.pubsub.score import ScoreParams, TopicScoreParams @@ -53,7 +57,7 @@ class GossipsubV11Node: def __init__(self, node_id: str, port: int, role: str = "honest"): self.node_id = node_id self.port = port - self.role = role # "honest" or "malicious" + self.role = role # "honest", "malicious", or "validator" self.host: IHost | None = None self.pubsub: Pubsub | None = None self.gossipsub: GossipSub | None = None @@ -70,7 +74,7 @@ async def start(self): muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, ) - # Configure Gossipsub 1.1 - adds peer scoring + # Configure Gossipsub 1.1 - adds peer scoring, P6/P7, prune backoff, PX score_params = ScoreParams( # Topic-scoped parameters (P1-P4) p1_time_in_mesh=TopicScoreParams(weight=0.1, cap=10.0, decay=0.99), @@ -84,6 +88,17 @@ async def start(self): # Global behavioral penalty (P5) p5_behavior_penalty_weight=1.0, p5_behavior_penalty_decay=0.99, + # P6: application-specific score (optional; + # in production: staking/reputation) + p6_appl_slack_weight=0.1, + p6_appl_slack_decay=0.99, + # P7: IP colocation factor - penalise many peers from same IP + p7_ip_colocation_weight=0.5, + p7_ip_colocation_threshold=3, + # Optional application score: + # e.g. validator/full node role, + # stake, reputation + app_specific_score_fn=self._application_score_function, ) self.gossipsub = GossipSub( @@ -94,6 +109,10 @@ async def start(self): heartbeat_interval=5, heartbeat_initial_delay=1.0, score_params=score_params, + do_px=True, + px_peers_count=16, + prune_back_off=60, + unsubscribe_back_off=10, # No max_idontwant_messages - v1.1 doesn't support IDONTWANT # No adaptive features - v1.1 doesn't have adaptive gossip # No advanced security features - v1.1 has basic security @@ -119,6 +138,13 @@ async def start(self): # Keep running await trio.sleep_forever() + def _application_score_function(self, peer_id: ID) -> float: + """ + Optional application-specific score (P6). In real applications this could + be based on staking, reputation, or role in the network (validator, full node). + """ + return 0.0 + async def publish_message(self, message: str): """Publish a message to the topic""" if self.pubsub: @@ -206,7 +232,10 @@ async def start_network(self, duration: int = 30): print(f"{'=' * 60}") print(f"Running for {duration} seconds...") print("Protocol: /meshsub/1.1.0") - print("Features: Peer scoring (P1-P5), behavioral penalties") + print( + "Features: Peer scoring (P1-P7), prune backoff, peer exchange (PX)," + "optional app score" + ) print(f"{'=' * 60}\n") while time.time() < end_time: @@ -298,6 +327,9 @@ def _print_statistics(self): print(" - P3: Mesh message deliveries") print(" - P4: Invalid messages penalty") print(" โœ“ Behavioral penalties (P5)") + print(" โœ“ P6: Application-specific score (optional; e.g. staking/role)") + print(" โœ“ P7: IP colocation factor (Behavioural Penalty)") + print(" โœ“ Prune backoff and peer exchange (PX) enabled") print(" โœ“ Signed peer records") print(" โœ— No IDONTWANT support") print(" โœ— No adaptive gossip") From c2f0375466d2c7130b3d29225f749bfdd4d04c15 Mon Sep 17 00:00:00 2001 From: Winter-Soren Date: Thu, 2 Apr 2026 01:04:53 +0530 Subject: [PATCH 5/9] added gossipsub v1.3 example --- examples/pubsub/gossipsub/README.md | 52 +- examples/pubsub/gossipsub/gossipsub_v1.3.py | 542 ++++++++++++++++++++ 2 files changed, 592 insertions(+), 2 deletions(-) create mode 100644 examples/pubsub/gossipsub/gossipsub_v1.3.py diff --git a/examples/pubsub/gossipsub/README.md b/examples/pubsub/gossipsub/README.md index 1de9a6ecd..5abdafb35 100644 --- a/examples/pubsub/gossipsub/README.md +++ b/examples/pubsub/gossipsub/README.md @@ -4,11 +4,12 @@ This directory contains comprehensive examples showcasing the differences betwee ## Overview -With the recent implementation of Gossipsub 2.0 support in py-libp2p, we now have full protocol version support spanning: +Py-libp2p has full protocol version support spanning: - **Gossipsub 1.0** (`/meshsub/1.0.0`) - Basic mesh-based pubsub - **Gossipsub 1.1** (`/meshsub/1.1.0`) - Added peer scoring and behavioral penalties - **Gossipsub 1.2** (`/meshsub/1.2.0`) - Added IDONTWANT message filtering +- **Gossipsub 1.3** (`/meshsub/1.3.0`) - Extensions Control Message and Topic Observation - **Gossipsub 2.0** (`/meshsub/2.0.0`) - Enhanced security, adaptive gossip, and advanced peer scoring ## Examples @@ -66,7 +67,39 @@ Demonstrates Gossipsub 1.2 (`/meshsub/1.2.0`) with IDONTWANT message filtering. python gossipsub_v1.2.py --nodes 5 --duration 30 ``` -### 4. Gossipsub 2.0 Demo (`gossipsub_v2.0.py`) +### 4. Gossipsub 1.3 Demo (`gossipsub_v1.3.py`) + +Demonstrates GossipSub 1.3 (`/meshsub/1.3.0`) with the Extensions Control Message +mechanism and the Topic Observation extension. + +**Features:** + +- All GossipSub 1.2 features (IDONTWANT, peer scoring) +- **Extensions Control Message**: sent exactly once in the first message per peer +- **At-most-once enforcement**: duplicate Extensions from a peer triggers a score penalty +- **Topic Observation**: observer nodes receive IHAVE presence notifications without + full message payloads +- **Protocol gating**: extension fields are only attached when `/meshsub/1.3.0` is negotiated + +**Node roles in the demo:** + +| Role | Behaviour | +| ------------ | --------------------------------------------------------- | +| `publisher` | Subscribes and publishes messages every 2 seconds | +| `subscriber` | Subscribes and reads full message payloads | +| `observer` | Uses Topic Observation (IHAVE-only); no full subscription | + +At the halfway point one observer sends UNOBSERVE to stop receiving notifications, +demonstrating the full OBSERVE โ†’ IHAVE โ†’ UNOBSERVE lifecycle. + +**Usage:** + +```bash +python gossipsub_v1.3.py --nodes 6 --duration 40 +python gossipsub_v1.3.py --nodes 6 --duration 40 --verbose +``` + +### 5. Gossipsub 2.0 Demo (`gossipsub_v2.0.py`) Demonstrates Gossipsub 2.0 (`/meshsub/2.0.0`) with adaptive gossip and advanced security features. @@ -131,6 +164,17 @@ python gossipsub_v2.0.py --nodes 5 --duration 60 - **Improved Efficiency**: Lower bandwidth usage in dense networks - All v1.1 features included +### Gossipsub 1.3 (`/meshsub/1.3.0`) + +- **Extensions Control Message**: carried in the first RPC on a stream, at most once per peer +- **Misbehaviour Scoring**: duplicate Extensions messages trigger `scorer.penalize_behavior` +- **Topic Observation**: `start_observing_topic` / `stop_observing_topic` API; observers + receive IHAVE notifications immediately on publish without fetching full payloads +- **Protocol Gating**: extension fields are injected only when the negotiated protocol + is `/meshsub/1.3.0` or later +- **Forward Compatibility**: unknown extension fields are silently ignored by decoders +- All v1.2 features included + ### Gossipsub 2.0 (`/meshsub/2.0.0`) - **Enhanced Peer Scoring**: P6 (application-specific) and P7 (IP colocation) parameters @@ -196,6 +240,7 @@ cd examples/pubsub/gossipsub python gossipsub_v1.0.py --nodes 5 --duration 30 python gossipsub_v1.1.py --nodes 5 --duration 30 python gossipsub_v1.2.py --nodes 5 --duration 30 +python gossipsub_v1.3.py --nodes 6 --duration 40 python gossipsub_v2.0.py --nodes 5 --duration 60 ``` @@ -281,6 +326,7 @@ Enable verbose logging for detailed information: python gossipsub_v1.0.py --verbose ... python gossipsub_v1.1.py --verbose ... python gossipsub_v1.2.py --verbose ... +python gossipsub_v1.3.py --verbose ... python gossipsub_v2.0.py --verbose ... ``` @@ -288,4 +334,6 @@ python gossipsub_v2.0.py --verbose ... - [Gossipsub v1.1 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md) - [Gossipsub v1.2 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.2.md) +- [Gossipsub v1.3 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.3.md) +- [Topic Observation proposal (ethresearch)](https://ethresear.ch/t/gossipsub-topic-observation-proposed-gossipsub-1-3/20907) - [Gossipsub v2.0 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v2.0.md) diff --git a/examples/pubsub/gossipsub/gossipsub_v1.3.py b/examples/pubsub/gossipsub/gossipsub_v1.3.py new file mode 100644 index 000000000..ea51ee62f --- /dev/null +++ b/examples/pubsub/gossipsub/gossipsub_v1.3.py @@ -0,0 +1,542 @@ +#!/usr/bin/env python3 +""" +GossipSub 1.3 Example + +This example demonstrates GossipSub v1.3 protocol (/meshsub/1.3.0). +GossipSub 1.3 adds the Extensions Control Message mechanism and the +Topic Observation extension. + +Features demonstrated: +- All GossipSub 1.2 features (IDONTWANT, peer scoring, etc.) +- GossipSub v1.3 Extensions Control Message (sent once, at most once per peer) +- Topic Observation: observer nodes receive IHAVE notifications without full + message payloads, enabling lightweight presence awareness +- Misbehaviour detection: duplicate Extensions messages from a peer are penalised +- Protocol gating: extension fields are only sent when /meshsub/1.3.0 is negotiated + +Roles in this demo: + - publisher : subscribes to the topic and publishes messages every few seconds + - subscriber : subscribes to the topic and reads full message payloads + - observer : starts observing (IHAVE-only) without subscribing; tracks presence + +Usage: + python gossipsub_v1.3.py --nodes 6 --duration 40 + python gossipsub_v1.3.py --nodes 6 --duration 40 --verbose +""" + +import argparse +import logging +import random +import time + +import trio + +from libp2p import new_host +from libp2p.abc import IHost, ISubscriptionAPI +from libp2p.crypto.rsa import create_new_key_pair +from libp2p.pubsub.extensions import PeerExtensions +from libp2p.pubsub.gossipsub import PROTOCOL_ID_V13, GossipSub +from libp2p.pubsub.pubsub import Pubsub +from libp2p.pubsub.score import ScoreParams, TopicScoreParams +from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex +from libp2p.tools.anyio_service import background_trio_service +from libp2p.utils.address_validation import find_free_port + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger("gossipsub-v1.3") + +TOPIC = "gossipsub-v1.3-demo" + + +class GossipsubV13Node: + """ + A node running GossipSub v1.3. + + Each node has one of three roles: + - "publisher" โ€“ subscribes and publishes messages + - "subscriber" โ€“ subscribes and reads messages (no publishing) + - "observer" โ€“ uses Topic Observation to receive IHAVE presence + notifications without a full subscription + """ + + def __init__(self, node_id: str, port: int, role: str = "publisher"): + self.node_id = node_id + self.port = port + self.role = role + + self.host: IHost | None = None + self.pubsub: Pubsub | None = None + self.gossipsub: GossipSub | None = None + self.subscription: ISubscriptionAPI | None = None + + self.messages_sent = 0 + self.messages_received = 0 + self.ihave_notifications = 0 # reserved for future explicit IHAVE hooks + + async def start(self) -> None: + """Initialise the libp2p host and GossipSub v1.3 router.""" + import multiaddr + + key_pair = create_new_key_pair() + + self.host = new_host( + key_pair=key_pair, + muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, + ) + + score_params = ScoreParams( + p1_time_in_mesh=TopicScoreParams(weight=0.1, cap=10.0, decay=0.99), + p2_first_message_deliveries=TopicScoreParams( + weight=0.5, cap=20.0, decay=0.99 + ), + p3_mesh_message_deliveries=TopicScoreParams( + weight=0.3, cap=10.0, decay=0.99 + ), + p4_invalid_messages=TopicScoreParams(weight=-1.0, cap=50.0, decay=0.99), + p5_behavior_penalty_weight=1.0, + p5_behavior_penalty_decay=0.99, + ) + + # Advertise GossipSub v1.3 extensions: both Topic Observation and the + # interop test extension (testExtension) are enabled for all nodes. + my_extensions = PeerExtensions( + topic_observation=True, + test_extension=True, + ) + + self.gossipsub = GossipSub( + protocols=[PROTOCOL_ID_V13], + degree=3, + degree_low=2, + degree_high=4, + heartbeat_interval=5, + heartbeat_initial_delay=1.0, + score_params=score_params, + max_idontwant_messages=20, + # GossipSub v1.3: advertise our supported extensions in the first + # message on every new stream (enforced at-most-once per peer). + my_extensions=my_extensions, + ) + + self.pubsub = Pubsub(self.host, self.gossipsub) + + listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{self.port}")] + + async with self.host.run(listen_addrs=listen_addrs): + async with background_trio_service(self.pubsub): + async with background_trio_service(self.gossipsub): + await self.pubsub.wait_until_ready() + + if self.role in ("publisher", "subscriber"): + self.subscription = await self.pubsub.subscribe(TOPIC) + logger.info( + "[%s] subscribed to topic '%s' (full payload mode)", + self.node_id, + TOPIC, + ) + + logger.info( + "Node %s started | role=%s | port=%d | %s", + self.node_id, + self.role, + self.port, + self.extensions_summary(), + ) + await trio.sleep_forever() + + async def publish_message(self, message: str) -> None: + """Publish a message to the topic (publisher role only).""" + if self.pubsub and self.role == "publisher": + await self.pubsub.publish(TOPIC, message.encode()) + self.messages_sent += 1 + logger.info( + "[PUBLISH] node=%s topic=%s payload=%s", + self.node_id, + TOPIC, + message, + ) + + async def receive_messages(self) -> None: + """Read full message payloads (subscriber / publisher roles).""" + if not self.subscription: + return + try: + while True: + message = await self.subscription.get() + decoded = message.data.decode("utf-8") + self.messages_received += 1 + logger.info( + "[RECEIVE] node=%s role=%s payload=%s", + self.node_id, + self.role, + decoded, + ) + except Exception as exc: + logger.debug("Node %s receive loop ended: %s", self.node_id, exc) + + async def start_observing(self) -> None: + """ + Start Topic Observation for TOPIC (observer role). + + This sends OBSERVE control messages to in-mesh peers that also + advertised Topic Observation support. From that point on those peers + will forward IHAVE presence notifications to us whenever a new message + arrives on TOPIC, without sending the full payload. + """ + if self.gossipsub and self.role == "observer": + await self.gossipsub.start_observing_topic(TOPIC) + logger.info( + "[OBSERVE-START] node=%s topic=%s observing_topics=%s", + self.node_id, + TOPIC, + sorted(self.gossipsub.topic_observation.get_observing_topics()), + ) + + async def stop_observing(self) -> None: + """ + Stop Topic Observation for TOPIC (observer role). + + Sends UNOBSERVE control messages to the peers we were observing through. + """ + if self.gossipsub and self.role == "observer": + await self.gossipsub.stop_observing_topic(TOPIC) + logger.info( + "[OBSERVE-STOP] node=%s topic=%s observing_topics=%s", + self.node_id, + TOPIC, + sorted(self.gossipsub.topic_observation.get_observing_topics()), + ) + + async def connect_to_peer(self, peer_addr: str) -> None: + """Connect to a remote peer by multiaddr.""" + if not self.host: + return + try: + import multiaddr + + from libp2p.peer.peerinfo import info_from_p2p_addr + + maddr = multiaddr.Multiaddr(peer_addr) + info = info_from_p2p_addr(maddr) + await self.host.connect(info) + logger.info( + "[CONNECT] %s -> %s", + self.node_id, + peer_addr, + ) + except Exception as exc: + logger.debug( + "Node %s failed to connect to %s: %s", self.node_id, peer_addr, exc + ) + + def extensions_summary(self) -> str: + """Return a human-readable summary of negotiated extensions.""" + if not self.gossipsub: + return "not started" + ext_state = self.gossipsub.extensions_state + my = ext_state.my_extensions + return ( + f"advertised=(topic_observation={my.topic_observation}, " + f"test_extension={my.test_extension})" + ) + + +class GossipsubV13Demo: + """ + Demo controller that sets up a mixed network of publishers, subscribers, + and observers and runs them for a configurable duration. + + Network layout (with default --nodes 6): + nodes 0-1 โ†’ publisher + nodes 2-3 โ†’ subscriber + nodes 4-5 โ†’ observer + """ + + def __init__(self) -> None: + self.nodes: list[GossipsubV13Node] = [] + + async def setup_network(self, node_count: int = 6) -> None: + """Allocate ports and create nodes with their roles.""" + roles = _assign_roles(node_count) + for i in range(node_count): + port = find_free_port() + node = GossipsubV13Node(f"node_{i}", port, roles[i]) + self.nodes.append(node) + + role_counts = {r: roles.count(r) for r in set(roles)} + logger.info( + "Created %d-node GossipSub v1.3 network: %s", + node_count, + role_counts, + ) + for node in self.nodes: + logger.info( + "[PLAN] node=%s role=%s listen=/ip4/127.0.0.1/tcp/%d", + node.node_id, + node.role, + node.port, + ) + + async def start_network(self, duration: int = 40) -> None: + """Start all nodes, wire them together, then run the demo loop.""" + try: + async with trio.open_nursery() as nursery: + # Boot all nodes concurrently. + for node in self.nodes: + nursery.start_soon(node.start) + + # Give nodes time to bind their listening ports. + await trio.sleep(3) + logger.info("[STAGE] all node services started") + + # Wire a ring + chord topology so every node has โ‰ฅ2 peers. + logger.info("[STAGE] wiring peer connections (ring + skip links)") + await self._connect_nodes() + await trio.sleep(2) + logger.info("[STAGE] peer wiring complete") + self._log_protocol_snapshot("after-connect") + + # Start receive loops for publishers and subscribers. + for node in self.nodes: + if node.role in ("publisher", "subscriber"): + nursery.start_soon(node.receive_messages) + logger.info("[STAGE] receive loops started (publishers/subscribers)") + + # Observer nodes start Topic Observation after wiring. + for node in self.nodes: + if node.role == "observer": + await node.start_observing() + self._log_observer_snapshot("after-observe-start") + + # Print banner. + _print_banner(duration) + + end_time = time.time() + duration + message_counter = 0 + half_time = duration // 2 + heartbeat_step = 0 + midpoint_unobserve_done = False + + while time.time() < end_time: + elapsed = duration - (end_time - time.time()) + + # Publishers take turns sending a message. + publishers = [n for n in self.nodes if n.role == "publisher"] + if publishers: + node = random.choice(publishers) + msg = f"msg_{message_counter}_{int(time.time())}" + await node.publish_message(msg) + message_counter += 1 + + # Halfway through, stop one observer to show UNOBSERVE. + if elapsed >= half_time and not midpoint_unobserve_done: + for node in self.nodes: + if ( + node.role == "observer" + and node.gossipsub is not None + and node.gossipsub.topic_observation.is_observing(TOPIC) + ): + await node.stop_observing() + self._log_observer_snapshot("after-midpoint-unobserve") + midpoint_unobserve_done = True + break # stop only the first one + + # Emit periodic runtime snapshots so demos are easier to narrate. + heartbeat_step += 1 + self._log_runtime_snapshot(heartbeat_step, elapsed) + await trio.sleep(2) + + await trio.sleep(1) + self._log_protocol_snapshot("final") + self._print_statistics() + nursery.cancel_scope.cancel() + + except Exception as exc: + logger.warning("Demo interrupted: %s", exc) + + async def _connect_nodes(self) -> None: + """Connect nodes in a ring + one-hop-skip topology.""" + n = len(self.nodes) + for i, node in enumerate(self.nodes): + for offset in (1, 2): + target = self.nodes[(i + offset) % n] + if target.host and node.host: + peer_addr = ( + f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + ) + await node.connect_to_peer(peer_addr) + logger.info("[STAGE] requested all topology connections") + + def _log_runtime_snapshot(self, tick: int, elapsed: float) -> None: + publishers = [n for n in self.nodes if n.role == "publisher"] + subscribers = [n for n in self.nodes if n.role == "subscriber"] + observers = [n for n in self.nodes if n.role == "observer"] + published = sum(n.messages_sent for n in publishers) + delivered = sum(n.messages_received for n in publishers + subscribers) + active_observers = sum( + 1 + for n in observers + if n.gossipsub is not None + and n.gossipsub.topic_observation.is_observing(TOPIC) + ) + logger.info( + "[SNAPSHOT t+%ds #%d] published=%d delivered=%d active_observers=%d/%d", + int(elapsed), + tick, + published, + delivered, + active_observers, + len(observers), + ) + + def _log_observer_snapshot(self, label: str) -> None: + for node in self.nodes: + if node.role != "observer" or node.gossipsub is None: + continue + observing_topics = sorted( + node.gossipsub.topic_observation.get_observing_topics() + ) + logger.info( + "[OBSERVER-SNAPSHOT:%s] node=%s observing_topics=%s", + label, + node.node_id, + observing_topics, + ) + + def _log_protocol_snapshot(self, label: str) -> None: + for node in self.nodes: + if node.gossipsub is None: + continue + router = node.gossipsub + peers_total = len(router.peer_protocol) + v13_peers = sum( + 1 for pid in router.peer_protocol if router.supports_v13_features(pid) + ) + ext_known = sum( + 1 + for pid in router.peer_protocol + if router.extensions_state.get_peer_extensions(pid) is not None + ) + topic_observation_peers = sum( + 1 + for pid in router.peer_protocol + if router.extensions_state.peer_supports_topic_observation(pid) + ) + logger.info( + "[PROTO-SNAPSHOT:%s] node=%s peers=%d v13=%d ext_known=%d topic_obs=%d", + label, + node.node_id, + peers_total, + v13_peers, + ext_known, + topic_observation_peers, + ) + + def _print_statistics(self) -> None: + """Print a summary table at the end of the demo.""" + print(f"\n{'=' * 65}") + print("DEMO STATISTICS") + print(f"{'=' * 65}") + + total_sent = sum(n.messages_sent for n in self.nodes) + total_received = sum(n.messages_received for n in self.nodes) + + print(f"Total messages published : {total_sent}") + print(f"Total messages received : {total_received}") + print() + print(f"{'Node':<10} {'Role':<12} {'Sent':>6} {'Recv':>6} Extensions") + print(f"{'-' * 65}") + for node in self.nodes: + print( + f"{node.node_id:<10} {node.role:<12} " + f"{node.messages_sent:>6} {node.messages_received:>6} " + f"{node.extensions_summary()}" + ) + + print(f"\n{'=' * 65}") + print("GossipSub 1.3 Features demonstrated:") + print(" + Extensions Control Message (first message, at most once per peer)") + print(" + Topic Observation (IHAVE presence notifications without payloads)") + print(" + Misbehaviour scoring on duplicate Extensions messages") + print(" + Protocol gating (/meshsub/1.3.0 only)") + print(" + All GossipSub 1.2 features (IDONTWANT, peer scoring, etc.)") + print(f"{'=' * 65}\n") + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _assign_roles(node_count: int) -> list[str]: + """ + Distribute roles across nodes. + + With 6 nodes the split is 2 publishers / 2 subscribers / 2 observers. + Smaller counts fall back gracefully. + """ + if node_count < 3: + return ["publisher"] * node_count + publishers = max(1, node_count // 3) + observers = max(1, node_count // 3) + subscribers = node_count - publishers - observers + return ( + ["publisher"] * publishers + + ["subscriber"] * subscribers + + ["observer"] * observers + ) + + +def _print_banner(duration: int) -> None: + print(f"\n{'=' * 65}") + print("GOSSIPSUB 1.3 DEMO") + print(f"{'=' * 65}") + print("Protocol : /meshsub/1.3.0") + print(f"Duration : {duration} seconds") + print("Features : Extensions Control Message, Topic Observation,") + print(" IDONTWANT filtering, peer scoring") + print() + print("Roles:") + print(" publisher โ€“ subscribes + publishes messages") + print(" subscriber โ€“ subscribes and reads payloads") + print(" observer โ€“ Topic Observation only (IHAVE-only, no payload)") + print() + print( + f"At t={duration // 2}s one observer will send UNOBSERVE to stop " + "receiving notifications." + ) + print(f"{'=' * 65}\n") + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +async def main() -> None: + parser = argparse.ArgumentParser(description="GossipSub 1.3 Example") + parser.add_argument( + "--nodes", type=int, default=6, help="Total number of nodes (default: 6)" + ) + parser.add_argument( + "--duration", + type=int, + default=40, + help="Demo duration in seconds (default: 40)", + ) + parser.add_argument("--verbose", action="store_true", help="Enable DEBUG logging") + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + demo = GossipsubV13Demo() + await demo.setup_network(args.nodes) + await demo.start_network(args.duration) + + +if __name__ == "__main__": + trio.run(main) From 41e5feaed77eed22e636e21764ff8b7e4461f24b Mon Sep 17 00:00:00 2001 From: Winter-Soren Date: Thu, 2 Apr 2026 22:52:28 +0530 Subject: [PATCH 6/9] added gossipsub v1.4 example --- examples/pubsub/gossipsub/README.md | 66 ++- examples/pubsub/gossipsub/gossipsub_v1.4.py | 561 ++++++++++++++++++++ 2 files changed, 626 insertions(+), 1 deletion(-) create mode 100644 examples/pubsub/gossipsub/gossipsub_v1.4.py diff --git a/examples/pubsub/gossipsub/README.md b/examples/pubsub/gossipsub/README.md index 5abdafb35..13b612947 100644 --- a/examples/pubsub/gossipsub/README.md +++ b/examples/pubsub/gossipsub/README.md @@ -10,6 +10,7 @@ Py-libp2p has full protocol version support spanning: - **Gossipsub 1.1** (`/meshsub/1.1.0`) - Added peer scoring and behavioral penalties - **Gossipsub 1.2** (`/meshsub/1.2.0`) - Added IDONTWANT message filtering - **Gossipsub 1.3** (`/meshsub/1.3.0`) - Extensions Control Message and Topic Observation +- **Gossipsub 1.4** (`/meshsub/1.4.0`) - Rate limiting, GRAFT flood protection, and adaptive gossip - **Gossipsub 2.0** (`/meshsub/2.0.0`) - Enhanced security, adaptive gossip, and advanced peer scoring ## Examples @@ -99,7 +100,48 @@ python gossipsub_v1.3.py --nodes 6 --duration 40 python gossipsub_v1.3.py --nodes 6 --duration 40 --verbose ``` -### 5. Gossipsub 2.0 Demo (`gossipsub_v2.0.py`) +### 5. Gossipsub 1.4 Demo (`gossipsub_v1.4.py`) + +Demonstrates GossipSub 1.4 (`/meshsub/1.4.0`) with enhanced rate limiting, GRAFT flood +protection, and adaptive gossip parameter tuning. + +**Features:** + +- All GossipSub 1.3 features (Extensions Control Message, Topic Observation) +- **IWANT request rate limiting** per peer โ€“ prevents IWANT spam storms; excess requests + trigger a `penalize_iwant_spam` score deduction +- **IHAVE message rate limiting** per peer per topic โ€“ caps IHAVE floods; + excess triggers `penalize_ihave_spam` +- **GRAFT flood protection** โ€“ a peer that sends GRAFT too soon after receiving PRUNE + is penalised via `penalize_graft_flood` and immediately re-pruned +- **Adaptive gossip factor** โ€“ the gossip spread factor scales with a multi-metric + network health score (connectivity, peer scores, delivery rate, mesh stability, churn) +- **Opportunistic grafting threshold adaptation** โ€“ in poor health the threshold + is lowered so high-quality peers are grafted more aggressively +- **Heartbeat interval adaptation** โ€“ under critical health the heartbeat is + accelerated for faster mesh recovery +- **Extended scoring (P5-P7)** gated to `/meshsub/1.4.0` + +**Node roles in the demo:** + +| Role | Behaviour | +| ----------- | ------------------------------------------------------------------ | +| `honest` | Subscribes and publishes at a normal cadence | +| `validator` | Subscribes and reads messages; no publishing | +| `spammer` | Bursts rapid messages to trigger IWANT / IHAVE rate-limiting paths | +| `observer` | Uses Topic Observation (IHAVE-only, inherited from v1.3) | + +At `t = duration/4` the spammer fires a burst of 12 messages in rapid succession to +demonstrate rate-limit enforcement. At `t = duration/2` one observer sends UNOBSERVE. + +**Usage:** + +```bash +python gossipsub_v1.4.py --nodes 6 --duration 40 +python gossipsub_v1.4.py --nodes 6 --duration 40 --verbose +``` + +### 6. Gossipsub 2.0 Demo (`gossipsub_v2.0.py`) Demonstrates Gossipsub 2.0 (`/meshsub/2.0.0`) with adaptive gossip and advanced security features. @@ -175,6 +217,25 @@ python gossipsub_v2.0.py --nodes 5 --duration 60 - **Forward Compatibility**: unknown extension fields are silently ignored by decoders - All v1.2 features included +### Gossipsub 1.4 (`/meshsub/1.4.0`) + +- **IWANT Rate Limiting**: caps IWANT requests per peer per second; excess triggers + `penalize_iwant_spam` score deduction +- **IHAVE Rate Limiting**: caps IHAVE messages per peer per topic per second; excess + triggers `penalize_ihave_spam` +- **GRAFT Flood Protection**: peers that re-GRAFT before the backoff window expires + receive `penalize_graft_flood` and are immediately re-pruned +- **Adaptive Gossip Factor**: gossip spread factor (`gossip_factor`) scales dynamically + based on a composite network health score (mesh connectivity, peer scores, delivery + rate, mesh stability, and connection churn) +- **Opportunistic Grafting Threshold Adaptation**: lower threshold in poor health + promotes more aggressive peer selection for mesh repair +- **Heartbeat Interval Adaptation**: heartbeat interval shrinks under critical health + for faster mesh recovery, expanding again once health improves +- **Extended Scoring (P5-P7) Gate**: `supports_protocol_feature(peer, "extended_scoring")` + returns `True` only for `/meshsub/1.4.0` +- All v1.3 features included + ### Gossipsub 2.0 (`/meshsub/2.0.0`) - **Enhanced Peer Scoring**: P6 (application-specific) and P7 (IP colocation) parameters @@ -241,6 +302,7 @@ python gossipsub_v1.0.py --nodes 5 --duration 30 python gossipsub_v1.1.py --nodes 5 --duration 30 python gossipsub_v1.2.py --nodes 5 --duration 30 python gossipsub_v1.3.py --nodes 6 --duration 40 +python gossipsub_v1.4.py --nodes 6 --duration 40 python gossipsub_v2.0.py --nodes 5 --duration 60 ``` @@ -327,6 +389,7 @@ python gossipsub_v1.0.py --verbose ... python gossipsub_v1.1.py --verbose ... python gossipsub_v1.2.py --verbose ... python gossipsub_v1.3.py --verbose ... +python gossipsub_v1.4.py --verbose ... python gossipsub_v2.0.py --verbose ... ``` @@ -336,4 +399,5 @@ python gossipsub_v2.0.py --verbose ... - [Gossipsub v1.2 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.2.md) - [Gossipsub v1.3 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.3.md) - [Topic Observation proposal (ethresearch)](https://ethresear.ch/t/gossipsub-topic-observation-proposed-gossipsub-1-3/20907) +- [Gossipsub v1.4 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.4.md) - [Gossipsub v2.0 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v2.0.md) diff --git a/examples/pubsub/gossipsub/gossipsub_v1.4.py b/examples/pubsub/gossipsub/gossipsub_v1.4.py new file mode 100644 index 000000000..8ae4a83ed --- /dev/null +++ b/examples/pubsub/gossipsub/gossipsub_v1.4.py @@ -0,0 +1,561 @@ +#!/usr/bin/env python3 +""" +GossipSub 1.4 Example + +This example demonstrates GossipSub v1.4 protocol (/meshsub/1.4.0). +GossipSub 1.4 focuses on enhanced rate limiting, GRAFT flood protection, +and adaptive gossip parameter tuning based on network health metrics. + +Features demonstrated: +- All GossipSub 1.3 features (Extensions Control Message, Topic Observation) +- IWANT request rate limiting per peer (anti-spam) +- IHAVE message rate limiting per peer per topic (anti-spam) +- GRAFT flood protection with automatic score penalty +- Adaptive gossip factor based on network health score +- Opportunistic grafting threshold adaptation +- Heartbeat interval adaptation under poor network conditions +- Extended scoring (P5-P7) with v1.4 protocol gating + +Node roles in this demo: + - honest : publishes and receives messages normally + - spammer : sends rapid IWANT / IHAVE style spam to trigger rate limits + - observer : uses Topic Observation (IHAVE-only) inherited from v1.3 + - validator : receives messages and validates them; no publishing + +Usage: + python gossipsub_v1.4.py --nodes 6 --duration 40 + python gossipsub_v1.4.py --nodes 6 --duration 40 --verbose +""" + +import argparse +import logging +import random +import time + +import trio + +from libp2p import new_host +from libp2p.abc import IHost, ISubscriptionAPI +from libp2p.crypto.rsa import create_new_key_pair +from libp2p.pubsub.extensions import PeerExtensions +from libp2p.pubsub.gossipsub import PROTOCOL_ID_V14, GossipSub +from libp2p.pubsub.pubsub import Pubsub +from libp2p.pubsub.score import ScoreParams, TopicScoreParams +from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex +from libp2p.tools.anyio_service import background_trio_service +from libp2p.utils.address_validation import find_free_port + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger("gossipsub-v1.4") + +TOPIC = "gossipsub-v1.4-demo" + + +class GossipsubV14Node: + """ + A node running GossipSub v1.4. + + Roles: + - "honest" โ€“ subscribes and publishes messages at a normal rate + - "spammer" โ€“ sends messages rapidly to exercise rate limiting + - "observer" โ€“ uses Topic Observation (inherited from v1.3) + - "validator" โ€“ subscribes and receives messages, no publishing + """ + + def __init__(self, node_id: str, port: int, role: str = "honest"): + self.node_id = node_id + self.port = port + self.role = role + + self.host: IHost | None = None + self.pubsub: Pubsub | None = None + self.gossipsub: GossipSub | None = None + self.subscription: ISubscriptionAPI | None = None + + self.messages_sent = 0 + self.messages_received = 0 + self.rate_limit_hits = 0 + + async def start(self) -> None: + """Initialise the libp2p host and GossipSub v1.4 router.""" + import multiaddr + + key_pair = create_new_key_pair() + + self.host = new_host( + key_pair=key_pair, + muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, + ) + + # Full peer scoring: P1-P4 topic-scoped + P5 behavior + P6/P7 global. + score_params = ScoreParams( + p1_time_in_mesh=TopicScoreParams(weight=0.1, cap=10.0, decay=0.99), + p2_first_message_deliveries=TopicScoreParams( + weight=0.5, cap=20.0, decay=0.99 + ), + p3_mesh_message_deliveries=TopicScoreParams( + weight=0.3, cap=10.0, decay=0.99 + ), + p4_invalid_messages=TopicScoreParams(weight=-1.0, cap=50.0, decay=0.99), + p5_behavior_penalty_weight=1.0, + p5_behavior_penalty_decay=0.99, + ) + + # Advertise v1.3-compatible extensions (Topic Observation + test extension). + my_extensions = PeerExtensions( + topic_observation=True, + test_extension=True, + ) + + # v1.4-specific constructor parameters: + # max_iwant_requests_per_second โ€“ caps IWANT storm per peer + # max_ihave_messages_per_second โ€“ caps IHAVE flood per peer/topic + # graft_flood_threshold โ€“ minimum seconds between PRUNE and GRAFT + # adaptive_gossip_enabled โ€“ turn on health-based parameter adaptation + self.gossipsub = GossipSub( + protocols=[PROTOCOL_ID_V14], + degree=3, + degree_low=2, + degree_high=4, + heartbeat_interval=5, + heartbeat_initial_delay=1.0, + score_params=score_params, + max_idontwant_messages=20, + my_extensions=my_extensions, + # v1.4 rate limiting + adaptive_gossip_enabled=True, + ) + + # Override v1.4 rate limiting thresholds directly on the router so the + # demo can observe them being triggered with a small message volume. + self.gossipsub.max_iwant_requests_per_second = 5.0 + self.gossipsub.max_ihave_messages_per_second = 5.0 + self.gossipsub.graft_flood_threshold = 8.0 + + self.pubsub = Pubsub(self.host, self.gossipsub) + + listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{self.port}")] + + async with self.host.run(listen_addrs=listen_addrs): + async with background_trio_service(self.pubsub): + async with background_trio_service(self.gossipsub): + await self.pubsub.wait_until_ready() + + # observers use Topic Observation โ€“ no full subscription needed. + if self.role in ("honest", "spammer", "validator"): + self.subscription = await self.pubsub.subscribe(TOPIC) + logger.info( + "[%s] subscribed to topic '%s' role=%s", + self.node_id, + TOPIC, + self.role, + ) + + logger.info( + "Node %s started | role=%s | port=%d | protocol=%s", + self.node_id, + self.role, + self.port, + PROTOCOL_ID_V14, + ) + await trio.sleep_forever() + + async def publish_message(self, message: str) -> None: + """Publish a message to TOPIC.""" + if self.pubsub and self.role in ("honest", "spammer"): + await self.pubsub.publish(TOPIC, message.encode()) + self.messages_sent += 1 + logger.info( + "[PUBLISH] node=%s role=%s payload=%s", + self.node_id, + self.role, + message, + ) + + async def receive_messages(self) -> None: + """Drain the subscription queue and count deliveries.""" + if not self.subscription: + return + try: + while True: + message = await self.subscription.get() + decoded = message.data.decode("utf-8") + self.messages_received += 1 + logger.info( + "[RECEIVE] node=%s role=%s payload=%s", + self.node_id, + self.role, + decoded, + ) + except Exception as exc: + logger.debug("Node %s receive loop ended: %s", self.node_id, exc) + + async def start_observing(self) -> None: + """Start Topic Observation (observer role โ€“ inherited from v1.3).""" + if self.gossipsub and self.role == "observer": + await self.gossipsub.start_observing_topic(TOPIC) + logger.info( + "[OBSERVE-START] node=%s topic=%s", + self.node_id, + TOPIC, + ) + + async def stop_observing(self) -> None: + """Stop Topic Observation (observer role).""" + if self.gossipsub and self.role == "observer": + await self.gossipsub.stop_observing_topic(TOPIC) + logger.info( + "[OBSERVE-STOP] node=%s topic=%s", + self.node_id, + TOPIC, + ) + + async def connect_to_peer(self, peer_addr: str) -> None: + """Connect to a remote peer by multiaddr.""" + if not self.host: + return + try: + import multiaddr + + from libp2p.peer.peerinfo import info_from_p2p_addr + + maddr = multiaddr.Multiaddr(peer_addr) + info = info_from_p2p_addr(maddr) + await self.host.connect(info) + logger.info("[CONNECT] %s -> %s", self.node_id, peer_addr) + except Exception as exc: + logger.debug( + "Node %s failed to connect to %s: %s", + self.node_id, + peer_addr, + exc, + ) + + def v14_status(self) -> str: + """Return a one-line v1.4 feature status string for this node.""" + if not self.gossipsub: + return "not started" + gs = self.gossipsub + return ( + f"health={gs.network_health_score:.2f} " + f"gossip_factor={gs.gossip_factor:.2f} " + f"iwant_limit={gs.max_iwant_requests_per_second:.0f}/s " + f"ihave_limit={gs.max_ihave_messages_per_second:.0f}/s " + f"graft_flood_threshold={gs.graft_flood_threshold:.0f}s " + f"opp_graft_threshold={gs.opportunistic_graft_threshold:.2f}" + ) + + +class GossipsubV14Demo: + """ + Demo controller: sets up a mixed-role network and runs a scenario that + exercises all GossipSub v1.4 features. + + Network layout (default --nodes 6): + nodes 0-1 โ†’ honest + nodes 2-3 โ†’ validator + nodes 4 โ†’ spammer + nodes 5 โ†’ observer + """ + + def __init__(self) -> None: + self.nodes: list[GossipsubV14Node] = [] + + async def setup_network(self, node_count: int = 6) -> None: + """Allocate ports and create nodes with appropriate roles.""" + roles = _assign_roles(node_count) + for i in range(node_count): + port = find_free_port() + node = GossipsubV14Node(f"node_{i}", port, roles[i]) + self.nodes.append(node) + + role_counts = {r: roles.count(r) for r in set(roles)} + logger.info( + "Created %d-node GossipSub v1.4 network: %s", + node_count, + role_counts, + ) + for node in self.nodes: + logger.info( + "[PLAN] node=%s role=%s listen=/ip4/127.0.0.1/tcp/%d", + node.node_id, + node.role, + node.port, + ) + + async def start_network(self, duration: int = 40) -> None: + """Boot all nodes, wire topology, then run the demo loop.""" + try: + async with trio.open_nursery() as nursery: + # Boot every node concurrently. + for node in self.nodes: + nursery.start_soon(node.start) + + # Wait for all listening ports to bind. + await trio.sleep(3) + logger.info("[STAGE] all node services started") + + # Ring + chord topology (โ‰ฅ2 peers per node). + logger.info("[STAGE] wiring peer connections (ring + skip links)") + await self._connect_nodes() + await trio.sleep(2) + logger.info("[STAGE] peer wiring complete") + self._log_v14_snapshot("after-connect") + + # Start receive loops for subscribing roles. + for node in self.nodes: + if node.role in ("honest", "validator", "spammer"): + nursery.start_soon(node.receive_messages) + logger.info("[STAGE] receive loops started") + + # Observer nodes activate Topic Observation after wiring. + for node in self.nodes: + if node.role == "observer": + await node.start_observing() + + _print_banner(duration) + + end_time = time.time() + duration + msg_counter = 0 + tick = 0 + half_time = duration // 2 + spam_burst_done = False + unobserve_done = False + + while time.time() < end_time: + elapsed = duration - (end_time - time.time()) + + # โ”€โ”€ Honest nodes publish at a normal cadence โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + honest_nodes = [n for n in self.nodes if n.role == "honest"] + if honest_nodes: + sender = random.choice(honest_nodes) + msg = f"msg_{msg_counter}_{int(time.time())}" + await sender.publish_message(msg) + msg_counter += 1 + + # โ”€โ”€ Spammer: burst of rapid messages at the 1/4 mark โ”€โ”€โ”€โ”€โ”€ + # This exercises the IWANT / IHAVE rate limiting paths. + if elapsed >= duration // 4 and not spam_burst_done: + spammers = [n for n in self.nodes if n.role == "spammer"] + if spammers: + logger.info( + "[STAGE] triggering spammer burst (rate-limit demo)" + ) + for _ in range(12): + for spammer in spammers: + burst_msg = f"spam_{msg_counter}_{int(time.time())}" + await spammer.publish_message(burst_msg) + msg_counter += 1 + await trio.sleep(0.05) + spam_burst_done = True + logger.info( + "[STAGE] spammer burst complete โ€“ " + "check logs for rate-limit warnings" + ) + + # โ”€โ”€ Midpoint: stop one observer (UNOBSERVE demo) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if elapsed >= half_time and not unobserve_done: + for node in self.nodes: + if ( + node.role == "observer" + and node.gossipsub is not None + and node.gossipsub.topic_observation.is_observing(TOPIC) + ): + await node.stop_observing() + logger.info( + "[STAGE] midpoint UNOBSERVE sent by %s", + node.node_id, + ) + unobserve_done = True + break + + tick += 1 + self._log_runtime_snapshot(tick, elapsed) + await trio.sleep(2) + + await trio.sleep(1) + self._log_v14_snapshot("final") + self._print_statistics() + nursery.cancel_scope.cancel() + + except Exception as exc: + logger.warning("Demo interrupted: %s", exc) + + async def _connect_nodes(self) -> None: + """Connect nodes in a ring + one-hop-skip topology.""" + n = len(self.nodes) + for i, node in enumerate(self.nodes): + for offset in (1, 2): + target = self.nodes[(i + offset) % n] + if target.host and node.host: + peer_addr = ( + f"/ip4/127.0.0.1/tcp/{target.port}/p2p/{target.host.get_id()}" + ) + await node.connect_to_peer(peer_addr) + logger.info("[STAGE] requested all topology connections") + + def _log_runtime_snapshot(self, tick: int, elapsed: float) -> None: + honest = [n for n in self.nodes if n.role == "honest"] + validators = [n for n in self.nodes if n.role == "validator"] + spammers = [n for n in self.nodes if n.role == "spammer"] + observers = [n for n in self.nodes if n.role == "observer"] + + published = sum(n.messages_sent for n in honest + spammers) + delivered = sum(n.messages_received for n in honest + validators) + active_observers = sum( + 1 + for n in observers + if n.gossipsub is not None + and n.gossipsub.topic_observation.is_observing(TOPIC) + ) + logger.info( + "[SNAPSHOT t+%ds #%d] published=%d delivered=%d observers=%d/%d", + int(elapsed), + tick, + published, + delivered, + active_observers, + len(observers), + ) + + def _log_v14_snapshot(self, label: str) -> None: + """Log v1.4 specific metrics for every node.""" + for node in self.nodes: + if node.gossipsub is None: + continue + gs = node.gossipsub + peers_total = len(gs.peer_protocol) + v14_peers = sum( + 1 + for pid in gs.peer_protocol + if gs.supports_protocol_feature(pid, "adaptive_gossip") + ) + logger.info( + "[V14-SNAPSHOT:%s] node=%s peers=%d v14=%d %s", + label, + node.node_id, + peers_total, + v14_peers, + node.v14_status(), + ) + + def _print_statistics(self) -> None: + """Print a summary table at the end of the demo.""" + print(f"\n{'=' * 70}") + print("DEMO STATISTICS") + print(f"{'=' * 70}") + + total_sent = sum(n.messages_sent for n in self.nodes) + total_received = sum(n.messages_received for n in self.nodes) + + print(f"Total messages published : {total_sent}") + print(f"Total messages received : {total_received}") + print() + print(f"{'Node':<10} {'Role':<12} {'Sent':>6} {'Recv':>6} v1.4 Status") + print(f"{'-' * 70}") + for node in self.nodes: + print( + f"{node.node_id:<10} {node.role:<12} " + f"{node.messages_sent:>6} {node.messages_received:>6} " + f"{node.v14_status()}" + ) + + print(f"\n{'=' * 70}") + print("GossipSub 1.4 Features demonstrated:") + print(" + IWANT request rate limiting per peer (anti-spam)") + print(" + IHAVE message rate limiting per peer per topic (anti-spam)") + print(" + GRAFT flood protection with automatic score penalty") + print(" + Adaptive gossip factor based on network health score") + print(" + Opportunistic grafting threshold adaptation") + print(" + Heartbeat interval adaptation under poor network conditions") + print(" + Extended scoring (P5-P7) gated to /meshsub/1.4.0") + print(" + Topic Observation (inherited from v1.3) with UNOBSERVE demo") + print(" + All GossipSub 1.3 features (Extensions Control Message, etc.)") + print(f"{'=' * 70}\n") + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _assign_roles(node_count: int) -> list[str]: + """ + Distribute roles across nodes. + + With 6 nodes: 2 honest / 2 validator / 1 spammer / 1 observer. + Smaller counts fall back gracefully. + """ + if node_count < 2: + return ["honest"] * node_count + if node_count == 2: + return ["honest", "validator"] + if node_count == 3: + return ["honest", "validator", "spammer"] + + honest = max(1, node_count // 3) + spammer = max(1, node_count // 6) + observer = max(1, node_count // 6) + validator = node_count - honest - spammer - observer + return ( + ["honest"] * honest + + ["validator"] * validator + + ["spammer"] * spammer + + ["observer"] * observer + ) + + +def _print_banner(duration: int) -> None: + print(f"\n{'=' * 70}") + print("GOSSIPSUB 1.4 DEMO") + print(f"{'=' * 70}") + print("Protocol : /meshsub/1.4.0") + print(f"Duration : {duration} seconds") + print("Features : IWANT/IHAVE rate limiting, GRAFT flood protection,") + print(" adaptive gossip factor, heartbeat adaptation,") + print(" opportunistic grafting threshold, extended scoring (P5-P7),") + print(" Topic Observation (inherited from v1.3)") + print() + print("Roles:") + print(" honest โ€“ subscribes + publishes at a normal cadence") + print(" validator โ€“ subscribes, reads messages, no publishing") + print(" spammer โ€“ bursts messages to trigger rate-limit paths") + print(" observer โ€“ Topic Observation only (no full subscription)") + print() + print(f" At t={duration // 4}s : spammer burst (IWANT/IHAVE rate-limit demo)") + print(f" At t={duration // 2}s : UNOBSERVE sent by one observer") + print(f"{'=' * 70}\n") + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +async def main() -> None: + parser = argparse.ArgumentParser(description="GossipSub 1.4 Example") + parser.add_argument( + "--nodes", type=int, default=6, help="Total number of nodes (default: 6)" + ) + parser.add_argument( + "--duration", + type=int, + default=40, + help="Demo duration in seconds (default: 40)", + ) + parser.add_argument("--verbose", action="store_true", help="Enable DEBUG logging") + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + demo = GossipsubV14Demo() + await demo.setup_network(args.nodes) + await demo.start_network(args.duration) + + +if __name__ == "__main__": + trio.run(main) From 9a152d445700f678b2860c9c9b75d5077ca8526d Mon Sep 17 00:00:00 2001 From: Winter-Soren Date: Thu, 2 Apr 2026 22:57:56 +0530 Subject: [PATCH 7/9] fix(examples): align gossipsub demos with anyio_service and satisfy pyrefly --- examples/pubsub/gossipsub/gossipsub_v1.0.py | 2 +- examples/pubsub/gossipsub/gossipsub_v1.1.py | 2 +- examples/pubsub/gossipsub/gossipsub_v1.2.py | 2 +- examples/pubsub/gossipsub/gossipsub_v1.3.py | 5 +++-- examples/pubsub/gossipsub/gossipsub_v1.4.py | 19 +++++++++++-------- examples/pubsub/gossipsub/gossipsub_v2.0.py | 2 +- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/examples/pubsub/gossipsub/gossipsub_v1.0.py b/examples/pubsub/gossipsub/gossipsub_v1.0.py index d5f0418b9..c3d0a26a1 100755 --- a/examples/pubsub/gossipsub/gossipsub_v1.0.py +++ b/examples/pubsub/gossipsub/gossipsub_v1.0.py @@ -33,7 +33,7 @@ from libp2p.pubsub.gossipsub import GossipSub from libp2p.pubsub.pubsub import Pubsub from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex -from libp2p.tools.async_service.trio_service import background_trio_service +from libp2p.tools.anyio_service import background_trio_service from libp2p.utils.address_validation import find_free_port # Configure logging diff --git a/examples/pubsub/gossipsub/gossipsub_v1.1.py b/examples/pubsub/gossipsub/gossipsub_v1.1.py index 8c0b00f28..d76bed34a 100755 --- a/examples/pubsub/gossipsub/gossipsub_v1.1.py +++ b/examples/pubsub/gossipsub/gossipsub_v1.1.py @@ -36,7 +36,7 @@ from libp2p.pubsub.pubsub import Pubsub from libp2p.pubsub.score import ScoreParams, TopicScoreParams from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex -from libp2p.tools.async_service.trio_service import background_trio_service +from libp2p.tools.anyio_service import background_trio_service from libp2p.utils.address_validation import find_free_port # Configure logging diff --git a/examples/pubsub/gossipsub/gossipsub_v1.2.py b/examples/pubsub/gossipsub/gossipsub_v1.2.py index 85e2edc64..f7ad46fb5 100755 --- a/examples/pubsub/gossipsub/gossipsub_v1.2.py +++ b/examples/pubsub/gossipsub/gossipsub_v1.2.py @@ -31,7 +31,7 @@ from libp2p.pubsub.pubsub import Pubsub from libp2p.pubsub.score import ScoreParams, TopicScoreParams from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex -from libp2p.tools.async_service.trio_service import background_trio_service +from libp2p.tools.anyio_service import background_trio_service from libp2p.utils.address_validation import find_free_port # Configure logging diff --git a/examples/pubsub/gossipsub/gossipsub_v1.3.py b/examples/pubsub/gossipsub/gossipsub_v1.3.py index ea51ee62f..0bd580a3c 100644 --- a/examples/pubsub/gossipsub/gossipsub_v1.3.py +++ b/examples/pubsub/gossipsub/gossipsub_v1.3.py @@ -161,11 +161,12 @@ async def publish_message(self, message: str) -> None: async def receive_messages(self) -> None: """Read full message payloads (subscriber / publisher roles).""" - if not self.subscription: + subscription = self.subscription + if subscription is None: return try: while True: - message = await self.subscription.get() + message = await subscription.get() decoded = message.data.decode("utf-8") self.messages_received += 1 logger.info( diff --git a/examples/pubsub/gossipsub/gossipsub_v1.4.py b/examples/pubsub/gossipsub/gossipsub_v1.4.py index 8ae4a83ed..e59dbdc9b 100644 --- a/examples/pubsub/gossipsub/gossipsub_v1.4.py +++ b/examples/pubsub/gossipsub/gossipsub_v1.4.py @@ -85,10 +85,11 @@ async def start(self) -> None: key_pair = create_new_key_pair() - self.host = new_host( + host = new_host( key_pair=key_pair, muxer_opt={MPLEX_PROTOCOL_ID: Mplex}, ) + self.host = host # Full peer scoring: P1-P4 topic-scoped + P5 behavior + P6/P7 global. score_params = ScoreParams( @@ -115,7 +116,7 @@ async def start(self) -> None: # max_ihave_messages_per_second โ€“ caps IHAVE flood per peer/topic # graft_flood_threshold โ€“ minimum seconds between PRUNE and GRAFT # adaptive_gossip_enabled โ€“ turn on health-based parameter adaptation - self.gossipsub = GossipSub( + gossipsub = GossipSub( protocols=[PROTOCOL_ID_V14], degree=3, degree_low=2, @@ -128,14 +129,15 @@ async def start(self) -> None: # v1.4 rate limiting adaptive_gossip_enabled=True, ) + self.gossipsub = gossipsub # Override v1.4 rate limiting thresholds directly on the router so the # demo can observe them being triggered with a small message volume. - self.gossipsub.max_iwant_requests_per_second = 5.0 - self.gossipsub.max_ihave_messages_per_second = 5.0 - self.gossipsub.graft_flood_threshold = 8.0 + gossipsub.max_iwant_requests_per_second = 5.0 + gossipsub.max_ihave_messages_per_second = 5.0 + gossipsub.graft_flood_threshold = 8.0 - self.pubsub = Pubsub(self.host, self.gossipsub) + self.pubsub = Pubsub(host, gossipsub) listen_addrs = [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{self.port}")] @@ -177,11 +179,12 @@ async def publish_message(self, message: str) -> None: async def receive_messages(self) -> None: """Drain the subscription queue and count deliveries.""" - if not self.subscription: + subscription = self.subscription + if subscription is None: return try: while True: - message = await self.subscription.get() + message = await subscription.get() decoded = message.data.decode("utf-8") self.messages_received += 1 logger.info( diff --git a/examples/pubsub/gossipsub/gossipsub_v2.0.py b/examples/pubsub/gossipsub/gossipsub_v2.0.py index a4997a7fc..6b86a2a9e 100755 --- a/examples/pubsub/gossipsub/gossipsub_v2.0.py +++ b/examples/pubsub/gossipsub/gossipsub_v2.0.py @@ -36,7 +36,7 @@ from libp2p.pubsub.pubsub import Pubsub from libp2p.pubsub.score import ScoreParams, TopicScoreParams from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex -from libp2p.tools.async_service.trio_service import background_trio_service +from libp2p.tools.anyio_service import background_trio_service from libp2p.utils.address_validation import find_free_port # Configure logging From 0a5f0663e9318566616eb4a0238ed8708f3ebd31 Mon Sep 17 00:00:00 2001 From: Winter-Soren Date: Thu, 2 Apr 2026 23:38:09 +0530 Subject: [PATCH 8/9] removed README.md (converted to discussions page) and added absolute path to run the examples --- examples/pubsub/gossipsub/README.md | 403 -------------------- examples/pubsub/gossipsub/gossipsub_v1.0.py | 4 +- examples/pubsub/gossipsub/gossipsub_v1.1.py | 4 +- examples/pubsub/gossipsub/gossipsub_v1.2.py | 4 +- examples/pubsub/gossipsub/gossipsub_v1.3.py | 6 +- examples/pubsub/gossipsub/gossipsub_v1.4.py | 6 +- examples/pubsub/gossipsub/gossipsub_v2.0.py | 4 +- 7 files changed, 14 insertions(+), 417 deletions(-) delete mode 100644 examples/pubsub/gossipsub/README.md diff --git a/examples/pubsub/gossipsub/README.md b/examples/pubsub/gossipsub/README.md deleted file mode 100644 index 13b612947..000000000 --- a/examples/pubsub/gossipsub/README.md +++ /dev/null @@ -1,403 +0,0 @@ -# Gossipsub Examples - -This directory contains comprehensive examples showcasing the differences between Gossipsub protocol versions and demonstrating advanced features of Gossipsub 2.0. - -## Overview - -Py-libp2p has full protocol version support spanning: - -- **Gossipsub 1.0** (`/meshsub/1.0.0`) - Basic mesh-based pubsub -- **Gossipsub 1.1** (`/meshsub/1.1.0`) - Added peer scoring and behavioral penalties -- **Gossipsub 1.2** (`/meshsub/1.2.0`) - Added IDONTWANT message filtering -- **Gossipsub 1.3** (`/meshsub/1.3.0`) - Extensions Control Message and Topic Observation -- **Gossipsub 1.4** (`/meshsub/1.4.0`) - Rate limiting, GRAFT flood protection, and adaptive gossip -- **Gossipsub 2.0** (`/meshsub/2.0.0`) - Enhanced security, adaptive gossip, and advanced peer scoring - -## Examples - -### 1. Gossipsub 1.0 Demo (`gossipsub_v1.0.py`) - -Basic mesh-based pubsub demo using Gossipsub 1.0 (`/meshsub/1.0.0`). - -**Features:** - -- Basic mesh-based pubsub -- Simple flooding for message dissemination -- Mesh topology maintenance -- **Fanout behaviour**: one node (node_0) is a fanout-only publisher: it publishes without subscribing, so messages are sent via *fanout* (a random set of topic subscribers) instead of mesh peers - -**Usage:** - -```bash -python gossipsub_v1.0.py --nodes 5 --duration 30 -``` - -### 2. Gossipsub 1.1 Demo (`gossipsub_v1.1.py`) - -Demonstrates Gossipsub 1.1 (`/meshsub/1.1.0`) with peer scoring and behavioral penalties. - -**Features:** - -- All Gossipsub 1.0 features -- Peer scoring with P1โ€“P4 topic-scoped parameters -- Behavioral penalties (P5) -- **P6 (application-specific score)** and **P7 (IP colocation factor / Behavioural Penalty)** -- **Prune backoff** and **peer exchange (PX)** enabled -- Optional **application score function** (in real applications: staking, reputation, or role e.g. validator, full node) -- Honest vs. malicious publisher behaviour - -**Usage:** - -```bash -python gossipsub_v1.1.py --nodes 5 --duration 30 -``` - -### 3. Gossipsub 1.2 Demo (`gossipsub_v1.2.py`) - -Demonstrates Gossipsub 1.2 (`/meshsub/1.2.0`) with IDONTWANT message filtering. - -**Features:** - -- All Gossipsub 1.1 features -- IDONTWANT messages and message filtering -- Reduced redundant traffic in denser meshes - -**Usage:** - -```bash -python gossipsub_v1.2.py --nodes 5 --duration 30 -``` - -### 4. Gossipsub 1.3 Demo (`gossipsub_v1.3.py`) - -Demonstrates GossipSub 1.3 (`/meshsub/1.3.0`) with the Extensions Control Message -mechanism and the Topic Observation extension. - -**Features:** - -- All GossipSub 1.2 features (IDONTWANT, peer scoring) -- **Extensions Control Message**: sent exactly once in the first message per peer -- **At-most-once enforcement**: duplicate Extensions from a peer triggers a score penalty -- **Topic Observation**: observer nodes receive IHAVE presence notifications without - full message payloads -- **Protocol gating**: extension fields are only attached when `/meshsub/1.3.0` is negotiated - -**Node roles in the demo:** - -| Role | Behaviour | -| ------------ | --------------------------------------------------------- | -| `publisher` | Subscribes and publishes messages every 2 seconds | -| `subscriber` | Subscribes and reads full message payloads | -| `observer` | Uses Topic Observation (IHAVE-only); no full subscription | - -At the halfway point one observer sends UNOBSERVE to stop receiving notifications, -demonstrating the full OBSERVE โ†’ IHAVE โ†’ UNOBSERVE lifecycle. - -**Usage:** - -```bash -python gossipsub_v1.3.py --nodes 6 --duration 40 -python gossipsub_v1.3.py --nodes 6 --duration 40 --verbose -``` - -### 5. Gossipsub 1.4 Demo (`gossipsub_v1.4.py`) - -Demonstrates GossipSub 1.4 (`/meshsub/1.4.0`) with enhanced rate limiting, GRAFT flood -protection, and adaptive gossip parameter tuning. - -**Features:** - -- All GossipSub 1.3 features (Extensions Control Message, Topic Observation) -- **IWANT request rate limiting** per peer โ€“ prevents IWANT spam storms; excess requests - trigger a `penalize_iwant_spam` score deduction -- **IHAVE message rate limiting** per peer per topic โ€“ caps IHAVE floods; - excess triggers `penalize_ihave_spam` -- **GRAFT flood protection** โ€“ a peer that sends GRAFT too soon after receiving PRUNE - is penalised via `penalize_graft_flood` and immediately re-pruned -- **Adaptive gossip factor** โ€“ the gossip spread factor scales with a multi-metric - network health score (connectivity, peer scores, delivery rate, mesh stability, churn) -- **Opportunistic grafting threshold adaptation** โ€“ in poor health the threshold - is lowered so high-quality peers are grafted more aggressively -- **Heartbeat interval adaptation** โ€“ under critical health the heartbeat is - accelerated for faster mesh recovery -- **Extended scoring (P5-P7)** gated to `/meshsub/1.4.0` - -**Node roles in the demo:** - -| Role | Behaviour | -| ----------- | ------------------------------------------------------------------ | -| `honest` | Subscribes and publishes at a normal cadence | -| `validator` | Subscribes and reads messages; no publishing | -| `spammer` | Bursts rapid messages to trigger IWANT / IHAVE rate-limiting paths | -| `observer` | Uses Topic Observation (IHAVE-only, inherited from v1.3) | - -At `t = duration/4` the spammer fires a burst of 12 messages in rapid succession to -demonstrate rate-limit enforcement. At `t = duration/2` one observer sends UNOBSERVE. - -**Usage:** - -```bash -python gossipsub_v1.4.py --nodes 6 --duration 40 -python gossipsub_v1.4.py --nodes 6 --duration 40 --verbose -``` - -### 6. Gossipsub 2.0 Demo (`gossipsub_v2.0.py`) - -Demonstrates Gossipsub 2.0 (`/meshsub/2.0.0`) with adaptive gossip and advanced security features. - -**Features:** - -#### Peer Scoring Visualization - -- **Real-time Score Display**: Shows peer scores (P1-P7 parameters) updating in real-time -- **Score Component Breakdown**: Visualizes individual scoring components -- **Behavioral Penalties**: Demonstrates how misbehavior affects peer scores -- **IP Colocation Penalties**: Shows P7 penalties for peers from same IP ranges -- **Application Scoring**: Demonstrates P6 custom application-defined scoring - -#### Adaptive Gossip Demonstration - -- **Network Health Monitoring**: Displays network health score calculation -- **Dynamic Parameter Adjustment**: Shows how gossip parameters adapt to network conditions -- **Mesh Quality Maintenance**: Visualizes mesh degree adjustments -- **Opportunistic Grafting**: Demonstrates score-based peer selection - -#### Security Features - -- **Spam Protection**: Shows rate limiting in action -- **Eclipse Attack Protection**: Demonstrates IP diversity enforcement -- **Equivocation Detection**: Shows detection and penalties for duplicate messages -- **Message Validation**: Demonstrates validation hooks and caching - -**Usage:** - -```bash -python gossipsub_v2.0.py --nodes 5 --duration 60 -``` - -## Protocol Version Differences - -### Gossipsub 1.0 (`/meshsub/1.0.0`) - -- Basic mesh-based pubsub protocol -- Simple flooding for message dissemination -- **Fanout**: publishers that are not in the mesh for a topic send to a random set of topic subscribers (fanout peers) -- No peer scoring or advanced security features -- Suitable for trusted networks with low adversarial activity - -### Gossipsub 1.1 (`/meshsub/1.1.0`) - -- **Added Peer Scoring**: P1-P4 topic-scoped parameters - - P1: Time in mesh - - P2: First message deliveries - - P3: Mesh message deliveries - - P4: Invalid messages penalty -- **Behavioral Penalties**: P5 global behavior penalty -- **P6 (Application-specific score)** and **P7 (IP colocation factor)** -- **Prune backoff** and **peer exchange (PX)** enabled -- Optional **application score function** (e.g. staking/reputation, validator/full node role) -- **Signed Peer Records**: Enhanced peer exchange with signed records -- Better resilience against basic attacks - -### Gossipsub 1.2 (`/meshsub/1.2.0`) - -- **IDONTWANT Messages**: Peers can signal they don't want specific messages -- **Message Filtering**: Reduces redundant message transmission -- **Improved Efficiency**: Lower bandwidth usage in dense networks -- All v1.1 features included - -### Gossipsub 1.3 (`/meshsub/1.3.0`) - -- **Extensions Control Message**: carried in the first RPC on a stream, at most once per peer -- **Misbehaviour Scoring**: duplicate Extensions messages trigger `scorer.penalize_behavior` -- **Topic Observation**: `start_observing_topic` / `stop_observing_topic` API; observers - receive IHAVE notifications immediately on publish without fetching full payloads -- **Protocol Gating**: extension fields are injected only when the negotiated protocol - is `/meshsub/1.3.0` or later -- **Forward Compatibility**: unknown extension fields are silently ignored by decoders -- All v1.2 features included - -### Gossipsub 1.4 (`/meshsub/1.4.0`) - -- **IWANT Rate Limiting**: caps IWANT requests per peer per second; excess triggers - `penalize_iwant_spam` score deduction -- **IHAVE Rate Limiting**: caps IHAVE messages per peer per topic per second; excess - triggers `penalize_ihave_spam` -- **GRAFT Flood Protection**: peers that re-GRAFT before the backoff window expires - receive `penalize_graft_flood` and are immediately re-pruned -- **Adaptive Gossip Factor**: gossip spread factor (`gossip_factor`) scales dynamically - based on a composite network health score (mesh connectivity, peer scores, delivery - rate, mesh stability, and connection churn) -- **Opportunistic Grafting Threshold Adaptation**: lower threshold in poor health - promotes more aggressive peer selection for mesh repair -- **Heartbeat Interval Adaptation**: heartbeat interval shrinks under critical health - for faster mesh recovery, expanding again once health improves -- **Extended Scoring (P5-P7) Gate**: `supports_protocol_feature(peer, "extended_scoring")` - returns `True` only for `/meshsub/1.4.0` -- All v1.3 features included - -### Gossipsub 2.0 (`/meshsub/2.0.0`) - -- **Enhanced Peer Scoring**: P6 (application-specific) and P7 (IP colocation) parameters -- **Adaptive Gossip**: Dynamic parameter adjustment based on network health -- **Advanced Security Features**: - - Spam protection with rate limiting - - Eclipse attack protection via IP diversity - - Equivocation detection - - Enhanced message validation -- **Network Health Monitoring**: Continuous assessment of network conditions -- **Opportunistic Grafting**: Score-based peer selection for mesh optimization - -## Peer Scoring Parameters (P1-P7) - -### Topic-Scoped Parameters (P1-P4) - -- **P1 (Time in Mesh)**: Rewards peers for staying in the mesh longer -- **P2 (First Message Deliveries)**: Rewards peers for delivering messages first -- **P3 (Mesh Message Deliveries)**: Rewards peers for consistent message delivery -- **P4 (Invalid Messages)**: Penalizes peers for sending invalid messages - -### Global Parameters (P5-P7) - -- **P5 (Behavior Penalty)**: General behavioral penalty for misbehavior -- **P6 (Application Score)**: Custom application-defined scoring -- **P7 (IP Colocation)**: Penalizes multiple peers from same IP address - -## Security Features in Gossipsub 2.0 - -### Spam Protection - -- Rate limiting per peer per topic -- Configurable message rate thresholds -- Automatic penalty application for rate limit violations - -### Eclipse Attack Protection - -- Minimum IP diversity requirements in mesh -- Penalties for excessive peers from same IP range -- Mesh diversity monitoring and enforcement - -### Equivocation Detection - -- Detection of duplicate messages with same sequence number -- Penalties for peers sending conflicting messages -- Message deduplication and validation - -### Message Validation - -- Configurable validation hooks -- Validation result caching -- Integration with peer scoring system - -## Running the Examples - -### Basic Usage - -```bash -# Navigate to the examples directory -cd examples/pubsub/gossipsub - -# Run per-version demos -python gossipsub_v1.0.py --nodes 5 --duration 30 -python gossipsub_v1.1.py --nodes 5 --duration 30 -python gossipsub_v1.2.py --nodes 5 --duration 30 -python gossipsub_v1.3.py --nodes 6 --duration 40 -python gossipsub_v1.4.py --nodes 6 --duration 40 -python gossipsub_v2.0.py --nodes 5 --duration 60 -``` - -## Understanding the Output - -The per-version demos print statistics summarising: - -- **Messages Sent/Received** per node -- **Roles** (honest, malicious, spammer, validator) and their behaviour -- **Feature Highlights** for the corresponding protocol version (for example, IDONTWANT support in v1.2, security and adaptive gossip in v2.0) - -## Network Topologies - -All demos create realistic network topologies: - -- **Mesh Connectivity**: Each node connects to 3-4 peers -- **Realistic Latency**: Simulated network delays -- **Diverse Roles**: Honest peers, spammers, validators, attackers -- **Dynamic Behavior**: Peer churn, network partitions, attacks - -## Customization - -### Adding Custom Scenarios - -To add new test scenarios to the per-version demos: - -```python -async def _run_custom_scenario(self, duration: int): - """Your custom scenario implementation""" - # Implement custom network behavior - pass - -# Register in run_scenario method -elif scenario == "custom": - await self._run_custom_scenario(duration) -``` - -### Custom Scoring Functions - -To implement custom application scoring: - -```python -def custom_app_score(peer_id: ID) -> float: - """Custom application-specific scoring logic""" - # Implement your scoring logic - return score - -# Use in ScoreParams -score_params = ScoreParams( - app_specific_score_fn=custom_app_score, - p6_appl_slack_weight=0.5 -) -``` - -### Custom Validation - -To add custom message validation: - -```python -def custom_validator(message) -> bool: - """Custom message validation logic""" - # Implement validation rules - return is_valid - -# Use in node setup -node._validate_message = custom_validator -``` - -## Troubleshooting - -### Common Issues - -1. **Port Conflicts**: If you get port binding errors, the examples will automatically find free ports -1. **Connection Failures**: Ensure firewall allows local connections on the used ports -1. **High CPU Usage**: Reduce the number of nodes or increase sleep intervals for testing -1. **Memory Usage**: Large networks may consume significant memory; monitor usage - -### Debug Mode - -Enable verbose logging for detailed information: - -```bash -python gossipsub_v1.0.py --verbose ... -python gossipsub_v1.1.py --verbose ... -python gossipsub_v1.2.py --verbose ... -python gossipsub_v1.3.py --verbose ... -python gossipsub_v1.4.py --verbose ... -python gossipsub_v2.0.py --verbose ... -``` - -## References - -- [Gossipsub v1.1 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md) -- [Gossipsub v1.2 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.2.md) -- [Gossipsub v1.3 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.3.md) -- [Topic Observation proposal (ethresearch)](https://ethresear.ch/t/gossipsub-topic-observation-proposed-gossipsub-1-3/20907) -- [Gossipsub v1.4 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.4.md) -- [Gossipsub v2.0 Specification](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v2.0.md) diff --git a/examples/pubsub/gossipsub/gossipsub_v1.0.py b/examples/pubsub/gossipsub/gossipsub_v1.0.py index c3d0a26a1..b2ba2dfe7 100755 --- a/examples/pubsub/gossipsub/gossipsub_v1.0.py +++ b/examples/pubsub/gossipsub/gossipsub_v1.0.py @@ -15,8 +15,8 @@ - Fanout behaviour: a publisher that is not in the mesh (e.g. does not subscribe) sends to a random set of topic subscribers (fanout peers) instead of mesh peers -Usage: - python gossipsub_v1.0.py --nodes 5 --duration 30 +Usage (from repository root): + python examples/pubsub/gossipsub/gossipsub_v1.0.py --nodes 5 --duration 30 """ import argparse diff --git a/examples/pubsub/gossipsub/gossipsub_v1.1.py b/examples/pubsub/gossipsub/gossipsub_v1.1.py index d76bed34a..31e55ad93 100755 --- a/examples/pubsub/gossipsub/gossipsub_v1.1.py +++ b/examples/pubsub/gossipsub/gossipsub_v1.1.py @@ -16,8 +16,8 @@ - Signed peer records - Better resilience against attacks -Usage: - python gossipsub_v1.1.py --nodes 5 --duration 30 +Usage (from repository root): + python examples/pubsub/gossipsub/gossipsub_v1.1.py --nodes 5 --duration 30 """ import argparse diff --git a/examples/pubsub/gossipsub/gossipsub_v1.2.py b/examples/pubsub/gossipsub/gossipsub_v1.2.py index f7ad46fb5..76dd928dc 100755 --- a/examples/pubsub/gossipsub/gossipsub_v1.2.py +++ b/examples/pubsub/gossipsub/gossipsub_v1.2.py @@ -12,8 +12,8 @@ - Reduced bandwidth usage - Improved efficiency in dense networks -Usage: - python gossipsub_v1.2.py --nodes 5 --duration 30 +Usage (from repository root): + python examples/pubsub/gossipsub/gossipsub_v1.2.py --nodes 5 --duration 30 """ import argparse diff --git a/examples/pubsub/gossipsub/gossipsub_v1.3.py b/examples/pubsub/gossipsub/gossipsub_v1.3.py index 0bd580a3c..121a29489 100644 --- a/examples/pubsub/gossipsub/gossipsub_v1.3.py +++ b/examples/pubsub/gossipsub/gossipsub_v1.3.py @@ -19,9 +19,9 @@ - subscriber : subscribes to the topic and reads full message payloads - observer : starts observing (IHAVE-only) without subscribing; tracks presence -Usage: - python gossipsub_v1.3.py --nodes 6 --duration 40 - python gossipsub_v1.3.py --nodes 6 --duration 40 --verbose +Usage (from repository root): + python examples/pubsub/gossipsub/gossipsub_v1.3.py --nodes 6 --duration 40 + python examples/pubsub/gossipsub/gossipsub_v1.3.py --nodes 6 --duration 40 --verbose """ import argparse diff --git a/examples/pubsub/gossipsub/gossipsub_v1.4.py b/examples/pubsub/gossipsub/gossipsub_v1.4.py index e59dbdc9b..442bc690f 100644 --- a/examples/pubsub/gossipsub/gossipsub_v1.4.py +++ b/examples/pubsub/gossipsub/gossipsub_v1.4.py @@ -22,9 +22,9 @@ - observer : uses Topic Observation (IHAVE-only) inherited from v1.3 - validator : receives messages and validates them; no publishing -Usage: - python gossipsub_v1.4.py --nodes 6 --duration 40 - python gossipsub_v1.4.py --nodes 6 --duration 40 --verbose +Usage (from repository root): + python examples/pubsub/gossipsub/gossipsub_v1.4.py --nodes 6 --duration 40 + python examples/pubsub/gossipsub/gossipsub_v1.4.py --nodes 6 --duration 40 --verbose """ import argparse diff --git a/examples/pubsub/gossipsub/gossipsub_v2.0.py b/examples/pubsub/gossipsub/gossipsub_v2.0.py index 6b86a2a9e..4569fe811 100755 --- a/examples/pubsub/gossipsub/gossipsub_v2.0.py +++ b/examples/pubsub/gossipsub/gossipsub_v2.0.py @@ -16,8 +16,8 @@ - Equivocation detection - Enhanced message validation -Usage: - python gossipsub_v2.0.py --nodes 5 --duration 30 +Usage (from repository root): + python examples/pubsub/gossipsub/gossipsub_v2.0.py --nodes 5 --duration 30 """ import argparse From 615f6c8d5f599123999b6d60c3d9dbf84013ee8b Mon Sep 17 00:00:00 2001 From: Winter-Soren Date: Fri, 3 Apr 2026 16:15:00 +0530 Subject: [PATCH 9/9] test: wait 2s in test_sparse_connect_degree_zero to fix flaky CI under xdist --- tests/core/pubsub/test_gossipsub.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/core/pubsub/test_gossipsub.py b/tests/core/pubsub/test_gossipsub.py index c93e11ec6..8139395d8 100644 --- a/tests/core/pubsub/test_gossipsub.py +++ b/tests/core/pubsub/test_gossipsub.py @@ -724,7 +724,9 @@ async def test_sparse_connect_degree_zero(): degree = 0 await sparse_connect(hosts, degree) - await trio.sleep(0.1) # Allow connections to establish + # Match test_sparse_connect / test_dense_connect_fallback: pubsub streams need + # time to settle; CI under pytest-xdist can exceed a short 0.1s window. + await trio.sleep(2) # With degree=0, sparse_connect should still create neighbor connections # for connectivity (this is part of the algorithm design)