This repository has been archived by the owner on Jan 6, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 44
Add blockchain sharding #169
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
57842ad
add v1 binary blockchain sharding
6c5163a
minor lints
26dfff9
update forkchoice to have unlimited number of shards
c0a13e9
cleanup sharding view
3c057cb
minor lints
a551af2
fix merge conflicts
94006e8
fix bug in sharding forkchoice
af6ffbd
minor updates to sharding protocol
677a728
minor lint
d9e58c2
fix bug in forkchoice
307e7d4
minor lints and cleanup to sharding
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
"""The block module implements the message data structure for a sharded blockchain""" | ||
from casper.message import Message | ||
NUM_MERGE_SHARDS = 2 | ||
|
||
|
||
class Block(Message): | ||
"""Message data structure for a sharded blockchain""" | ||
|
||
@classmethod | ||
def is_valid_estimate(cls, estimate): | ||
for key in ['prev_blocks', 'shard_ids']: | ||
if key not in estimate: | ||
return False | ||
if not isinstance(estimate[key], set): | ||
return False | ||
return True | ||
|
||
def on_shard(self, shard_id): | ||
return shard_id in self.estimate['shard_ids'] | ||
|
||
def prev_block(self, shard_id): | ||
"""Returns the previous block on the shard: shard_id | ||
Throws a KeyError if there is no previous block""" | ||
if shard_id not in self.estimate['shard_ids']: | ||
raise KeyError("No previous block on that shard") | ||
|
||
for block in self.estimate['prev_blocks']: | ||
# if this block is the genesis, previous is None | ||
if block is None: | ||
return None | ||
|
||
# otherwise, return the previous block on that shard | ||
if block.on_shard(shard_id): | ||
return block | ||
|
||
raise KeyError("Block on {}, but has no previous block on that shard!".format(shard_id)) | ||
|
||
@property | ||
def is_merge_block(self): | ||
return len(self.estimate['shard_ids']) == NUM_MERGE_SHARDS | ||
|
||
@property | ||
def is_genesis_block(self): | ||
return None in self.estimate['prev_blocks'] | ||
|
||
def conflicts_with(self, message): | ||
"""Returns true if self is not in the prev blocks of other_message""" | ||
assert isinstance(message, Block), "...expected a block" | ||
|
||
return not self.is_in_blockchain(message, '') | ||
|
||
def is_in_blockchain(self, block, shard_id): | ||
"""Could be a zero generation ancestor!""" | ||
if not block: | ||
return False | ||
|
||
if not block.on_shard(shard_id): | ||
return False | ||
|
||
if self == block: | ||
return True | ||
|
||
return self.is_in_blockchain(block.prev_block(shard_id), shard_id) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
"""The forkchoice module implements the estimator function a sharded blockchain""" | ||
|
||
|
||
def get_max_weight_indexes(scores): | ||
"""Returns the keys that map to the max value in a dict. | ||
The max value must be greater than zero.""" | ||
|
||
max_score = max(scores.values()) | ||
|
||
assert max_score > 0, "max_score should be greater than zero" | ||
|
||
max_weight_estimates = {e for e in scores if scores[e] == max_score} | ||
|
||
return max_weight_estimates | ||
|
||
|
||
def get_scores(starting_block, latest_messages, shard_id): | ||
"""Returns a dict of block => weight""" | ||
scores = dict() | ||
|
||
for validator, current_block in latest_messages.items(): | ||
if not current_block.on_shard(shard_id): | ||
continue | ||
|
||
while current_block and current_block != starting_block: | ||
scores[current_block] = scores.get(current_block, 0) + validator.weight | ||
current_block = current_block.prev_block(shard_id) | ||
|
||
return scores | ||
|
||
|
||
def get_shard_fork_choice(starting_block, children, latest_messages, shard_id): | ||
"""Get the forkchoice for a specific shard""" | ||
|
||
scores = get_scores(starting_block, latest_messages, shard_id) | ||
|
||
best_block = starting_block | ||
while best_block in children: | ||
curr_scores = dict() | ||
max_score = 0 | ||
for child in children[best_block]: | ||
if not child.on_shard(shard_id): | ||
continue # we only select children on the same shard | ||
# can't pick a child that a merge block with a higher shard | ||
if child.is_merge_block: | ||
not_in_forkchoice = False | ||
for shard in child.estimate['shard_ids']: | ||
if len(shard) < len(shard_id): | ||
not_in_forkchoice = True | ||
break | ||
if not_in_forkchoice: | ||
continue | ||
curr_scores[child] = scores.get(child, 0) | ||
max_score = max(curr_scores[child], max_score) | ||
|
||
# If no child on shard, or 0 weight block, stop | ||
if max_score == 0: | ||
break | ||
|
||
max_weight_children = get_max_weight_indexes(curr_scores) | ||
|
||
assert len(max_weight_children) == 1, "... there should be no ties!" | ||
|
||
best_block = max_weight_children.pop() | ||
|
||
return best_block | ||
|
||
|
||
def get_all_shards_fork_choice(starting_blocks, children, latest_messages_on_shard): | ||
"""Returns a dict of shard_id -> forkchoice. | ||
Starts from starting block for shard, and stops when it reaches tip""" | ||
|
||
# for any shard we have latest messages on, we should have a starting block | ||
for key in starting_blocks.keys(): | ||
assert key in latest_messages_on_shard | ||
for key in latest_messages_on_shard.keys(): | ||
assert key in latest_messages_on_shard | ||
|
||
shards_forkchoice = { | ||
shard_id: get_shard_fork_choice( | ||
starting_blocks[shard_id], | ||
children, | ||
latest_messages_on_shard[shard_id], | ||
shard_id | ||
) for shard_id in starting_blocks | ||
} | ||
|
||
return shards_forkchoice |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
"""The blockchain plot tool implements functions for plotting sharded blockchain data structures""" | ||
|
||
from casper.plot_tool import PlotTool | ||
from casper.safety_oracles.clique_oracle import CliqueOracle | ||
import casper.utils as utils | ||
|
||
|
||
class ShardingPlotTool(PlotTool): | ||
"""The module contains functions for plotting a blockchain data structure""" | ||
|
||
def __init__(self, display, save, view, validator_set): | ||
super().__init__(display, save, 's') | ||
self.view = view | ||
self.validator_set = validator_set | ||
self.starting_blocks = self.view.starting_blocks | ||
self.message_fault_tolerance = dict() | ||
|
||
self.blockchain = [] | ||
self.communications = [] | ||
|
||
self.block_fault_tolerance = {} | ||
self.message_labels = {} | ||
self.justifications = { | ||
validator: [] | ||
for validator in validator_set | ||
} | ||
|
||
def update(self, new_messages=None): | ||
"""Updates displayable items with new messages and paths""" | ||
return | ||
|
||
if new_messages is None: | ||
new_messages = [] | ||
|
||
self._update_new_justifications(new_messages) | ||
self._update_blockchain(new_messages) | ||
self._update_block_fault_tolerance() | ||
self._update_message_labels(new_messages) | ||
|
||
def plot(self): | ||
"""Builds relevant edges to display and creates next viewgraph using them""" | ||
return | ||
best_chain_edge = self.get_best_chain() | ||
|
||
validator_chain_edges = self.get_validator_chains() | ||
|
||
edgelist = [] | ||
edgelist.append(utils.edge(self.blockchain, 2, 'grey', 'solid')) | ||
edgelist.append(utils.edge(self.communications, 1, 'black', 'dotted')) | ||
edgelist.append(best_chain_edge) | ||
edgelist.extend(validator_chain_edges) | ||
|
||
self.next_viewgraph( | ||
self.view, | ||
self.validator_set, | ||
edges=edgelist, | ||
message_colors=self.block_fault_tolerance, | ||
message_labels=self.message_labels | ||
) | ||
|
||
def get_best_chain(self): | ||
"""Returns an edge made of the global forkchoice to genesis""" | ||
best_message = self.view.estimate() | ||
best_chain = utils.build_chain(best_message, None)[:-1] | ||
return utils.edge(best_chain, 5, 'red', 'solid') | ||
|
||
def get_validator_chains(self): | ||
"""Returns a list of edges main from validators current forkchoice to genesis""" | ||
vals_chain_edges = [] | ||
for validator in self.validator_set: | ||
chain = utils.build_chain(validator.my_latest_message(), None)[:-1] | ||
vals_chain_edges.append(utils.edge(chain, 2, 'blue', 'solid')) | ||
|
||
return vals_chain_edges | ||
|
||
def _update_new_justifications(self, new_messages): | ||
for message in new_messages: | ||
sender = message.sender | ||
for validator in message.justification: | ||
last_message = self.view.justified_messages[message.justification[validator]] | ||
# only show if new justification | ||
if last_message not in self.justifications[sender]: | ||
self.communications.append([last_message, message]) | ||
self.justifications[sender].append(last_message) | ||
|
||
def _update_blockchain(self, new_messages): | ||
for message in new_messages: | ||
if message.estimate is not None: | ||
self.blockchain.append([message, message.estimate]) | ||
|
||
def _update_message_labels(self, new_messages): | ||
for message in new_messages: | ||
self.message_labels[message] = message.sequence_number | ||
|
||
def _update_block_fault_tolerance(self): | ||
tip = self.view.estimate() | ||
|
||
while tip and self.block_fault_tolerance.get(tip, 0) != len(self.validator_set) - 1: | ||
oracle = CliqueOracle(tip, self.view, self.validator_set) | ||
fault_tolerance, num_node_ft = oracle.check_estimate_safety() | ||
|
||
if fault_tolerance > 0: | ||
self.block_fault_tolerance[tip] = num_node_ft | ||
|
||
tip = tip.estimate |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
from casper.protocols.sharding.sharding_view import ShardingView | ||
from casper.protocols.sharding.block import Block | ||
from casper.protocols.sharding.sharding_plot_tool import ShardingPlotTool | ||
from casper.protocol import Protocol | ||
|
||
|
||
class ShardingProtocol(Protocol): | ||
View = ShardingView | ||
Message = Block | ||
PlotTool = ShardingPlotTool | ||
|
||
shard_genesis_blocks = dict() | ||
curr_shard_idx = 0 | ||
curr_shard_ids = [''] | ||
|
||
"""Shard ID's look like this: | ||
'' | ||
/ \ | ||
'0' '1' | ||
/ \ / \ | ||
'00''01''10''11' | ||
|
||
|
||
Blocks can be merge mined between shards if | ||
there is an edge between shards | ||
That is, for ids shard_1 and shard_2, there can be a merge block if | ||
abs(len(shard_1) - len(shard_2)) = 1 AND | ||
for i in range(min(len(shard_1), len(shard_2))): | ||
shard_1[i] = shard_2[i] | ||
""" | ||
|
||
@classmethod | ||
def initial_message(cls, validator): | ||
"""Returns a starting block for a shard""" | ||
shard_id = cls.get_next_shard_id() | ||
|
||
estimate = {'prev_blocks': set([None]), 'shard_ids': set([shard_id])} | ||
cls.shard_genesis_blocks[shard_id] = Block(estimate, dict(), validator, -1, 0) | ||
|
||
return cls.shard_genesis_blocks[''] | ||
|
||
@classmethod | ||
def get_next_shard_id(cls): | ||
next_id = cls.curr_shard_ids[cls.curr_shard_idx] | ||
cls.curr_shard_idx += 1 | ||
|
||
if cls.curr_shard_idx == len(cls.curr_shard_ids): | ||
next_ids = [] | ||
for shard_id in cls.curr_shard_ids: | ||
next_ids.append(shard_id + '0') | ||
next_ids.append(shard_id + '1') | ||
|
||
cls.curr_shard_idx = 0 | ||
cls.curr_shard_ids = next_ids | ||
|
||
return next_id |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sick comment 😸