Initial commit

This commit is contained in:
anonpenguin 2025-08-03 16:24:04 +03:00
commit a6aa516d74
35 changed files with 9020 additions and 0 deletions

17
.env.example Normal file
View File

@ -0,0 +1,17 @@
# Bootstrap Node Configuration
# Add multiple bootstrap peers separated by commas for redundancy
# Primary bootstrap peer (currently running)
BOOTSTRAP_PEERS=/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWN3AQHuxAzXfu98tiFYw7W3N2SyDwdxDRANXJp3ktVf8j
# Example with multiple bootstrap peers:
# BOOTSTRAP_PEERS=/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWN3AQHuxAzXfu98tiFYw7W3N2SyDwdxDRANXJp3ktVf8j,/ip4/127.0.0.1/tcp/4002/p2p/12D3KooWSecondBootstrapPeer,/ip4/192.168.1.100/tcp/4001/p2p/12D3KooWRemoteBootstrapPeer
# For production, you would add external IPs:
# BOOTSTRAP_PEERS=/ip4/bootstrap1.example.com/tcp/4001/p2p/12D3KooWPeer1,/ip4/bootstrap2.example.com/tcp/4001/p2p/12D3KooWPeer2
# Default bootstrap port
BOOTSTRAP_PORT=4001
# Environment (development, staging, production)
ENVIRONMENT=development

46
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,46 @@
<!-- Use this file to provide workspace-specific custom instructions to Copilot. For more details, visit https://code.visualstudio.com/docs/copilot/copilot-customization#_use-a-githubcopilotinstructionsmd-file -->
# Network - Distributed P2P Database System
This is a distributed peer-to-peer network project built with Go and LibP2P. The system provides decentralized database capabilities with consensus and replication.
## Key Components
- **LibP2P Network Layer**: Core networking built on LibP2P for P2P communication
- **Distributed Database**: RQLite-based distributed SQLite with Raft consensus
- **Client Library**: Go API for applications to interact with the network
- **Application Isolation**: Each app gets isolated namespaces for data and messaging
## Development Guidelines
1. **Architecture Patterns**: Follow the client-server pattern where applications use the client library to interact with the distributed network
2. **Namespacing**: All data (database, storage, pub/sub) is namespaced per application to ensure isolation
3. **Error Handling**: Always check for connection status before performing operations
4. **Async Operations**: Use context.Context for cancellation and timeouts
5. **Logging**: Use structured logging with appropriate log levels
## Code Style
- Use standard Go conventions and naming
- Implement interfaces for testability
- Include comprehensive error messages
- Add context parameters to all network operations
- Use dependency injection for components
## Testing Strategy
- Unit tests for individual components
- Integration tests for client library
- E2E tests for full network scenarios
- Mock external dependencies (LibP2P, database)
## Future Applications
This network is designed to support applications like:
- Anchat (encrypted messaging)
- Distributed file storage
- IoT data collection
- Social networks
When implementing applications, they should use the client library rather than directly accessing network internals.

73
.gitignore vendored Normal file
View File

