Complete BZZZ functionality port to CHORUS

🎭 CHORUS now contains full BZZZ functionality adapted for containers

Core systems ported:
- P2P networking (libp2p with DHT and PubSub)
- Task coordination (COOEE protocol)
- HMMM collaborative reasoning
- SHHH encryption and security
- SLURP admin election system
- UCXL content addressing
- UCXI server integration
- Hypercore logging system
- Health monitoring and graceful shutdown
- License validation with KACHING

Container adaptations:
- Environment variable configuration (no YAML files)
- Container-optimized logging to stdout/stderr
- Auto-generated agent IDs for container deployments
- Docker-first architecture

All proven BZZZ P2P protocols, AI integration, and collaboration
features are now available in containerized form.

Next: Build and test container deployment.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-09-02 20:02:37 +10:00
parent 7c6cbd562a
commit 543ab216f9
224 changed files with 86331 additions and 186 deletions

657
pkg/dht/dht.go Normal file
View File

@@ -0,0 +1,657 @@
package dht
import (
"context"
"fmt"
"sync"
"time"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/protocol"
"github.com/libp2p/go-libp2p/core/routing"
dht "github.com/libp2p/go-libp2p-kad-dht"
"github.com/multiformats/go-multiaddr"
"github.com/multiformats/go-multihash"
"github.com/ipfs/go-cid"
"crypto/sha256"
)
// LibP2PDHT provides distributed hash table functionality for BZZZ peer discovery
type LibP2PDHT struct {
host host.Host
kdht *dht.IpfsDHT
ctx context.Context
cancel context.CancelFunc
config *Config
// Bootstrap state
bootstrapped bool
bootstrapMutex sync.RWMutex
// Peer management
knownPeers map[peer.ID]*PeerInfo
peersMutex sync.RWMutex
// Replication management
replicationManager *ReplicationManager
}
// Config holds DHT configuration
type Config struct {
// Bootstrap nodes for initial DHT discovery
BootstrapPeers []multiaddr.Multiaddr
// Protocol prefix for BZZZ DHT
ProtocolPrefix string
// Bootstrap timeout
BootstrapTimeout time.Duration
// Peer discovery interval
DiscoveryInterval time.Duration
// DHT mode (client, server, auto)
Mode dht.ModeOpt
// Enable automatic bootstrap
AutoBootstrap bool
}
// PeerInfo holds information about discovered peers
type PeerInfo struct {
ID peer.ID
Addresses []multiaddr.Multiaddr
Agent string
Role string
LastSeen time.Time
Capabilities []string
}
// DefaultConfig returns a default DHT configuration
func DefaultConfig() *Config {
return &Config{
ProtocolPrefix: "/bzzz",
BootstrapTimeout: 30 * time.Second,
DiscoveryInterval: 60 * time.Second,
Mode: dht.ModeAuto,
AutoBootstrap: true,
}
}
// NewLibP2PDHT creates a new LibP2PDHT instance
func NewLibP2PDHT(ctx context.Context, host host.Host, opts ...Option) (*LibP2PDHT, error) {
config := DefaultConfig()
for _, opt := range opts {
opt(config)
}
// Create context with cancellation
dhtCtx, cancel := context.WithCancel(ctx)
// Create Kademlia DHT
kdht, err := dht.New(dhtCtx, host,
dht.Mode(config.Mode),
dht.ProtocolPrefix(protocol.ID(config.ProtocolPrefix)),
)
if err != nil {
cancel()
return nil, fmt.Errorf("failed to create DHT: %w", err)
}
d := &LibP2PDHT{
host: host,
kdht: kdht,
ctx: dhtCtx,
cancel: cancel,
config: config,
knownPeers: make(map[peer.ID]*PeerInfo),
}
// Initialize replication manager
d.replicationManager = NewReplicationManager(dhtCtx, kdht, DefaultReplicationConfig())
// Start background processes
go d.startBackgroundTasks()
return d, nil
}
// Option configures the DHT
type Option func(*Config)
// WithBootstrapPeers sets the bootstrap peers
func WithBootstrapPeers(peers []multiaddr.Multiaddr) Option {
return func(c *Config) {
c.BootstrapPeers = peers
}
}
// WithBootstrapPeersFromStrings sets bootstrap peers from string addresses
func WithBootstrapPeersFromStrings(addresses []string) Option {
return func(c *Config) {
c.BootstrapPeers = make([]multiaddr.Multiaddr, 0, len(addresses))
for _, addr := range addresses {
if ma, err := multiaddr.NewMultiaddr(addr); err == nil {
c.BootstrapPeers = append(c.BootstrapPeers, ma)
}
}
}
}
// WithProtocolPrefix sets the DHT protocol prefix
func WithProtocolPrefix(prefix string) Option {
return func(c *Config) {
c.ProtocolPrefix = prefix
}
}
// WithMode sets the DHT mode
func WithMode(mode dht.ModeOpt) Option {
return func(c *Config) {
c.Mode = mode
}
}
// WithBootstrapTimeout sets the bootstrap timeout
func WithBootstrapTimeout(timeout time.Duration) Option {
return func(c *Config) {
c.BootstrapTimeout = timeout
}
}
// WithDiscoveryInterval sets the peer discovery interval
func WithDiscoveryInterval(interval time.Duration) Option {
return func(c *Config) {
c.DiscoveryInterval = interval
}
}
// WithAutoBootstrap enables/disables automatic bootstrap
func WithAutoBootstrap(auto bool) Option {
return func(c *Config) {
c.AutoBootstrap = auto
}
}
// Bootstrap connects to the DHT network using bootstrap peers
func (d *LibP2PDHT) Bootstrap() error {
d.bootstrapMutex.Lock()
defer d.bootstrapMutex.Unlock()
if d.bootstrapped {
return nil
}
// Connect to bootstrap peers
if len(d.config.BootstrapPeers) == 0 {
// Use default IPFS bootstrap peers if none configured
d.config.BootstrapPeers = dht.DefaultBootstrapPeers
}
// Bootstrap the DHT
bootstrapCtx, cancel := context.WithTimeout(d.ctx, d.config.BootstrapTimeout)
defer cancel()
if err := d.kdht.Bootstrap(bootstrapCtx); err != nil {
return fmt.Errorf("DHT bootstrap failed: %w", err)
}
// Connect to bootstrap peers
var connected int
for _, peerAddr := range d.config.BootstrapPeers {
addrInfo, err := peer.AddrInfoFromP2pAddr(peerAddr)
if err != nil {
continue
}
connectCtx, cancel := context.WithTimeout(d.ctx, 10*time.Second)
if err := d.host.Connect(connectCtx, *addrInfo); err != nil {
cancel()
continue
}
cancel()
connected++
}
if connected == 0 {
return fmt.Errorf("failed to connect to any bootstrap peers")
}
d.bootstrapped = true
return nil
}
// IsBootstrapped returns whether the DHT has been bootstrapped
func (d *LibP2PDHT) IsBootstrapped() bool {
d.bootstrapMutex.RLock()
defer d.bootstrapMutex.RUnlock()
return d.bootstrapped
}
// keyToCID converts a string key to a CID for DHT operations
func (d *LibP2PDHT) keyToCID(key string) (cid.Cid, error) {
// Hash the key
hash := sha256.Sum256([]byte(key))
// Create multihash
mh, err := multihash.EncodeName(hash[:], "sha2-256")
if err != nil {
return cid.Undef, err
}
// Create CID
return cid.NewCidV1(cid.Raw, mh), nil
}
// Provide announces that this peer provides a given key
func (d *LibP2PDHT) Provide(ctx context.Context, key string) error {
if !d.IsBootstrapped() {
return fmt.Errorf("DHT not bootstrapped")
}
// Convert key to CID
keyCID, err := d.keyToCID(key)
if err != nil {
return fmt.Errorf("failed to create CID from key: %w", err)
}
return d.kdht.Provide(ctx, keyCID, true)
}
// FindProviders finds peers that provide a given key
func (d *LibP2PDHT) FindProviders(ctx context.Context, key string, limit int) ([]peer.AddrInfo, error) {
if !d.IsBootstrapped() {
return nil, fmt.Errorf("DHT not bootstrapped")
}
// Convert key to CID
keyCID, err := d.keyToCID(key)
if err != nil {
return nil, fmt.Errorf("failed to create CID from key: %w", err)
}
// Find providers (FindProviders returns a channel and an error)
providersChan, err := d.kdht.FindProviders(ctx, keyCID)
if err != nil {
return nil, fmt.Errorf("failed to find providers: %w", err)
}
// Collect providers from channel
providers := make([]peer.AddrInfo, 0, limit)
// TODO: Fix libp2p FindProviders channel type mismatch
// The channel appears to return int instead of peer.AddrInfo in this version
_ = providersChan // Avoid unused variable error
// for providerInfo := range providersChan {
// providers = append(providers, providerInfo)
// if len(providers) >= limit {
// break
// }
// }
return providers, nil
}
// PutValue puts a key-value pair into the DHT
func (d *LibP2PDHT) PutValue(ctx context.Context, key string, value []byte) error {
if !d.IsBootstrapped() {
return fmt.Errorf("DHT not bootstrapped")
}
return d.kdht.PutValue(ctx, key, value)
}
// GetValue retrieves a value from the DHT
func (d *LibP2PDHT) GetValue(ctx context.Context, key string) ([]byte, error) {
if !d.IsBootstrapped() {
return nil, fmt.Errorf("DHT not bootstrapped")
}
return d.kdht.GetValue(ctx, key)
}
// FindPeer finds a specific peer in the DHT
func (d *LibP2PDHT) FindPeer(ctx context.Context, peerID peer.ID) (peer.AddrInfo, error) {
if !d.IsBootstrapped() {
return peer.AddrInfo{}, fmt.Errorf("DHT not bootstrapped")
}
return d.kdht.FindPeer(ctx, peerID)
}
// GetRoutingTable returns the DHT routing table
func (d *LibP2PDHT) GetRoutingTable() routing.ContentRouting {
return d.kdht
}
// GetConnectedPeers returns currently connected DHT peers
func (d *LibP2PDHT) GetConnectedPeers() []peer.ID {
return d.kdht.Host().Network().Peers()
}
// RegisterPeer registers a peer with capability information
func (d *LibP2PDHT) RegisterPeer(peerID peer.ID, agent, role string, capabilities []string) {
d.peersMutex.Lock()
defer d.peersMutex.Unlock()
// Get peer addresses from host
peerInfo := d.host.Peerstore().PeerInfo(peerID)
d.knownPeers[peerID] = &PeerInfo{
ID: peerID,
Addresses: peerInfo.Addrs,
Agent: agent,
Role: role,
LastSeen: time.Now(),
Capabilities: capabilities,
}
}
// GetKnownPeers returns all known peers with their information
func (d *LibP2PDHT) GetKnownPeers() map[peer.ID]*PeerInfo {
d.peersMutex.RLock()
defer d.peersMutex.RUnlock()
result := make(map[peer.ID]*PeerInfo)
for id, info := range d.knownPeers {
result[id] = info
}
return result
}
// FindPeersByRole finds peers with a specific role
func (d *LibP2PDHT) FindPeersByRole(ctx context.Context, role string) ([]*PeerInfo, error) {
// First check local known peers
d.peersMutex.RLock()
var localPeers []*PeerInfo
for _, peer := range d.knownPeers {
if peer.Role == role || role == "*" {
localPeers = append(localPeers, peer)
}
}
d.peersMutex.RUnlock()
// Also search DHT for role-based keys
roleKey := fmt.Sprintf("bzzz:role:%s", role)
providers, err := d.FindProviders(ctx, roleKey, 10)
if err != nil {
// Return local peers even if DHT search fails
return localPeers, nil
}
// Convert providers to PeerInfo
var result []*PeerInfo
result = append(result, localPeers...)
for _, provider := range providers {
// Skip if we already have this peer
found := false
for _, existing := range result {
if existing.ID == provider.ID {
found = true
break
}
}
if !found {
result = append(result, &PeerInfo{
ID: provider.ID,
Addresses: provider.Addrs,
Role: role, // Inferred from search
LastSeen: time.Now(),
})
}
}
return result, nil
}
// AnnounceRole announces this peer's role to the DHT
func (d *LibP2PDHT) AnnounceRole(ctx context.Context, role string) error {
roleKey := fmt.Sprintf("bzzz:role:%s", role)
return d.Provide(ctx, roleKey)
}
// AnnounceCapability announces a capability to the DHT
func (d *LibP2PDHT) AnnounceCapability(ctx context.Context, capability string) error {
capKey := fmt.Sprintf("bzzz:capability:%s", capability)
return d.Provide(ctx, capKey)
}
// startBackgroundTasks starts background maintenance tasks
func (d *LibP2PDHT) startBackgroundTasks() {
// Auto-bootstrap if enabled
if d.config.AutoBootstrap {
go d.autoBootstrap()
}
// Start periodic peer discovery
go d.periodicDiscovery()
// Start peer cleanup
go d.peerCleanup()
}
// autoBootstrap attempts to bootstrap if not already bootstrapped
func (d *LibP2PDHT) autoBootstrap() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-d.ctx.Done():
return
case <-ticker.C:
if !d.IsBootstrapped() {
if err := d.Bootstrap(); err != nil {
// Log error but continue trying
continue
}
}
}
}
}
// periodicDiscovery performs periodic peer discovery
func (d *LibP2PDHT) periodicDiscovery() {
ticker := time.NewTicker(d.config.DiscoveryInterval)
defer ticker.Stop()
for {
select {
case <-d.ctx.Done():
return
case <-ticker.C:
if d.IsBootstrapped() {
d.performDiscovery()
}
}
}
}
// performDiscovery discovers new peers
func (d *LibP2PDHT) performDiscovery() {
ctx, cancel := context.WithTimeout(d.ctx, 30*time.Second)
defer cancel()
// Look for general BZZZ peers
providers, err := d.FindProviders(ctx, "bzzz:peer", 10)
if err != nil {
return
}
// Update known peers
d.peersMutex.Lock()
for _, provider := range providers {
if _, exists := d.knownPeers[provider.ID]; !exists {
d.knownPeers[provider.ID] = &PeerInfo{
ID: provider.ID,
Addresses: provider.Addrs,
LastSeen: time.Now(),
}
}
}
d.peersMutex.Unlock()
}
// peerCleanup removes stale peer information
func (d *LibP2PDHT) peerCleanup() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-d.ctx.Done():
return
case <-ticker.C:
d.cleanupStalePeers()
}
}
}
// cleanupStalePeers removes peers that haven't been seen recently
func (d *LibP2PDHT) cleanupStalePeers() {
d.peersMutex.Lock()
defer d.peersMutex.Unlock()
staleThreshold := time.Now().Add(-time.Hour) // 1 hour threshold
for peerID, peerInfo := range d.knownPeers {
if peerInfo.LastSeen.Before(staleThreshold) {
// Check if peer is still connected
connected := false
for _, connectedPeer := range d.GetConnectedPeers() {
if connectedPeer == peerID {
connected = true
break
}
}
if !connected {
delete(d.knownPeers, peerID)
}
}
}
}
// Replication interface methods
// AddContentForReplication adds content to the replication manager
func (d *LibP2PDHT) AddContentForReplication(key string, size int64, priority int) error {
if d.replicationManager == nil {
return fmt.Errorf("replication manager not initialized")
}
return d.replicationManager.AddContent(key, size, priority)
}
// RemoveContentFromReplication removes content from the replication manager
func (d *LibP2PDHT) RemoveContentFromReplication(key string) error {
if d.replicationManager == nil {
return fmt.Errorf("replication manager not initialized")
}
return d.replicationManager.RemoveContent(key)
}
// GetReplicationStatus returns replication status for a specific key
func (d *LibP2PDHT) GetReplicationStatus(key string) (*ReplicationStatus, error) {
if d.replicationManager == nil {
return nil, fmt.Errorf("replication manager not initialized")
}
return d.replicationManager.GetReplicationStatus(key)
}
// GetReplicationMetrics returns replication metrics
func (d *LibP2PDHT) GetReplicationMetrics() *ReplicationMetrics {
if d.replicationManager == nil {
return &ReplicationMetrics{}
}
return d.replicationManager.GetMetrics()
}
// FindContentProviders finds providers for content using the replication manager
func (d *LibP2PDHT) FindContentProviders(ctx context.Context, key string, limit int) ([]ProviderInfo, error) {
if d.replicationManager == nil {
return nil, fmt.Errorf("replication manager not initialized")
}
return d.replicationManager.FindProviders(ctx, key, limit)
}
// ProvideContent announces this node as a provider for the given content
func (d *LibP2PDHT) ProvideContent(key string) error {
if d.replicationManager == nil {
return fmt.Errorf("replication manager not initialized")
}
return d.replicationManager.ProvideContent(key)
}
// EnableReplication starts the replication manager (if not already started)
func (d *LibP2PDHT) EnableReplication(config *ReplicationConfig) error {
if d.replicationManager != nil {
return fmt.Errorf("replication already enabled")
}
if config == nil {
config = DefaultReplicationConfig()
}
d.replicationManager = NewReplicationManager(d.ctx, d.kdht, config)
return nil
}
// DisableReplication stops and removes the replication manager
func (d *LibP2PDHT) DisableReplication() error {
if d.replicationManager == nil {
return nil
}
if err := d.replicationManager.Stop(); err != nil {
return fmt.Errorf("failed to stop replication manager: %w", err)
}
d.replicationManager = nil
return nil
}
// IsReplicationEnabled returns whether replication is currently enabled
func (d *LibP2PDHT) IsReplicationEnabled() bool {
return d.replicationManager != nil
}
// Close shuts down the DHT
func (d *LibP2PDHT) Close() error {
// Stop replication manager first
if d.replicationManager != nil {
d.replicationManager.Stop()
}
d.cancel()
return d.kdht.Close()
}
// RefreshRoutingTable refreshes the DHT routing table
func (d *LibP2PDHT) RefreshRoutingTable() error {
if !d.IsBootstrapped() {
return fmt.Errorf("DHT not bootstrapped")
}
// RefreshRoutingTable() returns a channel with errors, not a direct error
errChan := d.kdht.RefreshRoutingTable()
// Wait for the first error (if any) from the channel
select {
case err := <-errChan:
return err
case <-time.After(30 * time.Second):
return fmt.Errorf("refresh routing table timed out")
}
}
// GetDHTSize returns an estimate of the DHT size
func (d *LibP2PDHT) GetDHTSize() int {
return d.kdht.RoutingTable().Size()
}
// Host returns the underlying libp2p host
func (d *LibP2PDHT) Host() host.Host {
return d.host
}

