Skip to content

Commit

Permalink
Finish the implementation of Streamlet and add tests.
Browse files Browse the repository at this point in the history
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
  • Loading branch information
daira committed Dec 13, 2023
1 parent 58106ae commit 14877fd
Show file tree
Hide file tree
Showing 8 changed files with 386 additions and 61 deletions.
8 changes: 4 additions & 4 deletions simtfl/bc/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


from __future__ import annotations
from typing import Iterable, Optional
from typing import Iterable, Optional, TypeAlias
from collections.abc import Sequence
from dataclasses import dataclass
from enum import Enum, auto
Expand Down Expand Up @@ -245,13 +245,13 @@ def is_noncontextually_valid(self) -> bool:
class BCProtocol:
"""A best-chain protocol."""

Transaction: type[object] = BCTransaction
Transaction: TypeAlias = BCTransaction
"""The type of transactions for this protocol."""

Context: type[object] = BCContext
Context: TypeAlias = BCContext
"""The type of contexts for this protocol."""

Block: type[object] = BCBlock
Block: TypeAlias = BCBlock
"""The type of blocks for this protocol."""


Expand Down
107 changes: 75 additions & 32 deletions simtfl/bft/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,33 @@ def __init__(self, n: int, t: int):
Constructs a genesis block for a permissioned BFT protocol with
`n` nodes, of which at least `t` must sign each proposal.
"""

self.n = n
"""The number of voters."""

self.t = t
"""The threshold of votes required for notarization."""

self.parent = None
"""The genesis block has no parent (represented as `None`)."""

def last_final(self) -> PermissionedBFTBase:
"""
Returns the last final block in this block's ancestor chain.
For the genesis block, this is itself.
"""
return self
self.length = 1
"""The genesis chain length is 1."""

self.last_final = self
"""The last final block for the genesis block is itself."""

def preceq(self, other: PermissionedBFTBase):
"""Return True if this block is an ancestor of `other`."""
if self.length > other.length:
return False # optimization
return self == other or (other.parent is not None and self.preceq(other.parent))

def __eq__(self, other) -> bool:
return other.parent is None and (self.n, self.t) == (other.n, other.t)

def __hash__(self) -> int:
return hash((self.n, self.t))


class PermissionedBFTBlock(PermissionedBFTBase):
Expand All @@ -56,15 +73,24 @@ def __init__(self, proposal: PermissionedBFTProposal):

proposal.assert_notarized()
self.proposal = proposal
"""The proposal for this block."""

assert proposal.parent is not None
self.parent = proposal.parent
"""The parent of this block."""

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 if self.parent is None else self.parent.last_final()
self.length = proposal.length
"""The chain length of this block."""

self.last_final = self.parent.last_final
"""The last final block for this block."""

def __eq__(self, other) -> bool:
return (isinstance(other, PermissionedBFTBlock) and
(self.n, self.t, self.proposal) == (other.n, other.t, other.proposal))

def __hash__(self) -> int:
return hash((self.n, self.t, self.proposal))


class PermissionedBFTProposal(PermissionedBFTBase):
Expand All @@ -77,8 +103,22 @@ def __init__(self, parent: PermissionedBFTBase):
block.
"""
super().__init__(parent.n, parent.t)

self.parent = parent
self.signers = set()
"""The parent block of this proposal."""

self.length = parent.length + 1
"""The chain length of this proposal is one greater than its parent block."""

self.votes = set()
"""The set of voter indices that have voted for this proposal."""

def __eq__(self, other):
"""Two proposals are equal iff they are the same object."""
return self is other

def __hash__(self) -> int:
return id(self)

def assert_valid(self) -> None:
"""
Expand All @@ -102,7 +142,7 @@ def assert_notarized(self) -> None:
signatures.
"""
self.assert_valid()
assert len(self.signers) >= self.t
assert len(self.votes) >= self.t

def is_notarized(self) -> bool:
"""Is this proposal notarized?"""
Expand All @@ -112,14 +152,13 @@ def is_notarized(self) -> bool:
except AssertionError:
return False

