Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ PEER_COUNT ?= 4
BRANCHING ?= 2
PROGRESS ?= false
LOG_LEVEL ?= info
ENABLE_TTE ?= false
MAX_X ?=

# Paths
TOPOLOGY_GEN_DIR = topology/gen
Expand Down Expand Up @@ -46,8 +48,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"

Expand All @@ -66,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
Expand Down Expand Up @@ -110,13 +116,17 @@ 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 " 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)"
@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=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"
@echo " make plot NODE_COUNT=10 MAX_X=5000 # Plot with x-axis max at 5000ms"
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
21 changes: 18 additions & 3 deletions gossipsub/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down
8 changes: 6 additions & 2 deletions network_graph.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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}"],
Expand Down
69 changes: 62 additions & 7 deletions plot_propagation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -64,11 +65,43 @@ def parse_shadow_logs(node_count: int) -> List[Tuple[float, int]]:
return cumulative_data


def plot_propagation(node_count: int, output_file: str = 'message_propagation.png'):
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', max_x: float = None):
"""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")
Expand All @@ -87,12 +120,26 @@ 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)
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()

Expand All @@ -108,6 +155,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")
Expand All @@ -125,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:
Expand All @@ -143,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__":
Expand Down
25 changes: 14 additions & 11 deletions test_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading