Problem: voting code is not used anymore (#2357)

* Problem: voting code is not used anymore
Solution: remove all voting related code

* Problem: Some voting functionality is still present.

Solution: Remove it. Update some of the related tests.

* Problem: some skipped tests are now running
Solution: remove pytest mark to not run them

* Problem: fastquery is not related to votes
Solution: remove it in another PR
This commit is contained in:
codegeschrei 2018-06-28 11:30:13 +02:00 committed by Vanshdeep Singh
parent 67c4ce964a
commit 7449b026fa
15 changed files with 15 additions and 519 deletions

View File

@ -6,8 +6,6 @@ Attributes:
* ``backlog`` for incoming transactions awaiting to be put into
a block.
* ``bigchain`` for blocks.
* ``votes`` to store votes for each block by each federation
node.
"""
@ -21,7 +19,7 @@ from bigchaindb.common.utils import validate_all_values_for_key
logger = logging.getLogger(__name__)
TABLES = ('bigchain', 'backlog', 'votes', 'assets', 'metadata')
TABLES = ('bigchain', 'backlog', 'assets', 'metadata')
VALID_LANGUAGES = ('danish', 'dutch', 'english', 'finnish', 'french', 'german',
'hungarian', 'italian', 'norwegian', 'portuguese', 'romanian',
'russian', 'spanish', 'swedish', 'turkish', 'none',

View File

@ -66,14 +66,6 @@ class InvalidSignature(ValidationError):
"""
class ImproperVoteError(ValidationError):
"""Raised if a vote is not constructed correctly, or signed incorrectly"""
class MultipleVotesError(ValidationError):
"""Raised if a voter has voted more than once"""
class TransactionNotInValidBlock(ValidationError):
"""Raised when a transfer transaction is attempting to fulfill the
outputs of a transaction that is in an invalid or undecided block
@ -96,10 +88,6 @@ class TransactionOwnerError(ValidationError):
"""Raised if a user tries to transfer a transaction they don't own"""
class SybilError(ValidationError):
"""If a block or vote comes from an unidentifiable node"""
class DuplicateTransaction(ValidationError):
"""Raised if a duplicated transaction is found"""

View File

@ -1,44 +0,0 @@
---
"$schema": "http://json-schema.org/draft-04/schema#"
id: "http://www.bigchaindb.com/schema/vote.json"
type: object
additionalProperties: false
title: Vote Schema
required:
- node_pubkey
- signature
- vote
properties:
node_pubkey:
type: "string"
pattern: "[1-9a-zA-Z^OIl]{43,44}"
signature:
type: "string"
pattern: "[1-9a-zA-Z^OIl]{86,88}"
vote:
type: "object"
additionalProperties: false
required:
- invalid_reason
- is_block_valid
- previous_block
- voting_for_block
- timestamp
properties:
previous_block:
"$ref": "#/definitions/sha3_hexdigest"
voting_for_block:
"$ref": "#/definitions/sha3_hexdigest"
is_block_valid:
type: "boolean"
invalid_reason:
anyOf:
- type: "string"
- type: "null"
timestamp:
type: "string"
pattern: "[0-9]{10}"
definitions:
sha3_hexdigest:
pattern: "[0-9a-f]{64}"
type: string

View File

@ -1,4 +1,3 @@
from bigchaindb.voting import Voting
class BaseConsensusRules():
@ -10,8 +9,6 @@ class BaseConsensusRules():
"""
voting = Voting
@staticmethod
def validate_transaction(bigchain, transaction):
"""See :meth:`bigchaindb.models.Transaction.validate`

View File

@ -14,7 +14,7 @@ class Bigchain(object):
"""
BLOCK_INVALID = 'invalid'
"""return if a block has been voted invalid"""
"""return if a block is invalid"""
BLOCK_VALID = TX_VALID = 'valid'
"""return if a block is valid, or tx is in valid block"""
@ -372,11 +372,7 @@ class Bigchain(object):
self.write_metadata(metadatas)
# write the block
return backend.query.write_block(self.connection, block_dict)
def write_vote(self, vote):
"""Write the vote to the database."""
return backend.query.write_vote(self.connection, vote)
return backend.query.store_block(self.connection, block_dict)
def get_assets(self, asset_ids):
"""Return a list of assets that match the asset_ids
@ -409,7 +405,7 @@ class Bigchain(object):
assets (:obj:`list` of :obj:`dict`): A list of assets to write to
the database.
"""
return backend.query.write_assets(self.connection, assets)
return backend.query.store_assets(self.connection, assets)
def write_metadata(self, metadata):
"""Writes a list of metadata into the database.

View File

@ -8,7 +8,3 @@ class CriticalDoubleSpend(BigchainDBError):
class CriticalDoubleInclusion(BigchainDBError):
"""Data integrity error that requires attention"""
class CriticalDuplicateVote(BigchainDBError):
"""Data integrity error that requires attention"""

View File

@ -158,7 +158,7 @@ class Transaction(Transaction):
# TODO: Remove node_pubkey as part of cleanup II
class Block(object):
"""Bundle a list of Transactions in a Block. Nodes vote on its validity.
"""Bundle a list of Transactions in a Block.
Attributes:
transaction (:obj:`list` of :class:`~.Transaction`):
@ -326,7 +326,7 @@ class Block(object):
signature = block_body.get('signature')
return cls(transactions, block['node_pubkey'],
block['timestamp'], [], signature)
block['timestamp'], signature)
@property
def id(self):

