From 3de89bf329e75ade7402b180f374cb53153d9222 Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Mon, 25 Jun 2018 14:50:30 +0100 Subject: [PATCH] Extract local compute phase of BA into a function Also handles case where BA value is not equal to coin fixes #8 Except for the blocking operation of getting the coin, the code that is extracted into a function corresponds to phase 3 described in [MMR14]; lines 7-11 in fig. 2 (A BV-broadcast-based algorithm implementing binary consensus ...). --- honeybadgerbft/core/binaryagreement.py | 57 ++++++++++++--------- honeybadgerbft/exceptions.py | 4 ++ test/test_binaryagreement.py | 68 ++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 22 deletions(-) diff --git a/honeybadgerbft/core/binaryagreement.py b/honeybadgerbft/core/binaryagreement.py index 445b3c4..eb4e1ea 100644 --- a/honeybadgerbft/core/binaryagreement.py +++ b/honeybadgerbft/core/binaryagreement.py @@ -2,7 +2,7 @@ from gevent.event import Event from collections import defaultdict -from honeybadgerbft.exceptions import RedundantMessageError +from honeybadgerbft.exceptions import RedundantMessageError, AbandonedNodeError def binaryagreement(sid, pid, N, f, coin, input, decide, broadcast, receive): @@ -121,25 +121,38 @@ def _recv(): # Block until receiving the common coin value s = coin(r) - if len(values) == 1: - v = next(iter(values)) - if v == s: - if already_decided is None: - already_decided = v - decide(v) - # print('[sid:%s] [pid:%d] DECIDED %d in round %d' % (sid,pid,v,r)) - elif already_decided == v: - # Here corresponds to a proof that if one party - # decides at round r, then in all the following - # rounds, everybody will propose r as an - # estimation. (Lemma 2, Lemma 1) An abandoned - # party is a party who has decided but no enough - # peers to help him end the loop. Lemma: # of - # abandoned party <= t - # print('[sid:%s] [pid:%d] QUITTING in round %d' % (sid,pid,r))) - _thread_recv.kill() - return - est = v - else: - est = s + try: + est, already_decided = set_new_estimate( + values=values, + s=s, + already_decided=already_decided, + decide=decide, + ) + except AbandonedNodeError: + # print('[sid:%s] [pid:%d] QUITTING in round %d' % (sid,pid,r))) + _thread_recv.kill() + return + r += 1 + + +def set_new_estimate(*, values, s, already_decided, decide): + if len(values) == 1: + v = next(iter(values)) + if v == s: + if already_decided is None: + already_decided = v + decide(v) + elif already_decided == v: + # Here corresponds to a proof that if one party + # decides at round r, then in all the following + # rounds, everybody will propose r as an + # estimation. (Lemma 2, Lemma 1) An abandoned + # party is a party who has decided but no enough + # peers to help him end the loop. Lemma: # of + # abandoned party <= t + raise AbandonedNodeError + est = v + else: + est = s + return est, already_decided diff --git a/honeybadgerbft/exceptions.py b/honeybadgerbft/exceptions.py index 42f3c29..a80f28a 100644 --- a/honeybadgerbft/exceptions.py +++ b/honeybadgerbft/exceptions.py @@ -12,3 +12,7 @@ class UnknownTagError(BroadcastError): class RedundantMessageError(BroadcastError): """Raised when a rdundant message is received.""" + + +class AbandonedNodeError(HoneybadgerbftError): + """Raised when a node does not have enough peer to carry on a distirbuted task.""" diff --git a/test/test_binaryagreement.py b/test/test_binaryagreement.py index 6b7b837..539cf1b 100644 --- a/test/test_binaryagreement.py +++ b/test/test_binaryagreement.py @@ -272,3 +272,71 @@ def test_binaryagreement(): reason='Place holder for https://github.com/amiller/HoneyBadgerBFT/issues/59') def test_issue59_attack(): raise NotImplementedError("Placeholder test failure for Issue #59") + + +@mark.parametrize('values,s,already_decided,expected_est,' + 'expected_already_decided,expected_output', ( + ({0}, 0, None, 0, 0, 0), + ({1}, 1, None, 1, 1, 1), +)) +def test_set_next_round_estimate_with_decision(values, s, already_decided, + expected_est, expected_already_decided, expected_output): + from honeybadgerbft.core.binaryagreement import set_new_estimate + decide = Queue() + updated_est, updated_already_decided = set_new_estimate( + values=values, + s=s, + already_decided=already_decided, + decide=decide.put, + ) + assert updated_est == expected_est + assert updated_already_decided == expected_already_decided + assert decide.get() == expected_output + + +@mark.parametrize('values,s,already_decided,' + 'expected_est,expected_already_decided', ( + ({0}, 0, 1, 0, 1), + ({0}, 1, None, 0, None), + ({0}, 1, 0, 0, 0), + ({0}, 1, 1, 0, 1), + ({1}, 0, None, 1, None), + ({1}, 0, 0, 1, 0), + ({1}, 0, 1, 1, 1), + ({1}, 1, 0, 1, 0), + ({0, 1}, 0, None, 0, None), + ({0, 1}, 0, 0, 0, 0), + ({0, 1}, 0, 1, 0, 1), + ({0, 1}, 1, None, 1, None), + ({0, 1}, 1, 0, 1, 0), + ({0, 1}, 1, 1, 1, 1), +)) +def test_set_next_round_estimate(values, s, already_decided, + expected_est, expected_already_decided): + from honeybadgerbft.core.binaryagreement import set_new_estimate + decide = Queue() + updated_est, updated_already_decided = set_new_estimate( + values=values, + s=s, + already_decided=already_decided, + decide=decide.put, + ) + assert updated_est == expected_est + assert updated_already_decided == expected_already_decided + assert decide.empty() + + +@mark.parametrize('values,s,already_decided', ( + ({0}, 0, 0), + ({1}, 1, 1), +)) +def test_set_next_round_estimate_raises(values, s, already_decided): + from honeybadgerbft.core.binaryagreement import set_new_estimate + from honeybadgerbft.exceptions import AbandonedNodeError + with raises(AbandonedNodeError): + updated_est, updated_already_decided = set_new_estimate( + values=values, + s=s, + already_decided=already_decided, + decide=None, + )