Add IRREGULAR txType for txs which are not rule conform but not burnt

We don't want to burn BSQ in cases like that the tx was published too
late, which is a valid case if the tx does not make it in the next block.
We set such txs as IRREGULAR and allow spending of the BSQ, but there
function in the governance is invalidated.

We also add a check if the sum of all UTXO is the same as the sum of the
genesis + sum of issuance txs - burned fees.
This commit is contained in:
Manfred Karrer 2019-03-26 00:37:55 -05:00
parent c20964f47a
commit ce1da644c2
No known key found for this signature in database
GPG key ID: 401250966A6B2C46
11 changed files with 114 additions and 33 deletions

View file

@ -1440,6 +1440,7 @@ enum TxType {
UNLOCK = 13;
ASSET_LISTING_FEE = 14;
PROOF_OF_BURN = 15;
IRREGULAR = 16;
}
message TxInput {

View file

@ -56,6 +56,7 @@ import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.DaoStateStorageService;
import bisq.core.dao.state.model.blockchain.BaseTx;
import bisq.core.dao.state.model.blockchain.BaseTxOutput;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.core.dao.state.model.blockchain.Tx;
import bisq.core.dao.state.model.blockchain.TxOutput;
@ -543,6 +544,11 @@ public class DaoFacade implements DaoSetupService {
return daoStateService.getTotalAmountOfConfiscatedTxOutputs();
}
public long getTotalAmountOfUnspentTxOutputs() {
// Does not consider confiscated outputs (they stay as utxo)
return daoStateService.getUnspentTxOutputMap().values().stream().mapToLong(BaseTxOutput::getValue).sum();
}
public Optional<Integer> getLockTime(String txId) {
return daoStateService.getLockTime(txId);
}

View file

@ -109,10 +109,15 @@ public class VoteResultConsensus {
Optional<TxOutput> optionalBlindVoteStakeOutput = daoStateService.getConnectedTxOutput(stakeTxInput);
checkArgument(optionalBlindVoteStakeOutput.isPresent(), "blindVoteStakeOutput must be present");
TxOutput blindVoteStakeOutput = optionalBlindVoteStakeOutput.get();
checkArgument(blindVoteStakeOutput.getTxOutputType() == TxOutputType.BLIND_VOTE_LOCK_STAKE_OUTPUT,
"blindVoteStakeOutput must be of type BLIND_VOTE_LOCK_STAKE_OUTPUT but is " +
blindVoteStakeOutput.getTxOutputType() + ". VoteRevealTx=" + voteRevealTx);
if (blindVoteStakeOutput.getTxOutputType() != TxOutputType.BLIND_VOTE_LOCK_STAKE_OUTPUT) {
String message = "blindVoteStakeOutput must be of type BLIND_VOTE_LOCK_STAKE_OUTPUT but is " +
blindVoteStakeOutput.getTxOutputType();
log.warn(message + ". VoteRevealTx=" + voteRevealTx);
throw new VoteResultException.ValidationException(message + ". VoteRevealTxId=" + voteRevealTx.getId());
}
return blindVoteStakeOutput;
} catch (VoteResultException.ValidationException t) {
throw t;
} catch (Throwable t) {
throw new VoteResultException.ValidationException(t);
}

View file

@ -26,7 +26,9 @@ import bisq.core.dao.monitoring.network.messages.NewDaoStateHashMessage;
import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.GenesisTxInfo;
import bisq.core.dao.state.model.blockchain.BaseTxOutput;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.core.dao.state.model.governance.IssuanceType;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.Connection;
@ -134,6 +136,22 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
daoStateNetworkService.requestHashesFromAllConnectedSeedNodes(fromHeight);
}
@Override
public void onDaoStateChanged(Block block) {
long genesisTotalSupply = daoStateService.getGenesisTotalSupply().value;
long totalBurntFee = daoStateService.getTotalBurntFee();
long compensationIssuance = daoStateService.getTotalIssuedAmount(IssuanceType.COMPENSATION);
long reimbursementIssuance = daoStateService.getTotalIssuedAmount(IssuanceType.REIMBURSEMENT);
long totalConfiscatedAmount = daoStateService.getTotalAmountOfConfiscatedTxOutputs();
// confiscated funds are still in the utxo set
long sumUtxo = daoStateService.getUnspentTxOutputMap().values().stream().mapToLong(BaseTxOutput::getValue).sum();
long sumBsq = genesisTotalSupply + compensationIssuance + reimbursementIssuance - totalBurntFee;
if (sumBsq != sumUtxo) {
throw new RuntimeException("There is a mismatch between the UTXO set and the DAO state. Please contact the Bisq devlopers.");
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// StateNetworkService.Listener
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -36,7 +36,8 @@ enum JsonTxType {
LOCKUP("Lockup"),
UNLOCK("Unlock"),
ASSET_LISTING_FEE("Asset listing fee"),
PROOF_OF_BURN("Proof of burn");
PROOF_OF_BURN("Proof of burn"),
IRREGULAR("Irregular");
@Getter
private String displayString;

View file

@ -150,9 +150,13 @@ public class TxParser {
//****************************************************************************************
applyTxTypeAndTxOutputType(blockHeight, tempTx, remainingInputValue);
TxType txType = evaluateTxType(tempTx, optionalOpReturnType, hasBurntBsq, unLockInputValid);
tempTx.setTxType(txType);
TxType txType;
if (tempTx.getTxType() != TxType.IRREGULAR && tempTx.getTxType() != TxType.INVALID) {
txType = evaluateTxType(tempTx, optionalOpReturnType, hasBurntBsq, unLockInputValid);
tempTx.setTxType(txType);
} else {
txType = tempTx.getTxType();
}
if (isTxInvalid(tempTx, bsqOutputFound, hasBurntBond)) {
tempTx.setTxType(TxType.INVALID);
@ -162,6 +166,9 @@ public class TxParser {
log.warn("We have destroyed BSQ because of an invalid tx. Burned BSQ={}. tx={}",
burntBsq / 100D, tempTx);
}
} else if (txType == TxType.IRREGULAR) {
log.warn("We have an irregular tx {}", tempTx);
txOutputParser.commitUTXOCandidates();
} else {
txOutputParser.commitUTXOCandidates();
}
@ -231,8 +238,8 @@ public class TxParser {
private void processProposal(int blockHeight, TempTx tempTx, long bsqFee) {
boolean isFeeAndPhaseValid = isFeeAndPhaseValid(tempTx.getId(), blockHeight, bsqFee, DaoPhase.Phase.PROPOSAL, Param.PROPOSAL_FEE);
if (!isFeeAndPhaseValid) {
// TODO don't burn in such cases
tempTx.setTxType(TxType.INVALID);
// We tolerate such an incorrect tx and do not burn the BSQ
tempTx.setTxType(TxType.IRREGULAR);
}
}
@ -249,17 +256,17 @@ public class TxParser {
"As the BSQ fee is set it must be either a buggy tx or an manually crafted invalid tx.");
// Even though the request part if invalid the BSQ transfer and change output should still be valid
// as long as the BSQ change <= BSQ inputs.
// TODO do we want to burn in such a case?
tempTx.setTxType(TxType.INVALID);
// We tolerate such an incorrect tx and do not burn the BSQ
tempTx.setTxType(TxType.IRREGULAR);
}
} else {
// This could be a valid compensation request that failed to be included in a block during the
// correct phase due to no fault of the user. Better not burn the change as long as the BSQ inputs
// cover the value of the outputs.
// TODO don't burn in such cases
tempTx.setTxType(TxType.INVALID);
// We tolerate such an incorrect tx and do not burn the BSQ
tempTx.setTxType(TxType.IRREGULAR);
// TODO don't burn in such cases
// Make sure the optionalIssuanceCandidate is set the BTC
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.
@ -269,11 +276,11 @@ public class TxParser {
private void processBlindVote(int blockHeight, TempTx tempTx, long bsqFee) {
boolean isFeeAndPhaseValid = isFeeAndPhaseValid(tempTx.getId(), blockHeight, bsqFee, DaoPhase.Phase.BLIND_VOTE, Param.BLIND_VOTE_FEE);
if (!isFeeAndPhaseValid) {
// TODO don't burn in such cases
tempTx.setTxType(TxType.INVALID);
// We tolerate such an incorrect tx and do not burn the BSQ
tempTx.setTxType(TxType.IRREGULAR);
// TODO don't burn in such cases
txOutputParser.getOptionalBlindVoteLockStakeOutput().ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT));
// Set the stake output from BLIND_VOTE_LOCK_STAKE_OUTPUT to BSQ
txOutputParser.getOptionalBlindVoteLockStakeOutput().ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BSQ_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.
}
@ -374,7 +381,6 @@ public class TxParser {
if (!isUnLockInputValid)
return TxType.INVALID;
// UNLOCK tx has no fee, no OpReturn
return TxType.UNLOCK;
}
@ -393,9 +399,9 @@ public class TxParser {
boolean hasCorrectNumOutputs = tempTx.getTempTxOutputs().size() >= 3;
if (!hasCorrectNumOutputs) {
log.warn("Compensation/reimbursement request tx need to have at least 3 outputs");
// This is not an issuance request but it should still not burn the BSQ change
// TODO do we want to burn in such a case?
return TxType.INVALID;
// This is not a valid issuance tx
// We tolerate such an incorrect tx and do not burn the BSQ
return TxType.IRREGULAR;
}
TempTxOutput issuanceTxOutput = tempTx.getTempTxOutputs().get(1);
@ -403,9 +409,9 @@ public class TxParser {
if (!hasIssuanceOutput) {
log.warn("Compensation/reimbursement request txOutput type of output at index 1 need to be ISSUANCE_CANDIDATE_OUTPUT. " +
"TxOutputType={}", issuanceTxOutput.getTxOutputType());
// This is not an issuance request but it should still not burn the BSQ change
// TODO do we want to burn in such a case?
return TxType.INVALID;
// This is not a valid issuance tx
// We tolerate such an incorrect tx and do not burn the BSQ
return TxType.IRREGULAR;
}
return opReturnType == OpReturnType.COMPENSATION_REQUEST ?
@ -423,8 +429,8 @@ public class TxParser {
return TxType.PROOF_OF_BURN;
default:
log.warn("We got a BSQ tx with an unknown OP_RETURN. tx={}, opReturnType={}", tempTx, opReturnType);
// TODO do we want to burn in such a case?
return TxType.INVALID;
// We tolerate such an incorrect tx and do not burn the BSQ
return TxType.IRREGULAR;
}
}
}

View file

@ -45,7 +45,8 @@ public enum TxType implements ImmutableDaoStateModel {
LOCKUP(true, false),
UNLOCK(true, false),
ASSET_LISTING_FEE(true, true),
PROOF_OF_BURN(true, true);
PROOF_OF_BURN(true, true),
IRREGULAR(true, true); // the params are here irrelevant as we can have any tx which violated the rules set to irregular
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -115,6 +115,8 @@ public class UnconfirmedBsqChangeOutputListService implements PersistedDataHost
case PROOF_OF_BURN:
changeOutputIndex = 0;
break;
case IRREGULAR:
return;
default:
return;
}

View file

@ -1766,6 +1766,7 @@ dao.wallet.dashboard.totalLockedUpAmount=Locked up in bonds
dao.wallet.dashboard.totalUnlockingAmount=Unlocking BSQ from bonds
dao.wallet.dashboard.totalUnlockedAmount=Unlocked BSQ from bonds
dao.wallet.dashboard.totalConfiscatedAmount=Confiscated BSQ from bonds
dao.wallet.dashboard.totalInvalidatedAmount=Invalidated BSQ (burned due invalid tx)
dao.wallet.dashboard.allTx=No. of all BSQ transactions
dao.wallet.dashboard.utxo=No. of all unspent transaction outputs
dao.wallet.dashboard.compensationIssuanceTx=No. of all compensation request issuance transactions
@ -1841,6 +1842,8 @@ dao.tx.type.enum.UNLOCK=Unlock bond
dao.tx.type.enum.ASSET_LISTING_FEE=Asset listing fee
# suppress inspection "UnusedProperty"
dao.tx.type.enum.PROOF_OF_BURN=Proof of burn
# suppress inspection "UnusedProperty"
dao.tx.type.enum.IRREGULAR=Irregular
dao.tx.issuanceFromCompReq=Compensation request/issuance
dao.tx.issuanceFromCompReq.tooltip=Compensation request which led to an issuance of new BSQ.\n\

View file

@ -65,7 +65,7 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
private int gridRow = 0;
private TextField genesisIssueAmountTextField, compRequestIssueAmountTextField, reimbursementAmountTextField, availableAmountTextField,
burntAmountTextField, totalLockedUpAmountTextField, totalUnlockingAmountTextField,
totalUnlockedAmountTextField, totalConfiscatedAmountTextField, allTxTextField, burntTxTextField,
totalUnlockedAmountTextField, totalConfiscatedAmountTextField, totalInvalidatedAmountTextField, allTxTextField, burntTxTextField,
utxoTextField, compensationIssuanceTxTextField,
reimbursementIssuanceTxTextField, priceTextField, marketCapTextField;
private ChangeListener<Number> priceChangeListener;
@ -108,7 +108,7 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
totalUnlockingAmountTextField = FormBuilder.addTopLabelReadOnlyTextField(root, ++gridRow, columnIndex, Res.get("dao.wallet.dashboard.totalUnlockingAmount")).second;
totalUnlockedAmountTextField = FormBuilder.addTopLabelReadOnlyTextField(root, ++gridRow, columnIndex, Res.get("dao.wallet.dashboard.totalUnlockedAmount")).second;
totalConfiscatedAmountTextField = FormBuilder.addTopLabelReadOnlyTextField(root, ++gridRow, columnIndex, Res.get("dao.wallet.dashboard.totalConfiscatedAmount")).second;
gridRow++;
totalInvalidatedAmountTextField = FormBuilder.addTopLabelReadOnlyTextField(root, ++gridRow, columnIndex, Res.get("dao.wallet.dashboard.totalInvalidatedAmount")).second;
startRow = gridRow;
addTitledGroupBg(root, ++gridRow, 2, Res.get("dao.wallet.dashboard.market"), Layout.GROUP_DISTANCE);
@ -196,11 +196,17 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
Coin totalUnlockingAmount = Coin.valueOf(daoFacade.getTotalAmountOfUnLockingTxOutputs());
Coin totalUnlockedAmount = Coin.valueOf(daoFacade.getTotalAmountOfUnLockedTxOutputs());
Coin totalConfiscatedAmount = Coin.valueOf(daoFacade.getTotalAmountOfConfiscatedTxOutputs());
availableAmount = issuedAmountFromGenesis
Coin totalUtxoAmount = Coin.valueOf(daoFacade.getTotalAmountOfUnspentTxOutputs());
daoFacade.getTotalAmountOfConfiscatedTxOutputs();
availableAmount = totalUtxoAmount.subtract(totalConfiscatedAmount);
Coin totalAmount = issuedAmountFromGenesis
.add(issuedAmountFromCompRequests)
.add(issuedAmountFromReimbursementRequests)
.subtract(burntFee)
.subtract(totalConfiscatedAmount);
Coin totalInvalidatedAmount = totalAmount.subtract(availableAmount);
availableAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(availableAmount));
burntAmountTextField.setText("-" + bsqFormatter.formatAmountWithGroupSeparatorAndCode(burntFee));
@ -208,6 +214,7 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
totalUnlockingAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalUnlockingAmount));
totalUnlockedAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalUnlockedAmount));
totalConfiscatedAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalConfiscatedAmount));
totalInvalidatedAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalInvalidatedAmount));
allTxTextField.setText(String.valueOf(daoFacade.getTxs().size()));
utxoTextField.setText(String.valueOf(daoFacade.getUnspentTxOutputs().size()));
compensationIssuanceTxTextField.setText(String.valueOf(daoFacade.getNumIssuanceTransactions(IssuanceType.COMPENSATION)));

View file

@ -305,6 +305,33 @@ public class BsqTxView extends ActivatableView<GridPane, Void> implements BsqBal
observableList.setAll(items);
}
private boolean isValidType(TxType txType) {
switch (txType) {
case UNDEFINED:
case UNDEFINED_TX_TYPE:
case UNVERIFIED:
case INVALID:
return false;
case GENESIS:
case TRANSFER_BSQ:
case PAY_TRADE_FEE:
case PROPOSAL:
case COMPENSATION_REQUEST:
case REIMBURSEMENT_REQUEST:
case BLIND_VOTE:
case VOTE_REVEAL:
case LOCKUP:
case UNLOCK:
case ASSET_LISTING_FEE:
case PROOF_OF_BURN:
return true;
case IRREGULAR:
return false;
default:
return false;
}
}
private void addDateColumn() {
TableColumn<BsqTxListItem, BsqTxListItem> column = new AutoTooltipTableColumn<>(Res.get("shared.dateTime"));
column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue()));
@ -397,7 +424,7 @@ public class BsqTxView extends ActivatableView<GridPane, Void> implements BsqBal
final TxType txType = item.getTxType();
String labelString = Res.get("dao.tx.type.enum." + txType.name());
Label label;
if (item.getConfirmations() > 0 && txType.ordinal() > TxType.INVALID.ordinal()) {
if (item.getConfirmations() > 0 && isValidType(txType)) {
if (txType == TxType.COMPENSATION_REQUEST &&
daoFacade.isIssuanceTx(item.getTxId(), IssuanceType.COMPENSATION)) {
if (field != null)
@ -470,7 +497,7 @@ public class BsqTxView extends ActivatableView<GridPane, Void> implements BsqBal
super.updateItem(item, empty);
if (item != null && !empty) {
TxType txType = item.getTxType();
setText(item.getConfirmations() > 0 && txType.ordinal() > TxType.INVALID.ordinal() ?
setText(item.getConfirmations() > 0 && isValidType(txType) ?
bsqFormatter.formatCoin(item.getAmount()) :
Res.get("shared.na"));
} else
@ -621,6 +648,10 @@ public class BsqTxView extends ActivatableView<GridPane, Void> implements BsqBal
awesomeIcon = AwesomeIcon.FILE_TEXT;
style = "dao-tx-type-proposal-fee-icon";
break;
case IRREGULAR:
awesomeIcon = AwesomeIcon.WARNING_SIGN;
style = "dao-tx-type-invalid-icon";
break;
default:
awesomeIcon = AwesomeIcon.QUESTION_SIGN;
style = "dao-tx-type-unverified-icon";