Skip to content

Commit

Permalink
Merge pull request #2360 from ManfredKarrer/dao-fix-issues-with-major…
Browse files Browse the repository at this point in the history
…ity-hash

Dao fix issues with majority hash
  • Loading branch information
ManfredKarrer authored Feb 3, 2019
2 parents 686588f + 5a7d177 commit 2615464
Show file tree
Hide file tree
Showing 8 changed files with 61 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ public static byte[] getMajorityHash(List<VoteResultService.HashWithStake> 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.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -137,7 +138,6 @@ public void addListeners() {

@Override
public void start() {
maybeRevealVotes();
}


Expand All @@ -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 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) {
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 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
Expand All @@ -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.
// 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.

// TODO make more clear by using param like here:
/* List<BlindVote> 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());

Expand All @@ -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<BlindVote> sortedBlindVoteListOfCycle = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService);
rePublishBlindVotePayloadList(sortedBlindVoteListOfCycle);
if (inBlindVotePhase) {
// Just for additional resilience we republish our blind votes
List<BlindVote> sortedBlindVoteListOfCycle = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService);
rePublishBlindVotePayloadList(sortedBlindVoteListOfCycle);
}
} catch (IOException | WalletException | TransactionVerificationException
| InsufficientMoneyException e) {
voteRevealExceptions.add(new VoteRevealException("Exception at calling revealVote.",
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/java/bisq/core/dao/node/BsqNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ public abstract class BsqNode implements DaoSetupService {
@Nullable
protected Consumer<String> warnMessageHandler;
protected List<RawBlock> 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 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
protected int chainTipHeight;

Expand Down
1 change: 1 addition & 0 deletions core/src/main/resources/i18n/displayStrings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions desktop/src/main/java/bisq/desktop/main/MainViewModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,6 @@ private void setupHandlers() {
});
bisqSetup.setVoteResultExceptionHandler(voteResultException -> {
log.warn(voteResultException.toString());

//new Popup<>().error(voteResultException.toString()).show();
});

bisqSetup.setChainFileLockedExceptionHandler(msg -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -92,6 +93,7 @@ public class VoteResultView extends ActivatableView<GridPane, Void> 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;

Expand Down Expand Up @@ -126,6 +128,7 @@ public VoteResultView(DaoFacade daoFacade,
CycleService cycleService,
VoteResultService voteResultService,
ProposalService proposalService,
BsqWalletService bsqWalletService,
Preferences preferences,
BsqFormatter bsqFormatter) {
this.daoFacade = daoFacade;
Expand All @@ -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;
}
Expand Down Expand Up @@ -212,7 +216,7 @@ private void onResultsListItemSelected(CycleListItem item) {

private void maybeShowVoteResultErrors(Cycle cycle) {
List<VoteResultException> 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, "");
Expand Down Expand Up @@ -240,14 +244,18 @@ private void onSelectProposalResultListItem(ProposalListItem item) {


if (selectedProposalListItem != null) {

EvaluatedProposal evaluatedProposal = selectedProposalListItem.getEvaluatedProposal();
Optional<Ballot> 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);
}
}

Expand Down Expand Up @@ -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);

Expand All @@ -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;
}


Expand Down

0 comments on commit 2615464

Please sign in to comment.