Added comments, and made log-odds betting work

This commit is contained in:
vub 2015-09-12 17:46:40 -04:00
parent 5bbca50547
commit 4ae4c13952
5 changed files with 83 additions and 54 deletions

8
casper/README.md Normal file
View File

@ -0,0 +1,8 @@
The general idea of this implementation of Casper is as follows:
1. There exists a deterministic algorithm which determines a single proposer for each block. Here, the algorithm is simple: every validator is assigned an ID in the range `0 <= i < NUM_VALIDATORS`, and validator `i` proposes all blocks `NUM_VALIDATORS * k + i` for all `k ϵ Z`.
2. Validators perform a binary repeated betting procedure on every height, where they bet a value `0 < p < 1` for the probability that they think that a block at that height will be finalized. The bets are incentivized via logarithmic scoring rule, and the result of the bets themselves determines finalization (ie. if 2/3 of all validators bet `p > 0.9999`, the block is considered finalized, and if 2/3 of all validators bet `p < 0.0001`, then the state of no block existing at that height is considered finalized); hence, the betting process is self-referential.
3. From an incentive standpoint, each validator's optimal strategy is to bet the way they expect everyone else to be betting; hence, it is like a schellingcoin game in certain respects. Convergence in either direction is incentivized. As `p` approaches 0 or 1, the reward for betting correctly increases, but the penalty for betting incorrectly increases hyperbolically, so one only has the incentive to bet `p > 0.9999` or `p < 0.0001` if they are _really_ sure that their bet is correct.
4. If a validator's vote exceeds `p = 0.9`, they also need to supply the hash of the block header. Proposing two blocks at a given height is punishable by total deposit slashing.
5. From a BFT theory standpoint, this algorithm can be combined with a default strategy where bets are recorded in log odds (ie. `q = ln(p/(1-p))`), if 2/3 of voters vote `q = k` or higher for `k >= 1`, you vote `q = k+1`, and if 2/3 of voters vote `q = k` or lower for `k <= -1`, you vote `q = k-1`; this is similar to a highly protracted ten-round version of Tendermint (log odds of p = 0.9999 ~= 9.21).
6. If 2/3 of voters do not either vote `q >= 1` or `q <= -1`, then the default strategy is to vote 0 if a block has not yet arrived and it may arrive close to the specified time, vote `q = 1` if a block has arrived close to the specified time, and vote `q = -1` if a block either has not arrived and the time is far beyond the specified time, or if a block has arrived and the time is far beyond the specified time. Hence, if a block at a particular height does not appear, the votes will converge toward 0.

View File

