From 9c46b692ffaa3453373fde149759c6f1ae3d515c Mon Sep 17 00:00:00 2001 From: Suphanat Chunhapanya Date: Thu, 6 Nov 2025 17:47:21 +0700 Subject: [PATCH 1/4] Add topic table extension --- Makefile | 7 +++++-- go.mod | 4 +++- go.sum | 4 ++++ gossipsub/main.go | 21 ++++++++++++++++++--- network_graph.py | 8 ++++++-- 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 6194eed..285b666 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ PEER_COUNT ?= 4 BRANCHING ?= 2 PROGRESS ?= false LOG_LEVEL ?= info +ENABLE_TTE ?= false # Paths TOPOLOGY_GEN_DIR = topology/gen @@ -46,8 +47,8 @@ build: check-deps # Generate network graph and Shadow configuration generate-config: generate-topology - @echo "Generating Shadow network configuration for $(NODE_COUNT) nodes..." - uv run network_graph.py $(NODE_COUNT) $(MSG_SIZE) $(TOPOLOGY_FILE) + @echo "Generating Shadow network configuration for $(NODE_COUNT) nodes (TTE=$(ENABLE_TTE))..." + uv run network_graph.py $(NODE_COUNT) $(MSG_SIZE) $(TOPOLOGY_FILE) $(ENABLE_TTE) @test -f shadow-gossipsub.yaml && test -f graph.gml || (echo "Config generation failed" && exit 1) @echo "Configuration generated" @@ -110,6 +111,7 @@ help: @echo " BRANCHING - Branching factor for tree (default: $(BRANCHING))" @echo " PROGRESS - Show Shadow progress bar (default: $(PROGRESS))" @echo " LOG_LEVEL - Log level (default: $(LOG_LEVEL))" + @echo " ENABLE_TTE - Enable TopicTableExtension (default: $(ENABLE_TTE))" @echo "" @echo "Examples:" @echo " make all # Run simulation, test, and plot (random-regular)" @@ -117,6 +119,7 @@ help: @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=random-regular PEER_COUNT=6 # Random-regular with 6 peers" + @echo " make run-sim ENABLE_TTE=true # Enable TopicTableExtension" @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" diff --git a/go.mod b/go.mod index d360001..17ed5f7 100644 --- a/go.mod +++ b/go.mod @@ -109,5 +109,7 @@ 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 - lukechampine.com/blake3 v1.4.0 // indirect + lukechampine.com/blake3 v1.4.1 // indirect ) + +replace github.com/libp2p/go-libp2p-pubsub => github.com/ppopth/go-libp2p-pubsub v0.15.1-0.20251104105446-b797c629a411 diff --git a/go.sum b/go.sum index 4dc26cf..a5d0b57 100644 --- a/go.sum +++ b/go.sum @@ -256,6 +256,8 @@ 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/ppopth/go-libp2p-pubsub v0.15.1-0.20251104105446-b797c629a411 h1:DqtAOcQRMnIhZ0a95DCuJWsoTT+ptnpVfHcfyHeUaUw= +github.com/ppopth/go-libp2p-pubsub v0.15.1-0.20251104105446-b797c629a411/go.mod h1:lr4oE8bFgQaifRcoc2uWhWWiK6tPdOEKpUuR408GFN4= 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= @@ -508,5 +510,7 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh 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 43c49d4..94d0827 100644 --- a/gossipsub/main.go +++ b/gossipsub/main.go @@ -27,6 +27,7 @@ func main() { msgSize = flag.Int("msg-size", 32, "Size of the message in bytes") topologyFile = flag.String("topology-file", "", "Path to topology JSON file (required)") logLevel = flag.String("log-level", "info", "Log level (debug, info, warn, error)") + enableTTE = flag.Bool("enable-tte", false, "Enable TopicTableExtension") ) flag.Parse() @@ -104,15 +105,29 @@ func main() { gossipsubParams.HistoryLength = 6 // mcache_len: number of windows to retain full messages gossipsubParams.HistoryGossip = 3 // mcache_gossip: number of windows to gossip about - ps, err := pubsub.NewGossipSub(ctx, h, + topicName := "/eth2/ad532ceb/beacon_attestation_50/ssz_snappy" + + // Build GossipSub options + opts := []pubsub.Option{ pubsub.WithGossipSubParams(gossipsubParams), - ) + } + + // Conditionally add TopicTableExtension + if *enableTTE { + log.Printf("TopicTableExtension enabled") + opts = append(opts, pubsub.WithTopicTableExtension(pubsub.TopicTableExtensionConfig{ + TopicBundles: [][]string{[]string{topicName}}, + })) + } else { + log.Printf("TopicTableExtension disabled") + } + + ps, err := pubsub.NewGossipSub(ctx, h, opts...) if err != nil { log.Fatalf("Failed to create gossipsub: %v", err) } // Join topic - topicName := "gossipsub-sim" topic, err := ps.Join(topicName) if err != nil { log.Fatalf("Failed to join topic: %v", err) diff --git a/network_graph.py b/network_graph.py index 5e564b5..b27fe01 100755 --- a/network_graph.py +++ b/network_graph.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# usage: python network_graph.py [node-count] [msg-size] [topology-file] +# usage: python network_graph.py [node-count] [msg-size] [topology-file] [enable-tte] # Generates Shadow network configuration for GossipSub simulation from dataclasses import dataclass import random @@ -121,15 +121,17 @@ class NodeType: ] if len(sys.argv) < 3: - print("Usage: python network_graph.py [node-count] [msg-size] [topology-file]") + print("Usage: python network_graph.py [node-count] [msg-size] [topology-file] [enable-tte]") print(" node-count: Number of nodes in the simulation") print(" msg-size: Size of the message in bytes") print(" topology-file: Path to topology JSON file (required)") + print(" enable-tte: Enable TopicTableExtension (true/false, optional, default: false)") sys.exit(1) node_count = int(sys.argv[1]) msg_size = int(sys.argv[2]) topology_file = sys.argv[3] if len(sys.argv) > 3 else "" +enable_tte = sys.argv[4].lower() == "true" if len(sys.argv) > 4 else False if not topology_file: print("Error: Topology file is required") @@ -182,6 +184,8 @@ class NodeType: # Build args with topology file args = f"-node-id {i} -node-count {node_count} -msg-size {msg_size} -topology-file {topology_file}" + if enable_tte: + args += " -enable-tte" config["hosts"][f"node{i}"] = { "network_node_id": ids[f"{location.name}-{node_type.name}"], From 9412ef97f66314ddbffc5ee0ac402659f4dc4501 Mon Sep 17 00:00:00 2001 From: Suphanat Chunhapanya Date: Thu, 6 Nov 2025 18:35:53 +0700 Subject: [PATCH 2/4] Show TTE and message size in the chart --- plot_propagation.py | 47 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/plot_propagation.py b/plot_propagation.py index ae54466..d5dd981 100644 --- a/plot_propagation.py +++ b/plot_propagation.py @@ -13,6 +13,7 @@ from datetime import datetime from typing import List, Tuple import matplotlib.pyplot as plt +import yaml def parse_timestamp(timestamp_str: str) -> float: @@ -64,11 +65,43 @@ def parse_shadow_logs(node_count: int) -> List[Tuple[float, int]]: return cumulative_data +def extract_config_info() -> tuple: + """Extract message size and TTE status from Shadow configuration.""" + config_file = "shadow-gossipsub.yaml" + if not os.path.exists(config_file): + return None, None + + try: + with open(config_file, 'r') as f: + config = yaml.safe_load(f) + + # Get first host's process args + for host_name, host_config in config.get('hosts', {}).items(): + processes = host_config.get('processes', []) + if processes: + args = processes[0].get('args', '') + # Parse -msg-size from args + msg_size = None + match = re.search(r'-msg-size\s+(\d+)', args) + if match: + msg_size = int(match.group(1)) + + # Check for -enable-tte flag + enable_tte = '-enable-tte' in args + + return msg_size, enable_tte + except Exception as e: + print(f"Warning: Could not extract config info: {e}") + + return None, None + + def plot_propagation(node_count: int, output_file: str = 'message_propagation.png'): """Plot message propagation over time.""" print(f"Parsing Shadow logs for {node_count} nodes...") cumulative_data = parse_shadow_logs(node_count) + msg_size, enable_tte = extract_config_info() if not cumulative_data: print("Error: No message reception events found in logs") @@ -87,7 +120,15 @@ def plot_propagation(node_count: int, output_file: str = 'message_propagation.pn 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) + + # Build title with message size and TTE status + title = f'Message Propagation Over Time ({node_count} nodes' + if msg_size is not None: + title += f', {msg_size}B' + if enable_tte is not None: + title += f', TTE={"ON" if enable_tte else "OFF"}' + title += ')' + 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) @@ -108,6 +149,10 @@ def plot_propagation(node_count: int, output_file: str = 'message_propagation.pn final_avg = avg_per_node[-1] print(f"\nPropagation Statistics:") + if msg_size is not None: + print(f" Message size: {msg_size} bytes") + if enable_tte is not None: + print(f" TopicTableExtension: {'Enabled' if enable_tte else 'Disabled'}") print(f" Start time: {start_time:.3f}ms (relative to publish)") print(f" End time: {end_time:.3f}ms (relative to publish)") print(f" Duration: {duration:.3f}ms") From ab38e359eee9bd9fea3c5f42340824e3943c57d8 Mon Sep 17 00:00:00 2001 From: Suphanat Chunhapanya Date: Thu, 6 Nov 2025 18:51:34 +0700 Subject: [PATCH 3/4] Test should pass if >95% of messages are received --- test_results.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/test_results.py b/test_results.py index 53a53bb..b3f935c 100755 --- a/test_results.py +++ b/test_results.py @@ -51,41 +51,44 @@ 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.""" + """Test that nodes received >95% of messages from all publishers.""" print(f"Shadow GossipSub Simulation Test Results") print("=" * 60) received_counts = parse_shadow_logs(node_count) - all_passed = True total_received = 0 expected_messages = node_count # Each node publishes, so expect N messages per node + threshold = 0.95 # Pass if >95% of messages received - print(f"\nMessage Delivery (Expected: {expected_messages} messages per node)") + print(f"\nMessage Delivery (Expected: {expected_messages} messages per node, threshold: >95%)") print("-" * 60) + failed_nodes = [] for node_id in range(node_count): received = received_counts.get(node_id, 0) total_received += received + percentage = (received / expected_messages * 100) if expected_messages > 0 else 0 - if received == expected_messages: + if received >= expected_messages * threshold: status = "✓ PASS" else: status = "✗ FAIL" - all_passed = False + failed_nodes.append(node_id) - print(f"Node {node_id:2d}: {received}/{expected_messages} messages {status}") + print(f"Node {node_id:2d}: {received}/{expected_messages} messages ({percentage:.1f}%) {status}") print("-" * 60) - print(f"Total messages received: {total_received}/{node_count * expected_messages}") + total_expected = node_count * expected_messages + total_percentage = (total_received / total_expected * 100) if total_expected > 0 else 0 + print(f"Total messages received: {total_received}/{total_expected} ({total_percentage:.1f}%)") print() - if all_passed: - print("✓ ALL TESTS PASSED: All nodes received all messages") + if not failed_nodes: + print(f"✓ ALL TESTS PASSED: All nodes received >{threshold*100:.0f}% of 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") + print(f"✗ TEST FAILED: {len(failed_nodes)} node(s) received <{threshold*100:.0f}% of messages") print(f"Failed nodes: {failed_nodes}") return False From a28e727b8e765efd5869314c171f721680c8c4d3 Mon Sep 17 00:00:00 2001 From: Suphanat Chunhapanya Date: Thu, 6 Nov 2025 19:59:43 +0700 Subject: [PATCH 4/4] Allow custom max x in the chart --- Makefile | 9 ++++++++- plot_propagation.py | 22 ++++++++++++++++------ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 285b666..55de469 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ BRANCHING ?= 2 PROGRESS ?= false LOG_LEVEL ?= info ENABLE_TTE ?= false +MAX_X ?= # Paths TOPOLOGY_GEN_DIR = topology/gen @@ -67,7 +68,11 @@ test: # Plot message propagation plot: @echo "Plotting message propagation..." - uv run plot_propagation.py $(NODE_COUNT) + @if [ -n "$(MAX_X)" ]; then \ + uv run plot_propagation.py $(NODE_COUNT) --max-x $(MAX_X); \ + else \ + uv run plot_propagation.py $(NODE_COUNT); \ + fi @test -f message_propagation.png && echo "Plot generated: message_propagation.png" || echo "Plot generation failed" # Clean build artifacts and simulation results @@ -112,6 +117,7 @@ help: @echo " PROGRESS - Show Shadow progress bar (default: $(PROGRESS))" @echo " LOG_LEVEL - Log level (default: $(LOG_LEVEL))" @echo " ENABLE_TTE - Enable TopicTableExtension (default: $(ENABLE_TTE))" + @echo " MAX_X - Maximum x-axis value in ms for plot (default: auto)" @echo "" @echo "Examples:" @echo " make all # Run simulation, test, and plot (random-regular)" @@ -123,3 +129,4 @@ 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 plot NODE_COUNT=10 MAX_X=5000 # Plot with x-axis max at 5000ms" diff --git a/plot_propagation.py b/plot_propagation.py index d5dd981..915e112 100644 --- a/plot_propagation.py +++ b/plot_propagation.py @@ -96,7 +96,7 @@ def extract_config_info() -> tuple: return None, None -def plot_propagation(node_count: int, output_file: str = 'message_propagation.png'): +def plot_propagation(node_count: int, output_file: str = 'message_propagation.png', max_x: float = None): """Plot message propagation over time.""" print(f"Parsing Shadow logs for {node_count} nodes...") @@ -134,6 +134,12 @@ def plot_propagation(node_count: int, output_file: str = 'message_propagation.pn # 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}') + # Set x-axis limits: always start at 0, optionally set max + if max_x is not None: + plt.xlim(left=0, right=max_x) + else: + plt.xlim(left=0) + plt.legend() plt.tight_layout() @@ -170,15 +176,19 @@ def main(): python3 plot_propagation.py 10 python3 plot_propagation.py 20 -o propagation_20nodes.png python3 plot_propagation.py 10 --output results/test1.png + python3 plot_propagation.py 10 --max-x 5000 """ ) - - parser.add_argument('node_count', type=int, + + parser.add_argument('node_count', type=int, help='Number of nodes in the simulation') - parser.add_argument('-o', '--output', type=str, + parser.add_argument('-o', '--output', type=str, default='message_propagation.png', help='Output file path for the plot (default: message_propagation.png)') - + parser.add_argument('--max-x', type=float, + default=None, + help='Maximum x-axis value in milliseconds (default: auto)') + args = parser.parse_args() if args.node_count <= 0: @@ -188,7 +198,7 @@ def main(): print("Error: shadow.data directory not found. Run simulation first with 'make run-sim'") sys.exit(1) - plot_propagation(args.node_count, args.output) + plot_propagation(args.node_count, args.output, args.max_x) if __name__ == "__main__":