From 2d1dd0227bb0a82098332620afa6f139fd2e899a Mon Sep 17 00:00:00 2001 From: vub Date: Fri, 30 Dec 2016 14:41:50 -0500 Subject: [PATCH] Updated selfish validating sim for casper --- casper_sm/test.py | 246 +++++++++++++++++++++++++-------------- zksnark/bn128_pairing.py | 10 +- 2 files changed, 165 insertions(+), 91 deletions(-) diff --git a/casper_sm/test.py b/casper_sm/test.py index cf37008..eb53a57 100644 --- a/casper_sm/test.py +++ b/casper_sm/test.py @@ -1,97 +1,171 @@ +# The purpose of this script is to test selfish-mining-like strategies +# in the randao-based single-chain Casper. import random -CHECK_DEPTH = 4 -BLOCK_TIME = 1 -SKIP_TIME = 1 -FORK_PUNISHMENT_COEFF = 1.5 +# Penalty for mining a dunkle +DUNKLE_PENALTY = 1 +# Penalty to a main-chain block which has a dunkle as a sister +DUNKLE_SISTER_PENALTY = 0.8 -attacker_share = 0.45 +# Attacker stake power (out of 100). Try setting this value to any +# amount, even values above 50! +attacker_share = 40 + +# A simulated Casper randao +def randao_successor(parent, index): + return (((parent ^ 53) + index) ** 3) % (10**20 - 11) + +# We categorize "scenarios" by seeing how far ahead we get a chain +# of 0-skips from each "path"; if the path itself isn't clear then +# return zero +heads_of_interest = ['', '1'] +# Only scan down this far +scandepth = 4 +# eg. (0, 2) means "going straight from the current randao, you +# can descend zero, but if you make a one-skip block, from there +# you get two 0-skips in a row" +scenarios = [None, (0, 1), (0, 2), (0, 3), (0, 4)] +# For each scenario, this is the corresponding "path" to go down +paths = ['', '10', '100', '100', '1000'] + +# Determine the scenario ID (zero is catch-all) from a chain +def extract_scenario(chain): + chain = chain.copy() + o = [] + for h in heads_of_interest: + # Make sure that we can descend down "the path" + succeed = True + for step in h: + if not chain.can_i_extend(int(step)): + succeed = False + break + chain.extend_me(int(step)) + if not succeed: + o.append(0) + else: + # See how far down we can go + i = 0 + while chain.can_i_extend(0) and i < scandepth: + i += 1 + chain.extend_me(0) + o.append(i) + if tuple(o) in scenarios: + return scenarios.index(tuple(o)) + else: + return 0 + +# Class to represent simulated chains +class Chain(): + def __init__(self, randao=0, time=0, length=0, me=0, them=0): + self.randao = randao + self.time = time + self.length = length + self.me = me + self.them = them + + def copy(self): + return Chain(self.randao, self.time, self.length, self.me, self.them) + + def can_i_extend(self, skips): + return randao_successor(self.randao, skips) % 100 < attacker_share + + def can_they_extend(self, skips): + return randao_successor(self.randao, skips) % 100 >= attacker_share + + def extend_me(self, skips): + new_randao = randao_successor(self.randao, skips) + assert new_randao % 100 < attacker_share + self.randao = new_randao + self.time += skips + self.length += 1 + self.me += 1 + + def extend_them(self, skips): + new_randao = randao_successor(self.randao, skips) + assert new_randao % 100 >= attacker_share + self.randao = new_randao + self.time += skips + self.length += 1 + self.them += 1 + + def add_my_dunkles(self, n): + self.me -= n * DUNKLE_PENALTY + self.them -= n * DUNKLE_SISTER_PENALTY + + def add_their_dunkles(self, n): + self.them -= n * DUNKLE_PENALTY + self.me -= n * DUNKLE_SISTER_PENALTY -# randao_results[1101] = 1 -> if validator path is 1, 0, 1 then -# a colluding node will make the next block. -# randao_results[10011] = 0 -> if validator path is 1, 1, 0, 0 -# then a non-colluding node will make the next block. -randao_results = [None, None] -for i in range(2, 2**CHECK_DEPTH): - randao_results.append(1 if random.random() < attacker_share else 0) -def update_randao(): - for i in range(2, len(randao_results) / 2): - randao_results[i] = randao_results[i * 2] - for i in range(len(randao_results) / 2, len(randao_results)): - randao_results[i] = 1 if random.random() < attacker_share else 0 +for strat_id in range(2**len(scenarios)): + # Strategy map: scenario to 0 = publish, 1 = selfish-validate + strategy = [0] + [((strat_id // 2**i) % 2) for i in range(1, len(scenarios))] + # 1 = once we go through the selfish-validating "path", reveal it instantly + # 0 = don't reveal until the "main chain" looks like it's close to catching up + insta_reveal = strat_id % 2 -# Strategy map: number of blocks belonging to colluding node in -# the 0, 0, 0... chain -> 0 = publish, 1 = wait and compete, 2 = don't -# publish + print 'Testing strategy: %r, insta_reveal: %d', (strategy, insta_reveal) -for strat_id in range(2**CHECK_DEPTH): - strategy = [0] - k = strat_id - for _ in range(CHECK_DEPTH): - strategy.append(k % 2) - k /= 2 - - my_revenue = 0. - their_revenue = 0. - - print 'Testing strategy:', strategy + pubchain = Chain(randao=random.randrange(10**20)) time = 0 - while time < 200000: - child_0 = randao_results[2] - child_1 = randao_results[3] - situation = 0 - q = 2 - while situation < len(strategy) - 2 and randao_results[q] == 1: - situation += 1 - q *= 2 - if child_0 == 0: - their_revenue += 1 - time += BLOCK_TIME - elif strategy[situation] == 0: - my_revenue += 1 - time += BLOCK_TIME - elif strategy[situation] == 2: - their_revenue += 1 - time += BLOCK_TIME + SKIP_TIME - else: - assert situation >= 1 - my_score = situation - my_time = situation + 1 - their_time = 0 - their_score = 0 - update_randao() - while their_time <= situation: - if randao_results[2] == 0: - wait_time = BLOCK_TIME - elif randao_results[3] == 0: - wait_time = BLOCK_TIME + SKIP_TIME - else: - wait_time = BLOCK_TIME + SKIP_TIME * 2 - while random.random() < attacker_share: - wait_time += SKIP_TIME - their_score += 1 - their_time += wait_time - update_randao() - time += max(my_time, their_time) - while my_score > their_score + 1: - if random.random() < attacker_share: - my_score += 1 - if random.random() > attacker_share: - their_score += 1 - time += BLOCK_TIME - if my_score > their_score or (my_score == their_score and random.random() < 0.5): - my_revenue += my_score - FORK_PUNISHMENT_COEFF - their_revenue -= their_score * 0.5 + while time < 100000: + # You honestly get a block + if pubchain.can_i_extend(0): + pubchain.extend_me(0) + time += 1 + continue + e = extract_scenario(pubchain) + if strategy[e] == 0: + # You honestly let them get a block + pubchain.extend_them(0) + time += 1 + continue + # Build up the secret chain based on the detected path + # print 'Selfish mining along path %r' % paths[e] + old_me = pubchain.me + old_them = pubchain.them + old_time = time + secchain = pubchain.copy() + sectime = time + for skipz in paths[e]: + skips = int(skipz) + secchain.extend_me(skips) + sectime += skips + 1 + # Public chain builds itself up in the meantime + pubwait = 0 + while time < sectime: + if pubchain.can_they_extend(pubwait): + pubchain.extend_them(pubwait) + pubwait = 0 else: - their_revenue += their_score - FORK_PUNISHMENT_COEFF - my_revenue -= my_score * 0.5 - update_randao() - - if my_revenue * 1.0 / (my_revenue + their_revenue) > attacker_share - 0.01: - print 'My revenue:', my_revenue / time - print 'Their revenue:', their_revenue / time - print 'My share:', my_revenue / (my_revenue + their_revenue) - print 'Griefing factor:', (their_revenue / time / (1 - attacker_share) - 1) / (my_revenue / time / attacker_share - 1) + pubwait += 1 + time += 1 + secwait = 0 + # If the two chains have equal length, or if the secret chain is more than 1 longer, they duel + while (secchain.length > pubchain.length + 1 or secchain.length == pubchain.length) and time < 100000 and not insta_reveal: + if pubchain.can_they_extend(pubwait): + pubchain.extend_them(pubwait) + pubwait = 0 + else: + pubwait += 1 + if secchain.can_i_extend(secwait): + secchain.extend_me(secwait) + secwait = 0 + else: + secwait += 1 + time += 1 + # Secret chain is longer, takes over public chain, public chain goes in as dunkles + if secchain.length > pubchain.length: + pubchain_blocks = pubchain.them - old_them + assert pubchain.me == old_me + pubchain = secchain + pubchain.add_their_dunkles(pubchain_blocks) + # Public chain is longer, miner deletes secret chain so no dunkling + else: + pass + # print 'Score deltas: me %.2f them %.2f, time delta %d' % (pubchain.me - old_me, pubchain.them - old_them, time - old_time) + + gf = (pubchain.them - 100000. * (100 - attacker_share) / 100) / (pubchain.me - 100000 * attacker_share / 100) + print 'My revenue: %d, their revenue: %d, griefing factor %.2f' % (pubchain.me, pubchain.them, gf) diff --git a/zksnark/bn128_pairing.py b/zksnark/bn128_pairing.py index a66e897..0927232 100644 --- a/zksnark/bn128_pairing.py +++ b/zksnark/bn128_pairing.py @@ -53,15 +53,15 @@ def miller_loop(Q, P): if ate_loop_count & (2**i): f = f * linefunc(R, Q, P) R = add(R, Q) - assert R == multiply(Q, ate_loop_count) + # assert R == multiply(Q, ate_loop_count) Q1 = (Q[0] ** field_modulus, Q[1] ** field_modulus) - assert is_on_curve(Q1, b12) - nQ2 = (Q[0] ** (field_modulus ** 2), -Q[1] ** (field_modulus ** 2)) - assert is_on_curve(nQ2, b12) + # assert is_on_curve(Q1, b12) + nQ2 = (Q1[0] ** field_modulus, -Q1[1] ** field_modulus) + # assert is_on_curve(nQ2, b12) f = f * linefunc(R, Q1, P) R = add(R, Q1) f = f * linefunc(R, nQ2, P) - R = add(R, nQ2) + # R = add(R, nQ2) This line is in many specifications but it technically does nothing return f ** ((field_modulus ** 12 - 1) / curve_order) # Pairing computation