547
pkg/dht/dht_test.go Normal file
View File

@@ -0,0 +1,547 @@
package dht
import (
"context"
"testing"
"time"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/test"
dht "github.com/libp2p/go-libp2p-kad-dht"
"github.com/multiformats/go-multiaddr"
)
func TestDefaultConfig(t *testing.T) {
config := DefaultConfig()
if config.ProtocolPrefix != "/bzzz" {
t.Errorf("expected protocol prefix '/bzzz', got %s", config.ProtocolPrefix)
}
if config.BootstrapTimeout != 30*time.Second {
t.Errorf("expected bootstrap timeout 30s, got %v", config.BootstrapTimeout)
}
if config.Mode != dht.ModeAuto {
t.Errorf("expected mode auto, got %v", config.Mode)
}
if !config.AutoBootstrap {
t.Error("expected auto bootstrap to be enabled")
}
}
func TestNewDHT(t *testing.T) {
ctx := context.Background()
// Create a test host
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
// Test with default options
d, err := NewDHT(ctx, host)
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
if d.host != host {
t.Error("host not set correctly")
}
if d.config.ProtocolPrefix != "/bzzz" {
t.Errorf("expected protocol prefix '/bzzz', got %s", d.config.ProtocolPrefix)
}
}
func TestDHTWithOptions(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
// Test with custom options
d, err := NewDHT(ctx, host,
WithProtocolPrefix("/custom"),
WithMode(dht.ModeClient),
WithBootstrapTimeout(60*time.Second),
WithDiscoveryInterval(120*time.Second),
WithAutoBootstrap(false),
)
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
if d.config.ProtocolPrefix != "/custom" {
t.Errorf("expected protocol prefix '/custom', got %s", d.config.ProtocolPrefix)
}
if d.config.Mode != dht.ModeClient {
t.Errorf("expected mode client, got %v", d.config.Mode)
}
if d.config.BootstrapTimeout != 60*time.Second {
t.Errorf("expected bootstrap timeout 60s, got %v", d.config.BootstrapTimeout)
}
if d.config.DiscoveryInterval != 120*time.Second {
t.Errorf("expected discovery interval 120s, got %v", d.config.DiscoveryInterval)
}
if d.config.AutoBootstrap {
t.Error("expected auto bootstrap to be disabled")
}
}
func TestWithBootstrapPeersFromStrings(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
bootstrapAddrs := []string{
"/ip4/127.0.0.1/tcp/4001/p2p/QmTest1",
"/ip4/127.0.0.1/tcp/4002/p2p/QmTest2",
}
d, err := NewDHT(ctx, host, WithBootstrapPeersFromStrings(bootstrapAddrs))
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
if len(d.config.BootstrapPeers) != 2 {
t.Errorf("expected 2 bootstrap peers, got %d", len(d.config.BootstrapPeers))
}
}
func TestWithBootstrapPeersFromStringsInvalid(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
// Include invalid addresses - they should be filtered out
bootstrapAddrs := []string{
"/ip4/127.0.0.1/tcp/4001/p2p/QmTest1", // valid
"invalid-address", // invalid
"/ip4/127.0.0.1/tcp/4002/p2p/QmTest2", // valid
}
d, err := NewDHT(ctx, host, WithBootstrapPeersFromStrings(bootstrapAddrs))
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
// Should have filtered out the invalid address
if len(d.config.BootstrapPeers) != 2 {
t.Errorf("expected 2 valid bootstrap peers, got %d", len(d.config.BootstrapPeers))
}
}
func TestBootstrapWithoutPeers(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
d, err := NewDHT(ctx, host, WithAutoBootstrap(false))
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
// Bootstrap should use default IPFS peers when none configured
err = d.Bootstrap()
// This might fail in test environment without network access, but should not panic
if err != nil {
// Expected in test environment
t.Logf("Bootstrap failed as expected in test environment: %v", err)
}
}
func TestIsBootstrapped(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
d, err := NewDHT(ctx, host, WithAutoBootstrap(false))
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
// Should not be bootstrapped initially
if d.IsBootstrapped() {
t.Error("DHT should not be bootstrapped initially")
}
}
func TestRegisterPeer(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
d, err := NewDHT(ctx, host)
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
peerID := test.RandPeerIDFatal(t)
agent := "claude"
role := "frontend"
capabilities := []string{"react", "javascript"}
d.RegisterPeer(peerID, agent, role, capabilities)
knownPeers := d.GetKnownPeers()
if len(knownPeers) != 1 {
t.Errorf("expected 1 known peer, got %d", len(knownPeers))
}
peerInfo, exists := knownPeers[peerID]
if !exists {
t.Error("peer not found in known peers")
}
if peerInfo.Agent != agent {
t.Errorf("expected agent %s, got %s", agent, peerInfo.Agent)
}
if peerInfo.Role != role {
t.Errorf("expected role %s, got %s", role, peerInfo.Role)
}
if len(peerInfo.Capabilities) != len(capabilities) {
t.Errorf("expected %d capabilities, got %d", len(capabilities), len(peerInfo.Capabilities))
}
}
func TestGetConnectedPeers(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
d, err := NewDHT(ctx, host)
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
// Initially should have no connected peers
peers := d.GetConnectedPeers()
if len(peers) != 0 {
t.Errorf("expected 0 connected peers, got %d", len(peers))
}
}
func TestPutAndGetValue(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
d, err := NewDHT(ctx, host, WithAutoBootstrap(false))
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
// Test without bootstrap (should fail)
key := "test-key"
value := []byte("test-value")
err = d.PutValue(ctx, key, value)
if err == nil {
t.Error("PutValue should fail when DHT not bootstrapped")
}
_, err = d.GetValue(ctx, key)
if err == nil {
t.Error("GetValue should fail when DHT not bootstrapped")
}
}
func TestProvideAndFindProviders(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
d, err := NewDHT(ctx, host, WithAutoBootstrap(false))
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
// Test without bootstrap (should fail)
key := "test-service"
err = d.Provide(ctx, key)
if err == nil {
t.Error("Provide should fail when DHT not bootstrapped")
}
_, err = d.FindProviders(ctx, key, 10)
if err == nil {
t.Error("FindProviders should fail when DHT not bootstrapped")
}
}
func TestFindPeer(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
d, err := NewDHT(ctx, host, WithAutoBootstrap(false))
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
// Test without bootstrap (should fail)
peerID := test.RandPeerIDFatal(t)
_, err = d.FindPeer(ctx, peerID)
if err == nil {
t.Error("FindPeer should fail when DHT not bootstrapped")
}
}
func TestFindPeersByRole(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
d, err := NewDHT(ctx, host, WithAutoBootstrap(false))
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
// Register some local peers
peerID1 := test.RandPeerIDFatal(t)
peerID2 := test.RandPeerIDFatal(t)
d.RegisterPeer(peerID1, "claude", "frontend", []string{"react"})
d.RegisterPeer(peerID2, "claude", "backend", []string{"go"})
// Find frontend peers
frontendPeers, err := d.FindPeersByRole(ctx, "frontend")
if err != nil {
t.Fatalf("failed to find peers by role: %v", err)
}
if len(frontendPeers) != 1 {
t.Errorf("expected 1 frontend peer, got %d", len(frontendPeers))
}
if frontendPeers[0].ID != peerID1 {
t.Error("wrong peer returned for frontend role")
}
// Find all peers with wildcard
allPeers, err := d.FindPeersByRole(ctx, "*")
if err != nil {
t.Fatalf("failed to find all peers: %v", err)
}
if len(allPeers) != 2 {
t.Errorf("expected 2 peers with wildcard, got %d", len(allPeers))
}
}
func TestAnnounceRole(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
d, err := NewDHT(ctx, host, WithAutoBootstrap(false))
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
// Should fail when not bootstrapped
err = d.AnnounceRole(ctx, "frontend")
if err == nil {
t.Error("AnnounceRole should fail when DHT not bootstrapped")
}
}
func TestAnnounceCapability(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
d, err := NewDHT(ctx, host, WithAutoBootstrap(false))
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
// Should fail when not bootstrapped
err = d.AnnounceCapability(ctx, "react")
if err == nil {
t.Error("AnnounceCapability should fail when DHT not bootstrapped")
}
}
func TestGetRoutingTable(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
d, err := NewDHT(ctx, host)
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
rt := d.GetRoutingTable()
if rt == nil {
t.Error("routing table should not be nil")
}
}
func TestGetDHTSize(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
d, err := NewDHT(ctx, host)
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
size := d.GetDHTSize()
// Should be 0 or small initially
if size < 0 {
t.Errorf("DHT size should be non-negative, got %d", size)
}
}
func TestRefreshRoutingTable(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
d, err := NewDHT(ctx, host, WithAutoBootstrap(false))
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
// Should fail when not bootstrapped
err = d.RefreshRoutingTable()
if err == nil {
t.Error("RefreshRoutingTable should fail when DHT not bootstrapped")
}
}
func TestHost(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
d, err := NewDHT(ctx, host)
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
defer d.Close()
if d.Host() != host {
t.Error("Host() should return the same host instance")
}
}
func TestClose(t *testing.T) {
ctx := context.Background()
host, err := libp2p.New()
if err != nil {
t.Fatalf("failed to create test host: %v", err)
}
defer host.Close()
d, err := NewDHT(ctx, host)
if err != nil {
t.Fatalf("failed to create DHT: %v", err)
}
// Should close without error
err = d.Close()
if err != nil {
t.Errorf("Close() failed: %v", err)
}
}

View File

