Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions en/py/01-identity-and-swarm/app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
FROM python:3.9-slim AS builder

WORKDIR /app

# Install build dependencies
RUN apt-get update && apt-get install -y \
build-essential \
libssl-dev \
libffi-dev \
&& rm -rf /var/lib/apt/lists/*

# Copy requirements and install Python dependencies
COPY requirements.txt* ./
RUN if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

# Install common dependencies for the workshop
RUN pip install cryptography base58 aiohttp

# Copy the application
COPY . .

# Final stage
FROM python:3.9-slim

# Install runtime dependencies
RUN apt-get update && apt-get install -y \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copy Python packages from builder
COPY --from=builder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin

# Copy the application
COPY . .

# Configurable timeout duration
ARG TIMEOUT_DURATION=10s
ENV TIMEOUT_DURATION=${TIMEOUT_DURATION}

# Set the command to run with timeout and redirect output
CMD ["/bin/sh", "-c", "timeout ${TIMEOUT_DURATION} python app/main.py > stdout.log 2>&1"]
62 changes: 62 additions & 0 deletions en/py/01-identity-and-swarm/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import trio
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
import hashlib
import base58

class LibP2PHost:
"""Basic libp2p Host implementation"""

def __init__(self, private_key, peer_id):
self.private_key = private_key
self.peer_id = peer_id
self.is_running = False

async def start(self):
"""Start the host"""
self.is_running = True
print(f"Host started with PeerId: {self.peer_id}")

async def stop(self):
"""Stop the host"""
self.is_running = False
print("Host stopped")

def get_peer_id(self):
"""Get the peer ID"""
return self.peer_id

async def main():
print("Starting Universal Connectivity Application...")

# Generate Ed25519 keypair for peer identity
private_key = ed25519.Ed25519PrivateKey.generate()
public_key = private_key.public_key()

# Extract public key bytes for PeerId generation
public_key_bytes = public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)

# Create PeerId by hashing the public key
peer_id_hash = hashlib.sha256(public_key_bytes).digest()
peer_id = base58.b58encode(peer_id_hash).decode('ascii')

print(f"Local peer id: {peer_id}")

# Create and start the libp2p host
host = LibP2PHost(private_key, peer_id)
await host.start()

# Keep the application running
try:
while host.is_running:
await trio.sleep(1)
except KeyboardInterrupt:
print("Shutting down...")
await host.stop()

if __name__ == "__main__":
trio.run(main)
185 changes: 185 additions & 0 deletions en/py/01-identity-and-swarm/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""
Check script for Lesson 1: Identity and Basic Host
Validates that the student's solution creates a libp2p host with identity.
"""

import subprocess
import sys
import os
import re
import base58

def validate_peer_id(peer_id_str):
"""Validate that the peer ID string is a valid base58 format"""
try:
# Try to decode the peer ID as base58
decoded = base58.b58decode(peer_id_str)

# Should be 32 bytes (SHA256 hash length)
if len(decoded) != 32:
return False, f"Invalid peer ID length. Expected 32 bytes, got {len(decoded)}: {peer_id_str}"

# Check if it's a valid base58 string (no invalid characters)
re_encoded = base58.b58encode(decoded).decode('ascii')
if re_encoded != peer_id_str:
return False, f"Peer ID base58 encoding is inconsistent: {peer_id_str}"

return True, f"Valid peer ID format: {peer_id_str}"

except Exception as e:
return False, f"Invalid peer ID format: {peer_id_str} - Error: {e}"

def check_output():
"""Check the output log for expected content"""
if not os.path.exists("stdout.log"):
print("❌ Error: stdout.log file not found")
return False

try:
with open("stdout.log", "r") as f:
output = f.read()

print("ℹ️ Checking application output...")

if not output.strip():
print("❌ stdout.log is empty - application may have failed to start")
return False

# Check for startup message
if "Starting Universal Connectivity Application" not in output:
print("❌ Missing startup message. Expected: 'Starting Universal Connectivity Application...'")
print(f"ℹ️ Actual output: {repr(output[:200])}")
return False
print("✅ Found startup message")

# Check for peer ID output
peer_id_pattern = r"Local peer id: ([A-Za-z0-9]+)"
peer_id_match = re.search(peer_id_pattern, output)

if not peer_id_match:
print("❌ Missing peer ID output. Expected format: 'Local peer id: <base58_string>'")
print(f"ℹ️ Actual output: {repr(output[:200])}")
return False

peer_id = peer_id_match.group(1)

# Validate the peer ID format
valid, message = validate_peer_id(peer_id)
if not valid:
print(f"❌ {message}")
return False

print(f"✅ {message}")

# Check for host startup message
if "Host started with PeerId:" not in output:
print("❌ Missing host startup message. Expected: 'Host started with PeerId: ...'")
print(f"ℹ️ Actual output: {repr(output[:200])}")
return False
print("✅ Found host startup message")

# Check that the application doesn't crash immediately
lines = output.strip().split('\n')
if len(lines) < 3:
print("❌ Application seems to have crashed immediately after startup")
print(f"ℹ️ Output lines: {lines}")
return False

print("✅ Application started successfully and generated valid peer identity")
return True

except Exception as e:
print(f"❌ Error reading stdout.log: {e}")
return False

def check_code_structure():
"""Check if the code has the expected structure"""
app_file = "app/main.py"

if not os.path.exists(app_file):
print("❌ Error: app/main.py file not found")
return False

try:
with open(app_file, "r") as f:
code = f.read()

print("ℹ️ Checking code structure...")

# Check for required imports
required_imports = [
"trio",
"ed25519",
"base58"
]

for imp in required_imports:
if imp not in code:
print(f"❌ Missing import: {imp}")
return False
print("✅ Required imports found")

# Check for LibP2PHost class
if "class LibP2PHost" not in code:
print("❌ Missing LibP2PHost class definition")
return False
print("✅ LibP2PHost class found")

# Check for async main function
if "async def main" not in code:
print("❌ Missing async main function")
return False
print("✅ Async main function found")

# Check for key generation
if "Ed25519PrivateKey.generate()" not in code:
print("❌ Missing Ed25519 key generation")
return False
print("✅ Ed25519 key generation found")

# Check for PeerId creation
if "base58.b58encode" not in code:
print("❌ Missing PeerId base58 encoding")
return False
print("✅ PeerId creation found")

print("✅ Code structure is correct")
return True

except Exception as e:
print(f"❌ Error reading code file: {e}")
return False

def main():
"""Main check function"""
print("🔍 Checking Lesson 1: Identity and Basic Host")
print("=" * 60)

try:
# Check code structure first
if not check_code_structure():
return False

# Check the output
if not check_output():
return False

print("=" * 60)
print("🎉 All checks passed! Your libp2p host is working correctly.")
print("✅ You have successfully:")
print(" • Created a libp2p host with a stable Ed25519 identity")
print(" • Generated and displayed a valid peer ID")
print(" • Set up a basic async event loop")
print(" • Implemented proper host lifecycle management")
print("\n🚀 Ready for Lesson 2: Transport and Multiaddrs!")

return True

except Exception as e:
print(f"💥 Unexpected error during checking: {e}")
return False

if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)
10 changes: 10 additions & 0 deletions en/py/01-identity-and-swarm/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
services:
lesson:
build:
context: ${PROJECT_ROOT}
dockerfile: ${LESSON_PATH}/app/Dockerfile
stop_grace_period: 1m
environment:
- TIMEOUT_DURATION=${TIMEOUT_DURATION:-10s}
volumes:
- ${PROJECT_ROOT}/${LESSON_PATH}/stdout.log:/app/stdout.log
Loading