View File

@ -1,128 +0,0 @@
import collections
from bigchaindb.exceptions import CriticalDuplicateVote
from bigchaindb.common.utils import serialize
from bigchaindb.common.crypto import PublicKey
VALID = 'valid'
INVALID = 'invalid'
UNDECIDED = 'undecided'
class Voting:
"""Everything to do with verifying and counting votes for block election.
All functions in this class should be referentially transparent, that is,
they always give the same output for a given input. This makes it easier
to test. This also means no logging!
Assumptions regarding data:
* Vote is a dictionary, but no assumptions are made on it's properties.
* Everything else is assumed to be structurally correct, otherwise errors
may be thrown.
"""
@classmethod
def block_election(cls, block, votes):
"""Calculate the election status of a block."""
eligible_voters = set(block['block']['voters'])
n_voters = len(eligible_voters)
eligible_votes, ineligible_votes = \
cls.partition_eligible_votes(votes, eligible_voters)
by_voter = cls.dedupe_by_voter(eligible_votes)
results = cls.count_votes(by_voter)
results['block_id'] = block['id']
results['status'] = cls.decide_votes(n_voters, **results['counts'])
results['ineligible'] = ineligible_votes
return results
@classmethod
def partition_eligible_votes(cls, votes, eligible_voters):
"""Filter votes from unknown nodes or nodes that are not listed on
block. This is the primary Sybill protection.
"""
eligible, ineligible = ([], [])
for vote in votes:
voter_eligible = vote.get('node_pubkey') in eligible_voters
if voter_eligible:
try:
if cls.verify_vote_signature(vote):
eligible.append(vote)
continue
except ValueError:
pass
ineligible.append(vote)
return eligible, ineligible
@classmethod
def dedupe_by_voter(cls, eligible_votes):
"""Throw a critical error if there is a duplicate vote
"""
by_voter = {}
for vote in eligible_votes:
pubkey = vote['node_pubkey']
if pubkey in by_voter:
raise CriticalDuplicateVote(pubkey)
by_voter[pubkey] = vote
return by_voter
@classmethod
def count_votes(cls, by_voter):
"""Given a list of eligible votes, (votes from known nodes that are listed
as voters), produce the number that say valid and the number that say
invalid. Votes must agree on previous block, otherwise they become invalid.
"""
prev_blocks = collections.Counter()
malformed = []
for vote in by_voter.values():
if vote['vote']['is_block_valid'] is True:
prev_blocks[vote['vote']['previous_block']] += 1
n_valid = 0
prev_block = None
# Valid votes must agree on previous block
if prev_blocks:
prev_block, n_valid = prev_blocks.most_common()[0]
del prev_blocks[prev_block]
return {
'counts': {
'n_valid': n_valid,
'n_invalid': len(by_voter) - n_valid,
},
'malformed': malformed,
'previous_block': prev_block,
'other_previous_block': dict(prev_blocks),
}
@classmethod
def decide_votes(cls, n_voters, n_valid, n_invalid):
"""Decide on votes.
To return VALID there must be a clear majority that say VALID
and also agree on the previous block.
A tie on an even number of votes counts as INVALID.
"""
if n_invalid * 2 >= n_voters:
return INVALID
if n_valid * 2 > n_voters:
return VALID
return UNDECIDED
@classmethod
def verify_vote_signature(cls, vote):
"""Verify the signature of a vote
"""
signature = vote.get('signature')
pk_base58 = vote.get('node_pubkey')
if not (type(signature) == str and type(pk_base58) == str):
raise ValueError('Malformed vote: %s' % vote)
public_key = PublicKey(pk_base58)
body = serialize(vote['vote']).encode()
return public_key.verify(body, signature)

