Adversarial settings

In our experiments, the main goal of the adversary is to predict the source node for each message sent on the P2P network. The adversary is constantly eavesdropping on network traffic through a fixed set of nodes controlled by this entity to obtain these predictions. In our implementation, you can use the adversary.Adversary class to set up an adversary with various capabilities:

  1. You can set the fraction of nodes that the adversary controls.

  2. You can explicitly set the most central nodes (e.g., nodes with the highest degree, PageRank or Betweenness centrality) to be adversaries. Intuitively, central nodes are reached by messages faster than nodes on the periphery.

  3. You can set an adversary to be active that won’t propagate or broadcast messages further. This way, you can test the resilience of different protocols against malicious nodes that refuse to comply with the pre-defined rules of message spreading.

  4. You can choose from various protocol-specific adversaries (e.g., ethp2psim.adversary.DandelionAdversary ) that are more efficient than the baseline methods.

Node selection

See the examples below on how to select adversarial nodes uniformly at random or by network centrality.

class ethp2psim.adversary.Adversary(protocol, ratio=0.1, active=False, adversaries=None, seed=None)[source]

Abstraction for the entity that tries to deanonymize Ethereum addresses by observing p2p network traffic

Parameters:
  • protocol (protocol.Protocol) – protocol that determines the rules of message passing

  • ratio (float) – Fraction of adversary nodes in the P2P network

  • active (bool) – Turn on to enable adversary nodes to deny message propagation

  • adversaries (List[int]) – Optional list of nodes that can be set to be adversaries instead of randomly selecting them.

  • seed (int (optional)) – Random seed (disabled by default)

Examples

The simplest ways to select nodes controlled by the adversary is to randomly sample a given fraction (e.g, 20%) from all nodes.

>>> from .network import *
>>> from .protocols import BroadcastProtocol
>>> from .adversary import Adversary
>>> nw_gen = NodeWeightGenerator('stake')
>>> ew_gen = EdgeWeightGenerator('normal')
>>> net = Network(nw_gen, ew_gen, 10, 3)
>>> protocol = BroadcastProtocol(net, broadcast_mode='all')
>>> adversary = Adversary(protocol, 0.2)
>>> len(adversary.nodes)
2

Another possible approach is to manually set adversarial nodes. For example, you can choose to set nodes with the highest degrees.

>>> from .network import *
>>> from .protocols import BroadcastProtocol
>>> from .adversary import Adversary
>>> seed = 42
>>> G = nx.barabasi_albert_graph(20, 3, seed=seed)
>>> nw_gen = NodeWeightGenerator('stake')
>>> ew_gen = EdgeWeightGenerator('normal')
>>> net = Network(nw_gen, ew_gen, graph=G, seed=seed)
>>> adv_nodes = net.get_central_nodes(4, 'degree')
>>> protocol = BroadcastProtocol(net, broadcast_mode='all', seed=seed)
>>> adversary = Adversary(protocol, adversaries=adv_nodes, seed=seed)
>>> adversary.nodes
[5, 0, 4, 6]

Predict message sources

Next, let’s observe how to query the adversary for possible message sources.

ethp2psim.adversary.Adversary.predict_msg_source(self, estimator='first_reach')

Predict source nodes for each message

By default, the node from whom the adversary first heard the message is assigned 1.0 probability while every other node receives zero.

Parameters:

estimator ({'first_reach', 'first_sent', 'dummy'}, default 'first_reach') – Strategy to assign probabilities to network nodes: * first_reach: the node from whom the adversary first heard the message is assigned 1.0 probability while every other node receives zero. * first_sent: the node that sent the message the earliest to the receiver * dummy: the probability is divided equally between non-adversary nodes.

Return type:

DataFrame

Examples

In this small triangle graph, the triangle inequality does hold for the manually set channel latencies. That is why the adversary node 3 can correctly predicting node 1 to be the message source by using the first sent estimator heuristic.

>>> from .network import *
>>> from .message import Message
>>> from .protocols import BroadcastProtocol
>>> from .adversary import Adversary
>>> G = nx.DiGraph()
>>> G.add_nodes_from([1, 2, 3])
>>> G.add_weighted_edges_from([(1, 2, 0.9), (1, 3, 1.84), (2, 3, 0.85)], weight="latency")
>>> net = Network(NodeWeightGenerator("random"), EdgeWeightGenerator("custom"), graph=G)
>>> protocol = BroadcastProtocol(net, "all", seed=44)
>>> adv = Adversary(protocol, adversaries=[3])
>>> msg = Message(1)
>>> for _ in range(3):
...    _ = msg.process(adv)
>>> msg.flush_queue(adv)
>>> # first reach estimator thinks the message source is node 2 that is not true
>>> dict(adv.predict_msg_source(estimator='first_reach').iloc[0])
{1: 0.0, 2: 1.0, 3: 0.0}
>>> # first sent estimator is correct by saying that node 1 is the message source
>>> dict(adv.predict_msg_source(estimator='first_sent').iloc[0])
{1: 1.0, 2: 0.0, 3: 0.0}

Finally, we introduce the ethp2psim.adversary.EavesdropEvent class that is used within the ethp2psim.adversary.Adversary to store information observed by the adversary.

class ethp2psim.adversary.EavesdropEvent(mid, source, pe)[source]

Information related to the observed message

Parameters:
  • node (str) – Node identifier that observed the message

  • source (int) – Source node of the message

  • protocol_event (protocols.ProtocolEvent) – Contains message spreading related information

Examples

In this small triangle graph, the triangle inequality does hold for the manually set channel latencies. That is why the message originating from node 1 reaches node 3 (the adversary) faster through node 2.

>>> from .network import *
>>> from .message import Message
>>> from .protocols import BroadcastProtocol
>>> from .adversary import Adversary
>>> G = nx.DiGraph()
>>> G.add_nodes_from([1, 2, 3])
>>> G.add_weighted_edges_from([(1, 2, 0.9), (1, 3, 1.84), (2, 3, 0.85)], weight="latency")
>>> net = Network(NodeWeightGenerator("random"), EdgeWeightGenerator("custom"), graph=G)
>>> protocol = BroadcastProtocol(net, "all", seed=44)
>>> adv = Adversary(protocol, adversaries=[3])
>>> msg = Message(1)
>>> for _ in range(3):
...    _ = msg.process(adv)
>>> msg.flush_queue(adv)
>>> # message reached node 3 from both nodes 1 and 2
>>> len(adv.captured_events)
2
>>> # message reached node 3 faster through node 2 (1.75 ms)
>>> adv.captured_events[0].protocol_event
ProtocolEvent(2, 3, 1.750000, 2, True, None)
>>> # message reached node 3 slower from node 1 (1.84 ms)
>>> adv.captured_events[1].protocol_event
ProtocolEvent(1, 3, 1.840000, 1, True, None)

So far, we have mostly shown how to interact with only one ethp2psim.message.Message. But you can only gain meaningful insights by simulating various messages at once. In the next section, we show how to do this with ease.