From 63a4459ee0504fa7f24e7b315e2cda701725ed71 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 15 Oct 2025 12:06:09 -0700 Subject: [PATCH 01/19] go fmt --- topology/generators.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/topology/generators.go b/topology/generators.go index d4b007f..960efcc 100644 --- a/topology/generators.go +++ b/topology/generators.go @@ -1,7 +1,5 @@ package topology -import () - // GenerateMesh creates a fully connected mesh topology func GenerateMesh(nodeCount int) *Topology { topo := NewTopology(nodeCount) From e259aaecf33086b7832253bcc18d92bacf701eae Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 15 Oct 2025 15:13:50 -0700 Subject: [PATCH 02/19] add gmlparser package --- gmlparser/example/main.go | 42 +++++++ gmlparser/gmlparser.go | 243 ++++++++++++++++++++++++++++++++++++ gmlparser/gmlparser_test.go | 161 ++++++++++++++++++++++++ gmlparser/types.go | 38 ++++++ 4 files changed, 484 insertions(+) create mode 100644 gmlparser/example/main.go create mode 100644 gmlparser/gmlparser.go create mode 100644 gmlparser/gmlparser_test.go create mode 100644 gmlparser/types.go diff --git a/gmlparser/example/main.go b/gmlparser/example/main.go new file mode 100644 index 0000000..c4e67f9 --- /dev/null +++ b/gmlparser/example/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "log" + + "github.com/ethp2p/attsim/gmlparser" +) + +func main() { + // Parse the graph.gml file + graph, err := gmlparser.Parse("graph.gml") + if err != nil { + log.Fatalf("Failed to parse graph: %v", err) + } + + fmt.Printf("Graph is directed: %v\n", graph.Directed) + fmt.Printf("Total nodes: %d\n", len(graph.Nodes)) + + // Print information about a specific node + if node, ok := graph.GetNode(0); ok { + fmt.Printf("\nNode %d:\n", node.ID) + fmt.Printf(" Label: %s\n", node.Label) + fmt.Printf(" Bandwidth Up: %d bps (%.2f Mbps)\n", node.BandwidthUpBps, float64(node.BandwidthUpBps)/1e6) + fmt.Printf(" Bandwidth Down: %d bps (%.2f Mbps)\n", node.BandwidthDownBps, float64(node.BandwidthDownBps)/1e6) + } + + // Print information about an edge + if edge, ok := graph.GetEdge(0, 2); ok { + fmt.Printf("\nEdge 0 -> 2:\n") + fmt.Printf(" Label: %s\n", edge.Label) + fmt.Printf(" Latency: %.6f seconds (%.2f ms)\n", edge.LatencySec, edge.LatencySec*1000) + fmt.Printf(" Packet Loss: %.2f\n", edge.PacketLoss) + } + + // Count total edges + totalEdges := 0 + for _, targets := range graph.Edges { + totalEdges += len(targets) + } + fmt.Printf("\nTotal edges: %d\n", totalEdges) +} diff --git a/gmlparser/gmlparser.go b/gmlparser/gmlparser.go new file mode 100644 index 0000000..1829cee --- /dev/null +++ b/gmlparser/gmlparser.go @@ -0,0 +1,243 @@ +package gmlparser + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +// Parse reads a GML file and returns structured graph data +func Parse(filename string) (*Graph, error) { + file, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + + graph := &Graph{ + Nodes: make(map[int]*NodeInfo), + Edges: make(map[int]map[int]*EdgeCharacteristics), + } + + scanner := bufio.NewScanner(file) + var currentContext string + var currentNode *NodeInfo + var currentEdge *EdgeCharacteristics + var sourceID, targetID int + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // Skip empty lines + if line == "" { + continue + } + + // Handle context opening + if strings.HasPrefix(line, "graph [") { + currentContext = "graph" + continue + } else if strings.HasPrefix(line, "node [") { + currentContext = "node" + currentNode = &NodeInfo{} + continue + } else if strings.HasPrefix(line, "edge [") { + currentContext = "edge" + currentEdge = &EdgeCharacteristics{} + sourceID = 0 + targetID = 0 + continue + } + + // Handle context closing + if line == "]" { + switch currentContext { + case "node": + if currentNode != nil && currentNode.ID >= 0 { + graph.Nodes[currentNode.ID] = currentNode + } + currentNode = nil + currentContext = "graph" + case "edge": + if currentEdge != nil { + if graph.Edges[sourceID] == nil { + graph.Edges[sourceID] = make(map[int]*EdgeCharacteristics) + } + graph.Edges[sourceID][targetID] = currentEdge + } + currentEdge = nil + currentContext = "graph" + case "graph": + // End of graph + currentContext = "" + } + continue + } + + // Parse key-value pairs + parts := strings.SplitN(line, " ", 2) + if len(parts) != 2 { + continue + } + + key := parts[0] + value := strings.Trim(parts[1], "\"") + + switch currentContext { + case "graph": + if key == "directed" { + graph.Directed = value == "1" + } + case "node": + if currentNode != nil { + if err := parseNodeField(currentNode, key, value); err != nil { + return nil, err + } + } + case "edge": + if currentEdge != nil { + if err := parseEdgeField(currentEdge, &sourceID, &targetID, key, value); err != nil { + return nil, err + } + } + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading file: %w", err) + } + + return graph, nil +} + +// parseNodeField parses a single node field +func parseNodeField(node *NodeInfo, key, value string) error { + switch key { + case "id": + id, err := strconv.Atoi(value) + if err != nil { + return fmt.Errorf("invalid node id: %w", err) + } + node.ID = id + case "label": + node.Label = value + case "host_bandwidth_up": + bw, err := parseBandwidth(value) + if err != nil { + return fmt.Errorf("invalid bandwidth_up: %w", err) + } + node.BandwidthUpBps = bw + case "host_bandwidth_down": + bw, err := parseBandwidth(value) + if err != nil { + return fmt.Errorf("invalid bandwidth_down: %w", err) + } + node.BandwidthDownBps = bw + } + return nil +} + +// parseEdgeField parses a single edge field +func parseEdgeField(edge *EdgeCharacteristics, sourceID, targetID *int, key, value string) error { + switch key { + case "source": + id, err := strconv.Atoi(value) + if err != nil { + return fmt.Errorf("invalid source id: %w", err) + } + *sourceID = id + case "target": + id, err := strconv.Atoi(value) + if err != nil { + return fmt.Errorf("invalid target id: %w", err) + } + *targetID = id + case "label": + edge.Label = value + case "latency": + lat, err := parseLatency(value) + if err != nil { + return fmt.Errorf("invalid latency: %w", err) + } + edge.LatencySec = lat + case "packet_loss": + loss, err := strconv.ParseFloat(value, 64) + if err != nil { + return fmt.Errorf("invalid packet_loss: %w", err) + } + edge.PacketLoss = loss + } + return nil +} + +// parseBandwidth converts bandwidth string (e.g., "1024 Mbit") to bits per second +func parseBandwidth(s string) (uint64, error) { + parts := strings.Fields(s) + if len(parts) != 2 { + return 0, fmt.Errorf("invalid bandwidth format: %s", s) + } + + value, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + return 0, fmt.Errorf("invalid bandwidth value: %w", err) + } + + unit := strings.ToLower(parts[1]) + var multiplier uint64 + + switch unit { + case "bit", "bits": + multiplier = 1 + case "kbit", "kbits": + multiplier = 1000 + case "mbit", "mbits": + multiplier = 1000000 + case "gbit", "gbits": + multiplier = 1000000000 + case "byte", "bytes": + multiplier = 8 + case "kbyte", "kbytes", "kb": + multiplier = 8000 + case "mbyte", "mbytes", "mb": + multiplier = 8000000 + case "gbyte", "gbytes", "gb": + multiplier = 8000000000 + default: + return 0, fmt.Errorf("unknown bandwidth unit: %s", unit) + } + + return uint64(value * float64(multiplier)), nil +} + +// parseLatency converts latency string (e.g., "110 ms") to seconds +func parseLatency(s string) (float64, error) { + parts := strings.Fields(s) + if len(parts) != 2 { + return 0, fmt.Errorf("invalid latency format: %s", s) + } + + value, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + return 0, fmt.Errorf("invalid latency value: %w", err) + } + + unit := strings.ToLower(parts[1]) + var multiplier float64 + + switch unit { + case "s", "sec", "second", "seconds": + multiplier = 1.0 + case "ms", "msec", "millisecond", "milliseconds": + multiplier = 0.001 + case "us", "usec", "microsecond", "microseconds": + multiplier = 0.000001 + case "ns", "nsec", "nanosecond", "nanoseconds": + multiplier = 0.000000001 + default: + return 0, fmt.Errorf("unknown latency unit: %s", unit) + } + + return value * multiplier, nil +} diff --git a/gmlparser/gmlparser_test.go b/gmlparser/gmlparser_test.go new file mode 100644 index 0000000..f6a59a6 --- /dev/null +++ b/gmlparser/gmlparser_test.go @@ -0,0 +1,161 @@ +package gmlparser + +import ( + "math" + "testing" +) + +func TestParse(t *testing.T) { + // Test parsing the actual graph.gml file in the project + graph, err := Parse("../graph.gml") + if err != nil { + t.Fatalf("Failed to parse graph.gml: %v", err) + } + + // Verify graph is directed + if !graph.Directed { + t.Error("Expected graph to be directed") + } + + // Verify we have the expected number of nodes (16 nodes: 8 supernodes + 8 fullnodes) + expectedNodes := 16 + if len(graph.Nodes) != expectedNodes { + t.Errorf("Expected %d nodes, got %d", expectedNodes, len(graph.Nodes)) + } + + // Test a supernode (node 0: australia-supernode) + node0, ok := graph.GetNode(0) + if !ok { + t.Fatal("Node 0 not found") + } + if node0.Label != "australia-supernode" { + t.Errorf("Expected label 'australia-supernode', got '%s'", node0.Label) + } + // 1024 Mbit = 1024000000 bps + expectedBW := uint64(1024000000) + if node0.BandwidthUpBps != expectedBW { + t.Errorf("Expected bandwidth up %d bps, got %d", expectedBW, node0.BandwidthUpBps) + } + if node0.BandwidthDownBps != expectedBW { + t.Errorf("Expected bandwidth down %d bps, got %d", expectedBW, node0.BandwidthDownBps) + } + + // Test a fullnode (node 8: australia-fullnode) + node8, ok := graph.GetNode(8) + if !ok { + t.Fatal("Node 8 not found") + } + if node8.Label != "australia-fullnode" { + t.Errorf("Expected label 'australia-fullnode', got '%s'", node8.Label) + } + // 50 Mbit = 50000000 bps + expectedBW = uint64(50000000) + if node8.BandwidthUpBps != expectedBW { + t.Errorf("Expected bandwidth up %d bps, got %d", expectedBW, node8.BandwidthUpBps) + } + + // Test edge from node 0 to node 2 (australia-supernode to east_asia-supernode) + edge, ok := graph.GetEdge(0, 2) + if !ok { + t.Fatal("Edge 0->2 not found") + } + if edge.Label != "australia-supernode to east_asia-supernode" { + t.Errorf("Expected label 'australia-supernode to east_asia-supernode', got '%s'", edge.Label) + } + // 110 ms = 0.110 seconds + expectedLatency := 0.110 + if edge.LatencySec != expectedLatency { + t.Errorf("Expected latency %f seconds, got %f", expectedLatency, edge.LatencySec) + } + if edge.PacketLoss != 0.0 { + t.Errorf("Expected packet loss 0.0, got %f", edge.PacketLoss) + } + + // Test self-loop edge (node 0 to node 0) + selfEdge, ok := graph.GetEdge(0, 0) + if !ok { + t.Fatal("Self-loop edge 0->0 not found") + } + // 2 ms = 0.002 seconds + expectedLatency = 0.002 + if selfEdge.LatencySec != expectedLatency { + t.Errorf("Expected latency %f seconds, got %f", expectedLatency, selfEdge.LatencySec) + } + + // Test non-existent edge + _, ok = graph.GetEdge(999, 1000) + if ok { + t.Error("Expected edge 999->1000 to not exist") + } +} + +func TestParseBandwidth(t *testing.T) { + tests := []struct { + input string + expected uint64 + wantErr bool + }{ + {"1024 Mbit", 1024000000, false}, + {"50 Mbit", 50000000, false}, + {"1 Gbit", 1000000000, false}, + {"100 Kbit", 100000, false}, + {"1 bit", 1, false}, + {"invalid", 0, true}, + {"1024", 0, true}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result, err := parseBandwidth(tt.input) + if tt.wantErr { + if err == nil { + t.Error("Expected error but got none") + } + return + } + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + if result != tt.expected { + t.Errorf("Expected %d, got %d", tt.expected, result) + } + }) + } +} + +func TestParseLatency(t *testing.T) { + tests := []struct { + input string + expected float64 + wantErr bool + }{ + {"110 ms", 0.110, false}, + {"2 ms", 0.002, false}, + {"1 s", 1.0, false}, + {"500 us", 0.0005, false}, + {"1000 ns", 0.000001, false}, + {"invalid", 0, true}, + {"110", 0, true}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result, err := parseLatency(tt.input) + if tt.wantErr { + if err == nil { + t.Error("Expected error but got none") + } + return + } + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + // Use tolerance for floating point comparison + if math.Abs(result-tt.expected) > 1e-9 { + t.Errorf("Expected %f, got %f", tt.expected, result) + } + }) + } +} diff --git a/gmlparser/types.go b/gmlparser/types.go new file mode 100644 index 0000000..12cfa9c --- /dev/null +++ b/gmlparser/types.go @@ -0,0 +1,38 @@ +package gmlparser + +// NodeInfo contains node attributes from the GML graph +type NodeInfo struct { + ID int + Label string + BandwidthUpBps uint64 // bits per second + BandwidthDownBps uint64 // bits per second +} + +// EdgeCharacteristics contains edge attributes from the GML graph +type EdgeCharacteristics struct { + Label string + LatencySec float64 // latency in seconds + PacketLoss float64 +} + +// Graph contains the parsed GML data with maps for efficient lookups +type Graph struct { + Directed bool + Nodes map[int]*NodeInfo // node id -> node info + Edges map[int]map[int]*EdgeCharacteristics // source -> target -> edge info +} + +// GetNode returns the node info for a given node ID +func (g *Graph) GetNode(id int) (*NodeInfo, bool) { + node, ok := g.Nodes[id] + return node, ok +} + +// GetEdge returns the edge characteristics between source and target nodes +func (g *Graph) GetEdge(source, target int) (*EdgeCharacteristics, bool) { + if targets, ok := g.Edges[source]; ok { + edge, ok := targets[target] + return edge, ok + } + return nil, false +} From 795821f3697074c76f62383e368b850cb9da0659 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 15 Oct 2025 15:14:04 -0700 Subject: [PATCH 03/19] add shadowconfig parser package --- go.mod | 1 + shadowconfig/README.md | 111 +++++++++++++++++++ shadowconfig/cmd/example/main.go | 36 +++++++ shadowconfig/example_test.go | 34 ++++++ shadowconfig/shadowconfig.go | 98 +++++++++++++++++ shadowconfig/shadowconfig_test.go | 172 ++++++++++++++++++++++++++++++ 6 files changed, 452 insertions(+) create mode 100644 shadowconfig/README.md create mode 100644 shadowconfig/cmd/example/main.go create mode 100644 shadowconfig/example_test.go create mode 100644 shadowconfig/shadowconfig.go create mode 100644 shadowconfig/shadowconfig_test.go diff --git a/go.mod b/go.mod index d360001..04a7c8c 100644 --- a/go.mod +++ b/go.mod @@ -109,5 +109,6 @@ require ( golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.30.0 // indirect google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.4.0 // indirect ) diff --git a/shadowconfig/README.md b/shadowconfig/README.md new file mode 100644 index 0000000..048776c --- /dev/null +++ b/shadowconfig/README.md @@ -0,0 +1,111 @@ +# shadowconfig + +Package `shadowconfig` provides utilities for parsing Shadow GossipSub YAML configuration files, specifically extracting network node information from the hosts section. + +## Features + +- Parse Shadow YAML configuration files +- Extract node network information with index mapping to node IDs +- Verify sequential node IDs starting from 0 +- Type-safe access to network node IDs + +## Usage + +```go +package main + +import ( + "fmt" + "log" + + "github.com/ethp2p/attsim/shadowconfig" +) + +func main() { + // Parse the Shadow configuration file + nodes, err := shadowconfig.ParseConfig("shadow-gossipsub.yaml") + if err != nil { + log.Fatal(err) + } + + // Get total node count + fmt.Printf("Total nodes: %d\n", shadowconfig.GetNodeCount(nodes)) + + // Access network node ID by node ID (index) + for i := 0; i < shadowconfig.GetNodeCount(nodes); i++ { + networkNodeID, err := shadowconfig.GetNetworkNodeID(nodes, i) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Node %d -> Network Node ID: %d\n", i, networkNodeID) + } +} +``` + +## Data Structures + +### NodeNetworkInfo + +```go +type NodeNetworkInfo struct { + NodeID int // The logical node ID (0, 1, 2, ...) + NetworkNodeID int // The Shadow network node ID from graph.gml +} +``` + +The returned slice from `ParseConfig` is indexed by `NodeID`, so you can directly access node information by index: + +```go +nodes, _ := shadowconfig.ParseConfig("shadow-gossipsub.yaml") +// nodes[0] corresponds to node0 in the YAML +// nodes[1] corresponds to node1 in the YAML +// etc. +``` + +## Configuration Format + +The parser expects Shadow YAML files with the following structure: + +```yaml +hosts: + node0: + network_node_id: 4 + processes: + - args: ... + expected_final_state: running + path: ... + node1: + network_node_id: 9 + processes: + - args: ... + # ... more nodes +``` + +### Requirements + +- Host names must follow the pattern `node` where `` is an integer +- Node IDs must be sequential starting from 0 (node0, node1, node2, ...) +- Each host must have a `network_node_id` field + +## Error Handling + +The package validates: +- File existence and readability +- YAML syntax correctness +- Host name format (must be `nodeN`) +- Sequential node IDs (no gaps) +- Array bounds when accessing node information + +## Testing + +Run the test suite: + +```bash +go test -v +``` + +Run with coverage: + +```bash +go test -v -cover +``` diff --git a/shadowconfig/cmd/example/main.go b/shadowconfig/cmd/example/main.go new file mode 100644 index 0000000..8f7b64e --- /dev/null +++ b/shadowconfig/cmd/example/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "flag" + "fmt" + "log" + + "github.com/ethp2p/attsim/shadowconfig" +) + +func main() { + configFile := flag.String("config", "shadow-gossipsub.yaml", "Path to Shadow configuration file") + flag.Parse() + + // Parse the configuration + nodes, err := shadowconfig.ParseConfig(*configFile) + if err != nil { + log.Fatalf("Error parsing config: %v", err) + } + + // Display results + fmt.Printf("Shadow Configuration Analysis\n") + fmt.Printf("==============================\n\n") + fmt.Printf("Total nodes: %d\n\n", shadowconfig.GetNodeCount(nodes)) + fmt.Printf("Node Mapping:\n") + fmt.Printf("%-10s | %-15s\n", "Node ID", "Network Node ID") + fmt.Printf("%-10s-+-%-15s\n", "----------", "---------------") + + for i := 0; i < shadowconfig.GetNodeCount(nodes); i++ { + networkNodeID, err := shadowconfig.GetNetworkNodeID(nodes, i) + if err != nil { + log.Fatalf("Error getting network node ID: %v", err) + } + fmt.Printf("%-10d | %-15d\n", i, networkNodeID) + } +} diff --git a/shadowconfig/example_test.go b/shadowconfig/example_test.go new file mode 100644 index 0000000..0a7ead8 --- /dev/null +++ b/shadowconfig/example_test.go @@ -0,0 +1,34 @@ +package shadowconfig_test + +import ( + "fmt" + "log" + + "github.com/ethp2p/attsim/shadowconfig" +) + +func Example() { + // Parse the Shadow configuration file + nodes, err := shadowconfig.ParseConfig("../shadow-gossipsub.yaml") + if err != nil { + log.Fatal(err) + } + + // Print node information + fmt.Printf("Total nodes: %d\n", shadowconfig.GetNodeCount(nodes)) + + // Access node information by index (which matches node ID) + for i := 0; i < shadowconfig.GetNodeCount(nodes); i++ { + networkNodeID, err := shadowconfig.GetNetworkNodeID(nodes, i) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Node %d -> Network Node ID: %d\n", i, networkNodeID) + } + + // Output: + // Total nodes: 3 + // Node 0 -> Network Node ID: 4 + // Node 1 -> Network Node ID: 9 + // Node 2 -> Network Node ID: 13 +} diff --git a/shadowconfig/shadowconfig.go b/shadowconfig/shadowconfig.go new file mode 100644 index 0000000..3bdd784 --- /dev/null +++ b/shadowconfig/shadowconfig.go @@ -0,0 +1,98 @@ +package shadowconfig + +import ( + "fmt" + "os" + "sort" + "strconv" + "strings" + + "gopkg.in/yaml.v3" +) + +// NodeNetworkInfo contains network information for a Shadow host +type NodeNetworkInfo struct { + NodeID int + NetworkNodeID int +} + +// Process represents a Shadow process configuration +type Process struct { + Args string `yaml:"args"` + ExpectedFinalState string `yaml:"expected_final_state"` + Path string `yaml:"path"` +} + +// Host represents a Shadow host configuration +type Host struct { + NetworkNodeID int `yaml:"network_node_id"` + Processes []Process `yaml:"processes"` +} + +// ShadowConfig represents the complete Shadow configuration +type ShadowConfig struct { + Experimental map[string]interface{} `yaml:"experimental"` + General map[string]interface{} `yaml:"general"` + Hosts map[string]Host `yaml:"hosts"` + Network map[string]interface{} `yaml:"network"` +} + +// ParseConfig parses a Shadow YAML configuration file and returns a list of +// NodeNetworkInfo sorted by node ID, where the index maps to the node ID. +func ParseConfig(filename string) ([]NodeNetworkInfo, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + var config ShadowConfig + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse YAML: %w", err) + } + + // Extract node network info from hosts + var nodes []NodeNetworkInfo + for hostName, host := range config.Hosts { + // Extract node ID from host name (e.g., "node0" -> 0) + if !strings.HasPrefix(hostName, "node") { + return nil, fmt.Errorf("unexpected host name format: %s", hostName) + } + nodeIDStr := strings.TrimPrefix(hostName, "node") + nodeID, err := strconv.Atoi(nodeIDStr) + if err != nil { + return nil, fmt.Errorf("failed to parse node ID from %s: %w", hostName, err) + } + + nodes = append(nodes, NodeNetworkInfo{ + NodeID: nodeID, + NetworkNodeID: host.NetworkNodeID, + }) + } + + // Sort by node ID to ensure index matches node ID + sort.Slice(nodes, func(i, j int) bool { + return nodes[i].NodeID < nodes[j].NodeID + }) + + // Verify that node IDs are sequential starting from 0 + for i, node := range nodes { + if node.NodeID != i { + return nil, fmt.Errorf("non-sequential node IDs: expected %d at index %d, got %d", i, i, node.NodeID) + } + } + + return nodes, nil +} + +// GetNodeCount returns the total number of nodes in the configuration +func GetNodeCount(nodes []NodeNetworkInfo) int { + return len(nodes) +} + +// GetNetworkNodeID returns the network node ID for a given node ID +func GetNetworkNodeID(nodes []NodeNetworkInfo, nodeID int) (int, error) { + if nodeID < 0 || nodeID >= len(nodes) { + return 0, fmt.Errorf("node ID %d out of range [0, %d)", nodeID, len(nodes)) + } + return nodes[nodeID].NetworkNodeID, nil +} diff --git a/shadowconfig/shadowconfig_test.go b/shadowconfig/shadowconfig_test.go new file mode 100644 index 0000000..7f46f8b --- /dev/null +++ b/shadowconfig/shadowconfig_test.go @@ -0,0 +1,172 @@ +package shadowconfig + +import ( + "os" + "path/filepath" + "testing" +) + +func TestParseConfig(t *testing.T) { + // Create a temporary test YAML file + testYAML := `experimental: + use_cpu_pinning: true + use_memory_manager: false + use_syscall_counters: true +general: + heartbeat_interval: 1s + log_level: info + model_unblocked_syscall_latency: true + stop_time: 600s +hosts: + node0: + network_node_id: 4 + processes: + - args: -node-id 0 -node-count 3 -msg-size 256 -topology-file /tmp/topology.json + expected_final_state: running + path: gossipsub/gossipsub + node1: + network_node_id: 9 + processes: + - args: -node-id 1 -node-count 3 -msg-size 256 -topology-file /tmp/topology.json + expected_final_state: running + path: gossipsub/gossipsub + node2: + network_node_id: 13 + processes: + - args: -node-id 2 -node-count 3 -msg-size 256 -topology-file /tmp/topology.json + expected_final_state: running + path: gossipsub/gossipsub +network: + graph: + file: + path: graph.gml + type: gml +` + + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test-shadow.yaml") + if err := os.WriteFile(testFile, []byte(testYAML), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Parse the config + nodes, err := ParseConfig(testFile) + if err != nil { + t.Fatalf("ParseConfig failed: %v", err) + } + + // Verify the results + expectedNodeCount := 3 + if len(nodes) != expectedNodeCount { + t.Errorf("Expected %d nodes, got %d", expectedNodeCount, len(nodes)) + } + + // Check that index matches node ID + expectedNetworkNodeIDs := []int{4, 9, 13} + for i, node := range nodes { + if node.NodeID != i { + t.Errorf("Node at index %d has NodeID %d, expected %d", i, node.NodeID, i) + } + if node.NetworkNodeID != expectedNetworkNodeIDs[i] { + t.Errorf("Node %d has NetworkNodeID %d, expected %d", i, node.NetworkNodeID, expectedNetworkNodeIDs[i]) + } + } +} + +func TestGetNodeCount(t *testing.T) { + nodes := []NodeNetworkInfo{ + {NodeID: 0, NetworkNodeID: 4}, + {NodeID: 1, NetworkNodeID: 9}, + {NodeID: 2, NetworkNodeID: 13}, + } + + count := GetNodeCount(nodes) + if count != 3 { + t.Errorf("Expected node count 3, got %d", count) + } +} + +func TestGetNetworkNodeID(t *testing.T) { + nodes := []NodeNetworkInfo{ + {NodeID: 0, NetworkNodeID: 4}, + {NodeID: 1, NetworkNodeID: 9}, + {NodeID: 2, NetworkNodeID: 13}, + } + + tests := []struct { + nodeID int + expectedNetworkID int + expectError bool + }{ + {0, 4, false}, + {1, 9, false}, + {2, 13, false}, + {3, 0, true}, // Out of range + {-1, 0, true}, // Out of range + } + + for _, tt := range tests { + networkID, err := GetNetworkNodeID(nodes, tt.nodeID) + if tt.expectError { + if err == nil { + t.Errorf("Expected error for node ID %d, got nil", tt.nodeID) + } + } else { + if err != nil { + t.Errorf("Unexpected error for node ID %d: %v", tt.nodeID, err) + } + if networkID != tt.expectedNetworkID { + t.Errorf("For node ID %d, expected network ID %d, got %d", tt.nodeID, tt.expectedNetworkID, networkID) + } + } + } +} + +func TestParseConfigNonSequentialNodes(t *testing.T) { + // Create a test YAML with non-sequential node IDs + testYAML := `hosts: + node0: + network_node_id: 4 + node2: + network_node_id: 13 +` + + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test-invalid.yaml") + if err := os.WriteFile(testFile, []byte(testYAML), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Parse should fail due to non-sequential node IDs + _, err := ParseConfig(testFile) + if err == nil { + t.Error("Expected error for non-sequential node IDs, got nil") + } +} + +func TestParseConfigInvalidHostName(t *testing.T) { + // Create a test YAML with invalid host name + testYAML := `hosts: + invalidname: + network_node_id: 4 +` + + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test-invalid-name.yaml") + if err := os.WriteFile(testFile, []byte(testYAML), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Parse should fail due to invalid host name + _, err := ParseConfig(testFile) + if err == nil { + t.Error("Expected error for invalid host name, got nil") + } +} + +func TestParseConfigFileNotFound(t *testing.T) { + _, err := ParseConfig("/nonexistent/file.yaml") + if err == nil { + t.Error("Expected error for non-existent file, got nil") + } +} From fb52a63f051758fb28d565939a5f4360fbceb2a5 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 15 Oct 2025 15:24:04 -0700 Subject: [PATCH 04/19] Add parsing network info package --- networkinfo/README.md | 175 +++++++++++++++++++++ networkinfo/example_test.go | 82 ++++++++++ networkinfo/networkinfo.go | 168 ++++++++++++++++++++ networkinfo/networkinfo_test.go | 262 ++++++++++++++++++++++++++++++++ networkinfo/types.go | 38 +++++ 5 files changed, 725 insertions(+) create mode 100644 networkinfo/README.md create mode 100644 networkinfo/example_test.go create mode 100644 networkinfo/networkinfo.go create mode 100644 networkinfo/networkinfo_test.go create mode 100644 networkinfo/types.go diff --git a/networkinfo/README.md b/networkinfo/README.md new file mode 100644 index 0000000..00ab6bb --- /dev/null +++ b/networkinfo/README.md @@ -0,0 +1,175 @@ +# networkinfo + +Package `networkinfo` provides network characteristics extraction and shortest-path latency computation for Shadow simulation configurations. + +## Overview + +This package combines data from two sources: +1. **Shadow Configuration** (`shadow-gossipsub.yaml`) - Maps host IDs to network node IDs +2. **GML Graph** (`graph.gml`) - Contains network topology, bandwidth, and latency information + +It returns: +- Per-host bandwidth characteristics (upload/download) +- All-pairs shortest-path latencies computed using Dijkstra's algorithm + +## Key Concepts + +### Two ID Systems + +The simulation uses two distinct ID systems: + +1. **Host ID** - Sequential IDs for simulation hosts (e.g., 0, 1, 2 for node0, node1, node2) +2. **Network Node ID** - IDs in the GML network graph (e.g., 4, 9, 13) + +The Shadow configuration maps host IDs to network node IDs. For example: +- Host 0 (node0) → Network Node 4 (na_east-supernode) +- Host 1 (node1) → Network Node 9 (europe-fullnode) +- Host 2 (node2) → Network Node 13 (na_west-fullnode) + +### Shortest-Path Computation + +The package uses Dijkstra's algorithm to compute shortest-path latencies between all host pairs in the network graph. This is important because: +- Hosts may not have direct edges between them in the GML graph +- Even if direct edges exist, shorter paths through intermediate nodes may be available +- The GML graph is directed, so latencies may differ depending on direction + +## Usage + +```go +package main + +import ( + "fmt" + "log" + + "github.com/ethp2p/attsim/networkinfo" +) + +func main() { + // Parse Shadow configuration and GML graph + netInfo, err := networkinfo.Parse("shadow-gossipsub.yaml", "graph.gml") + if err != nil { + log.Fatal(err) + } + + // Get bandwidth for a specific host + if host, ok := netInfo.GetHostInfo(0); ok { + fmt.Printf("Host 0: Upload=%d bps, Download=%d bps\n", + host.BandwidthUpBps, host.BandwidthDownBps) + } + + // Get latency between two hosts + if latency, ok := netInfo.GetLatency(0, 1); ok { + fmt.Printf("Latency from Host 0 to Host 1: %.3f seconds\n", latency) + } + + // Iterate over all hosts + for _, host := range netInfo.Hosts { + fmt.Printf("Host %d (Network Node %d): %d bps up, %d bps down\n", + host.HostID, host.NetworkNodeID, + host.BandwidthUpBps, host.BandwidthDownBps) + } + + // Iterate over all latencies + for srcHostID, dstMap := range netInfo.LatencyMap { + for dstHostID, latency := range dstMap { + fmt.Printf("Latency %d → %d: %.3f seconds\n", + srcHostID, dstHostID, latency) + } + } +} +``` + +## Data Structures + +### HostNetworkInfo + +Contains network characteristics for a single host: + +```go +type HostNetworkInfo struct { + HostID int // Host ID (0, 1, 2, ...) + NetworkNodeID int // Corresponding network node ID in GML graph + BandwidthUpBps uint64 // Upload bandwidth in bits per second + BandwidthDownBps uint64 // Download bandwidth in bits per second +} +``` + +### NetworkInfo + +Contains complete network information: + +```go +type NetworkInfo struct { + // Hosts[i] contains information for host with HostID == i + Hosts []HostNetworkInfo + + // LatencyMap[srcHostID][dstHostID] gives shortest-path latency in seconds + LatencyMap map[int]map[int]float64 +} +``` + +## Methods + +### Parse + +```go +func Parse(shadowConfigPath, gmlPath string) (*NetworkInfo, error) +``` + +Main entry point that parses both Shadow configuration and GML graph files. + +### GetHostInfo + +```go +func (n *NetworkInfo) GetHostInfo(hostID int) (*HostNetworkInfo, bool) +``` + +Returns network information for a specific host ID. + +### GetLatency + +```go +func (n *NetworkInfo) GetLatency(srcHostID, dstHostID int) (float64, bool) +``` + +Returns shortest-path latency between two hosts in seconds. + +## Algorithm Details + +### Dijkstra's Algorithm + +The package implements Dijkstra's shortest-path algorithm with: +- Min-heap priority queue for efficient path finding +- Time complexity: O(E log V) per source node +- All-pairs computation: Run Dijkstra from each host's network node + +Key characteristics: +- Self-latency (node to itself) is always 0 +- Handles directed graphs correctly +- Gracefully handles disconnected nodes (returns infinite distance) + +## Testing + +Run tests with: + +```bash +cd networkinfo +go test -v +``` + +See example usage: + +```bash +go test -run Example +``` + +## Dependencies + +- `github.com/ethp2p/attsim/gmlparser` - GML graph parser +- `github.com/ethp2p/attsim/shadowconfig` - Shadow YAML configuration parser + +## Related Packages + +- [`gmlparser`](../gmlparser/) - Parses GML network graph files +- [`shadowconfig`](../shadowconfig/) - Parses Shadow YAML configuration files diff --git a/networkinfo/example_test.go b/networkinfo/example_test.go new file mode 100644 index 0000000..0944d45 --- /dev/null +++ b/networkinfo/example_test.go @@ -0,0 +1,82 @@ +package networkinfo_test + +import ( + "fmt" + "log" + + "github.com/ethp2p/attsim/networkinfo" +) + +func Example() { + // Parse Shadow configuration and GML graph files + netInfo, err := networkinfo.Parse("../shadow-gossipsub.yaml", "../graph.gml") + if err != nil { + log.Fatalf("Failed to parse network info: %v", err) + } + + // Print bandwidth characteristics for each host + fmt.Println("Host Network Characteristics:") + for _, host := range netInfo.Hosts { + fmt.Printf("Host %d (Network Node %d): Upload=%d bps, Download=%d bps\n", + host.HostID, host.NetworkNodeID, + host.BandwidthUpBps, host.BandwidthDownBps) + } + + // Print latency matrix + fmt.Println("\nLatency Matrix (in seconds):") + for dstHostID := 0; dstHostID < len(netInfo.Hosts); dstHostID++ { + if dstHostID == 0 { + fmt.Print(" ") + } + fmt.Printf("Host%d", dstHostID) + if dstHostID < len(netInfo.Hosts)-1 { + fmt.Print(" ") + } + } + fmt.Println() + + for srcHostID := 0; srcHostID < len(netInfo.Hosts); srcHostID++ { + fmt.Printf("Host%d", srcHostID) + for dstHostID := 0; dstHostID < len(netInfo.Hosts); dstHostID++ { + latency, ok := netInfo.GetLatency(srcHostID, dstHostID) + if dstHostID == 0 { + fmt.Print(" ") + } + if ok { + fmt.Printf("%.3f", latency) + } else { + fmt.Print("N/A") + } + if dstHostID < len(netInfo.Hosts)-1 { + fmt.Print(" ") + } + } + fmt.Println() + } + + // Get specific host info + if host, ok := netInfo.GetHostInfo(0); ok { + fmt.Printf("\nHost 0 Details: Network Node=%d, Upload=%d bps\n", + host.NetworkNodeID, host.BandwidthUpBps) + } + + // Get specific latency + if latency, ok := netInfo.GetLatency(0, 1); ok { + fmt.Printf("Latency from Host 0 to Host 1: %.3f seconds\n", latency) + } + + // Output: + // Host Network Characteristics: + // Host 0 (Network Node 4): Upload=1024000000 bps, Download=1024000000 bps + // Host 1 (Network Node 9): Upload=50000000 bps, Download=50000000 bps + // Host 2 (Network Node 13): Upload=50000000 bps, Download=50000000 bps + // + // Latency Matrix (in seconds): + // Host0 Host1 Host2 + // Host0 0.000 0.070 0.060 + // Host1 0.070 0.000 0.110 + // Host2 0.060 0.110 0.000 + // + // Host 0 Details: Network Node=4, Upload=1024000000 bps + // Latency from Host 0 to Host 1: 0.070 seconds +} diff --git a/networkinfo/networkinfo.go b/networkinfo/networkinfo.go new file mode 100644 index 0000000..93299a8 --- /dev/null +++ b/networkinfo/networkinfo.go @@ -0,0 +1,168 @@ +package networkinfo + +import ( + "container/heap" + "fmt" + "math" + + "github.com/ethp2p/attsim/gmlparser" + "github.com/ethp2p/attsim/shadowconfig" +) + +// Parse reads Shadow configuration and GML graph files and returns complete network information +// including host bandwidth characteristics and shortest-path latencies between all host pairs. +// +// Parameters: +// - shadowConfigPath: Path to Shadow YAML configuration file +// - gmlPath: Path to GML graph file +// +// Returns: +// - NetworkInfo containing host bandwidth info and latency map +// - error if parsing fails or data is inconsistent +func Parse(shadowConfigPath, gmlPath string) (*NetworkInfo, error) { + // Parse Shadow configuration to get host ID -> network node ID mapping + shadowNodes, err := shadowconfig.ParseConfig(shadowConfigPath) + if err != nil { + return nil, fmt.Errorf("failed to parse shadow config: %w", err) + } + + // Parse GML graph to get network topology and node characteristics + graph, err := gmlparser.Parse(gmlPath) + if err != nil { + return nil, fmt.Errorf("failed to parse GML graph: %w", err) + } + + // Build host network info list + hosts := make([]HostNetworkInfo, len(shadowNodes)) + for i, shadowNode := range shadowNodes { + // Get the network node corresponding to this host + networkNode, ok := graph.GetNode(shadowNode.NetworkNodeID) + if !ok { + return nil, fmt.Errorf("network node %d not found in GML graph for host %d", + shadowNode.NetworkNodeID, shadowNode.NodeID) + } + + hosts[i] = HostNetworkInfo{ + HostID: shadowNode.NodeID, + NetworkNodeID: shadowNode.NetworkNodeID, + BandwidthUpBps: networkNode.BandwidthUpBps, + BandwidthDownBps: networkNode.BandwidthDownBps, + } + } + + // Compute shortest-path latencies between all host pairs using Dijkstra + latencyMap := computeAllPairsLatencies(shadowNodes, graph) + + return &NetworkInfo{ + Hosts: hosts, + LatencyMap: latencyMap, + }, nil +} + +// computeAllPairsLatencies computes shortest-path latencies between all host pairs +func computeAllPairsLatencies(shadowNodes []shadowconfig.NodeNetworkInfo, graph *gmlparser.Graph) map[int]map[int]float64 { + latencyMap := make(map[int]map[int]float64) + + // Run Dijkstra from each host's network node + for _, srcHost := range shadowNodes { + // Compute shortest paths from this host's network node + distances := dijkstra(graph, srcHost.NetworkNodeID) + + // Store distances to all other hosts + latencyMap[srcHost.NodeID] = make(map[int]float64) + for _, dstHost := range shadowNodes { + latencyMap[srcHost.NodeID][dstHost.NodeID] = distances[dstHost.NetworkNodeID] + } + } + + return latencyMap +} + +// dijkstra computes shortest paths from a source network node to all other nodes +// Returns a map of network node ID -> shortest distance in seconds +func dijkstra(graph *gmlparser.Graph, sourceNetworkNodeID int) map[int]float64 { + // Initialize distances + distances := make(map[int]float64) + for nodeID := range graph.Nodes { + distances[nodeID] = math.Inf(1) // positive infinity + } + distances[sourceNetworkNodeID] = 0 + + // Priority queue for Dijkstra + pq := &priorityQueue{} + heap.Init(pq) + heap.Push(pq, &item{nodeID: sourceNetworkNodeID, distance: 0}) + + // Track visited nodes + visited := make(map[int]bool) + + for pq.Len() > 0 { + // Get node with minimum distance + current := heap.Pop(pq).(*item) + currentNodeID := current.nodeID + + // Skip if already visited + if visited[currentNodeID] { + continue + } + visited[currentNodeID] = true + + // Check all edges from current node + if edges, ok := graph.Edges[currentNodeID]; ok { + for targetNodeID, edge := range edges { + // Calculate new distance + newDistance := distances[currentNodeID] + edge.LatencySec + + // Update if shorter path found + if newDistance < distances[targetNodeID] { + distances[targetNodeID] = newDistance + heap.Push(pq, &item{ + nodeID: targetNodeID, + distance: newDistance, + }) + } + } + } + } + + return distances +} + +// Priority queue implementation for Dijkstra's algorithm + +type item struct { + nodeID int + distance float64 + index int // index in the heap +} + +type priorityQueue []*item + +func (pq priorityQueue) Len() int { return len(pq) } + +func (pq priorityQueue) Less(i, j int) bool { + return pq[i].distance < pq[j].distance +} + +func (pq priorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] + pq[i].index = i + pq[j].index = j +} + +func (pq *priorityQueue) Push(x interface{}) { + n := len(*pq) + item := x.(*item) + item.index = n + *pq = append(*pq, item) +} + +func (pq *priorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + old[n-1] = nil // avoid memory leak + item.index = -1 // for safety + *pq = old[0 : n-1] + return item +} diff --git a/networkinfo/networkinfo_test.go b/networkinfo/networkinfo_test.go new file mode 100644 index 0000000..8a1f35d --- /dev/null +++ b/networkinfo/networkinfo_test.go @@ -0,0 +1,262 @@ +package networkinfo + +import ( + "math" + "testing" +) + +func TestParse(t *testing.T) { + // Test with actual project files + netInfo, err := Parse("../shadow-gossipsub.yaml", "../graph.gml") + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + // Verify we have 3 hosts (node0, node1, node2) + expectedHostCount := 3 + if len(netInfo.Hosts) != expectedHostCount { + t.Errorf("Expected %d hosts, got %d", expectedHostCount, len(netInfo.Hosts)) + } + + // Verify host ID to network node ID mappings from shadow-gossipsub.yaml + expectedMappings := map[int]int{ + 0: 4, // node0 -> network_node_id 4 (na_east-supernode) + 1: 9, // node1 -> network_node_id 9 (europe-fullnode) + 2: 13, // node2 -> network_node_id 13 (na_west-fullnode) + } + + for hostID, expectedNetworkNodeID := range expectedMappings { + if hostID >= len(netInfo.Hosts) { + t.Errorf("Host %d not found", hostID) + continue + } + host := netInfo.Hosts[hostID] + if host.HostID != hostID { + t.Errorf("Host at index %d has HostID %d, expected %d", hostID, host.HostID, hostID) + } + if host.NetworkNodeID != expectedNetworkNodeID { + t.Errorf("Host %d has NetworkNodeID %d, expected %d", hostID, host.NetworkNodeID, expectedNetworkNodeID) + } + } + + // Verify bandwidth characteristics + // node0 -> network node 4 (na_east-supernode) -> 1024 Mbit = 1024000000 bps + host0 := netInfo.Hosts[0] + expectedSuperNodeBW := uint64(1024000000) + if host0.BandwidthUpBps != expectedSuperNodeBW { + t.Errorf("Host 0 bandwidth up: expected %d, got %d", expectedSuperNodeBW, host0.BandwidthUpBps) + } + if host0.BandwidthDownBps != expectedSuperNodeBW { + t.Errorf("Host 0 bandwidth down: expected %d, got %d", expectedSuperNodeBW, host0.BandwidthDownBps) + } + + // node1 -> network node 9 (europe-fullnode) -> 50 Mbit = 50000000 bps + host1 := netInfo.Hosts[1] + expectedFullNodeBW := uint64(50000000) + if host1.BandwidthUpBps != expectedFullNodeBW { + t.Errorf("Host 1 bandwidth up: expected %d, got %d", expectedFullNodeBW, host1.BandwidthUpBps) + } + if host1.BandwidthDownBps != expectedFullNodeBW { + t.Errorf("Host 1 bandwidth down: expected %d, got %d", expectedFullNodeBW, host1.BandwidthDownBps) + } + + // node2 -> network node 13 (na_west-fullnode) -> 50 Mbit = 50000000 bps + host2 := netInfo.Hosts[2] + if host2.BandwidthUpBps != expectedFullNodeBW { + t.Errorf("Host 2 bandwidth up: expected %d, got %d", expectedFullNodeBW, host2.BandwidthUpBps) + } + if host2.BandwidthDownBps != expectedFullNodeBW { + t.Errorf("Host 2 bandwidth down: expected %d, got %d", expectedFullNodeBW, host2.BandwidthDownBps) + } +} + +func TestParseLatencies(t *testing.T) { + netInfo, err := Parse("../shadow-gossipsub.yaml", "../graph.gml") + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + // Test that latency map is populated for all host pairs + for srcHostID := 0; srcHostID < 3; srcHostID++ { + for dstHostID := 0; dstHostID < 3; dstHostID++ { + latency, ok := netInfo.GetLatency(srcHostID, dstHostID) + if !ok { + t.Errorf("Latency not found for host %d -> host %d", srcHostID, dstHostID) + } + if latency < 0 { + t.Errorf("Invalid negative latency for host %d -> host %d: %f", srcHostID, dstHostID, latency) + } + } + } + + // Test self-latency (should be 0 in Dijkstra - distance from node to itself) + // Host 0 (network node 4) -> Host 0 (network node 4) + selfLatency0, ok := netInfo.GetLatency(0, 0) + if !ok { + t.Fatal("Self-latency for host 0 not found") + } + expectedSelfLatency := 0.0 + if math.Abs(selfLatency0-expectedSelfLatency) > 1e-9 { + t.Errorf("Host 0 self-latency: expected %f, got %f", expectedSelfLatency, selfLatency0) + } + + // Test direct edge latency + // Host 0 (network node 4: na_east-supernode) -> Host 1 (network node 9: europe-fullnode) + // From graph.gml: na_east-supernode to europe-fullnode = 70 ms = 0.070 s + latency01, ok := netInfo.GetLatency(0, 1) + if !ok { + t.Fatal("Latency for host 0 -> host 1 not found") + } + expectedLatency01 := 0.070 + if math.Abs(latency01-expectedLatency01) > 1e-9 { + t.Errorf("Host 0 -> Host 1 latency: expected %f, got %f", expectedLatency01, latency01) + } + + // Test another direct edge + // Host 0 (network node 4: na_east-supernode) -> Host 2 (network node 13: na_west-fullnode) + // From graph.gml: na_east-supernode to na_west-fullnode = 60 ms = 0.060 s + latency02, ok := netInfo.GetLatency(0, 2) + if !ok { + t.Fatal("Latency for host 0 -> host 2 not found") + } + expectedLatency02 := 0.060 + if math.Abs(latency02-expectedLatency02) > 1e-9 { + t.Errorf("Host 0 -> Host 2 latency: expected %f, got %f", expectedLatency02, latency02) + } + + // Test that latencies are not symmetric (directed graph) + // Host 1 (network node 9: europe-fullnode) -> Host 0 (network node 4: na_east-supernode) + // From graph.gml: europe-fullnode to na_east-supernode = 70 ms = 0.070 s + latency10, ok := netInfo.GetLatency(1, 0) + if !ok { + t.Fatal("Latency for host 1 -> host 0 not found") + } + expectedLatency10 := 0.070 + if math.Abs(latency10-expectedLatency10) > 1e-9 { + t.Errorf("Host 1 -> Host 0 latency: expected %f, got %f", expectedLatency10, latency10) + } +} + +func TestGetHostInfo(t *testing.T) { + netInfo, err := Parse("../shadow-gossipsub.yaml", "../graph.gml") + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + // Test valid host ID + host0, ok := netInfo.GetHostInfo(0) + if !ok { + t.Fatal("GetHostInfo(0) returned false") + } + if host0.HostID != 0 { + t.Errorf("Expected HostID 0, got %d", host0.HostID) + } + + // Test invalid host IDs + _, ok = netInfo.GetHostInfo(-1) + if ok { + t.Error("GetHostInfo(-1) should return false") + } + + _, ok = netInfo.GetHostInfo(999) + if ok { + t.Error("GetHostInfo(999) should return false") + } +} + +func TestGetLatency(t *testing.T) { + netInfo, err := Parse("../shadow-gossipsub.yaml", "../graph.gml") + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + // Test valid latency query + _, ok := netInfo.GetLatency(0, 1) + if !ok { + t.Error("GetLatency(0, 1) should return true") + } + + // Test invalid latency queries + _, ok = netInfo.GetLatency(-1, 0) + if ok { + t.Error("GetLatency(-1, 0) should return false") + } + + _, ok = netInfo.GetLatency(0, 999) + if ok { + t.Error("GetLatency(0, 999) should return false") + } +} + +func TestParseInvalidFiles(t *testing.T) { + // Test non-existent shadow config + _, err := Parse("/nonexistent/shadow.yaml", "../graph.gml") + if err == nil { + t.Error("Expected error for non-existent shadow config") + } + + // Test non-existent GML file + _, err = Parse("../shadow-gossipsub.yaml", "/nonexistent/graph.gml") + if err == nil { + t.Error("Expected error for non-existent GML file") + } +} + +func TestDijkstraShortestPath(t *testing.T) { + // This test verifies that Dijkstra finds the shortest path, not just direct edges + netInfo, err := Parse("../shadow-gossipsub.yaml", "../graph.gml") + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + // Verify that all computed latencies are valid (non-negative, finite) + for srcHostID := 0; srcHostID < 3; srcHostID++ { + for dstHostID := 0; dstHostID < 3; dstHostID++ { + latency, ok := netInfo.GetLatency(srcHostID, dstHostID) + if !ok { + t.Errorf("Latency missing for %d -> %d", srcHostID, dstHostID) + continue + } + if math.IsInf(latency, 1) { + t.Errorf("Infinite latency for %d -> %d (unreachable)", srcHostID, dstHostID) + } + if latency < 0 { + t.Errorf("Negative latency for %d -> %d: %f", srcHostID, dstHostID, latency) + } + } + } + + // Test symmetry for paths that should be symmetric + // (In this graph, direct edges may have same latency in both directions) + latency01, _ := netInfo.GetLatency(0, 1) + latency10, _ := netInfo.GetLatency(1, 0) + if latency01 != latency10 { + t.Logf("Note: Latency 0->1 (%f) != 1->0 (%f) - graph is directed", latency01, latency10) + } +} + +func TestLatencyMapStructure(t *testing.T) { + netInfo, err := Parse("../shadow-gossipsub.yaml", "../graph.gml") + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + // Verify latency map structure + if len(netInfo.LatencyMap) != 3 { + t.Errorf("Expected 3 source hosts in latency map, got %d", len(netInfo.LatencyMap)) + } + + for srcHostID, dstMap := range netInfo.LatencyMap { + if srcHostID < 0 || srcHostID > 2 { + t.Errorf("Invalid source host ID in latency map: %d", srcHostID) + } + if len(dstMap) != 3 { + t.Errorf("Expected 3 destinations for host %d, got %d", srcHostID, len(dstMap)) + } + for dstHostID := range dstMap { + if dstHostID < 0 || dstHostID > 2 { + t.Errorf("Invalid destination host ID in latency map: %d", dstHostID) + } + } + } +} diff --git a/networkinfo/types.go b/networkinfo/types.go new file mode 100644 index 0000000..ff7e154 --- /dev/null +++ b/networkinfo/types.go @@ -0,0 +1,38 @@ +package networkinfo + +// HostNetworkInfo contains network characteristics for a single host +type HostNetworkInfo struct { + HostID int // Host ID (e.g., 0, 1, 2 for node0, node1, node2) + NetworkNodeID int // Corresponding network node ID in the GML graph + BandwidthUpBps uint64 // Upload bandwidth in bits per second + BandwidthDownBps uint64 // Download bandwidth in bits per second +} + +// NetworkInfo contains complete network information for all hosts +type NetworkInfo struct { + // Hosts is a list of host network information, indexed by host ID + // hosts[i] contains information for host with HostID == i + Hosts []HostNetworkInfo + + // LatencyMap contains shortest-path latencies between all host pairs + // latencyMap[srcHostID][dstHostID] gives the latency in seconds from src to dst + LatencyMap map[int]map[int]float64 +} + +// GetHostInfo returns the network information for a given host ID +func (n *NetworkInfo) GetHostInfo(hostID int) (*HostNetworkInfo, bool) { + if hostID < 0 || hostID >= len(n.Hosts) { + return nil, false + } + return &n.Hosts[hostID], true +} + +// GetLatency returns the shortest-path latency between two hosts in seconds +func (n *NetworkInfo) GetLatency(srcHostID, dstHostID int) (float64, bool) { + if dstMap, ok := n.LatencyMap[srcHostID]; ok { + if latency, ok := dstMap[dstHostID]; ok { + return latency, true + } + } + return 0, false +} From 9b7544c3e7afa26bb298d507ba61317215b107e3 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 15 Oct 2025 15:36:38 -0700 Subject: [PATCH 05/19] refactor gossipsub.go to not have all logic in main --- gossipsub/main.go | 181 ++++++++++++++++++++++++++-------------------- 1 file changed, 102 insertions(+), 79 deletions(-) diff --git a/gossipsub/main.go b/gossipsub/main.go index 43c49d4..7ec733a 100644 --- a/gossipsub/main.go +++ b/gossipsub/main.go @@ -12,6 +12,7 @@ import ( "github.com/libp2p/go-libp2p" pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" "github.com/multiformats/go-multiaddr" @@ -91,37 +92,41 @@ func main() { log.Printf("Host created with ID: %s, listening on port: %d", h.ID(), hostPort) - // Create GossipSub instance with Ethereum consensus-specs parameters - // Parameters from: https://github.com/ethereum/consensus-specs/blob/master/specs/phase0/p2p-interface.md - // Start with default parameters and override specific values - gossipsubParams := pubsub.DefaultGossipSubParams() - gossipsubParams.D = 8 // topic stable mesh target count - gossipsubParams.Dlo = 6 // topic stable mesh low watermark - gossipsubParams.Dhi = 12 // topic stable mesh high watermark - gossipsubParams.Dlazy = 6 // gossip target - gossipsubParams.HeartbeatInterval = 700 * time.Millisecond // 0.7 seconds - gossipsubParams.FanoutTTL = 60 * time.Second // 60 seconds - gossipsubParams.HistoryLength = 6 // mcache_len: number of windows to retain full messages - gossipsubParams.HistoryGossip = 3 // mcache_gossip: number of windows to gossip about + // Run GossipSub simulation + topicName := "gossipsub-sim" + err = RunGossipSubSimulation(ctx, h, *nodeID, topo, *msgSize, topicName) + if err != nil { + log.Fatalf("Failed to run GossipSub simulation: %v", err) + } + + // Block forever to keep the process alive + select {} +} + +// RunGossipSubSimulation runs a GossipSub simulation with the given host and parameters +// This function sets up GossipSub, connects to peers, and runs the message publishing simulation +func RunGossipSubSimulation(ctx context.Context, h host.Host, nodeID int, topo *topology.Topology, msgSize int, topicName string) error { + log.Printf("Starting GossipSub simulation for node %d", nodeID) + // Create GossipSub instance with Ethereum consensus-specs parameters + gossipsubParams := createGossipSubParams() ps, err := pubsub.NewGossipSub(ctx, h, pubsub.WithGossipSubParams(gossipsubParams), ) if err != nil { - log.Fatalf("Failed to create gossipsub: %v", err) + return fmt.Errorf("failed to create gossipsub: %w", err) } // Join topic - topicName := "gossipsub-sim" topic, err := ps.Join(topicName) if err != nil { - log.Fatalf("Failed to join topic: %v", err) + return fmt.Errorf("failed to join topic: %w", err) } // Subscribe to topic sub, err := topic.Subscribe() if err != nil { - log.Fatalf("Failed to subscribe to topic: %v", err) + return fmt.Errorf("failed to subscribe to topic: %w", err) } log.Printf("Successfully joined topic: %s", topicName) @@ -130,57 +135,16 @@ func main() { log.Printf("Waiting for all nodes to initialize...") time.Sleep(3 * time.Second) - // Helper function to connect to a peer by node ID - connectToPeer := func(peerNodeID int) { - hostname := fmt.Sprintf("node%d", peerNodeID) - port := 8000 + peerNodeID - - // Resolve hostname to IP address - addrs, err := net.LookupHost(hostname) - if err != nil { - log.Printf("Failed to resolve hostname %s: %v", hostname, err) - return - } - if len(addrs) == 0 { - log.Printf("No addresses found for hostname %s", hostname) - return - } - - // Create multiaddr - maddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%d", addrs[0], port)) - if err != nil { - log.Printf("Failed to create multiaddr: %v", err) - return - } - - // Parse peer ID (we need to know it in advance - use deterministic key generation) - peerPriv, _, _ := crypto.GenerateEd25519Key( - deterministicRandSource(peerNodeID), - ) - peerID, _ := peer.IDFromPrivateKey(peerPriv) - - // Add peer address to peerstore - peerInfo := peer.AddrInfo{ - ID: peerID, - Addrs: []multiaddr.Multiaddr{maddr}, - } - - err = h.Connect(ctx, peerInfo) - if err != nil { - log.Printf("Failed to connect to node %d: %v", peerNodeID, err) - } else { - log.Printf("Connected to node %d (peer ID: %s)", peerNodeID, peerID) - } - } - // Connect to peers based on topology - connections := topo.GetConnections(*nodeID) - log.Printf("Node %d connections: %v", *nodeID, connections) + connections := topo.GetConnections(nodeID) + log.Printf("Node %d connections: %v", nodeID, connections) for _, peerNodeID := range connections { // Only connect to higher-numbered peers to avoid duplicate connections - if peerNodeID > *nodeID { - connectToPeer(peerNodeID) + if peerNodeID > nodeID { + if err := connectToPeer(ctx, h, peerNodeID); err != nil { + log.Printf("Error connecting to peer %d: %v", peerNodeID, err) + } } } @@ -210,19 +174,8 @@ func main() { // All nodes publish a message log.Printf("Publishing message") - // Create message with specified size, fully filled - msgContent := fmt.Sprintf("Message-from-node-%d", *nodeID) - msg := make([]byte, *msgSize) - - // Fill the entire message buffer - for j := 0; j < *msgSize; j++ { - if j < len(msgContent) { - msg[j] = msgContent[j] - } else { - // Fill remaining bytes with a pattern to use full message size - msg[j] = byte('A' + (j % 26)) - } - } + msg := generateMessage(nodeID, msgSize) + msgContent := fmt.Sprintf("Message-from-node-%d", nodeID) err = topic.Publish(ctx, msg) if err != nil { @@ -232,10 +185,80 @@ func main() { } // Keep the process running for Shadow - log.Printf("Node %d running indefinitely for Shadow simulation", *nodeID) + log.Printf("Node %d running indefinitely for Shadow simulation", nodeID) +} - // Block forever to keep the process alive - select {} +// connectToPeer connects to a peer by node ID using deterministic peer ID generation +func connectToPeer(ctx context.Context, h host.Host, peerNodeID int) error { + hostname := fmt.Sprintf("node%d", peerNodeID) + port := 8000 + peerNodeID + + // Resolve hostname to IP address + addrs, err := net.LookupHost(hostname) + if err != nil { + return fmt.Errorf("failed to resolve hostname %s: %w", hostname, err) + } + if len(addrs) == 0 { + return fmt.Errorf("no addresses found for hostname %s", hostname) + } + + // Create multiaddr + maddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%d", addrs[0], port)) + if err != nil { + return fmt.Errorf("failed to create multiaddr: %w", err) + } + + // Parse peer ID (we need to know it in advance - use deterministic key generation) + peerPriv, _, _ := crypto.GenerateEd25519Key( + deterministicRandSource(peerNodeID), + ) + peerID, _ := peer.IDFromPrivateKey(peerPriv) + + // Add peer address to peerstore + peerInfo := peer.AddrInfo{ + ID: peerID, + Addrs: []multiaddr.Multiaddr{maddr}, + } + + err = h.Connect(ctx, peerInfo) + if err != nil { + return fmt.Errorf("failed to connect to node %d: %w", peerNodeID, err) + } + + log.Printf("Connected to node %d (peer ID: %s)", peerNodeID, peerID) + return nil +} + +// generateMessage creates a message with the specified size, padded with a pattern +func generateMessage(nodeID int, msgSize int) []byte { + msgContent := fmt.Sprintf("Message-from-node-%d", nodeID) + msg := make([]byte, msgSize) + + // Fill the entire message buffer + for j := 0; j < msgSize; j++ { + if j < len(msgContent) { + msg[j] = msgContent[j] + } else { + // Fill remaining bytes with a pattern to use full message size + msg[j] = byte('A' + (j % 26)) + } + } + return msg +} + +// createGossipSubParams creates GossipSub parameters following Ethereum consensus-specs +// Parameters from: https://github.com/ethereum/consensus-specs/blob/master/specs/phase0/p2p-interface.md +func createGossipSubParams() pubsub.GossipSubParams { + params := pubsub.DefaultGossipSubParams() + params.D = 8 // topic stable mesh target count + params.Dlo = 6 // topic stable mesh low watermark + params.Dhi = 12 // topic stable mesh high watermark + params.Dlazy = 6 // gossip target + params.HeartbeatInterval = 700 * time.Millisecond // 0.7 seconds + params.FanoutTTL = 60 * time.Second // 60 seconds + params.HistoryLength = 6 // mcache_len: number of windows to retain full messages + params.HistoryGossip = 3 // mcache_gossip: number of windows to gossip about + return params } // deterministicRandSource creates a deterministic random source based on a seed From a99ca738189bc510e26c24985b7d49eecab1829a Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 15 Oct 2025 19:05:14 -0700 Subject: [PATCH 06/19] simnet test --- .gitignore | 1 - go.mod | 96 ++++++++-------- go.sum | 205 +++++++++++++-------------------- gossipsub/main.go | 1 + gossipsub/simnet_test.go | 237 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 359 insertions(+), 181 deletions(-) create mode 100644 gossipsub/simnet_test.go diff --git a/.gitignore b/.gitignore index 5e2babe..47aa3a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Binaries *-sim -gossipsub gossipsub/gossipsub topology-gen topology/gen/topology-gen diff --git a/go.mod b/go.mod index 04a7c8c..c64fa99 100644 --- a/go.mod +++ b/go.mod @@ -1,33 +1,29 @@ module github.com/ethp2p/attsim -go 1.24 - -toolchain go1.24.5 +go 1.24.6 require ( github.com/ipfs/go-log/v2 v2.8.2 - github.com/libp2p/go-libp2p v0.41.1 + github.com/libp2p/go-libp2p v0.44.0 github.com/libp2p/go-libp2p-pubsub v0.15.0 - github.com/multiformats/go-multiaddr v0.15.0 + github.com/marcopolo/simnet v0.0.2-0.20251016051945-15ccc6fdc3cc + github.com/multiformats/go-multiaddr v0.16.0 + gopkg.in/yaml.v3 v3.0.1 ) +// A small change to quic-go to fix a bug that affects synctest +replace github.com/quic-go/quic-go => github.com/marcopolo/quic-go v0.30.1-0.20251016052655-f538f46d1031 + require ( github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/containerd/cgroups v1.1.0 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/elastic/gosigar v0.14.3 // indirect github.com/flynn/noise v1.1.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gopacket v1.1.19 // indirect - github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect @@ -37,17 +33,17 @@ require ( github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/koron/go-ssdp v0.0.5 // indirect + github.com/koron/go-ssdp v0.0.6 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/libp2p/go-flow-metrics v0.2.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect - github.com/libp2p/go-netroute v0.2.2 // indirect + github.com/libp2p/go-netroute v0.3.0 // indirect github.com/libp2p/go-reuseport v0.4.0 // indirect - github.com/libp2p/go-yamux/v5 v5.0.0 // indirect + github.com/libp2p/go-yamux/v5 v5.0.1 // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/miekg/dns v1.1.63 // indirect + github.com/miekg/dns v1.1.66 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/minio/sha256-simd v1.0.1 // indirect @@ -57,58 +53,56 @@ require ( github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect - github.com/multiformats/go-multicodec v0.9.0 // indirect + github.com/multiformats/go-multicodec v0.9.1 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect - github.com/multiformats/go-multistream v0.6.0 // indirect + github.com/multiformats/go-multistream v0.6.1 // indirect github.com/multiformats/go-varint v0.0.7 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.22.2 // indirect - github.com/opencontainers/runtime-spec v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/dtls/v3 v3.0.4 // indirect - github.com/pion/ice/v4 v4.0.8 // indirect - github.com/pion/interceptor v0.1.37 // indirect + github.com/pion/dtls/v3 v3.0.6 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/interceptor v0.1.40 // indirect github.com/pion/logging v0.2.3 // indirect github.com/pion/mdns/v2 v2.0.7 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/rtcp v1.2.15 // indirect - github.com/pion/rtp v1.8.11 // indirect - github.com/pion/sctp v1.8.37 // indirect - github.com/pion/sdp/v3 v3.0.10 // indirect - github.com/pion/srtp/v3 v3.0.4 // indirect + github.com/pion/rtp v1.8.19 // indirect + github.com/pion/sctp v1.8.39 // indirect + github.com/pion/sdp/v3 v3.0.13 // indirect + github.com/pion/srtp/v3 v3.0.6 // indirect github.com/pion/stun v0.6.1 // indirect github.com/pion/stun/v3 v3.0.0 // indirect github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/transport/v3 v3.0.7 // indirect - github.com/pion/turn/v4 v4.0.0 // indirect - github.com/pion/webrtc/v4 v4.0.10 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.21.1 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/pion/turn/v4 v4.0.2 // indirect + github.com/pion/webrtc/v4 v4.1.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.64.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.50.1 // indirect - github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect - github.com/raulk/go-watchdog v1.3.0 // indirect + github.com/quic-go/quic-go v0.55.0 // indirect + github.com/quic-go/webtransport-go v0.9.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/wlynxg/anet v0.0.5 // indirect - go.uber.org/dig v1.18.0 // indirect - go.uber.org/fx v1.23.0 // indirect - go.uber.org/mock v0.5.0 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/fx v1.24.0 // indirect + go.uber.org/mock v0.5.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.35.0 // indirect - golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect - golang.org/x/mod v0.23.0 // indirect - golang.org/x/net v0.35.0 // indirect - golang.org/x/sync v0.11.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.22.0 // indirect - golang.org/x/tools v0.30.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - lukechampine.com/blake3 v1.4.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.36.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + lukechampine.com/blake3 v1.4.1 // indirect ) diff --git a/go.sum b/go.sum index 4dc26cf..3914590 100644 --- a/go.sum +++ b/go.sum @@ -9,7 +9,6 @@ dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -19,17 +18,8 @@ github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBT github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= -github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= -github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -39,13 +29,7 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= -github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= -github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= @@ -55,16 +39,7 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -75,17 +50,12 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= -github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc= -github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= @@ -110,15 +80,14 @@ github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPw github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/koron/go-ssdp v0.0.5 h1:E1iSMxIs4WqxTbIBLtmNBeOOC+1sCIXQeqTWVnpmwhk= -github.com/koron/go-ssdp v0.0.5/go.mod h1:Qm59B7hpKpDqfyRNWRNr00jGwLdXjDyZh6y7rH6VS0w= +github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= +github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -132,8 +101,8 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= -github.com/libp2p/go-libp2p v0.41.1 h1:8ecNQVT5ev/jqALTvisSJeVNvXYJyK4NhQx1nNRXQZE= -github.com/libp2p/go-libp2p v0.41.1/go.mod h1:DcGTovJzQl/I7HMrby5ZRjeD0kQkGiy+9w6aEkSZpRI= +github.com/libp2p/go-libp2p v0.44.0 h1:5Gtt8OrF8yiXmH+Mx4+/iBeFRMK1TY3a8OrEBDEqAvs= +github.com/libp2p/go-libp2p v0.44.0/go.mod h1:NovCojezAt4dnDd4fH048K7PKEqH0UFYYqJRjIIu8zc= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-pubsub v0.15.0 h1:cG7Cng2BT82WttmPFMi50gDNV+58K626m/wR00vGL1o= @@ -142,22 +111,26 @@ github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUI github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= -github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8= -github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE= +github.com/libp2p/go-netroute v0.3.0 h1:nqPCXHmeNmgTJnktosJ/sIef9hvwYCrsLxXmfNks/oc= +github.com/libp2p/go-netroute v0.3.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= -github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po= -github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= +github.com/libp2p/go-yamux/v5 v5.0.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfYRSg= +github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marcopolo/quic-go v0.30.1-0.20251016052655-f538f46d1031 h1:w1vO6D+o7elaAKoe2JM3e4JPw3giyVnJOeP92lc5S6c= +github.com/marcopolo/quic-go v0.30.1-0.20251016052655-f538f46d1031/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= +github.com/marcopolo/simnet v0.0.2-0.20251016051945-15ccc6fdc3cc h1:UpIQ1nYweARKOxFhUZCN+/QdU5QT8QtgXfFekXfG96g= +github.com/marcopolo/simnet v0.0.2-0.20251016051945-15ccc6fdc3cc/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= -github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= -github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= +github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= @@ -178,34 +151,27 @@ github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYg github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= -github.com/multiformats/go-multiaddr v0.15.0 h1:zB/HeaI/apcZiTDwhY5YqMvNVl/oQYvs3XySU+qeAVo= -github.com/multiformats/go-multiaddr v0.15.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= +github.com/multiformats/go-multiaddr v0.16.0 h1:oGWEVKioVQcdIOBlYM8BH1rZDWOGJSqr9/BKl6zQ4qc= +github.com/multiformats/go-multiaddr v0.16.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= -github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= -github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multicodec v0.9.1 h1:x/Fuxr7ZuR4jJV4Os5g444F7xC4XmyUaT/FWtE+9Zjo= +github.com/multiformats/go-multicodec v0.9.1/go.mod h1:LLWNMtyV5ithSBUo3vFIMaeDy+h3EbkMTek1m+Fybbo= github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= -github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA= -github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg= +github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= +github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= -github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= -github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= -github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= -github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= -github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= @@ -214,12 +180,12 @@ github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oL github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U= -github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg= -github.com/pion/ice/v4 v4.0.8 h1:ajNx0idNG+S+v9Phu4LSn2cs8JEfTsA1/tEjkkAVpFY= -github.com/pion/ice/v4 v4.0.8/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= -github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= -github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= +github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= +github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= @@ -229,14 +195,14 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= -github.com/pion/rtp v1.8.11 h1:17xjnY5WO5hgO6SD3/NTIUPvSFw/PbLsIJyz1r1yNIk= -github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= -github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs= -github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= -github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA= -github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= -github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M= -github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ= +github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= +github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4= +github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= @@ -247,39 +213,32 @@ github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQp github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= -github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= -github.com/pion/webrtc/v4 v4.0.10 h1:Hq/JLjhqLxi+NmCtE8lnRPDr8H4LcNvwg8OxVcdv56Q= -github.com/pion/webrtc/v4 v4.0.10/go.mod h1:ViHLVaNpiuvaH8pdiuQxuA9awuE6KVzAXx3vVWilOck= +github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= +github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= +github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= +github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= -github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= +github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q= -github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= -github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg= -github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw= -github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= -github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= +github.com/quic-go/webtransport-go v0.9.0 h1:jgys+7/wm6JarGDrW+lD/r9BGqBAmqY/ssklE09bA70= +github.com/quic-go/webtransport-go v0.9.0/go.mod h1:4FUYIiUc75XSsF6HShcLeXXYZJ9AGwo/xh3L8M/P1ao= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= @@ -301,10 +260,8 @@ github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= @@ -313,7 +270,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -321,7 +277,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= @@ -331,14 +286,14 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= -go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= -go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= -go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= @@ -357,22 +312,20 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= -golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -393,8 +346,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -410,17 +363,14 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -450,26 +400,24 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= -golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -490,10 +438,9 @@ google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmE google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= @@ -506,7 +453,7 @@ grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJd honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -lukechampine.com/blake3 v1.4.0 h1:xDbKOZCVbnZsfzM6mHSYcGRHZ3YrLDzqz8XnV4uaD5w= -lukechampine.com/blake3 v1.4.0/go.mod h1:MQJNQCTnR+kwOP/JEZSxj3MaQjp80FOFSNMMHXcSeX0= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/gossipsub/main.go b/gossipsub/main.go index 7ec733a..3bc3d2a 100644 --- a/gossipsub/main.go +++ b/gossipsub/main.go @@ -186,6 +186,7 @@ func RunGossipSubSimulation(ctx context.Context, h host.Host, nodeID int, topo * // Keep the process running for Shadow log.Printf("Node %d running indefinitely for Shadow simulation", nodeID) + return nil } // connectToPeer connects to a peer by node ID using deterministic peer ID generation diff --git a/gossipsub/simnet_test.go b/gossipsub/simnet_test.go new file mode 100644 index 0000000..25c2bfc --- /dev/null +++ b/gossipsub/simnet_test.go @@ -0,0 +1,237 @@ +//go:build go1.25 + +package main + +import ( + "context" + "fmt" + "net" + "testing" + "testing/synctest" + "time" + + "github.com/ethp2p/attsim/networkinfo" + "github.com/ethp2p/attsim/topology" + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/peer" + simlibp2p "github.com/libp2p/go-libp2p/x/simlibp2p" + "github.com/marcopolo/simnet" +) + +// createLatencyFunc creates a LatencyFunc for the downlink that returns +// the latency from sender to receiver based on network info. +// Panics if sender cannot be determined or latency is not available. +// Uses a lookup map to convert IP addresses to host IDs. +func createLatencyFunc(receiverHostID int, netInfo *networkinfo.NetworkInfo, ipToHostID map[string]int) func(simnet.Packet) time.Duration { + return func(p simnet.Packet) time.Duration { + // Extract sender IP from packet + udpAddr, ok := p.From.(*net.UDPAddr) + if !ok { + panic(fmt.Sprintf("packet source is not UDP address: %v", p.From)) + } + senderIP := udpAddr.IP.String() + + // Look up sender host ID from map + senderHostID, ok := ipToHostID[senderIP] + if !ok { + panic(fmt.Sprintf("couldn't determine sender host ID from IP: %v", senderIP)) + } + + // Get latency from network info + latencySec, ok := netInfo.GetLatency(senderHostID, receiverHostID) + if !ok { + panic(fmt.Sprintf("no latency info available for sender %d -> receiver %d", senderHostID, receiverHostID)) + } + + return time.Duration(latencySec * float64(time.Second)) + } +} + +func TestGossipSubWithSimnet(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + // Load network information from Shadow config and GML graph + netInfo, err := networkinfo.Parse( + "../shadow-gossipsub.yaml", + "../graph.gml", + ) + if err != nil { + t.Fatalf("Failed to load network info: %v", err) + } + + // Use the node count from the network info + nodeCount := len(netInfo.Hosts) + if nodeCount == 0 { + t.Fatal("No hosts found in network info") + } + t.Logf("Using %d nodes from network configuration", nodeCount) + + // Create IP to host ID lookup map + ipToHostID := make(map[string]int) + for i := range nodeCount { + ip := simnet.IntToPublicIPv4(i) + ipToHostID[ip.String()] = i + } + + // Create link settings for each node + var linkSettings []simlibp2p.NodeLinkSettingsAndCount + + for i := range nodeCount { + hostInfo := netInfo.Hosts[i] + + // Create link settings with: + // - Uplink latency = 0 (sender doesn't add latency) + // - Downlink latency = function of sender (receiver adds latency) + linkSettings = append(linkSettings, simlibp2p.NodeLinkSettingsAndCount{ + LinkSettings: simnet.NodeBiDiLinkSettings{ + Uplink: simnet.LinkSettings{ + BitsPerSecond: int(hostInfo.BandwidthUpBps), + // Only applying latency on the downlink + // TODO: write out the proper explanation, or maybe just + // change simnet to not accept latency on the uplink + Latency: 0, + }, + Downlink: simnet.LinkSettings{ + BitsPerSecond: int(hostInfo.BandwidthDownBps), + LatencyFunc: createLatencyFunc(i, netInfo, ipToHostID), // Receiver adds latency based on sender + }, + }, + Count: 1, + }) + } + + network, meta, error := simlibp2p.SimpleLibp2pNetwork(linkSettings, simlibp2p.NetworkSettings{ + UseBlankHost: true, + }) + if error != nil { + t.Fatalf("Failed to create network: %v", error) + } + t.Log("Starting simnet...") + network.Start() + defer network.Close() + defer func() { + for _, h := range meta.Nodes { + h.Close() + } + }() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Create GossipSub instances for each host + const topicName = "test-topic" + // var pubsubs []*pubsub.PubSub + var topics []*pubsub.Topic + var subs []*pubsub.Subscription + + t.Log("Creating GossipSub instances...") + for i, h := range meta.Nodes { + // Create GossipSub with Ethereum parameters + t.Logf("Creating GossipSub for host %d...", i) + ps, err := pubsub.NewGossipSub(ctx, h, + pubsub.WithGossipSubParams(createGossipSubParams()), + ) + if err != nil { + t.Fatalf("Failed to create GossipSub for host %d: %v", i, err) + } + // pubsubs = append(pubsubs, ps) + t.Logf("Created GossipSub for host %d at %s", i, time.Now()) + + time.Sleep(time.Millisecond) + // Join topic + topic, err := ps.Join(topicName) + if err != nil { + t.Fatalf("Failed to join topic for host %d: %v", i, err) + } + topics = append(topics, topic) + + // Subscribe to topic + sub, err := topic.Subscribe() + if err != nil { + t.Fatalf("Failed to subscribe for host %d: %v", i, err) + } + subs = append(subs, sub) + } + + // Clean up pubsub + defer func() { + for _, sub := range subs { + sub.Cancel() + } + for _, topic := range topics { + topic.Close() + } + }() + + // Load topology from JSON file + t.Log("Loading topology from topology.json...") + topo, err := topology.LoadFromFile("../topology.json") + if err != nil { + t.Fatalf("Failed to load topology: %v", err) + } + t.Logf("Loaded topology: %s", topo.GetDescription()) + + // Verify topology node count matches network info + if topo.NodeCount != nodeCount { + t.Fatalf("Topology node count (%d) doesn't match network info node count (%d)", topo.NodeCount, nodeCount) + } + + // Verify topology is connected + if !topo.IsConnected() { + t.Fatal("Topology graph is not connected") + } + + // Connect hosts based on topology connections + t.Log("Connecting hosts based on topology...") + for _, conn := range topo.GetAllConnections() { + i, j := conn.From, conn.To + peerInfo := peer.AddrInfo{ + ID: meta.Nodes[j].ID(), + Addrs: meta.Nodes[j].Addrs(), + } + t.Logf("Connecting nodes %d and %d", i, j) + err := meta.Nodes[i].Connect(ctx, peerInfo) + if err != nil { + t.Fatalf("Failed to connect host %d to host %d: %v", i, j, err) + } + } + + // Wait for connections to stabilize + t.Log("Waiting for network to stabilize...") + time.Sleep(2 * time.Second) + + // Publish a message from host 0 + testMsg := []byte("Hello from simnet GossipSub!") + if err := topics[0].Publish(ctx, testMsg); err != nil { + t.Fatalf("Failed to publish message: %v", err) + } + t.Log("Published test message from host 0") + + // Wait for message propagation + receivedCount := 0 + + for i := 1; i < nodeCount; i++ { + go func(idx int) { + for { + msg, err := subs[idx].Next(ctx) + if err != nil { + return + } + if string(msg.Data) == string(testMsg) { + receivedCount++ + t.Logf("Host %d received message", idx) + return + } + } + }(i) + } + + // Wait for all messages to be received + time.Sleep(5 * time.Second) + + if receivedCount != nodeCount-1 { + t.Fatalf("Expected %d hosts to receive message, got %d", nodeCount-1, receivedCount) + } + + t.Log("All hosts successfully received the message!") + }) +} From 0e8fb77448732cad0c39538786141d03709e7574 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Thu, 16 Oct 2025 09:34:26 -0700 Subject: [PATCH 07/19] add simnet run command to Makefile and CI --- .github/workflows/ci.yml | 9 +++++++++ Makefile | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fdd9e7..ef6068c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,6 +105,15 @@ jobs: PEER_COUNT=4 \ MSG_SIZE=256 timeout-minutes: 30 + + - name: Run simnet simulation (random-regular, 10 nodes) + run: | + make run-simnet \ + TOPOLOGY_TYPE=random-regular \ + NODE_COUNT=10 \ + DEGREE=4 \ + MSG_SIZE=256 + timeout-minutes: 3 - name: Upload propagation plot uses: actions/upload-artifact@v4 diff --git a/Makefile b/Makefile index 6194eed..47ff9f6 100644 --- a/Makefile +++ b/Makefile @@ -58,6 +58,12 @@ run-sim: build generate-config shadow --progress $(PROGRESS) shadow-gossipsub.yaml @echo "GossipSub simulation completed" +# Run the complete Shadow simulation +run-simnet: generate-config + @echo "Starting GossipSub simnet simulation ($(NODE_COUNT) nodes, $(MSG_SIZE) byte message)..." + cd $(GOSSIPSUB_DIR) && go test -v . + @echo "GossipSub simnet simulation completed" + # Test simulation results test: @echo "Testing GossipSub simulation results..." From fdbf9e15cc9005f1000e500581361ec265fad61d Mon Sep 17 00:00:00 2001 From: Suphanat Chunhapanya Date: Fri, 17 Oct 2025 19:26:49 +0700 Subject: [PATCH 08/19] Enhance and plot simnet simulations --- .gitignore | 10 ++ CLAUDE.md | 98 +++++++++++++++++-- Makefile | 30 ++++-- README.md | 118 ++++++++++++++++++----- gossipsub/simnet_test.go | 72 ++++++++++---- networkinfo/networkinfo.go | 1 + networkinfo/types.go | 1 + plot_simnet_propagation.py | 178 +++++++++++++++++++++++++++++++++++ shadowconfig/shadowconfig.go | 17 ++++ 9 files changed, 470 insertions(+), 55 deletions(-) create mode 100644 plot_simnet_propagation.py diff --git a/.gitignore b/.gitignore index 47aa3a5..b731cc9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,10 @@ gossipsub/topology.json shadow-gossipsub.yaml message_propagation.png +# Simnet simulation outputs +simnet.log +simnet_propagation.png + # Go build artifacts *.out @@ -23,3 +27,9 @@ message_propagation.png # Coverage files coverage.out *.coverprofile + +# Python virtual environment +.venv/ +__pycache__/ +*.pyc +*.pyo diff --git a/CLAUDE.md b/CLAUDE.md index 591746c..f25eb22 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is a GossipSub simulation framework for testing Ethereum attestation propagation using the Shadow discrete-event network simulator. The project simulates libp2p GossipSub networks with realistic network conditions and Ethereum consensus-specs parameters. +This is a GossipSub simulation framework for testing Ethereum attestation propagation. The project supports two simulation approaches: + +1. **Shadow Simulator**: Discrete-event network simulator that runs the actual GossipSub binary in a simulated network environment +2. **Simnet**: Go's deterministic network simulation using `testing/synctest` for fast, reproducible tests + +Both approaches simulate libp2p GossipSub networks with realistic network conditions and Ethereum consensus-specs parameters. ## Architecture @@ -15,12 +20,23 @@ This is a GossipSub simulation framework for testing Ethereum attestation propag - **generators.go**: Three topology generators (mesh, tree, random-regular) - **gen/main.go**: CLI tool for generating topology files -- **gossipsub/**: GossipSub simulation binary - - **main.go**: Simulation entry point using go-libp2p-pubsub +- **gossipsub/**: GossipSub simulation binaries and tests + - **main.go**: Shadow simulation entry point using go-libp2p-pubsub + - **simnet_test.go**: Simnet test using Go 1.25+ testing/synctest - Implements Ethereum consensus-specs GossipSub parameters - Uses deterministic peer IDs for reproducibility - All nodes publish one message each (N nodes → N messages total) +- **networkinfo/**: Network configuration parser + - **networkinfo.go**: Parses Shadow YAML and GML graph files + - **types.go**: Data structures for host network information + - Extracts bandwidth, latency, and message size per host + +- **shadowconfig/**: Shadow configuration parser + - **shadowconfig.go**: Parses Shadow YAML configuration + - Extracts node network IDs and process arguments + - Used by simnet to configure network conditions + - **network_graph.py**: Shadow configuration generator - Generates realistic network graphs with global latencies - Distributes nodes across geographic locations @@ -31,11 +47,16 @@ This is a GossipSub simulation framework for testing Ethereum attestation propag - Checks that all nodes received the message - Used in CI tests -- **plot_propagation.py**: Message propagation visualization +- **plot_propagation.py**: Shadow message propagation visualization - Parses Shadow logs to extract message reception timestamps - Plots average messages per node over time - Generates message_propagation.png +- **plot_simnet_propagation.py**: Simnet message propagation visualization + - Parses simnet.log to extract message reception timestamps + - Plots average messages per node over time + - Generates simnet_propagation.png + - **shadow.template.yaml**: Shadow configuration template - **pyproject.toml**: Python project configuration @@ -65,19 +86,28 @@ Python dependencies defined in `pyproject.toml`: ## Development Commands ### Make Commands (Primary Interface) + +**Shadow Simulation:** ```bash make help # Show all available commands -make all # Build, run simulation, test, and plot (default) +make all # Build, run Shadow simulation, test, and plot (default) make build # Build GossipSub binary make build-topology-gen # Build topology generator make generate-topology # Generate topology file make generate-config # Generate Shadow configuration make run-sim # Run complete Shadow simulation -make test # Test simulation results -make plot # Plot message propagation over time +make test # Test Shadow simulation results +make plot # Plot Shadow message propagation over time make clean # Clean build artifacts and results ``` +**Simnet Simulation:** +```bash +make run-simnet # Run simnet test (requires Go 1.25+) +make plot-simnet # Plot simnet message propagation +make all-simnet # Run simnet and plot (complete workflow) +``` + ### Configuration Variables ```bash # Topology settings @@ -130,7 +160,9 @@ uv run plot_propagation.py 10 shadow shadow-gossipsub.yaml ``` -## Simulation Workflow +## Simulation Workflows + +### Shadow Simulation Workflow 1. **Topology Generation** - Makefile generates topology.json using topology-gen @@ -144,6 +176,7 @@ shadow shadow-gossipsub.yaml - Geographic distribution: Australia, Europe, Asia, Americas, Africa 3. **GossipSub Simulation** + - Shadow runs gossipsub binary for each node in simulated network - Each node loads topology and creates libp2p host - Deterministic peer IDs (seed-based key generation) - All nodes start synchronized at 2000/01/01 00:02:00 @@ -156,6 +189,45 @@ shadow shadow-gossipsub.yaml - Verifies all nodes received all N messages - Reports pass/fail for each node +### Simnet Simulation Workflow + +1. **Configuration Loading** + - Simnet test reads shadow-gossipsub.yaml and graph.gml + - Parses network parameters (bandwidth, latency, message size) + - Loads topology.json for peer connections + - Requires Go 1.25+ for testing/synctest support + +2. **Network Setup** + - Creates simulated network using simlibp2p and simnet packages + - Configures per-host bandwidth and latency based on Shadow config + - Uses IP-to-host-ID mapping for latency calculations + - Latency applied on downlink (receiver adds latency based on sender) + +3. **GossipSub Test Execution** + - Creates libp2p hosts for all nodes in deterministic simulation + - Establishes connections based on topology + - All nodes subscribe to test topic + - Every node publishes one message + - Tracks received message count per node using atomic counters + +4. **Result Validation** + - Test verifies each node received exactly N messages + - Logs reception timestamps to simnet.log for plotting + - Test fails if any node didn't receive all messages + - Validates complete message propagation across the network + +## Shadow vs Simnet + +| Aspect | Shadow | Simnet | +|--------|--------|--------| +| **Execution** | Discrete-event simulator, runs actual binaries | Go test using testing/synctest | +| **Speed** | Slower (minutes for 10 nodes) | Faster (seconds for 20 nodes) | +| **Realism** | High (realistic network, process isolation) | Medium (simulated network in memory) | +| **Debugging** | Harder (separate processes) | Easier (single process, Go debugger) | +| **Requirements** | Shadow v3.2.0+ | Go 1.25+ | +| **Use Case** | Final validation, realistic scenarios | Development, quick iteration | +| **Determinism** | Deterministic with same config | Fully deterministic (synctest) | + ## GossipSub Parameters The simulation uses Ethereum consensus-specs parameters: @@ -254,17 +326,27 @@ make all - shimlog: Shadow internal logs ### Troubleshooting + +**Shadow:** - **"Topology file is required"**: Makefile generates this automatically - **"cannot find binary path"**: Check gossipsub/gossipsub exists - **Shadow simulation hangs**: Check Shadow version (requires v3.2.0) - **All nodes didn't receive message**: Check Shadow logs for connection errors +**Simnet:** +- **"no test files" or build constraint error**: Requires Go 1.25+ for testing/synctest +- **Test fails with wrong message count**: Check simnet.log for errors, ensure topology is connected +- **Network latency errors**: Verify shadow-gossipsub.yaml and graph.gml are generated +- **Plot script fails**: Run `make run-simnet` first to generate simnet.log + ## Module Structure The project uses `github.com/ethp2p/attsim` with key dependencies: - go-libp2p v0.41.1 - go-libp2p-pubsub v0.15.0 - go-log/v2 v2.8.2 +- github.com/marcopolo/simnet (for simnet simulation) +- testing/synctest (Go 1.25+ standard library, for deterministic testing) ## Related Projects diff --git a/Makefile b/Makefile index 47ff9f6..6a604a1 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Makefile for attsim GossipSub Shadow simulation -.PHONY: all build build-topology-gen generate-topology generate-config run-sim test plot clean help check-deps +.PHONY: all all-simnet build build-topology-gen generate-topology generate-config run-sim run-simnet test plot plot-simnet clean help check-deps # Default parameters NODE_COUNT ?= 10 @@ -58,10 +58,10 @@ run-sim: build generate-config shadow --progress $(PROGRESS) shadow-gossipsub.yaml @echo "GossipSub simulation completed" -# Run the complete Shadow simulation +# Run the complete simnet simulation run-simnet: generate-config @echo "Starting GossipSub simnet simulation ($(NODE_COUNT) nodes, $(MSG_SIZE) byte message)..." - cd $(GOSSIPSUB_DIR) && go test -v . + cd $(GOSSIPSUB_DIR) && go test -v . 2>&1 | tee ../simnet.log @echo "GossipSub simnet simulation completed" # Test simulation results @@ -75,6 +75,13 @@ plot: uv run plot_propagation.py $(NODE_COUNT) @test -f message_propagation.png && echo "Plot generated: message_propagation.png" || echo "Plot generation failed" +# Plot simnet message propagation +plot-simnet: + @echo "Plotting simnet message propagation..." + @test -f simnet.log || (echo "Error: simnet.log not found. Run 'make run-simnet' first" && exit 1) + uv run plot_simnet_propagation.py simnet.log + @test -f simnet_propagation.png && echo "Plot generated: simnet_propagation.png" || echo "Plot generation failed" + # Clean build artifacts and simulation results clean: @echo "Cleaning up..." @@ -83,6 +90,8 @@ clean: rm -f shadow-gossipsub.yaml rm -f graph.gml rm -f message_propagation.png + rm -f simnet_propagation.png + rm -f simnet.log rm -rf shadow.data/ rm -f $(TOPOLOGY_GEN_DIR)/topology-gen @echo "Cleanup completed" @@ -90,6 +99,9 @@ clean: # Complete workflow: build, run, test, and plot all: run-sim test plot +# Complete simnet workflow: run and plot +all-simnet: run-simnet plot-simnet + # Help target help: @echo "attsim GossipSub Shadow Simulation" @@ -102,9 +114,12 @@ help: @echo " build - Build the GossipSub simulation binary" @echo " generate-config - Generate Shadow configuration" @echo " run-sim - Run complete Shadow simulation" + @echo " run-simnet - Run complete simnet simulation (requires Go 1.25+)" @echo " test - Test simulation results" - @echo " plot - Plot message propagation over time" - @echo " all - Run simulation, test, and plot (default)" + @echo " plot - Plot message propagation over time (Shadow)" + @echo " plot-simnet - Plot message propagation over time (simnet)" + @echo " all - Run Shadow simulation, test, and plot (default)" + @echo " all-simnet - Run simnet simulation and plot" @echo " clean - Clean up build artifacts and results" @echo " help - Show this help message" @echo "" @@ -118,7 +133,8 @@ help: @echo " LOG_LEVEL - Log level (default: $(LOG_LEVEL))" @echo "" @echo "Examples:" - @echo " make all # Run simulation, test, and plot (random-regular)" + @echo " make all # Run Shadow simulation, test, and plot" + @echo " make all-simnet # Run simnet simulation and plot" @echo " make run-sim NODE_COUNT=20 MSG_SIZE=512 # Custom parameters" @echo " make run-sim TOPOLOGY_TYPE=mesh NODE_COUNT=10 # Mesh topology" @echo " make run-sim TOPOLOGY_TYPE=tree NODE_COUNT=31 BRANCHING=2 # Tree topology" @@ -126,3 +142,5 @@ help: @echo " make run-sim PROGRESS=true # Run with progress bar" @echo " make test # Test existing simulation results" @echo " make plot NODE_COUNT=10 # Plot message propagation" + @echo " make run-simnet NODE_COUNT=10 # Run simnet simulation" + @echo " make plot-simnet # Plot simnet propagation" diff --git a/README.md b/README.md index acaefae..142624c 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,34 @@ # attsim - Attestation Simulation -A GossipSub simulation framework for testing Ethereum attestation propagation using the Shadow discrete-event network simulator. +A GossipSub simulation framework for testing Ethereum attestation propagation. Supports both Shadow discrete-event network simulation and Simnet deterministic testing. ## Overview -This repository provides tools to simulate GossipSub-based peer-to-peer networks using Shadow, focusing on realistic network conditions and Ethereum consensus layer parameters. +This repository provides tools to simulate GossipSub-based peer-to-peer networks with realistic network conditions and Ethereum consensus layer parameters. + +**Simulation Methods:** +1. **Shadow** - Discrete-event network simulator for realistic, large-scale simulations +2. **Simnet** - Fast, deterministic Go tests using `testing/synctest` (Go 1.25+) **Key Features:** - libp2p GossipSub implementation with Ethereum consensus-specs parameters - Multiple network topology generators (mesh, tree, random-regular) -- Shadow discrete-event network simulation - Deterministic peer IDs and reproducible simulations +- Message propagation visualization and validation ## Prerequisites ### Required -- **Go 1.23+** - for building simulation binaries -- **Shadow** - discrete-event network simulator ([installation guide](https://shadow.github.io/docs/guide/supported_platforms.html)) +- **Go 1.23+** - for building Shadow simulation binaries +- **Go 1.25+** - for running Simnet tests (optional, uses `testing/synctest`) - **Python 3.12+** - for configuration generation and visualization - **uv** - Python package manager ([installation guide](https://docs.astral.sh/uv/getting-started/installation/)) +### Optional (for Shadow simulation) + +- **Shadow v3.2.0+** - discrete-event network simulator ([installation guide](https://shadow.github.io/docs/guide/supported_platforms.html)) + ```bash # Install uv (if not already installed) curl -LsSf https://astral.sh/uv/install.sh | sh @@ -44,8 +52,10 @@ uv --version ## Quick Start +### Shadow Simulation + ```bash -# Run simulation with default parameters (random-regular topology, 10 nodes) +# Run Shadow simulation with default parameters (random-regular topology, 10 nodes) # This runs the simulation, tests results, and generates a propagation plot make all @@ -53,21 +63,42 @@ make all make run-sim NODE_COUNT=20 MSG_SIZE=512 TOPOLOGY_TYPE=mesh ``` +### Simnet Testing (Go 1.25+) + +```bash +# Run simnet test and generate plot +make all-simnet + +# Or run and plot separately +make run-simnet # Run the test +make plot-simnet # Generate propagation plot +``` + ## Directory Structure ``` attsim/ -├── README.md # This file -├── Makefile # Build and run commands -├── network_graph.py # Shadow network configuration generator -├── shadow.template.yaml # Shadow configuration template -├── topology/ # Network topology generation -│ ├── topology.go # Topology data structures -│ ├── generators.go # Topology generation algorithms -│ └── gen/ # Topology generator CLI tool +├── README.md # This file +├── CLAUDE.md # Project documentation for Claude Code +├── Makefile # Build and run commands +├── network_graph.py # Shadow network configuration generator +├── plot_propagation.py # Shadow message propagation plotter +├── plot_simnet_propagation.py # Simnet message propagation plotter +├── test_results.py # Shadow simulation test harness +├── shadow.template.yaml # Shadow configuration template +├── topology/ # Network topology generation +│ ├── topology.go # Topology data structures +│ ├── generators.go # Topology generation algorithms +│ └── gen/ # Topology generator CLI tool │ └── main.go -└── gossipsub/ # GossipSub simulation - └── main.go # Simulation entry point +├── networkinfo/ # Network configuration parser +│ ├── networkinfo.go # Shadow YAML and GML parser +│ └── types.go # Network info data structures +├── shadowconfig/ # Shadow config parser +│ └── shadowconfig.go # Shadow YAML parser +└── gossipsub/ # GossipSub simulation + ├── main.go # Shadow simulation entry point + └── simnet_test.go # Simnet deterministic test ``` ## Usage @@ -94,13 +125,21 @@ make run-sim PROGRESS=true ### Makefile Targets -- `make all` - Complete workflow (build, generate topology, run simulation, test, plot) +**Shadow Simulation:** +- `make all` - Complete Shadow workflow (build, generate topology, run simulation, test, plot) - `make build` - Build the GossipSub binary - `make generate-topology` - Generate topology file - `make generate-config` - Generate Shadow configuration -- `make run-sim` - Run complete simulation -- `make test` - Test simulation results -- `make plot` - Plot message propagation over time +- `make run-sim` - Run Shadow simulation +- `make test` - Test Shadow simulation results +- `make plot` - Plot Shadow message propagation over time + +**Simnet Testing:** +- `make all-simnet` - Complete Simnet workflow (run test and plot) +- `make run-simnet` - Run simnet test (requires Go 1.25+) +- `make plot-simnet` - Plot simnet message propagation + +**General:** - `make clean` - Clean up artifacts and results - `make help` - Show help with all options @@ -202,9 +241,22 @@ make plot make clean ``` +## Simulation Comparison + +| Aspect | Shadow | Simnet | +|--------|--------|--------| +| **Execution** | Discrete-event simulator, runs actual binaries | Go test using testing/synctest | +| **Speed** | Slower (minutes for 10 nodes) | Faster (seconds for 20 nodes) | +| **Realism** | High (realistic network, process isolation) | Medium (simulated network in memory) | +| **Debugging** | Harder (separate processes) | Easier (single process, Go debugger) | +| **Requirements** | Shadow v3.2.0+ | Go 1.25+ | +| **Use Case** | Final validation, realistic scenarios | Development, quick iteration | +| **Output** | Shadow logs in shadow.data/ | simnet.log | +| **Visualization** | plot_propagation.py | plot_simnet_propagation.py | + ## Architecture -### Simulation Workflow +### Shadow Simulation Workflow 1. **Topology Generation** - The Makefile automatically generates a topology file based on parameters @@ -223,6 +275,30 @@ make clean - Every node publishes one message after network stabilization - All nodes receive and log all messages (N messages for N nodes) +### Simnet Test Workflow + +1. **Configuration Loading** + - Simnet test reads shadow-gossipsub.yaml and graph.gml + - Parses network parameters (bandwidth, latency, message size) + - Loads topology.json for peer connections + +2. **Network Setup** + - Creates simulated network using simlibp2p and simnet packages + - Configures per-host bandwidth and latency based on Shadow config + - Latency applied on downlink (receiver adds latency based on sender) + +3. **GossipSub Test Execution** + - Creates libp2p hosts for all nodes in deterministic simulation + - Establishes connections based on topology + - All nodes subscribe to test topic + - Every node publishes one message + - Tracks received message count per node using atomic counters + +4. **Validation** + - Test verifies each node received exactly N messages + - Logs reception timestamps to simnet.log for plotting + - Test fails if any node didn't receive all messages + ### Topology JSON Format ```json diff --git a/gossipsub/simnet_test.go b/gossipsub/simnet_test.go index 25c2bfc..04305e6 100644 --- a/gossipsub/simnet_test.go +++ b/gossipsub/simnet_test.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "net" + "sync/atomic" "testing" "testing/synctest" "time" @@ -199,39 +200,70 @@ func TestGossipSubWithSimnet(t *testing.T) { t.Log("Waiting for network to stabilize...") time.Sleep(2 * time.Second) - // Publish a message from host 0 - testMsg := []byte("Hello from simnet GossipSub!") - if err := topics[0].Publish(ctx, testMsg); err != nil { - t.Fatalf("Failed to publish message: %v", err) - } - t.Log("Published test message from host 0") - - // Wait for message propagation - receivedCount := 0 + // Track received message counts per node + receivedCount := make([]atomic.Int64, nodeCount) - for i := 1; i < nodeCount; i++ { + // Start message receivers for all nodes + publishTime := time.Now() + for i := range nodeCount { go func(idx int) { for { - msg, err := subs[idx].Next(ctx) + _, err := subs[idx].Next(ctx) if err != nil { return } - if string(msg.Data) == string(testMsg) { - receivedCount++ - t.Logf("Host %d received message", idx) - return - } + receivedCount[idx].Add(1) + receiveTime := time.Now() + elapsedMs := float64(receiveTime.Sub(publishTime).Microseconds()) / 1000.0 + t.Logf("SIMNET_RECEIVE: time=%s host=%d elapsed_ms=%.3f", + receiveTime.UTC().Format(time.RFC3339Nano), idx, elapsedMs) } }(i) } - // Wait for all messages to be received + // All nodes publish a message (like Shadow simulation) + for i := range nodeCount { + hostMsgSize := netInfo.Hosts[i].MsgSize + if hostMsgSize == 0 { + t.Fatalf("Message size not set for host %d", i) + } + + // Create message for this node + testMsg := make([]byte, hostMsgSize) + msgHeader := []byte(fmt.Sprintf("Message-from-node-%d", i)) + copy(testMsg, msgHeader) + // Fill remaining bytes with pattern + for j := len(msgHeader); j < hostMsgSize; j++ { + testMsg[j] = byte('A' + (j % 26)) + } + + pubTime := time.Now() + if err := topics[i].Publish(ctx, testMsg); err != nil { + t.Fatalf("Failed to publish message from host %d: %v", i, err) + } + t.Logf("SIMNET_PUBLISH: time=%s host=%d", pubTime.UTC().Format(time.RFC3339Nano), i) + } + + // Wait for message propagation (N nodes * N messages) time.Sleep(5 * time.Second) - if receivedCount != nodeCount-1 { - t.Fatalf("Expected %d hosts to receive message, got %d", nodeCount-1, receivedCount) + // Verify all nodes received all N messages + t.Log("Verifying message reception...") + allPassed := true + for i := range nodeCount { + count := receivedCount[i].Load() + if count != int64(nodeCount) { + t.Errorf("Node %d received %d messages, expected %d", i, count, nodeCount) + allPassed = false + } else { + t.Logf("Node %d: received %d/%d messages ✓", i, count, nodeCount) + } + } + + if !allPassed { + t.Fatal("Not all nodes received the expected number of messages") } - t.Log("All hosts successfully received the message!") + t.Log("Simulation completed!") }) } diff --git a/networkinfo/networkinfo.go b/networkinfo/networkinfo.go index 93299a8..5625f62 100644 --- a/networkinfo/networkinfo.go +++ b/networkinfo/networkinfo.go @@ -47,6 +47,7 @@ func Parse(shadowConfigPath, gmlPath string) (*NetworkInfo, error) { NetworkNodeID: shadowNode.NetworkNodeID, BandwidthUpBps: networkNode.BandwidthUpBps, BandwidthDownBps: networkNode.BandwidthDownBps, + MsgSize: shadowNode.MsgSize, } } diff --git a/networkinfo/types.go b/networkinfo/types.go index ff7e154..5b4cfa6 100644 --- a/networkinfo/types.go +++ b/networkinfo/types.go @@ -6,6 +6,7 @@ type HostNetworkInfo struct { NetworkNodeID int // Corresponding network node ID in the GML graph BandwidthUpBps uint64 // Upload bandwidth in bits per second BandwidthDownBps uint64 // Download bandwidth in bits per second + MsgSize int // Message size in bytes } // NetworkInfo contains complete network information for all hosts diff --git a/plot_simnet_propagation.py b/plot_simnet_propagation.py new file mode 100644 index 0000000..297db82 --- /dev/null +++ b/plot_simnet_propagation.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +""" +Plot message propagation over time from simnet test results. + +Plots the number of received messages by all nodes divided by the number of nodes +as a function of time, showing how messages propagate through the network. +""" + +import sys +import os +import re +from datetime import datetime +from typing import List, Tuple +import matplotlib.pyplot as plt + + +def parse_timestamp(timestamp_str: str) -> datetime: + """Parse RFC3339Nano timestamp from simnet logs.""" + # Format: 2000-01-01T00:00:05.176Z or 2000-01-01T00:00:05Z + # Python's datetime doesn't support nanoseconds, so we'll truncate to microseconds + if '.' in timestamp_str: + # Split timestamp and fractional seconds + base, frac = timestamp_str.split('.') + # Remove 'Z' from fractional part + frac_digits = frac.rstrip('Z') + # Take only first 6 digits (microseconds) and pad with zeros if needed + frac_us = frac_digits[:6].ljust(6, '0') + timestamp_str = f"{base}.{frac_us}Z" + + return datetime.fromisoformat(timestamp_str.replace('Z', '+00:00')) + + +def parse_simnet_logs(log_file: str) -> Tuple[List[Tuple[float, int]], datetime]: + """ + Parse simnet test logs to extract message reception times. + + Returns: + Tuple of (List of (elapsed_ms, cumulative_count) tuples, publish_time) + """ + events = [] # List of (elapsed_ms, host_id) + publish_time = None + + if not os.path.exists(log_file): + print(f"Error: Log file not found: {log_file}") + return [], None + + with open(log_file, 'r') as f: + for line in f: + # Match SIMNET_PUBLISH: time= host= + publish_match = re.search(r'SIMNET_PUBLISH: time=([^\s]+) host=(\d+)', line) + if publish_match: + timestamp_str = publish_match.group(1) + publish_time = parse_timestamp(timestamp_str) + continue + + # Match SIMNET_RECEIVE: time= host= elapsed_ms= + receive_match = re.search(r'SIMNET_RECEIVE: time=([^\s]+) host=(\d+) elapsed_ms=([\d.]+)', line) + if receive_match: + timestamp_str = receive_match.group(1) + host_id = int(receive_match.group(2)) + elapsed_ms = float(receive_match.group(3)) + events.append((elapsed_ms, host_id)) + + if not events: + return [], publish_time + + # Sort events by elapsed time + events.sort(key=lambda x: x[0]) + + # Calculate cumulative totals + cumulative_data = [] + total_count = 0 + + for elapsed_ms, host_id in events: + total_count += 1 + cumulative_data.append((elapsed_ms, total_count)) + + return cumulative_data, publish_time + + +def plot_propagation(log_file: str, node_count: int): + """Plot message propagation over time from simnet test results.""" + print(f"Parsing simnet logs for {node_count} nodes...") + + cumulative_data, publish_time = parse_simnet_logs(log_file) + + if not cumulative_data: + print("Error: No message reception events found in logs") + return + + if publish_time is None: + print("Warning: Publish time not found in logs") + + # Calculate metric: total_received / node_count + timestamps = [t for t, _ in cumulative_data] + avg_per_node = [count / node_count for _, count in cumulative_data] + + # Create plot + plt.figure(figsize=(10, 6)) + plt.plot(timestamps, avg_per_node, linewidth=2) + plt.xlabel('Time (ms)', fontsize=12) + plt.ylabel('Average messages received per node', fontsize=12) + plt.title(f'Simnet Message Propagation Over Time ({node_count} nodes)', fontsize=14) + plt.grid(True, alpha=0.3) + + # Add horizontal line at expected final value + # All nodes publish, so expected is N messages per node (like Shadow) + expected_final = node_count + plt.axhline(y=expected_final, color='r', linestyle='--', alpha=0.5, label=f'Expected final: {expected_final}') + + plt.legend() + plt.tight_layout() + + # Save plot + output_file = 'simnet_propagation.png' + plt.savefig(output_file, dpi=150) + print(f"\nPlot saved to: {output_file}") + + # Print statistics + if cumulative_data: + start_time = timestamps[0] + end_time = timestamps[-1] + duration = end_time - start_time + final_avg = avg_per_node[-1] + + print(f"\nPropagation Statistics:") + print(f" Start time: {start_time:.3f}ms (after publish)") + print(f" End time: {end_time:.3f}ms (after publish)") + print(f" Duration: {duration:.3f}ms") + print(f" Final average: {final_avg:.2f} messages/node") + print(f" Expected: {expected_final} messages/node") + print(f" Total events: {len(cumulative_data)}") + + if publish_time: + print(f" Publish time: {publish_time.isoformat()}") + + +def main(): + if len(sys.argv) < 2 or len(sys.argv) > 3: + print("Usage: python3 plot_simnet_propagation.py [node-count]") + print(" log-file: Path to simnet test output log file") + print(" node-count: Number of nodes in the simulation (optional, for plotting)") + sys.exit(1) + + log_file = sys.argv[1] + + # Try to determine node count from log file if not provided + node_count = None + if len(sys.argv) == 3: + try: + node_count = int(sys.argv[2]) + except ValueError: + print("Error: node-count must be an integer") + sys.exit(1) + else: + # Try to infer node count from the log file + # Look for "Using N nodes from network configuration" line + if os.path.exists(log_file): + with open(log_file, 'r') as f: + for line in f: + match = re.search(r'Using (\d+) nodes from network configuration', line) + if match: + node_count = int(match.group(1)) + break + + if node_count is None: + print("Error: Could not determine node count. Please provide it as second argument.") + sys.exit(1) + + if node_count <= 0: + print("Error: node-count must be positive") + sys.exit(1) + + plot_propagation(log_file, node_count) + + +if __name__ == "__main__": + main() diff --git a/shadowconfig/shadowconfig.go b/shadowconfig/shadowconfig.go index 3bdd784..3a7c0e4 100644 --- a/shadowconfig/shadowconfig.go +++ b/shadowconfig/shadowconfig.go @@ -14,6 +14,7 @@ import ( type NodeNetworkInfo struct { NodeID int NetworkNodeID int + MsgSize int // Message size in bytes from -msg-size argument } // Process represents a Shadow process configuration @@ -63,9 +64,25 @@ func ParseConfig(filename string) ([]NodeNetworkInfo, error) { return nil, fmt.Errorf("failed to parse node ID from %s: %w", hostName, err) } + // Parse message size from process arguments + var msgSize int + if len(host.Processes) > 0 { + args := strings.Fields(host.Processes[0].Args) + for i := 0; i < len(args)-1; i++ { + if args[i] == "-msg-size" { + msgSize, err = strconv.Atoi(args[i+1]) + if err != nil { + return nil, fmt.Errorf("failed to parse msg-size for %s: %w", hostName, err) + } + break + } + } + } + nodes = append(nodes, NodeNetworkInfo{ NodeID: nodeID, NetworkNodeID: host.NetworkNodeID, + MsgSize: msgSize, }) } From 4cb6be542c0b5c5ffeb900c72e8da6f83cfe913c Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 17 Oct 2025 12:57:54 -0700 Subject: [PATCH 09/19] ruff format python files --- network_graph.py | 57 ++++++++++++++++++++++++-------------- plot_propagation.py | 31 +++++++++++++++------ plot_simnet_propagation.py | 48 ++++++++++++++++++++------------ pyproject.toml | 1 + test_results.py | 21 +++++++++++--- uv.lock | 30 +++++++++++++++++++- 6 files changed, 136 insertions(+), 52 deletions(-) diff --git a/network_graph.py b/network_graph.py index 5e564b5..7c5d682 100755 --- a/network_graph.py +++ b/network_graph.py @@ -10,24 +10,28 @@ G = nx.DiGraph() + @dataclass class Location: name: str weight: int + @dataclass class Edge: src: Location dst: Location - latency: int # in ms + latency: int # in ms + @dataclass class NodeType: name: str - upload_bw: int # in Mbps - download_bw: int # in Mbps + upload_bw: int # in Mbps + download_bw: int # in Mbps weight: int + australia = Location("australia", 290) europe = Location("europe", 5599) @@ -44,7 +48,16 @@ class NodeType: fullnode = NodeType("fullnode", 50, 50, 80) node_types = [supernode, fullnode] -locations = [australia, europe, east_asia, west_asia, na_east, na_west, south_africa, south_america] +locations = [ + australia, + europe, + east_asia, + west_asia, + na_east, + na_west, + south_africa, + south_america, +] edges = [ Edge(australia, australia, 2), @@ -55,7 +68,6 @@ class NodeType: Edge(australia, south_america, 190), Edge(australia, south_africa, 220), Edge(australia, west_asia, 180), - Edge(east_asia, australia, 110), Edge(east_asia, east_asia, 4), Edge(east_asia, europe, 125), @@ -64,7 +76,6 @@ class NodeType: Edge(east_asia, south_america, 175), Edge(east_asia, south_africa, 175), Edge(east_asia, west_asia, 110), - Edge(europe, australia, 165), Edge(europe, east_asia, 125), Edge(europe, europe, 2), @@ -73,7 +84,6 @@ class NodeType: Edge(europe, south_america, 140), Edge(europe, south_africa, 95), Edge(europe, west_asia, 60), - Edge(na_west, australia, 110), Edge(na_west, east_asia, 100), Edge(na_west, europe, 110), @@ -82,7 +92,6 @@ class NodeType: Edge(na_west, south_america, 100), Edge(na_west, south_africa, 160), Edge(na_west, west_asia, 150), - Edge(na_east, australia, 150), Edge(na_east, east_asia, 140), Edge(na_east, europe, 70), @@ -91,7 +100,6 @@ class NodeType: Edge(na_east, south_america, 100), Edge(na_east, south_africa, 130), Edge(na_east, west_asia, 110), - Edge(south_america, australia, 190), Edge(south_america, east_asia, 175), Edge(south_america, europe, 140), @@ -100,7 +108,6 @@ class NodeType: Edge(south_america, south_america, 7), Edge(south_america, south_africa, 195), Edge(south_america, west_asia, 145), - Edge(south_africa, australia, 220), Edge(south_africa, east_asia, 175), Edge(south_africa, europe, 95), @@ -109,7 +116,6 @@ class NodeType: Edge(south_africa, south_america, 190), Edge(south_africa, south_africa, 7), Edge(south_africa, west_asia, 110), - Edge(west_asia, australia, 180), Edge(west_asia, east_asia, 110), Edge(west_asia, europe, 60), @@ -147,15 +153,22 @@ class NodeType: for location in locations: name = f"{location.name}-{node_type.name}" ids[name] = len(ids) - G.add_node(name, host_bandwidth_up=f"{node_type.upload_bw} Mbit", host_bandwidth_down=f"{node_type.download_bw} Mbit") + G.add_node( + name, + host_bandwidth_up=f"{node_type.upload_bw} Mbit", + host_bandwidth_down=f"{node_type.download_bw} Mbit", + ) for t1 in node_types: for t2 in node_types: for edge in edges: - G.add_edge(f"{edge.src.name}-{t1.name}", f"{edge.dst.name}-{t2.name}", - label=f"{edge.src.name}-{t1.name} to {edge.dst.name}-{t2.name}", - latency=f"{edge.latency} ms", - packet_loss=0.0) + G.add_edge( + f"{edge.src.name}-{t1.name}", + f"{edge.dst.name}-{t2.name}", + label=f"{edge.src.name}-{t1.name} to {edge.dst.name}-{t2.name}", + latency=f"{edge.latency} ms", + packet_loss=0.0, + ) with open("graph.gml", "w") as file: file.write("\n".join(nx.generate_gml(G))) @@ -185,11 +198,13 @@ class NodeType: config["hosts"][f"node{i}"] = { "network_node_id": ids[f"{location.name}-{node_type.name}"], - "processes": [{ - "args": args, - "expected_final_state": "running", - "path": "gossipsub/gossipsub", - }], + "processes": [ + { + "args": args, + "expected_final_state": "running", + "path": "gossipsub/gossipsub", + } + ], } with open("shadow-gossipsub.yaml", "w") as file: diff --git a/plot_propagation.py b/plot_propagation.py index df219cb..f654855 100644 --- a/plot_propagation.py +++ b/plot_propagation.py @@ -40,10 +40,13 @@ def parse_shadow_logs(node_count: int) -> List[Tuple[float, int]]: print(f"Warning: Log file not found: {log_file}") continue - with open(log_file, 'r') as f: + with open(log_file, "r") as f: for line in f: # Match: [gossipsub-node-X] YYYY/MM/DD HH:MM:SS.microseconds main.go:206: Received message N: - match = re.search(r'\[gossipsub-node-\d+\] (\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}\.\d+) main\.go:\d+: Received message \d+:', line) + match = re.search( + r"\[gossipsub-node-\d+\] (\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}\.\d+) main\.go:\d+: Received message \d+:", + line, + ) if match: timestamp_str = match.group(1) timestamp = parse_timestamp(timestamp_str) @@ -79,24 +82,32 @@ def plot_propagation(node_count: int): # Make timestamps relative to publish time (2000/01/01 00:02:00 = 120 seconds) publish_time = 120.0 - timestamps = [(t - publish_time) * 1000 for t in timestamps] # Convert to milliseconds + timestamps = [ + (t - publish_time) * 1000 for t in timestamps + ] # Convert to milliseconds # Create plot plt.figure(figsize=(10, 6)) plt.plot(timestamps, avg_per_node, linewidth=2) - plt.xlabel('Time (ms)', fontsize=12) - plt.ylabel('Average messages received per node', fontsize=12) - plt.title(f'Message Propagation Over Time ({node_count} nodes)', fontsize=14) + plt.xlabel("Time (ms)", fontsize=12) + plt.ylabel("Average messages received per node", fontsize=12) + plt.title(f"Message Propagation Over Time ({node_count} nodes)", fontsize=14) plt.grid(True, alpha=0.3) # Add horizontal line at expected final value (N messages per node for N nodes) - plt.axhline(y=node_count, color='r', linestyle='--', alpha=0.5, label=f'Expected final: {node_count}') + plt.axhline( + y=node_count, + color="r", + linestyle="--", + alpha=0.5, + label=f"Expected final: {node_count}", + ) plt.legend() plt.tight_layout() # Save plot - output_file = 'message_propagation.png' + output_file = "message_propagation.png" plt.savefig(output_file, dpi=150) print(f"\nPlot saved to: {output_file}") @@ -133,7 +144,9 @@ def main(): sys.exit(1) if not os.path.exists("shadow.data"): - print("Error: shadow.data directory not found. Run simulation first with 'make run-sim'") + print( + "Error: shadow.data directory not found. Run simulation first with 'make run-sim'" + ) sys.exit(1) plot_propagation(node_count) diff --git a/plot_simnet_propagation.py b/plot_simnet_propagation.py index 297db82..264672c 100644 --- a/plot_simnet_propagation.py +++ b/plot_simnet_propagation.py @@ -18,16 +18,16 @@ def parse_timestamp(timestamp_str: str) -> datetime: """Parse RFC3339Nano timestamp from simnet logs.""" # Format: 2000-01-01T00:00:05.176Z or 2000-01-01T00:00:05Z # Python's datetime doesn't support nanoseconds, so we'll truncate to microseconds - if '.' in timestamp_str: + if "." in timestamp_str: # Split timestamp and fractional seconds - base, frac = timestamp_str.split('.') + base, frac = timestamp_str.split(".") # Remove 'Z' from fractional part - frac_digits = frac.rstrip('Z') + frac_digits = frac.rstrip("Z") # Take only first 6 digits (microseconds) and pad with zeros if needed - frac_us = frac_digits[:6].ljust(6, '0') + frac_us = frac_digits[:6].ljust(6, "0") timestamp_str = f"{base}.{frac_us}Z" - return datetime.fromisoformat(timestamp_str.replace('Z', '+00:00')) + return datetime.fromisoformat(timestamp_str.replace("Z", "+00:00")) def parse_simnet_logs(log_file: str) -> Tuple[List[Tuple[float, int]], datetime]: @@ -44,17 +44,19 @@ def parse_simnet_logs(log_file: str) -> Tuple[List[Tuple[float, int]], datetime] print(f"Error: Log file not found: {log_file}") return [], None - with open(log_file, 'r') as f: + with open(log_file, "r") as f: for line in f: # Match SIMNET_PUBLISH: time= host= - publish_match = re.search(r'SIMNET_PUBLISH: time=([^\s]+) host=(\d+)', line) + publish_match = re.search(r"SIMNET_PUBLISH: time=([^\s]+) host=(\d+)", line) if publish_match: timestamp_str = publish_match.group(1) publish_time = parse_timestamp(timestamp_str) continue # Match SIMNET_RECEIVE: time= host= elapsed_ms= - receive_match = re.search(r'SIMNET_RECEIVE: time=([^\s]+) host=(\d+) elapsed_ms=([\d.]+)', line) + receive_match = re.search( + r"SIMNET_RECEIVE: time=([^\s]+) host=(\d+) elapsed_ms=([\d.]+)", line + ) if receive_match: timestamp_str = receive_match.group(1) host_id = int(receive_match.group(2)) @@ -98,21 +100,27 @@ def plot_propagation(log_file: str, node_count: int): # Create plot plt.figure(figsize=(10, 6)) plt.plot(timestamps, avg_per_node, linewidth=2) - plt.xlabel('Time (ms)', fontsize=12) - plt.ylabel('Average messages received per node', fontsize=12) - plt.title(f'Simnet Message Propagation Over Time ({node_count} nodes)', fontsize=14) + plt.xlabel("Time (ms)", fontsize=12) + plt.ylabel("Average messages received per node", fontsize=12) + plt.title(f"Simnet Message Propagation Over Time ({node_count} nodes)", fontsize=14) plt.grid(True, alpha=0.3) # Add horizontal line at expected final value # All nodes publish, so expected is N messages per node (like Shadow) expected_final = node_count - plt.axhline(y=expected_final, color='r', linestyle='--', alpha=0.5, label=f'Expected final: {expected_final}') + plt.axhline( + y=expected_final, + color="r", + linestyle="--", + alpha=0.5, + label=f"Expected final: {expected_final}", + ) plt.legend() plt.tight_layout() # Save plot - output_file = 'simnet_propagation.png' + output_file = "simnet_propagation.png" plt.savefig(output_file, dpi=150) print(f"\nPlot saved to: {output_file}") @@ -139,7 +147,9 @@ def main(): if len(sys.argv) < 2 or len(sys.argv) > 3: print("Usage: python3 plot_simnet_propagation.py [node-count]") print(" log-file: Path to simnet test output log file") - print(" node-count: Number of nodes in the simulation (optional, for plotting)") + print( + " node-count: Number of nodes in the simulation (optional, for plotting)" + ) sys.exit(1) log_file = sys.argv[1] @@ -156,15 +166,19 @@ def main(): # Try to infer node count from the log file # Look for "Using N nodes from network configuration" line if os.path.exists(log_file): - with open(log_file, 'r') as f: + with open(log_file, "r") as f: for line in f: - match = re.search(r'Using (\d+) nodes from network configuration', line) + match = re.search( + r"Using (\d+) nodes from network configuration", line + ) if match: node_count = int(match.group(1)) break if node_count is None: - print("Error: Could not determine node count. Please provide it as second argument.") + print( + "Error: Could not determine node count. Please provide it as second argument." + ) sys.exit(1) if node_count <= 0: diff --git a/pyproject.toml b/pyproject.toml index 9929b75..47888b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,4 +8,5 @@ dependencies = [ "networkx>=3.5", "pyyaml>=6.0.3", "matplotlib>=3.9.0", + "ruff>=0.14.1", ] diff --git a/test_results.py b/test_results.py index 53a53bb..1cf4463 100755 --- a/test_results.py +++ b/test_results.py @@ -11,6 +11,7 @@ import glob from typing import Dict + def parse_shadow_logs(node_count: int) -> Dict[int, int]: """ Parse Shadow log files to extract message reception counts. @@ -33,7 +34,7 @@ def parse_shadow_logs(node_count: int) -> Dict[int, int]: # Use the first stderr file found log_file = stderr_files[0] - with open(log_file, 'r') as f: + with open(log_file, "r") as f: for line in f: # Look for "Received message" log entries if "Received message" in line: @@ -50,6 +51,7 @@ def parse_shadow_logs(node_count: int) -> Dict[int, int]: return received_counts + def test_message_delivery(node_count: int) -> bool: """Test that all nodes received messages from all publishers.""" print(f"Shadow GossipSub Simulation Test Results") @@ -84,11 +86,18 @@ def test_message_delivery(node_count: int) -> bool: print("✓ ALL TESTS PASSED: All nodes received all messages") return True else: - failed_nodes = [i for i in range(node_count) if received_counts.get(i, 0) != expected_messages] - print(f"✗ TEST FAILED: {len(failed_nodes)} node(s) failed to receive all messages") + failed_nodes = [ + i + for i in range(node_count) + if received_counts.get(i, 0) != expected_messages + ] + print( + f"✗ TEST FAILED: {len(failed_nodes)} node(s) failed to receive all messages" + ) print(f"Failed nodes: {failed_nodes}") return False + def check_shadow_data_exists() -> bool: """Check if Shadow simulation data directory exists.""" if not os.path.exists("shadow.data"): @@ -97,11 +106,14 @@ def check_shadow_data_exists() -> bool: return False if not os.path.exists("shadow.data/hosts"): - print("Error: shadow.data/hosts directory not found. Simulation may have failed.") + print( + "Error: shadow.data/hosts directory not found. Simulation may have failed." + ) return False return True + def main(): """Main function.""" if len(sys.argv) < 2: @@ -125,5 +137,6 @@ def main(): success = test_message_delivery(node_count) sys.exit(0 if success else 1) + if __name__ == "__main__": main() diff --git a/uv.lock b/uv.lock index 7e2733a..0d39438 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12" [[package]] @@ -10,6 +10,7 @@ dependencies = [ { name = "matplotlib" }, { name = "networkx" }, { name = "pyyaml" }, + { name = "ruff" }, ] [package.metadata] @@ -17,6 +18,7 @@ requires-dist = [ { name = "matplotlib", specifier = ">=3.9.0" }, { name = "networkx", specifier = ">=3.5" }, { name = "pyyaml", specifier = ">=6.0.3" }, + { name = "ruff", specifier = ">=0.14.1" }, ] [[package]] @@ -478,6 +480,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "ruff" +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/58/6ca66896635352812de66f71cdf9ff86b3a4f79071ca5730088c0cd0fc8d/ruff-0.14.1.tar.gz", hash = "sha256:1dd86253060c4772867c61791588627320abcb6ed1577a90ef432ee319729b69", size = 5513429, upload-time = "2025-10-16T18:05:41.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/39/9cc5ab181478d7a18adc1c1e051a84ee02bec94eb9bdfd35643d7c74ca31/ruff-0.14.1-py3-none-linux_armv6l.whl", hash = "sha256:083bfc1f30f4a391ae09c6f4f99d83074416b471775b59288956f5bc18e82f8b", size = 12445415, upload-time = "2025-10-16T18:04:48.227Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2e/1226961855ccd697255988f5a2474890ac7c5863b080b15bd038df820818/ruff-0.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f6fa757cd717f791009f7669fefb09121cc5f7d9bd0ef211371fad68c2b8b224", size = 12784267, upload-time = "2025-10-16T18:04:52.515Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/fd9e95863124ed159cd0667ec98449ae461de94acda7101f1acb6066da00/ruff-0.14.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6191903d39ac156921398e9c86b7354d15e3c93772e7dbf26c9fcae59ceccd5", size = 11781872, upload-time = "2025-10-16T18:04:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5a/e890f7338ff537dba4589a5e02c51baa63020acfb7c8cbbaea4831562c96/ruff-0.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed04f0e04f7a4587244e5c9d7df50e6b5bf2705d75059f409a6421c593a35896", size = 12226558, upload-time = "2025-10-16T18:04:58.166Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7a/8ab5c3377f5bf31e167b73651841217542bcc7aa1c19e83030835cc25204/ruff-0.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9e6cf6cd4acae0febbce29497accd3632fe2025c0c583c8b87e8dbdeae5f61", size = 12187898, upload-time = "2025-10-16T18:05:01.455Z" }, + { url = "https://files.pythonhosted.org/packages/48/8d/ba7c33aa55406955fc124e62c8259791c3d42e3075a71710fdff9375134f/ruff-0.14.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fa2458527794ecdfbe45f654e42c61f2503a230545a91af839653a0a93dbc6", size = 12939168, upload-time = "2025-10-16T18:05:04.397Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c2/70783f612b50f66d083380e68cbd1696739d88e9b4f6164230375532c637/ruff-0.14.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:39f1c392244e338b21d42ab29b8a6392a722c5090032eb49bb4d6defcdb34345", size = 14386942, upload-time = "2025-10-16T18:05:07.102Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/cd7abb9c776b66d332119d67f96acf15830d120f5b884598a36d9d3f4d83/ruff-0.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7382fa12a26cce1f95070ce450946bec357727aaa428983036362579eadcc5cf", size = 13990622, upload-time = "2025-10-16T18:05:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/4259b696db12ac152fe472764b4f78bbdd9b477afd9bc3a6d53c01300b37/ruff-0.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0bf2be3ae8521e1093a487c4aa3b455882f139787770698530d28ed3fbb37c", size = 13431143, upload-time = "2025-10-16T18:05:13.46Z" }, + { url = "https://files.pythonhosted.org/packages/e0/35/266a80d0eb97bd224b3265b9437bd89dde0dcf4faf299db1212e81824e7e/ruff-0.14.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabcaa9ccf8089fb4fdb78d17cc0e28241520f50f4c2e88cb6261ed083d85151", size = 13132844, upload-time = "2025-10-16T18:05:16.1Z" }, + { url = "https://files.pythonhosted.org/packages/65/6e/d31ce218acc11a8d91ef208e002a31acf315061a85132f94f3df7a252b18/ruff-0.14.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:747d583400f6125ec11a4c14d1c8474bf75d8b419ad22a111a537ec1a952d192", size = 13401241, upload-time = "2025-10-16T18:05:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b5/dbc4221bf0b03774b3b2f0d47f39e848d30664157c15b965a14d890637d2/ruff-0.14.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5a6e74c0efd78515a1d13acbfe6c90f0f5bd822aa56b4a6d43a9ffb2ae6e56cd", size = 12132476, upload-time = "2025-10-16T18:05:22.163Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/ac99194e790ccd092d6a8b5f341f34b6e597d698e3077c032c502d75ea84/ruff-0.14.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ea6a864d2fb41a4b6d5b456ed164302a0d96f4daac630aeba829abfb059d020", size = 12139749, upload-time = "2025-10-16T18:05:25.162Z" }, + { url = "https://files.pythonhosted.org/packages/47/26/7df917462c3bb5004e6fdfcc505a49e90bcd8a34c54a051953118c00b53a/ruff-0.14.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0826b8764f94229604fa255918d1cc45e583e38c21c203248b0bfc9a0e930be5", size = 12544758, upload-time = "2025-10-16T18:05:28.018Z" }, + { url = "https://files.pythonhosted.org/packages/64/d0/81e7f0648e9764ad9b51dd4be5e5dac3fcfff9602428ccbae288a39c2c22/ruff-0.14.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cbc52160465913a1a3f424c81c62ac8096b6a491468e7d872cb9444a860bc33d", size = 13221811, upload-time = "2025-10-16T18:05:30.707Z" }, + { url = "https://files.pythonhosted.org/packages/c3/07/3c45562c67933cc35f6d5df4ca77dabbcd88fddaca0d6b8371693d29fd56/ruff-0.14.1-py3-none-win32.whl", hash = "sha256:e037ea374aaaff4103240ae79168c0945ae3d5ae8db190603de3b4012bd1def6", size = 12319467, upload-time = "2025-10-16T18:05:33.261Z" }, + { url = "https://files.pythonhosted.org/packages/02/88/0ee4ca507d4aa05f67e292d2e5eb0b3e358fbcfe527554a2eda9ac422d6b/ruff-0.14.1-py3-none-win_amd64.whl", hash = "sha256:59d599cdff9c7f925a017f6f2c256c908b094e55967f93f2821b1439928746a1", size = 13401123, upload-time = "2025-10-16T18:05:35.984Z" }, + { url = "https://files.pythonhosted.org/packages/b8/81/4b6387be7014858d924b843530e1b2a8e531846807516e9bea2ee0936bf7/ruff-0.14.1-py3-none-win_arm64.whl", hash = "sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44", size = 12436636, upload-time = "2025-10-16T18:05:38.995Z" }, +] + [[package]] name = "six" version = "1.17.0" From f448fb51bf8dba139c63be21bcc7a3f90de23bc2 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 17 Oct 2025 09:56:08 -0700 Subject: [PATCH 10/19] add chain topology generator --- Makefile | 3 ++- topology/gen/main.go | 8 ++++++-- topology/generators.go | 13 +++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 6a604a1..6161068 100644 --- a/Makefile +++ b/Makefile @@ -126,7 +126,7 @@ help: @echo "Configuration variables:" @echo " NODE_COUNT - Number of nodes (default: $(NODE_COUNT))" @echo " MSG_SIZE - Message size in bytes (default: $(MSG_SIZE))" - @echo " TOPOLOGY_TYPE - Topology type: mesh, tree, random-regular (default: $(TOPOLOGY_TYPE))" + @echo " TOPOLOGY_TYPE - Topology type: mesh, tree, chain, random-regular (default: $(TOPOLOGY_TYPE))" @echo " PEER_COUNT - Peer count for random-regular (default: $(PEER_COUNT))" @echo " BRANCHING - Branching factor for tree (default: $(BRANCHING))" @echo " PROGRESS - Show Shadow progress bar (default: $(PROGRESS))" @@ -138,6 +138,7 @@ help: @echo " make run-sim NODE_COUNT=20 MSG_SIZE=512 # Custom parameters" @echo " make run-sim TOPOLOGY_TYPE=mesh NODE_COUNT=10 # Mesh topology" @echo " make run-sim TOPOLOGY_TYPE=tree NODE_COUNT=31 BRANCHING=2 # Tree topology" + @echo " make run-sim TOPOLOGY_TYPE=chain NODE_COUNT=10 # Chain topology (linear)" @echo " make run-sim TOPOLOGY_TYPE=random-regular PEER_COUNT=6 # Random-regular with 6 peers" @echo " make run-sim PROGRESS=true # Run with progress bar" @echo " make test # Test existing simulation results" diff --git a/topology/gen/main.go b/topology/gen/main.go index 7d191a5..628df05 100644 --- a/topology/gen/main.go +++ b/topology/gen/main.go @@ -11,7 +11,7 @@ import ( func main() { var ( - topologyType = flag.String("type", "random-regular", "Topology type: mesh, tree, random-regular") + topologyType = flag.String("type", "random-regular", "Topology type: mesh, tree, chain, random-regular") nodeCount = flag.Int("nodes", 10, "Number of nodes") output = flag.String("output", "topology.json", "Output file path") @@ -34,6 +34,8 @@ func main() { fmt.Fprintf(os.Stderr, " %s -type mesh -nodes 10 -output mesh-10.json\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " Generate tree topology:\n") fmt.Fprintf(os.Stderr, " %s -type tree -nodes 31 -branching 2 -output tree-31.json\n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " Generate chain topology (linear):\n") + fmt.Fprintf(os.Stderr, " %s -type chain -nodes 10 -output chain-10.json\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " Generate random-regular topology (degree 4):\n") fmt.Fprintf(os.Stderr, " %s -type random-regular -nodes 20 -degree 4 -output rr-20.json\n\n", os.Args[0]) } @@ -53,10 +55,12 @@ func main() { topo = topology.GenerateMesh(*nodeCount) case "tree": topo = topology.GenerateTree(*nodeCount, *branchingFactor) + case "chain": + topo = topology.GenerateChain(*nodeCount) case "random-regular": topo = topology.GenerateRandomRegular(*nodeCount, *randomRegularDegree) default: - log.Fatalf("Unknown topology type: %s. Supported types: mesh, tree, random-regular", *topologyType) + log.Fatalf("Unknown topology type: %s. Supported types: mesh, tree, chain, random-regular", *topologyType) } if topo == nil { diff --git a/topology/generators.go b/topology/generators.go index 960efcc..031bf07 100644 --- a/topology/generators.go +++ b/topology/generators.go @@ -34,6 +34,19 @@ func GenerateTree(nodeCount int, branchingFactor int) *Topology { return topo } +// GenerateChain creates a linear chain topology where nodes are connected sequentially +// Node 0 connects to node 1, node 1 to node 2, etc. (0 -> 1 -> 2 -> ... -> N-1) +func GenerateChain(nodeCount int) *Topology { + topo := NewTopology(nodeCount) + + // Create sequential connections: 0-1, 1-2, 2-3, ..., (N-2)-(N-1) + for i := 0; i < nodeCount-1; i++ { + topo.AddConnection(i, i+1) + } + + return topo +} + // GenerateRandomRegular creates a random regular graph where each node has exactly 'degree' connections // to randomly selected other nodes. Guarantees connectivity by checking and regenerating until connected. func GenerateRandomRegular(nodeCount int, degree int) *Topology { From 395f7bf91a20d6209953a80c70ee3d7cf962202f Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 17 Oct 2025 13:01:32 -0700 Subject: [PATCH 11/19] tweak quic-go dep --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c64fa99..03e3216 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( ) // A small change to quic-go to fix a bug that affects synctest -replace github.com/quic-go/quic-go => github.com/marcopolo/quic-go v0.30.1-0.20251016052655-f538f46d1031 +replace github.com/quic-go/quic-go => github.com/marcopolo/quic-go v0.30.1-0.20251017180132-8b6fb2f67b20 require ( github.com/benbjohnson/clock v1.3.5 // indirect diff --git a/go.sum b/go.sum index 3914590..a0ca492 100644 --- a/go.sum +++ b/go.sum @@ -119,8 +119,8 @@ github.com/libp2p/go-yamux/v5 v5.0.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfY github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/marcopolo/quic-go v0.30.1-0.20251016052655-f538f46d1031 h1:w1vO6D+o7elaAKoe2JM3e4JPw3giyVnJOeP92lc5S6c= -github.com/marcopolo/quic-go v0.30.1-0.20251016052655-f538f46d1031/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= +github.com/marcopolo/quic-go v0.30.1-0.20251017180132-8b6fb2f67b20 h1:mdc82tJ7N/TlUSGTpQtb7BKb/AGVIH2g5K/ghK2cFgE= +github.com/marcopolo/quic-go v0.30.1-0.20251017180132-8b6fb2f67b20/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= github.com/marcopolo/simnet v0.0.2-0.20251016051945-15ccc6fdc3cc h1:UpIQ1nYweARKOxFhUZCN+/QdU5QT8QtgXfFekXfG96g= github.com/marcopolo/simnet v0.0.2-0.20251016051945-15ccc6fdc3cc/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= From 0f28cbde8a1d351c2bb814475edbbe8a8d0e2021 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 17 Oct 2025 13:01:55 -0700 Subject: [PATCH 12/19] deterministic network graphs --- network_graph.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/network_graph.py b/network_graph.py index 7c5d682..1f3fb64 100755 --- a/network_graph.py +++ b/network_graph.py @@ -8,6 +8,8 @@ import yaml import os +random.seed(0) + G = nx.DiGraph() From 9da81b2c574b0d41eeec82c6e351e084ad465581 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 17 Oct 2025 13:02:36 -0700 Subject: [PATCH 13/19] use the correct latency for edge with self --- networkinfo/networkinfo.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/networkinfo/networkinfo.go b/networkinfo/networkinfo.go index 5625f62..1bd8104 100644 --- a/networkinfo/networkinfo.go +++ b/networkinfo/networkinfo.go @@ -72,6 +72,10 @@ func computeAllPairsLatencies(shadowNodes []shadowconfig.NodeNetworkInfo, graph // Store distances to all other hosts latencyMap[srcHost.NodeID] = make(map[int]float64) for _, dstHost := range shadowNodes { + if srcHost.NetworkNodeID == dstHost.NetworkNodeID { + latencyMap[srcHost.NodeID][dstHost.NodeID] = graph.Edges[srcHost.NetworkNodeID][dstHost.NetworkNodeID].LatencySec + continue + } latencyMap[srcHost.NodeID][dstHost.NodeID] = distances[dstHost.NetworkNodeID] } } From dde5baeb701c883549373711678edbf68e051041 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 17 Oct 2025 13:02:59 -0700 Subject: [PATCH 14/19] disable shadow's modeling of unblocked syscall latency --- shadow.template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shadow.template.yaml b/shadow.template.yaml index 7f06328..726e420 100644 --- a/shadow.template.yaml +++ b/shadow.template.yaml @@ -1,6 +1,6 @@ general: stop_time: 600s - model_unblocked_syscall_latency: true + # model_unblocked_syscall_latency: true log_level: info heartbeat_interval: 1s From ac8198f093028facebccf95cde6d59de1d327294 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 17 Oct 2025 13:03:17 -0700 Subject: [PATCH 15/19] Add git commit hash to plots useful when debugging and iterating to be sure you're running on the expected changes --- plot_propagation.py | 19 ++++++++++++++++++- plot_simnet_propagation.py | 19 ++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/plot_propagation.py b/plot_propagation.py index f654855..bcded09 100644 --- a/plot_propagation.py +++ b/plot_propagation.py @@ -9,6 +9,7 @@ import sys import os import re +import subprocess from datetime import datetime from typing import List, Tuple import matplotlib.pyplot as plt @@ -86,12 +87,28 @@ def plot_propagation(node_count: int): (t - publish_time) * 1000 for t in timestamps ] # Convert to milliseconds + # Get git commit hash + git_hash = "" + try: + result = subprocess.run( + ["git", "rev-parse", "--short", "HEAD"], + capture_output=True, + text=True, + check=True, + ) + git_hash = result.stdout.strip() + except (subprocess.CalledProcessError, FileNotFoundError): + pass # Git not available or not a git repo + # Create plot plt.figure(figsize=(10, 6)) plt.plot(timestamps, avg_per_node, linewidth=2) plt.xlabel("Time (ms)", fontsize=12) plt.ylabel("Average messages received per node", fontsize=12) - plt.title(f"Message Propagation Over Time ({node_count} nodes)", fontsize=14) + title = f"Message Propagation Over Time ({node_count} nodes)" + if git_hash: + title += f" [{git_hash}]" + plt.title(title, fontsize=14) plt.grid(True, alpha=0.3) # Add horizontal line at expected final value (N messages per node for N nodes) diff --git a/plot_simnet_propagation.py b/plot_simnet_propagation.py index 264672c..3e3a632 100644 --- a/plot_simnet_propagation.py +++ b/plot_simnet_propagation.py @@ -9,6 +9,7 @@ import sys import os import re +import subprocess from datetime import datetime from typing import List, Tuple import matplotlib.pyplot as plt @@ -97,12 +98,28 @@ def plot_propagation(log_file: str, node_count: int): timestamps = [t for t, _ in cumulative_data] avg_per_node = [count / node_count for _, count in cumulative_data] + # Get git commit hash + git_hash = "" + try: + result = subprocess.run( + ["git", "rev-parse", "--short", "HEAD"], + capture_output=True, + text=True, + check=True, + ) + git_hash = result.stdout.strip() + except (subprocess.CalledProcessError, FileNotFoundError): + pass # Git not available or not a git repo + # Create plot plt.figure(figsize=(10, 6)) plt.plot(timestamps, avg_per_node, linewidth=2) plt.xlabel("Time (ms)", fontsize=12) plt.ylabel("Average messages received per node", fontsize=12) - plt.title(f"Simnet Message Propagation Over Time ({node_count} nodes)", fontsize=14) + title = f"Simnet Message Propagation Over Time ({node_count} nodes)" + if git_hash: + title += f" [{git_hash}]" + plt.title(title, fontsize=14) plt.grid(True, alpha=0.3) # Add horizontal line at expected final value From ae48568073af7343f024712c40eaa7e64865677d Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 17 Oct 2025 13:04:21 -0700 Subject: [PATCH 16/19] Add X_LIMIT env var for plots to get the same x axis limit --- plot_propagation.py | 10 ++++++++++ plot_simnet_propagation.py | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/plot_propagation.py b/plot_propagation.py index bcded09..623703c 100644 --- a/plot_propagation.py +++ b/plot_propagation.py @@ -120,6 +120,16 @@ def plot_propagation(node_count: int): label=f"Expected final: {node_count}", ) + # Set x-axis limit if X_LIMIT environment variable is set + x_limit = os.environ.get("X_LIMIT") + if x_limit: + try: + x_max = float(x_limit) + plt.xlim(0, x_max) + print(f"X-axis limited to: 0-{x_max}") + except ValueError: + print(f"Warning: Invalid X_LIMIT value '{x_limit}', ignoring") + plt.legend() plt.tight_layout() diff --git a/plot_simnet_propagation.py b/plot_simnet_propagation.py index 3e3a632..6b4610d 100644 --- a/plot_simnet_propagation.py +++ b/plot_simnet_propagation.py @@ -133,6 +133,16 @@ def plot_propagation(log_file: str, node_count: int): label=f"Expected final: {expected_final}", ) + # Set x-axis limit if X_LIMIT environment variable is set + x_limit = os.environ.get("X_LIMIT") + if x_limit: + try: + x_max = float(x_limit) + plt.xlim(0, x_max) + print(f"X-axis limited to: 0-{x_max}") + except ValueError: + print(f"Warning: Invalid X_LIMIT value '{x_limit}', ignoring") + plt.legend() plt.tight_layout() From 0e911351d889b7b49bfb4319ecae58cb6ac22282 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 17 Oct 2025 13:04:46 -0700 Subject: [PATCH 17/19] log who the message came from --- gossipsub/simnet_test.go | 14 +++++++++----- plot_simnet_propagation.py | 3 ++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/gossipsub/simnet_test.go b/gossipsub/simnet_test.go index 04305e6..3e288b7 100644 --- a/gossipsub/simnet_test.go +++ b/gossipsub/simnet_test.go @@ -3,6 +3,7 @@ package main import ( + "bytes" "context" "fmt" "net" @@ -208,15 +209,18 @@ func TestGossipSubWithSimnet(t *testing.T) { for i := range nodeCount { go func(idx int) { for { - _, err := subs[idx].Next(ctx) + msg, err := subs[idx].Next(ctx) if err != nil { return } receivedCount[idx].Add(1) receiveTime := time.Now() + fromSepEnd := bytes.Index(msg.Data, []byte("A")) + fromSepStart := bytes.LastIndex(msg.Data, []byte("-")) + from := string(msg.Data[fromSepStart+1 : fromSepEnd]) elapsedMs := float64(receiveTime.Sub(publishTime).Microseconds()) / 1000.0 - t.Logf("SIMNET_RECEIVE: time=%s host=%d elapsed_ms=%.3f", - receiveTime.UTC().Format(time.RFC3339Nano), idx, elapsedMs) + t.Logf("SIMNET_RECEIVE: time=%s host=%d from=%s elapsed_ms=%.3f", + receiveTime.UTC().Format(time.RFC3339Nano), idx, from, elapsedMs) } }(i) } @@ -230,7 +234,7 @@ func TestGossipSubWithSimnet(t *testing.T) { // Create message for this node testMsg := make([]byte, hostMsgSize) - msgHeader := []byte(fmt.Sprintf("Message-from-node-%d", i)) + msgHeader := []byte(fmt.Sprintf("Message-from-node-%dA", i)) copy(testMsg, msgHeader) // Fill remaining bytes with pattern for j := len(msgHeader); j < hostMsgSize; j++ { @@ -245,7 +249,7 @@ func TestGossipSubWithSimnet(t *testing.T) { } // Wait for message propagation (N nodes * N messages) - time.Sleep(5 * time.Second) + time.Sleep(10 * time.Second) // Verify all nodes received all N messages t.Log("Verifying message reception...") diff --git a/plot_simnet_propagation.py b/plot_simnet_propagation.py index 6b4610d..7e8038e 100644 --- a/plot_simnet_propagation.py +++ b/plot_simnet_propagation.py @@ -56,7 +56,8 @@ def parse_simnet_logs(log_file: str) -> Tuple[List[Tuple[float, int]], datetime] # Match SIMNET_RECEIVE: time= host= elapsed_ms= receive_match = re.search( - r"SIMNET_RECEIVE: time=([^\s]+) host=(\d+) elapsed_ms=([\d.]+)", line + r"SIMNET_RECEIVE: time=([^\s]+) host=(\d+) from=\d+ elapsed_ms=([\d.]+)", + line, ) if receive_match: timestamp_str = receive_match.group(1) From 2709a5d855d78414d6b2d81c1caebced865cbdcd Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 17 Oct 2025 13:05:29 -0700 Subject: [PATCH 18/19] Use quic-go in Shadow --- gossipsub/main.go | 55 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/gossipsub/main.go b/gossipsub/main.go index 3bc3d2a..7e0663f 100644 --- a/gossipsub/main.go +++ b/gossipsub/main.go @@ -7,6 +7,7 @@ import ( "io" "log" "net" + "sync/atomic" "time" "github.com/libp2p/go-libp2p" @@ -14,7 +15,10 @@ import ( "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/transport/quicreuse" "github.com/multiformats/go-multiaddr" + "github.com/quic-go/quic-go" + "go.uber.org/fx" "github.com/ethp2p/attsim/topology" @@ -79,18 +83,19 @@ func main() { // Create libp2p host with deterministic port based on node ID hostPort := 8000 + *nodeID - listenAddr := fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", hostPort) + listenAddr := fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic-v1", hostPort) h, err := libp2p.New( libp2p.Identity(priv), libp2p.ListenAddrStrings(listenAddr), + QUICOnShadow(), ) if err != nil { log.Fatalf("Failed to create libp2p host: %v", err) } defer h.Close() - log.Printf("Host created with ID: %s, listening on port: %d", h.ID(), hostPort) + log.Printf("Host created with ID: %s, listening on: %v", h.ID(), h.Addrs()) // Run GossipSub simulation topicName := "gossipsub-sim" @@ -204,7 +209,7 @@ func connectToPeer(ctx context.Context, h host.Host, peerNodeID int) error { } // Create multiaddr - maddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%d", addrs[0], port)) + maddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/udp/%d/quic-v1", addrs[0], port)) if err != nil { return fmt.Errorf("failed to create multiaddr: %w", err) } @@ -277,3 +282,47 @@ func (d deterministicRandSource) Read(p []byte) (int, error) { // Ensure it implements io.Reader var _ io.Reader = deterministicRandSource(0) + +type MockSourceIPSelector struct { + ip atomic.Pointer[net.IP] +} + +func (m *MockSourceIPSelector) PreferredSourceIPForDestination(_ *net.UDPAddr) (net.IP, error) { + return *m.ip.Load(), nil +} + +func QUICOnShadow() libp2p.Option { + m := &MockSourceIPSelector{} + quicReuseOpts := []quicreuse.Option{ + quicreuse.OverrideSourceIPSelector(func() (quicreuse.SourceIPSelector, error) { + return m, nil + }), + quicreuse.OverrideListenUDP(func(l string, address *net.UDPAddr) (net.PacketConn, error) { + m.ip.Store(&address.IP) + pc, err := net.ListenUDP(l, address) + if err != nil { + return nil, err + } + + return shadowUDPPacketConn{pc}, nil + }), + } + return libp2p.QUICReuse( + func(l fx.Lifecycle, statelessResetKey quic.StatelessResetKey, tokenKey quic.TokenGeneratorKey, opts ...quicreuse.Option) (*quicreuse.ConnManager, error) { + cm, err := quicreuse.NewConnManager(statelessResetKey, tokenKey, opts...) + if err != nil { + return nil, err + } + l.Append(fx.StopHook(func() error { + // When we pass in our own conn manager, we need to close it manually (??) + // TODO: this seems like a bug + return cm.Close() + })) + return cm, nil + }, quicReuseOpts...) + +} + +type shadowUDPPacketConn struct { + net.PacketConn +} From 7458a7ae3a1e9e89f7e0e371053440c45796304e Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 20 Oct 2025 10:19:29 -0700 Subject: [PATCH 19/19] bump simnet version --- go.mod | 6 +++--- go.sum | 4 ++-- gossipsub/simnet_test.go | 6 +----- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 03e3216..1c6b034 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,10 @@ require ( github.com/ipfs/go-log/v2 v2.8.2 github.com/libp2p/go-libp2p v0.44.0 github.com/libp2p/go-libp2p-pubsub v0.15.0 - github.com/marcopolo/simnet v0.0.2-0.20251016051945-15ccc6fdc3cc + github.com/marcopolo/simnet v0.0.2 github.com/multiformats/go-multiaddr v0.16.0 + github.com/quic-go/quic-go v0.55.0 + go.uber.org/fx v1.24.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -84,13 +86,11 @@ require ( github.com/prometheus/common v0.64.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.55.0 // indirect github.com/quic-go/webtransport-go v0.9.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/wlynxg/anet v0.0.5 // indirect go.uber.org/dig v1.19.0 // indirect - go.uber.org/fx v1.24.0 // indirect go.uber.org/mock v0.5.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect diff --git a/go.sum b/go.sum index a0ca492..4f753fc 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/marcopolo/quic-go v0.30.1-0.20251017180132-8b6fb2f67b20 h1:mdc82tJ7N/TlUSGTpQtb7BKb/AGVIH2g5K/ghK2cFgE= github.com/marcopolo/quic-go v0.30.1-0.20251017180132-8b6fb2f67b20/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= -github.com/marcopolo/simnet v0.0.2-0.20251016051945-15ccc6fdc3cc h1:UpIQ1nYweARKOxFhUZCN+/QdU5QT8QtgXfFekXfG96g= -github.com/marcopolo/simnet v0.0.2-0.20251016051945-15ccc6fdc3cc/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= +github.com/marcopolo/simnet v0.0.2 h1:5Khg+0B8aktz2vfM/lfIk5OAZBsz+0OoPyzy2UqcXQs= +github.com/marcopolo/simnet v0.0.2/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= diff --git a/gossipsub/simnet_test.go b/gossipsub/simnet_test.go index 3e288b7..4407baa 100644 --- a/gossipsub/simnet_test.go +++ b/gossipsub/simnet_test.go @@ -87,15 +87,11 @@ func TestGossipSubWithSimnet(t *testing.T) { LinkSettings: simnet.NodeBiDiLinkSettings{ Uplink: simnet.LinkSettings{ BitsPerSecond: int(hostInfo.BandwidthUpBps), - // Only applying latency on the downlink - // TODO: write out the proper explanation, or maybe just - // change simnet to not accept latency on the uplink - Latency: 0, }, Downlink: simnet.LinkSettings{ BitsPerSecond: int(hostInfo.BandwidthDownBps), - LatencyFunc: createLatencyFunc(i, netInfo, ipToHostID), // Receiver adds latency based on sender }, + LatencyFunc: createLatencyFunc(i, netInfo, ipToHostID), // Receiver adds latency based on sender }, Count: 1, })