diff --git a/casper/casper.py b/casper/casper.py index 4f04209..f89c054 100644 --- a/casper/casper.py +++ b/casper/casper.py @@ -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) diff --git a/casper/distributions.py b/casper/distributions.py index cdd3833..a37ce72 100644 --- a/casper/distributions.py +++ b/casper/distributions.py @@ -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 diff --git a/casper/networksim.py b/casper/networksim.py index 18398b2..301cd65 100644 --- a/casper/networksim.py +++ b/casper/networksim.py @@ -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: diff --git a/casper/voting_strategy.py b/casper/voting_strategy.py index 297969c..4c38def 100644 --- a/casper/voting_strategy.py +++ b/casper/voting_strategy.py @@ -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)