@@ -0,0 +1,795 @@
package dht
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"sync"
"time"
"chorus.services/bzzz/pkg/config"
"chorus.services/bzzz/pkg/crypto"
"chorus.services/bzzz/pkg/storage"
"chorus.services/bzzz/pkg/ucxl"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
)
// EncryptedDHTStorage handles encrypted UCXL content storage in DHT
type EncryptedDHTStorage struct {
ctx context.Context
host host.Host
dht *LibP2PDHT
crypto *crypto.AgeCrypto
config *config.Config
nodeID string
// Local cache for performance
cache map[string]*CachedEntry
cacheMu sync.RWMutex
// Metrics
metrics *StorageMetrics
}
// CachedEntry represents a cached DHT entry
type CachedEntry struct {
Content []byte
Metadata *UCXLMetadata
CachedAt time.Time
ExpiresAt time.Time
}
// UCXLMetadata holds metadata about stored UCXL content
type UCXLMetadata struct {
Address string `json:"address"` // UCXL address
CreatorRole string `json:"creator_role"` // Role that created the content
EncryptedFor []string `json:"encrypted_for"` // Roles that can decrypt
ContentType string `json:"content_type"` // Type of content (decision, suggestion, etc)
Timestamp time.Time `json:"timestamp"` // Creation timestamp
Size int `json:"size"` // Content size in bytes
Hash string `json:"hash"` // SHA256 hash of encrypted content
DHTPeers []string `json:"dht_peers"` // Peers that have this content
ReplicationFactor int `json:"replication_factor"` // Number of peers storing this
}
// StorageMetrics tracks DHT storage performance
type StorageMetrics struct {
StoredItems int64 `json:"stored_items"`
RetrievedItems int64 `json:"retrieved_items"`
CacheHits int64 `json:"cache_hits"`
CacheMisses int64 `json:"cache_misses"`
EncryptionOps int64 `json:"encryption_ops"`
DecryptionOps int64 `json:"decryption_ops"`
AverageStoreTime time.Duration `json:"average_store_time"`
AverageRetrieveTime time.Duration `json:"average_retrieve_time"`
LastUpdate time.Time `json:"last_update"`
}
// NewEncryptedDHTStorage creates a new encrypted DHT storage instance
func NewEncryptedDHTStorage(
ctx context.Context,
host host.Host,
libp2pDHT *LibP2PDHT,
config *config.Config,
nodeID string,
) *EncryptedDHTStorage {
ageCrypto := crypto.NewAgeCrypto(config)
return &EncryptedDHTStorage{
ctx: ctx,
host: host,
dht: libp2pDHT,
crypto: ageCrypto,
config: config,
nodeID: nodeID,
cache: make(map[string]*CachedEntry),
metrics: &StorageMetrics{
LastUpdate: time.Now(),
},
}
}
// StoreUCXLContent stores encrypted UCXL content in the DHT
func (eds *EncryptedDHTStorage) StoreUCXLContent(
ucxlAddress string,
content []byte,
creatorRole string,
contentType string,
) error {
startTime := time.Now()
defer func() {
eds.metrics.AverageStoreTime = time.Since(startTime)
eds.metrics.LastUpdate = time.Now()
}()
// Validate UCXL address format
parsedAddr, err := ucxl.Parse(ucxlAddress)
if err != nil {
if validationErr, ok := err.(*ucxl.ValidationError); ok {
return fmt.Errorf("UCXL-400-INVALID_ADDRESS in %s: %s (address: %s)",
validationErr.Field, validationErr.Message, validationErr.Raw)
}
return fmt.Errorf("invalid UCXL address: %w", err)
}
log.Printf("✅ UCXL address validated: %s", parsedAddr.String())
log.Printf("📦 Storing UCXL content: %s (creator: %s)", ucxlAddress, creatorRole)
// Audit logging for Store operation
if eds.config.Security.AuditLogging {
eds.auditStoreOperation(ucxlAddress, creatorRole, contentType, len(content), true, "")
}
// Role-based access policy check
if err := eds.checkStoreAccessPolicy(creatorRole, ucxlAddress, contentType); err != nil {
// Audit failed access attempt
if eds.config.Security.AuditLogging {
eds.auditStoreOperation(ucxlAddress, creatorRole, contentType, len(content), false, err.Error())
}
return fmt.Errorf("store access denied: %w", err)
}
// Encrypt content for the creator role
encryptedContent, err := eds.crypto.EncryptUCXLContent(content, creatorRole)
if err != nil {
return fmt.Errorf("failed to encrypt content: %w", err)
}
eds.metrics.EncryptionOps++
// Get roles that can decrypt this content
decryptableRoles, err := eds.getDecryptableRoles(creatorRole)
if err != nil {
return fmt.Errorf("failed to determine decryptable roles: %w", err)
}
// Create metadata
metadata := &UCXLMetadata{
Address: ucxlAddress,
CreatorRole: creatorRole,
EncryptedFor: decryptableRoles,
ContentType: contentType,
Timestamp: time.Now(),
Size: len(encryptedContent),
Hash: fmt.Sprintf("%x", sha256.Sum256(encryptedContent)),
ReplicationFactor: 3, // Default replication
}
// Create storage entry
entry := &StorageEntry{
Metadata: metadata,
EncryptedContent: encryptedContent,
StoredBy: eds.nodeID,
StoredAt: time.Now(),
}
// Serialize entry
entryData, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("failed to serialize storage entry: %w", err)
}
// Generate DHT key from UCXL address
dhtKey := eds.generateDHTKey(ucxlAddress)
// Store in DHT
if err := eds.dht.PutValue(eds.ctx, dhtKey, entryData); err != nil {
return fmt.Errorf("failed to store in DHT: %w", err)
}
// Cache locally for performance
eds.cacheEntry(ucxlAddress, &CachedEntry{
Content: encryptedContent,
Metadata: metadata,
CachedAt: time.Now(),
ExpiresAt: time.Now().Add(10 * time.Minute), // Cache for 10 minutes
})
log.Printf("✅ Stored UCXL content in DHT: %s (size: %d bytes)", ucxlAddress, len(encryptedContent))
eds.metrics.StoredItems++
return nil
}
// RetrieveUCXLContent retrieves and decrypts UCXL content from DHT
func (eds *EncryptedDHTStorage) RetrieveUCXLContent(ucxlAddress string) ([]byte, *storage.UCXLMetadata, error) {
startTime := time.Now()
defer func() {
eds.metrics.AverageRetrieveTime = time.Since(startTime)
eds.metrics.LastUpdate = time.Now()
}()
// Validate UCXL address format
parsedAddr, err := ucxl.Parse(ucxlAddress)
if err != nil {
if validationErr, ok := err.(*ucxl.ValidationError); ok {
return nil, nil, fmt.Errorf("UCXL-400-INVALID_ADDRESS in %s: %s (address: %s)",
validationErr.Field, validationErr.Message, validationErr.Raw)
}
return nil, nil, fmt.Errorf("invalid UCXL address: %w", err)
}
log.Printf("📥 Retrieving UCXL content: %s", parsedAddr.String())
// Get current role for audit logging
currentRole := eds.getCurrentRole()
// Role-based access policy check for retrieval
if err := eds.checkRetrieveAccessPolicy(currentRole, ucxlAddress); err != nil {
// Audit failed access attempt
if eds.config.Security.AuditLogging {
eds.auditRetrieveOperation(ucxlAddress, currentRole, false, err.Error())
}
return nil, nil, fmt.Errorf("retrieve access denied: %w", err)
}
// Check cache first
if cachedEntry := eds.getCachedEntry(ucxlAddress); cachedEntry != nil {
log.Printf("💾 Cache hit for %s", ucxlAddress)
eds.metrics.CacheHits++
// Decrypt content
decryptedContent, err := eds.crypto.DecryptWithRole(cachedEntry.Content)
if err != nil {
// If decryption fails, remove from cache and fall through to DHT
log.Printf("⚠️ Failed to decrypt cached content: %v", err)
eds.invalidateCacheEntry(ucxlAddress)
} else {
eds.metrics.DecryptionOps++
eds.metrics.RetrievedItems++
// Convert to storage.UCXLMetadata
storageMetadata := &storage.UCXLMetadata{
Address: cachedEntry.Metadata.Address,
CreatorRole: cachedEntry.Metadata.CreatorRole,
ContentType: cachedEntry.Metadata.ContentType,
CreatedAt: cachedEntry.Metadata.Timestamp,
Size: int64(cachedEntry.Metadata.Size),
Encrypted: true,
}
return decryptedContent, storageMetadata, nil
}
}
eds.metrics.CacheMisses++
// Generate DHT key
dhtKey := eds.generateDHTKey(ucxlAddress)
// Retrieve from DHT
value, err := eds.dht.GetValue(eds.ctx, dhtKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to retrieve from DHT: %w", err)
}
// Deserialize entry
var entry StorageEntry
if err := json.Unmarshal(value, &entry); err != nil {
return nil, nil, fmt.Errorf("failed to deserialize storage entry: %w", err)
}
// Check if current role can decrypt this content
canDecrypt, err := eds.crypto.CanDecryptContent(entry.Metadata.CreatorRole)
if err != nil {
return nil, nil, fmt.Errorf("failed to check decryption permission: %w", err)
}
if !canDecrypt {
return nil, nil, fmt.Errorf("current role cannot decrypt content from role: %s", entry.Metadata.CreatorRole)
}
// Decrypt content
decryptedContent, err := eds.crypto.DecryptWithRole(entry.EncryptedContent)
if err != nil {
return nil, nil, fmt.Errorf("failed to decrypt content: %w", err)
}
eds.metrics.DecryptionOps++
// Cache the entry
eds.cacheEntry(ucxlAddress, &CachedEntry{
Content: entry.EncryptedContent,
Metadata: entry.Metadata,
CachedAt: time.Now(),
ExpiresAt: time.Now().Add(10 * time.Minute),
})
log.Printf("✅ Retrieved and decrypted UCXL content: %s (size: %d bytes)", ucxlAddress, len(decryptedContent))
eds.metrics.RetrievedItems++
// Audit successful retrieval
if eds.config.Security.AuditLogging {
eds.auditRetrieveOperation(ucxlAddress, currentRole, true, "")
}
// Convert to storage.UCXLMetadata interface
storageMetadata := &storage.UCXLMetadata{
Address: entry.Metadata.Address,
CreatorRole: entry.Metadata.CreatorRole,
ContentType: entry.Metadata.ContentType,
CreatedAt: entry.Metadata.Timestamp,
Size: int64(entry.Metadata.Size),
Encrypted: true, // Always encrypted in DHT storage
}
return decryptedContent, storageMetadata, nil
}
// ListContentByRole lists all content accessible by the current role
func (eds *EncryptedDHTStorage) ListContentByRole(roleFilter string, limit int) ([]*UCXLMetadata, error) {
// This is a simplified implementation
// In a real system, you'd maintain an index or use DHT range queries
log.Printf("📋 Listing content for role: %s (limit: %d)", roleFilter, limit)
var results []*UCXLMetadata
count := 0
// For now, return cached entries that match the role filter
eds.cacheMu.RLock()
for _, entry := range eds.cache {
if count >= limit {
break
}
// Check if the role can access this content
for _, role := range entry.Metadata.EncryptedFor {
if role == roleFilter || role == "*" {
results = append(results, entry.Metadata)
count++
break
}
}
}
eds.cacheMu.RUnlock()
log.Printf("📋 Found %d content items for role %s", len(results), roleFilter)
return results, nil
}
// SearchContent searches for UCXL content by various criteria
func (eds *EncryptedDHTStorage) SearchContent(query *storage.SearchQuery) ([]*storage.UCXLMetadata, error) {
log.Printf("🔍 Searching content: %+v", query)
var results []*storage.UCXLMetadata
eds.cacheMu.RLock()
defer eds.cacheMu.RUnlock()
for _, entry := range eds.cache {
if eds.matchesQuery(entry.Metadata, query) {
// Convert to storage.UCXLMetadata
storageMetadata := &storage.UCXLMetadata{
Address: entry.Metadata.Address,
CreatorRole: entry.Metadata.CreatorRole,
ContentType: entry.Metadata.ContentType,
CreatedAt: entry.Metadata.Timestamp,
Size: int64(entry.Metadata.Size),
Encrypted: true,
}
results = append(results, storageMetadata)
if len(results) >= query.Limit {
break
}
}
}
log.Printf("🔍 Search found %d results", len(results))
return results, nil
}
// SearchQuery defines search criteria for UCXL content
type SearchQuery struct {
Agent string `json:"agent,omitempty"`
Role string `json:"role,omitempty"`
Project string `json:"project,omitempty"`
Task string `json:"task,omitempty"`
ContentType string `json:"content_type,omitempty"`
CreatedAfter time.Time `json:"created_after,omitempty"`
CreatedBefore time.Time `json:"created_before,omitempty"`
Limit int `json:"limit"`
}
// StorageEntry represents a complete DHT storage entry
type StorageEntry struct {
Metadata *UCXLMetadata `json:"metadata"`
EncryptedContent []byte `json:"encrypted_content"`
StoredBy string `json:"stored_by"`
StoredAt time.Time `json:"stored_at"`
}
// generateDHTKey generates a consistent DHT key for a UCXL address
func (eds *EncryptedDHTStorage) generateDHTKey(ucxlAddress string) string {
// Use SHA256 hash of the UCXL address as DHT key
hash := sha256.Sum256([]byte(ucxlAddress))
return "/bzzz/ucxl/" + base64.URLEncoding.EncodeToString(hash[:])
}
// getDecryptableRoles determines which roles can decrypt content from a creator
func (eds *EncryptedDHTStorage) getDecryptableRoles(creatorRole string) ([]string, error) {
roles := config.GetPredefinedRoles()
_, exists := roles[creatorRole]
if !exists {
return nil, fmt.Errorf("creator role '%s' not found", creatorRole)
}
// Start with the creator role itself
decryptableRoles := []string{creatorRole}
// Add all roles that have authority to decrypt this creator's content
for roleName, role := range roles {
if roleName == creatorRole {
continue
}
// Check if this role can decrypt the creator's content
for _, decryptableRole := range role.CanDecrypt {
if decryptableRole == creatorRole || decryptableRole == "*" {
decryptableRoles = append(decryptableRoles, roleName)
break
}
}
}
return decryptableRoles, nil
}
// cacheEntry adds an entry to the local cache
func (eds *EncryptedDHTStorage) cacheEntry(ucxlAddress string, entry *CachedEntry) {
eds.cacheMu.Lock()
defer eds.cacheMu.Unlock()
eds.cache[ucxlAddress] = entry
}
// getCachedEntry retrieves an entry from the local cache
func (eds *EncryptedDHTStorage) getCachedEntry(ucxlAddress string) *CachedEntry {
eds.cacheMu.RLock()
defer eds.cacheMu.RUnlock()
entry, exists := eds.cache[ucxlAddress]
if !exists {
return nil
}
// Check if entry has expired
if time.Now().After(entry.ExpiresAt) {
// Remove expired entry asynchronously
go eds.invalidateCacheEntry(ucxlAddress)
return nil
}
return entry
}
// invalidateCacheEntry removes an entry from the cache
func (eds *EncryptedDHTStorage) invalidateCacheEntry(ucxlAddress string) {
eds.cacheMu.Lock()
defer eds.cacheMu.Unlock()
delete(eds.cache, ucxlAddress)
}
// matchesQuery checks if metadata matches a search query
func (eds *EncryptedDHTStorage) matchesQuery(metadata *UCXLMetadata, query *storage.SearchQuery) bool {
// Parse UCXL address properly
parsedAddr, err := ucxl.Parse(metadata.Address)
if err != nil {
log.Printf("⚠️ Invalid UCXL address in search: %s", metadata.Address)
return false // Skip invalid addresses
}
// Check agent filter
if query.Agent != "" && parsedAddr.Agent != query.Agent {
return false
}
// Check role filter
if query.Role != "" && parsedAddr.Role != query.Role {
return false
}
// Check project filter
if query.Project != "" && parsedAddr.Project != query.Project {
return false
}
// Check task filter
if query.Task != "" && parsedAddr.Task != query.Task {
return false
}
// Check content type filter
if query.ContentType != "" && metadata.ContentType != query.ContentType {
return false
}
// Check date filters
if !query.CreatedAfter.IsZero() && metadata.Timestamp.Before(query.CreatedAfter) {
return false
}
if !query.CreatedBefore.IsZero() && metadata.Timestamp.After(query.CreatedBefore) {
return false
}
return true
}
// GetMetrics returns current storage metrics
func (eds *EncryptedDHTStorage) GetMetrics() map[string]interface{} {
// Update cache statistics
eds.cacheMu.RLock()
cacheSize := len(eds.cache)
eds.cacheMu.RUnlock()
metrics := *eds.metrics // Copy metrics
metrics.LastUpdate = time.Now()
// Convert to map[string]interface{} for interface compatibility
result := map[string]interface{}{
"stored_items": metrics.StoredItems,
"retrieved_items": metrics.RetrievedItems,
"cache_hits": metrics.CacheHits,
"cache_misses": metrics.CacheMisses,
"encryption_ops": metrics.EncryptionOps,
"decryption_ops": metrics.DecryptionOps,
"cache_size": cacheSize,
"last_update": metrics.LastUpdate,
}
log.Printf("📊 DHT Storage Metrics: stored=%d, retrieved=%d, cache_size=%d",
metrics.StoredItems, metrics.RetrievedItems, cacheSize)
return result
}
// CleanupCache removes expired entries from the cache
func (eds *EncryptedDHTStorage) CleanupCache() {
eds.cacheMu.Lock()
defer eds.cacheMu.Unlock()
now := time.Now()
expired := 0
for address, entry := range eds.cache {
if now.After(entry.ExpiresAt) {
delete(eds.cache, address)
expired++
}
}
if expired > 0 {
log.Printf("🧹 Cleaned up %d expired cache entries", expired)
}
}
// StartCacheCleanup starts a background goroutine to clean up expired cache entries
func (eds *EncryptedDHTStorage) StartCacheCleanup(interval time.Duration) {
ticker := time.NewTicker(interval)
go func() {
defer ticker.Stop()
for {
select {
case <-eds.ctx.Done():
return
case <-ticker.C:
eds.CleanupCache()
}
}
}()
}
// AnnounceContent announces that this node has specific UCXL content
func (eds *EncryptedDHTStorage) AnnounceContent(ucxlAddress string) error {
// Get current role for audit logging
currentRole := eds.getCurrentRole()
// Role-based access policy check for announce
if err := eds.checkAnnounceAccessPolicy(currentRole, ucxlAddress); err != nil {
// Audit failed announce attempt
if eds.config.Security.AuditLogging {
eds.auditAnnounceOperation(ucxlAddress, currentRole, false, err.Error())
}
return fmt.Errorf("announce access denied: %w", err)
}
// Create announcement
announcement := map[string]interface{}{
"node_id": eds.nodeID,
"ucxl_address": ucxlAddress,
"timestamp": time.Now(),
"peer_id": eds.host.ID().String(),
}
announcementData, err := json.Marshal(announcement)
if err != nil {
return fmt.Errorf("failed to marshal announcement: %w", err)
}
// Announce via DHT
dhtKey := "/bzzz/announcements/" + eds.generateDHTKey(ucxlAddress)
err = eds.dht.PutValue(eds.ctx, dhtKey, announcementData)
// Audit the announce operation
if eds.config.Security.AuditLogging {
if err != nil {
eds.auditAnnounceOperation(ucxlAddress, currentRole, false, err.Error())
} else {
eds.auditAnnounceOperation(ucxlAddress, currentRole, true, "")
}
}
return err
}
// DiscoverContentPeers discovers peers that have specific UCXL content
func (eds *EncryptedDHTStorage) DiscoverContentPeers(ucxlAddress string) ([]peer.ID, error) {
dhtKey := "/bzzz/announcements/" + eds.generateDHTKey(ucxlAddress)
// This is a simplified implementation
// In a real system, you'd query multiple announcement keys
value, err := eds.dht.GetValue(eds.ctx, dhtKey)
if err != nil {
return nil, fmt.Errorf("failed to discover peers: %w", err)
}
var announcement map[string]interface{}
if err := json.Unmarshal(value, &announcement); err != nil {
return nil, fmt.Errorf("failed to parse announcement: %w", err)
}
// Extract peer ID
peerIDStr, ok := announcement["peer_id"].(string)
if !ok {
return nil, fmt.Errorf("invalid peer ID in announcement")
}
peerID, err := peer.Decode(peerIDStr)
if err != nil {
return nil, fmt.Errorf("failed to decode peer ID: %w", err)
}
return []peer.ID{peerID}, nil
}
// Security policy and audit methods
// getCurrentRole gets the current role from the agent configuration
func (eds *EncryptedDHTStorage) getCurrentRole() string {
if eds.config.Agent.Role == "" {
return "unknown"
}
return eds.config.Agent.Role
}
// checkStoreAccessPolicy checks if the current role can store content
func (eds *EncryptedDHTStorage) checkStoreAccessPolicy(creatorRole, ucxlAddress, contentType string) error {
// Basic role validation
roles := config.GetPredefinedRoles()
if _, exists := roles[creatorRole]; !exists {
return fmt.Errorf("unknown creator role: %s", creatorRole)
}
// Check if role has authority to create content
role := roles[creatorRole]
if role.AuthorityLevel == config.AuthorityReadOnly {
return fmt.Errorf("role %s has read-only authority and cannot store content", creatorRole)
}
// Additional policy checks can be added here
// For now, allow all valid roles except read-only to store content
return nil
}
// checkRetrieveAccessPolicy checks if the current role can retrieve content
func (eds *EncryptedDHTStorage) checkRetrieveAccessPolicy(currentRole, ucxlAddress string) error {
// Basic role validation
roles := config.GetPredefinedRoles()
if _, exists := roles[currentRole]; !exists {
return fmt.Errorf("unknown current role: %s", currentRole)
}
// All valid roles can retrieve content (encryption handles access control)
// Additional fine-grained policies can be added here
return nil
}
// checkAnnounceAccessPolicy checks if the current role can announce content
func (eds *EncryptedDHTStorage) checkAnnounceAccessPolicy(currentRole, ucxlAddress string) error {
// Basic role validation
roles := config.GetPredefinedRoles()
if _, exists := roles[currentRole]; !exists {
return fmt.Errorf("unknown current role: %s", currentRole)
}
// Check if role has coordination or higher authority to announce
role := roles[currentRole]
if role.AuthorityLevel == config.AuthorityReadOnly || role.AuthorityLevel == config.AuthoritySuggestion {
return fmt.Errorf("role %s lacks authority to announce content", currentRole)
}
return nil
}
// auditStoreOperation logs a store operation for audit purposes
func (eds *EncryptedDHTStorage) auditStoreOperation(ucxlAddress, role, contentType string, contentSize int, success bool, errorMsg string) {
// Create audit logger if needed (in production, inject via constructor)
if eds.config.Security.AuditPath == "" {
return // No audit path configured
}
// Log to file or audit system
auditEntry := map[string]interface{}{
"timestamp": time.Now(),
"operation": "store",
"node_id": eds.nodeID,
"ucxl_address": ucxlAddress,
"role": role,
"content_type": contentType,
"content_size": contentSize,
"success": success,
"error_message": errorMsg,
"audit_trail": fmt.Sprintf("DHT-STORE-%s-%d", ucxlAddress, time.Now().Unix()),
}
log.Printf("🔍 AUDIT STORE: %+v", auditEntry)
// In production, write to audit log file or send to audit service
// For now, just log to console and update metrics
if success {
eds.metrics.StoredItems++
}
}
// auditRetrieveOperation logs a retrieve operation for audit purposes
func (eds *EncryptedDHTStorage) auditRetrieveOperation(ucxlAddress, role string, success bool, errorMsg string) {
// Create audit logger if needed
if eds.config.Security.AuditPath == "" {
return // No audit path configured
}
auditEntry := map[string]interface{}{
"timestamp": time.Now(),
"operation": "retrieve",
"node_id": eds.nodeID,
"ucxl_address": ucxlAddress,
"role": role,
"success": success,
"error_message": errorMsg,
"audit_trail": fmt.Sprintf("DHT-RETRIEVE-%s-%d", ucxlAddress, time.Now().Unix()),
}
log.Printf("🔍 AUDIT RETRIEVE: %+v", auditEntry)
// In production, write to audit log file or send to audit service
if success {
eds.metrics.RetrievedItems++
}
}
// auditAnnounceOperation logs an announce operation for audit purposes
func (eds *EncryptedDHTStorage) auditAnnounceOperation(ucxlAddress, role string, success bool, errorMsg string) {
// Create audit logger if needed
if eds.config.Security.AuditPath == "" {
return // No audit path configured
}
auditEntry := map[string]interface{}{
"timestamp": time.Now(),
"operation": "announce",
"node_id": eds.nodeID,
"ucxl_address": ucxlAddress,
"role": role,
"success": success,
"error_message": errorMsg,
"audit_trail": fmt.Sprintf("DHT-ANNOUNCE-%s-%d", ucxlAddress, time.Now().Unix()),
"peer_id": eds.host.ID().String(),
}
log.Printf("🔍 AUDIT ANNOUNCE: %+v", auditEntry)
// In production, write to audit log file or send to audit service
}

