From 4fd8490df1658fed7e669d88a3c2ec7c91de8773 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Thu, 3 Jan 2019 16:44:39 +0100 Subject: [PATCH] Handle late vote reveal txs If the phase and cycle for the vote reveal tx was missed we still publish it but it is considered invalid. We do not throw an exception but filter such txs away from the vote result evaluation. We cannot use the strategy to unlock the BSQ from the vote tx in such a case because the blind vote tx is already in the past and is not parsed again (snapshot). Alternatively we could have used a different tx type for the unlock purpose but we prefer to keep such an exceptional case simple. --- .../voteresult/VoteResultService.java | 6 +++ .../votereveal/VoteRevealConsensus.java | 25 +++++++++ .../votereveal/VoteRevealService.java | 53 ++++++++++--------- .../bisq/core/dao/node/parser/TxParser.java | 2 +- 4 files changed, 60 insertions(+), 26 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 dbd4ea462f..046272834e 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 @@ -254,6 +254,12 @@ public class VoteResultService implements DaoStateListener, DaoSetupService { } Tx voteRevealTx = optionalVoteRevealTx.get(); + // If we get a voteReveal tx which was published too late we ignore it. + if (!VoteRevealConsensus.isVoteRevealTxInCorrectPhaseAndCycle(periodService, voteRevealTx.getId(), chainHeight)) { + log.warn("We got a vote reveal tx with was not in the correct phase and/or cycle. voteRevealTxId={}", voteRevealTx.getId()); + return null; + } + try { // TODO maybe verify version in opReturn byte[] hashOfBlindVoteList = VoteResultConsensus.getHashOfBlindVoteList(opReturnData); diff --git a/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealConsensus.java b/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealConsensus.java index 19993205fe..1aab110725 100644 --- a/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealConsensus.java +++ b/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealConsensus.java @@ -18,7 +18,9 @@ package bisq.core.dao.governance.votereveal; import bisq.core.dao.governance.blindvote.BlindVote; +import bisq.core.dao.governance.period.PeriodService; import bisq.core.dao.state.model.blockchain.OpReturnType; +import bisq.core.dao.state.model.governance.DaoPhase; import bisq.common.app.Version; import bisq.common.crypto.Hash; @@ -65,4 +67,27 @@ public class VoteRevealConsensus { throw e; } } + + public static boolean isBlindVoteTxInCorrectPhaseAndCycle(PeriodService periodService, String blindVoteTxId, int chainHeight) { + boolean isVoteRevealPhase = periodService.getPhaseForHeight(chainHeight) == DaoPhase.Phase.VOTE_REVEAL; + boolean isBlindVoteTxInCorrectCycle = periodService.isTxInCorrectCycle(blindVoteTxId, chainHeight); + return isVoteRevealPhase && isBlindVoteTxInCorrectCycle; + } + + public static boolean missedPhaseOrCycle(PeriodService periodService, String blindVoteTxId, int chainHeight) { + boolean isBlindVoteTxInCorrectCycle = periodService.isTxInCorrectCycle(blindVoteTxId, chainHeight); + boolean isAfterVoteRevealPhase = periodService.getPhaseForHeight(chainHeight).ordinal() > DaoPhase.Phase.VOTE_REVEAL.ordinal(); + + // We missed the reveal phase but we are in the correct cycle + boolean missedPhaseSameCycle = isAfterVoteRevealPhase && isBlindVoteTxInCorrectCycle; + + // If we missed the cycle we don't care about the phase anymore. + boolean isBlindVoteTxInPastCycle = periodService.isTxInPastCycle(blindVoteTxId, chainHeight); + return missedPhaseSameCycle || isBlindVoteTxInPastCycle; + } + + public static boolean isVoteRevealTxInCorrectPhaseAndCycle(PeriodService periodService, String voteRevealTxId, int chainHeight) { + return periodService.isTxInPhase(voteRevealTxId, DaoPhase.Phase.VOTE_REVEAL) && + periodService.isTxInCorrectCycle(voteRevealTxId, chainHeight); + } } 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 586e325392..1952cea3f9 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 @@ -38,7 +38,6 @@ 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; import bisq.network.p2p.P2PService; @@ -181,16 +180,27 @@ public class VoteRevealService implements DaoStateListener, DaoSetupService { // 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(); + myVoteListService.getMyVoteList().stream() + .filter(myVote -> myVote.getRevealTxId() == null) // we have not already revealed + .forEach(myVote -> { + boolean isCorrectPhaseAndCycle = VoteRevealConsensus.isBlindVoteTxInCorrectPhaseAndCycle(periodService, myVote.getTxId(), chainHeight); + boolean missedPhaseOrCycle = VoteRevealConsensus.missedPhaseOrCycle(periodService, myVote.getTxId(), chainHeight); + if (isCorrectPhaseAndCycle || missedPhaseOrCycle) { + // We have a blind vote and reveal it as we are in the correct phase and cycle. - if (periodService.getPhaseForHeight(chainHeight) == DaoPhase.Phase.VOTE_REVEAL) { - myVoteListService.getMyVoteList().stream() - .filter(myVote -> myVote.getRevealTxId() == null) // we have not already revealed TODO - .filter(myVote -> periodService.isTxInCorrectCycle(myVote.getTxId(), chainHeight)) - .forEach(myVote -> { // 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. try { + if (missedPhaseOrCycle) { + // We cannot handle that case in the parser directly to avoid that tx and unlock the + // BSQ because the blind vote tx is already in the snapshot and does not get parsed + // again. It would require a reset of the snapshot and parse all blocks again. + // 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()); + } revealVote(myVote, chainHeight); } catch (IOException | WalletException | TransactionVerificationException | InsufficientMoneyException e) { @@ -199,8 +209,8 @@ public class VoteRevealService implements DaoStateListener, DaoSetupService { } catch (VoteRevealException e) { voteRevealExceptions.add(e); } - }); - } + } + }); } private void revealVote(MyVote myVote, int chainHeight) throws IOException, WalletException, @@ -230,25 +240,18 @@ public class VoteRevealService implements DaoStateListener, DaoSetupService { // TxOutput has to be in the current cycle. Phase is checked in the parser anyway. // TODO is phase check needed and done in parser still? - if (periodService.isTxInCorrectCycle(stakeTxOutput.getTxId(), chainHeight)) { - Transaction voteRevealTx = getVoteRevealTx(stakeTxOutput, opReturnData); - log.info("voteRevealTx={}", voteRevealTx); - publishTx(voteRevealTx); + Transaction voteRevealTx = getVoteRevealTx(stakeTxOutput, opReturnData); + 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()); + // 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); - } else { - final String msg = "Tx of stake out put is not in our cycle. That must not happen."; - log.error("{}. chainHeight={}, blindVoteTxId()={}", msg, chainHeight, myVote.getTxId()); - voteRevealExceptions.add(new VoteRevealException(msg, - stakeTxOutput.getTxId())); - } + // Just for additional resilience we republish our blind votes + List sortedBlindVoteListOfCycle = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); + rePublishBlindVotePayloadList(sortedBlindVoteListOfCycle); } private void publishTx(Transaction voteRevealTx) { diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxParser.java index 8badd3d168..3a3b4fbbf0 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/TxParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/TxParser.java @@ -309,7 +309,7 @@ public class TxParser { private boolean isPhaseValid(int blockHeight, DaoPhase.Phase phase) { boolean isInPhase = periodService.isInPhase(blockHeight, phase); if (!isInPhase) { - log.warn("Not in {} phase. blockHeight={}", phase, blockHeight); + log.warn("Tx is not in required phase ({}). blockHeight={}", phase, blockHeight); } return isInPhase; }