View File

@ -7,7 +7,6 @@ from bigchaindb.web.views import (
info,
transactions as tx,
outputs,
votes,
validators,
)
@ -34,7 +33,6 @@ ROUTES_API_V1 = [
r('transactions/<string:tx_id>', tx.TransactionApi),
r('transactions', tx.TransactionListApi),
r('outputs/', outputs.OutputListApi),
r('votes/', votes.VotesApi),
r('validators/', validators.ValidatorsApi),
]

View File

@ -1,42 +0,0 @@
"""This module provides the blueprint for the votes API endpoints.
For more information please refer to the documentation: http://bigchaindb.com/http-api
We might bring back a votes API endpoint in the future, see:
https://github.com/bigchaindb/bigchaindb/issues/2037
"""
from flask import jsonify
from flask_restful import Resource
# from flask import current_app
# from flask_restful import Resource, reqparse
# from bigchaindb import backend
class VotesApi(Resource):
def get(self):
"""API endpoint to get details about votes.
Return:
404 Not Found
"""
# parser = reqparse.RequestParser()
# parser.add_argument('block_id', type=str, required=True)
# args = parser.parse_args(strict=True)
# pool = current_app.config['bigchain_pool']
# with pool() as bigchain:
# votes = list(backend.query.get_votes_by_block_id(bigchain.connection, args['block_id']))
# return votes
# Return an HTTP status code 404 Not Found, which means:
# The requested resource could not be found but may be available in the future.
gone = 'The votes endpoint is gone now, but it might return in the future.'
response = jsonify({'message': gone})
response.status_code = 404
return response

View File

@ -312,21 +312,6 @@ def double_spend_tx(signed_create_tx, carol_pubkey, user_sk):
return tx.sign([user_sk])
@pytest.fixture
def structurally_valid_vote():
return {
'node_pubkey': 'c' * 44,
'signature': 'd' * 86,
'vote': {
'voting_for_block': 'a' * 64,
'previous_block': 'b' * 64,
'is_block_valid': False,
'invalid_reason': None,
'timestamp': '1111111111'
}
}
def _get_height(b):
maybe_block = b.get_latest_block()
return 0 if maybe_block is None else maybe_block['height']

View File

