|
| 1 | +# Python Implementation for Transport Test Framework |
| 2 | + |
| 3 | +## Summary |
| 4 | + |
| 5 | +This document describes the Python libp2p v0.x implementation for the transport test framework. The implementation follows the transport test framework specification with proper Redis coordination protocol, correct environment variable handling, standalone transport support, and YAML-formatted output. |
| 6 | + |
| 7 | +## Implementation Details |
| 8 | + |
| 9 | +### 1. Redis Coordination Protocol |
| 10 | + |
| 11 | +**Implementation**: Uses `RPUSH`/`BLPOP` (Redis list operations) as specified in the transport test framework, matching Rust and JavaScript implementations. |
| 12 | + |
| 13 | +**Listener** (`run_listener` method): |
| 14 | + |
| 15 | +- Uses `DEL` operation before `RPUSH` to prevent `WRONGTYPE` errors from stale data |
| 16 | +- Uses `RPUSH` to publish listener multiaddr as a list element |
| 17 | +- Key format: `{TEST_KEY}_listener_multiaddr` |
| 18 | + |
| 19 | +**Dialer** (`run_dialer` method): |
| 20 | + |
| 21 | +- Uses `BLPOP` (blocking list pop) to wait for listener address |
| 22 | +- Properly handles BLPOP return value `(key, value)` tuple format |
| 23 | +- Includes timeout handling with proper conversion for Redis commands |
| 24 | + |
| 25 | +**Current Implementation**: |
| 26 | + |
| 27 | +```python |
| 28 | +# Listener: Publish address |
| 29 | +redis_key = f"{self.test_key}_listener_multiaddr" |
| 30 | + |
| 31 | +# Clean up any existing key to ensure it's a list type |
| 32 | +try: |
| 33 | + self.redis_client.delete(redis_key) |
| 34 | +except Exception: |
| 35 | + pass # Ignore if key doesn't exist |
| 36 | + |
| 37 | +# Publish listener address using RPUSH (list operation) |
| 38 | +# Dialer will use BLPOP to block and read this value |
| 39 | +self.redis_client.rpush(redis_key, actual_addr) |
| 40 | + |
| 41 | +# Dialer: Wait for address |
| 42 | +blpop_result = self.redis_client.blpop( |
| 43 | + redis_key, timeout=remaining_timeout |
| 44 | +) |
| 45 | +if blpop_result: |
| 46 | + # BLPOP returns (key, value) tuple - extract the multiaddr string |
| 47 | + listener_addr = ( |
| 48 | + blpop_result[1] |
| 49 | + if isinstance(blpop_result, (list, tuple)) |
| 50 | + and len(blpop_result) > 1 |
| 51 | + else blpop_result |
| 52 | + ) |
| 53 | +``` |
| 54 | + |
| 55 | +**Files**: |
| 56 | + |
| 57 | +- `transport/images/python/v0.x/py-libp2p/interop/transport/ping_test.py` (lines 737-755, 902-930) |
| 58 | + |
| 59 | +### 2. Environment Variable Handling |
| 60 | + |
| 61 | +**Implementation**: All environment variables use uppercase names only, matching the test framework specification. Variables are strictly required (throw errors if not set) or optional with defaults. |
| 62 | + |
| 63 | +**Current Implementation**: |
| 64 | + |
| 65 | +```python |
| 66 | +# Required variables (throw error if not set) |
| 67 | +self.transport = os.getenv("TRANSPORT") |
| 68 | +if not self.transport: |
| 69 | + raise ValueError("TRANSPORT environment variable is required") |
| 70 | + |
| 71 | +self.redis_addr = os.getenv("REDIS_ADDR") |
| 72 | +if not self.redis_addr: |
| 73 | + raise ValueError("REDIS_ADDR environment variable is required") |
| 74 | + |
| 75 | +self.ip = os.getenv("LISTENER_IP") |
| 76 | +if not self.ip: |
| 77 | + raise ValueError("LISTENER_IP environment variable is required") |
| 78 | + |
| 79 | +self.test_key = os.getenv("TEST_KEY") |
| 80 | +if not self.test_key: |
| 81 | + raise ValueError("TEST_KEY environment variable is required") |
| 82 | + |
| 83 | +is_dialer_value = os.getenv("IS_DIALER") |
| 84 | +if is_dialer_value is None: |
| 85 | + raise ValueError("IS_DIALER environment variable is required") |
| 86 | +self.is_dialer = is_dialer_value == "true" # Case-sensitive match |
| 87 | + |
| 88 | +# Optional variables with defaults |
| 89 | +debug_value = os.getenv("DEBUG") or "false" # Optional, default to "false" |
| 90 | + |
| 91 | +timeout_value = os.getenv("TEST_TIMEOUT_SECS") or "180" |
| 92 | +raw_timeout = int(timeout_value) |
| 93 | +self.test_timeout_seconds = min(raw_timeout, MAX_TEST_TIMEOUT) |
| 94 | +``` |
| 95 | + |
| 96 | +**Files**: |
| 97 | + |
| 98 | +- `transport/images/python/v0.x/py-libp2p/interop/transport/ping_test.py` (lines 54-64, 94-150) |
| 99 | + |
| 100 | +### 3. Standalone Transport Support |
| 101 | + |
| 102 | +**Implementation**: Properly handles standalone transports (quic-v1) where `SECURE_CHANNEL` and `MUXER` environment variables are optional (not set by the test framework). |
| 103 | + |
| 104 | +**Environment Variable Handling**: |
| 105 | + |
| 106 | +```python |
| 107 | +# Standalone transports don't use separate security/muxer |
| 108 | +standalone_transports = ["quic-v1"] # Python currently only supports quic-v1 |
| 109 | + |
| 110 | +# Check if transport is standalone before requiring MUXER/SECURE_CHANNEL |
| 111 | +if self.transport not in standalone_transports: |
| 112 | + # Non-standalone transports: MUXER and SECURE_CHANNEL are required |
| 113 | + muxer_env = os.getenv("MUXER") |
| 114 | + if muxer_env is None: |
| 115 | + raise ValueError("MUXER environment variable is required") |
| 116 | + self.muxer = muxer_env |
| 117 | + |
| 118 | + security_env = os.getenv("SECURE_CHANNEL") |
| 119 | + if security_env is None: |
| 120 | + raise ValueError("SECURE_CHANNEL environment variable is required") |
| 121 | + self.security = security_env |
| 122 | +else: |
| 123 | + # Standalone transports: MUXER and SECURE_CHANNEL are optional |
| 124 | + muxer_env = os.getenv("MUXER") |
| 125 | + self.muxer = muxer_env if muxer_env else None |
| 126 | + |
| 127 | + security_env = os.getenv("SECURE_CHANNEL") |
| 128 | + self.security = security_env if security_env else None |
| 129 | +``` |
| 130 | + |
| 131 | +**Security Options for Standalone Transports**: |
| 132 | + |
| 133 | +```python |
| 134 | +def create_security_options(self): |
| 135 | + """Create security options based on configuration.""" |
| 136 | + # Standalone transports have security built-in, no separate security needed |
| 137 | + standalone_transports = ["quic-v1"] |
| 138 | + if self.transport in standalone_transports: |
| 139 | + # For standalone transports, return empty security options |
| 140 | + # The security is handled by the transport itself |
| 141 | + key_pair = create_new_key_pair() |
| 142 | + return {}, key_pair |
| 143 | + |
| 144 | + # Non-standalone transports: create security options as before |
| 145 | + key_pair = create_new_key_pair() |
| 146 | + |
| 147 | + if self.security == "noise": |
| 148 | + # ... noise setup ... |
| 149 | + elif self.security == "tls": |
| 150 | + # ... tls setup ... |
| 151 | + elif self.security == "plaintext": |
| 152 | + # ... plaintext setup ... |
| 153 | + else: |
| 154 | + raise ValueError(f"Unsupported security: {self.security}") |
| 155 | +``` |
| 156 | + |
| 157 | +**Muxer Options for Standalone Transports**: |
| 158 | + |
| 159 | +```python |
| 160 | +def create_muxer_options(self): |
| 161 | + """Create muxer options based on configuration.""" |
| 162 | + # Standalone transports have muxing built-in, no separate muxer needed |
| 163 | + standalone_transports = ["quic-v1"] |
| 164 | + if self.transport in standalone_transports: |
| 165 | + # For standalone transports, return None (no separate muxer) |
| 166 | + # The muxing is handled by the transport itself |
| 167 | + return None |
| 168 | + |
| 169 | + # Non-standalone transports: create muxer options as before |
| 170 | + if self.muxer == "yamux": |
| 171 | + return create_yamux_muxer_option() |
| 172 | + elif self.muxer == "mplex": |
| 173 | + return create_mplex_muxer_option() |
| 174 | + else: |
| 175 | + raise ValueError(f"Unsupported muxer: {self.muxer}") |
| 176 | +``` |
| 177 | + |
| 178 | +**Files**: |
| 179 | + |
| 180 | +- `transport/images/python/v0.x/py-libp2p/interop/transport/ping_test.py` (lines 98-119, 183-218) |
| 181 | + |
| 182 | +### 4. Output Format |
| 183 | + |
| 184 | +**Implementation**: Outputs YAML format to stdout as required by the transport test framework: |
| 185 | + |
| 186 | +```python |
| 187 | +print("latency:", file=sys.stdout) |
| 188 | +print(f" handshake_plus_one_rtt: {handshake_plus_one_rtt}", file=sys.stdout) |
| 189 | +print(f" ping_rtt: {ping_rtt}", file=sys.stdout) |
| 190 | +print(" unit: ms", file=sys.stdout) |
| 191 | +``` |
| 192 | + |
| 193 | +**Files**: |
| 194 | + |
| 195 | +- `transport/images/python/v0.x/py-libp2p/interop/transport/ping_test.py` (lines 1080-1086) |
| 196 | + |
| 197 | +### 5. TEST_KEY Support |
| 198 | + |
| 199 | +**Implementation**: Uses `TEST_KEY` environment variable for Redis key namespacing with strict validation: |
| 200 | + |
| 201 | +```python |
| 202 | +self.test_key = os.getenv("TEST_KEY") |
| 203 | +if not self.test_key: |
| 204 | + raise ValueError("TEST_KEY environment variable is required") |
| 205 | + |
| 206 | +redis_key = f"{self.test_key}_listener_multiaddr" |
| 207 | +``` |
| 208 | + |
| 209 | +**Files**: |
| 210 | + |
| 211 | +- `transport/images/python/v0.x/py-libp2p/interop/transport/ping_test.py` (lines 147-150, 745, 906) |
| 212 | + |
| 213 | +### 6. Code Quality and Error Handling |
| 214 | + |
| 215 | +**Implementation**: |
| 216 | + |
| 217 | +- Type hints: Uses Python type hints (`redis.Redis | None`, `INetStream`) |
| 218 | +- Error handling: Comprehensive error handling with descriptive messages |
| 219 | +- Redis connection: Retry mechanism with exponential backoff for Redis connections |
| 220 | +- Stream creation: Retry mechanism for stream creation to handle timing issues |
| 221 | +- Debug logging: Optional debug logging controlled by `DEBUG` environment variable |
| 222 | +- Validation: Configuration validation for transports, security, and muxers |
| 223 | + |
| 224 | +**Files**: |
| 225 | + |
| 226 | +- `transport/images/python/v0.x/py-libp2p/interop/transport/ping_test.py` (throughout) |
| 227 | + |
| 228 | +## Testing |
| 229 | + |
| 230 | +The Python v0.x implementation follows the transport test framework specification and is tested as part of the full test suite. |
| 231 | + |
| 232 | +## Compatibility |
| 233 | + |
| 234 | +- ✅ Follows transport test framework specification (`transport/README.md`) |
| 235 | +- ✅ Matches Redis protocol used by Rust and JavaScript implementations |
| 236 | +- ✅ Uses uppercase environment variables only (as set by test framework) |
| 237 | +- ✅ Consistent with JavaScript v3.x reference implementation |
| 238 | +- ✅ Supports standalone transports (quic-v1) with proper handling |
| 239 | + |
| 240 | +## Supported Transports |
| 241 | + |
| 242 | +- **Standard transports**: `tcp`, `ws`, `wss` |
| 243 | +- **Standalone transports**: `quic-v1` (with built-in security and muxing) |
| 244 | + |
| 245 | +## Supported Security Channels |
| 246 | + |
| 247 | +- **Noise**: Noise protocol for encryption |
| 248 | +- **TLS**: TLS protocol for encryption |
| 249 | +- **Plaintext**: Insecure transport (for testing) |
| 250 | + |
| 251 | +## Supported Muxers |
| 252 | + |
| 253 | +- **Yamux**: Yamux stream multiplexer |
| 254 | +- **Mplex**: Mplex stream multiplexer |
0 commit comments