From 5702ab03cca730d8505d3bc0ed420d8111320fda Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Tue, 5 Feb 2019 13:36:24 +0100 Subject: [PATCH 01/12] Fix handling of majority vote We need to add any valid vote reveal tx / blind vote tx pair for the majority hash calculation even if the blind vote payload is missing as that could be relevant for the majority hash calculation. We add an empty ballotList and meritList in such cases. --- .../voteresult/VoteResultException.java | 18 ---- .../voteresult/VoteResultService.java | 101 ++++++++++-------- .../DecryptedBallotsWithMerits.java | 2 + 3 files changed, 57 insertions(+), 64 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultException.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultException.java index e6f338c1bd9..eda4f1305c1 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultException.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultException.java @@ -83,24 +83,6 @@ private MissingDataException(String message) { } } - @EqualsAndHashCode(callSuper = true) - @Value - public static class MissingBlindVoteDataException extends MissingDataException { - private String blindVoteTxId; - - MissingBlindVoteDataException(String blindVoteTxId) { - super("Blind vote tx ID " + blindVoteTxId + " is missing"); - this.blindVoteTxId = blindVoteTxId; - } - - @Override - public String toString() { - return "MissingBlindVoteDataException{" + - "\n blindVoteTxId='" + blindVoteTxId + '\'' + - "\n} " + super.toString(); - } - } - @EqualsAndHashCode(callSuper = true) @Value public static class MissingBallotException extends MissingDataException { diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java index 6532bf212cf..f80784f953f 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java @@ -86,6 +86,7 @@ import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; /** * Calculates the result of the voting at the VoteResult period. @@ -173,6 +174,7 @@ private void maybeCalculateVoteResult(int chainHeight) { log.info("CalculateVoteResult at chainHeight={}", chainHeight); Cycle currentCycle = periodService.getCurrentCycle(); long startTs = System.currentTimeMillis(); + Set decryptedBallotsWithMeritsSet = getDecryptedBallotsWithMeritsSet(chainHeight); decryptedBallotsWithMeritsSet.stream() .filter(e -> !daoStateService.getDecryptedBallotsWithMeritsList().contains(e)) @@ -245,38 +247,46 @@ private Set getDecryptedBallotsWithMeritsSet(int cha // We want all voteRevealTxOutputs which are in current cycle we are processing. return daoStateService.getVoteRevealOpReturnTxOutputs().stream() .filter(txOutput -> periodService.isTxInCorrectCycle(txOutput.getTxId(), chainHeight)) - .map(txOutput -> { - // TODO make method - byte[] opReturnData = txOutput.getOpReturnData(); - String voteRevealTxId = txOutput.getTxId(); - Optional optionalVoteRevealTx = daoStateService.getTx(voteRevealTxId); - if (!optionalVoteRevealTx.isPresent()) { - log.error("optionalVoteRevealTx is not present. voteRevealTxId={}", voteRevealTxId); - //TODO throw exception - return null; - } - - Tx voteRevealTx = optionalVoteRevealTx.get(); + .filter(txOutput -> { // If we get a voteReveal tx which was published too late we ignore it. - if (!periodService.isTxInPhaseAndCycle(voteRevealTx.getId(), DaoPhase.Phase.VOTE_REVEAL, chainHeight)) { - log.warn("We got a vote reveal tx with was not in the correct phase and/or cycle. voteRevealTxId={}", voteRevealTx.getId()); - return null; - } + String voteRevealTx = txOutput.getTxId(); + boolean txInPhase = periodService.isTxInPhase(voteRevealTx, DaoPhase.Phase.VOTE_REVEAL); + if (!txInPhase) + log.warn("We got a vote reveal tx with was not in the correct phase of that cycle. voteRevealTxId={}", voteRevealTx); + return txInPhase; + }) + .map(txOutput -> { + String voteRevealTxId = txOutput.getTxId(); Cycle currentCycle = periodService.getCurrentCycle(); + checkNotNull(currentCycle, "currentCycle must not be null"); try { + byte[] opReturnData = txOutput.getOpReturnData(); + Optional optionalVoteRevealTx = daoStateService.getTx(voteRevealTxId); + checkArgument(optionalVoteRevealTx.isPresent(), "optionalVoteRevealTx must be present. voteRevealTxId=" + voteRevealTxId); + Tx voteRevealTx = optionalVoteRevealTx.get(); + // TODO maybe verify version in opReturn + // Here we use only blockchain tx data so far so we don't have risks with missing P2P network data. + // We work back from the voteRealTx to the blindVoteTx to caclulate the majority hash. From that we + // will derive the blind vote list we will use for result calculation and as it was based on + // blockchain data it will be consistent for all peers independent on their P2P network data state. TxOutput blindVoteStakeOutput = VoteResultConsensus.getConnectedBlindVoteStakeOutput(voteRevealTx, daoStateService); String blindVoteTxId = blindVoteStakeOutput.getTxId(); - boolean isBlindVoteInCorrectPhaseAndCycle = periodService.isTxInPhaseAndCycle(blindVoteTxId, DaoPhase.Phase.BLIND_VOTE, chainHeight); - // If we get a voteReveal tx which was published too late we ignore it. - if (!isBlindVoteInCorrectPhaseAndCycle) { - log.warn("We got a blind vote tx with was not in the correct phase and/or cycle. blindVoteTxId={}", blindVoteTxId); + + // If we get a blind vote tx which was published too late we ignore it. + if (!periodService.isTxInPhaseAndCycle(blindVoteTxId, DaoPhase.Phase.BLIND_VOTE, chainHeight)) { + log.warn("We got a blind vote tx with was not in the correct phase and/or cycle. " + + "We ignore that vote reveal and blind vote tx. voteRevealTx={}, blindVoteTxId={}", + voteRevealTx, blindVoteTxId); return null; } - VoteResultConsensus.validateBlindVoteTx(blindVoteStakeOutput.getTxId(), daoStateService, periodService, chainHeight); + VoteResultConsensus.validateBlindVoteTx(blindVoteTxId, daoStateService, periodService, chainHeight); + + byte[] hashOfBlindVoteList = VoteResultConsensus.getHashOfBlindVoteList(opReturnData); + long blindVoteStake = blindVoteStakeOutput.getValue(); List blindVoteList = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); Optional optionalBlindVote = blindVoteList.stream() @@ -284,46 +294,46 @@ private Set getDecryptedBallotsWithMeritsSet(int cha .findAny(); if (optionalBlindVote.isPresent()) { BlindVote blindVote = optionalBlindVote.get(); + SecretKey secretKey = VoteResultConsensus.getSecretKey(opReturnData); try { - SecretKey secretKey = VoteResultConsensus.getSecretKey(opReturnData); VoteWithProposalTxIdList voteWithProposalTxIdList = VoteResultConsensus.decryptVotes(blindVote.getEncryptedVotes(), secretKey); MeritList meritList = MeritConsensus.decryptMeritList(blindVote.getEncryptedMeritList(), secretKey); // We lookup for the proposals we have in our local list which match the txId from the // voteWithProposalTxIdList and create a ballot list with the proposal and the vote from // the voteWithProposalTxIdList BallotList ballotList = createBallotList(voteWithProposalTxIdList); - byte[] hashOfBlindVoteList = VoteResultConsensus.getHashOfBlindVoteList(opReturnData); - long blindVoteStake = blindVoteStakeOutput.getValue(); log.info("Add entry to decryptedBallotsWithMeritsSet: blindVoteTxId={}, voteRevealTxId={}, blindVoteStake={}, ballotList={}", blindVoteTxId, voteRevealTxId, blindVoteStake, ballotList); return new DecryptedBallotsWithMerits(hashOfBlindVoteList, blindVoteTxId, voteRevealTxId, blindVoteStake, ballotList, meritList); - } catch (VoteResultException.MissingBallotException missingBallotException) { - log.warn("We are missing proposals to create the vote result: " + missingBallotException.toString()); - missingDataRequestService.sendRepublishRequest(); - voteResultExceptions.add(new VoteResultException(currentCycle, missingBallotException)); - return null; } catch (VoteResultException.DecryptionException decryptionException) { - log.warn("Could not decrypt data: " + decryptionException.toString()); + // We don't consider such vote reveal txs valid for the majority hash + // calculation and don't add it to our result collection + log.error("Could not decrypt blind vote. This vote reveal and blind vote will be ignored. " + + "VoteRevealTxId={}. DecryptionException={}", voteRevealTxId, decryptionException.toString()); voteResultExceptions.add(new VoteResultException(currentCycle, decryptionException)); return null; } - } else { - log.warn("We have a blindVoteTx but we do not have the corresponding blindVote payload in our local database.\n" + - "That can happen if the blindVote item was not properly broadcast. We will go on " + - "and see if that blindVote was part of the majority data view. If so we should " + - "recover the missing blind vote by a request to our peers. blindVoteTxId={}", blindVoteTxId); - - VoteResultException.MissingBlindVoteDataException voteResultException = new VoteResultException.MissingBlindVoteDataException(blindVoteTxId); - missingDataRequestService.sendRepublishRequest(); - voteResultExceptions.add(new VoteResultException(currentCycle, voteResultException)); - return null; } - } catch (VoteResultException.ValidationException e) { - log.warn("Could not create DecryptedBallotsWithMerits because of voteResultValidationException: " + e.toString()); - voteResultExceptions.add(new VoteResultException(currentCycle, e)); - return null; + + log.warn("We have a blindVoteTx but we do not have the corresponding blindVote payload.\n" + + "That can happen if the blindVote item was not properly broadcast. " + + "We still add it to our result collection because it might be relevant for the majority " + + "hash by stake calculation. blindVoteTxId={}", blindVoteTxId); + + missingDataRequestService.sendRepublishRequest(); + + // We prefer to use an empty list here instead a null or optional value to avoid that + // client code need to handle nullable or optional values. + BallotList emptyBallotList = new BallotList(new ArrayList<>()); + MeritList emptyMeritList = new MeritList(new ArrayList<>()); + log.info("Add entry to decryptedBallotsWithMeritsSet: blindVoteTxId={}, voteRevealTxId={}, " + + "blindVoteStake={}, ballotList={}", + blindVoteTxId, voteRevealTxId, blindVoteStake, emptyBallotList); + return new DecryptedBallotsWithMerits(hashOfBlindVoteList, blindVoteTxId, voteRevealTxId, + blindVoteStake, emptyBallotList, emptyMeritList); } catch (Throwable e) { - log.error("Could not create DecryptedBallotsWithMerits because of an unknown exception: " + e.toString()); + log.error("Could not create DecryptedBallotsWithMerits from voteRevealTxId {} because of " + + "exception: {}", voteRevealTxId, e.toString()); voteResultExceptions.add(new VoteResultException(currentCycle, e)); return null; } @@ -424,7 +434,6 @@ private boolean isBlindVoteListMatchingMajority(byte[] majorityVoteListHash) { // missing items from the network. Optional> permutatedList = findPermutatedListMatchingMajority(majorityVoteListHash); if (permutatedList.isPresent()) { - //TODO do we need to apply/store it for later use? return true; } else { diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/DecryptedBallotsWithMerits.java b/core/src/main/java/bisq/core/dao/state/model/governance/DecryptedBallotsWithMerits.java index 5f6708206c2..2e20d524b11 100644 --- a/core/src/main/java/bisq/core/dao/state/model/governance/DecryptedBallotsWithMerits.java +++ b/core/src/main/java/bisq/core/dao/state/model/governance/DecryptedBallotsWithMerits.java @@ -46,6 +46,8 @@ public class DecryptedBallotsWithMerits implements PersistablePayload, Immutable private final String blindVoteTxId; private final String voteRevealTxId; private final long stake; + + // BallotList and meritList can be empty list in case we don't have a blind vote payload private final BallotList ballotList; private final MeritList meritList; From 076e3faf95a3d06f69e4c664bb04e45eea08ddff Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Tue, 5 Feb 2019 13:41:15 +0100 Subject: [PATCH 02/12] Refactor: Move code for adding decryptedBallotsWithMeritsSet to daoStateService --- .../core/dao/governance/voteresult/VoteResultService.java | 4 +--- .../src/main/java/bisq/core/dao/state/DaoStateService.java | 7 +++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java index f80784f953f..5a12e6ed00e 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java @@ -176,9 +176,7 @@ private void maybeCalculateVoteResult(int chainHeight) { long startTs = System.currentTimeMillis(); Set decryptedBallotsWithMeritsSet = getDecryptedBallotsWithMeritsSet(chainHeight); - decryptedBallotsWithMeritsSet.stream() - .filter(e -> !daoStateService.getDecryptedBallotsWithMeritsList().contains(e)) - .forEach(daoStateService.getDecryptedBallotsWithMeritsList()::add); + daoStateService.addAllDecryptedBallotsWithMeritsList(decryptedBallotsWithMeritsSet); if (!decryptedBallotsWithMeritsSet.isEmpty()) { // From the decryptedBallotsWithMerits we create a map with the hash of the blind vote list as key and the diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateService.java b/core/src/main/java/bisq/core/dao/state/DaoStateService.java index 76122a7afe8..1f29d53712d 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -912,6 +912,13 @@ public List getDecryptedBallotsWithMeritsList() { return daoState.getDecryptedBallotsWithMeritsList(); } + public void addAllDecryptedBallotsWithMeritsList(Set decryptedBallotsWithMeritsSet) { + decryptedBallotsWithMeritsSet.stream() + .filter(e -> !daoState.getDecryptedBallotsWithMeritsList().contains(e)) + .forEach(daoState.getDecryptedBallotsWithMeritsList()::add); + + } + /////////////////////////////////////////////////////////////////////////////////////////// // Asset listing fee From e2fac0c759ebd877202b70db558ca47c01c4a86e Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Tue, 5 Feb 2019 14:25:48 +0100 Subject: [PATCH 03/12] Only use blind vote payloads which are in majority list --- .../bisq/common/util/PermutationUtil.java | 2 + .../voteresult/VoteResultService.java | 79 +++++++++++++------ .../bisq/core/dao/state/DaoStateService.java | 9 ++- 3 files changed, 64 insertions(+), 26 deletions(-) diff --git a/common/src/main/java/bisq/common/util/PermutationUtil.java b/common/src/main/java/bisq/common/util/PermutationUtil.java index dbcab390a16..0a84e478179 100644 --- a/common/src/main/java/bisq/common/util/PermutationUtil.java +++ b/common/src/main/java/bisq/common/util/PermutationUtil.java @@ -53,6 +53,8 @@ public static List getPartialList(List list, List indicesToRe return altered; } + //TODO optimize algorithm so that it starts from all objects and goes down instead starting with from the bottom. + // That should help that we are not hitting the iteration limit so easily. /** * Returns a list of all possible permutations of a give sorted list ignoring duplicates. * E.g. List [A,B,C] results in this list of permutations: [[A], [B], [A,B], [C], [A,C], [B,C], [A,B,C]] diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java index 5a12e6ed00e..dc1dac3285b 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java @@ -173,13 +173,12 @@ private void maybeCalculateVoteResult(int chainHeight) { if (isInVoteResultPhase(chainHeight)) { log.info("CalculateVoteResult at chainHeight={}", chainHeight); Cycle currentCycle = periodService.getCurrentCycle(); + checkNotNull(currentCycle, "currentCycle must not be null"); long startTs = System.currentTimeMillis(); Set decryptedBallotsWithMeritsSet = getDecryptedBallotsWithMeritsSet(chainHeight); - daoStateService.addAllDecryptedBallotsWithMeritsList(decryptedBallotsWithMeritsSet); - if (!decryptedBallotsWithMeritsSet.isEmpty()) { - // From the decryptedBallotsWithMerits we create a map with the hash of the blind vote list as key and the + // From the decryptedBallotsWithMeritsSet we create a map with the hash of the blind vote list as key and the // aggregated stake+merit as value. That map is used for calculating the majority of the blind vote lists. // There might be conflicting versions due the eventually consistency of the P2P network (if some blind // votes do not arrive at all voters) which would lead to consensus failure in the result calculation. @@ -192,40 +191,35 @@ private void maybeCalculateVoteResult(int chainHeight) { try { // Get majority hash - byte[] majorityBlindVoteListHash = getMajorityBlindVoteListHash(stakeByHashOfBlindVoteListMap); + byte[] majorityBlindVoteListHash = calculateMajorityBlindVoteListHash(stakeByHashOfBlindVoteListMap); // Is our local list matching the majority data view? - if (isBlindVoteListMatchingMajority(majorityBlindVoteListHash)) { - //TODO should we write the decryptedBallotsWithMerits here into the state? + Optional> optionalBlindVoteListMatchingMajorityHash = findBlindVoteListMatchingMajorityHash(majorityBlindVoteListHash); + if (optionalBlindVoteListMatchingMajorityHash.isPresent()) { + // Only if we have all blind vote payloads and know the right list matching the majority we add + // it to our state. Otherwise we are not in consensus with the network. + daoStateService.addDecryptedBallotsWithMeritsSet(decryptedBallotsWithMeritsSet); - //TODO we get duplicated items in evaluatedProposals with diff. merit values + // FIXME we got duplicated items in evaluatedProposals with diff. merit values, find out why... Set evaluatedProposals = getEvaluatedProposals(decryptedBallotsWithMeritsSet, chainHeight); - + daoStateService.addEvaluatedProposalSet(evaluatedProposals); Set acceptedEvaluatedProposals = getAcceptedEvaluatedProposals(evaluatedProposals); applyAcceptedProposals(acceptedEvaluatedProposals, chainHeight); - - evaluatedProposals.stream() - .filter(e -> !daoStateService.getEvaluatedProposalList().contains(e)) - .forEach(daoStateService.getEvaluatedProposalList()::add); log.info("processAllVoteResults completed"); } else { - log.warn("Our list of received blind votes do not match the list from the majority of voters."); - // TODO request missing blind votes + String msg = "We could not find a list which matches the majority so we cannot calculate the vote result."; + log.warn(msg); + voteResultExceptions.add(new VoteResultException(currentCycle, new Exception(msg))); } - } catch (VoteResultException.ValidationException e) { log.warn(e.toString()); + log.warn("decryptedBallotsWithMeritsSet " + decryptedBallotsWithMeritsSet); e.printStackTrace(); voteResultExceptions.add(new VoteResultException(currentCycle, e)); } catch (VoteResultException.ConsensusException e) { log.warn(e.toString()); log.warn("decryptedBallotsWithMeritsSet " + decryptedBallotsWithMeritsSet); e.printStackTrace(); - - //TODO notify application of that case (e.g. add error handler) - // The vote cycle is invalid as conflicting data views of the blind vote data exist and the winner - // did not reach super majority of 80%. - voteResultExceptions.add(new VoteResultException(currentCycle, e)); } } else { @@ -405,7 +399,7 @@ private Map getStakeByHashOfBlindVoteListMap(Set return map; } - private byte[] getMajorityBlindVoteListHash(Map map) + private byte[] calculateMajorityBlindVoteListHash(Map map) throws VoteResultException.ValidationException, VoteResultException.ConsensusException { List list = map.entrySet().stream() .map(entry -> new HashWithStake(entry.getKey().bytes, entry.getValue())) @@ -414,6 +408,37 @@ private byte[] getMajorityBlindVoteListHash(Map } // Deal with eventually consistency of P2P network + private Optional> findBlindVoteListMatchingMajorityHash(byte[] majorityVoteListHash) { + // We reuse the method at voteReveal domain used when creating the hash + List blindVotes = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); + if (isListMatchingMajority(majorityVoteListHash, blindVotes, true)) { + // Out local list is matching the majority hash + return Optional.of(blindVotes); + } else { + log.warn("Our local list of blind vote payloads does not match the majorityVoteListHash. " + + "We try permuting our list to find a matching variant"); + // Each voter has re-published his blind vote list when broadcasting the reveal tx so there should have a very + // high chance that we have received all blind votes which have been used by the majority of the + // voters (majority by stake). + // It still could be that we have additional blind votes so our hash does not match. We can try to permute + // our list with excluding items to see if we get a matching list. If not last resort is to request the + // missing items from the network. + // TODO we should add some metadata about the published blind vote txId list so it becomes easier to find + // the majority list. We could add a new payload for that or add a message to request the list from peers. + Optional> permutatedList = findPermutatedListMatchingMajority(majorityVoteListHash); + if (permutatedList.isPresent()) { + return permutatedList; + } else { + log.warn("We did not find a permutation of our blindVote list which matches the majority view. " + + "We will request the blindVote data from the peers."); + // This is async operation. We will restart the whole verification process once we received the data. + missingDataRequestService.sendRepublishRequest(); + return Optional.empty(); + } + } + } + + /* // Deal with eventually consistency of P2P network private boolean isBlindVoteListMatchingMajority(byte[] majorityVoteListHash) { // We reuse the method at voteReveal domain used when creating the hash byte[] myBlindVoteListHash = voteRevealService.getHashOfBlindVoteList(); @@ -442,14 +467,14 @@ private boolean isBlindVoteListMatchingMajority(byte[] majorityVoteListHash) { } } return matches; - } + }*/ private Optional> findPermutatedListMatchingMajority(byte[] majorityVoteListHash) { List list = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); long ts = System.currentTimeMillis(); List> result = PermutationUtil.findAllPermutations(list, 1000000); for (List variation : result) { - if (isListMatchingMajority(majorityVoteListHash, variation)) { + if (isListMatchingMajority(majorityVoteListHash, variation, false)) { log.info("We found a variation of the blind vote list which matches the majority hash. variation={}", variation); log.info("findPermutatedListMatchingMajority for {} items took {} ms.", @@ -463,8 +488,14 @@ private Optional> findPermutatedListMatchingMajority(byte[] majo return Optional.empty(); } - private boolean isListMatchingMajority(byte[] majorityVoteListHash, List list) { + private boolean isListMatchingMajority(byte[] majorityVoteListHash, List list, boolean doLog) { byte[] hashOfBlindVoteList = VoteRevealConsensus.getHashOfBlindVoteList(list); + if (doLog) { + log.info("majorityVoteListHash " + Utilities.bytesAsHexString(majorityVoteListHash)); + log.info("hashOfBlindVoteList " + Utilities.bytesAsHexString(hashOfBlindVoteList)); + log.info("List of blindVoteTxIds " + list.stream().map(BlindVote::getTxId) + .collect(Collectors.joining(", "))); + } return Arrays.equals(majorityVoteListHash, hashOfBlindVoteList); } diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateService.java b/core/src/main/java/bisq/core/dao/state/DaoStateService.java index 1f29d53712d..0d46bdee650 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -908,15 +908,20 @@ public List getEvaluatedProposalList() { return daoState.getEvaluatedProposalList(); } + public void addEvaluatedProposalSet(Set evaluatedProposals) { + evaluatedProposals.stream() + .filter(e -> !daoState.getEvaluatedProposalList().contains(e)) + .forEach(daoState.getEvaluatedProposalList()::add); + } + public List getDecryptedBallotsWithMeritsList() { return daoState.getDecryptedBallotsWithMeritsList(); } - public void addAllDecryptedBallotsWithMeritsList(Set decryptedBallotsWithMeritsSet) { + public void addDecryptedBallotsWithMeritsSet(Set decryptedBallotsWithMeritsSet) { decryptedBallotsWithMeritsSet.stream() .filter(e -> !daoState.getDecryptedBallotsWithMeritsList().contains(e)) .forEach(daoState.getDecryptedBallotsWithMeritsList()::add); - } From df685ceb557f3dd69db208a5925ddcfe2636161f Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Tue, 5 Feb 2019 15:00:19 +0100 Subject: [PATCH 04/12] Remove merit from majority hash calculation (use only stake) --- .../voteresult/VoteResultService.java | 53 +++++-------------- 1 file changed, 12 insertions(+), 41 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java index dc1dac3285b..c376eb22333 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java @@ -179,13 +179,15 @@ private void maybeCalculateVoteResult(int chainHeight) { Set decryptedBallotsWithMeritsSet = getDecryptedBallotsWithMeritsSet(chainHeight); if (!decryptedBallotsWithMeritsSet.isEmpty()) { // From the decryptedBallotsWithMeritsSet we create a map with the hash of the blind vote list as key and the - // aggregated stake+merit as value. That map is used for calculating the majority of the blind vote lists. + // aggregated stake as value (no merit as that is part of the P2P network data and might lead to inconsistency). + // That map is used for calculating the majority of the blind vote lists. // There might be conflicting versions due the eventually consistency of the P2P network (if some blind - // votes do not arrive at all voters) which would lead to consensus failure in the result calculation. - // To solve that problem we will only consider the majority data view as valid. + // vote payloads do not arrive at all voters) which would lead to consensus failure in the result calculation. + // To solve that problem we will only consider the blind votes valid which are matching the majority hash. // If multiple data views would have the same stake we sort additionally by the hex value of the // blind vote hash and use the first one in the sorted list as winner. - // A node which has a local blindVote list which does not match the winner data view need to recover it's + // A node which has a local blindVote list which does not match the winner data view will try + // permutations of his local list and if that does not succeed he need to recover it's // local blindVote list by requesting the correct list from other peers. Map stakeByHashOfBlindVoteListMap = getStakeByHashOfBlindVoteListMap(decryptedBallotsWithMeritsSet); @@ -207,6 +209,7 @@ private void maybeCalculateVoteResult(int chainHeight) { applyAcceptedProposals(acceptedEvaluatedProposals, chainHeight); log.info("processAllVoteResults completed"); } else { + // TODO make sure we handle it in the UI -> ask user to restart String msg = "We could not find a list which matches the majority so we cannot calculate the vote result."; log.warn(msg); voteResultExceptions.add(new VoteResultException(currentCycle, new Exception(msg))); @@ -385,16 +388,15 @@ private Map getStakeByHashOfBlindVoteListMap(Set decryptedBallotsWithMeritsSet.forEach(decryptedBallotsWithMerits -> { P2PDataStorage.ByteArray hash = new P2PDataStorage.ByteArray(decryptedBallotsWithMerits.getHashOfBlindVoteList()); map.putIfAbsent(hash, 0L); + // We must not user the merit(stake) as that is from the P2P network data and it is not guaranteed that we + // have received it. We must rely only on blockchain data. The stake is from the vote reveal tx input. long aggregatedStake = map.get(hash); - //TODO move to consensus class - long merit = decryptedBallotsWithMerits.getMerit(daoStateService); long stake = decryptedBallotsWithMerits.getStake(); - long combinedStake = stake + merit; - aggregatedStake += combinedStake; + aggregatedStake += stake; map.put(hash, aggregatedStake); - log.debug("blindVoteTxId={}, meritStake={}, stake={}, combinedStake={}", - decryptedBallotsWithMerits.getBlindVoteTxId(), merit, stake, combinedStake); + log.debug("blindVoteTxId={}, stake={}", + decryptedBallotsWithMerits.getBlindVoteTxId(), stake); }); return map; } @@ -438,37 +440,6 @@ private Optional> findBlindVoteListMatchingMajorityHash(byte[] m } } - /* // Deal with eventually consistency of P2P network - private boolean isBlindVoteListMatchingMajority(byte[] majorityVoteListHash) { - // We reuse the method at voteReveal domain used when creating the hash - byte[] myBlindVoteListHash = voteRevealService.getHashOfBlindVoteList(); - log.info("majorityVoteListHash " + Utilities.bytesAsHexString(majorityVoteListHash)); - log.info("myBlindVoteListHash " + Utilities.bytesAsHexString(myBlindVoteListHash)); - boolean matches = Arrays.equals(majorityVoteListHash, myBlindVoteListHash); - // refactor to method - if (!matches) { - log.warn("myBlindVoteListHash does not match with majorityVoteListHash. We try permuting our list to " + - "find a matching variant"); - // Each voter has re-published his blind vote list when broadcasting the reveal tx so it should have a very - // high change that we have received all blind votes which have been used by the majority of the - // voters (e.g. its stake not nr. of voters). - // It still could be that we have additional blind votes so our hash does not match. We can try to permute - // our list with excluding items to see if we get a matching list. If not last resort is to request the - // missing items from the network. - Optional> permutatedList = findPermutatedListMatchingMajority(majorityVoteListHash); - if (permutatedList.isPresent()) { - - return true; - } else { - log.warn("We did not find a permutation of our blindVote list which matches the majority view. " + - "We will request the blindVote data from the peers."); - // This is async operation. We will restart the whole verification process once we received the data. - missingDataRequestService.sendRepublishRequest(); - } - } - return matches; - }*/ - private Optional> findPermutatedListMatchingMajority(byte[] majorityVoteListHash) { List list = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); long ts = System.currentTimeMillis(); From 4868b0e5dfed63d2e6fcda93840f1591689974a6 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Wed, 6 Feb 2019 00:26:11 +0100 Subject: [PATCH 05/12] Fix onParseBlockChainComplete handler We had 2 times the onParseTxsComplete called in the version before --- core/src/main/java/bisq/core/dao/state/DaoStateService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateService.java b/core/src/main/java/bisq/core/dao/state/DaoStateService.java index 0d46bdee650..281f7ef7949 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -220,8 +220,9 @@ public void onParseBlockComplete(Block block) { public void onParseBlockChainComplete() { parseBlockChainComplete = true; - // Now we need to trigger the onParseBlockComplete to update the state in the app - getLastBlock().ifPresent(this::onParseBlockComplete); + getLastBlock().ifPresent(block -> { + daoStateListeners.forEach(l -> l.onParseTxsCompleteAfterBatchProcessing(block)); + }); daoStateListeners.forEach(DaoStateListener::onParseBlockChainComplete); } From 1a71f4928fda6e4e75f76b6311a1752bcaa1ec0c Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Wed, 6 Feb 2019 00:39:27 +0100 Subject: [PATCH 06/12] Filter vote result items according to majority hash --- .../voteresult/VoteResultService.java | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java index c376eb22333..02849b525bd 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java @@ -30,7 +30,6 @@ import bisq.core.dao.governance.proposal.ProposalListPresentation; import bisq.core.dao.governance.voteresult.issuance.IssuanceService; import bisq.core.dao.governance.votereveal.VoteRevealConsensus; -import bisq.core.dao.governance.votereveal.VoteRevealService; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.Block; @@ -97,7 +96,6 @@ */ @Slf4j public class VoteResultService implements DaoStateListener, DaoSetupService { - private final VoteRevealService voteRevealService; private final ProposalListPresentation proposalListPresentation; private final DaoStateService daoStateService; private final PeriodService periodService; @@ -107,6 +105,8 @@ public class VoteResultService implements DaoStateListener, DaoSetupService { private final MissingDataRequestService missingDataRequestService; @Getter private final ObservableList voteResultExceptions = FXCollections.observableArrayList(); + @Getter + private Set invalidDecryptedBallotsWithMeritItems = new HashSet<>(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -114,15 +114,13 @@ public class VoteResultService implements DaoStateListener, DaoSetupService { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public VoteResultService(VoteRevealService voteRevealService, - ProposalListPresentation proposalListPresentation, + public VoteResultService(ProposalListPresentation proposalListPresentation, DaoStateService daoStateService, PeriodService periodService, BallotListService ballotListService, BlindVoteListService blindVoteListService, IssuanceService issuanceService, MissingDataRequestService missingDataRequestService) { - this.voteRevealService = voteRevealService; this.proposalListPresentation = proposalListPresentation; this.daoStateService = daoStateService; this.periodService = periodService; @@ -151,14 +149,6 @@ public void start() { // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public void onNewBlockHeight(int blockHeight) { - } - - @Override - public void onParseBlockChainComplete() { - } - @Override public void onParseTxsComplete(Block block) { maybeCalculateVoteResult(block.getHeight()); @@ -198,12 +188,29 @@ private void maybeCalculateVoteResult(int chainHeight) { // Is our local list matching the majority data view? Optional> optionalBlindVoteListMatchingMajorityHash = findBlindVoteListMatchingMajorityHash(majorityBlindVoteListHash); if (optionalBlindVoteListMatchingMajorityHash.isPresent()) { + List blindVoteList = optionalBlindVoteListMatchingMajorityHash.get(); + log.info("blindVoteListMatchingMajorityHash: " + blindVoteList.stream() + .map(e -> "blindVoteTxId=" + e.getTxId() + ", Stake=" + e.getStake()) + .collect(Collectors.toList())); + + Set blindVoteTxIdSet = blindVoteList.stream().map(BlindVote::getTxId).collect(Collectors.toSet()); + // We need to filter out result list according to the majority hash list + Set filteredDecryptedBallotsWithMeritsSet = decryptedBallotsWithMeritsSet.stream() + .filter(decryptedBallotsWithMerits -> { + boolean contains = blindVoteTxIdSet.contains(decryptedBallotsWithMerits.getBlindVoteTxId()); + if (!contains) { + invalidDecryptedBallotsWithMeritItems.add(decryptedBallotsWithMerits); + } + return contains; + }) + .collect(Collectors.toSet()); + // Only if we have all blind vote payloads and know the right list matching the majority we add // it to our state. Otherwise we are not in consensus with the network. - daoStateService.addDecryptedBallotsWithMeritsSet(decryptedBallotsWithMeritsSet); + daoStateService.addDecryptedBallotsWithMeritsSet(filteredDecryptedBallotsWithMeritsSet); // FIXME we got duplicated items in evaluatedProposals with diff. merit values, find out why... - Set evaluatedProposals = getEvaluatedProposals(decryptedBallotsWithMeritsSet, chainHeight); + Set evaluatedProposals = getEvaluatedProposals(filteredDecryptedBallotsWithMeritsSet, chainHeight); daoStateService.addEvaluatedProposalSet(evaluatedProposals); Set acceptedEvaluatedProposals = getAcceptedEvaluatedProposals(evaluatedProposals); applyAcceptedProposals(acceptedEvaluatedProposals, chainHeight); @@ -214,12 +221,7 @@ private void maybeCalculateVoteResult(int chainHeight) { log.warn(msg); voteResultExceptions.add(new VoteResultException(currentCycle, new Exception(msg))); } - } catch (VoteResultException.ValidationException e) { - log.warn(e.toString()); - log.warn("decryptedBallotsWithMeritsSet " + decryptedBallotsWithMeritsSet); - e.printStackTrace(); - voteResultExceptions.add(new VoteResultException(currentCycle, e)); - } catch (VoteResultException.ConsensusException e) { + } catch (Throwable e) { log.warn(e.toString()); log.warn("decryptedBallotsWithMeritsSet " + decryptedBallotsWithMeritsSet); e.printStackTrace(); From d10d91bb84137e27fe96735e56e92f2872faf22d Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Wed, 6 Feb 2019 00:43:02 +0100 Subject: [PATCH 07/12] Make methods in DaoStateListener default To avoid empty handlers I have set all methods to default so the client code implements only the used one. Move handler code from onNewBlockHeight to onParseTxsCompleteAfterBatchProcessing to avoid too much UI updates while parsing. --- .../governance/voteresult/VoteResultException.java | 1 + .../java/bisq/core/dao/state/DaoStateListener.java | 6 ++++-- .../bisq/desktop/main/dao/governance/PhasesView.java | 11 ++++------- .../governance/dashboard/GovernanceDashboardView.java | 9 +++------ .../main/dao/governance/make/MakeProposalView.java | 7 ++----- .../bisq/desktop/main/dao/wallet/tx/BsqTxView.java | 7 ------- 6 files changed, 14 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultException.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultException.java index eda4f1305c1..06cd04d46a9 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultException.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultException.java @@ -26,6 +26,7 @@ import lombok.Getter; import lombok.Value; +@EqualsAndHashCode(callSuper = true) public class VoteResultException extends Exception { @Getter private final int heightOfFirstBlockInCycle; diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateListener.java b/core/src/main/java/bisq/core/dao/state/DaoStateListener.java index 8c1f0b8b11d..b1d0af953f1 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateListener.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateListener.java @@ -20,9 +20,11 @@ import bisq.core.dao.state.model.blockchain.Block; public interface DaoStateListener { - void onNewBlockHeight(int blockHeight); + default void onNewBlockHeight(int blockHeight) { + } - void onParseBlockChainComplete(); + default void onParseBlockChainComplete() { + } default void onParseTxsCompleteAfterBatchProcessing(Block block) { } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/PhasesView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/PhasesView.java index 8b044b71d7c..82958af265d 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/PhasesView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/PhasesView.java @@ -23,6 +23,7 @@ import bisq.core.dao.DaoFacade; import bisq.core.dao.governance.period.PeriodService; import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.model.blockchain.Block; import bisq.core.dao.state.model.governance.DaoPhase; import bisq.core.locale.Res; @@ -77,8 +78,8 @@ public void deactivate() { /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void onNewBlockHeight(int height) { - applyData(height); + public void onParseTxsCompleteAfterBatchProcessing(Block block) { + applyData(block.getHeight()); phaseBarsItems.forEach(item -> { DaoPhase.Phase phase = item.getPhase(); @@ -86,7 +87,7 @@ public void onNewBlockHeight(int height) { // block which would be a break). Only at result phase we don't have that situation ans show the last block // as valid block in the phase. if (periodService.isInPhaseButNotLastBlock(phase) || - (phase == DaoPhase.Phase.RESULT && periodService.isInPhase(height, phase))) { + (phase == DaoPhase.Phase.RESULT && periodService.isInPhase(block.getHeight(), phase))) { item.setActive(); } else { item.setInActive(); @@ -94,10 +95,6 @@ public void onNewBlockHeight(int height) { }); } - @Override - public void onParseBlockChainComplete() { - } - /////////////////////////////////////////////////////////////////////////////////////////// // Private diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/dashboard/GovernanceDashboardView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/dashboard/GovernanceDashboardView.java index af1fd41ce1a..d31d1fa4a81 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/dashboard/GovernanceDashboardView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/dashboard/GovernanceDashboardView.java @@ -26,6 +26,7 @@ import bisq.core.dao.DaoFacade; import bisq.core.dao.governance.period.PeriodService; import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.model.blockchain.Block; import bisq.core.dao.state.model.governance.DaoPhase; import bisq.core.locale.Res; import bisq.core.util.BSFormatter; @@ -109,12 +110,8 @@ protected void deactivate() { /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void onNewBlockHeight(int height) { - applyData(height); - } - - @Override - public void onParseBlockChainComplete() { + public void onParseTxsCompleteAfterBatchProcessing(Block block) { + applyData(block.getHeight()); } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java index b7f80ebd12a..1583a196fb6 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java @@ -39,6 +39,7 @@ import bisq.core.dao.governance.proposal.TxException; import bisq.core.dao.governance.proposal.param.ChangeParamValidator; import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.model.blockchain.Block; import bisq.core.dao.state.model.governance.DaoPhase; import bisq.core.dao.state.model.governance.Proposal; import bisq.core.dao.state.model.governance.Role; @@ -184,17 +185,13 @@ protected void deactivate() { /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void onNewBlockHeight(int height) { + public void onParseTxsCompleteAfterBatchProcessing(Block block) { boolean isProposalPhase = daoFacade.isInPhaseButNotLastBlock(DaoPhase.Phase.PROPOSAL); proposalTypeComboBox.setDisable(!isProposalPhase); if (!isProposalPhase) proposalTypeComboBox.getSelectionModel().clearSelection(); } - @Override - public void onParseBlockChainComplete() { - } - /////////////////////////////////////////////////////////////////////////////////////////// // Private diff --git a/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java index 25486191ed7..345c718bbc0 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java @@ -213,18 +213,11 @@ public void onUpdateBalances(Coin confirmedBalance, // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public void onNewBlockHeight(int blockHeight) { - } - @Override public void onParseTxsCompleteAfterBatchProcessing(Block block) { onUpdateAnyChainHeight(); } - @Override - public void onParseBlockChainComplete() { - } /////////////////////////////////////////////////////////////////////////////////////////// From 1d5d9678bffeafbbc91acc948aa3730f068de48c Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Wed, 6 Feb 2019 00:50:24 +0100 Subject: [PATCH 08/12] Add popup for invalid votes --- .../dao/governance/result/VoteResultView.java | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java index d44c1f7d2c6..842330e7fa0 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java @@ -26,6 +26,7 @@ import bisq.desktop.components.TableGroupHeadline; import bisq.desktop.main.dao.governance.PhasesView; import bisq.desktop.main.dao.governance.ProposalDisplay; +import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.FormBuilder; import bisq.desktop.util.GUIUtil; import bisq.desktop.util.Layout; @@ -33,6 +34,7 @@ import bisq.core.btc.wallet.BsqWalletService; import bisq.core.dao.DaoFacade; import bisq.core.dao.governance.period.CycleService; +import bisq.core.dao.governance.period.PeriodService; import bisq.core.dao.governance.proposal.ProposalService; import bisq.core.dao.governance.voteresult.VoteResultException; import bisq.core.dao.governance.voteresult.VoteResultService; @@ -44,12 +46,15 @@ import bisq.core.dao.state.model.governance.DecryptedBallotsWithMerits; import bisq.core.dao.state.model.governance.EvaluatedProposal; import bisq.core.dao.state.model.governance.Proposal; +import bisq.core.dao.state.model.governance.Vote; import bisq.core.locale.Res; import bisq.core.user.Preferences; import bisq.core.util.BsqFormatter; import bisq.common.util.Tuple2; +import org.bitcoinj.core.Coin; + import javax.inject.Inject; import de.jensd.fx.fontawesome.AwesomeDude; @@ -94,6 +99,7 @@ public class VoteResultView extends ActivatableView implements D private final CycleService cycleService; private final VoteResultService voteResultService; private final ProposalService proposalService; + private final PeriodService periodService; private final BsqWalletService bsqWalletService; private final Preferences preferences; private final BsqFormatter bsqFormatter; @@ -129,6 +135,7 @@ public VoteResultView(DaoFacade daoFacade, CycleService cycleService, VoteResultService voteResultService, ProposalService proposalService, + PeriodService periodService, BsqWalletService bsqWalletService, Preferences preferences, BsqFormatter bsqFormatter, @@ -139,6 +146,7 @@ public VoteResultView(DaoFacade daoFacade, this.cycleService = cycleService; this.voteResultService = voteResultService; this.proposalService = proposalService; + this.periodService = periodService; this.bsqWalletService = bsqWalletService; this.preferences = preferences; this.bsqFormatter = bsqFormatter; @@ -186,14 +194,10 @@ protected void deactivate() { /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void onNewBlockHeight(int height) { + public void onParseTxsCompleteAfterBatchProcessing(Block block) { fillCycleList(); } - @Override - public void onParseBlockChainComplete() { - } - /////////////////////////////////////////////////////////////////////////////////////////// // UI handlers @@ -214,6 +218,37 @@ private void onResultsListItemSelected(CycleListItem item) { selectedProposalSubscription = EasyBind.subscribe(proposalsTableView.getSelectionModel().selectedItemProperty(), this::onSelectProposalResultListItem); + + StringBuilder sb = new StringBuilder(); + voteResultService.getInvalidDecryptedBallotsWithMeritItems().stream() + .filter(e -> periodService.isTxInCorrectCycle(e.getVoteRevealTxId(), + item.getResultsOfCycle().getCycle().getHeightOfFirstBlock())) + .forEach(e -> { + sb.append("\n") + .append(Res.getWithCol("dao.proposal.myVote.blindVoteTxId")).append(" ") + .append(e.getBlindVoteTxId()).append("\n") + .append(Res.getWithCol("dao.results.votes.table.header.stake")).append(" ") + .append(bsqFormatter.formatCoinWithCode(Coin.valueOf(e.getStake()))).append("\n"); + e.getBallotList().stream().forEach(ballot -> { + sb.append(Res.getWithCol("shared.name")).append(" ") + .append(ballot.getProposal().getName()).append("\n"); + sb.append(Res.getWithCol("dao.bond.table.column.link")).append(" ") + .append(ballot.getProposal().getLink()).append("\n"); + Vote vote = ballot.getVote(); + String voteString = vote == null ? Res.get("dao.proposal.display.myVote.ignored") : + vote.isAccepted() ? + Res.get("dao.proposal.display.myVote.accepted") : + Res.get("dao.proposal.display.myVote.rejected"); + sb.append(Res.getWithCol("dao.results.votes.table.header.vote")).append(" ") + .append(voteString).append("\n"); + + }); + }); + if (!sb.toString().isEmpty()) { + new Popup<>().information("We had invalid votes in that voting cycle. That can happen if a vote was " + + "not distributed well in the P2P network.\n" + + sb.toString()).show(); + } } } From 31a54b29bd36226b7e87ffae459c3d72db90ff04 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Wed, 6 Feb 2019 01:11:31 +0100 Subject: [PATCH 09/12] Cleanup DaoStateListener handlers --- .../bisq/core/btc/wallet/BsqWalletService.java | 8 -------- core/src/main/java/bisq/core/dao/DaoFacade.java | 4 ---- .../core/dao/governance/asset/AssetService.java | 8 -------- .../ballot/BallotListPresentation.java | 7 +------ .../blindvote/BlindVoteListService.java | 4 ---- .../blindvote/MyBlindVoteListService.java | 4 ---- .../core/dao/governance/bond/BondRepository.java | 8 -------- .../reputation/MyBondedReputationRepository.java | 8 -------- .../core/dao/governance/period/CycleService.java | 4 ---- .../proofofburn/ProofOfBurnService.java | 8 -------- .../proposal/MyProposalListService.java | 4 ---- .../proposal/ProposalListPresentation.java | 9 --------- .../dao/governance/proposal/ProposalService.java | 4 ---- .../governance/votereveal/VoteRevealService.java | 8 -------- .../core/dao/state/DaoStateSnapshotService.java | 16 ++++------------ .../dao/governance/proposals/ProposalsView.java | 4 ---- .../dao/wallet/dashboard/BsqDashboardView.java | 8 -------- 17 files changed, 5 insertions(+), 111 deletions(-) diff --git a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java index f59008d3385..893395cc61b 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java @@ -182,20 +182,12 @@ public void onWalletChanged(Wallet wallet) { // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public void onNewBlockHeight(int blockHeight) { - } - @Override public void onParseTxsCompleteAfterBatchProcessing(Block block) { if (isWalletReady()) updateBsqWalletTransactions(); } - @Override - public void onParseBlockChainComplete() { - } - /////////////////////////////////////////////////////////////////////////////////////////// // Overridden Methods diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index 59f636b75b2..1e9ebfb083c 100644 --- a/core/src/main/java/bisq/core/dao/DaoFacade.java +++ b/core/src/main/java/bisq/core/dao/DaoFacade.java @@ -186,10 +186,6 @@ public void onNewBlockHeight(int blockHeight) { if (blockHeight > 0 && periodService.getCurrentCycle() != null) periodService.getCurrentCycle().getPhaseForHeight(blockHeight).ifPresent(phaseProperty::set); } - - @Override - public void onParseBlockChainComplete() { - } }); } diff --git a/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java b/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java index 2fa80e3f4ec..f2d0f2bc247 100644 --- a/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java +++ b/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java @@ -270,10 +270,6 @@ private List getFeeTxs(StatefulAsset statefulAsset) { // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public void onNewBlockHeight(int blockHeight) { - } - @Override public void onParseTxsCompleteAfterBatchProcessing(Block block) { int chainHeight = daoStateService.getChainHeight(); @@ -283,10 +279,6 @@ public void onParseTxsCompleteAfterBatchProcessing(Block block) { } - @Override - public void onParseBlockChainComplete() { - } - /////////////////////////////////////////////////////////////////////////////////////////// // API diff --git a/core/src/main/java/bisq/core/dao/governance/ballot/BallotListPresentation.java b/core/src/main/java/bisq/core/dao/governance/ballot/BallotListPresentation.java index cf4e7c9acc5..bb18a0d089c 100644 --- a/core/src/main/java/bisq/core/dao/governance/ballot/BallotListPresentation.java +++ b/core/src/main/java/bisq/core/dao/governance/ballot/BallotListPresentation.java @@ -74,6 +74,7 @@ public BallotListPresentation(BallotListService ballotListService, @Override public void onNewBlockHeight(int blockHeight) { + //TODO should it be in onParseTxsComplete? ballotsOfCycle.setPredicate(ballot -> periodService.isTxInCorrectCycle(ballot.getTxId(), blockHeight)); } @@ -82,12 +83,6 @@ public void onParseTxsCompleteAfterBatchProcessing(Block block) { onListChanged(ballotListService.getValidatedBallotList()); } - @Override - public void onParseBlockChainComplete() { - // As we update the list in ProposalService.onParseBlockChainComplete we need to update here as well. - onListChanged(ballotListService.getValidatedBallotList()); - } - /////////////////////////////////////////////////////////////////////////////////////////// // BallotListService.BallotListChangeListener diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteListService.java b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteListService.java index 8d008eed410..8d5d2bdda4d 100644 --- a/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteListService.java +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteListService.java @@ -93,10 +93,6 @@ public void start() { // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public void onNewBlockHeight(int blockHeight) { - } - @Override public void onParseBlockChainComplete() { fillListFromAppendOnlyDataStore(); diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java index d27c2c9b134..6a2f49836fa 100644 --- a/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java @@ -170,10 +170,6 @@ public void readPersisted() { // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public void onNewBlockHeight(int blockHeight) { - } - @Override public void onParseBlockChainComplete() { rePublishOnceWellConnected(); diff --git a/core/src/main/java/bisq/core/dao/governance/bond/BondRepository.java b/core/src/main/java/bisq/core/dao/governance/bond/BondRepository.java index 52608f7ada1..512942ce417 100644 --- a/core/src/main/java/bisq/core/dao/governance/bond/BondRepository.java +++ b/core/src/main/java/bisq/core/dao/governance/bond/BondRepository.java @@ -156,18 +156,10 @@ public BondRepository(DaoStateService daoStateService, BsqWalletService bsqWalle @Override public void addListeners() { daoStateService.addBsqStateListener(new DaoStateListener() { - @Override - public void onNewBlockHeight(int blockHeight) { - } - @Override public void onParseTxsCompleteAfterBatchProcessing(Block block) { update(); } - - @Override - public void onParseBlockChainComplete() { - } }); bsqWalletService.getWalletTransactions().addListener((ListChangeListener) c -> update()); } diff --git a/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyBondedReputationRepository.java b/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyBondedReputationRepository.java index 96c8b6961d6..20766a754bf 100644 --- a/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyBondedReputationRepository.java +++ b/core/src/main/java/bisq/core/dao/governance/bond/reputation/MyBondedReputationRepository.java @@ -80,18 +80,10 @@ public MyBondedReputationRepository(DaoStateService daoStateService, @Override public void addListeners() { daoStateService.addBsqStateListener(new DaoStateListener() { - @Override - public void onNewBlockHeight(int blockHeight) { - } - @Override public void onParseTxsCompleteAfterBatchProcessing(Block block) { update(); } - - @Override - public void onParseBlockChainComplete() { - } }); bsqWalletService.getWalletTransactions().addListener((ListChangeListener) c -> update()); } diff --git a/core/src/main/java/bisq/core/dao/governance/period/CycleService.java b/core/src/main/java/bisq/core/dao/governance/period/CycleService.java index 53b49986098..66c45142dd9 100644 --- a/core/src/main/java/bisq/core/dao/governance/period/CycleService.java +++ b/core/src/main/java/bisq/core/dao/governance/period/CycleService.java @@ -82,10 +82,6 @@ public void onNewBlockHeight(int blockHeight) { .ifPresent(daoStateService.getCycles()::add); } - @Override - public void onParseBlockChainComplete() { - } - /////////////////////////////////////////////////////////////////////////////////////////// // API diff --git a/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java index 36ee3590118..9072d44b358 100644 --- a/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java @@ -120,19 +120,11 @@ public void updateList() { // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public void onNewBlockHeight(int blockHeight) { - } - @Override public void onParseTxsCompleteAfterBatchProcessing(Block block) { updateList(); } - @Override - public void onParseBlockChainComplete() { - } - /////////////////////////////////////////////////////////////////////////////////////////// // API diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java b/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java index 1fd63afe18d..8bd4dcdddcf 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java @@ -120,10 +120,6 @@ public void readPersisted() { // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public void onNewBlockHeight(int blockHeight) { - } - @Override public void onParseBlockChainComplete() { rePublishOnceWellConnected(); diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalListPresentation.java b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalListPresentation.java index 470b72cef78..2fd7a4d3dd7 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalListPresentation.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalListPresentation.java @@ -90,20 +90,11 @@ public ProposalListPresentation(ProposalService proposalService, // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - // At new cycle we don't get a list change but we want to update our predicates - @Override - public void onNewBlockHeight(int blockHeight) { - } - @Override public void onParseTxsCompleteAfterBatchProcessing(Block block) { updateLists(); } - @Override - public void onParseBlockChainComplete() { - } - /////////////////////////////////////////////////////////////////////////////////////////// // MyProposalListService.Listener diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java index 97807b5dd48..2dfb528e0ae 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java @@ -152,10 +152,6 @@ public void onAdded(PersistableNetworkPayload payload) { // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public void onNewBlockHeight(int blockHeight) { - } - @Override public void onParseTxsCompleteAfterBatchProcessing(Block block) { int heightForRepublishing = periodService.getFirstBlockOfPhase(daoStateService.getChainHeight(), DaoPhase.Phase.BREAK1); diff --git a/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealService.java b/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealService.java index fd16b898598..153519a4640 100644 --- a/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealService.java +++ b/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealService.java @@ -162,14 +162,6 @@ public void addVoteRevealTxPublishedListener(VoteRevealTxPublishedListener voteR // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public void onNewBlockHeight(int blockHeight) { - } - - @Override - public void onParseBlockChainComplete() { - } - @Override public void onParseTxsCompleteAfterBatchProcessing(Block block) { maybeRevealVotes(block.getHeight()); diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateSnapshotService.java b/core/src/main/java/bisq/core/dao/state/DaoStateSnapshotService.java index 73cb4e6645b..81c1623d192 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateSnapshotService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateSnapshotService.java @@ -70,10 +70,6 @@ public DaoStateSnapshotService(DaoStateService daoStateService, // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public void onNewBlockHeight(int blockHeight) { - } - // We listen to each ParseTxsComplete event even if the batch processing of all blocks at startup is not completed // as we need to write snapshots during that batch processing. @Override @@ -103,14 +99,6 @@ public void onParseTxsComplete(Block block) { } } - private boolean isValidHeight(int heightOfLastBlock) { - return heightOfLastBlock >= genesisTxInfo.getGenesisBlockHeight(); - } - - @Override - public void onParseBlockChainComplete() { - } - /////////////////////////////////////////////////////////////////////////////////////////// // API @@ -152,6 +140,10 @@ public void applySnapshot(boolean fromReorg) { // Private /////////////////////////////////////////////////////////////////////////////////////////// + private boolean isValidHeight(int heightOfLastBlock) { + return heightOfLastBlock >= genesisTxInfo.getGenesisBlockHeight(); + } + private void applyEmptySnapshot(DaoState persisted) { int genesisBlockHeight = genesisTxInfo.getGenesisBlockHeight(); persisted.setChainHeight(genesisBlockHeight); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java index 7c1734b71f0..a7d0881345a 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java @@ -273,10 +273,6 @@ public void onUpdateBalances(Coin confirmedBalance, // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public void onNewBlockHeight(int blockHeight) { - } - @Override public void onParseTxsCompleteAfterBatchProcessing(Block block) { updateViews(); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/wallet/dashboard/BsqDashboardView.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/dashboard/BsqDashboardView.java index de4f0c5e50e..f6027deda74 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/dashboard/BsqDashboardView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/dashboard/BsqDashboardView.java @@ -173,19 +173,11 @@ protected void deactivate() { // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public void onNewBlockHeight(int blockHeight) { - } - @Override public void onParseTxsCompleteAfterBatchProcessing(Block block) { updateWithBsqBlockChainData(); } - @Override - public void onParseBlockChainComplete() { - } - /////////////////////////////////////////////////////////////////////////////////////////// // Private From 9f005f44d60504acec26e373a6c4771ee29e18cc Mon Sep 17 00:00:00 2001 From: sqrrm Date: Fri, 8 Feb 2019 15:59:02 +0100 Subject: [PATCH 10/12] Refactor voteresult to make it easier to understand --- .../voteresult/VoteResultService.java | 237 ++++++++++-------- 1 file changed, 129 insertions(+), 108 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java index 02849b525bd..e68046d84bd 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java @@ -76,12 +76,15 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import lombok.Getter; import lombok.Value; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkArgument; @@ -244,101 +247,121 @@ private Set getDecryptedBallotsWithMeritsSet(int cha // We want all voteRevealTxOutputs which are in current cycle we are processing. return daoStateService.getVoteRevealOpReturnTxOutputs().stream() .filter(txOutput -> periodService.isTxInCorrectCycle(txOutput.getTxId(), chainHeight)) - .filter(txOutput -> { - // If we get a voteReveal tx which was published too late we ignore it. - String voteRevealTx = txOutput.getTxId(); - boolean txInPhase = periodService.isTxInPhase(voteRevealTx, DaoPhase.Phase.VOTE_REVEAL); - if (!txInPhase) - log.warn("We got a vote reveal tx with was not in the correct phase of that cycle. voteRevealTxId={}", voteRevealTx); - - return txInPhase; - }) - .map(txOutput -> { - String voteRevealTxId = txOutput.getTxId(); - Cycle currentCycle = periodService.getCurrentCycle(); - checkNotNull(currentCycle, "currentCycle must not be null"); - try { - byte[] opReturnData = txOutput.getOpReturnData(); - Optional optionalVoteRevealTx = daoStateService.getTx(voteRevealTxId); - checkArgument(optionalVoteRevealTx.isPresent(), "optionalVoteRevealTx must be present. voteRevealTxId=" + voteRevealTxId); - Tx voteRevealTx = optionalVoteRevealTx.get(); - - // TODO maybe verify version in opReturn - - // Here we use only blockchain tx data so far so we don't have risks with missing P2P network data. - // We work back from the voteRealTx to the blindVoteTx to caclulate the majority hash. From that we - // will derive the blind vote list we will use for result calculation and as it was based on - // blockchain data it will be consistent for all peers independent on their P2P network data state. - TxOutput blindVoteStakeOutput = VoteResultConsensus.getConnectedBlindVoteStakeOutput(voteRevealTx, daoStateService); - String blindVoteTxId = blindVoteStakeOutput.getTxId(); - - // If we get a blind vote tx which was published too late we ignore it. - if (!periodService.isTxInPhaseAndCycle(blindVoteTxId, DaoPhase.Phase.BLIND_VOTE, chainHeight)) { - log.warn("We got a blind vote tx with was not in the correct phase and/or cycle. " + - "We ignore that vote reveal and blind vote tx. voteRevealTx={}, blindVoteTxId={}", - voteRevealTx, blindVoteTxId); - return null; - } - - VoteResultConsensus.validateBlindVoteTx(blindVoteTxId, daoStateService, periodService, chainHeight); - - byte[] hashOfBlindVoteList = VoteResultConsensus.getHashOfBlindVoteList(opReturnData); - long blindVoteStake = blindVoteStakeOutput.getValue(); - - List blindVoteList = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); - Optional optionalBlindVote = blindVoteList.stream() - .filter(blindVote -> blindVote.getTxId().equals(blindVoteTxId)) - .findAny(); - if (optionalBlindVote.isPresent()) { - BlindVote blindVote = optionalBlindVote.get(); - SecretKey secretKey = VoteResultConsensus.getSecretKey(opReturnData); - try { - VoteWithProposalTxIdList voteWithProposalTxIdList = VoteResultConsensus.decryptVotes(blindVote.getEncryptedVotes(), secretKey); - MeritList meritList = MeritConsensus.decryptMeritList(blindVote.getEncryptedMeritList(), secretKey); - // We lookup for the proposals we have in our local list which match the txId from the - // voteWithProposalTxIdList and create a ballot list with the proposal and the vote from - // the voteWithProposalTxIdList - BallotList ballotList = createBallotList(voteWithProposalTxIdList); - log.info("Add entry to decryptedBallotsWithMeritsSet: blindVoteTxId={}, voteRevealTxId={}, blindVoteStake={}, ballotList={}", - blindVoteTxId, voteRevealTxId, blindVoteStake, ballotList); - return new DecryptedBallotsWithMerits(hashOfBlindVoteList, blindVoteTxId, voteRevealTxId, blindVoteStake, ballotList, meritList); - } catch (VoteResultException.DecryptionException decryptionException) { - // We don't consider such vote reveal txs valid for the majority hash - // calculation and don't add it to our result collection - log.error("Could not decrypt blind vote. This vote reveal and blind vote will be ignored. " + - "VoteRevealTxId={}. DecryptionException={}", voteRevealTxId, decryptionException.toString()); - voteResultExceptions.add(new VoteResultException(currentCycle, decryptionException)); - return null; - } - } - - log.warn("We have a blindVoteTx but we do not have the corresponding blindVote payload.\n" + - "That can happen if the blindVote item was not properly broadcast. " + - "We still add it to our result collection because it might be relevant for the majority " + - "hash by stake calculation. blindVoteTxId={}", blindVoteTxId); - - missingDataRequestService.sendRepublishRequest(); - - // We prefer to use an empty list here instead a null or optional value to avoid that - // client code need to handle nullable or optional values. - BallotList emptyBallotList = new BallotList(new ArrayList<>()); - MeritList emptyMeritList = new MeritList(new ArrayList<>()); - log.info("Add entry to decryptedBallotsWithMeritsSet: blindVoteTxId={}, voteRevealTxId={}, " + - "blindVoteStake={}, ballotList={}", - blindVoteTxId, voteRevealTxId, blindVoteStake, emptyBallotList); - return new DecryptedBallotsWithMerits(hashOfBlindVoteList, blindVoteTxId, voteRevealTxId, - blindVoteStake, emptyBallotList, emptyMeritList); - } catch (Throwable e) { - log.error("Could not create DecryptedBallotsWithMerits from voteRevealTxId {} because of " + - "exception: {}", voteRevealTxId, e.toString()); - voteResultExceptions.add(new VoteResultException(currentCycle, e)); - return null; - } - }) + .filter(this::isInVoteRevealPhase) + .map(txOutputToBallot(chainHeight)) .filter(Objects::nonNull) .collect(Collectors.toSet()); } + private boolean isInVoteRevealPhase(TxOutput txOutput) { + String voteRevealTx = txOutput.getTxId(); + boolean txInPhase = periodService.isTxInPhase(voteRevealTx, DaoPhase.Phase.VOTE_REVEAL); + if (!txInPhase) + log.warn("We got a vote reveal tx with was not in the correct phase of that cycle. voteRevealTxId={}", voteRevealTx); + + return txInPhase; + } + + @NotNull + private Function txOutputToBallot(int chainHeight) { + return voteRevealTxOutput -> { + String voteRevealTxId = voteRevealTxOutput.getTxId(); + Cycle currentCycle = periodService.getCurrentCycle(); + checkNotNull(currentCycle, "currentCycle must not be null"); + try { + byte[] voteRevealOpReturnData = voteRevealTxOutput.getOpReturnData(); + Optional optionalVoteRevealTx = daoStateService.getTx(voteRevealTxId); + checkArgument(optionalVoteRevealTx.isPresent(), "optionalVoteRevealTx must be present. voteRevealTxId=" + voteRevealTxId); + Tx voteRevealTx = optionalVoteRevealTx.get(); + + // TODO maybe verify version in opReturn + + // Here we use only blockchain tx data so far so we don't have risks with missing P2P network data. + // We work back from the voteRealTx to the blindVoteTx to caclulate the majority hash. From that we + // will derive the blind vote list we will use for result calculation and as it was based on + // blockchain data it will be consistent for all peers independent on their P2P network data state. + TxOutput blindVoteStakeOutput = VoteResultConsensus.getConnectedBlindVoteStakeOutput(voteRevealTx, daoStateService); + String blindVoteTxId = blindVoteStakeOutput.getTxId(); + + // If we get a blind vote tx which was published too late we ignore it. + if (!periodService.isTxInPhaseAndCycle(blindVoteTxId, DaoPhase.Phase.BLIND_VOTE, chainHeight)) { + log.warn("We got a blind vote tx with was not in the correct phase and/or cycle. " + + "We ignore that vote reveal and blind vote tx. voteRevealTx={}, blindVoteTxId={}", + voteRevealTx, blindVoteTxId); + return null; + } + + VoteResultConsensus.validateBlindVoteTx(blindVoteTxId, daoStateService, periodService, chainHeight); + + byte[] hashOfBlindVoteList = VoteResultConsensus.getHashOfBlindVoteList(voteRevealOpReturnData); + long blindVoteStake = blindVoteStakeOutput.getValue(); + + List blindVoteList = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); + Optional optionalBlindVote = blindVoteList.stream() + .filter(blindVote -> blindVote.getTxId().equals(blindVoteTxId)) + .findAny(); + if (optionalBlindVote.isPresent()) { + return getDecryptedBallotsWithMerits(voteRevealTxId, currentCycle, voteRevealOpReturnData, + blindVoteTxId, hashOfBlindVoteList, blindVoteStake, optionalBlindVote.get()); + } + return getEmptyDecryptedBallotsWithMerits(voteRevealTxId, blindVoteTxId, hashOfBlindVoteList, + blindVoteStake); + } catch (Throwable e) { + log.error("Could not create DecryptedBallotsWithMerits from voteRevealTxId {} because of " + + "exception: {}", voteRevealTxId, e.toString()); + voteResultExceptions.add(new VoteResultException(currentCycle, e)); + return null; + } + }; + } + + @NotNull + private DecryptedBallotsWithMerits getEmptyDecryptedBallotsWithMerits( + String voteRevealTxId, String blindVoteTxId, byte[] hashOfBlindVoteList, long blindVoteStake) { + log.warn("We have a blindVoteTx but we do not have the corresponding blindVote payload.\n" + + "That can happen if the blindVote item was not properly broadcast. " + + "We still add it to our result collection because it might be relevant for the majority " + + "hash by stake calculation. blindVoteTxId={}", blindVoteTxId); + + missingDataRequestService.sendRepublishRequest(); + + // We prefer to use an empty list here instead a null or optional value to avoid that + // client code need to handle nullable or optional values. + BallotList emptyBallotList = new BallotList(new ArrayList<>()); + MeritList emptyMeritList = new MeritList(new ArrayList<>()); + log.info("Add entry to decryptedBallotsWithMeritsSet: blindVoteTxId={}, voteRevealTxId={}, " + + "blindVoteStake={}, ballotList={}", + blindVoteTxId, voteRevealTxId, blindVoteStake, emptyBallotList); + return new DecryptedBallotsWithMerits(hashOfBlindVoteList, blindVoteTxId, voteRevealTxId, + blindVoteStake, emptyBallotList, emptyMeritList); + } + + @Nullable + private DecryptedBallotsWithMerits getDecryptedBallotsWithMerits( + String voteRevealTxId, Cycle currentCycle, byte[] voteRevealOpReturnData, String blindVoteTxId, + byte[] hashOfBlindVoteList, long blindVoteStake, BlindVote blindVote) + throws VoteResultException.MissingBallotException { + SecretKey secretKey = VoteResultConsensus.getSecretKey(voteRevealOpReturnData); + try { + VoteWithProposalTxIdList voteWithProposalTxIdList = VoteResultConsensus.decryptVotes(blindVote.getEncryptedVotes(), secretKey); + MeritList meritList = MeritConsensus.decryptMeritList(blindVote.getEncryptedMeritList(), secretKey); + // We lookup for the proposals we have in our local list which match the txId from the + // voteWithProposalTxIdList and create a ballot list with the proposal and the vote from + // the voteWithProposalTxIdList + BallotList ballotList = createBallotList(voteWithProposalTxIdList); + log.info("Add entry to decryptedBallotsWithMeritsSet: blindVoteTxId={}, voteRevealTxId={}, blindVoteStake={}, ballotList={}", + blindVoteTxId, voteRevealTxId, blindVoteStake, ballotList); + return new DecryptedBallotsWithMerits(hashOfBlindVoteList, blindVoteTxId, voteRevealTxId, blindVoteStake, ballotList, meritList); + } catch (VoteResultException.DecryptionException decryptionException) { + // We don't consider such vote reveal txs valid for the majority hash + // calculation and don't add it to our result collection + log.error("Could not decrypt blind vote. This vote reveal and blind vote will be ignored. " + + "VoteRevealTxId={}. DecryptionException={}", voteRevealTxId, decryptionException.toString()); + voteResultExceptions.add(new VoteResultException(currentCycle, decryptionException)); + return null; + } + } + private BallotList createBallotList(VoteWithProposalTxIdList voteWithProposalTxIdList) throws VoteResultException.MissingBallotException { // We convert the list to a map with proposalTxId as key and the vote as value @@ -390,7 +413,7 @@ private Map getStakeByHashOfBlindVoteListMap(Set decryptedBallotsWithMeritsSet.forEach(decryptedBallotsWithMerits -> { P2PDataStorage.ByteArray hash = new P2PDataStorage.ByteArray(decryptedBallotsWithMerits.getHashOfBlindVoteList()); map.putIfAbsent(hash, 0L); - // We must not user the merit(stake) as that is from the P2P network data and it is not guaranteed that we + // We must not use the merit(stake) as that is from the P2P network data and it is not guaranteed that we // have received it. We must rely only on blockchain data. The stake is from the vote reveal tx input. long aggregatedStake = map.get(hash); long stake = decryptedBallotsWithMerits.getStake(); @@ -403,12 +426,12 @@ private Map getStakeByHashOfBlindVoteListMap(Set return map; } - private byte[] calculateMajorityBlindVoteListHash(Map map) + private byte[] calculateMajorityBlindVoteListHash(Map stakes) throws VoteResultException.ValidationException, VoteResultException.ConsensusException { - List list = map.entrySet().stream() + List stakeList = stakes.entrySet().stream() .map(entry -> new HashWithStake(entry.getKey().bytes, entry.getValue())) .collect(Collectors.toList()); - return VoteResultConsensus.getMajorityHash(list); + return VoteResultConsensus.getMajorityHash(stakeList); } // Deal with eventually consistency of P2P network @@ -550,19 +573,17 @@ private long getRequiredVoteThreshold(int chainHeight, Proposal proposal) { private Map> getVoteWithStakeListByProposalMap(Set decryptedBallotsWithMeritsSet) { Map> voteWithStakeByProposalMap = new HashMap<>(); - decryptedBallotsWithMeritsSet.forEach(decryptedBallotsWithMerits -> { - decryptedBallotsWithMerits.getBallotList() - .forEach(ballot -> { - Proposal proposal = ballot.getProposal(); - voteWithStakeByProposalMap.putIfAbsent(proposal, new ArrayList<>()); - List voteWithStakeList = voteWithStakeByProposalMap.get(proposal); - long sumOfAllMerits = MeritConsensus.getMeritStake(decryptedBallotsWithMerits.getBlindVoteTxId(), - decryptedBallotsWithMerits.getMeritList(), daoStateService); - VoteWithStake voteWithStake = new VoteWithStake(ballot.getVote(), decryptedBallotsWithMerits.getStake(), sumOfAllMerits); - voteWithStakeList.add(voteWithStake); - log.info("Add entry to voteWithStakeListByProposalMap: proposalTxId={}, voteWithStake={} ", proposal.getTxId(), voteWithStake); - }); - }); + decryptedBallotsWithMeritsSet.forEach(decryptedBallotsWithMerits -> decryptedBallotsWithMerits.getBallotList() + .forEach(ballot -> { + Proposal proposal = ballot.getProposal(); + voteWithStakeByProposalMap.putIfAbsent(proposal, new ArrayList<>()); + List voteWithStakeList = voteWithStakeByProposalMap.get(proposal); + long sumOfAllMerits = MeritConsensus.getMeritStake(decryptedBallotsWithMerits.getBlindVoteTxId(), + decryptedBallotsWithMerits.getMeritList(), daoStateService); + VoteWithStake voteWithStake = new VoteWithStake(ballot.getVote(), decryptedBallotsWithMerits.getStake(), sumOfAllMerits); + voteWithStakeList.add(voteWithStake); + log.info("Add entry to voteWithStakeListByProposalMap: proposalTxId={}, voteWithStake={} ", proposal.getTxId(), voteWithStake); + })); return voteWithStakeByProposalMap; } From e8844ee50cc50e4a6052e1d81a856815a2c7e58a Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Sat, 9 Feb 2019 08:34:20 -0500 Subject: [PATCH 11/12] Rename DecryptedBallotsWithMerits to txOutputToDecryptedBallotsWithMerits --- .../core/dao/governance/voteresult/VoteResultService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java index e68046d84bd..c3b558f1c6c 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java @@ -248,7 +248,7 @@ private Set getDecryptedBallotsWithMeritsSet(int cha return daoStateService.getVoteRevealOpReturnTxOutputs().stream() .filter(txOutput -> periodService.isTxInCorrectCycle(txOutput.getTxId(), chainHeight)) .filter(this::isInVoteRevealPhase) - .map(txOutputToBallot(chainHeight)) + .map(txOutputToDecryptedBallotsWithMerits(chainHeight)) .filter(Objects::nonNull) .collect(Collectors.toSet()); } @@ -263,7 +263,7 @@ private boolean isInVoteRevealPhase(TxOutput txOutput) { } @NotNull - private Function txOutputToBallot(int chainHeight) { + private Function txOutputToDecryptedBallotsWithMerits(int chainHeight) { return voteRevealTxOutput -> { String voteRevealTxId = voteRevealTxOutput.getTxId(); Cycle currentCycle = periodService.getCurrentCycle(); From 01bd0983d63dc66ebe5ee66154fc9a1618430497 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Sat, 9 Feb 2019 08:38:16 -0500 Subject: [PATCH 12/12] Use property file for display string --- core/src/main/resources/i18n/displayStrings.properties | 3 +++ .../desktop/main/dao/governance/result/VoteResultView.java | 4 +--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index f38e3cbb9bb..1ea0352f78f 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1325,6 +1325,9 @@ dao.results.cycle.value.postFix.isDefaultValue=(default value) # suppress inspection "UnusedProperty" dao.results.cycle.value.postFix.hasChanged=(has been changed in voting) +dao.results.invalidVotes=We had invalid votes in that voting cycle. That can happen if a vote was \ + not distributed well in the P2P network.\n{0} + # suppress inspection "UnusedProperty" dao.phase.PHASE_UNDEFINED=Undefined # suppress inspection "UnusedProperty" diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java index 842330e7fa0..1b901ea2fb2 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java @@ -245,9 +245,7 @@ private void onResultsListItemSelected(CycleListItem item) { }); }); if (!sb.toString().isEmpty()) { - new Popup<>().information("We had invalid votes in that voting cycle. That can happen if a vote was " + - "not distributed well in the P2P network.\n" + - sb.toString()).show(); + new Popup<>().information(Res.get("dao.results.invalidVotes", sb.toString())).show(); } } }