Skip to content

Commit

Permalink
Problem: Node operator cannot vote on a ValidatorElection (#2428)
Browse files Browse the repository at this point in the history
* Problem: Node operator cannot vote on a ValidatorElection

Solution: Implement validator election voting spec

* Problem: Incorrent code comments

Solution: Update comments with correct context

* Problem: Delegated vote not casted back to election

Solution: Update test to cast votes back to election id and assert their validity
  • Loading branch information
kansi authored and muawiakh committed Aug 2, 2018
1 parent c2e61ae commit 1a74afa
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 91 deletions.
2 changes: 2 additions & 0 deletions bigchaindb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@
from bigchaindb.common.transaction import Transaction # noqa
from bigchaindb import models # noqa
from bigchaindb.upsert_validator import ValidatorElection # noqa
from bigchaindb.upsert_validator import ValidatorElectionVote # noqa

Transaction.register_type(Transaction.CREATE, models.Transaction)
Transaction.register_type(Transaction.TRANSFER, models.Transaction)
Transaction.register_type(ValidatorElection.VALIDATOR_ELECTION, ValidatorElection)
Transaction.register_type(ValidatorElectionVote.VALIDATOR_ELECTION_VOTE, ValidatorElectionVote)
3 changes: 3 additions & 0 deletions bigchaindb/common/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ def _load_schema(name, path=__file__):
_, TX_SCHEMA_VALIDATOR_ELECTION = _load_schema('transaction_validator_election_' +
TX_SCHEMA_VERSION)

_, TX_SCHEMA_VALIDATOR_ELECTION_VOTE = _load_schema('transaction_validator_election_vote_' +
TX_SCHEMA_VERSION)


def _validate_schema(schema, body):
"""Validate data against a schema"""
Expand Down
1 change: 1 addition & 0 deletions bigchaindb/common/schema/transaction_v2.0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ definitions:
- CREATE
- TRANSFER
- VALIDATOR_ELECTION
- VALIDATOR_ELECTION_VOTE
asset:
type: object
additionalProperties: false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"$schema": "http://json-schema.org/draft-04/schema#"
type: object
title: Validator Election Vote Schema - Vote on a validator set change
required:
- operation
- outputs
properties:
operation: "VALIDATOR_ELECTION_VOTE"
outputs:
type: array
items:
"$ref": "#/definitions/output"
definitions:
output:
type: object
properties:
condition:
type: object
required:
- uri
properties:
uri:
type: string
pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\
(fpt=ed25519-sha-256(&)?|cost=[0-9]+(&)?|\
subtypes=ed25519-sha-256(&)?){2,3}$"
118 changes: 88 additions & 30 deletions bigchaindb/common/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from bigchaindb.common.crypto import PrivateKey, hash_data
from bigchaindb.common.exceptions import (KeypairMismatchException,
InputDoesNotExist, DoubleSpend,
InvalidHash, InvalidSignature,
AmountError, AssetIdMismatch,
ThresholdTooDeep)
Expand Down Expand Up @@ -523,11 +524,11 @@ def __init__(self, operation, asset, inputs=None, outputs=None,
# Asset payloads for 'CREATE' operations must be None or
# dicts holding a `data` property. Asset payloads for 'TRANSFER'
# operations must be dicts holding an `id` property.
if (operation == Transaction.CREATE and
if (operation == self.CREATE and
asset is not None and not (isinstance(asset, dict) and 'data' in asset)):
raise TypeError(('`asset` must be None or a dict holding a `data` '
" property instance for '{}' Transactions".format(operation)))
elif (operation == Transaction.TRANSFER and
elif (operation == self.TRANSFER and
not (isinstance(asset, dict) and 'id' in asset)):
raise TypeError(('`asset` must be a dict holding an `id` property '
"for 'TRANSFER' Transactions".format(operation)))
Expand Down Expand Up @@ -555,9 +556,9 @@ def unspent_outputs(self):
structure containing relevant information for storing them in
a UTXO set, and performing validation.
"""
if self.operation == Transaction.CREATE:
if self.operation == self.CREATE:
self._asset_id = self._id
elif self.operation == Transaction.TRANSFER:
elif self.operation == self.TRANSFER:
self._asset_id = self.asset['id']
return (UnspentOutput(
transaction_id=self._id,
Expand Down Expand Up @@ -649,6 +650,31 @@ def create(cls, tx_signers, recipients, metadata=None, asset=None):
(inputs, outputs) = cls.validate_create(tx_signers, recipients, asset, metadata)
return cls(cls.CREATE, {'data': asset}, inputs, outputs, metadata)

@classmethod
def validate_transfer(cls, inputs, recipients, asset_id, metadata):
if not isinstance(inputs, list):
raise TypeError('`inputs` must be a list instance')
if len(inputs) == 0:
raise ValueError('`inputs` must contain at least one item')
if not isinstance(recipients, list):
raise TypeError('`recipients` must be a list instance')
if len(recipients) == 0:
raise ValueError('`recipients` list cannot be empty')

outputs = []
for recipient in recipients:
if not isinstance(recipient, tuple) or len(recipient) != 2:
raise ValueError(('Each `recipient` in the list must be a'
' tuple of `([<list of public keys>],'
' <amount>)`'))
pub_keys, amount = recipient
outputs.append(Output.generate(pub_keys, amount))

if not isinstance(asset_id, str):
raise TypeError('`asset_id` must be a string')

return (deepcopy(inputs), outputs)

@classmethod
def transfer(cls, inputs, recipients, asset_id, metadata=None):
"""A simple way to generate a `TRANSFER` transaction.
Expand Down Expand Up @@ -688,28 +714,7 @@ def transfer(cls, inputs, recipients, asset_id, metadata=None):
Returns:
:class:`~bigchaindb.common.transaction.Transaction`
"""
if not isinstance(inputs, list):
raise TypeError('`inputs` must be a list instance')
if len(inputs) == 0:
raise ValueError('`inputs` must contain at least one item')
if not isinstance(recipients, list):
raise TypeError('`recipients` must be a list instance')
if len(recipients) == 0:
raise ValueError('`recipients` list cannot be empty')

outputs = []
for recipient in recipients:
if not isinstance(recipient, tuple) or len(recipient) != 2:
raise ValueError(('Each `recipient` in the list must be a'
' tuple of `([<list of public keys>],'
' <amount>)`'))
pub_keys, amount = recipient
outputs.append(Output.generate(pub_keys, amount))

if not isinstance(asset_id, str):
raise TypeError('`asset_id` must be a string')

inputs = deepcopy(inputs)
(inputs, outputs) = cls.validate_transfer(inputs, recipients, asset_id, metadata)
return cls(cls.TRANSFER, {'id': asset_id}, inputs, outputs, metadata)

def __eq__(self, other):
Expand Down Expand Up @@ -954,7 +959,7 @@ def inputs_valid(self, outputs=None):
# greatly, as we do not have to check against `None` values.
return self._inputs_valid(['dummyvalue'
for _ in self.inputs])
elif self.operation == Transaction.TRANSFER:
elif self.operation == self.TRANSFER:
return self._inputs_valid([output.fulfillment.condition_uri
for output in outputs])
else:
Expand Down Expand Up @@ -1098,8 +1103,8 @@ def __str__(self):
tx = Transaction._remove_signatures(self.to_dict())
return Transaction._to_str(tx)

@staticmethod
def get_asset_id(transactions):
@classmethod
def get_asset_id(cls, transactions):
"""Get the asset id from a list of :class:`~.Transactions`.
This is useful when we want to check if the multiple inputs of a
Expand All @@ -1123,7 +1128,7 @@ def get_asset_id(transactions):
transactions = [transactions]

# create a set of the transactions' asset ids
asset_ids = {tx.id if tx.operation == Transaction.CREATE
asset_ids = {tx.id if tx.operation == tx.CREATE
else tx.asset['id']
for tx in transactions}

Expand Down Expand Up @@ -1242,3 +1247,56 @@ def resolve_class(operation):
@classmethod
def validate_schema(cls, tx):
pass

def validate_transfer_inputs(self, bigchain, current_transactions=[]):
# store the inputs so that we can check if the asset ids match
input_txs = []
input_conditions = []
for input_ in self.inputs:
input_txid = input_.fulfills.txid
input_tx = bigchain.get_transaction(input_txid)

if input_tx is None:
for ctxn in current_transactions:
if ctxn.id == input_txid:
input_tx = ctxn

if input_tx is None:
raise InputDoesNotExist("input `{}` doesn't exist"
.format(input_txid))

spent = bigchain.get_spent(input_txid, input_.fulfills.output,
current_transactions)
if spent:
raise DoubleSpend('input `{}` was already spent'
.format(input_txid))

output = input_tx.outputs[input_.fulfills.output]
input_conditions.append(output)
input_txs.append(input_tx)

# Validate that all inputs are distinct
links = [i.fulfills.to_uri() for i in self.inputs]
if len(links) != len(set(links)):
raise DoubleSpend('tx "{}" spends inputs twice'.format(self.id))

# validate asset id
asset_id = self.get_asset_id(input_txs)
if asset_id != self.asset['id']:
raise AssetIdMismatch(('The asset id of the input does not'
' match the asset id of the'
' transaction'))

input_amount = sum([input_condition.amount for input_condition in input_conditions])
output_amount = sum([output_condition.amount for output_condition in self.outputs])

if output_amount != input_amount:
raise AmountError(('The amount used in the inputs `{}`'
' needs to be same as the amount used'
' in the outputs `{}`')
.format(input_amount, output_amount))

if not self.inputs_valid(input_conditions):
raise InvalidSignature('Transaction signature is invalid.')

return True
67 changes: 6 additions & 61 deletions bigchaindb/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
from bigchaindb.common.exceptions import (InvalidSignature, DoubleSpend,
InputDoesNotExist,
TransactionNotInValidBlock,
AssetIdMismatch, AmountError,
from bigchaindb.common.exceptions import (InvalidSignature,
DuplicateTransaction)
from bigchaindb.common.transaction import Transaction
from bigchaindb.common.utils import (validate_txn_obj, validate_key)
Expand Down Expand Up @@ -32,64 +29,12 @@ def validate(self, bigchain, current_transactions=[]):
if bigchain.get_transaction(self.to_dict()['id']) or duplicates:
raise DuplicateTransaction('transaction `{}` already exists'
.format(self.id))

if not self.inputs_valid(input_conditions):
raise InvalidSignature('Transaction signature is invalid.')

elif self.operation == Transaction.TRANSFER:
# store the inputs so that we can check if the asset ids match
input_txs = []
for input_ in self.inputs:
input_txid = input_.fulfills.txid
input_tx, status = bigchain.\
get_transaction(input_txid, include_status=True)

if input_tx is None:
for ctxn in current_transactions:
# assume that the status as valid for previously validated
# transactions in current round
if ctxn.id == input_txid:
input_tx = ctxn
status = bigchain.TX_VALID

if input_tx is None:
raise InputDoesNotExist("input `{}` doesn't exist"
.format(input_txid))

if status != bigchain.TX_VALID:
raise TransactionNotInValidBlock(
'input `{}` does not exist in a valid block'.format(
input_txid))

spent = bigchain.get_spent(input_txid, input_.fulfills.output,
current_transactions)
if spent:
raise DoubleSpend('input `{}` was already spent'
.format(input_txid))

output = input_tx.outputs[input_.fulfills.output]
input_conditions.append(output)
input_txs.append(input_tx)

# Validate that all inputs are distinct
links = [i.fulfills.to_uri() for i in self.inputs]
if len(links) != len(set(links)):
raise DoubleSpend('tx "{}" spends the same output more than once'.format(self.id))

# validate asset id
asset_id = Transaction.get_asset_id(input_txs)
if asset_id != self.asset['id']:
raise AssetIdMismatch(('The asset id of the input does not'
' match the asset id of the'
' transaction'))

input_amount = sum([input_condition.amount for input_condition in input_conditions])
output_amount = sum([output_condition.amount for output_condition in self.outputs])

if output_amount != input_amount:
raise AmountError(('The amount used in the inputs `{}`'
' needs to be same as the amount used'
' in the outputs `{}`')
.format(input_amount, output_amount))

if not self.inputs_valid(input_conditions):
raise InvalidSignature('Transaction signature is invalid.')
self.validate_transfer_inputs(bigchain, current_transactions)

return self

Expand Down
1 change: 1 addition & 0 deletions bigchaindb/upsert_validator/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@

from bigchaindb.upsert_validator.validator_election import ValidatorElection # noqa
from bigchaindb.upsert_validator.validator_election_vote import ValidatorElectionVote # noqa
65 changes: 65 additions & 0 deletions bigchaindb/upsert_validator/validator_election_vote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import base58

from bigchaindb.common.transaction import Transaction
from bigchaindb.common.schema import (_validate_schema,
TX_SCHEMA_COMMON,
TX_SCHEMA_TRANSFER,
TX_SCHEMA_VALIDATOR_ELECTION_VOTE)


class ValidatorElectionVote(Transaction):

VALIDATOR_ELECTION_VOTE = 'VALIDATOR_ELECTION_VOTE'
# NOTE: This class inherits TRANSFER txn type. The `TRANSFER` property is
# overriden to re-use methods from parent class
TRANSFER = VALIDATOR_ELECTION_VOTE
ALLOWED_OPERATIONS = (VALIDATOR_ELECTION_VOTE,)

def validate(self, bigchain, current_transactions=[]):
"""Validate election vote transaction
NOTE: There are no additional validity conditions on casting votes i.e.
a vote is just a valid TRANFER transaction
For more details refer BEP-21: https://github.com/bigchaindb/BEPs/tree/master/21
Args:
bigchain (BigchainDB): an instantiated bigchaindb.lib.BigchainDB object.
Returns:
`True` if the election vote is valid
Raises:
ValidationError: If the election vote is invalid
"""
self.validate_transfer_inputs(bigchain, current_transactions)
return self

@classmethod
def to_public_key(cls, election_id):
return base58.b58encode(bytes.fromhex(election_id))

@classmethod
def generate(cls, inputs, recipients, election_id, metadata=None):
(inputs, outputs) = cls.validate_transfer(inputs, recipients, election_id, metadata)
election_vote = cls(cls.VALIDATOR_ELECTION_VOTE, {'id': election_id}, inputs, outputs, metadata)
cls.validate_schema(election_vote.to_dict(), skip_id=True)
return election_vote

@classmethod
def validate_schema(cls, tx, skip_id=False):
"""Validate the validator election vote transaction. Since `VALIDATOR_ELECTION_VOTE` extends `TRANFER`
transaction, all the validations for `CREATE` transaction should be inherited
"""
if not skip_id:
cls.validate_id(tx)
_validate_schema(TX_SCHEMA_COMMON, tx)
_validate_schema(TX_SCHEMA_TRANSFER, tx)
_validate_schema(TX_SCHEMA_VALIDATOR_ELECTION_VOTE, tx)

@classmethod
def create(cls, tx_signers, recipients, metadata=None, asset=None):
raise NotImplementedError

@classmethod
def transfer(cls, tx_signers, recipients, metadata=None, asset=None):
raise NotImplementedError

0 comments on commit 1a74afa

Please sign in to comment.