Skip to content

Commit

Permalink
Closes #50 - Rescind vote
Browse files Browse the repository at this point in the history
  • Loading branch information
FrankC01 committed Apr 24, 2018
1 parent 927f621 commit a185bef
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 14 deletions.
89 changes: 78 additions & 11 deletions cli/hashblock_cli/hbasset.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,18 @@ def _accept(candidate, public_key):
else:
proposal_asset = Resource()
proposal_asset.ParseFromString(candidate.proposal.asset)
print("RESOURCE {}: system '{}' key '{}' => value '{}' sku '{}'".format(
candidate.proposal_id,
proposal_asset.system,
proposal_asset.key,
proposal_asset.value,
proposal_asset.sku))
print(
"RESOURCE {}: system '{}' key '{}' => value '{}' sku '{}'".
format(
candidate.proposal_id,
proposal_asset.system,
proposal_asset.key,
proposal_asset.value,
proposal_asset.sku))
for vote in candidate.votes:
print(" voter {} => {}".format(
vote.public_key,
"accept" if vote.vote is AssetVote.ACCEPT else "reject"))
elif args.format == 'csv':
writer = csv.writer(sys.stdout, quoting=csv.QUOTE_ALL)
writer.writerow(['PROPOSAL_ID', 'SYSTEM', 'KEY', 'VALUE', 'SKU'])
Expand Down Expand Up @@ -268,7 +274,60 @@ def _do_config_proposal_vote(args):
batch_list = BatchList(batches=[batch])

x = rest_client.send_batches(batch_list)
print("Rest returns {}".format(x))


def _do_config_unset_vote(args):
"""Executes the 'unset vote' subcommand. Given a key file, a proposal
id and a vote value, generate a batch of hashblock_asset transactions
in a BatchList instance. The BatchList is saved to a file or
submitted to a validator.
"""
signer = _read_signer(args.key)
rest_client = RestClient(args.url)

if args.unit:
dimension = Address.DIMENSION_UNIT
elif args.resource:
dimension = Address.DIMENSION_RESOURCE
else:
raise AssertionError('Dimension must be one of {-u[nit], -r[esource]}')

proposals = _get_proposals(rest_client, dimension)

proposal = None
for candidate in proposals.candidates:
if candidate.proposal_id == args.proposal_id:
proposal = candidate
break

spubkey = signer.get_public_key().as_hex()
if proposal is None:
raise CliException('No proposal exists with the given id')

voter = None
for vote_record in proposal.votes:
if vote_record.public_key == spubkey:
voter = vote_record
break

if not voter:
raise CliException(
'There is no vote made by user key {}'.format(
signer.get_public_key().as_hex()))

print("Proposing to rescind vote")

txn = _create_vote_txn(
signer,
args.proposal_id,
dimension,
'rescind')
batch = _create_batch(signer, [txn])

batch_list = BatchList(batches=[batch])

x = rest_client.send_batches(batch_list)
print("Transaction submitted")


def _get_all_proposals(rest_client):
Expand Down Expand Up @@ -379,18 +438,23 @@ def _create_vote_txn(signer, proposal_id, dimension, vote_value):
"""Creates an individual hashblock_resource transaction for voting on a
proposal for a particular asset. The proposal_id is the asset address
"""
vote_action = AssetPayload.VOTE

if vote_value == 'accept':
vote_id = AssetVote.ACCEPT
else:
elif vote_value == 'reject':
vote_id = AssetVote.REJECT
elif vote_value == 'rescind':
vote_id = AssetVote.VOTE_UNSET
vote_action = AssetPayload.ACTION_UNSET

vote = AssetVote(
proposal_id=proposal_id,
vote=vote_id)
payload = AssetPayload(
data=vote.SerializeToString(),
dimension=dimension,
action=AssetPayload.VOTE)
action=vote_action)

return _make_txn(signer, dimension, proposal_id, payload)

Expand Down Expand Up @@ -645,7 +709,7 @@ def create_parser(prog_name):
vote_parser.add_argument(
'vote_value',
type=str,
choices=['accept', 'reject'],
choices=['accept', 'reject', 'rescind'],
help='specify the value of the vote')

