From 8517459e9063627adce225f3e906ae83d5ced0b4 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Fri, 1 Dec 2017 19:10:57 +0800 Subject: [PATCH 01/20] Stage 1 changes to validator service: cleanup Basic code cleanup: rename variables and functions, move a couple of things around to group related logic. Change a few conditionals so that they read better/make more intuitive sense. Improve log output, and make casper logging more verbose. --- .gitignore | 1 + pyethapp/validator_service.py | 135 ++++++++++++++++++---------------- 2 files changed, 72 insertions(+), 64 deletions(-) diff --git a/.gitignore b/.gitignore index 12b32f23..4a8db117 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ docs/_build # IDE crud .idea *.iml +.vscode diff --git a/pyethapp/validator_service.py b/pyethapp/validator_service.py index bc748721..812d5461 100644 --- a/pyethapp/validator_service.py +++ b/pyethapp/validator_service.py @@ -34,7 +34,6 @@ def __init__(self, app): self.latest_target_epoch = -1 self.latest_source_epoch = -1 self.epoch_length = self.chain.env.config['EPOCH_LENGTH'] - # self.chain.time = lambda: int(time.time()) if app.config['validate']: self.coinbase = app.services.accounts.find(app.config['validate'][0]) @@ -51,23 +50,25 @@ def on_new_head(self, block): return self.update() - def broadcast_deposit(self): - if not self.valcode_tx or not self.deposit_tx: - nonce = self.chain.state.get_nonce(self.coinbase.address) - # Generate transactions - valcode_tx = self.mk_validation_code_tx(nonce) - valcode_addr = utils.mk_contract_address(self.coinbase.address, nonce) - deposit_tx = self.mk_deposit_tx(self.deposit_size, valcode_addr, nonce+1) - # 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) - 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.chainservice.broadcast_transaction(valcode_tx) + def generate_valcode_and_deposit_tx(self): + nonce = self.chain.state.get_nonce(self.coinbase.address) + # Generate transactions + valcode_tx = self.mk_validation_code_tx(nonce) + valcode_addr = utils.mk_contract_address(self.coinbase.address, nonce) + deposit_tx = self.mk_deposit_tx(self.deposit_size, valcode_addr, nonce+1) + # 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) + + # We should never generate invalid txs + assert valcode_success and deposit_success + + 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))) def broadcast_logout(self, login_logout_flag): epoch = self.chain.state.block_number // self.epoch_length @@ -80,60 +81,47 @@ def broadcast_logout(self, login_logout_flag): temp_state = self.chain.state.ephemeral_clone() logout_success, o1 = apply_transaction(temp_state, logout_tx) if not logout_success: - raise Exception('Valcode tx or deposit tx failed') - log.info('Login/logout Tx generated: {}'.format(str(logout_tx))) + raise Exception('Logout tx failed') + log.info('[hybrid_casper] Broadcasting logout tx: {}'.format(str(logout_tx))) self.chainservice.broadcast_transaction(logout_tx) def update(self): if self.chain.state.get_balance(self.coinbase.address) < self.deposit_size: - log.info('Cannot login as validator: Not enough ETH!') + log.info('[hybrid_casper] Cannot login as validator: insufficient balance') return if not self.valcode_tx or not self.deposit_tx: - self.broadcast_deposit() - if not self.has_broadcasted_deposit and self.chain.state.get_code(self.valcode_addr): - log.info('Found code!') + self.generate_valcode_and_deposit_tx() + self.chainservice.broadcast_transaction(self.valcode_tx) + # LANE Can't this be done synchronously after generating the deposit tx? + # LANE: need to persist has_broadcasted_deposit so we don't do this twice + # Also need to allow login->logout->login, need to check if "logged in" + if self.chain.state.get_code(self.valcode_addr) and not self.has_broadcasted_deposit: + log.info('[hybrid_casper] Broadcasting deposit tx') self.chainservice.broadcast_transaction(self.deposit_tx) self.has_broadcasted_deposit = True - log.info('Validator index: {}'.format(self.get_validator_index(self.chain.state))) + log.info('[hybrid_casper] Validator index: {}'.format(self.get_validator_index(self.chain.state))) casper = tester.ABIContract(tester.State(self.chain.state.ephemeral_clone()), casper_utils.casper_abi, self.chain.casper_address) try: - log.info('&&& '.format()) - log.info('Vote percent: {} - Deposits: {} - Recommended Source: {} - Current Epoch: {}' + log.info('[hybrid_casper] Vote percent: {} - Deposits: {} - Recommended Source: {} - Current Epoch: {}' .format(casper.get_main_hash_voted_frac(), casper.get_total_curdyn_deposits(), casper.get_recommended_source_epoch(), casper.get_current_epoch())) is_justified = casper.get_votes__is_justified(casper.get_current_epoch()) is_finalized = casper.get_votes__is_finalized(casper.get_current_epoch()-1) if is_justified: - log.info('Justified epoch: {}'.format(casper.get_current_epoch())) + log.info('[hybrid_casper] Justified epoch: {}'.format(casper.get_current_epoch())) if is_finalized: - log.info('Finalized epoch: {}'.format(casper.get_current_epoch()-1)) - except: - log.info('&&& Vote frac failed') + log.info('[hybrid_casper] Finalized epoch: {}'.format(casper.get_current_epoch()-1)) + except e: + log.info('[hybrid_casper] Vote frac failed: {}'.format(e)) - # Vote # Generate vote messages and broadcast if possible vote_msg = self.generate_vote_message() if vote_msg: vote_tx = self.mk_vote_tx(vote_msg) + log.info('[hybrid_casper] Broadcasting vote: {}'.format(str(vote_tx))) self.chainservice.broadcast_transaction(vote_tx) - log.info('Sent vote! Tx: {}'.format(str(vote_tx))) - - def get_recommended_casper_msg_contents(self, casper, validator_index): - current_epoch = casper.get_current_epoch() - if current_epoch == 0: - return None, None, None - # NOTE: Using `epoch_blockhash` because currently calls to `blockhash` within contracts - # in the ephemeral state are off by one, so we can't use `get_recommended_target_hash()` :( - target_hash = self.epoch_blockhash(current_epoch) - source_epoch = casper.get_recommended_source_epoch() - return target_hash, current_epoch, source_epoch - - def epoch_blockhash(self, epoch): - if epoch == 0: - return b'\x00' * 32 - return self.chain.get_block_by_number(epoch*self.epoch_length-1).hash def is_logged_in(self, casper, target_epoch, validator_index): start_dynasty = casper.get_validators__start_dynasty(validator_index) @@ -142,42 +130,61 @@ def is_logged_in(self, casper, target_epoch, validator_index): past_dynasty = current_dynasty - 1 in_current_dynasty = ((start_dynasty <= current_dynasty) and (current_dynasty < end_dynasty)) in_prev_dynasty = ((start_dynasty <= past_dynasty) and (past_dynasty < end_dynasty)) - if not (in_current_dynasty or in_prev_dynasty): - return False - return True + return (in_current_dynasty or in_prev_dynasty) def generate_vote_message(self): state = self.chain.state.ephemeral_clone() - epoch = state.block_number // self.epoch_length # TODO: Add logic which waits until a specific blockheight before submitting vote, something like: # if state.block_number % self.epoch_length < 1: # return None + target_epoch = state.block_number // self.epoch_length # NO_DBL_VOTE: Don't vote if we have already - if epoch in self.votes: + if target_epoch in self.votes: + log.info('[hybrid_casper] Already voted for this epoch as target ({}), not voting again'.format(target_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) # Get the ancestry hash and source ancestry hash validator_index = self.get_validator_index(state) - target_hash, epoch, source_epoch = self.get_recommended_casper_msg_contents(casper, validator_index) + target_hash, target_epoch, source_epoch = self.get_recommended_casper_msg_contents(casper, validator_index) if target_hash is None: + log.info('[hybrid_casper] Failed to get target hash, not voting') return None - # Prevent NO_SURROUND slash - if epoch < self.latest_target_epoch or source_epoch < self.latest_source_epoch: + # Prevent NO_SURROUND slash. Note that this is a little over-conservative and thus suboptimal. + # It strictly only allows votes for later sources and targets than we've already voted for. + # It avoids voting in (rare) cases where it would be safe to do so, e.g., with an earlier + # source _and_ an earlier target. + if target_epoch < self.latest_target_epoch or source_epoch < self.latest_source_epoch: + log.info('[hybrid_casper] Not voting to avoid NO_SURROUND slash') return None - # Verify that we are either in the current dynasty or prev dynasty -- assert in_current_dynasty or in_prev_dynasty - if not self.is_logged_in(casper, epoch, validator_index): - log.info('Validator not logged in yet!') + # Assert that we are logged in + if not self.is_logged_in(casper, target_epoch, validator_index): + log.info('[hybrid_casper] Validator not logged in, not voting') return None - vote_msg = casper_utils.mk_vote(validator_index, target_hash, epoch, source_epoch, self.coinbase.privkey) + vote_msg = casper_utils.mk_vote(validator_index, target_hash, target_epoch, source_epoch, self.coinbase.privkey) # Save the vote message we generated - self.votes[epoch] = vote_msg - self.latest_target_epoch = epoch + self.votes[target_epoch] = vote_msg + self.latest_target_epoch = target_epoch self.latest_source_epoch = source_epoch - log.info('Vote submitted: validator %d - epoch %d - source_epoch %d - hash %s' % - (self.get_validator_index(state), epoch, source_epoch, utils.encode_hex(target_hash))) + log.info('[hybrid_casper] Generated vote: validator %d - epoch %d - source_epoch %d - hash %s' % + (self.get_validator_index(state), target_epoch, source_epoch, utils.encode_hex(target_hash))) return vote_msg + def get_recommended_casper_msg_contents(self, casper, validator_index): + current_epoch = casper.get_current_epoch() + if current_epoch == 0: + return None, None, None + # NOTE: Using `epoch_blockhash` because currently calls to `blockhash` within contracts + # in the ephemeral state are off by one, so we can't use `get_recommended_target_hash()` :( + target_hash = self.epoch_blockhash(current_epoch) + source_epoch = casper.get_recommended_source_epoch() + return target_hash, current_epoch, source_epoch + + def epoch_blockhash(self, epoch): + if epoch == 0: + return b'\x00' * 32 + return self.chain.get_block_by_number(epoch*self.epoch_length-1).hash + def get_validator_index(self, state): t = tester.State(state.ephemeral_clone()) casper = tester.ABIContract(t, casper_utils.casper_abi, self.chain.casper_address) From db626ab8f0ba159ed0432f09881b3f213e5c5ca7 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Fri, 1 Dec 2017 19:11:24 +0800 Subject: [PATCH 02/20] Stage 2 changes: Wait n blocks before voting --- pyethapp/validator_service.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyethapp/validator_service.py b/pyethapp/validator_service.py index 812d5461..27a34de7 100644 --- a/pyethapp/validator_service.py +++ b/pyethapp/validator_service.py @@ -134,10 +134,13 @@ def is_logged_in(self, casper, target_epoch, validator_index): def generate_vote_message(self): state = self.chain.state.ephemeral_clone() - # TODO: Add logic which waits until a specific blockheight before submitting vote, something like: - # if state.block_number % self.epoch_length < 1: - # return None target_epoch = state.block_number // self.epoch_length + # Wait until a specific blockheight before submitting vote + wait_blocks = self.epoch_length // 4 + epoch_block_number = state.block_number % self.epoch_length + if epoch_block_number < wait_blocks: + log.info('[hybrid_casper] Waiting for epoch block {} to submit vote, now block {}'.format(wait_blocks, epoch_block_number)) + return None # NO_DBL_VOTE: Don't vote if we have already if target_epoch in self.votes: log.info('[hybrid_casper] Already voted for this epoch as target ({}), not voting again'.format(target_epoch)) From e5366d77871055b0b9785749d697fe5f441b7aa7 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Sat, 2 Dec 2017 10:41:19 +0800 Subject: [PATCH 03/20] More general code cleanup, no logic changes Break up generate valcode, deposit tx. Rename a variable to use a clearer name. Make logic clearer. --- pyethapp/validator_service.py | 63 ++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/pyethapp/validator_service.py b/pyethapp/validator_service.py index 27a34de7..64930ae8 100644 --- a/pyethapp/validator_service.py +++ b/pyethapp/validator_service.py @@ -29,7 +29,7 @@ def __init__(self, app): self.deposit_tx = None self.deposit_size = 5000 * 10**18 self.valcode_addr = None - self.has_broadcasted_deposit = False + self.did_broadcast_deposit = False self.votes = dict() self.latest_target_epoch = -1 self.latest_source_epoch = -1 @@ -50,23 +50,33 @@ def on_new_head(self, block): return self.update() - def generate_valcode_and_deposit_tx(self): + def generate_valcode_tx(self): nonce = self.chain.state.get_nonce(self.coinbase.address) - # Generate transactions + # Generate transaction valcode_tx = self.mk_validation_code_tx(nonce) valcode_addr = utils.mk_contract_address(self.coinbase.address, nonce) - deposit_tx = self.mk_deposit_tx(self.deposit_size, valcode_addr, nonce+1) - # Verify the transactions pass + # Verify the transaction passes 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) # We should never generate invalid txs - assert valcode_success and deposit_success + assert valcode_success self.valcode_tx = valcode_tx log.info('Valcode Tx generated: {}'.format(str(valcode_tx))) self.valcode_addr = valcode_addr + + def generate_deposit_tx(self): + nonce = self.chain.state.get_nonce(self.coinbase.address) + # Generate transaction + deposit_tx = self.mk_deposit_tx(self.deposit_size, valcode_addr, nonce+1) + # Verify the transaction passes + temp_state = self.chain.state.ephemeral_clone() + deposit_success, o2 = apply_transaction(temp_state, deposit_tx) + + # We should never generate invalid txs + assert deposit_success + self.deposit_tx = deposit_tx log.info('Deposit Tx generated: {}'.format(str(deposit_tx))) @@ -86,20 +96,41 @@ def broadcast_logout(self, login_logout_flag): self.chainservice.broadcast_transaction(logout_tx) def update(self): + # Make sure we have enough ETH to deposit if self.chain.state.get_balance(self.coinbase.address) < self.deposit_size: log.info('[hybrid_casper] Cannot login as validator: insufficient balance') return - if not self.valcode_tx or not self.deposit_tx: - self.generate_valcode_and_deposit_tx() + + # Note about valcode and deposit transactions: these need to be broadcast in + # a particular order, valcode first. In order to ensure this, we check that + # the valcode tx exists before we attempt to broadcast the deposit tx. + + # Generate valcode and deposit transactions + elif not self.valcode_tx: + self.generate_valcode_tx() + log.info('[hybrid_casper] Broadcasting valcode tx and waiting for it to be mined') self.chainservice.broadcast_transaction(self.valcode_tx) - # LANE Can't this be done synchronously after generating the deposit tx? - # LANE: need to persist has_broadcasted_deposit so we don't do this twice - # Also need to allow login->logout->login, need to check if "logged in" - if self.chain.state.get_code(self.valcode_addr) and not self.has_broadcasted_deposit: + + # Wait for it to be mined + return + + # No point in going beyond this point until the valcode tx is mined + elif not self.chain.state.get_code(self.valcode_addr): + log.info('[hybrid_casper] Waiting for valcode tx to be mined') + return + + # Now we can broadcast the deposit + elif not self.did_broadcast_deposit: + self.generate_deposit_tx() log.info('[hybrid_casper] Broadcasting deposit tx') self.chainservice.broadcast_transaction(self.deposit_tx) - self.has_broadcasted_deposit = True - log.info('[hybrid_casper] Validator index: {}'.format(self.get_validator_index(self.chain.state))) + self.did_broadcast_deposit = True + + # Wait for it to be mined + return + + # We are clear to vote! + log.info('[hybrid_casper] Active validator index: {}'.format(self.get_validator_index(self.chain.state))) casper = tester.ABIContract(tester.State(self.chain.state.ephemeral_clone()), casper_utils.casper_abi, self.chain.casper_address) @@ -122,6 +153,8 @@ def update(self): vote_tx = self.mk_vote_tx(vote_msg) log.info('[hybrid_casper] Broadcasting vote: {}'.format(str(vote_tx))) self.chainservice.broadcast_transaction(vote_tx) + else: + log.info('[hybrid_casper] Not voting this round') def is_logged_in(self, casper, target_epoch, validator_index): start_dynasty = casper.get_validators__start_dynasty(validator_index) From 9568ed462f0c02164fd03f0bfc9999d86e0b18c7 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Sun, 3 Dec 2017 12:22:44 +0800 Subject: [PATCH 04/20] Add barebones validator_service test --- pyethapp/tests/test_validator_service.py | 76 ++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 pyethapp/tests/test_validator_service.py diff --git a/pyethapp/tests/test_validator_service.py b/pyethapp/tests/test_validator_service.py new file mode 100644 index 00000000..2861d376 --- /dev/null +++ b/pyethapp/tests/test_validator_service.py @@ -0,0 +1,76 @@ +from ethereum.config import default_config +from pyethapp.config import update_config_with_defaults, get_default_config +from ethereum.slogging import get_logger +from ethereum.tools import tester +import pytest +import shutil +import tempfile +from devp2p.app import BaseApp +from pyethapp.eth_service import ChainService +from pyethapp.db_service import DBService +from pyethapp.accounts import Account, AccountsService +from pyethapp.validator_service import ValidatorService +from ethereum.utils import encode_hex + +log = get_logger('tests.validator_service') + +@pytest.fixture() +def app(request): + config = { + 'accounts': { + 'keystore_dir': tempfile.mkdtemp(), + }, + 'data_dir': str(tempfile.gettempdir()), + 'db': {'implementation': 'EphemDB'}, + 'pow': {'activated': False}, + 'p2p': { + 'min_peers': 0, + 'max_peers': 0, + 'listen_port': 29873 + }, + 'discovery': { + 'boostrap_nodes': [], + 'listen_port': 29873 + }, + 'eth': { + 'block': { # reduced difficulty, increased gas limit, allocations to test accounts + 'GENESIS_DIFFICULTY': 1, + 'BLOCK_DIFF_FACTOR': 2, # greater than difficulty, thus difficulty is constant + 'GENESIS_GAS_LIMIT': 3141592, + 'GENESIS_INITIAL_ALLOC': { + encode_hex(tester.accounts[0]): {'balance': 10**24}, + } + } + }, + 'jsonrpc': {'listen_port': 29873}, + 'validate': [encode_hex(tester.accounts[0])], + } + + services = [ + DBService, + # AccountsService, + ChainService, + ValidatorService, + ] + update_config_with_defaults(config, get_default_config([BaseApp] + services)) + update_config_with_defaults(config, {'eth': {'block': default_config}}) + app = BaseApp(config) + + # Add AccountsService first and initialize with coinbase account + AccountsService.register_with_app(app) + app.services.accounts.add_account(Account.new('', tester.keys[0]), store=False) + + for service in services: + service.register_with_app(app) + + def fin(): + # cleanup temporary keystore directory + assert app.config['accounts']['keystore_dir'].startswith(tempfile.gettempdir()) + shutil.rmtree(app.config['accounts']['keystore_dir']) + log.debug('cleaned temporary keystore dir', dir=app.config['accounts']['keystore_dir']) + request.addfinalizer(fin) + + return app + +def test_foo(app): + assert True From 3e918f7504900d07602958851eabea4cd08450ea Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Sun, 3 Dec 2017 19:33:34 +0800 Subject: [PATCH 05/20] Mining is working in validator_test --- pyethapp/tests/test_validator_service.py | 112 +++++++++++++++++++---- pyethapp/validator_service.py | 3 + 2 files changed, 97 insertions(+), 18 deletions(-) diff --git a/pyethapp/tests/test_validator_service.py b/pyethapp/tests/test_validator_service.py index 2861d376..8eace642 100644 --- a/pyethapp/tests/test_validator_service.py +++ b/pyethapp/tests/test_validator_service.py @@ -1,26 +1,87 @@ -from ethereum.config import default_config -from pyethapp.config import update_config_with_defaults, get_default_config -from ethereum.slogging import get_logger -from ethereum.tools import tester +from itertools import count import pytest import shutil import tempfile -from devp2p.app import BaseApp +from devp2p.service import BaseService +from ethereum.config import default_config +from pyethapp.config import update_config_with_defaults, get_default_config +from ethereum.slogging import get_logger, configure_logging +from ethereum.tools import tester +from ethereum.pow.ethpow import mine +from ethereum.tests.hybrid_casper.testing_lang import TestLangHybrid +from ethereum.utils import encode_hex +# from devp2p.app import BaseApp +from pyethapp.app import EthApp from pyethapp.eth_service import ChainService from pyethapp.db_service import DBService from pyethapp.accounts import Account, AccountsService from pyethapp.validator_service import ValidatorService -from ethereum.utils import encode_hex +from pyethapp.pow_service import PoWService log = get_logger('tests.validator_service') +configure_logging('validator:debug') + +class PeerManagerMock(BaseService): + name = 'peermanager' + + def broadcast(*args, **kwargs): + pass @pytest.fixture() -def app(request): +def test_app(request, tmpdir): + class TestApp(EthApp): + + # def start(self): + # super(TestApp, self).start() + # log.debug('adding test accounts') + # # high balance account + # # self.services.accounts.add_account(Account.new('', tester.keys[0]), store=False) + # # # low balance account + # # self.services.accounts.add_account(Account.new('', tester.keys[1]), store=False) + # # # locked account + # # locked_account = Account.new('', tester.keys[2]) + # # locked_account.lock() + # # self.services.accounts.add_account(locked_account, store=False) + # assert set(acct.address for acct in self.services.accounts) == set(tester.accounts[:1]) + + def mine_next_block(self): + """Mine until a valid nonce is found. + + :returns: the new head + """ + log.debug('mining next block') + block = self.services.chain.head_candidate + chain = self.services.chain.chain + head_number = chain.head.number + delta_nonce = 10**6 + for start_nonce in count(0, delta_nonce): + bin_nonce, mixhash = mine(block.number, block.difficulty, block.mining_hash, + start_nonce=start_nonce, rounds=delta_nonce) + if bin_nonce: + break + self.services.chain.add_mined_block(block) + self.services.pow.recv_found_nonce(bin_nonce, mixhash, block.mining_hash) + if len(chain.time_queue) > 0: + # If we mine two blocks within one second, pyethereum will + # force the new block's timestamp to be in the future (see + # ethereum1_setup_block()), and when we try to add that block + # to the chain (via Chain.add_block()), it will be put in a + # queue for later processing. Since we need to ensure the + # block has been added before we continue the test, we + # have to manually process the time queue. + log.debug('block mined too fast, processing time queue') + chain.process_time_queue(new_time=block.timestamp) + log.debug('block mined') + assert chain.head.difficulty == 1 + assert chain.head.number == head_number + 1 + return chain.head + config = { - 'accounts': { - 'keystore_dir': tempfile.mkdtemp(), - }, - 'data_dir': str(tempfile.gettempdir()), + # 'accounts': { + # 'keystore_dir': tempfile.mkdtemp(), + # }, + # 'data_dir': str(tempfile.gettempdir()), + 'data_dir': str(tmpdir), 'db': {'implementation': 'EphemDB'}, 'pow': {'activated': False}, 'p2p': { @@ -44,17 +105,21 @@ def app(request): }, 'jsonrpc': {'listen_port': 29873}, 'validate': [encode_hex(tester.accounts[0])], + # 'validate': [tester.accounts[0]], + # 'validate': [], } services = [ DBService, # AccountsService, ChainService, + PoWService, + PeerManagerMock, ValidatorService, ] - update_config_with_defaults(config, get_default_config([BaseApp] + services)) + update_config_with_defaults(config, get_default_config([TestApp] + services)) update_config_with_defaults(config, {'eth': {'block': default_config}}) - app = BaseApp(config) + app = TestApp(config) # Add AccountsService first and initialize with coinbase account AccountsService.register_with_app(app) @@ -64,13 +129,24 @@ def app(request): service.register_with_app(app) def fin(): - # cleanup temporary keystore directory - assert app.config['accounts']['keystore_dir'].startswith(tempfile.gettempdir()) - shutil.rmtree(app.config['accounts']['keystore_dir']) - log.debug('cleaned temporary keystore dir', dir=app.config['accounts']['keystore_dir']) + log.debug('stopping test app') + app.stop() + # # cleanup temporary keystore directory + # assert app.config['accounts']['keystore_dir'].startswith(tempfile.gettempdir()) + # shutil.rmtree(app.config['accounts']['keystore_dir']) + # log.debug('cleaned temporary keystore dir', dir=app.config['accounts']['keystore_dir']) request.addfinalizer(fin) return app -def test_foo(app): +def test_generate_valcode(test_app): + # test = TestLangHybrid(5, 25, 0.02, 0.002) + # test.parse('B B') + # app.services.validator.chain = test.t.chain + # app.services.validator.chain.on_new_head_cbs.append(app.services.validator.on_new_head) + # test.parse('B1') + # test_chain = tester.Chain() + # test_chain.mine(30) + test_app.mine_next_block() + assert True diff --git a/pyethapp/validator_service.py b/pyethapp/validator_service.py index 64930ae8..0f70804a 100644 --- a/pyethapp/validator_service.py +++ b/pyethapp/validator_service.py @@ -45,8 +45,10 @@ def __init__(self, app): def on_new_head(self, block): if not self.validating: + log.info('[hybrid_casper] not validating, not updating') return if self.app.services.chain.is_syncing: + log.info('[hybrid_casper] chain syncing, not updating') return self.update() @@ -96,6 +98,7 @@ def broadcast_logout(self, login_logout_flag): self.chainservice.broadcast_transaction(logout_tx) def update(self): + log.info('[hybrid_casper] validator {} updating'.format(self)) # Make sure we have enough ETH to deposit if self.chain.state.get_balance(self.coinbase.address) < self.deposit_size: log.info('[hybrid_casper] Cannot login as validator: insufficient balance') From bf3785921fc9dc99e5981b93e78944de6aaaae83 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Sun, 3 Dec 2017 23:37:37 +0800 Subject: [PATCH 06/20] Successfully linked ethereum testing_lang chain --- pyethapp/tests/test_validator_service.py | 48 +++++++++--------------- 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/pyethapp/tests/test_validator_service.py b/pyethapp/tests/test_validator_service.py index 8eace642..3426ae7c 100644 --- a/pyethapp/tests/test_validator_service.py +++ b/pyethapp/tests/test_validator_service.py @@ -6,11 +6,11 @@ from ethereum.config import default_config from pyethapp.config import update_config_with_defaults, get_default_config from ethereum.slogging import get_logger, configure_logging +from ethereum.hybrid_casper import chain as hybrid_casper_chain from ethereum.tools import tester from ethereum.pow.ethpow import mine from ethereum.tests.hybrid_casper.testing_lang import TestLangHybrid from ethereum.utils import encode_hex -# from devp2p.app import BaseApp from pyethapp.app import EthApp from pyethapp.eth_service import ChainService from pyethapp.db_service import DBService @@ -30,20 +30,6 @@ def broadcast(*args, **kwargs): @pytest.fixture() def test_app(request, tmpdir): class TestApp(EthApp): - - # def start(self): - # super(TestApp, self).start() - # log.debug('adding test accounts') - # # high balance account - # # self.services.accounts.add_account(Account.new('', tester.keys[0]), store=False) - # # # low balance account - # # self.services.accounts.add_account(Account.new('', tester.keys[1]), store=False) - # # # locked account - # # locked_account = Account.new('', tester.keys[2]) - # # locked_account.lock() - # # self.services.accounts.add_account(locked_account, store=False) - # assert set(acct.address for acct in self.services.accounts) == set(tester.accounts[:1]) - def mine_next_block(self): """Mine until a valid nonce is found. @@ -77,10 +63,6 @@ def mine_next_block(self): return chain.head config = { - # 'accounts': { - # 'keystore_dir': tempfile.mkdtemp(), - # }, - # 'data_dir': str(tempfile.gettempdir()), 'data_dir': str(tmpdir), 'db': {'implementation': 'EphemDB'}, 'pow': {'activated': False}, @@ -105,8 +87,6 @@ def mine_next_block(self): }, 'jsonrpc': {'listen_port': 29873}, 'validate': [encode_hex(tester.accounts[0])], - # 'validate': [tester.accounts[0]], - # 'validate': [], } services = [ @@ -131,22 +111,28 @@ def mine_next_block(self): def fin(): log.debug('stopping test app') app.stop() - # # cleanup temporary keystore directory - # assert app.config['accounts']['keystore_dir'].startswith(tempfile.gettempdir()) - # shutil.rmtree(app.config['accounts']['keystore_dir']) - # log.debug('cleaned temporary keystore dir', dir=app.config['accounts']['keystore_dir']) request.addfinalizer(fin) + # app.start() return app def test_generate_valcode(test_app): - # test = TestLangHybrid(5, 25, 0.02, 0.002) - # test.parse('B B') - # app.services.validator.chain = test.t.chain - # app.services.validator.chain.on_new_head_cbs.append(app.services.validator.on_new_head) - # test.parse('B1') + test = TestLangHybrid(5, 25, 0.02, 0.002) + test.parse('B B') + + # Create a smart chain object + test.t.chain = hybrid_casper_chain.Chain(genesis=test.genesis, new_head_cb=test_app.services.validator.on_new_head) + + # print(test.t.chain) + # test.t.chain.on_new_head_cbs.append(app.services.validator.on_new_head) + test_app.services.chain.chain = test.t.chain + # print("on_new_head is: {}".format(test_app.services.chain.on_new_head_cbs)) + # test_app.services.chain.on_new_head_cbs(app.services.validator) + # test_app.services.validator.chain = test.t.chain + # test_app.services.validator.chain.on_new_head_cbs.append(app.services.validator.on_new_head) + test.parse('B1') # test_chain = tester.Chain() # test_chain.mine(30) - test_app.mine_next_block() + # test_app.mine_next_block() assert True From acf78142989c4920da5f379d60c9b355e2ec8993 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Mon, 4 Dec 2017 09:12:17 +0800 Subject: [PATCH 07/20] Cleanup and simplification --- pyethapp/tests/test_validator_service.py | 67 ++---------------------- 1 file changed, 4 insertions(+), 63 deletions(-) diff --git a/pyethapp/tests/test_validator_service.py b/pyethapp/tests/test_validator_service.py index 3426ae7c..e9c7d4c5 100644 --- a/pyethapp/tests/test_validator_service.py +++ b/pyethapp/tests/test_validator_service.py @@ -8,7 +8,6 @@ from ethereum.slogging import get_logger, configure_logging from ethereum.hybrid_casper import chain as hybrid_casper_chain from ethereum.tools import tester -from ethereum.pow.ethpow import mine from ethereum.tests.hybrid_casper.testing_lang import TestLangHybrid from ethereum.utils import encode_hex from pyethapp.app import EthApp @@ -29,52 +28,9 @@ def broadcast(*args, **kwargs): @pytest.fixture() def test_app(request, tmpdir): - class TestApp(EthApp): - def mine_next_block(self): - """Mine until a valid nonce is found. - - :returns: the new head - """ - log.debug('mining next block') - block = self.services.chain.head_candidate - chain = self.services.chain.chain - head_number = chain.head.number - delta_nonce = 10**6 - for start_nonce in count(0, delta_nonce): - bin_nonce, mixhash = mine(block.number, block.difficulty, block.mining_hash, - start_nonce=start_nonce, rounds=delta_nonce) - if bin_nonce: - break - self.services.chain.add_mined_block(block) - self.services.pow.recv_found_nonce(bin_nonce, mixhash, block.mining_hash) - if len(chain.time_queue) > 0: - # If we mine two blocks within one second, pyethereum will - # force the new block's timestamp to be in the future (see - # ethereum1_setup_block()), and when we try to add that block - # to the chain (via Chain.add_block()), it will be put in a - # queue for later processing. Since we need to ensure the - # block has been added before we continue the test, we - # have to manually process the time queue. - log.debug('block mined too fast, processing time queue') - chain.process_time_queue(new_time=block.timestamp) - log.debug('block mined') - assert chain.head.difficulty == 1 - assert chain.head.number == head_number + 1 - return chain.head - config = { 'data_dir': str(tmpdir), 'db': {'implementation': 'EphemDB'}, - 'pow': {'activated': False}, - 'p2p': { - 'min_peers': 0, - 'max_peers': 0, - 'listen_port': 29873 - }, - 'discovery': { - 'boostrap_nodes': [], - 'listen_port': 29873 - }, 'eth': { 'block': { # reduced difficulty, increased gas limit, allocations to test accounts 'GENESIS_DIFFICULTY': 1, @@ -85,7 +41,7 @@ def mine_next_block(self): } } }, - 'jsonrpc': {'listen_port': 29873}, + # 'jsonrpc': {'listen_port': 29873}, 'validate': [encode_hex(tester.accounts[0])], } @@ -93,13 +49,13 @@ def mine_next_block(self): DBService, # AccountsService, ChainService, - PoWService, + # PoWService, PeerManagerMock, ValidatorService, ] - update_config_with_defaults(config, get_default_config([TestApp] + services)) + update_config_with_defaults(config, get_default_config([EthApp] + services)) update_config_with_defaults(config, {'eth': {'block': default_config}}) - app = TestApp(config) + app = EthApp(config) # Add AccountsService first and initialize with coinbase account AccountsService.register_with_app(app) @@ -108,12 +64,6 @@ def mine_next_block(self): for service in services: service.register_with_app(app) - def fin(): - log.debug('stopping test app') - app.stop() - request.addfinalizer(fin) - - # app.start() return app def test_generate_valcode(test_app): @@ -123,16 +73,7 @@ def test_generate_valcode(test_app): # Create a smart chain object test.t.chain = hybrid_casper_chain.Chain(genesis=test.genesis, new_head_cb=test_app.services.validator.on_new_head) - # print(test.t.chain) - # test.t.chain.on_new_head_cbs.append(app.services.validator.on_new_head) test_app.services.chain.chain = test.t.chain - # print("on_new_head is: {}".format(test_app.services.chain.on_new_head_cbs)) - # test_app.services.chain.on_new_head_cbs(app.services.validator) - # test_app.services.validator.chain = test.t.chain - # test_app.services.validator.chain.on_new_head_cbs.append(app.services.validator.on_new_head) test.parse('B1') - # test_chain = tester.Chain() - # test_chain.mine(30) - # test_app.mine_next_block() assert True From f9641da8a9c8fe491e2ea68834a9b20b6673fd4a Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Tue, 5 Dec 2017 14:56:50 +0800 Subject: [PATCH 08/20] Improve initialization of Casper FFG params Move them into config where they belong. Remove hard-coded default allocation, which was overwriting the allocation in the config. --- pyethapp/eth_service.py | 23 ++++++++++------------- pyethapp/tests/test_validator_service.py | 12 +++++++++--- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/pyethapp/eth_service.py b/pyethapp/eth_service.py index 56216b00..a52dc666 100644 --- a/pyethapp/eth_service.py +++ b/pyethapp/eth_service.py @@ -166,21 +166,18 @@ def __init__(self, app): super(ChainService, self).__init__(app) log.info('initializing chain') coinbase = app.services.accounts.coinbase - # env = Env(self.db, sce['block']) + env = Env(self.db, sce['block']) - # genesis_data = sce.get('genesis_data', {}) - # if not genesis_data: + genesis_data = sce.get('genesis_data', {}) + if not genesis_data: + # genesis_data = casper_utils.make_casper_genesis(ALLOC, 10, 100, 0.02, 0.002) + genesis_data = casper_utils.make_casper_genesis(env) # genesis_data = mk_genesis_data(env) - # self.chain = Chain( - # env=env, genesis=genesis_data, coinbase=coinbase, - # new_head_cb=self._on_new_head) - ALLOC = {} - # TODO: Remove this dumb default alloc - ALLOC[decode_hex('7d577a597b2742b498cb5cf0c26cdcd726d39e6e')] = {'balance': 50000*10**19} - ALLOC[decode_hex('b96611e02f9eff3c8afc6226d4ebfa81a821547c')] = {'balance': 50000*10**19} - ALLOC[decode_hex('b42e5cafe87d951c5cf0022bfdab06fe56ba2ad2')] = {'balance': 5 * 10**9 * 10**19} - genesis_data = casper_utils.make_casper_genesis(ALLOC, 10, 100, 0.02, 0.002) - self.chain = Chain(genesis=genesis_data, reset_genesis=True, coinbase=coinbase, new_head_cb=self._on_new_head) + self.chain = Chain( + genesis=genesis_data, + reset_genesis=True, + 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: diff --git a/pyethapp/tests/test_validator_service.py b/pyethapp/tests/test_validator_service.py index e9c7d4c5..89282a80 100644 --- a/pyethapp/tests/test_validator_service.py +++ b/pyethapp/tests/test_validator_service.py @@ -38,9 +38,15 @@ def test_app(request, tmpdir): 'GENESIS_GAS_LIMIT': 3141592, 'GENESIS_INITIAL_ALLOC': { encode_hex(tester.accounts[0]): {'balance': 10**24}, - } + }, + # Casper FFG stuff + 'EPOCH_LENGTH': 10, + 'WITHDRAWAL_DELAY': 100, + 'BASE_INTEREST_FACTOR': 0.02, + 'BASE_PENALTY_FACTOR': 0.002, } }, + # 'genesis_data': {}, # 'jsonrpc': {'listen_port': 29873}, 'validate': [encode_hex(tester.accounts[0])], } @@ -70,9 +76,9 @@ def test_generate_valcode(test_app): test = TestLangHybrid(5, 25, 0.02, 0.002) test.parse('B B') - # Create a smart chain object + # Create a smart chain object: this ties the chain used in the tester + # to the validator chain. test.t.chain = hybrid_casper_chain.Chain(genesis=test.genesis, new_head_cb=test_app.services.validator.on_new_head) - test_app.services.chain.chain = test.t.chain test.parse('B1') From 43f031c28828b136d8c09f2331c1996685ec71f4 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Tue, 5 Dec 2017 16:54:40 +0800 Subject: [PATCH 09/20] Add mocked ChainService The only part of ChainService that the ValidatorService is using is add_transaction. Mock the ChainService and intercept this call to relay the transaction into the tester for test purposes. Rename some variables to be clearer. --- pyethapp/tests/test_validator_service.py | 66 +++++++++++++++++++----- pyethapp/validator_service.py | 12 ++--- 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/pyethapp/tests/test_validator_service.py b/pyethapp/tests/test_validator_service.py index 89282a80..912ab394 100644 --- a/pyethapp/tests/test_validator_service.py +++ b/pyethapp/tests/test_validator_service.py @@ -1,4 +1,5 @@ from itertools import count +import functools import pytest import shutil import tempfile @@ -11,14 +12,44 @@ from ethereum.tests.hybrid_casper.testing_lang import TestLangHybrid from ethereum.utils import encode_hex from pyethapp.app import EthApp -from pyethapp.eth_service import ChainService from pyethapp.db_service import DBService from pyethapp.accounts import Account, AccountsService from pyethapp.validator_service import ValidatorService from pyethapp.pow_service import PoWService log = get_logger('tests.validator_service') -configure_logging('validator:debug') +configure_logging('validator:debug,eth.chainservice:debug') + +class ChainServiceMock(BaseService): + name = 'chain' + + def __init__(self, app, test): + super(ChainServiceMock, self).__init__(app) + + class InnerNewHeadCbsMock(object): + def __init__(self, outer): + self.outer = outer + + def append(self, cb): + self.outer.chain.new_head_cb = cb + + # Save as interface to tester + self.test = test + self.on_new_head_cbs = InnerNewHeadCbsMock(self) + # self.chain = hybrid_casper_chain.Chain(genesis=test.genesis, new_head_cb=self.app.services.validator.on_new_head) + self.chain = hybrid_casper_chain.Chain(genesis=test.genesis) + self.is_syncing = False + + def add_transaction(self, tx): + # Relay transactions into the tester for mining + return self.test.t.direct_tx(tx) + + # Override this classmethod and add another arg + @classmethod + def register_with_app(klass, app, test): + s = klass(app, test) + app.register_service(s) + return s class PeerManagerMock(BaseService): name = 'peermanager' @@ -27,7 +58,11 @@ def broadcast(*args, **kwargs): pass @pytest.fixture() -def test_app(request, tmpdir): +def test(): + return TestLangHybrid(5, 25, 0.02, 0.002) + +@pytest.fixture() +def test_app(request, tmpdir, test): config = { 'data_dir': str(tmpdir), 'db': {'implementation': 'EphemDB'}, @@ -47,15 +82,11 @@ def test_app(request, tmpdir): } }, # 'genesis_data': {}, - # 'jsonrpc': {'listen_port': 29873}, 'validate': [encode_hex(tester.accounts[0])], } services = [ DBService, - # AccountsService, - ChainService, - # PoWService, PeerManagerMock, ValidatorService, ] @@ -67,19 +98,28 @@ def test_app(request, tmpdir): AccountsService.register_with_app(app) app.services.accounts.add_account(Account.new('', tester.keys[0]), store=False) + # Need to do this one manually too + ChainServiceMock.register_with_app(app, test) + for service in services: service.register_with_app(app) return app -def test_generate_valcode(test_app): - test = TestLangHybrid(5, 25, 0.02, 0.002) - test.parse('B B') +def test_generate_valcode(test, test_app): + + # Link the mock ChainService to the tester object. This is the interface between + # pyethapp and pyethereum's test interface. + # test_app.services.chainservice.set_tester(test.t) + + # test.parse('B B') # Create a smart chain object: this ties the chain used in the tester # to the validator chain. - test.t.chain = hybrid_casper_chain.Chain(genesis=test.genesis, new_head_cb=test_app.services.validator.on_new_head) - test_app.services.chain.chain = test.t.chain - test.parse('B1') + # test.t.chain = hybrid_casper_chain.Chain(genesis=test.genesis, new_head_cb=test_app.services.validator.on_new_head) + # test_app.services.chain.chain = test.t.chain + # test_app.chain = test.t.chain + test_app.chain = test.t.chain = test_app.services.chain.chain + test.parse('B') assert True diff --git a/pyethapp/validator_service.py b/pyethapp/validator_service.py index 0f70804a..a420622e 100644 --- a/pyethapp/validator_service.py +++ b/pyethapp/validator_service.py @@ -41,13 +41,13 @@ def __init__(self, app): else: self.validating = False - app.services.chain.on_new_head_cbs.append(self.on_new_head) + self.chainservice.on_new_head_cbs.append(self.on_new_head) def on_new_head(self, block): if not self.validating: log.info('[hybrid_casper] not validating, not updating') return - if self.app.services.chain.is_syncing: + if self.chainservice.is_syncing: log.info('[hybrid_casper] chain syncing, not updating') return self.update() @@ -95,7 +95,7 @@ def broadcast_logout(self, login_logout_flag): if not logout_success: raise Exception('Logout tx failed') log.info('[hybrid_casper] Broadcasting logout tx: {}'.format(str(logout_tx))) - self.chainservice.broadcast_transaction(logout_tx) + self.chainservice.add_transaction(logout_tx) def update(self): log.info('[hybrid_casper] validator {} updating'.format(self)) @@ -112,7 +112,7 @@ def update(self): elif not self.valcode_tx: self.generate_valcode_tx() log.info('[hybrid_casper] Broadcasting valcode tx and waiting for it to be mined') - self.chainservice.broadcast_transaction(self.valcode_tx) + self.chainservice.add_transaction(self.valcode_tx) # Wait for it to be mined return @@ -126,7 +126,7 @@ def update(self): elif not self.did_broadcast_deposit: self.generate_deposit_tx() log.info('[hybrid_casper] Broadcasting deposit tx') - self.chainservice.broadcast_transaction(self.deposit_tx) + self.chainservice.add_transaction(self.deposit_tx) self.did_broadcast_deposit = True # Wait for it to be mined @@ -155,7 +155,7 @@ def update(self): if vote_msg: vote_tx = self.mk_vote_tx(vote_msg) log.info('[hybrid_casper] Broadcasting vote: {}'.format(str(vote_tx))) - self.chainservice.broadcast_transaction(vote_tx) + self.chainservice.add_transaction(vote_tx) else: log.info('[hybrid_casper] Not voting this round') From 73eaab6825cdcff73caf2bfa0414982cb31e6847 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Tue, 5 Dec 2017 17:42:18 +0800 Subject: [PATCH 10/20] Minor cleanup --- pyethapp/tests/test_validator_service.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pyethapp/tests/test_validator_service.py b/pyethapp/tests/test_validator_service.py index 912ab394..75cc3bf5 100644 --- a/pyethapp/tests/test_validator_service.py +++ b/pyethapp/tests/test_validator_service.py @@ -1,5 +1,4 @@ from itertools import count -import functools import pytest import shutil import tempfile @@ -18,7 +17,7 @@ from pyethapp.pow_service import PoWService log = get_logger('tests.validator_service') -configure_logging('validator:debug,eth.chainservice:debug') +configure_logging('validator:debug,eth.chainservice:debug,eth.pb.tx:debug') class ChainServiceMock(BaseService): name = 'chain' @@ -107,19 +106,12 @@ def test_app(request, tmpdir, test): return app def test_generate_valcode(test, test_app): - # Link the mock ChainService to the tester object. This is the interface between # pyethapp and pyethereum's test interface. - # test_app.services.chainservice.set_tester(test.t) - - # test.parse('B B') # Create a smart chain object: this ties the chain used in the tester # to the validator chain. - # test.t.chain = hybrid_casper_chain.Chain(genesis=test.genesis, new_head_cb=test_app.services.validator.on_new_head) - # test_app.services.chain.chain = test.t.chain - # test_app.chain = test.t.chain test_app.chain = test.t.chain = test_app.services.chain.chain - test.parse('B') + test.parse('B B B') assert True From c311055b0dfe59e21d4000b224628a05a4b947fa Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Tue, 5 Dec 2017 19:55:15 +0800 Subject: [PATCH 11/20] Go back to mining manually in tests validator_service tests are now working. Fix some bugs in validator_service and improve debug output. --- pyethapp/tests/test_validator_service.py | 108 +++++++++++++---------- pyethapp/validator_service.py | 35 ++++++-- 2 files changed, 86 insertions(+), 57 deletions(-) diff --git a/pyethapp/tests/test_validator_service.py b/pyethapp/tests/test_validator_service.py index 75cc3bf5..b818dd67 100644 --- a/pyethapp/tests/test_validator_service.py +++ b/pyethapp/tests/test_validator_service.py @@ -5,6 +5,7 @@ from devp2p.service import BaseService from ethereum.config import default_config from pyethapp.config import update_config_with_defaults, get_default_config +from ethereum.pow.ethpow import mine from ethereum.slogging import get_logger, configure_logging from ethereum.hybrid_casper import chain as hybrid_casper_chain from ethereum.tools import tester @@ -12,43 +13,13 @@ from ethereum.utils import encode_hex from pyethapp.app import EthApp from pyethapp.db_service import DBService +from pyethapp.eth_service import ChainService from pyethapp.accounts import Account, AccountsService from pyethapp.validator_service import ValidatorService from pyethapp.pow_service import PoWService log = get_logger('tests.validator_service') -configure_logging('validator:debug,eth.chainservice:debug,eth.pb.tx:debug') - -class ChainServiceMock(BaseService): - name = 'chain' - - def __init__(self, app, test): - super(ChainServiceMock, self).__init__(app) - - class InnerNewHeadCbsMock(object): - def __init__(self, outer): - self.outer = outer - - def append(self, cb): - self.outer.chain.new_head_cb = cb - - # Save as interface to tester - self.test = test - self.on_new_head_cbs = InnerNewHeadCbsMock(self) - # self.chain = hybrid_casper_chain.Chain(genesis=test.genesis, new_head_cb=self.app.services.validator.on_new_head) - self.chain = hybrid_casper_chain.Chain(genesis=test.genesis) - self.is_syncing = False - - def add_transaction(self, tx): - # Relay transactions into the tester for mining - return self.test.t.direct_tx(tx) - - # Override this classmethod and add another arg - @classmethod - def register_with_app(klass, app, test): - s = klass(app, test) - app.register_service(s) - return s +configure_logging('validator:debug,eth.chainservice:debug') class PeerManagerMock(BaseService): name = 'peermanager' @@ -57,11 +28,47 @@ def broadcast(*args, **kwargs): pass @pytest.fixture() -def test(): - return TestLangHybrid(5, 25, 0.02, 0.002) +def test_app(request, tmpdir): + class TestApp(EthApp): + def mine_blocks(self, n): + for i in range(0, n): + self.mine_one_block() + + def mine_epoch(self): + epoch_length = self.config['eth']['block']['EPOCH_LENGTH'] + return self.mine_blocks(epoch_length) + + def mine_one_block(self): + """Mine until a valid nonce is found. + :returns: the new head + """ + log.debug('mining next block') + block = self.services.chain.head_candidate + chain = self.services.chain.chain + head_number = chain.head.number + delta_nonce = 10**6 + for start_nonce in count(0, delta_nonce): + bin_nonce, mixhash = mine(block.number, block.difficulty, block.mining_hash, + start_nonce=start_nonce, rounds=delta_nonce) + if bin_nonce: + break + self.services.chain.add_mined_block(block) + self.services.pow.recv_found_nonce(bin_nonce, mixhash, block.mining_hash) + if len(chain.time_queue) > 0: + # If we mine two blocks within one second, pyethereum will + # force the new block's timestamp to be in the future (see + # ethereum1_setup_block()), and when we try to add that block + # to the chain (via Chain.add_block()), it will be put in a + # queue for later processing. Since we need to ensure the + # block has been added before we continue the test, we + # have to manually process the time queue. + log.debug('block mined too fast, processing time queue') + chain.process_time_queue(new_time=block.timestamp) + log.debug('block mined') + assert chain.head.difficulty == 1 + assert chain.head.number == head_number + 1 + return chain.head -@pytest.fixture() -def test_app(request, tmpdir, test): config = { 'data_dir': str(tmpdir), 'db': {'implementation': 'EphemDB'}, @@ -80,38 +87,41 @@ def test_app(request, tmpdir, test): 'BASE_PENALTY_FACTOR': 0.002, } }, - # 'genesis_data': {}, 'validate': [encode_hex(tester.accounts[0])], } services = [ DBService, + ChainService, + PoWService, PeerManagerMock, ValidatorService, ] - update_config_with_defaults(config, get_default_config([EthApp] + services)) + update_config_with_defaults(config, get_default_config([TestApp] + services)) update_config_with_defaults(config, {'eth': {'block': default_config}}) - app = EthApp(config) + app = TestApp(config) # Add AccountsService first and initialize with coinbase account AccountsService.register_with_app(app) app.services.accounts.add_account(Account.new('', tester.keys[0]), store=False) - # Need to do this one manually too - ChainServiceMock.register_with_app(app, test) - for service in services: service.register_with_app(app) return app -def test_generate_valcode(test, test_app): - # Link the mock ChainService to the tester object. This is the interface between - # pyethapp and pyethereum's test interface. +def test_generate_valcode(test_app): + epoch_length = test_app.config['eth']['block']['EPOCH_LENGTH'] + + # This block should cause the validator to send the valcode tx + # This block should cause the validator to send the deposit tx + # In this block the validator should be active woop woop + test_app.mine_blocks(3) - # Create a smart chain object: this ties the chain used in the tester - # to the validator chain. - test_app.chain = test.t.chain = test_app.services.chain.chain - test.parse('B B B') + # Move to the next epoch + test_app.mine_epoch() + test_app.mine_epoch() + test_app.mine_epoch() + # test_app.mine_blocks(1) assert True diff --git a/pyethapp/validator_service.py b/pyethapp/validator_service.py index a420622e..1cc57ff4 100644 --- a/pyethapp/validator_service.py +++ b/pyethapp/validator_service.py @@ -71,7 +71,7 @@ def generate_valcode_tx(self): def generate_deposit_tx(self): nonce = self.chain.state.get_nonce(self.coinbase.address) # Generate transaction - deposit_tx = self.mk_deposit_tx(self.deposit_size, valcode_addr, nonce+1) + deposit_tx = self.mk_deposit_tx(self.deposit_size, self.valcode_addr, nonce) # Verify the transaction passes temp_state = self.chain.state.ephemeral_clone() deposit_success, o2 = apply_transaction(temp_state, deposit_tx) @@ -133,22 +133,41 @@ def update(self): return # We are clear to vote! - log.info('[hybrid_casper] Active validator index: {}'.format(self.get_validator_index(self.chain.state))) - casper = tester.ABIContract(tester.State(self.chain.state.ephemeral_clone()), casper_utils.casper_abi, self.chain.casper_address) + validator_index = self.get_validator_index(self.chain.state) + # validator_info = casper.get_validators(validator_index) + log.info('[hybrid_casper] Active validator index: {}'.format(validator_index)) + + # This fails if there are zero deposits (div. by zero), e.g. if we are the first + # validator to deposit + try: + voted_frac = casper.get_main_hash_voted_frac() + except tester.TransactionFailed: + voted_frac = "NaN" + try: - log.info('[hybrid_casper] Vote percent: {} - Deposits: {} - Recommended Source: {} - Current Epoch: {}' - .format(casper.get_main_hash_voted_frac(), casper.get_total_curdyn_deposits(), - casper.get_recommended_source_epoch(), casper.get_current_epoch())) + log.info('[hybrid_casper] Vote percent: {:.4%} - Deposits: {} - Recommended Source: {}'.format( + float(voted_frac), + casper.get_total_curdyn_deposits(), + casper.get_recommended_source_epoch(), + )) + log.info('[hybrid_casper] Current epoch: {} - Current dynasty: {} - Start dynasty: {} - End dynasty: {}'.format( + casper.get_current_epoch(), + casper.get_dynasty(), + "?", + "?", + # validator_info.start_dynasty, + # validator_info.end_dynasty, + )) is_justified = casper.get_votes__is_justified(casper.get_current_epoch()) is_finalized = casper.get_votes__is_finalized(casper.get_current_epoch()-1) if is_justified: log.info('[hybrid_casper] Justified epoch: {}'.format(casper.get_current_epoch())) if is_finalized: log.info('[hybrid_casper] Finalized epoch: {}'.format(casper.get_current_epoch()-1)) - except e: - log.info('[hybrid_casper] Vote frac failed: {}'.format(e)) + except tester.TransactionFailed as e: + log.info('[hybrid_casper] Casper contract call failed: {}'.format(e)) # Generate vote messages and broadcast if possible vote_msg = self.generate_vote_message() From 6eb158a52e316983d55d5637f9f299fba1255f05 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Wed, 6 Dec 2017 15:11:41 +0800 Subject: [PATCH 12/20] WIP: Cleanup, begin adding more tests --- pyethapp/tests/test_validator_service.py | 33 ++++++++++++++++++------ 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/pyethapp/tests/test_validator_service.py b/pyethapp/tests/test_validator_service.py index b818dd67..3190b7ee 100644 --- a/pyethapp/tests/test_validator_service.py +++ b/pyethapp/tests/test_validator_service.py @@ -7,9 +7,7 @@ from pyethapp.config import update_config_with_defaults, get_default_config from ethereum.pow.ethpow import mine from ethereum.slogging import get_logger, configure_logging -from ethereum.hybrid_casper import chain as hybrid_casper_chain from ethereum.tools import tester -from ethereum.tests.hybrid_casper.testing_lang import TestLangHybrid from ethereum.utils import encode_hex from pyethapp.app import EthApp from pyethapp.db_service import DBService @@ -19,7 +17,8 @@ from pyethapp.pow_service import PoWService log = get_logger('tests.validator_service') -configure_logging('validator:debug,eth.chainservice:debug') +# configure_logging('tests.validator_service:debug,validator:debug,eth.chainservice:debug,eth.pb.tx:debug') +configure_logging('tests.validator_service:debug,validator:debug,eth.chainservice:debug') class PeerManagerMock(BaseService): name = 'peermanager' @@ -111,17 +110,35 @@ def mine_one_block(self): return app def test_generate_valcode(test_app): + v = test_app.services.validator epoch_length = test_app.config['eth']['block']['EPOCH_LENGTH'] - # This block should cause the validator to send the valcode tx - # This block should cause the validator to send the deposit tx - # In this block the validator should be active woop woop + assert v.valcode_tx is None + assert v.deposit_tx is None + + # Mining these first three blocks does the following: + # 1. validator sends the valcode tx + test_app.mine_blocks(1) + assert v.valcode_tx is not None + assert v.deposit_tx is None + assert not v.chain.state.get_code(self.valcode_addr) + + # 2. validator sends the deposit tx + test_app.mine_blocks(1) + assert v.valcode_tx is not None + assert v.deposit_tx is not None + assert v.chain.state.get_code(self.valcode_addr) + + # 3. validator becomes active test_app.mine_blocks(3) - # Move to the next epoch + # Two more epochs and the validator should begin voting test_app.mine_epoch() test_app.mine_epoch() + + # Two more epochs and the vote_frac has a value (since it requires there + # to be at least one vote for both the current and the prev epoch) + test_app.mine_epoch() test_app.mine_epoch() - # test_app.mine_blocks(1) assert True From c8f5ef13c03ed70e6fa543f3b2d03c6fa77c0693 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Thu, 7 Dec 2017 17:47:43 +0800 Subject: [PATCH 13/20] Add first working test, mock AccountsService Mocking the AccountsService saves a ton of time since it takes > 30 sec to generate a new account or unlock an existing account. First test (which tests the validator login sequence) is relatively complete and is passing. --- pyethapp/tests/test_validator_service.py | 141 +++++++++++++++++++---- pyethapp/validator_service.py | 14 ++- 2 files changed, 128 insertions(+), 27 deletions(-) diff --git a/pyethapp/tests/test_validator_service.py b/pyethapp/tests/test_validator_service.py index 3190b7ee..4bdfdb0c 100644 --- a/pyethapp/tests/test_validator_service.py +++ b/pyethapp/tests/test_validator_service.py @@ -1,25 +1,46 @@ from itertools import count +import os import pytest +import rlp import shutil import tempfile from devp2p.service import BaseService from ethereum.config import default_config from pyethapp.config import update_config_with_defaults, get_default_config +from ethereum.hybrid_casper import casper_utils from ethereum.pow.ethpow import mine from ethereum.slogging import get_logger, configure_logging from ethereum.tools import tester -from ethereum.utils import encode_hex +from ethereum.utils import encode_hex, decode_int from pyethapp.app import EthApp from pyethapp.db_service import DBService from pyethapp.eth_service import ChainService -from pyethapp.accounts import Account, AccountsService from pyethapp.validator_service import ValidatorService from pyethapp.pow_service import PoWService log = get_logger('tests.validator_service') -# configure_logging('tests.validator_service:debug,validator:debug,eth.chainservice:debug,eth.pb.tx:debug') configure_logging('tests.validator_service:debug,validator:debug,eth.chainservice:debug') +class MockAddress(object): + def __init__(self): + self.address = tester.accounts[0] + self.privkey = tester.keys[0] + def sign_tx(self, tx): + return tx.sign(self.privkey) +mock_address = MockAddress() + +class AccountsServiceMock(BaseService): + name = 'accounts' + + def __init__(self, app): + super(AccountsServiceMock, self).__init__(app) + self.coinbase = mock_address + + def find(self, address): + + assert address == encode_hex(tester.accounts[0]) + return mock_address + class PeerManagerMock(BaseService): name = 'peermanager' @@ -33,9 +54,11 @@ def mine_blocks(self, n): for i in range(0, n): self.mine_one_block() - def mine_epoch(self): + def mine_to_next_epoch(self, number_of_epochs=1): epoch_length = self.config['eth']['block']['EPOCH_LENGTH'] - return self.mine_blocks(epoch_length) + distance_to_next_epoch = (epoch_length - self.services.chain.chain.state.block_number) % epoch_length + number_of_blocks = distance_to_next_epoch + epoch_length*(number_of_epochs-1) + 2 + return self.mine_blocks(number_of_blocks) def mine_one_block(self): """Mine until a valid nonce is found. @@ -69,7 +92,6 @@ def mine_one_block(self): return chain.head config = { - 'data_dir': str(tmpdir), 'db': {'implementation': 'EphemDB'}, 'eth': { 'block': { # reduced difficulty, increased gas limit, allocations to test accounts @@ -90,6 +112,7 @@ def mine_one_block(self): } services = [ + AccountsServiceMock, DBService, ChainService, PoWService, @@ -100,45 +123,119 @@ def mine_one_block(self): update_config_with_defaults(config, {'eth': {'block': default_config}}) app = TestApp(config) - # Add AccountsService first and initialize with coinbase account - AccountsService.register_with_app(app) - app.services.accounts.add_account(Account.new('', tester.keys[0]), store=False) - for service in services: service.register_with_app(app) return app -def test_generate_valcode(test_app): +def test_login_sequence(test_app): v = test_app.services.validator - epoch_length = test_app.config['eth']['block']['EPOCH_LENGTH'] assert v.valcode_tx is None assert v.deposit_tx is None + t = tester.State(v.chain.state.ephemeral_clone()) + c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) + assert c.get_current_epoch() == 0 + # Mining these first three blocks does the following: # 1. validator sends the valcode tx test_app.mine_blocks(1) assert v.valcode_tx is not None + assert v.valcode_addr is not None assert v.deposit_tx is None - assert not v.chain.state.get_code(self.valcode_addr) + assert not v.chain.state.get_code(v.valcode_addr) # 2. validator sends the deposit tx test_app.mine_blocks(1) assert v.valcode_tx is not None assert v.deposit_tx is not None - assert v.chain.state.get_code(self.valcode_addr) + assert v.chain.state.get_code(v.valcode_addr) + + # This should still fail + validator_index = v.get_validator_index(v.chain.state) + assert validator_index is None # 3. validator becomes active test_app.mine_blocks(3) - # Two more epochs and the validator should begin voting - test_app.mine_epoch() - test_app.mine_epoch() - - # Two more epochs and the vote_frac has a value (since it requires there + # Check validator index + validator_index = v.get_validator_index(v.chain.state) + assert validator_index == 1 + + # Go from epoch 0 -> epoch 1 + test_app.mine_to_next_epoch() + # Check that epoch 0 was finalized (no validators logged in) + t = tester.State(v.chain.state.ephemeral_clone()) + c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) + assert c.get_current_epoch() == 1 + assert c.get_votes__is_finalized(0) + + # Go from epoch 1 -> epoch 2 + test_app.mine_to_next_epoch() + # Check that epoch 1 was finalized (no validators logged in) + t = tester.State(v.chain.state.ephemeral_clone()) + c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) + assert c.get_current_epoch() == 2 + assert c.get_votes__is_finalized(1) + + # Make sure we're not logged in yet + target_epoch = v.chain.state.block_number // v.epoch_length + t = tester.State(v.chain.state.ephemeral_clone()) + c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) + assert not v.is_logged_in(c, target_epoch, validator_index) + + # Mine one more epoch and we should be logged in + test_app.mine_to_next_epoch() + t = tester.State(v.chain.state.ephemeral_clone()) + c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) + assert c.get_current_epoch() == 3 + target_epoch = v.chain.state.block_number // v.epoch_length + source_epoch = c.get_recommended_source_epoch() + assert v.is_logged_in(c, target_epoch, validator_index) + + # Make sure the vote transaction was generated + vote = v.votes[target_epoch] + assert vote is not None + vote_decoded = rlp.decode(vote) + # validator index + assert decode_int(vote_decoded[0]) == validator_index + # target + assert decode_int(vote_decoded[2]) == target_epoch + # source + assert decode_int(vote_decoded[3]) == source_epoch + + # Check deposit level + # TODO: do not hardcode; pass into validator service on create + assert c.get_total_curdyn_deposits() == 5000 * 10**18 + + # This should still fail + with pytest.raises(tester.TransactionFailed): + c.get_main_hash_voted_frac() + + # One more epoch and the vote_frac has a value (since it requires there # to be at least one vote for both the current and the prev epoch) - test_app.mine_epoch() - test_app.mine_epoch() + test_app.mine_to_next_epoch() + assert c.get_current_epoch() == 4 + + # One more block to mine the vote + test_app.mine_blocks(1) + + # Check deposit level (gone up) and vote_frac + t = tester.State(v.chain.state.ephemeral_clone()) + c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) + voted_frac = c.get_main_hash_voted_frac() + assert c.get_total_curdyn_deposits() > 5000 * 10**18 + assert voted_frac > 0.99 + +def test_logout_and_withdraw(test_app): + pass + +def test_double_login(test_app): + pass + +def test_login_logout_login(test_app): + pass - assert True +def catch_violation(test_app): + pass \ No newline at end of file diff --git a/pyethapp/validator_service.py b/pyethapp/validator_service.py index 1cc57ff4..c7a34789 100644 --- a/pyethapp/validator_service.py +++ b/pyethapp/validator_service.py @@ -155,10 +155,8 @@ def update(self): log.info('[hybrid_casper] Current epoch: {} - Current dynasty: {} - Start dynasty: {} - End dynasty: {}'.format( casper.get_current_epoch(), casper.get_dynasty(), - "?", - "?", - # validator_info.start_dynasty, - # validator_info.end_dynasty, + casper.get_validators__start_dynasty(validator_index), + casper.get_validators__end_dynasty(validator_index), )) is_justified = casper.get_votes__is_justified(casper.get_current_epoch()) is_finalized = casper.get_votes__is_finalized(casper.get_current_epoch()-1) @@ -179,6 +177,7 @@ def update(self): log.info('[hybrid_casper] Not voting this round') def is_logged_in(self, casper, target_epoch, validator_index): + log.debug('[hybrid_casper] checking is_logged_in for target {} and vidx {}'.format(target_epoch, validator_index)) start_dynasty = casper.get_validators__start_dynasty(validator_index) end_dynasty = casper.get_validators__end_dynasty(validator_index) current_dynasty = casper.get_dynasty_in_epoch(target_epoch) @@ -249,10 +248,15 @@ def get_validator_index(self, state): if self.valcode_addr is None: raise Exception('Valcode address not set') try: - return casper.get_validator_indexes(self.coinbase.address) + vidx = casper.get_validator_indexes(self.coinbase.address) except tester.TransactionFailed: return None + # Zero represents failure + if vidx == 0: + return None + return vidx + def mk_transaction(self, to=b'\x00' * 20, value=0, data=b'', gasprice=tester.GASPRICE, startgas=tester.STARTGAS, nonce=None): if nonce is None: From 12020a8013ae556c9f6100b75b37991d64e00ef9 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Sun, 10 Dec 2017 19:32:31 +0800 Subject: [PATCH 14/20] Fix minor issue in test Add placeholders for a couple more tests of slashing conditions. Remove unnecessary log line. --- pyethapp/tests/test_validator_service.py | 14 ++++++++++++-- pyethapp/validator_service.py | 1 - 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pyethapp/tests/test_validator_service.py b/pyethapp/tests/test_validator_service.py index 4bdfdb0c..df3a7a14 100644 --- a/pyethapp/tests/test_validator_service.py +++ b/pyethapp/tests/test_validator_service.py @@ -37,7 +37,6 @@ def __init__(self, app): self.coinbase = mock_address def find(self, address): - assert address == encode_hex(tester.accounts[0]) return mock_address @@ -216,6 +215,8 @@ def test_login_sequence(test_app): # One more epoch and the vote_frac has a value (since it requires there # to be at least one vote for both the current and the prev epoch) test_app.mine_to_next_epoch() + t = tester.State(v.chain.state.ephemeral_clone()) + c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) assert c.get_current_epoch() == 4 # One more block to mine the vote @@ -237,5 +238,14 @@ def test_double_login(test_app): def test_login_logout_login(test_app): pass -def catch_violation(test_app): +# Test slashing conditions--make sure that we don't violate them, and also +# make sure that we can catch slashable behavior on the part of another validator. + +def test_prevent_double_vote(test_app): + pass + +def test_no_surround(test_app): + pass + +def test_catch_violation(test_app): pass \ No newline at end of file diff --git a/pyethapp/validator_service.py b/pyethapp/validator_service.py index c7a34789..c88f660f 100644 --- a/pyethapp/validator_service.py +++ b/pyethapp/validator_service.py @@ -177,7 +177,6 @@ def update(self): log.info('[hybrid_casper] Not voting this round') def is_logged_in(self, casper, target_epoch, validator_index): - log.debug('[hybrid_casper] checking is_logged_in for target {} and vidx {}'.format(target_epoch, validator_index)) start_dynasty = casper.get_validators__start_dynasty(validator_index) end_dynasty = casper.get_validators__end_dynasty(validator_index) current_dynasty = casper.get_dynasty_in_epoch(target_epoch) From 3480f0adceb55e190c19a44a4b0c58d84a128557 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Sun, 10 Dec 2017 09:47:03 -0500 Subject: [PATCH 15/20] Very minor cleanup --- pyethapp/eth_service.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyethapp/eth_service.py b/pyethapp/eth_service.py index a52dc666..a93f6388 100644 --- a/pyethapp/eth_service.py +++ b/pyethapp/eth_service.py @@ -170,9 +170,7 @@ def __init__(self, app): genesis_data = sce.get('genesis_data', {}) if not genesis_data: - # genesis_data = casper_utils.make_casper_genesis(ALLOC, 10, 100, 0.02, 0.002) genesis_data = casper_utils.make_casper_genesis(env) - # genesis_data = mk_genesis_data(env) self.chain = Chain( genesis=genesis_data, reset_genesis=True, From cde018dcc169019c875393dd46c74d6084fe4352 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Sun, 10 Dec 2017 09:49:52 -0500 Subject: [PATCH 16/20] Cleanup handling of valcode, deposit tx There's no reason to store these. Standardize how we handle nonces. Generalize the code that validates and broadcasts transactions. Some other minor cleanup. --- pyethapp/validator_service.py | 98 +++++++++++++---------------------- 1 file changed, 36 insertions(+), 62 deletions(-) diff --git a/pyethapp/validator_service.py b/pyethapp/validator_service.py index c88f660f..88bda8d4 100644 --- a/pyethapp/validator_service.py +++ b/pyethapp/validator_service.py @@ -25,10 +25,9 @@ def __init__(self, app): self.chainservice = app.services.chain self.chain = self.chainservice.chain - self.valcode_tx = None - self.deposit_tx = None self.deposit_size = 5000 * 10**18 self.valcode_addr = None + self.did_broadcast_valcode = False self.did_broadcast_deposit = False self.votes = dict() self.latest_target_epoch = -1 @@ -52,50 +51,25 @@ def on_new_head(self, block): return self.update() - def generate_valcode_tx(self): - nonce = self.chain.state.get_nonce(self.coinbase.address) - # Generate transaction - valcode_tx = self.mk_validation_code_tx(nonce) - valcode_addr = utils.mk_contract_address(self.coinbase.address, nonce) - # Verify the transaction passes - temp_state = self.chain.state.ephemeral_clone() - valcode_success, o1 = apply_transaction(temp_state, valcode_tx) - - # We should never generate invalid txs - assert valcode_success - - self.valcode_tx = valcode_tx - log.info('Valcode Tx generated: {}'.format(str(valcode_tx))) - self.valcode_addr = valcode_addr - - def generate_deposit_tx(self): - nonce = self.chain.state.get_nonce(self.coinbase.address) - # Generate transaction - deposit_tx = self.mk_deposit_tx(self.deposit_size, self.valcode_addr, nonce) - # Verify the transaction passes + def broadcast_tx(self, tx): + """ + Make sure that a transaction succeeds, and "broadcast" it to the chain service. + """ temp_state = self.chain.state.ephemeral_clone() - deposit_success, o2 = apply_transaction(temp_state, deposit_tx) - - # We should never generate invalid txs - assert deposit_success - - self.deposit_tx = deposit_tx - log.info('Deposit Tx generated: {}'.format(str(deposit_tx))) + success, output = apply_transaction(temp_state, tx) + if not success: + raise tester.TransactionFailed('Transaction failed, not broadcasting: {}'.format(str(tx))) + log.info('[hybrid_casper] Broadcasting successful tx: {}'.format(str(tx))) + self.chainservice.add_transaction(tx) - def broadcast_logout(self, login_logout_flag): + def broadcast_logout(self): 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.coinbase.privkey) # Generate transactions logout_tx = self.mk_logout_tx(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: - raise Exception('Logout tx failed') - log.info('[hybrid_casper] Broadcasting logout tx: {}'.format(str(logout_tx))) - self.chainservice.add_transaction(logout_tx) + self.broadcast_tx(logout_tx) def update(self): log.info('[hybrid_casper] validator {} updating'.format(self)) @@ -109,10 +83,13 @@ def update(self): # the valcode tx exists before we attempt to broadcast the deposit tx. # Generate valcode and deposit transactions - elif not self.valcode_tx: - self.generate_valcode_tx() - log.info('[hybrid_casper] Broadcasting valcode tx and waiting for it to be mined') - self.chainservice.add_transaction(self.valcode_tx) + elif not self.did_broadcast_valcode: + valcode_tx = self.mk_validation_code_tx() + nonce = self.chain.state.get_nonce(self.coinbase.address) + self.valcode_addr = utils.mk_contract_address(self.coinbase.address, nonce) + log.info('[hybrid_casper] Broadcasting valcode tx: {}'.format(str(valcode_tx))) + self.broadcast_tx(valcode_tx) + self.did_broadcast_valcode = True # Wait for it to be mined return @@ -124,9 +101,9 @@ def update(self): # Now we can broadcast the deposit elif not self.did_broadcast_deposit: - self.generate_deposit_tx() - log.info('[hybrid_casper] Broadcasting deposit tx') - self.chainservice.add_transaction(self.deposit_tx) + deposit_tx = self.mk_deposit_tx(self.deposit_size, self.valcode_addr) + log.info('[hybrid_casper] Broadcasting deposit tx: {}'.format(str(deposit_tx))) + self.broadcast_tx(deposit_tx) self.did_broadcast_deposit = True # Wait for it to be mined @@ -136,7 +113,6 @@ def update(self): casper = tester.ABIContract(tester.State(self.chain.state.ephemeral_clone()), casper_utils.casper_abi, self.chain.casper_address) validator_index = self.get_validator_index(self.chain.state) - # validator_info = casper.get_validators(validator_index) log.info('[hybrid_casper] Active validator index: {}'.format(validator_index)) # This fails if there are zero deposits (div. by zero), e.g. if we are the first @@ -252,36 +228,34 @@ def get_validator_index(self, state): return None # Zero represents failure - if vidx == 0: - return None - return vidx + return None if vidx == 0 else vidx def mk_transaction(self, to=b'\x00' * 20, value=0, data=b'', - gasprice=tester.GASPRICE, startgas=tester.STARTGAS, nonce=None): - if nonce is None: - nonce = self.chain.state.get_nonce(self.coinbase.address) + gasprice=tester.GASPRICE, startgas=tester.STARTGAS): + nonce = self.chain.state.get_nonce(self.coinbase.address) tx = transactions.Transaction(nonce, gasprice, startgas, to, value, data) self.coinbase.sign_tx(tx) return tx - def mk_validation_code_tx(self, nonce): - valcode_tx = self.mk_transaction('', 0, casper_utils.mk_validation_code(self.coinbase.address), nonce=nonce) - return valcode_tx + def mk_validation_code_tx(self): + return self.mk_transaction('', 0, casper_utils.mk_validation_code(self.coinbase.address)) - def mk_deposit_tx(self, value, valcode_addr, nonce): + 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.address]) - deposit_tx = self.mk_transaction(self.chain.casper_address, value, deposit_func, nonce=nonce) - return deposit_tx + return self.mk_transaction(self.chain.casper_address, value, deposit_func) + + def mk_withdraw_tx(self, validator_idx): + casper_ct = abi.ContractTranslator(casper_utils.casper_abi) + withdraw_func = casper_ct.encode('withdraw', [validator_idx]) + return self.mk_transaction(self.chain.casper_address, data=withdraw_func) def mk_logout_tx(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 + return self.mk_transaction(self.chain.casper_address, data=logout_func) def mk_vote_tx(self, vote_msg): casper_ct = abi.ContractTranslator(casper_utils.casper_abi) vote_func = casper_ct.encode('vote', [vote_msg]) - vote_tx = self.mk_transaction(to=self.chain.casper_address, value=0, startgas=1000000, data=vote_func) - return vote_tx + return self.mk_transaction(to=self.chain.casper_address, value=0, startgas=1000000, data=vote_func) From 4adf4a73e121dee6874c1e9f348d8b510addd647 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Sun, 10 Dec 2017 09:50:20 -0500 Subject: [PATCH 17/20] Add broadcast_withdraw so can get our money back! --- pyethapp/validator_service.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pyethapp/validator_service.py b/pyethapp/validator_service.py index 88bda8d4..88025b38 100644 --- a/pyethapp/validator_service.py +++ b/pyethapp/validator_service.py @@ -25,7 +25,7 @@ def __init__(self, app): self.chainservice = app.services.chain self.chain = self.chainservice.chain - self.deposit_size = 5000 * 10**18 + self.deposit_size = self.config['eth']['block']['DEPOSIT_SIZE'] self.valcode_addr = None self.did_broadcast_valcode = False self.did_broadcast_deposit = False @@ -71,6 +71,22 @@ def broadcast_logout(self): logout_tx = self.mk_logout_tx(logout_msg) self.broadcast_tx(logout_tx) + def broadcast_withdraw(self): + casper = tester.ABIContract(tester.State(self.chain.state.ephemeral_clone()), casper_utils.casper_abi, + self.chain.casper_address) + validator_index = self.get_validator_index(self.chain.state) + withdrawal_delay = casper.get_withdrawal_delay() + end_dynasty = casper.get_validators__end_dynasty(validator_index) + end_epoch = casper.get_dynasty_start_epoch(end_dynasty + 1) + current_dynasty = casper.get_dynasty() + current_epoch = casper.get_current_epoch() + log.info(('[hybrid_casper] Attempting withdraw at dynasty {} - end dynasty {} - ' + + 'current_epoch {} - end_epoch {} - withdrawal_delay {} - validator idx {}').format( + current_dynasty, end_dynasty, current_epoch, end_epoch, withdrawal_delay, validator_index)) + + withdraw_tx = self.mk_withdraw_tx(validator_index) + self.broadcast_tx(withdraw_tx) + def update(self): log.info('[hybrid_casper] validator {} updating'.format(self)) # Make sure we have enough ETH to deposit From 217ad1eabd480bc08f887c3935405edebc558649 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Sun, 10 Dec 2017 09:52:13 -0500 Subject: [PATCH 18/20] Add test for logout and withdraw Change handling of valcode and deposit tx per validator service changes. Make sure we're in metropolis. Make deposit size dynamic. --- pyethapp/tests/test_validator_service.py | 126 ++++++++++++++++++++--- 1 file changed, 111 insertions(+), 15 deletions(-) diff --git a/pyethapp/tests/test_validator_service.py b/pyethapp/tests/test_validator_service.py index df3a7a14..c6e03948 100644 --- a/pyethapp/tests/test_validator_service.py +++ b/pyethapp/tests/test_validator_service.py @@ -100,11 +100,12 @@ def mine_one_block(self): 'GENESIS_INITIAL_ALLOC': { encode_hex(tester.accounts[0]): {'balance': 10**24}, }, - # Casper FFG stuff + # Casper FFG stuff: set these arbitrarily short to facilitate testing 'EPOCH_LENGTH': 10, - 'WITHDRAWAL_DELAY': 100, + 'WITHDRAWAL_DELAY': 5, 'BASE_INTEREST_FACTOR': 0.02, 'BASE_PENALTY_FACTOR': 0.002, + 'DEPOSIT_SIZE': 5000 * 10**18, } }, 'validate': [encode_hex(tester.accounts[0])], @@ -127,11 +128,26 @@ def mine_one_block(self): return app -def test_login_sequence(test_app): +def test_login_logout_withdraw(test_app): + """ + Basic full-circle test of normal validator behavior in normal circumstances + (i.e., no slashing). Login, wait to vote, begin voting, logout, wait to withdraw + deposit, then withdraw. + """ v = test_app.services.validator + epoch_length = test_app.config['eth']['block']['EPOCH_LENGTH'] + withdrawal_delay = test_app.config['eth']['block']['WITHDRAWAL_DELAY'] + base_interest_factor = test_app.config['eth']['block']['BASE_INTEREST_FACTOR'] + base_penalty_factor = test_app.config['eth']['block']['BASE_PENALTY_FACTOR'] + deposit_size = test_app.config['eth']['block']['DEPOSIT_SIZE'] + initial_balance = test_app.config['eth']['block']['GENESIS_INITIAL_ALLOC'][encode_hex(v.coinbase.address)]['balance'] - assert v.valcode_tx is None - assert v.deposit_tx is None + assert not v.did_broadcast_valcode + assert not v.did_broadcast_deposit + assert v.chain.state.get_balance(v.coinbase.address) == initial_balance + + # We get opcode errors if this isn't true + assert v.chain.state.is_METROPOLIS() t = tester.State(v.chain.state.ephemeral_clone()) c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) @@ -140,15 +156,15 @@ def test_login_sequence(test_app): # Mining these first three blocks does the following: # 1. validator sends the valcode tx test_app.mine_blocks(1) - assert v.valcode_tx is not None + assert v.did_broadcast_valcode + assert not v.did_broadcast_deposit assert v.valcode_addr is not None - assert v.deposit_tx is None assert not v.chain.state.get_code(v.valcode_addr) # 2. validator sends the deposit tx test_app.mine_blocks(1) - assert v.valcode_tx is not None - assert v.deposit_tx is not None + assert v.did_broadcast_valcode + assert v.did_broadcast_deposit assert v.chain.state.get_code(v.valcode_addr) # This should still fail @@ -158,6 +174,9 @@ def test_login_sequence(test_app): # 3. validator becomes active test_app.mine_blocks(3) + # Make sure the deposit moved + assert v.chain.state.get_balance(v.coinbase.address) == initial_balance - deposit_size + # Check validator index validator_index = v.get_validator_index(v.chain.state) assert validator_index == 1 @@ -179,7 +198,7 @@ def test_login_sequence(test_app): assert c.get_votes__is_finalized(1) # Make sure we're not logged in yet - target_epoch = v.chain.state.block_number // v.epoch_length + target_epoch = v.chain.state.block_number // epoch_length t = tester.State(v.chain.state.ephemeral_clone()) c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) assert not v.is_logged_in(c, target_epoch, validator_index) @@ -189,7 +208,7 @@ def test_login_sequence(test_app): t = tester.State(v.chain.state.ephemeral_clone()) c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) assert c.get_current_epoch() == 3 - target_epoch = v.chain.state.block_number // v.epoch_length + target_epoch = v.chain.state.block_number // epoch_length source_epoch = c.get_recommended_source_epoch() assert v.is_logged_in(c, target_epoch, validator_index) @@ -205,8 +224,7 @@ def test_login_sequence(test_app): assert decode_int(vote_decoded[3]) == source_epoch # Check deposit level - # TODO: do not hardcode; pass into validator service on create - assert c.get_total_curdyn_deposits() == 5000 * 10**18 + assert c.get_total_curdyn_deposits() == deposit_size # This should still fail with pytest.raises(tester.TransactionFailed): @@ -229,23 +247,101 @@ def test_login_sequence(test_app): assert c.get_total_curdyn_deposits() > 5000 * 10**18 assert voted_frac > 0.99 -def test_logout_and_withdraw(test_app): - pass + # Finally, test logout and withdraw + # Send logout + v.broadcast_logout() + test_app.mine_blocks(1) + + # Make sure we can't withdraw yet + with pytest.raises(tester.TransactionFailed): + v.broadcast_withdraw() + + # Make sure we are still logged in, can still vote, etc. + test_app.mine_to_next_epoch() + + # One more block to mine the vote + test_app.mine_blocks(1) + t = tester.State(v.chain.state.ephemeral_clone()) + c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) + assert c.get_current_epoch() == 5 + voted_frac = c.get_main_hash_voted_frac() + assert c.get_total_curdyn_deposits() > 5000 * 10**18 + assert voted_frac > 0.99 + target_epoch = v.chain.state.block_number // epoch_length + assert v.is_logged_in(c, target_epoch, validator_index) + assert v.votes[target_epoch] + + # Mine two epochs + test_app.mine_to_next_epoch() + test_app.mine_to_next_epoch() + + # Make sure we don't vote, are not logged in + t = tester.State(v.chain.state.ephemeral_clone()) + c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) + assert c.get_current_epoch() == 7 + + # This fails because of division by zero + with pytest.raises(tester.TransactionFailed): + c.get_main_hash_voted_frac() + assert c.get_total_curdyn_deposits() == 0 + target_epoch = v.chain.state.block_number // epoch_length + assert not v.is_logged_in(c, target_epoch, validator_index) + with pytest.raises(KeyError): + v.votes[target_epoch] + + # Mine until the epoch before we can withdraw + test_app.mine_to_next_epoch(withdrawal_delay) + t = tester.State(v.chain.state.ephemeral_clone()) + c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) + assert c.get_current_epoch() == 12 + + # Make sure we cannot withdraw yet + with pytest.raises(tester.TransactionFailed): + v.broadcast_withdraw() + + # Make sure we can withdraw exactly at this epoch, not before + test_app.mine_to_next_epoch() + t = tester.State(v.chain.state.ephemeral_clone()) + c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) + assert c.get_current_epoch() == 13 + v.broadcast_withdraw() + test_app.mine_blocks(1) + + # Make sure deposit was refunded (along with interest) + assert v.chain.state.get_balance(v.coinbase.address) > initial_balance def test_double_login(test_app): + """ + Make sure we cannot login a second time if already logged in. Make sure + second deposit tx fails. + """ pass def test_login_logout_login(test_app): + """ + Make sure we can login, logout, withdraw funds, then subsequently login + and deposit again. + """ pass # Test slashing conditions--make sure that we don't violate them, and also # make sure that we can catch slashable behavior on the part of another validator. def test_prevent_double_vote(test_app): + """ + Make sure the validator service never votes for the same target epoch twice. + """ pass def test_no_surround(test_app): + """ + Make sure the validator service never casts a vote surrounding another. + """ pass def test_catch_violation(test_app): + """ + Make sure the validator service recognizes and reports slashable behavior + on the part of another validator. + """ pass \ No newline at end of file From 1b38e7c15b84b5c5654cc12e6000f8fa686b4887 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Sun, 10 Dec 2017 15:29:05 -0500 Subject: [PATCH 19/20] Minor logic improvement We only need to check if we have sufficient balance to deposit before we deposit --- pyethapp/validator_service.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyethapp/validator_service.py b/pyethapp/validator_service.py index 88025b38..7360dc79 100644 --- a/pyethapp/validator_service.py +++ b/pyethapp/validator_service.py @@ -89,17 +89,13 @@ def broadcast_withdraw(self): def update(self): log.info('[hybrid_casper] validator {} updating'.format(self)) - # Make sure we have enough ETH to deposit - if self.chain.state.get_balance(self.coinbase.address) < self.deposit_size: - log.info('[hybrid_casper] Cannot login as validator: insufficient balance') - return # Note about valcode and deposit transactions: these need to be broadcast in # a particular order, valcode first. In order to ensure this, we check that # the valcode tx exists before we attempt to broadcast the deposit tx. # Generate valcode and deposit transactions - elif not self.did_broadcast_valcode: + if not self.did_broadcast_valcode: valcode_tx = self.mk_validation_code_tx() nonce = self.chain.state.get_nonce(self.coinbase.address) self.valcode_addr = utils.mk_contract_address(self.coinbase.address, nonce) @@ -117,6 +113,11 @@ def update(self): # Now we can broadcast the deposit elif not self.did_broadcast_deposit: + # Make sure we have enough ETH to deposit + if self.chain.state.get_balance(self.coinbase.address) < self.deposit_size: + log.info('[hybrid_casper] Cannot login as validator: insufficient balance') + return + deposit_tx = self.mk_deposit_tx(self.deposit_size, self.valcode_addr) log.info('[hybrid_casper] Broadcasting deposit tx: {}'.format(str(deposit_tx))) self.broadcast_tx(deposit_tx) From b1d73f435b5811b31862962aec8c28189a362ae2 Mon Sep 17 00:00:00 2001 From: Lane Rettig Date: Sun, 10 Dec 2017 15:30:56 -0500 Subject: [PATCH 20/20] Add a few more tests, make DRY Encapsulate casper logic inside the TestApp to make things cleaner. Add support for multiple validators using different tester accounts. Fill in a few more tests, including one for multiple validators. --- pyethapp/tests/test_validator_service.py | 204 ++++++++++++++++------- 1 file changed, 143 insertions(+), 61 deletions(-) diff --git a/pyethapp/tests/test_validator_service.py b/pyethapp/tests/test_validator_service.py index c6e03948..e5f0f403 100644 --- a/pyethapp/tests/test_validator_service.py +++ b/pyethapp/tests/test_validator_service.py @@ -21,24 +21,28 @@ log = get_logger('tests.validator_service') configure_logging('tests.validator_service:debug,validator:debug,eth.chainservice:debug') -class MockAddress(object): - def __init__(self): - self.address = tester.accounts[0] - self.privkey = tester.keys[0] +class MockAccount(object): + def __init__(self, account): + for i in range(0, len(tester.accounts)): + if encode_hex(tester.accounts[i]) == account: + self.address = tester.accounts[i] + self.privkey = tester.keys[i] + return + raise Exception("Bad account") + def sign_tx(self, tx): return tx.sign(self.privkey) -mock_address = MockAddress() class AccountsServiceMock(BaseService): name = 'accounts' def __init__(self, app): super(AccountsServiceMock, self).__init__(app) - self.coinbase = mock_address + self.coinbase = None - def find(self, address): - assert address == encode_hex(tester.accounts[0]) - return mock_address + def find(self, account): + self.coinbase = MockAccount(account) + return self.coinbase class PeerManagerMock(BaseService): name = 'peermanager' @@ -46,8 +50,7 @@ class PeerManagerMock(BaseService): def broadcast(*args, **kwargs): pass -@pytest.fixture() -def test_app(request, tmpdir): +def _test_app(account): class TestApp(EthApp): def mine_blocks(self, n): for i in range(0, n): @@ -90,6 +93,12 @@ def mine_one_block(self): assert chain.head.number == head_number + 1 return chain.head + @property + def casper(self): + v = self.services.validator + t = tester.State(v.chain.state.ephemeral_clone()) + return tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) + config = { 'db': {'implementation': 'EphemDB'}, 'eth': { @@ -99,6 +108,7 @@ def mine_one_block(self): 'GENESIS_GAS_LIMIT': 3141592, 'GENESIS_INITIAL_ALLOC': { encode_hex(tester.accounts[0]): {'balance': 10**24}, + encode_hex(tester.accounts[1]): {'balance': 10**24}, }, # Casper FFG stuff: set these arbitrarily short to facilitate testing 'EPOCH_LENGTH': 10, @@ -108,7 +118,7 @@ def mine_one_block(self): 'DEPOSIT_SIZE': 5000 * 10**18, } }, - 'validate': [encode_hex(tester.accounts[0])], + 'validate': [encode_hex(account)], } services = [ @@ -128,6 +138,14 @@ def mine_one_block(self): return app +@pytest.fixture() +def test_app(): + return _test_app(tester.accounts[0]) + +@pytest.fixture() +def test_app2(): + return _test_app(tester.accounts[1]) + def test_login_logout_withdraw(test_app): """ Basic full-circle test of normal validator behavior in normal circumstances @@ -149,9 +167,7 @@ def test_login_logout_withdraw(test_app): # We get opcode errors if this isn't true assert v.chain.state.is_METROPOLIS() - t = tester.State(v.chain.state.ephemeral_clone()) - c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) - assert c.get_current_epoch() == 0 + assert test_app.casper.get_current_epoch() == 0 # Mining these first three blocks does the following: # 1. validator sends the valcode tx @@ -184,33 +200,25 @@ def test_login_logout_withdraw(test_app): # Go from epoch 0 -> epoch 1 test_app.mine_to_next_epoch() # Check that epoch 0 was finalized (no validators logged in) - t = tester.State(v.chain.state.ephemeral_clone()) - c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) - assert c.get_current_epoch() == 1 - assert c.get_votes__is_finalized(0) + assert test_app.casper.get_current_epoch() == 1 + assert test_app.casper.get_votes__is_finalized(0) # Go from epoch 1 -> epoch 2 test_app.mine_to_next_epoch() # Check that epoch 1 was finalized (no validators logged in) - t = tester.State(v.chain.state.ephemeral_clone()) - c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) - assert c.get_current_epoch() == 2 - assert c.get_votes__is_finalized(1) + assert test_app.casper.get_current_epoch() == 2 + assert test_app.casper.get_votes__is_finalized(1) # Make sure we're not logged in yet target_epoch = v.chain.state.block_number // epoch_length - t = tester.State(v.chain.state.ephemeral_clone()) - c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) - assert not v.is_logged_in(c, target_epoch, validator_index) + assert not v.is_logged_in(test_app.casper, target_epoch, validator_index) # Mine one more epoch and we should be logged in test_app.mine_to_next_epoch() - t = tester.State(v.chain.state.ephemeral_clone()) - c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) - assert c.get_current_epoch() == 3 + assert test_app.casper.get_current_epoch() == 3 target_epoch = v.chain.state.block_number // epoch_length - source_epoch = c.get_recommended_source_epoch() - assert v.is_logged_in(c, target_epoch, validator_index) + source_epoch = test_app.casper.get_recommended_source_epoch() + assert v.is_logged_in(test_app.casper, target_epoch, validator_index) # Make sure the vote transaction was generated vote = v.votes[target_epoch] @@ -224,27 +232,23 @@ def test_login_logout_withdraw(test_app): assert decode_int(vote_decoded[3]) == source_epoch # Check deposit level - assert c.get_total_curdyn_deposits() == deposit_size + assert test_app.casper.get_total_curdyn_deposits() == deposit_size # This should still fail with pytest.raises(tester.TransactionFailed): - c.get_main_hash_voted_frac() + test_app.casper.get_main_hash_voted_frac() # One more epoch and the vote_frac has a value (since it requires there # to be at least one vote for both the current and the prev epoch) test_app.mine_to_next_epoch() - t = tester.State(v.chain.state.ephemeral_clone()) - c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) - assert c.get_current_epoch() == 4 + assert test_app.casper.get_current_epoch() == 4 # One more block to mine the vote test_app.mine_blocks(1) # Check deposit level (gone up) and vote_frac - t = tester.State(v.chain.state.ephemeral_clone()) - c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) - voted_frac = c.get_main_hash_voted_frac() - assert c.get_total_curdyn_deposits() > 5000 * 10**18 + voted_frac = test_app.casper.get_main_hash_voted_frac() + assert test_app.casper.get_total_curdyn_deposits() > deposit_size assert voted_frac > 0.99 # Finally, test logout and withdraw @@ -261,14 +265,12 @@ def test_login_logout_withdraw(test_app): # One more block to mine the vote test_app.mine_blocks(1) - t = tester.State(v.chain.state.ephemeral_clone()) - c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) - assert c.get_current_epoch() == 5 - voted_frac = c.get_main_hash_voted_frac() - assert c.get_total_curdyn_deposits() > 5000 * 10**18 + assert test_app.casper.get_current_epoch() == 5 + voted_frac = test_app.casper.get_main_hash_voted_frac() + assert test_app.casper.get_total_curdyn_deposits() > deposit_size assert voted_frac > 0.99 target_epoch = v.chain.state.block_number // epoch_length - assert v.is_logged_in(c, target_epoch, validator_index) + assert v.is_logged_in(test_app.casper, target_epoch, validator_index) assert v.votes[target_epoch] # Mine two epochs @@ -276,24 +278,20 @@ def test_login_logout_withdraw(test_app): test_app.mine_to_next_epoch() # Make sure we don't vote, are not logged in - t = tester.State(v.chain.state.ephemeral_clone()) - c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) - assert c.get_current_epoch() == 7 + assert test_app.casper.get_current_epoch() == 7 # This fails because of division by zero with pytest.raises(tester.TransactionFailed): - c.get_main_hash_voted_frac() - assert c.get_total_curdyn_deposits() == 0 + test_app.casper.get_main_hash_voted_frac() + assert test_app.casper.get_total_curdyn_deposits() == 0 target_epoch = v.chain.state.block_number // epoch_length - assert not v.is_logged_in(c, target_epoch, validator_index) + assert not v.is_logged_in(test_app.casper, target_epoch, validator_index) with pytest.raises(KeyError): v.votes[target_epoch] # Mine until the epoch before we can withdraw test_app.mine_to_next_epoch(withdrawal_delay) - t = tester.State(v.chain.state.ephemeral_clone()) - c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) - assert c.get_current_epoch() == 12 + assert test_app.casper.get_current_epoch() == 12 # Make sure we cannot withdraw yet with pytest.raises(tester.TransactionFailed): @@ -301,28 +299,112 @@ def test_login_logout_withdraw(test_app): # Make sure we can withdraw exactly at this epoch, not before test_app.mine_to_next_epoch() - t = tester.State(v.chain.state.ephemeral_clone()) - c = tester.ABIContract(t, casper_utils.casper_abi, v.chain.casper_address) - assert c.get_current_epoch() == 13 + assert test_app.casper.get_current_epoch() == 13 v.broadcast_withdraw() test_app.mine_blocks(1) # Make sure deposit was refunded (along with interest) assert v.chain.state.get_balance(v.coinbase.address) > initial_balance -def test_double_login(test_app): + # Make sure finalization is still happening with no validators + assert test_app.casper.get_votes__is_finalized(12) + +def test_double_deposit(test_app): """ Make sure we cannot login a second time if already logged in. Make sure second deposit tx fails. """ - pass + v = test_app.services.validator + initial_balance = test_app.config['eth']['block']['GENESIS_INITIAL_ALLOC'][encode_hex(v.coinbase.address)]['balance'] + epoch_length = test_app.config['eth']['block']['EPOCH_LENGTH'] + withdrawal_delay = test_app.config['eth']['block']['WITHDRAWAL_DELAY'] + assert test_app.casper.get_current_epoch() == 0 + + # Broadcast valcode and deposits, log in, begin voting + test_app.mine_to_next_epoch(3) + + # Try to login again, it should fail + # To test this, we have break abstraction somewhat and simulate it here. + deposit_tx = v.mk_deposit_tx(v.deposit_size, v.valcode_addr) + with pytest.raises(tester.TransactionFailed): + v.broadcast_tx(deposit_tx) def test_login_logout_login(test_app): """ Make sure we can login, logout, withdraw funds, then subsequently login and deposit again. """ - pass + v = test_app.services.validator + initial_balance = test_app.config['eth']['block']['GENESIS_INITIAL_ALLOC'][encode_hex(v.coinbase.address)]['balance'] + epoch_length = test_app.config['eth']['block']['EPOCH_LENGTH'] + withdrawal_delay = test_app.config['eth']['block']['WITHDRAWAL_DELAY'] + assert test_app.casper.get_current_epoch() == 0 + + # Broadcast valcode and deposits, log in, begin voting + test_app.mine_to_next_epoch(3) + target_epoch = v.chain.state.block_number // epoch_length + source_epoch = test_app.casper.get_recommended_source_epoch() + assert v.is_logged_in(test_app.casper, target_epoch, validator_index) + assert v.votes[target_epoch] + + # Broadcast logout + v.broadcast_logout() + test_app.mine_to_next_epoch(3+withdrawal_delay) + + # Redeem deposit + v.broadcast_withdraw() + test_app.mine_blocks(1) + balance1 = v.chain.state.get_balance(v.coinbase.address) + assert balance1 > initial_balance + + # Reset the validator state + test_app.deregister_service(v) + ValidatorService.register_with_app(test_app) + v = test_app.services.validator + + # Log in again, begin voting + test_app.mine_to_next_epoch(3) + target_epoch = v.chain.state.block_number // epoch_length + source_epoch = test_app.casper.get_recommended_source_epoch() + assert v.is_logged_in(test_app.casper, target_epoch, validator_index) + assert v.votes[target_epoch] + + # Broadcast logout again + v.broadcast_logout() + test_app.mine_to_next_epoch(3+withdrawal_delay) + + # Redeem deposit again + v.broadcast_withdraw() + test_app.mine_blocks(1) + assert v.chain.state.get_balance(v.coinbase.address) > balance_1 + +def test_multiple_validators(test_app, test_app2): + """ + Test how multiple validators behave in each others' presence. Make sure + they both deposit and vote. + """ + deposit_size = test_app.config['eth']['block']['DEPOSIT_SIZE'] + + # Link the two validators + v1 = test_app.services.validator + v2 = test_app2.services.validator + test_app2.services.chain = test_app.services.chain + v2.chainservice = test_app.services.chain + v2.chain = test_app.services.chain.chain + test_app.services.chain.on_new_head_cbs.append(test_app2.services.validator.on_new_head) + + # Make sure the valcode txs worked + test_app.mine_blocks(2) + assert v1.chain.state.get_code(v1.valcode_addr) + assert v2.chain.state.get_code(v2.valcode_addr) + + # Vote a bunch until vote frac is calculable + test_app.mine_to_next_epoch(6) + # One more block to mine the vote + test_app.mine_blocks(1) + voted_frac = test_app.casper.get_main_hash_voted_frac() + assert test_app.casper.get_total_curdyn_deposits() > deposit_size * 2 + assert voted_frac > 0.99 # Test slashing conditions--make sure that we don't violate them, and also # make sure that we can catch slashable behavior on the part of another validator.