Updated selfish validating sim for casper
This commit is contained in:
parent
803be67d0e
commit
2d1dd0227b
|
@ -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
|
import random
|
||||||
|
|
||||||
CHECK_DEPTH = 4
|
# Penalty for mining a dunkle
|
||||||
BLOCK_TIME = 1
|
DUNKLE_PENALTY = 1
|
||||||
SKIP_TIME = 1
|
# Penalty to a main-chain block which has a dunkle as a sister
|
||||||
FORK_PUNISHMENT_COEFF = 1.5
|
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 strat_id in range(2**len(scenarios)):
|
||||||
for i in range(2, len(randao_results) / 2):
|
# Strategy map: scenario to 0 = publish, 1 = selfish-validate
|
||||||
randao_results[i] = randao_results[i * 2]
|
strategy = [0] + [((strat_id // 2**i) % 2) for i in range(1, len(scenarios))]
|
||||||
for i in range(len(randao_results) / 2, len(randao_results)):
|
# 1 = once we go through the selfish-validating "path", reveal it instantly
|
||||||
randao_results[i] = 1 if random.random() < attacker_share else 0
|
# 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
|
print 'Testing strategy: %r, insta_reveal: %d', (strategy, insta_reveal)
|
||||||
# the 0, 0, 0... chain -> 0 = publish, 1 = wait and compete, 2 = don't
|
|
||||||
# publish
|
|
||||||
|
|
||||||
for strat_id in range(2**CHECK_DEPTH):
|
pubchain = Chain(randao=random.randrange(10**20))
|
||||||
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
|
|
||||||
|
|
||||||
time = 0
|
time = 0
|
||||||
while time < 200000:
|
while time < 100000:
|
||||||
child_0 = randao_results[2]
|
# You honestly get a block
|
||||||
child_1 = randao_results[3]
|
if pubchain.can_i_extend(0):
|
||||||
situation = 0
|
pubchain.extend_me(0)
|
||||||
q = 2
|
time += 1
|
||||||
while situation < len(strategy) - 2 and randao_results[q] == 1:
|
continue
|
||||||
situation += 1
|
e = extract_scenario(pubchain)
|
||||||
q *= 2
|
if strategy[e] == 0:
|
||||||
if child_0 == 0:
|
# You honestly let them get a block
|
||||||
their_revenue += 1
|
pubchain.extend_them(0)
|
||||||
time += BLOCK_TIME
|
time += 1
|
||||||
elif strategy[situation] == 0:
|
continue
|
||||||
my_revenue += 1
|
# Build up the secret chain based on the detected path
|
||||||
time += BLOCK_TIME
|
# print 'Selfish mining along path %r' % paths[e]
|
||||||
elif strategy[situation] == 2:
|
old_me = pubchain.me
|
||||||
their_revenue += 1
|
old_them = pubchain.them
|
||||||
time += BLOCK_TIME + SKIP_TIME
|
old_time = time
|
||||||
else:
|
secchain = pubchain.copy()
|
||||||
assert situation >= 1
|
sectime = time
|
||||||
my_score = situation
|
for skipz in paths[e]:
|
||||||
my_time = situation + 1
|
skips = int(skipz)
|
||||||
their_time = 0
|
secchain.extend_me(skips)
|
||||||
their_score = 0
|
sectime += skips + 1
|
||||||
update_randao()
|
# Public chain builds itself up in the meantime
|
||||||
while their_time <= situation:
|
pubwait = 0
|
||||||
if randao_results[2] == 0:
|
while time < sectime:
|
||||||
wait_time = BLOCK_TIME
|
if pubchain.can_they_extend(pubwait):
|
||||||
elif randao_results[3] == 0:
|
pubchain.extend_them(pubwait)
|
||||||
wait_time = BLOCK_TIME + SKIP_TIME
|
pubwait = 0
|
||||||
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
|
|
||||||
else:
|
else:
|
||||||
their_revenue += their_score - FORK_PUNISHMENT_COEFF
|
pubwait += 1
|
||||||
my_revenue -= my_score * 0.5
|
time += 1
|
||||||
update_randao()
|
secwait = 0
|
||||||
|
# If the two chains have equal length, or if the secret chain is more than 1 longer, they duel
|
||||||
if my_revenue * 1.0 / (my_revenue + their_revenue) > attacker_share - 0.01:
|
while (secchain.length > pubchain.length + 1 or secchain.length == pubchain.length) and time < 100000 and not insta_reveal:
|
||||||
print 'My revenue:', my_revenue / time
|
if pubchain.can_they_extend(pubwait):
|
||||||
print 'Their revenue:', their_revenue / time
|
pubchain.extend_them(pubwait)
|
||||||
print 'My share:', my_revenue / (my_revenue + their_revenue)
|
pubwait = 0
|
||||||
print 'Griefing factor:', (their_revenue / time / (1 - attacker_share) - 1) / (my_revenue / time / attacker_share - 1)
|
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)
|
||||||
|
|
|
@ -53,15 +53,15 @@ def miller_loop(Q, P):
|
||||||
if ate_loop_count & (2**i):
|
if ate_loop_count & (2**i):
|
||||||
f = f * linefunc(R, Q, P)
|
f = f * linefunc(R, Q, P)
|
||||||
R = add(R, Q)
|
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)
|
Q1 = (Q[0] ** field_modulus, Q[1] ** field_modulus)
|
||||||
assert is_on_curve(Q1, b12)
|
# assert is_on_curve(Q1, b12)
|
||||||
nQ2 = (Q[0] ** (field_modulus ** 2), -Q[1] ** (field_modulus ** 2))
|
nQ2 = (Q1[0] ** field_modulus, -Q1[1] ** field_modulus)
|
||||||
assert is_on_curve(nQ2, b12)
|
# assert is_on_curve(nQ2, b12)
|
||||||
f = f * linefunc(R, Q1, P)
|
f = f * linefunc(R, Q1, P)
|
||||||
R = add(R, Q1)
|
R = add(R, Q1)
|
||||||
f = f * linefunc(R, nQ2, P)
|
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)
|
return f ** ((field_modulus ** 12 - 1) / curve_order)
|
||||||
|
|
||||||
# Pairing computation
|
# Pairing computation
|
||||||
|
|
Loading…
Reference in New Issue