mirror of https://github.com/zcash/simtfl.git
Compare commits
2 Commits
c181d51638
...
ff8a6ed8e6
Author | SHA1 | Date |
---|---|---|
Daira Emma Hopwood | ff8a6ed8e6 | |
Daira Emma Hopwood | b3aa30572a |
|
@ -35,20 +35,41 @@ class PermissionedBFTBase:
|
|||
self.t = t
|
||||
self.parent = None
|
||||
|
||||
def last_final(self):
|
||||
"""
|
||||
Returns the last final block in this block's ancestor chain.
|
||||
For the genesis block, this is itself.
|
||||
"""
|
||||
return self
|
||||
|
||||
|
||||
class PermissionedBFTBlock(PermissionedBFTBase):
|
||||
"""
|
||||
A block for a BFT protocol. Each non-genesis block is based on a
|
||||
notarized proposal.
|
||||
notarized proposal, and in practice consists of the proposer's signature
|
||||
over the notarized proposal.
|
||||
|
||||
Honest proposers must only ever sign at most one valid proposal for the
|
||||
given epoch in which they are a proposer.
|
||||
|
||||
BFT blocks are taken to be notarized, and therefore valid, by definition.
|
||||
"""
|
||||
|
||||
def __init__(self, proposal):
|
||||
"""Constructs a `PermissionedBFTBlock` for the given proposal."""
|
||||
super().__init__(proposal.n, proposal.t)
|
||||
|
||||
proposal.assert_notarized()
|
||||
self.proposal = proposal
|
||||
self.parent = proposal.parent
|
||||
|
||||
def last_final(self):
|
||||
"""
|
||||
Returns the last final block in this block's ancestor chain.
|
||||
This should be overridden by subclasses; the default implementation
|
||||
will (inefficiently) just return the genesis block.
|
||||
"""
|
||||
return self.parent.last_final()
|
||||
|
||||
|
||||
class PermissionedBFTProposal(PermissionedBFTBase):
|
||||
|
@ -112,7 +133,9 @@ import unittest
|
|||
class TestPermissionedBFT(unittest.TestCase):
|
||||
def test_basic(self):
|
||||
# Construct the genesis block.
|
||||
current = PermissionedBFTBase(5, 2)
|
||||
genesis = PermissionedBFTBase(5, 2)
|
||||
current = genesis
|
||||
self.assertEqual(current.last_final(), genesis)
|
||||
|
||||
for i in range(2):
|
||||
proposal = PermissionedBFTProposal(current)
|
||||
|
@ -134,6 +157,7 @@ class TestPermissionedBFT(unittest.TestCase):
|
|||
self.assertTrue(proposal.is_notarized())
|
||||
|
||||
current = PermissionedBFTBlock(proposal)
|
||||
self.assertEqual(current.last_final(), genesis)
|
||||
|
||||
def test_assertions(self):
|
||||
genesis = PermissionedBFTBase(5, 2)
|
||||
|
|
|
@ -10,12 +10,32 @@ from .. import PermissionedBFTBase, PermissionedBFTBlock, PermissionedBFTProposa
|
|||
two_thirds_threshold
|
||||
|
||||
|
||||
class StreamletProposal(PermissionedBFTProposal):
|
||||
"""An adapted-Streamlet proposal."""
|
||||
|
||||
def __init__(self, parent, epoch):
|
||||
"""
|
||||
Constructs a `StreamletProposal` with the given parent `StreamletBlock`,
|
||||
for the given `epoch`. The parameters are determined by the parent block.
|
||||
"""
|
||||
assert isinstance(parent, StreamletBlock) or isinstance(parent, StreamletGenesis)
|
||||
super().__init__(parent)
|
||||
self.epoch = epoch
|
||||
|
||||
def __repr__(self):
|
||||
return "StreamletProposal(parent=%r, epoch=%r)" % (self.parent, self.epoch)
|
||||
|
||||
|
||||
class StreamletGenesis(PermissionedBFTBase):
|
||||
"""An adapted-Streamlet genesis block."""
|
||||
|
||||
def __init__(self, n):
|
||||
"""Constructs a genesis block for adapted-Streamlet with `n` nodes."""
|
||||
super().__init__(n, two_thirds_threshold(n))
|
||||
self.epoch = None
|
||||
|
||||
def __repr__(self):
|
||||
return "StreamletGenesis(n=%r)" % (self.n,)
|
||||
|
||||
|
||||
class StreamletBlock(PermissionedBFTBlock):
|
||||
|
@ -25,16 +45,126 @@ class StreamletBlock(PermissionedBFTBlock):
|
|||
|
||||
`StreamletBlock`s are taken to be notarized, and therefore valid, by definition.
|
||||
"""
|
||||
pass
|
||||
|
||||
def __init__(self, proposal):
|
||||
"""Constructs a `StreamletBlock` for the given proposal."""
|
||||
assert isinstance(proposal, StreamletProposal)
|
||||
super().__init__(proposal)
|
||||
self.epoch = proposal.epoch
|
||||
|
||||
class StreamletProposal(PermissionedBFTProposal):
|
||||
"""An adapted-Streamlet proposal."""
|
||||
|
||||
def __init__(self, parent, epoch):
|
||||
def last_final(self):
|
||||
"""
|
||||
Constructs a `StreamletProposal` with the given parent `StreamletBlock`,
|
||||
for the given `epoch`. The parameters are determined by the parent block.
|
||||
Returns the last final block in this block's ancestor chain.
|
||||
In Streamlet this is the middle block of the last group of three
|
||||
that were proposed in consecutive epochs.
|
||||
"""
|
||||
super.__init__(parent)
|
||||
self.epoch = epoch
|
||||
last = self
|
||||
if last.parent is None:
|
||||
return last
|
||||
middle = last.parent
|
||||
if middle.parent is None:
|
||||
return middle
|
||||
first = middle.parent
|
||||
while True:
|
||||
if first.parent is None:
|
||||
return first
|
||||
if (first.epoch + 1, middle.epoch + 1) == (middle.epoch, last.epoch):
|
||||
return middle
|
||||
(first, middle, last) = (first.parent, first, middle)
|
||||
|
||||
def __repr__(self):
|
||||
return "StreamletBlock(proposal=%r)" % (self.proposal,)
|
||||
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class TestStreamlet(unittest.TestCase):
|
||||
def test_simple(self):
|
||||
"""
|
||||
Very simple example.
|
||||
|
||||
0 --- 1 --- 2 --- 3
|
||||
"""
|
||||
self._test_last_final([0, 1, 2], [0, 0, 2])
|
||||
|
||||
def test_figure_1(self):
|
||||
"""
|
||||
Figure 1: Streamlet finalization example (without the invalid 'X' proposal).
|
||||
|
||||
0 --- 2 --- 5 --- 6 --- 7
|
||||
\
|
||||
-- 1 --- 3
|
||||
|
||||
0 - Genesis
|
||||
N - Notarized block
|
||||
|
||||
This diagram implies the epoch 6 block is the last-final block in the
|
||||
context of the epoch 7 block, because it is in the middle of 3 blocks
|
||||
with consecutive epoch numbers, and 6 is the most recent such block.
|
||||
|
||||
(We don't include the block/proposal with the red X because that's not
|
||||
what we're testing.)
|
||||
"""
|
||||
self._test_last_final([0, 0, 1, None, 2, 5, 6], [0, 0, 0, 0, 0, 0, 6])
|
||||
|
||||
def test_complex(self):
|
||||
"""
|
||||
Safety Violation: due to three simultaneous properties:
|
||||
|
||||
- 6 is `last_final` in the context of 7
|
||||
- 9 is `last_final` in the context of 10
|
||||
- 9 is not a descendent of 6
|
||||
|
||||
0 --- 2 --- 5 --- 6 --- 7
|
||||
\
|
||||
-- 1 --- 3 --- 8 --- 9 --- 10
|
||||
"""
|
||||
self._test_last_final([0, 0, 1, None, 2, 5, 6, 3, 8, 9], [0, 0, 0, 0, 0, 0, 6, 0, 0, 9])
|
||||
|
||||
def _test_last_final(self, parent_map, final_map):
|
||||
"""
|
||||
This test constructs a tree of proposals with structure determined by
|
||||
`parent_map`, and asserts `block.last_final()` matches the structure
|
||||
determined by `final_map`.
|
||||
|
||||
parent_map: maps { iteration_number -> parent_index }
|
||||
final_map: maps { iteration_number -> last_final_index }
|
||||
"""
|
||||
|
||||
assert len(parent_map) == len(final_map)
|
||||
|
||||
# Construct the genesis block.
|
||||
genesis = StreamletGenesis(3)
|
||||
current = genesis
|
||||
self.assertEqual(current.last_final(), genesis)
|
||||
blocks = [genesis]
|
||||
|
||||
for epoch in range(1, len(parent_map) + 1):
|
||||
parent_epoch = parent_map[epoch - 1]
|
||||
if parent_epoch is None:
|
||||
blocks.append(None)
|
||||
continue
|
||||
|
||||
proposal = StreamletProposal(blocks[parent_epoch], epoch)
|
||||
proposal.assert_valid()
|
||||
self.assertTrue(proposal.is_valid())
|
||||
self.assertFalse(proposal.is_notarized())
|
||||
|
||||
# not enough signatures
|
||||
proposal.add_signature(0)
|
||||
self.assertFalse(proposal.is_notarized())
|
||||
|
||||
# same index, so we still only have one signature
|
||||
proposal.add_signature(0)
|
||||
self.assertFalse(proposal.is_notarized())
|
||||
|
||||
# different index, now we have two signatures as required
|
||||
proposal.add_signature(1)
|
||||
proposal.assert_notarized()
|
||||
self.assertTrue(proposal.is_notarized())
|
||||
|
||||
current = StreamletBlock(proposal)
|
||||
blocks.append(current)
|
||||
final_epoch = final_map[epoch - 1]
|
||||
self.assertEqual(current.last_final(), blocks[final_epoch])
|
||||
|
|
Loading…
Reference in New Issue