Moved stuff from econ modeling to research

This commit is contained in:
vub 2016-08-15 08:04:03 -04:00
parent 1e9c9e7725
commit 768e21e523
5 changed files with 511 additions and 0 deletions

207
casper3/casper.py Normal file
View File

@ -0,0 +1,207 @@
from ethereum.casper_utils import RandaoManager, get_skips_and_block_making_time, \
generate_validation_code, call_casper, make_block, check_skips, get_timestamp, \
get_casper_ct, validator_sizes
from ethereum.utils import sha3, hash32, privtoaddr, ecsign, zpad, encode_int32, \
big_endian_to_int
from ethereum.block import Block
from ethereum.transactions import Transaction
from ethereum.chain import Chain
import networksim
import rlp
import random
CHECK_FOR_UNCLES_BACK = 8
global_block_counter = 0
casper_ct = get_casper_ct()
class ChildRequest(rlp.Serializable):
fields = [
('prevhash', hash32)
]
def __init__(self, prevhash):
self.prevhash = prevhash
@property
def hash(self):
return sha3(self.prevhash + '::salt:jhfqou213nry138o2r124124')
ids = []
class Validator():
def __init__(self, genesis, key, network, env, time_offset=5):
# Create a chain object
self.chain = Chain(genesis, env=env)
# Use the validator's time as the chain's time
self.chain.time = lambda: self.get_timestamp()
# My private key
self.key = key
# My address
self.address = privtoaddr(key)
# My randao
self.randao = RandaoManager(sha3(self.key))
# Pointer to the test p2p network
self.network = network
# Record of objects already received and processed
self.received_objects = {}
# The minimum eligible timestamp given a particular number of skips
self.next_skip_count = 0
self.next_skip_timestamp = 0
# This validator's indices in the state
self.indices = None
# Is this validator active?
self.active = False
# Code that verifies signatures from this validator
self.validation_code = generate_validation_code(privtoaddr(key))
# Parents that this validator has already built a block on
self.used_parents = {}
# This validator's clock offset (for testing purposes)
self.time_offset = random.randrange(time_offset) - (time_offset // 2)
# Determine the epoch length
self.epoch_length = self.call_casper('getEpochLength')
# Give this validator a unique ID
self.id = len(ids)
ids.append(self.id)
self.find_my_indices()
self.cached_head = self.chain.head_hash
def call_casper(self, fun, args=[]):
return call_casper(self.chain.state, fun, args)
def find_my_indices(self):
epoch = self.chain.state.block_number // self.epoch_length
print 'Finding indices for epoch %d' % epoch, self.call_casper('getEpoch')
for i in range(len(validator_sizes)):
valcount = self.call_casper('getHistoricalValidatorCount', [epoch, i])
print i, valcount, self.call_casper('getHistoricalValidatorCount', [0, i])
for j in range(valcount):
valcode = self.call_casper('getValidationCode', [i, j])
print (valcode, self.validation_code)
if valcode == self.validation_code:
self.indices = i, j
start = self.call_casper('getStartEpoch', [i, j])
end = self.call_casper('getEndEpoch', [i, j])
if start <= epoch < end:
self.active = True
self.next_skip_count = 0
self.next_skip_timestamp = get_timestamp(self.chain, self.next_skip_count)
print 'In current validator set at (%d, %d)' % (i, j)
return
else:
self.indices = None
self.active = False
self.next_skip_count, self.next_skip_timestamp = 0, 0
print 'Registered at (%d, %d) but not in current set' % (i, j)
return
self.indices = None
self.active = False
self.next_skip_count, self.next_skip_timestamp = 0, 0
print 'Not in current validator set'
def get_uncles(self):
anc = self.chain.get_block(self.chain.get_blockhash_by_number(self.chain.state.block_number - CHECK_FOR_UNCLES_BACK))
if anc:
descendants = self.chain.get_descendants(anc)
else:
descendants = self.chain.get_descendants(self.chain.db.get('GENESIS_HASH'))
potential_uncles = [x for x in descendants if x not in self.chain and isinstance(x, Block)]
uncles = [x.header for x in potential_uncles if not call_casper(self.chain.state, 'isDunkleIncluded', [x.header.hash])]
return uncles
def get_timestamp(self):
return int(self.network.time * 0.01) + self.time_offset
def on_receive(self, obj):
if isinstance(obj, list):
for _obj in obj:
self.on_receive(_obj)
return
if obj.hash in self.received_objects:
return
if isinstance(obj, Block):
print 'Receiving block', obj
assert obj.hash not in self.chain
block_success = self.chain.add_block(obj)
self.network.broadcast(self, obj)
self.network.broadcast(self, ChildRequest(obj.header.hash))
self.update_head()
elif isinstance(obj, Transaction):
if self.chain.add_transaction(obj):
self.network.broadcast(self, obj)
self.received_objects[obj.hash] = True
for x in self.chain.get_chain():
assert x.hash in self.received_objects
def tick(self):
# Try to create a block
# Conditions:
# (i) you are an active validator,
# (ii) you have not yet made a block with this parent
if self.indices and self.chain.head_hash not in self.used_parents:
t = self.get_timestamp()
# Is it early enough to create the block?
if t >= self.next_skip_timestamp and (not self.chain.head or t > self.chain.head.header.timestamp):
# Wrong validator; in this case, just wait for the next skip count
if not check_skips(self.chain, self.indices, self.next_skip_count):
self.next_skip_count += 1
self.next_skip_timestamp = get_timestamp(self.chain, self.next_skip_count)
# print 'Incrementing proposed timestamp for block %d to %d' % \
# (self.chain.head.header.number + 1 if self.chain.head else 0, self.next_skip_timestamp)
return
self.used_parents[self.chain.head_hash] = True
# Simulated 15% chance of validator failure to make a block
if random.random() > 0.999:
print 'Simulating validator failure, block %d not created' % (self.chain.head.header.number + 1 if self.chain.head else 0)
return
# Make the block, make sure it's valid
pre_dunkle_count = call_casper(self.chain.state, 'getTotalDunklesIncluded', [])
dunkle_txs = []
for i, u in enumerate(self.get_uncles()[:4]):
start_nonce = self.chain.state.get_nonce(self.address)
txdata = casper_ct.encode('includeDunkle', [rlp.encode(u)])
dunkle_txs.append(Transaction(start_nonce + i, 0, 650000, self.chain.config['CASPER_ADDR'], 0, txdata).sign(self.key))
for dtx in dunkle_txs[::-1]:
self.chain.add_transaction(dtx, force=True)
blk = make_block(self.chain, self.key, self.randao, self.indices, self.next_skip_count)
global global_block_counter
global_block_counter += 1
for dtx in dunkle_txs:
assert dtx in blk.transactions, (dtx, blk.transactions)
print 'made block with timestamp %d and %d dunkles' % (blk.timestamp, len(dunkle_txs))
assert blk.timestamp >= self.next_skip_timestamp
assert self.chain.add_block(blk)
self.update_head()
post_dunkle_count = call_casper(self.chain.state, 'getTotalDunklesIncluded', [])
assert post_dunkle_count - pre_dunkle_count == len(dunkle_txs)
self.received_objects[blk.hash] = True
print 'Validator %d making block %d (%s)' % (self.id, blk.header.number, blk.header.hash[:8].encode('hex'))
self.network.broadcast(self, blk)
# Sometimes we received blocks too early or out of order;
# run an occasional loop that processes these
if random.random() < 0.02:
self.chain.process_time_queue()
self.chain.process_parent_queue()
self.update_head()
def update_head(self):
if self.cached_head == self.chain.head_hash:
return
self.cached_head = self.chain.head_hash
if self.chain.state.block_number % self.epoch_length == 0:
self.find_my_indices()
if self.indices:
self.next_skip_count = 0
self.next_skip_timestamp = get_timestamp(self.chain, self.next_skip_count)
print 'Head changed: %s, will attempt creating a block at %d' % (self.chain.head_hash.encode('hex'), self.next_skip_timestamp)
def withdraw(self, gasprice=20 * 10**9):
h = sha3(b'withdrawwithdrawwithdrawwithdraw')
v, r, s = ecsign(h, self.key)
sigdata = encode_int32(v) + encode_int32(r) + encode_int32(s)
txdata = casper_ct.encode('startWithdrawal', [self.indices[0], self.indices[1], sigdata])
tx = Transaction(self.chain.state.get_nonce(self.address), gasprice, 650000, self.chain.config['CASPER_ADDR'], 0, txdata).sign(self.key)
self.chain.add_transaction(tx)
self.network.broadcast(self, tx)
print 'Withdrawing!'

37
casper3/distributions.py Normal file
View File

@ -0,0 +1,37 @@
import random, sys
def normal_distribution(mean, standev):
def f():
return int(random.normalvariate(mean, standev))
return f
def exponential_distribution(mean):
def f():
total = 0
while 1:
total += 1
if not random.randrange(32):
break
return int(total * 0.03125 * mean)
return f
def convolve(*args):
def f():
total = 0
for arg in args:
total += arg()
return total
return f
def transform(dist, xformer):
def f():
return xformer(dist())
return f

75
casper3/networksim.py Normal file
View File

@ -0,0 +1,75 @@
from distributions import transform, normal_distribution
import random
class NetworkSimulator():
def __init__(self, latency=50):
self.agents = []
self.latency_distribution_sample = transform(normal_distribution(latency, (latency * 2) // 5), lambda x: max(x, 0))
self.time = 0
self.objqueue = {}
self.peers = {}
self.reliability = 0.9
def generate_peers(self, num_peers=5):
self.peers = {}
for a in self.agents:
p = []
while len(p) <= num_peers // 2:
p.append(random.choice(self.agents))
if p[-1] == a:
p.pop()
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:
for recipient, obj in self.objqueue[self.time]:
if random.random() < self.reliability:
recipient.on_receive(obj)
del self.objqueue[self.time]
for a in self.agents:
a.tick()
self.time += 1
def run(self, steps):
for i in range(steps):
self.tick()
def broadcast(self, sender, obj):
for p in self.peers[sender.id]:
recv_time = self.time + self.latency_distribution_sample()
if recv_time not in self.objqueue:
self.objqueue[recv_time] = []
self.objqueue[recv_time].append((p, obj))
def direct_send(self, to_id, obj):
for a in self.agents:
if a.id == to_id:
recv_time = self.time + self.latency_distribution_sample()
if recv_time not in self.objqueue:
self.objqueue[recv_time] = []
self.objqueue[recv_time].append((a, obj))
def knock_offline_random(self, n):
ko = {}
while len(ko) < n:
c = random.choice(self.agents)
ko[c.id] = c
for c in ko.values():
self.peers[c.id] = []
for a in self.agents:
self.peers[a.id] = [x for x in self.peers[a.id] if x.id not in ko]
def partition(self):
a = {}
while len(a) < len(self.agents) / 2:
c = random.choice(self.agents)
a[c.id] = c
for c in self.agents:
if c.id in a:
self.peers[c.id] = [x for x in self.peers[c.id] if x.id in a]
else:
self.peers[c.id] = [x for x in self.peers[c.id] if x.id not in a]

69
casper3/test.py Normal file
View File

@ -0,0 +1,69 @@
import networksim
from casper import Validator
import casper
from ethereum.parse_genesis_declaration import mk_basic_state
from ethereum.config import Env
from ethereum.casper_utils import RandaoManager, generate_validation_code, call_casper, \
get_skips_and_block_making_time, sign_block, make_block, get_contract_code, \
casper_config, get_casper_ct, get_casper_code, get_rlp_decoder_code, \
get_hash_without_ed_code, make_casper_genesis, validator_sizes, find_indices
from ethereum.utils import sha3, privtoaddr
from ethereum.transactions import Transaction
from ethereum.state_transition import apply_transaction
from ethereum.slogging import LogRecorder, configure_logging, set_level
# config_string = ':info,eth.vm.log:trace,eth.vm.op:trace,eth.vm.stack:trace,eth.vm.exit:trace,eth.pb.msg:trace,eth.pb.tx:debug'
config_string = ':info,eth.vm.log:trace'
configure_logging(config_string=config_string)
n = networksim.NetworkSimulator(latency=150)
n.time = 2
print 'Generating keys'
keys = [sha3(str(i)) for i in range(20)]
print 'Initializing randaos'
randaos = [RandaoManager(sha3(k)) for k in keys]
deposit_sizes = [128] * 15 + [256] * 5
print 'Creating genesis state'
s = make_casper_genesis(validators=[(generate_validation_code(privtoaddr(k)), ds * 10**18, r.get(9999))
for k, ds, r in zip(keys, deposit_sizes, randaos)],
alloc={privtoaddr(k): {'balance': 10**18} for k in keys},
timestamp=2,
epoch_length=40)
g = s.to_snapshot()
print 'Genesis state created'
validators = [Validator(g, k, n, Env(config=casper_config), time_offset=4) for k in keys]
n.agents = validators
n.generate_peers()
lowest_shared_height = -1
made_101_check = 0
for i in range(100000):
# print 'ticking'
n.tick()
if i % 100 == 0:
print '%d ticks passed' % i
print 'Validator heads:', [v.chain.head.header.number if v.chain.head else None for v in validators]
print 'Total blocks created:', casper.global_block_counter
print 'Dunkle count:', call_casper(validators[0].chain.state, 'getTotalDunklesIncluded', [])
lowest_shared_height = min([v.chain.head.header.number if v.chain.head else -1 for v in validators])
if lowest_shared_height >= 101 and not made_101_check:
made_101_check = True
print 'Checking that withdrawn validators are inactive'
assert len([v for v in validators if v.active]) == len(validators) - 5, len([v for v in validators if v.active])
print 'Check successful'
break
if i == 1:
print 'Checking that all validators are active'
assert len([v for v in validators if v.active]) == len(validators)
print 'Check successful'
if i == 2000:
print 'Withdrawing a few validators'
for v in validators[:5]:
v.withdraw()
if i == 4000:
print 'Checking that validators have withdrawn'
for v in validators[:5]:
assert call_casper(v.chain.state, 'getEndEpoch', []) <= 2
print 'Check successful'

123
selfish_mining_strats.py Normal file
View File

@ -0,0 +1,123 @@
import random
import sys
def test_strat(strat, hashpower, gamma, reward, fees, uncle_rewards=1, uncle_coeff=0, max_uncles=2, rounds=25000):
# Block reward for attacker
me_reward = 0
# Block reward for others
them_reward = 0
# Fees for the attacker
me_fees = 0
# Fees for others
them_fees = 0
# Blocks in current private chain
me_blocks = 0
# Blocks in current public chain
them_blocks = 0
# Time elapsed since last chain merging
time_elapsed = 0
# Divisor for block rewards (diff adjustment)
divisor = 0
# Total blocks included from attacker
me_totblocks = 0
# Total blocks included from others
them_totblocks = 0
# Uncles included from attacker
me_totuncles = 0
# Uncles included from others
them_totuncles = 0
# Simulate the system
for i in range(rounds):
# Attacker makes a block
if random.random() < hashpower:
me_blocks += 1
last_is_me = 1
# Honest nodes make a block
else:
them_blocks += 1
last_is_me = 0
time_elapsed += random.expovariate(1)
# "Adopt" or "override"
if me_blocks >= len(strat) or them_blocks >= len(strat[me_blocks]) or strat[me_blocks][them_blocks] == 1:
# Override
if me_blocks > them_blocks or (me_blocks == them_blocks and random.random() < gamma):
me_reward += me_blocks * reward
me_fees += time_elapsed * fees
divisor += me_blocks
me_totblocks += me_blocks
# Add uncles
while me_blocks < 7 and them_blocks > 0:
r = min(them_blocks, max_uncles) * (0.875 - 0.125 * me_blocks) * uncle_rewards
them_totuncles += min(them_blocks, max_uncles)
divisor += min(them_blocks, max_uncles) * uncle_coeff
them_reward = them_reward + r
them_blocks -= min(them_blocks, max_uncles)
me_blocks += 1
# Adopt
else:
them_reward += them_blocks * reward
them_fees += time_elapsed * fees
divisor += them_blocks
them_totblocks += them_blocks
# Add uncles
while them_blocks < 7 and me_blocks > 0:
r = min(me_blocks, max_uncles) * (0.875 - 0.125 * them_blocks) * uncle_rewards
me_totuncles += min(me_blocks, max_uncles)
divisor += min(me_blocks, max_uncles) * uncle_coeff
me_reward = me_reward + r
me_blocks -= min(me_blocks, max_uncles)
them_blocks += 1
me_blocks = 0
them_blocks = 0
time_elapsed = 0
# Match
elif strat[me_blocks][them_blocks] == 2 and not last_is_me:
if random.random() < gamma:
me_reward += me_blocks * reward + time_elapsed * fees
me_totblocks += me_blocks
divisor += me_blocks
time_elapsed = 0
# Add uncles
while me_blocks < 7 and them_blocks > 0:
r = min(them_blocks, max_uncles) * (0.875 - 0.125 * me_blocks)
them_totuncles += min(them_blocks, max_uncles)
divisor += min(them_blocks, max_uncles) * uncle_coeff
them_reward = them_reward + r
them_blocks -= min(them_blocks, max_uncles)
me_blocks += 1
me_blocks = 0
them_blocks = 0
# print 'rat', (me_totblocks + me_totuncles) / (me_totblocks + me_totuncles + them_totblocks + them_totuncles * 1.0)
return me_reward / divisor + me_fees / rounds, them_reward / divisor + them_fees / rounds
# A 20x20 array meaning "what to do if I made i blocks and the network
# made j blocks?". 1 = publish, 0 = do nothing.
def gen_selfish_mining_strat():
o = [([0] * 20) for i in range(20)]
for me in range(20):
for them in range(20):
# Adopt
if them == 1 and me == 0:
o[me][them] = 1
if them == me + 1:
o[me][them] = 1
# Overtake
if me >= 2 and me == them + 1:
o[me][them] = 1
# Match
if me >= 1 and me == them:
o[me][them] = 2
return o
dic = {"rewards": 1, "fees": 0, "gamma": 0.5, "uncle_coeff": 0, "uncle_rewards": 0, "max_uncles": 2}
for a in sys.argv[1:]:
param, val = a[:a.index('=')], a[a.index('=')+1:]
dic[param] = float(val)
print dic
s = gen_selfish_mining_strat()
for i in range(1, 50):
x, y = test_strat(s, i * 0.01, dic["gamma"], dic["rewards"], dic["fees"],
dic["uncle_rewards"], dic["uncle_coeff"], dic["max_uncles"], rounds=200000)
print '%d%% hashpower, %f%% of rewards, (%f attacker, %f honest)' % \
(i, x * 100.0 / (x + y), x * 100.0 / i, y * 100.0 / (100-i))