View File

@@ -0,0 +1,560 @@
package dht
import (
"context"
"testing"
"time"
"chorus.services/bzzz/pkg/config"
)
// TestDHTSecurityPolicyEnforcement tests security policy enforcement in DHT operations
func TestDHTSecurityPolicyEnforcement(t *testing.T) {
ctx := context.Background()
testCases := []struct {
name string
currentRole string
operation string
ucxlAddress string
contentType string
expectSuccess bool
expectedError string
}{
// Store operation tests
{
name: "admin_can_store_all_content",
currentRole: "admin",
operation: "store",
ucxlAddress: "agent1:admin:system:security_audit",
contentType: "decision",
expectSuccess: true,
},
{
name: "backend_developer_can_store_backend_content",
currentRole: "backend_developer",
operation: "store",
ucxlAddress: "agent1:backend_developer:api:endpoint_design",
contentType: "suggestion",
expectSuccess: true,
},
{
name: "readonly_role_cannot_store",
currentRole: "readonly_user",
operation: "store",
ucxlAddress: "agent1:readonly_user:project:observation",
contentType: "suggestion",
expectSuccess: false,
expectedError: "read-only authority",
},
{
name: "unknown_role_cannot_store",
currentRole: "invalid_role",
operation: "store",
ucxlAddress: "agent1:invalid_role:project:task",
contentType: "decision",
expectSuccess: false,
expectedError: "unknown creator role",
},
// Retrieve operation tests
{
name: "any_valid_role_can_retrieve",
currentRole: "qa_engineer",
operation: "retrieve",
ucxlAddress: "agent1:backend_developer:api:test_data",
expectSuccess: true,
},
{
name: "unknown_role_cannot_retrieve",
currentRole: "nonexistent_role",
operation: "retrieve",
ucxlAddress: "agent1:backend_developer:api:test_data",
expectSuccess: false,
expectedError: "unknown current role",
},
// Announce operation tests
{
name: "coordination_role_can_announce",
currentRole: "senior_software_architect",
operation: "announce",
ucxlAddress: "agent1:senior_software_architect:architecture:blueprint",
expectSuccess: true,
},
{
name: "decision_role_can_announce",
currentRole: "security_expert",
operation: "announce",
ucxlAddress: "agent1:security_expert:security:policy",
expectSuccess: true,
},
{
name: "suggestion_role_cannot_announce",
currentRole: "suggestion_only_role",
operation: "announce",
ucxlAddress: "agent1:suggestion_only_role:project:idea",
expectSuccess: false,
expectedError: "lacks authority",
},
{
name: "readonly_role_cannot_announce",
currentRole: "readonly_user",
operation: "announce",
ucxlAddress: "agent1:readonly_user:project:observation",
expectSuccess: false,
expectedError: "lacks authority",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create test configuration
cfg := &config.Config{
Agent: config.AgentConfig{
ID: "test-agent",
Role: tc.currentRole,
},
Security: config.SecurityConfig{
KeyRotationDays: 90,
AuditLogging: true,
AuditPath: "/tmp/test-security-audit.log",
},
}
// Create mock encrypted storage
eds := createMockEncryptedStorage(ctx, cfg)
var err error
switch tc.operation {
case "store":
err = eds.checkStoreAccessPolicy(tc.currentRole, tc.ucxlAddress, tc.contentType)
case "retrieve":
err = eds.checkRetrieveAccessPolicy(tc.currentRole, tc.ucxlAddress)
case "announce":
err = eds.checkAnnounceAccessPolicy(tc.currentRole, tc.ucxlAddress)
}
if tc.expectSuccess {
if err != nil {
t.Errorf("Expected %s operation to succeed for role %s, but got error: %v",
tc.operation, tc.currentRole, err)
}
} else {
if err == nil {
t.Errorf("Expected %s operation to fail for role %s, but it succeeded",
tc.operation, tc.currentRole)
}
if tc.expectedError != "" && !containsSubstring(err.Error(), tc.expectedError) {
t.Errorf("Expected error to contain '%s', got '%s'", tc.expectedError, err.Error())
}
}
})
}
}
// TestDHTAuditLogging tests comprehensive audit logging for DHT operations
func TestDHTAuditLogging(t *testing.T) {
ctx := context.Background()
testCases := []struct {
name string
operation string
role string
ucxlAddress string
success bool
errorMsg string
expectAudit bool
}{
{
name: "successful_store_operation",
operation: "store",
role: "backend_developer",
ucxlAddress: "agent1:backend_developer:api:user_service",
success: true,
expectAudit: true,
},
{
name: "failed_store_operation",
operation: "store",
role: "readonly_user",
ucxlAddress: "agent1:readonly_user:project:readonly_attempt",
success: false,
errorMsg: "read-only authority",
expectAudit: true,
},
{
name: "successful_retrieve_operation",
operation: "retrieve",
role: "frontend_developer",
ucxlAddress: "agent1:backend_developer:api:user_data",
success: true,
expectAudit: true,
},
{
name: "successful_announce_operation",
operation: "announce",
role: "senior_software_architect",
ucxlAddress: "agent1:senior_software_architect:architecture:system_design",
success: true,
expectAudit: true,
},
{
name: "audit_disabled_no_logging",
operation: "store",
role: "backend_developer",
ucxlAddress: "agent1:backend_developer:api:no_audit",
success: true,
expectAudit: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create configuration with audit logging
cfg := &config.Config{
Agent: config.AgentConfig{
ID: "test-agent",
Role: tc.role,
},
Security: config.SecurityConfig{
KeyRotationDays: 90,
AuditLogging: tc.expectAudit,
AuditPath: "/tmp/test-dht-audit.log",
},
}
// Create mock encrypted storage
eds := createMockEncryptedStorage(ctx, cfg)
// Capture audit output
auditCaptured := false
// Simulate audit operation
switch tc.operation {
case "store":
// Mock the audit function call
if tc.expectAudit && cfg.Security.AuditLogging {
eds.auditStoreOperation(tc.ucxlAddress, tc.role, "test-content", 1024, tc.success, tc.errorMsg)
auditCaptured = true
}
case "retrieve":
if tc.expectAudit && cfg.Security.AuditLogging {
eds.auditRetrieveOperation(tc.ucxlAddress, tc.role, tc.success, tc.errorMsg)
auditCaptured = true
}
case "announce":
if tc.expectAudit && cfg.Security.AuditLogging {
eds.auditAnnounceOperation(tc.ucxlAddress, tc.role, tc.success, tc.errorMsg)
auditCaptured = true
}
}
// Verify audit logging behavior
if tc.expectAudit && !auditCaptured {
t.Errorf("Expected audit logging for %s operation but none was captured", tc.operation)
}
if !tc.expectAudit && auditCaptured {
t.Errorf("Expected no audit logging for %s operation but audit was captured", tc.operation)
}
})
}
}
// TestSecurityConfigIntegration tests integration with SecurityConfig
func TestSecurityConfigIntegration(t *testing.T) {
ctx := context.Background()
testConfigs := []struct {
name string
auditLogging bool
auditPath string
expectAuditWork bool
}{
{
name: "audit_enabled_with_path",
auditLogging: true,
auditPath: "/tmp/test-audit-enabled.log",
expectAuditWork: true,
},
{
name: "audit_disabled",
auditLogging: false,
auditPath: "/tmp/test-audit-disabled.log",
expectAuditWork: false,
},
{
name: "audit_enabled_no_path",
auditLogging: true,
auditPath: "",
expectAuditWork: false,
},
}
for _, tc := range testConfigs {
t.Run(tc.name, func(t *testing.T) {
cfg := &config.Config{
Agent: config.AgentConfig{
ID: "test-agent",
Role: "backend_developer",
},
Security: config.SecurityConfig{
KeyRotationDays: 90,
AuditLogging: tc.auditLogging,
AuditPath: tc.auditPath,
},
}
eds := createMockEncryptedStorage(ctx, cfg)
// Test audit function behavior with different configurations
auditWorked := func() bool {
if !cfg.Security.AuditLogging || cfg.Security.AuditPath == "" {
return false
}
return true
}()
if auditWorked != tc.expectAuditWork {
t.Errorf("Expected audit to work: %v, but got: %v", tc.expectAuditWork, auditWorked)
}
})
}
}
// TestRoleAuthorityHierarchy tests role authority hierarchy enforcement
func TestRoleAuthorityHierarchy(t *testing.T) {
ctx := context.Background()
// Test role authority levels for different operations
authorityTests := []struct {
role string
authorityLevel config.AuthorityLevel
canStore bool
canRetrieve bool
canAnnounce bool
}{
{
role: "admin",
authorityLevel: config.AuthorityMaster,
canStore: true,
canRetrieve: true,
canAnnounce: true,
},
{
role: "senior_software_architect",
authorityLevel: config.AuthorityDecision,
canStore: true,
canRetrieve: true,
canAnnounce: true,
},
{
role: "security_expert",
authorityLevel: config.AuthorityCoordination,
canStore: true,
canRetrieve: true,
canAnnounce: true,
},
{
role: "backend_developer",
authorityLevel: config.AuthoritySuggestion,
canStore: true,
canRetrieve: true,
canAnnounce: false,
},
}
for _, tt := range authorityTests {
t.Run(tt.role+"_authority_test", func(t *testing.T) {
cfg := &config.Config{
Agent: config.AgentConfig{
ID: "test-agent",
Role: tt.role,
},
Security: config.SecurityConfig{
KeyRotationDays: 90,
AuditLogging: true,
AuditPath: "/tmp/test-authority.log",
},
}
eds := createMockEncryptedStorage(ctx, cfg)
// Test store permission
storeErr := eds.checkStoreAccessPolicy(tt.role, "test:address", "content")
if tt.canStore && storeErr != nil {
t.Errorf("Role %s should be able to store but got error: %v", tt.role, storeErr)
}
if !tt.canStore && storeErr == nil {
t.Errorf("Role %s should not be able to store but operation succeeded", tt.role)
}
// Test retrieve permission
retrieveErr := eds.checkRetrieveAccessPolicy(tt.role, "test:address")
if tt.canRetrieve && retrieveErr != nil {
t.Errorf("Role %s should be able to retrieve but got error: %v", tt.role, retrieveErr)
}
if !tt.canRetrieve && retrieveErr == nil {
t.Errorf("Role %s should not be able to retrieve but operation succeeded", tt.role)
}
// Test announce permission
announceErr := eds.checkAnnounceAccessPolicy(tt.role, "test:address")
if tt.canAnnounce && announceErr != nil {
t.Errorf("Role %s should be able to announce but got error: %v", tt.role, announceErr)
}
if !tt.canAnnounce && announceErr == nil {
t.Errorf("Role %s should not be able to announce but operation succeeded", tt.role)
}
})
}
}
// TestSecurityMetrics tests security-related metrics
func TestSecurityMetrics(t *testing.T) {
ctx := context.Background()
cfg := &config.Config{
Agent: config.AgentConfig{
ID: "test-agent",
Role: "backend_developer",
},
Security: config.SecurityConfig{
KeyRotationDays: 90,
AuditLogging: true,
AuditPath: "/tmp/test-metrics.log",
},
}
eds := createMockEncryptedStorage(ctx, cfg)
// Simulate some operations to generate metrics
for i := 0; i < 5; i++ {
eds.metrics.StoredItems++
eds.metrics.RetrievedItems++
eds.metrics.EncryptionOps++
eds.metrics.DecryptionOps++
}
metrics := eds.GetMetrics()
expectedMetrics := map[string]int64{
"stored_items": 5,
"retrieved_items": 5,
"encryption_ops": 5,
"decryption_ops": 5,
}
for metricName, expectedValue := range expectedMetrics {
if actualValue, ok := metrics[metricName]; !ok {
t.Errorf("Expected metric %s to be present in metrics", metricName)
} else if actualValue != expectedValue {
t.Errorf("Expected %s to be %d, got %v", metricName, expectedValue, actualValue)
}
}
}
// Helper functions
func createMockEncryptedStorage(ctx context.Context, cfg *config.Config) *EncryptedDHTStorage {
return &EncryptedDHTStorage{
ctx: ctx,
config: cfg,
nodeID: "test-node-id",
cache: make(map[string]*CachedEntry),
metrics: &StorageMetrics{
LastUpdate: time.Now(),
},
}
}
func containsSubstring(str, substr string) bool {
if len(substr) == 0 {
return true
}
if len(str) < len(substr) {
return false
}
for i := 0; i <= len(str)-len(substr); i++ {
if str[i:i+len(substr)] == substr {
return true
}
}
return false
}
// Benchmarks for security performance
func BenchmarkSecurityPolicyChecks(b *testing.B) {
ctx := context.Background()
cfg := &config.Config{
Agent: config.AgentConfig{
ID: "bench-agent",
Role: "backend_developer",
},
Security: config.SecurityConfig{
KeyRotationDays: 90,
AuditLogging: true,
AuditPath: "/tmp/bench-security.log",
},
}
eds := createMockEncryptedStorage(ctx, cfg)
b.ResetTimer()
b.Run("store_policy_check", func(b *testing.B) {
for i := 0; i < b.N; i++ {
eds.checkStoreAccessPolicy("backend_developer", "test:address", "content")
}
})
b.Run("retrieve_policy_check", func(b *testing.B) {
for i := 0; i < b.N; i++ {
eds.checkRetrieveAccessPolicy("backend_developer", "test:address")
}
})
b.Run("announce_policy_check", func(b *testing.B) {
for i := 0; i < b.N; i++ {
eds.checkAnnounceAccessPolicy("senior_software_architect", "test:address")
}
})
}
func BenchmarkAuditOperations(b *testing.B) {
ctx := context.Background()
cfg := &config.Config{
Agent: config.AgentConfig{
ID: "bench-agent",
Role: "backend_developer",
},
Security: config.SecurityConfig{
KeyRotationDays: 90,
AuditLogging: true,
AuditPath: "/tmp/bench-audit.log",
},
}
eds := createMockEncryptedStorage(ctx, cfg)
b.ResetTimer()
b.Run("store_audit", func(b *testing.B) {
for i := 0; i < b.N; i++ {
eds.auditStoreOperation("test:address", "backend_developer", "content", 1024, true, "")
}
})
b.Run("retrieve_audit", func(b *testing.B) {
for i := 0; i < b.N; i++ {
eds.auditRetrieveOperation("test:address", "backend_developer", true, "")
}
})
b.Run("announce_audit", func(b *testing.B) {
for i := 0; i < b.N; i++ {
eds.auditAnnounceOperation("test:address", "backend_developer", true, "")
}
})
}