@ -10,16 +10,12 @@ class TestBlockModel(object):
block = Block()
assert block.transactions == []
assert block.voters == []
assert block.timestamp == '1'
assert block.node_pubkey is None
assert block.signature is None
with raises(TypeError):
Block('not a list or None')
with raises(TypeError):
Block(None, 'valid node_pubkey', 'valid timestamp',
'not a list or None')
def test_block_serialization(self, b, alice):
from bigchaindb.common.crypto import hash_data
@ -28,12 +24,10 @@ class TestBlockModel(object):
transactions = [Transaction.create([alice.public_key], [([alice.public_key], 1)])]
timestamp = gen_timestamp()
voters = ['Qaaa', 'Qbbb']
expected_block = {
'timestamp': timestamp,
'transactions': [tx.to_dict() for tx in transactions],
'node_pubkey': alice.public_key,
'voters': voters,
}
expected = {
'id': hash_data(serialize(expected_block)),
@ -41,7 +35,7 @@ class TestBlockModel(object):
'signature': None,
}
block = Block(transactions, alice.public_key, timestamp, voters)
block = Block(transactions, alice.public_key, timestamp)
assert block.to_dict() == expected
@ -60,14 +54,12 @@ class TestBlockModel(object):
transaction = Transaction.create([alice.public_key], [([alice.public_key], 1)])
transaction.sign([alice.private_key])
timestamp = gen_timestamp()
voters = ['Qaaa', 'Qbbb']
expected = Block([transaction], alice.public_key, timestamp, voters)
expected = Block([transaction], alice.public_key, timestamp)
block = {
'timestamp': timestamp,
'transactions': [transaction.to_dict()],
'node_pubkey': alice.public_key,
'voters': voters,
}
block_body = {
@ -106,7 +98,6 @@ class TestBlockModel(object):
'timestamp': timestamp,
'transactions': [transaction.to_dict()],
'node_pubkey': alice.public_key,
'voters': list(b.federation),
}
block_body = {
@ -133,16 +124,14 @@ class TestBlockModel(object):
transactions = [Transaction.create([alice.public_key], [([alice.public_key], 1)])]
timestamp = gen_timestamp()
voters = ['Qaaa', 'Qbbb']
expected_block = {
'timestamp': timestamp,
'transactions': [tx.to_dict() for tx in transactions],
'node_pubkey': alice.public_key,
'voters': voters,
}
expected_block_serialized = serialize(expected_block).encode()
expected = PrivateKey(alice.private_key).sign(expected_block_serialized)
block = Block(transactions, alice.public_key, timestamp, voters)
block = Block(transactions, alice.public_key, timestamp)
block = block.sign(alice.private_key)
assert block.signature == expected.decode()
@ -150,12 +139,15 @@ class TestBlockModel(object):
assert public_key.verify(expected_block_serialized, block.signature)
def test_block_dupe_tx(self, b, alice):
from bigchaindb.models import Transaction
from bigchaindb.models import Block, Transaction
from bigchaindb.common.exceptions import DuplicateTransaction
tx = Transaction.create([alice.public_key], [([alice.public_key], 1)])
block = b.create_block([tx, tx])
block = Block([tx, tx], alice.public_key)
block.sign(alice.private_key)
b.store_block(block.to_dict())
with raises(DuplicateTransaction):
block._validate_block(b)
block.validate(b)
def test_decouple_assets(self, b, alice):
from bigchaindb.models import Block, Transaction

View File

@ -1,137 +0,0 @@
import pytest
from bigchaindb.core import Bigchain
from bigchaindb.voting import Voting, INVALID, VALID, UNDECIDED
################################################################################
# Tests for checking vote eligibility
def test_partition_eligible_votes():
class TestVoting(Voting):
@classmethod
def verify_vote_signature(cls, vote):
if vote['node_pubkey'] == 'invalid sig':
return False
if vote['node_pubkey'] == 'value error':
raise ValueError()
return True
voters = ['valid', 'invalid sig', 'value error', 'not in set']
votes = [{'node_pubkey': k} for k in voters]
el, inel = TestVoting.partition_eligible_votes(votes, voters[:-1])
assert el == [votes[0]]
assert inel == votes[1:]
################################################################################
# Test vote counting
def test_count_votes():
class TestVoting(Voting):
@classmethod
def verify_vote_schema(cls, vote):
return vote['node_pubkey'] != 'malformed'
voters = (['says invalid', 'malformed'] +
['kosher' + str(i) for i in range(10)])
votes = [Bigchain(v).vote('block', 'a', True) for v in voters]
votes[0]['vote']['is_block_valid'] = False
# Incorrect previous block subtracts from n_valid and adds to n_invalid
votes[-1]['vote']['previous_block'] = 'z'
by_voter = dict(enumerate(votes))
assert TestVoting.count_votes(by_voter) == {
'counts': {
'n_valid': 9, # 9 kosher votes
'n_invalid': 3, # 1 invalid, 1 malformed, 1 rogue prev block
},
'malformed': [votes[1]],
'previous_block': 'a',
'other_previous_block': {'z': 1},
}
def test_must_agree_prev_block():
class TestVoting(Voting):
@classmethod
def verify_vote_schema(cls, vote):
return True
voters = 'abcd'
votes = [Bigchain(v).vote('block', 'a', True) for v in voters]
votes[0]['vote']['previous_block'] = 'b'
votes[1]['vote']['previous_block'] = 'c'
by_voter = dict(enumerate(votes))
assert TestVoting.count_votes(by_voter) == {
'counts': {
'n_valid': 2,
'n_invalid': 2,
},
'previous_block': 'a',
'other_previous_block': {'b': 1, 'c': 1},
'malformed': [],
}
################################################################################
# Tests for vote decision making
DECISION_TESTS = [
{'n_voters': 1, 'n_valid': 1, 'n_invalid': 1},
{'n_voters': 2, 'n_valid': 2, 'n_invalid': 1},
{'n_voters': 3, 'n_valid': 2, 'n_invalid': 2},
{'n_voters': 4, 'n_valid': 3, 'n_invalid': 2},
{'n_voters': 5, 'n_valid': 3, 'n_invalid': 3},
{'n_voters': 6, 'n_valid': 4, 'n_invalid': 3},
{'n_voters': 7, 'n_valid': 4, 'n_invalid': 4},
{'n_voters': 8, 'n_valid': 5, 'n_invalid': 4}
]
@pytest.mark.parametrize('kwargs', DECISION_TESTS)
def test_decide_votes_valid(kwargs):
kwargs = kwargs.copy()
kwargs['n_invalid'] = 0
assert Voting.decide_votes(**kwargs) == VALID
kwargs['n_valid'] -= 1
assert Voting.decide_votes(**kwargs) == UNDECIDED
@pytest.mark.parametrize('kwargs', DECISION_TESTS)
def test_decide_votes_invalid(kwargs):
kwargs = kwargs.copy()
kwargs['n_valid'] = 0
assert Voting.decide_votes(**kwargs) == INVALID
kwargs['n_invalid'] -= 1
assert Voting.decide_votes(**kwargs) == UNDECIDED
################################################################################
# Tests for vote signature
def test_verify_vote_signature_passes(b):
vote = b.vote('block', 'a', True)
assert Voting.verify_vote_signature(vote)
vote['signature'] = ''
assert not Voting.verify_vote_signature(vote)
################################################################################
# Tests for vote schema
def test_verify_vote_schema(b):
vote = b.vote('b' * 64, 'a' * 64, True)
assert Voting.verify_vote_schema(vote)
vote = b.vote('b' * 64, 'a', True)
assert not Voting.verify_vote_schema(vote)
vote = b.vote('b', 'a' * 64, True)
assert not Voting.verify_vote_schema(vote)

View File

@ -86,10 +86,6 @@ def test_get_divisble_transactions_returns_500(b, client):
block = b.create_block(tx_list)
b.write_block(block)
# vote the block valid
vote = b.vote(block.id, b.get_last_voted_block().id, True)
b.write_vote(vote)
alice_priv, alice_pub = crypto.generate_key_pair()
bob_priv, bob_pub = crypto.generate_key_pair()
carly_priv, carly_pub = crypto.generate_key_pair()

View File

@ -1,99 +0,0 @@
import pytest
# from bigchaindb.models import Transaction
VOTES_ENDPOINT = '/api/v1/votes'
@pytest.mark.tendermint
def test_get_votes_endpoint(client):
gone = 'The votes endpoint is gone now, but it might return in the future.'
response = {'message': gone}
res = client.get(VOTES_ENDPOINT)
assert response == res.json
assert res.status_code == 404
res = client.get(VOTES_ENDPOINT + '?block_id=')
assert response == res.json
assert res.status_code == 404
res = client.get(VOTES_ENDPOINT + '?block_id=123')
assert response == res.json
assert res.status_code == 404
"""
# Old tests are below. We're keeping their code in a long comment block for now,
# because we might bring back a votes endpoint in the future.
# https://github.com/bigchaindb/bigchaindb/issues/2037
@pytest.mark.bdb
@pytest.mark.usefixtures('inputs')
def test_get_votes_endpoint(b, client, alice):
tx = Transaction.create([alice.public_key], [([alice.public_key], 1)])
tx = tx.sign([alice.private_key])
block = b.create_block([tx])
b.write_block(block)
# vote the block valid
vote = b.vote(block.id, b.get_last_voted_block().id, True)
b.write_vote(vote)
res = client.get(VOTES_ENDPOINT + '?block_id=' + block.id)
assert vote == res.json[0]
assert len(res.json) == 1
assert res.status_code == 200
@pytest.mark.bdb
@pytest.mark.usefixtures('inputs')
def test_get_votes_endpoint_multiple_votes(b, client):
from bigchaindb.common.crypto import generate_key_pair
tx = Transaction.create([alice.public_key], [([alice.public_key], 1)])
tx = tx.sign([alice.private_key])
block = b.create_block([tx])
b.write_block(block)
last_block = b.get_last_voted_block().id
# vote the block valid
vote_valid = b.vote(block.id, last_block, True)
b.write_vote(vote_valid)
# vote the block invalid
# a note can only vote once so we need a new node_pubkey for the second
# vote
_, pk = generate_key_pair()
vote_invalid = b.vote(block.id, last_block, False)
vote_invalid['node_pubkey'] = pk
b.write_vote(vote_invalid)
res = client.get(VOTES_ENDPOINT + '?block_id=' + block.id)
assert len(res.json) == 2
assert res.status_code == 200
@pytest.mark.bdb
def test_get_votes_endpoint_returns_empty_list_not_found(client):
res = client.get(VOTES_ENDPOINT + '?block_id=')
assert [] == res.json
assert res.status_code == 200
res = client.get(VOTES_ENDPOINT + '?block_id=123')
assert [] == res.json
assert res.status_code == 200
@pytest.mark.bdb
def test_get_votes_endpoint_returns_400_bad_query_params(client):
res = client.get(VOTES_ENDPOINT)
assert res.status_code == 400
res = client.get(VOTES_ENDPOINT + '?ts_id=123')
assert res.status_code == 400
res = client.get(VOTES_ENDPOINT + '?tx_id=123&block_id=123')
assert res.status_code == 400
"""