Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
934cd76
Test Partial Message in interop tester
MarcoPolo Sep 24, 2025
3512e52
add rust implementation for partial messages
jxs Sep 26, 2025
aba93ed
remove instructions type field
jxs Sep 26, 2025
cc1284f
fix PublishPartial group_id rename
jxs Sep 29, 2025
fdcad15
gossipsub-interop: partial messages, special case 2 nodes
MarcoPolo Sep 29, 2025
85280da
fixup add partial to gossipsub interop subscript tester
MarcoPolo Sep 29, 2025
3a2d0b2
workaround for rust bug
MarcoPolo Sep 29, 2025
6fe76b3
gossipsub-interop(rust-libp2p): fix instruction parsing
MarcoPolo Sep 29, 2025
6c06c19
gossipsub-interop(rust-libp2p): Fix cargo warning
MarcoPolo Sep 29, 2025
ebe9685
gossipsub-interop(rust-libp2p): fix bitmap's available/missing methods
MarcoPolo Sep 29, 2025
8d1b94d
gossipsub-interop(rust-libp2p): implement republishing logic
MarcoPolo Sep 29, 2025
589e474
debug print lines
MarcoPolo Sep 29, 2025
c34821f
update rust version to latest rust-libp2p
jxs Sep 29, 2025
d42548e
update rust-libp2p dep
jxs Sep 30, 2025
74570e0
update for latest rust-libp2p changes
jxs Oct 1, 2025
5054fae
update rust-libp2p sim to the updated scriptparams
jxs Oct 1, 2025
ede3ac0
gossipsub-interop: lint fixes
MarcoPolo Oct 1, 2025
3a0ccef
gossipsub-intero(go): update to latest partial message impl
MarcoPolo Oct 1, 2025
6ad0380
gossipsub-interop: update go-libp2p to the latest version
sukunrt Oct 12, 2025
45c1a61
gossipsub-interop: bump go-libp2p version
MarcoPolo Nov 22, 2025
685d9da
add missing MergeMedata func
MarcoPolo Nov 22, 2025
1ce3e77
properly return err on nested instruction
MarcoPolo Nov 22, 2025
7f78d33
add partial_messages check
MarcoPolo Nov 22, 2025
108c37d
gossipsub-interop: add a new scenario for partial messages
sukunrt Oct 12, 2025
f222e1a
gossipsub-interop: add partial message fanout scenario test
MarcoPolo Nov 22, 2025
543eaa3
log "All parts received" only once per group id
MarcoPolo Nov 22, 2025
f0610e1
gossipsub-interop: Add a basic `make test` command to run all tests
MarcoPolo Nov 22, 2025
883f29a
gossipsub-interop: Change rust log to match checker
MarcoPolo Nov 22, 2025
5cf6a9a
Update rust-libp2p
MarcoPolo Dec 10, 2025
dc07917
update rust-libp2p
jxs Dec 11, 2025
c90e563
fix(rust): handle partial messages correctly in fanout scenario
jxs Dec 11, 2025
97e2e4f
fix count occurences check
MarcoPolo Dec 15, 2025
766f87c
Expand makefile to add rust-go interop tests
MarcoPolo Dec 15, 2025
ad5014f
avoid simultaneous connections
MarcoPolo Dec 16, 2025
b93f07a
only connect to a node once in random mesh
MarcoPolo Jan 7, 2026
05ce079
echo command in make test
MarcoPolo Jan 7, 2026
6622f8f
Add CI to gossipsub-interop tests
MarcoPolo Dec 16, 2025
ad24f43
use jxs' branch of rust-libp2p
MarcoPolo Jan 6, 2026
2aed087
update to latest libp2p changes
jxs Jan 20, 2026
285149f
fix typos on doc
jxs Jan 28, 2026
2d7b242
Update go gossipsub library
MarcoPolo Mar 5, 2026
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
62 changes: 62 additions & 0 deletions .github/workflows/gossipsub-interop-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Gossipsub Interoperability Tests (PR)

on:
pull_request:
paths:
- 'gossipsub-interop/**'
- '.github/workflows/gossipsub-interop-pr.yml'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: gossipsub-interop/go-libp2p/go.mod
cache: true

- name: Set up Rust
uses: dtolnay/rust-toolchain@stable

- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH

- name: Install Shadow simulator
run: |
# Install Shadow dependencies
sudo apt-get update
sudo apt-get install -y \
cmake \
findutils \
libclang-dev \
libc-dbg \
libglib2.0-0 \
libglib2.0-dev \
make \
netbase \
python3 \
python3-networkx \
xz-utils

# Build and install Shadow v3.3.0 from source
git clone --depth 1 --branch v3.3.0 https://github.com/shadow/shadow.git shadow-simulator
cd shadow-simulator
git checkout v3.3.0
./setup build --clean
./setup install
echo "$HOME/.local/bin" >> $GITHUB_PATH

