From 13c948232df4a7e4bbac52b1c038ff266c39b6f2 Mon Sep 17 00:00:00 2001 From: "J. Eckert" Date: Tue, 13 Jan 2026 12:27:06 -0800 Subject: [PATCH 1/2] combo-refactor pass - dht_messages were moved into dht.go since the previous refactor super-streamed it down. - dht_lookup was also merged into dht.go for the same reasons - seeds.go was moved into bootstrap, since seeds calls ONLY happen during bootstrapping. - hardware and nat.go were both moved into a network.go file, since both of them revolve around networking device management and I was looking for more excuses to combine small files --- README.md | 3 +- bootstrap.go | 74 +++++++ dht.go | 501 +++++++++++++++++++++++++++++++++++++++++++ dht_lookup.go | 252 ---------------------- dht_messages.go | 263 ----------------------- hardware.go | 108 ---------- nat.go => network.go | 107 ++++++++- seeds.go | 77 ------- 8 files changed, 683 insertions(+), 702 deletions(-) delete mode 100644 dht_lookup.go delete mode 100644 dht_messages.go delete mode 100644 hardware.go rename nat.go => network.go (52%) delete mode 100644 seeds.go diff --git a/README.md b/README.md index 5997a89..f41f586 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ STELLAR_PUBLIC_ADDRESS: "your-domain.com:7868" ### Prerequisites -- Go 1.21+ +- Go 1.18+ - GCC (for SQLite CGO) - **macOS**: `xcode-select --install` - **Linux**: `sudo apt-get install build-essential` @@ -195,6 +195,7 @@ Each node runs two HTTP servers: | Liveness | 5 min | Ping sample of 50 peers, evict unresponsive nodes | | Gossip Validation | 10 min | Verify unverified systems learned via gossip | | Cache Prune | 2 hours | Remove stale cache entries (>48h unverified) | +| Compaction | Daily 3 AM | Aggregate old attestations into summaries | | Credits | 1 hour | Calculate and award earned credits | ### Star Types & Peer Capacity diff --git a/bootstrap.go b/bootstrap.go index 757392d..5832b20 100644 --- a/bootstrap.go +++ b/bootstrap.go @@ -1,15 +1,89 @@ package main import ( + "bufio" "encoding/json" "fmt" "log" "net/http" + "strings" "time" "github.com/google/uuid" ) +// === Seed Node Management === + +// SeedNodeListURL is the URL to fetch the seed node list from +const SeedNodeListURL = "https://raw.githubusercontent.com/sargonas/stellar-lab/main/SEED-NODES.txt" + +// FallbackSeedNodes are used if GitHub is unreachable +var FallbackSeedNodes = []string{ + // Add stable fallback seeds here if needed +} + +// FetchSeedNodes retrieves the current seed node list from GitHub +func FetchSeedNodes() []string { + log.Printf("Fetching seed node list from GitHub...") + + client := &http.Client{ + Timeout: 10 * time.Second, + } + + req, err := http.NewRequest("GET", SeedNodeListURL, nil) + if err != nil { + log.Printf("Warning: Could not create request: %v", err) + log.Printf("Using fallback seed nodes") + return FallbackSeedNodes + } + req.Header.Set("Cache-Control", "no-cache") + + resp, err := client.Do(req) + if err != nil { + log.Printf("Warning: Could not fetch seed list from GitHub: %v", err) + log.Printf("Using fallback seed nodes") + return FallbackSeedNodes + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + log.Printf("Warning: GitHub seed list returned status %d", resp.StatusCode) + log.Printf("Using fallback seed nodes") + return FallbackSeedNodes + } + + var seeds []string + scanner := bufio.NewScanner(resp.Body) + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // Skip empty lines and comments + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + seeds = append(seeds, line) + } + + if err := scanner.Err(); err != nil { + log.Printf("Warning: Error reading seed list: %v", err) + log.Printf("Using fallback seed nodes") + return FallbackSeedNodes + } + + if len(seeds) == 0 { + log.Printf("Warning: No seeds found in GitHub list") + log.Printf("Using fallback seed nodes") + return FallbackSeedNodes + } + + log.Printf("Loaded %d seed nodes from GitHub", len(seeds)) + return seeds +} + +// === Full Sync === + // tryFullSync attempts to get the complete galaxy state from a peer via /api/full-sync // This is the preferred method for new nodes to learn about the entire network quickly. // Returns the number of new systems learned, or error if full-sync is not available. diff --git a/dht.go b/dht.go index 37aac50..71ae0c5 100644 --- a/dht.go +++ b/dht.go @@ -39,6 +39,271 @@ const ( VerificationCutoff = 36 * time.Hour ) +// DHT Message Types +const ( + MessageTypePing = "ping" + MessageTypeFindNode = "find_node" + MessageTypeAnnounce = "announce" +) + +// Error codes +const ( + ErrCodeInvalidMessage = 400 + ErrCodeMissingAttestation = 401 + ErrCodeInvalidAttestation = 402 + ErrCodeIncompatibleVersion = 403 + ErrCodeInternalError = 500 +) + +// DHTMessage is the unified message format for all DHT operations +type DHTMessage struct { + Type string `json:"type"` // "ping", "find_node", "announce" + Version string `json:"version"` // Protocol version (e.g., "1.0.0") + FromSystem *System `json:"from_system"` // Sender's full system info (always included) + TargetID *uuid.UUID `json:"target_id,omitempty"` // For find_node: the ID we're looking for + ClosestNodes []*System `json:"closest_nodes,omitempty"` // For find_node response: K closest nodes + Attestation *Attestation `json:"attestation"` // Cryptographic proof (required) + Timestamp time.Time `json:"timestamp"` + IsResponse bool `json:"is_response"` // True if this is a response to a request + RequestID string `json:"request_id,omitempty"` // Correlates requests with responses +} + +// DHTError represents an error response +type DHTError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// Error implements the error interface for DHTError +func (e *DHTError) Error() string { + return e.Message +} + +// Custom errors +var ( + ErrNoKeys = &DHTError{Code: ErrCodeInternalError, Message: "no cryptographic keys available"} +) + +// NewPingRequest creates a new ping request message +// toSystemID should be the recipient's UUID if known, or uuid.Nil for first contact +func NewPingRequest(fromSystem *System, toSystemID uuid.UUID, requestID string) (*DHTMessage, error) { + if fromSystem.Keys == nil { + return nil, ErrNoKeys + } + + attestation := SignAttestation( + fromSystem.ID, + toSystemID, + "dht_ping", + fromSystem.Keys.PrivateKey, + fromSystem.Keys.PublicKey, + ) + + return &DHTMessage{ + Type: MessageTypePing, + Version: CurrentProtocolVersion.String(), + FromSystem: fromSystem, + Attestation: attestation, + Timestamp: time.Now(), + IsResponse: false, + RequestID: requestID, + }, nil +} + +// NewPingResponse creates a ping response message +// toSystemID should be the original requester's UUID +func NewPingResponse(fromSystem *System, toSystemID uuid.UUID, requestID string) (*DHTMessage, error) { + if fromSystem.Keys == nil { + return nil, ErrNoKeys + } + + attestation := SignAttestation( + fromSystem.ID, + toSystemID, + "dht_ping_response", + fromSystem.Keys.PrivateKey, + fromSystem.Keys.PublicKey, + ) + + return &DHTMessage{ + Type: MessageTypePing, + Version: CurrentProtocolVersion.String(), + FromSystem: fromSystem, + Attestation: attestation, + Timestamp: time.Now(), + IsResponse: true, + RequestID: requestID, + }, nil +} + +// NewFindNodeRequest creates a new find_node request message +// toSystemID should be the recipient's UUID if known, or uuid.Nil for first contact +func NewFindNodeRequest(fromSystem *System, toSystemID uuid.UUID, targetID uuid.UUID, requestID string) (*DHTMessage, error) { + if fromSystem.Keys == nil { + return nil, ErrNoKeys + } + + attestation := SignAttestation( + fromSystem.ID, + toSystemID, + "dht_find_node", + fromSystem.Keys.PrivateKey, + fromSystem.Keys.PublicKey, + ) + + return &DHTMessage{ + Type: MessageTypeFindNode, + Version: CurrentProtocolVersion.String(), + FromSystem: fromSystem, + TargetID: &targetID, + Attestation: attestation, + Timestamp: time.Now(), + IsResponse: false, + RequestID: requestID, + }, nil +} + +// NewFindNodeResponse creates a find_node response with closest nodes +// toSystemID should be the original requester's UUID +func NewFindNodeResponse(fromSystem *System, toSystemID uuid.UUID, closestNodes []*System, requestID string) (*DHTMessage, error) { + if fromSystem.Keys == nil { + return nil, ErrNoKeys + } + + attestation := SignAttestation( + fromSystem.ID, + toSystemID, + "dht_find_node_response", + fromSystem.Keys.PrivateKey, + fromSystem.Keys.PublicKey, + ) + + return &DHTMessage{ + Type: MessageTypeFindNode, + Version: CurrentProtocolVersion.String(), + FromSystem: fromSystem, + ClosestNodes: closestNodes, + Attestation: attestation, + Timestamp: time.Now(), + IsResponse: true, + RequestID: requestID, + }, nil +} + +// NewAnnounceRequest creates an announce request (node advertising itself) +// toSystemID should be the recipient's UUID if known, or uuid.Nil for first contact +func NewAnnounceRequest(fromSystem *System, toSystemID uuid.UUID, requestID string) (*DHTMessage, error) { + if fromSystem.Keys == nil { + return nil, ErrNoKeys + } + + attestation := SignAttestation( + fromSystem.ID, + toSystemID, + "dht_announce", + fromSystem.Keys.PrivateKey, + fromSystem.Keys.PublicKey, + ) + + return &DHTMessage{ + Type: MessageTypeAnnounce, + Version: CurrentProtocolVersion.String(), + FromSystem: fromSystem, + Attestation: attestation, + Timestamp: time.Now(), + IsResponse: false, + RequestID: requestID, + }, nil +} + +// NewAnnounceResponse creates an announce response +// toSystemID should be the original requester's UUID +func NewAnnounceResponse(fromSystem *System, toSystemID uuid.UUID, requestID string) (*DHTMessage, error) { + if fromSystem.Keys == nil { + return nil, ErrNoKeys + } + + attestation := SignAttestation( + fromSystem.ID, + toSystemID, + "dht_announce_response", + fromSystem.Keys.PrivateKey, + fromSystem.Keys.PublicKey, + ) + + return &DHTMessage{ + Type: MessageTypeAnnounce, + Version: CurrentProtocolVersion.String(), + FromSystem: fromSystem, + Attestation: attestation, + Timestamp: time.Now(), + IsResponse: true, + RequestID: requestID, + }, nil +} + +// Validate checks if a DHT message is valid +func (msg *DHTMessage) Validate() error { + if msg.FromSystem == nil { + return &DHTError{Code: ErrCodeInvalidMessage, Message: "missing from_system"} + } + + if msg.Attestation == nil { + return &DHTError{Code: ErrCodeMissingAttestation, Message: "missing attestation"} + } + + if !msg.Attestation.Verify() { + return &DHTError{Code: ErrCodeInvalidAttestation, Message: "invalid attestation signature"} + } + + if msg.Attestation.FromSystemID != msg.FromSystem.ID { + return &DHTError{Code: ErrCodeInvalidAttestation, Message: "attestation sender mismatch"} + } + + if !msg.Attestation.IsTimestampValid(5 * time.Minute) { + return &DHTError{Code: ErrCodeInvalidAttestation, Message: "attestation timestamp out of range"} + } + + if len(msg.FromSystem.Name) > 64 { + return &DHTError{Code: ErrCodeInvalidMessage, Message: "system name too long"} + } + + // Verify star configuration matches what the UUID should produce + if !ValidateStarSystem(msg.FromSystem) { + return &DHTError{Code: ErrCodeInvalidMessage, Message: "star system configuration invalid for UUID"} + } + + switch msg.Type { + case MessageTypePing: + // No additional validation needed + case MessageTypeFindNode: + if !msg.IsResponse && msg.TargetID == nil { + return &DHTError{Code: ErrCodeInvalidMessage, Message: "find_node request requires target_id"} + } + case MessageTypeAnnounce: + // No additional validation needed + default: + return &DHTError{Code: ErrCodeInvalidMessage, Message: "unknown message type: " + msg.Type} + } + + return nil +} + +// HasTargetedAttestation returns true if the attestation includes a specific recipient +// (ToSystemID is not uuid.Nil). This indicates the sender is using protocol v1.6.0+ +func (msg *DHTMessage) HasTargetedAttestation() bool { + return msg.Attestation != nil && msg.Attestation.ToSystemID != uuid.Nil +} + +// LookupResult contains the result of a DHT lookup +type LookupResult struct { + Target uuid.UUID + ClosestNodes []*System + Found *System // Non-nil if exact target was found + Hops int + Duration time.Duration +} + // DHT is the main coordinator for distributed hash table operations type DHT struct { localSystem *System @@ -763,4 +1028,240 @@ func (dht *DHT) GetLocalSystem() *System { // GetStorage returns the storage func (dht *DHT) GetStorage() *Storage { return dht.storage +} + +// === DHT Lookup Operations === + +// queryResponse holds the result of querying a single node +type queryResponse struct { + nodeID uuid.UUID + nodes []*System + err error +} + +// FindNode performs an iterative lookup to discover peers and find a target ID +// Simplified from Kademlia - we query peers to learn about more peers +func (dht *DHT) FindNode(targetID uuid.UUID) *LookupResult { + startTime := time.Now() + result := &LookupResult{ + Target: targetID, + } + + // Check if we have the target cached + if cached := dht.routingTable.GetCachedSystem(targetID); cached != nil { + result.Found = cached + result.ClosestNodes = []*System{cached} + result.Duration = time.Since(startTime) + return result + } + + // Get initial closest nodes from our routing table + shortlist := dht.routingTable.GetClosest(targetID, Alpha) + if len(shortlist) == 0 { + log.Printf("FindNode: no nodes in routing table, cannot lookup %s", targetID.String()[:8]) + result.Duration = time.Since(startTime) + return result + } + + // Track which nodes we've queried + queried := make(map[uuid.UUID]bool) + + // Track all nodes we've learned about, sorted by distance + allNodes := make(map[uuid.UUID]*System) + for _, sys := range shortlist { + allNodes[sys.ID] = sys + } + + hops := 0 + maxHops := 20 // Safety limit + + for hops < maxHops { + hops++ + + // Select Alpha closest unqueried nodes + toQuery := selectUnqueried(shortlist, queried, Alpha) + if len(toQuery) == 0 { + // No more unqueried nodes in shortlist + break + } + + // Query nodes in parallel + responses := dht.queryNodesParallel(toQuery, targetID) + + // Process responses + newNodesFound := false + for _, resp := range responses { + if resp.err != nil { + dht.routingTable.MarkFailed(resp.nodeID) + continue + } + + // Mark responding node as verified (successful communication) + dht.routingTable.MarkVerified(resp.nodeID) + + // Mark as queried + queried[resp.nodeID] = true + + // Save learned peer connections (responder knows these nodes) + if len(resp.nodes) > 0 { + peerIDs := make([]uuid.UUID, 0, len(resp.nodes)) + for _, sys := range resp.nodes { + if sys.ID != dht.localSystem.ID && sys.ID != resp.nodeID { + peerIDs = append(peerIDs, sys.ID) + } + } + if len(peerIDs) > 0 { + dht.storage.SavePeerConnections(resp.nodeID, peerIDs) + } + } + + // Process returned nodes + for _, sys := range resp.nodes { + if sys.ID == dht.localSystem.ID { + continue // Skip ourselves + } + + // Check if this is the exact target + if sys.ID == targetID { + result.Found = sys + } + + // Add to allNodes if not seen before + if _, exists := allNodes[sys.ID]; !exists { + allNodes[sys.ID] = sys + newNodesFound = true + + // Cache this peer + dht.updateRoutingTable(sys) + } + } + } + + // If we found the exact target, we can stop + if result.Found != nil { + break + } + + // Update shortlist with all known nodes, sorted by distance + shortlist = sortByDistance(allNodes, targetID) + + // Check termination condition: K closest nodes have all been queried + if allClosestQueried(shortlist, queried, K) && !newNodesFound { + break + } + } + + // Final result is K closest nodes + result.ClosestNodes = truncateToK(shortlist, K) + result.Hops = hops + result.Duration = time.Since(startTime) + + log.Printf("FindNode(%s): found %d nodes in %d hops (%v)", + targetID.String()[:8], len(result.ClosestNodes), hops, result.Duration) + + return result +} + +// Lookup finds a specific system by ID +func (dht *DHT) Lookup(targetID uuid.UUID) (*System, error) { + result := dht.FindNode(targetID) + if result.Found != nil { + return result.Found, nil + } + + // Check if any of the closest nodes is the target + for _, sys := range result.ClosestNodes { + if sys.ID == targetID { + return sys, nil + } + } + + return nil, &DHTError{Code: 404, Message: "system not found"} +} + +// queryNodesParallel queries multiple nodes in parallel +func (dht *DHT) queryNodesParallel(nodes []*System, targetID uuid.UUID) []queryResponse { + var wg sync.WaitGroup + responses := make([]queryResponse, len(nodes)) + + for i, node := range nodes { + wg.Add(1) + go func(idx int, sys *System) { + defer wg.Done() + + responses[idx].nodeID = sys.ID + + if sys.PeerAddress == "" { + responses[idx].err = &DHTError{Code: 400, Message: "no peer address"} + return + } + + closestNodes, err := dht.FindNodeDirectToSystem(sys, targetID) + if err != nil { + responses[idx].err = err + return + } + + responses[idx].nodes = closestNodes + }(i, node) + } + + wg.Wait() + return responses +} + +// selectUnqueried returns up to count nodes from the list that haven't been queried +func selectUnqueried(nodes []*System, queried map[uuid.UUID]bool, count int) []*System { + result := make([]*System, 0, count) + for _, node := range nodes { + if !queried[node.ID] { + result = append(result, node) + if len(result) >= count { + break + } + } + } + return result +} + +// sortByDistance returns all nodes (no longer sorted by XOR distance since we want full visibility) +func sortByDistance(nodes map[uuid.UUID]*System, target uuid.UUID) []*System { + result := make([]*System, 0, len(nodes)) + for _, sys := range nodes { + result = append(result, sys) + } + // No sorting needed - in full-visibility mode, all peers are equally useful + return result +} + +// allClosestQueried checks if the K closest nodes have all been queried +func allClosestQueried(nodes []*System, queried map[uuid.UUID]bool, k int) bool { + count := 0 + for _, node := range nodes { + if count >= k { + break + } + if !queried[node.ID] { + return false + } + count++ + } + return true +} + +// truncateToK returns at most K nodes from the list +func truncateToK(nodes []*System, k int) []*System { + if len(nodes) <= k { + return nodes + } + return nodes[:k] +} + +// FindClosestNodes is a convenience wrapper for FindNode +func (dht *DHT) FindClosestNodes(targetID uuid.UUID, count int) []*System { + result := dht.FindNode(targetID) + if len(result.ClosestNodes) <= count { + return result.ClosestNodes + } + return result.ClosestNodes[:count] } \ No newline at end of file diff --git a/dht_lookup.go b/dht_lookup.go deleted file mode 100644 index 087cab8..0000000 --- a/dht_lookup.go +++ /dev/null @@ -1,252 +0,0 @@ -package main - -import ( - "log" - "sync" - "time" - - "github.com/google/uuid" -) - -// LookupResult contains the result of a DHT lookup -type LookupResult struct { - Target uuid.UUID - ClosestNodes []*System - Found *System // Non-nil if exact target was found - Hops int - Duration time.Duration -} - -// FindNode performs an iterative lookup to discover peers and find a target ID -// Simplified from Kademlia - we query peers to learn about more peers -func (dht *DHT) FindNode(targetID uuid.UUID) *LookupResult { - startTime := time.Now() - result := &LookupResult{ - Target: targetID, - } - - // Check if we have the target cached - if cached := dht.routingTable.GetCachedSystem(targetID); cached != nil { - result.Found = cached - result.ClosestNodes = []*System{cached} - result.Duration = time.Since(startTime) - return result - } - - // Get initial closest nodes from our routing table - shortlist := dht.routingTable.GetClosest(targetID, Alpha) - if len(shortlist) == 0 { - log.Printf("FindNode: no nodes in routing table, cannot lookup %s", targetID.String()[:8]) - result.Duration = time.Since(startTime) - return result - } - - // Track which nodes we've queried - queried := make(map[uuid.UUID]bool) - - // Track all nodes we've learned about, sorted by distance - allNodes := make(map[uuid.UUID]*System) - for _, sys := range shortlist { - allNodes[sys.ID] = sys - } - - hops := 0 - maxHops := 20 // Safety limit - - for hops < maxHops { - hops++ - - // Select Alpha closest unqueried nodes - toQuery := selectUnqueried(shortlist, queried, Alpha) - if len(toQuery) == 0 { - // No more unqueried nodes in shortlist - break - } - - // Query nodes in parallel - responses := dht.queryNodesParallel(toQuery, targetID) - - // Process responses - newNodesFound := false - for _, resp := range responses { - if resp.err != nil { - dht.routingTable.MarkFailed(resp.nodeID) - continue - } - - // Mark responding node as verified (successful communication) - dht.routingTable.MarkVerified(resp.nodeID) - - // Mark as queried - queried[resp.nodeID] = true - - // Save learned peer connections (responder knows these nodes) - if len(resp.nodes) > 0 { - peerIDs := make([]uuid.UUID, 0, len(resp.nodes)) - for _, sys := range resp.nodes { - if sys.ID != dht.localSystem.ID && sys.ID != resp.nodeID { - peerIDs = append(peerIDs, sys.ID) - } - } - if len(peerIDs) > 0 { - dht.storage.SavePeerConnections(resp.nodeID, peerIDs) - } - } - - // Process returned nodes - for _, sys := range resp.nodes { - if sys.ID == dht.localSystem.ID { - continue // Skip ourselves - } - - // Check if this is the exact target - if sys.ID == targetID { - result.Found = sys - } - - // Add to allNodes if not seen before - if _, exists := allNodes[sys.ID]; !exists { - allNodes[sys.ID] = sys - newNodesFound = true - - // Cache this peer - dht.updateRoutingTable(sys) - } - } - } - - // If we found the exact target, we can stop - if result.Found != nil { - break - } - - // Update shortlist with all known nodes, sorted by distance - shortlist = sortByDistance(allNodes, targetID) - - // Check termination condition: K closest nodes have all been queried - if allClosestQueried(shortlist, queried, K) && !newNodesFound { - break - } - } - - // Final result is K closest nodes - result.ClosestNodes = truncateToK(shortlist, K) - result.Hops = hops - result.Duration = time.Since(startTime) - - log.Printf("FindNode(%s): found %d nodes in %d hops (%v)", - targetID.String()[:8], len(result.ClosestNodes), hops, result.Duration) - - return result -} - -// Lookup finds a specific system by ID -func (dht *DHT) Lookup(targetID uuid.UUID) (*System, error) { - result := dht.FindNode(targetID) - if result.Found != nil { - return result.Found, nil - } - - // Check if any of the closest nodes is the target - for _, sys := range result.ClosestNodes { - if sys.ID == targetID { - return sys, nil - } - } - - return nil, &DHTError{Code: 404, Message: "system not found"} -} - -// queryResponse holds the result of querying a single node -type queryResponse struct { - nodeID uuid.UUID - nodes []*System - err error -} - -// queryNodesParallel queries multiple nodes in parallel -func (dht *DHT) queryNodesParallel(nodes []*System, targetID uuid.UUID) []queryResponse { - var wg sync.WaitGroup - responses := make([]queryResponse, len(nodes)) - - for i, node := range nodes { - wg.Add(1) - go func(idx int, sys *System) { - defer wg.Done() - - responses[idx].nodeID = sys.ID - - if sys.PeerAddress == "" { - responses[idx].err = &DHTError{Code: 400, Message: "no peer address"} - return - } - - closestNodes, err := dht.FindNodeDirectToSystem(sys, targetID) - if err != nil { - responses[idx].err = err - return - } - - responses[idx].nodes = closestNodes - }(i, node) - } - - wg.Wait() - return responses -} - -// selectUnqueried returns up to count nodes from the list that haven't been queried -func selectUnqueried(nodes []*System, queried map[uuid.UUID]bool, count int) []*System { - result := make([]*System, 0, count) - for _, node := range nodes { - if !queried[node.ID] { - result = append(result, node) - if len(result) >= count { - break - } - } - } - return result -} - -// sortByDistance returns all nodes (no longer sorted by XOR distance since we want full visibility) -func sortByDistance(nodes map[uuid.UUID]*System, target uuid.UUID) []*System { - result := make([]*System, 0, len(nodes)) - for _, sys := range nodes { - result = append(result, sys) - } - // No sorting needed - in full-visibility mode, all peers are equally useful - return result -} - -// allClosestQueried checks if the K closest nodes have all been queried -func allClosestQueried(nodes []*System, queried map[uuid.UUID]bool, k int) bool { - count := 0 - for _, node := range nodes { - if count >= k { - break - } - if !queried[node.ID] { - return false - } - count++ - } - return true -} - -// truncateToK returns at most K nodes from the list -func truncateToK(nodes []*System, k int) []*System { - if len(nodes) <= k { - return nodes - } - return nodes[:k] -} - -// FindClosestNodes is a convenience wrapper for FindNode -func (dht *DHT) FindClosestNodes(targetID uuid.UUID, count int) []*System { - result := dht.FindNode(targetID) - if len(result.ClosestNodes) <= count { - return result.ClosestNodes - } - return result.ClosestNodes[:count] -} \ No newline at end of file diff --git a/dht_messages.go b/dht_messages.go deleted file mode 100644 index cc2b5ff..0000000 --- a/dht_messages.go +++ /dev/null @@ -1,263 +0,0 @@ -package main - -import ( - "time" - - "github.com/google/uuid" -) - -// DHT Message Types -const ( - MessageTypePing = "ping" - MessageTypeFindNode = "find_node" - MessageTypeAnnounce = "announce" -) - -// DHTMessage is the unified message format for all DHT operations -type DHTMessage struct { - Type string `json:"type"` // "ping", "find_node", "announce" - Version string `json:"version"` // Protocol version (e.g., "1.0.0") - FromSystem *System `json:"from_system"` // Sender's full system info (always included) - TargetID *uuid.UUID `json:"target_id,omitempty"` // For find_node: the ID we're looking for - ClosestNodes []*System `json:"closest_nodes,omitempty"` // For find_node response: K closest nodes - Attestation *Attestation `json:"attestation"` // Cryptographic proof (required) - Timestamp time.Time `json:"timestamp"` - IsResponse bool `json:"is_response"` // True if this is a response to a request - RequestID string `json:"request_id,omitempty"` // Correlates requests with responses -} - -// DHTError represents an error response -type DHTError struct { - Code int `json:"code"` - Message string `json:"message"` -} - -// Error codes -const ( - ErrCodeInvalidMessage = 400 - ErrCodeMissingAttestation = 401 - ErrCodeInvalidAttestation = 402 - ErrCodeIncompatibleVersion = 403 - ErrCodeInternalError = 500 -) - -// NewPingRequest creates a new ping request message -// toSystemID should be the recipient's UUID if known, or uuid.Nil for first contact -func NewPingRequest(fromSystem *System, toSystemID uuid.UUID, requestID string) (*DHTMessage, error) { - if fromSystem.Keys == nil { - return nil, ErrNoKeys - } - - attestation := SignAttestation( - fromSystem.ID, - toSystemID, - "dht_ping", - fromSystem.Keys.PrivateKey, - fromSystem.Keys.PublicKey, - ) - - return &DHTMessage{ - Type: MessageTypePing, - Version: CurrentProtocolVersion.String(), - FromSystem: fromSystem, - Attestation: attestation, - Timestamp: time.Now(), - IsResponse: false, - RequestID: requestID, - }, nil -} - -// NewPingResponse creates a ping response message -// toSystemID should be the original requester's UUID -func NewPingResponse(fromSystem *System, toSystemID uuid.UUID, requestID string) (*DHTMessage, error) { - if fromSystem.Keys == nil { - return nil, ErrNoKeys - } - - attestation := SignAttestation( - fromSystem.ID, - toSystemID, - "dht_ping_response", - fromSystem.Keys.PrivateKey, - fromSystem.Keys.PublicKey, - ) - - return &DHTMessage{ - Type: MessageTypePing, - Version: CurrentProtocolVersion.String(), - FromSystem: fromSystem, - Attestation: attestation, - Timestamp: time.Now(), - IsResponse: true, - RequestID: requestID, - }, nil -} - -// NewFindNodeRequest creates a new find_node request message -// toSystemID should be the recipient's UUID if known, or uuid.Nil for first contact -func NewFindNodeRequest(fromSystem *System, toSystemID uuid.UUID, targetID uuid.UUID, requestID string) (*DHTMessage, error) { - if fromSystem.Keys == nil { - return nil, ErrNoKeys - } - - attestation := SignAttestation( - fromSystem.ID, - toSystemID, - "dht_find_node", - fromSystem.Keys.PrivateKey, - fromSystem.Keys.PublicKey, - ) - - return &DHTMessage{ - Type: MessageTypeFindNode, - Version: CurrentProtocolVersion.String(), - FromSystem: fromSystem, - TargetID: &targetID, - Attestation: attestation, - Timestamp: time.Now(), - IsResponse: false, - RequestID: requestID, - }, nil -} - -// NewFindNodeResponse creates a find_node response with closest nodes -// toSystemID should be the original requester's UUID -func NewFindNodeResponse(fromSystem *System, toSystemID uuid.UUID, closestNodes []*System, requestID string) (*DHTMessage, error) { - if fromSystem.Keys == nil { - return nil, ErrNoKeys - } - - attestation := SignAttestation( - fromSystem.ID, - toSystemID, - "dht_find_node_response", - fromSystem.Keys.PrivateKey, - fromSystem.Keys.PublicKey, - ) - - return &DHTMessage{ - Type: MessageTypeFindNode, - Version: CurrentProtocolVersion.String(), - FromSystem: fromSystem, - ClosestNodes: closestNodes, - Attestation: attestation, - Timestamp: time.Now(), - IsResponse: true, - RequestID: requestID, - }, nil -} - -// NewAnnounceRequest creates an announce request (node advertising itself) -// toSystemID should be the recipient's UUID if known, or uuid.Nil for first contact -func NewAnnounceRequest(fromSystem *System, toSystemID uuid.UUID, requestID string) (*DHTMessage, error) { - if fromSystem.Keys == nil { - return nil, ErrNoKeys - } - - attestation := SignAttestation( - fromSystem.ID, - toSystemID, - "dht_announce", - fromSystem.Keys.PrivateKey, - fromSystem.Keys.PublicKey, - ) - - return &DHTMessage{ - Type: MessageTypeAnnounce, - Version: CurrentProtocolVersion.String(), - FromSystem: fromSystem, - Attestation: attestation, - Timestamp: time.Now(), - IsResponse: false, - RequestID: requestID, - }, nil -} - -// NewAnnounceResponse creates an announce response -// toSystemID should be the original requester's UUID -func NewAnnounceResponse(fromSystem *System, toSystemID uuid.UUID, requestID string) (*DHTMessage, error) { - if fromSystem.Keys == nil { - return nil, ErrNoKeys - } - - attestation := SignAttestation( - fromSystem.ID, - toSystemID, - "dht_announce_response", - fromSystem.Keys.PrivateKey, - fromSystem.Keys.PublicKey, - ) - - return &DHTMessage{ - Type: MessageTypeAnnounce, - Version: CurrentProtocolVersion.String(), - FromSystem: fromSystem, - Attestation: attestation, - Timestamp: time.Now(), - IsResponse: true, - RequestID: requestID, - }, nil -} - -// Validate checks if a DHT message is valid -func (msg *DHTMessage) Validate() error { - if msg.FromSystem == nil { - return &DHTError{Code: ErrCodeInvalidMessage, Message: "missing from_system"} - } - - if msg.Attestation == nil { - return &DHTError{Code: ErrCodeMissingAttestation, Message: "missing attestation"} - } - - if !msg.Attestation.Verify() { - return &DHTError{Code: ErrCodeInvalidAttestation, Message: "invalid attestation signature"} - } - - if msg.Attestation.FromSystemID != msg.FromSystem.ID { - return &DHTError{Code: ErrCodeInvalidAttestation, Message: "attestation sender mismatch"} - } - - if !msg.Attestation.IsTimestampValid(5 * time.Minute) { - return &DHTError{Code: ErrCodeInvalidAttestation, Message: "attestation timestamp out of range"} - } - - if len(msg.FromSystem.Name) > 64 { - return &DHTError{Code: ErrCodeInvalidMessage, Message: "system name too long"} - } - - // Verify star configuration matches what the UUID should produce - if !ValidateStarSystem(msg.FromSystem) { - return &DHTError{Code: ErrCodeInvalidMessage, Message: "star system configuration invalid for UUID"} - } - - switch msg.Type { - case MessageTypePing: - // No additional validation needed - case MessageTypeFindNode: - if !msg.IsResponse && msg.TargetID == nil { - return &DHTError{Code: ErrCodeInvalidMessage, Message: "find_node request requires target_id"} - } - case MessageTypeAnnounce: - // No additional validation needed - default: - return &DHTError{Code: ErrCodeInvalidMessage, Message: "unknown message type: " + msg.Type} - } - - return nil -} - -// HasTargetedAttestation returns true if the attestation includes a specific recipient -// (ToSystemID is not uuid.Nil). This indicates the sender is using protocol v1.6.0+ -func (msg *DHTMessage) HasTargetedAttestation() bool { - return msg.Attestation != nil && msg.Attestation.ToSystemID != uuid.Nil -} - -// Error implements the error interface for DHTError -func (e *DHTError) Error() string { - return e.Message -} - -// Custom errors -var ( - ErrNoKeys = &DHTError{Code: ErrCodeInternalError, Message: "no cryptographic keys available"} -) \ No newline at end of file diff --git a/hardware.go b/hardware.go deleted file mode 100644 index ba61646..0000000 --- a/hardware.go +++ /dev/null @@ -1,108 +0,0 @@ -package main - -import ( - "crypto/sha256" - "fmt" - "io/ioutil" - "net" - "os" - "runtime" - "strings" - - "github.com/google/uuid" -) - -// HardwareIdentifier contains various hardware identifiers -type HardwareIdentifier struct { - MACAddress string - Hostname string - MachineID string - CombinedHash string -} - -// GetHardwareID attempts to get a stable hardware identifier -func GetHardwareID() (*HardwareIdentifier, error) { - hwid := &HardwareIdentifier{} - - // Get MAC address - interfaces, err := net.Interfaces() - if err == nil { - for _, iface := range interfaces { - // Skip loopback and virtual interfaces - if iface.Flags&net.FlagLoopback != 0 || strings.HasPrefix(iface.Name, "veth") { - continue - } - if iface.HardwareAddr != nil && len(iface.HardwareAddr) > 0 { - hwid.MACAddress = iface.HardwareAddr.String() - break - } - } - } - - // Get hostname - hostname, err := os.Hostname() - if err == nil { - hwid.Hostname = hostname - } - - // Try to get machine-id (Linux) - if runtime.GOOS == "linux" { - if data, err := ioutil.ReadFile("/etc/machine-id"); err == nil { - hwid.MachineID = strings.TrimSpace(string(data)) - } else if data, err := ioutil.ReadFile("/var/lib/dbus/machine-id"); err == nil { - hwid.MachineID = strings.TrimSpace(string(data)) - } - } - - // Create combined hash - combined := fmt.Sprintf("%s|%s|%s", hwid.MACAddress, hwid.Hostname, hwid.MachineID) - hash := sha256.Sum256([]byte(combined)) - hwid.CombinedHash = fmt.Sprintf("%x", hash[:8]) - - return hwid, nil -} - -// GenerateSemiDeterministicUUID creates a UUID based on hardware ID and optional seed -// If seed is provided, the same hardware + seed will always generate the same UUID -// This allows for deterministic regeneration while still being unique per system -func GenerateSemiDeterministicUUID(seed string) (uuid.UUID, error) { - hwid, err := GetHardwareID() - if err != nil { - // Fall back to random UUID if we can't get hardware ID - return uuid.New(), nil - } - - // Combine hardware ID with user seed - combined := fmt.Sprintf("%s|%s|%s|%s", - hwid.MACAddress, - hwid.Hostname, - hwid.MachineID, - seed) - - // Hash to create deterministic UUID - hash := sha256.Sum256([]byte(combined)) - - // Create UUID from hash (Version 5 style) - var u uuid.UUID - copy(u[:], hash[:16]) - - // Set version (5) and variant bits - u[6] = (u[6] & 0x0f) | 0x50 // Version 5 - u[8] = (u[8] & 0x3f) | 0x80 // Variant - - return u, nil -} - -// GenerateRandomUUID creates a completely random UUID (for comparison) -func GenerateRandomUUID() uuid.UUID { - return uuid.New() -} - -// GetHardwareFingerprint returns a short fingerprint of the hardware -func GetHardwareFingerprint() string { - hwid, err := GetHardwareID() - if err != nil { - return "unknown" - } - return hwid.CombinedHash -} diff --git a/nat.go b/network.go similarity index 52% rename from nat.go rename to network.go index f154af4..db22cc4 100644 --- a/nat.go +++ b/network.go @@ -2,13 +2,118 @@ package main import ( "context" + "crypto/sha256" "fmt" "log" + "net" + "os" + "runtime" + "strings" "time" + "github.com/google/uuid" "github.com/libp2p/go-nat" ) +// === Hardware Identification === + +// HardwareIdentifier contains various hardware identifiers +type HardwareIdentifier struct { + MACAddress string + Hostname string + MachineID string + CombinedHash string +} + +// GetHardwareID attempts to get a stable hardware identifier +func GetHardwareID() (*HardwareIdentifier, error) { + hwid := &HardwareIdentifier{} + + // Get MAC address + interfaces, err := net.Interfaces() + if err == nil { + for _, iface := range interfaces { + // Skip loopback and virtual interfaces + if iface.Flags&net.FlagLoopback != 0 || strings.HasPrefix(iface.Name, "veth") { + continue + } + if iface.HardwareAddr != nil && len(iface.HardwareAddr) > 0 { + hwid.MACAddress = iface.HardwareAddr.String() + break + } + } + } + + // Get hostname + hostname, err := os.Hostname() + if err == nil { + hwid.Hostname = hostname + } + + // Try to get machine-id (Linux) + if runtime.GOOS == "linux" { + if data, err := os.ReadFile("/etc/machine-id"); err == nil { + hwid.MachineID = strings.TrimSpace(string(data)) + } else if data, err := os.ReadFile("/var/lib/dbus/machine-id"); err == nil { + hwid.MachineID = strings.TrimSpace(string(data)) + } + } + + // Create combined hash + combined := fmt.Sprintf("%s|%s|%s", hwid.MACAddress, hwid.Hostname, hwid.MachineID) + hash := sha256.Sum256([]byte(combined)) + hwid.CombinedHash = fmt.Sprintf("%x", hash[:8]) + + return hwid, nil +} + +// GenerateSemiDeterministicUUID creates a UUID based on hardware ID and optional seed +// If seed is provided, the same hardware + seed will always generate the same UUID +// This allows for deterministic regeneration while still being unique per system +func GenerateSemiDeterministicUUID(seed string) (uuid.UUID, error) { + hwid, err := GetHardwareID() + if err != nil { + // Fall back to random UUID if we can't get hardware ID + return uuid.New(), nil + } + + // Combine hardware ID with user seed + combined := fmt.Sprintf("%s|%s|%s|%s", + hwid.MACAddress, + hwid.Hostname, + hwid.MachineID, + seed) + + // Hash to create deterministic UUID + hash := sha256.Sum256([]byte(combined)) + + // Create UUID from hash (Version 5 style) + var u uuid.UUID + copy(u[:], hash[:16]) + + // Set version (5) and variant bits + u[6] = (u[6] & 0x0f) | 0x50 // Version 5 + u[8] = (u[8] & 0x3f) | 0x80 // Variant + + return u, nil +} + +// GenerateRandomUUID creates a completely random UUID (for comparison) +func GenerateRandomUUID() uuid.UUID { + return uuid.New() +} + +// GetHardwareFingerprint returns a short fingerprint of the hardware +func GetHardwareFingerprint() string { + hwid, err := GetHardwareID() + if err != nil { + return "unknown" + } + return hwid.CombinedHash +} + +// === NAT Traversal === + // NATTraversal handles automatic port forwarding via UPnP or NAT-PMP type NATTraversal struct { nat nat.NAT @@ -113,4 +218,4 @@ func (n *NATTraversal) GetProtocol() string { return n.nat.Type() } return "unknown" -} \ No newline at end of file +} diff --git a/seeds.go b/seeds.go deleted file mode 100644 index c6a9e3d..0000000 --- a/seeds.go +++ /dev/null @@ -1,77 +0,0 @@ -package main - -import ( - "bufio" - "log" - "net/http" - "strings" - "time" -) - -// SeedNodeListURL is the URL to fetch the seed node list from -const SeedNodeListURL = "https://raw.githubusercontent.com/sargonas/stellar-lab/main/SEED-NODES.txt" - -// FallbackSeedNodes are used if GitHub is unreachable -var FallbackSeedNodes = []string{ - // Add stable fallback seeds here if needed -} - -// FetchSeedNodes retrieves the current seed node list from GitHub -func FetchSeedNodes() []string { - log.Printf("Fetching seed node list from GitHub...") - - client := &http.Client{ - Timeout: 10 * time.Second, - } - - req, err := http.NewRequest("GET", SeedNodeListURL, nil) - if err != nil { - log.Printf("Warning: Could not create request: %v", err) - log.Printf("Using fallback seed nodes") - return FallbackSeedNodes - } - req.Header.Set("Cache-Control", "no-cache") - - resp, err := client.Do(req) - if err != nil { - log.Printf("Warning: Could not fetch seed list from GitHub: %v", err) - log.Printf("Using fallback seed nodes") - return FallbackSeedNodes - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - log.Printf("Warning: GitHub seed list returned status %d", resp.StatusCode) - log.Printf("Using fallback seed nodes") - return FallbackSeedNodes - } - - var seeds []string - scanner := bufio.NewScanner(resp.Body) - - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - - // Skip empty lines and comments - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - seeds = append(seeds, line) - } - - if err := scanner.Err(); err != nil { - log.Printf("Warning: Error reading seed list: %v", err) - log.Printf("Using fallback seed nodes") - return FallbackSeedNodes - } - - if len(seeds) == 0 { - log.Printf("Warning: No seeds found in GitHub list") - log.Printf("Using fallback seed nodes") - return FallbackSeedNodes - } - - log.Printf("Loaded %d seed nodes from GitHub", len(seeds)) - return seeds -} From a7b8625ebb60f50875f87504bfd611d3be95b0d7 Mon Sep 17 00:00:00 2001 From: "J. Eckert" Date: Tue, 13 Jan 2026 12:39:34 -0800 Subject: [PATCH 2/2] added version check skip for dev version --- dht.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dht.go b/dht.go index 71ae0c5..be3164c 100644 --- a/dht.go +++ b/dht.go @@ -988,11 +988,15 @@ var ( // warnIfOldProtocol logs a warning if the sender is using an old protocol version // that doesn't include ToSystemID in attestations. Only warns once per hour per system. func (dht *DHT) warnIfOldProtocol(msg *DHTMessage) { - // Check if attestation has targeted recipient (v1.6.0+) + // Check if attestation has targeted recipient (v1.6.0+) and not a v0.0.0 dev version if msg.HasTargetedAttestation() { return // New protocol, all good } + if msg.Version == "0.0.0" { + return + } + // Rate limit warnings to once per hour per system oldProtocolWarnedMu.RLock() lastWarn, warned := oldProtocolWarned[msg.FromSystem.ID]