From 4ae4c13952a1648ed4784206211dc939eb3e7e6c Mon Sep 17 00:00:00 2001 From: vub Date: Sat, 12 Sep 2015 17:46:40 -0400 Subject: [PATCH] Added comments, and made log-odds betting work --- casper/README.md | 8 ++++ casper/casper.py | 94 +++++++++++++++++++++++---------------- casper/distributions.py | 2 - casper/run.py | 2 +- casper/voting_strategy.py | 31 ++++++++----- 5 files changed, 83 insertions(+), 54 deletions(-) create mode 100644 casper/README.md diff --git a/casper/README.md b/casper/README.md new file mode 100644 index 0000000..c83343b --- /dev/null +++ b/casper/README.md @@ -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. diff --git a/casper/casper.py b/casper/casper.py index 0cf4600..48f68b4 100644 --- a/casper/casper.py +++ b/casper/casper.py @@ -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: diff --git a/casper/distributions.py b/casper/distributions.py index a37ce72..6412e3d 100644 --- a/casper/distributions.py +++ b/casper/distributions.py @@ -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 diff --git a/casper/run.py b/casper/run.py index ccff7fa..37906f7 100644 --- a/casper/run.py +++ b/casper/run.py @@ -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) diff --git a/casper/voting_strategy.py b/casper/voting_strategy.py index 4c38def..ba8be3b 100644 --- a/casper/voting_strategy.py +++ b/casper/voting_strategy.py @@ -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]