research/casper.py

246 lines
8.2 KiB
Python

import copy, random, hashlib
# GhostTable: { block number: { block: validators } }
# The ghost table represents the entire current "view" of a user, and
# every block produced contains the producer's ghost table at the time.
# Signature slashing rules (not implemented)
# 1. Sign two blocks at the same height
# 2. Sign an invalid block
# 3. Sign a block which fully confirms A at height H, and sign B at height H
h = [3**50]
ids = [0]
# Number of validators
NUM_VALIDATORS = 50
# Block time in ticks (eg. 1 tick = 0.1 seconds)
BLKTIME = 30
# Disparity in the blocks of nodes
CLOCK_DISPARITY = 10
# An exponential distribution for latency offset by a minimum
LATENCY_MIN = 4
LATENCY_BASE = 2
LATENCY_PROB = 0.25
def assign_hash():
h[0] = (h[0] * 3) % 2**48
return h[0]
def assign_id():
ids[0] += 1
return ids[0] - 1
def latency_distribution_sample():
v = LATENCY_BASE
while random.random() > LATENCY_PROB:
v *= 2
return v + LATENCY_MIN
def clock_offset_distribution_sample():
return random.randrange(-CLOCK_DISPARITY, CLOCK_DISPARITY)
# A signature represents the entire "view" of a signer,
# where the view is the set of blocks that the signer
# considered most likely to be valid at the time that
# they were produced
class Signature():
def __init__(self, signer, view):
self.signer = signer
self.view = copy.deepcopy(view)
# A ghost table represents the view that a user had of the signatures
# available at the time that the block was produced.
class GhostTable():
def __init__(self):
self.confirmed = []
self.unconfirmed = []
def process_signature(self, sig):
# Process every block height in the signature
for i in range(len(self.confirmed), len(sig.view)):
# A ghost table entry at a height is a mapping of
# block hash -> signers
if i >= len(self.unconfirmed):
self.unconfirmed.append({})
cur_entry = self.unconfirmed[i]
# If the block hash is not yet in the ghost table, add it, and
# initialize it with an empty signer set
if sig.view[i] not in cur_entry:
cur_entry[sig.view[i]] = {}
# Add the signer
cur_entry[sig.view[i]][sig.signer] = True
# If it has 67% signatures, finalize
if len(cur_entry[sig.view[i]]) > NUM_VALIDATORS * 2 / 3:
# prevgt = block_map[sig.view[i]].gt
prevgt = self
print 'confirmed', block_map[sig.view[i]].height, sig.view[i]
# Update blocks between the previous confirmation and the
# current confirmation based on the newly confirmed block's
# ghost table
for j in range(len(self.confirmed), i):
# At each intermediate height, add the block for which we
# havethe most signatures
maxkey, maxval = 0, 0
for k in prevgt.unconfirmed[j]:
if len(prevgt.unconfirmed[j][k]) > maxval:
maxkey, maxval = k, len(prevgt.unconfirmed[j][k])
self.confirmed.append(maxkey)
print j, {k: len(prevgt.unconfirmed[j][k]) for k in prevgt.unconfirmed[j]}
# Then add the new block that got 67% signatures
print i, sig.view[i]
self.confirmed.append(sig.view[i])
# Hash of the ghost table's contents (to make sure that it's not
# being modified when it's already supposed to be set in stone)
def hash(self):
print hashlib.sha256(repr(self.unconfirmed) +
repr(self.confirmed)).hexdigest()[:15]
# Create a new ghost table that appends to an existing ghost table, adding
# some set of signatures
def append(self, sigs):
x = GhostTable()
x.confirmed = copy.deepcopy(self.confirmed)
x.unconfirmed = copy.deepcopy(self.unconfirmed)
for sig in sigs:
x.process_signature(sig)
return x
class Block():
def __init__(self, h, gt, maker):
self.gt = gt
self.height = h
self.maker = maker
self.hash = assign_hash()
class Validator():
def __init__(self):
self.gt = GhostTable()
self.view = []
self.id = assign_id()
self.new_sigs = []
self.clock_offset = clock_offset_distribution_sample()
self.last_block_produced = -99999
self.last_unseen = 0
# Is this block compatible with our view?
def is_compatible_with_view(self, block):
return block.height >= len(self.view) or \
self.view[block.height] is None
# Add a block to this validator's view of probably valid
# blocks
def add_to_view(self, block):
while len(self.view) <= block.height:
self.view.append(None)
self.view[block.height] = block.hash
while self.last_unseen < len(self.view) and \
self.view[self.last_unseen] is not None:
self.last_unseen += 1
# Make a block
def produce_block(self):
self.gt = self.gt.append(self.new_sigs)
newblk = Block(self.last_unseen, self.gt, self.id)
print 'newblk', newblk.height
self.add_to_view(newblk)
publish(newblk)
newsig = Signature(self.id, self.view[:self.last_unseen])
self.new_sigs = [newsig]
publish(newsig)
# Callback function upon receiving a block
def on_receive(self, obj):
if isinstance(obj, Block):
desired_maker = (self.time() // BLKTIME) % NUM_VALIDATORS
if 0 <= (desired_maker - obj.maker) % 100 <= 0:
if self.is_compatible_with_view(obj):
self.add_to_view(obj)
publish(Signature(self.id, self.view[:self.last_unseen]))
if isinstance(obj, Signature):
self.new_sigs.append(obj)
# Do everything that you need to do in this particular round
def tick(self):
if (self.time() // BLKTIME) % NUM_VALIDATORS == self.id:
if self.time() - self.last_block_produced > \
BLKTIME * NUM_VALIDATORS:
self.produce_block()
self.last_block_produced = self.time()
# Calculate the validator's own clock based on the actual time
# plus a time offset that this validator happens to be wrong by
# (eg. +1 second)
def time(self):
return real_time[0] + self.clock_offset
block_map = {}
listening_queue = {}
real_time = [0]
validators = {}
# Publish a block or a signature
def publish(obj):
if isinstance(obj, Block):
block_map[obj.hash] = obj
# For every validator, add it to the validator's listening queue
# at a time randomly sampled from the latency distribution
for v in validators:
arrival_time = real_time[0] + latency_distribution_sample()
if arrival_time not in listening_queue:
listening_queue[arrival_time] = []
listening_queue[arrival_time].append((v, obj))
# One round of the clock ticking
def tick():
for _, v in validators.items():
v.tick()
if real_time[0] in listening_queue:
for validator_id, obj in listening_queue[real_time[0]]:
validators[validator_id].on_receive(obj)
real_time[0] += 1
print real_time[0]
# Main function: run(7000) = simulate casper for 7000 ticks
def run(steps):
for k in block_map.keys():
del block_map[k]
for k in listening_queue.keys():
del listening_queue[k]
for k in validators.keys():
del validators[k]
real_time[0] = 0
ids[0] = 0
for i in range(NUM_VALIDATORS):
v = Validator()
validators[v.id] = v
for i in range(steps):
tick()
c = []
for _, v in validators.items():
for i, b in enumerate(v.gt.confirmed):
assert block_map[b].height == i
if v.gt.confirmed[:len(c)] != c[:len(v.gt.confirmed)]:
for i in range(min(len(c), len(v.gt.confirmed))):
if c[i] != v.gt.confirmed[i]:
print i, c[i], v.gt.confirmed[i]
raise Exception("Confirmed block list mismatch")
c.extend(v.gt.confirmed[len(c):])
print c