-
Notifications
You must be signed in to change notification settings - Fork 96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allowed adversary to control message delivery #141
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -143,15 +143,20 @@ impl<D: DistAlgorithm> MessageWithSender<D> { | |
/// An adversary that can control a set of nodes and pick the next good node to receive a message. | ||
/// | ||
/// See `TestNetwork::step()` for a more detailed description of its capabilities. | ||
pub trait Adversary<D: DistAlgorithm> { | ||
pub trait Adversary<D: DistAlgorithm> | ||
where | ||
<D as DistAlgorithm>::Message: Clone, | ||
{ | ||
/// Chooses a node to be the next one to handle a message. | ||
/// | ||
/// Starvation is illegal, i.e. in every iteration a node that has pending incoming messages | ||
/// must be chosen. | ||
fn pick_node(&self, nodes: &BTreeMap<D::NodeUid, TestNode<D>>) -> D::NodeUid; | ||
|
||
/// Called when a node controlled by the adversary receives a message. | ||
fn push_message(&mut self, sender_id: D::NodeUid, msg: TargetedMessage<D::Message, D::NodeUid>); | ||
/// Called when a correct node emits a message. The adversary is allowed to perform an arbitrary | ||
/// transformation of the message. It returns the result of the transformation in a map from | ||
/// node IDs to targeted messages. | ||
fn push_message(&mut self, msg: MessageWithSender<D>) -> Vec<MessageWithSender<D>>; | ||
|
||
/// Produces a list of messages to be sent from the adversary's nodes. | ||
fn step(&mut self) -> Vec<MessageWithSender<D>>; | ||
|
@@ -179,13 +184,17 @@ impl SilentAdversary { | |
} | ||
} | ||
|
||
impl<D: DistAlgorithm> Adversary<D> for SilentAdversary { | ||
impl<D: DistAlgorithm> Adversary<D> for SilentAdversary | ||
where | ||
<D as DistAlgorithm>::Message: Clone, | ||
{ | ||
fn pick_node(&self, nodes: &BTreeMap<D::NodeUid, TestNode<D>>) -> D::NodeUid { | ||
self.scheduler.pick_node(nodes) | ||
} | ||
|
||
fn push_message(&mut self, _: D::NodeUid, _: TargetedMessage<D::Message, D::NodeUid>) { | ||
// All messages are ignored. | ||
fn push_message(&mut self, msg: MessageWithSender<D>) -> Vec<MessageWithSender<D>> { | ||
// Output the message without changes. | ||
vec![msg] | ||
} | ||
|
||
fn step(&mut self) -> Vec<MessageWithSender<D>> { | ||
|
@@ -257,6 +266,8 @@ impl<D: DistAlgorithm, F> RandomAdversary<D, F> { | |
|
||
impl<D: DistAlgorithm, F: Fn() -> TargetedMessage<D::Message, D::NodeUid>> Adversary<D> | ||
for RandomAdversary<D, F> | ||
where | ||
<D as DistAlgorithm>::Message: Clone, | ||
{ | ||
fn init( | ||
&mut self, | ||
|
@@ -272,18 +283,18 @@ impl<D: DistAlgorithm, F: Fn() -> TargetedMessage<D::Message, D::NodeUid>> Adver | |
self.scheduler.pick_node(nodes) | ||
} | ||
|
||
fn push_message(&mut self, _: D::NodeUid, msg: TargetedMessage<D::Message, D::NodeUid>) { | ||
fn push_message(&mut self, msg: MessageWithSender<D>) -> Vec<MessageWithSender<D>> { | ||
// If we have not discovered the network topology yet, abort. | ||
if self.known_node_ids.is_empty() { | ||
return; | ||
return vec![msg]; | ||
} | ||
|
||
// only replay a message in some cases | ||
if !randomly(self.p_replay) { | ||
return; | ||
return vec![msg]; | ||
} | ||
|
||
let TargetedMessage { message, target } = msg; | ||
let TargetedMessage { message, target } = msg.tm; | ||
|
||
match target { | ||
Target::All => { | ||
|
@@ -292,23 +303,34 @@ impl<D: DistAlgorithm, F: Fn() -> TargetedMessage<D::Message, D::NodeUid>> Adver | |
// network topology. To re-send a broadcast message from one of the attacker | ||
// controlled nodes, we would have to get a list of attacker controlled nodes | ||
// here and use a random one as the origin/sender, this is not done here. | ||
return; | ||
vec![MessageWithSender { | ||
sender: msg.sender, | ||
tm: Target::All.message(message), | ||
}] | ||
} | ||
Target::Node(our_node_id) => { | ||
Target::Node(target_id) => { | ||
// Choose a new target to send the message to. The unwrap never fails, because we | ||
// ensured that `known_node_ids` is non-empty earlier. | ||
let mut rng = rand::thread_rng(); | ||
let new_target_node = rng.choose(&self.known_node_ids).unwrap().clone(); | ||
let new_target_id = rng.choose(&self.known_node_ids).unwrap().clone(); | ||
|
||
// TODO: We could randomly broadcast it instead, if we had access to topology | ||
// information. | ||
self.outgoing.push(MessageWithSender::new( | ||
our_node_id, | ||
target_id.clone(), | ||
TargetedMessage { | ||
target: Target::Node(new_target_node), | ||
message, | ||
target: Target::Node(new_target_id), | ||
message: message.clone(), | ||
}, | ||
)); | ||
// Return the original message unchanged. | ||
vec![MessageWithSender::new( | ||
msg.sender, | ||
TargetedMessage { | ||
target: Target::Node(target_id), | ||
message, | ||
}, | ||
)] | ||
} | ||
} | ||
} | ||
|
@@ -351,7 +373,10 @@ impl<D: DistAlgorithm, F: Fn() -> TargetedMessage<D::Message, D::NodeUid>> Adver | |
/// 2. Send arbitrary messages to any node originating from one of the nodes they control. | ||
/// | ||
/// See the `step` function for details on actual operation of the network. | ||
pub struct TestNetwork<A: Adversary<D>, D: DistAlgorithm> { | ||
pub struct TestNetwork<A: Adversary<D>, D: DistAlgorithm> | ||
where | ||
<D as DistAlgorithm>::Message: Clone, | ||
{ | ||
pub nodes: BTreeMap<D::NodeUid, TestNode<D>>, | ||
pub observer: TestNode<D>, | ||
pub adv_nodes: BTreeMap<D::NodeUid, Arc<NetworkInfo<D::NodeUid>>>, | ||
|
@@ -439,28 +464,39 @@ where | |
Q: IntoIterator<Item = TargetedMessage<D::Message, NodeUid>> + Debug, | ||
{ | ||
for msg in msgs { | ||
match msg.target { | ||
Target::All => { | ||
for node in self.nodes.values_mut() { | ||
if node.id != sender_id { | ||
node.queue.push_back((sender_id, msg.message.clone())) | ||
// Transform the queued message by means of the adversary. | ||
let adv_msgs = self.adversary.push_message(MessageWithSender { | ||
sender: sender_id, | ||
tm: msg.clone(), | ||
}); | ||
// Dispatch the result of the transformation. | ||
for adv_msg in adv_msgs { | ||
match adv_msg.tm.target { | ||
Target::All => { | ||
for node in self.nodes.values_mut() { | ||
if node.id != adv_msg.sender { | ||
node.queue | ||
.push_back((adv_msg.sender, adv_msg.tm.message.clone())); | ||
} | ||
} | ||
self.observer | ||
.queue | ||
.push_back((adv_msg.sender, adv_msg.tm.message)); | ||
} | ||
self.observer | ||
.queue | ||
.push_back((sender_id, msg.message.clone())); | ||
self.adversary.push_message(sender_id, msg); | ||
} | ||
Target::Node(to_id) => { | ||
if self.adv_nodes.contains_key(&to_id) { | ||
self.adversary.push_message(sender_id, msg); | ||
} else if let Some(node) = self.nodes.get_mut(&to_id) { | ||
node.queue.push_back((sender_id, msg.message)); | ||
} else { | ||
warn!( | ||
"Unknown recipient {:?} for message: {:?}", | ||
to_id, msg.message | ||
); | ||
Target::Node(to_id) => { | ||
if self.adv_nodes.contains_key(&to_id) { | ||
self.adversary.push_message(MessageWithSender { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or should that even be a different method? This one is for messages sent to the adversary, while the other invocation is kind of eavesdropping on any communication. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or we could rename |
||
sender: sender_id, | ||
tm: msg.clone(), | ||
}); | ||
} else if let Some(node) = self.nodes.get_mut(&to_id) { | ||
node.queue.push_back((sender_id, msg.message.clone())); | ||
} else { | ||
warn!( | ||
"Unknown recipient {:?} for message: {:?}", | ||
to_id, msg.message | ||
); | ||
} | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should at least make sure that the original message, if it was from a correct to a correct node, is unchanged. E.g. we could just give a reference to
push_message
, and dispatch its outputs in addition to the original one.Also, we should probably assert that all the
adv_msgs
have as sender one of the adversary's nodes: The adversary is not able to forge, modify or indefinitely block messages from a different sender.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For #37,
push_message
can withhold the message within the adversary and return an empty vector. In that case nothing should be dispatched. In the current tests this is still not implemented. That's why allpush_message
s return the same message. But it is not the case in general.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What you propose for
adv_msgs
would not allow implementing #37.adv_msgs
is the result of the adversarial message transformation such as reordering of messages from correct nodes.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But maybe reordering should be handled differently: by allowing adversaries to flip entries in the nodes' incoming queues? That, in combination with picking the next node to handle a message, should allow the adversary to do everything they are allowed to do.
(Alternatively, I think @mbr suggested to have just one queue for all the nodes, where entries have both sender and recipient IDs. Reordering that queue would replace both of the above: It allows the adversary to choose message order and the next node.)
At least I think that functionality should be separate from the method that informs the adversary about a message arriving in one of their own nodes. (Which is all that
push_message
currently does, I think. Not a great name, admittedly. 😬 )There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also think one queue for all nodes would be great. This PR is in that direction. The adversary owns that queue. Do we really need removing incoming queues of nodes? If so, can we remove the incoming queues in a separate PR? This would require modifying the current scheduling mechanism which is at the moment based on picking nodes and delivering all queued incoming messages to it. With the single queue, we could remove those messages from it which have the scheduled node as the recipient.
The alternative of the adversary directly modifying nodes (that is, their incoming queues) sounds more complicated. It would need to keep the order of all messages across all individual queues somehow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly; it would replace
pick_node
with something likeshuffle_queue
, and we could keeppush_message
in its current form. (Or later replace it with something else, too — I think one idea was to actually instantiate correct nodes for the adversary, and just allow the adversary to modify their incoming and outgoing messages?)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we keep
push_message
in its current form, where do you think the messages should be pushed onto the single queue?shuffle_queue
is a great idea. I don't quite see where the queue itself comes from in your proposal though.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking that the
TestNetwork
itself would own the queue, and only allow the adversary to move messages inside it. (Not sure how theAdversary
interface would have to look for that… maybe a method that gets a non-mutable reference to the queue and outputs a list of positions, i.e. a permutation?) That would make it impossible to write an adversary that cheats by removing or modifying entries.But I guess it's also okay to do something simpler, and just make sure all our adversaries behave.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm maybe OK with the permutation interface. However, if we need another kind of attack in the future, we would probably rewrite the queue to have mutable access rather than add another interface to it.
@mbr, what do you think, does the single queue have to be owned by
Adversary
, in which case we have to ensure adversaries don't perform unexpected transformations on the queue, or should the queue be owned byTestNetwork
with specialized interfaces for particular kinds of adversarial transformations?