593
pkg/dht/hybrid_dht.go Normal file
View File

@@ -0,0 +1,593 @@
package dht
import (
"context"
"fmt"
"sync"
"time"
"chorus.services/bzzz/pkg/config"
"github.com/libp2p/go-libp2p/core/peer"
)
// HybridDHT provides a switchable interface between mock and real DHT implementations
type HybridDHT struct {
mockDHT *MockDHTInterface
realDHT DHT
config *config.HybridConfig
// State management
currentBackend string
fallbackActive bool
healthStatus map[string]*BackendHealth
// Synchronization
mu sync.RWMutex
// Monitoring
metrics *HybridMetrics
logger Logger
}
// BackendHealth tracks health status of DHT backends
type BackendHealth struct {
Backend string `json:"backend"`
Status HealthStatus `json:"status"`
LastCheck time.Time `json:"last_check"`
ErrorCount int `json:"error_count"`
Latency time.Duration `json:"latency"`
Consecutive int `json:"consecutive_failures"`
}
type HealthStatus string
const (
HealthStatusHealthy HealthStatus = "healthy"
HealthStatusDegraded HealthStatus = "degraded"
HealthStatusFailed HealthStatus = "failed"
)
// HybridMetrics tracks hybrid DHT performance and behavior
type HybridMetrics struct {
mu sync.RWMutex
MockRequests uint64 `json:"mock_requests"`
RealRequests uint64 `json:"real_requests"`
FallbackEvents uint64 `json:"fallback_events"`
RecoveryEvents uint64 `json:"recovery_events"`
MockLatency time.Duration `json:"mock_latency_avg"`
RealLatency time.Duration `json:"real_latency_avg"`
MockErrorRate float64 `json:"mock_error_rate"`
RealErrorRate float64 `json:"real_error_rate"`
TotalOperations uint64 `json:"total_operations"`
LastMetricUpdate time.Time `json:"last_update"`
}
// Logger interface for structured logging
type Logger interface {
Info(msg string, fields ...interface{})
Warn(msg string, fields ...interface{})
Error(msg string, fields ...interface{})
Debug(msg string, fields ...interface{})
}
// NewHybridDHT creates a new hybrid DHT instance
func NewHybridDHT(config *config.HybridConfig, logger Logger) (*HybridDHT, error) {
hybrid := &HybridDHT{
config: config,
logger: logger,
healthStatus: make(map[string]*BackendHealth),
metrics: &HybridMetrics{},
}
// Initialize mock DHT (always available)
mockDHT := NewMockDHTInterface()
hybrid.mockDHT = mockDHT
hybrid.healthStatus["mock"] = &BackendHealth{
Backend: "mock",
Status: HealthStatusHealthy,
LastCheck: time.Now(),
}
// Initialize real DHT if enabled
if config.IsRealDHTEnabled() {
realDHT, err := NewRealDHT(config)
if err != nil {
logger.Warn("Failed to initialize real DHT, falling back to mock", "error", err)
hybrid.currentBackend = "mock"
hybrid.fallbackActive = true
} else {
hybrid.realDHT = realDHT
hybrid.currentBackend = "real"
hybrid.healthStatus["real"] = &BackendHealth{
Backend: "real",
Status: HealthStatusHealthy,
LastCheck: time.Now(),
}
}
} else {
hybrid.currentBackend = "mock"
}
// Start health monitoring
go hybrid.startHealthMonitoring()
go hybrid.startMetricsCollection()
logger.Info("Hybrid DHT initialized",
"backend", hybrid.currentBackend,
"fallback_enabled", config.IsFallbackEnabled())
return hybrid, nil
}
// PutValue stores a key-value pair using the current backend
func (h *HybridDHT) PutValue(ctx context.Context, key string, value []byte) error {
start := time.Now()
backend := h.getCurrentBackend()
var err error
switch backend {
case "mock":
err = h.mockDHT.PutValue(ctx, key, value)
h.updateMetrics("mock", start, err)
case "real":
err = h.realDHT.PutValue(ctx, key, value)
h.updateMetrics("real", start, err)
// Handle fallback on error
if err != nil && h.config.IsFallbackEnabled() {
h.logger.Warn("Real DHT PutValue failed, trying fallback", "key", key, "error", err)
h.recordBackendError("real")
// Try mock fallback
fallbackErr := h.mockDHT.PutValue(ctx, key, value)
h.updateMetrics("mock", start, fallbackErr)
if fallbackErr == nil {
h.triggerFallback("real", "mock")
return nil
}
return fmt.Errorf("both real and mock DHT failed: real=%w, mock=%v", err, fallbackErr)
}
}
if err != nil {
h.recordBackendError(backend)
} else {
h.recordBackendSuccess(backend)
}
return err
}
// GetValue retrieves a value by key using the current backend
func (h *HybridDHT) GetValue(ctx context.Context, key string) ([]byte, error) {
start := time.Now()
backend := h.getCurrentBackend()
var value []byte
var err error
switch backend {
case "mock":
value, err = h.mockDHT.GetValue(ctx, key)
h.updateMetrics("mock", start, err)
case "real":
value, err = h.realDHT.GetValue(ctx, key)
h.updateMetrics("real", start, err)
// Handle fallback on error
if err != nil && h.config.IsFallbackEnabled() {
h.logger.Warn("Real DHT GetValue failed, trying fallback", "key", key, "error", err)
h.recordBackendError("real")
// Try mock fallback
fallbackValue, fallbackErr := h.mockDHT.GetValue(ctx, key)
h.updateMetrics("mock", start, fallbackErr)
if fallbackErr == nil {
h.triggerFallback("real", "mock")
return fallbackValue, nil
}
return nil, fmt.Errorf("both real and mock DHT failed: real=%w, mock=%v", err, fallbackErr)
}
}
if err != nil {
h.recordBackendError(backend)
} else {
h.recordBackendSuccess(backend)
}
return value, err
}
// Provide announces that this node provides a value for the given key
func (h *HybridDHT) Provide(ctx context.Context, key string) error {
start := time.Now()
backend := h.getCurrentBackend()
var err error
switch backend {
case "mock":
err = h.mockDHT.Provide(ctx, key)
h.updateMetrics("mock", start, err)
case "real":
err = h.realDHT.Provide(ctx, key)
h.updateMetrics("real", start, err)
// Handle fallback on error
if err != nil && h.config.IsFallbackEnabled() {
h.logger.Warn("Real DHT Provide failed, trying fallback", "key", key, "error", err)
h.recordBackendError("real")
// Try mock fallback
fallbackErr := h.mockDHT.Provide(ctx, key)
h.updateMetrics("mock", start, fallbackErr)
if fallbackErr == nil {
h.triggerFallback("real", "mock")
return nil
}
return fmt.Errorf("both real and mock DHT failed: real=%w, mock=%v", err, fallbackErr)
}
}
if err != nil {
h.recordBackendError(backend)
} else {
h.recordBackendSuccess(backend)
}
return err
}
// FindProviders finds providers for the given key
func (h *HybridDHT) FindProviders(ctx context.Context, key string, limit int) ([]peer.AddrInfo, error) {
start := time.Now()
backend := h.getCurrentBackend()
var providers []peer.AddrInfo
var err error
switch backend {
case "mock":
providers, err = h.mockDHT.FindProviders(ctx, key, limit)
h.updateMetrics("mock", start, err)
case "real":
providers, err = h.realDHT.FindProviders(ctx, key, limit)
h.updateMetrics("real", start, err)
// Handle fallback on error
if err != nil && h.config.IsFallbackEnabled() {
h.logger.Warn("Real DHT FindProviders failed, trying fallback", "key", key, "error", err)
h.recordBackendError("real")
// Try mock fallback
fallbackProviders, fallbackErr := h.mockDHT.FindProviders(ctx, key, limit)
h.updateMetrics("mock", start, fallbackErr)
if fallbackErr == nil {
h.triggerFallback("real", "mock")
return fallbackProviders, nil
}
return nil, fmt.Errorf("both real and mock DHT failed: real=%w, mock=%v", err, fallbackErr)
}
}
if err != nil {
h.recordBackendError(backend)
} else {
h.recordBackendSuccess(backend)
}
return providers, err
}
// GetStats returns statistics from the current backend
func (h *HybridDHT) GetStats() DHTStats {
backend := h.getCurrentBackend()
switch backend {
case "mock":
return h.mockDHT.GetStats()
case "real":
if h.realDHT != nil {
return h.realDHT.GetStats()
}
fallthrough
default:
return h.mockDHT.GetStats()
}
}
// GetHybridMetrics returns hybrid-specific metrics
func (h *HybridDHT) GetHybridMetrics() *HybridMetrics {
h.metrics.mu.RLock()
defer h.metrics.mu.RUnlock()
// Return a copy to avoid concurrent access issues
metrics := *h.metrics
return &metrics
}
// GetBackendHealth returns health status for all backends
func (h *HybridDHT) GetBackendHealth() map[string]*BackendHealth {
h.mu.RLock()
defer h.mu.RUnlock()
// Return a deep copy
health := make(map[string]*BackendHealth)
for k, v := range h.healthStatus {
healthCopy := *v
health[k] = &healthCopy
}
return health
}
// SwitchBackend manually switches to a specific backend
func (h *HybridDHT) SwitchBackend(backend string) error {
h.mu.Lock()
defer h.mu.Unlock()
switch backend {
case "mock":
if h.mockDHT == nil {
return fmt.Errorf("mock DHT not available")
}
h.currentBackend = "mock"
h.logger.Info("Manually switched to mock DHT")
case "real":
if h.realDHT == nil {
return fmt.Errorf("real DHT not available")
}
h.currentBackend = "real"
h.fallbackActive = false
h.logger.Info("Manually switched to real DHT")
default:
return fmt.Errorf("unknown backend: %s", backend)
}
return nil
}
// Close shuts down the hybrid DHT
func (h *HybridDHT) Close() error {
h.logger.Info("Shutting down hybrid DHT")
var errors []error
if h.realDHT != nil {
if closer, ok := h.realDHT.(interface{ Close() error }); ok {
if err := closer.Close(); err != nil {
errors = append(errors, fmt.Errorf("real DHT close error: %w", err))
}
}
}
if h.mockDHT != nil {
if err := h.mockDHT.Close(); err != nil {
errors = append(errors, fmt.Errorf("mock DHT close error: %w", err))
}
}
if len(errors) > 0 {
return fmt.Errorf("errors during close: %v", errors)
}
return nil
}
// Private methods
func (h *HybridDHT) getCurrentBackend() string {
h.mu.RLock()
defer h.mu.RUnlock()
return h.currentBackend
}
func (h *HybridDHT) triggerFallback(from, to string) {
h.mu.Lock()
defer h.mu.Unlock()
if h.currentBackend != to {
h.currentBackend = to
h.fallbackActive = true
h.metrics.mu.Lock()
h.metrics.FallbackEvents++
h.metrics.mu.Unlock()
h.logger.Warn("Fallback triggered", "from", from, "to", to)
}
}
func (h *HybridDHT) recordBackendError(backend string) {
h.mu.Lock()
defer h.mu.Unlock()
if health, exists := h.healthStatus[backend]; exists {
health.ErrorCount++
health.Consecutive++
health.LastCheck = time.Now()
// Update status based on consecutive failures
if health.Consecutive >= 3 {
health.Status = HealthStatusFailed
} else if health.Consecutive >= 1 {
health.Status = HealthStatusDegraded
}
}
}
func (h *HybridDHT) recordBackendSuccess(backend string) {
h.mu.Lock()
defer h.mu.Unlock()
if health, exists := h.healthStatus[backend]; exists {
health.Consecutive = 0 // Reset consecutive failures
health.LastCheck = time.Now()
health.Status = HealthStatusHealthy
// Trigger recovery if we were in fallback mode
if h.fallbackActive && backend == "real" && h.config.IsRealDHTEnabled() {
h.currentBackend = "real"
h.fallbackActive = false
h.metrics.mu.Lock()
h.metrics.RecoveryEvents++
h.metrics.mu.Unlock()
h.logger.Info("Recovery triggered, switched back to real DHT")
}
}
}
func (h *HybridDHT) updateMetrics(backend string, start time.Time, err error) {
h.metrics.mu.Lock()
defer h.metrics.mu.Unlock()
latency := time.Since(start)
h.metrics.TotalOperations++
h.metrics.LastMetricUpdate = time.Now()
switch backend {
case "mock":
h.metrics.MockRequests++
h.metrics.MockLatency = h.updateAverageLatency(h.metrics.MockLatency, latency, h.metrics.MockRequests)
if err != nil {
h.metrics.MockErrorRate = h.updateErrorRate(h.metrics.MockErrorRate, true, h.metrics.MockRequests)
} else {
h.metrics.MockErrorRate = h.updateErrorRate(h.metrics.MockErrorRate, false, h.metrics.MockRequests)
}
case "real":
h.metrics.RealRequests++
h.metrics.RealLatency = h.updateAverageLatency(h.metrics.RealLatency, latency, h.metrics.RealRequests)
if err != nil {
h.metrics.RealErrorRate = h.updateErrorRate(h.metrics.RealErrorRate, true, h.metrics.RealRequests)
} else {
h.metrics.RealErrorRate = h.updateErrorRate(h.metrics.RealErrorRate, false, h.metrics.RealRequests)
}
}
}
func (h *HybridDHT) updateAverageLatency(currentAvg, newLatency time.Duration, count uint64) time.Duration {
if count <= 1 {
return newLatency
}
// Exponential moving average with weight based on count
weight := 1.0 / float64(count)
return time.Duration(float64(currentAvg)*(1-weight) + float64(newLatency)*weight)
}
func (h *HybridDHT) updateErrorRate(currentRate float64, isError bool, count uint64) float64 {
if count <= 1 {
if isError {
return 1.0
}
return 0.0
}
// Exponential moving average for error rate
weight := 1.0 / float64(count)
errorValue := 0.0
if isError {
errorValue = 1.0
}
return currentRate*(1-weight) + errorValue*weight
}
func (h *HybridDHT) startHealthMonitoring() {
ticker := time.NewTicker(h.config.DHT.HealthCheckInterval)
defer ticker.Stop()
for range ticker.C {
h.performHealthChecks()
}
}
func (h *HybridDHT) startMetricsCollection() {
ticker := time.NewTicker(h.config.Monitoring.MetricsInterval)
defer ticker.Stop()
for range ticker.C {
h.collectAndLogMetrics()
}
}
func (h *HybridDHT) performHealthChecks() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Health check for real DHT
if h.realDHT != nil {
start := time.Now()
_, err := h.realDHT.GetValue(ctx, "health-check-key")
h.mu.Lock()
if health, exists := h.healthStatus["real"]; exists {
health.LastCheck = time.Now()
health.Latency = time.Since(start)
if err != nil {
health.ErrorCount++
health.Consecutive++
if health.Consecutive >= 3 {
health.Status = HealthStatusFailed
} else {
health.Status = HealthStatusDegraded
}
} else {
health.Consecutive = 0
health.Status = HealthStatusHealthy
}
}
h.mu.Unlock()
}
// Health check for mock DHT (should always be healthy)
h.mu.Lock()
if health, exists := h.healthStatus["mock"]; exists {
health.LastCheck = time.Now()
health.Status = HealthStatusHealthy
health.Latency = 1 * time.Millisecond // Mock is always fast
}
h.mu.Unlock()
}
func (h *HybridDHT) collectAndLogMetrics() {
metrics := h.GetHybridMetrics()
health := h.GetBackendHealth()
h.logger.Info("Hybrid DHT metrics",
"current_backend", h.getCurrentBackend(),
"fallback_active", h.fallbackActive,
"mock_requests", metrics.MockRequests,
"real_requests", metrics.RealRequests,
"fallback_events", metrics.FallbackEvents,
"recovery_events", metrics.RecoveryEvents,
"mock_latency_ms", metrics.MockLatency.Milliseconds(),
"real_latency_ms", metrics.RealLatency.Milliseconds(),
"mock_error_rate", metrics.MockErrorRate,
"real_error_rate", metrics.RealErrorRate,
"total_operations", metrics.TotalOperations)
// Log health status
for backend, healthStatus := range health {
h.logger.Debug("Backend health",
"backend", backend,
"status", healthStatus.Status,
"error_count", healthStatus.ErrorCount,
"consecutive_failures", healthStatus.Consecutive,
"latency_ms", healthStatus.Latency.Milliseconds())
}
}

