mirror of
https://github.com/status-im/research.git
synced 2025-01-27 07:15:14 +00:00
Added comments, and made log-odds betting work
This commit is contained in:
parent
5bbca50547
commit
4ae4c13952
8
casper/README.md
Normal file
8
casper/README.md
Normal 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.
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
|
Loading…
x
Reference in New Issue
Block a user