Simulating Ethereum transactions

At last, you know every component that is essential to simulate the Ethereum P2P network. This is where the fun begins! Let’s get familiar with our ethp2psim.simulator.Simulator and ethp2psim.simulator.Evaluator that gives you the power to simulate and understand the Ethereum P2P network under various different setups.

Simulator setups

You have two key parameters that you can use to design significantly different experiments:

  1. Playing with the use_node_weights parameter you can decide whether to sample message sources with respect to node weights (defined by ethp2psim.network.NodeWeightGenerator) or just sample them uniformly at random from the nodes of the P2P network. We note that adversarial nodes (stored within the ethp2psim.adversary.Adversary) never get sampled to be message sources!

  2. On the other hand, you can also set message sources manually. This is extremely useful when you want to analyse a specific scenario.

Below, you can find a few examples for these setups.

class ethp2psim.simulator.Simulator(adversary, num_msg=10, use_node_weights=False, messages=None, seed=None, verbose=False)[source]

Abstraction to simulate message passing on a P2P network

Parameters:
  • adversary (adversary.Adversary) – adversary that observe messages in the P2P network

  • num_msg (Optional[int] (Default: 10)) – number of messages to simulate

  • use_node_weights (bool) – sample message sources with respect to node weights

  • messages (Optional[List[Message]]) – Set messages manually

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

Examples

Sample message sources with respect to stake distribution

>>> from .network import *
>>> from .adversary import Adversary
>>> from .protocols import BroadcastProtocol
>>> 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.4)
>>> simulator = Simulator(adversary, 20, use_node_weights=True)
>>> len(simulator.messages)
20

Set 5 messages originating from node 0

>>> from .network import *
>>> from .message import Message
>>> from .adversary import Adversary
>>> from .protocols import BroadcastProtocol
>>> 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.4)
>>> simulator = Simulator(adversary, messages=[Message(0) for _ in range(5)])
>>> len(simulator.messages)
5
>>> simulator.message_sources
[0, 0, 0, 0, 0]

Use the following function to simulate the propagation of sampled messages on the P2P network. We note that in this experiment messages are simulated independently. During this process we record individual messages as they reach given nodes of the P2P network, including adversarial nodes.

ethp2psim.simulator.Simulator.run(self, coverage_threshold=1.0, max_trials=100, disable_progress_bar=True)

Run simulation

Parameters:
  • coverage_threshold (float) – stop propagating a message if it reached the given fraction of network nodes

  • max_trials (int) – stop propagating a message if it does not reach any new nodes within max_trials steps

Return type:

list

Examples

Run simulation until each message reaches 90% of all nodes

>>> from .network import *
>>> from .adversary import Adversary
>>> from .protocols import BroadcastProtocol
>>> seed = 42
>>> nw_gen = NodeWeightGenerator('stake')
>>> ew_gen = EdgeWeightGenerator('normal')
>>> net = Network(nw_gen, ew_gen, 10, 3, seed=seed)
>>> protocol = BroadcastProtocol(net, broadcast_mode='all', seed=seed)
>>> adversary = Adversary(protocol, 0.4, seed=seed)
>>> simulator = Simulator(adversary, 5, use_node_weights=True, seed=seed)
>>> len(simulator.messages)
5
>>> simulator.run(coverage_threshold=0.9)
[0.9, 0.9, 0.9, 0.9, 0.9]

after running the simulator, you can query the mean and standard deviation of spreading time (measured in milliseconds) for all messages when they reached given fraction of the nodes.

ethp2psim.simulator.Simulator.node_contact_time_quantiles(self, q=array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]))

Calculate the mean and the standard deviation for first node contact time quantiles

Parameters:

q (list (Default: numpy.arange(0.1, 1.0, 0.1)))) – Node quantiles

Return type:

Iterable[array]

Examples

Observe the mean and standard deviation of propagation times until the messages reach 50% and 95% of all nodes.

>>> from .network import *
>>> from .adversary import Adversary
>>> from .protocols import BroadcastProtocol
>>> seed = 42
>>> nw_gen = NodeWeightGenerator('stake')
>>> ew_gen = EdgeWeightGenerator('normal')
>>> net = Network(nw_gen, ew_gen, 10, 3, seed=seed)
>>> protocol = BroadcastProtocol(net, broadcast_mode='all', seed=seed)
>>> adversary = Adversary(protocol, 0.4, seed=seed)
>>> simulator = Simulator(adversary, 5, use_node_weights=True, seed=seed)
>>> _ = simulator.run()
>>> mean_t, std_t = simulator.node_contact_time_quantiles(q=[0.5,0.95])
>>> # messages on average reach 50% of all nodes within 273 milliseconds.
>>> mean_t
array([273.25546384, 460.11754328])
>>> std_t
array([24.51004285, 11.76789887])

How to evaluate a simulation?

We designed the ethp2psim.simulator.Evaluator class to gain meaningful insights related to the deanonymization power of the ethp2psim.adversary.Adversary and the average message node coverage (fraction of nodes reached by a given ethp2psim.message.Message).

Using the ethp2psim.simulator.Evaluator is very straightforward:

  1. First, you run your simulator

  2. Then, feed it to the ethp2psim.simulator.Evaluator and specify the estimator that you want to use for deanonymization.

  3. Finally, query the report from the evaluator that will be most useful when you want to compare simulations with different network topology, protocol or other setups.

class ethp2psim.simulator.Evaluator(simulator, estimator='first_reach')[source]

Measures the deanonymization performance of the adversary for a given simulation

Parameters:
  • simulator (Simulator) – Specify the simulation to evaluate

  • estimator ({'first_reach', 'first_sent', 'dummy'}, default 'first_reach') – Define adversary stategy to predict source node for each message: * 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.

Examples

Observe the complete evaluation pipeline below. First, initialize network, protocol, adversary. Then, simulate 20 messages with these components. Finally, query the report using the first sent estimator for the aversary.

>>> from .network import *
>>> from .adversary import Adversary
>>> from .protocols import BroadcastProtocol
>>> seed = 42
>>> nw_gen = NodeWeightGenerator('stake')
>>> ew_gen = EdgeWeightGenerator('normal')
>>> net = Network(nw_gen, ew_gen, 10, 3, seed=seed)
>>> protocol = BroadcastProtocol(net, broadcast_mode='all', seed=seed)
>>> adversary = Adversary(protocol, 0.4, seed=seed)
>>> simulator = Simulator(adversary, 20, use_node_weights=True, seed=seed)
>>> _ = simulator.run()
>>> evaluator = Evaluator(simulator, estimator='first_sent')
>>> evaluator.get_report()
{'estimator': 'first_sent', 'hit_ratio': 0.25, 'inverse_rank': 0.32499999999999996, 'entropy': 0.0, 'ndcg': 0.46679861973841597, 'message_spread_ratio': 1.0}

What should be my next step?

Now, you’re good to go to run your own experiments. Here, we propose two possible directions that you might want to consider:

  1. If you can’t wait to play with ethp2psim then we recommend you to explore all related resources that we prepared for you to ease your work with complex simulations. Find related details in the Experimental results with various protocols section.

  2. On the other hand, if your are more interested in the theory of privacy-enhancing solutions for Ethereum then move to the next section.