Moved stuff from econ modeling to research
This commit is contained in:
parent
1e9c9e7725
commit
768e21e523
|
@ -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!'
|
|
@ -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
|
|
@ -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]
|
|
@ -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'
|
|
@ -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))
|
Loading…
Reference in New Issue