Added comments, and made log-odds betting work
This commit is contained in:
parent
5bbca50547
commit
4ae4c13952
|
@ -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.
|
|
@ -1,14 +1,22 @@
|
||||||
import copy, random, hashlib
|
import copy, random, hashlib
|
||||||
from distributions import normal_distribution
|
from distributions import normal_distribution
|
||||||
import networksim
|
import networksim
|
||||||
from voting_strategy import vote
|
from voting_strategy import vote, default_vote
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
# Number of validators
|
||||||
NUM_VALIDATORS = 20
|
NUM_VALIDATORS = 20
|
||||||
|
# Block time
|
||||||
BLKTIME = 40
|
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
|
NETSPLITS = 2
|
||||||
|
# Check the equality of finalized states
|
||||||
CHECK_INTEGRITY = True
|
CHECK_INTEGRITY = True
|
||||||
|
# The genesis state root
|
||||||
GENESIS_STATE = 0
|
GENESIS_STATE = 0
|
||||||
|
|
||||||
logging_level = 0
|
logging_level = 0
|
||||||
|
@ -19,10 +27,14 @@ def log(s, lvl):
|
||||||
print(s)
|
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():
|
class Signature():
|
||||||
def __init__(self, signer, probs, finalized_state, sign_from):
|
def __init__(self, signer, probs, finalized_state, sign_from):
|
||||||
|
# The ID of the signer
|
||||||
self.signer = signer
|
self.signer = signer
|
||||||
# List of maps from block hash to probability
|
# List of probability bets, expressed in log odds
|
||||||
self.probs = probs
|
self.probs = probs
|
||||||
# Hash of the signature (for db storage purposes)
|
# Hash of the signature (for db storage purposes)
|
||||||
self.hash = random.randrange(10**14)
|
self.hash = random.randrange(10**14)
|
||||||
|
@ -35,6 +47,7 @@ class Signature():
|
||||||
return self.sign_from + len(self.probs)
|
return self.sign_from + len(self.probs)
|
||||||
|
|
||||||
|
|
||||||
|
# Right now, a block simply specifies a proposer and a height.
|
||||||
class Block():
|
class Block():
|
||||||
def __init__(self, maker, height):
|
def __init__(self, maker, height):
|
||||||
# The producer of the block
|
# The producer of the block
|
||||||
|
@ -45,6 +58,7 @@ class Block():
|
||||||
self.hash = random.randrange(10**20) + 10**21 + 10**23 * self.height
|
self.hash = random.randrange(10**20) + 10**21 + 10**23 * self.height
|
||||||
|
|
||||||
|
|
||||||
|
# A request to receive a block at a particular height
|
||||||
class BlockRequest():
|
class BlockRequest():
|
||||||
def __init__(self, sender, height):
|
def __init__(self, sender, height):
|
||||||
self.sender = sender
|
self.sender = sender
|
||||||
|
@ -52,6 +66,8 @@ class BlockRequest():
|
||||||
self.hash = random.randrange(10**14)
|
self.hash = random.randrange(10**14)
|
||||||
|
|
||||||
|
|
||||||
|
# Toy state transition function (in production, do sequential
|
||||||
|
# apply_transaction here)
|
||||||
def state_transition(state, block):
|
def state_transition(state, block):
|
||||||
return state if block is None else (state ** 3 + block.hash ** 5) % 10**40
|
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
|
# A validator
|
||||||
class Validator():
|
class Validator():
|
||||||
def __init__(self, pos, network):
|
def __init__(self, pos, network):
|
||||||
# Map from height to {node_id: latest_opinion}
|
# Map from height to {node_id: latest_bet}
|
||||||
self.received_signatures = []
|
self.received_signatures = []
|
||||||
# List of received blocks
|
# List of received blocks
|
||||||
self.received_blocks = []
|
self.received_blocks = []
|
||||||
|
@ -67,16 +83,14 @@ class Validator():
|
||||||
self.probs = []
|
self.probs = []
|
||||||
# All objects that this validator has received; basically a database
|
# All objects that this validator has received; basically a database
|
||||||
self.received_objects = {}
|
self.received_objects = {}
|
||||||
|
# Time when the object was received
|
||||||
self.time_received = {}
|
self.time_received = {}
|
||||||
# The validator's ID, and its position in the queue
|
# The validator's ID, and its position in the queue
|
||||||
self.pos = self.id = pos
|
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)()
|
self.time_offset = normal_distribution(0, 100)()
|
||||||
# The highest height that this validator has seen
|
# The highest height that this validator has seen
|
||||||
self.max_height = 0
|
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
|
# The validator's hash chain
|
||||||
self.finalized_hashes = []
|
self.finalized_hashes = []
|
||||||
# Finalized states
|
# Finalized states
|
||||||
|
@ -87,50 +101,55 @@ class Validator():
|
||||||
self.network = network
|
self.network = network
|
||||||
# Last time signed
|
# Last time signed
|
||||||
self.last_time_signed = 0
|
self.last_time_signed = 0
|
||||||
# Next neight to mine
|
# Next height to mine
|
||||||
self.next_height = self.pos
|
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):
|
def get_time(self):
|
||||||
return self.network.time + self.time_offset
|
return self.network.time + self.time_offset
|
||||||
|
|
||||||
|
# Broadcast an object to the network
|
||||||
def broadcast(self, obj):
|
def broadcast(self, obj):
|
||||||
self.network.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
|
# Initialize the probability array, the core of the signature
|
||||||
best_guesses = [None] * len(self.received_blocks)
|
best_guesses = [None] * len(self.received_blocks)
|
||||||
sign_from = max(0, self.max_finalized_height - 30)
|
sign_from = max(0, self.max_finalized_height - 30)
|
||||||
for i, b in list(enumerate(self.received_blocks))[sign_from:]:
|
for i, b in list(enumerate(self.received_blocks))[sign_from:]:
|
||||||
if self.received_blocks[i] is None:
|
# Compute this validator's own initial vote based on when the block
|
||||||
time_delta = self.get_time() - BLKTIME * i
|
# was received, compared to what time the block should have arrived
|
||||||
my_opinion = 0.35 / (1 + max(0, time_delta) * 0.2 / BLKTIME) + 0.14
|
received_time = self.time_received[b.hash] if b is not None else None
|
||||||
else:
|
my_opinion = default_vote(BLKTIME * i, received_time, self.get_time(), blktime=BLKTIME)
|
||||||
time_delta = self.time_received[b.hash] - BLKTIME * i
|
# Get others' bets on this height
|
||||||
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
|
|
||||||
votes = self.received_signatures[i].values() if i < len(self.received_signatures) else []
|
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))
|
votes += [my_opinion] * (NUM_VALIDATORS - len(votes))
|
||||||
vote_from_signatures = vote(votes)
|
vote_from_signatures = int(vote(votes))
|
||||||
bg = min(vote_from_signatures, 1 if self.received_blocks[i] is not None else my_opinion)
|
# Add the bet to the list
|
||||||
# In case we fall into an equilibrium trap at 0.5, eventually force divergence
|
bg = min(vote_from_signatures, 10 if self.received_blocks[i] is not None else my_opinion)
|
||||||
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
|
|
||||||
best_guesses[i] = bg
|
best_guesses[i] = bg
|
||||||
|
# Request a block if we should have it, and should have had it for
|
||||||
if vote_from_signatures > 0.95 and self.received_blocks[i] is None:
|
# a long time, but don't
|
||||||
|
if vote_from_signatures > 3 and self.received_blocks[i] is None:
|
||||||
self.broadcast(BlockRequest(self.id, i))
|
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:
|
while len(self.finalized_hashes) <= i:
|
||||||
self.finalized_hashes.append(None)
|
self.finalized_hashes.append(None)
|
||||||
self.finalized_hashes[i] = self.received_blocks[i].hash
|
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:
|
while len(self.finalized_hashes) <= i:
|
||||||
self.finalized_hashes.append(None)
|
self.finalized_hashes.append(None)
|
||||||
self.finalized_hashes[i] = False
|
self.finalized_hashes[i] = False
|
||||||
|
# Add to the list of finalized states
|
||||||
while self.max_finalized_height < len(self.finalized_hashes) - 1 \
|
while self.max_finalized_height < len(self.finalized_hashes) - 1 \
|
||||||
and self.finalized_hashes[self.max_finalized_height + 1] is not None:
|
and self.finalized_hashes[self.max_finalized_height + 1] is not None:
|
||||||
self.max_finalized_height += 1
|
self.max_finalized_height += 1
|
||||||
|
@ -155,8 +174,8 @@ class Validator():
|
||||||
self.received_blocks.append(None)
|
self.received_blocks.append(None)
|
||||||
self.received_blocks[obj.height] = obj
|
self.received_blocks[obj.height] = obj
|
||||||
self.time_received[obj.hash] = self.get_time()
|
self.time_received[obj.hash] = self.get_time()
|
||||||
# If we have not yet produced a signature at this height, do so now
|
# Upon receiving a new block, make a new signature
|
||||||
s = self.sign(obj)
|
s = self.sign()
|
||||||
self.network.broadcast(self, s)
|
self.network.broadcast(self, s)
|
||||||
self.on_receive(s)
|
self.on_receive(s)
|
||||||
self.network.broadcast(self, obj)
|
self.network.broadcast(self, obj)
|
||||||
|
@ -207,9 +226,6 @@ def who_heard_of(h, n):
|
||||||
return o
|
return o
|
||||||
|
|
||||||
|
|
||||||
ALPHA = '0123456789'
|
|
||||||
|
|
||||||
|
|
||||||
def get_opinions(n):
|
def get_opinions(n):
|
||||||
o = []
|
o = []
|
||||||
maxheight = 0
|
maxheight = 0
|
||||||
|
@ -221,12 +237,12 @@ def get_opinions(n):
|
||||||
for x in n.agents:
|
for x in n.agents:
|
||||||
if len(x.probs) <= h:
|
if len(x.probs) <= h:
|
||||||
p += '_'
|
p += '_'
|
||||||
elif x.probs[h] < 0.0001:
|
elif x.probs[h] <= -10:
|
||||||
p += '-'
|
p += '-'
|
||||||
elif x.probs[h] > 0.9999:
|
elif x.probs[h] >= 10:
|
||||||
p += '+'
|
p += '+'
|
||||||
else:
|
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'
|
q += 'n' if len(x.received_blocks) <= h or x.received_blocks[h] is None else 'y'
|
||||||
o.append((h, p, q))
|
o.append((h, p, q))
|
||||||
return o
|
return o
|
||||||
|
@ -252,7 +268,7 @@ def calibrate(finalized_hashes):
|
||||||
continue
|
continue
|
||||||
actual_result = 1 if finalized_hashes[i + s.sign_from] else 0
|
actual_result = 1 if finalized_hashes[i + s.sign_from] else 0
|
||||||
index = 0
|
index = 0
|
||||||
while prob > thresholds[index + 1]:
|
while index + 2 < len(thresholds) and prob > thresholds[index + 1]:
|
||||||
index += 1
|
index += 1
|
||||||
signed[index] += 1
|
signed[index] += 1
|
||||||
if actual_result == 1:
|
if actual_result == 1:
|
||||||
|
|
|
@ -4,8 +4,6 @@ import random, sys
|
||||||
def normal_distribution(mean, standev):
|
def normal_distribution(mean, standev):
|
||||||
def f():
|
def f():
|
||||||
return int(random.normalvariate(mean, standev))
|
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
|
return f
|
||||||
|
|
||||||
|
|
|
@ -2,4 +2,4 @@ import casper
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
casper.logging_level = int(sys.argv[1]) if len(sys.argv) > 1 else 0
|
casper.logging_level = int(sys.argv[1]) if len(sys.argv) > 1 else 0
|
||||||
casper.run(100000)
|
casper.run(50000)
|
||||||
|
|
|
@ -3,22 +3,29 @@ import random
|
||||||
# The voting strategy. Validators see what every other validator votes,
|
# The voting strategy. Validators see what every other validator votes,
|
||||||
# and return their vote.
|
# 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/ !)
|
# http://lesswrong.com/lw/mp/0_and_1_are_not_probabilities/ !)
|
||||||
|
|
||||||
def vote_transform(p):
|
def default_vote(scheduled_time, received_time, now, **kwargs):
|
||||||
return (abs(2 * p - 1) ** 0.333 * (1 if p > 0.5 else -1) + 1) / 2
|
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):
|
def vote(probs):
|
||||||
if len(probs) == 0:
|
if len(probs) == 0:
|
||||||
return 0.5
|
return 0
|
||||||
probs = sorted(probs)
|
probs = sorted(probs)
|
||||||
score = (probs[len(probs)/2] + probs[-len(probs)/2]) * 0.5
|
if probs[len(probs)/3] >= 1:
|
||||||
if score > 0.9:
|
return probs[len(probs)/3] + 1
|
||||||
score2 = probs[len(probs)/3]
|
elif probs[len(probs)*2/3] <= -1:
|
||||||
score = min(score, max(score2, 0.9))
|
return probs[len(probs)*2/3] - 1
|
||||||
elif score < 0.1:
|
else:
|
||||||
score2 = probs[len(probs)*2/3]
|
return probs[len(probs)/2]
|
||||||
score = max(score, min(score2, 0.1))
|
|
||||||
return vote_transform(score)
|
|
||||||
|
|
Loading…
Reference in New Issue