Skip to content
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

Reproduce attack and implement fix for liveness issue with ABA/commoncoin #12

Merged
merged 7 commits into from
Aug 24, 2018
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 110 additions & 2 deletions honeybadgerbft/core/binaryagreement.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import gevent
from gevent.event import Event

from collections import defaultdict
from distutils.util import strtobool
from os import environ
import logging

from honeybadgerbft.exceptions import RedundantMessageError, AbandonedNodeError


logger = logging.getLogger(__name__)
CONF_PHASE = strtobool(environ.get('CONF_PHASE', '1'))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are environment variables documented anywhere?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to disable the conf phase except for testing?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove the switch. It was put there temporarily to ease the demonstration of the attack.

By putting the CONF phase logic in a function we could also simply mock that function in order to demonstrate the attack.



def binaryagreement(sid, pid, N, f, coin, input, decide, broadcast, receive):
"""Binary consensus from [MMR14]. It takes an input ``vi`` and will
finally write the decided value into ``decide`` channel.
Expand All @@ -23,7 +31,9 @@ def binaryagreement(sid, pid, N, f, coin, input, decide, broadcast, receive):
# Messages received are routed to either a shared coin, the broadcast, or AUX
est_values = defaultdict(lambda: [set(), set()])
aux_values = defaultdict(lambda: [set(), set()])
conf_values = defaultdict(lambda: {(0,): set(), (1,): set(), (0, 1): set()})
est_sent = defaultdict(lambda: [False, False])
conf_sent = defaultdict(lambda: {(0,): False, (1,): False, (0, 1): False})
bin_values = defaultdict(set)

# This event is triggered whenever bin_values or aux_values changes
Expand All @@ -32,6 +42,8 @@ def binaryagreement(sid, pid, N, f, coin, input, decide, broadcast, receive):
def _recv():
while True: # not finished[pid]:
(sender, msg) = receive()
logger.debug(f'receive {msg} from node {sender}',
extra={'nodeid': pid, 'epoch': msg[1]})
assert sender in range(N)
if msg[0] == 'EST':
# BV_Broadcast message
Expand All @@ -41,7 +53,11 @@ def _recv():
# FIXME: raise or continue? For now will raise just
# because it appeared first, but maybe the protocol simply
# needs to continue.
print('Redundant EST received', msg)
print(f'Redundant EST received by {sender}', msg)
logger.warn(
f'Redundant EST message received by {sender}: {msg}',
extra={'nodeid': pid, 'epoch': msg[1]}
)
raise RedundantMessageError(
'Redundant EST received {}'.format(msg))
# continue
Expand All @@ -51,10 +67,18 @@ def _recv():
if len(est_values[r][v]) >= f + 1 and not est_sent[r][v]:
est_sent[r][v] = True
broadcast(('EST', r, v))
logger.debug(f"broadcast {('EST', r, v)}",
extra={'nodeid': pid, 'epoch': r})

# Output after reaching second threshold
if len(est_values[r][v]) >= 2 * f + 1:
logger.debug(
f'add v = {v} to bin_value[{r}] = {bin_values[r]}',
extra={'nodeid': pid, 'epoch': r},
)
bin_values[r].add(v)
logger.debug(f'bin_values[{r}] is now: {bin_values[r]}',
extra={'nodeid': pid, 'epoch': r})
bv_signal.set()

elif msg[0] == 'AUX':
Expand All @@ -68,9 +92,38 @@ def _recv():
print('Redundant AUX received', msg)
raise RedundantMessageError(
'Redundant AUX received {}'.format(msg))
# continue

logger.debug(
f'add sender = {sender} to aux_value[{r}][{v}] = {aux_values[r][v]}',
extra={'nodeid': pid, 'epoch': r},
)
aux_values[r][v].add(sender)
logger.debug(
f'aux_value[{r}][{v}] is now: {aux_values[r][v]}',
extra={'nodeid': pid, 'epoch': r},
)

bv_signal.set()

elif msg[0] == 'CONF' and CONF_PHASE:
_, r, v = msg
assert v in ((0,), (1,), (0, 1))
if sender in conf_values[r][v]:
logger.warn(f'Redundant CONF received {msg} by {sender}',
extra={'nodeid': pid, 'epoch': r})
# FIXME: Raise for now to simplify things & be consistent
# with how other TAGs are handled. Will replace the raise
# with a continue statement as part of
# https://github.com/initc3/HoneyBadgerBFT-Python/issues/10
raise RedundantMessageError(
'Redundant CONF received {}'.format(msg))

conf_values[r][v].add(sender)
logger.debug(
f'add v = {v} to conf_value[{r}] = {conf_values[r]}',
extra={'nodeid': pid, 'epoch': r},
)

bv_signal.set()

# Translate mmr14 broadcast into coin.broadcast
Expand All @@ -88,6 +141,9 @@ def _recv():
r = 0
already_decided = None
while True: # Unbounded number of rounds
logger.info(f'Starting with est = {est}',
extra={'nodeid': pid, 'epoch': r})

if not est_sent[r][est]:
est_sent[r][est] = True
broadcast(('EST', r, est))
Expand All @@ -98,10 +154,19 @@ def _recv():
bv_signal.wait()

w = next(iter(bin_values[r])) # take an element
logger.debug(f"broadcast {('AUX', r, w)}",
extra={'nodeid': pid, 'epoch': r})
broadcast(('AUX', r, w))

values = None
logger.debug(
f'block until at least N-f ({N-f}) AUX values are received',
extra={'nodeid': pid, 'epoch': r})
while True:
logger.debug(f'bin_values[{r}]: {bin_values[r]}',
extra={'nodeid': pid, 'epoch': r})
logger.debug(f'aux_values[{r}]: {aux_values[r]}',
extra={'nodeid': pid, 'epoch': r})
# Block until at least N-f AUX values are received
if 1 in bin_values[r] and len(aux_values[r][1]) >= N - f:
values = set((1,))
Expand All @@ -118,8 +183,49 @@ def _recv():
bv_signal.clear()
bv_signal.wait()

logger.debug(f'Completed AUX phase with values = {values}',
extra={'nodeid': pid, 'epoch': r})

# XXX CONF phase
logger.debug(
f'block until at least N-f ({N-f}) CONF values are received',
extra={'nodeid': pid, 'epoch': r})
if CONF_PHASE and not conf_sent[r][tuple(values)]:
conf_sent[r][tuple(values)] = True
logger.debug(f"broadcast {('CONF', r, tuple(values))}",
extra={'nodeid': pid, 'epoch': r})
broadcast(('CONF', r, tuple(bin_values[r])))
while True:
logger.debug(
f'looping ... conf_values[r] is: {conf_values[r]}',
extra={'nodeid': pid, 'epoch': r},
)
if 1 in bin_values[r] and len(conf_values[r][(1,)]) >= N - f:
values = set((1,))
break
if 0 in bin_values[r] and len(conf_values[r][(0,)]) >= N - f:
values = set((0,))
break
if (sum(len(senders) for conf_value, senders in
conf_values[r].items() if senders and
set(conf_value).issubset(bin_values[r])) >= N - f):
values = set((0, 1))
break

bv_signal.clear()
bv_signal.wait()

logger.debug(f'Completed CONF phase with values = {values}',
extra={'nodeid': pid, 'epoch': r})

logger.debug(
f'Block until receiving the common coin value',
extra={'nodeid': pid, 'epoch': r},
)
# Block until receiving the common coin value
s = coin(r)
logger.info(f'Received coin with value = {s}',
extra={'nodeid': pid, 'epoch': r})

try:
est, already_decided = set_new_estimate(
Expand All @@ -130,6 +236,8 @@ def _recv():
)
except AbandonedNodeError:
# print('[sid:%s] [pid:%d] QUITTING in round %d' % (sid,pid,r)))
logger.debug(f'QUIT!',
extra={'nodeid': pid, 'epoch': r})
_thread_recv.kill()
return

Expand Down
16 changes: 16 additions & 0 deletions honeybadgerbft/core/commoncoin.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import logging

from honeybadgerbft.crypto.threshsig.boldyreva import serialize
from collections import defaultdict
from gevent import Greenlet
from gevent.queue import Queue
import hashlib

logger = logging.getLogger(__name__)


class CommonCoinFailureException(Exception):
"""Raised for common coin failures."""
Expand Down Expand Up @@ -34,8 +38,12 @@ def shared_coin(sid, pid, N, f, PK, SK, broadcast, receive):

def _recv():
while True: # main receive loop
logger.debug(f'entering loop',
extra={'nodeid': pid, 'epoch': '?'})
# New shares for some round r, from sender i
(i, (_, r, sig)) = receive()
logger.debug(f'received i, _, r, sig: {i, _, r, sig}',
extra={'nodeid': pid, 'epoch': r})
assert i in range(N)
assert r >= 0
if i in received[r]:
Expand All @@ -56,6 +64,10 @@ def _recv():

# After reaching the threshold, compute the output and
# make it available locally
logger.debug(
f'if len(received[r]) == f + 1: {len(received[r]) == f + 1}',
extra={'nodeid': pid, 'epoch': r},
)
if len(received[r]) == f + 1:

# Verify and get the combined signature
Expand All @@ -65,6 +77,8 @@ def _recv():

# Compute the bit from the least bit of the hash
bit = hash(serialize(sig))[0] % 2
logger.debug(f'put bit {bit} in output queue',
extra={'nodeid': pid, 'epoch': r})
outputQueue[r].put_nowait(bit)

# greenletPacker(Greenlet(_recv), 'shared_coin', (pid, N, f, broadcast, receive)).start()
Expand All @@ -79,6 +93,8 @@ def getCoin(round):
"""
# I have to do mapping to 1..l
h = PK.hash_message(str((sid, round)))
logger.debug(f"broadcast {('COIN', round, SK.sign(h))}",
extra={'nodeid': pid, 'epoch': round})
broadcast(('COIN', round, SK.sign(h)))
return outputQueue[round].get()

Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
tests_require = [
'coverage',
'flake8',
'logutils',
'pytest',
'pytest-cov',
'pytest-mock',
Expand Down
Loading