def add_signature(self, index: int) -> None:
def add_vote(self, index: int) -> None:
"""
Record that the node with the given `index` has signed this proposal.
If the same node signs more than once, the subsequent signatures are
ignored.
Record that the node with the given `index` has voted for this proposal.
Calls that add the same vote more than once are ignored.
"""
self.signers.add(index)
assert len(self.signers) <= self.n
self.votes.add(index)
assert len(self.votes) <= self.n


__all__ = ['two_thirds_threshold', 'PermissionedBFTBase', 'PermissionedBFTBlock', 'PermissionedBFTProposal']
Expand All @@ -132,35 +171,39 @@ def test_basic(self) -> None:
# Construct the genesis block.
genesis = PermissionedBFTBase(5, 2)
current = genesis
self.assertEqual(current.last_final(), genesis)
self.assertEqual(current.last_final, genesis)

for _ in range(2):
proposal = PermissionedBFTProposal(current)
parent = current
proposal = PermissionedBFTProposal(parent)
proposal.assert_valid()
self.assertTrue(proposal.is_valid())
self.assertFalse(proposal.is_notarized())

# not enough signatures
proposal.add_signature(0)
# not enough votes
proposal.add_vote(0)
self.assertFalse(proposal.is_notarized())

# same index, so we still only have one signature
proposal.add_signature(0)
# same index, so we still only have one vote
proposal.add_vote(0)
self.assertFalse(proposal.is_notarized())

# different index, now we have two signatures as required
proposal.add_signature(1)
# different index, now we have two votes as required
proposal.add_vote(1)
proposal.assert_notarized()
self.assertTrue(proposal.is_notarized())

current = PermissionedBFTBlock(proposal)
self.assertEqual(current.last_final(), genesis)
self.assertTrue(parent.preceq(current))
self.assertFalse(current.preceq(parent))
self.assertNotEqual(current, parent)
self.assertEqual(current.last_final, genesis)

def test_assertions(self) -> None:
genesis = PermissionedBFTBase(5, 2)
proposal = PermissionedBFTProposal(genesis)
self.assertRaises(AssertionError, PermissionedBFTBlock, proposal)
proposal.add_signature(0)
proposal.add_vote(0)
self.assertRaises(AssertionError, PermissionedBFTBlock, proposal)
proposal.add_signature(1)
proposal.add_vote(1)
_ = PermissionedBFTBlock(proposal)
4 changes: 4 additions & 0 deletions simtfl/bft/streamlet/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"""
An implementation of adapted-Streamlet ([CS2020] as modified in [Crosslink]).
[CS2020] https://eprint.iacr.org/2020/088.pdf
[Crosslink] https://hackmd.io/JqENg--qSmyqRt_RqY7Whw?view
"""
6 changes: 3 additions & 3 deletions simtfl/bft/streamlet/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def __init__(self, parent: StreamletBlock | StreamletGenesis, epoch: int):
"""The epoch of this proposal."""

def __str__(self) -> str:
return "StreamletProposal(parent=%s, epoch=%s)" % (self.parent, self.epoch)
return f"StreamletProposal(parent={self.parent}, epoch={self.epoch}, length={self.length})"


class StreamletGenesis(PermissionedBFTBase):
Expand All @@ -49,7 +49,7 @@ def __init__(self, n: int):
"""The last final block of the genesis block is itself."""

def __str__(self) -> str:
return "StreamletGenesis(n=%s)" % (self.n,)
return f"StreamletGenesis(n={self.n})"

def proposer_for_epoch(self, epoch: int):
assert epoch > 0
Expand Down Expand Up @@ -97,4 +97,4 @@ def _compute_last_final(self) -> StreamletBlock | StreamletGenesis:
(first, middle, last) = (first.parent, first, middle)

def __str__(self) -> str:
return "StreamletBlock(proposal=%s)" % (self.proposal,)
return f"StreamletBlock(proposal={self.proposal})"
Loading

0 comments on commit 14877fd

Please sign in to comment.