@ -1,14 +1,22 @@
import copy, random, hashlib
from distributions import normal_distribution
import networksim
from voting_strategy import vote
from voting_strategy import vote, default_vote
import math
# Number of validators
NUM_VALIDATORS = 20
# Block time
BLKTIME = 40
# 0 for no netsplits
# 1 for simulating a netsplit where 20% of validators jump off
# the network
# 2 for simulating the above netsplit, plus a 50-50 netsplit,
# plus reconvergence
NETSPLITS = 2
# Check the equality of finalized states
CHECK_INTEGRITY = True
# The genesis state root
GENESIS_STATE = 0
logging_level = 0
@ -19,10 +27,14 @@ def log(s, lvl):
print(s)
# A signture specifies an initial height ("sign_from"), a finalized
# state from all blocks before that height and a list of probability
# bets from that height up to the latest height
class Signature():
def __init__(self, signer, probs, finalized_state, sign_from):
# The ID of the signer
self.signer = signer
# List of maps from block hash to probability
# List of probability bets, expressed in log odds
self.probs = probs
# Hash of the signature (for db storage purposes)
self.hash = random.randrange(10**14)
@ -35,6 +47,7 @@ class Signature():
return self.sign_from + len(self.probs)
# Right now, a block simply specifies a proposer and a height.
class Block():
def __init__(self, maker, height):
# The producer of the block
@ -45,6 +58,7 @@ class Block():
self.hash = random.randrange(10**20) + 10**21 + 10**23 * self.height
# A request to receive a block at a particular height
class BlockRequest():
def __init__(self, sender, height):
self.sender = sender
@ -52,6 +66,8 @@ class BlockRequest():
self.hash = random.randrange(10**14)
# Toy state transition function (in production, do sequential
# apply_transaction here)
def state_transition(state, block):
return state if block is None else (state ** 3 + block.hash ** 5) % 10**40
@ -59,7 +75,7 @@ def state_transition(state, block):
# A validator
class Validator():
def __init__(self, pos, network):
# Map from height to {node_id: latest_opinion}
# Map from height to {node_id: latest_bet}
self.received_signatures = []
# List of received blocks
self.received_blocks = []
@ -67,16 +83,14 @@ class Validator():
self.probs = []
# All objects that this validator has received; basically a database
self.received_objects = {}
# Time when the object was received
self.time_received = {}
# The validator's ID, and its position in the queue
self.pos = self.id = pos
# This validator's offset from the clock
# The offset of this validator's clock vs. real time
self.time_offset = normal_distribution(0, 100)()
# The highest height that this validator has seen
self.max_height = 0
self.head = None
# The last time the validator made a block
self.last_time_made_block = -999999999999
# The validator's hash chain
self.finalized_hashes = []
# Finalized states
@ -87,50 +101,55 @@ class Validator():
self.network = network
# Last time signed
self.last_time_signed = 0
# Next neight to mine
# Next height to mine
self.next_height = self.pos
# Get the local time from the point of view of this validator, using the
# validator's offset from real time
def get_time(self):
return self.network.time + self.time_offset
# Broadcast an object to the network
def broadcast(self, obj):
self.network.broadcast(self, obj)
def sign(self, block):
# Create a signature
def sign(self):
# 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.2 / BLKTIME) + 0.14
else:
time_delta = self.time_received[b.hash] - BLKTIME * i
my_opinion = 0.7 / (1 + abs(time_delta) * 0.2 / BLKTIME) + 0.15
# print 'tdpost', time_delta, my_opinion
if my_opinion == 0.5:
my_opinion = 0.5001
# Compute this validator's own initial vote based on when the block
# was received, compared to what time the block should have arrived
received_time = self.time_received[b.hash] if b is not None else None
my_opinion = default_vote(BLKTIME * i, received_time, self.get_time(), blktime=BLKTIME)
# Get others' bets on this height
votes = self.received_signatures[i].values() if i < len(self.received_signatures) else []
votes = [x for x in votes if x != 0]
# Fill in the not-yet-received votes with this validator's default bet
votes += [my_opinion] * (NUM_VALIDATORS - len(votes))
vote_from_signatures = vote(votes)
bg = min(vote_from_signatures, 1 if self.received_blocks[i] is not None else my_opinion)
# In case we fall into an equilibrium trap at 0.5, eventually force divergence
if self.get_time() - BLKTIME * i > BLKTIME * 40:
fac = 1.0 / (1.0 + (self.get_time() - BLKTIME * i) / (BLKTIME * 40))
if 0.14 < bg < 1 - 0.5 * fac:
bg = 0.14 + (bg - 0.14) * fac
vote_from_signatures = int(vote(votes))
# Add the bet to the list
bg = min(vote_from_signatures, 10 if self.received_blocks[i] is not None else my_opinion)
best_guesses[i] = bg
if vote_from_signatures > 0.95 and self.received_blocks[i] is None:
# Request a block if we should have it, and should have had it for
# a long time, but don't
if vote_from_signatures > 3 and self.received_blocks[i] is None:
self.broadcast(BlockRequest(self.id, i))
if best_guesses[i] > 0.9999:
elif i < len(self.received_blocks) - 50 and self.received_blocks[i] is None:
if random.random() < 0.05:
self.broadcast(BlockRequest(self.id, i))
# Block finalized
if best_guesses[i] >= 10:
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:
# Absense of the block finalized
elif best_guesses[i] <= -10:
while len(self.finalized_hashes) <= i:
self.finalized_hashes.append(None)
self.finalized_hashes[i] = False
# Add to the list of finalized states
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
@ -155,8 +174,8 @@ class Validator():
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
s = self.sign(obj)
# Upon receiving a new block, make a new signature
s = self.sign()
self.network.broadcast(self, s)
self.on_receive(s)
self.network.broadcast(self, obj)
@ -207,9 +226,6 @@ def who_heard_of(h, n):
return o
ALPHA = '0123456789'
def get_opinions(n):
o = []
maxheight = 0
@ -221,12 +237,12 @@ def get_opinions(n):
for x in n.agents:
if len(x.probs) <= h:
p += '_'
elif x.probs[h] < 0.0001:
elif x.probs[h] <= -10:
p += '-'
elif x.probs[h] > 0.9999:
elif x.probs[h] >= 10:
p += '+'
else:
p += ALPHA[int(x.probs[h] * (len(ALPHA) - 0.0001))]
p += str(x.probs[h])+','
q += 'n' if len(x.received_blocks) <= h or x.received_blocks[h] is None else 'y'
o.append((h, p, q))
return o
@ -252,7 +268,7 @@ def calibrate(finalized_hashes):
continue
actual_result = 1 if finalized_hashes[i + s.sign_from] else 0
index = 0
while prob > thresholds[index + 1]:
while index + 2 < len(thresholds) and prob > thresholds[index + 1]:
index += 1
signed[index] += 1
if actual_result == 1:

View File

@ -4,8 +4,6 @@ import random, sys
def normal_distribution(mean, standev):
def f():
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

View File

@ -2,4 +2,4 @@ import casper
import sys
casper.logging_level = int(sys.argv[1]) if len(sys.argv) > 1 else 0
casper.run(100000)
casper.run(50000)

View File

@ -3,22 +3,29 @@ import random
# The voting strategy. Validators see what every other validator votes,
# and return their vote.
#
# Votes are probabilities with 0 < p < 1 (see
# Votes are log odds, ie. ln(p / (1-p))
#
# Remember, 0 and 1 are not probabilities!
# 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 default_vote(scheduled_time, received_time, now, **kwargs):
if received_time is None:
time_delta = now - scheduled_time
my_opinion_prob = 1 if time_delta < kwargs["blktime"] * 4 else 4.0 / (4 + time_delta * 1.0 / kwargs["blktime"])
return 0 if random.random() < my_opinion_prob else -1
else:
time_delta = received_time * 0.9 + now * 0.1 - scheduled_time
my_opinion_prob = 1 if abs(time_delta) < kwargs["blktime"] * 4 else 4.0 / (4 + abs(time_delta) * 1.0 / kwargs["blktime"])
return 1 if random.random() < my_opinion_prob else -1
def vote(probs):
if len(probs) == 0:
return 0.5
return 0
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)
if probs[len(probs)/3] >= 1:
return probs[len(probs)/3] + 1
elif probs[len(probs)*2/3] <= -1:
return probs[len(probs)*2/3] - 1
else:
return probs[len(probs)/2]