From cef559cdb0f680f5baf2232eebfd54a12fc074ab Mon Sep 17 00:00:00 2001 From: Lev Berman Date: Thu, 30 Aug 2018 17:31:59 +0200 Subject: [PATCH] Adjust ABCI handlers to shift migration height. And abort if the current chain is not synced. --- bigchaindb/core.py | 58 +++++++++++++++++------ tests/tendermint/test_core.py | 87 ++++++++++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 14 deletions(-) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index a0b75f89..b4dbba5d 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -49,6 +49,19 @@ class App(BaseApplication): self.validators = None self.new_height = None + def log_abci_migration_error(self, chain_id, validators): + logger.error(f'An ABCI chain migration is in process. ' + + 'Download the new ABCI client and configure it with ' + + 'chain_id={chain_id} and validators={validators}.') + + def abort_if_abci_chain_is_not_synced(self, chain): + if chain is None or chain['is_synced']: + return + + validators = self.bigchaindb.get_validators() + self.log_abci_migration_error(chain['chain_id'], validators) + sys.exit(1) + def init_chain(self, genesis): """Initialize chain upon genesis or a migration""" @@ -60,17 +73,14 @@ class App(BaseApplication): chain_id = known_chain['chain_id'] if known_chain['is_synced']: - msg = f'Ignoring the InitChain ABCI request ({genesis}) - ' + \ + msg = f'Got invalid InitChain ABCI request ({genesis}) - ' + \ 'the chain {chain_id} is already synced.' - logger.error(msg) sys.exit(1) if chain_id != genesis.chain_id: - msg = f'Got mismatching chain ID in the InitChain ' + \ - 'ABCI request - you need to migrate the ABCI client ' + \ - 'and set new chain ID: {chain_id}.' - logger.error(msg) + validators = self.bigchaindb.get_validators() + self.log_abci_migration_error(chain_id, validators) sys.exit(1) # set migration values for app hash and height @@ -83,10 +93,8 @@ class App(BaseApplication): for v in genesis.validators] if known_validators and known_validators != validator_set: - msg = f'Got mismatching validator set in the InitChain ' + \ - 'ABCI request - you need to migrate the ABCI client ' + \ - 'and set new validator set: {known_validators}.' - logger.error(msg) + self.log_abci_migration_error(known_chain['chain_id'], + known_validators) sys.exit(1) block = Block(app_hash=app_hash, height=height, transactions=[]) @@ -99,10 +107,15 @@ class App(BaseApplication): def info(self, request): """Return height of the latest committed block.""" + + chain = self.bigchaindb.get_latest_abci_chain() + self.abort_if_abci_chain_is_not_synced(chain) + r = ResponseInfo() block = self.bigchaindb.get_latest_block() if block: - r.last_block_height = block['height'] + chain_shift = 0 if chain is None else chain['height'] + r.last_block_height = block['height'] - chain_shift r.last_block_app_hash = block['app_hash'].encode('utf-8') else: r.last_block_height = 0 @@ -117,6 +130,9 @@ class App(BaseApplication): raw_tx: a raw string (in bytes) transaction. """ + chain = self.bigchaindb.get_latest_abci_chain() + self.abort_if_abci_chain_is_not_synced(chain) + logger.benchmark('CHECK_TX_INIT') logger.debug('check_tx: %s', raw_transaction) transaction = decode_transaction(raw_transaction) @@ -135,8 +151,12 @@ class App(BaseApplication): req_begin_block: block object which contains block header and block hash. """ + chain = self.bigchaindb.get_latest_abci_chain() + self.abort_if_abci_chain_is_not_synced(chain) + + chain_shift = 0 if chain is None else chain['height'] logger.benchmark('BEGIN BLOCK, height:%s, num_txs:%s', - req_begin_block.header.height, + req_begin_block.header.height + chain_shift, req_begin_block.header.num_txs) self.block_txn_ids = [] @@ -149,6 +169,10 @@ class App(BaseApplication): Args: raw_tx: a raw string (in bytes) transaction. """ + + chain = self.bigchaindb.get_latest_abci_chain() + self.abort_if_abci_chain_is_not_synced(chain) + logger.debug('deliver_tx: %s', raw_transaction) transaction = self.bigchaindb.is_valid_transaction( decode_transaction(raw_transaction), self.block_transactions) @@ -170,7 +194,12 @@ class App(BaseApplication): height (int): new height of the chain. """ - height = request_end_block.height + chain = self.bigchaindb.get_latest_abci_chain() + self.abort_if_abci_chain_is_not_synced(chain) + + chain_shift = 0 if chain is None else chain['height'] + + height = request_end_block.height + chain_shift self.new_height = height block_txn_hash = calculate_hash(self.block_txn_ids) block = self.bigchaindb.get_latest_block() @@ -198,6 +227,9 @@ class App(BaseApplication): def commit(self): """Store the new height and along with block hash.""" + chain = self.bigchaindb.get_latest_abci_chain() + self.abort_if_abci_chain_is_not_synced(chain) + data = self.block_txn_hash.encode('utf-8') # register a new block only when new transactions are received diff --git a/tests/tendermint/test_core.py b/tests/tendermint/test_core.py index 98e8d31a..ea730cf8 100644 --- a/tests/tendermint/test_core.py +++ b/tests/tendermint/test_core.py @@ -11,6 +11,7 @@ from abci.types_pb2 import ( PubKey, ResponseInitChain, RequestInitChain, + RequestInfo, RequestBeginBlock, RequestEndBlock, Validator, @@ -166,6 +167,39 @@ def test_init_chain_recognizes_new_chain_after_migration(b): } +def test_info(b): + r = RequestInfo() + app = App(b) + + res = app.info(r) + assert res.last_block_height == 0 + assert res.last_block_app_hash == b'' + + b.store_block(Block(app_hash='1', height=1, transactions=[])._asdict()) + res = app.info(r) + assert res.last_block_height == 1 + assert res.last_block_app_hash == b'1' + + # simulate a migration and assert the height is shifted + b.store_abci_chain(2, 'chain-XYZ') + b.store_block(Block(app_hash='2', height=2, transactions=[])._asdict()) + res = app.info(r) + assert res.last_block_height == 0 + assert res.last_block_app_hash == b'2' + + b.store_block(Block(app_hash='3', height=3, transactions=[])._asdict()) + res = app.info(r) + assert res.last_block_height == 1 + assert res.last_block_app_hash == b'3' + + # it's always the latest migration that is taken into account + b.store_abci_chain(4, 'chain-XYZ-new') + b.store_block(Block(app_hash='4', height=4, transactions=[])._asdict()) + res = app.info(r) + assert res.last_block_height == 0 + assert res.last_block_app_hash == b'4' + + def test_check_tx__signed_create_is_ok(b): from bigchaindb import App from bigchaindb.models import Transaction @@ -199,7 +233,6 @@ def test_check_tx__unsigned_create_is_error(b): assert result.code == CodeTypeError -@pytest.mark.bdb def test_deliver_tx__valid_create_updates_db(b, init_chain_request): from bigchaindb import App from bigchaindb.models import Transaction @@ -367,6 +400,16 @@ def test_store_pre_commit_state_in_end_block(b, alice, init_chain_request): assert resp['height'] == 100 assert resp['transactions'] == [tx.id] + # simulate a chain migration and assert the height is shifted + b.store_abci_chain(100, 'new-chain') + app.begin_block(begin_block) + app.deliver_tx(encode_tx_to_bytes(tx)) + app.end_block(RequestEndBlock(height=1)) + resp = query.get_pre_commit_state(b.connection, PRE_COMMIT_ID) + assert resp['commit_id'] == PRE_COMMIT_ID + assert resp['height'] == 101 + assert resp['transactions'] == [tx.id] + def test_new_validator_set(b): node1 = {'pub_key': {'type': 'ed25519', @@ -389,3 +432,45 @@ def test_new_validator_set(b): 'voting_power': u['power']}) assert updated_validator_set == updated_validators + + +def test_info_aborts_if_chain_is_not_synced(b): + b.store_abci_chain(0, 'chain-XYZ', False) + + with pytest.raises(SystemExit): + App(b).info(RequestInfo()) + + +def test_check_tx_aborts_if_chain_is_not_synced(b): + b.store_abci_chain(0, 'chain-XYZ', False) + + with pytest.raises(SystemExit): + App(b).check_tx('some bytes') + + +def test_begin_aborts_if_chain_is_not_synced(b): + b.store_abci_chain(0, 'chain-XYZ', False) + + with pytest.raises(SystemExit): + App(b).info(RequestBeginBlock()) + + +def test_deliver_tx_aborts_if_chain_is_not_synced(b): + b.store_abci_chain(0, 'chain-XYZ', False) + + with pytest.raises(SystemExit): + App(b).deliver_tx('some bytes') + + +def test_end_block_aborts_if_chain_is_not_synced(b): + b.store_abci_chain(0, 'chain-XYZ', False) + + with pytest.raises(SystemExit): + App(b).info(RequestEndBlock()) + + +def test_commit_aborts_if_chain_is_not_synced(b): + b.store_abci_chain(0, 'chain-XYZ', False) + + with pytest.raises(SystemExit): + App(b).commit()