/swarm-host

Programmable tool for protocol simulation and traffic flow analysis in peer-to-peer networks

Primary LanguageRustGNU General Public License v2.0GPL-2.0

swarm-host

swarm-host is a protocol simulation and traffic flow analysis tool for peer-to-peer (P2P) programs. It specifies a trait the application must implement which defines how its network protocols work and exposes a Python framework for programming said protocols. It also exposes a simple heuristics backend which provides visualization of the network traffic.

swarm-host allows running protocol simulations against live nodes of the P2P network and provides the ability to analyze the network behavior and traffic flow patterns during the simulation. It can be used to evaluate how, e.g., functional changes to protocol implementations or choices made in peer connections affect the security and bandwidth usage of the network.

swarm-host works by spawning Sybil nodes (called interfaces) in the network that masquerade as normal P2P nodes which honest nodes can connect to. The traffic flow in these interfaces is programmable in Python, allowing a wide range of simulations to be performed. The protocols can be programmed by installing a filter which is called each time a network event, such as an incoming peer or a block request, is received and the filter decides what it wants to do with that event. It can, e.g., decide to drop the received message, delay or modify it, or only forward it to some of its connected peers. The filters can also be tweaked during runtime and reinstalled if such a need exists.

In addition to being generic over the network protocol, swarm-host is also generic over the filter executor so if Python is not suitable for whatever reason, another executor can be implemented provided it can do the necessary conversions for the types the backend defines and that it can be called from Rust.

Figure 1 depicts a high-level architecture of swarm-host and an example testing scenario with 5:2 majority for honest nodes

Following Python code shows how the testing scenario described above can be implemented for Substrate with a custom filter for the /block-announces/1 protocol:

from swarm_host.swarm_host import SwarmHost
from swarm_host.filter import NotificationFilter
from swarm_host.backend.substrate.node import SubstrateNode

import time

swarm_host = SwarmHost(
    rpc_port = 8888,
    backend = "substrate",
    genesis_hash = "0x36d171b4279dc05f16e8960afd9ae9e28cd620b610b84d6374949c099231c585"
)

preinit = open("swarm_host/backend/substrate/filters/preinit.py").read()
context = open("swarm_host/backend/substrate/filters/context.py").read()

id1 = swarm_host.create_interface(6666, context, preinit)
id2 = swarm_host.create_interface(7777, context, preinit)

swarm_host.link_interface(id1, id2)

# create custom filter for the `/block-announces/1` protocol
# which stops forwarding block announcements after block 10
class CustomBlockAnnounceFilter(NotificationFilter):
    def inject_notification(ctx, peer, protocol, notification):
        from swarm_host.backend.substrate.primitives.block_announce import BlockAnnounce
        from swarm_host.backend.substrate.primitives.block_announce import init_runtime_config

        if ctx.runtime_config is None:
            ctx.runtime_config = init_runtime_config()

        try:
            block_announce = BlockAnnounce(ctx.runtime_config, bytes(notification))
        except Exception as e:
            print("failed to decode block announce:", e)
            return

        number = block_announce.number()
        hash = block_announce.hash(ctx.runtime_config)
        ctx.peers[peer].known_blocks.add(hash)

        # stop forwarding block announcements after block 10
        if number > 10:
            return

        forward_table = []
        for peer in ctx.peers:
            if hash not in ctx.peers[peer].known_blocks:
                forward_table.append(peer)

        if len(forward_table) != 0:
            ctx.forward_notification(protocol, forward_table, notification)

swarm_host.install_notification_filter(
    id1,
    "/sup/block-announces/1",
    CustomBlockAnnounceFilter().export()
)
swarm_host.install_notification_filter(
    id2,
    "/sup/block-announces/1",
    CustomBlockAnnounceFilter().export()
)

# start Substrate nodes
nodes = []
nodes.append(SubstrateNode([
    "--in-peers", "3",
    "--out-peers", "2",
    "--port", "0",
    "--chain=dev",
    "--alice",
    "--force-authoring",
    "--tmp",
]))

for i in range(0, 4):
    nodes.append(SubstrateNode([
        "--in-peers", "3",
        "--out-peers", "2",
        "--port", "0",
        "--chain=dev",
        "--tmp",
    ])) 

time.sleep(100)

Alternatively you can start swarm-host as a binary:

export PYTHONPATH=<path to swarm-host/framework>
cargo run -- --rpc-port 8888 substrate --genesis-hash 0x36d171b4279dc05f16e8960afd9ae9e28cd620b610b84d6374949c099231c585

and write the Python testing code without staring the SwarmHost object. This way you need to send commands to the running swarm-host binary over RPC. Calling conventions are documented in src/rpc.rs and framework/swarm_host/swarm_host.py.