def test_create_collation_with_txs(): """Test create_collation with transactions """ shard_id = 1 t = chain(shard_id) parent_collation_hash = t.chain.shards[shard_id].head_hash expected_period_number = t.chain.get_expected_period_number() txqueue = TransactionQueue() tx1 = t.generate_shard_tx(shard_id, tester.k2, tester.a4, int(0.03 * utils.denoms.ether)) tx2 = t.generate_shard_tx(shard_id, tester.k3, tester.a5, int(0.03 * utils.denoms.ether)) txqueue.add_transaction(tx1) txqueue.add_transaction(tx2) collation = collator.create_collation(t.chain, shard_id, parent_collation_hash, expected_period_number, coinbase=tester.a0, key=tester.k0, txqueue=txqueue) assert collation.transaction_count == 2
def mine_on_chain(chain, parent=None, transactions=[], coinbase=None, timestamp=None): """Mine the next block on a chain. The newly mined block will be considered to be the head of the chain, regardless of its total difficulty. :param parent: the parent of the block to mine, or `None` to use the current chain head :param transactions: a list of transactions to include in the new block :param coinbase: optional coinbase to replace ``chain.coinbase`` """ txqueue = TransactionQueue() for t in transactions: txqueue.add_transaction(t) parent_timestamp = parent.timestamp if parent else chain.state.timestamp hc, _ = meta.make_head_candidate(chain, txqueue, parent, timestamp or parent_timestamp + 1, coinbase or '\x00' * 20) assert hc.difficulty == 1 m = ethpow.Miner(hc) rounds = 100 nonce = 0 while True: b = m.mine(rounds=rounds, start_nonce=nonce) if b: break nonce += rounds assert chain.add_block(b) return b
def test_apply_collation(): """Apply collation to ShardChain """ shard_id = 1 t = chain(shard_id) txqueue = TransactionQueue() tx1 = t.generate_shard_tx(shard_id, tester.k2, tester.a4, int(0.03 * utils.denoms.ether)) tx2 = t.generate_shard_tx(shard_id, tester.k3, tester.a5, int(0.03 * utils.denoms.ether)) txqueue.add_transaction(tx1) txqueue.add_transaction(tx2) state = t.chain.shards[shard_id].state prev_state_root = state.trie.root_hash collation = t.generate_collation(shard_id=1, coinbase=tester.a1, key=tester.k1, txqueue=txqueue) period_start_prevblock = t.chain.get_block( collation.header.period_start_prevhash) collator.apply_collation(state, collation, period_start_prevblock) assert state.trie.root_hash != prev_state_root assert collation.header.post_state_root == state.trie.root_hash assert collation.header.post_state_root == t.chain.shards[ shard_id].state.trie.root_hash
def test_verify_collation_header(): shard_id = 1 t = chain(shard_id) parent_collation_hash = t.chain.shards[shard_id].head_hash expected_period_number = t.chain.get_expected_period_number() txqueue = TransactionQueue() tx1 = t.generate_shard_tx(shard_id, tester.k2, tester.a4, int(0.03 * utils.denoms.ether)) tx2 = t.generate_shard_tx(shard_id, tester.k3, tester.a5, int(0.03 * utils.denoms.ether)) txqueue.add_transaction(tx1) txqueue.add_transaction(tx2) collation = collator.create_collation(t.chain, shard_id, parent_collation_hash, expected_period_number, coinbase=tester.a0, key=tester.k0, txqueue=txqueue) # Verify collation header assert collator.verify_collation_header(t.chain, collation.header) # Bad collation header 1 collation = collator.create_collation(t.chain, shard_id, parent_collation_hash, expected_period_number, coinbase=tester.a1, key=tester.k1, txqueue=txqueue) collation.header.shard_id = -1 with pytest.raises(ValueError): collator.verify_collation_header(t.chain, collation.header) # Bad collation header 2 - call_msg_add_header error collation = collator.create_collation(t.chain, shard_id, parent_collation_hash, expected_period_number, coinbase=tester.a1, key=tester.k1, txqueue=txqueue) collation.header.sig = utils.sha3('hello') with pytest.raises(ValueError): collator.verify_collation_header(t.chain, collation.header)
def test_create_collation_empty_txqueue(): """Test create_collation without transactions """ shard_id = 1 t = chain(shard_id) parent_collation_hash = t.chain.shards[shard_id].head_hash expected_period_number = t.chain.get_expected_period_number() txqueue = TransactionQueue() collation = collator.create_collation(t.chain, shard_id, parent_collation_hash, expected_period_number, coinbase=tester.a1, key=tester.k1, txqueue=txqueue) assert collation.transaction_count == 0 assert collation.header.coinbase == tester.a1 # sign error with pytest.raises(TypeError): collation = collator.create_collation(t.chain, shard_id, parent_collation_hash, expected_period_number, coinbase=tester.a1, key=123, txqueue=txqueue)
def __init__(self, app): super(ChainServiceMock, self).__init__(app) self.on_new_head_cbs = [] self.transaction_queue = TransactionQueue() self.is_syncing = False self.mined_block = None self.block_mined_event = Event() self.head_candidate = Block(BlockHeader(difficulty=DIFFICULTY), db=DB())
def test_transaction(): """Test create and apply collation with transactions """ shard_id = 1 t = chain(shard_id) log.info('head state: {}'.format( encode_hex(t.chain.shards[shard_id].state.trie.root_hash))) tx1 = t.generate_shard_tx(shard_id, tester.k2, tester.a4, int(0.03 * utils.denoms.ether)) tx2 = t.generate_shard_tx(shard_id, tester.k3, tester.a5, int(0.03 * utils.denoms.ether)) # Prepare txqueue txqueue = TransactionQueue() txqueue.add_transaction(tx1) txqueue.add_transaction(tx2) collation = t.generate_collation(shard_id=1, coinbase=tester.a1, key=tester.k1, txqueue=txqueue) log.debug('collation: {}, transaction_count:{}'.format( collation.to_dict(), collation.transaction_count)) period_start_prevblock = t.chain.get_block( collation.header.period_start_prevhash) log.debug('period_start_prevblock: {}'.format( encode_hex(period_start_prevblock.header.hash))) t.chain.shards[shard_id].add_collation(collation, period_start_prevblock, t.chain.handle_ignored_collation) state = t.chain.shards[shard_id].mk_poststate_of_collation_hash( collation.header.hash) # Check to addesss received value assert state.get_balance(tester.a4) == 1000030000000000000000 # Check incentives assert state.get_balance(tester.a1) == 1000002000000000000000 # mk_poststate_of_collation_hash error with pytest.raises(Exception): state = t.chain.shards[shard_id].mk_poststate_of_collation_hash( b'1234')
def test_validate_transaction_tree(): """Test validate_transaction_tree(collation) """ t = chain(shard_id) tx1 = t.generate_shard_tx(shard_id, tester.k2, tester.a4, int(0.03 * utils.denoms.ether)) tx2 = t.generate_shard_tx(shard_id, tester.k3, tester.a5, int(0.03 * utils.denoms.ether)) txqueue = TransactionQueue() txqueue.add_transaction(tx1) txqueue.add_transaction(tx2) collation = t.generate_collation(shard_id=1, coinbase=tester.a1, key=tester.k1, txqueue=txqueue) assert state_transition.validate_transaction_tree(collation) collation.header.tx_list_root = trie.BLANK_ROOT with pytest.raises(ValueError): state_transition.validate_transaction_tree(collation)
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 __init__(self, key, genesis, network, valcode_addr=None, mining=False): self.key = key self.coinbase = utils.privtoaddr(self.key) self.chain = chain.Chain(genesis=genesis, reset_genesis=True, coinbase=self.coinbase, new_head_cb=self._on_new_head) self.mining = mining self.nonce = self.chain.state.get_nonce(self.coinbase) self.valcode_tx = None self.deposit_tx = None self.valcode_addr = valcode_addr self.prepares = dict() self.prev_prepare_epoch = 0 self.prev_commit_epoch = 0 self.epoch_length = self.chain.env.config['EPOCH_LENGTH'] # When the transaction_queue is modified, we must set # self._head_candidate_needs_updating to True in order to force the # head candidate to be updated. self.transaction_queue = TransactionQueue() self._head_candidate_needs_updating = True # Add validator to the network self.network = network self.network.join(self)
def test_add_transactions(): """Test add_transactions(state, collation, txqueue, min_gasprice=0) """ t = chain(shard_id) tx1 = t.generate_shard_tx(shard_id, tester.k2, tester.a4, int(0.03 * utils.denoms.ether)) tx2 = t.generate_shard_tx(shard_id, tester.k3, tester.a5, int(0.03 * utils.denoms.ether)) txqueue = TransactionQueue() txqueue.add_transaction(tx1) txqueue.add_transaction(tx2) coinbase = tester.a1 state = t.chain.shards[shard_id].state.ephemeral_clone() collation = state_transition.mk_collation_from_prevstate( t.chain.shards[shard_id], state, coinbase) state_transition.add_transactions(state, collation, txqueue, shard_id, mainchain_state=t.head_state) assert collation.transaction_count == 2 assert state.get_balance(tester.a4) == 1000 * utils.denoms.ether + int( 0.03 * utils.denoms.ether) # InsufficientBalance -> don't include this transaction tx3 = t.generate_shard_tx(shard_id, tester.k2, tester.a4, int(100000000000 * utils.denoms.ether)) txqueue.add_transaction(tx3) state_transition.add_transactions(state, collation, txqueue, shard_id, mainchain_state=t.head_state) assert collation.transaction_count == 2
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)]))
class ChainService(WiredService): """ Manages the chain and requests to it. """ # required by BaseService name = 'chain' default_config = dict(eth=dict(network_id=0, genesis='', pruning=-1), block=ethereum_config.default_config) # required by WiredService wire_protocol = eth_protocol.ETHProtocol # create for each peer # initialized after configure: chain = None genesis = None synchronizer = None config = None block_queue_size = 1024 processed_gas = 0 processed_elapsed = 0 process_time_queue_period = 5 def __init__(self, app): self.config = app.config sce = self.config['eth'] if int(sce['pruning']) >= 0: self.db = RefcountDB(app.services.db) if "I am not pruning" in self.db.db: raise RuntimeError( "The database in '{}' was initialized as non-pruning. " "Can not enable pruning now.".format( self.config['data_dir'])) self.db.ttl = int(sce['pruning']) self.db.db.put("I am pruning", "1") else: self.db = app.services.db if "I am pruning" in self.db: raise RuntimeError( "The database in '{}' was initialized as pruning. " "Can not disable pruning now".format( self.config['data_dir'])) self.db.put("I am not pruning", "1") if 'network_id' in self.db: db_network_id = self.db.get(b'network_id') if db_network_id != to_string(sce['network_id']): raise RuntimeError( "The database in '{}' was initialized with network id {} and can not be used " "when connecting to network id {}. Please choose a different data directory." .format(self.config['data_dir'], db_network_id, sce['network_id'])) else: self.db.put(b'network_id', to_string(sce['network_id'])) self.db.commit() assert self.db is not None super(ChainService, self).__init__(app) log.info('initializing chain') coinbase = app.services.accounts.coinbase env = Env(self.db, sce['block']) genesis_data = sce.get('genesis_data', {}) if not genesis_data: genesis_data = mk_genesis_data(env) self.chain = Chain(env=env, genesis=genesis_data, coinbase=coinbase, new_head_cb=self._on_new_head) header = self.chain.state.prev_headers[0] log.info('chain at', number=header.number) if 'genesis_hash' in sce: assert sce['genesis_hash'] == self.chain.genesis.hex_hash, \ "Genesis hash mismatch.\n Expected: %s\n Got: %s" % ( sce['genesis_hash'], self.chain.genesis.hex_hash) self.dao_challenges = dict() self.synchronizer = Synchronizer(self, force_sync=None) self.block_queue = Queue(maxsize=self.block_queue_size) # When the transaction_queue is modified, we must set # self._head_candidate_needs_updating to True in order to force the # head candidate to be updated. self.transaction_queue = TransactionQueue() self._head_candidate_needs_updating = True # Initialize a new head candidate. _ = self.head_candidate self.min_gasprice = 20 * 10**9 # TODO: better be an option to validator service? self.add_blocks_lock = False self.add_transaction_lock = gevent.lock.Semaphore() self.broadcast_filter = DuplicatesFilter() self.on_new_head_cbs = [] self.newblock_processing_times = deque(maxlen=1000) gevent.spawn_later(self.process_time_queue_period, self.process_time_queue) @property def is_syncing(self): return self.synchronizer.synctask is not None @property def is_mining(self): if 'pow' in self.app.services: return self.app.services.pow.active if 'validator' in self.app.services: return self.app.services.validator.active return False def process_time_queue(self): try: self.chain.process_time_queue() except Exception as e: log.info(str(e)) finally: gevent.spawn_later(self.process_time_queue_period, self.process_time_queue) # TODO: Move to pyethereum def get_receipts(self, block): # Receipts are no longer stored in the database, so need to generate # them on the fly here. temp_state = self.chain.mk_poststate_of_blockhash( block.header.prevhash) initialize(temp_state, block) for tx in block.transactions: apply_transaction(temp_state, tx) return temp_state.receipts def _on_new_head(self, block): log.debug('new head cbs', num=len(self.on_new_head_cbs)) self.transaction_queue = self.transaction_queue.diff( block.transactions) self._head_candidate_needs_updating = True for cb in self.on_new_head_cbs: cb(block) @property def head_candidate(self): if self._head_candidate_needs_updating: self._head_candidate_needs_updating = False # Make a copy of self.transaction_queue because # make_head_candidate modifies it. txqueue = copy.deepcopy(self.transaction_queue) self._head_candidate, self._head_candidate_state = make_head_candidate( self.chain, txqueue, timestamp=int(time.time())) return self._head_candidate def add_transaction(self, tx, origin=None, force_broadcast=False, force=False): if self.is_syncing: if force_broadcast: assert origin is None # only allowed for local txs log.debug('force broadcasting unvalidated tx') self.broadcast_transaction(tx, origin=origin) return # we can not evaluate the tx based on outdated state log.debug('add_transaction', locked=(not self.add_transaction_lock.locked()), tx=tx) assert isinstance(tx, Transaction) assert origin is None or isinstance(origin, BaseProtocol) if tx.hash in self.broadcast_filter: log.debug('discarding known tx') # discard early return # validate transaction try: # Transaction validation for broadcasting. Transaction is validated # against the current head candidate. validate_transaction(self._head_candidate_state, tx) log.debug('valid tx, broadcasting') self.broadcast_transaction(tx, origin=origin) # asap except InvalidTransaction as e: log.debug('invalid tx', error=e) return if origin is not None: # not locally added via jsonrpc if not self.is_mining or self.is_syncing: log.debug('discarding tx', syncing=self.is_syncing, mining=self.is_mining) return if tx.gasprice >= self.min_gasprice: self.add_transaction_lock.acquire() self.transaction_queue.add_transaction(tx, force=force) self._head_candidate_needs_updating = True self.add_transaction_lock.release() else: log.info("too low gasprice, ignore", tx=encode_hex(tx.hash)[:8], gasprice=tx.gasprice) def check_header(self, header): return check_pow(self.chain.state, header) def add_block(self, t_block, proto): "adds a block to the block_queue and spawns _add_block if not running" self.block_queue.put((t_block, proto)) # blocks if full if not self.add_blocks_lock: self.add_blocks_lock = True # need to lock here (ctx switch is later) gevent.spawn(self._add_blocks) def add_mined_block(self, block): log.debug('adding mined block', block=block) assert isinstance(block, Block) if self.chain.add_block(block): log.debug('added', block=block, ts=time.time()) assert block == self.chain.head self.transaction_queue = self.transaction_queue.diff( block.transactions) self._head_candidate_needs_updating = True self.broadcast_newblock( block, chain_difficulty=self.chain.get_score(block)) return True log.debug('failed to add', block=block, ts=time.time()) return False def knows_block(self, block_hash): "if block is in chain or in queue" if self.chain.has_blockhash(block_hash): return True # check if queued or processed for i in range(len(self.block_queue.queue)): if block_hash == self.block_queue.queue[i][0].header.hash: return True return False def _add_blocks(self): log.debug('add_blocks', qsize=self.block_queue.qsize(), add_tx_lock=self.add_transaction_lock.locked()) assert self.add_blocks_lock is True self.add_transaction_lock.acquire() try: while not self.block_queue.empty(): # sleep at the beginning because continue keywords will skip bottom gevent.sleep(0.001) t_block, proto = self.block_queue.peek( ) # peek: knows_block while processing if self.chain.has_blockhash(t_block.header.hash): log.warn('known block', block=t_block) self.block_queue.get() continue if not self.chain.has_blockhash(t_block.header.prevhash): log.warn('missing parent', block=t_block, head=self.chain.head) self.block_queue.get() continue try: # deserialize st = time.time() block = t_block.to_block() elapsed = time.time() - st log.debug('deserialized', elapsed='%.4fs' % elapsed, ts=time.time(), gas_used=block.gas_used, gpsec=self.gpsec(block.gas_used, elapsed)) except InvalidTransaction as e: log.warn('invalid transaction', block=t_block, error=e, FIXME='ban node') errtype = \ 'InvalidNonce' if isinstance(e, InvalidNonce) else \ 'NotEnoughCash' if isinstance(e, InsufficientBalance) else \ 'OutOfGasBase' if isinstance(e, InsufficientStartGas) else \ 'other_transaction_error' sentry.warn_invalid(t_block, errtype) self.block_queue.get() continue except VerificationFailed as e: log.warn('verification failed', error=e, FIXME='ban node') sentry.warn_invalid(t_block, 'other_block_error') self.block_queue.get() continue # All checks passed log.debug('adding', block=block, ts=time.time()) if self.chain.add_block(block): now = time.time() log.info('added', block=block, txs=block.transaction_count, gas_used=block.gas_used) if t_block.newblock_timestamp: total = now - t_block.newblock_timestamp self.newblock_processing_times.append(total) avg = statistics.mean(self.newblock_processing_times) med = statistics.median(self.newblock_processing_times) max_ = max(self.newblock_processing_times) min_ = min(self.newblock_processing_times) log.info('processing time', last=total, avg=avg, max=max_, min=min_, median=med) if self.is_mining: self.transaction_queue = self.transaction_queue.diff( block.transactions) else: log.warn('could not add', block=block) self.block_queue.get( ) # remove block from queue (we peeked only) finally: self.add_blocks_lock = False self.add_transaction_lock.release() def gpsec(self, gas_spent=0, elapsed=0): if gas_spent: self.processed_gas += gas_spent self.processed_elapsed += elapsed return int( old_div(self.processed_gas, (0.001 + self.processed_elapsed))) def broadcast_newblock(self, block, chain_difficulty=None, origin=None): if not chain_difficulty: assert self.chain.has_blockhash(block.hash) chain_difficulty = self.chain.get_score(block) assert isinstance(block, (eth_protocol.TransientBlock, Block)) if self.broadcast_filter.update(block.header.hash): log.debug('broadcasting newblock', origin=origin) bcast = self.app.services.peermanager.broadcast bcast(eth_protocol.ETHProtocol, 'newblock', args=(block, chain_difficulty), exclude_peers=[origin.peer] if origin else []) else: log.debug('already broadcasted block') def broadcast_transaction(self, tx, origin=None): assert isinstance(tx, Transaction) if self.broadcast_filter.update(tx.hash): log.debug('broadcasting tx', origin=origin) bcast = self.app.services.peermanager.broadcast bcast(eth_protocol.ETHProtocol, 'transactions', args=(tx, ), exclude_peers=[origin.peer] if origin else []) else: log.debug('already broadcasted tx') def query_headers(self, hash_mode, max_hashes, skip, reverse, origin_hash=None, number=None): headers = [] unknown = False while not unknown and len(headers) < max_hashes: if hash_mode: if not origin_hash: break block = self.chain.get_block(origin_hash) if not block: break # If reached genesis, stop if block.number == 0: break origin = block.header else: # If reached genesis, stop if number is None or number == 0: break block = self.chain.get_block_by_number(number) if block is None: break origin = block.header headers.append(origin) if hash_mode: # hash traversal if reverse: for i in range(skip + 1): try: block = self.chain.get_block(origin_hash) if block: origin_hash = block.prevhash else: unknown = True break except KeyError: unknown = True break else: blockhash = self.chain.get_blockhash_by_number( origin.number + skip + 1) try: # block = self.chain.get_block(blockhash) if block and self.chain.get_blockhashes_from_hash( blockhash, skip + 1)[skip] == origin_hash: origin_hash = blockhash else: unknown = True except KeyError: unknown = True else: # number traversal if reverse: if number >= (skip + 1): number -= (skip + 1) else: unknown = True else: number += (skip + 1) return headers # wire protocol receivers ########### def on_wire_protocol_start(self, proto): log.debug('----------------------------------') log.debug('on_wire_protocol_start', proto=proto) assert isinstance(proto, self.wire_protocol) # register callbacks proto.receive_status_callbacks.append(self.on_receive_status) proto.receive_newblockhashes_callbacks.append(self.on_newblockhashes) proto.receive_transactions_callbacks.append( self.on_receive_transactions) proto.receive_getblockheaders_callbacks.append( self.on_receive_getblockheaders) proto.receive_blockheaders_callbacks.append( self.on_receive_blockheaders) proto.receive_getblockbodies_callbacks.append( self.on_receive_getblockbodies) proto.receive_blockbodies_callbacks.append(self.on_receive_blockbodies) proto.receive_newblock_callbacks.append(self.on_receive_newblock) # send status head = self.chain.head proto.send_status(chain_difficulty=self.chain.get_score(head), chain_head_hash=head.hash, genesis_hash=self.chain.genesis.hash) def on_wire_protocol_stop(self, proto): assert isinstance(proto, self.wire_protocol) log.debug('----------------------------------') log.debug('on_wire_protocol_stop', proto=proto) def on_receive_status(self, proto, eth_version, network_id, chain_difficulty, chain_head_hash, genesis_hash): log.debug('----------------------------------') log.debug('status received', proto=proto, eth_version=eth_version) if eth_version != proto.version: if ('eth', proto.version) in proto.peer.remote_capabilities: # if remote peer is capable of our version, keep the connection # even the peer tried a different version pass else: log.debug("no capable protocol to use, disconnect", proto=proto, eth_version=eth_version) proto.send_disconnect(proto.disconnect.reason.useless_peer) return if network_id != self.config['eth'].get('network_id', proto.network_id): log.debug("invalid network id", remote_network_id=network_id, expected_network_id=self.config['eth'].get( 'network_id', proto.network_id)) raise eth_protocol.ETHProtocolError('wrong network_id') # check genesis if genesis_hash != self.chain.genesis.hash: log.warn("invalid genesis hash", remote_id=proto, genesis=encode_hex(genesis_hash)) raise eth_protocol.ETHProtocolError('wrong genesis block') # initiate DAO challenge self.dao_challenges[proto] = (DAOChallenger(self, proto), chain_head_hash, chain_difficulty) def on_dao_challenge_answer(self, proto, result): if result: log.debug("DAO challenge passed") _, chain_head_hash, chain_difficulty = self.dao_challenges[proto] # request chain self.synchronizer.receive_status(proto, chain_head_hash, chain_difficulty) # send transactions transactions = self.transaction_queue.peek() if transactions: log.debug("sending transactions", remote_id=proto) proto.send_transactions(*transactions) else: log.debug("peer failed to answer DAO challenge, stop.", proto=proto) if proto.peer: proto.peer.stop() del self.dao_challenges[proto] # transactions def on_receive_transactions(self, proto, transactions): "receives rlp.decoded serialized" log.debug('----------------------------------') log.debug('remote_transactions_received', count=len(transactions), remote_id=proto) for tx in transactions: self.add_transaction(tx, origin=proto) # blockhashes ########### def on_newblockhashes(self, proto, newblockhashes): """ msg sent out if not the full block is propagated chances are high, that we get the newblock, though. """ log.debug('----------------------------------') log.debug("recv newblockhashes", num=len(newblockhashes), remote_id=proto) assert len(newblockhashes) <= 256 self.synchronizer.receive_newblockhashes(proto, newblockhashes) def on_receive_getblockheaders(self, proto, hash_or_number, block, amount, skip, reverse): hash_mode = 1 if hash_or_number[0] else 0 block_id = encode_hex( hash_or_number[0]) if hash_mode else hash_or_number[1] log.debug('----------------------------------') log.debug("handle_getblockheaders", amount=amount, block=block_id) headers = [] max_hashes = min(amount, self.wire_protocol.max_getblockheaders_count) if hash_mode: origin_hash = hash_or_number[0] else: if is_dao_challenge(self.config['eth']['block'], hash_or_number[1], amount, skip): log.debug("sending: answer DAO challenge") headers.append(build_dao_header(self.config['eth']['block'])) proto.send_blockheaders(*headers) return try: origin_hash = self.chain.get_blockhash_by_number( hash_or_number[1]) except KeyError: origin_hash = b'' if not origin_hash or not self.chain.has_blockhash(origin_hash): log.debug('unknown block: {}'.format(encode_hex(origin_hash))) proto.send_blockheaders(*[]) return headers = self.query_headers( hash_mode, max_hashes, skip, reverse, origin_hash=origin_hash, number=block_id, ) log.debug("sending: found blockheaders", count=len(headers)) proto.send_blockheaders(*headers) def on_receive_blockheaders(self, proto, blockheaders): log.debug('----------------------------------') if blockheaders: log.debug("on_receive_blockheaders", count=len(blockheaders), remote_id=proto, first=encode_hex(blockheaders[0].hash), last=encode_hex(blockheaders[-1].hash)) else: log.debug("recv 0 remote block headers, signifying genesis block") if proto in self.dao_challenges: self.dao_challenges[proto][0].receive_blockheaders( proto, blockheaders) else: self.synchronizer.receive_blockheaders(proto, blockheaders) # blocks ################ def on_receive_getblockbodies(self, proto, blockhashes): log.debug('----------------------------------') log.debug("on_receive_getblockbodies", count=len(blockhashes)) found = [] for bh in blockhashes[:self.wire_protocol.max_getblocks_count]: try: found.append(self.chain.get_block(bh)) except KeyError: log.debug("unknown block requested", block_hash=encode_hex(bh)) if found: log.debug("found", count=len(found)) proto.send_blockbodies(*found) def on_receive_blockbodies(self, proto, bodies): log.debug('----------------------------------') log.debug("recv block bodies", count=len(bodies), remote_id=proto) if bodies: self.synchronizer.receive_blockbodies(proto, bodies) def on_receive_newblock(self, proto, block, chain_difficulty): log.debug('----------------------------------') log.debug("recv newblock", block=block, remote_id=proto) self.synchronizer.receive_newblock(proto, block, chain_difficulty)
def __init__(self, app): self.config = app.config sce = self.config['eth'] if int(sce['pruning']) >= 0: self.db = RefcountDB(app.services.db) if "I am not pruning" in self.db.db: raise RuntimeError( "The database in '{}' was initialized as non-pruning. " "Can not enable pruning now.".format( self.config['data_dir'])) self.db.ttl = int(sce['pruning']) self.db.db.put("I am pruning", "1") else: self.db = app.services.db if "I am pruning" in self.db: raise RuntimeError( "The database in '{}' was initialized as pruning. " "Can not disable pruning now".format( self.config['data_dir'])) self.db.put("I am not pruning", "1") if 'network_id' in self.db: db_network_id = self.db.get(b'network_id') if db_network_id != to_string(sce['network_id']): raise RuntimeError( "The database in '{}' was initialized with network id {} and can not be used " "when connecting to network id {}. Please choose a different data directory." .format(self.config['data_dir'], db_network_id, sce['network_id'])) else: self.db.put(b'network_id', to_string(sce['network_id'])) self.db.commit() assert self.db is not None super(ChainService, self).__init__(app) log.info('initializing chain') coinbase = app.services.accounts.coinbase env = Env(self.db, sce['block']) genesis_data = sce.get('genesis_data', {}) if not genesis_data: genesis_data = mk_genesis_data(env) self.chain = Chain(env=env, genesis=genesis_data, coinbase=coinbase, new_head_cb=self._on_new_head) header = self.chain.state.prev_headers[0] log.info('chain at', number=header.number) if 'genesis_hash' in sce: assert sce['genesis_hash'] == self.chain.genesis.hex_hash, \ "Genesis hash mismatch.\n Expected: %s\n Got: %s" % ( sce['genesis_hash'], self.chain.genesis.hex_hash) self.dao_challenges = dict() self.synchronizer = Synchronizer(self, force_sync=None) self.block_queue = Queue(maxsize=self.block_queue_size) # When the transaction_queue is modified, we must set # self._head_candidate_needs_updating to True in order to force the # head candidate to be updated. self.transaction_queue = TransactionQueue() self._head_candidate_needs_updating = True # Initialize a new head candidate. _ = self.head_candidate self.min_gasprice = 20 * 10**9 # TODO: better be an option to validator service? self.add_blocks_lock = False self.add_transaction_lock = gevent.lock.Semaphore() self.broadcast_filter = DuplicatesFilter() self.on_new_head_cbs = [] self.newblock_processing_times = deque(maxlen=1000) gevent.spawn_later(self.process_time_queue_period, self.process_time_queue)
class Validator(object): def __init__(self, key, genesis, network, valcode_addr=None, mining=False): self.key = key self.coinbase = utils.privtoaddr(self.key) self.chain = chain.Chain(genesis=genesis, reset_genesis=True, coinbase=self.coinbase, new_head_cb=self._on_new_head) self.mining = mining self.nonce = self.chain.state.get_nonce(self.coinbase) self.valcode_tx = None self.deposit_tx = None self.valcode_addr = valcode_addr self.prepares = dict() self.prev_prepare_epoch = 0 self.prev_commit_epoch = 0 self.epoch_length = self.chain.env.config['EPOCH_LENGTH'] # When the transaction_queue is modified, we must set # self._head_candidate_needs_updating to True in order to force the # head candidate to be updated. self.transaction_queue = TransactionQueue() self._head_candidate_needs_updating = True # Add validator to the network self.network = network self.network.join(self) @property def head_candidate(self): if self._head_candidate_needs_updating: self._head_candidate_needs_updating = False # Make a copy of self.transaction_queue because # make_head_candidate modifies it. txqueue = copy.deepcopy(self.transaction_queue) self._head_candidate, self._head_candidate_state = make_head_candidate( self.chain, txqueue=txqueue, timestamp=self.chain.state.timestamp + 14) return self._head_candidate def _on_new_head(self, block): self.transaction_queue = self.transaction_queue.diff( block.transactions) self._head_candidate_needs_updating = True def epoch_blockhash(self, state, epoch): if epoch == 0: return b'\x00' * 32 return state.prev_headers[epoch * self.epoch_length * -1 - 1].hash def get_recommended_casper_msg_contents(self, casper, validator_index): return \ casper.get_current_epoch(), casper.get_recommended_ancestry_hash(), \ casper.get_recommended_source_epoch(), casper.get_recommended_source_ancestry_hash(), \ casper.get_validators__prev_commit_epoch(validator_index) def get_validator_index(self, state): t = tester.State(state.ephemeral_clone()) t.state.gas_limit = 9999999999 casper = tester.ABIContract(t, casper_utils.casper_abi, self.chain.casper_address) if self.valcode_addr is None: raise Exception('Valcode address not set') try: return casper.get_validator_indexes(self.coinbase) except tester.TransactionFailed: return None # Check the state, and determine if we should commit or prepare def on_receive(self, msg): if isinstance(msg, block.Block): self.accept_block(msg) elif isinstance(msg, transactions.Transaction): self.accept_transaction(msg) def accept_block(self, block): self.chain.process_time_queue() if not self.chain.add_block(block): return # Verify this block is a part of our head chain if block != self.chain.get_block_by_number(block.header.number): return # Verify this block is far enough in our epoch if block.header.number % self.epoch_length < self.epoch_length // 3: return # Block is part of the head chain, so attempt to prepare & commit: # Create a poststate based on the blockhash we recieved post_state = self.chain.mk_poststate_of_blockhash(block.hash) post_state.gas_limit = 9999999999999 # Generate prepare & commit messages and broadcast if possible prepare_msg = self.generate_prepare_message(post_state) if prepare_msg: prepare_tx = self.mk_prepare_tx(prepare_msg) self.broadcast_transaction(prepare_tx) commit_msg = self.generate_commit_message(post_state) if commit_msg: commit_tx = self.mk_commit_tx(commit_msg) self.broadcast_transaction(commit_tx) def accept_transaction(self, tx): self.transaction_queue.add_transaction(tx) if self.mining: log.info('Mining tx: {}'.format(tx)) self.mine_and_broadcast_blocks(1) def broadcast_transaction(self, tx): log.info('Broadcasting transaction {} from validator {}'.format( str(tx), utils.encode_hex(self.valcode_addr))) self.network.broadcast(tx) def broadcast_newblock(self, block): log.info('Broadcasting block with hash: %s and txs: %s' % (utils.encode_hex(block.hash), str(block.transactions))) self.network.broadcast(block) def generate_prepare_message(self, state): epoch = state.block_number // self.epoch_length # NO_DBL_PREPARE: Don't prepare if we have already if epoch in self.prepares: return None # Create a Casper contract which we can use to get related values casper = tester.ABIContract(tester.State(state), casper_utils.casper_abi, self.chain.casper_address) # Get the ancestry hash and source ancestry hash validator_index = self.get_validator_index(state) _e, _a, _se, _sa, _pce = self.get_recommended_casper_msg_contents( casper, validator_index) # PREPARE_COMMIT_CONSISTENCY if _se < self.prev_commit_epoch and self.prev_commit_epoch < epoch: return None prepare_msg = casper_utils.mk_prepare(validator_index, _e, _a, _se, _sa, self.key) try: # Attempt to submit the prepare, to make sure that it is justified casper.prepare(prepare_msg) except tester.TransactionFailed: log.info( 'Prepare failed! Validator {} - hash justified {} - validator start {} - valcode addr {}' .format( self.get_validator_index(state), casper.get_consensus_messages__ancestry_hash_justified( epoch, _a), casper.get_validators__dynasty_start(validator_index), utils.encode_hex(self.valcode_addr))) return None # Save the prepare message we generated self.prepares[epoch] = prepare_msg # Save the highest source epoch we have referenced in our prepare's source epoch if epoch > self.prev_prepare_epoch: self.prev_prepare_epoch = epoch log.info( 'Prepare submitted: validator %d - epoch %d - prev_commit_epoch %d - hash %s' % (self.get_validator_index(state), epoch, self.prev_commit_epoch, utils.encode_hex(self.epoch_blockhash(state, epoch)))) return prepare_msg def generate_commit_message(self, state): epoch = state.block_number // self.epoch_length # PREPARE_COMMIT_CONSISTENCY if self.prev_prepare_epoch < self.prev_commit_epoch and self.prev_commit_epoch < epoch: return None # Create a Casper contract which we can use to get related values casper = tester.ABIContract(tester.State(state), casper_utils.casper_abi, self.chain.casper_address) validator_index = self.get_validator_index(state) _e, _a, _se, _sa, _pce = self.get_recommended_casper_msg_contents( casper, validator_index) # Make the commit message commit_msg = casper_utils.mk_commit(validator_index, _e, _a, _pce, self.key) try: # Attempt to submit the commit, to make sure that it doesn't doesn't violate DBL_PREPARE & it is justified casper.commit(commit_msg) except tester.TransactionFailed: log.info( 'Commit failed! Validator {} - blockhash {} - valcode addr {}'. format(self.get_validator_index(state), self.epoch_blockhash(state, epoch), utils.encode_hex(self.valcode_addr))) return None # Save the commit as now our last commit epoch log.info( 'Commit submitted: validator %d - epoch %d - prev_commit_epoch %d - hash %s' % (self.get_validator_index(state), epoch, self.prev_commit_epoch, utils.encode_hex(self.epoch_blockhash(state, epoch)))) self.prev_commit_epoch = epoch return commit_msg def mine_and_broadcast_blocks(self, number_of_blocks=1): for i in range(number_of_blocks): self._head_candidate_needs_updating = True block = Miner(self.head_candidate).mine(rounds=100, start_nonce=0) self.transaction_queue = self.transaction_queue.diff( block.transactions) self.broadcast_newblock(block) def broadcast_deposit(self): if not self.valcode_tx or not self.deposit_tx: # Generate transactions valcode_tx = self.mk_validation_code_tx() valcode_addr = utils.mk_contract_address(self.coinbase, self.nonce - 1) deposit_tx = self.mk_deposit_tx(3 * 10**18, valcode_addr) # Verify the transactions pass temp_state = self.chain.state.ephemeral_clone() valcode_success, o1 = apply_transaction(temp_state, valcode_tx) deposit_success, o2 = apply_transaction(temp_state, deposit_tx) if not (valcode_success and deposit_success): self.nonce = self.chain.state.get_nonce(self.coinbase) raise Exception('Valcode tx or deposit tx failed') self.valcode_tx = valcode_tx log.info('Valcode Tx generated: {}'.format(str(valcode_tx))) self.valcode_addr = valcode_addr self.deposit_tx = deposit_tx log.info('Deposit Tx generated: {}'.format(str(deposit_tx))) self.broadcast_transaction(self.valcode_tx) self.broadcast_transaction(self.deposit_tx) def broadcast_logout(self, login_logout_flag): epoch = self.chain.state.block_number // self.epoch_length # Generage the message logout_msg = casper_utils.mk_logout( self.get_validator_index(self.chain.state), epoch, self.key) # Generate transactions logout_tx = self.mk_logout(logout_msg) # Verify the transactions pass temp_state = self.chain.state.ephemeral_clone() logout_success, o1 = apply_transaction(temp_state, logout_tx) if not logout_success: self.nonce = self.chain.state.get_nonce(self.coinbase) raise Exception('Valcode tx or deposit tx failed') log.info('Login/logout Tx generated: {}'.format(str(logout_tx))) self.broadcast_transaction(logout_tx) def mk_transaction(self, to=b'\x00' * 20, value=0, data=b'', gasprice=tester.GASPRICE, startgas=tester.STARTGAS): tx = transactions.Transaction(self.nonce, gasprice, startgas, to, value, data).sign(self.key) self.nonce += 1 return tx def mk_validation_code_tx(self): valcode_tx = self.mk_transaction( '', 0, casper_utils.mk_validation_code(self.coinbase)) return valcode_tx def mk_deposit_tx(self, value, valcode_addr): casper_ct = abi.ContractTranslator(casper_utils.casper_abi) deposit_func = casper_ct.encode('deposit', [valcode_addr, self.coinbase]) deposit_tx = self.mk_transaction(self.chain.casper_address, value, deposit_func) return deposit_tx def mk_logout(self, login_logout_msg): casper_ct = abi.ContractTranslator(casper_utils.casper_abi) logout_func = casper_ct.encode('logout', [login_logout_msg]) logout_tx = self.mk_transaction(self.chain.casper_address, data=logout_func) return logout_tx def mk_prepare_tx(self, prepare_msg): casper_ct = abi.ContractTranslator(casper_utils.casper_abi) prepare_func = casper_ct.encode('prepare', [prepare_msg]) prepare_tx = self.mk_transaction(to=self.chain.casper_address, value=0, data=prepare_func) return prepare_tx def mk_commit_tx(self, commit_msg): casper_ct = abi.ContractTranslator(casper_utils.casper_abi) commit_func = casper_ct.encode('commit', [commit_msg]) commit_tx = self.mk_transaction(self.chain.casper_address, 0, commit_func) return commit_tx
def test_apply_collation_wrong_root(): """Test apply_collation with wrong roots in header test verify_execution_results """ shard_id = 1 t = chain(shard_id) # test 1 - arrange state = t.chain.shards[shard_id].state txqueue = TransactionQueue() tx1 = t.generate_shard_tx(shard_id, tester.k2, tester.a4, int(0.03 * utils.denoms.ether)) txqueue.add_transaction(tx1) # post_state_root collation = t.generate_collation(shard_id=1, coinbase=tester.a1, key=tester.k1, txqueue=txqueue) period_start_prevblock = t.chain.get_block( collation.header.period_start_prevhash) # Set wrong root collation.header.post_state_root = trie.BLANK_ROOT with pytest.raises(ValueError): collator.apply_collation(state, collation, period_start_prevblock) # test 2 - arrange state = t.chain.shards[shard_id].state txqueue = TransactionQueue() tx1 = t.generate_shard_tx(shard_id, tester.k2, tester.a4, int(0.03 * utils.denoms.ether)) txqueue.add_transaction(tx1) # receipts_root collation = t.generate_collation(shard_id=1, coinbase=tester.a1, key=tester.k1, txqueue=txqueue) period_start_prevblock = t.chain.get_block( collation.header.period_start_prevhash) # Set wrong root collation.header.receipts_root = trie.BLANK_ROOT with pytest.raises(ValueError): collator.apply_collation(state, collation, period_start_prevblock) # test 3 - arrange state = t.chain.shards[shard_id].state txqueue = TransactionQueue() tx1 = t.generate_shard_tx(shard_id, tester.k2, tester.a4, int(0.03 * utils.denoms.ether)) txqueue.add_transaction(tx1) # receipts_root collation = t.generate_collation(shard_id=1, coinbase=tester.a1, key=tester.k1, txqueue=txqueue) period_start_prevblock = t.chain.get_block( collation.header.period_start_prevhash) # Set wrong root collation.header.tx_list_root = trie.BLANK_ROOT with pytest.raises(ValueError): collator.apply_collation(state, collation, period_start_prevblock)
def make_block(chain, key, randao, vchash, skips): h, _ = make_head_candidate(chain, TransactionQueue(), timestamp=get_timestamp(chain, skips)) return sign_block(h, key, randao.get_parent(call_casper(chain.state, 'getRandao', [vchash])), vchash, skips)
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)]))