Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dao fix issues with majority hash #2360

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
ManfredKarrer marked this conversation as resolved.
Show resolved Hide resolved

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