From 71723b5e4b93f08352e9c68e82a62acffd57c793 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Sun, 3 Feb 2019 14:48:52 +0100 Subject: [PATCH 1/7] Add comment --- core/src/main/java/bisq/core/dao/node/BsqNode.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/main/java/bisq/core/dao/node/BsqNode.java b/core/src/main/java/bisq/core/dao/node/BsqNode.java index 160ec4438b6..985187cefa7 100644 --- a/core/src/main/java/bisq/core/dao/node/BsqNode.java +++ b/core/src/main/java/bisq/core/dao/node/BsqNode.java @@ -63,7 +63,11 @@ public abstract class BsqNode implements DaoSetupService { @Nullable protected Consumer warnMessageHandler; protected List pendingBlocks = new ArrayList<>(); + // The chain height of the latest Block we either get reported by Bitcoin Core or from the seed node + // This property should not be used in consensus code but only or retrieving blocks as it is not in sync with the + // parsing and the daoState. It also does not represent the latest blockHeight but the currently received + // (not parsed) block. @Getter protected int chainTipHeight; From 3005df42bf6c1925d81eef5b2e78561a49c13b39 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Sun, 3 Feb 2019 14:51:07 +0100 Subject: [PATCH 2/7] Fixes https://github.com/bisq-network/bisq/issues/2358 --- .../votereveal/VoteRevealService.java | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) 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 ca60e72b9d6..12a8e470b39 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 @@ -36,6 +36,7 @@ import bisq.core.dao.node.BsqNodeProvider; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; import bisq.core.dao.state.model.blockchain.TxOutput; import bisq.core.dao.state.model.governance.DaoPhase; @@ -137,7 +138,6 @@ public void addListeners() { @Override public void start() { - maybeRevealVotes(); } @@ -161,40 +161,43 @@ public void addVoteRevealTxPublishedListener(VoteRevealTxPublishedListener voteR @Override public void onNewBlockHeight(int blockHeight) { - // TODO check if we should use onParseTxsComplete for calling maybeCalculateVoteResult - - maybeRevealVotes(); } @Override public void onParseBlockChainComplete() { } + @Override + public void onParseTxsCompleteAfterBatchProcessing(Block block) { + maybeRevealVotes(block.getHeight()); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// // Creation of vote reveal tx is done without user activity! - // We create automatically the vote reveal tx when we enter the reveal phase of the current cycle when + // We create automatically the vote reveal tx when we are in the reveal phase of the current cycle when // the blind vote was created in case we have not done it already. // The voter need to be at least once online in the reveal phase when he has a blind vote created, - // otherwise his vote becomes invalid and his locked stake will get unlocked - private void maybeRevealVotes() { - // We must not use daoStateService.getChainHeight() because that gets updated with each parsed block but we - // only want to publish the vote reveal tx if our current real chain height is matching the cycle and phase and - // not at any intermediate height during parsing all blocks. The bsqNode knows the latest height from either - // Bitcoin Core or from the seed node. - int chainHeight = bsqNode.getChainTipHeight(); + // otherwise his vote becomes invalid. + // In case the user miss the vote reveal phase an (invalid) vote reveal tx will be created the next time the user is + // online. That tx only serves the purpose to unlock the stake from teh blind vote but it will be ignored for voting. + // A blind vote which did not get revealed might still be part of the majority hash calculation as we cannot know + // which blind votes might be revealed until the phase is over at the moment when we publish the vote reveal tx. + private void maybeRevealVotes(int chainHeight) { myVoteListService.getMyVoteList().stream() .filter(myVote -> myVote.getRevealTxId() == null) // we have not already revealed .forEach(myVote -> { boolean isInVoteRevealPhase = periodService.getPhaseForHeight(chainHeight) == DaoPhase.Phase.VOTE_REVEAL; boolean isBlindVoteTxInCorrectPhaseAndCycle = periodService.isTxInPhaseAndCycle(myVote.getTxId(), DaoPhase.Phase.BLIND_VOTE, chainHeight); if (isInVoteRevealPhase && isBlindVoteTxInCorrectPhaseAndCycle) { + log.info("We call revealVote at blockHeight {} for blindVoteTxId {}", chainHeight, myVote.getTxId()); // Standard case that we are in the correct phase and cycle and create the reveal tx. - revealVote(myVote); + revealVote(myVote, true); } else { + // We missed the vote reveal phase but publish a vote reveal tx to unlock he blind vote stake. boolean isAfterVoteRevealPhase = periodService.getPhaseForHeight(chainHeight).ordinal() > DaoPhase.Phase.VOTE_REVEAL.ordinal(); // We missed the reveal phase but we are in the correct cycle @@ -213,31 +216,29 @@ private void maybeRevealVotes() { // As this is an exceptional case we prefer to have a simple solution instead and just // publish the vote reveal tx but are aware that is is invalid. log.warn("We missed the vote reveal phase but publish now the tx to unlock our locked " + - "BSQ from the blind vote tx. BlindVoteTxId={}", myVote.getTxId()); + "BSQ from the blind vote tx. BlindVoteTxId={}, blockHeight={}", + myVote.getTxId(), chainHeight); // We handle the exception here inside the stream iteration as we have not get triggered from an // outside user intent anyway. We keep errors in a observable list so clients can observe that to // get notified if anything went wrong. - revealVote(myVote); + revealVote(myVote, false); } } }); } - private void revealVote(MyVote myVote) { + private void revealVote(MyVote myVote, boolean inBlindVotePhase) { try { // We collect all valid blind vote items we received via the p2p network. // It might be that different nodes have a different collection of those items. // To ensure we get a consensus of the data for later calculating the result we will put a hash of each - // voters blind vote collection into the opReturn data and check for a majority at issuance time. + // voters blind vote collection into the opReturn data and check for a majority at the vote result phase. // The voters "vote" with their stake at the reveal tx for their version of the blind vote collection. - // TODO make more clear by using param like here: - /* List blindVotes = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); - VoteRevealConsensus.getHashOfBlindVoteList(blindVotes);*/ - - byte[] hashOfBlindVoteList = getHashOfBlindVoteList(); - + // If we are not in the right phase we just add an empty hash (still need to have the hash as otherwise we + // would not recognize the tx as vote reveal tx) + byte[] hashOfBlindVoteList = inBlindVotePhase ? getHashOfBlindVoteList() : new byte[20]; log.info("Sha256Ripemd160 hash of hashOfBlindVoteList " + Utilities.bytesAsHexString(hashOfBlindVoteList)); byte[] opReturnData = VoteRevealConsensus.getOpReturnData(hashOfBlindVoteList, myVote.getSecretKey()); @@ -255,14 +256,15 @@ private void revealVote(MyVote myVote) { log.info("voteRevealTx={}", voteRevealTx); publishTx(voteRevealTx); - // TODO add comment... // We don't want to wait for a successful broadcast to avoid issues if the broadcast succeeds delayed or at // next startup but the tx was actually broadcasted. myVoteListService.applyRevealTxId(myVote, voteRevealTx.getHashAsString()); - // Just for additional resilience we republish our blind votes - List sortedBlindVoteListOfCycle = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); - rePublishBlindVotePayloadList(sortedBlindVoteListOfCycle); + if (inBlindVotePhase) { + // Just for additional resilience we republish our blind votes + List sortedBlindVoteListOfCycle = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); + rePublishBlindVotePayloadList(sortedBlindVoteListOfCycle); + } } catch (IOException | WalletException | TransactionVerificationException | InsufficientMoneyException e) { voteRevealExceptions.add(new VoteRevealException("Exception at calling revealVote.", From 39d94eec99fbacdb012238b7314ea00e757c6f66 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Sun, 3 Feb 2019 16:38:51 +0100 Subject: [PATCH 3/7] Show myVote as invalid if not included in result list --- .../voteresult/VoteResultConsensus.java | 4 +++- .../voteresult/VoteResultException.java | 6 +++--- .../resources/i18n/displayStrings.properties | 1 + .../java/bisq/desktop/main/MainViewModel.java | 2 -- .../main/dao/governance/ProposalDisplay.java | 8 ++++++++ .../dao/governance/result/VoteResultView.java | 17 +++++++++++++---- 6 files changed, 28 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultConsensus.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultConsensus.java index 18c1c082927..4a678eaf023 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultConsensus.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultConsensus.java @@ -84,7 +84,9 @@ public static byte[] getMajorityHash(List hashW long stakeOfAll = hashWithStakeList.stream().mapToLong(VoteResultService.HashWithStake::getStake).sum(); long stakeOfFirst = hashWithStakeList.get(0).getStake(); if ((double) stakeOfFirst / (double) stakeOfAll < 0.8) { - log.warn("hashWithStakeList " + hashWithStakeList); + log.warn("The winning data view has less then 80% of the " + + "total stake of all data views. We consider the voting cycle as invalid if the " + + "winning data view does not reach a super majority. hashWithStakeList={}", hashWithStakeList); throw new VoteResultException.ConsensusException("The winning data view has less then 80% of the " + "total stake of all data views. We consider the voting cycle as invalid if the " + "winning data view does not reach a super majority."); 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 754f8f420c7..f07ac358b63 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 @@ -28,17 +28,17 @@ public class VoteResultException extends Exception { @Getter - private final Cycle cycle; + private final int heightOfFirstBlock; VoteResultException(Cycle cycle, Throwable cause) { super(cause); - this.cycle = cycle; + this.heightOfFirstBlock = cycle.getHeightOfFirstBlock(); } @Override public String toString() { return "VoteResultException{" + - "\n cycle=" + cycle + + "\n heightOfFirstBlock=" + heightOfFirstBlock + "\n} " + super.toString(); } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 3f2bb4003dc..2f716a19e3e 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1594,6 +1594,7 @@ dao.proposal.display.myVote.accepted=Accepted dao.proposal.display.myVote.rejected=Rejected dao.proposal.display.myVote.ignored=Ignored dao.proposal.myVote.summary=Voted: {0}; Vote weight: {1} (earned: {2} + stake: {3}); +dao.proposal.myVote.invalid=Vote was invalid dao.proposal.voteResult.success=Accepted dao.proposal.voteResult.failed=Rejected diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index 0ebec9697e5..39a143b35cb 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -284,8 +284,6 @@ private void setupHandlers() { }); bisqSetup.setVoteResultExceptionHandler(voteResultException -> { log.warn(voteResultException.toString()); - - //new Popup<>().error(voteResultException.toString()).show(); }); bisqSetup.setChainFileLockedExceptionHandler(msg -> { diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java index 1709a324911..40221e4163c 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java @@ -460,6 +460,14 @@ public void applyBallotAndVoteWeight(@Nullable Ballot ballot, long merit, long s myVoteTextField.setManaged(show); } + public void setIsVoteIncludedInResult(boolean isVoteIncludedInResult) { + if (!isVoteIncludedInResult && myVoteTextField != null && !myVoteTextField.getText().isEmpty()) { + String text = myVoteTextField.getText(); + myVoteTextField.setText(Res.get("dao.proposal.myVote.invalid") + " - " + text); + myVoteTextField.getStyleClass().add("error-text"); + } + } + public void applyProposalPayload(Proposal proposal) { proposalTypeTextField.setText(proposal.getType().getDisplayName()); nameTextField.setText(proposal.getName()); 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 34bb675a340..8586f35237b 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 @@ -29,6 +29,7 @@ import bisq.desktop.util.GUIUtil; import bisq.desktop.util.Layout; +import bisq.core.btc.wallet.BsqWalletService; import bisq.core.dao.DaoFacade; import bisq.core.dao.governance.period.CycleService; import bisq.core.dao.governance.proposal.ProposalService; @@ -92,6 +93,7 @@ public class VoteResultView extends ActivatableView implements D private final CycleService cycleService; private final VoteResultService voteResultService; private final ProposalService proposalService; + private final BsqWalletService bsqWalletService; private final Preferences preferences; private final BsqFormatter bsqFormatter; @@ -126,6 +128,7 @@ public VoteResultView(DaoFacade daoFacade, CycleService cycleService, VoteResultService voteResultService, ProposalService proposalService, + BsqWalletService bsqWalletService, Preferences preferences, BsqFormatter bsqFormatter) { this.daoFacade = daoFacade; @@ -134,6 +137,7 @@ public VoteResultView(DaoFacade daoFacade, this.cycleService = cycleService; this.voteResultService = voteResultService; this.proposalService = proposalService; + this.bsqWalletService = bsqWalletService; this.preferences = preferences; this.bsqFormatter = bsqFormatter; } @@ -212,7 +216,7 @@ private void onResultsListItemSelected(CycleListItem item) { private void maybeShowVoteResultErrors(Cycle cycle) { List exceptions = voteResultService.getVoteResultExceptions().stream() - .filter(voteResultException -> cycle.equals(voteResultException.getCycle())) + .filter(voteResultException -> cycle.getHeightOfFirstBlock() == voteResultException.getHeightOfFirstBlock()) .collect(Collectors.toList()); if (!exceptions.isEmpty()) { TextArea textArea = FormBuilder.addTextArea(root, ++gridRow, ""); @@ -240,14 +244,18 @@ private void onSelectProposalResultListItem(ProposalListItem item) { if (selectedProposalListItem != null) { - EvaluatedProposal evaluatedProposal = selectedProposalListItem.getEvaluatedProposal(); Optional optionalBallot = daoFacade.getAllValidBallots().stream() .filter(ballot -> ballot.getTxId().equals(evaluatedProposal.getProposalTxId())) .findAny(); Ballot ballot = optionalBallot.orElse(null); - createProposalDisplay(evaluatedProposal, ballot); + ProposalDisplay proposalDisplay = createProposalDisplay(evaluatedProposal, ballot); createVotesTable(); + + // Check if my vote is included in result + boolean isVoteIncludedInResult = voteListItemList.stream() + .anyMatch(voteListItem -> bsqWalletService.getTransaction(voteListItem.getBlindVoteTxId()) != null); + proposalDisplay.setIsVoteIncludedInResult(isVoteIncludedInResult); } } @@ -372,7 +380,7 @@ private void createProposalsTable() { // Create views: proposalDisplay /////////////////////////////////////////////////////////////////////////////////////////// - private void createProposalDisplay(EvaluatedProposal evaluatedProposal, Ballot ballot) { + private ProposalDisplay createProposalDisplay(EvaluatedProposal evaluatedProposal, Ballot ballot) { Proposal proposal = evaluatedProposal.getProposal(); ProposalDisplay proposalDisplay = new ProposalDisplay(new GridPane(), bsqFormatter, daoFacade, null); @@ -395,6 +403,7 @@ private void createProposalDisplay(EvaluatedProposal evaluatedProposal, Ballot b long merit = meritAndStakeTuple.first; long stake = meritAndStakeTuple.second; proposalDisplay.applyBallotAndVoteWeight(ballot, merit, stake); + return proposalDisplay; } From 2fc010098ab70e1264e0c14f33df967c961a9637 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Sun, 3 Feb 2019 18:47:11 +0100 Subject: [PATCH 4/7] Update core/src/main/java/bisq/core/dao/node/BsqNode.java Co-Authored-By: ManfredKarrer --- core/src/main/java/bisq/core/dao/node/BsqNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/dao/node/BsqNode.java b/core/src/main/java/bisq/core/dao/node/BsqNode.java index 985187cefa7..4ac79809216 100644 --- a/core/src/main/java/bisq/core/dao/node/BsqNode.java +++ b/core/src/main/java/bisq/core/dao/node/BsqNode.java @@ -65,7 +65,7 @@ public abstract class BsqNode implements DaoSetupService { protected List pendingBlocks = new ArrayList<>(); // The chain height of the latest Block we either get reported by Bitcoin Core or from the seed node - // This property should not be used in consensus code but only or retrieving blocks as it is not in sync with the + // This property should not be used in consensus code but only for retrieving blocks as it is not in sync with the // parsing and the daoState. It also does not represent the latest blockHeight but the currently received // (not parsed) block. @Getter From 25948c966e2bd3b9c3156f2d662bc46aa5c7a61d Mon Sep 17 00:00:00 2001 From: sqrrm Date: Sun, 3 Feb 2019 18:47:36 +0100 Subject: [PATCH 5/7] Update core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealService.java Co-Authored-By: ManfredKarrer --- .../bisq/core/dao/governance/votereveal/VoteRevealService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 12a8e470b39..2a1fd2f6f76 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 @@ -183,7 +183,7 @@ public void onParseTxsCompleteAfterBatchProcessing(Block block) { // The voter need to be at least once online in the reveal phase when he has a blind vote created, // otherwise his vote becomes invalid. // In case the user miss the vote reveal phase an (invalid) vote reveal tx will be created the next time the user is - // online. That tx only serves the purpose to unlock the stake from teh blind vote but it will be ignored for voting. + // online. That tx only serves the purpose to unlock the stake from the blind vote but it will be ignored for voting. // A blind vote which did not get revealed might still be part of the majority hash calculation as we cannot know // which blind votes might be revealed until the phase is over at the moment when we publish the vote reveal tx. private void maybeRevealVotes(int chainHeight) { From 964407f8e0ac3841b171c06d253d0af57a06a230 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Sun, 3 Feb 2019 18:47:46 +0100 Subject: [PATCH 6/7] Update core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealService.java Co-Authored-By: ManfredKarrer --- .../bisq/core/dao/governance/votereveal/VoteRevealService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2a1fd2f6f76..47d9d5b906a 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 @@ -197,7 +197,7 @@ private void maybeRevealVotes(int chainHeight) { // Standard case that we are in the correct phase and cycle and create the reveal tx. revealVote(myVote, true); } else { - // We missed the vote reveal phase but publish a vote reveal tx to unlock he blind vote stake. + // We missed the vote reveal phase but publish a vote reveal tx to unlock the blind vote stake. boolean isAfterVoteRevealPhase = periodService.getPhaseForHeight(chainHeight).ordinal() > DaoPhase.Phase.VOTE_REVEAL.ordinal(); // We missed the reveal phase but we are in the correct cycle From 5a7d177e7cae937d1668db41a76e3c9adc4cebc6 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Sun, 3 Feb 2019 18:48:08 +0100 Subject: [PATCH 7/7] Update core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealService.java Co-Authored-By: ManfredKarrer --- .../bisq/core/dao/governance/votereveal/VoteRevealService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 47d9d5b906a..d7e34d4b96d 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 @@ -233,7 +233,7 @@ private void revealVote(MyVote myVote, boolean inBlindVotePhase) { // We collect all valid blind vote items we received via the p2p network. // It might be that different nodes have a different collection of those items. // To ensure we get a consensus of the data for later calculating the result we will put a hash of each - // voters blind vote collection into the opReturn data and check for a majority at the vote result phase. + // voter's blind vote collection into the opReturn data and check for a majority in the vote result phase. // The voters "vote" with their stake at the reveal tx for their version of the blind vote collection. // If we are not in the right phase we just add an empty hash (still need to have the hash as otherwise we