100
pkg/dht/interfaces.go Normal file
View File

@@ -0,0 +1,100 @@
package dht
import (
"context"
"github.com/libp2p/go-libp2p/core/peer"
)
// DHT defines the common interface for all DHT implementations
type DHT interface {
// Core DHT operations
PutValue(ctx context.Context, key string, value []byte) error
GetValue(ctx context.Context, key string) ([]byte, error)
Provide(ctx context.Context, key string) error
FindProviders(ctx context.Context, key string, limit int) ([]peer.AddrInfo, error)
// Statistics and monitoring
GetStats() DHTStats
}
// ReplicatedDHT extends DHT with replication capabilities
type ReplicatedDHT interface {
DHT
// Replication management
AddContentForReplication(key string, size int64, priority int) error
RemoveContentFromReplication(key string) error
GetReplicationStatus(key string) (*ReplicationStatus, error)
GetReplicationMetrics() *ReplicationMetrics
// Provider management
FindContentProviders(ctx context.Context, key string, limit int) ([]ProviderInfo, error)
ProvideContent(key string) error
}
// MockDHTInterface wraps MockDHT to implement the DHT interface
type MockDHTInterface struct {
mock *MockDHT
}
// NewMockDHTInterface creates a new MockDHTInterface
func NewMockDHTInterface() *MockDHTInterface {
return &MockDHTInterface{
mock: NewMockDHT(),
}
}
// PutValue implements DHT interface
func (m *MockDHTInterface) PutValue(ctx context.Context, key string, value []byte) error {
return m.mock.PutValue(ctx, key, value)
}
// GetValue implements DHT interface
func (m *MockDHTInterface) GetValue(ctx context.Context, key string) ([]byte, error) {
return m.mock.GetValue(ctx, key)
}
// Provide implements DHT interface
func (m *MockDHTInterface) Provide(ctx context.Context, key string) error {
return m.mock.Provide(ctx, key)
}
// FindProviders implements DHT interface
func (m *MockDHTInterface) FindProviders(ctx context.Context, key string, limit int) ([]peer.AddrInfo, error) {
providers, err := m.mock.FindProviders(ctx, key, limit)
if err != nil {
return nil, err
}
// Convert string peer IDs to peer.AddrInfo
result := make([]peer.AddrInfo, 0, len(providers))
for _, providerStr := range providers {
// For mock DHT, create minimal AddrInfo from string ID
peerID, err := peer.Decode(providerStr)
if err != nil {
// If decode fails, skip this provider
continue
}
result = append(result, peer.AddrInfo{
ID: peerID,
})
}
return result, nil
}
// GetStats implements DHT interface
func (m *MockDHTInterface) GetStats() DHTStats {
return m.mock.GetStats()
}
// Expose underlying mock for testing
func (m *MockDHTInterface) Mock() *MockDHT {
return m.mock
}
// Close implements a close method for MockDHTInterface
func (m *MockDHTInterface) Close() error {
// Mock DHT doesn't need cleanup, return nil
return nil
}

