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.
This commit is contained in:
Manfred Karrer 2019-01-03 16:44:39 +01:00
parent 00d0aa51e3
commit 4fd8490df1
No known key found for this signature in database
GPG Key ID: 401250966A6B2C46
4 changed files with 60 additions and 26 deletions

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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<BlindVote> 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<BlindVote> sortedBlindVoteListOfCycle = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService);
rePublishBlindVotePayloadList(sortedBlindVoteListOfCycle);
}
private void publishTx(Transaction voteRevealTx) {

View File

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