Protect against proposal withhold attack

Set proposals which have been not included in the blind vote but which
have been later published and are part if the cycles proposals to
rejected.
A malicious voter could manipulate the software to withhold publishing
of his proposal and be the only voter on it. At the last block in the
blind vote phase he could publish the proposal but others cannot vote
anymore as they likely have already voted. In the vote result the
other voters would have treated it like ignored and if the voter had
enough BSQ to pass the quorum he could get accepted his proposal.
With this change we set all proposals which are not part in the blind
vote data but found in the cycle's ballot list as rejected.
This commit is contained in:
Manfred Karrer 2019-03-27 21:39:10 -05:00
parent f3ad4ea5b5
commit 39ee4c079f
No known key found for this signature in database
GPG key ID: 401250966A6B2C46

View file

@ -358,29 +358,46 @@ public class VoteResultService implements DaoStateListener, DaoSetupService {
private BallotList createBallotList(VoteWithProposalTxIdList voteWithProposalTxIdList) private BallotList createBallotList(VoteWithProposalTxIdList voteWithProposalTxIdList)
throws VoteResultException.MissingBallotException { throws VoteResultException.MissingBallotException {
// We convert the list to a map with proposalTxId as key and the vote as value // voteWithProposalTxIdList is the list of ProposalTxId + vote from the blind vote (decrypted vote data)
Map<String, Vote> voteByTxIdMap = voteWithProposalTxIdList.stream()
.filter(voteWithProposalTxId -> voteWithProposalTxId.getVote() != null)
.collect(Collectors.toMap(VoteWithProposalTxId::getProposalTxId, VoteWithProposalTxId::getVote));
// We make a map with proposalTxId as key and the ballot as value out of our stored ballot list // We convert the list to a map with proposalTxId as key and the vote as value. As the vote can be null we
Map<String, Ballot> ballotByTxIdMap = ballotListService.getValidatedBallotList().stream() // wrap it into an optional.
Map<String, Optional<Vote>> voteByTxIdMap = voteWithProposalTxIdList.stream()
.collect(Collectors.toMap(VoteWithProposalTxId::getProposalTxId, e -> Optional.ofNullable(e.getVote())));
// We make a map with proposalTxId as key and the ballot as value out of our stored ballot list.
// This can contain ballots which have been added later and have a null value for the vote.
Map<String, Ballot> ballotByTxIdMap = ballotListService.getValidBallotsOfCycle().stream()
.collect(Collectors.toMap(Ballot::getTxId, ballot -> ballot)); .collect(Collectors.toMap(Ballot::getTxId, ballot -> ballot));
// It could be that we missed some proposalPayloads.
// If we have votes with proposals which are not found in our ballots we add it to missingBallots.
List<String> missingBallots = new ArrayList<>(); List<String> missingBallots = new ArrayList<>();
List<Ballot> ballots = voteByTxIdMap.entrySet().stream() List<Ballot> ballots = voteByTxIdMap.entrySet().stream()
.map(entry -> { .map(entry -> {
String txId = entry.getKey(); String txId = entry.getKey();
if (ballotByTxIdMap.containsKey(txId)) { if (ballotByTxIdMap.containsKey(txId)) {
// why not use proposalList?
Ballot ballot = ballotByTxIdMap.get(txId); Ballot ballot = ballotByTxIdMap.get(txId);
// We create a new Ballot with the proposal from the ballot list and the vote from our decrypted votes // We create a new Ballot with the proposal from the ballot list and the vote from our decrypted votes
Vote vote = entry.getValue();
// We clone the ballot instead applying the vote to the existing ballot from ballotListService // We clone the ballot instead applying the vote to the existing ballot from ballotListService
// The items from ballotListService.getBallotList() contains my votes. // The items from ballotListService.getBallotList() contains my votes.
// Maybe we should cross verify if the vote we had in our local list matches my own vote we
// received from the network? if (ballot.getVote() != null) {
return new Ballot(ballot.getProposal(), vote); // If we had set a vote it was an own active vote
if (!entry.getValue().isPresent()) {
log.warn("We found a local vote but don't have that vote in the data from the " +
"blind vote. ballot={}", ballot);
}
if (ballot.getVote() != entry.getValue().get()) {
log.warn("We found a local vote but the vote from the " +
"blind vote does not match. ballot={}, vote from blindVote data={}",
ballot, entry.getValue().get());
}
}
// We only return accpeted or rejected votes
return entry.getValue().map(vote -> new Ballot(ballot.getProposal(), vote)).orElse(null);
} else { } else {
// We got a vote but we don't have the ballot (which includes the proposal) // We got a vote but we don't have the ballot (which includes the proposal)
// We add it to the missing list to handle it as exception later. We want all missing data so we // We add it to the missing list to handle it as exception later. We want all missing data so we
@ -396,6 +413,17 @@ public class VoteResultService implements DaoStateListener, DaoSetupService {
if (!missingBallots.isEmpty()) if (!missingBallots.isEmpty())
throw new VoteResultException.MissingBallotException(ballots, missingBallots); throw new VoteResultException.MissingBallotException(ballots, missingBallots);
// If we received a proposal after we had already voted we consider it as an proposla withhold attack and
// treat the proposal as it was voted with a rejected vote.
ballotByTxIdMap.entrySet().stream()
.filter(e -> !voteByTxIdMap.keySet().contains(e.getKey()))
.map(Map.Entry::getValue)
.forEach(ballot -> {
log.warn("We have a proposal which was not part of our blind vote and reject it. " +
"Proposal ={}" + ballot.getProposal());
ballots.add(new Ballot(ballot.getProposal(), new Vote(false)));
});
// Let's keep the data more deterministic by sorting it by txId. Though we are not using the sorting. // Let's keep the data more deterministic by sorting it by txId. Though we are not using the sorting.
ballots.sort(Comparator.comparing(Ballot::getTxId)); ballots.sort(Comparator.comparing(Ballot::getTxId));
return new BallotList(ballots); return new BallotList(ballots);