Merge pull request #1718 from sqrrm/refactor-dao-parser

[WIP] Refactor dao parser
This commit is contained in:
Manfred Karrer 2018-10-03 17:10:13 -05:00 committed by GitHub
commit 126d279e6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 141 additions and 93 deletions

View File

@ -24,9 +24,12 @@ import bisq.core.dao.state.blockchain.TempTx;
import bisq.core.dao.state.blockchain.TempTxOutput;
import bisq.core.dao.state.blockchain.TxOutput;
import bisq.core.dao.state.blockchain.TxOutputType;
import bisq.core.dao.state.blockchain.TxType;
import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import lombok.Getter;
@ -47,6 +50,8 @@ public class TxOutputParser {
@Getter
@Setter
private long availableInputValue = 0;
private int lockTime;
private List<TempTxOutput> bsqOutputs = new ArrayList<>();
@Setter
private int unlockBlockHeight;
@Setter
@ -58,8 +63,6 @@ public class TxOutputParser {
@Getter
private boolean bsqOutputFound;
@Getter
private Optional<OpReturnType> optionalOpReturnTypeCandidate = Optional.empty();
@Getter
private Optional<OpReturnType> optionalVerifiedOpReturnType = Optional.empty();
@Getter
private Optional<TempTxOutput> optionalIssuanceCandidate = Optional.empty();
@ -74,15 +77,41 @@ public class TxOutputParser {
this.bsqStateService = bsqStateService;
}
public void commitTxOutputs() {
bsqOutputs.forEach(bsqOutput -> {
bsqStateService.addUnspentTxOutput(TxOutput.fromTempOutput(bsqOutput));
});
}
/**
* This sets all outputs to BTC_OUTPUT and doesn't add any txOutputs to the bsqStateService
*/
public void commitTxOutputsForInvalidTx() {
bsqOutputs.forEach(bsqOutput -> {
bsqOutput.setTxOutputType(TxOutputType.BTC_OUTPUT);
});
}
public void processGenesisTxOutput(TempTx genesisTx) {
for (int i = 0; i < genesisTx.getTempTxOutputs().size(); ++i) {
TempTxOutput tempTxOutput = genesisTx.getTempTxOutputs().get(i);
bsqStateService.addUnspentTxOutput(TxOutput.fromTempOutput(tempTxOutput));
bsqOutputs.add(tempTxOutput);
}
commitTxOutputs();
}
void processOpReturnCandidate(TempTxOutput txOutput) {
optionalOpReturnTypeCandidate = OpReturnParser.getOptionalOpReturnTypeCandidate(txOutput);
boolean isOpReturnOutput(TempTxOutput txOutput) {
return txOutput.getOpReturnData() != null;
}
void processOpReturnOutput(boolean isLastOutput, TempTxOutput tempTxOutput) {
byte[] opReturnData = tempTxOutput.getOpReturnData();
if (opReturnData != null) {
handleOpReturnOutput(tempTxOutput, isLastOutput);
} else {
log.error("This should be an opReturn output");
}
}
/**
@ -97,7 +126,10 @@ public class TxOutputParser {
byte[] opReturnData = tempTxOutput.getOpReturnData();
if (opReturnData == null) {
long txOutputValue = tempTxOutput.getValue();
if (isUnlockBondTx(tempTxOutput.getValue(), index)) {
if (tempTx.getTxType() == TxType.INVALID) {
// Set all non opReturn outputs to BTC_OUTPUT if the tx is invalid
tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT);
} else if (isUnlockBondTx(tempTxOutput.getValue(), index)) {
// We need to handle UNLOCK transactions separately as they don't follow the pattern on spending BSQ
// The LOCKUP BSQ is burnt unless the output exactly matches the input, that would cause the
// output to not be BSQ output at all
@ -108,14 +140,10 @@ public class TxOutputParser {
handleBtcOutput(tempTxOutput, index);
}
} else {
handleOpReturnOutput(tempTxOutput, isLastOutput);
log.error("This should not be an opReturn output");
}
}
boolean isOpReturnOutput(TempTxOutput txOutput) {
return txOutput.getOpReturnData() != null;
}
/**
* Whether a transaction is a valid unlock bond transaction or not.
*
@ -136,7 +164,7 @@ public class TxOutputParser {
availableInputValue -= optionalSpentLockupTxOutput.get().getValue();
txOutput.setTxOutputType(TxOutputType.UNLOCK);
bsqStateService.addUnspentTxOutput(TxOutput.fromTempOutput(txOutput));
bsqOutputs.add(txOutput);
//TODO move up to TxParser
// We should add unlockBlockHeight to TempTxOutput and remove unlockBlockHeight from tempTx
@ -152,8 +180,8 @@ public class TxOutputParser {
boolean isFirstOutput = index == 0;
OpReturnType opReturnTypeCandidate = null;
if (optionalOpReturnTypeCandidate.isPresent())
opReturnTypeCandidate = optionalOpReturnTypeCandidate.get();
if (optionalVerifiedOpReturnType.isPresent())
opReturnTypeCandidate = optionalVerifiedOpReturnType.get();
TxOutputType bsqOutput;
if (isFirstOutput && opReturnTypeCandidate == OpReturnType.BLIND_VOTE) {
@ -164,12 +192,13 @@ public class TxOutputParser {
optionalVoteRevealUnlockStakeOutput = Optional.of(txOutput);
} else if (isFirstOutput && opReturnTypeCandidate == OpReturnType.LOCKUP) {
bsqOutput = TxOutputType.LOCKUP;
txOutput.setLockTime(lockTime);
optionalLockupOutput = Optional.of(txOutput);
} else {
bsqOutput = TxOutputType.BSQ_OUTPUT;
}
txOutput.setTxOutputType(bsqOutput);
bsqStateService.addUnspentTxOutput(TxOutput.fromTempOutput(txOutput));
bsqOutputs.add(txOutput);
bsqOutputFound = true;
}
@ -179,8 +208,8 @@ public class TxOutputParser {
// candidate to the parsingModel and we don't apply the TxOutputType as we do that later as the OpReturn check.
if (availableInputValue > 0 &&
index == 1 &&
optionalOpReturnTypeCandidate.isPresent() &&
optionalOpReturnTypeCandidate.get() == OpReturnType.COMPENSATION_REQUEST) {
optionalVerifiedOpReturnType.isPresent() &&
optionalVerifiedOpReturnType.get() == OpReturnType.COMPENSATION_REQUEST) {
// We don't set the txOutputType yet as we have not fully validated the tx but put the candidate
// into our local optionalIssuanceCandidate.
@ -199,8 +228,7 @@ public class TxOutputParser {
.ifPresent(verifiedOpReturnType -> {
byte[] opReturnData = tempTxOutput.getOpReturnData();
checkNotNull(opReturnData, "opReturnData must not be null");
int lockTime = BondingConsensus.getLockTime(opReturnData);
tempTxOutput.setLockTime(lockTime);
lockTime = BondingConsensus.getLockTime(opReturnData);
});
}

View File

@ -66,7 +66,7 @@ public class TxParser {
}
// Apply state changes to tx, inputs and outputs
// return true if any input contained BSQ
// return Tx if any input contained BSQ
// Any tx with BSQ input is a BSQ tx (except genesis tx but that is not handled in
// that class).
// There might be txs without any valid BSQ txOutput but we still keep track of it,
@ -101,6 +101,15 @@ public class TxParser {
long accumulatedInputValue = txInputParser.getAccumulatedInputValue();
txOutputParser.setAvailableInputValue(accumulatedInputValue);
// We don't allow multiple opReturn outputs (they are non-standard but to be safe lets check it)
long numOpReturnOutputs = tempTx.getTempTxOutputs().stream().filter(txOutputParser::isOpReturnOutput).count();
if (numOpReturnOutputs > 1) {
tempTx.setTxType(TxType.INVALID);
String msg = "Invalid tx. We have multiple opReturn outputs. tx=" + tempTx;
log.warn(msg);
}
txOutputParser.setUnlockBlockHeight(txInputParser.getUnlockBlockHeight());
txOutputParser.setOptionalSpentLockupTxOutput(txInputParser.getOptionalSpentLockupTxOutput());
txOutputParser.setTempTx(tempTx); //TODO remove
@ -115,11 +124,16 @@ public class TxParser {
// We keep the temporary opReturn type in the parsingModel object.
checkArgument(!outputs.isEmpty(), "outputs must not be empty");
int lastIndex = outputs.size() - 1;
txOutputParser.processOpReturnCandidate(outputs.get(lastIndex));
int lastNonOpReturnIndex = lastIndex;
if (txOutputParser.isOpReturnOutput(outputs.get(lastIndex))) {
// TODO(SQ): perhaps the check for isLastOutput could be skipped
txOutputParser.processOpReturnOutput(true, outputs.get(lastIndex));
lastNonOpReturnIndex -= 1;
}
// We use order of output index. An output is a BSQ utxo as long there is enough input value
// We iterate all outputs including the opReturn to do a full validation including the BSQ fee
for (int index = 0; index < outputs.size(); index++) {
for (int index = 0; index <= lastNonOpReturnIndex; index++) {
boolean isLastOutput = index == lastIndex;
txOutputParser.processTxOutput(isLastOutput,
outputs.get(index),
@ -131,9 +145,12 @@ public class TxParser {
processOpReturnType(blockHeight, tempTx);
// We don't allow multiple opReturn outputs (they are non-standard but to be safe lets check it)
long numOpReturnOutputs = tempTx.getTempTxOutputs().stream().filter(txOutputParser::isOpReturnOutput).count();
if (numOpReturnOutputs <= 1) {
// TODO(SQ): Should the destroyed BSQ from an INVALID tx be considered as burnt fee?
if (remainingInputValue > 0)
tempTx.setBurntFee(remainingInputValue);
// Process the type of transaction if not already determined to be INVALID
if (tempTx.getTxType() != TxType.INVALID) {
boolean isAnyTxOutputTypeUndefined = tempTx.getTempTxOutputs().stream()
.anyMatch(txOutput -> TxOutputType.UNDEFINED == txOutput.getTxOutputType());
if (!isAnyTxOutputTypeUndefined) {
@ -141,23 +158,22 @@ public class TxParser {
// use RawTx?
TxType txType = TxParser.getBisqTxType(
tempTx,
txOutputParser.getOptionalOpReturnTypeCandidate().isPresent(),
txOutputParser.getOptionalVerifiedOpReturnType().isPresent(),
remainingInputValue,
getOptionalOpReturnType()
);
tempTx.setTxType(txType);
if (remainingInputValue > 0)
tempTx.setBurntFee(remainingInputValue);
} else {
tempTx.setTxType(TxType.INVALID);
String msg = "We have undefined txOutput types which must not happen. tx=" + tempTx;
DevEnv.logErrorAndThrowIfDevMode(msg);
}
}
if (tempTx.getTxType() != TxType.INVALID){
txOutputParser.commitTxOutputs();
} else {
// We don't consider a tx with multiple OpReturn outputs valid.
tempTx.setTxType(TxType.INVALID);
String msg = "Invalid tx. We have multiple opReturn outputs. tx=" + tempTx;
log.warn(msg);
txOutputParser.commitTxOutputsForInvalidTx();
}
}
@ -186,55 +202,16 @@ public class TxParser {
boolean isFeeAndPhaseValid;
switch (verifiedOpReturnType) {
case PROPOSAL:
isFeeAndPhaseValid = isFeeAndPhaseValid(blockHeight, bsqFee, DaoPhase.Phase.PROPOSAL, Param.PROPOSAL_FEE);
if (!isFeeAndPhaseValid) {
tempTx.setTxType(TxType.INVALID);
}
processProposal(blockHeight, tempTx, bsqFee);
break;
case COMPENSATION_REQUEST:
isFeeAndPhaseValid = isFeeAndPhaseValid(blockHeight, bsqFee, DaoPhase.Phase.PROPOSAL, Param.PROPOSAL_FEE);
Optional<TempTxOutput> optionalIssuanceCandidate = txOutputParser.getOptionalIssuanceCandidate();
if (isFeeAndPhaseValid) {
if (optionalIssuanceCandidate.isPresent()) {
// Now after we have validated the opReturn data we will apply the TxOutputType
optionalIssuanceCandidate.get().setTxOutputType(TxOutputType.ISSUANCE_CANDIDATE_OUTPUT);
} else {
log.warn("It can be that we have a opReturn which is correct from its structure but the whole tx " +
"in not valid as the issuanceCandidate in not there. " +
"As the BSQ fee is set it must be either a buggy tx or an manually crafted invalid tx.");
}
} else {
tempTx.setTxType(TxType.INVALID);
optionalIssuanceCandidate.ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT));
// Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a
// valid BSQ tx.
}
processCompensationRequest(blockHeight, tempTx, bsqFee);
break;
case BLIND_VOTE:
isFeeAndPhaseValid = isFeeAndPhaseValid(blockHeight, bsqFee, DaoPhase.Phase.BLIND_VOTE, Param.BLIND_VOTE_FEE);
if (!isFeeAndPhaseValid) {
tempTx.setTxType(TxType.INVALID);
Optional<TempTxOutput> optionalBlindVoteLockStakeOutput = txOutputParser.getOptionalBlindVoteLockStakeOutput();
optionalBlindVoteLockStakeOutput.ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT));
// Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a
// valid BSQ tx.
}
processBlindVote(blockHeight, tempTx, bsqFee);
break;
case VOTE_REVEAL:
boolean isPhaseValid = isPhaseValid(blockHeight, DaoPhase.Phase.VOTE_REVEAL);
boolean isVoteRevealInputInValid = txInputParser.getVoteRevealInputState() != TxInputParser.VoteRevealInputState.VALID;
if (!isPhaseValid) {
tempTx.setTxType(TxType.INVALID);
}
if (!isPhaseValid || isVoteRevealInputInValid) {
Optional<TempTxOutput> optionalVoteRevealUnlockStakeOutput = txOutputParser
.getOptionalVoteRevealUnlockStakeOutput();
optionalVoteRevealUnlockStakeOutput.ifPresent(
tempTxOutput -> tempTxOutput
.setTxOutputType(TxOutputType.BTC_OUTPUT));
// Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a
// valid BSQ tx.
}
processVoteReveal(blockHeight, tempTx);
break;
case LOCKUP:
// do nothing
@ -261,6 +238,64 @@ public class TxParser {
}
}
private void processVoteReveal(int blockHeight, TempTx tempTx) {
boolean isPhaseValid = isPhaseValid(blockHeight, DaoPhase.Phase.VOTE_REVEAL);
boolean isVoteRevealInputInValid = txInputParser.getVoteRevealInputState() != TxInputParser.VoteRevealInputState.VALID;
if (!isPhaseValid) {
tempTx.setTxType(TxType.INVALID);
}
if (!isPhaseValid || isVoteRevealInputInValid) {
Optional<TempTxOutput> optionalVoteRevealUnlockStakeOutput = txOutputParser
.getOptionalVoteRevealUnlockStakeOutput();
optionalVoteRevealUnlockStakeOutput.ifPresent(
tempTxOutput -> tempTxOutput
.setTxOutputType(TxOutputType.BTC_OUTPUT));
// Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a
// valid BSQ tx.
}
}
private void processBlindVote(int blockHeight, TempTx tempTx, long bsqFee) {
boolean isFeeAndPhaseValid;
isFeeAndPhaseValid = isFeeAndPhaseValid(blockHeight, bsqFee, DaoPhase.Phase.BLIND_VOTE, Param.BLIND_VOTE_FEE);
if (!isFeeAndPhaseValid) {
tempTx.setTxType(TxType.INVALID);
Optional<TempTxOutput> optionalBlindVoteLockStakeOutput = txOutputParser.getOptionalBlindVoteLockStakeOutput();
optionalBlindVoteLockStakeOutput.ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT));
// Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a
// valid BSQ tx.
}
}
private void processCompensationRequest(int blockHeight, TempTx tempTx, long bsqFee) {
boolean isFeeAndPhaseValid =
isFeeAndPhaseValid(blockHeight, bsqFee, DaoPhase.Phase.PROPOSAL, Param.PROPOSAL_FEE);
Optional<TempTxOutput> optionalIssuanceCandidate = txOutputParser.getOptionalIssuanceCandidate();
if (isFeeAndPhaseValid) {
if (optionalIssuanceCandidate.isPresent()) {
// Now after we have validated the opReturn data we will apply the TxOutputType
optionalIssuanceCandidate.get().setTxOutputType(TxOutputType.ISSUANCE_CANDIDATE_OUTPUT);
} else {
log.warn("It can be that we have a opReturn which is correct from its structure but the whole tx " +
"in not valid as the issuanceCandidate in not there. " +
"As the BSQ fee is set it must be either a buggy tx or an manually crafted invalid tx.");
}
} else {
tempTx.setTxType(TxType.INVALID);
optionalIssuanceCandidate.ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT));
// Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a
// valid BSQ tx.
}
}
private void processProposal(int blockHeight, TempTx tempTx, long bsqFee) {
boolean isFeeAndPhaseValid =
isFeeAndPhaseValid(blockHeight, bsqFee, DaoPhase.Phase.PROPOSAL, Param.PROPOSAL_FEE);
if (!isFeeAndPhaseValid) {
tempTx.setTxType(TxType.INVALID);
}
}
/**
* Whether the BSQ fee and phase is valid for a transaction.
*
@ -369,20 +404,7 @@ public class TxParser {
*/
private Optional<OpReturnType> getOptionalOpReturnType() {
if (txOutputParser.isBsqOutputFound()) {
// We want to be sure that the initial assumption of the opReturn type was matching the result after full
// validation.
Optional<OpReturnType> optionalOpReturnTypeCandidate = txOutputParser.getOptionalOpReturnTypeCandidate();
Optional<OpReturnType> optionalVerifiedOpReturnType = txOutputParser.getOptionalVerifiedOpReturnType();
if (optionalOpReturnTypeCandidate.isPresent() && optionalVerifiedOpReturnType.isPresent()) {
if (optionalOpReturnTypeCandidate.get() == optionalVerifiedOpReturnType.get()) {
return optionalVerifiedOpReturnType;
} else {
String msg = "We got a different opReturn type after validation as we expected initially. " +
"optionalOpReturnTypeCandidate=" + optionalOpReturnTypeCandidate +
", optionalVerifiedOpReturnType=" + txOutputParser.getOptionalVerifiedOpReturnType();
log.warn(msg);
}
}
return txOutputParser.getOptionalVerifiedOpReturnType();
} else {
String msg = "We got a tx without any valid BSQ output but with burned BSQ. " +
"Burned fee=" + remainingInputValue / 100D + " BSQ.";

View File

@ -138,14 +138,12 @@ public final class Tx extends BaseTx implements PersistablePayload {
/**
* OpReturn output might contain the lockTime in case of a LockTx. It has to be the last output.
* We store technically the lockTime there as is is stored in the OpReturn data but conceptually we want to provide
* it from the transaction.
* The locktime is stored in the LOCKUP txOutput, which is the first txOutput.
*
* @return
*/
public int getLockTime() {
return getLastTxOutput().getLockTime();
return txOutputs.get(0).getLockTime();
}
@Override