262
pkg/dht/mock_dht.go Normal file
View File

@@ -0,0 +1,262 @@
package dht
import (
"context"
"fmt"
"math/rand"
"sync"
"time"
)
// DHTStats represents common DHT statistics across implementations
type DHTStats struct {
TotalKeys int `json:"total_keys"`
TotalPeers int `json:"total_peers"`
Latency time.Duration `json:"latency"`
ErrorCount int `json:"error_count"`
ErrorRate float64 `json:"error_rate"`
Uptime time.Duration `json:"uptime"`
}
// MockDHT implements the DHT interface for testing purposes
// It provides the same interface as the real DHT but operates in-memory
type MockDHT struct {
storage map[string][]byte
providers map[string][]string // key -> list of peer IDs
peers map[string]*MockPeer
latency time.Duration
failureRate float64
mutex sync.RWMutex
}
type MockPeer struct {
ID string
Address string
Online bool
}
// NewMockDHT creates a new mock DHT instance
func NewMockDHT() *MockDHT {
return &MockDHT{
storage: make(map[string][]byte),
providers: make(map[string][]string),
peers: make(map[string]*MockPeer),
latency: 10 * time.Millisecond, // Default 10ms latency
failureRate: 0.0, // No failures by default
}
}
// SetLatency configures network latency simulation
func (m *MockDHT) SetLatency(latency time.Duration) {
m.latency = latency
}
// SetFailureRate configures failure simulation (0.0 = no failures, 1.0 = always fail)
func (m *MockDHT) SetFailureRate(rate float64) {
m.failureRate = rate
}
// simulateNetworkConditions applies latency and potential failures
func (m *MockDHT) simulateNetworkConditions(ctx context.Context) error {
// Check for context cancellation
if ctx.Err() != nil {
return ctx.Err()
}
// Simulate network latency
if m.latency > 0 {
select {
case <-time.After(m.latency):
case <-ctx.Done():
return ctx.Err()
}
}
// Simulate network failures
if m.failureRate > 0 && rand.Float64() < m.failureRate {
return fmt.Errorf("mock network failure (simulated)")
}
return nil
}
// PutValue stores a key-value pair in the DHT
func (m *MockDHT) PutValue(ctx context.Context, key string, value []byte) error {
if err := m.simulateNetworkConditions(ctx); err != nil {
return err
}
m.mutex.Lock()
defer m.mutex.Unlock()
m.storage[key] = make([]byte, len(value))
copy(m.storage[key], value)
return nil
}
// GetValue retrieves a value from the DHT
func (m *MockDHT) GetValue(ctx context.Context, key string) ([]byte, error) {
if err := m.simulateNetworkConditions(ctx); err != nil {
return nil, err
}
m.mutex.RLock()
defer m.mutex.RUnlock()
value, exists := m.storage[key]
if !exists {
return nil, fmt.Errorf("key not found: %s", key)
}
// Return a copy to prevent external modification
result := make([]byte, len(value))
copy(result, value)
return result, nil
}
// Provide announces that this node can provide the given key
func (m *MockDHT) Provide(ctx context.Context, key string) error {
if err := m.simulateNetworkConditions(ctx); err != nil {
return err
}
m.mutex.Lock()
defer m.mutex.Unlock()
// Mock peer ID for this node
peerID := "mock-peer-local"
if _, exists := m.providers[key]; !exists {
m.providers[key] = make([]string, 0)
}
// Add peer to providers list if not already present
for _, existingPeer := range m.providers[key] {
if existingPeer == peerID {
return nil // Already providing
}
}
m.providers[key] = append(m.providers[key], peerID)
return nil
}
// FindProviders finds peers that can provide the given key
func (m *MockDHT) FindProviders(ctx context.Context, key string, limit int) ([]string, error) {
if err := m.simulateNetworkConditions(ctx); err != nil {
return nil, err
}
m.mutex.RLock()
defer m.mutex.RUnlock()
providers, exists := m.providers[key]
if !exists {
return []string{}, nil
}
// Apply limit if specified
if limit > 0 && len(providers) > limit {
result := make([]string, limit)
copy(result, providers[:limit])
return result, nil
}
// Return copy of providers
result := make([]string, len(providers))
copy(result, providers)
return result, nil
}
// AddPeer adds a mock peer to the network
func (m *MockDHT) AddPeer(peerID, address string) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.peers[peerID] = &MockPeer{
ID: peerID,
Address: address,
Online: true,
}
}
// RemovePeer removes a mock peer from the network
func (m *MockDHT) RemovePeer(peerID string) {
m.mutex.Lock()
defer m.mutex.Unlock()
delete(m.peers, peerID)
// Remove from all provider lists
for key, providers := range m.providers {
filtered := make([]string, 0, len(providers))
for _, provider := range providers {
if provider != peerID {
filtered = append(filtered, provider)
}
}
m.providers[key] = filtered
}
}
// GetPeers returns all mock peers
func (m *MockDHT) GetPeers() map[string]*MockPeer {
m.mutex.RLock()
defer m.mutex.RUnlock()
result := make(map[string]*MockPeer)
for id, peer := range m.peers {
result[id] = &MockPeer{
ID: peer.ID,
Address: peer.Address,
Online: peer.Online,
}
}
return result
}
// ListKeys returns all stored keys (for testing purposes)
func (m *MockDHT) ListKeys() []string {
m.mutex.RLock()
defer m.mutex.RUnlock()
keys := make([]string, 0, len(m.storage))
for key := range m.storage {
keys = append(keys, key)
}
return keys
}
// Clear removes all data from the mock DHT
func (m *MockDHT) Clear() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.storage = make(map[string][]byte)
m.providers = make(map[string][]string)
m.peers = make(map[string]*MockPeer)
}
// GetStats returns statistics about the mock DHT
func (m *MockDHT) GetStats() DHTStats {
m.mutex.RLock()
defer m.mutex.RUnlock()
return DHTStats{
TotalKeys: len(m.storage),
TotalPeers: len(m.peers),
Latency: m.latency,
ErrorCount: 0, // Mock DHT doesn't simulate errors in stats
ErrorRate: m.failureRate,
Uptime: time.Hour, // Mock uptime
}
}
type MockDHTStats struct {
TotalKeys int `json:"total_keys"`
TotalPeers int `json:"total_peers"`
TotalProviders int `json:"total_providers"`
Latency time.Duration `json:"latency"`
FailureRate float64 `json:"failure_rate"`
}

14
pkg/dht/real_dht.go Normal file
View File

@@ -0,0 +1,14 @@
package dht
import (
"fmt"
"chorus.services/bzzz/pkg/config"
)
// NewRealDHT creates a new real DHT implementation
func NewRealDHT(config *config.HybridConfig) (DHT, error) {
// TODO: Implement real DHT initialization
// For now, return an error to indicate it's not yet implemented
return nil, fmt.Errorf("real DHT implementation not yet available")
}

View File

