Initial commit
This commit is contained in:
commit
322d597916
17
.env.example
Normal file
17
.env.example
Normal 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
46
.github/copilot-instructions.md
vendored
Normal 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
73
.gitignore
vendored
Normal 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
493
AI_CONTEXT.md
Normal 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
184
Makefile
Normal 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
914
README.md
Normal 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
373
WHITEPAPER.md
Normal 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
112
cmd/bootstrap/main.go
Normal 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
578
cmd/cli/main.go
Normal 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
111
cmd/node/main.go
Normal 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
34
configs/bootstrap.yaml
Normal 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
36
configs/node.yaml
Normal 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
195
examples/basic_usage.go
Normal 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)
|
||||
}
|
48
generate-bootstrap-identity.go
Normal file
48
generate-bootstrap-identity.go
Normal 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
134
go.mod
Normal 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
585
go.sum
Normal 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
554
install-debros-network.sh
Executable 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
653
pkg/client/client.go
Normal 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("")
|
||||
}
|
||||
}
|
||||
}
|
565
pkg/client/implementations.go
Normal file
565
pkg/client/implementations.go
Normal 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
140
pkg/client/interface.go
Normal 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
156
pkg/config/config.go
Normal 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
192
pkg/constants/bootstrap.go
Normal 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
46
pkg/database/adapter.go
Normal 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
170
pkg/database/rqlite.go
Normal 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
177
pkg/discovery/discovery.go
Normal 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
280
pkg/logging/logger.go
Normal 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
635
pkg/node/node.go
Normal 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
44
pkg/pubsub/adapter.go
Normal 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
332
pkg/pubsub/manager.go
Normal 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
5
pkg/pubsub/types.go
Normal 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
190
pkg/storage/client.go
Normal 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
60
pkg/storage/protocol.go
Normal 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
286
pkg/storage/service.go
Normal 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
|
||||
}
|
48
scripts/generate-bootstrap-identity.go
Normal file
48
scripts/generate-bootstrap-identity.go
Normal 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
554
scripts/install-debros-network.sh
Executable 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 "$@"
|
Loading…
x
Reference in New Issue
Block a user