diff --git a/common/src/main/java/bisq/common/util/ExtraDataMapValidator.java b/common/src/main/java/bisq/common/util/ExtraDataMapValidator.java new file mode 100644 index 00000000000..df158557165 --- /dev/null +++ b/common/src/main/java/bisq/common/util/ExtraDataMapValidator.java @@ -0,0 +1,83 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.common.util; + +import java.util.HashMap; +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Validator for extraDataMap fields used in network payloads. + * Ensures that we don't get the network attacked by huge data inserted there. + */ +@Slf4j +public class ExtraDataMapValidator { + // ExtraDataMap is only used for exceptional cases to not break backward compatibility. + // We don't expect many entries there. + public final static int MAX_SIZE = 10; + public final static int MAX_KEY_LENGTH = 100; + public final static int MAX_VALUE_LENGTH = 100000; // 100 kb + + public static Map getValidatedExtraDataMap(@Nullable Map extraDataMap) { + return getValidatedExtraDataMap(extraDataMap, MAX_SIZE, MAX_KEY_LENGTH, MAX_VALUE_LENGTH); + } + + public static Map getValidatedExtraDataMap(@Nullable Map extraDataMap, int maxSize, + int maxKeyLength, int maxValueLength) { + if (extraDataMap == null) + return null; + + try { + checkArgument(extraDataMap.entrySet().size() <= maxSize, + "Size of map must not exceed " + maxSize); + extraDataMap.forEach((key, value) -> { + checkArgument(key.length() <= maxKeyLength, + "Length of key must not exceed " + maxKeyLength); + checkArgument(value.length() <= maxValueLength, + "Length of value must not exceed " + maxValueLength); + }); + return extraDataMap; + } catch (Throwable t) { + return new HashMap<>(); + } + } + + public static void validate(@Nullable Map extraDataMap) { + validate(extraDataMap, MAX_SIZE, MAX_KEY_LENGTH, MAX_VALUE_LENGTH); + } + + public static void validate(@Nullable Map extraDataMap, int maxSize, int maxKeyLength, + int maxValueLength) { + if (extraDataMap == null) + return; + + checkArgument(extraDataMap.entrySet().size() <= maxSize, + "Size of map must not exceed " + maxSize); + extraDataMap.forEach((key, value) -> { + checkArgument(key.length() <= maxKeyLength, + "Length of key must not exceed " + maxKeyLength); + checkArgument(value.length() <= maxValueLength, + "Length of value must not exceed " + maxValueLength); + }); + } +} diff --git a/common/src/main/proto/pb.proto b/common/src/main/proto/pb.proto index f2f9b57a528..65afa4df9f8 100644 --- a/common/src/main/proto/pb.proto +++ b/common/src/main/proto/pb.proto @@ -1420,7 +1420,7 @@ message Tx { // Because of the way how PB implements inheritence we need to use the super class as type repeated BaseTxOutput tx_outputs = 1; TxType txType = 2; - int64 burnt_fee = 3; + int64 burnt_bsq = 3; } enum TxType { @@ -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/alert/Alert.java b/core/src/main/java/bisq/core/alert/Alert.java index cb2569f3901..4081b231f26 100644 --- a/core/src/main/java/bisq/core/alert/Alert.java +++ b/core/src/main/java/bisq/core/alert/Alert.java @@ -22,6 +22,7 @@ import bisq.common.app.Version; import bisq.common.crypto.Sig; +import bisq.common.util.ExtraDataMapValidator; import io.bisq.generated.protobuffer.PB; @@ -91,7 +92,7 @@ public Alert(String message, this.version = version; this.ownerPubKeyBytes = ownerPubKeyBytes; this.signatureAsBase64 = signatureAsBase64; - this.extraDataMap = extraDataMap; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyBytes); } diff --git a/core/src/main/java/bisq/core/app/BisqExecutable.java b/core/src/main/java/bisq/core/app/BisqExecutable.java index dcd87d1c38d..fcc3142e5c8 100644 --- a/core/src/main/java/bisq/core/app/BisqExecutable.java +++ b/core/src/main/java/bisq/core/app/BisqExecutable.java @@ -494,7 +494,7 @@ protected void customizeOptionParsing(OptionParser parser) { format("Base currency network (default: %s)", BisqEnvironment.getDefaultBaseCurrencyNetwork().name())) .withRequiredArg() .ofType(String.class) - .describedAs(format("%s|%s|%s|%s", BTC_MAINNET, BTC_TESTNET, BTC_REGTEST, BTC_DAO_TESTNET, BTC_DAO_BETANET)); + .describedAs(format("%s|%s|%s|%s", BTC_MAINNET, BTC_TESTNET, BTC_REGTEST, BTC_DAO_TESTNET, BTC_DAO_BETANET, BTC_DAO_REGTEST)); parser.accepts(BtcOptionKeys.REG_TEST_HOST, format("Bitcoin regtest host when using BTC_REGTEST network (default: %s)", RegTestHost.DEFAULT_HOST)) diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index b8d355a0455..07668b9b241 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -418,7 +418,7 @@ private void maybeShowTac() { private void checkIfLocalHostNodeIsRunning() { // For DAO testnet we ignore local btc node - if (BisqEnvironment.getBaseCurrencyNetwork().isDaoTestNet()) { + if (BisqEnvironment.getBaseCurrencyNetwork().isDaoRegTest() || BisqEnvironment.getBaseCurrencyNetwork().isDaoTestNet()) { step3(); } else { Thread checkIfLocalHostNodeIsRunningThread = new Thread(() -> { diff --git a/core/src/main/java/bisq/core/arbitration/Arbitrator.java b/core/src/main/java/bisq/core/arbitration/Arbitrator.java index a9b6d738c45..ffc6a8f4670 100644 --- a/core/src/main/java/bisq/core/arbitration/Arbitrator.java +++ b/core/src/main/java/bisq/core/arbitration/Arbitrator.java @@ -23,6 +23,7 @@ import bisq.common.crypto.PubKeyRing; import bisq.common.proto.ProtoUtil; +import bisq.common.util.ExtraDataMapValidator; import bisq.common.util.Utilities; import io.bisq.generated.protobuffer.PB; @@ -91,7 +92,7 @@ public Arbitrator(NodeAddress nodeAddress, this.registrationSignature = registrationSignature; this.emailAddress = emailAddress; this.info = info; - this.extraDataMap = extraDataMap; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/arbitration/Mediator.java b/core/src/main/java/bisq/core/arbitration/Mediator.java index b5d4e3ca643..0673b5465c0 100644 --- a/core/src/main/java/bisq/core/arbitration/Mediator.java +++ b/core/src/main/java/bisq/core/arbitration/Mediator.java @@ -23,6 +23,7 @@ import bisq.common.crypto.PubKeyRing; import bisq.common.proto.ProtoUtil; +import bisq.common.util.ExtraDataMapValidator; import io.bisq.generated.protobuffer.PB; @@ -84,7 +85,7 @@ public Mediator(NodeAddress nodeAddress, this.registrationSignature = registrationSignature; this.emailAddress = emailAddress; this.info = info; - this.extraDataMap = extraDataMap; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); } diff --git a/core/src/main/java/bisq/core/btc/BaseCurrencyNetwork.java b/core/src/main/java/bisq/core/btc/BaseCurrencyNetwork.java index 12d50336223..c4468563299 100644 --- a/core/src/main/java/bisq/core/btc/BaseCurrencyNetwork.java +++ b/core/src/main/java/bisq/core/btc/BaseCurrencyNetwork.java @@ -28,8 +28,9 @@ public enum BaseCurrencyNetwork { BTC_MAINNET(MainNetParams.get(), "BTC", "MAINNET", "Bitcoin"), BTC_TESTNET(TestNet3Params.get(), "BTC", "TESTNET", "Bitcoin"), BTC_REGTEST(RegTestParams.get(), "BTC", "REGTEST", "Bitcoin"), - BTC_DAO_TESTNET(RegTestParams.get(), "BTC", "REGTEST", "Bitcoin"), // server side regtest - BTC_DAO_BETANET(MainNetParams.get(), "BTC", "MAINNET", "Bitcoin"); // mainnet test genesis + BTC_DAO_TESTNET(RegTestParams.get(), "BTC", "REGTEST", "Bitcoin"), // server side regtest until v0.9.5 + BTC_DAO_BETANET(MainNetParams.get(), "BTC", "MAINNET", "Bitcoin"), // mainnet test genesis + BTC_DAO_REGTEST(RegTestParams.get(), "BTC", "REGTEST", "Bitcoin"); // server side regtest after v0.9.5, had breaking code changes so we started over again @Getter private final NetworkParameters parameters; @@ -59,6 +60,10 @@ public boolean isDaoTestNet() { return "BTC_DAO_TESTNET".equals(name()); } + public boolean isDaoRegTest() { + return "BTC_DAO_REGTEST".equals(name()); + } + public boolean isDaoBetaNet() { return "BTC_DAO_BETANET".equals(name()); } diff --git a/core/src/main/java/bisq/core/btc/BitcoinModule.java b/core/src/main/java/bisq/core/btc/BitcoinModule.java index 60c55d31c65..29b174539fd 100644 --- a/core/src/main/java/bisq/core/btc/BitcoinModule.java +++ b/core/src/main/java/bisq/core/btc/BitcoinModule.java @@ -54,13 +54,15 @@ public BitcoinModule(Environment environment) { @Override protected void configure() { - // We we have selected BTC_DAO_TESTNET we use our master regtest node, otherwise the specified host or default - // (localhost) + // If we we have selected BTC_DAO_REGTEST or BTC_DAO_TESTNET we use our master regtest node, + // otherwise the specified host or default (localhost) String regTestHost = environment.getProperty(BtcOptionKeys.REG_TEST_HOST, String.class, ""); if (regTestHost.isEmpty()) { regTestHost = BisqEnvironment.getBaseCurrencyNetwork().isDaoTestNet() ? "104.248.31.39" : - RegTestHost.DEFAULT_HOST; + BisqEnvironment.getBaseCurrencyNetwork().isDaoRegTest() ? + "134.209.242.206" : + RegTestHost.DEFAULT_HOST; } RegTestHost.HOST = regTestHost; diff --git a/core/src/main/java/bisq/core/btc/setup/WalletConfig.java b/core/src/main/java/bisq/core/btc/setup/WalletConfig.java index e8c19ea9c60..6ecdbf453ca 100644 --- a/core/src/main/java/bisq/core/btc/setup/WalletConfig.java +++ b/core/src/main/java/bisq/core/btc/setup/WalletConfig.java @@ -235,7 +235,7 @@ private PeerGroup createPeerGroup() { // For dao testnet (server side regtest) we prevent to connect to a localhost node to avoid confusion // if local btc node is not synced with our dao testnet master node. - if (BisqEnvironment.getBaseCurrencyNetwork().isDaoTestNet()) + if (BisqEnvironment.getBaseCurrencyNetwork().isDaoRegTest() || BisqEnvironment.getBaseCurrencyNetwork().isDaoTestNet()) peerGroup.setUseLocalhostPeerWhenPossible(false); return peerGroup; diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index e3413b6ea9e..7c5c90f772a 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.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,28 @@ public long getTotalAmountOfConfiscatedTxOutputs() { return daoStateService.getTotalAmountOfConfiscatedTxOutputs(); } + public long getTotalAmountOfInvalidatedBsq() { + return daoStateService.getTotalAmountOfInvalidatedBsq(); + } + + // Contains burned fee and invalidated bsq due invalid txs + public long getTotalAmountOfBurntBsq() { + return daoStateService.getTotalAmountOfBurntBsq(); + } + + public List getInvalidTxs() { + return daoStateService.getInvalidTxs(); + } + + public List getIrregularTxs() { + return daoStateService.getIrregularTxs(); + } + + 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); } @@ -589,7 +612,7 @@ public int getNumIssuanceTransactions(IssuanceType issuanceType) { return daoStateService.getIssuanceSet(issuanceType).size(); } - public Set getFeeTxs() { + public Set getBurntFeeTxs() { return daoStateService.getBurntFeeTxs(); } diff --git a/core/src/main/java/bisq/core/dao/governance/ballot/BallotListPresentation.java b/core/src/main/java/bisq/core/dao/governance/ballot/BallotListPresentation.java index ada2b27499a..e7e148061a3 100644 --- a/core/src/main/java/bisq/core/dao/governance/ballot/BallotListPresentation.java +++ b/core/src/main/java/bisq/core/dao/governance/ballot/BallotListPresentation.java @@ -45,7 +45,6 @@ public class BallotListPresentation implements BallotListService.BallotListChangeListener, DaoStateListener { private final BallotListService ballotListService; private final PeriodService periodService; - private final DaoStateService daoStateService; private final ProposalValidatorProvider proposalValidatorProvider; @Getter @@ -65,31 +64,20 @@ public BallotListPresentation(BallotListService ballotListService, ProposalValidatorProvider proposalValidatorProvider) { this.ballotListService = ballotListService; this.periodService = periodService; - this.daoStateService = daoStateService; this.proposalValidatorProvider = proposalValidatorProvider; daoStateService.addDaoStateListener(this); ballotListService.addListener(this); } + /////////////////////////////////////////////////////////////////////////////////////////// // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public void onNewBlockHeight(int blockHeight) { - if (daoStateService.isParseBlockChainComplete()) { - ballotsOfCycle.setPredicate(ballot -> periodService.isTxInCorrectCycle(ballot.getTxId(), blockHeight)); - } - } - - @Override - public void onParseBlockChainComplete() { - ballotsOfCycle.setPredicate(ballot -> periodService.isTxInCorrectCycle(ballot.getTxId(), daoStateService.getChainHeight())); - } - @Override public void onParseBlockCompleteAfterBatchProcessing(Block block) { + ballotsOfCycle.setPredicate(ballot -> periodService.isTxInCorrectCycle(ballot.getTxId(), block.getHeight())); onListChanged(ballotListService.getValidatedBallotList()); } diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVote.java b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVote.java index c5fa1c92cf8..09e961bcf16 100644 --- a/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVote.java +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVote.java @@ -21,6 +21,7 @@ import bisq.common.proto.network.NetworkPayload; import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.ExtraDataMapValidator; import bisq.common.util.Utilities; import io.bisq.generated.protobuffer.PB; @@ -65,7 +66,7 @@ public BlindVote(byte[] encryptedVotes, this.txId = txId; this.stake = stake; this.encryptedMeritList = encryptedMeritList; - this.extraDataMap = extraDataMap; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); } diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteListService.java b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteListService.java index eb920dcbc61..bafaeb98aac 100644 --- a/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteListService.java +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteListService.java @@ -136,20 +136,20 @@ private void fillListFromAppendOnlyDataStore() { p2PService.getP2PDataStorage().getAppendOnlyDataStoreMap().values().forEach(e -> onAppendOnlyDataAdded(e, false)); } - private void onAppendOnlyDataAdded(PersistableNetworkPayload persistableNetworkPayload, boolean doLog) { + private void onAppendOnlyDataAdded(PersistableNetworkPayload persistableNetworkPayload, boolean fromBroadcastMessage) { if (persistableNetworkPayload instanceof BlindVotePayload) { BlindVotePayload blindVotePayload = (BlindVotePayload) persistableNetworkPayload; if (!blindVotePayloads.contains(blindVotePayload)) { BlindVote blindVote = blindVotePayload.getBlindVote(); String txId = blindVote.getTxId(); - // We don't check the phase and the cycle as we want to add all object independently when we receive it - // (or when we start the app to fill our list from the data we gor from the seed node). + // We don't validate as we might receive blindVotes from other cycles or phases at startup. + // Beside that we might receive payloads we requested at the vote result phase in case we missed some + // payloads. We prefer here resilience over protection against late publishing attacks. if (blindVoteValidator.areDataFieldsValid(blindVote)) { - // We don't validate as we might receive blindVotes from other cycles or phases at startup. - blindVotePayloads.add(blindVotePayload); - if (doLog) { + if (fromBroadcastMessage) { log.info("We received a blindVotePayload. blindVoteTxId={}", txId); } + blindVotePayloads.add(blindVotePayload); } else { log.warn("We received an invalid blindVotePayload. blindVoteTxId={}", txId); } diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteValidator.java b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteValidator.java index e16542887f4..4b1035a25e8 100644 --- a/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteValidator.java +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/BlindVoteValidator.java @@ -17,12 +17,15 @@ package bisq.core.dao.governance.blindvote; +import bisq.core.btc.wallet.Restrictions; import bisq.core.dao.governance.period.PeriodService; import bisq.core.dao.governance.proposal.ProposalValidationException; import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.Tx; import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.common.util.ExtraDataMapValidator; + import javax.inject.Inject; import java.util.Optional; @@ -58,12 +61,20 @@ private void validateDataFields(BlindVote blindVote) throws ProposalValidationEx checkNotNull(blindVote.getEncryptedVotes(), "encryptedProposalList must not be null"); checkArgument(blindVote.getEncryptedVotes().length > 0, "encryptedProposalList must not be empty"); - checkNotNull(blindVote.getTxId(), "txId must not be null"); - checkArgument(!blindVote.getTxId().isEmpty(), "txId must not be empty"); - checkArgument(blindVote.getStake() > 0, "stake must be positive"); + checkArgument(blindVote.getEncryptedVotes().length <= 100000, + "encryptedProposalList must not exceed 100kb"); + + checkNotNull(blindVote.getTxId(), "Tx ID must not be null"); + checkArgument(blindVote.getTxId().length() == 64, "Tx ID must be 64 chars"); + checkArgument(blindVote.getStake() >= Restrictions.getMinNonDustOutput().value, "Stake must be at least MinNonDustOutput"); + checkNotNull(blindVote.getEncryptedMeritList(), "getEncryptedMeritList must not be null"); checkArgument(blindVote.getEncryptedMeritList().length > 0, "getEncryptedMeritList must not be empty"); + checkArgument(blindVote.getEncryptedMeritList().length <= 100000, + "getEncryptedMeritList must not exceed 100kb"); + + ExtraDataMapValidator.validate(blindVote.getExtraDataMap()); } catch (Throwable e) { log.warn(e.toString()); throw new ProposalValidationException(e); diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java index 443f6d0a9db..bf25bac3505 100644 --- a/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java @@ -135,7 +135,7 @@ public MyBlindVoteListService(P2PService p2PService, this.myVoteListService = myVoteListService; this.myProposalListService = myProposalListService; - numConnectedPeersListener = (observable, oldValue, newValue) -> rePublishOnceWellConnected(); + numConnectedPeersListener = (observable, oldValue, newValue) -> rePublishMyBlindVoteOnceWellConnected(); } @@ -176,7 +176,7 @@ public void readPersisted() { @Override public void onParseBlockChainComplete() { - rePublishOnceWellConnected(); + rePublishMyBlindVoteOnceWellConnected(); } @@ -351,15 +351,19 @@ private Transaction getBlindVoteTx(Coin stake, Coin fee, byte[] opReturnData) return bsqWalletService.signTx(txWithBtcFee); } - private void rePublishOnceWellConnected() { + private void rePublishMyBlindVoteOnceWellConnected() { + // We republish at each startup at any block during the cycle. We filter anyway for valid blind votes + // of that cycle so it is 1 blind vote getting rebroadcast at each startup to my neighbors. + // Republishing only will have effect if the payload creation date is < 5 hours as other nodes would not + // accept payloads which are too old or are in future. + // Only payloads received from seed nodes would ignore that date check. int minPeers = BisqEnvironment.getBaseCurrencyNetwork().isMainnet() ? 4 : 1; if ((p2PService.getNumConnectedPeers().get() >= minPeers && p2PService.isBootstrapped()) || BisqEnvironment.getBaseCurrencyNetwork().isRegtest()) { - int chainHeight = periodService.getChainHeight(); myBlindVoteList.stream() .filter(blindVote -> periodService.isTxInPhaseAndCycle(blindVote.getTxId(), DaoPhase.Phase.BLIND_VOTE, - chainHeight)) + periodService.getChainHeight())) .forEach(blindVote -> addToP2PNetwork(blindVote, null)); // We delay removal of listener as we call that inside listener itself. @@ -369,13 +373,15 @@ private void rePublishOnceWellConnected() { private void addToP2PNetwork(BlindVote blindVote, @Nullable ErrorMessageHandler errorMessageHandler) { BlindVotePayload blindVotePayload = new BlindVotePayload(blindVote); + // We use reBroadcast flag here as we only broadcast our own blindVote and want to be sure it gets distributed + // well. boolean success = p2PService.addPersistableNetworkPayload(blindVotePayload, true); if (success) { log.info("We added a blindVotePayload to the P2P network as append only data. blindVoteTxId={}", blindVote.getTxId()); } else { - final String msg = "Adding of blindVotePayload to P2P network failed. blindVoteTxId=" + blindVote.getTxId(); + String msg = "Adding of blindVotePayload to P2P network failed. blindVoteTxId=" + blindVote.getTxId(); log.error(msg); if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(msg); diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/network/RepublishGovernanceDataHandler.java b/core/src/main/java/bisq/core/dao/governance/blindvote/network/RepublishGovernanceDataHandler.java index 569796343b8..d5f77c6035c 100644 --- a/core/src/main/java/bisq/core/dao/governance/blindvote/network/RepublishGovernanceDataHandler.java +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/network/RepublishGovernanceDataHandler.java @@ -92,7 +92,8 @@ public void sendRepublishRequest() { private void sendRepublishRequest(NodeAddress nodeAddress) { RepublishGovernanceDataRequest republishGovernanceDataRequest = new RepublishGovernanceDataRequest(); if (timeoutTimer == null) { - timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions + timeoutTimer = UserThread.runAfter(() -> { + // setup before sending to avoid race conditions if (!stopped) { String errorMessage = "A timeout occurred at sending republishGovernanceDataRequest:" + " to nodeAddress:" + nodeAddress; diff --git a/core/src/main/java/bisq/core/dao/governance/merit/MeritConsensus.java b/core/src/main/java/bisq/core/dao/governance/merit/MeritConsensus.java index b77347495ce..29d0a03167b 100644 --- a/core/src/main/java/bisq/core/dao/governance/merit/MeritConsensus.java +++ b/core/src/main/java/bisq/core/dao/governance/merit/MeritConsensus.java @@ -54,20 +54,19 @@ public static MeritList decryptMeritList(byte[] encryptedMeritList, SecretKey se } public static long getMeritStake(String blindVoteTxId, MeritList meritList, DaoStateService daoStateService) { - int txChainHeight = daoStateService.getTx(blindVoteTxId).map(Tx::getBlockHeight).orElse(0); - return getMeritStake(blindVoteTxId, meritList, txChainHeight); - } - - private static long getMeritStake(String blindVoteTxId, MeritList meritList, int txChainHeight) { // We need to take the chain height when the blindVoteTx got published so we get the same merit for the vote even at // later blocks (merit decreases with each block). - if (txChainHeight == 0) { + int blindVoteTxHeight = daoStateService.getTx(blindVoteTxId).map(Tx::getBlockHeight).orElse(0); + if (blindVoteTxHeight == 0) { log.error("Error at getMeritStake: blindVoteTx not found in daoStateService. blindVoteTxId=" + blindVoteTxId); return 0; } + // We only use past issuance. In case we would calculate the merit after the vote result phase we have the + // issuance from the same cycle but we must not add that to the merit. return meritList.getList().stream() .filter(merit -> isSignatureValid(merit.getSignature(), merit.getIssuance().getPubKey(), blindVoteTxId)) + .filter(merit -> merit.getIssuance().getChainHeight() <= blindVoteTxHeight) .mapToLong(merit -> { try { Issuance issuance = merit.getIssuance(); @@ -75,7 +74,7 @@ private static long getMeritStake(String blindVoteTxId, MeritList meritList, int "issuance must be of type COMPENSATION"); return getWeightedMeritAmount(issuance.getAmount(), issuance.getChainHeight(), - txChainHeight, + blindVoteTxHeight, BLOCKS_PER_YEAR); } catch (Throwable t) { log.error("Error at getMeritStake: error={}, merit={}", t.toString(), merit); @@ -145,17 +144,20 @@ public static long getWeightedMeritAmount(long amount, int issuanceHeight, int b public static long getCurrentlyAvailableMerit(MeritList meritList, int currentChainHeight) { // We need to take the chain height when the blindVoteTx got published so we get the same merit for the vote even at // later blocks (merit decreases with each block). + // We add 1 block to currentChainHeight so that the displayed merit would match the merit in case we get the + // blind vote tx into the next block. + int height = currentChainHeight + 1; return meritList.getList().stream() .mapToLong(merit -> { try { Issuance issuance = merit.getIssuance(); checkArgument(issuance.getIssuanceType() == IssuanceType.COMPENSATION, "issuance must be of type COMPENSATION"); int issuanceHeight = issuance.getChainHeight(); - checkArgument(issuanceHeight <= currentChainHeight, + checkArgument(issuanceHeight <= height, "issuanceHeight must not be larger as currentChainHeight"); return getWeightedMeritAmount(issuance.getAmount(), issuanceHeight, - currentChainHeight, + height, BLOCKS_PER_YEAR); } catch (Throwable t) { log.error("Error at getCurrentlyAvailableMerit: " + t.toString()); diff --git a/core/src/main/java/bisq/core/dao/governance/myvote/MyVote.java b/core/src/main/java/bisq/core/dao/governance/myvote/MyVote.java index 92c3ac8d925..507eaaadb70 100644 --- a/core/src/main/java/bisq/core/dao/governance/myvote/MyVote.java +++ b/core/src/main/java/bisq/core/dao/governance/myvote/MyVote.java @@ -126,7 +126,7 @@ public static MyVote fromProto(PB.MyVote proto) { // API /////////////////////////////////////////////////////////////////////////////////////////// - public String getTxId() { + public String getBlindVoteTxId() { return blindVote.getTxId(); } diff --git a/core/src/main/java/bisq/core/dao/governance/myvote/MyVoteList.java b/core/src/main/java/bisq/core/dao/governance/myvote/MyVoteList.java index fd7421e6645..169aeb21e03 100644 --- a/core/src/main/java/bisq/core/dao/governance/myvote/MyVoteList.java +++ b/core/src/main/java/bisq/core/dao/governance/myvote/MyVoteList.java @@ -64,7 +64,7 @@ public static PersistableEnvelope fromProto(PB.MyVoteList proto) { @Override public String toString() { return "List of TxId's in MyVoteList: " + getList().stream() - .map(MyVote::getTxId) + .map(MyVote::getBlindVoteTxId) .collect(Collectors.toList()); } } diff --git a/core/src/main/java/bisq/core/dao/governance/param/Param.java b/core/src/main/java/bisq/core/dao/governance/param/Param.java index c8aed6948d3..81fa09ede95 100644 --- a/core/src/main/java/bisq/core/dao/governance/param/Param.java +++ b/core/src/main/java/bisq/core/dao/governance/param/Param.java @@ -142,7 +142,9 @@ public enum Param { "4" : // regtest BisqEnvironment.getBaseCurrencyNetwork().isDaoBetaNet() ? "144" : // daoBetaNet; 1 day - "380", // testnet or dao testnet (server side regtest); 2.6 days + BisqEnvironment.getBaseCurrencyNetwork().isDaoRegTest() ? + "134" : // dao regtest; 0.93 days + "380", // testnet or dao testnet (server side regtest); 2.6 days ParamType.BLOCK, 2, 2), PHASE_BREAK1(BisqEnvironment.getBaseCurrencyNetwork().isMainnet() ? "149" : // mainnet; 1 day @@ -150,7 +152,9 @@ public enum Param { "1" : // regtest BisqEnvironment.getBaseCurrencyNetwork().isDaoBetaNet() ? "10" : // daoBetaNet - "10", // testnet or dao testnet (server side regtest) + BisqEnvironment.getBaseCurrencyNetwork().isDaoRegTest() ? + "10" : // dao regtest + "10", // testnet or dao testnet (server side regtest) ParamType.BLOCK, 2, 2), PHASE_BLIND_VOTE(BisqEnvironment.getBaseCurrencyNetwork().isMainnet() ? "451" : // mainnet; 3 days @@ -158,7 +162,9 @@ public enum Param { "2" : // regtest BisqEnvironment.getBaseCurrencyNetwork().isDaoBetaNet() ? "144" : // daoBetaNet; 1 day - "300", // testnet or dao testnet (server side regtest); 2 days + BisqEnvironment.getBaseCurrencyNetwork().isDaoRegTest() ? + "134" : // dao regtest; 0.93 days + "300", // testnet or dao testnet (server side regtest); 2 days ParamType.BLOCK, 2, 2), PHASE_BREAK2(BisqEnvironment.getBaseCurrencyNetwork().isMainnet() ? "9" : // mainnet @@ -166,7 +172,9 @@ public enum Param { "1" : // regtest BisqEnvironment.getBaseCurrencyNetwork().isDaoBetaNet() ? "10" : // daoBetaNet - "10", // testnet or dao testnet (server side regtest) + BisqEnvironment.getBaseCurrencyNetwork().isDaoRegTest() ? + "10" : // dao regtest + "10", // testnet or dao testnet (server side regtest) ParamType.BLOCK, 2, 2), PHASE_VOTE_REVEAL(BisqEnvironment.getBaseCurrencyNetwork().isMainnet() ? "451" : // mainnet; 3 days @@ -174,7 +182,9 @@ public enum Param { "2" : // regtest BisqEnvironment.getBaseCurrencyNetwork().isDaoBetaNet() ? "144" : // daoBetaNet; 1 day - "300", // testnet or dao testnet (server side regtest); 2 days + BisqEnvironment.getBaseCurrencyNetwork().isDaoRegTest() ? + "132" : // dao regtest; 0.93 days + "300", // testnet or dao testnet (server side regtest); 2 days ParamType.BLOCK, 2, 2), PHASE_BREAK3(BisqEnvironment.getBaseCurrencyNetwork().isMainnet() ? "9" : // mainnet @@ -182,7 +192,9 @@ public enum Param { "1" : // regtest BisqEnvironment.getBaseCurrencyNetwork().isDaoBetaNet() ? "10" : // daoBetaNet - "10", // testnet or dao testnet (server side regtest) + BisqEnvironment.getBaseCurrencyNetwork().isDaoRegTest() ? + "10" : // dao regtest + "10", // testnet or dao testnet (server side regtest) ParamType.BLOCK, 2, 2), PHASE_RESULT(BisqEnvironment.getBaseCurrencyNetwork().isMainnet() ? "10" : // mainnet @@ -190,7 +202,9 @@ public enum Param { "2" : // regtest BisqEnvironment.getBaseCurrencyNetwork().isDaoBetaNet() ? "10" : // daoBetaNet - "2", // testnet or dao testnet (server side regtest) + BisqEnvironment.getBaseCurrencyNetwork().isDaoRegTest() ? + "2" : // dao regtest + "2", // testnet or dao testnet (server side regtest) ParamType.BLOCK, 2, 2); @Getter diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java b/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java index 3649cedcb12..0357ce7958e 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java @@ -30,6 +30,7 @@ import bisq.network.p2p.P2PService; +import bisq.common.UserThread; import bisq.common.app.DevEnv; import bisq.common.crypto.KeyRing; import bisq.common.handlers.ErrorMessageHandler; @@ -93,7 +94,7 @@ public MyProposalListService(P2PService p2PService, signaturePubKey = keyRing.getPubKeyRing().getSignaturePubKey(); - numConnectedPeersListener = (observable, oldValue, newValue) -> rePublishOnceWellConnected(); + numConnectedPeersListener = (observable, oldValue, newValue) -> rePublishMyProposalsOnceWellConnected(); daoStateService.addDaoStateListener(this); p2PService.getNumConnectedPeers().addListener(numConnectedPeersListener); } @@ -122,7 +123,7 @@ public void readPersisted() { @Override public void onParseBlockChainComplete() { - rePublishOnceWellConnected(); + rePublishMyProposalsOnceWellConnected(); } @@ -216,26 +217,23 @@ private boolean addToP2PNetworkAsProtectedData(Proposal proposal) { return p2PService.addProtectedStorageEntry(new TempProposalPayload(proposal, signaturePubKey), true); } - private void rePublishOnceWellConnected() { + private void rePublishMyProposalsOnceWellConnected() { + // We republish at each startup at any block during the cycle. We filter anyway for valid blind votes + // of that cycle so it is 1 blind vote getting rebroadcast at each startup to my neighbors. int minPeers = BisqEnvironment.getBaseCurrencyNetwork().isMainnet() ? 4 : 1; if ((p2PService.getNumConnectedPeers().get() >= minPeers && p2PService.isBootstrapped()) || BisqEnvironment.getBaseCurrencyNetwork().isRegtest()) { - p2PService.getNumConnectedPeers().removeListener(numConnectedPeersListener); - rePublish(); + myProposalList.stream() + .filter(proposal -> periodService.isTxInPhaseAndCycle(proposal.getTxId(), + DaoPhase.Phase.PROPOSAL, + periodService.getChainHeight())) + .forEach(this::addToP2PNetworkAsProtectedData); + + // We delay removal of listener as we call that inside listener itself. + UserThread.execute(() -> p2PService.getNumConnectedPeers().removeListener(numConnectedPeersListener)); } } - private void rePublish() { - myProposalList.forEach(proposal -> { - final String txId = proposal.getTxId(); - if (periodService.isTxInPhaseAndCycle(txId, DaoPhase.Phase.PROPOSAL, periodService.getChainHeight())) { - boolean result = addToP2PNetworkAsProtectedData(proposal); - if (!result) - log.warn("Adding of proposal to P2P network failed.\nproposal=" + proposal); - } - }); - } - private void persist() { storage.queueUpForSave(); } diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java index cb264b875f8..464d840ef96 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java @@ -159,11 +159,12 @@ public void onAdded(PersistableNetworkPayload payload) { @Override public void onParseBlockCompleteAfterBatchProcessing(Block block) { - int heightForRepublishing = periodService.getFirstBlockOfPhase(daoStateService.getChainHeight(), DaoPhase.Phase.BREAK1); - if (block.getHeight() == heightForRepublishing) { + // We try to broadcast at any block in the break1 phase. If we have received the data already we do not + // broadcast so we do not flood the network. + if (periodService.isInPhase(block.getHeight(), DaoPhase.Phase.BREAK1)) { // We only republish if we are completed with parsing old blocks, otherwise we would republish old // proposals all the time - publishToAppendOnlyDataStore(); + maybePublishToAppendOnlyDataStore(); fillListFromAppendOnlyDataStore(); } } @@ -214,36 +215,49 @@ private void fillListFromAppendOnlyDataStore() { p2PService.getP2PDataStorage().getAppendOnlyDataStoreMap().values().forEach(e -> onAppendOnlyDataAdded(e, false)); } - private void publishToAppendOnlyDataStore() { + private void maybePublishToAppendOnlyDataStore() { + // We set reBroadcast to false to avoid to flood the network. + // If we have 20 proposals and 200 nodes with 10 neighbor peers we would send 40 000 messages if we would set + // reBroadcast to ! tempProposals.stream() .filter(proposal -> validatorProvider.getValidator(proposal).isValidAndConfirmed(proposal)) .map(ProposalPayload::new) .forEach(proposalPayload -> { - boolean success = p2PService.addPersistableNetworkPayload(proposalPayload, true); - if (success) + boolean success = p2PService.addPersistableNetworkPayload(proposalPayload, false); + if (success) { log.info("We published a ProposalPayload to the P2P network as append-only data. proposalTxId={}", proposalPayload.getProposal().getTxId()); - else - log.warn("publishToAppendOnlyDataStore failed for proposal " + proposalPayload.getProposal()); + } + // If we had data already we did not broadcast and success is false }); } - private void onProtectedDataAdded(ProtectedStorageEntry entry, boolean doLog) { + private void onProtectedDataAdded(ProtectedStorageEntry entry, boolean fromBroadcastMessage) { ProtectedStoragePayload protectedStoragePayload = entry.getProtectedStoragePayload(); if (protectedStoragePayload instanceof TempProposalPayload) { Proposal proposal = ((TempProposalPayload) protectedStoragePayload).getProposal(); // We do not validate if we are in current cycle and if tx is confirmed yet as the tx might be not - // available/confirmed. But we check if we are in the proposal phase. - if (!tempProposals.contains(proposal)) { - if (validatorProvider.getValidator(proposal).isValidOrUnconfirmed(proposal)) { - if (doLog) { - log.info("We received a TempProposalPayload and store it to our protectedStoreList. proposalTxId={}", - proposal.getTxId()); + // available/confirmed. + // We check if we are in the proposal or break1 phase. We are tolerant to accept tempProposals in the break1 + // phase to avoid risks that a proposal published very closely to the end of the proposal phase will not be + // sufficiently broadcast. + // When we receive tempProposals from the seed node at startup we only keep those which are in the current + // proposal/break1 phase if we are in that phase. We ignore tempProposals in case we are not in the + // proposal/break1 phase as they are not used anyway but the proposalPayloads will be relevant once we + // left the proposal/break1 phase. + if (periodService.isInPhase(daoStateService.getChainHeight(), DaoPhase.Phase.PROPOSAL) || + periodService.isInPhase(daoStateService.getChainHeight(), DaoPhase.Phase.BREAK1)) { + if (!tempProposals.contains(proposal)) { + if (validatorProvider.getValidator(proposal).areDataFieldsValid(proposal)) { + if (fromBroadcastMessage) { + log.info("We received a TempProposalPayload and store it to our protectedStoreList. proposalTxId={}", + proposal.getTxId()); + } + tempProposals.add(proposal); + } else { + log.debug("We received an invalid proposal from the P2P network. Proposal.txId={}, blockHeight={}", + proposal.getTxId(), daoStateService.getChainHeight()); } - tempProposals.add(proposal); - } else { - log.debug("We received an invalid proposal from the P2P network. Proposal.txId={}, blockHeight={}", - proposal.getTxId(), daoStateService.getChainHeight()); } } } @@ -275,13 +289,17 @@ private void onProtectedDataRemoved(ProtectedStorageEntry entry) { } } - private void onAppendOnlyDataAdded(PersistableNetworkPayload persistableNetworkPayload, boolean doLog) { + private void onAppendOnlyDataAdded(PersistableNetworkPayload persistableNetworkPayload, boolean fromBroadcastMessage) { if (persistableNetworkPayload instanceof ProposalPayload) { ProposalPayload proposalPayload = (ProposalPayload) persistableNetworkPayload; if (!proposalPayloads.contains(proposalPayload)) { Proposal proposal = proposalPayload.getProposal(); + + // We don't validate phase and cycle as we might receive proposals from other cycles or phases at startup. + // Beside that we might receive payloads we requested at the vote result phase in case we missed some + // payloads. We prefer here resilience over protection against late publishing attacks. if (validatorProvider.getValidator(proposal).areDataFieldsValid(proposal)) { - if (doLog) { + if (fromBroadcastMessage) { log.info("We received a ProposalPayload and store it to our appendOnlyStoreList. proposalTxId={}", proposal.getTxId()); } diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalValidator.java b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalValidator.java index 66acd772ec3..e0f0118b1cc 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalValidator.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalValidator.java @@ -28,10 +28,13 @@ import bisq.core.dao.state.model.governance.Proposal; import bisq.core.dao.state.model.governance.ReimbursementProposal; +import bisq.common.util.ExtraDataMapValidator; + import java.util.Optional; import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkArgument; import static org.apache.commons.lang3.Validate.notEmpty; /** @@ -61,6 +64,12 @@ public void validateDataFields(Proposal proposal) throws ProposalValidationExcep try { notEmpty(proposal.getName(), "name must not be empty"); notEmpty(proposal.getLink(), "link must not be empty"); + checkArgument(proposal.getName().length() <= 200, "Name must not exceed 200 chars"); + checkArgument(proposal.getLink().length() <= 200, "Link must not exceed 200 chars"); + if (proposal.getTxId() != null) + checkArgument(proposal.getTxId().length() == 64, "Tx ID must be 64 chars"); + + ExtraDataMapValidator.validate(proposal.getExtraDataMap()); } catch (Throwable throwable) { throw new ProposalValidationException(throwable); } diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/confiscatebond/ConfiscateBondValidator.java b/core/src/main/java/bisq/core/dao/governance/proposal/confiscatebond/ConfiscateBondValidator.java index dd6d68225db..02124b7949f 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/confiscatebond/ConfiscateBondValidator.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/confiscatebond/ConfiscateBondValidator.java @@ -29,6 +29,7 @@ import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkArgument; import static org.apache.commons.lang3.Validate.notEmpty; /** @@ -48,6 +49,7 @@ public void validateDataFields(Proposal proposal) throws ProposalValidationExcep super.validateDataFields(proposal); ConfiscateBondProposal confiscateBondProposal = (ConfiscateBondProposal) proposal; notEmpty(confiscateBondProposal.getLockupTxId(), "LockupTxId must not be empty"); + checkArgument(confiscateBondProposal.getLockupTxId().length() == 64, "LockupTxId must be 64 chars"); } catch (ProposalValidationException e) { throw e; } catch (Throwable throwable) { diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/param/ChangeParamValidator.java b/core/src/main/java/bisq/core/dao/governance/proposal/param/ChangeParamValidator.java index 8e86d9976a1..e209bc204f7 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/param/ChangeParamValidator.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/param/ChangeParamValidator.java @@ -65,6 +65,7 @@ public void validateDataFields(Proposal proposal) throws ProposalValidationExcep super.validateDataFields(proposal); ChangeParamProposal changeParamProposal = (ChangeParamProposal) proposal; validateParamValue(changeParamProposal.getParam(), changeParamProposal.getParamValue(), getBlockHeight(proposal)); + checkArgument(changeParamProposal.getParamValue().length() <= 200, "ParamValue must not exceed 200 chars"); } catch (ProposalValidationException e) { throw e; } catch (Throwable throwable) { diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/removeAsset/RemoveAssetValidator.java b/core/src/main/java/bisq/core/dao/governance/proposal/removeAsset/RemoveAssetValidator.java index 9a6f59d7c2f..a3712482c98 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/removeAsset/RemoveAssetValidator.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/removeAsset/RemoveAssetValidator.java @@ -29,6 +29,7 @@ import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkArgument; import static org.apache.commons.lang3.Validate.notEmpty; /** @@ -49,6 +50,10 @@ public void validateDataFields(Proposal proposal) throws ProposalValidationExcep RemoveAssetProposal removeAssetProposal = (RemoveAssetProposal) proposal; notEmpty(removeAssetProposal.getTickerSymbol(), "TickerSymbol must not be empty"); + + // We want to avoid that someone cause damage by inserting a super long string. Real ticker symbols + // are usually very short but we don't want to add additional restrictions here. + checkArgument(removeAssetProposal.getTickerSymbol().length() <= 100, "TickerSymbol must not exceed 100 chars"); } catch (ProposalValidationException e) { throw e; } catch (Throwable throwable) { diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalPayload.java b/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalPayload.java index 8ba16390920..38b993f1e6d 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalPayload.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/storage/temp/TempProposalPayload.java @@ -28,6 +28,7 @@ import bisq.common.app.Capability; import bisq.common.crypto.Sig; import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.ExtraDataMapValidator; import io.bisq.generated.protobuffer.PB; @@ -88,7 +89,7 @@ private TempProposalPayload(Proposal proposal, @Nullable Map extraDataMap) { this.proposal = proposal; this.ownerPubKeyEncoded = ownerPubPubKeyEncoded; - this.extraDataMap = extraDataMap; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyEncoded); } diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/MissingDataRequestService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/MissingDataRequestService.java index 6ee09711b15..8042ac5c735 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/MissingDataRequestService.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/MissingDataRequestService.java @@ -43,6 +43,7 @@ public class MissingDataRequestService implements DaoSetupService { private final BlindVoteListService blindVoteListService; private final ProposalService proposalService; private final P2PService p2PService; + private boolean reRepublishAllGovernanceDataDone; @Inject public MissingDataRequestService(RepublishGovernanceDataHandler republishGovernanceDataHandler, @@ -77,38 +78,44 @@ public void sendRepublishRequest() { republishGovernanceDataHandler.sendRepublishRequest(); } + // Can be triggered with shortcut ctrl+UP or alt+UP public void reRepublishAllGovernanceData() { - ObservableList proposalPayloads = proposalService.getProposalPayloads(); - proposalPayloads.forEach(proposalPayload -> { - // We want a random delay between 0.1 and 30 sec. depending on the number of items - int delay = Math.max(100, Math.min(30_000, new Random().nextInt(proposalPayloads.size() * 500))); - UserThread.runAfter(() -> { - boolean success = p2PService.addPersistableNetworkPayload(proposalPayload, true); - String txId = proposalPayload.getProposal().getTxId(); - if (success) { - log.debug("We received a RepublishGovernanceDataRequest and re-published a proposalPayload to " + - "the P2P network as append only data. proposalTxId={}", txId); - } else { - log.error("Adding of proposalPayload to P2P network failed. proposalTxId={}", txId); - } - }, delay, TimeUnit.MILLISECONDS); - }); - - ObservableList blindVotePayloads = blindVoteListService.getBlindVotePayloads(); - blindVotePayloads - .forEach(blindVotePayload -> { - // We want a random delay between 0.1 and 30 sec. depending on the number of items - int delay = Math.max(100, Math.min(30_000, new Random().nextInt(blindVotePayloads.size() * 500))); - UserThread.runAfter(() -> { - boolean success = p2PService.addPersistableNetworkPayload(blindVotePayload, true); - String txId = blindVotePayload.getBlindVote().getTxId(); - if (success) { - log.debug("We received a RepublishGovernanceDataRequest and re-published a blindVotePayload to " + - "the P2P network as append only data. blindVoteTxId={}", txId); - } else { - log.error("Adding of blindVotePayload to P2P network failed. blindVoteTxId={}", txId); - } - }, delay, TimeUnit.MILLISECONDS); - }); + // We only want to do it once in case we would get flooded with requests. + if (!reRepublishAllGovernanceDataDone) { + reRepublishAllGovernanceDataDone = true; + ObservableList proposalPayloads = proposalService.getProposalPayloads(); + proposalPayloads.forEach(proposalPayload -> { + // We want a random delay between 0.1 and 300 sec. depending on the number of items. + // We send all proposals including those from old cycles. + int delay = Math.max(100, Math.min(300_000, new Random().nextInt(proposalPayloads.size() * 1000))); + UserThread.runAfter(() -> { + boolean success = p2PService.addPersistableNetworkPayload(proposalPayload, true); + String txId = proposalPayload.getProposal().getTxId(); + if (success) { + log.debug("We received a RepublishGovernanceDataRequest and re-published a proposalPayload to " + + "the P2P network as append only data. proposalTxId={}", txId); + } else { + log.error("Adding of proposalPayload to P2P network failed. proposalTxId={}", txId); + } + }, delay, TimeUnit.MILLISECONDS); + }); + + ObservableList blindVotePayloads = blindVoteListService.getBlindVotePayloads(); + blindVotePayloads.forEach(blindVotePayload -> { + // We want a random delay between 0.1 and 300 sec. depending on the number of items. + // We send all blindVotes including those from old cycles. + int delay = Math.max(100, Math.min(300_000, new Random().nextInt(blindVotePayloads.size() * 1000))); + UserThread.runAfter(() -> { + boolean success = p2PService.addPersistableNetworkPayload(blindVotePayload, true); + String txId = blindVotePayload.getBlindVote().getTxId(); + if (success) { + log.debug("We received a RepublishGovernanceDataRequest and re-published a blindVotePayload to " + + "the P2P network as append only data. blindVoteTxId={}", txId); + } else { + log.error("Adding of blindVotePayload to P2P network failed. blindVoteTxId={}", txId); + } + }, delay, TimeUnit.MILLISECONDS); + }); + } } } 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 15c50a1b096..1f0301db8b8 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 static TxOutput getConnectedBlindVoteStakeOutput(Tx voteRevealTx, DaoStat 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/governance/voteresult/VoteResultService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java index b1be48bc1c7..03f5f7ffa9e 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 @@ -358,29 +358,44 @@ private DecryptedBallotsWithMerits getDecryptedBallotsWithMerits( private BallotList createBallotList(VoteWithProposalTxIdList voteWithProposalTxIdList) throws VoteResultException.MissingBallotException { - // We convert the list to a map with proposalTxId as key and the vote as value - Map voteByTxIdMap = voteWithProposalTxIdList.stream() - .filter(voteWithProposalTxId -> voteWithProposalTxId.getVote() != null) - .collect(Collectors.toMap(VoteWithProposalTxId::getProposalTxId, VoteWithProposalTxId::getVote)); + // voteWithProposalTxIdList is the list of ProposalTxId + vote from the blind vote (decrypted vote data) - // We make a map with proposalTxId as key and the ballot as value out of our stored ballot list - Map ballotByTxIdMap = ballotListService.getValidatedBallotList().stream() + // We convert the list to a map with proposalTxId as key and the vote as value. As the vote can be null we + // wrap it into an optional. + Map> voteByTxIdMap = voteWithProposalTxIdList.stream() + .collect(Collectors.toMap(VoteWithProposalTxId::getProposalTxId, e -> Optional.ofNullable(e.getVote()))); + + // We make a map with proposalTxId as key and the ballot as value out of our stored ballot list. + // This can contain ballots which have been added later and have a null value for the vote. + Map ballotByTxIdMap = ballotListService.getValidBallotsOfCycle().stream() .collect(Collectors.toMap(Ballot::getTxId, ballot -> ballot)); + // It could be that we missed some proposalPayloads. + // If we have votes with proposals which are not found in our ballots we add it to missingBallots. List missingBallots = new ArrayList<>(); List ballots = voteByTxIdMap.entrySet().stream() .map(entry -> { String txId = entry.getKey(); if (ballotByTxIdMap.containsKey(txId)) { - // why not use proposalList? Ballot ballot = ballotByTxIdMap.get(txId); // We create a new Ballot with the proposal from the ballot list and the vote from our decrypted votes - Vote vote = entry.getValue(); // We clone the ballot instead applying the vote to the existing ballot from ballotListService // The items from ballotListService.getBallotList() contains my votes. - // Maybe we should cross verify if the vote we had in our local list matches my own vote we - // received from the network? - return new Ballot(ballot.getProposal(), vote); + + if (ballot.getVote() != null) { + // If we had set a vote it was an own active vote + if (!entry.getValue().isPresent()) { + log.warn("We found a local vote but don't have that vote in the data from the " + + "blind vote. ballot={}", ballot); + } else if (ballot.getVote() != entry.getValue().get()) { + log.warn("We found a local vote but the vote from the " + + "blind vote does not match. ballot={}, vote from blindVote data={}", + ballot, entry.getValue().get()); + } + } + + // We only return accepted or rejected votes + return entry.getValue().map(vote -> new Ballot(ballot.getProposal(), vote)).orElse(null); } else { // We got a vote but we don't have the ballot (which includes the proposal) // We add it to the missing list to handle it as exception later. We want all missing data so we @@ -396,6 +411,17 @@ private BallotList createBallotList(VoteWithProposalTxIdList voteWithProposalTxI if (!missingBallots.isEmpty()) throw new VoteResultException.MissingBallotException(ballots, missingBallots); + // If we received a proposal after we had already voted we consider it as an proposal withhold attack and + // treat the proposal as it was voted with a rejected vote. + ballotByTxIdMap.entrySet().stream() + .filter(e -> !voteByTxIdMap.keySet().contains(e.getKey())) + .map(Map.Entry::getValue) + .forEach(ballot -> { + log.warn("We have a proposal which was not part of our blind vote and reject it. " + + "Proposal ={}" + ballot.getProposal()); + ballots.add(new Ballot(ballot.getProposal(), new Vote(false))); + }); + // Let's keep the data more deterministic by sorting it by txId. Though we are not using the sorting. ballots.sort(Comparator.comparing(Ballot::getTxId)); return new BallotList(ballots); 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 e3433fa4043..5c208fe1436 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 @@ -32,7 +32,6 @@ import bisq.core.dao.governance.myvote.MyVote; import bisq.core.dao.governance.myvote.MyVoteListService; import bisq.core.dao.governance.period.PeriodService; -import bisq.core.dao.node.BsqNodeProvider; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.Block; @@ -42,6 +41,7 @@ import bisq.network.p2p.P2PService; +import bisq.common.UserThread; import bisq.common.util.Utilities; import org.bitcoinj.core.InsufficientMoneyException; @@ -102,8 +102,7 @@ public VoteRevealService(DaoStateService daoStateService, BsqWalletService bsqWalletService, BtcWalletService btcWalletService, P2PService p2PService, - WalletsManager walletsManager, - BsqNodeProvider bsqNodeProvider) { + WalletsManager walletsManager) { this.daoStateService = daoStateService; this.blindVoteListService = blindVoteListService; this.periodService = periodService; @@ -179,9 +178,13 @@ private void maybeRevealVotes(int chainHeight) { .filter(myVote -> myVote.getRevealTxId() == null) // we have not already revealed .forEach(myVote -> { boolean isInVoteRevealPhase = periodService.getPhaseForHeight(chainHeight) == DaoPhase.Phase.VOTE_REVEAL; - boolean isBlindVoteTxInCorrectPhaseAndCycle = periodService.isTxInPhaseAndCycle(myVote.getTxId(), DaoPhase.Phase.BLIND_VOTE, chainHeight); - if (isInVoteRevealPhase && isBlindVoteTxInCorrectPhaseAndCycle) { - log.info("We call revealVote at blockHeight {} for blindVoteTxId {}", chainHeight, myVote.getTxId()); + // If we would create the tx in the last block it would be confirmed in the best case in th next + // block which would be already the break and would invalidate the vote reveal. + boolean isLastBlockInPhase = chainHeight == periodService.getLastBlockOfPhase(chainHeight, DaoPhase.Phase.VOTE_REVEAL); + String blindVoteTxId = myVote.getBlindVoteTxId(); + boolean isBlindVoteTxInCorrectPhaseAndCycle = periodService.isTxInPhaseAndCycle(blindVoteTxId, DaoPhase.Phase.BLIND_VOTE, chainHeight); + if (isInVoteRevealPhase && !isLastBlockInPhase && isBlindVoteTxInCorrectPhaseAndCycle) { + log.info("We call revealVote at blockHeight {} for blindVoteTxId {}", chainHeight, blindVoteTxId); // Standard case that we are in the correct phase and cycle and create the reveal tx. revealVote(myVote, true); } else { @@ -192,7 +195,7 @@ private void maybeRevealVotes(int chainHeight) { boolean missedPhaseSameCycle = isAfterVoteRevealPhase && isBlindVoteTxInCorrectPhaseAndCycle; // If we missed the cycle we don't care about the phase anymore. - boolean isBlindVoteTxInPastCycle = periodService.isTxInPastCycle(myVote.getTxId(), chainHeight); + boolean isBlindVoteTxInPastCycle = periodService.isTxInPastCycle(blindVoteTxId, chainHeight); if (missedPhaseSameCycle || isBlindVoteTxInPastCycle) { // Exceptional case that the user missed the vote reveal phase. We still publish the vote @@ -205,7 +208,7 @@ private void maybeRevealVotes(int chainHeight) { // 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={}, blockHeight={}", - myVote.getTxId(), chainHeight); + blindVoteTxId, chainHeight); // 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 @@ -233,7 +236,7 @@ private void revealVote(MyVote myVote, boolean isInVoteRevealPhase) { // myVote is already tested if it is in current cycle at maybeRevealVotes // We expect that the blind vote tx and stake output is available. If not we throw an exception. TxOutput stakeTxOutput = daoStateService.getUnspentBlindVoteStakeTxOutputs().stream() - .filter(txOutput -> txOutput.getTxId().equals(myVote.getTxId())) + .filter(txOutput -> txOutput.getTxId().equals(myVote.getBlindVoteTxId())) .findFirst() .orElseThrow(() -> new VoteRevealException("stakeTxOutput is not found for myVote.", myVote)); @@ -253,7 +256,7 @@ private void revealVote(MyVote myVote, boolean isInVoteRevealPhase) { } catch (IOException | WalletException | TransactionVerificationException | InsufficientMoneyException e) { voteRevealExceptions.add(new VoteRevealException("Exception at calling revealVote.", - e, myVote.getTxId())); + e, myVote.getBlindVoteTxId())); } catch (VoteRevealException e) { voteRevealExceptions.add(e); } @@ -284,12 +287,18 @@ private Transaction getVoteRevealTx(TxOutput stakeTxOutput, byte[] opReturnData) } private void rePublishBlindVotePayloadList(List blindVoteList) { + // If we have 20 blind votes from 20 voters we would have 400 messages sent to their 10 neighbor peers. + // Most of the neighbors will already have the data so they will not continue broadcast. + // To not flood the network too much we use a long random delay to spread the load over 5 minutes. + // As this is only for extra resilience we don't care so much for the case that the user might shut down the + // app before we are finished with our delayed broadcast. + // We cannot set reBroadcast to false as otherwise it would not have any effect as we have the data already and + // broadcast would only be triggered at new data. blindVoteList.stream() .map(BlindVotePayload::new) .forEach(blindVotePayload -> { - boolean success = p2PService.addPersistableNetworkPayload(blindVotePayload, true); - if (!success) - log.warn("publishToAppendOnlyDataStore failed for blindVote " + blindVotePayload.getBlindVote()); + UserThread.runAfterRandomDelay(() -> p2PService.addPersistableNetworkPayload(blindVotePayload, true), + 1, 300); }); } } diff --git a/core/src/main/java/bisq/core/dao/monitoring/BlindVoteStateMonitoringService.java b/core/src/main/java/bisq/core/dao/monitoring/BlindVoteStateMonitoringService.java index 7690d7cbe22..6daf9e95ef7 100644 --- a/core/src/main/java/bisq/core/dao/monitoring/BlindVoteStateMonitoringService.java +++ b/core/src/main/java/bisq/core/dao/monitoring/BlindVoteStateMonitoringService.java @@ -159,7 +159,7 @@ public void onParseBlockChainComplete() { // We wait for processing messages until we have completed batch processing - // We request data from last 5 cycles. We ignore possible duration changes done by voting as that request + // We request data from last 5 cycles. We ignore possible duration changes done by voting. // period is arbitrary anyway... Cycle currentCycle = periodService.getCurrentCycle(); checkNotNull(currentCycle, "currentCycle must not be null"); 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 abf8240c881..41533981efd 100644 --- a/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java +++ b/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java @@ -20,13 +20,16 @@ import bisq.core.dao.DaoSetupService; import bisq.core.dao.monitoring.model.DaoStateBlock; import bisq.core.dao.monitoring.model.DaoStateHash; +import bisq.core.dao.monitoring.model.UtxoMismatch; import bisq.core.dao.monitoring.network.DaoStateNetworkService; import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest; 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; @@ -38,6 +41,9 @@ import org.apache.commons.lang3.ArrayUtils; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + import java.util.LinkedList; import java.util.List; import java.util.Optional; @@ -86,6 +92,8 @@ public interface Listener { private boolean parseBlockChainComplete; @Getter private boolean isInConflict; + @Getter + private ObservableList utxoMismatches = FXCollections.observableArrayList(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -134,6 +142,21 @@ public void onParseBlockChainComplete() { daoStateNetworkService.requestHashesFromAllConnectedSeedNodes(fromHeight); } + @Override + public void onDaoStateChanged(Block block) { + long genesisTotalSupply = daoStateService.getGenesisTotalSupply().value; + long compensationIssuance = daoStateService.getTotalIssuedAmount(IssuanceType.COMPENSATION); + long reimbursementIssuance = daoStateService.getTotalIssuedAmount(IssuanceType.REIMBURSEMENT); + long totalAmountOfBurntBsq = daoStateService.getTotalAmountOfBurntBsq(); + // confiscated funds are still in the utxo set + long sumUtxo = daoStateService.getUnspentTxOutputMap().values().stream().mapToLong(BaseTxOutput::getValue).sum(); + long sumBsq = genesisTotalSupply + compensationIssuance + reimbursementIssuance - totalAmountOfBurntBsq; + + if (sumBsq != sumUtxo) { + utxoMismatches.add(new UtxoMismatch(block.getHeight(), sumUtxo, sumBsq)); + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // StateNetworkService.Listener /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/dao/monitoring/ProposalStateMonitoringService.java b/core/src/main/java/bisq/core/dao/monitoring/ProposalStateMonitoringService.java index bfd837b0704..91b62c649c0 100644 --- a/core/src/main/java/bisq/core/dao/monitoring/ProposalStateMonitoringService.java +++ b/core/src/main/java/bisq/core/dao/monitoring/ProposalStateMonitoringService.java @@ -161,7 +161,7 @@ public void onParseBlockChainComplete() { // We wait for processing messages until we have completed batch processing - // We request data from last 5 cycles. We ignore possible duration changes done by voting as that request + // We request data from last 5 cycles. We ignore possible duration changes done by voting. // period is arbitrary anyway... Cycle currentCycle = periodService.getCurrentCycle(); checkNotNull(currentCycle, "currentCycle must not be null"); diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/UtxoMismatch.java b/core/src/main/java/bisq/core/dao/monitoring/model/UtxoMismatch.java new file mode 100644 index 00000000000..1509023c922 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/UtxoMismatch.java @@ -0,0 +1,33 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.dao.monitoring.model; + +import lombok.Value; + +@Value +public class UtxoMismatch { + private final int height; + private final long sumUtxo; + private final long sumBsq; + + public UtxoMismatch(int height, long sumUtxo, long sumBsq) { + this.height = height; + this.sumUtxo = sumUtxo; + this.sumBsq = sumBsq; + } +} diff --git a/core/src/main/java/bisq/core/dao/node/explorer/ExportJsonFilesService.java b/core/src/main/java/bisq/core/dao/node/explorer/ExportJsonFilesService.java index 712d32a4c64..d747395653f 100644 --- a/core/src/main/java/bisq/core/dao/node/explorer/ExportJsonFilesService.java +++ b/core/src/main/java/bisq/core/dao/node/explorer/ExportJsonFilesService.java @@ -193,7 +193,8 @@ private JsonTx getJsonTx(Tx tx) { getJsonTxOutputs(tx), jsonTxType, jsonTxTypeDisplayString, - daoStateService.getBurntFee(tx.getId()), + tx.getBurntFee(), + tx.getInvalidatedBsq(), tx.getUnlockBlockHeight()); } @@ -239,7 +240,8 @@ private List getJsonTxOutputs(Tx tx) { btcAmount, tx.getBlockHeight(), isBsqTxOutputType, - daoStateService.getBurntFee(tx.getId()), + tx.getBurntFee(), + tx.getInvalidatedBsq(), txOutput.getAddress(), scriptPubKey, spentInfo, diff --git a/core/src/main/java/bisq/core/dao/node/explorer/JsonTx.java b/core/src/main/java/bisq/core/dao/node/explorer/JsonTx.java index a23ff2413d1..145826bf60a 100644 --- a/core/src/main/java/bisq/core/dao/node/explorer/JsonTx.java +++ b/core/src/main/java/bisq/core/dao/node/explorer/JsonTx.java @@ -36,9 +36,26 @@ class JsonTx { private final JsonTxType txType; private final String txTypeDisplayString; private final long burntFee; + private final long invalidatedBsq; // If not set it is -1. LockTime of 0 is a valid value. private final int unlockBlockHeight; + JsonTx(String id, int blockHeight, String blockHash, long time, List inputs, + List outputs, JsonTxType txType, String txTypeDisplayString, long burntFee, + long invalidatedBsq, int unlockBlockHeight) { + this.id = id; + this.blockHeight = blockHeight; + this.blockHash = blockHash; + this.time = time; + this.inputs = inputs; + this.outputs = outputs; + this.txType = txType; + this.txTypeDisplayString = txTypeDisplayString; + this.burntFee = burntFee; + this.invalidatedBsq = invalidatedBsq; + this.unlockBlockHeight = unlockBlockHeight; + } + // Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! // The equals and hashCode methods cannot be overwritten in Enums. @Override @@ -50,6 +67,7 @@ public boolean equals(Object o) { return blockHeight == jsonTx.blockHeight && time == jsonTx.time && burntFee == jsonTx.burntFee && + invalidatedBsq == jsonTx.invalidatedBsq && unlockBlockHeight == jsonTx.unlockBlockHeight && Objects.equals(txVersion, jsonTx.txVersion) && Objects.equals(id, jsonTx.id) && @@ -62,7 +80,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - - return Objects.hash(super.hashCode(), txVersion, id, blockHeight, blockHash, time, inputs, outputs, txType.name(), txTypeDisplayString, burntFee, unlockBlockHeight); + return Objects.hash(super.hashCode(), txVersion, id, blockHeight, blockHash, time, inputs, outputs, + txType.name(), txTypeDisplayString, burntFee, invalidatedBsq, unlockBlockHeight); } } diff --git a/core/src/main/java/bisq/core/dao/node/explorer/JsonTxOutput.java b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxOutput.java index a2cd3ea4b3e..65ce85374f5 100644 --- a/core/src/main/java/bisq/core/dao/node/explorer/JsonTxOutput.java +++ b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxOutput.java @@ -35,6 +35,7 @@ class JsonTxOutput { private final int height; private final boolean isVerified; // isBsqTxOutputType private final long burntFee; + private final long invalidatedBsq; private final String address; @Nullable private final JsonScriptPubKey scriptPubKey; @@ -50,6 +51,31 @@ class JsonTxOutput { private final int lockTime; private final boolean isUnspent; + JsonTxOutput(String txId, int index, long bsqAmount, long btcAmount, int height, boolean isVerified, long burntFee, + long invalidatedBsq, String address, JsonScriptPubKey scriptPubKey, JsonSpentInfo spentInfo, + long time, JsonTxType txType, String txTypeDisplayString, JsonTxOutputType txOutputType, + String txOutputTypeDisplayString, String opReturn, int lockTime, boolean isUnspent) { + this.txId = txId; + this.index = index; + this.bsqAmount = bsqAmount; + this.btcAmount = btcAmount; + this.height = height; + this.isVerified = isVerified; + this.burntFee = burntFee; + this.invalidatedBsq = invalidatedBsq; + this.address = address; + this.scriptPubKey = scriptPubKey; + this.spentInfo = spentInfo; + this.time = time; + this.txType = txType; + this.txTypeDisplayString = txTypeDisplayString; + this.txOutputType = txOutputType; + this.txOutputTypeDisplayString = txOutputTypeDisplayString; + this.opReturn = opReturn; + this.lockTime = lockTime; + this.isUnspent = isUnspent; + } + String getId() { return txId + ":" + index; } @@ -68,6 +94,7 @@ public boolean equals(Object o) { height == that.height && isVerified == that.isVerified && burntFee == that.burntFee && + invalidatedBsq == that.invalidatedBsq && time == that.time && lockTime == that.lockTime && isUnspent == that.isUnspent && @@ -85,7 +112,8 @@ public boolean equals(Object o) { @Override public int hashCode() { - - return Objects.hash(super.hashCode(), txVersion, txId, index, bsqAmount, btcAmount, height, isVerified, burntFee, address, scriptPubKey, spentInfo, time, txType.name(), txTypeDisplayString, txOutputType, txOutputTypeDisplayString, opReturn, lockTime, isUnspent); + return Objects.hash(super.hashCode(), txVersion, txId, index, bsqAmount, btcAmount, height, isVerified, + burntFee, invalidatedBsq, address, scriptPubKey, spentInfo, time, txType.name(), txTypeDisplayString, + txOutputType, txOutputTypeDisplayString, opReturn, lockTime, isUnspent); } } 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 f0e972c6ea8..d14e17f8a56 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/full/RpcService.java b/core/src/main/java/bisq/core/dao/node/full/RpcService.java index 3225905e9f3..b956a288167 100644 --- a/core/src/main/java/bisq/core/dao/node/full/RpcService.java +++ b/core/src/main/java/bisq/core/dao/node/full/RpcService.java @@ -99,12 +99,10 @@ public RpcService(Preferences preferences, boolean isPortSet = rpcPort != null && !rpcPort.isEmpty(); boolean isMainnet = BisqEnvironment.getBaseCurrencyNetwork().isMainnet(); boolean isTestnet = BisqEnvironment.getBaseCurrencyNetwork().isTestnet(); - boolean isDaoTestNet = BisqEnvironment.getBaseCurrencyNetwork().isDaoTestNet(); boolean isDaoBetaNet = BisqEnvironment.getBaseCurrencyNetwork().isDaoBetaNet(); this.rpcPort = isPortSet ? rpcPort : isMainnet || isDaoBetaNet ? "8332" : isTestnet ? "18332" : - isDaoTestNet ? "18443" : "18443"; // regtest this.rpcBlockPort = rpcBlockPort != null && !rpcBlockPort.isEmpty() ? rpcBlockPort : "5125"; diff --git a/core/src/main/java/bisq/core/dao/node/parser/TempTx.java b/core/src/main/java/bisq/core/dao/node/parser/TempTx.java index a2e37ff2f22..67d6c1c593a 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/TempTx.java +++ b/core/src/main/java/bisq/core/dao/node/parser/TempTx.java @@ -55,12 +55,7 @@ static TempTx fromRawTx(RawTx rawTx) { // Mutable data @Nullable private TxType txType; - private long burntFee; - - - /////////////////////////////////////////////////////////////////////////////////////////// - // PROTO BUFFER - /////////////////////////////////////////////////////////////////////////////////////////// + private long burntBsq; private TempTx(String txVersion, String id, @@ -70,7 +65,7 @@ private TempTx(String txVersion, ImmutableList txInputs, ImmutableList tempTxOutputs, @Nullable TxType txType, - long burntFee) { + long burntBsq) { super(txVersion, id, blockHeight, @@ -79,7 +74,7 @@ private TempTx(String txVersion, txInputs); this.tempTxOutputs = tempTxOutputs; this.txType = txType; - this.burntFee = burntFee; + this.burntBsq = burntBsq; } @Override @@ -87,7 +82,7 @@ public String toString() { return "TempTx{" + "\n txOutputs=" + tempTxOutputs + ",\n txType=" + txType + - ",\n burntFee=" + burntFee + + ",\n burntBsq=" + burntBsq + "\n} " + super.toString(); } @@ -103,13 +98,13 @@ public boolean equals(Object o) { String name = txType != null ? txType.name() : ""; String name1 = tempTx.txType != null ? tempTx.txType.name() : ""; boolean isTxTypeEquals = name.equals(name1); - return burntFee == tempTx.burntFee && + return burntBsq == tempTx.burntBsq && Objects.equals(tempTxOutputs, tempTx.tempTxOutputs) && isTxTypeEquals; } @Override public int hashCode() { - return Objects.hash(super.hashCode(), tempTxOutputs, txType, burntFee); + return Objects.hash(super.hashCode(), tempTxOutputs, txType, burntBsq); } } 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 5decbbbe4e4..b19fadfc781 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 @@ -142,7 +142,7 @@ private Optional findTx(RawTx rawTx) { long burntBsq = remainingInputValue + burntBondValue; boolean hasBurntBsq = burntBsq > 0; if (hasBurntBsq) - tempTx.setBurntFee(burntBsq); + tempTx.setBurntBsq(burntBsq); //**************************************************************************************** @@ -150,18 +150,23 @@ private Optional findTx(RawTx rawTx) { //**************************************************************************************** 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); + // We consider all BSQ inputs as burned if the tx is invalid. + tempTx.setBurntBsq(accumulatedInputValue); txOutputParser.invalidateUTXOCandidates(); - - if (hasBurntBsq) { - log.warn("We have destroyed BSQ because of an invalid tx. Burned BSQ={}. tx={}", - burntBsq / 100D, tempTx); - } + log.warn("We have destroyed BSQ because of an invalid tx. Burned BSQ={}. tx={}", accumulatedInputValue / 100D, tempTx); + } else if (txType == TxType.IRREGULAR) { + log.warn("We have an irregular tx {}", tempTx); + txOutputParser.commitUTXOCandidates(); } else { txOutputParser.commitUTXOCandidates(); } @@ -231,8 +236,8 @@ private void applyTxTypeAndTxOutputType(int blockHeight, TempTx tempTx, long bsq 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 +254,17 @@ private void processIssuance(int blockHeight, TempTx tempTx, long bsqFee) { "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 to 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 +274,11 @@ private void processIssuance(int blockHeight, TempTx tempTx, long bsqFee) { 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 +379,6 @@ static TxType evaluateTxType(TempTx tempTx, Optional optionalOpRet if (!isUnLockInputValid) return TxType.INVALID; - // UNLOCK tx has no fee, no OpReturn return TxType.UNLOCK; } @@ -393,8 +397,7 @@ static TxType evaluateTxTypeFromOpReturnType(TempTx tempTx, OpReturnType opRetur 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? + // Such a transaction cannot be created by the Bisq client and is considered invalid. return TxType.INVALID; } @@ -403,8 +406,7 @@ static TxType evaluateTxTypeFromOpReturnType(TempTx tempTx, OpReturnType opRetur 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? + // Such a transaction cannot be created by the Bisq client and is considered invalid. return TxType.INVALID; } @@ -423,8 +425,9 @@ static TxType evaluateTxTypeFromOpReturnType(TempTx tempTx, OpReturnType opRetur 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. We might need that in case we add new + // opReturn types in future. + return TxType.IRREGULAR; } } } diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateService.java b/core/src/main/java/bisq/core/dao/state/DaoStateService.java index 0050af09cd2..05c6f3d9018 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -355,6 +355,14 @@ public Optional getTx(String txId) { return getTxStream().filter(tx -> tx.getId().equals(txId)).findAny(); } + public List getInvalidTxs() { + return getTxStream().filter(tx -> tx.getTxType() == TxType.INVALID).collect(Collectors.toList()); + } + + public List getIrregularTxs() { + return getTxStream().filter(tx -> tx.getTxType() == TxType.IRREGULAR).collect(Collectors.toList()); + } + public boolean containsTx(String txId) { return getTx(txId).isPresent(); } @@ -382,9 +390,7 @@ public boolean hasTxBurntFee(String txId) { } public long getTotalBurntFee() { - return getTxStream() - .mapToLong(Tx::getBurntFee) - .sum(); + return getTxStream().mapToLong(Tx::getBurntFee).sum(); } public Set getBurntFeeTxs() { @@ -811,6 +817,15 @@ public long getTotalAmountOfConfiscatedTxOutputs() { .sum(); } + public long getTotalAmountOfInvalidatedBsq() { + return getTxStream().mapToLong(Tx::getInvalidatedBsq).sum(); + } + + // Contains burnt fee and invalidated bsq due invalid txs + public long getTotalAmountOfBurntBsq() { + return getTxStream().mapToLong(Tx::getBurntBsq).sum(); + } + // Confiscate bond public void confiscateBond(String lockupTxId) { Optional optionalTxOutput = getLockupTxOutput(lockupTxId); diff --git a/core/src/main/java/bisq/core/dao/state/GenesisTxInfo.java b/core/src/main/java/bisq/core/dao/state/GenesisTxInfo.java index e970b2e6a12..aced9faa3f1 100644 --- a/core/src/main/java/bisq/core/dao/state/GenesisTxInfo.java +++ b/core/src/main/java/bisq/core/dao/state/GenesisTxInfo.java @@ -46,7 +46,7 @@ public class GenesisTxInfo { private static final String MAINNET_GENESIS_TX_ID = "81855816eca165f17f0668898faa8724a105196e90ffc4993f4cac980176674e"; private static final int MAINNET_GENESIS_BLOCK_HEIGHT = 524717; // 2018-05-27 - private static final Coin MAINNET_GENESIS_TOTAL_SUPPLY = Coin.parseCoin("2.5"); // 2.5M BSQ / 2.50000000 BTC + private static final Coin MAINNET_GENESIS_TOTAL_SUPPLY = Coin.parseCoin("0.001"); private static final String TESTNET_GENESIS_TX_ID = "09e70ce0ab7a962a82a2ca84c9ae8a89140bf1c3fb6f7efad6162e39e4b362ae"; private static final int TESTNET_GENESIS_BLOCK_HEIGHT = 1446300; // 2018-12-02 @@ -60,6 +60,10 @@ public class GenesisTxInfo { private static final int DAO_BETANET_GENESIS_BLOCK_HEIGHT = 567405; // 2019-03-16 private static final Coin DAO_BETANET_GENESIS_TOTAL_SUPPLY = Coin.parseCoin("0.49998644"); // 499 986.44 BSQ / 0.49998644 BTC + private static final String DAO_REGTEST_GENESIS_TX_ID = "d594ad0c5de53e261b5784e5eb2acec8b807c45b74450401f488d36b8acf2e14"; + private static final int DAO_REGTEST_GENESIS_BLOCK_HEIGHT = 104; // 2019-03-26 + private static final Coin DAO_REGTEST_GENESIS_TOTAL_SUPPLY = Coin.parseCoin("2.5"); // 2.5M BSQ / 2.50000000 BTC + private static final String REGTEST_GENESIS_TX_ID = "30af0050040befd8af25068cc697e418e09c2d8ebd8d411d2240591b9ec203cf"; private static final int REGTEST_GENESIS_BLOCK_HEIGHT = 111; private static final Coin REGTEST_GENESIS_TOTAL_SUPPLY = Coin.parseCoin("2.5"); // 2.5M BSQ / 2.50000000 BTC @@ -108,6 +112,7 @@ public GenesisTxInfo(@Named(DaoOptionKeys.GENESIS_TX_ID) String genesisTxId, boolean isTestnet = baseCurrencyNetwork.isTestnet(); boolean isDaoTestNet = baseCurrencyNetwork.isDaoTestNet(); boolean isDaoBetaNet = baseCurrencyNetwork.isDaoBetaNet(); + boolean isDaoRegTest = baseCurrencyNetwork.isDaoRegTest(); boolean isRegtest = baseCurrencyNetwork.isRegtest(); if (!genesisTxId.isEmpty()) { this.genesisTxId = genesisTxId; @@ -119,6 +124,8 @@ public GenesisTxInfo(@Named(DaoOptionKeys.GENESIS_TX_ID) String genesisTxId, this.genesisTxId = DAO_TESTNET_GENESIS_TX_ID; } else if (isDaoBetaNet) { this.genesisTxId = DAO_BETANET_GENESIS_TX_ID; + } else if (isDaoRegTest) { + this.genesisTxId = DAO_REGTEST_GENESIS_TX_ID; } else if (isRegtest) { this.genesisTxId = REGTEST_GENESIS_TX_ID; } else { @@ -135,6 +142,8 @@ public GenesisTxInfo(@Named(DaoOptionKeys.GENESIS_TX_ID) String genesisTxId, this.genesisBlockHeight = DAO_TESTNET_GENESIS_BLOCK_HEIGHT; } else if (isDaoBetaNet) { this.genesisBlockHeight = DAO_BETANET_GENESIS_BLOCK_HEIGHT; + } else if (isDaoRegTest) { + this.genesisBlockHeight = DAO_REGTEST_GENESIS_BLOCK_HEIGHT; } else if (isRegtest) { this.genesisBlockHeight = REGTEST_GENESIS_BLOCK_HEIGHT; } else { @@ -151,6 +160,8 @@ public GenesisTxInfo(@Named(DaoOptionKeys.GENESIS_TX_ID) String genesisTxId, this.genesisTotalSupply = DAO_TESTNET_GENESIS_TOTAL_SUPPLY.value; } else if (isDaoBetaNet) { this.genesisTotalSupply = DAO_BETANET_GENESIS_TOTAL_SUPPLY.value; + } else if (isDaoRegTest) { + this.genesisTotalSupply = DAO_REGTEST_GENESIS_TOTAL_SUPPLY.value; } else if (isRegtest) { this.genesisTotalSupply = REGTEST_GENESIS_TOTAL_SUPPLY.value; } else { diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/Tx.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/Tx.java index 16365306113..63ca99189a5 100644 --- a/core/src/main/java/bisq/core/dao/state/model/blockchain/Tx.java +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/Tx.java @@ -57,13 +57,14 @@ public static Tx fromTempTx(TempTx tempTx) { tempTx.getTxInputs(), txOutputs, tempTx.getTxType(), - tempTx.getBurntFee()); + tempTx.getBurntBsq()); } private final ImmutableList txOutputs; @Nullable private final TxType txType; - private final long burntFee; + // Can be burned fee or in case of an invalid tx the burned BSQ from all BSQ inputs + private final long burntBsq; /////////////////////////////////////////////////////////////////////////////////////////// @@ -78,7 +79,7 @@ private Tx(String txVersion, ImmutableList txInputs, ImmutableList txOutputs, @Nullable TxType txType, - long burntFee) { + long burntBsq) { super(txVersion, id, blockHeight, @@ -87,7 +88,7 @@ private Tx(String txVersion, txInputs); this.txOutputs = txOutputs; this.txType = txType; - this.burntFee = burntFee; + this.burntBsq = burntBsq; } @@ -97,7 +98,7 @@ public PB.BaseTx toProtoMessage() { .addAllTxOutputs(txOutputs.stream() .map(TxOutput::toProtoMessage) .collect(Collectors.toList())) - .setBurntFee(burntFee); + .setBurntBsq(burntBsq); Optional.ofNullable(txType).ifPresent(txType -> builder.setTxType(txType.toProtoMessage())); return getBaseTxBuilder().setTx(builder).build(); } @@ -122,7 +123,7 @@ public static Tx fromProto(PB.BaseTx protoBaseTx) { txInputs, outputs, TxType.fromProto(protoTx.getTxType()), - protoTx.getBurntFee()); + protoTx.getBurntBsq()); } @@ -135,6 +136,18 @@ public TxOutput getLastTxOutput() { } + public long getBurntBsq() { + return burntBsq; + } + + public long getBurntFee() { + return txType == TxType.INVALID ? 0 : burntBsq; + } + + public long getInvalidatedBsq() { + return txType == TxType.INVALID ? burntBsq : 0; + } + public int getLockTime() { return getLockupOutput().getLockTime(); } @@ -158,7 +171,7 @@ public String toString() { return "Tx{" + "\n txOutputs=" + txOutputs + ",\n txType=" + txType + - ",\n burntFee=" + burntFee + + ",\n burntBsq=" + burntBsq + "\n} " + super.toString(); } @@ -175,13 +188,13 @@ public boolean equals(Object o) { String name1 = tx.txType != null ? tx.txType.name() : ""; boolean isTxTypeEquals = name.equals(name1); - return burntFee == tx.burntFee && + return burntBsq == tx.burntBsq && Objects.equals(txOutputs, tx.txOutputs) && isTxTypeEquals; } @Override public int hashCode() { - return Objects.hash(super.hashCode(), txOutputs, txType, burntFee); + return Objects.hash(super.hashCode(), txOutputs, txType, burntBsq); } } 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 8e5339db7fc..5083ae9690a 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(false, false); // 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/model/governance/Ballot.java b/core/src/main/java/bisq/core/dao/state/model/governance/Ballot.java index 2e862e00035..b3b48e7f5c3 100644 --- a/core/src/main/java/bisq/core/dao/state/model/governance/Ballot.java +++ b/core/src/main/java/bisq/core/dao/state/model/governance/Ballot.java @@ -64,8 +64,7 @@ public Ballot(Proposal proposal) { // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - public Ballot(Proposal proposal, - @Nullable Vote vote) { + public Ballot(Proposal proposal, @Nullable Vote vote) { this.proposal = proposal; this.vote = vote; } diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/ChangeParamProposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/ChangeParamProposal.java index 895dd14fc31..b78decf2cf5 100644 --- a/core/src/main/java/bisq/core/dao/state/model/governance/ChangeParamProposal.java +++ b/core/src/main/java/bisq/core/dao/state/model/governance/ChangeParamProposal.java @@ -55,7 +55,7 @@ public ChangeParamProposal(String name, paramValue, Version.PROPOSAL, new Date().getTime(), - "", + null, extraDataMap); } diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/CompensationProposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/CompensationProposal.java index 29e1812a652..45d749923f4 100644 --- a/core/src/main/java/bisq/core/dao/state/model/governance/CompensationProposal.java +++ b/core/src/main/java/bisq/core/dao/state/model/governance/CompensationProposal.java @@ -62,7 +62,7 @@ public CompensationProposal(String name, requestedBsq.value, Version.COMPENSATION_REQUEST, new Date().getTime(), - "", + null, extraDataMap); } diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/ConfiscateBondProposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/ConfiscateBondProposal.java index 6d578b685da..38eb9ef320d 100644 --- a/core/src/main/java/bisq/core/dao/state/model/governance/ConfiscateBondProposal.java +++ b/core/src/main/java/bisq/core/dao/state/model/governance/ConfiscateBondProposal.java @@ -53,7 +53,7 @@ public ConfiscateBondProposal(String name, lockupTxId, Version.PROPOSAL, new Date().getTime(), - "", + null, extraDataMap); } diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/GenericProposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/GenericProposal.java index 3963c539b48..92002f559f5 100644 --- a/core/src/main/java/bisq/core/dao/state/model/governance/GenericProposal.java +++ b/core/src/main/java/bisq/core/dao/state/model/governance/GenericProposal.java @@ -50,7 +50,7 @@ public GenericProposal(String name, link, Version.PROPOSAL, new Date().getTime(), - "", + null, extraDataMap); } diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/Proposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/Proposal.java index 7b2081f45db..395b29c9597 100644 --- a/core/src/main/java/bisq/core/dao/state/model/governance/Proposal.java +++ b/core/src/main/java/bisq/core/dao/state/model/governance/Proposal.java @@ -26,6 +26,7 @@ import bisq.common.proto.ProtobufferRuntimeException; import bisq.common.proto.network.NetworkPayload; import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.ExtraDataMapValidator; import io.bisq.generated.protobuffer.PB; @@ -64,13 +65,13 @@ protected Proposal(String name, byte version, long creationDate, @Nullable String txId, - @SuppressWarnings("NullableProblems") Map extraDataMap) { + @Nullable Map extraDataMap) { this.name = name; this.link = link; this.version = version; this.creationDate = creationDate; this.txId = txId; - this.extraDataMap = extraDataMap; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); } @@ -143,7 +144,7 @@ public Date getCreationDateAsDate() { @Override public String toString() { return "Proposal{" + - "\n uid='" + txId + '\'' + + "\n txId='" + txId + '\'' + ",\n name='" + name + '\'' + ",\n link='" + link + '\'' + ",\n txId='" + txId + '\'' + diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/ReimbursementProposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/ReimbursementProposal.java index b5b0bb91a4f..40146666101 100644 --- a/core/src/main/java/bisq/core/dao/state/model/governance/ReimbursementProposal.java +++ b/core/src/main/java/bisq/core/dao/state/model/governance/ReimbursementProposal.java @@ -62,7 +62,7 @@ public ReimbursementProposal(String name, requestedBsq.value, Version.REIMBURSEMENT_REQUEST, new Date().getTime(), - "", + null, extraDataMap); } diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/RemoveAssetProposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/RemoveAssetProposal.java index 0b4ebe14b11..f4452e10163 100644 --- a/core/src/main/java/bisq/core/dao/state/model/governance/RemoveAssetProposal.java +++ b/core/src/main/java/bisq/core/dao/state/model/governance/RemoveAssetProposal.java @@ -53,7 +53,7 @@ public RemoveAssetProposal(String name, tickerSymbol, Version.PROPOSAL, new Date().getTime(), - "", + null, extraDataMap); } diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/RoleProposal.java b/core/src/main/java/bisq/core/dao/state/model/governance/RoleProposal.java index bc783bec78e..c4caa7adb68 100644 --- a/core/src/main/java/bisq/core/dao/state/model/governance/RoleProposal.java +++ b/core/src/main/java/bisq/core/dao/state/model/governance/RoleProposal.java @@ -54,7 +54,7 @@ public RoleProposal(Role role, Map extraDataMap) { role.getBondedRoleType().getUnlockTimeInBlocks(), Version.PROPOSAL, new Date().getTime(), - "", + null, extraDataMap); } 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 e213b60586a..03d6d8961b6 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 void onCommitTx(Transaction tx, TxType txType, Wallet wallet) { case PROOF_OF_BURN: changeOutputIndex = 0; break; + case IRREGULAR: + return; default: return; } diff --git a/core/src/main/java/bisq/core/filter/Filter.java b/core/src/main/java/bisq/core/filter/Filter.java index c7b3db9a898..b2fc9f92020 100644 --- a/core/src/main/java/bisq/core/filter/Filter.java +++ b/core/src/main/java/bisq/core/filter/Filter.java @@ -21,6 +21,7 @@ import bisq.network.p2p.storage.payload.ProtectedStoragePayload; import bisq.common.crypto.Sig; +import bisq.common.util.ExtraDataMapValidator; import io.bisq.generated.protobuffer.PB; @@ -146,7 +147,7 @@ public Filter(List bannedOfferIds, disableDao); this.signatureAsBase64 = signatureAsBase64; this.ownerPubKeyBytes = ownerPubKeyBytes; - this.extraDataMap = extraDataMap; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyBytes); } diff --git a/core/src/main/java/bisq/core/network/p2p/seed/DefaultSeedNodeRepository.java b/core/src/main/java/bisq/core/network/p2p/seed/DefaultSeedNodeRepository.java index aa385dab463..6b3a021789f 100644 --- a/core/src/main/java/bisq/core/network/p2p/seed/DefaultSeedNodeRepository.java +++ b/core/src/main/java/bisq/core/network/p2p/seed/DefaultSeedNodeRepository.java @@ -37,14 +37,19 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; + import javax.annotation.Nullable; +// If a new BaseCurrencyNetwork type gets added we need to add the resource file for it as well! +@Slf4j public class DefaultSeedNodeRepository implements SeedNodeRepository { //TODO add support for localhost addresses private static final Pattern pattern = Pattern.compile("^([a-z0-9]+\\.onion:\\d+)"); private static final String ENDING = ".seednodes"; private static final Collection cache = new HashSet<>(); private final BisqEnvironment bisqEnvironment; + @Nullable private final String seedNodes; @Inject @@ -55,35 +60,42 @@ public DefaultSeedNodeRepository(BisqEnvironment environment, } private void reload() { + try { + // see if there are any seed nodes configured manually + if (seedNodes != null && !seedNodes.isEmpty()) { + cache.clear(); + Arrays.stream(seedNodes.split(",")).forEach(s -> cache.add(new NodeAddress(s))); - // see if there are any seed nodes configured manually - if (seedNodes != null && !seedNodes.isEmpty()) { - cache.clear(); - Arrays.stream(seedNodes.split(",")).forEach(s -> cache.add(new NodeAddress(s))); - - return; - } - - // else, we fetch the seed nodes from our resources - final InputStream fileInputStream = DefaultSeedNodeRepository.class.getClassLoader().getResourceAsStream(BisqEnvironment.getBaseCurrencyNetwork().name().toLowerCase() + ENDING); - final BufferedReader seedNodeFile = new BufferedReader(new InputStreamReader(fileInputStream)); - - // only clear if we have a fresh data source (otherwise, an exception would prevent us from getting here) - cache.clear(); + return; + } - // refill the cache - seedNodeFile.lines().forEach(line -> { - final Matcher matcher = pattern.matcher(line); - if (matcher.find()) - cache.add(new NodeAddress(matcher.group(1))); + // else, we fetch the seed nodes from our resources + InputStream fileInputStream = DefaultSeedNodeRepository.class.getClassLoader().getResourceAsStream(BisqEnvironment.getBaseCurrencyNetwork().name().toLowerCase() + ENDING); + BufferedReader seedNodeFile = new BufferedReader(new InputStreamReader(fileInputStream)); - // Maybe better include in regex... - if (line.startsWith("localhost")) - cache.add(new NodeAddress(line)); - }); + // only clear if we have a fresh data source (otherwise, an exception would prevent us from getting here) + cache.clear(); - // filter - cache.removeAll(bisqEnvironment.getBannedSeedNodes().stream().map(NodeAddress::new).collect(Collectors.toSet())); + // refill the cache + seedNodeFile.lines().forEach(line -> { + Matcher matcher = pattern.matcher(line); + if (matcher.find()) + cache.add(new NodeAddress(matcher.group(1))); + + // Maybe better include in regex... + if (line.startsWith("localhost")) + cache.add(new NodeAddress(line)); + }); + + // filter + cache.removeAll(bisqEnvironment.getBannedSeedNodes().stream().map(NodeAddress::new).collect(Collectors.toSet())); + + log.info("Seed nodes: {}", cache); + } catch (Throwable t) { + log.error(t.toString()); + t.printStackTrace(); + throw t; + } } public Collection getSeedNodeAddresses() { diff --git a/core/src/main/java/bisq/core/offer/OfferPayload.java b/core/src/main/java/bisq/core/offer/OfferPayload.java index eb7e37cb030..822f62ce225 100644 --- a/core/src/main/java/bisq/core/offer/OfferPayload.java +++ b/core/src/main/java/bisq/core/offer/OfferPayload.java @@ -24,6 +24,7 @@ import bisq.common.crypto.PubKeyRing; import bisq.common.proto.ProtoUtil; +import bisq.common.util.ExtraDataMapValidator; import bisq.common.util.JsonExclude; import io.bisq.generated.protobuffer.PB; @@ -235,7 +236,7 @@ public OfferPayload(String id, this.upperClosePrice = upperClosePrice; this.isPrivateOffer = isPrivateOffer; this.hashOfChallenge = hashOfChallenge; - this.extraDataMap = extraDataMap; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); this.protocolVersion = protocolVersion; } diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics.java index f68b99838ae..47608fed711 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics.java @@ -29,6 +29,7 @@ import bisq.common.crypto.Sig; import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.ExtraDataMapValidator; import bisq.common.util.JsonExclude; import io.bisq.generated.protobuffer.PB; @@ -147,7 +148,7 @@ public TradeStatistics(OfferPayload offerPayload, this.tradeDate = tradeDate; this.depositTxId = depositTxId; this.signaturePubKeyBytes = signaturePubKeyBytes; - this.extraDataMap = extraDataMap; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); signaturePubKey = Sig.getPublicKeyFromBytes(signaturePubKeyBytes); } diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java index 443b489fe67..ecd3b529371 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java @@ -32,6 +32,7 @@ import bisq.common.app.Capability; import bisq.common.crypto.Hash; import bisq.common.proto.persistable.PersistableEnvelope; +import bisq.common.util.ExtraDataMapValidator; import bisq.common.util.JsonExclude; import bisq.common.util.Utilities; @@ -150,7 +151,7 @@ public TradeStatistics2(OfferPayload.Direction direction, this.tradeAmount = tradeAmount; this.tradeDate = tradeDate; this.depositTxId = depositTxId; - this.extraDataMap = extraDataMap; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); if (hash == null) // We create hash from all fields excluding hash itself. We use json as simple data serialisation. diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index 754e6b75a0e..d28bc95e8a7 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -655,6 +655,8 @@ public BlockChainExplorer getBlockChainExplorer() { return BTC_DAO_TEST_NET_EXPLORERS.get(0); case BTC_DAO_BETANET: return prefPayload.getBlockChainExplorerMainNet(); + case BTC_DAO_REGTEST: + return BTC_DAO_TEST_NET_EXPLORERS.get(0); default: throw new RuntimeException("BaseCurrencyNetwork not defined. BaseCurrencyNetwork=" + baseCurrencyNetwork); } @@ -672,6 +674,8 @@ public ArrayList getBlockChainExplorers() { return BTC_DAO_TEST_NET_EXPLORERS; case BTC_DAO_BETANET: return BTC_MAIN_NET_EXPLORERS; + case BTC_DAO_REGTEST: + return BTC_DAO_TEST_NET_EXPLORERS; default: throw new RuntimeException("BaseCurrencyNetwork not defined. BaseCurrencyNetwork=" + baseCurrencyNetwork); } diff --git a/core/src/main/resources/btc_dao_regtest.seednodes b/core/src/main/resources/btc_dao_regtest.seednodes new file mode 100644 index 00000000000..4c6367383f9 --- /dev/null +++ b/core/src/main/resources/btc_dao_regtest.seednodes @@ -0,0 +1,6 @@ +# nodeaddress.onion:port [(@owner)] +2bnvhfkdrnx5hrlv.onion:8005 +b3jnw7fyph2jsu6n.onion:8005 + +# omentgpxrxy5lehq.onion:8005 +# r7cucuwouvhdhdgo.onion:8005 diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 0af3223e2f4..cae8d4511f2 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1422,6 +1422,7 @@ dao.results.cycle.duration.value={0} block(s) dao.results.cycle.value.postFix.isDefaultValue=(default value) dao.results.cycle.value.postFix.hasChanged=(has been changed in voting) +dao.results.invalidVotes.proposal=Proposal dao.results.invalidVotes=We had invalid votes in that voting cycle. That can happen if a vote was \ not distributed well in the P2P network.\n{0} @@ -1823,6 +1824,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\ @@ -1893,6 +1896,10 @@ dao.monitor.daoState.table.blockHeight=Block height dao.monitor.daoState.table.hash=Hash of DAO state dao.monitor.daoState.table.prev=Previous hash dao.monitor.daoState.conflictTable.headline=DAO state hashes from peers in conflict +dao.monitor.daoState.utxoConflicts=UTXO conflicts +dao.monitor.daoState.utxoConflicts.blockHeight=Block height: {0} +dao.monitor.daoState.utxoConflicts.sumUtxo=Sum of all UTXO: {0} BSQ +dao.monitor.daoState.utxoConflicts.sumBsq=Sum of all BSQ: {0} BSQ dao.monitor.proposal.headline=Proposals state dao.monitor.proposal.daoStateInSync=Your local proposals state is in consensus with the network @@ -1936,6 +1943,7 @@ dao.factsAndFigures.supply.totalLockedUpAmount=Locked up in bonds dao.factsAndFigures.supply.totalUnlockingAmount=Unlocking BSQ from bonds dao.factsAndFigures.supply.totalUnlockedAmount=Unlocked BSQ from bonds dao.factsAndFigures.supply.totalConfiscatedAmount=Confiscated BSQ from bonds +dao.factsAndFigures.supply.invalidTxs=Burned BSQ (invalid transactions) dao.factsAndFigures.supply.burntAmount=Burned BSQ (fees) dao.factsAndFigures.transactions.genesis=Genesis transaction @@ -1947,6 +1955,8 @@ dao.factsAndFigures.transactions.utxo=No. of all unspent transaction outputs dao.factsAndFigures.transactions.compensationIssuanceTx=No. of all compensation request issuance transactions dao.factsAndFigures.transactions.reimbursementIssuanceTx=No. of all reimbursement request issuance transactions dao.factsAndFigures.transactions.burntTx=No. of all fee payments transactions +dao.factsAndFigures.transactions.invalidTx=No. of all invalid transactions +dao.factsAndFigures.transactions.irregularTx=No. of all irregular transactions #################################################################### # Windows @@ -2412,9 +2422,11 @@ BTC_TESTNET=Bitcoin Testnet # suppress inspection "UnusedProperty" BTC_REGTEST=Bitcoin Regtest # suppress inspection "UnusedProperty" -BTC_DAO_TESTNET=Bitcoin DAO Testnet +BTC_DAO_TESTNET=Bitcoin DAO Testnet (deprecated) # suppress inspection "UnusedProperty" BTC_DAO_BETANET=Bitcoin DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Testnet time.year=Year time.month=Month diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 0312a6d0d3d..6dc69f3d26c 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -2104,9 +2104,11 @@ BTC_TESTNET=Bitcoin-Testnetzwerk # suppress inspection "UnusedProperty" BTC_REGTEST=Bitcoin-Regtest # suppress inspection "UnusedProperty" -BTC_DAO_TESTNET=Bitcoin-DAO-Testnetzwerk +BTC_DAO_TESTNET=Bitcoin-DAO-Testnetzwerk (deprecated) # suppress inspection "UnusedProperty" BTC_DAO_BETANET=Bitcoin DAO Betanetz (Bitcoin Hauptnetz) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin-DAO-Testnetzwerk time.year=Jahr time.month=Monat diff --git a/core/src/main/resources/i18n/displayStrings_el.properties b/core/src/main/resources/i18n/displayStrings_el.properties index 557cd909b61..3bbb8678afc 100644 --- a/core/src/main/resources/i18n/displayStrings_el.properties +++ b/core/src/main/resources/i18n/displayStrings_el.properties @@ -2104,9 +2104,11 @@ BTC_TESTNET=Bitcoin Testnet # suppress inspection "UnusedProperty" BTC_REGTEST=Bitcoin Regtest # suppress inspection "UnusedProperty" -BTC_DAO_TESTNET=Bitcoin DAO Testnet +BTC_DAO_TESTNET=Bitcoin DAO Testnet (deprecated) # suppress inspection "UnusedProperty" BTC_DAO_BETANET=Bitcoin DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Testnet time.year=Έτος time.month=Μήνας diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 22301a0f1a3..473f941b593 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -2104,9 +2104,11 @@ BTC_TESTNET=Red de prueba de Bitcoin # suppress inspection "UnusedProperty" BTC_REGTEST=Regtest Bitcoin # suppress inspection "UnusedProperty" -BTC_DAO_TESTNET=Red de prueba de Bitcoin +BTC_DAO_TESTNET=Bitcoin DAO Testnet (deprecated) # suppress inspection "UnusedProperty" BTC_DAO_BETANET=Bitcoin DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Testnet time.year=Año time.month=Mes diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 1854bc075b4..8603086c340 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -2104,9 +2104,11 @@ BTC_TESTNET=Bitcoin Testnet # suppress inspection "UnusedProperty" BTC_REGTEST=Bitcoin Regtest # suppress inspection "UnusedProperty" -BTC_DAO_TESTNET=Bitcoin DAO Testnet +BTC_DAO_TESTNET=Bitcoin DAO Testnet (deprecated) # suppress inspection "UnusedProperty" BTC_DAO_BETANET=Bitcoin DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Testnet time.year=سال time.month=ماه diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 36c7963f2a1..9118ef9179a 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -2104,9 +2104,11 @@ BTC_TESTNET=Bitcoin Testnet # suppress inspection "UnusedProperty" BTC_REGTEST=Bitcoin Regtest # suppress inspection "UnusedProperty" -BTC_DAO_TESTNET=Bitcoin DAO Testnet +BTC_DAO_TESTNET=Bitcoin DAO Testnet (deprecated) # suppress inspection "UnusedProperty" BTC_DAO_BETANET=Bitcoin DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Testnet time.year=Année time.month=Mois diff --git a/core/src/main/resources/i18n/displayStrings_hu.properties b/core/src/main/resources/i18n/displayStrings_hu.properties index f81188c94e9..1594ab18264 100644 --- a/core/src/main/resources/i18n/displayStrings_hu.properties +++ b/core/src/main/resources/i18n/displayStrings_hu.properties @@ -2104,9 +2104,11 @@ BTC_TESTNET=Bitcoin Testnet # suppress inspection "UnusedProperty" BTC_REGTEST=Bitcoin Regtest # suppress inspection "UnusedProperty" -BTC_DAO_TESTNET=Bitcoin DAO Testnet +BTC_DAO_TESTNET=Bitcoin DAO Testnet (deprecated) # suppress inspection "UnusedProperty" BTC_DAO_BETANET=Bitcoin DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Testnet time.year=Év time.month=Hónap diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 5a17ab10479..44c3028e959 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -2104,9 +2104,11 @@ BTC_TESTNET=Testnet do Bitcoin # suppress inspection "UnusedProperty" BTC_REGTEST=Regtest do Bitcoin # suppress inspection "UnusedProperty" -BTC_DAO_TESTNET=Bitcoin DAO Testnet +BTC_DAO_TESTNET=Bitcoin DAO Testnet (deprecated) # suppress inspection "UnusedProperty" BTC_DAO_BETANET=Bitcoin DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Testnet time.year=Ano time.month=Mês diff --git a/core/src/main/resources/i18n/displayStrings_ro.properties b/core/src/main/resources/i18n/displayStrings_ro.properties index 931ce06f3f8..6debf3f89f5 100644 --- a/core/src/main/resources/i18n/displayStrings_ro.properties +++ b/core/src/main/resources/i18n/displayStrings_ro.properties @@ -2104,9 +2104,11 @@ BTC_TESTNET=Bitcoin Testnet # suppress inspection "UnusedProperty" BTC_REGTEST=Bitcoin Regtest # suppress inspection "UnusedProperty" -BTC_DAO_TESTNET=Bitcoin DAO Testnet +BTC_DAO_TESTNET=Bitcoin DAO Testnet (deprecated) # suppress inspection "UnusedProperty" BTC_DAO_BETANET=Bitcoin DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Testnet time.year=An time.month=Lună diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index d57126f6a7e..aeb9c659bcc 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -2107,6 +2107,8 @@ BTC_REGTEST=Биткойн режим регрессионного тестир BTC_DAO_TESTNET=Биткойн DAO Testnet # suppress inspection "UnusedProperty" BTC_DAO_BETANET=Бета-сеть ДАО Биткойн (основная сеть Биткойн) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Биткойн DAO Testnet time.year=Год time.month=Месяц diff --git a/core/src/main/resources/i18n/displayStrings_sr.properties b/core/src/main/resources/i18n/displayStrings_sr.properties index 3a925fdc160..79f21a259ae 100644 --- a/core/src/main/resources/i18n/displayStrings_sr.properties +++ b/core/src/main/resources/i18n/displayStrings_sr.properties @@ -2104,9 +2104,11 @@ BTC_TESTNET=Bitkoin Testnet # suppress inspection "UnusedProperty" BTC_REGTEST=Bitkoin Regtest # suppress inspection "UnusedProperty" -BTC_DAO_TESTNET=Bitcoin DAO Testnet +BTC_DAO_TESTNET=Bitcoin DAO Testnet (deprecated) # suppress inspection "UnusedProperty" BTC_DAO_BETANET=Bitcoin DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Testnet time.year=Godina time.month=Mesec diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index b9d0edb93cd..951c62c6541 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -2104,9 +2104,11 @@ BTC_TESTNET=Bitcoin Testnet # suppress inspection "UnusedProperty" BTC_REGTEST=Bitcoin Regtest # suppress inspection "UnusedProperty" -BTC_DAO_TESTNET=Bitcoin DAO Testnet +BTC_DAO_TESTNET=Bitcoin DAO Testnet (deprecated) # suppress inspection "UnusedProperty" BTC_DAO_BETANET=Bitcoin DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Testnet time.year=ปี time.month=เดือน diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 5212ca166dd..4ebbc00fb1a 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -2104,9 +2104,11 @@ BTC_TESTNET=Bitcoin Testnet # suppress inspection "UnusedProperty" BTC_REGTEST=Bitcoin Regtest # suppress inspection "UnusedProperty" -BTC_DAO_TESTNET=Bitcoin DAO Testnet +BTC_DAO_TESTNET=Bitcoin DAO Testnet (deprecated) # suppress inspection "UnusedProperty" BTC_DAO_BETANET=Bitcoin DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Testnet time.year=Năm time.month=Tháng diff --git a/core/src/main/resources/i18n/displayStrings_zh.properties b/core/src/main/resources/i18n/displayStrings_zh.properties index 16898e0d645..aada127d2f7 100644 --- a/core/src/main/resources/i18n/displayStrings_zh.properties +++ b/core/src/main/resources/i18n/displayStrings_zh.properties @@ -2104,9 +2104,11 @@ BTC_TESTNET=比特币测试网络 # suppress inspection "UnusedProperty" BTC_REGTEST=比特币回归测试 # suppress inspection "UnusedProperty" -BTC_DAO_TESTNET=Bitcoin DAO Testnet +BTC_DAO_TESTNET=Bitcoin DAO Testnet (deprecated) # suppress inspection "UnusedProperty" BTC_DAO_BETANET=Bitcoin DAO Betanet (Bitcoin Mainnet) +# suppress inspection "UnusedProperty" +BTC_DAO_REGTEST=Bitcoin DAO Testnet time.year=年线 time.month=月线 diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java index 65986ee0291..0de2e7e6252 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/dashboard/BsqDashboardView.java @@ -291,13 +291,14 @@ private void updateWithBsqBlockChainData() { Coin issuedAmountFromGenesis = daoFacade.getGenesisTotalSupply(); Coin issuedAmountFromCompRequests = Coin.valueOf(daoFacade.getTotalIssuedAmount(IssuanceType.COMPENSATION)); Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoFacade.getTotalIssuedAmount(IssuanceType.REIMBURSEMENT)); - Coin burntFee = Coin.valueOf(daoFacade.getTotalBurntFee()); Coin totalConfiscatedAmount = Coin.valueOf(daoFacade.getTotalAmountOfConfiscatedTxOutputs()); + // Contains burnt fee and invalidated bsq due invalid txs + Coin totalAmountOfBurntBsq = Coin.valueOf(daoFacade.getTotalAmountOfBurntBsq()); availableAmount = issuedAmountFromGenesis .add(issuedAmountFromCompRequests) .add(issuedAmountFromReimbursementRequests) - .subtract(burntFee) + .subtract(totalAmountOfBurntBsq) .subtract(totalConfiscatedAmount); availableAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(availableAmount)); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java index 1d28e15ab0a..43b0b77b033 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/SupplyView.java @@ -64,8 +64,10 @@ import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -87,8 +89,8 @@ public class SupplyView extends ActivatableView implements DaoSt private int gridRow = 0; private TextField genesisIssueAmountTextField, compRequestIssueAmountTextField, reimbursementAmountTextField, - burntAmountTextField, totalLockedUpAmountTextField, totalUnlockingAmountTextField, - totalUnlockedAmountTextField, totalConfiscatedAmountTextField; + totalBurntFeeAmountTextField, totalLockedUpAmountTextField, totalUnlockingAmountTextField, + totalUnlockedAmountTextField, totalConfiscatedAmountTextField, totalAmountOfInvalidatedBsqTextField; private XYChart.Series seriesBSQIssued, seriesBSQBurnt; private static final Map ADJUSTERS = new HashMap<>(); @@ -165,11 +167,10 @@ private void createSupplyIncreasedInformation() { private void createSupplyReducedInformation() { addTitledGroupBg(root, ++gridRow, 2, Res.get("dao.factsAndFigures.supply.burnt"), Layout.GROUP_DISTANCE); - Tuple3 burntAmountTuple = addTopLabelReadOnlyTextField(root, gridRow, - Res.get("dao.factsAndFigures.supply.burntAmount"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); - burntAmountTextField = burntAmountTuple.second; - - GridPane.setColumnSpan(burntAmountTuple.third, 2); + totalBurntFeeAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, + Res.get("dao.factsAndFigures.supply.burntAmount"), Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + totalAmountOfInvalidatedBsqTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, + Res.get("dao.factsAndFigures.supply.invalidTxs"), Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; seriesBSQBurnt = new XYChart.Series<>(); createChart(seriesBSQBurnt, Res.get("dao.factsAndFigures.supply.burnt")); @@ -190,7 +191,6 @@ private void createSupplyLockedInformation() { Res.get("dao.factsAndFigures.supply.totalUnlockedAmount")).second; totalConfiscatedAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1, Res.get("dao.factsAndFigures.supply.totalConfiscatedAmount")).second; - } private void createChart(XYChart.Series series, String seriesLabel) { @@ -271,17 +271,20 @@ private void updateWithBsqBlockChainData() { Coin issuedAmountFromReimbursementRequests = Coin.valueOf(daoFacade.getTotalIssuedAmount(IssuanceType.REIMBURSEMENT)); reimbursementAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(issuedAmountFromReimbursementRequests)); - Coin burntFee = Coin.valueOf(daoFacade.getTotalBurntFee()); + Coin totalBurntFee = Coin.valueOf(daoFacade.getTotalBurntFee()); Coin totalLockedUpAmount = Coin.valueOf(daoFacade.getTotalLockupAmount()); Coin totalUnlockingAmount = Coin.valueOf(daoFacade.getTotalAmountOfUnLockingTxOutputs()); Coin totalUnlockedAmount = Coin.valueOf(daoFacade.getTotalAmountOfUnLockedTxOutputs()); Coin totalConfiscatedAmount = Coin.valueOf(daoFacade.getTotalAmountOfConfiscatedTxOutputs()); + Coin totalAmountOfInvalidatedBsq = Coin.valueOf(daoFacade.getTotalAmountOfInvalidatedBsq()); - burntAmountTextField.setText("-" + bsqFormatter.formatAmountWithGroupSeparatorAndCode(burntFee)); + totalBurntFeeAmountTextField.setText("-" + bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalBurntFee)); totalLockedUpAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalLockedUpAmount)); totalUnlockingAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalUnlockingAmount)); totalUnlockedAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalUnlockedAmount)); totalConfiscatedAmountTextField.setText(bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalConfiscatedAmount)); + String minusSign = totalAmountOfInvalidatedBsq.isPositive() ? "-" : ""; + totalAmountOfInvalidatedBsqTextField.setText(minusSign + bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalAmountOfInvalidatedBsq)); updateCharts(); } @@ -290,31 +293,34 @@ private void updateCharts() { seriesBSQIssued.getData().clear(); seriesBSQBurnt.getData().clear(); - Map> feesBurntByMonth = daoStateService.getBurntFeeTxs().stream() + Set burntTxs = new HashSet<>(daoStateService.getBurntFeeTxs()); + burntTxs.addAll(daoStateService.getInvalidTxs()); + + Map> burntBsqByMonth = burntTxs.stream() .sorted(Comparator.comparing(Tx::getTime)) .collect(Collectors.groupingBy(item -> new Date(item.getTime()).toLocalDate() .with(ADJUSTERS.get(MONTH)))); - List> updatedBurntBSQ = feesBurntByMonth.keySet().stream() + List> updatedBurntBsq = burntBsqByMonth.keySet().stream() .map(date -> { ZonedDateTime zonedDateTime = date.atStartOfDay(ZoneId.systemDefault()); - return new XYChart.Data(zonedDateTime.toInstant().getEpochSecond(), feesBurntByMonth.get(date) + return new XYChart.Data(zonedDateTime.toInstant().getEpochSecond(), burntBsqByMonth.get(date) .stream() - .mapToDouble(Tx::getBurntFee) + .mapToDouble(Tx::getBurntBsq) .sum() ); }) .collect(Collectors.toList()); - seriesBSQBurnt.getData().setAll(updatedBurntBSQ); + seriesBSQBurnt.getData().setAll(updatedBurntBsq); Stream bsqByCompensation = daoStateService.getIssuanceSet(IssuanceType.COMPENSATION).stream() .sorted(Comparator.comparing(Issuance::getChainHeight)); - Stream bsqByReImbursement = daoStateService.getIssuanceSet(IssuanceType.REIMBURSEMENT).stream() + Stream bsqByReimbursement = daoStateService.getIssuanceSet(IssuanceType.REIMBURSEMENT).stream() .sorted(Comparator.comparing(Issuance::getChainHeight)); - Map> bsqAddedByVote = Stream.concat(bsqByCompensation, bsqByReImbursement) + Map> bsqAddedByVote = Stream.concat(bsqByCompensation, bsqByReimbursement) .collect(Collectors.groupingBy(item -> new Date(daoFacade.getBlockTime(item.getChainHeight())).toLocalDate() .with(ADJUSTERS.get(MONTH)))); @@ -329,7 +335,6 @@ private void updateCharts() { .collect(Collectors.toList()); seriesBSQIssued.getData().setAll(updatedAddedBSQ); - } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/transactions/BSQTransactionsView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/transactions/BSQTransactionsView.java index 3e4e6a49e78..95effae2877 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/transactions/BSQTransactionsView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/transactions/BSQTransactionsView.java @@ -51,9 +51,9 @@ public class BSQTransactionsView extends ActivatableView impleme private final Preferences preferences; private int gridRow = 0; - private TextField allTxTextField, burntTxTextField, + private TextField allTxTextField, burntFeeTxsTextField, utxoTextField, compensationIssuanceTxTextField, - reimbursementIssuanceTxTextField; + reimbursementIssuanceTxTextField, invalidTxsTextField, irregularTxsTextField; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -95,19 +95,23 @@ public void initialize() { utxoTextField = addTopLabelReadOnlyTextField(root, ++gridRow, Res.get("dao.factsAndFigures.transactions.utxo")).second; compensationIssuanceTxTextField = addTopLabelReadOnlyTextField(root, ++gridRow, Res.get("dao.factsAndFigures.transactions.compensationIssuanceTx")).second; + reimbursementIssuanceTxTextField = addTopLabelReadOnlyTextField(root, ++gridRow, + Res.get("dao.factsAndFigures.transactions.reimbursementIssuanceTx")).second; int columnIndex = 1; - gridRow = startRow; titledGroupBg = addTitledGroupBg(root, startRow, columnIndex, 3, "", Layout.GROUP_DISTANCE); titledGroupBg.getStyleClass().add("last"); - reimbursementIssuanceTxTextField = addTopLabelReadOnlyTextField(root, gridRow, columnIndex, - Res.get("dao.factsAndFigures.transactions.reimbursementIssuanceTx"), + burntFeeTxsTextField = addTopLabelReadOnlyTextField(root, gridRow, columnIndex, + Res.get("dao.factsAndFigures.transactions.burntTx"), Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; - burntTxTextField = addTopLabelReadOnlyTextField(root, ++gridRow, columnIndex, - Res.get("dao.factsAndFigures.transactions.burntTx")).second; + invalidTxsTextField = addTopLabelReadOnlyTextField(root, ++gridRow, columnIndex, + Res.get("dao.factsAndFigures.transactions.invalidTx")).second; + irregularTxsTextField = addTopLabelReadOnlyTextField(root, ++gridRow, columnIndex, + Res.get("dao.factsAndFigures.transactions.irregularTx")).second; + gridRow++; } @@ -143,7 +147,9 @@ private void updateWithBsqBlockChainData() { utxoTextField.setText(String.valueOf(daoFacade.getUnspentTxOutputs().size())); compensationIssuanceTxTextField.setText(String.valueOf(daoFacade.getNumIssuanceTransactions(IssuanceType.COMPENSATION))); reimbursementIssuanceTxTextField.setText(String.valueOf(daoFacade.getNumIssuanceTransactions(IssuanceType.REIMBURSEMENT))); - burntTxTextField.setText(String.valueOf(daoFacade.getFeeTxs().size())); + burntFeeTxsTextField.setText(String.valueOf(daoFacade.getBurntFeeTxs().size())); + invalidTxsTextField.setText(String.valueOf(daoFacade.getInvalidTxs().size())); + irregularTxsTextField.setText(String.valueOf(daoFacade.getIrregularTxs().size())); } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java index 9b49c0155c3..da3d883c7a9 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java @@ -144,7 +144,7 @@ public class ProposalDisplay { private VBox linkWithIconContainer, comboBoxValueContainer, myVoteBox, voteResultBox; private int votingBoxRowSpan; - private Optional navigateHandlerOptional; + private Optional navigateHandlerOptional = Optional.empty(); public ProposalDisplay(GridPane gridPane, BsqFormatter bsqFormatter, @@ -355,13 +355,12 @@ public BondedRoleType fromString(String string) { confiscateBondComboBox.setConverter(new StringConverter<>() { @Override public String toString(Bond bond) { - String bondDetails; + String details = " (" + Res.get("dao.bond.table.column.lockupTxId") + ": " + bond.getLockupTxId() + ")"; if (bond instanceof BondedRole) { - bondDetails = bond.getBondedAsset().getDisplayString(); + return bond.getBondedAsset().getDisplayString() + details; } else { - bondDetails = Res.get("dao.bond.bondedReputation"); + return Res.get("dao.bond.bondedReputation") + details; } - return bondDetails + " (" + Res.get("shared.id") + ": " + bond.getBondedAsset().getUid() + ")"; } @Override @@ -648,7 +647,7 @@ public void removeAllFields() { } public void onNavigate(Runnable navigateHandler) { - this.navigateHandlerOptional = Optional.of(navigateHandler); + navigateHandlerOptional = Optional.of(navigateHandler); } public int incrementAndGetGridRow() { diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java index 1a5190642dc..7a7da8268fb 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java @@ -40,6 +40,7 @@ import bisq.core.btc.wallet.BsqWalletService; import bisq.core.dao.DaoFacade; import bisq.core.dao.governance.blindvote.BlindVoteConsensus; +import bisq.core.dao.governance.blindvote.MyBlindVoteListService; import bisq.core.dao.governance.myvote.MyVote; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; @@ -54,6 +55,7 @@ import bisq.core.util.BSFormatter; import bisq.core.util.BsqFormatter; +import bisq.common.UserThread; import bisq.common.app.DevEnv; import bisq.common.util.Tuple2; import bisq.common.util.Tuple3; @@ -112,6 +114,7 @@ public class ProposalsView extends ActivatableView implements Bs private final BsqWalletService bsqWalletService; private final PhasesView phasesView; private final DaoStateService daoStateService; + private final MyBlindVoteListService myBlindVoteListService; private final Preferences preferences; private final BsqFormatter bsqFormatter; private final BSFormatter btcFormatter; @@ -148,7 +151,7 @@ public class ProposalsView extends ActivatableView implements Bs int extraRows = screenSize <= INITIAL_WINDOW_HEIGHT ? 0 : (int) ((screenSize - INITIAL_WINDOW_HEIGHT) / pixelsPerProposalTableRow); return extraRows == 0 ? initialProposalTableViewHeight : Math.ceil(initialProposalTableViewHeight + (extraRows * pixelsPerProposalTableRow)); }; - private ChangeListener bisqWindowVerticalSizeListener; + private ChangeListener sceneHeightListener; private TableGroupHeadline proposalsHeadline; @@ -161,6 +164,7 @@ private ProposalsView(DaoFacade daoFacade, BsqWalletService bsqWalletService, PhasesView phasesView, DaoStateService daoStateService, + MyBlindVoteListService myBlindVoteListService, Preferences preferences, BsqFormatter bsqFormatter, BSFormatter btcFormatter, @@ -169,6 +173,7 @@ private ProposalsView(DaoFacade daoFacade, this.bsqWalletService = bsqWalletService; this.phasesView = phasesView; this.daoStateService = daoStateService; + this.myBlindVoteListService = myBlindVoteListService; this.preferences = preferences; this.bsqFormatter = bsqFormatter; this.btcFormatter = btcFormatter; @@ -187,14 +192,7 @@ public void initialize() { ballotListChangeListener = c -> updateListItems(); proposalListChangeListener = c -> updateListItems(); - bisqWindowVerticalSizeListener = (observable, oldValue, newValue) -> { - double newTableViewHeight = proposalTableViewHeight.apply(newValue.doubleValue()); - if (tableView.getHeight() != newTableViewHeight) { - tableView.setMinHeight(newTableViewHeight); - double diff = newTableViewHeight - tableView.getHeight(); - proposalsHeadline.setMaxHeight(proposalsHeadline.getHeight() + diff); - } - }; + sceneHeightListener = (observable, oldValue, newValue) -> updateTableHeight(newValue.doubleValue()); stakeListener = (observable, oldValue, newValue) -> updateViews(); } @@ -203,15 +201,12 @@ public void initialize() { protected void activate() { phasesView.activate(); - phaseSubscription = EasyBind.subscribe(daoFacade.phaseProperty(), this::onPhaseChanged); selectedProposalSubscription = EasyBind.subscribe(tableView.getSelectionModel().selectedItemProperty(), this::onSelectProposal); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); - - daoFacade.getActiveOrMyUnconfirmedProposals().addListener(proposalListChangeListener); - daoFacade.getAllBallots().addListener(ballotListChangeListener); - daoFacade.addBsqStateListener(this); - bsqWalletService.addBsqBalanceListener(this); + tableView.setPrefHeight(100); + root.getScene().heightProperty().addListener(sceneHeightListener); + UserThread.execute(() -> updateTableHeight(root.getScene().getHeight())); stakeInputTextField.textProperty().addListener(stakeListener); voteButton.setOnAction(e -> onVote()); @@ -224,18 +219,21 @@ protected void activate() { bsqWalletService.getLockupBondsBalance(), bsqWalletService.getUnlockingBondsBalance()); - updateListItems(); - GUIUtil.setFitToRowsForTableView(tableView, 38, 28, 4, 4); - updateViews(); + if (daoStateService.isParseBlockChainComplete()) { + addListenersAfterParseBlockChainComplete(); - root.getScene().heightProperty().addListener(bisqWindowVerticalSizeListener); + updateListItems(); + applyMerit(); + updateViews(); + } } @Override protected void deactivate() { phasesView.deactivate(); - phaseSubscription.unsubscribe(); + if (phaseSubscription != null) + phaseSubscription.unsubscribe(); selectedProposalSubscription.unsubscribe(); sortedList.comparatorProperty().unbind(); @@ -286,20 +284,36 @@ public void onUpdateBalances(Coin availableConfirmedBalance, @Override public void onParseBlockCompleteAfterBatchProcessing(Block block) { - updateViews(); + updateListItems(); + applyMerit(); } @Override public void onParseBlockChainComplete() { - updateListItems(); - applyMerit(); + addListenersAfterParseBlockChainComplete(); } /////////////////////////////////////////////////////////////////////////////////////////// - // Protected + // Private /////////////////////////////////////////////////////////////////////////////////////////// + private void addListenersAfterParseBlockChainComplete() { + daoFacade.getActiveOrMyUnconfirmedProposals().addListener(proposalListChangeListener); + daoFacade.getAllBallots().addListener(ballotListChangeListener); + daoFacade.addBsqStateListener(this); + bsqWalletService.addBsqBalanceListener(this); + + phaseSubscription = EasyBind.subscribe(daoFacade.phaseProperty(), this::onPhaseChanged); + } + + private void updateListItems() { + listItems.forEach(ProposalsListItem::cleanup); + listItems.clear(); + + fillListItems(); + } + private void fillListItems() { if (daoFacade.phaseProperty().get().ordinal() < DaoPhase.Phase.BLIND_VOTE.ordinal()) { // proposal phase @@ -318,16 +332,8 @@ private void fillListItems() { updateViews(); } - private void updateListItems() { - listItems.forEach(ProposalsListItem::cleanup); - listItems.clear(); - - fillListItems(); - } - private void showVoteOnProposalWindow(Proposal proposal, @Nullable Ballot ballot, @Nullable EvaluatedProposal evaluatedProposal) { - if (!shownVoteOnProposalWindowForTxId.equals(proposal.getTxId())) { shownVoteOnProposalWindowForTxId = proposal.getTxId(); @@ -406,13 +412,15 @@ private void applyMerit() { // use the merit based on all past issuance with the time decay applied. // The merit from the vote stays the same over blocks, the merit from daoFacade.getMeritAndStake() // decreases with every block a bit (over 2 years it goes to zero). - boolean hasConfirmedVoteTxInCycle = daoFacade.getMyVoteListForCycle().stream() - .map(myVote -> daoFacade.getTx(myVote.getTxId())) - .findAny() - .isPresent(); + Optional optionalMyVote = daoFacade.getMyVoteListForCycle().stream() + .filter(myVote -> daoFacade.getTx(myVote.getBlindVoteTxId()).isPresent()) + .findAny(); + boolean hasConfirmedMyVoteInCycle = optionalMyVote.isPresent(); long merit; - if (selectedItem != null && hasConfirmedVoteTxInCycle) { + if (selectedItem != null && hasConfirmedMyVoteInCycle) { merit = daoFacade.getMeritAndStakeForProposal(selectedItem.getProposal().getTxId()).first; + } else if (selectedItem == null && hasConfirmedMyVoteInCycle) { + merit = optionalMyVote.get().getMerit(myBlindVoteListService, daoStateService); } else { merit = daoFacade.getAvailableMerit(); } @@ -542,8 +550,8 @@ private void updateViews() { Coin stake = Coin.valueOf(myVote.getBlindVote().getStake()); stakeInputTextField.setText(bsqFormatter.formatCoinWithCode(stake)); - if (myVote.getTxId() != null) { - blindVoteTxIdTextField.setup(myVote.getTxId()); + if (myVote.getBlindVoteTxId() != null) { + blindVoteTxIdTextField.setup(myVote.getBlindVoteTxId()); blindVoteTxIdContainer.setVisible(true); blindVoteTxIdContainer.setManaged(true); } @@ -591,6 +599,15 @@ private boolean isBlindVotePhaseButNotLastBlock() { return daoFacade.isInPhaseButNotLastBlock(DaoPhase.Phase.BLIND_VOTE); } + private void updateTableHeight(double height) { + double newTableViewHeight = proposalTableViewHeight.apply(height); + if (tableView.getHeight() != newTableViewHeight) { + tableView.setMinHeight(newTableViewHeight); + double diff = newTableViewHeight - tableView.getHeight(); + proposalsHeadline.setMaxHeight(proposalsHeadline.getHeight() + diff); + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // Create views diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java index 85138d20e9c..e4b72499148 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java @@ -275,10 +275,11 @@ private void onResultsListItemSelected(CycleListItem item) { .append(Res.getWithCol("dao.results.votes.table.header.stake")).append(" ") .append(bsqFormatter.formatCoinWithCode(Coin.valueOf(e.getStake()))).append("\n"); e.getBallotList().stream().forEach(ballot -> { - sb.append(Res.getWithCol("shared.name")).append(" ") - .append(ballot.getProposal().getName()).append("\n"); + sb.append(Res.getWithCol("dao.results.invalidVotes.proposal")).append("\n\t") + .append(Res.getWithCol("shared.name")).append(" ") + .append(ballot.getProposal().getName()).append("\n\t"); sb.append(Res.getWithCol("dao.bond.table.column.link")).append(" ") - .append(ballot.getProposal().getLink()).append("\n"); + .append(ballot.getProposal().getLink()).append("\n\t"); Vote vote = ballot.getVote(); String voteString = vote == null ? Res.get("dao.proposal.display.myVote.ignored") : vote.isAccepted() ? diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateMonitorView.java b/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateMonitorView.java index e5b9cb79d1e..722a44684f4 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateMonitorView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateMonitorView.java @@ -19,6 +19,7 @@ import bisq.desktop.common.view.FxmlView; import bisq.desktop.main.dao.monitor.StateMonitorView; +import bisq.desktop.main.overlays.Overlay; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.FormBuilder; @@ -28,11 +29,16 @@ import bisq.core.dao.monitoring.DaoStateMonitoringService; import bisq.core.dao.monitoring.model.DaoStateBlock; import bisq.core.dao.monitoring.model.DaoStateHash; +import bisq.core.dao.monitoring.model.UtxoMismatch; import bisq.core.dao.state.DaoStateService; import bisq.core.locale.Res; +import bisq.common.util.Utilities; + import javax.inject.Inject; +import javafx.collections.ListChangeListener; + import java.util.Map; import java.util.stream.Collectors; @@ -40,6 +46,9 @@ public class DaoStateMonitorView extends StateMonitorView implements DaoStateMonitoringService.Listener { private final DaoStateMonitoringService daoStateMonitoringService; + private ListChangeListener utxoMismatchListChangeListener; + private Overlay warningPopup; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -59,6 +68,8 @@ private DaoStateMonitorView(DaoStateService daoStateService, @Override public void initialize() { + utxoMismatchListChangeListener = c -> updateUtxoMismatches(); + FormBuilder.addTitledGroupBg(root, gridRow, 3, Res.get("dao.monitor.daoState.headline")); statusTextField = FormBuilder.addTopLabelTextField(root, ++gridRow, @@ -71,7 +82,11 @@ public void initialize() { @Override protected void activate() { super.activate(); + daoStateMonitoringService.addListener(this); + daoStateMonitoringService.getUtxoMismatches().addListener(utxoMismatchListChangeListener); + + updateUtxoMismatches(); resyncButton.setOnAction(e -> daoFacade.resyncDao(() -> new Popup<>().attention(Res.get("setting.preferences.dao.resync.popup")) @@ -84,7 +99,9 @@ protected void activate() { @Override protected void deactivate() { super.deactivate(); + daoStateMonitoringService.removeListener(this); + daoStateMonitoringService.getUtxoMismatches().removeListener(utxoMismatchListChangeListener); } @@ -185,4 +202,28 @@ protected void onDataUpdate() { protected void requestHashesFromGenesisBlockHeight(String peerAddress) { daoStateMonitoringService.requestHashesFromGenesisBlockHeight(peerAddress); } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateUtxoMismatches() { + if (!daoStateMonitoringService.getUtxoMismatches().isEmpty()) { + StringBuilder sb = new StringBuilder(); + daoStateMonitoringService.getUtxoMismatches().forEach(e -> { + sb.append("\n").append(Res.get("dao.monitor.daoState.utxoConflicts.blockHeight", e.getHeight())).append("\n") + .append(Res.get("dao.monitor.daoState.utxoConflicts.sumUtxo", e.getSumUtxo() / 100)).append("\n") + .append(Res.get("dao.monitor.daoState.utxoConflicts.sumBsq", e.getSumBsq() / 100)); + }); + + if (warningPopup == null) { + warningPopup = new Popup<>().headLine(Res.get("dao.monitor.daoState.utxoConflicts")) + .warning(Utilities.toTruncatedString(sb.toString(), 500, false)).onClose(() -> { + warningPopup = null; + }); + warningPopup.show(); + } + } + } } 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 new file mode 100644 index 00000000000..e69de29bb2d 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 aa9e20e41e1..1c15b629ead 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 @@ private void updateList() { 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 void updateItem(final BsqTxListItem item, boolean empty) { 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 void updateItem(final BsqTxListItem item, boolean empty) { 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 void updateItem(final BsqTxListItem item, boolean empty) { 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"; diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ProposalResultsWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ProposalResultsWindow.java index 494a3d29244..e557ce8bdf9 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ProposalResultsWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ProposalResultsWindow.java @@ -134,6 +134,7 @@ private void addContent(EvaluatedProposal evaluatedProposal, Ballot ballot) { proposalDisplay.createAllFields("", rowIndex, -Layout.FIRST_ROW_DISTANCE, proposal.getType(), false, "last"); proposalDisplay.setEditable(false); + proposalDisplay.onNavigate(this::doClose); proposalDisplay.applyProposalPayload(proposal); proposalDisplay.applyEvaluatedProposal(evaluatedProposal); diff --git a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java index 9cc73f18778..9c4982806af 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java @@ -593,7 +593,7 @@ private void activateGeneralOptions() { // We only show mainnet and dao testnet. Testnet is rather un-usable for application testing when asics // create 10000s of blocks per day. baseCurrencyNetworks = baseCurrencyNetworks.stream() - .filter(e -> e.isMainnet() || e.isDaoTestNet() || e.isDaoBetaNet()) + .filter(e -> e.isMainnet() || e.isDaoBetaNet() || e.isDaoRegTest()) .collect(Collectors.toList()); selectBaseCurrencyNetworkComboBox.setItems(FXCollections.observableArrayList(baseCurrencyNetworks)); selectBaseCurrencyNetworkComboBox.setOnAction(e -> onSelectNetwork()); diff --git a/gradle/witness/gradle-witness.gradle b/gradle/witness/gradle-witness.gradle index 42fa8be0381..48e81b11c63 100644 --- a/gradle/witness/gradle-witness.gradle +++ b/gradle/witness/gradle-witness.gradle @@ -38,12 +38,12 @@ dependencyVerification { 'com.googlecode.json-simple:json-simple:4e69696892b88b41c55d49ab2fdcc21eead92bf54acc588c0050596c3b75199c', 'org.springframework:spring-core:c451e8417adb2ffb2445636da5e44a2f59307c4100037a1fe387c3fba4f29b52', 'ch.qos.logback:logback-classic:e66efc674e94837344bc5b748ff510c37a44eeff86cbfdbf9e714ef2eb374013', - 'com.github.bisq-network.bitcoinj:bitcoinj-core:816e976a7efcfb650ff9009059059068b785ead864a6321fe77bba65ebd3d273', 'org.slf4j:slf4j-api:3a4cd4969015f3beb4b5b4d81dbafc01765fb60b8a439955ca64d8476fef553e', 'ch.qos.logback:logback-core:4cd46fa17d77057b39160058df2f21ebbc2aded51d0edcc25d2c1cecc042a005', 'com.google.code.findbugs:jsr305:766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7', 'com.google.guava:guava:36a666e3b71ae7f0f0dca23654b67e086e6c93d192f60ba5dfd5519db6c288c8', 'com.google.inject:guice:9b9df27a5b8c7864112b4137fd92b36c3f1395bfe57be42fedf2f520ead1a93e', + 'com.github.bisq-network.bitcoinj:bitcoinj-core:816e976a7efcfb650ff9009059059068b785ead864a6321fe77bba65ebd3d273', 'com.github.JesusMcCloud.netlayer:tor:35cf892e6ce3a8d942cfd2b589cfbde5aed31d49777aee873d6614e134df0b42', 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:193ab7813e4d249f2ea4fc1b968fea8c2126bcbeeb5d6127050ce1b93dbaa7c2', 'io.github.microutils:kotlin-logging:4992504fd3c6ecdf9ed10874b9508e758bb908af9e9d7af19a61e9afb6b7e27a', @@ -66,17 +66,17 @@ dependencyVerification { 'commons-logging:commons-logging:daddea1ea0be0f56978ab3006b8ac92834afeefbd9b7e4e6316fca57df0fa636', 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', 'aopalliance:aopalliance:0addec670fedcd3f113c5c8091d783280d23f75e3acb841b61a9cdb079376a08', + 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f', + 'net.jcip:jcip-annotations:be5805392060c71474bf6c9a67a099471274d30b83eef84bfc4e0889a4f1dcc0', 'com.lambdaworks:scrypt:9a82d218099fb14c10c0e86e7eefeebd8c104de920acdc47b8b4b7a686fb73b4', + 'org.bitcoinj:orchid:f836325cfa0466a011cb755c9b0fee6368487a2352eb45f4306ad9e4c18de080', + 'com.squareup.okhttp:okhttp:b4c943138fcef2bcc9d2006b2250c4aabbedeafc5947ed7c0af7fd103ceb2707', 'com.google.zxing:core:11aae8fd974ab25faa8208be50468eb12349cd239e93e7c797377fa13e381729', 'com.github.JesusMcCloud.tor-binary:tor-binary-geoip:766e4400e5651cf0b11788ea440cc72721be9b92e42f20809c22d0ff129df83c', 'com.github.JesusMcCloud:jtorctl:904f7c53332179a3479c64d63fb303afa6a02b6889aabdab5b235f3efc725ca7', 'org.apache.commons:commons-compress:5f2df1e467825e4cac5996d44890c4201c000b43c0b23cffc0782d28a0beb9b0', 'org.tukaani:xz:a594643d73cc01928cf6ca5ce100e094ea9d73af760a5d4fb6b75fa673ecec96', - 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f', - 'net.jcip:jcip-annotations:be5805392060c71474bf6c9a67a099471274d30b83eef84bfc4e0889a4f1dcc0', - 'org.bitcoinj:orchid:f836325cfa0466a011cb755c9b0fee6368487a2352eb45f4306ad9e4c18de080', - 'com.squareup.okhttp:okhttp:b4c943138fcef2bcc9d2006b2250c4aabbedeafc5947ed7c0af7fd103ceb2707', - 'org.jetbrains.kotlin:kotlin-stdlib-common:4b161ef619eee0d1a49b1c4f0c4a8e46f4e342573efd8e0106a765f47475fe39', 'com.squareup.okio:okio:114bdc1f47338a68bcbc95abf2f5cdc72beeec91812f2fcd7b521c1937876266', + 'org.jetbrains.kotlin:kotlin-stdlib-common:4b161ef619eee0d1a49b1c4f0c4a8e46f4e342573efd8e0106a765f47475fe39', ] } diff --git a/p2p/src/main/java/bisq/network/p2p/storage/payload/MailboxStoragePayload.java b/p2p/src/main/java/bisq/network/p2p/storage/payload/MailboxStoragePayload.java index 88dd96cc945..80ce47f5634 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/payload/MailboxStoragePayload.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/payload/MailboxStoragePayload.java @@ -20,6 +20,7 @@ import bisq.network.p2p.PrefixedSealedAndSignedMessage; import bisq.common.crypto.Sig; +import bisq.common.util.ExtraDataMapValidator; import io.bisq.generated.protobuffer.PB; @@ -85,7 +86,7 @@ private MailboxStoragePayload(PrefixedSealedAndSignedMessage prefixedSealedAndSi this.prefixedSealedAndSignedMessage = prefixedSealedAndSignedMessage; this.senderPubKeyForAddOperationBytes = senderPubKeyForAddOperationBytes; this.ownerPubKeyBytes = ownerPubKeyBytes; - this.extraDataMap = extraDataMap; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); senderPubKeyForAddOperation = Sig.getPublicKeyFromBytes(senderPubKeyForAddOperationBytes); ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyBytes);