diff --git a/planetmint/core.py b/planetmint/core.py index ee3a4fc..9ea3452 100644 --- a/planetmint/core.py +++ b/planetmint/core.py @@ -209,7 +209,7 @@ class App(BaseApplication): else: self.block_txn_hash = block["app_hash"] - validator_update = Election.process_block(self.planetmint_node, self.new_height, self.block_transactions) + validator_update = self.planetmint_node.process_block(self.new_height, self.block_transactions) return ResponseEndBlock(validator_updates=validator_update) diff --git a/planetmint/lib.py b/planetmint/lib.py index 5d60bb3..e6d1738 100644 --- a/planetmint/lib.py +++ b/planetmint/lib.py @@ -9,7 +9,7 @@ MongoDB. """ import logging import json -from collections import namedtuple +from collections import namedtuple, OrderedDict from uuid import uuid4 import rapidjson @@ -812,4 +812,71 @@ class Planetmint(object): txns = list(backend.query.get_asset_tokens_for_public_key(self.connection, transaction.id, election_pk)) return self.count_votes(election_pk, txns, dict.get) + def _get_initiated_elections(self, height, txns): # TODO: move somewhere else + elections = [] + for tx in txns: + if not isinstance(tx, Election): + continue + + elections.append({"election_id": tx.id, "height": height, "is_concluded": False}) + return elections + + def _get_votes(self, txns): # TODO: move somewhere else + elections = OrderedDict() + for tx in txns: + if not isinstance(tx, Vote): + continue + + election_id = tx.asset["id"] + if election_id not in elections: + elections[election_id] = [] + elections[election_id].append(tx) + return elections + + def process_block(self, new_height, txns): # TODO: move somewhere else + """Looks for election and vote transactions inside the block, records + and processes elections. + + Every election is recorded in the database. + + Every vote has a chance to conclude the corresponding election. When + an election is concluded, the corresponding database record is + marked as such. + + Elections and votes are processed in the order in which they + appear in the block. Elections are concluded in the order of + appearance of their first votes in the block. + + For every election concluded in the block, calls its `on_approval` + method. The returned value of the last `on_approval`, if any, + is a validator set update to be applied in one of the following blocks. + + `on_approval` methods are implemented by elections of particular type. + The method may contain side effects but should be idempotent. To account + for other concluded elections, if it requires so, the method should + rely on the database state. + """ + # elections initiated in this block + initiated_elections = self._get_initiated_elections(new_height, txns) + + if initiated_elections: + self.store_elections(initiated_elections) + + # elections voted for in this block and their votes + elections = self._get_votes(txns) + + validator_update = None + for election_id, votes in elections.items(): + election = self.get_transaction(election_id) + if election is None: + continue + + if not election.has_concluded(self, votes): + continue + + validator_update = election.on_approval(self, new_height) + self.store_election(election.id, new_height, is_concluded=True) + + return [validator_update] if validator_update else [] + Block = namedtuple("Block", ("app_hash", "height", "transactions")) diff --git a/planetmint/transactions/types/elections/election.py b/planetmint/transactions/types/elections/election.py index e002f53..4aa6978 100644 --- a/planetmint/transactions/types/elections/election.py +++ b/planetmint/transactions/types/elections/election.py @@ -4,11 +4,9 @@ # Code is Apache-2.0 and docs are CC-BY-4.0 from collections import OrderedDict -import base58 from uuid import uuid4 from typing import Optional -from planetmint import backend from planetmint.transactions.types.elections.vote import Vote from planetmint.transactions.common.transaction import Transaction from planetmint.transactions.common.schema import _validate_schema, TX_SCHEMA_COMMON @@ -95,76 +93,6 @@ class Election(Transaction): return False - @classmethod - def _get_initiated_elections(cls, height, txns): # TODO: move somewhere else - elections = [] - for tx in txns: - if not isinstance(tx, Election): - continue - - elections.append({"election_id": tx.id, "height": height, "is_concluded": False}) - return elections - - @classmethod - def _get_votes(cls, txns): # TODO: move somewhere else - elections = OrderedDict() - for tx in txns: - if not isinstance(tx, Vote): - continue - - election_id = tx.asset["id"] - if election_id not in elections: - elections[election_id] = [] - elections[election_id].append(tx) - return elections - - @classmethod - def process_block(cls, planet, new_height, txns): # TODO: move somewhere else - """Looks for election and vote transactions inside the block, records - and processes elections. - - Every election is recorded in the database. - - Every vote has a chance to conclude the corresponding election. When - an election is concluded, the corresponding database record is - marked as such. - - Elections and votes are processed in the order in which they - appear in the block. Elections are concluded in the order of - appearance of their first votes in the block. - - For every election concluded in the block, calls its `on_approval` - method. The returned value of the last `on_approval`, if any, - is a validator set update to be applied in one of the following blocks. - - `on_approval` methods are implemented by elections of particular type. - The method may contain side effects but should be idempotent. To account - for other concluded elections, if it requires so, the method should - rely on the database state. - """ - # elections initiated in this block - initiated_elections = cls._get_initiated_elections(new_height, txns) - - if initiated_elections: - planet.store_elections(initiated_elections) - - # elections voted for in this block and their votes - elections = cls._get_votes(txns) - - validator_update = None - for election_id, votes in elections.items(): - election = planet.get_transaction(election_id) - if election is None: - continue - - if not election.has_concluded(planet, votes): - continue - - validator_update = election.on_approval(planet, new_height) - planet.store_election(election.id, new_height, is_concluded=True) - - return [validator_update] if validator_update else [] - @classmethod def rollback(cls, planet, new_height, txn_ids): # TODO: move somewhere else """Looks for election and vote transactions inside the block and @@ -179,7 +107,7 @@ class Election(Transaction): txns = [planet.get_transaction(tx_id) for tx_id in txn_ids] - elections = cls._get_votes(txns) + elections = planet._get_votes(txns) for election_id in elections: election = planet.get_transaction(election_id) election.on_rollback(planet, new_height) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 9065cfe..d55b1ac 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -524,7 +524,7 @@ def test_chain_migration_election_show_shows_inconclusive(b): assert not run_election_show(Namespace(election_id=election.id), b) - Election.process_block(b, 1, [election]) + b.process_block(1, [election]) b.store_bulk_transactions([election]) assert run_election_show(Namespace(election_id=election.id), b) == "status=ongoing" @@ -554,13 +554,13 @@ def test_chain_migration_election_show_shows_concluded(b): assert not run_election_show(Namespace(election_id=election.id), b) b.store_bulk_transactions([election]) - Election.process_block(b, 1, [election]) + b.process_block(1, [election]) assert run_election_show(Namespace(election_id=election.id), b) == "status=ongoing" b.store_abci_chain(1, "chain-X") b.store_block(Block(height=1, transactions=[v.id for v in votes], app_hash="last_app_hash")._asdict()) - Election.process_block(b, 2, votes) + b.process_block(2, votes) assert ( run_election_show(Namespace(election_id=election.id), b) diff --git a/tests/elections/test_election.py b/tests/elections/test_election.py index 1f8825b..ae95a42 100644 --- a/tests/elections/test_election.py +++ b/tests/elections/test_election.py @@ -31,11 +31,11 @@ def test_process_block_concludes_all_elections(b): total_votes += votes b.store_abci_chain(1, "chain-X") - Election.process_block(b, 1, txs) + b.process_block(1, txs) b.store_block(Block(height=1, transactions=[tx.id for tx in txs], app_hash="")._asdict()) b.store_bulk_transactions(txs) - Election.process_block(b, 2, total_votes) + b.process_block(2, total_votes) validators = b.get_validators() assert len(validators) == 5 @@ -78,11 +78,11 @@ def test_process_block_approves_only_one_validator_update(b): txs += [election] total_votes += votes - Election.process_block(b, 1, txs) + b.process_block(1, txs) b.store_block(Block(height=1, transactions=[tx.id for tx in txs], app_hash="")._asdict()) b.store_bulk_transactions(txs) - Election.process_block(b, 2, total_votes) + b.process_block(2, total_votes) validators = b.get_validators() assert len(validators) == 5 @@ -124,11 +124,11 @@ def test_process_block_approves_after_pending_validator_update(b): total_votes += votes b.store_abci_chain(1, "chain-X") - Election.process_block(b, 1, txs) + b.process_block(1, txs) b.store_block(Block(height=1, transactions=[tx.id for tx in txs], app_hash="")._asdict()) b.store_bulk_transactions(txs) - Election.process_block(b, 2, total_votes) + b.process_block(2, total_votes) validators = b.get_validators() assert len(validators) == 5 @@ -160,19 +160,19 @@ def test_process_block_does_not_approve_after_validator_update(b): total_votes = votes b.store_block(Block(height=1, transactions=[tx.id for tx in txs], app_hash="")._asdict()) - Election.process_block(b, 1, txs) + b.process_block(1, txs) b.store_bulk_transactions(txs) second_election, second_votes = generate_election( b, ChainMigrationElection, public_key, private_key, {}, voter_keys ) - Election.process_block(b, 2, total_votes + [second_election]) + b.process_block(2, total_votes + [second_election]) b.store_block(Block(height=2, transactions=[v.id for v in total_votes + [second_election]], app_hash="")._asdict()) b.store_abci_chain(1, "chain-X") - Election.process_block(b, 3, second_votes) + b.process_block(3, second_votes) assert not b.get_election(second_election.id)["is_concluded"] assert b.get_latest_abci_chain() == {"height": 1, "chain_id": "chain-X", "is_synced": True} @@ -197,11 +197,11 @@ def test_process_block_applies_only_one_migration(b): total_votes += votes b.store_abci_chain(1, "chain-X") - Election.process_block(b, 1, txs) + b.process_block(1, txs) b.store_block(Block(height=1, transactions=[tx.id for tx in txs], app_hash="")._asdict()) b.store_bulk_transactions(txs) - Election.process_block(b, 1, total_votes) + b.process_block(1, total_votes) chain = b.get_latest_abci_chain() assert chain assert chain == { @@ -215,4 +215,4 @@ def test_process_block_applies_only_one_migration(b): def test_process_block_gracefully_handles_empty_block(b): - Election.process_block(b, 1, []) + b.process_block(1, []) diff --git a/tests/tendermint/test_core.py b/tests/tendermint/test_core.py index 1884356..4c8d9f5 100644 --- a/tests/tendermint/test_core.py +++ b/tests/tendermint/test_core.py @@ -341,7 +341,7 @@ def test_end_block_return_validator_updates(b, init_chain_request): ) b.store_block(Block(height=1, transactions=[election.id], app_hash="")._asdict()) b.store_bulk_transactions([election]) - Election.process_block(b, 1, [election]) + b.process_block(1, [election]) app.block_transactions = votes diff --git a/tests/upsert_validator/test_upsert_validator_vote.py b/tests/upsert_validator/test_upsert_validator_vote.py index e5e81c8..1bf1118 100644 --- a/tests/upsert_validator/test_upsert_validator_vote.py +++ b/tests/upsert_validator/test_upsert_validator_vote.py @@ -288,10 +288,10 @@ def test_get_validator_update(b, node_keys, node_key, ed25519_node_keys): assert not election.has_concluded(b, [tx_vote0, tx_vote1]) assert election.has_concluded(b, [tx_vote0, tx_vote1, tx_vote2]) - assert Election.process_block(b, 4, [tx_vote0]) == [] - assert Election.process_block(b, 4, [tx_vote0, tx_vote1]) == [] + assert b.process_block(4, [tx_vote0]) == [] + assert b.process_block(4, [tx_vote0, tx_vote1]) == [] - update = Election.process_block(b, 4, [tx_vote0, tx_vote1, tx_vote2]) + update = b.process_block(4, [tx_vote0, tx_vote1, tx_vote2]) assert len(update) == 1 update_public_key = codecs.encode(update[0].pub_key.ed25519, "base64").decode().rstrip("\n") assert update_public_key == public_key64 @@ -314,7 +314,7 @@ def test_get_validator_update(b, node_keys, node_key, ed25519_node_keys): b.store_bulk_transactions([tx_vote0, tx_vote1]) - update = Election.process_block(b, 9, [tx_vote2]) + update = b.process_block(9, [tx_vote2]) assert len(update) == 1 update_public_key = codecs.encode(update[0].pub_key.ed25519, "base64").decode().rstrip("\n") assert update_public_key == public_key64