mirror of https://github.com/poanetwork/hbbft.git
300 lines
11 KiB
Rust
300 lines
11 KiB
Rust
#![deny(unused_must_use)]
|
|
//! Network tests for Honey Badger.
|
|
|
|
pub mod net;
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use itertools::Itertools;
|
|
use log::info;
|
|
use proptest::{prelude::ProptestConfig, proptest, proptest_helper};
|
|
use rand::{seq::SliceRandom, Rng, SeedableRng};
|
|
|
|
use hbbft::honey_badger::{Batch, EncryptionSchedule, HoneyBadger, MessageContent};
|
|
use hbbft::sender_queue::{self, SenderQueue, Step};
|
|
use hbbft::transaction_queue::TransactionQueue;
|
|
use hbbft::{threshold_decrypt, util, CpStep, NetworkInfo, Target};
|
|
|
|
use crate::net::adversary::{
|
|
sort_by_random_node, Adversary, NetMutHandle, NodeOrderAdversary, RandomAdversary,
|
|
ReorderingAdversary,
|
|
};
|
|
use crate::net::proptest::{gen_seed, TestRng, TestRngSeed};
|
|
use crate::net::{CrankError, NetBuilder, NetMessage, NewNodeInfo, Node, VirtualNet};
|
|
|
|
type NodeId = u16;
|
|
type NetworkInfoMap = BTreeMap<NodeId, Arc<NetworkInfo<NodeId>>>;
|
|
type UsizeHoneyBadger = SenderQueue<HoneyBadger<Vec<usize>, NodeId>>;
|
|
type HoneyBadgerMessage = NetMessage<UsizeHoneyBadger>;
|
|
|
|
/// An adversary whose nodes only send messages with incorrect decryption shares.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct FaultyShareAdversary {
|
|
share_triggers: BTreeMap<u64, bool>,
|
|
// TODO this is really hacky but there's no better way to get this value
|
|
// Solution taken from binary_agreement_mitm test - ideally the new network simulator
|
|
// should be altered to store the netinfo structure alongside nodes similar to
|
|
// the way the old network simulator did it.
|
|
netinfo_mutex: Arc<Mutex<NetworkInfoMap>>,
|
|
}
|
|
|
|
impl FaultyShareAdversary {
|
|
/// Creates a new adversary with the necessary network info instances
|
|
pub fn new(netinfo_mutex: Arc<Mutex<NetworkInfoMap>>) -> Self {
|
|
FaultyShareAdversary {
|
|
share_triggers: BTreeMap::new(),
|
|
netinfo_mutex,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Adversary<UsizeHoneyBadger> for FaultyShareAdversary {
|
|
#[inline]
|
|
fn pre_crank<R: Rng>(
|
|
&mut self,
|
|
mut net: NetMutHandle<'_, UsizeHoneyBadger, Self>,
|
|
rng: &mut R,
|
|
) {
|
|
sort_by_random_node(&mut net, rng);
|
|
}
|
|
|
|
#[inline]
|
|
fn tamper<R: Rng>(
|
|
&mut self,
|
|
mut net: NetMutHandle<'_, UsizeHoneyBadger, Self>,
|
|
msg: HoneyBadgerMessage,
|
|
rng: &mut R,
|
|
) -> Result<CpStep<UsizeHoneyBadger>, CrankError<UsizeHoneyBadger>> {
|
|
if let sender_queue::Message::Algo(hb_msg) = msg.payload() {
|
|
let epoch = hb_msg.epoch();
|
|
// Set the trigger to simulate decryption share messages
|
|
// if epoch has not been encountered yet.
|
|
self.share_triggers.entry(epoch).or_insert(true);
|
|
}
|
|
|
|
let mut step = net.dispatch_message(msg, rng)?;
|
|
|
|
let fake_proposal = &Vec::from("X marks the spot");
|
|
// For each untriggered epoch, send fake shares
|
|
for (epoch, trigger_set) in &mut self.share_triggers {
|
|
if *trigger_set {
|
|
// Unset the trigger.
|
|
*trigger_set = false;
|
|
|
|
// Get node id vectors up-front to avoid borrow issues
|
|
let faulty_node_ids: Vec<NodeId> =
|
|
net.faulty_nodes_mut().map(|node| *node.id()).collect();
|
|
let all_node_ids: Vec<NodeId> = net.nodes_mut().map(|node| node.id()).collect();
|
|
|
|
// Broadcast fake decryption shares from all adversarial nodes.
|
|
for faulty_node_id in faulty_node_ids {
|
|
// get the adversarial's net info
|
|
let netinfo = self
|
|
.netinfo_mutex
|
|
.lock()
|
|
.unwrap()
|
|
.get(&faulty_node_id)
|
|
.cloned()
|
|
.expect("Adversary netinfo mutex not populated");
|
|
|
|
// encrypt false share
|
|
let fake_ciphertext = (*netinfo)
|
|
.public_key_set()
|
|
.public_key()
|
|
.encrypt(fake_proposal);
|
|
let share = netinfo
|
|
.secret_key_share()
|
|
.expect("missing adversary key share")
|
|
.decrypt_share(&fake_ciphertext)
|
|
.expect("decryption share");
|
|
|
|
// Send the share to remote nodes.
|
|
for proposer_id in &all_node_ids {
|
|
step.messages.push(
|
|
Target::All.message(sender_queue::Message::Algo(
|
|
MessageContent::DecryptionShare {
|
|
proposer_id: *proposer_id,
|
|
share: threshold_decrypt::Message(share.clone()),
|
|
}
|
|
.with_epoch(*epoch),
|
|
)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(step)
|
|
}
|
|
}
|
|
|
|
/// Proposes `num_txs` values and expects nodes to output and order them.
|
|
fn test_honey_badger<A>(
|
|
mut net: VirtualNet<UsizeHoneyBadger, A>,
|
|
num_txs: usize,
|
|
mut rng: &mut TestRng,
|
|
) where
|
|
A: Adversary<UsizeHoneyBadger>,
|
|
{
|
|
let mut queues: BTreeMap<_, _> = net
|
|
.correct_nodes()
|
|
.map(|node| (*node.id(), (0..num_txs).collect::<Vec<usize>>()))
|
|
.collect();
|
|
|
|
// Returns `true` if the node has not output all transactions yet.
|
|
// If it has, and has advanced another epoch, it clears all messages for later epochs.
|
|
let node_busy = |node: &Node<UsizeHoneyBadger>| {
|
|
node.outputs().iter().flat_map(Batch::iter).unique().count() < num_txs
|
|
};
|
|
|
|
// Handle messages in random order until all nodes have output all transactions.
|
|
while net.correct_nodes().any(node_busy) {
|
|
// If a node is expecting input, take it from the queue. Otherwise handle a message.
|
|
let input_ids: Vec<_> = net
|
|
.correct_nodes()
|
|
.filter(|node| !node.algorithm().algo().has_input())
|
|
.map(|node| *node.id())
|
|
.collect();
|
|
|
|
if let Some(id) = input_ids[..].choose(&mut rng) {
|
|
let queue = queues.get_mut(id).unwrap();
|
|
queue.remove_multiple(net.get(*id).unwrap().outputs().iter().flat_map(Batch::iter));
|
|
let _ = net.send_input(*id, queue.choose(&mut rng, 3, 10), &mut rng);
|
|
} else {
|
|
let _ = net.crank_expect(&mut rng);
|
|
}
|
|
}
|
|
verify_output_sequence(&net);
|
|
}
|
|
|
|
/// Verifies that all instances output the same sequence of batches.
|
|
fn verify_output_sequence<A>(network: &VirtualNet<UsizeHoneyBadger, A>)
|
|
where
|
|
A: Adversary<UsizeHoneyBadger>,
|
|
{
|
|
let mut expected: Option<BTreeMap<u64, &_>> = None;
|
|
for node in network.correct_nodes() {
|
|
assert!(!node.outputs().is_empty());
|
|
let outputs: BTreeMap<u64, &BTreeMap<NodeId, Vec<usize>>> = node
|
|
.outputs()
|
|
.iter()
|
|
.map(|batch| (batch.epoch, &batch.contributions))
|
|
.collect();
|
|
if expected.is_none() {
|
|
expected = Some(outputs);
|
|
} else if let Some(expected) = &expected {
|
|
assert_eq!(expected, &outputs);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn new_honey_badger(
|
|
netinfo: Arc<NetworkInfo<NodeId>>,
|
|
) -> (UsizeHoneyBadger, Step<HoneyBadger<Vec<usize>, NodeId>>) {
|
|
let our_id = *netinfo.our_id();
|
|
let nc = netinfo.clone();
|
|
let peer_ids = nc.all_ids().filter(|&&them| them != our_id).cloned();
|
|
let hb = HoneyBadger::builder(netinfo)
|
|
.encryption_schedule(EncryptionSchedule::EveryNthEpoch(2))
|
|
.build();
|
|
SenderQueue::builder(hb, peer_ids).build(our_id)
|
|
}
|
|
|
|
fn test_honey_badger_different_sizes<A, F>(
|
|
new_adversary: F,
|
|
num_txs: usize,
|
|
seed: TestRngSeed,
|
|
adversary_netinfo: &Arc<Mutex<NetworkInfoMap>>,
|
|
) where
|
|
A: Adversary<UsizeHoneyBadger>,
|
|
F: Fn() -> A,
|
|
{
|
|
// This returns an error in all but the first test.
|
|
let _ = env_logger::try_init();
|
|
|
|
let mut rng: TestRng = TestRng::from_seed(seed);
|
|
let sizes = vec![1, 2, 3, 5, rng.gen_range(6, 10)];
|
|
for size in sizes {
|
|
// cloning since it gets moved into a closure
|
|
let cloned_netinfo_map = adversary_netinfo.clone();
|
|
|
|
let num_adv_nodes = util::max_faulty(size);
|
|
let num_good_nodes = size - num_adv_nodes;
|
|
info!(
|
|
"Network size: {} good nodes, {} faulty nodes",
|
|
num_good_nodes, num_adv_nodes
|
|
);
|
|
|
|
let (net, _) = NetBuilder::new(0..size as u16)
|
|
.num_faulty(num_adv_nodes as usize)
|
|
.message_limit(10_000 * size as usize)
|
|
.no_time_limit()
|
|
.adversary(new_adversary())
|
|
.using_step(move |info: NewNodeInfo<_>| {
|
|
let netinfo = Arc::new(info.netinfo);
|
|
cloned_netinfo_map
|
|
.lock()
|
|
.unwrap()
|
|
.insert(info.id, netinfo.clone());
|
|
new_honey_badger(netinfo)
|
|
})
|
|
.build(&mut rng)
|
|
.expect("Could not construct test network.");
|
|
|
|
test_honey_badger(net, num_txs, &mut rng);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig {
|
|
cases: 1, .. ProptestConfig::default()
|
|
})]
|
|
|
|
#[test]
|
|
#[allow(clippy::unnecessary_operation)]
|
|
fn test_honey_badger_random_delivery_silent(seed in gen_seed()) {
|
|
do_test_honey_badger_random_delivery_silent(seed)
|
|
}
|
|
|
|
#[test]
|
|
#[allow(clippy::unnecessary_operation)]
|
|
fn test_honey_badger_first_delivery_silent(seed in gen_seed()) {
|
|
do_test_honey_badger_first_delivery_silent(seed)
|
|
}
|
|
|
|
#[test]
|
|
#[allow(clippy::unnecessary_operation)]
|
|
fn test_honey_badger_faulty_share(seed in gen_seed()) {
|
|
do_test_honey_badger_faulty_share(seed)
|
|
}
|
|
|
|
#[test]
|
|
#[allow(clippy::unnecessary_operation)]
|
|
fn test_honey_badger_random_adversary(seed in gen_seed()) {
|
|
do_test_honey_badger_random_adversary(seed)
|
|
}
|
|
}
|
|
|
|
fn do_test_honey_badger_random_delivery_silent(seed: TestRngSeed) {
|
|
test_honey_badger_different_sizes(ReorderingAdversary::new, 30, seed, &Default::default());
|
|
}
|
|
|
|
fn do_test_honey_badger_first_delivery_silent(seed: TestRngSeed) {
|
|
test_honey_badger_different_sizes(NodeOrderAdversary::new, 30, seed, &Default::default());
|
|
}
|
|
|
|
fn do_test_honey_badger_faulty_share(seed: TestRngSeed) {
|
|
let adversary_netinfo: Arc<Mutex<NetworkInfoMap>> = Default::default();
|
|
let new_adversary = || FaultyShareAdversary::new(adversary_netinfo.clone());
|
|
test_honey_badger_different_sizes(new_adversary, 8, seed, &adversary_netinfo);
|
|
}
|
|
|
|
fn do_test_honey_badger_random_adversary(seed: TestRngSeed) {
|
|
let new_adversary = || {
|
|
// A 10% injection chance is roughly ~13k extra messages added.
|
|
RandomAdversary::new(0.1, 0.1)
|
|
};
|
|
test_honey_badger_different_sizes(new_adversary, 8, seed, &Default::default());
|
|
}
|