Integrated Onion Routing for Peer-to-Peer Validator Privacy in the Ethereum Network

In this section, we introduce the API for the Onion Routing based protocol that we propose for P2P validator privacy in the Ethereum Network.

Our Protocol

The general idea of an onion routing scheme is similar to Dandelion-style schemes in that the message is relayed from the originator to the broadcaster through a number of hops. The main difference, however, is that the relayers forward the message encrypted, and only the broadcaster sees the message in plain-text. The originator encrypts the message using the public key of each relayer, and each relayer forwards the message by first unencrypting it using its private key. Successfully executed, onion routing has the potential to prevent any single party from linking the IP address and public key of the originator.

Currently, you can only specify the number of relayers that we sample uniformly at random from P2P network nodes to form one encrypted channel for each originator. Later, we plan to add the number of channels per originator as a new parameter.

class ethp2psim.protocols.OnionRoutingProtocol(network, num_relayers=3, broadcast_mode='sqrt', seed=None)[source]

Message propagation is first based on an anonymity phase that is followed by a spreading phase. During the anonymity phase the messages are propagated on pre-selected channels that contain multiple relayer nodes. The message source uses Onion Routing to encrypt the message with the public keys of relayer nodes. When an encrypted message reaches the last relayer in a given chain then it is broadcasted as a cleartext message. This is the start of the spreading phase.

Parameters:
  • network (network.Network) – Represent the underlying P2P network used for message passing

  • num_relayers (int (Default: 2)) – Number of hops (intermediary nodes) on each arm (currently only 1 arm is supported). Intermediary nodes relay the message to the final node on each arm that is the broadcaster node.

  • broadcast_mode (str) – Use value ‘sqrt’ to broadcast the message only to a randomly selected square root of neighbors. Otherwise the message will be sent to every neighbor in the spreading phase.

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

Examples

In this small example, we show how you can access encrypted channel(s) for each originator.

>>> from .network import *
>>> seed = 42
>>> nw_generator = NodeWeightGenerator("random", seed)
>>> ew_generator = EdgeWeightGenerator("normal", seed)
>>> net = Network(nw_generator, ew_generator, num_nodes=5, k=2, seed=seed)
>>> num_edges = net.graph.number_of_edges()
>>> tor = OnionRoutingProtocol(net, num_relayers=3, broadcast_mode='all', seed=seed)
>>> tor.tor_network
{0: [[3, 1, 4]], 1: [[0, 4, 2]], 2: [[1, 4, 3]], 4: [[2, 1, 0]], 3: [[1, 2, 0]]}

References

Domokos M. Kelen, Istvan Andras Seres, Ferenc Beres, Andras A. Benczur, Integrated Onion Routing for Peer-to-Peer Validator Privacy in the Ethereum Network, https://info.ilab.sztaki.hu/~kdomokos/OnionRoutingP2PEthereumPrivacy.pdf

Onion Routing Adversary

In order to properly compare our approach with previous privacy-enhancing solutions (e.g, Dandelion(++)) we implemented an adversary that cannot differentiate between messages during the anonymity phase, when the messages are propagated in an ancrypted form from the originator towards the broadcaster.

You can initialize this adversary in an identical way to the formerly identified adversaries.

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

Abstraction for the entity that tries to deanonymize Ethereum addresses when message passing is executed with our Onion Routing protocol. The main restriction here is that adversary cannot differentiate between messages during the anonymity phase as they are encrypted.

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)

References

Domokos M. Kelen, Istvan Andras Seres, Ferenc Beres, Andras A. Benczur, Integrated Onion Routing for Peer-to-Peer Validator Privacy in the Ethereum Network, https://info.ilab.sztaki.hu/~kdomokos/OnionRoutingP2PEthereumPrivacy.pdf

Basically, the this adversary is constantly recording the timestamps of received and forwarded encrypted packages and tries to backtrack each channel from the broadcaster to the originator with the following functions.

ethp2psim.adversary.OnionRoutingAdversary.eavesdrop_msg(self, ee)

Adversary records the observed information.

Parameters:

ee (EavesdropEvent) – EavesdropEvent that the adversary receives

Return type:

NoReturn

ethp2psim.adversary.OnionRoutingAdversary.record_packet(self, pe)

Record sent encrypted packages

Parameters:

pe (ProtocolEvent) – Event sent by the adversary to the next relayer in the encrypted channel.

Return type:

NoReturn

Finally, you can query the predicted originator for each message in a dataframe:

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

Predict originator for each message in a run of the OnionRoutingProtocol.

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 example, the adversary reconstructs the channel originating from node 9 from the two adversarial nodes (7 and 1).

>>> from ethp2psim.network import *
>>> from ethp2psim.message import Message
>>> from ethp2psim.simulator import Simulator
>>> seed = 42
>>> nw = NodeWeightGenerator("random", seed)
>>> ew = EdgeWeightGenerator("random", seed)
>>> net = Network(nw, ew, 20, 3, seed=seed)
>>> orp = OnionRoutingProtocol(net, broadcast_mode="all", seed=seed)
>>> assert orp.tor_network[9] == [[7, 5, 1]]
>>> adv = OnionRoutingAdversary(orp, adversaries=[7, 1], seed=seed)
>>> msgs = [Message(9)] # simulate a single message originating from node 9
>>> sim = Simulator(adv, messages=msgs, seed=seed)
>>> _ = sim.run()
>>> predictions = adv.predict_msg_source(estimator="first_sent")
>>> # the adversary could reconstruct the channel from node 1 and 7
>>> predictions.loc[msgs[0].mid, 9] # and it predicted node 9 to be the originator
1.0

Next, we evaluate a simulation with 20 random messages.

>>> from ethp2psim.network import *
>>> from ethp2psim.message import Message
>>> from ethp2psim.simulator import *
>>> seed = 42
>>> nw = NodeWeightGenerator("random", seed)
>>> ew = EdgeWeightGenerator("random", seed)
>>> net = Network(nw, ew, 50, 10, seed=seed)
>>> orp = OnionRoutingProtocol(net, broadcast_mode="all", seed=seed)
>>> # adversary controls 10% of the network nodes
>>> adv = OnionRoutingAdversary(orp, 0.1, seed=seed)
>>> sim = Simulator(adv, 20, seed=seed)
>>> _ = sim.run()
>>> Evaluator(sim, estimator="first_sent").get_report()
{'estimator': 'first_sent', 'hit_ratio': 0.0, 'inverse_rank': 0.05774664045611379, 'entropy': 0.0, 'ndcg': 0.22931979405367153, 'message_spread_ratio': 1.0}