@ -0,0 +1,73 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# Built binaries
bin/
dist/
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Log files
*.log
# Environment variables
.env
.env.local
.env.*.local
# Temporary files
tmp/
temp/
*.tmp
# Coverage reports
coverage.txt
coverage.html
profile.out
# Build artifacts
*.deb
*.rpm
*.tar.gz
*.zip
# Local development files
.local/
local/
data/*
./bootstrap
./node
data/bootstrap/rqlite/
.env

493
AI_CONTEXT.md Normal file
View File

@ -0,0 +1,493 @@
# AI Context - DeBros Network Cluster
## Table of Contents
- [Project Overview](#project-overview)
- [Product Requirements Document (PRD)](#product-requirements-document-prd)
- [Architecture Overview](#architecture-overview)
- [Codebase Structure](#codebase-structure)
- [Key Components](#key-components)
- [Network Protocol](#network-protocol)
- [Data Flow](#data-flow)
- [Build & Development](#build--development)
- [API Reference](#api-reference)
- [Troubleshooting](#troubleshooting)
## Project Overview
**DeBros Network Cluster** is a decentralized peer-to-peer (P2P) network built in Go that provides distributed database operations, key-value storage, pub/sub messaging, and peer discovery. The system is designed for applications that need resilient, distributed data management without relying on centralized infrastructure.
## Product Requirements Document (PRD)
### Vision
Create a robust, decentralized network platform that enables applications to seamlessly share data, communicate, and discover peers in a distributed environment.
### Core Requirements
#### Functional Requirements
1. **Distributed Database Operations**
- SQL query execution across network nodes
- ACID transactions with eventual consistency
- Schema management and table operations
- Multi-node resilience with automatic failover
2. **Key-Value Storage**
- Distributed storage with namespace isolation
- CRUD operations with consistency guarantees
- Prefix-based querying and key enumeration
- Data replication across network participants
3. **Pub/Sub Messaging**
- Topic-based publish/subscribe communication
- Real-time message delivery with ordering guarantees
- Subscription management with automatic cleanup
- Namespace isolation per application
4. **Peer Discovery & Management**
- Automatic peer discovery using DHT (Distributed Hash Table)
- Bootstrap node support for network joining
- Connection health monitoring and recovery
- Peer exchange for network growth
5. **Application Isolation**
- Namespace-based multi-tenancy
- Per-application data segregation
- Independent configuration and lifecycle management
#### Non-Functional Requirements
1. **Reliability**: 99.9% uptime with automatic failover
2. **Scalability**: Support 100+ nodes with linear performance
3. **Security**: End-to-end encryption for sensitive data
4. **Performance**: <100ms latency for local operations
5. **Developer Experience**: Simple client API with comprehensive examples
### Success Metrics
- Network uptime > 99.9%
- Peer discovery time < 30 seconds
- Database operation latency < 500ms
- Message delivery success rate > 99.5%
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────┐
│ DeBros Network Cluster │
├─────────────────────────────────────────────────────────────┤
│ Application Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Anchat │ │ Custom App │ │ CLI Tools │ │
│ │ (Chat) │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Client API │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Database │ │ Storage │ │ PubSub │ │
│ │ Client │ │ Client │ │ Client │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Network Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Discovery │ │ PubSub │ │ Consensus │ │
│ │ Manager │ │ Manager │ │ (RQLite) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Transport Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ LibP2P │ │ DHT │ │ RQLite │ │
│ │ Host │ │ Kademlia │ │ Database │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Key Design Principles
1. **Modularity**: Each component can be developed and tested independently
2. **Fault Tolerance**: Network continues operating even with node failures
3. **Consistency**: Strong consistency for database operations, eventual consistency for discovery
4. **Security**: Defense in depth with multiple security layers
5. **Performance**: Optimized for common operations with caching and connection pooling
## Codebase Structure
```
debros-testing/
├── cmd/ # Executables
│ ├── bootstrap/main.go # Bootstrap node (network entry point)
│ ├── node/main.go # Regular network node
│ └── cli/main.go # Command-line interface
├── pkg/ # Core packages
│ ├── client/ # Client API and implementations
│ │ ├── client.go # Main client implementation
│ │ ├── implementations.go # Database, storage, network implementations
│ │ └── interface.go # Public API interfaces
│ ├── config/ # Configuration management
│ │ └── config.go # Node and client configuration
│ ├── constants/ # System constants
│ │ └── bootstrap.go # Bootstrap node constants
│ ├── database/ # Database layer
│ │ ├── adapter.go # Database adapter interface
│ │ └── rqlite.go # RQLite implementation
│ ├── discovery/ # Peer discovery
│ │ └── discovery.go # DHT-based peer discovery
│ ├── node/ # Node implementation
│ │ └── node.go # Network node logic
│ ├── pubsub/ # Publish/Subscribe messaging
│ │ ├── manager.go # Core pub/sub logic
│ │ ├── adapter.go # Client interface adapter
│ │ └── types.go # Shared types
│ └── storage/ # Distributed storage
│ ├── client.go # Storage client
│ ├── protocol.go # Storage protocol
│ └── service.go # Storage service
├── anchat/ # Example chat application
│ ├── cmd/cli/main.go # Chat CLI
│ └── pkg/
│ ├── chat/manager.go # Chat message management
│ └── crypto/crypto.go # End-to-end encryption
├── examples/ # Usage examples
│ └── basic_usage.go # Basic API usage
├── configs/ # Configuration files
│ ├── bootstrap.yaml # Bootstrap node config
│ └── node.yaml # Regular node config
├── data/ # Runtime data
│ ├── bootstrap/ # Bootstrap node data
│ └── node/ # Regular node data
└── scripts/ # Utility scripts
└── test-multinode.sh # Multi-node testing
```
## Key Components
### 1. Network Client (`pkg/client/`)
The main entry point for applications to interact with the network.
**Core Interfaces:**
- `NetworkClient`: Main client interface
- `DatabaseClient`: SQL database operations
- `StorageClient`: Key-value storage operations
- `PubSubClient`: Publish/subscribe messaging
- `NetworkInfo`: Network status and peer information
**Key Features:**
- Automatic connection management with retry logic
- Namespace isolation per application
- Health monitoring and status reporting
- Graceful shutdown and cleanup
### 2. Peer Discovery (`pkg/discovery/`)
Handles automatic peer discovery and network topology management.
**Discovery Strategies:**
- **DHT-based**: Uses Kademlia DHT for efficient peer routing
- **Peer Exchange**: Learns about new peers from existing connections
- **Bootstrap**: Connects to known bootstrap nodes for network entry
**Configuration:**
- Discovery interval (default: 10 seconds)
- Maximum concurrent connections (default: 3)
- Connection timeout and retry policies
### 3. Pub/Sub System (`pkg/pubsub/`)
Provides reliable, topic-based messaging with ordering guarantees.
**Features:**
- Topic-based routing with wildcard support
- Namespace isolation per application
- Automatic subscription management
- Message deduplication and ordering
**Message Flow:**
1. Client subscribes to topic with handler
2. Publisher sends message to topic
3. Network propagates message to all subscribers
4. Handlers process messages asynchronously
### 4. Database Layer (`pkg/database/`)
Distributed SQL database built on RQLite (Raft-based SQLite).
**Capabilities:**
- ACID transactions with strong consistency
- Automatic leader election and failover
- Multi-node replication with conflict resolution
- Schema management and migrations
**Query Types:**
- Read operations: Served from any node
- Write operations: Routed to leader node
- Transactions: Atomic across multiple statements
### 5. Storage System (`pkg/storage/`)
Distributed key-value store with eventual consistency.
**Operations:**
- `Put(key, value)`: Store value with key
- `Get(key)`: Retrieve value by key
- `Delete(key)`: Remove key-value pair
- `List(prefix, limit)`: Enumerate keys with prefix
- `Exists(key)`: Check key existence
## Network Protocol
### Connection Establishment
1. **Bootstrap Connection**: New nodes connect to bootstrap peers
2. **DHT Bootstrap**: Initialize Kademlia DHT for routing
3. **Peer Discovery**: Discover additional peers through DHT
4. **Service Registration**: Register available services (database, storage, pubsub)
### Message Types
- **Control Messages**: Node status, heartbeats, topology updates
- **Database Messages**: SQL queries, transactions, schema operations
- **Storage Messages**: Key-value operations, replication data
- **PubSub Messages**: Topic subscriptions, published content
### Security Model
- **Transport Security**: All connections use TLS/Noise encryption
- **Peer Authentication**: Cryptographic peer identity verification
- **Message Integrity**: Hash-based message authentication codes
- **Namespace Isolation**: Application-level access control
## Data Flow
### Database Operation Flow
```
Client App → DatabaseClient → RQLite Leader → Raft Consensus → All Nodes
↑ ↓
└─────────────────── Query Result ←─────────────────────────────┘
```
### Storage Operation Flow
```
Client App → StorageClient → DHT Routing → Target Nodes → Replication
↑ ↓
└─────────────── Response ←─────────────────────────────────┘
```
### PubSub Message Flow
```
Publisher → PubSub Manager → Topic Router → All Subscribers → Message Handlers
```
## Build & Development
### Prerequisites
- Go 1.19+
- Make
- Git
### Build Commands
```bash
# Build all executables
make build
# Run tests
make test
# Clean build artifacts
make clean
# Start bootstrap node
make start-bootstrap
# Start regular node
make start-node
```
### Development Workflow
1. **Local Development**: Use `make start-bootstrap` + `make start-node`
2. **Testing**: Run `make test` for unit tests
3. **Integration Testing**: Use `scripts/test-multinode.sh`
4. **Configuration**: Edit `configs/*.yaml` files
### Configuration Files
#### Bootstrap Node (`configs/bootstrap.yaml`)
```yaml
node:
data_dir: "./data/bootstrap"
listen_addresses:
- "/ip4/0.0.0.0/tcp/4001"
- "/ip4/0.0.0.0/udp/4001/quic"
database:
rqlite_port: 5001
rqlite_raft_port: 7001
```
#### Regular Node (`configs/node.yaml`)
```yaml
node:
data_dir: "./data/node"
listen_addresses:
- "/ip4/0.0.0.0/tcp/4002"
discovery:
bootstrap_peers:
- "/ip4/127.0.0.1/tcp/4001/p2p/{BOOTSTRAP_PEER_ID}"
discovery_interval: "10s"
database:
rqlite_port: 5002
rqlite_raft_port: 7002
rqlite_join_address: "http://localhost:5001"
```
## API Reference
### Client Creation
```go
import "network/pkg/client"
config := client.DefaultClientConfig("my-app")
config.BootstrapPeers = []string{"/ip4/127.0.0.1/tcp/4001/p2p/{PEER_ID}"}
client, err := client.NewClient(config)
if err != nil {
log.Fatal(err)
}
err = client.Connect()
if err != nil {
log.Fatal(err)
}
defer client.Disconnect()
```
### Database Operations
```go
// Create table
err := client.Database().CreateTable(ctx, `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
)
`)
// Insert data
result, err := client.Database().Query(ctx,
"INSERT INTO users (name, email) VALUES (?, ?)",
"Alice", "alice@example.com")
// Query data
result, err := client.Database().Query(ctx,
"SELECT id, name, email FROM users WHERE name = ?", "Alice")
```
### Storage Operations
```go
// Store data
err := client.Storage().Put(ctx, "user:123", []byte(`{"name":"Alice"}`))
// Retrieve data
data, err := client.Storage().Get(ctx, "user:123")
// List keys
keys, err := client.Storage().List(ctx, "user:", 10)
// Check existence
exists, err := client.Storage().Exists(ctx, "user:123")
```
### PubSub Operations
```go
// Subscribe to messages
handler := func(topic string, data []byte) error {
fmt.Printf("Received on %s: %s\n", topic, string(data))
return nil
}
err := client.PubSub().Subscribe(ctx, "notifications", handler)
// Publish message
err := client.PubSub().Publish(ctx, "notifications", []byte("Hello, World!"))
// List subscribed topics
topics, err := client.PubSub().ListTopics(ctx)
```
### Network Information
```go
// Get network status
status, err := client.Network().GetStatus(ctx)
fmt.Printf("Node ID: %s, Peers: %d\n", status.NodeID, status.PeerCount)
// Get connected peers
peers, err := client.Network().GetPeers(ctx)
for _, peer := range peers {
fmt.Printf("Peer: %s, Connected: %v\n", peer.ID, peer.Connected)
}
// Connect to specific peer
err := client.Network().ConnectToPeer(ctx, "/ip4/192.168.1.100/tcp/4002/p2p/{PEER_ID}")
```
## Troubleshooting
### Common Issues
#### 1. Bootstrap Connection Failed
**Symptoms**: `Failed to connect to bootstrap peer`
**Solutions**:
- Verify bootstrap node is running and accessible
- Check firewall settings and port availability
- Validate peer ID in bootstrap address
#### 2. Database Operations Timeout
**Symptoms**: `Query timeout` or `No RQLite connection available`
**Solutions**:
- Ensure RQLite ports are not blocked
- Check if leader election has completed
- Verify cluster join configuration
#### 3. Message Delivery Failures
**Symptoms**: Messages not received by subscribers
**Solutions**:
- Verify topic names match exactly
- Check subscription is active before publishing
- Ensure network connectivity between peers
#### 4. High Memory Usage
**Symptoms**: Memory usage grows continuously
**Solutions**:
- Check for subscription leaks (unsubscribe when done)
- Monitor connection pool size
- Review message retention policies
### Debug Mode
Enable debug logging by setting environment variable:
```bash
export LOG_LEVEL=debug
```
### Health Checks
```go
health, err := client.Health()
if health.Status != "healthy" {
log.Printf("Unhealthy: %+v", health.Checks)
}
```
### Network Diagnostics
```bash
# Check node connectivity
./bin/network-cli peers
# Verify database status
./bin/network-cli query "SELECT 1"
# Test pub/sub
./bin/network-cli pubsub publish test "hello"
./bin/network-cli pubsub subscribe test 10s
```
---
## Example Application: Anchat
The `anchat/` directory contains a complete example application demonstrating how to build a decentralized chat system using the DeBros network. It showcases:
- User registration with Solana wallet integration
- End-to-end encrypted messaging
- IRC-style chat rooms
- Real-time message delivery
- Persistent chat history
This serves as both a practical example and a reference implementation for building applications on the DeBros network platform.
---
*This document provides comprehensive context for AI systems to understand the DeBros Network Cluster project architecture, implementation details, and usage patterns.*

184
Makefile Normal file
View File

@ -0,0 +1,184 @@
# Network - Distributed P2P Database System
# Makefile for development and build tasks
.PHONY: build clean test run-bootstrap run-node run-example deps tidy fmt vet
# Build targets
build: deps
@echo "Building network executables..."
@mkdir -p bin
go build -o bin/bootstrap cmd/bootstrap/main.go
go build -o bin/node cmd/node/main.go
go build -o bin/network-cli cmd/cli/main.go
@echo "Build complete!"
# Clean build artifacts
clean:
@echo "Cleaning build artifacts..."
rm -rf bin/
rm -rf data/
@echo "Clean complete!"
# Run tests
test:
@echo "Running tests..."
go test -v ./...
# Run bootstrap node
run-bootstrap:
@echo "Starting bootstrap node..."
go run cmd/bootstrap/main.go -port 4001 -data ./data/bootstrap
# Run regular node
run-node:
@echo "Starting regular node..."
go run cmd/node/main.go -data ./data/node
# Show current bootstrap configuration
show-bootstrap:
@echo "Current bootstrap configuration from .env:"
@cat .env 2>/dev/null || echo "No .env file found - using defaults"
# Run example
run-example:
@echo "Running basic usage example..."
go run examples/basic_usage.go
# Build Anchat
build-anchat:
@echo "Building Anchat..."
cd anchat && go build -o bin/anchat cmd/cli/main.go
# Run Anchat demo
run-anchat:
@echo "Starting Anchat demo..."
cd anchat && go run cmd/cli/main.go demo_user
# Run network CLI
run-cli:
@echo "Running network CLI help..."
./bin/network-cli help
# Network CLI helper commands
cli-health:
@echo "Checking network health..."
./bin/network-cli health
cli-peers:
@echo "Listing network peers..."
./bin/network-cli peers
cli-status:
@echo "Getting network status..."
./bin/network-cli status
cli-storage-test:
@echo "Testing storage operations..."
@./bin/network-cli storage put test-key "Hello Network" || echo "Storage test requires running network"
@./bin/network-cli storage get test-key || echo "Storage test requires running network"
@./bin/network-cli storage list || echo "Storage test requires running network"
cli-pubsub-test:
@echo "Testing pub/sub operations..."
@./bin/network-cli pubsub publish test-topic "Hello World" || echo "PubSub test requires running network"
@./bin/network-cli pubsub topics || echo "PubSub test requires running network"
# Download dependencies
deps:
@echo "Downloading dependencies..."
go mod download
# Tidy dependencies
tidy:
@echo "Tidying dependencies..."
go mod tidy
# Format code
fmt:
@echo "Formatting code..."
go fmt ./...
# Vet code
vet:
@echo "Vetting code..."
go vet ./...
# Development setup
dev-setup: deps
@echo "Setting up development environment..."
@mkdir -p data/bootstrap data/node1 data/node2
@mkdir -p data/test-bootstrap data/test-node1 data/test-node2
@mkdir -p anchat/bin
@echo "Development setup complete!"
# Multi-node testing
test-multinode: build
@echo "🧪 Starting comprehensive multi-node test..."
@chmod +x scripts/test-multinode.sh
@./scripts/test-multinode.sh
test-peer-discovery: build
@echo "🔍 Testing peer discovery (requires running nodes)..."
@echo "Connected peers:"
@./bin/network-cli peers --timeout 10s
test-replication: build
@echo "🔄 Testing data replication (requires running nodes)..."
@./bin/network-cli storage put "replication:test:$$(date +%s)" "Test data - $$(date)"
@sleep 2
@echo "Retrieving replicated data:"
@./bin/network-cli storage list replication:test:
test-consensus: build
@echo "🗄️ Testing database consensus (requires running nodes)..."
@./bin/network-cli query "CREATE TABLE IF NOT EXISTS consensus_test (id INTEGER PRIMARY KEY, test_data TEXT, timestamp TEXT)"
@./bin/network-cli query "INSERT INTO consensus_test (test_data, timestamp) VALUES ('Makefile test', '$$(date)')"
@./bin/network-cli query "SELECT * FROM consensus_test ORDER BY id DESC LIMIT 5"
# Start development cluster (requires multiple terminals)
dev-cluster:
@echo "To start a development cluster, run these commands in separate terminals:"
@echo "1. make run-bootstrap # Start bootstrap node"
@echo "2. make run-node # Start regular node (auto-loads bootstrap from .env)"
@echo "3. make run-example # Test basic functionality"
@echo "4. make run-anchat # Start messaging app"
@echo "5. make show-bootstrap # Check bootstrap configuration"
@echo "6. make cli-health # Check network health"
@echo "7. make cli-peers # List peers"
@echo "8. make cli-storage-test # Test storage"
@echo "9. make cli-pubsub-test # Test messaging"
# Full development workflow
dev: clean build build-anchat test
@echo "Development workflow complete!"
# Help
help:
@echo "Available targets:"
@echo " build - Build all executables"
@echo " build-anchat - Build Anchat application"
@echo " clean - Clean build artifacts"
@echo " test - Run tests"
@echo " run-bootstrap - Start bootstrap node"
@echo " run-node - Start regular node (auto-loads bootstrap from .env)"
@echo " run-example - Run usage example"
@echo " run-anchat - Run Anchat demo"
@echo " run-cli - Run network CLI help"
@echo " show-bootstrap - Show current bootstrap configuration"
@echo " cli-health - Check network health"
@echo " cli-peers - List network peers"
@echo " cli-status - Get network status"
@echo " cli-storage-test - Test storage operations"
@echo " cli-pubsub-test - Test pub/sub operations"
@echo " test-multinode - Full multi-node test with 1 bootstrap + 2 nodes"
@echo " test-peer-discovery - Test peer discovery (requires running nodes)"
@echo " test-replication - Test data replication (requires running nodes)"
@echo " test-consensus - Test database consensus (requires running nodes)"
@echo " deps - Download dependencies"
@echo " tidy - Tidy dependencies"
@echo " fmt - Format code"
@echo " vet - Vet code"
@echo " dev-setup - Setup development environment"
@echo " dev-cluster - Show cluster startup commands"
@echo " dev - Full development workflow"
@echo " help - Show this help"

914
README.md Normal file
View File

@ -0,0 +1,914 @@
# Network - Distributed P2P Database System
A distributed peer-to-peer network built with Go and LibP2P, providing decentralized database capabilities with RQLite consensus and replication.
## Features
- **Peer-to-Peer Networking**: Built on LibP2P for robust P2P communication
- **Distributed Database**: RQLite-based distributed SQLite with Raft consensus
- **Automatic Peer Discovery**: Bootstrap nodes help new peers join the network
- **CLI Tool**: Command-line interface for network operations and testing
- **Client Library**: Simple Go API for applications to interact with the network
- **Application Isolation**: Namespaced storage and messaging per application
## System Requirements
### Software Dependencies
- **Go**: Version 1.21 or later
- **RQLite**: Distributed SQLite database
- **Git**: For cloning the repository
- **Make**: For build automation (optional but recommended)
### Installation
#### macOS
```bash
# Install Homebrew if you don't have it
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Install dependencies
brew install go rqlite git make
# Verify installation
go version # Should show Go 1.21+
rqlited --version
```
#### Ubuntu/Debian
```bash
# Install Go (latest version)
sudo rm -rf /usr/local/go
wget https://go.dev/dl/go1.21.6.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.21.6.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
# Install RQLite
wget https://github.com/rqlite/rqlite/releases/download/v8.43.0/rqlite-v8.43.0-linux-amd64.tar.gz
tar -xzf rqlite-v8.43.0-linux-amd64.tar.gz
sudo mv rqlite-v8.43.0-linux-amd64/rqlited /usr/local/bin/
# Install other dependencies
sudo apt update
sudo apt install git make
# Verify installation
go version
rqlited --version
```
#### Windows
```powershell
# Install Go from https://golang.org/dl/
# Install Git from https://git-scm.com/download/win
# Install RQLite from https://github.com/rqlite/rqlite/releases
# Or use Chocolatey
choco install golang git make
# Download RQLite manually from releases page
```
### Hardware Requirements
**Minimum:**
- CPU: 2 cores
- RAM: 4GB
- Storage: 10GB free space
- Network: Stable internet connection
**Recommended:**
- CPU: 4+ cores
- RAM: 8GB+
- Storage: 50GB+ SSD
- Network: Low-latency internet connection
### Network Ports
The system uses these ports by default:
- **4001-4003**: LibP2P communication
- **5001-5003**: RQLite HTTP API
- **7001-7003**: RQLite Raft consensus
Ensure these ports are available or configure firewall rules accordingly.
## Quick Start
### 1. Clone and Setup Environment
```bash
# Clone the repository
git clone https://git.debros.io/DeBros/network-cluster.git
cd network-cluster
# Copy environment configuration
cp .env.example .env
```
### 2. Generate Bootstrap Identity (Development Only)
For development, you need to generate a consistent bootstrap peer identity:
```bash
# Generate bootstrap peer identity
go run scripts/generate-bootstrap-identity.go
# This will create data/bootstrap/identity.key and show the peer ID
# Copy the peer ID and update your .env files
```
**Important:** After generating the bootstrap identity, update both `.env` files:
```bash
# Update main .env file
nano .env
# Update BOOTSTRAP_PEERS with the generated peer ID
# Update Anchat .env file
nano anchat/.env
# Update BOOTSTRAP_PEERS with the same peer ID
```
### 3. Build the Project
```bash
# Build all network executables
make build
# Build Anchat application
cd anchat
make build
cd ..
# Or build everything at once
make build && make build-anchat
```
### 4. Start the Network
**Terminal 1 - Bootstrap Node:**
```bash
make run-bootstrap
# This starts the bootstrap node on port 4001
```
**Terminal 2 - Regular Node:**
```bash
make run-node
# This automatically connects to bootstrap peers from .env
# No need to specify bootstrap manually anymore!
```
**Terminal 3 - Another Node (optional):**
```bash
# For additional nodes, use different ports
go run cmd/node/main.go -data ./data/node2 -port 4003
```
### 5. Test with CLI
```bash
# Check current bootstrap configuration
make show-bootstrap
# Check network health
./bin/cli health
# Test storage operations
./bin/cli storage put test-key "Hello Network"
./bin/cli storage get test-key
# List connected peers
./bin/cli peers
```
### 6. Test Anchat Messaging
```bash
# Terminal 1 - First user
cd anchat
./bin/anchat
# Terminal 2 - Second user
cd anchat
./bin/anchat
```
## Deployment
### Production Installation Script
For production deployments on Linux servers, we provide an automated installation script that handles all dependencies, configuration, and service setup.
#### One-Command Installation
```bash
# Download and run the installation script
curl -sSL https://raw.githubusercontent.com/DeBrosOfficial/debros-network/main/scripts/install-debros-network.sh | bash
```
#### What the Script Does
1. **System Setup**:
- Detects OS (Ubuntu/Debian/CentOS/RHEL/Fedora)
- Installs Go 1.21+ with architecture detection
- Installs system dependencies (git, make, build tools)
- Checks port availability (4001-4003, 5001-5003, 7001-7003)
2. **Configuration Wizard**:
- Node type selection (bootstrap vs regular node)
- Solana wallet address for node operator rewards
- Installation directory (default: `/opt/debros`)
- Automatic firewall configuration (UFW)
3. **Secure Installation**:
- Creates dedicated `debros` system user
- Sets up secure directory structure with proper permissions
- Generates LibP2P identity keys with secure storage
- Clones source code and builds binaries
4. **Service Management**:
- Creates systemd service with security hardening
- Enables automatic startup and restart on failure
- Configures structured logging to systemd journal
#### Directory Structure
The script creates a production-ready directory structure:
```
/opt/debros/
├── bin/ # Compiled binaries
│ ├── bootstrap # Bootstrap node executable
│ ├── node # Regular node executable
│ └── cli # CLI tools
├── configs/ # Configuration files
│ ├── bootstrap.yaml # Bootstrap node config
│ └── node.yaml # Regular node config
├── keys/ # Identity keys (secure 700 permissions)
│ ├── bootstrap/
│ │ └── identity.key
│ └── node/
│ └── identity.key
├── data/ # Runtime data
│ ├── bootstrap/
│ │ ├── rqlite/ # RQLite database files
│ │ └── storage/ # P2P storage data
│ └── node/
│ ├── rqlite/
│ └── storage/
├── logs/ # Application logs
│ ├── bootstrap.log
│ └── node.log
└── src/ # Source code (for updates)
```
#### Node Types
**Bootstrap Node**:
- Network entry point that other nodes connect to
- Runs on ports: 4001 (P2P), 5001 (RQLite), 7001 (Raft)
- Should be deployed on stable, publicly accessible servers
- Acts as initial seed for peer discovery
**Regular Node**:
- Connects to bootstrap peers automatically (hardcoded in code)
- Runs on ports: 4002 (P2P), 5002 (RQLite), 7002 (Raft)
- Participates in DHT for peer discovery and data replication
- Can be deployed on any server or VPS
#### Service Management
After installation, manage your node with these commands:
```bash
# Check service status
sudo systemctl status debros-bootstrap # or debros-node
# Start/stop/restart service
sudo systemctl start debros-bootstrap
sudo systemctl stop debros-bootstrap
sudo systemctl restart debros-bootstrap
# View real-time logs
sudo journalctl -u debros-bootstrap.service -f
# Enable/disable auto-start
sudo systemctl enable debros-bootstrap
sudo systemctl disable debros-bootstrap
# Use CLI tools
/opt/debros/bin/cli health
/opt/debros/bin/cli peers
/opt/debros/bin/cli storage put key value
```
#### Configuration Files
The script generates YAML configuration files:
**Bootstrap Node (`/opt/debros/configs/bootstrap.yaml`)**:
```yaml
node:
data_dir: "/opt/debros/data/bootstrap"
key_file: "/opt/debros/keys/bootstrap/identity.key"
listen_addresses:
- "/ip4/0.0.0.0/tcp/4001"
solana_wallet: "YOUR_WALLET_ADDRESS"
database:
rqlite_port: 5001
rqlite_raft_port: 7001
logging:
level: "info"
file: "/opt/debros/logs/bootstrap.log"
```
**Regular Node (`/opt/debros/configs/node.yaml`)**:
```yaml
node:
data_dir: "/opt/debros/data/node"
key_file: "/opt/debros/keys/node/identity.key"
listen_addresses:
- "/ip4/0.0.0.0/tcp/4002"
solana_wallet: "YOUR_WALLET_ADDRESS"
database:
rqlite_port: 5002
rqlite_raft_port: 7002
logging:
level: "info"
file: "/opt/debros/logs/node.log"
```
#### Security Features
The installation script implements production security best practices:
- **Dedicated User**: Runs as `debros` system user (not root)
- **File Permissions**: Key files have 600 permissions, directories have proper ownership
- **Systemd Security**: Service runs with `NoNewPrivileges`, `PrivateTmp`, `ProtectSystem=strict`
- **Firewall**: Automatic UFW configuration for required ports
- **Network Isolation**: Each node type uses different ports to avoid conflicts
#### Network Discovery
- **Bootstrap Peers**: Hardcoded in the application for automatic connection
- **DHT Discovery**: Nodes automatically join Kademlia DHT for peer discovery
- **Peer Exchange**: Connected nodes share information about other peers
- **No Manual Configuration**: Regular nodes connect automatically without user intervention
#### Updates and Maintenance
```bash
# Update to latest version (re-run the installation script)
curl -sSL https://raw.githubusercontent.com/DeBrosOfficial/debros-network/main/scripts/install-debros-network.sh | bash
# Manual source update
cd /opt/debros/src
sudo -u debros git pull
sudo -u debros make build
sudo cp bin/* /opt/debros/bin/
sudo systemctl restart debros-bootstrap # or debros-node
# Backup configuration and keys
sudo cp -r /opt/debros/configs /backup/
sudo cp -r /opt/debros/keys /backup/
```
#### Monitoring and Troubleshooting
```bash
# Check if ports are open
sudo netstat -tuln | grep -E "(4001|4002|5001|5002|7001|7002)"
# Check service logs
sudo journalctl -u debros-bootstrap.service --since "1 hour ago"
# Check network connectivity
/opt/debros/bin/cli health
/opt/debros/bin/cli peers
# Check disk usage
du -sh /opt/debros/data/*
# Process information
ps aux | grep debros
```
For more advanced configuration options and development setup, see the sections below.
## Environment Configuration
### Bootstrap Peers Configuration
The network uses `.env` files to configure bootstrap peers automatically. This eliminates the need to manually specify bootstrap peer addresses when starting nodes.
#### Setup for Development
1. **Copy example configuration:**
```bash
cp .env.example .env
cp anchat/.env.example anchat/.env
```
2. **Generate bootstrap identity:**
```bash
go run scripts/generate-bootstrap-identity.go
```
3. **Update .env files with the generated peer ID:**
```bash
# Main network .env
BOOTSTRAP_PEERS=/ip4/127.0.0.1/tcp/4001/p2p/YOUR_GENERATED_PEER_ID
# Anchat .env
BOOTSTRAP_PEERS=/ip4/127.0.0.1/tcp/4001/p2p/YOUR_GENERATED_PEER_ID
```
#### Configuration Files
**Main Network (.env):**
```bash
# Bootstrap Node Configuration for Development
BOOTSTRAP_PEERS=/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWN3AQHuxAzXfu98tiFYw7W3N2SyDwdxDRANXJp3ktVf8j
BOOTSTRAP_PORT=4001
ENVIRONMENT=development
```
**Anchat Application (anchat/.env):**
```bash
# Anchat Bootstrap Configuration
BOOTSTRAP_PEERS=/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWN3AQHuxAzXfu98tiFYw7W3N2SyDwdxDRANXJp3ktVf8j
BOOTSTRAP_PORT=4001
ENVIRONMENT=development
ANCHAT_LOG_LEVEL=info
ANCHAT_DATABASE_NAME=anchattestingdb1
```
#### Multiple Bootstrap Peers
For production or redundancy, you can specify multiple bootstrap peers:
```bash
BOOTSTRAP_PEERS=/ip4/bootstrap1.example.com/tcp/4001/p2p/12D3KooWPeer1,/ip4/bootstrap2.example.com/tcp/4001/p2p/12D3KooWPeer2,/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWLocalPeer
```
#### Checking Configuration
```bash
# View current bootstrap configuration
make show-bootstrap
# Check which .env file is being used
cat .env
cat anchat/.env
```
## CLI Commands
The CLI and nodes now automatically load bootstrap peers from `.env` files - no manual configuration needed!
### Network Operations
```bash
./bin/cli health # Check network health
./bin/cli status # Get network status
./bin/cli peers # List connected peers
```
### Storage Operations
```bash
./bin/cli storage put <key> <value> # Store data
./bin/cli storage get <key> # Retrieve data
./bin/cli storage list [prefix] # List keys
```
### Database Operations
```bash
./bin/cli query "SELECT * FROM table" # Execute SQL
./bin/cli query "CREATE TABLE users (id INTEGER)" # DDL operations
```
### Pub/Sub Messaging
```bash
./bin/cli pubsub publish <topic> <message> # Send message
./bin/cli pubsub subscribe <topic> [duration] # Listen for messages
./bin/cli pubsub topics # List active topics
```
### CLI Options
```bash
--format json # Output in JSON format
--timeout 30s # Set operation timeout
--bootstrap <multiaddr> # Override bootstrap peer
```
## Development
### Project Structure
```
network-cluster/
├── cmd/
│ ├── bootstrap/ # Bootstrap node
│ ├── node/ # Regular network node
│ └── cli/ # Command-line interface
├── pkg/
│ ├── client/ # Client library
│ ├── node/ # Node implementation
│ ├── database/ # RQLite integration
│ ├── storage/ # Storage service
│ ├── constants/ # Bootstrap configuration
│ └── config/ # System configuration
├── anchat/ # Anchat messaging application
│ ├── cmd/cli/ # Anchat CLI
│ ├── pkg/
│ │ ├── chat/ # Chat functionality
│ │ ├── crypto/ # Encryption
│ │ └── constants/ # Anchat bootstrap config
│ ├── .env # Anchat environment config
│ └── .env.example # Anchat config template
├── scripts/
│ └── generate-bootstrap-identity.go # Bootstrap ID generator
├── .env # Main environment config
├── .env.example # Main config template
├── bin/ # Built executables
├── data/ # Runtime data directories
└── Makefile # Build and run commands
```
### Building and Testing
```bash
# Build all network executables
make build
# Build Anchat application
cd anchat && make build && cd ..
# or
make build-anchat
# Show current bootstrap configuration
make show-bootstrap
# Run bootstrap node (uses .env automatically)
make run-bootstrap
# Run regular node (uses .env automatically - no bootstrap flag needed!)
make run-node
# Clean data directories
make clean
# Run tests
go test ./...
# Full development workflow
make dev
```
### Development Workflow
1. **Initial Setup:**
```bash
# Copy environment templates
cp .env.example .env
cp anchat/.env.example anchat/.env
# Generate consistent bootstrap identity
go run scripts/generate-bootstrap-identity.go
# Update both .env files with the generated peer ID
```
2. **Build Everything:**
```bash
make build # Build network components
make build-anchat # Build Anchat application
```
3. **Start Development Cluster:**
```bash
# Terminal 1: Bootstrap node
make run-bootstrap
# Terminal 2: Regular node (auto-connects via .env)
make run-node
# Terminal 3: Test with CLI
./bin/cli health
./bin/cli peers
# Terminal 4 & 5: Test Anchat
cd anchat && ./bin/anchat
```
### Environment Setup
1. **Install Dependencies:**
```bash
# macOS
brew install go rqlite git make
# Ubuntu/Debian
sudo apt install golang-go git make
# Install RQLite from https://github.com/rqlite/rqlite/releases
```
2. **Verify Installation:**
```bash
go version # Should be 1.21+
rqlited --version
make --version
```
3. **Configure Environment:**
```bash
# Setup .env files
cp .env.example .env
cp anchat/.env.example anchat/.env
# Generate bootstrap identity
go run scripts/generate-bootstrap-identity.go
# Update .env files with generated peer ID
```
### Configuration System
The network uses a dual configuration system:
1. **Environment Variables (.env files):** Primary configuration method
2. **Hardcoded Constants:** Fallback when .env files are not found
#### Bootstrap Configuration Priority:
1. Command line flags (if provided)
2. Environment variables from `.env` files
3. Hardcoded constants in `pkg/constants/bootstrap.go`
4. Auto-discovery from running bootstrap nodes
This ensures the network can start even without configuration files, while allowing easy customization for different environments.
## Client Library Usage
```go
package main
import (
"context"
"log"
"network/pkg/client"
)
func main() {
// Create client (bootstrap peer discovered automatically)
config := client.DefaultClientConfig("my-app")
networkClient, err := client.NewClient(config)
if err != nil {
log.Fatal(err)
}
// Connect to network
if err := networkClient.Connect(); err != nil {
log.Fatal(err)
}
defer networkClient.Disconnect()
// Use storage
ctx := context.Background()
storage := networkClient.Storage()
err = storage.Put(ctx, "user:123", []byte("user data"))
if err != nil {
log.Fatal(err)
}
data, err := storage.Get(ctx, "user:123")
if err != nil {
log.Fatal(err)
}
log.Printf("Retrieved: %s", string(data))
}
```
## Anchat - Decentralized Messaging Application
Anchat is a demonstration application built on the network that provides decentralized, encrypted messaging capabilities.
### Features
- **Decentralized Messaging**: No central servers, messages flow through the P2P network
- **Wallet-based Authentication**: Connect using Solana wallet addresses
- **Encrypted Communications**: End-to-end encryption for private messages
- **Room-based Chat**: Create and join chat rooms
- **Network Auto-discovery**: Automatically finds and connects to other Anchat users
### Quick Start with Anchat
1. **Setup Environment:**
```bash
# Ensure main network is configured
cp .env.example .env
cp anchat/.env.example anchat/.env
# Generate bootstrap identity and update .env files
go run scripts/generate-bootstrap-identity.go
```
2. **Build Anchat:**
```bash
cd anchat
make build
```
3. **Start Network Infrastructure:**
```bash
# Terminal 1: Bootstrap node
make run-bootstrap
# Terminal 2: Regular node (optional but recommended)
make run-node
```
4. **Start Anchat Clients:**
```bash
# Terminal 3: First user
cd anchat
./bin/anchat
# Terminal 4: Second user
cd anchat
./bin/anchat
```
### Anchat Commands
```bash
# Room management
/list # List available rooms
/join <room> # Join a room
/leave # Leave current room
/create <room> [desc] # Create a new room
# Messaging
<message> # Send message to current room
/msg <user> <message> # Send private message
/me <action> # Send action message
# User management
/users # List users in current room
/nick <username> # Change username
/who # Show your user info
# System
/help # Show all commands
/debug # Show debug information
/quit # Exit Anchat
```
### Anchat Configuration
Anchat uses its own bootstrap configuration in `anchat/.env`:
```bash
# Anchat-specific environment variables
BOOTSTRAP_PEERS=/ip4/127.0.0.1/tcp/4001/p2p/YOUR_BOOTSTRAP_PEER_ID
BOOTSTRAP_PORT=4001
ENVIRONMENT=development
ANCHAT_LOG_LEVEL=info
ANCHAT_DATABASE_NAME=anchattestingdb1
```
The Anchat application also includes hardcoded fallback bootstrap peers in `anchat/pkg/constants/bootstrap.go` for reliability.
## Troubleshooting
### Common Issues
**Bootstrap peer not found / Peer ID mismatch:**
- Generate a new bootstrap identity: `go run scripts/generate-bootstrap-identity.go`
- Update both `.env` and `anchat/.env` with the new peer ID
- Restart the bootstrap node: `make run-bootstrap`
- Check configuration: `make show-bootstrap`
**Nodes can't connect:**
- Verify `.env` files have the correct bootstrap peer ID
- Check that the bootstrap node is running: `ps aux | grep bootstrap`
- Verify firewall settings and port availability (4001, 5001, 7001)
- Try restarting with clean data: `make clean && make run-bootstrap`
**Storage operations fail:**
- Ensure at least one node is running and connected
- Check network health: `./bin/cli health`
- Verify RQLite is properly installed: `rqlited --version`
- Check for port conflicts: `netstat -an | grep -E "(4001|5001|7001)"`
**Anchat clients can't discover each other:**
- Ensure both clients use the same bootstrap peer ID in `anchat/.env`
- Verify the bootstrap node is running
- Check that both clients successfully connect to bootstrap
- Look for "peer id mismatch" errors in the logs
### Debug Commands
```bash
# Check current configuration
make show-bootstrap
cat .env
cat anchat/.env
# Check running processes
ps aux | grep -E "(bootstrap|node|rqlite)"
# Check port usage
netstat -an | grep -E "(4001|4002|4003|5001|5002|5003|7001|7002|7003)"
# Check bootstrap peer info
cat data/bootstrap/peer.info
# Clean and restart everything
make clean
make run-bootstrap # In one terminal
make run-node # In another terminal
```
### Environment-specific Issues
**Development Environment:**
- Always run `go run scripts/generate-bootstrap-identity.go` first
- Update `.env` files with the generated peer ID
- Use `make run-node` instead of manual bootstrap specification
**Production Environment:**
- Use stable, external bootstrap peer addresses
- Configure multiple bootstrap peers for redundancy
- Set `ENVIRONMENT=production` in `.env` files
### Configuration Validation
```bash
# Test bootstrap configuration loading
go run -c 'package main; import "fmt"; import "network/pkg/constants"; func main() { fmt.Printf("Bootstrap peers: %v\n", constants.GetBootstrapPeers()) }'
# Verify .env file syntax
grep -E "^[A-Z_]+=.*" .env
grep -E "^[A-Z_]+=.*" anchat/.env
```
### Logs and Data
- Node logs: Console output from each running process
- Data directories: `./data/bootstrap/`, `./data/node/`, etc.
- RQLite data: `./data/<node>/rqlite/`
- Peer info: `./data/<node>/peer.info`
- Bootstrap identity: `./data/bootstrap/identity.key`
- Environment config: `./.env`, `./anchat/.env`
## License
MIT License - see LICENSE file for details.

373
WHITEPAPER.md Normal file
View File

@ -0,0 +1,373 @@
# DeBros Network: A Peer-to-Peer Decentralized Database Ecosystem
**DeBros**
info@debros.io
https://debros.io
August 2, 2025
## Abstract
We propose a decentralized ecosystem, the DeBros Network, enabling peer-to-peer application deployment and operation across a global network of nodes, free from centralized control. Built with Go and LibP2P for robust networking, RQLite for distributed consensus, and integrated with the Solana blockchain for identity and governance, the network provides a resilient, privacy-first platform for decentralized applications. Participation is governed by NFT ownership and token staking, with demonstrated applications like Anchat showcasing real-world messaging capabilities. The architecture eliminates single points of failure while maintaining developer simplicity and end-user accessibility.
## 1. Introduction
Centralized systems dominate modern technology, imposing control over data, access, and development through single points of failure and intermediaries. These structures compromise privacy, resilience, innovation, and freedom, while existing decentralized solutions often lack the simplicity or scalability needed for widespread adoption.
The DeBros Network resolves these issues by establishing a peer-to-peer platform where nodes form a decentralized backbone for applications. Built on Go's performance and LibP2P's proven networking stack, it empowers a global community of developers and users to collaborate as equals, delivering scalable, privacy-first solutions without centralized oversight.
## 2. Problem Statement
Centralized application platforms introduce critical vulnerabilities:
- **Data breaches** through single points of failure
- **Censorship** and restricted access by gatekeeping authorities
- **Limited development access** controlled by platform owners
- **Privacy erosion** through centralized data collection
- **Vendor lock-in** preventing migration and innovation
Existing blockchain-based alternatives prioritize financial systems over general-purpose application hosting, leaving a gap for a decentralized, developer-friendly infrastructure that balances scalability, performance, and accessibility.
## 3. Solution Architecture
The DeBros Network is a decentralized system where nodes, built with Go and running on various platforms, collaboratively host and serve applications through a distributed database layer. The architecture eliminates central authorities by distributing control across participants via cryptographic mechanisms and consensus protocols.
### 3.1 Core Components
**Network Layer:**
- **LibP2P**: Peer-to-peer communication and discovery
- **DHT (Distributed Hash Table)**: Peer routing and content discovery
- **Bootstrap nodes**: Network entry points for peer discovery
- **Auto-discovery**: Dynamic peer finding and connection management
**Database Layer:**
- **RQLite**: Distributed SQLite with Raft consensus
- **Consensus**: Automatic leader election and data replication
- **Isolation**: Application-specific database namespaces
- **ACID compliance**: Strong consistency guarantees
**Application Layer:**
- **Client Library**: Go SDK for application development
- **Namespace isolation**: Per-application data and messaging separation
- **RESTful API**: Standard HTTP interface for application integration
- **Real-time messaging**: Pub/sub system for live communications
### 3.2 Privacy and Security
Privacy is achieved through:
- **Distributed data storage** across multiple nodes
- **Wallet-based authentication** using Solana addresses
- **End-to-end encryption** for sensitive communications
- **No central data collection** or surveillance capabilities
- **Cryptographic verification** of all network operations
## 4. Participation Mechanism
The DeBros Network governs participation through an 800-NFT collection and DeBros token staking system, ensuring both accessibility and incentivized collaboration.
### 4.1 NFT-Based Access Control
**700 Standard NFTs:**
- Lifetime access to all DeBros applications
- Node operation capabilities without token staking
- Independent development rights
- Community participation privileges
**100 Team NFTs:**
- Exclusive team membership and collaboration rights
- Full development dashboard access
- Strategic influence over partnerships and tokenomics
- Revenue sharing opportunities (75% of funded applications)
- Unlimited application access and deployment rights
### 4.2 Token Staking Alternative
**DeBros Token Staking:**
- 100 DeBros tokens enable node operation without NFT ownership
- Alternative pathway for network participation
- Aligned economic incentives with network health
- Flexible entry mechanism for diverse participants
### 4.3 Revenue Distribution
When Team NFT holders fund applications:
- **75%** to the funding team sub-group
- **15%** distributed to node operators based on performance metrics
- **10%** allocated to network treasury for sustainability and development
## 5. Technical Implementation
### 5.1 Network Bootstrap and Discovery
```go
// Automatic bootstrap peer discovery from environment
bootstrapPeers := constants.GetBootstrapPeers()
config.Discovery.BootstrapPeers = bootstrapPeers
// Nodes automatically discover and connect to peers
networkClient, err := client.NewClient(config)
```
**Environment-Based Configuration:**
- `.env` files for development and production settings
- Multiple bootstrap peer support for redundancy
- Automatic peer discovery through DHT
- Graceful handling of bootstrap peer failures
### 5.2 Database Consensus and Replication
**RQLite Integration:**
- Distributed SQLite with Raft consensus protocol
- Automatic leader election and failover
- Strong consistency across all nodes
- ACID transaction guarantees
**Data Isolation:**
- Application-specific database namespaces
- Independent data storage per application
- Secure multi-tenancy without interference
- Scalable partition management
### 5.3 Application Development Model
**Client Library Usage:**
```go
// Simple application integration
config := client.DefaultClientConfig("my-app")
networkClient, err := client.NewClient(config)
// Automatic network connection and discovery
if err := networkClient.Connect(); err != nil {
log.Fatal(err)
}
// Isolated storage operations
storage := networkClient.Storage()
err = storage.Put(ctx, "user:123", userData)
```
**Development Features:**
- **Go SDK** with comprehensive documentation
- **Automatic peer discovery** and connection management
- **Namespace isolation** preventing application interference
- **Built-in messaging** for real-time communications
- **Command-line tools** for testing and deployment
### 5.4 Real-World Application: Anchat
Anchat demonstrates the network's capabilities through a fully functional decentralized messaging application:
**Features Implemented:**
- Wallet-based authentication using Solana addresses
- Real-time messaging across distributed nodes
- Room-based chat with persistent message history
- Automatic peer discovery and network joining
- End-to-end encrypted communications
**Technical Achievement:**
- Zero central servers or coordination points
- Messages flow directly between peers
- Persistent storage across network nodes
- Seamless user experience matching centralized alternatives
## 6. Network Topology and Deployment
### 6.1 Node Architecture
**Bootstrap Nodes:**
- Network entry points for new peers
- Environment-configurable for flexibility
- Automatic identity generation for development
- Production-ready multi-bootstrap support
**Regular Nodes:**
- Automatic bootstrap peer discovery
- Independent RQLite database instances
- Configurable ports for multi-node testing
- Graceful shutdown and restart capabilities
**Client Applications:**
- Lightweight connection to network nodes
- Automatic failover between available peers
- Application-specific database isolation
- Built-in pub/sub messaging capabilities
### 6.2 Deployment Process
**Development Setup:**
```bash
# Environment configuration
cp .env.example .env
go run scripts/generate-bootstrap-identity.go
# Automatic network startup
make run-bootstrap # Bootstrap node with auto-config
make run-node # Regular node with .env discovery
# Application deployment
cd anchat && make build && ./bin/anchat
```
**Production Deployment:**
- Multiple geographic bootstrap peers
- Load balancing across regional nodes
- Automated monitoring and health checks
- Disaster recovery and data backup procedures
## 7. Security Model and Consensus
### 7.1 Consensus Mechanism
**Raft Protocol via RQLite:**
- Leader election for write operations
- Strong consistency guarantees
- Automatic failover and recovery
- Partition tolerance with eventual consistency
**Network Security:**
- LibP2P transport encryption
- Peer identity verification
- Cryptographic message signing
- Protection against Sybil attacks
### 7.2 Data Integrity
**Application Isolation:**
- Namespace-based data separation
- Independent database instances per application
- Secure multi-tenancy architecture
- Prevention of cross-application data leakage
**Replication and Backup:**
- Automatic data replication across nodes
- Configurable replication factors
- Geographic distribution support
- Point-in-time recovery capabilities
## 8. Performance and Scalability
### 8.1 Network Performance
**Measured Capabilities:**
- Sub-second peer discovery and connection
- Real-time message delivery across distributed nodes
- Concurrent multi-user application support
- Automatic load balancing across available peers
**Optimization Features:**
- Connection pooling and reuse
- Efficient peer routing through DHT
- Minimal bandwidth usage for consensus
- Adaptive timeout and retry mechanisms
### 8.2 Scalability Architecture
**Horizontal Scaling:**
- Linear node addition without coordination
- Automatic peer discovery and integration
- Dynamic load distribution
- Geographic distribution support
**Application Scaling:**
- Independent scaling per application namespace
- Resource isolation preventing interference
- Configurable replication and redundancy
- Support for high-availability deployments
## 9. Advantages
### 9.1 Technical Advantages
- **True Decentralization**: No central entities or coordination points
- **Developer Simplicity**: Clean Go SDK with comprehensive documentation
- **Production Ready**: Proven components (LibP2P, RQLite, Go)
- **Real-World Validation**: Working applications like Anchat demonstrate capabilities
- **Performance**: Native Go implementation for optimal speed and efficiency
### 9.2 Ecosystem Advantages
- **NFT-Gated Access**: 100 Team NFTs for collaborative development
- **Accessible Entry**: 700 access NFTs plus token staking options
- **Revenue Sharing**: Direct monetization for application developers
- **Community Driven**: Decentralized governance and decision making
- **Innovation Friendly**: Low barriers to application development and deployment
## 10. Future Development
### 10.1 Network Enhancements
**DePIN Hardware Integration:**
- Specialized hardware nodes with GPU compute capabilities
- AI agent hosting and inference capabilities
- Enhanced performance for compute-intensive applications
- Geographic distribution of specialized resources
**Protocol Improvements:**
- Advanced consensus mechanisms for specialized workloads
- Enhanced privacy features including zero-knowledge proofs
- Cross-chain integration with additional blockchain networks
- Improved bandwidth efficiency and optimization
### 10.2 Application Ecosystem
**Development Tools:**
- Visual application builder and deployment interface
- Advanced monitoring and analytics dashboard
- Automated testing and quality assurance tools
- Integration templates for common application patterns
**Use Cases:**
- Decentralized social networks and messaging platforms
- Distributed file storage and content delivery
- IoT data collection and analysis systems
- Collaborative development and project management tools
## 11. Conclusion
The DeBros Network delivers a production-ready peer-to-peer platform for decentralized applications, demonstrated through working implementations like Anchat. With 100 Team NFTs enabling collaborative development, 700 access NFTs providing widespread adoption, and flexible token staking options, it fosters a genuinely collaborative and equitable ecosystem.
Built on proven technologies including Go, LibP2P, and RQLite, with Solana blockchain integration for governance, the network offers a resilient, privacy-first alternative to centralized systems. The successful deployment of real-world applications validates the architecture's capabilities, paving the way for a future of truly decentralized innovation.
## References
[1] **Go Programming Language** - https://golang.org
[2] **LibP2P Networking Stack** - https://libp2p.io
[3] **RQLite Distributed Database** - https://rqlite.io
[4] **Solana Blockchain** - https://solana.com
[5] **Raft Consensus Algorithm** - https://raft.github.io
[6] **DeBros Network Repository** - https://git.debros.io/DeBros/network-cluster
---
_This whitepaper reflects the current implementation as of August 2025, with demonstrated applications including Anchat decentralized messaging platform._

112
cmd/bootstrap/main.go Normal file
View File

@ -0,0 +1,112 @@
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"syscall"
"network/pkg/config"
"network/pkg/logging"
"network/pkg/node"
)
func main() {
var (
dataDir = flag.String("data", "./data/bootstrap", "Data directory")
port = flag.Int("port", 4001, "Listen port")
help = flag.Bool("help", false, "Show help")
)
flag.Parse()
if *help {
flag.Usage()
return
}
// Create colored logger for bootstrap
logger, err := logging.NewStandardLogger(logging.ComponentBootstrap)
if err != nil {
log.Fatalf("Failed to create logger: %v", err)
}
// Load configuration
cfg := config.BootstrapConfig()
cfg.Node.DataDir = *dataDir
cfg.Node.ListenAddresses = []string{
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", *port),
fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic", *port),
}
// Configure RQLite ports for bootstrap node
cfg.Database.RQLitePort = *port + 1000 // e.g., 5001 for bootstrap port 4001
cfg.Database.RQLiteRaftPort = *port + 3000 // e.g., 7001 for bootstrap port 4001 (changed to avoid conflicts)
cfg.Database.RQLiteJoinAddress = "" // Bootstrap node doesn't join anyone
logger.Printf("Starting bootstrap node...")
logger.Printf("Data directory: %s", cfg.Node.DataDir)
logger.Printf("Listen addresses: %v", cfg.Node.ListenAddresses)
logger.Printf("RQLite HTTP port: %d", cfg.Database.RQLitePort)
logger.Printf("RQLite Raft port: %d", cfg.Database.RQLiteRaftPort)
// Create context for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Start bootstrap node in a goroutine
errChan := make(chan error, 1)
go func() {
if err := startBootstrapNode(ctx, cfg, *port, logger); err != nil {
errChan <- err
}
}()
// Wait for interrupt signal or error
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
select {
case err := <-errChan:
logger.Printf("Failed to start bootstrap node: %v", err)
os.Exit(1)
case <-c:
logger.Printf("Shutting down bootstrap node...")
cancel()
}
}
func startBootstrapNode(ctx context.Context, cfg *config.Config, port int, logger *logging.StandardLogger) error {
// Create and start bootstrap node using the new node implementation
n, err := node.NewNode(cfg)
if err != nil {
return fmt.Errorf("failed to create bootstrap node: %w", err)
}
if err := n.Start(ctx); err != nil {
return fmt.Errorf("failed to start bootstrap node: %w", err)
}
// Save the peer ID to a file for CLI access
peerID := n.GetPeerID()
peerInfoFile := filepath.Join(cfg.Node.DataDir, "peer.info")
peerMultiaddr := fmt.Sprintf("/ip4/127.0.0.1/tcp/%d/p2p/%s", port, peerID)
if err := os.WriteFile(peerInfoFile, []byte(peerMultiaddr), 0644); err != nil {
logger.Printf("Warning: Failed to save peer info: %v", err)
} else {
logger.Printf("Peer info saved to: %s", peerInfoFile)
logger.Printf("Bootstrap multiaddr: %s", peerMultiaddr)
}
logger.Printf("Bootstrap node started successfully")
// Wait for context cancellation
<-ctx.Done()
// Stop node
return n.Stop()
}

578
cmd/cli/main.go Normal file
View File

@ -0,0 +1,578 @@
package main
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
"network/pkg/client"
)
var (
bootstrapPeer = "/ip4/127.0.0.1/tcp/4001"
timeout = 30 * time.Second
format = "table"
)
func main() {
if len(os.Args) < 2 {
showHelp()
return
}
command := os.Args[1]
args := os.Args[2:]
// Parse global flags
parseGlobalFlags(args)
switch command {
case "health":
handleHealth()
case "peers":
handlePeers()
case "status":
handleStatus()
case "query":
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Usage: network-cli query <sql>\n")
os.Exit(1)
}
handleQuery(args[0])
case "storage":
handleStorage(args)
case "pubsub":
handlePubSub(args)
case "connect":
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Usage: network-cli connect <peer_address>\n")
os.Exit(1)
}
handleConnect(args[0])
case "help", "--help", "-h":
showHelp()
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command)
showHelp()
os.Exit(1)
}
}
func parseGlobalFlags(args []string) {
for i, arg := range args {
switch arg {
case "-b", "--bootstrap":
if i+1 < len(args) {
bootstrapPeer = args[i+1]
}
case "-f", "--format":
if i+1 < len(args) {
format = args[i+1]
}
case "-t", "--timeout":
if i+1 < len(args) {
if d, err := time.ParseDuration(args[i+1]); err == nil {
timeout = d
}
}
}
}
}
func handleHealth() {
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer client.Disconnect()
health, err := client.Health()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get health: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(health)
} else {
printHealth(health)
}
}
func handlePeers() {
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer client.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
peers, err := client.Network().GetPeers(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get peers: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(peers)
} else {
printPeers(peers)
}
}
func handleStatus() {
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer client.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
status, err := client.Network().GetStatus(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get status: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(status)
} else {
printStatus(status)
}
}
func handleQuery(sql string) {
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer client.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
result, err := client.Database().Query(ctx, sql)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to execute query: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(result)
} else {
printQueryResult(result)
}
}
func handleStorage(args []string) {
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Usage: network-cli storage <get|put|list> [args...]\n")
os.Exit(1)
}
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer client.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
subcommand := args[0]
switch subcommand {
case "get":
if len(args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: network-cli storage get <key>\n")
os.Exit(1)
}
value, err := client.Storage().Get(ctx, args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get value: %v\n", err)
os.Exit(1)
}
// Try to decode if it looks like base64
decoded := tryDecodeBase64(string(value))
fmt.Printf("%s\n", decoded)
case "put":
if len(args) < 3 {
fmt.Fprintf(os.Stderr, "Usage: network-cli storage put <key> <value>\n")
os.Exit(1)
}
err := client.Storage().Put(ctx, args[1], []byte(args[2]))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to store value: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Stored key: %s\n", args[1])
case "list":
prefix := ""
if len(args) > 1 {
prefix = args[1]
}
keys, err := client.Storage().List(ctx, prefix, 100)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to list keys: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(keys)
} else {
for _, key := range keys {
fmt.Println(key)
}
}
default:
fmt.Fprintf(os.Stderr, "Unknown storage command: %s\n", subcommand)
os.Exit(1)
}
}
func handlePubSub(args []string) {
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Usage: network-cli pubsub <publish|subscribe|topics> [args...]\n")
os.Exit(1)
}
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer client.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
subcommand := args[0]
switch subcommand {
case "publish":
if len(args) < 3 {
fmt.Fprintf(os.Stderr, "Usage: network-cli pubsub publish <topic> <message>\n")
os.Exit(1)
}
err := client.PubSub().Publish(ctx, args[1], []byte(args[2]))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to publish message: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Published message to topic: %s\n", args[1])
case "subscribe":
if len(args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: network-cli pubsub subscribe <topic> [duration]\n")
os.Exit(1)
}
duration := 30 * time.Second
if len(args) > 2 {
if d, err := time.ParseDuration(args[2]); err == nil {
duration = d
}
}
ctx, cancel := context.WithTimeout(context.Background(), duration)
defer cancel()
fmt.Printf("🔔 Subscribing to topic '%s' for %v...\n", args[1], duration)
messageHandler := func(topic string, data []byte) error {
fmt.Printf("📨 [%s] %s: %s\n", time.Now().Format("15:04:05"), topic, string(data))
return nil
}
err := client.PubSub().Subscribe(ctx, args[1], messageHandler)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to subscribe: %v\n", err)
os.Exit(1)
}
<-ctx.Done()
fmt.Printf("✅ Subscription ended\n")
case "topics":
topics, err := client.PubSub().ListTopics(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to list topics: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(topics)
} else {
for _, topic := range topics {
fmt.Println(topic)
}
}
default:
fmt.Fprintf(os.Stderr, "Unknown pubsub command: %s\n", subcommand)
os.Exit(1)
}
}
func handleConnect(peerAddr string) {
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer client.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
err = client.Network().ConnectToPeer(ctx, peerAddr)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to connect to peer: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Connected to peer: %s\n", peerAddr)
}
func createClient() (client.NetworkClient, error) {
// Try to discover the bootstrap peer from saved peer info
discoveredPeer := discoverBootstrapPeer()
if discoveredPeer != "" {
bootstrapPeer = discoveredPeer
}
config := client.DefaultClientConfig("network-cli")
config.BootstrapPeers = []string{bootstrapPeer}
config.ConnectTimeout = timeout
config.QuietMode = true // Suppress debug/info logs for CLI
networkClient, err := client.NewClient(config)
if err != nil {
return nil, err
}
if err := networkClient.Connect(); err != nil {
return nil, err
}
return networkClient, nil
}
// discoverBootstrapPeer tries to find the bootstrap peer from saved peer info
func discoverBootstrapPeer() string {
// Look for peer info in common locations
peerInfoPaths := []string{
"./data/bootstrap/peer.info",
"./data/test-bootstrap/peer.info",
"/tmp/bootstrap-peer.info",
}
for _, path := range peerInfoPaths {
if data, err := os.ReadFile(path); err == nil {
peerAddr := strings.TrimSpace(string(data))
if peerAddr != "" {
// Only print discovery message in table format
if format != "json" {
fmt.Printf("🔍 Discovered bootstrap peer: %s\n", peerAddr)
}
return peerAddr
}
}
}
return "" // Return empty string if no peer info found
}
// tryDecodeBase64 attempts to decode a string as base64, returns original if not valid base64
func tryDecodeBase64(s string) string {
// Only try to decode if it looks like base64 (no spaces, reasonable length)
if len(s) > 0 && len(s)%4 == 0 && !strings.ContainsAny(s, " \n\r\t") {
if decoded, err := base64.StdEncoding.DecodeString(s); err == nil {
// Check if decoded result looks like readable text
decodedStr := string(decoded)
if isPrintableText(decodedStr) {
return decodedStr
}
}
}
return s
}
// isPrintableText checks if a string contains mostly printable characters
func isPrintableText(s string) bool {
printableCount := 0
for _, r := range s {
if r >= 32 && r <= 126 || r == '\n' || r == '\r' || r == '\t' {
printableCount++
}
}
return len(s) > 0 && float64(printableCount)/float64(len(s)) > 0.8
}
func showHelp() {
fmt.Printf("Network CLI - Distributed P2P Network Management Tool\n\n")
fmt.Printf("Usage: network-cli <command> [args...]\n\n")
fmt.Printf("Commands:\n")
fmt.Printf(" health - Check network health\n")
fmt.Printf(" peers - List connected peers\n")
fmt.Printf(" status - Show network status\n")
fmt.Printf(" query <sql> - Execute database query\n")
fmt.Printf(" storage get <key> - Get value from storage\n")
fmt.Printf(" storage put <key> <value> - Store value in storage\n")
fmt.Printf(" storage list [prefix] - List storage keys\n")
fmt.Printf(" pubsub publish <topic> <msg> - Publish message\n")
fmt.Printf(" pubsub subscribe <topic> [duration] - Subscribe to topic\n")
fmt.Printf(" pubsub topics - List topics\n")
fmt.Printf(" connect <peer_address> - Connect to peer\n")
fmt.Printf(" help - Show this help\n\n")
fmt.Printf("Global Flags:\n")
fmt.Printf(" -b, --bootstrap <addr> - Bootstrap peer address (default: /ip4/127.0.0.1/tcp/4001)\n")
fmt.Printf(" -f, --format <format> - Output format: table, json (default: table)\n")
fmt.Printf(" -t, --timeout <duration> - Operation timeout (default: 30s)\n\n")
fmt.Printf("Examples:\n")
fmt.Printf(" network-cli health\n")
fmt.Printf(" network-cli peers --format json\n")
fmt.Printf(" network-cli storage put user:123 '{\"name\":\"Alice\"}'\n")
fmt.Printf(" network-cli pubsub subscribe notifications 1m\n")
}
// Print functions
func printHealth(health *client.HealthStatus) {
fmt.Printf("🏥 Network Health\n")
fmt.Printf("Status: %s\n", getStatusEmoji(health.Status)+health.Status)
fmt.Printf("Last Updated: %s\n", health.LastUpdated.Format("2006-01-02 15:04:05"))
fmt.Printf("Response Time: %v\n", health.ResponseTime)
fmt.Printf("\nChecks:\n")
for check, status := range health.Checks {
emoji := "✅"
if status != "ok" {
emoji = "❌"
}
fmt.Printf(" %s %s: %s\n", emoji, check, status)
}
}
func printPeers(peers []client.PeerInfo) {
fmt.Printf("👥 Connected Peers (%d)\n\n", len(peers))
if len(peers) == 0 {
fmt.Printf("No peers connected\n")
return
}
for i, peer := range peers {
connEmoji := "🔴"
if peer.Connected {
connEmoji = "🟢"
}
fmt.Printf("%d. %s %s\n", i+1, connEmoji, peer.ID)
fmt.Printf(" Addresses: %v\n", peer.Addresses)
fmt.Printf(" Last Seen: %s\n", peer.LastSeen.Format("2006-01-02 15:04:05"))
fmt.Println()
}
}
func printStatus(status *client.NetworkStatus) {
fmt.Printf("🌐 Network Status\n")
fmt.Printf("Node ID: %s\n", status.NodeID)
fmt.Printf("Connected: %s\n", getBoolEmoji(status.Connected)+strconv.FormatBool(status.Connected))
fmt.Printf("Peer Count: %d\n", status.PeerCount)
fmt.Printf("Database Size: %s\n", formatBytes(status.DatabaseSize))
fmt.Printf("Uptime: %v\n", status.Uptime.Round(time.Second))
}
func printQueryResult(result *client.QueryResult) {
fmt.Printf("📊 Query Result\n")
fmt.Printf("Rows: %d\n\n", result.Count)
if len(result.Rows) == 0 {
fmt.Printf("No data returned\n")
return
}
// Print header
for i, col := range result.Columns {
if i > 0 {
fmt.Printf(" | ")
}
fmt.Printf("%-15s", col)
}
fmt.Println()
// Print separator
for i := range result.Columns {
if i > 0 {
fmt.Printf("-+-")
}
fmt.Printf("%-15s", "---------------")
}
fmt.Println()
// Print rows
for _, row := range result.Rows {
for i, cell := range row {
if i > 0 {
fmt.Printf(" | ")
}
fmt.Printf("%-15v", cell)
}
fmt.Println()
}
}
func printJSON(data interface{}) {
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to marshal JSON: %v\n", err)
return
}
fmt.Println(string(jsonData))
}
// Helper functions
func getStatusEmoji(status string) string {
switch status {
case "healthy":
return "🟢 "
case "degraded":
return "🟡 "
case "unhealthy":
return "🔴 "
default:
return "⚪ "
}
}
func getBoolEmoji(b bool) string {
if b {
return "✅ "
}
return "❌ "
}
func formatBytes(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}

111
cmd/node/main.go Normal file
View File

@ -0,0 +1,111 @@
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"network/pkg/config"
"network/pkg/constants"
"network/pkg/node"
)
func main() {
var (
dataDir = flag.String("data", "./data/node", "Data directory")
port = flag.Int("port", 4002, "Listen port")
bootstrap = flag.String("bootstrap", "", "Bootstrap peer address")
help = flag.Bool("help", false, "Show help")
)
flag.Parse()
if *help {
flag.Usage()
return
}
// Load configuration
cfg := config.DefaultConfig()
cfg.Node.DataDir = *dataDir
cfg.Node.ListenAddresses = []string{
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", *port),
fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic", *port),
}
// Configure RQLite ports based on node port
cfg.Database.RQLitePort = *port + 1000 // e.g., 5002 for node port 4002
cfg.Database.RQLiteRaftPort = *port + 3000 // e.g., 7002 for node port 4002 (changed to avoid conflicts)
// Configure bootstrap peers
if *bootstrap != "" {
// Use command line bootstrap if provided
cfg.Discovery.BootstrapPeers = []string{*bootstrap}
log.Printf("Using command line bootstrap peer: %s", *bootstrap)
} else {
// Use environment-configured bootstrap peers
bootstrapPeers := constants.GetBootstrapPeers()
if len(bootstrapPeers) > 0 {
cfg.Discovery.BootstrapPeers = bootstrapPeers
log.Printf("Using environment bootstrap peers: %v", bootstrapPeers)
} else {
log.Printf("Warning: No bootstrap peers configured")
}
}
// For LibP2P peer discovery testing, don't join RQLite cluster
// Each node will have its own independent RQLite instance
cfg.Database.RQLiteJoinAddress = "" // Keep RQLite independent
log.Printf("Starting network node...")
log.Printf("Data directory: %s", cfg.Node.DataDir)
log.Printf("Listen addresses: %v", cfg.Node.ListenAddresses)
log.Printf("Bootstrap peers: %v", cfg.Discovery.BootstrapPeers)
log.Printf("RQLite HTTP port: %d", cfg.Database.RQLitePort)
log.Printf("RQLite Raft port: %d", cfg.Database.RQLiteRaftPort)
// Create context for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Start node in a goroutine
errChan := make(chan error, 1)
go func() {
if err := startNode(ctx, cfg); err != nil {
errChan <- err
}
}()
// Wait for interrupt signal or error
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
select {
case err := <-errChan:
log.Fatalf("Failed to start node: %v", err)
case <-c:
log.Printf("Shutting down node...")
cancel()
}
}
func startNode(ctx context.Context, cfg *config.Config) error {
// Create and start node
n, err := node.NewNode(cfg)
if err != nil {
return fmt.Errorf("failed to create node: %w", err)
}
if err := n.Start(ctx); err != nil {
return fmt.Errorf("failed to start node: %w", err)
}
// Wait for context cancellation
<-ctx.Done()
// Stop node
return n.Stop()
}

34
configs/bootstrap.yaml Normal file
View File

@ -0,0 +1,34 @@
node:
id: "" # Auto-generated
type: "bootstrap"
listen_addresses:
- "/ip4/0.0.0.0/tcp/4001"
- "/ip4/0.0.0.0/udp/4001/quic"
data_dir: "./data/bootstrap"
max_connections: 100
is_bootstrap: true
database:
data_dir: "./data/bootstrap/db"
replication_factor: 3
shard_count: 16
max_database_size: 1073741824 # 1GB
backup_interval: "24h"
discovery:
bootstrap_peers: [] # Bootstrap nodes don't need peers
enable_mdns: true
enable_dht: true
dht_prefix: "/network/kad/1.0.0"
discovery_interval: "5m"
security:
enable_tls: false
private_key_file: ""
certificate_file: ""
auth_enabled: false
logging:
level: "info"
format: "console"
output_file: ""

36
configs/node.yaml Normal file
View File

@ -0,0 +1,36 @@
node:
id: "" # Auto-generated
type: "node"
listen_addresses:
- "/ip4/0.0.0.0/tcp/0" # Random port
- "/ip4/0.0.0.0/udp/0/quic" # Random port
data_dir: "./data/node"
max_connections: 50
is_bootstrap: false
database:
data_dir: "./data/node/db"
replication_factor: 3
shard_count: 16
max_database_size: 1073741824 # 1GB
backup_interval: "24h"
discovery:
bootstrap_peers:
- "/ip4/127.0.0.1/tcp/4001/p2p/1111KooWFmsnHjrSLRiv1MdwDCKKyEBhrAn3vLTpdqT8pQ8S1111"
# Add more bootstrap peers as needed
enable_mdns: true
enable_dht: true
dht_prefix: "/network/kad/1.0.0"
discovery_interval: "5m"
security:
enable_tls: false
private_key_file: ""
certificate_file: ""
auth_enabled: false
logging:
level: "info"
format: "console"
output_file: ""

195
examples/basic_usage.go Normal file
View File

@ -0,0 +1,195 @@
package main
import (
"context"
"log"
"time"
"network/pkg/client"
)
func main() {
// Create client configuration
config := client.DefaultClientConfig("example_app")
config.BootstrapPeers = []string{
"/ip4/127.0.0.1/tcp/4001/p2p/QmBootstrap1",
}
// Create network client
networkClient, err := client.NewClient(config)
if err != nil {
log.Fatalf("Failed to create network client: %v", err)
}
// Connect to network
if err := networkClient.Connect(); err != nil {
log.Fatalf("Failed to connect to network: %v", err)
}
defer networkClient.Disconnect()
log.Printf("Connected to network successfully!")
// Example: Database operations
demonstrateDatabase(networkClient)
// Example: Storage operations
demonstrateStorage(networkClient)
// Example: Pub/Sub messaging
demonstratePubSub(networkClient)
// Example: Network information
demonstrateNetworkInfo(networkClient)
log.Printf("Example completed successfully!")
}
func demonstrateDatabase(client client.NetworkClient) {
ctx := context.Background()
db := client.Database()
log.Printf("=== Database Operations ===")
// Create a table
schema := `
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY,
content TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
`
if err := db.CreateTable(ctx, schema); err != nil {
log.Printf("Error creating table: %v", err)
return
}
log.Printf("Table created successfully")
// Insert some data
insertSQL := "INSERT INTO messages (content) VALUES (?)"
result, err := db.Query(ctx, insertSQL, "Hello, distributed world!")
if err != nil {
log.Printf("Error inserting data: %v", err)
return
}
log.Printf("Data inserted, result: %+v", result)
// Query data
selectSQL := "SELECT * FROM messages"
result, err = db.Query(ctx, selectSQL)
if err != nil {
log.Printf("Error querying data: %v", err)
return
}
log.Printf("Query result: %+v", result)
}
func demonstrateStorage(client client.NetworkClient) {
ctx := context.Background()
storage := client.Storage()
log.Printf("=== Storage Operations ===")
// Store some data
key := "user:123"
value := []byte(`{"name": "Alice", "age": 30}`)
if err := storage.Put(ctx, key, value); err != nil {
log.Printf("Error storing data: %v", err)
return
}
log.Printf("Data stored successfully")
// Retrieve data
retrieved, err := storage.Get(ctx, key)
if err != nil {
log.Printf("Error retrieving data: %v", err)
return
}
log.Printf("Retrieved data: %s", string(retrieved))
// Check if key exists
exists, err := storage.Exists(ctx, key)
if err != nil {
log.Printf("Error checking existence: %v", err)
return
}
log.Printf("Key exists: %v", exists)
// List keys
keys, err := storage.List(ctx, "user:", 10)
if err != nil {
log.Printf("Error listing keys: %v", err)
return
}
log.Printf("Keys: %v", keys)
}
func demonstratePubSub(client client.NetworkClient) {
ctx := context.Background()
pubsub := client.PubSub()
log.Printf("=== Pub/Sub Operations ===")
// Subscribe to a topic
topic := "notifications"
handler := func(topic string, data []byte) error {
log.Printf("Received message on topic '%s': %s", topic, string(data))
return nil
}
if err := pubsub.Subscribe(ctx, topic, handler); err != nil {
log.Printf("Error subscribing: %v", err)
return
}
log.Printf("Subscribed to topic: %s", topic)
// Publish a message
message := []byte("Hello from pub/sub!")
if err := pubsub.Publish(ctx, topic, message); err != nil {
log.Printf("Error publishing: %v", err)
return
}
log.Printf("Message published")
// Wait a bit for message delivery
time.Sleep(time.Millisecond * 100)
// List topics
topics, err := pubsub.ListTopics(ctx)
if err != nil {
log.Printf("Error listing topics: %v", err)
return
}
log.Printf("Subscribed topics: %v", topics)
}
func demonstrateNetworkInfo(client client.NetworkClient) {
ctx := context.Background()
network := client.Network()
log.Printf("=== Network Information ===")
// Get network status
status, err := network.GetStatus(ctx)
if err != nil {
log.Printf("Error getting status: %v", err)
return
}
log.Printf("Network status: %+v", status)
// Get peers
peers, err := network.GetPeers(ctx)
if err != nil {
log.Printf("Error getting peers: %v", err)
return
}
log.Printf("Connected peers: %+v", peers)
// Get client health
health, err := client.Health()
if err != nil {
log.Printf("Error getting health: %v", err)
return
}
log.Printf("Client health: %+v", health)
}

View File

@ -0,0 +1,48 @@
package main
import (
"crypto/rand"
"fmt"
"os"
"path/filepath"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
)
func main() {
// Generate a fixed identity
priv, pub, err := crypto.GenerateKeyPairWithReader(crypto.Ed25519, 2048, rand.Reader)
if err != nil {
panic(err)
}
// Get peer ID
peerID, err := peer.IDFromPublicKey(pub)
if err != nil {
panic(err)
}
fmt.Printf("Generated Peer ID: %s\n", peerID.String())
// Marshal private key
data, err := crypto.MarshalPrivateKey(priv)
if err != nil {
panic(err)
}
// Create data directory
dataDir := "./data/bootstrap"
if err := os.MkdirAll(dataDir, 0755); err != nil {
panic(err)
}
// Save identity
identityFile := filepath.Join(dataDir, "identity.key")
if err := os.WriteFile(identityFile, data, 0600); err != nil {
panic(err)
}
fmt.Printf("Identity saved to: %s\n", identityFile)
fmt.Printf("Bootstrap address: /ip4/127.0.0.1/tcp/4001/p2p/%s\n", peerID.String())
}

134
go.mod Normal file
View File

@ -0,0 +1,134 @@
module network
go 1.23.8
toolchain go1.24.1
require (
github.com/libp2p/go-libp2p v0.41.1
github.com/libp2p/go-libp2p-kad-dht v0.33.1
github.com/libp2p/go-libp2p-pubsub v0.14.2
github.com/multiformats/go-multiaddr v0.15.0
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8
go.uber.org/zap v1.27.0
)
require (
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/cgroups v1.1.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/elastic/gosigar v0.14.3 // indirect
github.com/flynn/noise v1.1.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/ipfs/boxo v0.30.0 // indirect
github.com/ipfs/go-cid v0.5.0 // indirect
github.com/ipfs/go-datastore v0.8.2 // indirect
github.com/ipfs/go-log/v2 v2.6.0 // indirect
github.com/ipld/go-ipld-prime v0.21.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/koron/go-ssdp v0.0.5 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-cidranger v1.1.0 // indirect
github.com/libp2p/go-flow-metrics v0.2.0 // indirect
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect
github.com/libp2p/go-libp2p-kbucket v0.7.0 // indirect
github.com/libp2p/go-libp2p-record v0.3.1 // indirect
github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect
github.com/libp2p/go-msgio v0.3.0 // indirect
github.com/libp2p/go-netroute v0.2.2 // indirect
github.com/libp2p/go-reuseport v0.4.0 // indirect
github.com/libp2p/go-yamux/v5 v5.0.0 // indirect
github.com/libp2p/zeroconf/v2 v2.2.0 // indirect
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.66 // indirect
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multicodec v0.9.0 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-multistream v0.6.0 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/opencontainers/runtime-spec v1.2.0 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/dtls/v3 v3.0.4 // indirect
github.com/pion/ice/v4 v4.0.8 // indirect
github.com/pion/interceptor v0.1.37 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.15 // indirect
github.com/pion/rtp v1.8.11 // indirect
github.com/pion/sctp v1.8.37 // indirect
github.com/pion/sdp/v3 v3.0.10 // indirect
github.com/pion/srtp/v3 v3.0.4 // indirect
github.com/pion/stun v0.6.1 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pion/webrtc/v4 v4.0.10 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/polydawn/refmt v0.89.0 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.63.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.50.1 // indirect
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect
github.com/raulk/go-watchdog v1.3.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.uber.org/dig v1.18.0 // indirect
go.uber.org/fx v1.23.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.35.0 // indirect
gonum.org/v1/gonum v0.16.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
)

585
go.sum Normal file
View File

@ -0,0 +1,585 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU=
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U=
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo=
github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg=
github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc=
github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/ipfs/boxo v0.30.0 h1:7afsoxPGGqfoH7Dum/wOTGUB9M5fb8HyKPMlLfBvIEQ=
github.com/ipfs/boxo v0.30.0/go.mod h1:BPqgGGyHB9rZZcPSzah2Dc9C+5Or3U1aQe7EH1H7370=
github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs=
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
github.com/ipfs/go-datastore v0.8.2 h1:Jy3wjqQR6sg/LhyY0NIePZC3Vux19nLtg7dx0TVqr6U=
github.com/ipfs/go-datastore v0.8.2/go.mod h1:W+pI1NsUsz3tcsAACMtfC+IZdnQTnC/7VfPoJBQuts0=
github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0=
github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs=
github.com/ipfs/go-log/v2 v2.6.0 h1:2Nu1KKQQ2ayonKp4MPo6pXCjqw1ULc9iohRqWV5EYqg=
github.com/ipfs/go-log/v2 v2.6.0/go.mod h1:p+Efr3qaY5YXpx9TX7MoLCSEZX5boSWj9wh86P5HJa8=
github.com/ipfs/go-test v0.2.1 h1:/D/a8xZ2JzkYqcVcV/7HYlCnc7bv/pKHQiX5TdClkPE=
github.com/ipfs/go-test v0.2.1/go.mod h1:dzu+KB9cmWjuJnXFDYJwC25T3j1GcN57byN+ixmK39M=
github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E=
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/koron/go-ssdp v0.0.5 h1:E1iSMxIs4WqxTbIBLtmNBeOOC+1sCIXQeqTWVnpmwhk=
github.com/koron/go-ssdp v0.0.5/go.mod h1:Qm59B7hpKpDqfyRNWRNr00jGwLdXjDyZh6y7rH6VS0w=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c=
github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic=
github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw=
github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc=
github.com/libp2p/go-libp2p v0.41.1 h1:8ecNQVT5ev/jqALTvisSJeVNvXYJyK4NhQx1nNRXQZE=
github.com/libp2p/go-libp2p v0.41.1/go.mod h1:DcGTovJzQl/I7HMrby5ZRjeD0kQkGiy+9w6aEkSZpRI=
github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94=
github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8=
github.com/libp2p/go-libp2p-kad-dht v0.33.1 h1:hKFhHMf7WH69LDjaxsJUWOU6qZm71uO47M/a5ijkiP0=
github.com/libp2p/go-libp2p-kad-dht v0.33.1/go.mod h1:CdmNk4VeGJa9EXM9SLNyNVySEvduKvb+5rSC/H4pLAo=
github.com/libp2p/go-libp2p-kbucket v0.7.0 h1:vYDvRjkyJPeWunQXqcW2Z6E93Ywx7fX0jgzb/dGOKCs=
github.com/libp2p/go-libp2p-kbucket v0.7.0/go.mod h1:blOINGIj1yiPYlVEX0Rj9QwEkmVnz3EP8LK1dRKBC6g=
github.com/libp2p/go-libp2p-pubsub v0.14.2 h1:nT5lFHPQOFJcp9CW8hpKtvbpQNdl2udJuzLQWbgRum8=
github.com/libp2p/go-libp2p-pubsub v0.14.2/go.mod h1:MKPU5vMI8RRFyTP0HfdsF9cLmL1nHAeJm44AxJGJx44=
github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg=
github.com/libp2p/go-libp2p-record v0.3.1/go.mod h1:T8itUkLcWQLCYMqtX7Th6r7SexyUJpIyPgks757td/E=
github.com/libp2p/go-libp2p-routing-helpers v0.7.5 h1:HdwZj9NKovMx0vqq6YNPTh6aaNzey5zHD7HeLJtq6fI=
github.com/libp2p/go-libp2p-routing-helpers v0.7.5/go.mod h1:3YaxrwP0OBPDD7my3D0KxfR89FlcX/IEbxDEDfAmj98=
github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA=
github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg=
github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0=
github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM=
github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8=
github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE=
github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s=
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po=
github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU=
github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q=
github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk=
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8=
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms=
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU=
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc=
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s=
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo=
github.com/multiformats/go-multiaddr v0.15.0 h1:zB/HeaI/apcZiTDwhY5YqMvNVl/oQYvs3XySU+qeAVo=
github.com/multiformats/go-multiaddr v0.15.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0=
github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M=
github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc=
github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA=
github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg=
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U=
github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg=
github.com/pion/ice/v4 v4.0.8 h1:ajNx0idNG+S+v9Phu4LSn2cs8JEfTsA1/tEjkkAVpFY=
github.com/pion/ice/v4 v4.0.8/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.8.11 h1:17xjnY5WO5hgO6SD3/NTIUPvSFw/PbLsIJyz1r1yNIk=
github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs=
github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA=
github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pion/webrtc/v4 v4.0.10 h1:Hq/JLjhqLxi+NmCtE8lnRPDr8H4LcNvwg8OxVcdv56Q=
github.com/pion/webrtc/v4 v4.0.10/go.mod h1:ViHLVaNpiuvaH8pdiuQxuA9awuE6KVzAXx3vVWilOck=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4=
github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q=
github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg=
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw=
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8 h1:BoxiqWvhprOB2isgM59s8wkgKwAoyQH66Twfmof41oE=
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k=
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc=
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw=
go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg=
go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

554
install-debros-network.sh Executable file
View File

@ -0,0 +1,554 @@
#!/bin/bash
set -e # Exit on any error
trap 'echo -e "${RED}An error occurred. Installation aborted.${NOCOLOR}"; exit 1' ERR
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
BLUE='\033[38;2;2;128;175m'
YELLOW='\033[1;33m'
NOCOLOR='\033[0m'
# Default values
INSTALL_DIR="/opt/debros"
REPO_URL="https://github.com/DeBrosOfficial/debros-network.git"
MIN_GO_VERSION="1.19"
BOOTSTRAP_PORT="4001"
NODE_PORT="4002"
RQLITE_BOOTSTRAP_PORT="5001"
RQLITE_NODE_PORT="5002"
RAFT_BOOTSTRAP_PORT="7001"
RAFT_NODE_PORT="7002"
log() {
echo -e "${CYAN}[$(date '+%Y-%m-%d %H:%M:%S')]${NOCOLOR} $1"
}
error() {
echo -e "${RED}[ERROR]${NOCOLOR} $1"
}
success() {
echo -e "${GREEN}[SUCCESS]${NOCOLOR} $1"
}
warning() {
echo -e "${YELLOW}[WARNING]${NOCOLOR} $1"
}
# Check if running as root
if [[ $EUID -eq 0 ]]; then
error "This script should not be run as root. Please run as a regular user with sudo privileges."
exit 1
fi
# Check if sudo is available
if ! command -v sudo &>/dev/null; then
error "sudo command not found. Please ensure you have sudo privileges."
exit 1
fi
# Detect OS
detect_os() {
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$ID
VERSION=$VERSION_ID
else
error "Cannot detect operating system"
exit 1
fi
case $OS in
ubuntu|debian)
PACKAGE_MANAGER="apt"
;;
centos|rhel|fedora)
PACKAGE_MANAGER="yum"
if command -v dnf &> /dev/null; then
PACKAGE_MANAGER="dnf"
fi
;;
*)
error "Unsupported operating system: $OS"
exit 1
;;
esac
log "Detected OS: $OS $VERSION"
}
# Check Go installation and version
check_go_installation() {
if command -v go &> /dev/null; then
GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//')
log "Found Go version: $GO_VERSION"
# Compare versions (simplified)
if [ "$(printf '%s\n' "$MIN_GO_VERSION" "$GO_VERSION" | sort -V | head -n1)" = "$MIN_GO_VERSION" ]; then
success "Go version is sufficient"
return 0
else
warning "Go version $GO_VERSION is too old. Minimum required: $MIN_GO_VERSION"
return 1
fi
else
log "Go not found on system"
return 1
fi
}
# Install Go
install_go() {
log "Installing Go..."
case $PACKAGE_MANAGER in
apt)
sudo apt update
sudo apt install -y wget
;;
yum|dnf)
sudo $PACKAGE_MANAGER install -y wget
;;
esac
# Download and install Go
GO_TARBALL="go1.21.0.linux-amd64.tar.gz"
ARCH=$(uname -m)
if [ "$ARCH" = "aarch64" ]; then
GO_TARBALL="go1.21.0.linux-arm64.tar.gz"
fi
cd /tmp
wget -q "https://golang.org/dl/$GO_TARBALL"
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf "$GO_TARBALL"
# Add Go to PATH
if ! grep -q "/usr/local/go/bin" ~/.bashrc; then
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
fi
export PATH=$PATH:/usr/local/go/bin
success "Go installed successfully"
}
# Install system dependencies
install_dependencies() {
log "Installing system dependencies..."
case $PACKAGE_MANAGER in
apt)
sudo apt update
sudo apt install -y git make build-essential curl
;;
yum|dnf)
sudo $PACKAGE_MANAGER groupinstall -y "Development Tools"
sudo $PACKAGE_MANAGER install -y git make curl
;;
esac
success "System dependencies installed"
}
# Check port availability
check_ports() {
local ports=($BOOTSTRAP_PORT $NODE_PORT $RQLITE_BOOTSTRAP_PORT $RQLITE_NODE_PORT $RAFT_BOOTSTRAP_PORT $RAFT_NODE_PORT)
for port in "${ports[@]}"; do
if sudo netstat -tuln 2>/dev/null | grep -q ":$port " || ss -tuln 2>/dev/null | grep -q ":$port "; then
error "Port $port is already in use. Please free it up and try again."
exit 1
fi
done
success "All required ports are available"
}
# Configuration wizard
configuration_wizard() {
log "${BLUE}==================================================${NOCOLOR}"
log "${GREEN} DeBros Network Configuration Wizard ${NOCOLOR}"
log "${BLUE}==================================================${NOCOLOR}"
# Node type selection
while true; do
echo -e "${GREEN}Select node type:${NOCOLOR}"
echo -e "${CYAN}1) Bootstrap Node (Network entry point)${NOCOLOR}"
echo -e "${CYAN}2) Regular Node (Connects to existing network)${NOCOLOR}"
read -rp "Enter your choice (1 or 2): " NODE_TYPE_CHOICE
case $NODE_TYPE_CHOICE in
1)
NODE_TYPE="bootstrap"
break
;;
2)
NODE_TYPE="regular"
break
;;
*)
error "Invalid choice. Please enter 1 or 2."
;;
esac
done
# Solana wallet address
log "${GREEN}Enter your Solana wallet address to be eligible for node operator rewards:${NOCOLOR}"
while true; do
read -rp "Solana Wallet Address: " SOLANA_WALLET
if [[ -n "$SOLANA_WALLET" && ${#SOLANA_WALLET} -ge 32 ]]; then
break
else
error "Please enter a valid Solana wallet address"
fi
done
# Data directory
read -rp "Installation directory [default: $INSTALL_DIR]: " CUSTOM_INSTALL_DIR
if [[ -n "$CUSTOM_INSTALL_DIR" ]]; then
INSTALL_DIR="$CUSTOM_INSTALL_DIR"
fi
# Firewall configuration
read -rp "Configure firewall automatically? (yes/no) [default: yes]: " CONFIGURE_FIREWALL
CONFIGURE_FIREWALL="${CONFIGURE_FIREWALL:-yes}"
success "Configuration completed"
}
# Create user and directories
setup_directories() {
log "Setting up directories and permissions..."
# Create debros user if it doesn't exist
if ! id "debros" &>/dev/null; then
sudo useradd -r -s /bin/false -d "$INSTALL_DIR" debros
log "Created debros user"
fi
# Create directory structure
sudo mkdir -p "$INSTALL_DIR"/{bin,configs,keys,data,logs}
sudo mkdir -p "$INSTALL_DIR/keys/$NODE_TYPE"
sudo mkdir -p "$INSTALL_DIR/data/$NODE_TYPE"/{rqlite,storage}
# Set ownership and permissions
sudo chown -R debros:debros "$INSTALL_DIR"
sudo chmod 755 "$INSTALL_DIR"
sudo chmod 700 "$INSTALL_DIR/keys"
sudo chmod 600 "$INSTALL_DIR/keys/$NODE_TYPE" 2>/dev/null || true
success "Directory structure created"
}
# Clone or update repository
setup_source_code() {
log "Setting up source code..."
if [ -d "$INSTALL_DIR/src" ]; then
log "Updating existing repository..."
cd "$INSTALL_DIR/src"
sudo -u debros git pull
else
log "Cloning repository..."
sudo -u debros git clone "$REPO_URL" "$INSTALL_DIR/src"
cd "$INSTALL_DIR/src"
fi
success "Source code ready"
}
# Generate identity key
generate_identity() {
log "Generating node identity..."
cd "$INSTALL_DIR/src"
# Create a temporary Go program for key generation
cat > /tmp/generate_identity.go << 'EOF'
package main
import (
"crypto/rand"
"fmt"
"os"
"path/filepath"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
)
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: go run generate_identity.go <key_file_path>")
os.Exit(1)
}
keyFile := os.Args[1]
// Generate identity
priv, pub, err := crypto.GenerateKeyPairWithReader(crypto.Ed25519, 2048, rand.Reader)
if err != nil {
panic(err)
}
// Get peer ID
peerID, err := peer.IDFromPublicKey(pub)
if err != nil {
panic(err)
}
// Marshal private key
data, err := crypto.MarshalPrivateKey(priv)
if err != nil {
panic(err)
}
// Create directory
if err := os.MkdirAll(filepath.Dir(keyFile), 0700); err != nil {
panic(err)
}
// Save identity
if err := os.WriteFile(keyFile, data, 0600); err != nil {
panic(err)
}
fmt.Printf("Generated Peer ID: %s\n", peerID.String())
fmt.Printf("Identity saved to: %s\n", keyFile)
}
EOF
# Generate the identity key
sudo -u debros go run /tmp/generate_identity.go "$INSTALL_DIR/keys/$NODE_TYPE/identity.key"
rm /tmp/generate_identity.go
success "Node identity generated"
}
# Build binaries
build_binaries() {
log "Building DeBros Network binaries..."
cd "$INSTALL_DIR/src"
# Build all binaries
sudo -u debros make build
# Copy binaries to installation directory
sudo cp bin/* "$INSTALL_DIR/bin/"
sudo chown debros:debros "$INSTALL_DIR/bin/"*
success "Binaries built and installed"
}
# Generate configuration files
generate_configs() {
log "Generating configuration files..."
if [ "$NODE_TYPE" = "bootstrap" ]; then
cat > /tmp/config.yaml << EOF
node:
data_dir: "$INSTALL_DIR/data/bootstrap"
key_file: "$INSTALL_DIR/keys/bootstrap/identity.key"
listen_addresses:
- "/ip4/0.0.0.0/tcp/$BOOTSTRAP_PORT"
solana_wallet: "$SOLANA_WALLET"
database:
rqlite_port: $RQLITE_BOOTSTRAP_PORT
rqlite_raft_port: $RAFT_BOOTSTRAP_PORT
logging:
level: "info"
file: "$INSTALL_DIR/logs/bootstrap.log"
EOF
else
cat > /tmp/config.yaml << EOF
node:
data_dir: "$INSTALL_DIR/data/node"
key_file: "$INSTALL_DIR/keys/node/identity.key"
listen_addresses:
- "/ip4/0.0.0.0/tcp/$NODE_PORT"
solana_wallet: "$SOLANA_WALLET"
database:
rqlite_port: $RQLITE_NODE_PORT
rqlite_raft_port: $RAFT_NODE_PORT
logging:
level: "info"
file: "$INSTALL_DIR/logs/node.log"
EOF
fi
sudo mv /tmp/config.yaml "$INSTALL_DIR/configs/$NODE_TYPE.yaml"
sudo chown debros:debros "$INSTALL_DIR/configs/$NODE_TYPE.yaml"
success "Configuration files generated"
}
# Configure firewall
configure_firewall() {
if [[ "$CONFIGURE_FIREWALL" == "yes" ]]; then
log "Configuring firewall..."
if command -v ufw &> /dev/null; then
if [ "$NODE_TYPE" = "bootstrap" ]; then
sudo ufw allow $BOOTSTRAP_PORT
sudo ufw allow $RQLITE_BOOTSTRAP_PORT
sudo ufw allow $RAFT_BOOTSTRAP_PORT
else
sudo ufw allow $NODE_PORT
sudo ufw allow $RQLITE_NODE_PORT
sudo ufw allow $RAFT_NODE_PORT
fi
# Enable ufw if not already active
UFW_STATUS=$(sudo ufw status | grep -o "Status: [a-z]*" | awk '{print $2}' || echo "inactive")
if [[ "$UFW_STATUS" != "active" ]]; then
echo "y" | sudo ufw enable
fi
success "Firewall configured"
else
warning "UFW not found. Please configure firewall manually."
fi
fi
}
# Create systemd service
create_systemd_service() {
log "Creating systemd service..."
cat > /tmp/debros-$NODE_TYPE.service << EOF
[Unit]
Description=DeBros Network $NODE_TYPE Node
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=debros
Group=debros
WorkingDirectory=$INSTALL_DIR
ExecStart=$INSTALL_DIR/bin/$NODE_TYPE -config $INSTALL_DIR/configs/$NODE_TYPE.yaml
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=debros-$NODE_TYPE
# Security settings
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=$INSTALL_DIR
[Install]
WantedBy=multi-user.target
EOF
sudo mv /tmp/debros-$NODE_TYPE.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable debros-$NODE_TYPE.service
success "Systemd service created and enabled"
}
# Start the service
start_service() {
log "Starting DeBros Network $NODE_TYPE node..."
sudo systemctl start debros-$NODE_TYPE.service
sleep 3
if systemctl is-active --quiet debros-$NODE_TYPE.service; then
success "DeBros Network $NODE_TYPE node started successfully"
else
error "Failed to start DeBros Network $NODE_TYPE node"
log "Check logs with: sudo journalctl -u debros-$NODE_TYPE.service"
exit 1
fi
}
# Display banner
display_banner() {
echo -e "${BLUE}========================================================================${NOCOLOR}"
echo -e "${CYAN}
____ ____ _ _ _ _
| _ \ ___| __ ) _ __ ___ ___ | \ | | ___| |___ _____ _ __| | __
| | | |/ _ \ _ \| __/ _ \/ __| | \| |/ _ \ __\ \ /\ / / _ \| __| |/ /
| |_| | __/ |_) | | | (_) \__ \ | |\ | __/ |_ \ V V / (_) | | | <
|____/ \___|____/|_| \___/|___/ |_| \_|\___|\__| \_/\_/ \___/|_| |_|\_\\
${NOCOLOR}"
echo -e "${BLUE}========================================================================${NOCOLOR}"
}
# Main installation function
main() {
display_banner
log "${BLUE}==================================================${NOCOLOR}"
log "${GREEN} Starting DeBros Network Installation ${NOCOLOR}"
log "${BLUE}==================================================${NOCOLOR}"
detect_os
check_ports
# Check and install Go if needed
if ! check_go_installation; then
install_go
fi
install_dependencies
configuration_wizard
setup_directories
setup_source_code
generate_identity
build_binaries
generate_configs
configure_firewall
create_systemd_service
start_service
# Display completion information
log "${BLUE}==================================================${NOCOLOR}"
log "${GREEN} Installation Complete! ${NOCOLOR}"
log "${BLUE}==================================================${NOCOLOR}"
log "${GREEN}Node Type:${NOCOLOR} ${CYAN}$NODE_TYPE${NOCOLOR}"
log "${GREEN}Installation Directory:${NOCOLOR} ${CYAN}$INSTALL_DIR${NOCOLOR}"
log "${GREEN}Configuration:${NOCOLOR} ${CYAN}$INSTALL_DIR/configs/$NODE_TYPE.yaml${NOCOLOR}"
log "${GREEN}Logs:${NOCOLOR} ${CYAN}$INSTALL_DIR/logs/$NODE_TYPE.log${NOCOLOR}"
if [ "$NODE_TYPE" = "bootstrap" ]; then
log "${GREEN}Bootstrap Port:${NOCOLOR} ${CYAN}$BOOTSTRAP_PORT${NOCOLOR}"
log "${GREEN}RQLite Port:${NOCOLOR} ${CYAN}$RQLITE_BOOTSTRAP_PORT${NOCOLOR}"
log "${GREEN}Raft Port:${NOCOLOR} ${CYAN}$RAFT_BOOTSTRAP_PORT${NOCOLOR}"
else
log "${GREEN}Node Port:${NOCOLOR} ${CYAN}$NODE_PORT${NOCOLOR}"
log "${GREEN}RQLite Port:${NOCOLOR} ${CYAN}$RQLITE_NODE_PORT${NOCOLOR}"
log "${GREEN}Raft Port:${NOCOLOR} ${CYAN}$RAFT_NODE_PORT${NOCOLOR}"
fi
log "${BLUE}==================================================${NOCOLOR}"
log "${GREEN}Management Commands:${NOCOLOR}"
log "${CYAN} - sudo systemctl status debros-$NODE_TYPE${NOCOLOR} (Check status)"
log "${CYAN} - sudo systemctl restart debros-$NODE_TYPE${NOCOLOR} (Restart service)"
log "${CYAN} - sudo systemctl stop debros-$NODE_TYPE${NOCOLOR} (Stop service)"
log "${CYAN} - sudo systemctl start debros-$NODE_TYPE${NOCOLOR} (Start service)"
log "${CYAN} - sudo journalctl -u debros-$NODE_TYPE.service -f${NOCOLOR} (View logs)"
log "${CYAN} - $INSTALL_DIR/bin/cli${NOCOLOR} (Use CLI tools)"
log "${BLUE}==================================================${NOCOLOR}"
success "DeBros Network $NODE_TYPE node is now running!"
log "${CYAN}For documentation visit: https://docs.debros.io${NOCOLOR}"
}
# Run main function
main "$@"

653
pkg/client/client.go Normal file
View File

@ -0,0 +1,653 @@
package client
import (
"context"
"fmt"
"sync"
"time"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/p2p/security/noise"
libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic"
"github.com/libp2p/go-libp2p/p2p/transport/tcp"
"github.com/multiformats/go-multiaddr"
"go.uber.org/zap"
dht "github.com/libp2p/go-libp2p-kad-dht"
libp2ppubsub "github.com/libp2p/go-libp2p-pubsub"
"github.com/libp2p/go-libp2p/p2p/discovery/mdns"
"network/pkg/discovery"
"network/pkg/pubsub"
"network/pkg/storage"
)
// Client implements the NetworkClient interface
type Client struct {
config *ClientConfig
// Network components
host host.Host
libp2pPS *libp2ppubsub.PubSub
dht *dht.IpfsDHT
logger *zap.Logger
// Components
database *DatabaseClientImpl
storage *StorageClientImpl
network *NetworkInfoImpl
pubsub *pubSubBridge
// Managers
discoveryMgr *discovery.Manager
// State
connected bool
startTime time.Time
mu sync.RWMutex
}
// pubSubBridge bridges between our PubSubClient interface and the pubsub package
type pubSubBridge struct {
adapter *pubsub.ClientAdapter
}
func (p *pubSubBridge) Subscribe(ctx context.Context, topic string, handler MessageHandler) error {
// Convert our MessageHandler to the pubsub package MessageHandler
pubsubHandler := func(topic string, data []byte) error {
return handler(topic, data)
}
return p.adapter.Subscribe(ctx, topic, pubsubHandler)
}
func (p *pubSubBridge) Publish(ctx context.Context, topic string, data []byte) error {
return p.adapter.Publish(ctx, topic, data)
}
func (p *pubSubBridge) Unsubscribe(ctx context.Context, topic string) error {
return p.adapter.Unsubscribe(ctx, topic)
}
func (p *pubSubBridge) ListTopics(ctx context.Context) ([]string, error) {
return p.adapter.ListTopics(ctx)
}
// NewClient creates a new network client
func NewClient(config *ClientConfig) (NetworkClient, error) {
if config == nil {
return nil, fmt.Errorf("config cannot be nil")
}
if config.AppName == "" {
return nil, fmt.Errorf("app name is required")
}
// Create zap logger - use different config for quiet mode
var logger *zap.Logger
var err error
if config.QuietMode {
// For quiet mode, only show warnings and errors
zapConfig := zap.NewProductionConfig()
zapConfig.Level = zap.NewAtomicLevelAt(zap.WarnLevel)
// Disable caller info for cleaner output
zapConfig.DisableCaller = true
// Disable stacktrace for cleaner output
zapConfig.DisableStacktrace = true
logger, err = zapConfig.Build()
} else {
// Development logger shows debug/info logs
logger, err = zap.NewDevelopment()
}
if err != nil {
return nil, fmt.Errorf("failed to create logger: %w", err)
}
client := &Client{
config: config,
logger: logger,
startTime: time.Now(),
}
// Initialize components (will be configured when connected)
client.database = &DatabaseClientImpl{client: client}
client.network = &NetworkInfoImpl{client: client}
return client, nil
}
// Database returns the database client
func (c *Client) Database() DatabaseClient {
return c.database
}
// Storage returns the storage client
func (c *Client) Storage() StorageClient {
return c.storage
}
// PubSub returns the pub/sub client
func (c *Client) PubSub() PubSubClient {
return c.pubsub
}
// Network returns the network info client
func (c *Client) Network() NetworkInfo {
return c.network
}
// Connect establishes connection to the network
func (c *Client) Connect() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.connected {
return nil
}
// Create LibP2P host
h, err := libp2p.New(
libp2p.ListenAddrStrings("/ip4/0.0.0.0/tcp/0"), // Random port
libp2p.Security(noise.ID, noise.New),
libp2p.Transport(tcp.NewTCPTransport),
libp2p.Transport(libp2pquic.NewTransport),
libp2p.DefaultMuxers,
)
if err != nil {
return fmt.Errorf("failed to create libp2p host: %w", err)
}
c.host = h
// Create LibP2P PubSub with enhanced discovery for Anchat
var ps *libp2ppubsub.PubSub
if c.config.AppName == "anchat" {
// For Anchat, use more aggressive GossipSub settings for better peer discovery
ps, err = libp2ppubsub.NewGossipSub(context.Background(), h,
libp2ppubsub.WithPeerExchange(true), // Enable peer exchange
libp2ppubsub.WithFloodPublish(true), // Flood publish for small networks
)
} else {
// Standard GossipSub for other applications
ps, err = libp2ppubsub.NewGossipSub(context.Background(), h)
}
if err != nil {
h.Close()
return fmt.Errorf("failed to create pubsub: %w", err)
}
c.libp2pPS = ps
// Create pubsub bridge once and store it
adapter := pubsub.NewClientAdapter(c.libp2pPS, c.getAppNamespace())
c.pubsub = &pubSubBridge{adapter: adapter}
// Create DHT for peer discovery - Use server mode for better peer discovery in small networks
kademliaDHT, err := dht.New(context.Background(), h, dht.Mode(dht.ModeServer))
if err != nil {
h.Close()
return fmt.Errorf("failed to create DHT: %w", err)
}
c.dht = kademliaDHT
// Create storage client with the host
storageClient := storage.NewClient(h, c.getAppNamespace(), c.logger)
c.storage = &StorageClientImpl{
client: c,
storageClient: storageClient,
}
// Connect to bootstrap peers FIRST
ctx, cancel := context.WithTimeout(context.Background(), c.config.ConnectTimeout)
defer cancel()
bootstrapPeersConnected := 0
for _, bootstrapAddr := range c.config.BootstrapPeers {
if err := c.connectToBootstrap(ctx, bootstrapAddr); err != nil {
c.logger.Warn("Failed to connect to bootstrap peer",
zap.String("addr", bootstrapAddr),
zap.Error(err))
continue
}
bootstrapPeersConnected++
}
if bootstrapPeersConnected == 0 {
c.logger.Warn("No bootstrap peers connected, continuing anyway")
}
// Add bootstrap peers to DHT routing table explicitly BEFORE bootstrapping
for _, bootstrapAddr := range c.config.BootstrapPeers {
if ma, err := multiaddr.NewMultiaddr(bootstrapAddr); err == nil {
if peerInfo, err := peer.AddrInfoFromP2pAddr(ma); err == nil {
c.host.Peerstore().AddAddrs(peerInfo.ID, peerInfo.Addrs, time.Hour*24)
// Force add to DHT routing table
if added, err := c.dht.RoutingTable().TryAddPeer(peerInfo.ID, true, true); err == nil && added {
c.logger.Debug("Added bootstrap peer to DHT routing table",
zap.String("peer", peerInfo.ID.String()))
}
}
}
}
// Bootstrap the DHT AFTER connecting to bootstrap peers
if err = kademliaDHT.Bootstrap(context.Background()); err != nil {
c.logger.Warn("Failed to bootstrap DHT", zap.Error(err))
// Don't fail - continue without DHT
} else {
c.logger.Debug("DHT bootstrap initiated successfully")
}
// Initialize discovery manager
c.discoveryMgr = discovery.NewManager(c.host, c.dht, c.logger)
// Start peer discovery
discoveryConfig := discovery.Config{
DiscoveryInterval: 5 * time.Second, // More frequent discovery
MaxConnections: 10, // Allow more connections
}
if err := c.discoveryMgr.Start(discoveryConfig); err != nil {
c.logger.Warn("Failed to start peer discovery", zap.Error(err))
}
// For Anchat clients, ensure we connect to all other clients through bootstrap
if c.config.AppName == "anchat" {
// Start mDNS discovery for local network peer discovery
go c.startMDNSDiscovery()
go c.ensureAnchatPeerConnectivity()
} else {
// Start aggressive peer discovery for other apps
go c.startAggressivePeerDiscovery()
}
// Start connection monitoring
c.startConnectionMonitoring()
c.connected = true
return nil
}
// connectToBootstrap connects to a bootstrap peer
func (c *Client) connectToBootstrap(ctx context.Context, addr string) error {
ma, err := multiaddr.NewMultiaddr(addr)
if err != nil {
return fmt.Errorf("invalid multiaddr: %w", err)
}
// Try to extract peer info if it's a full multiaddr with peer ID
peerInfo, err := peer.AddrInfoFromP2pAddr(ma)
if err != nil {
// If there's no peer ID, try to discover the peer at this address
return c.connectToAddress(ctx, ma)
}
if err := c.host.Connect(ctx, *peerInfo); err != nil {
return fmt.Errorf("failed to connect to peer: %w", err)
}
c.logger.Debug("Connected to bootstrap peer",
zap.String("peer", peerInfo.ID.String()),
zap.String("addr", addr))
return nil
}
// connectToAddress attempts to discover and connect to a peer at the given address
func (c *Client) connectToAddress(ctx context.Context, ma multiaddr.Multiaddr) error {
// For the simple case, we'll just warn and continue
// In a production environment, you'd implement proper peer discovery
// using mDNS, DHT, or other mechanisms
c.logger.Warn("No peer ID provided in address, skipping bootstrap connection",
zap.String("addr", ma.String()),
zap.String("suggestion", "Use full multiaddr with peer ID like: /ip4/127.0.0.1/tcp/4001/p2p/<peer-id>"))
return nil // Don't fail - let the client continue without bootstrap
} // Disconnect closes the connection to the network
func (c *Client) Disconnect() error {
c.mu.Lock()
defer c.mu.Unlock()
if !c.connected {
return nil
}
// Stop peer discovery
if c.discoveryMgr != nil {
c.discoveryMgr.Stop()
}
// Close pubsub adapter
if c.pubsub != nil && c.pubsub.adapter != nil {
if err := c.pubsub.adapter.Close(); err != nil {
c.logger.Error("Failed to close pubsub adapter", zap.Error(err))
}
c.pubsub = nil
}
// Close DHT
if c.dht != nil {
if err := c.dht.Close(); err != nil {
c.logger.Error("Failed to close DHT", zap.Error(err))
}
}
// Close LibP2P host
if c.host != nil {
if err := c.host.Close(); err != nil {
c.logger.Error("Failed to close host", zap.Error(err))
}
}
c.connected = false
return nil
}
// Health returns the current health status
func (c *Client) Health() (*HealthStatus, error) {
c.mu.RLock()
defer c.mu.RUnlock()
status := "healthy"
if !c.connected {
status = "unhealthy"
}
checks := map[string]string{
"connection": "ok",
"database": "ok",
"storage": "ok",
"pubsub": "ok",
}
if !c.connected {
checks["connection"] = "disconnected"
}
return &HealthStatus{
Status: status,
Checks: checks,
LastUpdated: time.Now(),
ResponseTime: time.Millisecond * 10, // Simulated
}, nil
}
// isConnected checks if the client is connected
func (c *Client) isConnected() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.connected
}
// getAppNamespace returns the namespace for this app
func (c *Client) getAppNamespace() string {
return c.config.AppName
}
// startConnectionMonitoring monitors connection health and logs status
func (c *Client) startConnectionMonitoring() {
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C {
if !c.isConnected() {
return
}
// Remove connection status logging for cleaner output
// connectedPeers := c.host.Network().Peers()
// Only log if there are connection issues
_ = c.host.Network().Peers()
}
}()
}
// ensureAnchatPeerConnectivity ensures Anchat clients can discover each other through bootstrap
func (c *Client) ensureAnchatPeerConnectivity() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for i := 0; i < 30; i++ { // Run for 1 minute
<-ticker.C
if !c.isConnected() {
return
}
// Get current connected peers
connectedPeers := c.host.Network().Peers()
// For Anchat, we need to be very aggressive about finding other clients
// The key insight: we need to ask our connected peers (like bootstrap) for their peers
if c.dht != nil {
// Try to find peers through DHT routing table
routingPeers := c.dht.RoutingTable().ListPeers()
for _, peerID := range routingPeers {
if peerID == c.host.ID() {
continue
}
// Check if we're already connected to this peer
alreadyConnected := false
for _, alreadyConnectedPeer := range connectedPeers {
if alreadyConnectedPeer == peerID {
alreadyConnected = true
break
}
}
if !alreadyConnected {
// Try to connect to this peer
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
peerInfo := c.host.Peerstore().PeerInfo(peerID)
// If we don't have addresses, try to find them through the DHT
if len(peerInfo.Addrs) == 0 {
if foundPeerInfo, err := c.dht.FindPeer(ctx, peerID); err == nil {
peerInfo = foundPeerInfo
// Add to peerstore for future use
c.host.Peerstore().AddAddrs(peerInfo.ID, peerInfo.Addrs, time.Hour*24)
}
}
if len(peerInfo.Addrs) > 0 {
err := c.host.Connect(ctx, peerInfo)
if err == nil {
c.logger.Info("Anchat discovered and connected to peer",
zap.String("peer", peerID.String()[:8]+"..."))
// Add newly connected peer to DHT routing table
if added, addErr := c.dht.RoutingTable().TryAddPeer(peerID, true, true); addErr == nil && added {
c.logger.Debug("Added new peer to DHT routing table",
zap.String("peer", peerID.String()[:8]+"..."))
}
// Force pubsub to recognize the new peer and form mesh connections
if c.libp2pPS != nil {
// Wait a moment for connection to stabilize
time.Sleep(100 * time.Millisecond)
// List peers to trigger mesh formation
_ = c.libp2pPS.ListPeers("")
}
} else {
c.logger.Debug("Failed to connect to discovered peer",
zap.String("peer", peerID.String()[:8]+"..."),
zap.Error(err))
}
}
cancel()
}
}
// If DHT routing table is still empty, try to force populate it
if len(routingPeers) == 0 {
// Try to add all connected peers to DHT routing table
for _, connectedPeerID := range connectedPeers {
if connectedPeerID != c.host.ID() {
if added, err := c.dht.RoutingTable().TryAddPeer(connectedPeerID, true, true); err == nil && added {
c.logger.Info("Force-added connected peer to DHT routing table",
zap.String("peer", connectedPeerID.String()[:8]+"..."))
}
}
}
// Force DHT refresh
c.dht.RefreshRoutingTable()
}
}
// Also try to connect to any peers we might have in our peerstore but aren't connected to
allKnownPeers := c.host.Peerstore().Peers()
for _, knownPeerID := range allKnownPeers {
if knownPeerID == c.host.ID() {
continue
}
// Check if we're already connected
alreadyConnected := false
for _, connectedPeer := range connectedPeers {
if connectedPeer == knownPeerID {
alreadyConnected = true
break
}
}
if !alreadyConnected {
// Try to connect to this known peer
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
peerInfo := c.host.Peerstore().PeerInfo(knownPeerID)
if len(peerInfo.Addrs) > 0 {
err := c.host.Connect(ctx, peerInfo)
if err == nil {
c.logger.Info("Anchat reconnected to known peer",
zap.String("peer", knownPeerID.String()[:8]+"..."))
// Force pubsub mesh formation
if c.libp2pPS != nil {
time.Sleep(100 * time.Millisecond)
_ = c.libp2pPS.ListPeers("")
}
}
}
cancel()
}
}
// Log status every 5 iterations (10 seconds)
if i%5 == 0 && len(connectedPeers) > 0 {
c.logger.Info("Anchat peer discovery progress",
zap.Int("iteration", i+1),
zap.Int("connected_peers", len(connectedPeers)),
zap.Int("known_peers", len(allKnownPeers)))
}
}
} // startAggressivePeerDiscovery implements aggressive peer discovery for non-Anchat apps
func (c *Client) startAggressivePeerDiscovery() {
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
for i := 0; i < 20; i++ { // Run for 1 minute
<-ticker.C
if !c.isConnected() {
return
}
// Get current connected peers
connectedPeers := c.host.Network().Peers()
// Try to discover more peers through the DHT
if c.dht != nil {
// Get peers from the DHT routing table
routingPeers := c.dht.RoutingTable().ListPeers()
for _, peerID := range routingPeers {
if peerID == c.host.ID() {
continue
}
// Check if we're already connected
alreadyConnected := false
for _, connectedPeer := range connectedPeers {
if connectedPeer == peerID {
alreadyConnected = true
break
}
}
if !alreadyConnected {
// Try to connect
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
peerInfo := c.host.Peerstore().PeerInfo(peerID)
if len(peerInfo.Addrs) > 0 {
err := c.host.Connect(ctx, peerInfo)
if err == nil {
c.logger.Debug("Connected to discovered peer",
zap.String("peer", peerID.String()[:8]+"..."))
}
}
cancel()
}
}
}
// Log current status every 10 iterations (30 seconds)
if i%10 == 0 {
c.logger.Debug("Peer discovery status",
zap.Int("iteration", i+1),
zap.Int("connected_peers", len(connectedPeers)))
}
}
}
// startMDNSDiscovery enables mDNS peer discovery for local network
func (c *Client) startMDNSDiscovery() {
// Setup mDNS discovery service for Anchat
mdnsService := mdns.NewMdnsService(c.host, "anchat-p2p", &discoveryNotifee{
client: c,
logger: c.logger,
})
if err := mdnsService.Start(); err != nil {
c.logger.Warn("Failed to start mDNS discovery", zap.Error(err))
return
}
c.logger.Info("Started mDNS discovery for Anchat")
}
// discoveryNotifee handles mDNS peer discovery notifications
type discoveryNotifee struct {
client *Client
logger *zap.Logger
}
func (n *discoveryNotifee) HandlePeerFound(pi peer.AddrInfo) {
n.logger.Info("mDNS discovered Anchat peer",
zap.String("peer", pi.ID.String()[:8]+"..."),
zap.Int("addrs", len(pi.Addrs)))
// Connect to the discovered peer
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := n.client.host.Connect(ctx, pi); err != nil {
n.logger.Debug("Failed to connect to mDNS discovered peer",
zap.String("peer", pi.ID.String()[:8]+"..."),
zap.Error(err))
} else {
n.logger.Info("Successfully connected to mDNS discovered peer",
zap.String("peer", pi.ID.String()[:8]+"..."))
// Force pubsub to recognize the new peer
if n.client.libp2pPS != nil {
_ = n.client.libp2pPS.ListPeers("")
}
}
}

View File

@ -0,0 +1,565 @@
package client
import (
"context"
"fmt"
"strings"
"sync"
"time"
"network/pkg/storage"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/multiformats/go-multiaddr"
"github.com/rqlite/gorqlite"
)
// DatabaseClientImpl implements DatabaseClient
type DatabaseClientImpl struct {
client *Client
connection *gorqlite.Connection
mu sync.RWMutex
}
// checkConnection verifies the client is connected
func (d *DatabaseClientImpl) checkConnection() error {
if !d.client.isConnected() {
return fmt.Errorf("client not connected")
}
return nil
}
// withRetry executes an operation with retry logic
func (d *DatabaseClientImpl) withRetry(operation func(*gorqlite.Connection) error) error {
maxRetries := 3
var lastErr error
for attempt := 1; attempt <= maxRetries; attempt++ {
conn, err := d.getRQLiteConnection()
if err != nil {
lastErr = err
d.clearConnection()
continue
}
if err := operation(conn); err != nil {
lastErr = err
d.clearConnection()
continue
}
return nil
}
return fmt.Errorf("operation failed after %d attempts. Last error: %w", maxRetries, lastErr)
}
// Query executes a SQL query
func (d *DatabaseClientImpl) Query(ctx context.Context, sql string, args ...interface{}) (*QueryResult, error) {
if err := d.checkConnection(); err != nil {
return nil, err
}
// Determine if this is a read or write operation
isWriteOperation := d.isWriteOperation(sql)
// Retry logic for resilient querying
maxRetries := 3
var lastErr error
for attempt := 1; attempt <= maxRetries; attempt++ {
// Get RQLite connection (tries multiple nodes)
conn, err := d.getRQLiteConnection()
if err != nil {
lastErr = err
// Clear any cached connection and try again
d.clearConnection()
continue
}
if isWriteOperation {
// Execute write operation with parameters
_, err := conn.WriteOneParameterized(gorqlite.ParameterizedStatement{
Query: sql,
Arguments: args,
})
if err != nil {
lastErr = err
d.clearConnection()
continue
}
// For write operations, return empty result set
return &QueryResult{
Columns: []string{"affected"},
Rows: [][]interface{}{{"success"}},
Count: 1,
}, nil
} else {
// Execute read operation with parameters
result, err := conn.QueryOneParameterized(gorqlite.ParameterizedStatement{
Query: sql,
Arguments: args,
})
if err != nil {
lastErr = err
d.clearConnection()
continue
}
// Convert gorqlite.QueryResult to our QueryResult
columns := result.Columns()
numRows := int(result.NumRows())
queryResult := &QueryResult{
Columns: columns,
Rows: make([][]interface{}, 0, numRows),
Count: result.NumRows(),
}
// Iterate through rows
for result.Next() {
row, err := result.Slice()
if err != nil {
continue
}
queryResult.Rows = append(queryResult.Rows, row)
}
return queryResult, nil
}
}
return nil, fmt.Errorf("query failed after %d attempts. Last error: %w", maxRetries, lastErr)
}
// isWriteOperation determines if a SQL statement is a write operation
func (d *DatabaseClientImpl) isWriteOperation(sql string) bool {
// Convert to uppercase for comparison
sqlUpper := strings.ToUpper(strings.TrimSpace(sql))
// List of write operation keywords
writeKeywords := []string{
"INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER",
"TRUNCATE", "REPLACE", "MERGE", "PRAGMA",
}
for _, keyword := range writeKeywords {
if strings.HasPrefix(sqlUpper, keyword) {
return true
}
}
return false
} // clearConnection clears the cached connection to force reconnection
func (d *DatabaseClientImpl) clearConnection() {
d.mu.Lock()
defer d.mu.Unlock()
d.connection = nil
} // getRQLiteConnection returns a connection to RQLite, creating one if needed
func (d *DatabaseClientImpl) getRQLiteConnection() (*gorqlite.Connection, error) {
d.mu.Lock()
defer d.mu.Unlock()
// Always try to get a fresh connection to handle leadership changes
// and node failures gracefully
return d.connectToAvailableNode()
}
// connectToAvailableNode tries to connect to any available RQLite node
func (d *DatabaseClientImpl) connectToAvailableNode() (*gorqlite.Connection, error) {
// List of RQLite nodes to try (bootstrap, node1, node2, etc.)
rqliteNodes := []string{
"http://localhost:5001", // bootstrap
"http://localhost:5002", // node1
"http://localhost:5003", // node2
"http://localhost:5004", // node3 (if exists)
"http://localhost:5005", // node4 (if exists)
}
var lastErr error
for _, rqliteURL := range rqliteNodes {
conn, err := gorqlite.Open(rqliteURL)
if err != nil {
lastErr = err
continue
}
// Test the connection with a simple query to ensure it's working
// and the node has leadership or can serve reads
if err := d.testConnection(conn); err != nil {
lastErr = err
continue
}
d.connection = conn
return conn, nil
}
return nil, fmt.Errorf("failed to connect to any RQLite instance. Last error: %w", lastErr)
}
// testConnection performs a health check on the RQLite connection
func (d *DatabaseClientImpl) testConnection(conn *gorqlite.Connection) error {
// Try a simple read query first (works even without leadership)
result, err := conn.QueryOne("SELECT 1")
if err != nil {
return fmt.Errorf("read test failed: %w", err)
}
// Check if we got a valid result
if result.NumRows() == 0 {
return fmt.Errorf("read test returned no rows")
}
return nil
}
// Transaction executes multiple queries in a transaction
func (d *DatabaseClientImpl) Transaction(ctx context.Context, queries []string) error {
if !d.client.isConnected() {
return fmt.Errorf("client not connected")
}
maxRetries := 3
var lastErr error
for attempt := 1; attempt <= maxRetries; attempt++ {
// Get RQLite connection
conn, err := d.getRQLiteConnection()
if err != nil {
lastErr = err
d.clearConnection()
continue
}
// Execute all queries in the transaction
success := true
for _, query := range queries {
_, err := conn.WriteOne(query)
if err != nil {
lastErr = err
success = false
d.clearConnection()
break
}
}
if success {
return nil
}
}
return fmt.Errorf("transaction failed after %d attempts. Last error: %w", maxRetries, lastErr)
}
// CreateTable creates a new table
func (d *DatabaseClientImpl) CreateTable(ctx context.Context, schema string) error {
if err := d.checkConnection(); err != nil {
return err
}
return d.withRetry(func(conn *gorqlite.Connection) error {
_, err := conn.WriteOne(schema)
return err
})
}
// DropTable drops a table
func (d *DatabaseClientImpl) DropTable(ctx context.Context, tableName string) error {
if err := d.checkConnection(); err != nil {
return err
}
return d.withRetry(func(conn *gorqlite.Connection) error {
dropSQL := fmt.Sprintf("DROP TABLE IF EXISTS %s", tableName)
_, err := conn.WriteOne(dropSQL)
return err
})
}
// GetSchema returns schema information
func (d *DatabaseClientImpl) GetSchema(ctx context.Context) (*SchemaInfo, error) {
if !d.client.isConnected() {
return nil, fmt.Errorf("client not connected")
}
// Get RQLite connection
conn, err := d.getRQLiteConnection()
if err != nil {
return nil, fmt.Errorf("failed to get RQLite connection: %w", err)
}
// Query for all tables
result, err := conn.QueryOne("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
if err != nil {
return nil, fmt.Errorf("failed to query table list: %w", err)
}
schema := &SchemaInfo{
Tables: make([]TableInfo, 0),
}
// Iterate through tables
for result.Next() {
row, err := result.Slice()
if err != nil {
return nil, fmt.Errorf("failed to get table row: %w", err)
}
if len(row) > 0 {
tableName := fmt.Sprintf("%v", row[0])
// Get column information for this table
columnResult, err := conn.QueryOne(fmt.Sprintf("PRAGMA table_info(%s)", tableName))
if err != nil {
continue // Skip this table if we can't get column info
}
tableInfo := TableInfo{
Name: tableName,
Columns: make([]ColumnInfo, 0),
}
// Parse column information
for columnResult.Next() {
colRow, err := columnResult.Slice()
if err != nil {
continue
}
if len(colRow) >= 6 {
columnInfo := ColumnInfo{
Name: fmt.Sprintf("%v", colRow[1]), // name
Type: fmt.Sprintf("%v", colRow[2]), // type
Nullable: fmt.Sprintf("%v", colRow[3]) == "0", // notnull (0 = nullable, 1 = not null)
}
tableInfo.Columns = append(tableInfo.Columns, columnInfo)
}
}
schema.Tables = append(schema.Tables, tableInfo)
}
}
return schema, nil
}
// StorageClientImpl implements StorageClient using distributed storage
type StorageClientImpl struct {
client *Client
storageClient *storage.Client
}
// Get retrieves a value by key
func (s *StorageClientImpl) Get(ctx context.Context, key string) ([]byte, error) {
if !s.client.isConnected() {
return nil, fmt.Errorf("client not connected")
}
return s.storageClient.Get(ctx, key)
}
// Put stores a value by key
func (s *StorageClientImpl) Put(ctx context.Context, key string, value []byte) error {
if !s.client.isConnected() {
return fmt.Errorf("client not connected")
}
err := s.storageClient.Put(ctx, key, value)
if err != nil {
return err
}
return nil
}
// Delete removes a key
func (s *StorageClientImpl) Delete(ctx context.Context, key string) error {
if !s.client.isConnected() {
return fmt.Errorf("client not connected")
}
err := s.storageClient.Delete(ctx, key)
if err != nil {
return err
}
return nil
}
// List returns keys with a given prefix
func (s *StorageClientImpl) List(ctx context.Context, prefix string, limit int) ([]string, error) {
if !s.client.isConnected() {
return nil, fmt.Errorf("client not connected")
}
return s.storageClient.List(ctx, prefix, limit)
}
// Exists checks if a key exists
func (s *StorageClientImpl) Exists(ctx context.Context, key string) (bool, error) {
if !s.client.isConnected() {
return false, fmt.Errorf("client not connected")
}
return s.storageClient.Exists(ctx, key)
}
// NetworkInfoImpl implements NetworkInfo
type NetworkInfoImpl struct {
client *Client
}
// GetPeers returns information about connected peers
func (n *NetworkInfoImpl) GetPeers(ctx context.Context) ([]PeerInfo, error) {
if !n.client.isConnected() {
return nil, fmt.Errorf("client not connected")
}
// Get peers from LibP2P host
host := n.client.host
if host == nil {
return nil, fmt.Errorf("no host available")
}
// Get connected peers
connectedPeers := host.Network().Peers()
peers := make([]PeerInfo, 0, len(connectedPeers)+1) // +1 for self
// Add connected peers
for _, peerID := range connectedPeers {
// Get peer addresses
peerInfo := host.Peerstore().PeerInfo(peerID)
// Convert multiaddrs to strings
addrs := make([]string, len(peerInfo.Addrs))
for i, addr := range peerInfo.Addrs {
addrs[i] = addr.String()
}
peers = append(peers, PeerInfo{
ID: peerID.String(),
Addresses: addrs,
Connected: true,
LastSeen: time.Now(), // LibP2P doesn't track last seen, so use current time
})
}
// Add self node
selfPeerInfo := host.Peerstore().PeerInfo(host.ID())
selfAddrs := make([]string, len(selfPeerInfo.Addrs))
for i, addr := range selfPeerInfo.Addrs {
selfAddrs[i] = addr.String()
}
// Insert self node at the beginning of the list
selfPeer := PeerInfo{
ID: host.ID().String(),
Addresses: selfAddrs,
Connected: true,
LastSeen: time.Now(),
}
// Prepend self to the list
peers = append([]PeerInfo{selfPeer}, peers...)
return peers, nil
}
// GetStatus returns network status
func (n *NetworkInfoImpl) GetStatus(ctx context.Context) (*NetworkStatus, error) {
if !n.client.isConnected() {
return nil, fmt.Errorf("client not connected")
}
host := n.client.host
if host == nil {
return nil, fmt.Errorf("no host available")
}
// Get actual network status
connectedPeers := host.Network().Peers()
// Try to get database size from RQLite (optional - don't fail if unavailable)
var dbSize int64 = 0
dbClient := n.client.database
if conn, err := dbClient.getRQLiteConnection(); err == nil {
// Query database size (rough estimate)
if result, err := conn.QueryOne("SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()"); err == nil {
for result.Next() {
if row, err := result.Slice(); err == nil && len(row) > 0 {
if size, ok := row[0].(int64); ok {
dbSize = size
}
}
}
}
}
return &NetworkStatus{
NodeID: host.ID().String(),
Connected: true,
PeerCount: len(connectedPeers),
DatabaseSize: dbSize,
Uptime: time.Since(n.client.startTime),
}, nil
}
// ConnectToPeer connects to a specific peer
func (n *NetworkInfoImpl) ConnectToPeer(ctx context.Context, peerAddr string) error {
if !n.client.isConnected() {
return fmt.Errorf("client not connected")
}
host := n.client.host
if host == nil {
return fmt.Errorf("no host available")
}
// Parse the multiaddr
ma, err := multiaddr.NewMultiaddr(peerAddr)
if err != nil {
return fmt.Errorf("invalid multiaddr: %w", err)
}
// Extract peer info
peerInfo, err := peer.AddrInfoFromP2pAddr(ma)
if err != nil {
return fmt.Errorf("failed to extract peer info: %w", err)
}
// Connect to the peer
if err := host.Connect(ctx, *peerInfo); err != nil {
return fmt.Errorf("failed to connect to peer: %w", err)
}
return nil
}
// DisconnectFromPeer disconnects from a specific peer
func (n *NetworkInfoImpl) DisconnectFromPeer(ctx context.Context, peerID string) error {
if !n.client.isConnected() {
return fmt.Errorf("client not connected")
}
host := n.client.host
if host == nil {
return fmt.Errorf("no host available")
}
// Parse the peer ID
pid, err := peer.Decode(peerID)
if err != nil {
return fmt.Errorf("invalid peer ID: %w", err)
}
// Close the connection to the peer
if err := host.Network().ClosePeer(pid); err != nil {
return fmt.Errorf("failed to disconnect from peer: %w", err)
}
return nil
}

140
pkg/client/interface.go Normal file
View File

@ -0,0 +1,140 @@
package client
import (
"context"
"fmt"
"time"
)
// NetworkClient provides the main interface for applications to interact with the network
type NetworkClient interface {
// Database operations (namespaced per app)
Database() DatabaseClient
// Key-value storage (namespaced per app)
Storage() StorageClient
// Pub/Sub messaging
PubSub() PubSubClient
// Network information
Network() NetworkInfo
// Lifecycle
Connect() error
Disconnect() error
Health() (*HealthStatus, error)
}
// DatabaseClient provides database operations for applications
type DatabaseClient interface {
Query(ctx context.Context, sql string, args ...interface{}) (*QueryResult, error)
Transaction(ctx context.Context, queries []string) error
CreateTable(ctx context.Context, schema string) error
DropTable(ctx context.Context, tableName string) error
GetSchema(ctx context.Context) (*SchemaInfo, error)
}
// StorageClient provides key-value storage operations
type StorageClient interface {
Get(ctx context.Context, key string) ([]byte, error)
Put(ctx context.Context, key string, value []byte) error
Delete(ctx context.Context, key string) error
List(ctx context.Context, prefix string, limit int) ([]string, error)
Exists(ctx context.Context, key string) (bool, error)
}
// PubSubClient provides publish/subscribe messaging
type PubSubClient interface {
Subscribe(ctx context.Context, topic string, handler MessageHandler) error
Publish(ctx context.Context, topic string, data []byte) error
Unsubscribe(ctx context.Context, topic string) error
ListTopics(ctx context.Context) ([]string, error)
}
// NetworkInfo provides network status and peer information
type NetworkInfo interface {
GetPeers(ctx context.Context) ([]PeerInfo, error)
GetStatus(ctx context.Context) (*NetworkStatus, error)
ConnectToPeer(ctx context.Context, peerAddr string) error
DisconnectFromPeer(ctx context.Context, peerID string) error
}
// MessageHandler is called when a pub/sub message is received
type MessageHandler func(topic string, data []byte) error
// Data structures
// QueryResult represents the result of a database query
type QueryResult struct {
Columns []string `json:"columns"`
Rows [][]interface{} `json:"rows"`
Count int64 `json:"count"`
}
// SchemaInfo contains database schema information
type SchemaInfo struct {
Tables []TableInfo `json:"tables"`
}
// TableInfo contains information about a database table
type TableInfo struct {
Name string `json:"name"`
Columns []ColumnInfo `json:"columns"`
}
// ColumnInfo contains information about a table column
type ColumnInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Nullable bool `json:"nullable"`
Default string `json:"default"`
}
// PeerInfo contains information about a network peer
type PeerInfo struct {
ID string `json:"id"`
Addresses []string `json:"addresses"`
Connected bool `json:"connected"`
LastSeen time.Time `json:"last_seen"`
}
// NetworkStatus contains overall network status
type NetworkStatus struct {
NodeID string `json:"node_id"`
Connected bool `json:"connected"`
PeerCount int `json:"peer_count"`
DatabaseSize int64 `json:"database_size"`
Uptime time.Duration `json:"uptime"`
}
// HealthStatus contains health check information
type HealthStatus struct {
Status string `json:"status"` // "healthy", "degraded", "unhealthy"
Checks map[string]string `json:"checks"`
LastUpdated time.Time `json:"last_updated"`
ResponseTime time.Duration `json:"response_time"`
}
// ClientConfig represents configuration for network clients
type ClientConfig struct {
AppName string `json:"app_name"`
DatabaseName string `json:"database_name"`
BootstrapPeers []string `json:"bootstrap_peers"`
ConnectTimeout time.Duration `json:"connect_timeout"`
RetryAttempts int `json:"retry_attempts"`
RetryDelay time.Duration `json:"retry_delay"`
QuietMode bool `json:"quiet_mode"` // Suppress debug/info logs
}
// DefaultClientConfig returns a default client configuration
func DefaultClientConfig(appName string) *ClientConfig {
return &ClientConfig{
AppName: appName,
DatabaseName: fmt.Sprintf("%s_db", appName),
BootstrapPeers: []string{},
ConnectTimeout: time.Second * 30,
RetryAttempts: 3,
RetryDelay: time.Second * 5,
}
}

156
pkg/config/config.go Normal file
View File

@ -0,0 +1,156 @@
package config
import (
"time"
"github.com/multiformats/go-multiaddr"
)
// Config represents the main configuration for a network node
type Config struct {
Node NodeConfig `yaml:"node"`
Database DatabaseConfig `yaml:"database"`
Discovery DiscoveryConfig `yaml:"discovery"`
Security SecurityConfig `yaml:"security"`
Logging LoggingConfig `yaml:"logging"`
}
// NodeConfig contains node-specific configuration
type NodeConfig struct {
ID string `yaml:"id"` // Auto-generated if empty
Type string `yaml:"type"` // "bootstrap" or "node"
ListenAddresses []string `yaml:"listen_addresses"` // LibP2P listen addresses
DataDir string `yaml:"data_dir"` // Data directory
MaxConnections int `yaml:"max_connections"` // Maximum peer connections
// Bootstrap configuration (only for bootstrap nodes)
IsBootstrap bool `yaml:"is_bootstrap"`
}
// DatabaseConfig contains database-related configuration
type DatabaseConfig struct {
DataDir string `yaml:"data_dir"`
ReplicationFactor int `yaml:"replication_factor"`
ShardCount int `yaml:"shard_count"`
MaxDatabaseSize int64 `yaml:"max_database_size"` // In bytes
BackupInterval time.Duration `yaml:"backup_interval"`
// RQLite-specific configuration
RQLitePort int `yaml:"rqlite_port"` // RQLite HTTP API port
RQLiteRaftPort int `yaml:"rqlite_raft_port"` // RQLite Raft consensus port
RQLiteJoinAddress string `yaml:"rqlite_join_address"` // Address to join RQLite cluster
}
// DiscoveryConfig contains peer discovery configuration
type DiscoveryConfig struct {
BootstrapPeers []string `yaml:"bootstrap_peers"` // Bootstrap peer addresses
EnableMDNS bool `yaml:"enable_mdns"` // Enable mDNS discovery
EnableDHT bool `yaml:"enable_dht"` // Enable DHT discovery
DHTPrefix string `yaml:"dht_prefix"` // DHT protocol prefix
DiscoveryInterval time.Duration `yaml:"discovery_interval"` // Discovery announcement interval
}
// SecurityConfig contains security-related configuration
type SecurityConfig struct {
EnableTLS bool `yaml:"enable_tls"`
PrivateKeyFile string `yaml:"private_key_file"`
CertificateFile string `yaml:"certificate_file"`
AuthEnabled bool `yaml:"auth_enabled"`
}
// LoggingConfig contains logging configuration
type LoggingConfig struct {
Level string `yaml:"level"` // debug, info, warn, error
Format string `yaml:"format"` // json, console
OutputFile string `yaml:"output_file"` // Empty for stdout
}
// ClientConfig represents configuration for network clients
type ClientConfig struct {
AppName string `yaml:"app_name"`
DatabaseName string `yaml:"database_name"`
BootstrapPeers []string `yaml:"bootstrap_peers"`
ConnectTimeout time.Duration `yaml:"connect_timeout"`
RetryAttempts int `yaml:"retry_attempts"`
}
// ParseMultiaddrs converts string addresses to multiaddr objects
func (c *Config) ParseMultiaddrs() ([]multiaddr.Multiaddr, error) {
var addrs []multiaddr.Multiaddr
for _, addr := range c.Node.ListenAddresses {
ma, err := multiaddr.NewMultiaddr(addr)
if err != nil {
return nil, err
}
addrs = append(addrs, ma)
}
return addrs, nil
}
// GetBootstrapMultiaddrs converts bootstrap peer strings to multiaddr objects
func (c *Config) GetBootstrapMultiaddrs() ([]multiaddr.Multiaddr, error) {
var addrs []multiaddr.Multiaddr
for _, addr := range c.Discovery.BootstrapPeers {
ma, err := multiaddr.NewMultiaddr(addr)
if err != nil {
return nil, err
}
addrs = append(addrs, ma)
}
return addrs, nil
}
// DefaultConfig returns a default configuration
func DefaultConfig() *Config {
return &Config{
Node: NodeConfig{
Type: "node",
ListenAddresses: []string{
"/ip4/0.0.0.0/tcp/0",
"/ip4/0.0.0.0/udp/0/quic",
},
DataDir: "./data",
MaxConnections: 50,
IsBootstrap: false,
},
Database: DatabaseConfig{
DataDir: "./data/db",
ReplicationFactor: 3,
ShardCount: 16,
MaxDatabaseSize: 1024 * 1024 * 1024, // 1GB
BackupInterval: time.Hour * 24, // Daily backups
// RQLite-specific configuration
RQLitePort: 4001,
RQLiteRaftPort: 4002,
RQLiteJoinAddress: "", // Empty for bootstrap node
},
Discovery: DiscoveryConfig{
BootstrapPeers: []string{},
EnableMDNS: true,
EnableDHT: true,
DHTPrefix: "/network/kad/1.0.0",
DiscoveryInterval: time.Minute * 5,
},
Security: SecurityConfig{
EnableTLS: false,
AuthEnabled: false,
},
Logging: LoggingConfig{
Level: "info",
Format: "console",
},
}
}
// BootstrapConfig returns a default configuration for bootstrap nodes
func BootstrapConfig() *Config {
config := DefaultConfig()
config.Node.Type = "bootstrap"
config.Node.IsBootstrap = true
config.Node.ListenAddresses = []string{
"/ip4/0.0.0.0/tcp/4001",
"/ip4/0.0.0.0/udp/4001/quic",
}
return config
}

192
pkg/constants/bootstrap.go Normal file
View File

@ -0,0 +1,192 @@
package constants
import (
"os"
"path/filepath"
"strconv"
"strings"
"github.com/joho/godotenv"
)
// Bootstrap node configuration
var (
// BootstrapPeerIDs are the fixed peer IDs for bootstrap nodes
// Each corresponds to a specific Ed25519 private key
BootstrapPeerIDs []string
// BootstrapAddresses are the full multiaddrs for bootstrap nodes
BootstrapAddresses []string
// BootstrapPort is the default port for bootstrap nodes
BootstrapPort int = 4001
)
// Load environment variables and initialize bootstrap configuration
func init() {
loadEnvironmentConfig()
}
// loadEnvironmentConfig loads bootstrap configuration from .env file
func loadEnvironmentConfig() {
// Try to load .env file from current directory and parent directories
envPaths := []string{
".env",
"../.env",
"../../.env", // For when running from anchat subdirectory
}
var envLoaded bool
for _, path := range envPaths {
if _, err := os.Stat(path); err == nil {
if err := godotenv.Load(path); err == nil {
envLoaded = true
break
}
}
}
if !envLoaded {
// Fallback to default values if no .env file found
setDefaultBootstrapConfig()
return
}
// Load bootstrap peers from environment
if peersEnv := os.Getenv("BOOTSTRAP_PEERS"); peersEnv != "" {
// Split by comma and trim whitespace
peerAddrs := strings.Split(peersEnv, ",")
BootstrapAddresses = make([]string, 0, len(peerAddrs))
BootstrapPeerIDs = make([]string, 0, len(peerAddrs))
for _, addr := range peerAddrs {
addr = strings.TrimSpace(addr)
if addr != "" {
BootstrapAddresses = append(BootstrapAddresses, addr)
// Extract peer ID from multiaddr
if peerID := extractPeerIDFromMultiaddr(addr); peerID != "" {
BootstrapPeerIDs = append(BootstrapPeerIDs, peerID)
}
}
}
}
// Load bootstrap port from environment
if portEnv := os.Getenv("BOOTSTRAP_PORT"); portEnv != "" {
if port, err := strconv.Atoi(portEnv); err == nil && port > 0 {
BootstrapPort = port
}
}
// If no environment config found, use defaults
if len(BootstrapAddresses) == 0 {
setDefaultBootstrapConfig()
}
}
// setDefaultBootstrapConfig sets default bootstrap configuration
func setDefaultBootstrapConfig() {
BootstrapPeerIDs = []string{
"12D3KooWN3AQHuxAzXfu98tiFYw7W3N2SyDwdxDRANXJp3ktVf8j",
}
BootstrapAddresses = []string{
"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWN3AQHuxAzXfu98tiFYw7W3N2SyDwdxDRANXJp3ktVf8j",
}
BootstrapPort = 4001
}
// extractPeerIDFromMultiaddr extracts the peer ID from a multiaddr string
func extractPeerIDFromMultiaddr(multiaddr string) string {
// Look for /p2p/ followed by the peer ID
parts := strings.Split(multiaddr, "/p2p/")
if len(parts) >= 2 {
return parts[1]
}
return ""
}
// Constants for backward compatibility
var (
// Primary bootstrap peer ID (first in the list)
BootstrapPeerID string
// Primary bootstrap address (first in the list)
BootstrapAddress string
)
// updateBackwardCompatibilityConstants updates the single constants for backward compatibility
func updateBackwardCompatibilityConstants() {
if len(BootstrapPeerIDs) > 0 {
BootstrapPeerID = BootstrapPeerIDs[0]
}
if len(BootstrapAddresses) > 0 {
BootstrapAddress = BootstrapAddresses[0]
}
}
// Call this after loading environment config
func init() {
// This runs after the first init() that calls loadEnvironmentConfig()
updateBackwardCompatibilityConstants()
}
// Helper functions for working with bootstrap peers
// GetBootstrapPeers returns a copy of all bootstrap peer addresses
func GetBootstrapPeers() []string {
if len(BootstrapAddresses) == 0 {
setDefaultBootstrapConfig()
updateBackwardCompatibilityConstants()
}
peers := make([]string, len(BootstrapAddresses))
copy(peers, BootstrapAddresses)
return peers
}
// GetBootstrapPeerIDs returns a copy of all bootstrap peer IDs
func GetBootstrapPeerIDs() []string {
if len(BootstrapPeerIDs) == 0 {
setDefaultBootstrapConfig()
updateBackwardCompatibilityConstants()
}
ids := make([]string, len(BootstrapPeerIDs))
copy(ids, BootstrapPeerIDs)
return ids
}
// AddBootstrapPeer adds a new bootstrap peer to the lists (runtime only)
func AddBootstrapPeer(peerID, address string) {
BootstrapPeerIDs = append(BootstrapPeerIDs, peerID)
BootstrapAddresses = append(BootstrapAddresses, address)
updateBackwardCompatibilityConstants()
}
// ReloadEnvironmentConfig reloads the configuration from environment
func ReloadEnvironmentConfig() {
loadEnvironmentConfig()
updateBackwardCompatibilityConstants()
}
// GetEnvironmentInfo returns information about the current configuration
func GetEnvironmentInfo() map[string]interface{} {
return map[string]interface{}{
"bootstrap_peers": GetBootstrapPeers(),
"bootstrap_peer_ids": GetBootstrapPeerIDs(),
"bootstrap_port": BootstrapPort,
"environment": os.Getenv("ENVIRONMENT"),
"config_loaded_from": getConfigSource(),
}
}
// getConfigSource returns where the configuration was loaded from
func getConfigSource() string {
envPaths := []string{".env", "../.env", "../../.env"}
for _, path := range envPaths {
if _, err := os.Stat(path); err == nil {
abs, _ := filepath.Abs(path)
return abs
}
}
return "default values (no .env file found)"
}

46
pkg/database/adapter.go Normal file
View File

@ -0,0 +1,46 @@
package database
import (
"database/sql"
"fmt"
_ "github.com/rqlite/gorqlite/stdlib" // Import the database/sql driver
)
// RQLiteAdapter adapts RQLite to the sql.DB interface
type RQLiteAdapter struct {
manager *RQLiteManager
db *sql.DB
}
// NewRQLiteAdapter creates a new adapter that provides sql.DB interface for RQLite
func NewRQLiteAdapter(manager *RQLiteManager) (*RQLiteAdapter, error) {
// Use the gorqlite database/sql driver
db, err := sql.Open("rqlite", fmt.Sprintf("http://localhost:%d", manager.config.RQLitePort))
if err != nil {
return nil, fmt.Errorf("failed to open RQLite SQL connection: %w", err)
}
return &RQLiteAdapter{
manager: manager,
db: db,
}, nil
}
// GetSQLDB returns the sql.DB interface for compatibility with existing storage service
func (a *RQLiteAdapter) GetSQLDB() *sql.DB {
return a.db
}
// GetManager returns the underlying RQLite manager for advanced operations
func (a *RQLiteAdapter) GetManager() *RQLiteManager {
return a.manager
}
// Close closes the adapter connections
func (a *RQLiteAdapter) Close() error {
if a.db != nil {
a.db.Close()
}
return a.manager.Stop()
}

170
pkg/database/rqlite.go Normal file
View File

@ -0,0 +1,170 @@
package database
import (
"context"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/rqlite/gorqlite"
"go.uber.org/zap"
"network/pkg/config"
)
// RQLiteManager manages an RQLite node instance
type RQLiteManager struct {
config *config.DatabaseConfig
dataDir string
logger *zap.Logger
cmd *exec.Cmd
connection *gorqlite.Connection
}
// NewRQLiteManager creates a new RQLite manager
func NewRQLiteManager(cfg *config.DatabaseConfig, dataDir string, logger *zap.Logger) *RQLiteManager {
return &RQLiteManager{
config: cfg,
dataDir: dataDir,
logger: logger,
}
}
// Start starts the RQLite node
func (r *RQLiteManager) Start(ctx context.Context) error {
// Create data directory
rqliteDataDir := filepath.Join(r.dataDir, "rqlite")
if err := os.MkdirAll(rqliteDataDir, 0755); err != nil {
return fmt.Errorf("failed to create RQLite data directory: %w", err)
}
// Build RQLite command
args := []string{
"-http-addr", fmt.Sprintf("localhost:%d", r.config.RQLitePort),
"-raft-addr", fmt.Sprintf("localhost:%d", r.config.RQLiteRaftPort),
}
// Add join address if specified (for non-bootstrap nodes)
if r.config.RQLiteJoinAddress != "" {
args = append(args, "-join", r.config.RQLiteJoinAddress)
}
// Add data directory as positional argument
args = append(args, rqliteDataDir)
r.logger.Info("Starting RQLite node",
zap.String("data_dir", rqliteDataDir),
zap.Int("http_port", r.config.RQLitePort),
zap.Int("raft_port", r.config.RQLiteRaftPort),
zap.String("join_address", r.config.RQLiteJoinAddress),
)
// Start RQLite process
r.cmd = exec.CommandContext(ctx, "rqlited", args...)
r.cmd.Stdout = os.Stdout
r.cmd.Stderr = os.Stderr
if err := r.cmd.Start(); err != nil {
return fmt.Errorf("failed to start RQLite: %w", err)
}
// Wait for RQLite to be ready
if err := r.waitForReady(ctx); err != nil {
r.cmd.Process.Kill()
return fmt.Errorf("RQLite failed to become ready: %w", err)
}
// Create connection
conn, err := gorqlite.Open(fmt.Sprintf("http://localhost:%d", r.config.RQLitePort))
if err != nil {
r.cmd.Process.Kill()
return fmt.Errorf("failed to connect to RQLite: %w", err)
}
r.connection = conn
// Wait for RQLite to establish leadership (for bootstrap nodes)
if r.config.RQLiteJoinAddress == "" {
if err := r.waitForLeadership(ctx); err != nil {
r.cmd.Process.Kill()
return fmt.Errorf("RQLite failed to establish leadership: %w", err)
}
}
r.logger.Info("RQLite node started successfully")
return nil
}
// waitForReady waits for RQLite to be ready to accept connections
func (r *RQLiteManager) waitForReady(ctx context.Context) error {
url := fmt.Sprintf("http://localhost:%d/status", r.config.RQLitePort)
client := &http.Client{Timeout: 2 * time.Second}
for i := 0; i < 30; i++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
resp, err := client.Get(url)
if err == nil {
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return nil
}
}
time.Sleep(1 * time.Second)
}
return fmt.Errorf("RQLite did not become ready within timeout")
}
// waitForLeadership waits for RQLite to establish leadership (for bootstrap nodes)
func (r *RQLiteManager) waitForLeadership(ctx context.Context) error {
r.logger.Info("Waiting for RQLite to establish leadership...")
for i := 0; i < 30; i++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Try a simple query to check if leadership is established
if r.connection != nil {
_, err := r.connection.QueryOne("SELECT 1")
if err == nil {
r.logger.Info("RQLite leadership established")
return nil
}
r.logger.Debug("Waiting for leadership", zap.Error(err))
}
time.Sleep(1 * time.Second)
}
return fmt.Errorf("RQLite failed to establish leadership within timeout")
}
// GetConnection returns the RQLite connection
func (r *RQLiteManager) GetConnection() *gorqlite.Connection {
return r.connection
}
// Stop stops the RQLite node
func (r *RQLiteManager) Stop() error {
if r.connection != nil {
r.connection.Close()
}
if r.cmd != nil && r.cmd.Process != nil {
r.logger.Info("Stopping RQLite node")
return r.cmd.Process.Kill()
}
return nil
}

177
pkg/discovery/discovery.go Normal file
View File

@ -0,0 +1,177 @@
package discovery
import (
"context"
"errors"
"time"
dht "github.com/libp2p/go-libp2p-kad-dht"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"go.uber.org/zap"
)
// Manager handles peer discovery operations
type Manager struct {
host host.Host
dht *dht.IpfsDHT
logger *zap.Logger
cancel context.CancelFunc
}
// Config contains discovery configuration
type Config struct {
DiscoveryInterval time.Duration
MaxConnections int
}
// NewManager creates a new discovery manager
func NewManager(host host.Host, dht *dht.IpfsDHT, logger *zap.Logger) *Manager {
return &Manager{
host: host,
dht: dht,
logger: logger,
}
}
// Start begins periodic peer discovery
func (d *Manager) Start(config Config) error {
ctx, cancel := context.WithCancel(context.Background())
d.cancel = cancel
go func() {
// Do initial discovery immediately
d.discoverPeers(ctx, config)
// Continue with periodic discovery
ticker := time.NewTicker(config.DiscoveryInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
d.discoverPeers(ctx, config)
}
}
}()
return nil
}
// Stop stops peer discovery
func (d *Manager) Stop() {
if d.cancel != nil {
d.cancel()
}
}
// discoverPeers discovers and connects to new peers
func (d *Manager) discoverPeers(ctx context.Context, config Config) {
connectedPeers := d.host.Network().Peers()
initialCount := len(connectedPeers)
d.logger.Debug("Starting peer discovery",
zap.Int("current_peers", initialCount))
// Strategy 1: Use DHT to find peers
newConnections := d.discoverViaDHT(ctx, config.MaxConnections)
// Strategy 2: Ask connected peers about their connections
newConnections += d.discoverViaPeerExchange(ctx, config.MaxConnections)
finalPeerCount := len(d.host.Network().Peers())
if newConnections > 0 || finalPeerCount != initialCount {
d.logger.Debug("Peer discovery completed",
zap.Int("new_connections", newConnections),
zap.Int("initial_peers", initialCount),
zap.Int("final_peers", finalPeerCount))
}
}
// discoverViaDHT uses the DHT to find random peers
func (d *Manager) discoverViaDHT(ctx context.Context, maxConnections int) int {
if d.dht == nil {
return 0
}
connected := 0
// Get peers from routing table
routingTablePeers := d.dht.RoutingTable().ListPeers()
d.logger.Debug("DHT routing table has peers", zap.Int("count", len(routingTablePeers)))
for _, peerID := range routingTablePeers {
if peerID == d.host.ID() {
continue
}
if connected >= maxConnections {
break
}
// Check if we're already connected
if d.host.Network().Connectedness(peerID) != network.NotConnected {
continue
}
// Try to connect
if err := d.connectToPeer(ctx, peerID); err == nil {
connected++
}
}
return connected
}
// discoverViaPeerExchange asks connected peers about their connections
func (d *Manager) discoverViaPeerExchange(ctx context.Context, maxConnections int) int {
connected := 0
connectedPeers := d.host.Network().Peers()
for _, peerID := range connectedPeers {
if connected >= maxConnections {
break
}
// Get peer connections (this is a simplified implementation)
// In a real implementation, you might use a custom protocol
peerInfo := d.host.Peerstore().PeerInfo(peerID)
for _, addr := range peerInfo.Addrs {
if connected >= maxConnections {
break
}
// Extract peer ID from multiaddr and try to connect
// This is simplified - in practice you'd need proper multiaddr parsing
_ = addr // Placeholder for actual implementation
}
}
return connected
}
// connectToPeer attempts to connect to a specific peer
func (d *Manager) connectToPeer(ctx context.Context, peerID peer.ID) error {
// Get peer info from DHT
peerInfo := d.host.Peerstore().PeerInfo(peerID)
if len(peerInfo.Addrs) == 0 {
return errors.New("no addresses for peer")
}
// Attempt connection
if err := d.host.Connect(ctx, peerInfo); err != nil {
d.logger.Debug("Failed to connect to DHT peer",
zap.String("peer_id", peerID.String()[:8]+"..."),
zap.Error(err))
return err
}
d.logger.Debug("Successfully connected to DHT peer",
zap.String("peer_id", peerID.String()[:8]+"..."))
return nil
}

280
pkg/logging/logger.go Normal file
View File

@ -0,0 +1,280 @@
package logging
import (
"fmt"
"os"
"strings"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// ANSI color codes
const (
Reset = "\033[0m"
Bold = "\033[1m"
Dim = "\033[2m"
// Standard colors
Red = "\033[31m"
Green = "\033[32m"
Yellow = "\033[33m"
Blue = "\033[34m"
Magenta = "\033[35m"
Cyan = "\033[36m"
White = "\033[37m"
Gray = "\033[90m"
// Bright colors
BrightRed = "\033[91m"
BrightGreen = "\033[92m"
BrightYellow = "\033[93m"
BrightBlue = "\033[94m"
BrightMagenta = "\033[95m"
BrightCyan = "\033[96m"
BrightWhite = "\033[97m"
)
// ColoredLogger wraps zap.Logger with colored output
type ColoredLogger struct {
*zap.Logger
enableColors bool
}
// Component represents different parts of the system for color coding
type Component string
const (
ComponentBootstrap Component = "BOOTSTRAP"
ComponentNode Component = "NODE"
ComponentRQLite Component = "RQLITE"
ComponentLibP2P Component = "LIBP2P"
ComponentStorage Component = "STORAGE"
ComponentDatabase Component = "DATABASE"
ComponentClient Component = "CLIENT"
ComponentDHT Component = "DHT"
ComponentGeneral Component = "GENERAL"
)
// getComponentColor returns the color for a specific component
func getComponentColor(component Component) string {
switch component {
case ComponentBootstrap:
return BrightGreen
case ComponentNode:
return BrightBlue
case ComponentRQLite:
return BrightMagenta
case ComponentLibP2P:
return BrightCyan
case ComponentStorage:
return BrightYellow
case ComponentDatabase:
return Green
case ComponentClient:
return Blue
case ComponentDHT:
return Cyan
default:
return White
}
}
// getLevelColor returns the color for a log level
func getLevelColor(level zapcore.Level) string {
switch level {
case zapcore.DebugLevel:
return Gray
case zapcore.InfoLevel:
return BrightWhite
case zapcore.WarnLevel:
return BrightYellow
case zapcore.ErrorLevel:
return BrightRed
case zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel:
return Red
default:
return White
}
}
// coloredConsoleEncoder creates a custom encoder with colors
func coloredConsoleEncoder(enableColors bool) zapcore.Encoder {
config := zap.NewDevelopmentEncoderConfig()
config.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
timeStr := t.Format("2006-01-02T15:04:05.000Z0700")
if enableColors {
enc.AppendString(fmt.Sprintf("%s%s%s", Dim, timeStr, Reset))
} else {
enc.AppendString(timeStr)
}
}
config.EncodeLevel = func(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
levelStr := strings.ToUpper(level.String())
if enableColors {
color := getLevelColor(level)
enc.AppendString(fmt.Sprintf("%s%s%-5s%s", color, Bold, levelStr, Reset))
} else {
enc.AppendString(fmt.Sprintf("%-5s", levelStr))
}
}
config.EncodeCaller = func(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) {
if enableColors {
enc.AppendString(fmt.Sprintf("%s%s%s", Dim, caller.TrimmedPath(), Reset))
} else {
enc.AppendString(caller.TrimmedPath())
}
}
return zapcore.NewConsoleEncoder(config)
}
// NewColoredLogger creates a new colored logger
func NewColoredLogger(component Component, enableColors bool) (*ColoredLogger, error) {
// Auto-detect color support if not explicitly disabled
if enableColors {
enableColors = supportsColor()
}
// Create encoder
encoder := coloredConsoleEncoder(enableColors)
// Create core
core := zapcore.NewCore(
encoder,
zapcore.AddSync(os.Stdout),
zapcore.DebugLevel,
)
// Create logger with caller information
logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
return &ColoredLogger{
Logger: logger,
enableColors: enableColors,
}, nil
}
// NewDefaultLogger creates a logger with default settings and color auto-detection
func NewDefaultLogger(component Component) (*ColoredLogger, error) {
return NewColoredLogger(component, true)
}
// Component-specific logging methods
func (l *ColoredLogger) ComponentInfo(component Component, msg string, fields ...zap.Field) {
if l.enableColors {
color := getComponentColor(component)
msg = fmt.Sprintf("%s[%s]%s %s", color, component, Reset, msg)
} else {
msg = fmt.Sprintf("[%s] %s", component, msg)
}
l.Info(msg, fields...)
}
func (l *ColoredLogger) ComponentWarn(component Component, msg string, fields ...zap.Field) {
if l.enableColors {
color := getComponentColor(component)
msg = fmt.Sprintf("%s[%s]%s %s", color, component, Reset, msg)
} else {
msg = fmt.Sprintf("[%s] %s", component, msg)
}
l.Warn(msg, fields...)
}
func (l *ColoredLogger) ComponentError(component Component, msg string, fields ...zap.Field) {
if l.enableColors {
color := getComponentColor(component)
msg = fmt.Sprintf("%s[%s]%s %s", color, component, Reset, msg)
} else {
msg = fmt.Sprintf("[%s] %s", component, msg)
}
l.Error(msg, fields...)
}
func (l *ColoredLogger) ComponentDebug(component Component, msg string, fields ...zap.Field) {
if l.enableColors {
color := getComponentColor(component)
msg = fmt.Sprintf("%s[%s]%s %s", color, component, Reset, msg)
} else {
msg = fmt.Sprintf("[%s] %s", component, msg)
}
l.Debug(msg, fields...)
}
// supportsColor detects if the terminal supports color
func supportsColor() bool {
// Check environment variables
term := os.Getenv("TERM")
colorTerm := os.Getenv("COLORTERM")
// Common indicators of color support
if colorTerm != "" {
return true
}
if term != "" {
colorTerms := []string{
"xterm", "xterm-color", "xterm-256color",
"screen", "screen-256color",
"tmux", "tmux-256color",
"ansi", "color",
}
for _, ct := range colorTerms {
if strings.Contains(term, ct) {
return true
}
}
}
// Check if we're not in a pipe/redirect
if fileInfo, _ := os.Stdout.Stat(); fileInfo != nil {
return (fileInfo.Mode() & os.ModeCharDevice) == os.ModeCharDevice
}
return false
}
// StandardLogger provides colored standard library compatible logging
type StandardLogger struct {
logger *ColoredLogger
component Component
}
// NewStandardLogger creates a standard library compatible colored logger
func NewStandardLogger(component Component) (*StandardLogger, error) {
coloredLogger, err := NewDefaultLogger(component)
if err != nil {
return nil, err
}
return &StandardLogger{
logger: coloredLogger,
component: component,
}, nil
}
// Printf implements the standard library log interface with colors
func (s *StandardLogger) Printf(format string, v ...interface{}) {
msg := fmt.Sprintf(format, v...)
// Remove trailing newline if present (zap adds its own)
msg = strings.TrimSuffix(msg, "\n")
s.logger.ComponentInfo(s.component, msg)
}
// Print implements the standard library log interface with colors
func (s *StandardLogger) Print(v ...interface{}) {
msg := fmt.Sprint(v...)
msg = strings.TrimSuffix(msg, "\n")
s.logger.ComponentInfo(s.component, msg)
}
// Println implements the standard library log interface with colors
func (s *StandardLogger) Println(v ...interface{}) {
msg := fmt.Sprintln(v...)
msg = strings.TrimSuffix(msg, "\n")
s.logger.ComponentInfo(s.component, msg)
}

635
pkg/node/node.go Normal file
View File

@ -0,0 +1,635 @@
package node
import (
"context"
"crypto/rand"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/libp2p/go-libp2p"
dht "github.com/libp2p/go-libp2p-kad-dht"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
noise "github.com/libp2p/go-libp2p/p2p/security/noise"
libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic"
"github.com/libp2p/go-libp2p/p2p/transport/tcp"
"github.com/multiformats/go-multiaddr"
"go.uber.org/zap"
"network/pkg/config"
"network/pkg/database"
"network/pkg/logging"
"network/pkg/storage"
)
// Node represents a network node with RQLite database
type Node struct {
config *config.Config
logger *logging.ColoredLogger
host host.Host
dht *dht.IpfsDHT
rqliteManager *database.RQLiteManager
rqliteAdapter *database.RQLiteAdapter
storageService *storage.Service
// Peer discovery
discoveryCancel context.CancelFunc
}
// NewNode creates a new network node
func NewNode(cfg *config.Config) (*Node, error) {
// Create colored logger
logger, err := logging.NewDefaultLogger(logging.ComponentNode)
if err != nil {
return nil, fmt.Errorf("failed to create logger: %w", err)
}
return &Node{
config: cfg,
logger: logger,
}, nil
}
// Start starts the network node
func (n *Node) Start(ctx context.Context) error {
n.logger.ComponentInfo(logging.ComponentNode, "Starting network node",
zap.String("data_dir", n.config.Node.DataDir),
zap.String("type", "bootstrap"),
)
// Create data directory
if err := os.MkdirAll(n.config.Node.DataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory: %w", err)
}
// Start RQLite
if err := n.startRQLite(ctx); err != nil {
return fmt.Errorf("failed to start RQLite: %w", err)
}
// Start LibP2P host
if err := n.startLibP2P(); err != nil {
return fmt.Errorf("failed to start LibP2P: %w", err)
}
// Start storage service
if err := n.startStorageService(); err != nil {
return fmt.Errorf("failed to start storage service: %w", err)
}
// Get listen addresses for logging
var listenAddrs []string
for _, addr := range n.host.Addrs() {
listenAddrs = append(listenAddrs, addr.String())
}
n.logger.ComponentInfo(logging.ComponentNode, "Network node started successfully",
zap.String("peer_id", n.host.ID().String()),
zap.Strings("listen_addrs", listenAddrs),
)
return nil
}
// startRQLite initializes and starts the RQLite database
func (n *Node) startRQLite(ctx context.Context) error {
n.logger.ComponentInfo(logging.ComponentDatabase, "Starting RQLite database")
// Create RQLite manager
n.rqliteManager = database.NewRQLiteManager(&n.config.Database, n.config.Node.DataDir, n.logger.Logger)
// Start RQLite
if err := n.rqliteManager.Start(ctx); err != nil {
return err
}
// Create adapter for sql.DB compatibility
adapter, err := database.NewRQLiteAdapter(n.rqliteManager)
if err != nil {
return fmt.Errorf("failed to create RQLite adapter: %w", err)
}
n.rqliteAdapter = adapter
return nil
}
// startLibP2P initializes the LibP2P host
func (n *Node) startLibP2P() error {
n.logger.ComponentInfo(logging.ComponentLibP2P, "Starting LibP2P host")
// Get listen addresses
listenAddrs, err := n.config.ParseMultiaddrs()
if err != nil {
return fmt.Errorf("failed to parse listen addresses: %w", err)
}
// Load or create persistent identity
identity, err := n.loadOrCreateIdentity()
if err != nil {
return fmt.Errorf("failed to load identity: %w", err)
}
// Create LibP2P host with persistent identity
h, err := libp2p.New(
libp2p.Identity(identity),
libp2p.ListenAddrs(listenAddrs...),
libp2p.Security(noise.ID, noise.New),
libp2p.Transport(tcp.NewTCPTransport),
libp2p.Transport(libp2pquic.NewTransport),
libp2p.DefaultMuxers,
)
if err != nil {
return err
}
n.host = h
// Create DHT for peer discovery - Use server mode for better peer discovery
kademliaDHT, err := dht.New(context.Background(), h, dht.Mode(dht.ModeServer))
if err != nil {
return fmt.Errorf("failed to create DHT: %w", err)
}
n.dht = kademliaDHT
// Connect to LibP2P bootstrap peers if configured
if err := n.connectToBootstrapPeers(); err != nil {
n.logger.Warn("Failed to connect to bootstrap peers", zap.Error(err))
// Don't fail - continue without bootstrap connections
}
// Add bootstrap peers to DHT routing table BEFORE bootstrapping
if len(n.config.Discovery.BootstrapPeers) > 0 {
n.logger.Info("Adding bootstrap peers to DHT routing table")
for _, bootstrapAddr := range n.config.Discovery.BootstrapPeers {
if ma, err := multiaddr.NewMultiaddr(bootstrapAddr); err == nil {
if peerInfo, err := peer.AddrInfoFromP2pAddr(ma); err == nil {
// Add to peerstore with longer TTL
n.host.Peerstore().AddAddrs(peerInfo.ID, peerInfo.Addrs, time.Hour*24)
// Force add to DHT routing table
added, err := n.dht.RoutingTable().TryAddPeer(peerInfo.ID, true, true)
if err != nil {
n.logger.Debug("Failed to add bootstrap peer to DHT routing table",
zap.String("peer", peerInfo.ID.String()),
zap.Error(err))
} else if added {
n.logger.Info("Successfully added bootstrap peer to DHT routing table",
zap.String("peer", peerInfo.ID.String()))
}
}
}
}
}
// Bootstrap the DHT AFTER connecting to bootstrap peers and adding them to routing table
if err = kademliaDHT.Bootstrap(context.Background()); err != nil {
n.logger.Warn("Failed to bootstrap DHT", zap.Error(err))
// Don't fail - continue without DHT
} else {
n.logger.ComponentInfo(logging.ComponentDHT, "DHT bootstrap initiated successfully")
}
// Give DHT a moment to initialize, then add connected peers to routing table
go func() {
time.Sleep(2 * time.Second)
connectedPeers := n.host.Network().Peers()
for _, peerID := range connectedPeers {
if peerID != n.host.ID() {
addrs := n.host.Peerstore().Addrs(peerID)
if len(addrs) > 0 {
n.host.Peerstore().AddAddrs(peerID, addrs, time.Hour*24)
n.logger.Info("Added connected peer to DHT peerstore",
zap.String("peer", peerID.String()))
// Try to add this peer to DHT routing table explicitly
if n.dht != nil {
added, err := n.dht.RoutingTable().TryAddPeer(peerID, true, true)
if err != nil {
n.logger.Debug("Failed to add peer to DHT routing table",
zap.String("peer", peerID.String()),
zap.Error(err))
} else if added {
n.logger.Info("Successfully added peer to DHT routing table",
zap.String("peer", peerID.String()))
} else {
n.logger.Debug("Peer already in DHT routing table or rejected",
zap.String("peer", peerID.String()))
}
}
}
}
}
// Force multiple DHT refresh attempts to populate routing table
if n.dht != nil {
n.logger.Info("Forcing DHT refresh to discover peers")
for i := 0; i < 3; i++ {
time.Sleep(1 * time.Second)
n.dht.RefreshRoutingTable()
// Check if routing table is populated
routingPeers := n.dht.RoutingTable().ListPeers()
n.logger.Info("DHT routing table status after refresh",
zap.Int("attempt", i+1),
zap.Int("peers_in_table", len(routingPeers)))
if len(routingPeers) > 0 {
break // Success!
}
}
}
}()
// Start peer discovery and monitoring
n.startPeerDiscovery()
n.startConnectionMonitoring()
n.logger.ComponentInfo(logging.ComponentLibP2P, "LibP2P host started with DHT enabled",
zap.String("peer_id", h.ID().String()))
return nil
}
// connectToBootstrapPeers connects to configured LibP2P bootstrap peers
func (n *Node) connectToBootstrapPeers() error {
if len(n.config.Discovery.BootstrapPeers) == 0 {
n.logger.ComponentDebug(logging.ComponentDHT, "No bootstrap peers configured")
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
for _, bootstrapAddr := range n.config.Discovery.BootstrapPeers {
if err := n.connectToBootstrapPeer(ctx, bootstrapAddr); err != nil {
n.logger.Warn("Failed to connect to bootstrap peer",
zap.String("addr", bootstrapAddr),
zap.Error(err))
continue
}
}
return nil
}
// connectToBootstrapPeer connects to a single bootstrap peer
func (n *Node) connectToBootstrapPeer(ctx context.Context, addr string) error {
ma, err := multiaddr.NewMultiaddr(addr)
if err != nil {
return fmt.Errorf("invalid multiaddr: %w", err)
}
// Extract peer info from multiaddr
peerInfo, err := peer.AddrInfoFromP2pAddr(ma)
if err != nil {
return fmt.Errorf("failed to extract peer info: %w", err)
}
// Connect to the peer
if err := n.host.Connect(ctx, *peerInfo); err != nil {
return fmt.Errorf("failed to connect to peer: %w", err)
}
n.logger.Info("Connected to bootstrap peer",
zap.String("peer", peerInfo.ID.String()),
zap.String("addr", addr))
return nil
}
// loadOrCreateIdentity loads an existing identity or creates a new one
func (n *Node) loadOrCreateIdentity() (crypto.PrivKey, error) {
identityFile := filepath.Join(n.config.Node.DataDir, "identity.key")
// Try to load existing identity
if _, err := os.Stat(identityFile); err == nil {
data, err := os.ReadFile(identityFile)
if err != nil {
return nil, fmt.Errorf("failed to read identity file: %w", err)
}
priv, err := crypto.UnmarshalPrivateKey(data)
if err != nil {
n.logger.Warn("Failed to unmarshal existing identity, creating new one", zap.Error(err))
} else {
n.logger.ComponentInfo(logging.ComponentNode, "Loaded existing identity", zap.String("file", identityFile))
return priv, nil
}
}
// Create new identity
n.logger.Info("Creating new identity", zap.String("file", identityFile))
priv, _, err := crypto.GenerateKeyPairWithReader(crypto.Ed25519, 2048, rand.Reader)
if err != nil {
return nil, fmt.Errorf("failed to generate key pair: %w", err)
}
// Save identity
data, err := crypto.MarshalPrivateKey(priv)
if err != nil {
return nil, fmt.Errorf("failed to marshal private key: %w", err)
}
if err := os.WriteFile(identityFile, data, 0600); err != nil {
return nil, fmt.Errorf("failed to save identity: %w", err)
}
n.logger.Info("Identity saved", zap.String("file", identityFile))
return priv, nil
}
// startStorageService initializes the storage service
func (n *Node) startStorageService() error {
n.logger.ComponentInfo(logging.ComponentStorage, "Starting storage service")
// Create storage service using the RQLite SQL adapter
service, err := storage.NewService(n.rqliteAdapter.GetSQLDB(), n.logger.Logger)
if err != nil {
return err
}
n.storageService = service
// Set up stream handler for storage protocol
n.host.SetStreamHandler("/network/storage/1.0.0", n.storageService.HandleStorageStream)
return nil
}
// getListenAddresses returns the current listen addresses as strings
// Stop stops the node and all its services
func (n *Node) Stop() error {
n.logger.ComponentInfo(logging.ComponentNode, "Stopping network node")
// Stop peer discovery
n.stopPeerDiscovery()
// Stop storage service
if n.storageService != nil {
n.storageService.Close()
}
// Stop DHT
if n.dht != nil {
n.dht.Close()
}
// Stop LibP2P host
if n.host != nil {
n.host.Close()
}
// Stop RQLite
if n.rqliteAdapter != nil {
n.rqliteAdapter.Close()
}
n.logger.ComponentInfo(logging.ComponentNode, "Network node stopped")
return nil
}
// GetPeerID returns the peer ID of this node
func (n *Node) GetPeerID() string {
if n.host == nil {
return ""
}
return n.host.ID().String()
}
// startPeerDiscovery starts periodic peer discovery for the node
func (n *Node) startPeerDiscovery() {
// Create a cancellation context for discovery
ctx, cancel := context.WithCancel(context.Background())
n.discoveryCancel = cancel
// Start discovery in a goroutine
go func() {
// Do initial discovery immediately (no delay for faster discovery)
n.discoverPeers(ctx)
// Start with frequent discovery for the first minute
rapidTicker := time.NewTicker(10 * time.Second)
rapidAttempts := 0
maxRapidAttempts := 6 // 6 attempts * 10 seconds = 1 minute
for {
select {
case <-ctx.Done():
rapidTicker.Stop()
return
case <-rapidTicker.C:
n.discoverPeers(ctx)
rapidAttempts++
// After rapid attempts, switch to slower periodic discovery
if rapidAttempts >= maxRapidAttempts {
rapidTicker.Stop()
// Continue with slower periodic discovery every 15 seconds
slowTicker := time.NewTicker(15 * time.Second)
defer slowTicker.Stop()
for {
select {
case <-ctx.Done():
return
case <-slowTicker.C:
n.discoverPeers(ctx)
}
}
}
}
}
}()
}
// discoverPeers discovers and connects to new peers
func (n *Node) discoverPeers(ctx context.Context) {
if n.host == nil || n.dht == nil {
return
}
connectedPeers := n.host.Network().Peers()
initialCount := len(connectedPeers)
n.logger.Debug("Node peer discovery",
zap.Int("current_peers", initialCount))
// Strategy 1: Use DHT to find new peers
newConnections := n.discoverViaDHT(ctx)
// Strategy 2: Search for random peers using DHT FindPeer
finalPeerCount := len(n.host.Network().Peers())
if newConnections > 0 || finalPeerCount != initialCount {
n.logger.Debug("Node peer discovery completed",
zap.Int("new_connections", newConnections),
zap.Int("initial_peers", initialCount),
zap.Int("final_peers", finalPeerCount))
}
}
// discoverViaDHT uses the DHT to find and connect to new peers
func (n *Node) discoverViaDHT(ctx context.Context) int {
if n.dht == nil {
return 0
}
connected := 0
maxConnections := 5
// Get peers from routing table
routingTablePeers := n.dht.RoutingTable().ListPeers()
n.logger.ComponentDebug(logging.ComponentDHT, "Node DHT routing table has peers", zap.Int("count", len(routingTablePeers)))
// Strategy 1: Connect to peers in DHT routing table
for _, peerID := range routingTablePeers {
if peerID == n.host.ID() {
continue
}
// Check if already connected
if n.host.Network().Connectedness(peerID) == 1 {
continue
}
// Get addresses for this peer
addrs := n.host.Peerstore().Addrs(peerID)
if len(addrs) == 0 {
continue
}
// Try to connect
connectCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
peerInfo := peer.AddrInfo{ID: peerID, Addrs: addrs}
if err := n.host.Connect(connectCtx, peerInfo); err != nil {
cancel()
n.logger.Debug("Failed to connect to DHT peer",
zap.String("peer", peerID.String()),
zap.Error(err))
continue
}
cancel()
n.logger.Debug("Node connected to new peer via DHT",
zap.String("peer", peerID.String()))
connected++
if connected >= maxConnections {
break
}
}
// Strategy 2: Use peer exchange - check what peers our connected peers know about
connectedPeers := n.host.Network().Peers()
for _, connectedPeer := range connectedPeers {
if connectedPeer == n.host.ID() {
continue
}
// Get all peers from peerstore (this includes peers that connected peers might know about)
allKnownPeers := n.host.Peerstore().Peers()
for _, knownPeer := range allKnownPeers {
if knownPeer == n.host.ID() || knownPeer == connectedPeer {
continue
}
// Skip if already connected
if n.host.Network().Connectedness(knownPeer) == 1 {
continue
}
// Get addresses for this peer
addrs := n.host.Peerstore().Addrs(knownPeer)
if len(addrs) == 0 {
continue
}
// Filter addresses to only include listening ports (not ephemeral client ports)
var validAddrs []multiaddr.Multiaddr
for _, addr := range addrs {
addrStr := addr.String()
// Skip ephemeral ports (typically above 49152) and keep standard ports
if !strings.Contains(addrStr, ":53") && // Skip ephemeral ports starting with 53
!strings.Contains(addrStr, ":54") && // Skip ephemeral ports starting with 54
!strings.Contains(addrStr, ":55") && // Skip ephemeral ports starting with 55
!strings.Contains(addrStr, ":56") && // Skip ephemeral ports starting with 56
!strings.Contains(addrStr, ":57") && // Skip ephemeral ports starting with 57
!strings.Contains(addrStr, ":58") && // Skip ephemeral ports starting with 58
!strings.Contains(addrStr, ":59") && // Skip ephemeral ports starting with 59
!strings.Contains(addrStr, ":6") && // Skip ephemeral ports starting with 6
(strings.Contains(addrStr, ":400") || // Include 4000-4999 range
strings.Contains(addrStr, ":401") ||
strings.Contains(addrStr, ":402") ||
strings.Contains(addrStr, ":403")) {
validAddrs = append(validAddrs, addr)
}
}
if len(validAddrs) == 0 {
continue
}
// Try to connect using only valid addresses
connectCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
peerInfo := peer.AddrInfo{ID: knownPeer, Addrs: validAddrs}
if err := n.host.Connect(connectCtx, peerInfo); err != nil {
cancel()
n.logger.Debug("Failed to connect to peerstore peer",
zap.String("peer", knownPeer.String()),
zap.Error(err))
continue
}
cancel()
n.logger.Debug("Node connected to new peer via peerstore",
zap.String("peer", knownPeer.String()))
connected++
if connected >= maxConnections {
return connected
}
}
}
return connected
}
// startConnectionMonitoring monitors connection health and logs status
func (n *Node) startConnectionMonitoring() {
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if n.host == nil {
return
}
connectedPeers := n.host.Network().Peers()
if len(connectedPeers) == 0 {
n.logger.Debug("Node has no connected peers - seeking connections",
zap.String("node_id", n.host.ID().String()))
}
}
}
}()
}
// stopPeerDiscovery stops peer discovery
func (n *Node) stopPeerDiscovery() {
if n.discoveryCancel != nil {
n.discoveryCancel()
n.discoveryCancel = nil
}
}

44
pkg/pubsub/adapter.go Normal file
View File

@ -0,0 +1,44 @@
package pubsub
import (
"context"
pubsub "github.com/libp2p/go-libp2p-pubsub"
)
// ClientAdapter adapts the pubsub Manager to work with the existing client interface
type ClientAdapter struct {
manager *Manager
}
// NewClientAdapter creates a new adapter for the pubsub manager
func NewClientAdapter(ps *pubsub.PubSub, namespace string) *ClientAdapter {
return &ClientAdapter{
manager: NewManager(ps, namespace),
}
}
// Subscribe subscribes to a topic
func (a *ClientAdapter) Subscribe(ctx context.Context, topic string, handler MessageHandler) error {
return a.manager.Subscribe(ctx, topic, handler)
}
// Publish publishes a message to a topic
func (a *ClientAdapter) Publish(ctx context.Context, topic string, data []byte) error {
return a.manager.Publish(ctx, topic, data)
}
// Unsubscribe unsubscribes from a topic
func (a *ClientAdapter) Unsubscribe(ctx context.Context, topic string) error {
return a.manager.Unsubscribe(ctx, topic)
}
// ListTopics returns all subscribed topics
func (a *ClientAdapter) ListTopics(ctx context.Context) ([]string, error) {
return a.manager.ListTopics(ctx)
}
// Close closes all subscriptions and topics
func (a *ClientAdapter) Close() error {
return a.manager.Close()
}

332
pkg/pubsub/manager.go Normal file
View File

@ -0,0 +1,332 @@
package pubsub
import (
"context"
"fmt"
"sync"
"time"
pubsub "github.com/libp2p/go-libp2p-pubsub"
)
// Manager handles pub/sub operations
type Manager struct {
pubsub *pubsub.PubSub
topics map[string]*pubsub.Topic
subscriptions map[string]*subscription
namespace string
mu sync.RWMutex
}
// subscription holds subscription data
type subscription struct {
sub *pubsub.Subscription
cancel context.CancelFunc
}
// NewManager creates a new pubsub manager
func NewManager(ps *pubsub.PubSub, namespace string) *Manager {
return &Manager{
pubsub: ps,
topics: make(map[string]*pubsub.Topic),
subscriptions: make(map[string]*subscription),
namespace: namespace,
}
}
// getOrCreateTopic gets an existing topic or creates a new one
func (m *Manager) getOrCreateTopic(topicName string) (*pubsub.Topic, error) {
m.mu.Lock()
defer m.mu.Unlock()
// Return existing topic if available
if topic, exists := m.topics[topicName]; exists {
return topic, nil
}
// Join the topic - LibP2P allows multiple clients to join the same topic
topic, err := m.pubsub.Join(topicName)
if err != nil {
return nil, fmt.Errorf("failed to join topic: %w", err)
}
m.topics[topicName] = topic
return topic, nil
}
// Subscribe subscribes to a topic
func (m *Manager) Subscribe(ctx context.Context, topic string, handler MessageHandler) error {
if m.pubsub == nil {
return fmt.Errorf("pubsub not initialized")
}
namespacedTopic := fmt.Sprintf("%s.%s", m.namespace, topic)
// Check if already subscribed
m.mu.Lock()
if _, exists := m.subscriptions[namespacedTopic]; exists {
m.mu.Unlock()
// Already subscribed - this is normal for LibP2P pubsub
return nil
}
m.mu.Unlock()
// Get or create topic
libp2pTopic, err := m.getOrCreateTopic(namespacedTopic)
if err != nil {
return fmt.Errorf("failed to get topic: %w", err)
}
// Subscribe to topic
sub, err := libp2pTopic.Subscribe()
if err != nil {
return fmt.Errorf("failed to subscribe to topic: %w", err)
}
// Create cancellable context for this subscription
subCtx, cancel := context.WithCancel(context.Background())
// Store subscription
m.mu.Lock()
m.subscriptions[namespacedTopic] = &subscription{
sub: sub,
cancel: cancel,
}
m.mu.Unlock()
// Start message handler goroutine
go func() {
defer func() {
sub.Cancel()
}()
for {
select {
case <-subCtx.Done():
return
default:
msg, err := sub.Next(subCtx)
if err != nil {
if subCtx.Err() != nil {
return // Context cancelled
}
continue
}
// Call the handler
if err := handler(topic, msg.Data); err != nil {
// Log error but continue processing
continue
}
}
}
}()
// Force peer discovery for this topic
go m.announceTopicInterest(namespacedTopic)
// For Anchat, also try to actively find topic peers through the libp2p pubsub system
if len(m.namespace) > 6 && m.namespace[:6] == "anchat" {
go m.enhancedAnchatTopicDiscovery(namespacedTopic, libp2pTopic)
}
return nil
}
// Publish publishes a message to a topic
func (m *Manager) Publish(ctx context.Context, topic string, data []byte) error {
if m.pubsub == nil {
return fmt.Errorf("pubsub not initialized")
}
namespacedTopic := fmt.Sprintf("%s.%s", m.namespace, topic)
// Get or create topic
libp2pTopic, err := m.getOrCreateTopic(namespacedTopic)
if err != nil {
return fmt.Errorf("failed to get topic for publishing: %w", err)
}
// Publish message
if err := libp2pTopic.Publish(ctx, data); err != nil {
return fmt.Errorf("failed to publish message: %w", err)
}
return nil
}
// Unsubscribe unsubscribes from a topic
func (m *Manager) Unsubscribe(ctx context.Context, topic string) error {
m.mu.Lock()
defer m.mu.Unlock()
namespacedTopic := fmt.Sprintf("%s.%s", m.namespace, topic)
if subscription, exists := m.subscriptions[namespacedTopic]; exists {
// Cancel the subscription context to stop the message handler goroutine
subscription.cancel()
delete(m.subscriptions, namespacedTopic)
}
return nil
}
// ListTopics returns all subscribed topics
func (m *Manager) ListTopics(ctx context.Context) ([]string, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var topics []string
prefix := m.namespace + "."
for topic := range m.subscriptions {
if len(topic) > len(prefix) && topic[:len(prefix)] == prefix {
originalTopic := topic[len(prefix):]
topics = append(topics, originalTopic)
}
}
return topics, nil
}
// Close closes all subscriptions and topics
func (m *Manager) Close() error {
m.mu.Lock()
defer m.mu.Unlock()
// Cancel all subscriptions
for _, sub := range m.subscriptions {
sub.cancel()
}
m.subscriptions = make(map[string]*subscription)
// Close all topics
for _, topic := range m.topics {
topic.Close()
}
m.topics = make(map[string]*pubsub.Topic)
return nil
}
// announceTopicInterest helps with peer discovery by announcing interest in a topic
func (m *Manager) announceTopicInterest(topicName string) {
// Wait a bit for the subscription to be established
time.Sleep(100 * time.Millisecond)
// Get the topic
m.mu.RLock()
topic, exists := m.topics[topicName]
m.mu.RUnlock()
if !exists {
return
}
// For Anchat specifically, be more aggressive about finding peers
if len(m.namespace) > 6 && m.namespace[:6] == "anchat" {
go m.aggressiveTopicPeerDiscovery(topicName, topic)
} else {
// Start a periodic check to monitor topic peer growth
go m.monitorTopicPeers(topicName, topic)
}
}
// aggressiveTopicPeerDiscovery for Anchat - actively seeks topic peers
func (m *Manager) aggressiveTopicPeerDiscovery(topicName string, topic *pubsub.Topic) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for i := 0; i < 30; i++ { // Monitor for 30 seconds
<-ticker.C
peers := topic.ListPeers()
// If we have peers, reduce frequency but keep monitoring
if len(peers) > 0 {
// Switch to normal monitoring once we have peers
go m.monitorTopicPeers(topicName, topic)
return
}
// For Anchat, try to actively discover and connect to peers on this topic
// This is critical because LibP2P pubsub requires direct connections for message propagation
m.forceTopicPeerDiscovery(topicName, topic)
}
}
// enhancedAnchatTopicDiscovery implements enhanced peer discovery specifically for Anchat
func (m *Manager) enhancedAnchatTopicDiscovery(topicName string, topic *pubsub.Topic) {
// Wait for subscription to be fully established
time.Sleep(200 * time.Millisecond)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for i := 0; i < 20; i++ { // Monitor for 20 seconds
<-ticker.C
peers := topic.ListPeers()
if len(peers) > 0 {
// Success! We found topic peers
return
}
// Try various discovery strategies
if i%3 == 0 {
// Strategy: Send discovery heartbeat
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
discoveryMsg := []byte("ANCHAT_DISCOVERY_PING")
topic.Publish(ctx, discoveryMsg)
cancel()
}
// Wait a bit and check again
time.Sleep(500 * time.Millisecond)
peers = topic.ListPeers()
if len(peers) > 0 {
return
}
}
}
// forceTopicPeerDiscovery uses multiple strategies to find and connect to topic peers
func (m *Manager) forceTopicPeerDiscovery(topicName string, topic *pubsub.Topic) {
// Strategy 1: Check if pubsub knows about any peers for this topic
peers := topic.ListPeers()
if len(peers) > 0 {
return // We already have peers
}
// Strategy 2: Try to actively announce our presence and wait for responses
// Send a ping/heartbeat to the topic to announce our presence
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
// Create a discovery message to announce our presence on this topic
discoveryMsg := []byte("ANCHAT_PEER_DISCOVERY")
topic.Publish(ctx, discoveryMsg)
// Strategy 3: Wait briefly and check again
time.Sleep(500 * time.Millisecond)
_ = topic.ListPeers() // Check again but we don't need to use the result
// Note: In LibP2P, topics don't automatically form connections between subscribers
// The underlying network layer needs to ensure peers are connected first
// This is why our enhanced client peer discovery is crucial
}
// monitorTopicPeers periodically checks topic peer connectivity
func (m *Manager) monitorTopicPeers(topicName string, topic *pubsub.Topic) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for i := 0; i < 6; i++ { // Monitor for 30 seconds
<-ticker.C
peers := topic.ListPeers()
// If we have peers, we're good
if len(peers) > 0 {
return
}
}
}

5
pkg/pubsub/types.go Normal file
View File

@ -0,0 +1,5 @@
package pubsub
// MessageHandler represents a message handler function signature
// This matches the client.MessageHandler type to avoid circular imports
type MessageHandler func(topic string, data []byte) error

190
pkg/storage/client.go Normal file
View File

@ -0,0 +1,190 @@
package storage
import (
"context"
"fmt"
"io"
"time"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/protocol"
"go.uber.org/zap"
)
// Client provides distributed storage client functionality
type Client struct {
host host.Host
logger *zap.Logger
namespace string
}
// NewClient creates a new storage client
func NewClient(h host.Host, namespace string, logger *zap.Logger) *Client {
return &Client{
host: h,
logger: logger,
namespace: namespace,
}
}
// Put stores a key-value pair in the distributed storage
func (c *Client) Put(ctx context.Context, key string, value []byte) error {
request := &StorageRequest{
Type: MessageTypePut,
Key: key,
Value: value,
Namespace: c.namespace,
}
return c.sendRequest(ctx, request)
}
// Get retrieves a value by key from the distributed storage
func (c *Client) Get(ctx context.Context, key string) ([]byte, error) {
request := &StorageRequest{
Type: MessageTypeGet,
Key: key,
Namespace: c.namespace,
}
response, err := c.sendRequestWithResponse(ctx, request)
if err != nil {
return nil, err
}
if !response.Success {
return nil, fmt.Errorf(response.Error)
}
return response.Value, nil
}
// Delete removes a key from the distributed storage
func (c *Client) Delete(ctx context.Context, key string) error {
request := &StorageRequest{
Type: MessageTypeDelete,
Key: key,
Namespace: c.namespace,
}
return c.sendRequest(ctx, request)
}
// List returns keys with a given prefix
func (c *Client) List(ctx context.Context, prefix string, limit int) ([]string, error) {
request := &StorageRequest{
Type: MessageTypeList,
Prefix: prefix,
Limit: limit,
Namespace: c.namespace,
}
response, err := c.sendRequestWithResponse(ctx, request)
if err != nil {
return nil, err
}
if !response.Success {
return nil, fmt.Errorf(response.Error)
}
return response.Keys, nil
}
// Exists checks if a key exists in the distributed storage
func (c *Client) Exists(ctx context.Context, key string) (bool, error) {
request := &StorageRequest{
Type: MessageTypeExists,
Key: key,
Namespace: c.namespace,
}
response, err := c.sendRequestWithResponse(ctx, request)
if err != nil {
return false, err
}
if !response.Success {
return false, fmt.Errorf(response.Error)
}
return response.Exists, nil
}
// sendRequest sends a request without expecting a response
func (c *Client) sendRequest(ctx context.Context, request *StorageRequest) error {
_, err := c.sendRequestWithResponse(ctx, request)
return err
}
// sendRequestWithResponse sends a request and waits for a response
func (c *Client) sendRequestWithResponse(ctx context.Context, request *StorageRequest) (*StorageResponse, error) {
// Get connected peers
peers := c.host.Network().Peers()
if len(peers) == 0 {
return nil, fmt.Errorf("no peers connected")
}
// Try to send to the first available peer
// In a production system, you might want to implement peer selection logic
for _, peerID := range peers {
response, err := c.sendToPeer(ctx, peerID, request)
if err != nil {
c.logger.Debug("Failed to send to peer",
zap.String("peer", peerID.String()),
zap.Error(err))
continue
}
return response, nil
}
return nil, fmt.Errorf("failed to send request to any peer")
}
// sendToPeer sends a request to a specific peer
func (c *Client) sendToPeer(ctx context.Context, peerID peer.ID, request *StorageRequest) (*StorageResponse, error) {
// Create a new stream to the peer
stream, err := c.host.NewStream(ctx, peerID, protocol.ID(StorageProtocolID))
if err != nil {
return nil, fmt.Errorf("failed to create stream: %w", err)
}
defer stream.Close()
// Set deadline for the operation
deadline, ok := ctx.Deadline()
if ok {
stream.SetDeadline(deadline)
} else {
stream.SetDeadline(time.Now().Add(30 * time.Second))
}
// Marshal and send request
requestData, err := request.Marshal()
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
if _, err := stream.Write(requestData); err != nil {
return nil, fmt.Errorf("failed to write request: %w", err)
}
// Close write side to signal end of request
if err := stream.CloseWrite(); err != nil {
return nil, fmt.Errorf("failed to close write: %w", err)
}
// Read response
responseData, err := io.ReadAll(stream)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
// Unmarshal response
var response StorageResponse
if err := response.Unmarshal(responseData); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &response, nil
}

60
pkg/storage/protocol.go Normal file
View File

@ -0,0 +1,60 @@
package storage
import (
"encoding/json"
)
// Storage protocol definitions for distributed storage
const (
StorageProtocolID = "/network/storage/1.0.0"
)
// Message types for storage operations
type MessageType string
const (
MessageTypePut MessageType = "put"
MessageTypeGet MessageType = "get"
MessageTypeDelete MessageType = "delete"
MessageTypeList MessageType = "list"
MessageTypeExists MessageType = "exists"
)
// StorageRequest represents a storage operation request
type StorageRequest struct {
Type MessageType `json:"type"`
Key string `json:"key"`
Value []byte `json:"value,omitempty"`
Prefix string `json:"prefix,omitempty"`
Limit int `json:"limit,omitempty"`
Namespace string `json:"namespace"`
}
// StorageResponse represents a storage operation response
type StorageResponse struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
Value []byte `json:"value,omitempty"`
Keys []string `json:"keys,omitempty"`
Exists bool `json:"exists,omitempty"`
}
// Marshal serializes a request to JSON
func (r *StorageRequest) Marshal() ([]byte, error) {
return json.Marshal(r)
}
// Unmarshal deserializes a request from JSON
func (r *StorageRequest) Unmarshal(data []byte) error {
return json.Unmarshal(data, r)
}
// Marshal serializes a response to JSON
func (r *StorageResponse) Marshal() ([]byte, error) {
return json.Marshal(r)
}
// Unmarshal deserializes a response from JSON
func (r *StorageResponse) Unmarshal(data []byte) error {
return json.Unmarshal(data, r)
}

286
pkg/storage/service.go Normal file
View File

@ -0,0 +1,286 @@
package storage
import (
"database/sql"
"fmt"
"io"
"sync"
"github.com/libp2p/go-libp2p/core/network"
"go.uber.org/zap"
)
// Service provides distributed storage functionality using RQLite
type Service struct {
logger *zap.Logger
db *sql.DB
mu sync.RWMutex
}
// NewService creates a new storage service backed by RQLite
func NewService(db *sql.DB, logger *zap.Logger) (*Service, error) {
service := &Service{
logger: logger,
db: db,
}
// Initialize storage tables
if err := service.initTables(); err != nil {
return nil, fmt.Errorf("failed to initialize storage tables: %w", err)
}
return service, nil
}
// initTables creates the necessary tables for key-value storage
func (s *Service) initTables() error {
// Create storage table with namespace support
createTableSQL := `
CREATE TABLE IF NOT EXISTS kv_storage (
namespace TEXT NOT NULL,
key TEXT NOT NULL,
value BLOB NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (namespace, key)
)
`
// Create index for faster queries
createIndexSQL := `
CREATE INDEX IF NOT EXISTS idx_kv_storage_namespace_key
ON kv_storage(namespace, key)
`
if _, err := s.db.Exec(createTableSQL); err != nil {
return fmt.Errorf("failed to create storage table: %w", err)
}
if _, err := s.db.Exec(createIndexSQL); err != nil {
return fmt.Errorf("failed to create storage index: %w", err)
}
s.logger.Info("Storage tables initialized successfully")
return nil
}
// HandleStorageStream handles incoming storage protocol streams
func (s *Service) HandleStorageStream(stream network.Stream) {
defer stream.Close()
// Read request
data, err := io.ReadAll(stream)
if err != nil {
s.logger.Error("Failed to read storage request", zap.Error(err))
return
}
var request StorageRequest
if err := request.Unmarshal(data); err != nil {
s.logger.Error("Failed to unmarshal storage request", zap.Error(err))
return
}
// Process request
response := s.processRequest(&request)
// Send response
responseData, err := response.Marshal()
if err != nil {
s.logger.Error("Failed to marshal storage response", zap.Error(err))
return
}
if _, err := stream.Write(responseData); err != nil {
s.logger.Error("Failed to write storage response", zap.Error(err))
return
}
s.logger.Debug("Handled storage request",
zap.String("type", string(request.Type)),
zap.String("key", request.Key),
zap.String("namespace", request.Namespace),
zap.Bool("success", response.Success),
)
}
// processRequest processes a storage request and returns a response
func (s *Service) processRequest(req *StorageRequest) *StorageResponse {
switch req.Type {
case MessageTypePut:
return s.handlePut(req)
case MessageTypeGet:
return s.handleGet(req)
case MessageTypeDelete:
return s.handleDelete(req)
case MessageTypeList:
return s.handleList(req)
case MessageTypeExists:
return s.handleExists(req)
default:
return &StorageResponse{
Success: false,
Error: fmt.Sprintf("unknown message type: %s", req.Type),
}
}
}
// handlePut stores a key-value pair
func (s *Service) handlePut(req *StorageRequest) *StorageResponse {
s.mu.Lock()
defer s.mu.Unlock()
// Use REPLACE to handle both insert and update
query := `
REPLACE INTO kv_storage (namespace, key, value, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
`
_, err := s.db.Exec(query, req.Namespace, req.Key, req.Value)
if err != nil {
return &StorageResponse{
Success: false,
Error: fmt.Sprintf("failed to store key: %v", err),
}
}
s.logger.Debug("Stored key", zap.String("key", req.Key), zap.String("namespace", req.Namespace))
return &StorageResponse{Success: true}
}
// handleGet retrieves a value by key
func (s *Service) handleGet(req *StorageRequest) *StorageResponse {
s.mu.RLock()
defer s.mu.RUnlock()
query := `SELECT value FROM kv_storage WHERE namespace = ? AND key = ?`
var value []byte
err := s.db.QueryRow(query, req.Namespace, req.Key).Scan(&value)
if err != nil {
if err == sql.ErrNoRows {
return &StorageResponse{
Success: false,
Error: fmt.Sprintf("key not found: %s", req.Key),
}
}
return &StorageResponse{
Success: false,
Error: fmt.Sprintf("failed to get key: %v", err),
}
}
return &StorageResponse{
Success: true,
Value: value,
}
}
// handleDelete removes a key
func (s *Service) handleDelete(req *StorageRequest) *StorageResponse {
s.mu.Lock()
defer s.mu.Unlock()
query := `DELETE FROM kv_storage WHERE namespace = ? AND key = ?`
result, err := s.db.Exec(query, req.Namespace, req.Key)
if err != nil {
return &StorageResponse{
Success: false,
Error: fmt.Sprintf("failed to delete key: %v", err),
}
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
return &StorageResponse{
Success: false,
Error: fmt.Sprintf("key not found: %s", req.Key),
}
}
s.logger.Debug("Deleted key", zap.String("key", req.Key), zap.String("namespace", req.Namespace))
return &StorageResponse{Success: true}
}
// handleList lists keys with a prefix
func (s *Service) handleList(req *StorageRequest) *StorageResponse {
s.mu.RLock()
defer s.mu.RUnlock()
var query string
var args []interface{}
if req.Prefix == "" {
// List all keys in namespace
query = `SELECT key FROM kv_storage WHERE namespace = ?`
args = []interface{}{req.Namespace}
} else {
// List keys with prefix
query = `SELECT key FROM kv_storage WHERE namespace = ? AND key LIKE ?`
args = []interface{}{req.Namespace, req.Prefix + "%"}
}
if req.Limit > 0 {
query += ` LIMIT ?`
args = append(args, req.Limit)
}
rows, err := s.db.Query(query, args...)
if err != nil {
return &StorageResponse{
Success: false,
Error: fmt.Sprintf("failed to query keys: %v", err),
}
}
defer rows.Close()
var keys []string
for rows.Next() {
var key string
if err := rows.Scan(&key); err != nil {
continue
}
keys = append(keys, key)
}
return &StorageResponse{
Success: true,
Keys: keys,
}
}
// handleExists checks if a key exists
func (s *Service) handleExists(req *StorageRequest) *StorageResponse {
s.mu.RLock()
defer s.mu.RUnlock()
query := `SELECT 1 FROM kv_storage WHERE namespace = ? AND key = ? LIMIT 1`
var exists int
err := s.db.QueryRow(query, req.Namespace, req.Key).Scan(&exists)
if err != nil {
if err == sql.ErrNoRows {
return &StorageResponse{
Success: true,
Exists: false,
}
}
return &StorageResponse{
Success: false,
Error: fmt.Sprintf("failed to check key existence: %v", err),
}
}
return &StorageResponse{
Success: true,
Exists: true,
}
}
// Close closes the storage service
func (s *Service) Close() error {
// The database connection is managed elsewhere
s.logger.Info("Storage service closed")
return nil
}

View File

@ -0,0 +1,48 @@
package main
import (
"crypto/rand"
"fmt"
"os"
"path/filepath"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
)
func main() {
// Generate a fixed identity
priv, pub, err := crypto.GenerateKeyPairWithReader(crypto.Ed25519, 2048, rand.Reader)
if err != nil {
panic(err)
}
// Get peer ID
peerID, err := peer.IDFromPublicKey(pub)
if err != nil {
panic(err)
}
fmt.Printf("Generated Peer ID: %s\n", peerID.String())
// Marshal private key
data, err := crypto.MarshalPrivateKey(priv)
if err != nil {
panic(err)
}
// Create data directory
dataDir := "./data/bootstrap"
if err := os.MkdirAll(dataDir, 0755); err != nil {
panic(err)
}
// Save identity
identityFile := filepath.Join(dataDir, "identity.key")
if err := os.WriteFile(identityFile, data, 0600); err != nil {
panic(err)
}
fmt.Printf("Identity saved to: %s\n", identityFile)
fmt.Printf("Bootstrap address: /ip4/127.0.0.1/tcp/4001/p2p/%s\n", peerID.String())
}

554
scripts/install-debros-network.sh Executable file
View File

@ -0,0 +1,554 @@
#!/bin/bash
set -e # Exit on any error
trap 'echo -e "${RED}An error occurred. Installation aborted.${NOCOLOR}"; exit 1' ERR
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
BLUE='\033[38;2;2;128;175m'
YELLOW='\033[1;33m'
NOCOLOR='\033[0m'
# Default values
INSTALL_DIR="/opt/debros"
REPO_URL="https://github.com/DeBrosOfficial/debros-network.git"
MIN_GO_VERSION="1.19"
BOOTSTRAP_PORT="4001"
NODE_PORT="4002"
RQLITE_BOOTSTRAP_PORT="5001"
RQLITE_NODE_PORT="5002"
RAFT_BOOTSTRAP_PORT="7001"
RAFT_NODE_PORT="7002"
log() {
echo -e "${CYAN}[$(date '+%Y-%m-%d %H:%M:%S')]${NOCOLOR} $1"
}
error() {
echo -e "${RED}[ERROR]${NOCOLOR} $1"
}
success() {
echo -e "${GREEN}[SUCCESS]${NOCOLOR} $1"
}
warning() {
echo -e "${YELLOW}[WARNING]${NOCOLOR} $1"
}
# Check if running as root
if [[ $EUID -eq 0 ]]; then
error "This script should not be run as root. Please run as a regular user with sudo privileges."
exit 1
fi
# Check if sudo is available
if ! command -v sudo &>/dev/null; then
error "sudo command not found. Please ensure you have sudo privileges."
exit 1
fi
# Detect OS
detect_os() {
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$ID
VERSION=$VERSION_ID
else
error "Cannot detect operating system"
exit 1
fi
case $OS in
ubuntu|debian)
PACKAGE_MANAGER="apt"
;;
centos|rhel|fedora)
PACKAGE_MANAGER="yum"
if command -v dnf &> /dev/null; then
PACKAGE_MANAGER="dnf"
fi
;;
*)
error "Unsupported operating system: $OS"
exit 1
;;
esac
log "Detected OS: $OS $VERSION"
}
# Check Go installation and version
check_go_installation() {
if command -v go &> /dev/null; then
GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//')
log "Found Go version: $GO_VERSION"
# Compare versions (simplified)
if [ "$(printf '%s\n' "$MIN_GO_VERSION" "$GO_VERSION" | sort -V | head -n1)" = "$MIN_GO_VERSION" ]; then
success "Go version is sufficient"
return 0
else
warning "Go version $GO_VERSION is too old. Minimum required: $MIN_GO_VERSION"
return 1
fi
else
log "Go not found on system"
return 1
fi
}
# Install Go
install_go() {
log "Installing Go..."
case $PACKAGE_MANAGER in
apt)
sudo apt update
sudo apt install -y wget
;;
yum|dnf)
sudo $PACKAGE_MANAGER install -y wget
;;
esac
# Download and install Go
GO_TARBALL="go1.21.0.linux-amd64.tar.gz"
ARCH=$(uname -m)
if [ "$ARCH" = "aarch64" ]; then
GO_TARBALL="go1.21.0.linux-arm64.tar.gz"
fi
cd /tmp
wget -q "https://golang.org/dl/$GO_TARBALL"
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf "$GO_TARBALL"
# Add Go to PATH
if ! grep -q "/usr/local/go/bin" ~/.bashrc; then
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
fi
export PATH=$PATH:/usr/local/go/bin
success "Go installed successfully"
}
# Install system dependencies
install_dependencies() {
log "Installing system dependencies..."
case $PACKAGE_MANAGER in
apt)
sudo apt update
sudo apt install -y git make build-essential curl
;;
yum|dnf)
sudo $PACKAGE_MANAGER groupinstall -y "Development Tools"
sudo $PACKAGE_MANAGER install -y git make curl
;;
esac
success "System dependencies installed"
}
# Check port availability
check_ports() {
local ports=($BOOTSTRAP_PORT $NODE_PORT $RQLITE_BOOTSTRAP_PORT $RQLITE_NODE_PORT $RAFT_BOOTSTRAP_PORT $RAFT_NODE_PORT)
for port in "${ports[@]}"; do
if sudo netstat -tuln 2>/dev/null | grep -q ":$port " || ss -tuln 2>/dev/null | grep -q ":$port "; then
error "Port $port is already in use. Please free it up and try again."
exit 1
fi
done
success "All required ports are available"
}
# Configuration wizard
configuration_wizard() {
log "${BLUE}==================================================${NOCOLOR}"
log "${GREEN} DeBros Network Configuration Wizard ${NOCOLOR}"
log "${BLUE}==================================================${NOCOLOR}"
# Node type selection
while true; do
echo -e "${GREEN}Select node type:${NOCOLOR}"
echo -e "${CYAN}1) Bootstrap Node (Network entry point)${NOCOLOR}"
echo -e "${CYAN}2) Regular Node (Connects to existing network)${NOCOLOR}"
read -rp "Enter your choice (1 or 2): " NODE_TYPE_CHOICE
case $NODE_TYPE_CHOICE in
1)
NODE_TYPE="bootstrap"
break
;;
2)
NODE_TYPE="regular"
break
;;
*)
error "Invalid choice. Please enter 1 or 2."
;;
esac
done
# Solana wallet address
log "${GREEN}Enter your Solana wallet address to be eligible for node operator rewards:${NOCOLOR}"
while true; do
read -rp "Solana Wallet Address: " SOLANA_WALLET
if [[ -n "$SOLANA_WALLET" && ${#SOLANA_WALLET} -ge 32 ]]; then
break
else
error "Please enter a valid Solana wallet address"
fi
done
# Data directory
read -rp "Installation directory [default: $INSTALL_DIR]: " CUSTOM_INSTALL_DIR
if [[ -n "$CUSTOM_INSTALL_DIR" ]]; then
INSTALL_DIR="$CUSTOM_INSTALL_DIR"
fi
# Firewall configuration
read -rp "Configure firewall automatically? (yes/no) [default: yes]: " CONFIGURE_FIREWALL
CONFIGURE_FIREWALL="${CONFIGURE_FIREWALL:-yes}"
success "Configuration completed"
}
# Create user and directories
setup_directories() {
log "Setting up directories and permissions..."
# Create debros user if it doesn't exist
if ! id "debros" &>/dev/null; then
sudo useradd -r -s /bin/false -d "$INSTALL_DIR" debros
log "Created debros user"
fi
# Create directory structure
sudo mkdir -p "$INSTALL_DIR"/{bin,configs,keys,data,logs}
sudo mkdir -p "$INSTALL_DIR/keys/$NODE_TYPE"
sudo mkdir -p "$INSTALL_DIR/data/$NODE_TYPE"/{rqlite,storage}
# Set ownership and permissions
sudo chown -R debros:debros "$INSTALL_DIR"
sudo chmod 755 "$INSTALL_DIR"
sudo chmod 700 "$INSTALL_DIR/keys"
sudo chmod 600 "$INSTALL_DIR/keys/$NODE_TYPE" 2>/dev/null || true
success "Directory structure created"
}
# Clone or update repository
setup_source_code() {
log "Setting up source code..."
if [ -d "$INSTALL_DIR/src" ]; then
log "Updating existing repository..."
cd "$INSTALL_DIR/src"
sudo -u debros git pull
else
log "Cloning repository..."
sudo -u debros git clone "$REPO_URL" "$INSTALL_DIR/src"
cd "$INSTALL_DIR/src"
fi
success "Source code ready"
}
# Generate identity key
generate_identity() {
log "Generating node identity..."
cd "$INSTALL_DIR/src"
# Create a temporary Go program for key generation
cat > /tmp/generate_identity.go << 'EOF'
package main
import (
"crypto/rand"
"fmt"
"os"
"path/filepath"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
)
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: go run generate_identity.go <key_file_path>")
os.Exit(1)
}
keyFile := os.Args[1]
// Generate identity
priv, pub, err := crypto.GenerateKeyPairWithReader(crypto.Ed25519, 2048, rand.Reader)
if err != nil {
panic(err)
}
// Get peer ID
peerID, err := peer.IDFromPublicKey(pub)
if err != nil {
panic(err)
}
// Marshal private key
data, err := crypto.MarshalPrivateKey(priv)
if err != nil {
panic(err)
}
// Create directory
if err := os.MkdirAll(filepath.Dir(keyFile), 0700); err != nil {
panic(err)
}
// Save identity
if err := os.WriteFile(keyFile, data, 0600); err != nil {
panic(err)
}
fmt.Printf("Generated Peer ID: %s\n", peerID.String())
fmt.Printf("Identity saved to: %s\n", keyFile)
}
EOF
# Generate the identity key
sudo -u debros go run /tmp/generate_identity.go "$INSTALL_DIR/keys/$NODE_TYPE/identity.key"
rm /tmp/generate_identity.go
success "Node identity generated"
}
# Build binaries
build_binaries() {
log "Building DeBros Network binaries..."
cd "$INSTALL_DIR/src"
# Build all binaries
sudo -u debros make build
# Copy binaries to installation directory
sudo cp bin/* "$INSTALL_DIR/bin/"
sudo chown debros:debros "$INSTALL_DIR/bin/"*
success "Binaries built and installed"
}
# Generate configuration files
generate_configs() {
log "Generating configuration files..."
if [ "$NODE_TYPE" = "bootstrap" ]; then
cat > /tmp/config.yaml << EOF
node:
data_dir: "$INSTALL_DIR/data/bootstrap"
key_file: "$INSTALL_DIR/keys/bootstrap/identity.key"
listen_addresses:
- "/ip4/0.0.0.0/tcp/$BOOTSTRAP_PORT"
solana_wallet: "$SOLANA_WALLET"
database:
rqlite_port: $RQLITE_BOOTSTRAP_PORT
rqlite_raft_port: $RAFT_BOOTSTRAP_PORT
logging:
level: "info"
file: "$INSTALL_DIR/logs/bootstrap.log"
EOF
else
cat > /tmp/config.yaml << EOF
node:
data_dir: "$INSTALL_DIR/data/node"
key_file: "$INSTALL_DIR/keys/node/identity.key"
listen_addresses:
- "/ip4/0.0.0.0/tcp/$NODE_PORT"
solana_wallet: "$SOLANA_WALLET"
database:
rqlite_port: $RQLITE_NODE_PORT
rqlite_raft_port: $RAFT_NODE_PORT
logging:
level: "info"
file: "$INSTALL_DIR/logs/node.log"
EOF
fi
sudo mv /tmp/config.yaml "$INSTALL_DIR/configs/$NODE_TYPE.yaml"
sudo chown debros:debros "$INSTALL_DIR/configs/$NODE_TYPE.yaml"
success "Configuration files generated"
}
# Configure firewall
configure_firewall() {
if [[ "$CONFIGURE_FIREWALL" == "yes" ]]; then
log "Configuring firewall..."
if command -v ufw &> /dev/null; then
if [ "$NODE_TYPE" = "bootstrap" ]; then
sudo ufw allow $BOOTSTRAP_PORT
sudo ufw allow $RQLITE_BOOTSTRAP_PORT
sudo ufw allow $RAFT_BOOTSTRAP_PORT
else
sudo ufw allow $NODE_PORT
sudo ufw allow $RQLITE_NODE_PORT
sudo ufw allow $RAFT_NODE_PORT
fi
# Enable ufw if not already active
UFW_STATUS=$(sudo ufw status | grep -o "Status: [a-z]*" | awk '{print $2}' || echo "inactive")
if [[ "$UFW_STATUS" != "active" ]]; then
echo "y" | sudo ufw enable
fi
success "Firewall configured"
else
warning "UFW not found. Please configure firewall manually."
fi
fi
}
# Create systemd service
create_systemd_service() {
log "Creating systemd service..."
cat > /tmp/debros-$NODE_TYPE.service << EOF
[Unit]
Description=DeBros Network $NODE_TYPE Node
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=debros
Group=debros
WorkingDirectory=$INSTALL_DIR
ExecStart=$INSTALL_DIR/bin/$NODE_TYPE -config $INSTALL_DIR/configs/$NODE_TYPE.yaml
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=debros-$NODE_TYPE
# Security settings
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=$INSTALL_DIR
[Install]
WantedBy=multi-user.target
EOF
sudo mv /tmp/debros-$NODE_TYPE.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable debros-$NODE_TYPE.service
success "Systemd service created and enabled"
}
# Start the service
start_service() {
log "Starting DeBros Network $NODE_TYPE node..."
sudo systemctl start debros-$NODE_TYPE.service
sleep 3
if systemctl is-active --quiet debros-$NODE_TYPE.service; then
success "DeBros Network $NODE_TYPE node started successfully"
else
error "Failed to start DeBros Network $NODE_TYPE node"
log "Check logs with: sudo journalctl -u debros-$NODE_TYPE.service"
exit 1
fi
}
# Display banner
display_banner() {
echo -e "${BLUE}========================================================================${NOCOLOR}"
echo -e "${CYAN}
____ ____ _ _ _ _
| _ \ ___| __ ) _ __ ___ ___ | \ | | ___| |___ _____ _ __| | __
| | | |/ _ \ _ \| __/ _ \/ __| | \| |/ _ \ __\ \ /\ / / _ \| __| |/ /
| |_| | __/ |_) | | | (_) \__ \ | |\ | __/ |_ \ V V / (_) | | | <
|____/ \___|____/|_| \___/|___/ |_| \_|\___|\__| \_/\_/ \___/|_| |_|\_\\
${NOCOLOR}"
echo -e "${BLUE}========================================================================${NOCOLOR}"
}
# Main installation function
main() {
display_banner
log "${BLUE}==================================================${NOCOLOR}"
log "${GREEN} Starting DeBros Network Installation ${NOCOLOR}"
log "${BLUE}==================================================${NOCOLOR}"
detect_os
check_ports
# Check and install Go if needed
if ! check_go_installation; then
install_go
fi
install_dependencies
configuration_wizard
setup_directories
setup_source_code
generate_identity
build_binaries
generate_configs
configure_firewall
create_systemd_service
start_service
# Display completion information
log "${BLUE}==================================================${NOCOLOR}"
log "${GREEN} Installation Complete! ${NOCOLOR}"
log "${BLUE}==================================================${NOCOLOR}"
log "${GREEN}Node Type:${NOCOLOR} ${CYAN}$NODE_TYPE${NOCOLOR}"
log "${GREEN}Installation Directory:${NOCOLOR} ${CYAN}$INSTALL_DIR${NOCOLOR}"
log "${GREEN}Configuration:${NOCOLOR} ${CYAN}$INSTALL_DIR/configs/$NODE_TYPE.yaml${NOCOLOR}"
log "${GREEN}Logs:${NOCOLOR} ${CYAN}$INSTALL_DIR/logs/$NODE_TYPE.log${NOCOLOR}"
if [ "$NODE_TYPE" = "bootstrap" ]; then
log "${GREEN}Bootstrap Port:${NOCOLOR} ${CYAN}$BOOTSTRAP_PORT${NOCOLOR}"
log "${GREEN}RQLite Port:${NOCOLOR} ${CYAN}$RQLITE_BOOTSTRAP_PORT${NOCOLOR}"
log "${GREEN}Raft Port:${NOCOLOR} ${CYAN}$RAFT_BOOTSTRAP_PORT${NOCOLOR}"
else
log "${GREEN}Node Port:${NOCOLOR} ${CYAN}$NODE_PORT${NOCOLOR}"
log "${GREEN}RQLite Port:${NOCOLOR} ${CYAN}$RQLITE_NODE_PORT${NOCOLOR}"
log "${GREEN}Raft Port:${NOCOLOR} ${CYAN}$RAFT_NODE_PORT${NOCOLOR}"
fi
log "${BLUE}==================================================${NOCOLOR}"
log "${GREEN}Management Commands:${NOCOLOR}"
log "${CYAN} - sudo systemctl status debros-$NODE_TYPE${NOCOLOR} (Check status)"
log "${CYAN} - sudo systemctl restart debros-$NODE_TYPE${NOCOLOR} (Restart service)"
log "${CYAN} - sudo systemctl stop debros-$NODE_TYPE${NOCOLOR} (Stop service)"
log "${CYAN} - sudo systemctl start debros-$NODE_TYPE${NOCOLOR} (Start service)"
log "${CYAN} - sudo journalctl -u debros-$NODE_TYPE.service -f${NOCOLOR} (View logs)"
log "${CYAN} - $INSTALL_DIR/bin/cli${NOCOLOR} (Use CLI tools)"
log "${BLUE}==================================================${NOCOLOR}"
success "DeBros Network $NODE_TYPE node is now running!"
log "${CYAN}For documentation visit: https://docs.debros.io${NOCOLOR}"
}
# Run main function
main "$@"