Made binary by-block consensus work

This commit is contained in:
Vitalik Buterin 2015-08-27 09:38:48 -04:00
parent 098bdd0e24
commit 04bcd31ea3
4 changed files with 185 additions and 353 deletions

View File

@ -2,9 +2,14 @@ import copy, random, hashlib
from distributions import normal_distribution
import networksim
from voting_strategy import vote
import math
NUM_VALIDATORS = 20
BLKTIME = 100
BLKTIME = 40
BY_CHAIN = False
NETSPLITS = False
GENESIS_STATE = 0
logging_level = 0
@ -15,17 +20,16 @@ def log(s, lvl):
class Signature():
def __init__(self, signer, probs, height, last_finalized):
def __init__(self, signer, probs, finalized_state, sign_from):
self.signer = signer
# List of maps from block hash to probability
self.probs = probs
# Top height of the signature
self.height = height
# The HashChainObj that represents the last finalized hash
# of this signature
self.last_finalized = last_finalized
# Hash of the signature (for db storage purposes)
self.hash = random.randrange(10**14) + 10**14 * self.height
self.hash = random.randrange(10**14)
# Finalized state
self.finalized_state = finalized_state
# Finalized height
self.sign_from = sign_from
class Block():
@ -35,73 +39,27 @@ class Block():
# The height of the block
self.height = height
# Hash of the signature (for db storage purposes)
self.hash = random.randrange(10**20) + 10**20 * self.height
self.hash = random.randrange(10**20) + 10**21 + 10**23 * self.height
# An object containing the hash of a block and the hash of a previous
# hash chain object. These objects end up forming the actual "blockchain"
# in this scheme
class HashChainObj():
def __init__(self, block, prev):
# Genesis hash chain object
if block is None:
self.hash = 0
self.prev = None
self.blockhash = None
# All other hash chain objects
else:
self.hash = (prev.hash ** 7 + block.hash ** 3) ** 5 % 10**30
self.prev = prev.hash
self.blockhash = block.hash
# A request for info from a node that needs to synchronize after a period
# of being offline from the network
class SyncRequest():
def __init__(self, sender):
self.sender_id = sender.id
self.last_finalized_height = sender.max_finalized_height
self.hash = random.randrange(10**10)
# A response to a sync request
class SyncResponse():
def __init__(self, blocks, signatures, finalized_chain, requester_mfh, responder_mfh, responder_hashchain):
self.blocks = blocks
self.signatures = signatures
self.finalized_chain = finalized_chain
self.requester_mfh = requester_mfh
self.responder_mfh = responder_mfh
self.responder_hashchain = responder_hashchain
self.hash = random.randrange(10**10)
# A request to get an object with a particular hash
class ObjRequest():
def __init__(self, sender, ask_hash):
self.sender_id = sender.id
self.ask_hash = ask_hash
self.hash = random.randrange(10**10)
# A response to an object request
class ObjResponse():
def __init__(self, obj):
self.obj = obj
self.hash = random.randrange(10**10)
def state_transition(state, block):
return state if block is None else (state ** 3 + block.hash ** 5) % 10**40
# A validator
class Validator():
def __init__(self, pos, network):
# Map from height to { blocks: blocks, signatures: signatures }
self.heights = {0: {"blocks": {}, "signatures": {}}}
# Map from height to {node_id: latest_opinion}
self.received_signatures = []
# List of received blocks
self.received_blocks = []
# Own probability estimates
self.probs = []
# All objects that this validator has received; basically a database
self.received_objects = {}
self.time_received = {}
# The validator's ID, and its position in the queue
self.pos = self.id = pos
# Heights that this validator has already signed
self.signed_heights = {}
# This validator's offset from the clock
self.time_offset = max(normal_distribution(200, 100)(), 0)
# The highest height that this validator has seen
@ -109,142 +67,58 @@ class Validator():
self.head = None
# The last time the validator made a block
self.last_time_made_block = -999999999999
# Block hashes of finalized blocks
self.finalized = [None]
# The validator's hash chain
self.finalized_hashes = [HashChainObj(None, None)]
self.finalized_hashes = []
# Finalized states
self.finalized_states = []
# The highest height that the validator has finalized
self.max_finalized_height = 0
self.max_finalized_height = -1
# The network object
self.network = network
# Last time signed
self.last_time_signed = 0
# Next neight to mine
self.next_height = self.pos
def get_time(self):
return self.network.time + self.time_offset
def sign(self, block):
# Calculate this validator's (randomly offset) view of the current time
mytime = now[0] + self.time_offset
offset = (mytime - (block.maker * BLKTIME)) % (BLKTIME * NUM_VALIDATORS)
# Iniitalize the probability array, the core of the signature
probs = []
# Compute the validator's opinion of the latest block, based on whether
# or not it arrived at the correct time
if offset < BLKTIME:
probs.append({block.hash: 0.67 + random.random() * 0.05})
else:
probs.append({block.hash: 0.33 - random.random() * 0.05})
# Compute the validator's current view of previous blocks up to the
# point of finalization
probs.extend(self.compute_view(block.height))
if self.pos == 0:
for i, v in enumerate(probs):
log('Signatures for block %d: %r' % (block.height - i, v), lvl=1)
# Add the end of the node's finalized hash chain to the signature, and
# create the signature
pre_probs_h = block.height - len(probs)
o = Signature(self.pos, probs, block.height, self.finalized_hashes[pre_probs_h])
# Sanity check
if o.last_finalized.blockhash is not None:
assert o.last_finalized.blockhash // 10**20 == block.height - len(probs)
# Append the signature to the node's list of signatures produced and return it
signatures.append(o)
return o
def compute_view(self, from_height):
# Fetch the latest signature from each validator, along with its age
signatures = {}
for v in range(NUM_VALIDATORS):
for q in xrange(1, from_height):
if from_height - q in self.heights and v in self.heights[from_height - q]['signatures']:
signatures[v] = (q, self.heights[from_height - q]['signatures'][v])
break
# If we have no signatures, then we have no opinion
if len(signatures) == 0:
return []
# For every height between the node's current maximum seen height and
# its last known finalized height...
probs = []
for i in xrange(1, from_height - self.max_finalized_height):
block_scores = {}
if from_height - i not in self.heights:
self.heights[from_height - i] = {"blocks": {}, "signatures": {}}
# For every signature...
for q, sig in signatures.values():
assert sig.height == from_height - q
# If the signature is older than this height, then it has
# nothing to say about this height so its vote for every block
# is assumed to be zero
if i < q:
continue
# Otherwise, grab the signature's probability estimate for
# every block at the height
elif len(sig.probs) > i-q:
for blockhash, prob in sig.probs[i-q].items():
assert blockhash // 10**20 == from_height - q - (i-q)
if blockhash not in block_scores:
block_scores[blockhash] = []
block_scores[blockhash].append(prob)
# Every signature has a hash chain object at the end; this
# implicitly attests with probability 0.999999 that every block
# in that chain going back is final. Hence, we go back through
# the chain and add such an attestation to our list of votes
else:
h = sig.last_finalized
assert h.blockhash // 10**20 == from_height - q - len(sig.probs)
success = True
for _ in range(i-q - len(sig.probs)):
if h.prev not in self.received_objects:
success = False
break
h = self.received_objects[h.prev]
if success and h.blockhash is not None:
assert h.blockhash // 10**20 == from_height - i, (h.blockhash, from_height - i)
if h.blockhash not in block_scores:
block_scores[h.blockhash] = []
block_scores[h.blockhash].append(0.999999)
if i-q-len(sig.probs) > 0:
log('Decoding hash succeeded', lvl=3)
else:
log('Decoding hash failed', lvl=3)
self.network.broadcast(self, SyncRequest(self))
# Use the array of previous votes that we have collected, and
# compute from that our own vote for every block
probs.append(vote(block_scores, self.received_objects, NUM_VALIDATORS))
for b in block_scores:
if b not in self.received_objects:
self.network.broadcast(self, ObjRequest(self, b))
# Log a single node's viewpoint changing over time
if self.pos == 0:
log('%d %r' % (from_height - i, self.heights[from_height - i]["blocks"].keys()), lvl=2)
log(block_scores, lvl=2)
log(probs[-1], lvl=2)
# See if our vote corresponds to finality anywhere
for blockhash, p in probs[-1].items():
assert blockhash // 10**20 == from_height - i
# 0.9999 = finality threshold
if p > 0.9999:
while len(self.finalized) <= from_height - i:
self.finalized.append(None)
self.finalized[from_height - i] = blockhash
# Add the hash to a global list of finalized blocks
finalized_blocks[blockhash] = True
# Add all other hashes at that height to a global list of
# discarded blocks
if from_height - i in self.heights:
for b in self.heights[from_height - i]['blocks']:
if b != blockhash:
discarded[b] = True
# Advance the max_finalized_height and re-calculate the
# hash chain
while self.max_finalized_height + 1 < len(self.finalized) and self.finalized[self.max_finalized_height + 1] is not None:
self.max_finalized_height += 1
last_finalized_block = self.received_objects[self.finalized[self.max_finalized_height]]
new_state = HashChainObj(last_finalized_block, self.finalized_hashes[-1])
self.received_objects[new_state.hash] = new_state
self.finalized_hashes.append(new_state)
# Sanity check
for j, p in enumerate(probs):
for h, sig in p.items():
assert h // 10**20 == from_height - j - 1, (probs, from_height, block_scores)
log('Probabilities: %r' % probs, lvl=4)
return probs
# Initialize the probability array, the core of the signature
best_guesses = [None] * len(self.received_blocks)
sign_from = max(0, self.max_finalized_height - 30)
for i, b in list(enumerate(self.received_blocks))[sign_from:]:
if self.received_blocks[i] is None:
time_delta = self.get_time() - BLKTIME * i
my_opinion = 0.35 / (1 + max(0, time_delta) * 0.3 / BLKTIME) + 0.14
else:
time_delta = self.time_received[b.hash] - BLKTIME * i
my_opinion = 0.7 / (1 + abs(time_delta) * 0.3 / BLKTIME) + 0.15
if my_opinion == 0.5:
my_opinion = 0.5001
votes = self.received_signatures[i].values() if i < len(self.received_signatures) else []
votes += [my_opinion] * (NUM_VALIDATORS - len(votes))
best_guesses[i] = min(vote(votes), 1 if self.received_blocks[i] is not None else my_opinion)
if best_guesses[i] > 0.9999:
while len(self.finalized_hashes) <= i:
self.finalized_hashes.append(None)
self.finalized_hashes[i] = self.received_blocks[i].hash
elif best_guesses[i] < 0.0001:
while len(self.finalized_hashes) <= i:
self.finalized_hashes.append(None)
self.finalized_hashes[i] = False
while self.max_finalized_height < len(self.finalized_hashes) - 1 \
and self.finalized_hashes[self.max_finalized_height + 1] is not None:
self.max_finalized_height += 1
last_state = self.finalized_states[-1] if len(self.finalized_states) else GENESIS_STATE
self.finalized_states.append(state_transition(last_state, self.received_blocks[self.max_finalized_height]))
self.probs = self.probs[:sign_from] + best_guesses[sign_from:]
log('Making signature: %r' % self.probs[-10:], lvl=1)
sign_from_state = self.finalized_states[sign_from - 1] if sign_from > 0 else GENESIS_STATE
s = Signature(self.pos, self.probs[sign_from:], sign_from_state, sign_from)
all_signatures.append(s)
return s
def on_receive(self, obj):
# Ignore objects that we already know about
@ -253,67 +127,22 @@ class Validator():
# When receiving a block
if isinstance(obj, Block):
log('received block: %d %d' % (obj.height, obj.hash), lvl=2)
if obj.height > self.max_finalized_height + 40:
self.network.broadcast(self, SyncRequest(self))
while len(self.received_blocks) <= obj.height:
self.received_blocks.append(None)
self.received_blocks[obj.height] = obj
self.time_received[obj.hash] = self.get_time()
# If we have not yet produced a signature at this height, do so now
if obj.height not in self.signed_heights:
s = self.sign(obj)
self.signed_heights[obj.height] = True
self.on_receive(s)
self.network.broadcast(self, s)
if obj.height not in self.heights:
self.heights[obj.height] = {"blocks": {}, "signatures": {}}
self.heights[obj.height]["blocks"][obj.hash] = obj
if obj.height > self.max_height:
self.max_height = obj.height
self.head = obj
s = self.sign(obj)
self.network.broadcast(self, s)
self.on_receive(s)
self.network.broadcast(self, obj)
# When receiving a signature
elif isinstance(obj, Signature):
if obj.height not in self.heights:
self.heights[obj.height] = {"blocks": {}, "signatures": {}}
self.heights[obj.height]["signatures"][obj.signer] = obj
while len(self.received_signatures) <= len(obj.probs) + obj.sign_from:
self.received_signatures.append({})
for i, p in enumerate(obj.probs):
self.received_signatures[i + obj.sign_from][obj.signer] = p
self.network.broadcast(self, obj)
self.received_objects[obj.last_finalized.hash] = obj.last_finalized
# Received a synchronization request from another node
elif isinstance(obj, SyncRequest):
blocks, signatures, hashchainobjs = [], [], []
# Respond only if we have something to say
if self.max_finalized_height > obj.last_finalized_height:
# Add blocks and signatures at all requested and possible
# heights
for h in range(obj.last_finalized_height, self.max_height):
if h in self.heights:
for b in self.heights[h]["blocks"].values():
blocks.append(b)
for s in self.heights[h]["signatures"].values():
signatures.append(s)
# Add the finalized hash chain
for h in range(obj.last_finalized_height, self.max_finalized_height + 1):
hashchainobjs.append(self.finalized_hashes[h])
log('Responding to request with height %d, my finalized height %d and my max height %d' %
(obj.last_finalized_height, self.max_finalized_height, self.max_height), lvl=2)
# Create and send a synchronization response object
self.network.direct_send(obj.sender_id, SyncResponse(blocks, signatures, hashchainobjs,
obj.last_finalized_height, self.max_finalized_height, self.finalized))
# Received a synchronization response object from another node
elif isinstance(obj, SyncResponse):
# Process it only if the object has something to give us
if obj.responder_mfh > self.max_finalized_height:
log('Received response, my finalized height was %d and my height was %d, their MFH was %d'
% (self.max_finalized_height, self.max_height, obj.responder_mfh), lvl=2)
for s in obj.finalized_chain:
self.received_objects[s.hash] = s
for s in obj.signatures:
self.on_receive(s)
for b in obj.blocks:
if b.height not in self.heights:
self.heights[b.height] = {"signatures": {}, "blocks": {}}
self.heights[b.height]["blocks"][b.hash] = b
self.received_objects[b.hash] = b
self.compute_view(self.max_height)
log('And now they are %d and %d' %
(self.max_finalized_height, self.max_height), lvl=2)
# Received an object request, respond if we have it
elif isinstance(obj, ObjRequest):
if obj.ask_hash in self.received_objects:
@ -323,50 +152,90 @@ class Validator():
elif isinstance(obj, ObjResponse):
self.received_objects[obj.obj.hash] = obj.obj
self.received_objects[obj.hash] = obj
self.time_received[obj.hash] = self.get_time()
# Run every tick
def tick(self):
mytime = self.network.time + self.time_offset
offset = (mytime - (self.pos * BLKTIME)) % (BLKTIME * NUM_VALIDATORS)
if offset < BLKTIME and self.last_time_made_block < mytime - (BLKTIME * NUM_VALIDATORS / 2):
self.last_time_made_block = mytime
o = Block(self.pos, self.max_height + 1)
mytime = self.get_time()
target_time = BLKTIME * self.next_height
if mytime >= target_time:
o = Block(self.pos, self.next_height)
self.next_height += NUM_VALIDATORS
log('making block: %d %d' % (o.height, o.hash), lvl=1)
self.network.broadcast(self, o)
if random.random() < 0.9:
self.network.broadcast(self, o)
while len(self.received_blocks) <= o.height:
self.received_blocks.append(None)
self.received_blocks[o.height] = o
self.received_objects[o.hash] = o
self.time_received[o.hash] = mytime
return o
validator_list = []
future = {}
discarded = {}
finalized_blocks = {}
signatures = []
all_signatures = []
now = [0]
def who_heard_of(h, n):
o = ''
for x in n.agents:
o += '1' if h in x.received_objects else '0'
return o
def get_opinions(n):
o = []
maxheight = 0
for x in n.agents:
maxheight = max(maxheight, len(x.probs))
for h in range(maxheight):
p = ''
q = ''
for x in n.agents:
if len(x.probs) <= h:
p += '_'
elif x.probs[h] < 0.5:
p += str(int(5 - math.log(x.probs[h]) / math.log(0.0001) * 4) if x.probs[h] > 0.0001 else 0)
elif x.probs[h] >= 0.5:
p += str(int(5 + math.log(1 - x.probs[h]) / math.log(0.0001) * 4) if x.probs[h] < 0.9999 else 9)
q += 'n' if len(x.received_blocks) <= h or x.received_blocks[h] is None else 'y'
o.append((h, p, q))
return o
def get_finalization_heights(n):
o = []
for x in n.agents:
o.append(x.max_finalized_height)
return o
# Check how often blocks that are assigned particular probabilities of
# finalization by our algorithm are actually finalized
def calibrate():
def calibrate(finalized_hashes):
thresholds = [0, 0.25, 0.5, 0.75] + [1 - 0.5**k for k in range(10)] + [1]
signed = [0] * (len(thresholds) - 1)
_finalized = [0] * (len(thresholds) - 1)
_discarded = [0] * (len(thresholds) - 1)
for s in signatures:
for probs in s.probs:
for blockhash, p in probs.items():
index = 0
while p > thresholds[index + 1]:
index += 1
signed[index] += 1
if blockhash in finalized_blocks:
_finalized[index] += 1
assert blockhash not in discarded, blockhash
if blockhash in discarded:
_discarded[index] += 1
assert blockhash not in finalized_blocks, blockhash
for s in all_signatures:
for i, prob in enumerate(s.probs):
if i + s.sign_from >= len(finalized_hashes):
continue
actual_result = 1 if finalized_hashes[i + s.sign_from] else 0
index = 0
while prob > thresholds[index + 1]:
index += 1
signed[index] += 1
if actual_result == 1:
_finalized[index] += 1
elif actual_result == 0:
_discarded[index] += 1
for i in range(len(thresholds) - 1):
if _finalized[i] + _discarded[i]:
print 'Probability from %f to %f: %f' % (thresholds[i], thresholds[i+1], _finalized[i] * 1.0 / (_finalized[i] + _discarded[i]))
print 'Percentage of blocks nonempty: %f%%' % (len([x for x in finalized_hashes if x]) * 100.0 / len(finalized_hashes))
def run(steps=4000):
@ -374,8 +243,8 @@ def run(steps=4000):
for i in range(NUM_VALIDATORS):
n.agents.append(Validator(i, n))
n.generate_peers()
while len(signatures):
signatures.pop()
while len(all_signatures):
all_signatures.pop()
for x in future.keys():
del future[x]
for x in finalized_blocks.keys():
@ -385,27 +254,31 @@ def run(steps=4000):
for i in range(steps):
n.tick()
if i % 250 == 0:
finalized = [(v.max_finalized_height, v.finalized) for v in n.agents]
finalized = sorted(finalized, key=lambda x: len(x[1]))
print get_opinions(n)[-60:]
finalized0 = [(v.max_finalized_height, v.finalized_hashes) for v in n.agents]
finalized = sorted(finalized0, key=lambda x: len(x[1]))
for j in range(len(n.agents) - 1):
for k in range(len(finalized[j][1])):
assert finalized[j][1][k] is None or finalized[j+1][1][k] is \
None or finalized[j][1][k] == finalized[j+1][1][k], (finalized, j)
print 'Finalized status: %r' % [x[1][x[0]] for x in finalized]
if i == 10000:
if finalized[j][1][k] is not None and finalized[j+1][1][k] is not None:
if finalized[j][1][k] != finalized[j+1][1][k]:
print finalized[j]
print finalized[j+1]
raise Exception("Finalization mismatch: %r %r" % (finalized[j][1][k], finalized[j+1][1][k]))
print 'Finalized status: %r' % [x[0] for x in finalized0]
if i == 10000 and NETSPLITS:
print "###########################################################"
print "Knocking off 20% of the network!!!!!"
print "###########################################################"
n.knock_offline_random(NUM_VALIDATORS // 5)
if i == 20000:
if i == 20000 and NETSPLITS:
print "###########################################################"
print "Simluating a netsplit!!!!!"
print "###########################################################"
n.generate_peers()
n.partition()
if i == 30000:
if i == 30000 and NETSPLITS:
print "###########################################################"
print "Network health back to normal!"
print "###########################################################"
n.generate_peers()
calibrate()
calibrate(n.agents[0].finalized_hashes)

View File

@ -3,8 +3,9 @@ import random, sys
def normal_distribution(mean, standev):
def f():
total = sum([random.choice([1, -1]) for i in range(529)])
return int(total * (standev / 23.0) + mean)
return int(random.normalvariate(mean, standev))
# total = sum([random.choice([2, 0, 0, -2]) for i in range(8)])
# return int(total * (standev / 4.0) + mean)
return f
@ -14,9 +15,9 @@ def exponential_distribution(mean):
total = 0
while 1:
total += 1
if not random.randrange(500):
if not random.randrange(32):
break
return int(total * 0.002 * mean)
return int(total * 0.03125 * mean)
return f

View File

@ -16,11 +16,13 @@ class NetworkSimulator():
self.peers = {}
for a in self.agents:
p = []
while len(p) < num_peers:
while len(p) <= num_peers // 2:
p.append(random.choice(self.agents))
if p[-1] == a:
p.pop()
self.peers[a.id] = p
self.peers[a.id] = self.peers.get(a.id, []) + p
for peer in p:
self.peers[peer.id] = self.peers.get(peer.id, []) + [a]
def tick(self):
if self.time in self.objqueue:

View File

@ -1,68 +1,24 @@
# The voting strategy. Validators see what every other validator's most
# recent vote for very particular block, in the format
import random
# The voting strategy. Validators see what every other validator votes,
# and return their vote.
#
# {
# blockhash1: [vote1, vote2, vote3...],
# blockhash2: [vote1, vote2, vote3...],
# ...
# }
#
# Where the votes are probabilities with 0 < p < 1 (see
# http://lesswrong.com/lw/mp/0_and_1_are_not_probabilities/ !), and the
# strategy should itself return an object of the format
# {
# blockhash1: vote,
# blockhash2: vote,
# ...
# }
# Votes are probabilities with 0 < p < 1 (see
# http://lesswrong.com/lw/mp/0_and_1_are_not_probabilities/ !)
def vote_transform(p):
return (abs(2 * p - 1) ** 0.333 * (1 if p > 0.5 else -1) + 1) / 2
def vote(probs, db, num_validators):
pass1 = {k: get_vote_from_scores(v, num_validators)
for k, v in probs.items() if k in db}
pass2 = normalize(pass1, num_validators)
return pass2
# Get the list of scores from other users and come up with
# your own base score
def get_vote_from_scores(probs, num_validators):
if len(probs) <= num_validators * 2 / 3:
o1 = 0
else:
o1 = sorted(probs)[::-1][num_validators * 2 / 3]
return 0.8 + 0.2 * o1
# Given a set of independently computed block probabilities, "normalize" the
# probabilities (ie. make sure they sum to at most 1; less than 1 is fine
# because the difference reflects the probability that some as-yet-unknown
# block will ultimately be finalized)
def normalize(block_results, num_validators):
# Trivial base cases
if len(block_results) == 1:
return {k: v for k, v in block_results.items()}
elif len(block_results) == 0:
return {}
a = {k: v for k, v in block_results.items()}
for v in a.values():
assert v <= 1, a
# Artificially privilege the maximum value at the expense of the
# others; this ensures more rapid convergence toward one equilibrium
maxkey, maxval = None, 0
for v in a:
if a[v] > maxval:
maxkey, maxval = v, a[v]
for v in a:
if v == maxkey:
a[v] = a[v] * 0.8 + 0.2
else:
a[v] *= 0.8
# If probabilities sum to more than 1, keep reducing them via a
# transform that preserves proportional probability of non-inclusion
while 1:
for v in a.values():
assert v <= 1, a
if sum(a.values()) < 1:
return a
a = {k: v * 1.05 - 0.050001 for k, v in a.items() if v > 0.050001}
def vote(probs):
if len(probs) == 0:
return 0.5
probs = sorted(probs)
score = (probs[len(probs)/2] + probs[-len(probs)/2]) * 0.5
if score > 0.9:
score2 = probs[len(probs)/3]
score = min(score, max(score2, 0.9))
elif score < 0.1:
score2 = probs[len(probs)*2/3]
score = max(score, min(score2, 0.1))
return vote_transform(score)