196 lines
8.7 KiB
Python
196 lines
8.7 KiB
Python
from ethereum.casper_utils import RandaoManager, get_skips_and_block_making_time, \
|
|
generate_validation_code, call_casper, sign_block, check_skips, get_timestamp, \
|
|
get_casper_ct, get_dunkle_candidates, \
|
|
make_withdrawal_signature
|
|
from ethereum.utils import sha3, hash32, privtoaddr, ecsign, zpad, encode_int32, \
|
|
big_endian_to_int
|
|
from ethereum.transaction_queue import TransactionQueue
|
|
from ethereum.block_creation import make_head_candidate
|
|
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)
|
|
# Create a transaction queue
|
|
self.txqueue = TransactionQueue()
|
|
# 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
|
|
# Is this validator active?
|
|
self.active = False
|
|
# Code that verifies signatures from this validator
|
|
self.validation_code = generate_validation_code(privtoaddr(key))
|
|
# Validation code hash
|
|
self.vchash = sha3(self.validation_code)
|
|
# 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')
|
|
# My minimum gas price
|
|
self.mingasprice = 20 * 10**9
|
|
# Give this validator a unique ID
|
|
self.id = len(ids)
|
|
ids.append(self.id)
|
|
self.update_activity_status()
|
|
self.cached_head = self.chain.head_hash
|
|
|
|
def call_casper(self, fun, args=[]):
|
|
return call_casper(self.chain.state, fun, args)
|
|
|
|
def update_activity_status(self):
|
|
start_epoch = self.call_casper('getStartEpoch', [self.vchash])
|
|
now_epoch = self.call_casper('getEpoch')
|
|
end_epoch = self.call_casper('getEndEpoch', [self.vchash])
|
|
if start_epoch <= now_epoch < end_epoch:
|
|
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'
|
|
else:
|
|
self.active = False
|
|
|
|
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):
|
|
print 'Receiving transaction', obj
|
|
if obj.gasprice >= self.mingasprice:
|
|
self.txqueue.add_transaction(obj)
|
|
print 'Added transaction, txqueue size %d' % len(self.txqueue.txs)
|
|
self.network.broadcast(self, obj)
|
|
else:
|
|
print 'Gasprice too low'
|
|
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.active 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.vchash, 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
|
|
s1 = self.chain.state.trie.root_hash
|
|
pre_dunkle_count = self.call_casper('getTotalDunklesIncluded')
|
|
dunkle_txs = get_dunkle_candidates(self.chain, self.chain.state)
|
|
blk = make_head_candidate(self.chain, self.txqueue)
|
|
randao = self.randao.get_parent(self.call_casper('getRandao', [self.vchash]))
|
|
blk = sign_block(blk, self.key, randao, self.vchash, self.next_skip_count)
|
|
# Make sure it's valid
|
|
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))
|
|
s2 = self.chain.state.trie.root_hash
|
|
assert s1 == s2
|
|
assert blk.timestamp >= self.next_skip_timestamp
|
|
assert self.chain.add_block(blk)
|
|
self.update_head()
|
|
post_dunkle_count = self.call_casper('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.update_activity_status()
|
|
if self.active:
|
|
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):
|
|
sigdata = make_withdrawal_signature(self.key)
|
|
txdata = casper_ct.encode('startWithdrawal', [self.vchash, sigdata])
|
|
tx = Transaction(self.chain.state.get_nonce(self.address), gasprice, 650000, self.chain.config['CASPER_ADDR'], 0, txdata).sign(self.key)
|
|
self.txqueue.add_transaction(tx, force=True)
|
|
self.network.broadcast(self, tx)
|
|
print 'Withdrawing!'
|
|
|
|
def deposit(self, gasprice=20 * 10**9):
|
|
assert value * 10**18 >= self.chain.state.get_balance(self.address) + gasprice * 1000000
|
|
tx = Transaction(self.chain.state.get_nonce(self.address) * 10**18, gasprice, 1000000,
|
|
casper_config['CASPER_ADDR'], value * 10**18,
|
|
ct.encode('deposit', [self.validation_code, self.randao.get(9999)]))
|