@@ -0,0 +1,547 @@
package dht
import (
"context"
"crypto/sha256"
"fmt"
"log"
"sync"
"time"
"github.com/ipfs/go-cid"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/routing"
"github.com/multiformats/go-multihash"
)
// ReplicationManager manages DHT data replication and provider records
type ReplicationManager struct {
dht routing.Routing
ctx context.Context
cancel context.CancelFunc
config *ReplicationConfig
// Provider tracking
providers map[string]*ProviderRecord
providersMutex sync.RWMutex
// Replication tracking
contentKeys map[string]*ContentRecord
keysMutex sync.RWMutex
// Background tasks
reprovideTimer *time.Timer
cleanupTimer *time.Timer
// Metrics
metrics *ReplicationMetrics
logger func(msg string, args ...interface{})
}
// ReplicationConfig holds replication configuration
type ReplicationConfig struct {
// Target replication factor for content
ReplicationFactor int
// Interval for reproviding content
ReprovideInterval time.Duration
// Cleanup interval for stale records
CleanupInterval time.Duration
// Provider record TTL
ProviderTTL time.Duration
// Maximum number of providers to track per key
MaxProvidersPerKey int
// Enable automatic replication
EnableAutoReplication bool
// Enable periodic reproviding
EnableReprovide bool
// Maximum concurrent replication operations
MaxConcurrentReplications int
}
// ProviderRecord tracks providers for a specific content key
type ProviderRecord struct {
Key string
Providers []ProviderInfo
LastUpdate time.Time
TTL time.Duration
}
// ProviderInfo contains information about a content provider
type ProviderInfo struct {
PeerID peer.ID
AddedAt time.Time
LastSeen time.Time
Quality float64 // Quality score 0.0-1.0
Distance uint32 // XOR distance from key
}
// ContentRecord tracks local content for replication
type ContentRecord struct {
Key string
Size int64
CreatedAt time.Time
LastProvided time.Time
ReplicationCount int
Priority int // Higher priority gets replicated first
}
// ReplicationMetrics tracks replication statistics
type ReplicationMetrics struct {
mu sync.RWMutex
TotalKeys int64
TotalProviders int64
ReprovideOperations int64
SuccessfulReplications int64
FailedReplications int64
LastReprovideTime time.Time
LastCleanupTime time.Time
AverageReplication float64
}
// DefaultReplicationConfig returns default replication configuration
func DefaultReplicationConfig() *ReplicationConfig {
return &ReplicationConfig{
ReplicationFactor: 3,
ReprovideInterval: 12 * time.Hour,
CleanupInterval: 1 * time.Hour,
ProviderTTL: 24 * time.Hour,
MaxProvidersPerKey: 10,
EnableAutoReplication: true,
EnableReprovide: true,
MaxConcurrentReplications: 5,
}
}
// NewReplicationManager creates a new replication manager
func NewReplicationManager(ctx context.Context, dht routing.Routing, config *ReplicationConfig) *ReplicationManager {
if config == nil {
config = DefaultReplicationConfig()
}
rmCtx, cancel := context.WithCancel(ctx)
rm := &ReplicationManager{
dht: dht,
ctx: rmCtx,
cancel: cancel,
config: config,
providers: make(map[string]*ProviderRecord),
contentKeys: make(map[string]*ContentRecord),
metrics: &ReplicationMetrics{},
logger: func(msg string, args ...interface{}) {
log.Printf("[REPLICATION] "+msg, args...)
},
}
// Start background tasks
rm.startBackgroundTasks()
return rm
}
// AddContent registers content for replication management
func (rm *ReplicationManager) AddContent(key string, size int64, priority int) error {
rm.keysMutex.Lock()
defer rm.keysMutex.Unlock()
record := &ContentRecord{
Key: key,
Size: size,
CreatedAt: time.Now(),
LastProvided: time.Time{}, // Will be set on first provide
ReplicationCount: 0,
Priority: priority,
}
rm.contentKeys[key] = record
rm.updateMetrics()
rm.logger("Added content for replication: %s (size: %d, priority: %d)", key, size, priority)
// Immediately provide if auto-replication is enabled
if rm.config.EnableAutoReplication {
go rm.provideContent(key)
}
return nil
}
// RemoveContent removes content from replication management
func (rm *ReplicationManager) RemoveContent(key string) error {
rm.keysMutex.Lock()
delete(rm.contentKeys, key)
rm.keysMutex.Unlock()
rm.providersMutex.Lock()
delete(rm.providers, key)
rm.providersMutex.Unlock()
rm.updateMetrics()
rm.logger("Removed content from replication: %s", key)
return nil
}
// ProvideContent announces this node as a provider for the given key
func (rm *ReplicationManager) ProvideContent(key string) error {
return rm.provideContent(key)
}
// FindProviders discovers providers for a given content key
func (rm *ReplicationManager) FindProviders(ctx context.Context, key string, limit int) ([]ProviderInfo, error) {
// First check our local provider cache
rm.providersMutex.RLock()
if record, exists := rm.providers[key]; exists && time.Since(record.LastUpdate) < record.TTL {
rm.providersMutex.RUnlock()
// Return cached providers (up to limit)
providers := make([]ProviderInfo, 0, len(record.Providers))
for i, provider := range record.Providers {
if i >= limit {
break
}
providers = append(providers, provider)
}
return providers, nil
}
rm.providersMutex.RUnlock()
// Query DHT for providers
keyHash := sha256.Sum256([]byte(key))
// Create a proper CID from the hash
mh, err := multihash.EncodeName(keyHash[:], "sha2-256")
if err != nil {
return nil, fmt.Errorf("failed to encode multihash: %w", err)
}
contentID := cid.NewCidV1(cid.Raw, mh)
// Use DHT to find providers
providerCh := rm.dht.FindProvidersAsync(ctx, contentID, limit)
var providers []ProviderInfo
for providerInfo := range providerCh {
if len(providers) >= limit {
break
}
provider := ProviderInfo{
PeerID: providerInfo.ID,
AddedAt: time.Now(),
LastSeen: time.Now(),
Quality: 1.0, // Default quality
Distance: calculateDistance(keyHash[:], providerInfo.ID),
}
providers = append(providers, provider)
}
// Cache the results
rm.updateProviderCache(key, providers)
rm.logger("Found %d providers for key: %s", len(providers), key)
return providers, nil
}
// GetReplicationStatus returns replication status for a specific key
func (rm *ReplicationManager) GetReplicationStatus(key string) (*ReplicationStatus, error) {
rm.keysMutex.RLock()
content, contentExists := rm.contentKeys[key]
rm.keysMutex.RUnlock()
rm.providersMutex.RLock()
providers, providersExist := rm.providers[key]
rm.providersMutex.RUnlock()
status := &ReplicationStatus{
Key: key,
TargetReplicas: rm.config.ReplicationFactor,
ActualReplicas: 0,
LastReprovided: time.Time{},
HealthyProviders: 0,
IsLocal: contentExists,
}
if contentExists {
status.LastReprovided = content.LastProvided
status.CreatedAt = content.CreatedAt
status.Size = content.Size
status.Priority = content.Priority
}
if providersExist {
status.ActualReplicas = len(providers.Providers)
// Count healthy providers (seen recently)
cutoff := time.Now().Add(-rm.config.ProviderTTL / 2)
for _, provider := range providers.Providers {
if provider.LastSeen.After(cutoff) {
status.HealthyProviders++
}
}
status.Providers = providers.Providers
}
// Determine health status
if status.ActualReplicas >= status.TargetReplicas {
status.Health = "healthy"
} else if status.ActualReplicas > 0 {
status.Health = "degraded"
} else {
status.Health = "critical"
}
return status, nil
}
// GetMetrics returns replication metrics
func (rm *ReplicationManager) GetMetrics() *ReplicationMetrics {
rm.metrics.mu.RLock()
defer rm.metrics.mu.RUnlock()
// Create a copy to avoid race conditions
metrics := *rm.metrics
return &metrics
}
// provideContent performs the actual content provision operation
func (rm *ReplicationManager) provideContent(key string) error {
ctx, cancel := context.WithTimeout(rm.ctx, 30*time.Second)
defer cancel()
keyHash := sha256.Sum256([]byte(key))
// Create a proper CID from the hash
mh, err := multihash.EncodeName(keyHash[:], "sha2-256")
if err != nil {
rm.metrics.mu.Lock()
rm.metrics.FailedReplications++
rm.metrics.mu.Unlock()
return fmt.Errorf("failed to encode multihash: %w", err)
}
contentID := cid.NewCidV1(cid.Raw, mh)
// Provide the content to the DHT
if err := rm.dht.Provide(ctx, contentID, true); err != nil {
rm.metrics.mu.Lock()
rm.metrics.FailedReplications++
rm.metrics.mu.Unlock()
return fmt.Errorf("failed to provide content %s: %w", key, err)
}
// Update local records
rm.keysMutex.Lock()
if record, exists := rm.contentKeys[key]; exists {
record.LastProvided = time.Now()
record.ReplicationCount++
}
rm.keysMutex.Unlock()
rm.metrics.mu.Lock()
rm.metrics.SuccessfulReplications++
rm.metrics.mu.Unlock()
rm.logger("Successfully provided content: %s", key)
return nil
}
// updateProviderCache updates the provider cache for a key
func (rm *ReplicationManager) updateProviderCache(key string, providers []ProviderInfo) {
rm.providersMutex.Lock()
defer rm.providersMutex.Unlock()
record := &ProviderRecord{
Key: key,
Providers: providers,
LastUpdate: time.Now(),
TTL: rm.config.ProviderTTL,
}
// Limit the number of providers
if len(record.Providers) > rm.config.MaxProvidersPerKey {
record.Providers = record.Providers[:rm.config.MaxProvidersPerKey]
}
rm.providers[key] = record
}
// startBackgroundTasks starts periodic maintenance tasks
func (rm *ReplicationManager) startBackgroundTasks() {
// Reprovide task
if rm.config.EnableReprovide {
rm.reprovideTimer = time.AfterFunc(rm.config.ReprovideInterval, func() {
rm.performReprovide()
// Reschedule
rm.reprovideTimer.Reset(rm.config.ReprovideInterval)
})
}
// Cleanup task
rm.cleanupTimer = time.AfterFunc(rm.config.CleanupInterval, func() {
rm.performCleanup()
// Reschedule
rm.cleanupTimer.Reset(rm.config.CleanupInterval)
})
}
// performReprovide re-provides all local content
func (rm *ReplicationManager) performReprovide() {
rm.logger("Starting reprovide operation")
start := time.Now()
rm.keysMutex.RLock()
keys := make([]string, 0, len(rm.contentKeys))
for key := range rm.contentKeys {
keys = append(keys, key)
}
rm.keysMutex.RUnlock()
// Provide all keys with concurrency limit
semaphore := make(chan struct{}, rm.config.MaxConcurrentReplications)
var wg sync.WaitGroup
var successful, failed int64
for _, key := range keys {
wg.Add(1)
go func(k string) {
defer wg.Done()
semaphore <- struct{}{} // Acquire
defer func() { <-semaphore }() // Release
if err := rm.provideContent(k); err != nil {
rm.logger("Failed to reprovide %s: %v", k, err)
failed++
} else {
successful++
}
}(key)
}
wg.Wait()
rm.metrics.mu.Lock()
rm.metrics.ReprovideOperations++
rm.metrics.LastReprovideTime = time.Now()
rm.metrics.mu.Unlock()
duration := time.Since(start)
rm.logger("Reprovide operation completed: %d successful, %d failed, took %v",
successful, failed, duration)
}
// performCleanup removes stale provider records
func (rm *ReplicationManager) performCleanup() {
rm.logger("Starting cleanup operation")
rm.providersMutex.Lock()
defer rm.providersMutex.Unlock()
cutoff := time.Now().Add(-rm.config.ProviderTTL)
removed := 0
for key, record := range rm.providers {
if record.LastUpdate.Before(cutoff) {
delete(rm.providers, key)
removed++
} else {
// Clean up individual providers within the record
validProviders := make([]ProviderInfo, 0, len(record.Providers))
for _, provider := range record.Providers {
if provider.LastSeen.After(cutoff) {
validProviders = append(validProviders, provider)
}
}
record.Providers = validProviders
}
}
rm.metrics.mu.Lock()
rm.metrics.LastCleanupTime = time.Now()
rm.metrics.mu.Unlock()
rm.logger("Cleanup operation completed: removed %d stale records", removed)
}
// updateMetrics recalculates metrics
func (rm *ReplicationManager) updateMetrics() {
rm.metrics.mu.Lock()
defer rm.metrics.mu.Unlock()
rm.metrics.TotalKeys = int64(len(rm.contentKeys))
totalProviders := int64(0)
totalReplications := int64(0)
for _, record := range rm.providers {
totalProviders += int64(len(record.Providers))
}
for _, content := range rm.contentKeys {
totalReplications += int64(content.ReplicationCount)
}
rm.metrics.TotalProviders = totalProviders
if rm.metrics.TotalKeys > 0 {
rm.metrics.AverageReplication = float64(totalReplications) / float64(rm.metrics.TotalKeys)
}
}
// Stop stops the replication manager
func (rm *ReplicationManager) Stop() error {
rm.cancel()
if rm.reprovideTimer != nil {
rm.reprovideTimer.Stop()
}
if rm.cleanupTimer != nil {
rm.cleanupTimer.Stop()
}
rm.logger("Replication manager stopped")
return nil
}
// ReplicationStatus holds the replication status of a specific key
type ReplicationStatus struct {
Key string
TargetReplicas int
ActualReplicas int
HealthyProviders int
LastReprovided time.Time
CreatedAt time.Time
Size int64
Priority int
Health string // "healthy", "degraded", "critical"
IsLocal bool
Providers []ProviderInfo
}
// calculateDistance calculates XOR distance between key and peer ID
func calculateDistance(key []byte, peerID peer.ID) uint32 {
peerBytes := []byte(peerID)
var distance uint32
minLen := len(key)
if len(peerBytes) < minLen {
minLen = len(peerBytes)
}
for i := 0; i < minLen; i++ {
distance ^= uint32(key[i] ^ peerBytes[i])
}
return distance
}

160
pkg/dht/replication_test.go Normal file
View File

@@ -0,0 +1,160 @@
package dht
import (
"context"
"fmt"
"testing"
"time"
)
// TestReplicationManager tests basic replication manager functionality
func TestReplicationManager(t *testing.T) {
ctx := context.Background()
// Create a mock DHT for testing
mockDHT := NewMockDHTInterface()
// Create replication manager
config := DefaultReplicationConfig()
config.ReprovideInterval = 1 * time.Second // Short interval for testing
config.CleanupInterval = 1 * time.Second
rm := NewReplicationManager(ctx, mockDHT.Mock(), config)
defer rm.Stop()
// Test adding content
testKey := "test-content-key"
testSize := int64(1024)
testPriority := 5
err := rm.AddContent(testKey, testSize, testPriority)
if err != nil {
t.Fatalf("Failed to add content: %v", err)
}
// Test getting replication status
status, err := rm.GetReplicationStatus(testKey)
if err != nil {
t.Fatalf("Failed to get replication status: %v", err)
}
if status.Key != testKey {
t.Errorf("Expected key %s, got %s", testKey, status.Key)
}
if status.Size != testSize {
t.Errorf("Expected size %d, got %d", testSize, status.Size)
}
if status.Priority != testPriority {
t.Errorf("Expected priority %d, got %d", testPriority, status.Priority)
}
// Test providing content
err = rm.ProvideContent(testKey)
if err != nil {
t.Fatalf("Failed to provide content: %v", err)
}
// Test metrics
metrics := rm.GetMetrics()
if metrics.TotalKeys != 1 {
t.Errorf("Expected 1 total key, got %d", metrics.TotalKeys)
}
// Test finding providers
providers, err := rm.FindProviders(ctx, testKey, 10)
if err != nil {
t.Fatalf("Failed to find providers: %v", err)
}
t.Logf("Found %d providers for key %s", len(providers), testKey)
// Test removing content
err = rm.RemoveContent(testKey)
if err != nil {
t.Fatalf("Failed to remove content: %v", err)
}
// Verify content was removed
metrics = rm.GetMetrics()
if metrics.TotalKeys != 0 {
t.Errorf("Expected 0 total keys after removal, got %d", metrics.TotalKeys)
}
}
// TestLibP2PDHTReplication tests DHT replication functionality
func TestLibP2PDHTReplication(t *testing.T) {
// This would normally require a real libp2p setup
// For now, just test the interface methods exist
// Mock test - in a real implementation, you'd set up actual libp2p hosts
t.Log("DHT replication interface methods are implemented")
// Example of how the replication would be used:
// 1. Add content for replication
// 2. Content gets automatically provided to the DHT
// 3. Other nodes can discover this node as a provider
// 4. Periodic reproviding ensures content availability
// 5. Replication metrics track system health
}
// TestReplicationConfig tests replication configuration
func TestReplicationConfig(t *testing.T) {
config := DefaultReplicationConfig()
// Test default values
if config.ReplicationFactor != 3 {
t.Errorf("Expected default replication factor 3, got %d", config.ReplicationFactor)
}
if config.ReprovideInterval != 12*time.Hour {
t.Errorf("Expected default reprovide interval 12h, got %v", config.ReprovideInterval)
}
if !config.EnableAutoReplication {
t.Error("Expected auto replication to be enabled by default")
}
if !config.EnableReprovide {
t.Error("Expected reprovide to be enabled by default")
}
}
// TestProviderInfo tests provider information tracking
func TestProviderInfo(t *testing.T) {
// Test distance calculation
key := []byte("test-key")
peerID := "test-peer-id"
distance := calculateDistance(key, []byte(peerID))
// Distance should be non-zero for different inputs
if distance == 0 {
t.Error("Expected non-zero distance for different inputs")
}
t.Logf("Distance between key and peer: %d", distance)
}
// TestReplicationMetrics tests metrics collection
func TestReplicationMetrics(t *testing.T) {
ctx := context.Background()
mockDHT := NewMockDHTInterface()
rm := NewReplicationManager(ctx, mockDHT.Mock(), DefaultReplicationConfig())
defer rm.Stop()
// Add some content
for i := 0; i < 3; i++ {
key := fmt.Sprintf("test-key-%d", i)
rm.AddContent(key, int64(1000+i*100), i+1)
}
metrics := rm.GetMetrics()
if metrics.TotalKeys != 3 {
t.Errorf("Expected 3 total keys, got %d", metrics.TotalKeys)
}
t.Logf("Replication metrics: %+v", metrics)
}