return parser
Expand All @@ -670,7 +734,10 @@ def main(prog_name=os.path.basename(sys.argv[0]), args=None,
elif args.subcommand == 'proposal' and args.proposal_cmd == 'list':
_do_config_proposal_list(args)
elif args.subcommand == 'proposal' and args.proposal_cmd == 'vote':
_do_config_proposal_vote(args)
if args.vote_value == 'rescind':
_do_config_unset_vote(args)
else:
_do_config_proposal_vote(args)
else:
raise CliException(
'"{}" is not a valid subcommand of "config"'.format(
Expand Down
57 changes: 55 additions & 2 deletions families/asset/hashblock_asset/processor/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,15 @@ def apply(self, transaction, context):
auth_keys,
asset_payload.data,
context)
elif asset_payload.action == AssetPayload.ACTION_UNSET:
return self._apply_unset_vote(
public_key,
auth_keys,
asset_payload.data,
context)
else:
raise InvalidTransaction(
"'action' must be one of {PROPOSE, VOTE}")
"'action' must be one of {ACTION_UNSET, PROPOSE, VOTE}")

def _apply_proposal(self, public_key, proposal_data, context):
asset_proposal = AssetProposal()
Expand Down Expand Up @@ -142,11 +148,16 @@ def _apply_proposal(self, public_key, proposal_data, context):
_set_asset(context, self.asset_type)
LOGGER.debug('Set asset {}'.format(self.asset_type.asset))

def _apply_vote(self, public_key, authorized_keys, vote_data, context):
def _apply_unset_vote(
self, public_key, authorized_keys, vote_data, context):
"""Apply an UNSET vote on a proposal
"""
LOGGER.debug("Request to rescind vote")
asset_vote = AssetVote()
asset_vote.ParseFromString(vote_data)
proposal_id = asset_vote.proposal_id

# Find the candidate based on proposal_id
asset_candidates = self._get_candidates(context)
candidate = _first(
asset_candidates.candidates,
Expand All @@ -156,8 +167,46 @@ def _apply_vote(self, public_key, authorized_keys, vote_data, context):
raise InvalidTransaction(
"Proposal {} does not exist.".format(proposal_id))

vote_record = _first(candidate.votes,
lambda record: record.public_key == public_key)

if vote_record is None:
raise InvalidTransaction(
'{} has not voted'.format(public_key))

vote_index = _index_of(candidate.votes, vote_record)
candidate_index = _index_of(asset_candidates.candidates, candidate)

# Delete the vote from the votes collection
del candidate.votes[vote_index]

# Test if there are still votes and save if so,
# else delete the candidate as well

if len(candidate.votes) == 0:
LOGGER.debug("No votes remain for proposal... removing")
del asset_candidates.candidates[candidate_index]
else:
LOGGER.debug("Votes remain for proposal... preserving")

self._set_candidates(context, asset_candidates)

def _apply_vote(self, public_key, authorized_keys, vote_data, context):
"""Apply an ACCEPT or REJECT vote to a proposal
"""
asset_vote = AssetVote()
asset_vote.ParseFromString(vote_data)
proposal_id = asset_vote.proposal_id

asset_candidates = self._get_candidates(context)
candidate = _first(
asset_candidates.candidates,
lambda candidate: candidate.proposal_id == proposal_id)

if candidate is None:
raise InvalidTransaction(
"Proposal {} does not exist.".format(proposal_id))

approval_threshold = self._get_approval_threshold(
context,
self.asset_type)
Expand All @@ -169,6 +218,8 @@ def _apply_vote(self, public_key, authorized_keys, vote_data, context):
raise InvalidTransaction(
'{} has already voted'.format(public_key))

candidate_index = _index_of(asset_candidates.candidates, candidate)

candidate.votes.add(
public_key=public_key,
vote=asset_vote.vote)
Expand Down Expand Up @@ -202,6 +253,8 @@ def _apply_vote(self, public_key, authorized_keys, vote_data, context):
self._set_candidates(context, asset_candidates)

def _get_candidates(self, context):
"""Get the candidate container from state.
"""
candidates = _get_candidates(
context,
self.asset_type.candidates_address)
Expand Down
12 changes: 12 additions & 0 deletions families/asset/tests/hashblock_asset_test/asset_message_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,18 @@ def create_vote_transaction(self, proposal_id, asset, dimension, vote):

return self._create_tp_process_request(asset, dimension, payload)

def create_unset_vote_transaction(
self, proposal_id, asset, dimension, vote):
avote = AssetVote(
proposal_id=proposal_id,
vote=vote)
payload = AssetPayload(
action=AssetPayload.ACTION_UNSET,
dimension=dimension,
data=avote.SerializeToString())

return self._create_tp_process_request(asset, dimension, payload)

def create_get_request(self, address):
addresses = [address]
return self._factory.create_get_request(addresses)
Expand Down
61 changes: 60 additions & 1 deletion families/asset/tests/test_tp_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ def _vote(self, proposal_id, asset, dimension, vote):
self.validator.send(self.factory.create_vote_transaction(
proposal_id, asset, dimension, vote))

def _unset_vote(self, proposal_id, asset, dimension, vote):
self.validator.send(self.factory.create_unset_vote_transaction(
proposal_id, asset, dimension, vote))

@property
def _public_key(self):
return self.factory.public_key
Expand Down Expand Up @@ -191,14 +195,35 @@ def test_propose_asset_resource(self):
"""
self._test_valid_propose(self.resource, Address.DIMENSION_RESOURCE)

def _setup_vote(self, asset, dimension, vote, voter):
def _setup_vote_get_setting(self, asset, dimension, vote):
proposal_id = self._proposal_id(asset, dimension)
self._vote(
proposal_id,
asset,
dimension,
vote)
self._get_setting(dimension)
return proposal_id

def _setup_vote(self, asset, dimension, vote, voter):
proposal_id = self._setup_vote_get_setting(
asset, dimension, vote)
self._expect_get(
_asset_addr.candidates(dimension),
self._build_first_candidate(
voter,
asset,
dimension))
return proposal_id

def _setup_unset_vote_get_setting(self, asset, dimension, vote, voter):
proposal_id = self._proposal_id(asset, dimension)
self._unset_vote(
proposal_id,
asset,
dimension,
vote)
self._get_setting(dimension)
self._expect_get(
_asset_addr.candidates(dimension),
self._build_first_candidate(
Expand Down Expand Up @@ -228,8 +253,21 @@ def _test_valid_reject_vote(self, asset, dimension):
self._set_empty_candidates(dimension)
self._expect_ok()

def _test_valid_unset_vote(self, asset, dimension):
self._setup_unset_vote_get_setting(
asset,
dimension,
AssetVote.VOTE_UNSET,
self._public_key)
# Expect set of candidates with at least one valid vote to be
# preserved, otherwise we should have empty candidates
self._set_empty_candidates(dimension)
self._expect_ok()

# Proposing and voting for two authorized, threshold 2
# Assume 2 authorized voters with threshold 2
# Also checks UNSET
# All Sunny Day
def test_vote_accept_unit(self):
"""Test a valid vote for unit-of-measure asset
This assumes setting and candidates in state
Expand All @@ -252,6 +290,16 @@ def test_vote_reject_resource(self):
"""
self._test_valid_reject_vote(self.resource, Address.DIMENSION_RESOURCE)

def test_vote_unset_unit(self):
"""Test a valid unset vote for unit asset proposal
"""
self._test_valid_unset_vote(self.unit, Address.DIMENSION_UNIT)

def test_vote_unset_resource(self):
"""Test a valid unset vote for resource asset proposal
"""
self._test_valid_unset_vote(self.resource, Address.DIMENSION_RESOURCE)

def _build_disjoint_candidate(self, proposal_id, voter, asset, dimension):
proposal = AssetProposal(
asset=asset.SerializeToString(),
Expand All @@ -268,6 +316,17 @@ def _build_disjoint_candidate(self, proposal_id, voter, asset, dimension):
proposal=proposal,
votes=[record])]).SerializeToString()

# All the negative tests follow
def test_vote_unset_vote_not_exist(self):
"""Tests unset vote where previous vote does not exist
"""
self._setup_unset_vote_get_setting(
self.unit,
Address.DIMENSION_UNIT,
AssetVote.VOTE_UNSET,
VOTER2)
self._expect_invalid_transaction()

def test_vote_proposal_id_not_found(self):
"""Test disjoint proposal id between vote and candidates
"""
Expand Down

0 comments on commit a185bef

Please sign in to comment.