- name: Run gossipsub interop tests
working-directory: gossipsub-interop
run: make test
2 changes: 2 additions & 0 deletions gossipsub-interop/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
*.swp
/shadow.data
shadow-outputs/*
latest
synctest*.data
/shadow.yaml
/graph.gml
Expand Down
43 changes: 41 additions & 2 deletions gossipsub-interop/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,46 @@ binaries:

# Clean all generated shadow simulation files
clean:
rm -rf *.data || true
rm -rf shadow-outputs || true
rm plots/* || true

.PHONY: binaries all clean
test:
# Testing partial messages
@echo "Testing partial messages"
@uv run run.py --node_count 32 --composition "rust-and-go" --scenario "partial-messages" && uv run checks/partial_messages.py latest --count 1

@echo "Testing partial messages chain"
@uv run run.py --node_count 8 --composition "rust-and-go" --scenario "partial-messages-chain" && uv run checks/partial_messages.py latest --count 16

@echo "Testing fanout"
uv run run.py --node_count 8 --composition "rust-and-go" --scenario "partial-messages-fanout" && uv run checks/partial_messages.py latest/
uv run run.py --node_count 8 --seed 1 --composition "rust-and-go" --scenario "partial-messages-fanout" && uv run checks/partial_messages.py latest/
uv run run.py --node_count 8 --seed 2 --composition "rust-and-go" --scenario "partial-messages-fanout" && uv run checks/partial_messages.py latest/
uv run run.py --node_count 8 --seed 3 --composition "rust-and-go" --scenario "partial-messages-fanout" && uv run checks/partial_messages.py latest/

test-go:
# Testing partial messages
@echo "Testing partial messages"
@uv run run.py --node_count 8 --composition "all-go" --scenario "partial-messages" && uv run checks/partial_messages.py latest --count 1

@echo "Testing partial messages chain"
@uv run run.py --node_count 8 --composition "all-go" --scenario "partial-messages-chain" && uv run checks/partial_messages.py latest --count 16

@echo "Testing fanout"
@uv run run.py --node_count 2 --composition "all-go" --scenario "partial-messages-fanout" && uv run checks/partial_messages.py latest/


test-rust-only:
# Testing partial messages
@echo "Testing partial messages"
@uv run run.py --node_count 8 --composition "all-rust" --scenario "partial-messages" && uv run checks/partial_messages.py latest --count 1

@echo "Testing partial messages chain"
@uv run run.py --node_count 8 --composition "all-rust" --scenario "partial-messages-chain" && uv run checks/partial_messages.py latest --count 16

@echo "Testing fanout"
@uv run run.py --node_count 2 --composition "all-rust" --scenario "partial-messages-fanout" && uv run checks/partial_messages.py latest/



.PHONY: binaries all clean test
18 changes: 18 additions & 0 deletions gossipsub-interop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,24 @@ After implementing it, make sure to add build commands in the Makefile's `binari

Finally, add it to the `composition` function in `experiment.py`.

## Examples

Minimal test of partial messages

```bash
uv run run.py --node_count 2 --composition "all-go" --scenario "partial-messages" && uv run checks/partial_messages.py latest/
```

That command runs the shadow simulation and then verifies the stdout logs have the expected message.

## Tests

```bash
make test
```

This runs various shadow simulations and checks.

## Future work (contributions welcome)

- Add more scenarios.
Expand Down
98 changes: 98 additions & 0 deletions gossipsub-interop/checks/partial_messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Verify that each node stdout log contains the expected completion message."""

from __future__ import annotations

import argparse
import sys
from pathlib import Path

MESSAGE_SUBSTRING = '"msg":"All parts received"'


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Validate that every node stdout log inside a Shadow output directory "
"contains the expected completion message."
)
)
parser.add_argument(
"shadow_output",
help="Path to the Shadow output directory (the one containing the hosts/ folder).",
)
parser.add_argument(
"--count",
type=int,
default=1,
help="Minimum number of times each stdout log must contain the target message (default: 1).",
)
return parser.parse_args()


def iter_stdout_logs(hosts_dir: Path):
"""Yield all stdout log files under the given hosts directory."""
for stdout_file in sorted(hosts_dir.rglob("*.stdout")):
if stdout_file.is_file():
yield stdout_file


def count_occurrences(path: Path, needle: str) -> int:
"""Count how many times the string appears inside the file."""
if not needle:
return 0
total = 0
with path.open("r", encoding="utf-8", errors="replace") as handle:
for line in handle:
total += line.count(needle)
return total


def main() -> int:
args = parse_args()
base_dir = Path(args.shadow_output).expanduser().resolve()
if not base_dir.exists():
print(f"shadow output directory does not exist: {base_dir}", file=sys.stderr)
return 1

hosts_dir = base_dir / "hosts"
if not hosts_dir.is_dir():
print(f"hosts directory not found under: {base_dir}", file=sys.stderr)
return 1

stdout_logs = list(iter_stdout_logs(hosts_dir))
if not stdout_logs:
print(f"no stdout logs found under: {hosts_dir}", file=sys.stderr)
return 1

missing = []
for log_path in stdout_logs:
occurrences = count_occurrences(log_path, MESSAGE_SUBSTRING)
if occurrences < args.count:
missing.append((log_path, occurrences))

if missing:
print(
"The following stdout logs do not contain the required message:",
file=sys.stderr,
)
for log_path, occurrences in missing:
rel_path = log_path.relative_to(base_dir)
print(
f" - {rel_path}: found {occurrences} occurrences (expected >= {args.count})",
file=sys.stderr,
)
print(
f"{len(missing)} / {len(stdout_logs)} logs missing the message.",
file=sys.stderr,
)
return 1

print(
f"All {len(stdout_logs)} stdout logs under {hosts_dir} contain the required message."
)
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading