diff --git a/common/src/main/proto/pb.proto b/common/src/main/proto/pb.proto index f2f9b57a52..2b9cf09813 100644 --- a/common/src/main/proto/pb.proto +++ b/common/src/main/proto/pb.proto @@ -1440,6 +1440,7 @@ enum TxType { UNLOCK = 13; ASSET_LISTING_FEE = 14; PROOF_OF_BURN = 15; + IRREGULAR = 16; } message TxInput { diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index e3413b6ea9..a5681e4795 100644 --- a/core/src/main/java/bisq/core/dao/DaoFacade.java +++ b/core/src/main/java/bisq/core/dao/DaoFacade.java @@ -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 getLockTime(String txId) { return daoStateService.getLockTime(txId); } diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultConsensus.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultConsensus.java index 15c50a1b09..1f0301db8b 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultConsensus.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultConsensus.java @@ -109,10 +109,15 @@ public class VoteResultConsensus { Optional 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); } diff --git a/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java b/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java index abf8240c88..f99d7ea355 100644 --- a/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java +++ b/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java @@ -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 /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/dao/node/explorer/JsonTxType.java b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxType.java index f0e972c6ea..d14e17f8a5 100644 --- a/core/src/main/java/bisq/core/dao/node/explorer/JsonTxType.java +++ b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxType.java @@ -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; 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 5decbbbe4e..e6d3bc69b4 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 @@ -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; } } } diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/TxType.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxType.java index 8e5339db7f..390362e778 100644 --- a/core/src/main/java/bisq/core/dao/state/model/blockchain/TxType.java +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxType.java @@ -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 /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedBsqChangeOutputListService.java b/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedBsqChangeOutputListService.java index e213b60586..03d6d8961b 100644 --- a/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedBsqChangeOutputListService.java +++ b/core/src/main/java/bisq/core/dao/state/unconfirmed/UnconfirmedBsqChangeOutputListService.java @@ -115,6 +115,8 @@ public class UnconfirmedBsqChangeOutputListService implements PersistedDataHost case PROOF_OF_BURN: changeOutputIndex = 0; break; + case IRREGULAR: + return; default: return; } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 440fcdbf78..c3193e57f4 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -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\ diff --git a/desktop/src/main/java/bisq/desktop/main/dao/wallet/dashboard/BsqDashboardView.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/dashboard/BsqDashboardView.java index 5458c02bc4..491d9c7412 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/dashboard/BsqDashboardView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/dashboard/BsqDashboardView.java @@ -65,7 +65,7 @@ public class BsqDashboardView extends ActivatableView 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 priceChangeListener; @@ -108,7 +108,7 @@ public class BsqDashboardView extends ActivatableView 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 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 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))); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java index aa9e20e41e..1c15b629ea 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java @@ -305,6 +305,33 @@ public class BsqTxView extends ActivatableView 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 column = new AutoTooltipTableColumn<>(Res.get("shared.dateTime")); column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); @@ -397,7 +424,7 @@ public class BsqTxView extends ActivatableView 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 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 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";